| /* |
| * jQuery Easing v1.3 - http://gsgd.co.uk/sandbox/jquery/easing/ |
| * |
| * Uses the built in easing capabilities added In jQuery 1.1 |
| * to offer multiple easing options |
| * |
| * TERMS OF USE - jQuery Easing |
| * |
| * Open source under the BSD License. |
| * |
| * Copyright © 2008 George McGinley Smith |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without modification, |
| * are permitted provided that the following conditions are met: |
| * |
| * Redistributions of source code must retain the above copyright notice, this list of |
| * conditions and the following disclaimer. |
| * Redistributions in binary form must reproduce the above copyright notice, this list |
| * of conditions and the following disclaimer in the documentation and/or other materials |
| * provided with the distribution. |
| * |
| * Neither the name of the author nor the names of contributors may be used to endorse |
| * or promote products derived from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
| * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE |
| * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE |
| * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
| * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
| * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED |
| * OF THE POSSIBILITY OF SUCH DAMAGE. |
| * |
| */ |
| |
| |
| // t: current time, b: begInnIng value, c: change In value, d: duration |
| jQuery.easing['jswing'] = jQuery.easing['swing']; |
| |
| jQuery.extend( jQuery.easing, |
| { |
| def: 'easeOutQuad', |
| swing: function (x, t, b, c, d) { |
| //alert(jQuery.easing.default); |
| return jQuery.easing[jQuery.easing.def](x, t, b, c, d); |
| }, |
| easeInQuad: function (x, t, b, c, d) { |
| return c*(t/=d)*t + b; |
| }, |
| easeOutQuad: function (x, t, b, c, d) { |
| return -c *(t/=d)*(t-2) + b; |
| }, |
| easeInOutQuad: function (x, t, b, c, d) { |
| if ((t/=d/2) < 1) return c/2*t*t + b; |
| return -c/2 * ((--t)*(t-2) - 1) + b; |
| }, |
| easeInCubic: function (x, t, b, c, d) { |
| return c*(t/=d)*t*t + b; |
| }, |
| easeOutCubic: function (x, t, b, c, d) { |
| return c*((t=t/d-1)*t*t + 1) + b; |
| }, |
| easeInOutCubic: function (x, t, b, c, d) { |
| if ((t/=d/2) < 1) return c/2*t*t*t + b; |
| return c/2*((t-=2)*t*t + 2) + b; |
| }, |
| easeInQuart: function (x, t, b, c, d) { |
| return c*(t/=d)*t*t*t + b; |
| }, |
| easeOutQuart: function (x, t, b, c, d) { |
| return -c * ((t=t/d-1)*t*t*t - 1) + b; |
| }, |
| easeInOutQuart: function (x, t, b, c, d) { |
| if ((t/=d/2) < 1) return c/2*t*t*t*t + b; |
| return -c/2 * ((t-=2)*t*t*t - 2) + b; |
| }, |
| easeInQuint: function (x, t, b, c, d) { |
| return c*(t/=d)*t*t*t*t + b; |
| }, |
| easeOutQuint: function (x, t, b, c, d) { |
| return c*((t=t/d-1)*t*t*t*t + 1) + b; |
| }, |
| easeInOutQuint: function (x, t, b, c, d) { |
| if ((t/=d/2) < 1) return c/2*t*t*t*t*t + b; |
| return c/2*((t-=2)*t*t*t*t + 2) + b; |
| }, |
| easeInSine: function (x, t, b, c, d) { |
| return -c * Math.cos(t/d * (Math.PI/2)) + c + b; |
| }, |
| easeOutSine: function (x, t, b, c, d) { |
| return c * Math.sin(t/d * (Math.PI/2)) + b; |
| }, |
| easeInOutSine: function (x, t, b, c, d) { |
| return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b; |
| }, |
| easeInExpo: function (x, t, b, c, d) { |
| return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b; |
| }, |
| easeOutExpo: function (x, t, b, c, d) { |
| return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b; |
| }, |
| easeInOutExpo: function (x, t, b, c, d) { |
| if (t==0) return b; |
| if (t==d) return b+c; |
| if ((t/=d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b; |
| return c/2 * (-Math.pow(2, -10 * --t) + 2) + b; |
| }, |
| easeInCirc: function (x, t, b, c, d) { |
| return -c * (Math.sqrt(1 - (t/=d)*t) - 1) + b; |
| }, |
| easeOutCirc: function (x, t, b, c, d) { |
| return c * Math.sqrt(1 - (t=t/d-1)*t) + b; |
| }, |
| easeInOutCirc: function (x, t, b, c, d) { |
| if ((t/=d/2) < 1) return -c/2 * (Math.sqrt(1 - t*t) - 1) + b; |
| return c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b; |
| }, |
| easeInElastic: function (x, t, b, c, d) { |
| var s=1.70158;var p=0;var a=c; |
| if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3; |
| if (a < Math.abs(c)) { a=c; var s=p/4; } |
| else var s = p/(2*Math.PI) * Math.asin (c/a); |
| return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; |
| }, |
| easeOutElastic: function (x, t, b, c, d) { |
| var s=1.70158;var p=0;var a=c; |
| if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3; |
| if (a < Math.abs(c)) { a=c; var s=p/4; } |
| else var s = p/(2*Math.PI) * Math.asin (c/a); |
| return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b; |
| }, |
| easeInOutElastic: function (x, t, b, c, d) { |
| var s=1.70158;var p=0;var a=c; |
| if (t==0) return b; if ((t/=d/2)==2) return b+c; if (!p) p=d*(.3*1.5); |
| if (a < Math.abs(c)) { a=c; var s=p/4; } |
| else var s = p/(2*Math.PI) * Math.asin (c/a); |
| if (t < 1) return -.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; |
| return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*.5 + c + b; |
| }, |
| easeInBack: function (x, t, b, c, d, s) { |
| if (s == undefined) s = 1.70158; |
| return c*(t/=d)*t*((s+1)*t - s) + b; |
| }, |
| easeOutBack: function (x, t, b, c, d, s) { |
| if (s == undefined) s = 1.70158; |
| return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b; |
| }, |
| easeInOutBack: function (x, t, b, c, d, s) { |
| if (s == undefined) s = 1.70158; |
| if ((t/=d/2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b; |
| return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b; |
| }, |
| easeInBounce: function (x, t, b, c, d) { |
| return c - jQuery.easing.easeOutBounce (x, d-t, 0, c, d) + b; |
| }, |
| easeOutBounce: function (x, t, b, c, d) { |
| if ((t/=d) < (1/2.75)) { |
| return c*(7.5625*t*t) + b; |
| } else if (t < (2/2.75)) { |
| return c*(7.5625*(t-=(1.5/2.75))*t + .75) + b; |
| } else if (t < (2.5/2.75)) { |
| return c*(7.5625*(t-=(2.25/2.75))*t + .9375) + b; |
| } else { |
| return c*(7.5625*(t-=(2.625/2.75))*t + .984375) + b; |
| } |
| }, |
| easeInOutBounce: function (x, t, b, c, d) { |
| if (t < d/2) return jQuery.easing.easeInBounce (x, t*2, 0, c, d) * .5 + b; |
| return jQuery.easing.easeOutBounce (x, t*2-d, 0, c, d) * .5 + c*.5 + b; |
| } |
| }); |
| |
| /* |
| * |
| * TERMS OF USE - EASING EQUATIONS |
| * |
| * Open source under the BSD License. |
| * |
| * Copyright © 2001 Robert Penner |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without modification, |
| * are permitted provided that the following conditions are met: |
| * |
| * Redistributions of source code must retain the above copyright notice, this list of |
| * conditions and the following disclaimer. |
| * Redistributions in binary form must reproduce the above copyright notice, this list |
| * of conditions and the following disclaimer in the documentation and/or other materials |
| * provided with the distribution. |
| * |
| * Neither the name of the author nor the names of contributors may be used to endorse |
| * or promote products derived from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
| * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE |
| * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE |
| * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
| * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
| * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED |
| * OF THE POSSIBILITY OF SUCH DAMAGE. |
| * |
| */ |
| /*! |
| * jQuery Templates Plugin 1.0.0pre |
| * http://github.com/jquery/jquery-tmpl |
| * Requires jQuery 1.4.2 |
| * |
| * Copyright Software Freedom Conservancy, Inc. |
| * Dual licensed under the MIT or GPL Version 2 licenses. |
| * http://jquery.org/license |
| */ |
| |
| |
| (function( jQuery, undefined ){ |
| var oldManip = jQuery.fn.domManip, tmplItmAtt = "_tmplitem", htmlExpr = /^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /, |
| newTmplItems = {}, wrappedItems = {}, appendToTmplItems, topTmplItem = { key: 0, data: {} }, itemKey = 0, cloneIndex = 0, stack = []; |
| |
| function newTmplItem( options, parentItem, fn, data ) { |
| // Returns a template item data structure for a new rendered instance of a template (a 'template item'). |
| // The content field is a hierarchical array of strings and nested items (to be |
| // removed and replaced by nodes field of dom elements, once inserted in DOM). |
| var newItem = { |
| data: data || (data === 0 || data === false) ? data : (parentItem ? parentItem.data : {}), |
| _wrap: parentItem ? parentItem._wrap : null, |
| tmpl: null, |
| parent: parentItem || null, |
| nodes: [], |
| calls: tiCalls, |
| nest: tiNest, |
| wrap: tiWrap, |
| html: tiHtml, |
| update: tiUpdate |
| }; |
| if ( options ) { |
| jQuery.extend( newItem, options, { nodes: [], parent: parentItem }); |
| } |
| if ( fn ) { |
| // Build the hierarchical content to be used during insertion into DOM |
| newItem.tmpl = fn; |
| newItem._ctnt = newItem._ctnt || newItem.tmpl( jQuery, newItem ); |
| newItem.key = ++itemKey; |
| // Keep track of new template item, until it is stored as jQuery Data on DOM element |
| (stack.length ? wrappedItems : newTmplItems)[itemKey] = newItem; |
| } |
| return newItem; |
| } |
| |
| // Override appendTo etc., in order to provide support for targeting multiple elements. (This code would disappear if integrated in jquery core). |
| jQuery.each({ |
| appendTo: "append", |
| prependTo: "prepend", |
| insertBefore: "before", |
| insertAfter: "after", |
| replaceAll: "replaceWith" |
| }, function( name, original ) { |
| jQuery.fn[ name ] = function( selector ) { |
| var ret = [], insert = jQuery( selector ), elems, i, l, tmplItems, |
| parent = this.length === 1 && this[0].parentNode; |
| |
| appendToTmplItems = newTmplItems || {}; |
| if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) { |
| insert[ original ]( this[0] ); |
| ret = this; |
| } else { |
| for ( i = 0, l = insert.length; i < l; i++ ) { |
| cloneIndex = i; |
| elems = (i > 0 ? this.clone(true) : this).get(); |
| jQuery( insert[i] )[ original ]( elems ); |
| ret = ret.concat( elems ); |
| } |
| cloneIndex = 0; |
| ret = this.pushStack( ret, name, insert.selector ); |
| } |
| tmplItems = appendToTmplItems; |
| appendToTmplItems = null; |
| jQuery.tmpl.complete( tmplItems ); |
| return ret; |
| }; |
| }); |
| |
| jQuery.fn.extend({ |
| // Use first wrapped element as template markup. |
| // Return wrapped set of template items, obtained by rendering template against data. |
| tmpl: function( data, options, parentItem ) { |
| return jQuery.tmpl( this[0], data, options, parentItem ); |
| }, |
| |
| // Find which rendered template item the first wrapped DOM element belongs to |
| tmplItem: function() { |
| return jQuery.tmplItem( this[0] ); |
| }, |
| |
| // Consider the first wrapped element as a template declaration, and get the compiled template or store it as a named template. |
| template: function( name ) { |
| return jQuery.template( name, this[0] ); |
| }, |
| |
| domManip: function( args, table, callback, options ) { |
| if ( args[0] && jQuery.isArray( args[0] )) { |
| var dmArgs = jQuery.makeArray( arguments ), elems = args[0], elemsLength = elems.length, i = 0, tmplItem; |
| while ( i < elemsLength && !(tmplItem = jQuery.data( elems[i++], "tmplItem" ))) {} |
| if ( tmplItem && cloneIndex ) { |
| dmArgs[2] = function( fragClone ) { |
| // Handler called by oldManip when rendered template has been inserted into DOM. |
| jQuery.tmpl.afterManip( this, fragClone, callback ); |
| }; |
| } |
| oldManip.apply( this, dmArgs ); |
| } else { |
| oldManip.apply( this, arguments ); |
| } |
| cloneIndex = 0; |
| if ( !appendToTmplItems ) { |
| jQuery.tmpl.complete( newTmplItems ); |
| } |
| return this; |
| } |
| }); |
| |
| jQuery.extend({ |
| // Return wrapped set of template items, obtained by rendering template against data. |
| tmpl: function( tmpl, data, options, parentItem ) { |
| var ret, topLevel = !parentItem; |
| if ( topLevel ) { |
| // This is a top-level tmpl call (not from a nested template using {{tmpl}}) |
| parentItem = topTmplItem; |
| tmpl = jQuery.template[tmpl] || jQuery.template( null, tmpl ); |
| wrappedItems = {}; // Any wrapped items will be rebuilt, since this is top level |
| } else if ( !tmpl ) { |
| // The template item is already associated with DOM - this is a refresh. |
| // Re-evaluate rendered template for the parentItem |
| tmpl = parentItem.tmpl; |
| newTmplItems[parentItem.key] = parentItem; |
| parentItem.nodes = []; |
| if ( parentItem.wrapped ) { |
| updateWrapped( parentItem, parentItem.wrapped ); |
| } |
| // Rebuild, without creating a new template item |
| return jQuery( build( parentItem, null, parentItem.tmpl( jQuery, parentItem ) )); |
| } |
| if ( !tmpl ) { |
| return []; // Could throw... |
| } |
| if ( typeof data === "function" ) { |
| data = data.call( parentItem || {} ); |
| } |
| if ( options && options.wrapped ) { |
| updateWrapped( options, options.wrapped ); |
| } |
| ret = jQuery.isArray( data ) ? |
| jQuery.map( data, function( dataItem ) { |
| return dataItem ? newTmplItem( options, parentItem, tmpl, dataItem ) : null; |
| }) : |
| [ newTmplItem( options, parentItem, tmpl, data ) ]; |
| return topLevel ? jQuery( build( parentItem, null, ret ) ) : ret; |
| }, |
| |
| // Return rendered template item for an element. |
| tmplItem: function( elem ) { |
| var tmplItem; |
| if ( elem instanceof jQuery ) { |
| elem = elem[0]; |
| } |
| while ( elem && elem.nodeType === 1 && !(tmplItem = jQuery.data( elem, "tmplItem" )) && (elem = elem.parentNode) ) {} |
| return tmplItem || topTmplItem; |
| }, |
| |
| // Set: |
| // Use $.template( name, tmpl ) to cache a named template, |
| // where tmpl is a template string, a script element or a jQuery instance wrapping a script element, etc. |
| // Use $( "selector" ).template( name ) to provide access by name to a script block template declaration. |
| |
| // Get: |
| // Use $.template( name ) to access a cached template. |
| // Also $( selectorToScriptBlock ).template(), or $.template( null, templateString ) |
| // will return the compiled template, without adding a name reference. |
| // If templateString includes at least one HTML tag, $.template( templateString ) is equivalent |
| // to $.template( null, templateString ) |
| template: function( name, tmpl ) { |
| if (tmpl) { |
| // Compile template and associate with name |
| if ( typeof tmpl === "string" ) { |
| // This is an HTML string being passed directly in. |
| tmpl = buildTmplFn( tmpl ); |
| } else if ( tmpl instanceof jQuery ) { |
| tmpl = tmpl[0] || {}; |
| } |
| if ( tmpl.nodeType ) { |
| // If this is a template block, use cached copy, or generate tmpl function and cache. |
| tmpl = jQuery.data( tmpl, "tmpl" ) || jQuery.data( tmpl, "tmpl", buildTmplFn( tmpl.innerHTML )); |
| // Issue: In IE, if the container element is not a script block, the innerHTML will remove quotes from attribute values whenever the value does not include white space. |
| // This means that foo="${x}" will not work if the value of x includes white space: foo="${x}" -> foo=value of x. |
| // To correct this, include space in tag: foo="${ x }" -> foo="value of x" |
| } |
| return typeof name === "string" ? (jQuery.template[name] = tmpl) : tmpl; |
| } |
| // Return named compiled template |
| return name ? (typeof name !== "string" ? jQuery.template( null, name ): |
| (jQuery.template[name] || |
| // If not in map, and not containing at least on HTML tag, treat as a selector. |
| // (If integrated with core, use quickExpr.exec) |
| jQuery.template( null, htmlExpr.test( name ) ? name : jQuery( name )))) : null; |
| }, |
| |
| encode: function( text ) { |
| // Do HTML encoding replacing < > & and ' and " by corresponding entities. |
| return ("" + text).split("<").join("<").split(">").join(">").split('"').join(""").split("'").join("'"); |
| } |
| }); |
| |
| jQuery.extend( jQuery.tmpl, { |
| tag: { |
| "tmpl": { |
| _default: { $2: "null" }, |
| open: "if($notnull_1){__=__.concat($item.nest($1,$2));}" |
| // tmpl target parameter can be of type function, so use $1, not $1a (so not auto detection of functions) |
| // This means that {{tmpl foo}} treats foo as a template (which IS a function). |
| // Explicit parens can be used if foo is a function that returns a template: {{tmpl foo()}}. |
| }, |
| "wrap": { |
| _default: { $2: "null" }, |
| open: "$item.calls(__,$1,$2);__=[];", |
| close: "call=$item.calls();__=call._.concat($item.wrap(call,__));" |
| }, |
| "each": { |
| _default: { $2: "$index, $value" }, |
| open: "if($notnull_1){$.each($1a,function($2){with(this){", |
| close: "}});}" |
| }, |
| "if": { |
| open: "if(($notnull_1) && $1a){", |
| close: "}" |
| }, |
| "else": { |
| _default: { $1: "true" }, |
| open: "}else if(($notnull_1) && $1a){" |
| }, |
| "html": { |
| // Unecoded expression evaluation. |
| open: "if($notnull_1){__.push($1a);}" |
| }, |
| "=": { |
| // Encoded expression evaluation. Abbreviated form is ${}. |
| _default: { $1: "$data" }, |
| open: "if($notnull_1){__.push($.encode($1a));}" |
| }, |
| "!": { |
| // Comment tag. Skipped by parser |
| open: "" |
| } |
| }, |
| |
| // This stub can be overridden, e.g. in jquery.tmplPlus for providing rendered events |
| complete: function( items ) { |
| newTmplItems = {}; |
| }, |
| |
| // Call this from code which overrides domManip, or equivalent |
| // Manage cloning/storing template items etc. |
| afterManip: function afterManip( elem, fragClone, callback ) { |
| // Provides cloned fragment ready for fixup prior to and after insertion into DOM |
| var content = fragClone.nodeType === 11 ? |
| jQuery.makeArray(fragClone.childNodes) : |
| fragClone.nodeType === 1 ? [fragClone] : []; |
| |
| // Return fragment to original caller (e.g. append) for DOM insertion |
| callback.call( elem, fragClone ); |
| |
| // Fragment has been inserted:- Add inserted nodes to tmplItem data structure. Replace inserted element annotations by jQuery.data. |
| storeTmplItems( content ); |
| cloneIndex++; |
| } |
| }); |
| |
| //========================== Private helper functions, used by code above ========================== |
| |
| function build( tmplItem, nested, content ) { |
| // Convert hierarchical content into flat string array |
| // and finally return array of fragments ready for DOM insertion |
| var frag, ret = content ? jQuery.map( content, function( item ) { |
| return (typeof item === "string") ? |
| // Insert template item annotations, to be converted to jQuery.data( "tmplItem" ) when elems are inserted into DOM. |
| (tmplItem.key ? item.replace( /(<\w+)(?=[\s>])(?![^>]*_tmplitem)([^>]*)/g, "$1 " + tmplItmAtt + "=\"" + tmplItem.key + "\" $2" ) : item) : |
| // This is a child template item. Build nested template. |
| build( item, tmplItem, item._ctnt ); |
| }) : |
| // If content is not defined, insert tmplItem directly. Not a template item. May be a string, or a string array, e.g. from {{html $item.html()}}. |
| tmplItem; |
| if ( nested ) { |
| return ret; |
| } |
| |
| // top-level template |
| ret = ret.join(""); |
| |
| // Support templates which have initial or final text nodes, or consist only of text |
| // Also support HTML entities within the HTML markup. |
| ret.replace( /^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/, function( all, before, middle, after) { |
| frag = jQuery( middle ).get(); |
| |
| storeTmplItems( frag ); |
| if ( before ) { |
| frag = unencode( before ).concat(frag); |
| } |
| if ( after ) { |
| frag = frag.concat(unencode( after )); |
| } |
| }); |
| return frag ? frag : unencode( ret ); |
| } |
| |
| function unencode( text ) { |
| // Use createElement, since createTextNode will not render HTML entities correctly |
| var el = document.createElement( "div" ); |
| el.innerHTML = text; |
| return jQuery.makeArray(el.childNodes); |
| } |
| |
| // Generate a reusable function that will serve to render a template against data |
| function buildTmplFn( markup ) { |
| return new Function("jQuery","$item", |
| // Use the variable __ to hold a string array while building the compiled template. (See https://github.com/jquery/jquery-tmpl/issues#issue/10). |
| "var $=jQuery,call,__=[],$data=$item.data;" + |
| |
| // Introduce the data as local variables using with(){} |
| "with($data){__.push('" + |
| |
| // Convert the template into pure JavaScript |
| jQuery.trim(markup) |
| .replace( /([\\'])/g, "\\$1" ) |
| .replace( /[\r\t\n]/g, " " ) |
| .replace( /\$\{([^\}]*)\}/g, "{{= $1}}" ) |
| .replace( /\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g, |
| function( all, slash, type, fnargs, target, parens, args ) { |
| var tag = jQuery.tmpl.tag[ type ], def, expr, exprAutoFnDetect; |
| if ( !tag ) { |
| throw "Unknown template tag: " + type; |
| } |
| def = tag._default || []; |
| if ( parens && !/\w$/.test(target)) { |
| target += parens; |
| parens = ""; |
| } |
| if ( target ) { |
| target = unescape( target ); |
| args = args ? ("," + unescape( args ) + ")") : (parens ? ")" : ""); |
| // Support for target being things like a.toLowerCase(); |
| // In that case don't call with template item as 'this' pointer. Just evaluate... |
| expr = parens ? (target.indexOf(".") > -1 ? target + unescape( parens ) : ("(" + target + ").call($item" + args)) : target; |
| exprAutoFnDetect = parens ? expr : "(typeof(" + target + ")==='function'?(" + target + ").call($item):(" + target + "))"; |
| } else { |
| exprAutoFnDetect = expr = def.$1 || "null"; |
| } |
| fnargs = unescape( fnargs ); |
| return "');" + |
| tag[ slash ? "close" : "open" ] |
| .split( "$notnull_1" ).join( target ? "typeof(" + target + ")!=='undefined' && (" + target + ")!=null" : "true" ) |
| .split( "$1a" ).join( exprAutoFnDetect ) |
| .split( "$1" ).join( expr ) |
| .split( "$2" ).join( fnargs || def.$2 || "" ) + |
| "__.push('"; |
| }) + |
| "');}return __;" |
| ); |
| } |
| function updateWrapped( options, wrapped ) { |
| // Build the wrapped content. |
| options._wrap = build( options, true, |
| // Suport imperative scenario in which options.wrapped can be set to a selector or an HTML string. |
| jQuery.isArray( wrapped ) ? wrapped : [htmlExpr.test( wrapped ) ? wrapped : jQuery( wrapped ).html()] |
| ).join(""); |
| } |
| |
| function unescape( args ) { |
| return args ? args.replace( /\\'/g, "'").replace(/\\\\/g, "\\" ) : null; |
| } |
| function outerHtml( elem ) { |
| var div = document.createElement("div"); |
| div.appendChild( elem.cloneNode(true) ); |
| return div.innerHTML; |
| } |
| |
| // Store template items in jQuery.data(), ensuring a unique tmplItem data data structure for each rendered template instance. |
| function storeTmplItems( content ) { |
| var keySuffix = "_" + cloneIndex, elem, elems, newClonedItems = {}, i, l, m; |
| for ( i = 0, l = content.length; i < l; i++ ) { |
| if ( (elem = content[i]).nodeType !== 1 ) { |
| continue; |
| } |
| elems = elem.getElementsByTagName("*"); |
| for ( m = elems.length - 1; m >= 0; m-- ) { |
| processItemKey( elems[m] ); |
| } |
| processItemKey( elem ); |
| } |
| function processItemKey( el ) { |
| var pntKey, pntNode = el, pntItem, tmplItem, key; |
| // Ensure that each rendered template inserted into the DOM has its own template item, |
| if ( (key = el.getAttribute( tmplItmAtt ))) { |
| while ( pntNode.parentNode && (pntNode = pntNode.parentNode).nodeType === 1 && !(pntKey = pntNode.getAttribute( tmplItmAtt ))) { } |
| if ( pntKey !== key ) { |
| // The next ancestor with a _tmplitem expando is on a different key than this one. |
| // So this is a top-level element within this template item |
| // Set pntNode to the key of the parentNode, or to 0 if pntNode.parentNode is null, or pntNode is a fragment. |
| pntNode = pntNode.parentNode ? (pntNode.nodeType === 11 ? 0 : (pntNode.getAttribute( tmplItmAtt ) || 0)) : 0; |
| if ( !(tmplItem = newTmplItems[key]) ) { |
| // The item is for wrapped content, and was copied from the temporary parent wrappedItem. |
| tmplItem = wrappedItems[key]; |
| tmplItem = newTmplItem( tmplItem, newTmplItems[pntNode]||wrappedItems[pntNode] ); |
| tmplItem.key = ++itemKey; |
| newTmplItems[itemKey] = tmplItem; |
| } |
| if ( cloneIndex ) { |
| cloneTmplItem( key ); |
| } |
| } |
| el.removeAttribute( tmplItmAtt ); |
| } else if ( cloneIndex && (tmplItem = jQuery.data( el, "tmplItem" )) ) { |
| // This was a rendered element, cloned during append or appendTo etc. |
| // TmplItem stored in jQuery data has already been cloned in cloneCopyEvent. We must replace it with a fresh cloned tmplItem. |
| cloneTmplItem( tmplItem.key ); |
| newTmplItems[tmplItem.key] = tmplItem; |
| pntNode = jQuery.data( el.parentNode, "tmplItem" ); |
| pntNode = pntNode ? pntNode.key : 0; |
| } |
| if ( tmplItem ) { |
| pntItem = tmplItem; |
| // Find the template item of the parent element. |
| // (Using !=, not !==, since pntItem.key is number, and pntNode may be a string) |
| while ( pntItem && pntItem.key != pntNode ) { |
| // Add this element as a top-level node for this rendered template item, as well as for any |
| // ancestor items between this item and the item of its parent element |
| pntItem.nodes.push( el ); |
| pntItem = pntItem.parent; |
| } |
| // Delete content built during rendering - reduce API surface area and memory use, and avoid exposing of stale data after rendering... |
| delete tmplItem._ctnt; |
| delete tmplItem._wrap; |
| // Store template item as jQuery data on the element |
| jQuery.data( el, "tmplItem", tmplItem ); |
| } |
| function cloneTmplItem( key ) { |
| key = key + keySuffix; |
| tmplItem = newClonedItems[key] = |
| (newClonedItems[key] || newTmplItem( tmplItem, newTmplItems[tmplItem.parent.key + keySuffix] || tmplItem.parent )); |
| } |
| } |
| } |
| |
| //---- Helper functions for template item ---- |
| |
| function tiCalls( content, tmpl, data, options ) { |
| if ( !content ) { |
| return stack.pop(); |
| } |
| stack.push({ _: content, tmpl: tmpl, item:this, data: data, options: options }); |
| } |
| |
| function tiNest( tmpl, data, options ) { |
| // nested template, using {{tmpl}} tag |
| return jQuery.tmpl( jQuery.template( tmpl ), data, options, this ); |
| } |
| |
| function tiWrap( call, wrapped ) { |
| // nested template, using {{wrap}} tag |
| var options = call.options || {}; |
| options.wrapped = wrapped; |
| // Apply the template, which may incorporate wrapped content, |
| return jQuery.tmpl( jQuery.template( call.tmpl ), call.data, options, call.item ); |
| } |
| |
| function tiHtml( filter, textOnly ) { |
| var wrapped = this._wrap; |
| return jQuery.map( |
| jQuery( jQuery.isArray( wrapped ) ? wrapped.join("") : wrapped ).filter( filter || "*" ), |
| function(e) { |
| return textOnly ? |
| e.innerText || e.textContent : |
| e.outerHTML || outerHtml(e); |
| }); |
| } |
| |
| function tiUpdate() { |
| var coll = this.nodes; |
| jQuery.tmpl( null, null, null, this).insertBefore( coll[0] ); |
| jQuery( coll ).remove(); |
| } |
| })( jQuery ); |
| |
| /* |
| * jQuery Mobile Framework Git Build: SHA1: 4e0055a4eaa6c052f6ff1f6869a6473cd14c00fc <> Date: Mon Jul 15 15:58:34 2013 +0900 |
| * http://jquerymobile.com |
| * |
| * Copyright 2012 jQuery Foundation and other contributors |
| * Released under the MIT license. |
| * http://jquery.org/license |
| * |
| */ |
| |
| |
| (function ( root, doc, factory ) { |
| if ( typeof define === "function" && define.amd ) { |
| // AMD. Register as an anonymous module. |
| define( [ "jquery" ], function ( $ ) { |
| factory( $, root, doc ); |
| return $.mobile; |
| }); |
| } else { |
| // Browser globals |
| factory( root.jQuery, root, doc ); |
| } |
| }( this, document, function ( jQuery, window, document, undefined ) { |
| (function( $, window, undefined ) { |
| |
| var nsNormalizeDict = {}; |
| |
| // jQuery.mobile configurable options |
| $.mobile = $.extend( {}, { |
| |
| // Version of the jQuery Mobile Framework |
| version: "1.2.0", |
| |
| // Namespace used framework-wide for data-attrs. Default is no namespace |
| ns: "", |
| |
| // Define the url parameter used for referencing widget-generated sub-pages. |
| // Translates to to example.html&ui-page=subpageIdentifier |
| // hash segment before &ui-page= is used to make Ajax request |
| subPageUrlKey: "ui-page", |
| |
| // Class assigned to page currently in view, and during transitions |
| activePageClass: "ui-page-active", |
| |
| // Class used for "active" button state, from CSS framework |
| activeBtnClass: "ui-btn-active", |
| |
| // Class used for "focus" form element state, from CSS framework |
| focusClass: "ui-focus", |
| |
| // Automatically handle clicks and form submissions through Ajax, when same-domain |
| ajaxEnabled: true, |
| |
| // Automatically load and show pages based on location.hash |
| hashListeningEnabled: true, |
| |
| // disable to prevent jquery from bothering with links |
| linkBindingEnabled: true, |
| |
| // Set default page transition - 'none' for no transitions |
| defaultPageTransition: "fade", |
| |
| // Set maximum window width for transitions to apply - 'false' for no limit |
| maxTransitionWidth: false, |
| |
| // Minimum scroll distance that will be remembered when returning to a page |
| minScrollBack: 250, |
| |
| // DEPRECATED: the following property is no longer in use, but defined until 2.0 to prevent conflicts |
| touchOverflowEnabled: false, |
| |
| // Set default dialog transition - 'none' for no transitions |
| defaultDialogTransition: "pop", |
| |
| // Error response message - appears when an Ajax page request fails |
| pageLoadErrorMessage: "Error Loading Page", |
| |
| // For error messages, which theme does the box uses? |
| pageLoadErrorMessageTheme: "e", |
| |
| // replace calls to window.history.back with phonegaps navigation helper |
| // where it is provided on the window object |
| phonegapNavigationEnabled: false, |
| |
| //automatically initialize the DOM when it's ready |
| autoInitializePage: true, |
| |
| pushStateEnabled: true, |
| |
| // allows users to opt in to ignoring content by marking a parent element as |
| // data-ignored |
| ignoreContentEnabled: false, |
| |
| // turn of binding to the native orientationchange due to android orientation behavior |
| orientationChangeEnabled: true, |
| |
| buttonMarkup: { |
| hoverDelay: 200 |
| }, |
| |
| // define the window and the document objects |
| $window: $( window ), |
| $document: $( document ), |
| |
| getAttrFixed : function( e, key ) { |
| var value = e.getAttribute( key ); |
| |
| return value === "true" ? true : |
| value === "false" ? false : |
| value === null ? undefined : value; |
| }, |
| |
| // TODO might be useful upstream in jquery itself ? |
| keyCode: { |
| ALT: 18, |
| BACKSPACE: 8, |
| CAPS_LOCK: 20, |
| COMMA: 188, |
| COMMAND: 91, |
| COMMAND_LEFT: 91, // COMMAND |
| COMMAND_RIGHT: 93, |
| CONTROL: 17, |
| DELETE: 46, |
| DOWN: 40, |
| END: 35, |
| ENTER: 13, |
| ESCAPE: 27, |
| HOME: 36, |
| INSERT: 45, |
| LEFT: 37, |
| MENU: 93, // COMMAND_RIGHT |
| NUMPAD_ADD: 107, |
| NUMPAD_DECIMAL: 110, |
| NUMPAD_DIVIDE: 111, |
| NUMPAD_ENTER: 108, |
| NUMPAD_MULTIPLY: 106, |
| NUMPAD_SUBTRACT: 109, |
| PAGE_DOWN: 34, |
| PAGE_UP: 33, |
| PERIOD: 190, |
| RIGHT: 39, |
| SHIFT: 16, |
| SPACE: 32, |
| TAB: 9, |
| UP: 38, |
| WINDOWS: 91 // COMMAND |
| }, |
| |
| // Scroll page vertically: scroll to 0 to hide iOS address bar, or pass a Y value |
| silentScroll: function( ypos ) { |
| if ( $.type( ypos ) !== "number" ) { |
| ypos = $.mobile.defaultHomeScroll; |
| } |
| |
| // prevent scrollstart and scrollstop events |
| $.event.special.scrollstart.enabled = false; |
| |
| setTimeout( function() { |
| window.scrollTo( 0, ypos ); |
| $.mobile.$document.trigger( "silentscroll", { x: 0, y: ypos }); |
| }, 20 ); |
| |
| setTimeout( function() { |
| $.event.special.scrollstart.enabled = true; |
| }, 150 ); |
| }, |
| |
| // Expose our cache for testing purposes. |
| nsNormalizeDict: nsNormalizeDict, |
| |
| // Take a data attribute property, prepend the namespace |
| // and then camel case the attribute string. Add the result |
| // to our nsNormalizeDict so we don't have to do this again. |
| nsNormalize: function( prop ) { |
| if ( !prop ) { |
| return; |
| } |
| |
| return nsNormalizeDict[ prop ] || ( nsNormalizeDict[ prop ] = $.camelCase( $.mobile.ns + prop ) ); |
| }, |
| |
| // Find the closest parent with a theme class on it. Note that |
| // we are not using $.fn.closest() on purpose here because this |
| // method gets called quite a bit and we need it to be as fast |
| // as possible. |
| getInheritedTheme: function( el, defaultTheme ) { |
| var e = el[ 0 ], |
| ltr = "", |
| re = /ui-(bar|body|overlay)-([a-z])\b/, |
| c, m; |
| |
| while ( e ) { |
| c = e.className || ""; |
| if ( c && ( m = re.exec( c ) ) && ( ltr = m[ 2 ] ) ) { |
| // We found a parent with a theme class |
| // on it so bail from this loop. |
| break; |
| } |
| |
| e = e.parentNode; |
| } |
| |
| // Return the theme letter we found, if none, return the |
| // specified default. |
| |
| return ltr || defaultTheme || "a"; |
| }, |
| |
| // TODO the following $ and $.fn extensions can/probably should be moved into jquery.mobile.core.helpers |
| // |
| // Find the closest javascript page element to gather settings data jsperf test |
| // http://jsperf.com/single-complex-selector-vs-many-complex-selectors/edit |
| // possibly naive, but it shows that the parsing overhead for *just* the page selector vs |
| // the page and dialog selector is negligable. This could probably be speed up by |
| // doing a similar parent node traversal to the one found in the inherited theme code above |
| closestPageData: function( $target ) { |
| return $target |
| .closest( ':jqmData(role="page"), :jqmData(role="dialog")' ) |
| .data( "page" ); |
| }, |
| |
| enhanceable: function( $set ) { |
| return this.haveParents( $set, "enhance" ); |
| }, |
| |
| hijackable: function( $set ) { |
| return this.haveParents( $set, "ajax" ); |
| }, |
| |
| haveParents: function( $set, attr ) { |
| if ( !$.mobile.ignoreContentEnabled ) { |
| return $set; |
| } |
| |
| var count = $set.length, |
| $newSet = $(), |
| e, $element, excluded; |
| |
| for ( var i = 0; i < count; i++ ) { |
| $element = $set.eq( i ); |
| excluded = false; |
| e = $set[ i ]; |
| |
| while ( e ) { |
| var c = e.getAttribute ? e.getAttribute( "data-" + $.mobile.ns + attr ) : ""; |
| |
| if ( c === "false" ) { |
| excluded = true; |
| break; |
| } |
| |
| e = e.parentNode; |
| } |
| |
| if ( !excluded ) { |
| $newSet = $newSet.add( $element ); |
| } |
| } |
| |
| return $newSet; |
| }, |
| |
| getScreenHeight: function() { |
| // Native innerHeight returns more accurate value for this across platforms, |
| // jQuery version is here as a normalized fallback for platforms like Symbian |
| return window.innerHeight || $.mobile.$window.height(); |
| } |
| }, $.mobile ); |
| |
| // Mobile version of data and removeData and hasData methods |
| // ensures all data is set and retrieved using jQuery Mobile's data namespace |
| $.fn.jqmData = function( prop, value ) { |
| var result; |
| if ( typeof prop !== "undefined" ) { |
| if ( prop ) { |
| prop = $.mobile.nsNormalize( prop ); |
| } |
| |
| // undefined is permitted as an explicit input for the second param |
| // in this case it returns the value and does not set it to undefined |
| if( arguments.length < 2 || value === undefined ){ |
| result = this.data( prop ); |
| } else { |
| result = this.data( prop, value ); |
| } |
| } |
| return result; |
| }; |
| |
| $.jqmData = function( elem, prop, value ) { |
| var result; |
| if ( typeof prop !== "undefined" ) { |
| result = $.data( elem, prop ? $.mobile.nsNormalize( prop ) : prop, value ); |
| } |
| return result; |
| }; |
| |
| $.fn.jqmRemoveData = function( prop ) { |
| return this.removeData( $.mobile.nsNormalize( prop ) ); |
| }; |
| |
| $.jqmRemoveData = function( elem, prop ) { |
| return $.removeData( elem, $.mobile.nsNormalize( prop ) ); |
| }; |
| |
| $.fn.removeWithDependents = function() { |
| $.removeWithDependents( this ); |
| }; |
| |
| $.removeWithDependents = function( elem ) { |
| var $elem = $( elem ); |
| |
| ( $elem.jqmData( 'dependents' ) || $() ).remove(); |
| $elem.remove(); |
| }; |
| |
| $.fn.addDependents = function( newDependents ) { |
| $.addDependents( $( this ), newDependents ); |
| }; |
| |
| $.addDependents = function( elem, newDependents ) { |
| var dependents = $( elem ).jqmData( 'dependents' ) || $(); |
| |
| $( elem ).jqmData( 'dependents', $.merge( dependents, newDependents ) ); |
| }; |
| |
| // note that this helper doesn't attempt to handle the callback |
| // or setting of an html elements text, its only purpose is |
| // to return the html encoded version of the text in all cases. (thus the name) |
| $.fn.getEncodedText = function() { |
| return $( "<div/>" ).text( $( this ).text() ).html(); |
| }; |
| |
| // fluent helper function for the mobile namespaced equivalent |
| $.fn.jqmEnhanceable = function() { |
| return $.mobile.enhanceable( this ); |
| }; |
| |
| $.fn.jqmHijackable = function() { |
| return $.mobile.hijackable( this ); |
| }; |
| |
| // Monkey-patching Sizzle to filter the :jqmData selector |
| var oldFind = $.find, |
| jqmDataRE = /:jqmData\(([^)]*)\)/g; |
| |
| $.find = function( selector, context, ret, extra ) { |
| selector = selector.replace( jqmDataRE, "[data-" + ( $.mobile.ns || "" ) + "$1]" ); |
| |
| return oldFind.call( this, selector, context, ret, extra ); |
| }; |
| |
| $.extend( $.find, oldFind ); |
| |
| $.find.matches = function( expr, set ) { |
| return $.find( expr, null, null, set ); |
| }; |
| |
| $.find.matchesSelector = function( node, expr ) { |
| return $.find( expr, null, null, [ node ] ).length > 0; |
| }; |
| |
| $.extend({ |
| creatorDict: {}, |
| |
| delegateSelfInitWithSingleSelector: function( target, useKeepNative ) { |
| if ( typeof target !== 'function' ) { |
| return false; |
| } |
| var selector = target.prototype.options.initSelector; |
| var selectorRE = /:jqmData\(role='[A-z\-]+'\)$/; |
| if ( selectorRE.test(selector) ) { |
| var firstIdx = selector.indexOf( "'" ) + 1; |
| var lastIdx = selector.lastIndexOf( "'" ); |
| var key = selector.substring( firstIdx, lastIdx ); |
| if ( !$.creatorDict.hasOwnProperty( key ) ) { |
| $.creatorDict[key] = {}; |
| $.creatorDict[key].target = target; |
| if ( useKeepNative === true ) { |
| $.creatorDict[key].useKeepNative = useKeepNative; |
| } |
| return true; |
| } |
| } |
| return false; |
| } |
| }); |
| |
| //auto self-init widgets |
| $( document ).bind( "pagecreate create", function( e ) { |
| var selector = "*[data-" + $.mobile.ns + "role]"; |
| $( selector, e.target ).each( function () { |
| var dataRoleValue = this.getAttribute( "data-role" ), |
| matchedObj = $.creatorDict[dataRoleValue]; |
| if ( matchedObj ) { |
| matchedObj.target.prototype.enhance( this, matchedObj.useKeepNative ); |
| } |
| }); |
| }); |
| })( jQuery, this ); |
| |
| |
| /*! |
| * jQuery UI Widget v1.9.0-beta.1 |
| * |
| * Copyright 2012, https://github.com/jquery/jquery-ui/blob/1.9.0-beta.1/AUTHORS.txt (http://jqueryui.com/about) |
| * Dual licensed under the MIT or GPL Version 2 licenses. |
| * http://jquery.org/license |
| * |
| * http://docs.jquery.com/UI/Widget |
| */ |
| |
| |
| (function( $, undefined ) { |
| |
| var uuid = 0, |
| slice = Array.prototype.slice, |
| _cleanData = $.cleanData; |
| $.cleanData = function( elems ) { |
| for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { |
| try { |
| $( elem ).triggerHandler( "remove" ); |
| // http://bugs.jquery.com/ticket/8235 |
| } catch( e ) {} |
| } |
| _cleanData( elems ); |
| }; |
| |
| $.widget = function( name, base, prototype ) { |
| var fullName, existingConstructor, constructor, basePrototype, |
| namespace = name.split( "." )[ 0 ]; |
| |
| name = name.split( "." )[ 1 ]; |
| fullName = namespace + "-" + name; |
| |
| if ( !prototype ) { |
| prototype = base; |
| base = $.Widget; |
| } |
| |
| // create selector for plugin |
| $.expr[ ":" ][ fullName ] = function( elem ) { |
| return !!$.data( elem, fullName ); |
| }; |
| |
| $[ namespace ] = $[ namespace ] || {}; |
| existingConstructor = $[ namespace ][ name ]; |
| constructor = $[ namespace ][ name ] = function( options, element ) { |
| // allow instantiation without "new" keyword |
| if ( !this._createWidget ) { |
| return new constructor( options, element ); |
| } |
| |
| // allow instantiation without initializing for simple inheritance |
| // must use "new" keyword (the code above always passes args) |
| if ( arguments.length ) { |
| this._createWidget( options, element ); |
| } |
| }; |
| // extend with the existing constructor to carry over any static properties |
| $.extend( constructor, existingConstructor, { |
| version: prototype.version, |
| // copy the object used to create the prototype in case we need to |
| // redefine the widget later |
| _proto: $.extend( {}, prototype ), |
| // track widgets that inherit from this widget in case this widget is |
| // redefined after a widget inherits from it |
| _childConstructors: [] |
| }); |
| |
| basePrototype = new base(); |
| // we need to make the options hash a property directly on the new instance |
| // otherwise we'll modify the options hash on the prototype that we're |
| // inheriting from |
| basePrototype.options = $.widget.extend( {}, basePrototype.options ); |
| $.each( prototype, function( prop, value ) { |
| if ( $.isFunction( value ) ) { |
| prototype[ prop ] = (function() { |
| var _super = function() { |
| return base.prototype[ prop ].apply( this, arguments ); |
| }, |
| _superApply = function( args ) { |
| return base.prototype[ prop ].apply( this, args ); |
| }; |
| return function() { |
| var __super = this._super, |
| __superApply = this._superApply, |
| returnValue; |
| |
| this._super = _super; |
| this._superApply = _superApply; |
| |
| returnValue = value.apply( this, arguments ); |
| |
| this._super = __super; |
| this._superApply = __superApply; |
| |
| return returnValue; |
| }; |
| })(); |
| } |
| }); |
| constructor.prototype = $.widget.extend( basePrototype, { |
| // TODO: remove support for widgetEventPrefix |
| // always use the name + a colon as the prefix, e.g., draggable:start |
| // don't prefix for widgets that aren't DOM-based |
| widgetEventPrefix: name |
| }, prototype, { |
| constructor: constructor, |
| namespace: namespace, |
| widgetName: name, |
| // TODO remove widgetBaseClass, see #8155 |
| widgetBaseClass: fullName, |
| widgetFullName: fullName |
| }); |
| |
| // If this widget is being redefined then we need to find all widgets that |
| // are inheriting from it and redefine all of them so that they inherit from |
| // the new version of this widget. We're essentially trying to replace one |
| // level in the prototype chain. |
| if ( existingConstructor ) { |
| $.each( existingConstructor._childConstructors, function( i, child ) { |
| var childPrototype = child.prototype; |
| |
| // redefine the child widget using the same prototype that was |
| // originally used, but inherit from the new version of the base |
| $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); |
| }); |
| // remove the list of existing child constructors from the old constructor |
| // so the old child constructors can be garbage collected |
| delete existingConstructor._childConstructors; |
| } else { |
| base._childConstructors.push( constructor ); |
| } |
| |
| $.widget.bridge( name, constructor ); |
| }; |
| |
| $.widget.extend = function( target ) { |
| var input = slice.call( arguments, 1 ), |
| inputIndex = 0, |
| inputLength = input.length, |
| key, |
| value; |
| for ( ; inputIndex < inputLength; inputIndex++ ) { |
| for ( key in input[ inputIndex ] ) { |
| value = input[ inputIndex ][ key ]; |
| if (input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { |
| target[ key ] = $.isPlainObject( value ) ? $.widget.extend( {}, target[ key ], value ) : value; |
| } |
| } |
| } |
| return target; |
| }; |
| |
| $.widget.bridge = function( name, object ) { |
| var fullName = object.prototype.widgetFullName; |
| $.fn[ name ] = function( options ) { |
| var isMethodCall = typeof options === "string", |
| args = slice.call( arguments, 1 ), |
| returnValue = this; |
| |
| // allow multiple hashes to be passed on init |
| options = !isMethodCall && args.length ? |
| $.widget.extend.apply( null, [ options ].concat(args) ) : |
| options; |
| |
| if ( isMethodCall ) { |
| this.each(function() { |
| var methodValue, |
| instance = $.data( this, fullName ); |
| if ( !instance ) { |
| return $.error( "cannot call methods on " + name + " prior to initialization; " + |
| "attempted to call method '" + options + "'" ); |
| } |
| if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { |
| return $.error( "no such method '" + options + "' for " + name + " widget instance" ); |
| } |
| methodValue = instance[ options ].apply( instance, args ); |
| if ( methodValue !== instance && methodValue !== undefined ) { |
| returnValue = methodValue && methodValue.jquery ? |
| returnValue.pushStack( methodValue.get() ) : |
| methodValue; |
| return false; |
| } |
| }); |
| } else { |
| this.each(function() { |
| var instance = $.data( this, fullName ); |
| if ( instance ) { |
| instance.option( options || {} )._init(); |
| } else { |
| new object( options, this ); |
| } |
| }); |
| } |
| |
| return returnValue; |
| }; |
| }; |
| |
| $.Widget = function( options, element ) {}; |
| $.Widget._childConstructors = []; |
| |
| $.Widget.prototype = { |
| widgetName: "widget", |
| widgetEventPrefix: "", |
| defaultElement: "<div>", |
| options: { |
| disabled: false, |
| |
| // callbacks |
| create: null |
| }, |
| _createWidget: function( options, element ) { |
| element = $( element || this.defaultElement || this )[ 0 ]; |
| this.element = $( element ); |
| this.uuid = uuid++; |
| this.eventNamespace = "." + this.widgetName + this.uuid; |
| this.options = $.widget.extend( {}, |
| this.options, |
| this._getCreateOptions(), |
| options ); |
| |
| this.bindings = $(); |
| this.hoverable = $(); |
| this.focusable = $(); |
| |
| if ( element !== this ) { |
| // 1.9 BC for #7810 |
| // TODO remove dual storage |
| $.data( element, this.widgetName, this ); |
| $.data( element, this.widgetFullName, this ); |
| this._on({ remove: "destroy" }); |
| this.document = $( element.style ? |
| // element within the document |
| element.ownerDocument : |
| // element is window or document |
| element.document || element ); |
| this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); |
| } |
| |
| this._create(); |
| this._trigger( "create", null, this._getCreateEventData() ); |
| this._init(); |
| }, |
| _getCreateOptions: $.noop, |
| _getCreateEventData: $.noop, |
| _create: $.noop, |
| _init: $.noop, |
| |
| destroy: function() { |
| this._destroy(); |
| // we can probably remove the unbind calls in 2.0 |
| // all event bindings should go through this._on() |
| this.element |
| .unbind( this.eventNamespace ) |
| // 1.9 BC for #7810 |
| // TODO remove dual storage |
| .removeData( this.widgetName ) |
| .removeData( this.widgetFullName ) |
| // support: jquery <1.6.3 |
| // http://bugs.jquery.com/ticket/9413 |
| .removeData( $.camelCase( this.widgetFullName ) ); |
| this.widget() |
| .unbind( this.eventNamespace ) |
| .removeAttr( "aria-disabled" ) |
| .removeClass( |
| this.widgetFullName + "-disabled " + |
| "ui-state-disabled" ); |
| |
| // clean up events and states |
| this.bindings.unbind( this.eventNamespace ); |
| this.hoverable.removeClass( "ui-state-hover" ); |
| this.focusable.removeClass( "ui-state-focus" ); |
| }, |
| _destroy: $.noop, |
| |
| widget: function() { |
| return this.element; |
| }, |
| |
| option: function( key, value ) { |
| var options = key, |
| parts, |
| curOption, |
| i; |
| |
| if ( arguments.length === 0 ) { |
| // don't return a reference to the internal hash |
| return $.widget.extend( {}, this.options ); |
| } |
| |
| if ( typeof key === "string" ) { |
| // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } |
| options = {}; |
| parts = key.split( "." ); |
| key = parts.shift(); |
| if ( parts.length ) { |
| curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); |
| for ( i = 0; i < parts.length - 1; i++ ) { |
| curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; |
| curOption = curOption[ parts[ i ] ]; |
| } |
| key = parts.pop(); |
| if ( value === undefined ) { |
| return curOption[ key ] === undefined ? null : curOption[ key ]; |
| } |
| curOption[ key ] = value; |
| } else { |
| if ( value === undefined ) { |
| return this.options[ key ] === undefined ? null : this.options[ key ]; |
| } |
| options[ key ] = value; |
| } |
| } |
| |
| this._setOptions( options ); |
| |
| return this; |
| }, |
| _setOptions: function( options ) { |
| var key; |
| |
| for ( key in options ) { |
| this._setOption( key, options[ key ] ); |
| } |
| |
| return this; |
| }, |
| _setOption: function( key, value ) { |
| this.options[ key ] = value; |
| |
| if ( key === "disabled" ) { |
| this.widget() |
| .toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value ) |
| .attr( "aria-disabled", value ); |
| this.hoverable.removeClass( "ui-state-hover" ); |
| this.focusable.removeClass( "ui-state-focus" ); |
| } |
| |
| return this; |
| }, |
| |
| enable: function() { |
| return this._setOption( "disabled", false ); |
| }, |
| disable: function() { |
| return this._setOption( "disabled", true ); |
| }, |
| |
| _on: function( element, handlers ) { |
| // no element argument, shuffle and use this.element |
| if ( !handlers ) { |
| handlers = element; |
| element = this.element; |
| } else { |
| // accept selectors, DOM elements |
| element = $( element ); |
| this.bindings = this.bindings.add( element ); |
| } |
| |
| var instance = this; |
| $.each( handlers, function( event, handler ) { |
| function handlerProxy() { |
| // allow widgets to customize the disabled handling |
| // - disabled as an array instead of boolean |
| // - disabled class as method for disabling individual parts |
| if ( instance.options.disabled === true || |
| $( this ).hasClass( "ui-state-disabled" ) ) { |
| return; |
| } |
| return ( typeof handler === "string" ? instance[ handler ] : handler ) |
| .apply( instance, arguments ); |
| } |
| |
| // copy the guid so direct unbinding works |
| if ( typeof handler !== "string" ) { |
| handlerProxy.guid = handler.guid = |
| handler.guid || handlerProxy.guid || $.guid++; |
| } |
| |
| var match = event.match( /^(\w+)\s*(.*)$/ ), |
| eventName = match[1] + instance.eventNamespace, |
| selector = match[2]; |
| if ( selector ) { |
| instance.widget().delegate( selector, eventName, handlerProxy ); |
| } else { |
| element.bind( eventName, handlerProxy ); |
| } |
| }); |
| }, |
| |
| _off: function( element, eventName ) { |
| eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace; |
| element.unbind( eventName ).undelegate( eventName ); |
| }, |
| |
| _delay: function( handler, delay ) { |
| function handlerProxy() { |
| return ( typeof handler === "string" ? instance[ handler ] : handler ) |
| .apply( instance, arguments ); |
| } |
| var instance = this; |
| return setTimeout( handlerProxy, delay || 0 ); |
| }, |
| |
| _hoverable: function( element ) { |
| this.hoverable = this.hoverable.add( element ); |
| this._on( element, { |
| mouseenter: function( event ) { |
| $( event.currentTarget ).addClass( "ui-state-hover" ); |
| }, |
| mouseleave: function( event ) { |
| $( event.currentTarget ).removeClass( "ui-state-hover" ); |
| } |
| }); |
| }, |
| |
| _focusable: function( element ) { |
| this.focusable = this.focusable.add( element ); |
| this._on( element, { |
| focusin: function( event ) { |
| $( event.currentTarget ).addClass( "ui-state-focus" ); |
| }, |
| focusout: function( event ) { |
| $( event.currentTarget ).removeClass( "ui-state-focus" ); |
| } |
| }); |
| }, |
| |
| _trigger: function( type, event, data ) { |
| var prop, orig, |
| callback = this.options[ type ]; |
| |
| data = data || {}; |
| event = $.Event( event ); |
| event.type = ( type === this.widgetEventPrefix ? |
| type : |
| this.widgetEventPrefix + type ).toLowerCase(); |
| // the original event may come from any element |
| // so we need to reset the target on the new event |
| event.target = this.element[ 0 ]; |
| |
| // copy original event properties over to the new event |
| orig = event.originalEvent; |
| if ( orig ) { |
| for ( prop in orig ) { |
| if ( !( prop in event ) ) { |
| event[ prop ] = orig[ prop ]; |
| } |
| } |
| } |
| |
| this.element.trigger( event, data ); |
| return !( $.isFunction( callback ) && |
| callback.apply( this.element[0], [ event ].concat( data ) ) === false || |
| event.isDefaultPrevented() ); |
| } |
| }; |
| |
| $.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { |
| $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { |
| if ( typeof options === "string" ) { |
| options = { effect: options }; |
| } |
| var hasOptions, |
| effectName = !options ? |
| method : |
| options === true || typeof options === "number" ? |
| defaultEffect : |
| options.effect || defaultEffect; |
| options = options || {}; |
| if ( typeof options === "number" ) { |
| options = { duration: options }; |
| } |
| hasOptions = !$.isEmptyObject( options ); |
| options.complete = callback; |
| if ( options.delay ) { |
| element.delay( options.delay ); |
| } |
| if ( hasOptions && $.effects && ( $.effects.effect[ effectName ] || $.uiBackCompat !== false && $.effects[ effectName ] ) ) { |
| element[ method ]( options ); |
| } else if ( effectName !== method && element[ effectName ] ) { |
| element[ effectName ]( options.duration, options.easing, callback ); |
| } else { |
| element.queue(function( next ) { |
| $( this )[ method ](); |
| if ( callback ) { |
| callback.call( element[ 0 ] ); |
| } |
| next(); |
| }); |
| } |
| }; |
| }); |
| |
| // DEPRECATED |
| if ( $.uiBackCompat !== false ) { |
| $.Widget.prototype._getCreateOptions = function() { |
| return $.metadata && $.metadata.get( this.element[0] )[ this.widgetName ]; |
| }; |
| } |
| |
| })( jQuery ); |
| (function( $, undefined ) { |
| |
| $.widget( "mobile.widget", { |
| // decorate the parent _createWidget to trigger `widgetinit` for users |
| // who wish to do post post `widgetcreate` alterations/additions |
| // |
| // TODO create a pull request for jquery ui to trigger this event |
| // in the original _createWidget |
| _createWidget: function() { |
| $.Widget.prototype._createWidget.apply( this, arguments ); |
| this._trigger( 'init' ); |
| }, |
| |
| _getCreateOptions: function() { |
| |
| var elem = this.element, |
| options = {}; |
| |
| $.each( this.options, function( option ) { |
| |
| var value = elem.jqmData( option.replace( /[A-Z]/g, function( c ) { |
| return "-" + c.toLowerCase(); |
| }) |
| ); |
| |
| if ( value !== undefined ) { |
| options[ option ] = value; |
| } |
| }); |
| |
| return options; |
| }, |
| |
| enhanceWithin: function( target, useKeepNative ) { |
| this.enhance( $( this.options.initSelector, $( target )), useKeepNative ); |
| }, |
| |
| enhance: function( targets, useKeepNative ) { |
| var page, keepNative, $widgetElements = $( targets ), self = this; |
| |
| // if ignoreContentEnabled is set to true the framework should |
| // only enhance the selected elements when they do NOT have a |
| // parent with the data-namespace-ignore attribute |
| $widgetElements = $.mobile.enhanceable( $widgetElements ); |
| |
| if ( useKeepNative && $widgetElements.length ) { |
| // TODO remove dependency on the page widget for the keepNative. |
| // Currently the keepNative value is defined on the page prototype so |
| // the method is as well |
| page = $.mobile.closestPageData( $widgetElements ); |
| keepNative = ( page && page.keepNativeSelector()) || ""; |
| |
| $widgetElements = $widgetElements.not( keepNative ); |
| } |
| |
| $widgetElements[ this.widgetName ](); |
| }, |
| |
| raise: function( msg ) { |
| throw "Widget [" + this.widgetName + "]: " + msg; |
| } |
| }); |
| |
| })( jQuery ); |
| |
| |
| (function( $, window ) { |
| // DEPRECATED |
| // NOTE global mobile object settings |
| $.extend( $.mobile, { |
| // DEPRECATED Should the text be visble in the loading message? |
| loadingMessageTextVisible: undefined, |
| |
| // DEPRECATED When the text is visible, what theme does the loading box use? |
| loadingMessageTheme: undefined, |
| |
| // DEPRECATED default message setting |
| loadingMessage: undefined, |
| |
| // DEPRECATED |
| // Turn on/off page loading message. Theme doubles as an object argument |
| // with the following shape: { theme: '', text: '', html: '', textVisible: '' } |
| // NOTE that the $.mobile.loading* settings and params past the first are deprecated |
| showPageLoadingMsg: function( theme, msgText, textonly ) { |
| $.mobile.loading( 'show', theme, msgText, textonly ); |
| }, |
| |
| // DEPRECATED |
| hidePageLoadingMsg: function() { |
| $.mobile.loading( 'hide' ); |
| }, |
| |
| loading: function() { |
| this.loaderWidget.loader.apply( this.loaderWidget, arguments ); |
| } |
| }); |
| |
| // TODO move loader class down into the widget settings |
| var loaderClass = "ui-loader", $html = $( "html" ), $window = $.mobile.$window; |
| |
| $.widget( "mobile.loader", { |
| // NOTE if the global config settings are defined they will override these |
| // options |
| options: { |
| // the theme for the loading message |
| theme: "a", |
| |
| // whether the text in the loading message is shown |
| textVisible: false, |
| |
| // custom html for the inner content of the loading message |
| html: "", |
| |
| // the text to be displayed when the popup is shown |
| text: "loading" |
| }, |
| |
| defaultHtml: "<div class='" + loaderClass + "'>" + |
| "<span class='ui-icon ui-icon-loading'></span>" + |
| "<h1></h1>" + |
| "</div>", |
| |
| // For non-fixed supportin browsers. Position at y center (if scrollTop supported), above the activeBtn (if defined), or just 100px from top |
| fakeFixLoader: function() { |
| var activeBtn = $( "." + $.mobile.activeBtnClass ).first(); |
| |
| this.element |
| .css({ |
| top: $.support.scrollTop && $window.scrollTop() + $window.height() / 2 || |
| activeBtn.length && activeBtn.offset().top || 100 |
| }); |
| }, |
| |
| // check position of loader to see if it appears to be "fixed" to center |
| // if not, use abs positioning |
| checkLoaderPosition: function() { |
| var offset = this.element.offset(), |
| scrollTop = $window.scrollTop(), |
| screenHeight = $.mobile.getScreenHeight(); |
| |
| if ( offset.top < scrollTop || ( offset.top - scrollTop ) > screenHeight ) { |
| this.element.addClass( "ui-loader-fakefix" ); |
| this.fakeFixLoader(); |
| $window |
| .unbind( "scroll", this.checkLoaderPosition ) |
| .bind( "scroll", this.fakeFixLoader ); |
| } |
| }, |
| |
| resetHtml: function() { |
| this.element.html( $( this.defaultHtml ).html() ); |
| }, |
| |
| // Turn on/off page loading message. Theme doubles as an object argument |
| // with the following shape: { theme: '', text: '', html: '', textVisible: '' } |
| // NOTE that the $.mobile.loading* settings and params past the first are deprecated |
| // TODO sweet jesus we need to break some of this out |
| show: function( theme, msgText, textonly ) { |
| var textVisible, message, $header, loadSettings; |
| |
| this.resetHtml(); |
| |
| // use the prototype options so that people can set them globally at |
| // mobile init. Consistency, it's what's for dinner |
| if ( $.type(theme) === "object" ) { |
| loadSettings = $.extend( {}, this.options, theme ); |
| |
| // prefer object property from the param then the old theme setting |
| theme = loadSettings.theme || $.mobile.loadingMessageTheme; |
| } else { |
| loadSettings = this.options; |
| |
| // here we prefer the them value passed as a string argument, then |
| // we prefer the global option because we can't use undefined default |
| // prototype options, then the prototype option |
| theme = theme || $.mobile.loadingMessageTheme || loadSettings.theme; |
| } |
| |
| // set the message text, prefer the param, then the settings object |
| // then loading message |
| message = msgText || $.mobile.loadingMessage || loadSettings.text; |
| |
| // prepare the dom |
| $html.addClass( "ui-loading" ); |
| |
| if ( $.mobile.loadingMessage !== false || loadSettings.html ) { |
| // boolean values require a bit more work :P, supports object properties |
| // and old settings |
| if ( $.mobile.loadingMessageTextVisible !== undefined ) { |
| textVisible = $.mobile.loadingMessageTextVisible; |
| } else { |
| textVisible = loadSettings.textVisible; |
| } |
| |
| // add the proper css given the options (theme, text, etc) |
| // Force text visibility if the second argument was supplied, or |
| // if the text was explicitly set in the object args |
| this.element.attr("class", loaderClass + |
| " ui-corner-all ui-body-" + theme + |
| " ui-loader-" + ( textVisible || msgText || theme.text ? "verbose" : "default" ) + |
| ( loadSettings.textonly || textonly ? " ui-loader-textonly" : "" ) ); |
| |
| // TODO verify that jquery.fn.html is ok to use in both cases here |
| // this might be overly defensive in preventing unknowing xss |
| // if the html attribute is defined on the loading settings, use that |
| // otherwise use the fallbacks from above |
| if ( loadSettings.html ) { |
| this.element.html( loadSettings.html ); |
| } else { |
| this.element.find( "h1" ).text( message ); |
| } |
| |
| // attach the loader to the DOM |
| this.element.appendTo( $.mobile.pageContainer ); |
| |
| // check that the loader is visible |
| this.checkLoaderPosition(); |
| |
| // on scroll check the loader position |
| $window.bind( "scroll", $.proxy( this.checkLoaderPosition, this ) ); |
| } |
| }, |
| |
| hide: function() { |
| $html.removeClass( "ui-loading" ); |
| |
| if ( $.mobile.loadingMessage ) { |
| this.element.removeClass( "ui-loader-fakefix" ); |
| } |
| |
| $.mobile.$window.unbind( "scroll", $.proxy( this.fakeFixLoader, this) ); |
| $.mobile.$window.unbind( "scroll", $.proxy( this.checkLoaderPosition, this ) ); |
| } |
| }); |
| |
| $window.bind( 'pagecontainercreate', function() { |
| $.mobile.loaderWidget = $.mobile.loaderWidget || $( $.mobile.loader.prototype.defaultHtml ).loader(); |
| }); |
| })(jQuery, this); |
| |
| |
| |
| // This plugin is an experiment for abstracting away the touch and mouse |
| // events so that developers don't have to worry about which method of input |
| // the device their document is loaded on supports. |
| // |
| // The idea here is to allow the developer to register listeners for the |
| // basic mouse events, such as mousedown, mousemove, mouseup, and click, |
| // and the plugin will take care of registering the correct listeners |
| // behind the scenes to invoke the listener at the fastest possible time |
| // for that device, while still retaining the order of event firing in |
| // the traditional mouse environment, should multiple handlers be registered |
| // on the same element for different events. |
| // |
| // The current version exposes the following virtual events to jQuery bind methods: |
| // "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel" |
| |
| (function( $, window, document, undefined ) { |
| |
| var dataPropertyName = "virtualMouseBindings", |
| touchTargetPropertyName = "virtualTouchID", |
| virtualEventNames = "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel".split( " " ), |
| touchEventProps = "clientX clientY pageX pageY screenX screenY".split( " " ), |
| mouseHookProps = $.event.mouseHooks ? $.event.mouseHooks.props : [], |
| mouseEventProps = $.event.props.concat( mouseHookProps ), |
| activeDocHandlers = {}, |
| resetTimerID = 0, |
| startX = 0, |
| startY = 0, |
| didScroll = false, |
| clickBlockList = [], |
| blockMouseTriggers = false, |
| blockTouchTriggers = false, |
| eventCaptureSupported = "addEventListener" in document, |
| $document = $.mobile.$document, |
| nextTouchID = 1, |
| lastTouchID = 0, threshold; |
| |
| $.vmouse = { |
| moveDistanceThreshold: 10, |
| clickDistanceThreshold: 10, |
| resetTimerDuration: 1500 |
| }; |
| |
| function getNativeEvent( event ) { |
| |
| while ( event && typeof event.originalEvent !== "undefined" ) { |
| event = event.originalEvent; |
| } |
| return event; |
| } |
| |
| function createVirtualEvent( event, eventType ) { |
| |
| var t = event.type, |
| oe, props, ne, prop, ct, touch, i, j, len; |
| |
| event = $.Event( event ); |
| event.type = eventType; |
| |
| oe = event.originalEvent; |
| props = $.event.props; |
| |
| // addresses separation of $.event.props in to $.event.mouseHook.props and Issue 3280 |
| // https://github.com/jquery/jquery-mobile/issues/3280 |
| if ( t.search( /^(mouse|click)/ ) > -1 ) { |
| props = mouseEventProps; |
| } |
| |
| // copy original event properties over to the new event |
| // this would happen if we could call $.event.fix instead of $.Event |
| // but we don't have a way to force an event to be fixed multiple times |
| if ( oe ) { |
| for ( i = props.length, prop; i; ) { |
| prop = props[ --i ]; |
| event[ prop ] = oe[ prop ]; |
| } |
| } |
| |
| // make sure that if the mouse and click virtual events are generated |
| // without a .which one is defined |
| if ( t.search(/mouse(down|up)|click/) > -1 && !event.which ) { |
| event.which = 1; |
| } |
| |
| if ( t.search(/^touch/) !== -1 ) { |
| ne = getNativeEvent( oe ); |
| t = ne.touches; |
| ct = ne.changedTouches; |
| touch = ( t && t.length ) ? t[0] : ( ( ct && ct.length ) ? ct[ 0 ] : undefined ); |
| |
| if ( touch ) { |
| for ( j = 0, len = touchEventProps.length; j < len; j++) { |
| prop = touchEventProps[ j ]; |
| event[ prop ] = touch[ prop ]; |
| } |
| } |
| } |
| |
| return event; |
| } |
| |
| function getVirtualBindingFlags( element ) { |
| |
| var flags = {}, |
| b, k; |
| |
| while ( element ) { |
| |
| b = $.data( element, dataPropertyName ); |
| |
| for ( k in b ) { |
| if ( b[ k ] ) { |
| flags[ k ] = flags.hasVirtualBinding = true; |
| } |
| } |
| element = element.parentNode; |
| } |
| return flags; |
| } |
| |
| function getClosestElementWithVirtualBinding( element, eventType ) { |
| var b; |
| while ( element ) { |
| |
| b = $.data( element, dataPropertyName ); |
| |
| if ( b && ( !eventType || b[ eventType ] ) ) { |
| return element; |
| } |
| element = element.parentNode; |
| } |
| return null; |
| } |
| |
| function enableTouchBindings() { |
| blockTouchTriggers = false; |
| } |
| |
| function disableTouchBindings() { |
| blockTouchTriggers = true; |
| } |
| |
| function enableMouseBindings() { |
| lastTouchID = 0; |
| clickBlockList.length = 0; |
| blockMouseTriggers = false; |
| |
| // When mouse bindings are enabled, our |
| // touch bindings are disabled. |
| disableTouchBindings(); |
| } |
| |
| function disableMouseBindings() { |
| // When mouse bindings are disabled, our |
| // touch bindings are enabled. |
| enableTouchBindings(); |
| } |
| |
| function startResetTimer() { |
| clearResetTimer(); |
| resetTimerID = setTimeout( function() { |
| resetTimerID = 0; |
| enableMouseBindings(); |
| }, $.vmouse.resetTimerDuration ); |
| } |
| |
| function clearResetTimer() { |
| if ( resetTimerID ) { |
| clearTimeout( resetTimerID ); |
| resetTimerID = 0; |
| } |
| } |
| |
| function triggerVirtualEvent( eventType, event, flags ) { |
| var ve; |
| |
| if ( ( flags && flags[ eventType ] ) || |
| ( !flags && getClosestElementWithVirtualBinding( event.target, eventType ) ) ) { |
| |
| ve = createVirtualEvent( event, eventType ); |
| |
| $( event.target).trigger( ve ); |
| } |
| |
| return ve; |
| } |
| |
| function mouseEventCallback( event ) { |
| var touchID = $.data( event.target, touchTargetPropertyName ); |
| |
| if ( ( $.support.touch === true ) && ( touchID === undefined ) ) { |
| return; |
| } |
| |
| if ( !blockMouseTriggers && ( !lastTouchID || lastTouchID !== touchID ) ) { |
| var ve = triggerVirtualEvent( "v" + event.type, event ); |
| if ( ve ) { |
| if ( ve.isDefaultPrevented() ) { |
| event.preventDefault(); |
| } |
| if ( ve.isPropagationStopped() ) { |
| event.stopPropagation(); |
| } |
| if ( ve.isImmediatePropagationStopped() ) { |
| event.stopImmediatePropagation(); |
| } |
| } |
| } |
| } |
| |
| function handleTouchStart( event ) { |
| |
| var touches = getNativeEvent( event ).touches, |
| target, flags; |
| |
| if ( touches && touches.length === 1 ) { |
| |
| target = event.target; |
| flags = getVirtualBindingFlags( target ); |
| |
| if ( flags.hasVirtualBinding ) { |
| |
| lastTouchID = nextTouchID++; |
| $.data( target, touchTargetPropertyName, lastTouchID ); |
| |
| clearResetTimer(); |
| |
| disableMouseBindings(); |
| didScroll = false; |
| |
| var t = getNativeEvent( event ).touches[ 0 ]; |
| startX = t.pageX; |
| startY = t.pageY; |
| |
| triggerVirtualEvent( "vmouseover", event, flags ); |
| triggerVirtualEvent( "vmousedown", event, flags ); |
| } |
| } |
| } |
| |
| function handleScroll( event ) { |
| if ( blockTouchTriggers ) { |
| return; |
| } |
| |
| if ( !didScroll ) { |
| triggerVirtualEvent( "vmousecancel", event, getVirtualBindingFlags( event.target ) ); |
| } |
| |
| didScroll = true; |
| startResetTimer(); |
| } |
| |
| function handleTouchMove( event ) { |
| if ( blockTouchTriggers ) { |
| return; |
| } |
| |
| var t = getNativeEvent( event ).touches[ 0 ], |
| didCancel = didScroll, |
| moveThreshold = $.vmouse.moveDistanceThreshold, |
| flags = getVirtualBindingFlags( event.target ); |
| |
| didScroll = didScroll || |
| ( Math.abs( t.pageX - startX ) > moveThreshold || |
| Math.abs( t.pageY - startY ) > moveThreshold ); |
| |
| |
| if ( didScroll && !didCancel ) { |
| triggerVirtualEvent( "vmousecancel", event, flags ); |
| } |
| |
| triggerVirtualEvent( "vmousemove", event, flags ); |
| startResetTimer(); |
| } |
| |
| function handleTouchEnd( event ) { |
| if ( blockTouchTriggers ) { |
| return; |
| } |
| |
| disableTouchBindings(); |
| |
| var flags = getVirtualBindingFlags( event.target ), |
| t; |
| triggerVirtualEvent( "vmouseup", event, flags ); |
| |
| if ( !didScroll ) { |
| var ve = triggerVirtualEvent( "vclick", event, flags ); |
| if ( ve && ve.isDefaultPrevented() ) { |
| // The target of the mouse events that follow the touchend |
| // event don't necessarily match the target used during the |
| // touch. This means we need to rely on coordinates for blocking |
| // any click that is generated. |
| t = getNativeEvent( event ).changedTouches[ 0 ]; |
| clickBlockList.push({ |
| touchID: lastTouchID, |
| target: event.target, |
| x: t.clientX, |
| y: t.clientY |
| }); |
| |
| // Prevent any mouse events that follow from triggering |
| // virtual event notifications. |
| blockMouseTriggers = true; |
| } |
| } |
| triggerVirtualEvent( "vmouseout", event, flags); |
| didScroll = false; |
| |
| startResetTimer(); |
| } |
| |
| function hasVirtualBindings( ele ) { |
| var bindings = $.data( ele, dataPropertyName ), |
| k; |
| |
| if ( bindings ) { |
| for ( k in bindings ) { |
| if ( bindings[ k ] ) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| function dummyMouseHandler() {} |
| |
| function getSpecialEventObject( eventType ) { |
| var realType = eventType.substr( 1 ); |
| |
| return { |
| setup: function( data, namespace ) { |
| // If this is the first virtual mouse binding for this element, |
| // add a bindings object to its data. |
| |
| if ( !hasVirtualBindings( this ) ) { |
| $.data( this, dataPropertyName, {} ); |
| } |
| |
| // If setup is called, we know it is the first binding for this |
| // eventType, so initialize the count for the eventType to zero. |
| var bindings = $.data( this, dataPropertyName ); |
| bindings[ eventType ] = true; |
| |
| // If this is the first virtual mouse event for this type, |
| // register a global handler on the document. |
| |
| activeDocHandlers[ eventType ] = ( activeDocHandlers[ eventType ] || 0 ) + 1; |
| |
| if ( activeDocHandlers[ eventType ] === 1 ) { |
| $document.bind( realType, mouseEventCallback ); |
| } |
| |
| // Some browsers, like Opera Mini, won't dispatch mouse/click events |
| // for elements unless they actually have handlers registered on them. |
| // To get around this, we register dummy handlers on the elements. |
| |
| $( this ).bind( realType, dummyMouseHandler ); |
| |
| // For now, if event capture is not supported, we rely on mouse handlers. |
| if ( eventCaptureSupported ) { |
| // If this is the first virtual mouse binding for the document, |
| // register our touchstart handler on the document. |
| |
| activeDocHandlers[ "touchstart" ] = ( activeDocHandlers[ "touchstart" ] || 0) + 1; |
| |
| if ( activeDocHandlers[ "touchstart" ] === 1 ) { |
| $document.bind( "touchstart", handleTouchStart ) |
| .bind( "touchend", handleTouchEnd ) |
| |
| // On touch platforms, touching the screen and then dragging your finger |
| // causes the window content to scroll after some distance threshold is |
| // exceeded. On these platforms, a scroll prevents a click event from being |
| // dispatched, and on some platforms, even the touchend is suppressed. To |
| // mimic the suppression of the click event, we need to watch for a scroll |
| // event. Unfortunately, some platforms like iOS don't dispatch scroll |
| // events until *AFTER* the user lifts their finger (touchend). This means |
| // we need to watch both scroll and touchmove events to figure out whether |
| // or not a scroll happenens before the touchend event is fired. |
| |
| .bind( "touchmove", handleTouchMove ) |
| .bind( "scroll", handleScroll ); |
| } |
| } |
| }, |
| |
| teardown: function( data, namespace ) { |
| // If this is the last virtual binding for this eventType, |
| // remove its global handler from the document. |
| |
| --activeDocHandlers[ eventType ]; |
| |
| if ( !activeDocHandlers[ eventType ] ) { |
| $document.unbind( realType, mouseEventCallback ); |
| } |
| |
| if ( eventCaptureSupported ) { |
| // If this is the last virtual mouse binding in existence, |
| // remove our document touchstart listener. |
| |
| --activeDocHandlers[ "touchstart" ]; |
| |
| if ( !activeDocHandlers[ "touchstart" ] ) { |
| $document.unbind( "touchstart", handleTouchStart ) |
| .unbind( "touchmove", handleTouchMove ) |
| .unbind( "touchend", handleTouchEnd ) |
| .unbind( "scroll", handleScroll ); |
| } |
| } |
| |
| var $this = $( this ), |
| bindings = $.data( this, dataPropertyName ); |
| |
| // teardown may be called when an element was |
| // removed from the DOM. If this is the case, |
| // jQuery core may have already stripped the element |
| // of any data bindings so we need to check it before |
| // using it. |
| if ( bindings ) { |
| bindings[ eventType ] = false; |
| } |
| |
| // Unregister the dummy event handler. |
| |
| $this.unbind( realType, dummyMouseHandler ); |
| |
| // If this is the last virtual mouse binding on the |
| // element, remove the binding data from the element. |
| |
| if ( !hasVirtualBindings( this ) ) { |
| $this.removeData( dataPropertyName ); |
| } |
| } |
| }; |
| } |
| |
| // Expose our custom events to the jQuery bind/unbind mechanism. |
| |
| for ( var i = 0; i < virtualEventNames.length; i++ ) { |
| $.event.special[ virtualEventNames[ i ] ] = getSpecialEventObject( virtualEventNames[ i ] ); |
| } |
| |
| // Add a capture click handler to block clicks. |
| // Note that we require event capture support for this so if the device |
| // doesn't support it, we punt for now and rely solely on mouse events. |
| if ( eventCaptureSupported ) { |
| document.addEventListener( "click", function( e ) { |
| var cnt = clickBlockList.length, |
| target = e.target, |
| x, y, ele, i, o, touchID; |
| |
| if ( cnt ) { |
| x = e.clientX; |
| y = e.clientY; |
| threshold = $.vmouse.clickDistanceThreshold; |
| |
| // The idea here is to run through the clickBlockList to see if |
| // the current click event is in the proximity of one of our |
| // vclick events that had preventDefault() called on it. If we find |
| // one, then we block the click. |
| // |
| // Why do we have to rely on proximity? |
| // |
| // Because the target of the touch event that triggered the vclick |
| // can be different from the target of the click event synthesized |
| // by the browser. The target of a mouse/click event that is syntehsized |
| // from a touch event seems to be implementation specific. For example, |
| // some browsers will fire mouse/click events for a link that is near |
| // a touch event, even though the target of the touchstart/touchend event |
| // says the user touched outside the link. Also, it seems that with most |
| // browsers, the target of the mouse/click event is not calculated until the |
| // time it is dispatched, so if you replace an element that you touched |
| // with another element, the target of the mouse/click will be the new |
| // element underneath that point. |
| // |
| // Aside from proximity, we also check to see if the target and any |
| // of its ancestors were the ones that blocked a click. This is necessary |
| // because of the strange mouse/click target calculation done in the |
| // Android 2.1 browser, where if you click on an element, and there is a |
| // mouse/click handler on one of its ancestors, the target will be the |
| // innermost child of the touched element, even if that child is no where |
| // near the point of touch. |
| |
| ele = target; |
| |
| while ( ele ) { |
| for ( i = 0; i < cnt; i++ ) { |
| o = clickBlockList[ i ]; |
| touchID = 0; |
| |
| if ( ( ele === target && target === o.target && Math.abs( o.x - x ) < threshold && Math.abs( o.y - y ) < threshold ) || |
| $.data( ele, touchTargetPropertyName ) === o.touchID ) { |
| // XXX: We may want to consider removing matches from the block list |
| // instead of waiting for the reset timer to fire. |
| e.preventDefault(); |
| e.stopPropagation(); |
| return; |
| } |
| } |
| ele = ele.parentNode; |
| } |
| } |
| }, true); |
| } |
| })( jQuery, window, document ); |
| |
| |
| // throttled resize event |
| (function( $ ) { |
| $.event.special.throttledresize = { |
| setup: function() { |
| $( this ).bind( "resize", handler ); |
| }, |
| teardown: function() { |
| $( this ).unbind( "resize", handler ); |
| } |
| }; |
| |
| var throttle = 250, |
| handler = function() { |
| curr = ( new Date() ).getTime(); |
| diff = curr - lastCall; |
| |
| if ( diff >= throttle ) { |
| |
| lastCall = curr; |
| $( this ).trigger( "throttledresize" ); |
| |
| } else { |
| |
| if ( heldCall ) { |
| clearTimeout( heldCall ); |
| } |
| |
| // Promise a held call will still execute |
| heldCall = setTimeout( handler, throttle - diff ); |
| } |
| }, |
| lastCall = 0, |
| heldCall, |
| curr, |
| diff; |
| })( jQuery ); |
| (function( $, undefined ) { |
| |
| var $window = $.mobile.$window, |
| $html = $( "html" ); |
| |
| /* $.mobile.media method: pass a CSS media type or query and get a bool return |
| note: this feature relies on actual media query support for media queries, though types will work most anywhere |
| examples: |
| $.mobile.media('screen') // tests for screen media type |
| $.mobile.media('screen and (min-width: 480px)') // tests for screen media type with window width > 480px |
| $.mobile.media('@media screen and (-webkit-min-device-pixel-ratio: 2)') // tests for webkit 2x pixel ratio (iPhone 4) |
| */ |
| $.mobile.media = (function() { |
| // TODO: use window.matchMedia once at least one UA implements it |
| var cache = {}, |
| testDiv = $( "<div id='jquery-mediatest'></div>" ), |
| fakeBody = $( "<body>" ).append( testDiv ); |
| |
| return function( query ) { |
| if ( !( query in cache ) ) { |
| var styleBlock = document.createElement( "style" ), |
| cssrule = "@media " + query + " { #jquery-mediatest { position:absolute; } }"; |
| |
| //must set type for IE! |
| styleBlock.type = "text/css"; |
| |
| if ( styleBlock.styleSheet ) { |
| styleBlock.styleSheet.cssText = cssrule; |
| } else { |
| styleBlock.appendChild( document.createTextNode(cssrule) ); |
| } |
| |
| $html.prepend( fakeBody ).prepend( styleBlock ); |
| cache[ query ] = testDiv.css( "position" ) === "absolute"; |
| fakeBody.add( styleBlock ).remove(); |
| } |
| return cache[ query ]; |
| }; |
| })(); |
| |
| })(jQuery); |
| |
| (function( $, undefined ) { |
| |
| // thx Modernizr |
| function propExists( prop ) { |
| var uc_prop = prop.charAt( 0 ).toUpperCase() + prop.substr( 1 ), |
| props = ( prop + " " + vendors.join( uc_prop + " " ) + uc_prop ).split( " " ); |
| |
| for ( var v in props ) { |
| if ( fbCSS[ props[ v ] ] !== undefined ) { |
| return true; |
| } |
| } |
| } |
| |
| var fakeBody = $( "<body>" ).prependTo( "html" ), |
| fbCSS = fakeBody[ 0 ].style, |
| vendors = [ "Webkit", "Moz", "O" ], |
| webos = "palmGetResource" in window, //only used to rule out scrollTop |
| opera = window.opera, |
| operamini = window.operamini && ({}).toString.call( window.operamini ) === "[object OperaMini]", |
| bb = window.blackberry && !propExists( "-webkit-transform" ); //only used to rule out box shadow, as it's filled opaque on BB 5 and lower |
| |
| |
| function validStyle( prop, value, check_vend ) { |
| var div = document.createElement( 'div' ), |
| uc = function( txt ) { |
| return txt.charAt( 0 ).toUpperCase() + txt.substr( 1 ); |
| }, |
| vend_pref = function( vend ) { |
| return "-" + vend.charAt( 0 ).toLowerCase() + vend.substr( 1 ) + "-"; |
| }, |
| check_style = function( vend ) { |
| var vend_prop = vend_pref( vend ) + prop + ": " + value + ";", |
| uc_vend = uc( vend ), |
| propStyle = uc_vend + uc( prop ); |
| |
| div.setAttribute( "style", vend_prop ); |
| |
| if ( !!div.style[ propStyle ] ) { |
| ret = true; |
| } |
| }, |
| check_vends = check_vend ? [ check_vend ] : vendors, |
| ret; |
| |
| for( var i = 0; i < check_vends.length; i++ ) { |
| check_style( check_vends[i] ); |
| } |
| return !!ret; |
| } |
| |
| // Thanks to Modernizr src for this test idea. `perspective` check is limited to Moz to prevent a false positive for 3D transforms on Android. |
| function transform3dTest() { |
| var prop = "transform-3d"; |
| return validStyle( 'perspective', '10px', 'moz' ) || $.mobile.media( "(-" + vendors.join( "-" + prop + "),(-" ) + "-" + prop + "),(" + prop + ")" ); |
| } |
| |
| // Test for dynamic-updating base tag support ( allows us to avoid href,src attr rewriting ) |
| function baseTagTest() { |
| var fauxBase = location.protocol + "//" + location.host + location.pathname + "ui-dir/", |
| base = $( "head base" ), |
| fauxEle = null, |
| href = "", |
| link, rebase; |
| |
| if ( !base.length ) { |
| base = fauxEle = $( "<base>", { "href": fauxBase }).appendTo( "head" ); |
| } else { |
| href = base.attr( "href" ); |
| } |
| |
| link = $( "<a href='testurl' />" ).prependTo( fakeBody ); |
| rebase = link[ 0 ].href; |
| base[ 0 ].href = href || location.pathname; |
| |
| if ( fauxEle ) { |
| fauxEle.remove(); |
| } |
| return rebase.indexOf( fauxBase ) === 0; |
| } |
| |
| // Thanks Modernizr |
| function cssPointerEventsTest() { |
| var element = document.createElement( 'x' ), |
| documentElement = document.documentElement, |
| getComputedStyle = window.getComputedStyle, |
| supports; |
| |
| if ( !( 'pointerEvents' in element.style ) ) { |
| return false; |
| } |
| |
| element.style.pointerEvents = 'auto'; |
| element.style.pointerEvents = 'x'; |
| documentElement.appendChild( element ); |
| supports = getComputedStyle && |
| getComputedStyle( element, '' ).pointerEvents === 'auto'; |
| documentElement.removeChild( element ); |
| return !!supports; |
| } |
| |
| function boundingRect() { |
| var div = document.createElement( "div" ); |
| return typeof div.getBoundingClientRect !== "undefined"; |
| } |
| |
| // non-UA-based IE version check by James Padolsey, modified by jdalton - from http://gist.github.com/527683 |
| // allows for inclusion of IE 6+, including Windows Mobile 7 |
| $.extend( $.mobile, { browser: {} } ); |
| $.mobile.browser.ie = (function() { |
| var v = 3, |
| div = document.createElement( "div" ), |
| a = div.all || []; |
| |
| do { |
| div.innerHTML = "<!--[if gt IE " + ( ++v ) + "]><br><![endif]-->"; |
| } while( a[0] ); |
| |
| return v > 4 ? v : !v; |
| })(); |
| |
| |
| $.extend( $.support, { |
| cssTransitions: "WebKitTransitionEvent" in window || validStyle( 'transition', 'height 100ms linear' ) && !opera, |
| pushState: "pushState" in history && "replaceState" in history, |
| mediaquery: $.mobile.media( "only all" ), |
| cssPseudoElement: !!propExists( "content" ), |
| touchOverflow: !!propExists( "overflowScrolling" ), |
| cssTransform3d: transform3dTest(), |
| boxShadow: !!propExists( "boxShadow" ) && !bb, |
| scrollTop: ( "pageXOffset" in window || "scrollTop" in document.documentElement || "scrollTop" in fakeBody[ 0 ] ) && !webos && !operamini, |
| dynamicBaseTag: baseTagTest(), |
| cssPointerEvents: cssPointerEventsTest(), |
| boundingRect: boundingRect() |
| }); |
| |
| fakeBody.remove(); |
| |
| |
| // $.mobile.ajaxBlacklist is used to override ajaxEnabled on platforms that have known conflicts with hash history updates (BB5, Symbian) |
| // or that generally work better browsing in regular http for full page refreshes (Opera Mini) |
| // Note: This detection below is used as a last resort. |
| // We recommend only using these detection methods when all other more reliable/forward-looking approaches are not possible |
| var nokiaLTE7_3 = (function() { |
| |
| var ua = window.navigator.userAgent; |
| |
| //The following is an attempt to match Nokia browsers that are running Symbian/s60, with webkit, version 7.3 or older |
| return ua.indexOf( "Nokia" ) > -1 && |
| ( ua.indexOf( "Symbian/3" ) > -1 || ua.indexOf( "Series60/5" ) > -1 ) && |
| ua.indexOf( "AppleWebKit" ) > -1 && |
| ua.match( /(BrowserNG|NokiaBrowser)\/7\.[0-3]/ ); |
| })(); |
| |
| // Support conditions that must be met in order to proceed |
| // default enhanced qualifications are media query support OR IE 7+ |
| |
| $.mobile.gradeA = function() { |
| return ( $.support.mediaquery || $.mobile.browser.ie && $.mobile.browser.ie >= 7 ) && ( $.support.boundingRect || $.fn.jquery.match(/1\.[0-7+]\.[0-9+]?/) !== null ); |
| }; |
| |
| $.mobile.ajaxBlacklist = |
| // BlackBerry browsers, pre-webkit |
| window.blackberry && !window.WebKitPoint || |
| // Opera Mini |
| operamini || |
| // Symbian webkits pre 7.3 |
| nokiaLTE7_3; |
| |
| // Lastly, this workaround is the only way we've found so far to get pre 7.3 Symbian webkit devices |
| // to render the stylesheets when they're referenced before this script, as we'd recommend doing. |
| // This simply reappends the CSS in place, which for some reason makes it apply |
| if ( nokiaLTE7_3 ) { |
| $(function() { |
| $( "head link[rel='stylesheet']" ).attr( "rel", "alternate stylesheet" ).attr( "rel", "stylesheet" ); |
| }); |
| } |
| |
| // For ruling out shadows via css |
| if ( !$.support.boxShadow ) { |
| $( "html" ).addClass( "ui-mobile-nosupport-boxshadow" ); |
| } |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| var support = { |
| touch: "ontouchend" in document |
| }; |
| |
| $.mobile = $.mobile || {}; |
| $.mobile.support = $.mobile.support || {}; |
| $.extend( $.support, support ); |
| $.extend( $.mobile.support, support ); |
| }( jQuery )); |
| |
| |
| (function( $, window, undefined ) { |
| // add new event shortcuts |
| $.each( ( "touchstart touchmove touchend " + |
| "tap taphold " + |
| "swipe swipeleft swiperight " + |
| "scrollstart scrollstop" ).split( " " ), function( i, name ) { |
| |
| $.fn[ name ] = function( fn ) { |
| return fn ? this.bind( name, fn ) : this.trigger( name ); |
| }; |
| |
| // jQuery < 1.8 |
| if ( $.attrFn ) { |
| $.attrFn[ name ] = true; |
| } |
| }); |
| |
| var supportTouch = $.mobile.support.touch, |
| scrollEvent = "touchmove scroll", |
| touchStartEvent = supportTouch ? "touchstart" : "mousedown", |
| touchStopEvent = supportTouch ? "touchend" : "mouseup", |
| touchMoveEvent = supportTouch ? "touchmove" : "mousemove"; |
| |
| function triggerCustomEvent( obj, eventType, event ) { |
| var originalType = event.type; |
| event.type = eventType; |
| // event.liveFired is already set by basic events, e.g. vclick, which is fired already. |
| // To fire this custom event, event.liveFired must be cleared. |
| event.liveFired = undefined; |
| |
| $.event.handle.call( obj, event ); |
| event.type = originalType; |
| } |
| |
| // also handles scrollstop |
| $.event.special.scrollstart = { |
| |
| enabled: true, |
| |
| setup: function() { |
| |
| var thisObject = this, |
| $this = $( thisObject ), |
| scrolling, |
| timer; |
| |
| function trigger( event, state ) { |
| scrolling = state; |
| triggerCustomEvent( thisObject, scrolling ? "scrollstart" : "scrollstop", event ); |
| } |
| |
| // iPhone triggers scroll after a small delay; use touchmove instead |
| $this.bind( scrollEvent, function( event ) { |
| |
| if ( !$.event.special.scrollstart.enabled ) { |
| return; |
| } |
| |
| if ( !scrolling ) { |
| trigger( event, true ); |
| } |
| |
| clearTimeout( timer ); |
| timer = setTimeout( function() { |
| trigger( event, false ); |
| }, 50 ); |
| }); |
| } |
| }; |
| |
| // also handles taphold |
| $.event.special.tap = { |
| tapholdThreshold: 750, |
| |
| setup: function() { |
| var thisObject = this, |
| $this = $( thisObject ); |
| |
| $this.bind( "vmousedown", function( event ) { |
| |
| if ( event.which && event.which !== 1 ) { |
| return false; |
| } |
| |
| var origTarget = event.target, |
| origEvent = event.originalEvent, |
| timer; |
| |
| function clearTapTimer() { |
| clearTimeout( timer ); |
| } |
| |
| function clearTapHandlers() { |
| clearTapTimer(); |
| |
| $this.unbind( "vclick", clickHandler ) |
| .unbind( "vmouseup", clearTapTimer ); |
| $.mobile.$document.unbind( "vmousecancel", clearTapHandlers ); |
| } |
| |
| function clickHandler( event ) { |
| clearTapHandlers(); |
| |
| // ONLY trigger a 'tap' event if the start target is |
| // the same as the stop target. |
| if ( origTarget === event.target ) { |
| triggerCustomEvent( thisObject, "tap", event ); |
| } |
| } |
| |
| $this.bind( "vmouseup", clearTapTimer ) |
| .bind( "vclick", clickHandler ); |
| $.mobile.$document.bind( "vmousecancel", clearTapHandlers ); |
| |
| timer = setTimeout( function() { |
| triggerCustomEvent( thisObject, "taphold", $.Event( "taphold", { target: origTarget } ) ); |
| }, $.event.special.tap.tapholdThreshold ); |
| }); |
| } |
| }; |
| |
| // also handles swipeleft, swiperight |
| $.event.special.swipe = { |
| scrollSupressionThreshold: 30, // More than this horizontal displacement, and we will suppress scrolling. |
| |
| durationThreshold: 1000, // More time than this, and it isn't a swipe. |
| |
| horizontalDistanceThreshold: 30, // Swipe horizontal displacement must be more than this. |
| |
| verticalDistanceThreshold: 75, // Swipe vertical displacement must be less than this. |
| |
| setup: function() { |
| var thisObject = this, |
| $this = $( thisObject ); |
| |
| $this.bind( touchStartEvent, function( event ) { |
| var data = event.originalEvent.touches ? |
| event.originalEvent.touches[ 0 ] : event, |
| start = { |
| time: ( new Date() ).getTime(), |
| coords: [ data.pageX, data.pageY ], |
| origin: $( event.target ) |
| }, |
| stop; |
| |
| function moveHandler( event ) { |
| |
| if ( !start ) { |
| return; |
| } |
| |
| var data = event.originalEvent.touches ? |
| event.originalEvent.touches[ 0 ] : event; |
| |
| stop = { |
| time: ( new Date() ).getTime(), |
| coords: [ data.pageX, data.pageY ] |
| }; |
| |
| // prevent scrolling |
| if ( Math.abs( start.coords[ 0 ] - stop.coords[ 0 ] ) > $.event.special.swipe.scrollSupressionThreshold ) { |
| event.preventDefault(); |
| } |
| } |
| |
| $this.bind( touchMoveEvent, moveHandler ) |
| .one( touchStopEvent, function( event ) { |
| $this.unbind( touchMoveEvent, moveHandler ); |
| |
| if ( start && stop ) { |
| if ( stop.time - start.time < $.event.special.swipe.durationThreshold && |
| Math.abs( start.coords[ 0 ] - stop.coords[ 0 ] ) > $.event.special.swipe.horizontalDistanceThreshold && |
| Math.abs( start.coords[ 1 ] - stop.coords[ 1 ] ) < $.event.special.swipe.verticalDistanceThreshold ) { |
| |
| start.origin.trigger( "swipe" ) |
| .trigger( start.coords[0] > stop.coords[ 0 ] ? "swipeleft" : "swiperight" ); |
| } |
| } |
| start = stop = undefined; |
| }); |
| }); |
| } |
| }; |
| $.each({ |
| scrollstop: "scrollstart", |
| taphold: "tap", |
| swipeleft: "swipe", |
| swiperight: "swipe" |
| }, function( event, sourceEvent ) { |
| |
| $.event.special[ event ] = { |
| setup: function() { |
| $( this ).bind( sourceEvent, $.noop ); |
| } |
| }; |
| }); |
| |
| })( jQuery, this ); |
| |
| (function( $, undefined ) { |
| $.extend( $.support, { |
| orientation: "orientation" in window && "onorientationchange" in window |
| }); |
| }( jQuery )); |
| |
| |
| (function( $, window ) { |
| var win = $.mobile.$window, |
| event_name = "orientationchange", |
| special_event, |
| get_orientation, |
| last_orientation, |
| initial_orientation_is_landscape, |
| initial_orientation_is_default, |
| portrait_map = { "0": true, "180": true }; |
| |
| // It seems that some device/browser vendors use window.orientation values 0 and 180 to |
| // denote the "default" orientation. For iOS devices, and most other smart-phones tested, |
| // the default orientation is always "portrait", but in some Android and RIM based tablets, |
| // the default orientation is "landscape". The following code attempts to use the window |
| // dimensions to figure out what the current orientation is, and then makes adjustments |
| // to the to the portrait_map if necessary, so that we can properly decode the |
| // window.orientation value whenever get_orientation() is called. |
| // |
| // Note that we used to use a media query to figure out what the orientation the browser |
| // thinks it is in: |
| // |
| // initial_orientation_is_landscape = $.mobile.media("all and (orientation: landscape)"); |
| // |
| // but there was an iPhone/iPod Touch bug beginning with iOS 4.2, up through iOS 5.1, |
| // where the browser *ALWAYS* applied the landscape media query. This bug does not |
| // happen on iPad. |
| |
| if ( $.support.orientation ) { |
| |
| // Check the window width and height to figure out what the current orientation |
| // of the device is at this moment. Note that we've initialized the portrait map |
| // values to 0 and 180, *AND* we purposely check for landscape so that if we guess |
| // wrong, , we default to the assumption that portrait is the default orientation. |
| // We use a threshold check below because on some platforms like iOS, the iPhone |
| // form-factor can report a larger width than height if the user turns on the |
| // developer console. The actual threshold value is somewhat arbitrary, we just |
| // need to make sure it is large enough to exclude the developer console case. |
| |
| var ww = window.innerWidth || $.mobile.$window.width(), |
| wh = window.innerHeight || $.mobile.$window.height(), |
| landscape_threshold = 50; |
| |
| initial_orientation_is_landscape = ww > wh && ( ww - wh ) > landscape_threshold; |
| |
| |
| // Now check to see if the current window.orientation is 0 or 180. |
| initial_orientation_is_default = portrait_map[ window.orientation ]; |
| |
| // If the initial orientation is landscape, but window.orientation reports 0 or 180, *OR* |
| // if the initial orientation is portrait, but window.orientation reports 90 or -90, we |
| // need to flip our portrait_map values because landscape is the default orientation for |
| // this device/browser. |
| if ( ( initial_orientation_is_landscape && initial_orientation_is_default ) || ( !initial_orientation_is_landscape && !initial_orientation_is_default ) ) { |
| portrait_map = { "-90": true, "90": true }; |
| } |
| } |
| |
| $.event.special.orientationchange = $.extend( {}, $.event.special.orientationchange, { |
| setup: function() { |
| // If the event is supported natively, return false so that jQuery |
| // will bind to the event using DOM methods. |
| if ( $.support.orientation && |
| $.event.special.orientationchange.disabled === false ) { |
| return false; |
| } |
| |
| // Get the current orientation to avoid initial double-triggering. |
| last_orientation = get_orientation(); |
| |
| // Because the orientationchange event doesn't exist, simulate the |
| // event by testing window dimensions on resize. |
| win.bind( "throttledresize", handler ); |
| }, |
| teardown: function() { |
| // If the event is not supported natively, return false so that |
| // jQuery will unbind the event using DOM methods. |
| if ( $.support.orientation && !$.event.special.orientationchange.disabled ) { |
| return false; |
| } |
| |
| // Because the orientationchange event doesn't exist, unbind the |
| // resize event handler. |
| win.unbind( "throttledresize", handler ); |
| }, |
| add: function( handleObj ) { |
| // Save a reference to the bound event handler. |
| var old_handler = handleObj.handler; |
| |
| |
| handleObj.handler = function( event ) { |
| // Modify event object, adding the .orientation property. |
| event.orientation = get_orientation(); |
| |
| // Call the originally-bound event handler and return its result. |
| return old_handler.apply( this, arguments ); |
| }; |
| } |
| }); |
| |
| // If the event is not supported natively, this handler will be bound to |
| // the window resize event to simulate the orientationchange event. |
| function handler() { |
| // Get the current orientation. |
| var orientation = get_orientation(); |
| |
| if ( orientation !== last_orientation ) { |
| // The orientation has changed, so trigger the orientationchange event. |
| last_orientation = orientation; |
| win.trigger( event_name ); |
| } |
| } |
| |
| // Get the current page orientation. This method is exposed publicly, should it |
| // be needed, as jQuery.event.special.orientationchange.orientation() |
| $.event.special.orientationchange.orientation = get_orientation = function() { |
| var isPortrait = true, elem = document.documentElement; |
| |
| // prefer window orientation to the calculation based on screensize as |
| // the actual screen resize takes place before or after the orientation change event |
| // has been fired depending on implementation (eg android 2.3 is before, iphone after). |
| // More testing is required to determine if a more reliable method of determining the new screensize |
| // is possible when orientationchange is fired. (eg, use media queries + element + opacity) |
| if ( $.support.orientation ) { |
| // if the window orientation registers as 0 or 180 degrees report |
| // portrait, otherwise landscape |
| isPortrait = portrait_map[ window.orientation ]; |
| } else { |
| isPortrait = elem && elem.clientWidth / elem.clientHeight < 1.1; |
| } |
| |
| return isPortrait ? "portrait" : "landscape"; |
| }; |
| |
| $.fn[ event_name ] = function( fn ) { |
| return fn ? this.bind( event_name, fn ) : this.trigger( event_name ); |
| }; |
| |
| // jQuery < 1.8 |
| if ( $.attrFn ) { |
| $.attrFn[ event_name ] = true; |
| } |
| |
| }( jQuery, this )); |
| |
| |
| |
| // Script: jQuery hashchange event |
| // |
| // *Version: 1.3, Last updated: 7/21/2010* |
| // |
| // Project Home - http://benalman.com/projects/jquery-hashchange-plugin/ |
| // GitHub - http://github.com/cowboy/jquery-hashchange/ |
| // Source - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.js |
| // (Minified) - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.min.js (0.8kb gzipped) |
| // |
| // About: License |
| // |
| // Copyright (c) 2010 "Cowboy" Ben Alman, |
| // Dual licensed under the MIT and GPL licenses. |
| // http://benalman.com/about/license/ |
| // |
| // About: Examples |
| // |
| // These working examples, complete with fully commented code, illustrate a few |
| // ways in which this plugin can be used. |
| // |
| // hashchange event - http://benalman.com/code/projects/jquery-hashchange/examples/hashchange/ |
| // document.domain - http://benalman.com/code/projects/jquery-hashchange/examples/document_domain/ |
| // |
| // About: Support and Testing |
| // |
| // Information about what version or versions of jQuery this plugin has been |
| // tested with, what browsers it has been tested in, and where the unit tests |
| // reside (so you can test it yourself). |
| // |
| // jQuery Versions - 1.2.6, 1.3.2, 1.4.1, 1.4.2 |
| // Browsers Tested - Internet Explorer 6-8, Firefox 2-4, Chrome 5-6, Safari 3.2-5, |
| // Opera 9.6-10.60, iPhone 3.1, Android 1.6-2.2, BlackBerry 4.6-5. |
| // Unit Tests - http://benalman.com/code/projects/jquery-hashchange/unit/ |
| // |
| // About: Known issues |
| // |
| // While this jQuery hashchange event implementation is quite stable and |
| // robust, there are a few unfortunate browser bugs surrounding expected |
| // hashchange event-based behaviors, independent of any JavaScript |
| // window.onhashchange abstraction. See the following examples for more |
| // information: |
| // |
| // Chrome: Back Button - http://benalman.com/code/projects/jquery-hashchange/examples/bug-chrome-back-button/ |
| // Firefox: Remote XMLHttpRequest - http://benalman.com/code/projects/jquery-hashchange/examples/bug-firefox-remote-xhr/ |
| // WebKit: Back Button in an Iframe - http://benalman.com/code/projects/jquery-hashchange/examples/bug-webkit-hash-iframe/ |
| // Safari: Back Button from a different domain - http://benalman.com/code/projects/jquery-hashchange/examples/bug-safari-back-from-diff-domain/ |
| // |
| // Also note that should a browser natively support the window.onhashchange |
| // event, but not report that it does, the fallback polling loop will be used. |
| // |
| // About: Release History |
| // |
| // 1.3 - (7/21/2010) Reorganized IE6/7 Iframe code to make it more |
| // "removable" for mobile-only development. Added IE6/7 document.title |
| // support. Attempted to make Iframe as hidden as possible by using |
| // techniques from http://www.paciellogroup.com/blog/?p=604. Added |
| // support for the "shortcut" format $(window).hashchange( fn ) and |
| // $(window).hashchange() like jQuery provides for built-in events. |
| // Renamed jQuery.hashchangeDelay to <jQuery.fn.hashchange.delay> and |
| // lowered its default value to 50. Added <jQuery.fn.hashchange.domain> |
| // and <jQuery.fn.hashchange.src> properties plus document-domain.html |
| // file to address access denied issues when setting document.domain in |
| // IE6/7. |
| // 1.2 - (2/11/2010) Fixed a bug where coming back to a page using this plugin |
| // from a page on another domain would cause an error in Safari 4. Also, |
| // IE6/7 Iframe is now inserted after the body (this actually works), |
| // which prevents the page from scrolling when the event is first bound. |
| // Event can also now be bound before DOM ready, but it won't be usable |
| // before then in IE6/7. |
| // 1.1 - (1/21/2010) Incorporated document.documentMode test to fix IE8 bug |
| // where browser version is incorrectly reported as 8.0, despite |
| // inclusion of the X-UA-Compatible IE=EmulateIE7 meta tag. |
| // 1.0 - (1/9/2010) Initial Release. Broke out the jQuery BBQ event.special |
| // window.onhashchange functionality into a separate plugin for users |
| // who want just the basic event & back button support, without all the |
| // extra awesomeness that BBQ provides. This plugin will be included as |
| // part of jQuery BBQ, but also be available separately. |
| |
| |
| |
| (function( $, window, undefined ) { |
| // Reused string. |
| var str_hashchange = 'hashchange', |
| |
| // Method / object references. |
| doc = document, |
| fake_onhashchange, |
| special = $.event.special, |
| |
| // Does the browser support window.onhashchange? Note that IE8 running in |
| // IE7 compatibility mode reports true for 'onhashchange' in window, even |
| // though the event isn't supported, so also test document.documentMode. |
| doc_mode = doc.documentMode, |
| supports_onhashchange = 'on' + str_hashchange in window && ( doc_mode === undefined || doc_mode > 7 ); |
| |
| // Get location.hash (or what you'd expect location.hash to be) sans any |
| // leading #. Thanks for making this necessary, Firefox! |
| function get_fragment( url ) { |
| url = url || location.href; |
| return '#' + url.replace( /^[^#]*#?(.*)$/, '$1' ); |
| }; |
| |
| // Method: jQuery.fn.hashchange |
| // |
| // Bind a handler to the window.onhashchange event or trigger all bound |
| // window.onhashchange event handlers. This behavior is consistent with |
| // jQuery's built-in event handlers. |
| // |
| // Usage: |
| // |
| // > jQuery(window).hashchange( [ handler ] ); |
| // |
| // Arguments: |
| // |
| // handler - (Function) Optional handler to be bound to the hashchange |
| // event. This is a "shortcut" for the more verbose form: |
| // jQuery(window).bind( 'hashchange', handler ). If handler is omitted, |
| // all bound window.onhashchange event handlers will be triggered. This |
| // is a shortcut for the more verbose |
| // jQuery(window).trigger( 'hashchange' ). These forms are described in |
| // the <hashchange event> section. |
| // |
| // Returns: |
| // |
| // (jQuery) The initial jQuery collection of elements. |
| |
| // Allow the "shortcut" format $(elem).hashchange( fn ) for binding and |
| // $(elem).hashchange() for triggering, like jQuery does for built-in events. |
| $.fn[ str_hashchange ] = function( fn ) { |
| return fn ? this.bind( str_hashchange, fn ) : this.trigger( str_hashchange ); |
| }; |
| |
| // Property: jQuery.fn.hashchange.delay |
| // |
| // The numeric interval (in milliseconds) at which the <hashchange event> |
| // polling loop executes. Defaults to 50. |
| |
| // Property: jQuery.fn.hashchange.domain |
| // |
| // If you're setting document.domain in your JavaScript, and you want hash |
| // history to work in IE6/7, not only must this property be set, but you must |
| // also set document.domain BEFORE jQuery is loaded into the page. This |
| // property is only applicable if you are supporting IE6/7 (or IE8 operating |
| // in "IE7 compatibility" mode). |
| // |
| // In addition, the <jQuery.fn.hashchange.src> property must be set to the |
| // path of the included "document-domain.html" file, which can be renamed or |
| // modified if necessary (note that the document.domain specified must be the |
| // same in both your main JavaScript as well as in this file). |
| // |
| // Usage: |
| // |
| // jQuery.fn.hashchange.domain = document.domain; |
| |
| // Property: jQuery.fn.hashchange.src |
| // |
| // If, for some reason, you need to specify an Iframe src file (for example, |
| // when setting document.domain as in <jQuery.fn.hashchange.domain>), you can |
| // do so using this property. Note that when using this property, history |
| // won't be recorded in IE6/7 until the Iframe src file loads. This property |
| // is only applicable if you are supporting IE6/7 (or IE8 operating in "IE7 |
| // compatibility" mode). |
| // |
| // Usage: |
| // |
| // jQuery.fn.hashchange.src = 'path/to/file.html'; |
| |
| $.fn[ str_hashchange ].delay = 50; |
| /* |
| $.fn[ str_hashchange ].domain = null; |
| $.fn[ str_hashchange ].src = null; |
| */ |
| |
| // Event: hashchange event |
| // |
| // Fired when location.hash changes. In browsers that support it, the native |
| // HTML5 window.onhashchange event is used, otherwise a polling loop is |
| // initialized, running every <jQuery.fn.hashchange.delay> milliseconds to |
| // see if the hash has changed. In IE6/7 (and IE8 operating in "IE7 |
| // compatibility" mode), a hidden Iframe is created to allow the back button |
| // and hash-based history to work. |
| // |
| // Usage as described in <jQuery.fn.hashchange>: |
| // |
| // > // Bind an event handler. |
| // > jQuery(window).hashchange( function(e) { |
| // > var hash = location.hash; |
| // > ... |
| // > }); |
| // > |
| // > // Manually trigger the event handler. |
| // > jQuery(window).hashchange(); |
| // |
| // A more verbose usage that allows for event namespacing: |
| // |
| // > // Bind an event handler. |
| // > jQuery(window).bind( 'hashchange', function(e) { |
| // > var hash = location.hash; |
| // > ... |
| // > }); |
| // > |
| // > // Manually trigger the event handler. |
| // > jQuery(window).trigger( 'hashchange' ); |
| // |
| // Additional Notes: |
| // |
| // * The polling loop and Iframe are not created until at least one handler |
| // is actually bound to the 'hashchange' event. |
| // * If you need the bound handler(s) to execute immediately, in cases where |
| // a location.hash exists on page load, via bookmark or page refresh for |
| // example, use jQuery(window).hashchange() or the more verbose |
| // jQuery(window).trigger( 'hashchange' ). |
| // * The event can be bound before DOM ready, but since it won't be usable |
| // before then in IE6/7 (due to the necessary Iframe), recommended usage is |
| // to bind it inside a DOM ready handler. |
| |
| // Override existing $.event.special.hashchange methods (allowing this plugin |
| // to be defined after jQuery BBQ in BBQ's source code). |
| special[ str_hashchange ] = $.extend( special[ str_hashchange ], { |
| |
| // Called only when the first 'hashchange' event is bound to window. |
| setup: function() { |
| // If window.onhashchange is supported natively, there's nothing to do.. |
| if ( supports_onhashchange ) { return false; } |
| |
| // Otherwise, we need to create our own. And we don't want to call this |
| // until the user binds to the event, just in case they never do, since it |
| // will create a polling loop and possibly even a hidden Iframe. |
| $( fake_onhashchange.start ); |
| }, |
| |
| // Called only when the last 'hashchange' event is unbound from window. |
| teardown: function() { |
| // If window.onhashchange is supported natively, there's nothing to do.. |
| if ( supports_onhashchange ) { return false; } |
| |
| // Otherwise, we need to stop ours (if possible). |
| $( fake_onhashchange.stop ); |
| } |
| |
| }); |
| |
| // fake_onhashchange does all the work of triggering the window.onhashchange |
| // event for browsers that don't natively support it, including creating a |
| // polling loop to watch for hash changes and in IE 6/7 creating a hidden |
| // Iframe to enable back and forward. |
| fake_onhashchange = (function() { |
| var self = {}, |
| timeout_id, |
| |
| // Remember the initial hash so it doesn't get triggered immediately. |
| last_hash = get_fragment(), |
| |
| fn_retval = function( val ) { return val; }, |
| history_set = fn_retval, |
| history_get = fn_retval; |
| |
| // Start the polling loop. |
| self.start = function() { |
| timeout_id || poll(); |
| }; |
| |
| // Stop the polling loop. |
| self.stop = function() { |
| timeout_id && clearTimeout( timeout_id ); |
| timeout_id = undefined; |
| }; |
| |
| // This polling loop checks every $.fn.hashchange.delay milliseconds to see |
| // if location.hash has changed, and triggers the 'hashchange' event on |
| // window when necessary. |
| function poll() { |
| var hash = get_fragment(), |
| history_hash = history_get( last_hash ); |
| |
| if ( hash !== last_hash ) { |
| history_set( last_hash = hash, history_hash ); |
| |
| $(window).trigger( str_hashchange ); |
| |
| } else if ( history_hash !== last_hash ) { |
| location.href = location.href.replace( /#.*/, '' ) + history_hash; |
| } |
| |
| timeout_id = setTimeout( poll, $.fn[ str_hashchange ].delay ); |
| }; |
| |
| // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv |
| // vvvvvvvvvvvvvvvvvvv REMOVE IF NOT SUPPORTING IE6/7/8 vvvvvvvvvvvvvvvvvvv |
| // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv |
| $.browser.msie && !supports_onhashchange && (function() { |
| // Not only do IE6/7 need the "magical" Iframe treatment, but so does IE8 |
| // when running in "IE7 compatibility" mode. |
| |
| var iframe, |
| iframe_src; |
| |
| // When the event is bound and polling starts in IE 6/7, create a hidden |
| // Iframe for history handling. |
| self.start = function() { |
| if ( !iframe ) { |
| iframe_src = $.fn[ str_hashchange ].src; |
| iframe_src = iframe_src && iframe_src + get_fragment(); |
| |
| // Create hidden Iframe. Attempt to make Iframe as hidden as possible |
| // by using techniques from http://www.paciellogroup.com/blog/?p=604. |
| iframe = $('<iframe tabindex="-1" title="empty"/>').hide() |
| |
| // When Iframe has completely loaded, initialize the history and |
| // start polling. |
| .one( 'load', function() { |
| iframe_src || history_set( get_fragment() ); |
| poll(); |
| }) |
| |
| // Load Iframe src if specified, otherwise nothing. |
| .attr( 'src', iframe_src || 'javascript:0' ) |
| |
| // Append Iframe after the end of the body to prevent unnecessary |
| // initial page scrolling (yes, this works). |
| .insertAfter( 'body' )[0].contentWindow; |
| |
| // Whenever `document.title` changes, update the Iframe's title to |
| // prettify the back/next history menu entries. Since IE sometimes |
| // errors with "Unspecified error" the very first time this is set |
| // (yes, very useful) wrap this with a try/catch block. |
| doc.onpropertychange = function() { |
| try { |
| if ( event.propertyName === 'title' ) { |
| iframe.document.title = doc.title; |
| } |
| } catch(e) {} |
| }; |
| |
| } |
| }; |
| |
| // Override the "stop" method since an IE6/7 Iframe was created. Even |
| // if there are no longer any bound event handlers, the polling loop |
| // is still necessary for back/next to work at all! |
| self.stop = fn_retval; |
| |
| // Get history by looking at the hidden Iframe's location.hash. |
| history_get = function() { |
| return get_fragment( iframe.location.href ); |
| }; |
| |
| // Set a new history item by opening and then closing the Iframe |
| // document, *then* setting its location.hash. If document.domain has |
| // been set, update that as well. |
| history_set = function( hash, history_hash ) { |
| var iframe_doc = iframe.document, |
| domain = $.fn[ str_hashchange ].domain; |
| |
| if ( hash !== history_hash ) { |
| // Update Iframe with any initial `document.title` that might be set. |
| iframe_doc.title = doc.title; |
| |
| // Opening the Iframe's document after it has been closed is what |
| // actually adds a history entry. |
| iframe_doc.open(); |
| |
| // Set document.domain for the Iframe document as well, if necessary. |
| domain && iframe_doc.write( '<script>document.domain="' + domain + '"</script>' ); |
| |
| iframe_doc.close(); |
| |
| // Update the Iframe's hash, for great justice. |
| iframe.location.hash = hash; |
| } |
| }; |
| |
| })(); |
| // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| // ^^^^^^^^^^^^^^^^^^^ REMOVE IF NOT SUPPORTING IE6/7/8 ^^^^^^^^^^^^^^^^^^^ |
| // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| |
| return self; |
| })(); |
| |
| })(jQuery,this); |
| (function( $, undefined ) { |
| |
| $.widget( "mobile.page", $.mobile.widget, { |
| options: { |
| theme: "c", |
| domCache: false, |
| keepNativeDefault: ":jqmData(role='none'), :jqmData(role='nojs')" |
| }, |
| |
| _create: function() { |
| |
| var self = this; |
| |
| // if false is returned by the callbacks do not create the page |
| if ( self._trigger( "beforecreate" ) === false ) { |
| return false; |
| } |
| |
| self.element |
| .addClass( "ui-page ui-body-" + self.options.theme ) |
| .bind( "pagebeforehide", function() { |
| self.removeContainerBackground(); |
| } ) |
| .bind( "pagebeforeshow", function() { |
| self.setContainerBackground(); |
| } ); |
| |
| }, |
| |
| refresh: function() { |
| $( this.element ).children( ".ui-content" ).trigger("updatelayout", ["external"]); |
| }, |
| |
| /* GUI Builder only : redesign page when user drag&drop header, footer */ |
| setToolbar: function () { |
| $( this.element ).trigger( "pagebeforeshow" ); |
| }, |
| |
| removeContainerBackground: function() { |
| $.mobile.pageContainer.removeClass( "ui-overlay-" + $.mobile.getInheritedTheme( this.element.parent() ) ); |
| }, |
| |
| // set the page container background to the page theme |
| setContainerBackground: function( theme ) { |
| if ( this.options.theme ) { |
| $.mobile.pageContainer.addClass( "ui-overlay-" + ( theme || this.options.theme ) ); |
| } |
| }, |
| |
| addBackBtn : function ( target ) { |
| var $dest = $( ".ui-page-active .ui-footer" ); |
| |
| if ( target == "header" ) { |
| $dest = $( ".ui-page-active .ui-header" ); |
| } |
| backBtn = $( "<a href='#' class='ui-btn-back' data-" + $.mobile.ns + "rel='back'></a>" ) |
| .buttonMarkup( {icon: "header-back-btn", theme : "s"} ); |
| if ( !$dest.find( ".ui-btn-back").length ) { |
| backBtn.prependTo( $dest ); |
| } |
| }, |
| |
| keepNativeSelector: function() { |
| var options = this.options, |
| keepNativeDefined = options.keepNative && $.trim( options.keepNative ); |
| |
| if ( keepNativeDefined && options.keepNative !== options.keepNativeDefault ) { |
| return [options.keepNative, options.keepNativeDefault].join( ", " ); |
| } |
| |
| return options.keepNativeDefault; |
| } |
| }); |
| })( jQuery ); |
| |
| |
| (function( $, window, undefined ) { |
| |
| var createHandler = function( sequential ) { |
| |
| // Default to sequential |
| if ( sequential === undefined ) { |
| sequential = true; |
| } |
| |
| return function( name, reverse, $to, $from ) { |
| |
| var deferred = new $.Deferred(), |
| reverseClass = reverse ? " reverse" : "", |
| active = $.mobile.urlHistory.getActive(), |
| toScroll = active.lastScroll || $.mobile.defaultHomeScroll, |
| screenHeight = $.mobile.getScreenHeight(), |
| maxTransitionOverride = $.mobile.maxTransitionWidth !== false && $.mobile.$window.width() > $.mobile.maxTransitionWidth, |
| none = !$.support.cssTransitions || maxTransitionOverride || !name || name === "none" || Math.max( $.mobile.$window.scrollTop(), toScroll ) > $.mobile.getMaxScrollForTransition(), |
| toPreClass = " ui-page-pre-in", |
| toggleViewportClass = function() { |
| $.mobile.pageContainer.toggleClass( "ui-mobile-viewport-transitioning viewport-" + name ); |
| }, |
| scrollPage = function() { |
| // Prevent blinking on page scrolling in Tizen/Android devices. |
| // Don't scoll window, when current scroll top(scrollTop()) is already at toScroll, |
| // or when current scroll top is 0 and toScroll is same to defaultHomeScroll |
| // (which means the top position of page). In these case, page scrolling is not needed. |
| var st = $.mobile.$window.scrollTop(); |
| if( st === toScroll || ( $.mobile.defaultHomeScroll === toScroll && st == 0 ) ) { |
| return; |
| } |
| |
| // By using scrollTo instead of silentScroll, we can keep things better in order |
| // Just to be precautios, disable scrollstart listening like silentScroll would |
| $.event.special.scrollstart.enabled = false; |
| |
| window.scrollTo( 0, toScroll ); |
| |
| // reenable scrollstart listening like silentScroll would |
| setTimeout( function() { |
| $.event.special.scrollstart.enabled = true; |
| }, 150 ); |
| }, |
| cleanFrom = function() { |
| $from |
| .removeClass( $.mobile.activePageClass + " out in reverse " + name ) |
| .height( "" ); |
| }, |
| startOut = function() { |
| // if it's not sequential, call the doneOut transition to start the TO page animating in simultaneously |
| if ( !sequential ) { |
| doneOut(); |
| } |
| else { |
| $from.animationComplete( doneOut ); |
| } |
| |
| // Set the from page's height and start it transitioning out |
| // Note: setting an explicit height helps eliminate tiling in the transitions |
| $from |
| .height( screenHeight + $.mobile.$window.scrollTop() ) |
| .addClass( name + " out" + reverseClass ); |
| }, |
| |
| doneOut = function() { |
| |
| if ( $from && sequential ) { |
| cleanFrom(); |
| } |
| |
| startIn(); |
| }, |
| |
| startIn = function() { |
| |
| // Prevent flickering in phonegap container: see comments at #4024 regarding iOS |
| $to.css( "z-index", -10 ); |
| |
| $to.addClass( $.mobile.activePageClass + toPreClass ); |
| |
| // Send focus to page as it is now display: block |
| $.mobile.focusPage( $to ); |
| |
| // Set to page height |
| $to.height( screenHeight + toScroll ); |
| |
| scrollPage(); |
| |
| // Restores visibility of the new page: added together with $to.css( "z-index", -10 ); |
| $to.css( "z-index", "" ); |
| |
| if ( !none ) { |
| $to.animationComplete( doneIn ); |
| } |
| |
| $to |
| .removeClass( toPreClass ) |
| .addClass( name + " in" + reverseClass ); |
| |
| if ( none ) { |
| setTimeout( doneIn, 0 ); |
| } |
| |
| }, |
| |
| doneIn = function() { |
| |
| if ( !sequential ) { |
| |
| if ( $from ) { |
| cleanFrom(); |
| } |
| } |
| |
| $to |
| .removeClass( "out in reverse " + name ) |
| .height( "" ); |
| |
| toggleViewportClass(); |
| |
| // In some browsers (iOS5), 3D transitions block the ability to scroll to the desired location during transition |
| // This ensures we jump to that spot after the fact, if we aren't there already. |
| if ( $.mobile.$window.scrollTop() !== toScroll ) { |
| scrollPage(); |
| } |
| |
| deferred.resolve( name, reverse, $to, $from, true ); |
| }; |
| |
| toggleViewportClass(); |
| |
| if ( $from && !none ) { |
| startOut(); |
| } |
| else { |
| doneOut(); |
| } |
| |
| return deferred.promise(); |
| }; |
| }; |
| |
| // generate the handlers from the above |
| var sequentialHandler = createHandler(), |
| simultaneousHandler = createHandler( false ), |
| defaultGetMaxScrollForTransition = function() { |
| return $.mobile.getScreenHeight() * 3; |
| }; |
| |
| // Make our transition handler the public default. |
| $.mobile.defaultTransitionHandler = sequentialHandler; |
| |
| //transition handler dictionary for 3rd party transitions |
| $.mobile.transitionHandlers = { |
| "default": $.mobile.defaultTransitionHandler, |
| "sequential": sequentialHandler, |
| "simultaneous": simultaneousHandler |
| }; |
| |
| $.mobile.transitionFallbacks = {}; |
| |
| // If transition is defined, check if css 3D transforms are supported, and if not, if a fallback is specified |
| $.mobile._maybeDegradeTransition = function( transition ) { |
| if ( transition && !$.support.cssTransform3d && $.mobile.transitionFallbacks[ transition ] ) { |
| transition = $.mobile.transitionFallbacks[ transition ]; |
| } |
| |
| return transition; |
| }; |
| |
| // Set the getMaxScrollForTransition to default if no implementation was set by user |
| $.mobile.getMaxScrollForTransition = $.mobile.getMaxScrollForTransition || defaultGetMaxScrollForTransition; |
| })( jQuery, this ); |
| |
| (function( $, undefined ) { |
| |
| //define vars for interal use |
| var $window = $.mobile.$window, |
| $html = $( 'html' ), |
| $head = $( 'head' ), |
| |
| //url path helpers for use in relative url management |
| path = { |
| |
| // This scary looking regular expression parses an absolute URL or its relative |
| // variants (protocol, site, document, query, and hash), into the various |
| // components (protocol, host, path, query, fragment, etc that make up the |
| // URL as well as some other commonly used sub-parts. When used with RegExp.exec() |
| // or String.match, it parses the URL into a results array that looks like this: |
| // |
| // [0]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread#msg-content |
| // [1]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread |
| // [2]: http://jblas:password@mycompany.com:8080/mail/inbox |
| // [3]: http://jblas:password@mycompany.com:8080 |
| // [4]: http: |
| // [5]: // |
| // [6]: jblas:password@mycompany.com:8080 |
| // [7]: jblas:password |
| // [8]: jblas |
| // [9]: password |
| // [10]: mycompany.com:8080 |
| // [11]: mycompany.com |
| // [12]: 8080 |
| // [13]: /mail/inbox |
| // [14]: /mail/ |
| // [15]: inbox |
| // [16]: ?msg=1234&type=unread |
| // [17]: #msg-content |
| // |
| urlParseRE: /^(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/, |
| |
| // Abstraction to address xss (Issue #4787) by removing the authority in |
| // browsers that auto decode it. All references to location.href should be |
| // replaced with a call to this method so that it can be dealt with properly here |
| getLocation: function( url ) { |
| var uri = url ? this.parseUrl( url ) : location, |
| hash = this.parseUrl( url || location.href ).hash; |
| |
| // mimic the browser with an empty string when the hash is empty |
| hash = hash === "#" ? "" : hash; |
| |
| // Make sure to parse the url or the location object for the hash because using location.hash |
| // is autodecoded in firefox, the rest of the url should be from the object (location unless |
| // we're testing) to avoid the inclusion of the authority |
| return uri.protocol + "//" + uri.host + uri.pathname + uri.search + hash; |
| }, |
| |
| parseLocation: function() { |
| return this.parseUrl( this.getLocation() ); |
| }, |
| |
| //Parse a URL into a structure that allows easy access to |
| //all of the URL components by name. |
| parseUrl: function( url ) { |
| // If we're passed an object, we'll assume that it is |
| // a parsed url object and just return it back to the caller. |
| if ( $.type( url ) === "object" ) { |
| return url; |
| } |
| |
| var matches = path.urlParseRE.exec( url || "" ) || []; |
| |
| // Create an object that allows the caller to access the sub-matches |
| // by name. Note that IE returns an empty string instead of undefined, |
| // like all other browsers do, so we normalize everything so its consistent |
| // no matter what browser we're running on. |
| return { |
| href: matches[ 0 ] || "", |
| hrefNoHash: matches[ 1 ] || "", |
| hrefNoSearch: matches[ 2 ] || "", |
| domain: matches[ 3 ] || "", |
| protocol: matches[ 4 ] || "", |
| doubleSlash: matches[ 5 ] || "", |
| authority: matches[ 6 ] || "", |
| username: matches[ 8 ] || "", |
| password: matches[ 9 ] || "", |
| host: matches[ 10 ] || "", |
| hostname: matches[ 11 ] || "", |
| port: matches[ 12 ] || "", |
| pathname: matches[ 13 ] || "", |
| directory: matches[ 14 ] || "", |
| filename: matches[ 15 ] || "", |
| search: matches[ 16 ] || "", |
| hash: matches[ 17 ] || "" |
| }; |
| }, |
| |
| //Turn relPath into an asbolute path. absPath is |
| //an optional absolute path which describes what |
| //relPath is relative to. |
| makePathAbsolute: function( relPath, absPath ) { |
| if ( relPath && relPath.charAt( 0 ) === "/" ) { |
| return relPath; |
| } |
| |
| relPath = relPath || ""; |
| absPath = absPath ? absPath.replace( /^\/|(\/[^\/]*|[^\/]+)$/g, "" ) : ""; |
| |
| var absStack = absPath ? absPath.split( "/" ) : [], |
| relStack = relPath.split( "/" ); |
| for ( var i = 0; i < relStack.length; i++ ) { |
| var d = relStack[ i ]; |
| switch ( d ) { |
| case ".": |
| break; |
| case "..": |
| if ( absStack.length ) { |
| absStack.pop(); |
| } |
| break; |
| default: |
| absStack.push( d ); |
| break; |
| } |
| } |
| return "/" + absStack.join( "/" ); |
| }, |
| |
| //Returns true if both urls have the same domain. |
| isSameDomain: function( absUrl1, absUrl2 ) { |
| return path.parseUrl( absUrl1 ).domain === path.parseUrl( absUrl2 ).domain; |
| }, |
| |
| //Returns true for any relative variant. |
| isRelativeUrl: function( url ) { |
| // All relative Url variants have one thing in common, no protocol. |
| return path.parseUrl( url ).protocol === ""; |
| }, |
| |
| //Returns true for an absolute url. |
| isAbsoluteUrl: function( url ) { |
| return path.parseUrl( url ).protocol !== ""; |
| }, |
| |
| //Turn the specified realtive URL into an absolute one. This function |
| //can handle all relative variants (protocol, site, document, query, fragment). |
| makeUrlAbsolute: function( relUrl, absUrl ) { |
| if ( !path.isRelativeUrl( relUrl ) ) { |
| return relUrl; |
| } |
| |
| if ( absUrl === undefined ) { |
| absUrl = documentBase; |
| } |
| |
| var relObj = path.parseUrl( relUrl ), |
| absObj = path.parseUrl( absUrl ), |
| protocol = relObj.protocol || absObj.protocol, |
| doubleSlash = relObj.protocol ? relObj.doubleSlash : ( relObj.doubleSlash || absObj.doubleSlash ), |
| authority = relObj.authority || absObj.authority, |
| hasPath = relObj.pathname !== "", |
| pathname = path.makePathAbsolute( relObj.pathname || absObj.filename, absObj.pathname ), |
| search = relObj.search || ( !hasPath && absObj.search ) || "", |
| hash = relObj.hash; |
| |
| return protocol + doubleSlash + authority + pathname + search + hash; |
| }, |
| |
| //Add search (aka query) params to the specified url. |
| addSearchParams: function( url, params ) { |
| var u = path.parseUrl( url ), |
| p = ( typeof params === "object" ) ? $.param( params ) : params, |
| s = u.search || "?"; |
| return u.hrefNoSearch + s + ( s.charAt( s.length - 1 ) !== "?" ? "&" : "" ) + p + ( u.hash || "" ); |
| }, |
| |
| convertUrlToDataUrl: function( absUrl ) { |
| var u = path.parseUrl( absUrl ); |
| if ( path.isEmbeddedPage( u ) ) { |
| // For embedded pages, remove the dialog hash key as in getFilePath(), |
| // otherwise the Data Url won't match the id of the embedded Page. |
| return u.hash.split( dialogHashKey )[0].replace( /^#/, "" ); |
| } else if ( path.isSameDomain( u, documentBase ) ) { |
| return u.hrefNoHash.replace( documentBase.domain, "" ).split( dialogHashKey )[0]; |
| } |
| |
| return window.decodeURIComponent(absUrl); |
| }, |
| |
| //get path from current hash, or from a file path |
| get: function( newPath ) { |
| if ( newPath === undefined ) { |
| newPath = path.parseLocation().hash; |
| } |
| return path.stripHash( newPath ).replace( /[^\/]*\.[^\/*]+$/, '' ); |
| }, |
| |
| //return the substring of a filepath before the sub-page key, for making a server request |
| getFilePath: function( path ) { |
| var splitkey = '&' + $.mobile.subPageUrlKey; |
| return path && path.split( splitkey )[0].split( dialogHashKey )[0]; |
| }, |
| |
| //set location hash to path |
| set: function( path ) { |
| location.hash = path; |
| }, |
| |
| //test if a given url (string) is a path |
| //NOTE might be exceptionally naive |
| isPath: function( url ) { |
| return ( /\// ).test( url ); |
| }, |
| |
| //return a url path with the window's location protocol/hostname/pathname removed |
| clean: function( url ) { |
| return url.replace( documentBase.domain, "" ); |
| }, |
| |
| //just return the url without an initial # |
| stripHash: function( url ) { |
| return url.replace( /^#/, "" ); |
| }, |
| |
| //remove the preceding hash, any query params, and dialog notations |
| cleanHash: function( hash ) { |
| return path.stripHash( hash.replace( /\?.*$/, "" ).replace( dialogHashKey, "" ) ); |
| }, |
| |
| isHashValid: function( hash ) { |
| return ( /^#[^#]+$/ ).test( hash ); |
| }, |
| |
| //check whether a url is referencing the same domain, or an external domain or different protocol |
| //could be mailto, etc |
| isExternal: function( url ) { |
| var u = path.parseUrl( url ); |
| return u.protocol && u.domain !== documentUrl.domain ? true : false; |
| }, |
| |
| hasProtocol: function( url ) { |
| return ( /^(:?\w+:)/ ).test( url ); |
| }, |
| |
| //check if the specified url refers to the first page in the main application document. |
| isFirstPageUrl: function( url ) { |
| // We only deal with absolute paths. |
| var u = path.parseUrl( path.makeUrlAbsolute( url, documentBase ) ), |
| |
| // Does the url have the same path as the document? |
| samePath = u.hrefNoHash === documentUrl.hrefNoHash || ( documentBaseDiffers && u.hrefNoHash === documentBase.hrefNoHash ), |
| |
| // Get the first page element. |
| fp = $.mobile.firstPage, |
| |
| // Get the id of the first page element if it has one. |
| fpId = fp && fp[0] ? fp[0].id : undefined; |
| |
| // The url refers to the first page if the path matches the document and |
| // it either has no hash value, or the hash is exactly equal to the id of the |
| // first page element. |
| return samePath && ( !u.hash || u.hash === "#" || ( fpId && u.hash.replace( /^#/, "" ) === fpId ) ); |
| }, |
| |
| isEmbeddedPage: function( url ) { |
| var u = path.parseUrl( url ); |
| |
| //if the path is absolute, then we need to compare the url against |
| //both the documentUrl and the documentBase. The main reason for this |
| //is that links embedded within external documents will refer to the |
| //application document, whereas links embedded within the application |
| //document will be resolved against the document base. |
| if ( u.protocol !== "" ) { |
| return ( u.hash && ( u.hrefNoHash === documentUrl.hrefNoHash || ( documentBaseDiffers && u.hrefNoHash === documentBase.hrefNoHash ) ) ); |
| } |
| return ( /^#/ ).test( u.href ); |
| }, |
| |
| |
| // Some embedded browsers, like the web view in Phone Gap, allow cross-domain XHR |
| // requests if the document doing the request was loaded via the file:// protocol. |
| // This is usually to allow the application to "phone home" and fetch app specific |
| // data. We normally let the browser handle external/cross-domain urls, but if the |
| // allowCrossDomainPages option is true, we will allow cross-domain http/https |
| // requests to go through our page loading logic. |
| isPermittedCrossDomainRequest: function( docUrl, reqUrl ) { |
| return $.mobile.allowCrossDomainPages && |
| docUrl.protocol === "file:" && |
| reqUrl.search( /^https?:/ ) !== -1; |
| } |
| }, |
| |
| //will be defined when a link is clicked and given an active class |
| $activeClickedLink = null, |
| |
| //urlHistory is purely here to make guesses at whether the back or forward button was clicked |
| //and provide an appropriate transition |
| urlHistory = { |
| // Array of pages that are visited during a single page load. |
| // Each has a url and optional transition, title, and pageUrl (which represents the file path, in cases where URL is obscured, such as dialogs) |
| stack: [], |
| |
| //maintain an index number for the active page in the stack |
| activeIndex: 0, |
| |
| //get active |
| getActive: function() { |
| return urlHistory.stack[ urlHistory.activeIndex ]; |
| }, |
| |
| getPrev: function() { |
| return urlHistory.stack[ urlHistory.activeIndex - 1 ]; |
| }, |
| |
| getNext: function() { |
| return urlHistory.stack[ urlHistory.activeIndex + 1 ]; |
| }, |
| |
| // addNew is used whenever a new page is added |
| addNew: function( url, transition, title, pageUrl, role ) { |
| //if there's forward history, wipe it |
| if ( urlHistory.getNext() ) { |
| urlHistory.clearForward(); |
| } |
| |
| urlHistory.stack.push( {url : url, transition: transition, title: title, pageUrl: pageUrl, role: role } ); |
| |
| urlHistory.activeIndex = urlHistory.stack.length - 1; |
| }, |
| |
| //wipe urls ahead of active index |
| clearForward: function() { |
| urlHistory.stack = urlHistory.stack.slice( 0, urlHistory.activeIndex + 1 ); |
| }, |
| |
| directHashChange: function( opts ) { |
| var back , forward, newActiveIndex, prev = this.getActive(); |
| |
| // check if url is in history and if it's ahead or behind current page |
| $.each( urlHistory.stack, function( i, historyEntry ) { |
| |
| //if the url is in the stack, it's a forward or a back |
| if ( decodeURIComponent( opts.currentUrl ) === decodeURIComponent( historyEntry.url ) ) { |
| //define back and forward by whether url is older or newer than current page |
| back = i < urlHistory.activeIndex; |
| forward = !back; |
| newActiveIndex = i; |
| } |
| }); |
| |
| // save new page index, null check to prevent falsey 0 result |
| this.activeIndex = newActiveIndex !== undefined ? newActiveIndex : this.activeIndex; |
| |
| if ( back ) { |
| ( opts.either || opts.isBack )( true ); |
| } else if ( forward ) { |
| ( opts.either || opts.isForward )( false ); |
| } |
| }, |
| |
| //disable hashchange event listener internally to ignore one change |
| //toggled internally when location.hash is updated to match the url of a successful page load |
| ignoreNextHashChange: false |
| }, |
| |
| //define first selector to receive focus when a page is shown |
| focusable = "[tabindex],a,button:visible,select:visible,input", |
| |
| //queue to hold simultanious page transitions |
| pageTransitionQueue = [], |
| |
| //indicates whether or not page is in process of transitioning |
| isPageTransitioning = false, |
| |
| //nonsense hash change key for dialogs, so they create a history entry |
| dialogHashKey = "&ui-state=dialog", |
| |
| //existing base tag? |
| $base = $head.children( "base" ), |
| |
| //tuck away the original document URL minus any fragment. |
| documentUrl = path.parseLocation(), |
| |
| //if the document has an embedded base tag, documentBase is set to its |
| //initial value. If a base tag does not exist, then we default to the documentUrl. |
| documentBase = $base.length ? path.parseUrl( path.makeUrlAbsolute( $base.attr( "href" ), documentUrl.href ) ) : documentUrl, |
| |
| //cache the comparison once. |
| documentBaseDiffers = ( documentUrl.hrefNoHash !== documentBase.hrefNoHash ), |
| |
| getScreenHeight = $.mobile.getScreenHeight; |
| |
| //base element management, defined depending on dynamic base tag support |
| var base = $.support.dynamicBaseTag ? { |
| |
| //define base element, for use in routing asset urls that are referenced in Ajax-requested markup |
| element: ( $base.length ? $base : $( "<base>", { href: documentBase.hrefNoHash } ).prependTo( $head ) ), |
| |
| //set the generated BASE element's href attribute to a new page's base path |
| set: function( href ) { |
| base.element.attr( "href", path.makeUrlAbsolute( href, documentBase ) ); |
| }, |
| |
| //set the generated BASE element's href attribute to a new page's base path |
| reset: function() { |
| base.element.attr( "href", documentBase.hrefNoHash ); |
| } |
| |
| } : undefined; |
| |
| /* internal utility functions */ |
| |
| // NOTE Issue #4950 Android phonegap doesn't navigate back properly |
| // when a full page refresh has taken place. It appears that hashchange |
| // and replacestate history alterations work fine but we need to support |
| // both forms of history traversal in our code that uses backward history |
| // movement |
| $.mobile.back = function() { |
| var nav = window.navigator; |
| |
| // if the setting is on and the navigator object is |
| // available use the phonegap navigation capability |
| if( this.phonegapNavigationEnabled && |
| nav && |
| nav.app && |
| nav.app.backHistory ){ |
| nav.app.backHistory(); |
| } else { |
| window.history.back(); |
| } |
| }; |
| |
| //direct focus to the page title, or otherwise first focusable element |
| $.mobile.focusPage = function ( page ) { |
| var autofocus = page.find( "[autofocus]" ), |
| pageTitle = page.find( ".ui-title:eq(0)" ); |
| |
| if ( autofocus.length ) { |
| autofocus.focus(); |
| return; |
| } |
| |
| if ( pageTitle.length ) { |
| pageTitle.focus(); |
| } else{ |
| page.focus(); |
| } |
| }; |
| |
| //remove active classes after page transition or error |
| function removeActiveLinkClass( forceRemoval ) { |
| if ( !!$activeClickedLink && ( !$activeClickedLink.closest( "." + $.mobile.activePageClass ).length || forceRemoval ) ) { |
| $activeClickedLink.removeClass( $.mobile.activeBtnClass ); |
| } |
| $activeClickedLink = null; |
| } |
| |
| function releasePageTransitionLock() { |
| isPageTransitioning = false; |
| if ( pageTransitionQueue.length > 0 ) { |
| $.mobile.changePage.apply( null, pageTransitionQueue.pop() ); |
| } |
| } |
| |
| // Save the last scroll distance per page, before it is hidden |
| var setLastScrollEnabled = true, |
| setLastScroll, delayedSetLastScroll; |
| |
| setLastScroll = function() { |
| // this barrier prevents setting the scroll value based on the browser |
| // scrolling the window based on a hashchange |
| if ( !setLastScrollEnabled ) { |
| return; |
| } |
| |
| var active = $.mobile.urlHistory.getActive(); |
| |
| if ( active ) { |
| var lastScroll = $window.scrollTop(); |
| |
| // Set active page's lastScroll prop. |
| // If the location we're scrolling to is less than minScrollBack, let it go. |
| active.lastScroll = lastScroll < $.mobile.minScrollBack ? $.mobile.defaultHomeScroll : lastScroll; |
| } |
| }; |
| |
| // bind to scrollstop to gather scroll position. The delay allows for the hashchange |
| // event to fire and disable scroll recording in the case where the browser scrolls |
| // to the hash targets location (sometimes the top of the page). once pagechange fires |
| // getLastScroll is again permitted to operate |
| delayedSetLastScroll = function() { |
| setTimeout( setLastScroll, 100 ); |
| }; |
| |
| // disable an scroll setting when a hashchange has been fired, this only works |
| // because the recording of the scroll position is delayed for 100ms after |
| // the browser might have changed the position because of the hashchange |
| $window.bind( $.support.pushState ? "popstate" : "hashchange", function() { |
| setLastScrollEnabled = false; |
| }); |
| |
| // handle initial hashchange from chrome :( |
| $window.one( $.support.pushState ? "popstate" : "hashchange", function() { |
| setLastScrollEnabled = true; |
| }); |
| |
| // wait until the mobile page container has been determined to bind to pagechange |
| $window.one( "pagecontainercreate", function() { |
| // once the page has changed, re-enable the scroll recording |
| $.mobile.pageContainer.bind( "pagechange", function() { |
| |
| setLastScrollEnabled = true; |
| |
| // remove any binding that previously existed on the get scroll |
| // which may or may not be different than the scroll element determined for |
| // this page previously |
| $window.unbind( "scrollstop", delayedSetLastScroll ); |
| |
| // determine and bind to the current scoll element which may be the window |
| // or in the case of touch overflow the element with touch overflow |
| $window.bind( "scrollstop", delayedSetLastScroll ); |
| }); |
| }); |
| |
| // bind to scrollstop for the first page as "pagechange" won't be fired in that case |
| $window.bind( "scrollstop", delayedSetLastScroll ); |
| |
| // No-op implementation of transition degradation |
| $.mobile._maybeDegradeTransition = $.mobile._maybeDegradeTransition || function( transition ) { |
| return transition; |
| }; |
| |
| //function for transitioning between two existing pages |
| function transitionPages( toPage, fromPage, transition, reverse ) { |
| |
| if ( fromPage ) { |
| //trigger before show/hide events |
| fromPage.data( "page" )._trigger( "beforehide", null, { nextPage: toPage } ); |
| } |
| |
| toPage.data( "page" )._trigger( "beforeshow", null, { prevPage: fromPage || $( "" ) } ); |
| |
| //clear page loader |
| $.mobile.hidePageLoadingMsg(); |
| |
| transition = $.mobile._maybeDegradeTransition( transition ); |
| |
| //find the transition handler for the specified transition. If there |
| //isn't one in our transitionHandlers dictionary, use the default one. |
| //call the handler immediately to kick-off the transition. |
| var th = $.mobile.transitionHandlers[ transition || "default" ] || $.mobile.defaultTransitionHandler, |
| promise = th( transition, reverse, toPage, fromPage ); |
| |
| promise.done(function() { |
| |
| //trigger show/hide events |
| if ( fromPage ) { |
| fromPage.data( "page" )._trigger( "hide", null, { nextPage: toPage } ); |
| } |
| |
| //trigger pageshow, define prevPage as either fromPage or empty jQuery obj |
| toPage.data( "page" )._trigger( "show", null, { prevPage: fromPage || $( "" ) } ); |
| |
| setTimeout( function () { |
| $.mobile.removeEventBlocker(); |
| }, 0 ); |
| }); |
| |
| return promise; |
| } |
| |
| //shared page enhancements |
| function enhancePage( $page, role ) { |
| // If a role was specified, make sure the data-role attribute |
| // on the page element is in sync. |
| if ( role ) { |
| $page.attr( "data-" + $.mobile.ns + "role", role ); |
| } |
| |
| //run page plugin |
| $page.page(); |
| } |
| |
| /* exposed $.mobile methods */ |
| |
| //animation complete callback |
| $.fn.animationComplete = function( callback ) { |
| if ( $.support.cssTransitions ) { |
| return $( this ).one( 'webkitAnimationEnd animationend', callback ); |
| } |
| else{ |
| // defer execution for consistency between webkit/non webkit |
| setTimeout( callback, 0 ); |
| return $( this ); |
| } |
| }; |
| |
| //expose path object on $.mobile |
| $.mobile.path = path; |
| |
| //expose base object on $.mobile |
| $.mobile.base = base; |
| |
| //history stack |
| $.mobile.urlHistory = urlHistory; |
| |
| $.mobile.dialogHashKey = dialogHashKey; |
| |
| |
| |
| //enable cross-domain page support |
| $.mobile.allowCrossDomainPages = false; |
| |
| //return the original document url |
| $.mobile.getDocumentUrl = function( asParsedObject ) { |
| return asParsedObject ? $.extend( {}, documentUrl ) : documentUrl.href; |
| }; |
| |
| //return the original document base url |
| $.mobile.getDocumentBase = function( asParsedObject ) { |
| return asParsedObject ? $.extend( {}, documentBase ) : documentBase.href; |
| }; |
| |
| $.mobile._bindPageRemove = function() { |
| var page = $( this ); |
| |
| // when dom caching is not enabled or the page is embedded bind to remove the page on hide |
| if ( !page.data( "page" ).options.domCache && |
| page.is( ":jqmData(external-page='true')" ) ) { |
| |
| page.bind( 'pagehide.remove', function() { |
| var $this = $( this ), |
| prEvent = new $.Event( "pageremove" ); |
| |
| $this.trigger( prEvent ); |
| |
| if ( !prEvent.isDefaultPrevented() ) { |
| $this.removeWithDependents(); |
| } |
| }); |
| } |
| }; |
| |
| // Load a page into the DOM. |
| $.mobile.loadPage = function( url, options ) { |
| // This function uses deferred notifications to let callers |
| // know when the page is done loading, or if an error has occurred. |
| var deferred = $.Deferred(), |
| |
| // The default loadPage options with overrides specified by |
| // the caller. |
| settings = $.extend( {}, $.mobile.loadPage.defaults, options ), |
| |
| // The DOM element for the page after it has been loaded. |
| page = null, |
| |
| // If the reloadPage option is true, and the page is already |
| // in the DOM, dupCachedPage will be set to the page element |
| // so that it can be removed after the new version of the |
| // page is loaded off the network. |
| dupCachedPage = null, |
| |
| // determine the current base url |
| findBaseWithDefault = function() { |
| var closestBase = ( $.mobile.activePage && getClosestBaseUrl( $.mobile.activePage ) ); |
| return closestBase || documentBase.hrefNoHash; |
| }, |
| |
| // The absolute version of the URL passed into the function. This |
| // version of the URL may contain dialog/subpage params in it. |
| absUrl = path.makeUrlAbsolute( url, findBaseWithDefault() ); |
| |
| |
| // If the caller provided data, and we're using "get" request, |
| // append the data to the URL. |
| if ( settings.data && settings.type === "get" ) { |
| absUrl = path.addSearchParams( absUrl, settings.data ); |
| settings.data = undefined; |
| } |
| |
| // If the caller is using a "post" request, reloadPage must be true |
| if ( settings.data && settings.type === "post" ) { |
| settings.reloadPage = true; |
| } |
| |
| // The absolute version of the URL minus any dialog/subpage params. |
| // In otherwords the real URL of the page to be loaded. |
| var fileUrl = path.getFilePath( absUrl ), |
| |
| // The version of the Url actually stored in the data-url attribute of |
| // the page. For embedded pages, it is just the id of the page. For pages |
| // within the same domain as the document base, it is the site relative |
| // path. For cross-domain pages (Phone Gap only) the entire absolute Url |
| // used to load the page. |
| dataUrl = path.convertUrlToDataUrl( absUrl ); |
| |
| // Make sure we have a pageContainer to work with. |
| settings.pageContainer = settings.pageContainer || $.mobile.pageContainer; |
| |
| // Check to see if the page already exists in the DOM. |
| // NOTE do _not_ use the :jqmData psuedo selector because parenthesis |
| // are a valid url char and it breaks on the first occurence |
| page = settings.pageContainer.children( "[data-" + $.mobile.ns +"url='" + dataUrl + "']" ); |
| |
| // If we failed to find the page, check to see if the url is a |
| // reference to an embedded page. If so, it may have been dynamically |
| // injected by a developer, in which case it would be lacking a data-url |
| // attribute and in need of enhancement. |
| if ( page.length === 0 && dataUrl && !path.isPath( dataUrl ) ) { |
| page = settings.pageContainer.children( "#" + dataUrl ) |
| .attr( "data-" + $.mobile.ns + "url", dataUrl ) |
| .jqmData( "url", dataUrl ); |
| } |
| |
| // If we failed to find a page in the DOM, check the URL to see if it |
| // refers to the first page in the application. If it isn't a reference |
| // to the first page and refers to non-existent embedded page, error out. |
| if ( page.length === 0 ) { |
| if ( $.mobile.firstPage && path.isFirstPageUrl( fileUrl ) ) { |
| // Check to make sure our cached-first-page is actually |
| // in the DOM. Some user deployed apps are pruning the first |
| // page from the DOM for various reasons, we check for this |
| // case here because we don't want a first-page with an id |
| // falling through to the non-existent embedded page error |
| // case. If the first-page is not in the DOM, then we let |
| // things fall through to the ajax loading code below so |
| // that it gets reloaded. |
| if ( $.mobile.firstPage.parent().length ) { |
| page = $( $.mobile.firstPage ); |
| } |
| } else if ( path.isEmbeddedPage( fileUrl ) ) { |
| deferred.reject( absUrl, options ); |
| return deferred.promise(); |
| } |
| } |
| |
| // If the page we are interested in is already in the DOM, |
| // and the caller did not indicate that we should force a |
| // reload of the file, we are done. Otherwise, track the |
| // existing page as a duplicated. |
| if ( page.length ) { |
| if ( !settings.reloadPage ) { |
| enhancePage( page, settings.role ); |
| deferred.resolve( absUrl, options, page ); |
| //if we are reloading the page make sure we update the base if its not a prefetch |
| if( base && ( !options || !options.prefetch ) ){ |
| base.set(url); |
| } |
| return deferred.promise(); |
| } |
| dupCachedPage = page; |
| } |
| |
| var mpc = settings.pageContainer, |
| pblEvent = new $.Event( "pagebeforeload" ), |
| triggerData = { url: url, absUrl: absUrl, dataUrl: dataUrl, deferred: deferred, options: settings }; |
| |
| // Let listeners know we're about to load a page. |
| mpc.trigger( pblEvent, triggerData ); |
| |
| // If the default behavior is prevented, stop here! |
| if ( pblEvent.isDefaultPrevented() ) { |
| return deferred.promise(); |
| } |
| |
| if ( settings.showLoadMsg ) { |
| |
| // This configurable timeout allows cached pages a brief delay to load without showing a message |
| var loadMsgDelay = setTimeout(function() { |
| $.mobile.showPageLoadingMsg(); |
| }, settings.loadMsgDelay ), |
| |
| // Shared logic for clearing timeout and removing message. |
| hideMsg = function() { |
| |
| // Stop message show timer |
| clearTimeout( loadMsgDelay ); |
| |
| // Hide loading message |
| $.mobile.hidePageLoadingMsg(); |
| }; |
| } |
| |
| // Reset base to the default document base. |
| // only reset if we are not prefetching |
| if ( base && ( typeof options === "undefined" || typeof options.prefetch === "undefined" ) ) { |
| base.reset(); |
| } |
| |
| if ( !( $.mobile.allowCrossDomainPages || path.isSameDomain( documentUrl, absUrl ) ) ) { |
| deferred.reject( absUrl, options ); |
| } else { |
| // Load the new page. |
| $.ajax({ |
| url: fileUrl, |
| type: settings.type, |
| data: settings.data, |
| dataType: "html", |
| success: function( html, textStatus, xhr ) { |
| //pre-parse html to check for a data-url, |
| //use it as the new fileUrl, base path, etc |
| var all = $( "<div></div>" ), |
| |
| //page title regexp |
| newPageTitle = html.match( /<title[^>]*>([^<]*)/ ) && RegExp.$1, |
| |
| // TODO handle dialogs again |
| pageElemRegex = new RegExp( "(<[^>]+\\bdata-" + $.mobile.ns + "role=[\"']?page[\"']?[^>]*>)" ), |
| dataUrlRegex = new RegExp( "\\bdata-" + $.mobile.ns + "url=[\"']?([^\"'>]*)[\"']?" ); |
| |
| |
| // data-url must be provided for the base tag so resource requests can be directed to the |
| // correct url. loading into a temprorary element makes these requests immediately |
| if ( pageElemRegex.test( html ) && |
| RegExp.$1 && |
| dataUrlRegex.test( RegExp.$1 ) && |
| RegExp.$1 ) { |
| url = fileUrl = path.getFilePath( $( "<div>" + RegExp.$1 + "</div>" ).text() ); |
| } |
| |
| //dont update the base tag if we are prefetching |
| if ( base && ( typeof options === "undefined" || typeof options.prefetch === "undefined" ) ) { |
| base.set( fileUrl ); |
| } |
| |
| //workaround to allow scripts to execute when included in page divs |
| all.get( 0 ).innerHTML = html; |
| page = all.find( ":jqmData(role='page'), :jqmData(role='dialog')" ).first(); |
| |
| //if page elem couldn't be found, create one and insert the body element's contents |
| if ( !page.length ) { |
| page = $( "<div data-" + $.mobile.ns + "role='page'>" + html.split( /<\/?body[^>]*>/gmi )[1] + "</div>" ); |
| } |
| |
| if ( newPageTitle && !page.jqmData( "title" ) ) { |
| if ( ~newPageTitle.indexOf( "&" ) ) { |
| newPageTitle = $( "<div>" + newPageTitle + "</div>" ).text(); |
| } |
| page.jqmData( "title", newPageTitle ); |
| } |
| |
| //rewrite src and href attrs to use a base url |
| if ( !$.support.dynamicBaseTag ) { |
| var newPath = path.get( fileUrl ); |
| page.find( "[src], link[href], a[rel='external'], :jqmData(ajax='false'), a[target]" ).each(function() { |
| var thisAttr = $( this ).is( '[href]' ) ? 'href' : |
| $( this ).is( '[src]' ) ? 'src' : 'action', |
| thisUrl = $( this ).attr( thisAttr ); |
| |
| // XXX_jblas: We need to fix this so that it removes the document |
| // base URL, and then prepends with the new page URL. |
| //if full path exists and is same, chop it - helps IE out |
| thisUrl = thisUrl.replace( location.protocol + '//' + location.host + location.pathname, '' ); |
| |
| if ( !/^(\w+:|#|\/)/.test( thisUrl ) ) { |
| $( this ).attr( thisAttr, newPath + thisUrl ); |
| } |
| }); |
| } |
| |
| //append to page and enhance |
| // TODO taging a page with external to make sure that embedded pages aren't removed |
| // by the various page handling code is bad. Having page handling code in many |
| // places is bad. Solutions post 1.0 |
| page |
| .attr( "data-" + $.mobile.ns + "url", path.convertUrlToDataUrl( fileUrl ) ) |
| .attr( "data-" + $.mobile.ns + "external-page", true ) |
| .appendTo( settings.pageContainer ); |
| |
| // wait for page creation to leverage options defined on widget |
| page.one( 'pagecreate', $.mobile._bindPageRemove ); |
| |
| enhancePage( page, settings.role ); |
| |
| // Enhancing the page may result in new dialogs/sub pages being inserted |
| // into the DOM. If the original absUrl refers to a sub-page, that is the |
| // real page we are interested in. |
| if ( absUrl.indexOf( "&" + $.mobile.subPageUrlKey ) > -1 ) { |
| page = settings.pageContainer.children( "[data-" + $.mobile.ns +"url='" + dataUrl + "']" ); |
| } |
| |
| //bind pageHide to removePage after it's hidden, if the page options specify to do so |
| |
| // Remove loading message. |
| if ( settings.showLoadMsg ) { |
| hideMsg(); |
| } |
| |
| // Add the page reference and xhr to our triggerData. |
| triggerData.xhr = xhr; |
| triggerData.textStatus = textStatus; |
| triggerData.page = page; |
| |
| // Let listeners know the page loaded successfully. |
| settings.pageContainer.trigger( "pageload", triggerData ); |
| |
| deferred.resolve( absUrl, options, page, dupCachedPage ); |
| }, |
| error: function( xhr, textStatus, errorThrown ) { |
| //set base back to current path |
| if ( base ) { |
| base.set( path.get() ); |
| } |
| |
| // Add error info to our triggerData. |
| triggerData.xhr = xhr; |
| triggerData.textStatus = textStatus; |
| triggerData.errorThrown = errorThrown; |
| |
| var plfEvent = new $.Event( "pageloadfailed" ); |
| |
| // Let listeners know the page load failed. |
| settings.pageContainer.trigger( plfEvent, triggerData ); |
| |
| // If the default behavior is prevented, stop here! |
| // Note that it is the responsibility of the listener/handler |
| // that called preventDefault(), to resolve/reject the |
| // deferred object within the triggerData. |
| if ( plfEvent.isDefaultPrevented() ) { |
| return; |
| } |
| |
| // Remove loading message. |
| if ( settings.showLoadMsg ) { |
| |
| // Remove loading message. |
| hideMsg(); |
| |
| // show error message |
| $.mobile.showPageLoadingMsg( $.mobile.pageLoadErrorMessageTheme, $.mobile.pageLoadErrorMessage, true ); |
| |
| // hide after delay |
| setTimeout( $.mobile.hidePageLoadingMsg, 1500 ); |
| } |
| |
| deferred.reject( absUrl, options ); |
| } |
| }); |
| } |
| |
| return deferred.promise(); |
| }; |
| |
| $.mobile.loadPage.defaults = { |
| type: "get", |
| data: undefined, |
| reloadPage: false, |
| role: undefined, // By default we rely on the role defined by the @data-role attribute. |
| showLoadMsg: false, |
| pageContainer: undefined, |
| loadMsgDelay: 50 // This delay allows loads that pull from browser cache to occur without showing the loading message. |
| }; |
| |
| // Show a specific page in the page container. |
| $.mobile.changePage = function( toPage, options ) { |
| // If we are in the midst of a transition, queue the current request. |
| // We'll call changePage() once we're done with the current transition to |
| // service the request. |
| if ( isPageTransitioning ) { |
| pageTransitionQueue.unshift( arguments ); |
| return; |
| } |
| |
| var settings = $.extend( {}, $.mobile.changePage.defaults, options ); |
| |
| // Make sure we have a pageContainer to work with. |
| settings.pageContainer = settings.pageContainer || $.mobile.pageContainer; |
| |
| // Make sure we have a fromPage. |
| settings.fromPage = settings.fromPage || $.mobile.activePage; |
| |
| var mpc = settings.pageContainer, |
| pbcEvent = new $.Event( "pagebeforechange" ), |
| triggerData = { toPage: toPage, options: settings }; |
| |
| // Let listeners know we're about to change the current page. |
| mpc.trigger( pbcEvent, triggerData ); |
| |
| // If the default behavior is prevented, stop here! |
| if ( pbcEvent.isDefaultPrevented() ) { |
| return; |
| } |
| |
| // We allow "pagebeforechange" observers to modify the toPage in the trigger |
| // data to allow for redirects. Make sure our toPage is updated. |
| |
| toPage = triggerData.toPage; |
| |
| // Set the isPageTransitioning flag to prevent any requests from |
| // entering this method while we are in the midst of loading a page |
| // or transitioning. |
| |
| isPageTransitioning = true; |
| |
| // If the caller passed us a url, call loadPage() |
| // to make sure it is loaded into the DOM. We'll listen |
| // to the promise object it returns so we know when |
| // it is done loading or if an error ocurred. |
| if ( typeof toPage === "string" ) { |
| $.mobile.loadPage( toPage, settings ) |
| .done(function( url, options, newPage, dupCachedPage ) { |
| isPageTransitioning = false; |
| options.duplicateCachedPage = dupCachedPage; |
| $.mobile.changePage( newPage, options ); |
| }) |
| .fail(function( url, options ) { |
| isPageTransitioning = false; |
| |
| //clear out the active button state |
| removeActiveLinkClass( true ); |
| |
| //release transition lock so navigation is free again |
| releasePageTransitionLock(); |
| settings.pageContainer.trigger( "pagechangefailed", triggerData ); |
| }); |
| return; |
| } |
| |
| // If we are going to the first-page of the application, we need to make |
| // sure settings.dataUrl is set to the application document url. This allows |
| // us to avoid generating a document url with an id hash in the case where the |
| // first-page of the document has an id attribute specified. |
| if ( toPage[ 0 ] === $.mobile.firstPage[ 0 ] && !settings.dataUrl ) { |
| settings.dataUrl = documentUrl.hrefNoHash; |
| } |
| |
| // The caller passed us a real page DOM element. Update our |
| // internal state and then trigger a transition to the page. |
| var fromPage = settings.fromPage, |
| url = ( settings.dataUrl && path.convertUrlToDataUrl( settings.dataUrl ) ) || toPage.jqmData( "url" ), |
| // The pageUrl var is usually the same as url, except when url is obscured as a dialog url. pageUrl always contains the file path |
| pageUrl = url, |
| fileUrl = path.getFilePath( url ), |
| active = urlHistory.getActive(), |
| activeIsInitialPage = urlHistory.activeIndex === 0, |
| historyDir = 0, |
| pageTitle = document.title, |
| isDialog = settings.role === "dialog" || $.mobile.getAttrFixed( toPage [0], "data-" + $.mobile.ns + "role" ) === "dialog"; |
| |
| // By default, we prevent changePage requests when the fromPage and toPage |
| // are the same element, but folks that generate content manually/dynamically |
| // and reuse pages want to be able to transition to the same page. To allow |
| // this, they will need to change the default value of allowSamePageTransition |
| // to true, *OR*, pass it in as an option when they manually call changePage(). |
| // It should be noted that our default transition animations assume that the |
| // formPage and toPage are different elements, so they may behave unexpectedly. |
| // It is up to the developer that turns on the allowSamePageTransitiona option |
| // to either turn off transition animations, or make sure that an appropriate |
| // animation transition is used. |
| if ( fromPage && fromPage[0] === toPage[0] && !settings.allowSamePageTransition ) { |
| isPageTransitioning = false; |
| mpc.trigger( "pagechange", triggerData ); |
| |
| // Even if there is no page change to be done, we should keep the urlHistory in sync with the hash changes |
| if ( settings.fromHashChange ) { |
| urlHistory.directHashChange({ |
| currentUrl: url, |
| isBack: function() {}, |
| isForward: function() {} |
| }); |
| } |
| |
| return; |
| } |
| |
| // We need to make sure the page we are given has already been enhanced. |
| enhancePage( toPage, settings.role ); |
| |
| // If the changePage request was sent from a hashChange event, check to see if the |
| // page is already within the urlHistory stack. If so, we'll assume the user hit |
| // the forward/back button and will try to match the transition accordingly. |
| if ( settings.fromHashChange ) { |
| urlHistory.directHashChange({ |
| currentUrl: url, |
| isBack: function() { historyDir = -1; }, |
| isForward: function() { historyDir = 1; } |
| }); |
| } |
| |
| // Kill the keyboard. |
| // XXX_jblas: We need to stop crawling the entire document to kill focus. Instead, |
| // we should be tracking focus with a delegate() handler so we already have |
| // the element in hand at this point. |
| // Wrap this in a try/catch block since IE9 throw "Unspecified error" if document.activeElement |
| // is undefined when we are in an IFrame. |
| try { |
| if ( document.activeElement && document.activeElement.nodeName.toLowerCase() !== 'body' ) { |
| $( document.activeElement ).blur(); |
| } else { |
| $( "input:focus, textarea:focus, select:focus" ).blur(); |
| } |
| } catch( e ) {} |
| |
| // Record whether we are at a place in history where a dialog used to be - if so, do not add a new history entry and do not change the hash either |
| var alreadyThere = false; |
| |
| // If we're displaying the page as a dialog, we don't want the url |
| // for the dialog content to be used in the hash. Instead, we want |
| // to append the dialogHashKey to the url of the current page. |
| if ( isDialog && active ) { |
| // on the initial page load active.url is undefined and in that case should |
| // be an empty string. Moving the undefined -> empty string back into |
| // urlHistory.addNew seemed imprudent given undefined better represents |
| // the url state |
| |
| // If we are at a place in history that once belonged to a dialog, reuse |
| // this state without adding to urlHistory and without modifying the hash. |
| // However, if a dialog is already displayed at this point, and we're |
| // about to display another dialog, then we must add another hash and |
| // history entry on top so that one may navigate back to the original dialog |
| if ( active.url.indexOf( dialogHashKey ) > -1 && !$.mobile.activePage.is( ".ui-dialog" ) ) { |
| settings.changeHash = false; |
| alreadyThere = true; |
| } |
| |
| // Normally, we tack on a dialog hash key, but if this is the location of a stale dialog, |
| // we reuse the URL from the entry |
| url = ( active.url || "" ) + ( alreadyThere ? "" : dialogHashKey ); |
| |
| // tack on another dialogHashKey if this is the same as the initial hash |
| // this makes sure that a history entry is created for this dialog |
| if ( urlHistory.activeIndex === 0 && url === urlHistory.initialDst ) { |
| url += dialogHashKey; |
| } |
| } |
| |
| // Set the location hash. |
| if ( settings.changeHash !== false && url ) { |
| //disable hash listening temporarily |
| urlHistory.ignoreNextHashChange = true; |
| //update hash and history |
| path.set( url ); |
| } |
| |
| // if title element wasn't found, try the page div data attr too |
| // If this is a deep-link or a reload ( active === undefined ) then just use pageTitle |
| var newPageTitle = ( !active )? pageTitle : toPage.jqmData( "title" ) || toPage.children( ":jqmData(role='header')" ).find( ".ui-title" ).getEncodedText(); |
| if ( !!newPageTitle && pageTitle === document.title ) { |
| pageTitle = newPageTitle; |
| } |
| if ( !toPage.jqmData( "title" ) ) { |
| toPage.jqmData( "title", pageTitle ); |
| } |
| |
| // Make sure we have a transition defined. |
| settings.transition = settings.transition || |
| ( ( historyDir && !activeIsInitialPage ) ? active.transition : undefined ) || |
| ( isDialog ? $.mobile.defaultDialogTransition : $.mobile.defaultPageTransition ); |
| |
| //add page to history stack if it's not back or forward |
| if ( !historyDir ) { |
| // Overwrite the current entry if it's a leftover from a dialog |
| if ( alreadyThere ) { |
| urlHistory.activeIndex = Math.max( 0, urlHistory.activeIndex - 1 ); |
| } |
| urlHistory.addNew( url, settings.transition, pageTitle, pageUrl, settings.role ); |
| } |
| |
| //set page title |
| document.title = urlHistory.getActive().title; |
| |
| //set "toPage" as activePage |
| $.mobile.activePage = toPage; |
| |
| // If we're navigating back in the URL history, set reverse accordingly. |
| settings.reverse = settings.reverse || historyDir < 0; |
| |
| transitionPages( toPage, fromPage, settings.transition, settings.reverse ) |
| .done(function( name, reverse, $to, $from, alreadyFocused ) { |
| removeActiveLinkClass(); |
| |
| //if there's a duplicateCachedPage, remove it from the DOM now that it's hidden |
| if ( settings.duplicateCachedPage ) { |
| settings.duplicateCachedPage.remove(); |
| } |
| |
| // Send focus to the newly shown page. Moved from promise .done binding in transitionPages |
| // itself to avoid ie bug that reports offsetWidth as > 0 (core check for visibility) |
| // despite visibility: hidden addresses issue #2965 |
| // https://github.com/jquery/jquery-mobile/issues/2965 |
| if ( !alreadyFocused ) { |
| $.mobile.focusPage( toPage ); |
| } |
| |
| releasePageTransitionLock(); |
| |
| // Let listeners know we're all done changing the current page. |
| mpc.trigger( "pagechange", triggerData ); |
| }); |
| }; |
| |
| $.mobile.changePage.defaults = { |
| transition: undefined, |
| reverse: false, |
| changeHash: true, |
| fromHashChange: false, |
| role: undefined, // By default we rely on the role defined by the @data-role attribute. |
| duplicateCachedPage: undefined, |
| pageContainer: undefined, |
| showLoadMsg: true, //loading message shows by default when pages are being fetched during changePage |
| dataUrl: undefined, |
| fromPage: undefined, |
| allowSamePageTransition: false |
| }; |
| |
| /* Event Bindings - hashchange, submit, and click */ |
| function findClosestLink( ele ) |
| { |
| while ( ele ) { |
| // Look for the closest element with a nodeName of "a". |
| // Note that we are checking if we have a valid nodeName |
| // before attempting to access it. This is because the |
| // node we get called with could have originated from within |
| // an embedded SVG document where some symbol instance elements |
| // don't have nodeName defined on them, or strings are of type |
| // SVGAnimatedString. |
| if ( ( typeof ele.nodeName === "string" ) && ele.nodeName.toLowerCase() === "a" ) { |
| break; |
| } |
| ele = ele.parentNode; |
| } |
| return ele; |
| } |
| |
| // The base URL for any given element depends on the page it resides in. |
| function getClosestBaseUrl( ele ) |
| { |
| // Find the closest page and extract out its url. |
| var url = $( ele ).closest( ".ui-page" ).jqmData( "url" ), |
| base = documentBase.hrefNoHash; |
| |
| if ( !url || !path.isPath( url ) ) { |
| url = base; |
| } |
| |
| return path.makeUrlAbsolute( url, base); |
| } |
| |
| //The following event bindings should be bound after mobileinit has been triggered |
| //the following deferred is resolved in the init file |
| $.mobile.navreadyDeferred = $.Deferred(); |
| $.mobile.navreadyDeferred.done(function() { |
| //bind to form submit events, handle with Ajax |
| $.mobile.$document.delegate( "form", "submit", function( event ) { |
| var $this = $( this ); |
| |
| if ( !$.mobile.ajaxEnabled || |
| // test that the form is, itself, ajax false |
| $this.is( ":jqmData(ajax='false')" ) || |
| // test that $.mobile.ignoreContentEnabled is set and |
| // the form or one of it's parents is ajax=false |
| !$this.jqmHijackable().length ) { |
| return; |
| } |
| |
| var type = $this.attr( "method" ), |
| target = $this.attr( "target" ), |
| url = $this.attr( "action" ); |
| |
| // If no action is specified, browsers default to using the |
| // URL of the document containing the form. Since we dynamically |
| // pull in pages from external documents, the form should submit |
| // to the URL for the source document of the page containing |
| // the form. |
| if ( !url ) { |
| // Get the @data-url for the page containing the form. |
| url = getClosestBaseUrl( $this ); |
| if ( url === documentBase.hrefNoHash ) { |
| // The url we got back matches the document base, |
| // which means the page must be an internal/embedded page, |
| // so default to using the actual document url as a browser |
| // would. |
| url = documentUrl.hrefNoSearch; |
| } |
| } |
| |
| url = path.makeUrlAbsolute( url, getClosestBaseUrl( $this ) ); |
| |
| if ( ( path.isExternal( url ) && !path.isPermittedCrossDomainRequest( documentUrl, url ) ) || target ) { |
| return; |
| } |
| |
| $.mobile.changePage( |
| url, |
| { |
| type: type && type.length && type.toLowerCase() || "get", |
| data: $this.serialize(), |
| transition: $.mobile.getAttrFixed( $this [0], "data-" + $.mobile.ns + "transition" ), |
| reverse: $.mobile.getAttrFixed( $this [0], "data-" + $.mobile.ns + "direction" ) === "reverse", |
| reloadPage: true |
| } |
| ); |
| event.preventDefault(); |
| }); |
| |
| //add active state on vclick |
| $.mobile.$document.bind( "vclick", function( event ) { |
| // if this isn't a left click we don't care. Its important to note |
| // that when the virtual event is generated it will create the which attr |
| if ( event.which > 1 || !$.mobile.linkBindingEnabled ) { |
| return; |
| } |
| |
| var link = findClosestLink( event.target ); |
| |
| // split from the previous return logic to avoid find closest where possible |
| // TODO teach $.mobile.hijackable to operate on raw dom elements so the link wrapping |
| // can be avoided |
| if ( !$( link ).jqmHijackable().length ) { |
| return; |
| } |
| |
| if ( link ) { |
| if ( path.parseUrl( link.getAttribute( "href" ) || "#" ).hash !== "#" ) { |
| removeActiveLinkClass( true ); |
| $activeClickedLink = $( link ).closest( ".ui-btn" ).not( ".ui-disabled" ); |
| $activeClickedLink.addClass( $.mobile.activeBtnClass ); |
| } |
| } |
| }); |
| |
| // click routing - direct to HTTP or Ajax, accordingly |
| $.mobile.$document.bind( "click", function( event ) { |
| if ( !$.mobile.linkBindingEnabled ) { |
| return; |
| } |
| |
| var link = findClosestLink( event.target ), $link = $( link ), httpCleanup; |
| |
| // If there is no link associated with the click or its not a left |
| // click we want to ignore the click |
| // TODO teach $.mobile.hijackable to operate on raw dom elements so the link wrapping |
| // can be avoided |
| if ( !link || event.which > 1 || !$link.jqmHijackable().length ) { |
| return; |
| } |
| |
| //remove active link class if external (then it won't be there if you come back) |
| httpCleanup = function() { |
| window.setTimeout(function() { removeActiveLinkClass( true ); }, 200 ); |
| }; |
| |
| //if there's a data-rel=back attr, go back in history |
| if ( $link.is( ":jqmData(rel='back')" ) ) { |
| $.mobile.back(); |
| return false; |
| } |
| |
| var baseUrl = getClosestBaseUrl( $link ), |
| |
| //get href, if defined, otherwise default to empty hash |
| href = path.makeUrlAbsolute( $link.attr( "href" ) || "#", baseUrl ); |
| |
| //if ajax is disabled, exit early |
| if ( !$.mobile.ajaxEnabled && !path.isEmbeddedPage( href ) ) { |
| httpCleanup(); |
| //use default click handling |
| return; |
| } |
| |
| // XXX_jblas: Ideally links to application pages should be specified as |
| // an url to the application document with a hash that is either |
| // the site relative path or id to the page. But some of the |
| // internal code that dynamically generates sub-pages for nested |
| // lists and select dialogs, just write a hash in the link they |
| // create. This means the actual URL path is based on whatever |
| // the current value of the base tag is at the time this code |
| // is called. For now we are just assuming that any url with a |
| // hash in it is an application page reference. |
| if ( href.search( "#" ) !== -1 ) { |
| href = href.replace( /[^#]*#/, "" ); |
| if ( !href ) { |
| //link was an empty hash meant purely |
| //for interaction, so we ignore it. |
| event.preventDefault(); |
| return; |
| } else if ( path.isPath( href ) ) { |
| //we have apath so make it the href we want to load. |
| href = path.makeUrlAbsolute( href, baseUrl ); |
| } else { |
| //we have a simple id so use the documentUrl as its base. |
| href = path.makeUrlAbsolute( "#" + href, documentUrl.hrefNoHash ); |
| } |
| } |
| |
| // Should we handle this link, or let the browser deal with it? |
| var useDefaultUrlHandling = $link.is( "[rel='external']" ) || $link.is( ":jqmData(ajax='false')" ) || $link.is( "[target]" ), |
| |
| // Some embedded browsers, like the web view in Phone Gap, allow cross-domain XHR |
| // requests if the document doing the request was loaded via the file:// protocol. |
| // This is usually to allow the application to "phone home" and fetch app specific |
| // data. We normally let the browser handle external/cross-domain urls, but if the |
| // allowCrossDomainPages option is true, we will allow cross-domain http/https |
| // requests to go through our page loading logic. |
| |
| //check for protocol or rel and its not an embedded page |
| //TODO overlap in logic from isExternal, rel=external check should be |
| // moved into more comprehensive isExternalLink |
| isExternal = useDefaultUrlHandling || ( path.isExternal( href ) && !path.isPermittedCrossDomainRequest( documentUrl, href ) ); |
| |
| if ( isExternal ) { |
| httpCleanup(); |
| //use default click handling |
| return; |
| } |
| |
| //use ajax |
| var transition = $.mobile.getAttrFixed( $link [0], "data-" + $.mobile.ns + "transition" ), |
| reverse = $.mobile.getAttrFixed( $link [0], "data-" + $.mobile.ns + "direction" ) === "reverse" || |
| // deprecated - remove by 1.0 |
| $.mobile.getAttrFixed( $link [0], "data-" + $.mobile.ns + "back" ), |
| |
| //this may need to be more specific as we use data-rel more |
| role = $link.attr( "data-" + $.mobile.ns + "rel" ) || undefined; |
| |
| $.mobile.changePage( href, { transition: transition, reverse: reverse, role: role, link: $link } ); |
| event.preventDefault(); |
| }); |
| |
| //prefetch pages when anchors with data-prefetch are encountered |
| $.mobile.$document.delegate( ".ui-page", "pageshow.prefetch", function() { |
| var urls = []; |
| $( this ).find( "a:jqmData(prefetch)" ).each(function() { |
| var $link = $( this ), |
| url = $link.attr( "href" ); |
| |
| if ( url && $.inArray( url, urls ) === -1 ) { |
| urls.push( url ); |
| |
| $.mobile.loadPage( url, { role: $link.attr( "data-" + $.mobile.ns + "rel" ),prefetch: true } ); |
| } |
| }); |
| }); |
| |
| $.mobile._handleHashChange = function( hash ) { |
| //find first page via hash |
| var to = path.stripHash( hash ), |
| //transition is false if it's the first page, undefined otherwise (and may be overridden by default) |
| transition = $.mobile.urlHistory.stack.length === 0 ? "none" : undefined, |
| |
| // "navigate" event fired to allow others to take advantage of the more robust hashchange handling |
| navEvent = new $.Event( "navigate" ), |
| |
| // default options for the changPage calls made after examining the current state |
| // of the page and the hash |
| changePageOptions = { |
| transition: transition, |
| changeHash: false, |
| fromHashChange: true |
| }; |
| |
| if ( 0 === urlHistory.stack.length ) { |
| urlHistory.initialDst = to; |
| } |
| |
| // We should probably fire the "navigate" event from those places that make calls to _handleHashChange, |
| // and have _handleHashChange hook into the "navigate" event instead of triggering it here |
| $.mobile.pageContainer.trigger( navEvent ); |
| if ( navEvent.isDefaultPrevented() ) { |
| return; |
| } |
| |
| //if listening is disabled (either globally or temporarily), or it's a dialog hash |
| if ( !$.mobile.hashListeningEnabled || urlHistory.ignoreNextHashChange ) { |
| urlHistory.ignoreNextHashChange = false; |
| return; |
| } |
| |
| // special case for dialogs |
| if ( urlHistory.stack.length > 1 && to.indexOf( dialogHashKey ) > -1 && urlHistory.initialDst !== to ) { |
| |
| // If current active page is not a dialog skip the dialog and continue |
| // in the same direction |
| if ( !$.mobile.activePage.is( ".ui-dialog" ) ) { |
| //determine if we're heading forward or backward and continue accordingly past |
| //the current dialog |
| urlHistory.directHashChange({ |
| currentUrl: to, |
| isBack: function() { $.mobile.back(); }, |
| isForward: function() { window.history.forward(); } |
| }); |
| |
| // prevent changePage() |
| return; |
| } else { |
| // if the current active page is a dialog and we're navigating |
| // to a dialog use the dialog objected saved in the stack |
| urlHistory.directHashChange({ |
| currentUrl: to, |
| |
| // regardless of the direction of the history change |
| // do the following |
| either: function( isBack ) { |
| var active = $.mobile.urlHistory.getActive(); |
| |
| to = active.pageUrl; |
| |
| // make sure to set the role, transition and reversal |
| // as most of this is lost by the domCache cleaning |
| $.extend( changePageOptions, { |
| role: active.role, |
| transition: active.transition, |
| reverse: isBack |
| }); |
| } |
| }); |
| } |
| } |
| |
| //if to is defined, load it |
| if ( to ) { |
| // At this point, 'to' can be one of 3 things, a cached page element from |
| // a history stack entry, an id, or site-relative/absolute URL. If 'to' is |
| // an id, we need to resolve it against the documentBase, not the location.href, |
| // since the hashchange could've been the result of a forward/backward navigation |
| // that crosses from an external page/dialog to an internal page/dialog. |
| to = ( typeof to === "string" && !path.isPath( to ) ) ? ( path.makeUrlAbsolute( '#' + to, documentBase ) ) : to; |
| |
| // If we're about to go to an initial URL that contains a reference to a non-existent |
| // internal page, go to the first page instead. We know that the initial hash refers to a |
| // non-existent page, because the initial hash did not end up in the initial urlHistory entry |
| if ( to === path.makeUrlAbsolute( '#' + urlHistory.initialDst, documentBase ) && |
| urlHistory.stack.length && urlHistory.stack[0].url !== urlHistory.initialDst.replace( dialogHashKey, "" ) ) { |
| to = $.mobile.firstPage; |
| } |
| $.mobile.changePage( to, changePageOptions ); |
| } else { |
| //there's no hash, go to the first page in the dom |
| $.mobile.changePage( $.mobile.firstPage, changePageOptions ); |
| } |
| }; |
| |
| //hashchange event handler |
| $window.bind( "hashchange", function( e, triggered ) { |
| // Firefox auto-escapes the location.hash as for v13 but |
| // leaves the href untouched |
| $.mobile._handleHashChange( path.parseLocation().hash ); |
| }); |
| |
| });//navreadyDeferred done callback |
| |
| })( jQuery ); |
| |
| (function( $, window ) { |
| // For now, let's Monkeypatch this onto the end of $.mobile._registerInternalEvents |
| // Scope self to pushStateHandler so we can reference it sanely within the |
| // methods handed off as event handlers |
| var pushStateHandler = {}, |
| self = pushStateHandler, |
| $win = $.mobile.$window, |
| url = $.mobile.path.parseLocation(), |
| mobileinitDeferred = $.Deferred(), |
| domreadyDeferred = $.Deferred(); |
| |
| $.mobile.$document.ready( $.proxy( domreadyDeferred, "resolve" ) ); |
| |
| $.mobile.$document.one( "mobileinit", $.proxy( mobileinitDeferred, "resolve" ) ); |
| |
| $.extend( pushStateHandler, { |
| // TODO move to a path helper, this is rather common functionality |
| initialFilePath: (function() { |
| return url.pathname + url.search; |
| })(), |
| |
| hashChangeTimeout: 200, |
| |
| hashChangeEnableTimer: undefined, |
| |
| initialHref: url.hrefNoHash, |
| |
| state: function() { |
| return { |
| // firefox auto decodes the url when using location.hash but not href |
| hash: $.mobile.path.parseLocation().hash || "#" + self.initialFilePath, |
| title: document.title, |
| |
| // persist across refresh |
| initialHref: self.initialHref |
| }; |
| }, |
| |
| resetUIKeys: function( url ) { |
| var dialog = $.mobile.dialogHashKey, |
| subkey = "&" + $.mobile.subPageUrlKey, |
| dialogIndex = url.indexOf( dialog ); |
| |
| if ( dialogIndex > -1 ) { |
| url = url.slice( 0, dialogIndex ) + "#" + url.slice( dialogIndex ); |
| } else if ( url.indexOf( subkey ) > -1 ) { |
| url = url.split( subkey ).join( "#" + subkey ); |
| } |
| |
| return url; |
| }, |
| |
| // TODO sort out a single barrier to hashchange functionality |
| nextHashChangePrevented: function( value ) { |
| $.mobile.urlHistory.ignoreNextHashChange = value; |
| self.onHashChangeDisabled = value; |
| }, |
| |
| // on hash change we want to clean up the url |
| // NOTE this takes place *after* the vanilla navigation hash change |
| // handling has taken place and set the state of the DOM |
| onHashChange: function( e ) { |
| // disable this hash change |
| if ( self.onHashChangeDisabled ) { |
| return; |
| } |
| |
| var href, state, |
| // firefox auto decodes the url when using location.hash but not href |
| hash = $.mobile.path.parseLocation().hash, |
| isPath = $.mobile.path.isPath( hash ), |
| resolutionUrl = isPath ? $.mobile.path.getLocation() : $.mobile.getDocumentUrl(); |
| |
| hash = isPath ? hash.replace( "#", "" ) : hash; |
| |
| |
| // propulate the hash when its not available |
| state = self.state(); |
| |
| // make the hash abolute with the current href |
| href = $.mobile.path.makeUrlAbsolute( hash, resolutionUrl ); |
| |
| if ( isPath ) { |
| href = self.resetUIKeys( href ); |
| } |
| |
| // replace the current url with the new href and store the state |
| // Note that in some cases we might be replacing an url with the |
| // same url. We do this anyways because we need to make sure that |
| // all of our history entries have a state object associated with |
| // them. This allows us to work around the case where $.mobile.back() |
| // is called to transition from an external page to an embedded page. |
| // In that particular case, a hashchange event is *NOT* generated by the browser. |
| // Ensuring each history entry has a state object means that onPopState() |
| // will always trigger our hashchange callback even when a hashchange event |
| // is not fired. |
| history.replaceState( state, document.title, href ); |
| }, |
| |
| // on popstate (ie back or forward) we need to replace the hash that was there previously |
| // cleaned up by the additional hash handling |
| onPopState: function( e ) { |
| var poppedState = e.originalEvent.state, |
| fromHash, toHash, hashChanged; |
| |
| // if there's no state its not a popstate we care about, eg chrome's initial popstate |
| if ( poppedState ) { |
| // if we get two pop states in under this.hashChangeTimeout |
| // make sure to clear any timer set for the previous change |
| clearTimeout( self.hashChangeEnableTimer ); |
| |
| // make sure to enable hash handling for the the _handleHashChange call |
| self.nextHashChangePrevented( false ); |
| |
| // change the page based on the hash in the popped state |
| $.mobile._handleHashChange( poppedState.hash ); |
| |
| // prevent any hashchange in the next self.hashChangeTimeout |
| self.nextHashChangePrevented( true ); |
| |
| // re-enable hash change handling after swallowing a possible hash |
| // change event that comes on all popstates courtesy of browsers like Android |
| self.hashChangeEnableTimer = setTimeout( function() { |
| self.nextHashChangePrevented( false ); |
| }, self.hashChangeTimeout ); |
| } |
| }, |
| |
| init: function() { |
| $win.bind( "hashchange", self.onHashChange ); |
| |
| // Handle popstate events the occur through history changes |
| $win.bind( "popstate", self.onPopState ); |
| |
| // if there's no hash, we need to replacestate for returning to home |
| if ( location.hash === "" ) { |
| history.replaceState( self.state(), document.title, $.mobile.path.getLocation() ); |
| } |
| } |
| }); |
| |
| // We need to init when "mobileinit", "domready", and "navready" have all happened |
| $.when( domreadyDeferred, mobileinitDeferred, $.mobile.navreadyDeferred ).done(function() { |
| if ( $.mobile.pushStateEnabled && $.support.pushState ) { |
| pushStateHandler.init(); |
| } |
| }); |
| })( jQuery, this ); |
| |
| /* |
| * fallback transition for flip in non-3D supporting browsers (which tend to handle complex transitions poorly in general |
| */ |
| |
| (function( $, window, undefined ) { |
| |
| $.mobile.transitionFallbacks.flip = "fade"; |
| |
| })( jQuery, this ); |
| /* |
| * fallback transition for flow in non-3D supporting browsers (which tend to handle complex transitions poorly in general |
| */ |
| |
| (function( $, window, undefined ) { |
| |
| $.mobile.transitionFallbacks.flow = "fade"; |
| |
| })( jQuery, this ); |
| /* |
| * fallback transition for pop in non-3D supporting browsers (which tend to handle complex transitions poorly in general |
| */ |
| |
| (function( $, window, undefined ) { |
| |
| $.mobile.transitionFallbacks.pop = "fade"; |
| |
| })( jQuery, this ); |
| /* |
| * fallback transition for slide in non-3D supporting browsers (which tend to handle complex transitions poorly in general |
| */ |
| |
| (function( $, window, undefined ) { |
| |
| // Use the simultaneous transitions handler for slide transitions |
| $.mobile.transitionHandlers.slide = $.mobile.transitionHandlers.simultaneous; |
| |
| // Set the slide transitions's fallback to "fade" |
| $.mobile.transitionFallbacks.slide = "fade"; |
| |
| })( jQuery, this ); |
| /* |
| * fallback transition for slidedown in non-3D supporting browsers (which tend to handle complex transitions poorly in general |
| */ |
| |
| (function( $, window, undefined ) { |
| |
| $.mobile.transitionFallbacks.slidedown = "fade"; |
| |
| })( jQuery, this ); |
| /* |
| * fallback transition for slidefade in non-3D supporting browsers (which tend to handle complex transitions poorly in general |
| */ |
| |
| (function( $, window, undefined ) { |
| |
| // Set the slide transitions's fallback to "fade" |
| $.mobile.transitionFallbacks.slidefade = "fade"; |
| |
| })( jQuery, this ); |
| /* |
| * fallback transition for slideup in non-3D supporting browsers (which tend to handle complex transitions poorly in general |
| */ |
| |
| (function( $, window, undefined ) { |
| |
| $.mobile.transitionFallbacks.slideup = "fade"; |
| |
| })( jQuery, this ); |
| /* |
| * fallback transition for turn in non-3D supporting browsers (which tend to handle complex transitions poorly in general |
| */ |
| |
| (function( $, window, undefined ) { |
| |
| $.mobile.transitionFallbacks.turn = "fade"; |
| |
| })( jQuery, this ); |
| |
| (function( $, undefined ) { |
| |
| $.mobile.page.prototype.options.degradeInputs = { |
| color: false, |
| date: false, |
| datetime: false, |
| "datetime-local": false, |
| email: false, |
| month: false, |
| number: false, |
| range: "number", |
| search: "text", |
| tel: false, |
| time: false, |
| url: false, |
| week: false |
| }; |
| |
| |
| //auto self-init widgets |
| $.mobile.$document.bind( "pagecreate create", function( e ) { |
| |
| var page = $.mobile.closestPageData( $( e.target ) ), options; |
| |
| if ( !page ) { |
| return; |
| } |
| |
| options = page.options; |
| |
| // degrade inputs to avoid poorly implemented native functionality |
| $( e.target ).find( "input" ).not( page.keepNativeSelector() ).each(function() { |
| var $this = $( this ), |
| type = this.getAttribute( "type" ), |
| optType = options.degradeInputs[ type ] || "text"; |
| |
| if ( options.degradeInputs[ type ] ) { |
| var html = $( "<div>" ).html( $this.clone() ).html(), |
| // In IE browsers, the type sometimes doesn't exist in the cloned markup, so we replace the closing tag instead |
| hasType = html.indexOf( " type=" ) > -1, |
| findstr = hasType ? /\s+type=["']?\w+['"]?/ : /\/?>/, |
| repstr = " type=\"" + optType + "\" data-" + $.mobile.ns + "type=\"" + type + "\"" + ( hasType ? "" : ">" ); |
| |
| $this.replaceWith( html.replace( findstr, repstr ) ); |
| } |
| }); |
| |
| }); |
| |
| })( jQuery ); |
| |
| (function( $, window, undefined ) { |
| |
| $.widget( "mobile.dialog", $.mobile.widget, { |
| options: { |
| closeBtnText: "Close", |
| overlayTheme: "a", |
| initSelector: ":jqmData(role='dialog')" |
| }, |
| _create: function() { |
| var self = this, |
| $el = this.element, |
| headerCloseButton = $( "<a href='#' data-" + $.mobile.ns + "icon='delete' data-" + $.mobile.ns + "iconpos='notext'>"+ this.options.closeBtnText + "</a>" ), |
| dialogWrap = $( "<div/>", { |
| "role" : "dialog", |
| "class" : "ui-dialog-contain ui-corner-all ui-overlay-shadow" |
| }); |
| |
| $el.addClass( "ui-dialog ui-overlay-" + this.options.overlayTheme ); |
| |
| // Class the markup for dialog styling |
| // Set aria role |
| $el |
| .wrapInner( dialogWrap ) |
| .children() |
| .find( ":jqmData(role='header')" ) |
| .prepend( headerCloseButton ) |
| .end() |
| .children( ':first-child') |
| .addClass( "ui-corner-top" ) |
| .end() |
| .children( ":last-child" ) |
| .addClass( "ui-corner-bottom" ); |
| |
| // this must be an anonymous function so that select menu dialogs can replace |
| // the close method. This is a change from previously just defining data-rel=back |
| // on the button and letting nav handle it |
| // |
| // Use click rather than vclick in order to prevent the possibility of unintentionally |
| // reopening the dialog if the dialog opening item was directly under the close button. |
| headerCloseButton.bind( "click", function() { |
| self.close(); |
| }); |
| |
| /* bind events |
| - clicks and submits should use the closing transition that the dialog opened with |
| unless a data-transition is specified on the link/form |
| - if the click was on the close button, or the link has a data-rel="back" it'll go back in history naturally |
| */ |
| $el.bind( "vclick submit", function( event ) { |
| var $target = $( event.target ).closest( event.type === "vclick" ? "a" : "form" ), |
| active; |
| |
| if ( $target.length && !$.mobile.getAttrFixed( $target[0], "data-" + $.mobile.ns + "transition" ) ) { |
| |
| active = $.mobile.urlHistory.getActive() || {}; |
| |
| $target.attr( "data-" + $.mobile.ns + "transition", ( active.transition || $.mobile.defaultDialogTransition ) ) |
| .attr( "data-" + $.mobile.ns + "direction", "reverse" ); |
| } |
| }) |
| .bind( "pagehide", function( e, ui ) { |
| $( this ).find( "." + $.mobile.activeBtnClass ).not( ".ui-slider-bg" ).removeClass( $.mobile.activeBtnClass ); |
| }) |
| // Override the theme set by the page plugin on pageshow |
| .bind( "pagebeforeshow", function() { |
| self._isCloseable = true; |
| if ( self.options.overlayTheme ) { |
| self.element |
| .page( "removeContainerBackground" ) |
| .page( "setContainerBackground", self.options.overlayTheme ); |
| } |
| }); |
| }, |
| |
| // Close method goes back in history |
| close: function() { |
| var dst; |
| |
| if ( this._isCloseable ) { |
| this._isCloseable = false; |
| if ( $.mobile.hashListeningEnabled ) { |
| $.mobile.back(); |
| } else { |
| dst = $.mobile.urlHistory.getPrev().url; |
| if ( !$.mobile.path.isPath( dst ) ) { |
| dst = $.mobile.path.makeUrlAbsolute( "#" + dst ); |
| } |
| |
| $.mobile.changePage( dst, { changeHash: false, fromHashChange: true } ); |
| } |
| } |
| } |
| }); |
| |
| //auto self-init widgets |
| $.mobile.$document.delegate( $.mobile.dialog.prototype.options.initSelector, "pagecreate", function() { |
| $.mobile.dialog.prototype.enhance( this ); |
| }); |
| |
| })( jQuery, this ); |
| |
| (function( $, undefined ) { |
| |
| $.mobile.page.prototype.options.backBtnText = "Back"; |
| $.mobile.page.prototype.options.addBackBtn = false; |
| $.mobile.page.prototype.options.backBtnTheme = null; |
| $.mobile.page.prototype.options.headerTheme = "a"; |
| $.mobile.page.prototype.options.footerTheme = "a"; |
| $.mobile.page.prototype.options.contentTheme = null; |
| |
| // NOTE bind used to force this binding to run before the buttonMarkup binding |
| // which expects .ui-footer top be applied in its gigantic selector |
| // TODO remove the buttonMarkup giant selector and move it to the various modules |
| // on which it depends |
| $.mobile.$document.bind( "pagecreate", function( e ) { |
| var $page = $( e.target ), |
| o = $page.data( "page" ).options, |
| prefix = "data-"+$.mobile.ns, |
| pageRole = $page[0].getAttribute( prefix + "role" ) || undefined, |
| pageTheme = o.theme; |
| |
| $( ":jqmData(role='header'), :jqmData(role='footer'), :jqmData(role='content')", $page ) |
| .jqmEnhanceable() |
| .each(function() { |
| |
| var $this = $( this ), |
| role = $this[0].getAttribute( prefix + "role" ) || undefined, |
| theme = $this[0].getAttribute( prefix + "theme" ) || undefined, |
| contentTheme = theme || o.contentTheme || ( pageRole === "dialog" && pageTheme ), |
| $headeranchors, |
| leftbtn, |
| rightbtn, |
| $dest = $page.find( ".ui-footer" ), |
| backBtn; |
| |
| $this.addClass( "ui-" + role ); |
| |
| //apply theming and markup modifications to page,header,content,footer |
| if ( role === "header" || role === "footer" ) { |
| |
| var thisTheme = theme || ( role === "header" ? o.headerTheme : o.footerTheme ) || pageTheme; |
| |
| $this |
| //add theme class |
| .addClass( "ui-bar-" + thisTheme ) |
| // Add ARIA role |
| .attr( "role", role === "header" ? "banner" : "contentinfo" ); |
| |
| if ( role === "header") { |
| // Right,left buttons |
| $headeranchors = $this.children( "a, div.naviframe-button, a.naviframe-button, button" ); |
| leftbtn = $headeranchors.hasClass( "ui-btn-left" ); |
| rightbtn = $headeranchors.hasClass( "ui-btn-right" ); |
| |
| leftbtn = leftbtn || $headeranchors.eq( 0 ).not( ".ui-btn-right" ).addClass( "ui-btn-left" ).length; |
| |
| rightbtn = rightbtn || $headeranchors.eq( 1 ).addClass( "ui-btn-right" ).length; |
| |
| $( $headeranchors.get().reverse() ).each( function ( i ) { |
| $( this ).addClass( "ui-btn-right-" + i ); |
| }); |
| } |
| |
| // Auto-add back btn on pages beyond first view |
| if ( o.addBackBtn && |
| ( role === "footer" || role === "header" ) && |
| $page[0].getAttribute( prefix + "url" ) !== $.mobile.path.stripHash( location.hash ) && |
| !leftbtn ) { |
| |
| if ( o.addBackBtn == "header" ) { |
| $dest = $page.find( ".ui-header" ); |
| } else { |
| $dest = $page.find( ".ui-footer" ); |
| } |
| |
| if ( !$dest.find( ".ui-btn-back" ).length ) { |
| backBtn = $( "<a href='javascript:void(0);' class='ui-btn-back' data-" + $.mobile.ns + "rel='back'></a>" ) |
| // // If theme is provided, override default inheritance |
| .buttonMarkup( { icon: "header-back-btn", theme: o.backBtnTheme || thisTheme } ); |
| |
| backBtn.find( ".ui-btn-text" ).text( o.backBtnText ); |
| backBtn.appendTo( $dest ); |
| } |
| } |
| |
| // Page title |
| $this.children( "h1, h2, h3, h4, h5, h6" ) |
| .addClass( "ui-title" ) |
| // Regardless of h element number in src, it becomes h1 for the enhanced page |
| .attr({ |
| "role": "heading", |
| "aria-level": "1", |
| "aria-label": "title", |
| "tabindex": "0" |
| }); |
| |
| $( ".ui-title-text-sub" ).attr( { "tabindex": "0", "aria-label": "subtitle" } ); |
| |
| } else if ( role === "content" ) { |
| if ( contentTheme ) { |
| $this.addClass( "ui-body-" + ( contentTheme ) ); |
| } |
| |
| // Add ARIA role |
| $this.attr( "role", "main" ); |
| } |
| }); |
| }); |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| // filter function removes whitespace between label and form element so we can use inline-block (nodeType 3 = text) |
| $.fn.fieldcontain = function( options ) { |
| return this |
| .addClass( "ui-field-contain ui-body ui-br" ) |
| .contents().filter( function() { |
| return ( this.nodeType === 3 && !/\S/.test( this.nodeValue ) ); |
| }).remove(); |
| }; |
| |
| //auto self-init widgets |
| $.mobile.$document.bind( "pagecreate create", function( e ) { |
| $( ":jqmData(role='fieldcontain')", e.target ).jqmEnhanceable().fieldcontain(); |
| }); |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| $.fn.grid = function( options ) { |
| return this.each(function() { |
| |
| var $this = $( this ), |
| o = $.extend({ |
| grid: null |
| }, options ), |
| $kids = $this.children(), |
| gridCols = { solo:1, a:2, b:3, c:4, d:5 }, |
| grid = o.grid, |
| iterator; |
| |
| if ( !grid ) { |
| if ( $kids.length <= 5 ) { |
| for ( var letter in gridCols ) { |
| if ( gridCols[ letter ] === $kids.length ) { |
| grid = letter; |
| } |
| } |
| } else { |
| grid = "a"; |
| $this.addClass( "ui-grid-duo" ); |
| } |
| } |
| iterator = gridCols[grid]; |
| |
| $this.addClass( "ui-grid-" + grid ); |
| |
| $kids.filter( ":nth-child(" + iterator + "n+1)" ).addClass( "ui-block-a" ); |
| |
| if ( iterator > 1 ) { |
| $kids.filter( ":nth-child(" + iterator + "n+2)" ).addClass( "ui-block-b" ); |
| } |
| if ( iterator > 2 ) { |
| $kids.filter( ":nth-child(" + iterator + "n+3)" ).addClass( "ui-block-c" ); |
| } |
| if ( iterator > 3 ) { |
| $kids.filter( ":nth-child(" + iterator + "n+4)" ).addClass( "ui-block-d" ); |
| } |
| if ( iterator > 4 ) { |
| $kids.filter( ":nth-child(" + iterator + "n+5)" ).addClass( "ui-block-e" ); |
| } |
| }); |
| }; |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| $.mobile.$document.bind( "pagecreate create", function( e ) { |
| $( ":jqmData(role='nojs')", e.target ).addClass( "ui-nojs" ); |
| |
| }); |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| $.fn.buttonMarkup = function( options ) { |
| var $workingSet = this, |
| prefix = "data-" + $.mobile.ns, |
| mapToDataAttr = function( key, value ) { |
| e.setAttribute( "data-" + $.mobile.ns + key, value ); |
| el.jqmData( key, value ); |
| }; |
| |
| // Enforce options to be of type string |
| options = ( options && ( $.type( options ) === "object" ) )? options : {}; |
| for ( var i = 0; i < $workingSet.length; i++ ) { |
| var el = $workingSet.eq( i ), |
| e = el[ 0 ], |
| o = $.extend( {}, $.fn.buttonMarkup.defaults, { |
| icon: options.icon !== undefined ? options.icon : $.mobile.getAttrFixed( e, prefix + "icon" ), |
| iconpos: options.iconpos !== undefined ? options.iconpos : $.mobile.getAttrFixed( e, prefix + "iconpos" ), |
| theme: options.theme !== undefined ? options.theme : $.mobile.getAttrFixed( e, prefix + "theme" ) || $.mobile.getInheritedTheme( el, $.fn.buttonMarkup.defaults["theme"] ), |
| inline: options.inline !== undefined ? options.inline : $.mobile.getAttrFixed( e, prefix + "inline" ), |
| shadow: options.shadow !== undefined ? options.shadow : $.mobile.getAttrFixed( e, prefix + "shadow" ), |
| corners: options.corners !== undefined ? options.corners : $.mobile.getAttrFixed( e, prefix + "corners" ), |
| iconshadow: options.iconshadow !== undefined ? options.iconshadow : $.mobile.getAttrFixed( e, prefix + "iconshadow" ), |
| mini: options.mini !== undefined ? options.mini : $.mobile.getAttrFixed( e, prefix + "mini" ) |
| }, options ), |
| |
| // Classes Defined |
| innerClass = "ui-btn-inner", |
| textClass = "ui-btn-text", |
| buttonClass, iconClass, |
| // Button inner markup |
| buttonInner, |
| buttonText, |
| buttonIcon, |
| buttonElements; |
| |
| for ( key in o ) { |
| e.setAttribute ( prefix + key, o[ key ]) |
| } |
| |
| if ( $.mobile.getAttrFixed( e, prefix + "rel" ) === "popup" && el.attr( "href" ) ) { |
| e.setAttribute( "aria-haspopup", true ); |
| e.setAttribute( "aria-owns", e.getAttribute( "href" ) ); |
| } |
| |
| if ( e.tagName !== "LI" && e.tagName !== "LABEL" ) { |
| e.setAttribute( "role", "button" ); |
| e.setAttribute( "tabindex", "0" ); |
| } |
| |
| // Check if this element is already enhanced |
| buttonElements = $.data( ( ( e.tagName === "INPUT" || e.tagName === "BUTTON" ) ? e.parentNode : e ), "buttonElements" ); |
| |
| if ( buttonElements ) { |
| e = buttonElements.outer; |
| el = $( e ); |
| buttonInner = buttonElements.inner; |
| buttonText = buttonElements.text; |
| // We will recreate this icon below |
| $( buttonElements.icon ).remove(); |
| buttonElements.icon = null; |
| } |
| else { |
| buttonInner = document.createElement( o.wrapperEls ); |
| buttonText = document.createElement( o.wrapperEls ); |
| } |
| buttonIcon = o.icon ? document.createElement( "span" ) : null; |
| |
| if ( attachEvents && !buttonElements ) { |
| attachEvents(); |
| } |
| |
| // if not, try to find closest theme container |
| if ( !o.theme ) { |
| o.theme = $.mobile.getInheritedTheme( el, "c" ); |
| } |
| |
| buttonClass = "ui-btn ui-btn-up-" + o.theme; |
| buttonClass += o.shadow ? " ui-shadow" : ""; |
| buttonClass += o.corners ? " ui-btn-corner-all" : ""; |
| |
| // To distinguish real buttons |
| if( $.mobile.getAttrFixed( e, prefix + "role" ) == "button" || e.tagName == "BUTTON" || e.tagName == "DIV" ){ |
| buttonClass += " ui-btn-box-" + o.theme; |
| } |
| |
| /* TIZEN style markup */ |
| var buttonStyle = $.mobile.getAttrFixed( e, prefix + "style" ); |
| |
| if ( buttonStyle == "circle" && !($(el).text().length > 0) ) { |
| /* style : no text, Icon only */ |
| buttonClass += " ui-btn-corner-circle"; |
| buttonClass += " ui-btn-icon_only"; |
| } else if ( buttonStyle == "nobg" ) { |
| /* style : no text, Icon only, no bg */ |
| buttonClass += " ui-btn-icon-nobg"; |
| buttonClass += " ui-btn-icon_only"; |
| } else if ( buttonStyle == "edit" ) { |
| buttonClass += " ui-btn-edit"; |
| } else if ( buttonStyle == "round" || ( buttonStyle == "circle" && $(el).text().length > 0 ) ) { |
| buttonClass += " ui-btn-round"; |
| } |
| if ( o.icon ) { |
| if ( $(el).text().length > 0 ) { |
| |
| switch ( o.iconpos ) { |
| case "right" : |
| case "left" : |
| case "top" : |
| case "bottom" : |
| textClass += " ui-btn-text-padding-" + o.iconpos; |
| break; |
| default: |
| textClass += " ui-btn-text-padding-left"; |
| break; |
| } |
| |
| innerClass += " ui-btn-hastxt"; |
| } else { |
| if ( buttonStyle == "circle" ) { |
| /* style : no text, Icon only */ |
| innerClass += " ui-btn-corner-circle"; |
| } else if ( buttonStyle == "nobg" ) { |
| /* style : no text, Icon only, no bg */ |
| innerClass += " ui-btn-icon-nobg"; |
| } |
| |
| buttonClass += " ui-btn-icon_only"; |
| innerClass += " ui-btn-icon-only"; |
| |
| if ( e.tagName !== "LABEL" ) { |
| $( el ).text( o.icon.replace( "naviframe-", "" ) ); |
| } |
| } |
| } else { |
| if ( $(el).text().length > 0 ) { |
| innerClass += " ui-btn-hastxt"; |
| } else if ( buttonStyle == "circle" ){ |
| buttonClass += " ui-btn-round"; |
| } |
| } |
| if ( o.mini !== undefined ) { |
| // Used to control styling in headers/footers, where buttons default to `mini` style. |
| buttonClass += o.mini === true ? " ui-mini" : " ui-fullsize"; |
| } |
| |
| if ( o.inline !== undefined ) { |
| // Used to control styling in headers/footers, where buttons default to `inline` style. |
| buttonClass += o.inline === true ? " ui-btn-inline" : " ui-btn-block"; |
| } |
| |
| if ( o.icon ) { |
| o.icon = "ui-icon-" + o.icon; |
| o.iconpos = o.iconpos || "left"; |
| |
| iconClass = "ui-icon " + o.icon; |
| |
| if ( o.iconshadow ) { |
| iconClass += " ui-icon-shadow"; |
| } |
| } |
| |
| if ( o.iconpos ) { |
| buttonClass += " ui-btn-icon-" + o.iconpos; |
| |
| if ( o.iconpos === "notext" && !el.attr( "title" ) ) { |
| el.attr( "title", el.getEncodedText() ); |
| } |
| } |
| |
| innerClass += o.corners ? " ui-btn-corner-all" : ""; |
| |
| if ( o.iconpos && o.iconpos === "notext" && !el.attr( "title" ) ) { |
| el.attr( "title", el.getEncodedText() ); |
| } |
| |
| if ( buttonElements ) { |
| el.removeClass( buttonElements.bcls || "" ); |
| } |
| el.removeClass( "ui-link" ).addClass( buttonClass ); |
| |
| buttonInner.className = innerClass; |
| |
| buttonText.className = textClass; |
| if ( !buttonElements ) { |
| buttonInner.appendChild( buttonText ); |
| } |
| if ( buttonIcon ) { |
| buttonIcon.className = iconClass; |
| if ( !( buttonElements && buttonElements.icon ) ) { |
| buttonIcon.innerHTML = " "; |
| buttonInner.appendChild( buttonIcon ); |
| } |
| } |
| |
| while ( e.firstChild && !buttonElements ) { |
| buttonText.appendChild( e.firstChild ); |
| } |
| |
| if ( !buttonElements ) { |
| e.appendChild( buttonInner ); |
| } |
| |
| // Assign a structure containing the elements of this button to the elements of this button. This |
| // will allow us to recognize this as an already-enhanced button in future calls to buttonMarkup(). |
| buttonElements = { |
| bcls : buttonClass, |
| outer : e, |
| inner : buttonInner, |
| text : buttonText, |
| icon : buttonIcon |
| }; |
| |
| $.data( e, 'buttonElements', buttonElements ); |
| $.data( buttonInner, 'buttonElements', buttonElements ); |
| $.data( buttonText, 'buttonElements', buttonElements ); |
| if ( buttonIcon ) { |
| $.data( buttonIcon, 'buttonElements', buttonElements ); |
| } |
| } |
| |
| return this; |
| }; |
| |
| $.fn.buttonMarkup.defaults = { |
| theme: "c", |
| corners: true, |
| shadow: true, |
| iconshadow: true, |
| wrapperEls: "span" |
| }; |
| |
| function closestEnabledButton( element ) { |
| var cname; |
| |
| while ( element ) { |
| // Note that we check for typeof className below because the element we |
| // handed could be in an SVG DOM where className on SVG elements is defined to |
| // be of a different type (SVGAnimatedString). We only operate on HTML DOM |
| // elements, so we look for plain "string". |
| cname = ( typeof element.className === 'string' ) && ( element.className + ' ' ); |
| if ( cname && cname.indexOf( "ui-btn " ) > -1 && cname.indexOf( "ui-disabled " ) < 0 ) { |
| break; |
| } |
| |
| element = element.parentNode; |
| } |
| |
| return element; |
| } |
| |
| var attachEvents = function() { |
| var hoverDelay = $.mobile.buttonMarkup.hoverDelay, hov, foc; |
| |
| $.mobile.$document.bind( { |
| "vmousedown vmousecancel vmouseup vmouseover vmouseout focus blur scrollstart touchend touchcancel": function( event ) { |
| var theme, |
| $btn = $( closestEnabledButton( event.target ) ), |
| isTouchEvent = event.originalEvent && /^touch/.test( event.originalEvent.type ), |
| evt = event.type; |
| |
| if ( $btn.length ) { |
| theme = $btn.attr( "data-" + $.mobile.ns + "theme" ); |
| |
| if ( evt === "vmousedown" ) { |
| if ( isTouchEvent ) { |
| // Use a short delay to determine if the user is scrolling before highlighting |
| hov = setTimeout( function() { |
| $btn.removeClass( "ui-btn-up-" + theme ).addClass( "ui-btn-down-" + theme ); |
| }, hoverDelay ); |
| } else { |
| $btn.removeClass( "ui-btn-up-" + theme ).addClass( "ui-btn-down-" + theme ); |
| } |
| } else if ( evt === "vmousecancel" || evt === "vmouseup" || evt === "touchend" || evt === "touchcancel" ) { |
| $btn.removeClass( "ui-btn-down-" + theme ).addClass( "ui-btn-up-" + theme ); |
| } else if ( evt === "vmouseover" || evt === "focus" ) { |
| if ( isTouchEvent ) { |
| // Use a short delay to determine if the user is scrolling before highlighting |
| foc = setTimeout( function() { |
| $btn.removeClass( "ui-btn-up-" + theme ).addClass( "ui-btn-hover-" + theme ); |
| }, hoverDelay ); |
| } else { |
| $btn.removeClass( "ui-btn-up-" + theme ).addClass( "ui-btn-hover-" + theme ); |
| } |
| } else if ( evt === "vmouseout" || evt === "blur" || evt === "scrollstart" ) { |
| $btn.removeClass( "ui-btn-hover-" + theme + " ui-btn-down-" + theme ).addClass( "ui-btn-up-" + theme ); |
| if ( hov ) { |
| clearTimeout( hov ); |
| } |
| if ( foc ) { |
| clearTimeout( foc ); |
| } |
| } |
| } |
| }, |
| "focusin focus": function( event ) { |
| $( closestEnabledButton( event.target ) ).addClass( $.mobile.focusClass ); |
| }, |
| "focusout blur": function( event ) { |
| $( closestEnabledButton( event.target ) ).removeClass( $.mobile.focusClass ); |
| } |
| }); |
| |
| attachEvents = null; |
| }; |
| |
| //links in bars, or those with data-role become buttons |
| //auto self-init widgets |
| $.mobile.$document.bind( "pagecreate create", function( e ) { |
| |
| $( ":jqmData(role='button'), .ui-bar > a, .ui-header > a, .ui-footer > a, .ui-bar > :jqmData(role='controlgroup') > a", e.target ) |
| .jqmEnhanceable() |
| .not( "button, input, .ui-btn, :jqmData(role='none'), :jqmData(role='nojs')" ) |
| .buttonMarkup(); |
| }); |
| |
| })( jQuery ); |
| |
| |
| (function( $, undefined ) { |
| |
| $.widget( "mobile.collapsible", $.mobile.widget, { |
| options: { |
| expandCueText: " Expandable list, tap to open list", |
| collapseCueText: " Expandable list, tap to close list", |
| collapsed: true, |
| heading: "h1,h2,h3,h4,h5,h6,legend", |
| theme: null, |
| contentTheme: null, |
| inset: true, |
| mini: false, |
| initSelector: ":jqmData(role='collapsible')" |
| }, |
| _create: function() { |
| |
| var $el = this.element, |
| o = this.options, |
| collapsible = $el.addClass( "ui-collapsible" ), |
| collapsibleHeading = $el.children( o.heading ).first(), |
| collapsedIcon = $.mobile.getAttrFixed( $el[0], "data-" + $.mobile.ns + "collapsed-icon" ) || o.collapsedIcon, |
| expandedIcon = $.mobile.getAttrFixed( $el[0], "data-" + $.mobile.ns + "expanded-icon" ) || o.expandedIcon, |
| collapsibleContent = collapsible.wrapInner( "<div class='ui-collapsible-content'></div>" ).children( ".ui-collapsible-content" ), |
| collapsibleSet = $el.closest( ":jqmData(role='collapsible-set')" ).addClass( "ui-collapsible-set" ); |
| |
| // Replace collapsibleHeading if it's a legend |
| if ( collapsibleHeading.is( "legend" ) ) { |
| collapsibleHeading = $( "<div role='heading'>"+ collapsibleHeading.html() +"</div>" ).insertBefore( collapsibleHeading ); |
| collapsibleHeading.next().remove(); |
| } |
| |
| // If we are in a collapsible set |
| if ( collapsibleSet.length ) { |
| // Inherit the theme from collapsible-set |
| if ( !o.theme ) { |
| o.theme = $.mobile.getAttrFixed( collapsibleSet[0], "data-" + $.mobile.ns + "theme" ) || $.mobile.getInheritedTheme( collapsibleSet, "c" ); |
| } |
| // Inherit the content-theme from collapsible-set |
| if ( !o.contentTheme ) { |
| o.contentTheme = $.mobile.getAttrFixed( collapsibleSet[0], "data-" + $.mobile.ns + "content-theme" ); |
| } |
| |
| // Get the preference for collapsed icon in the set |
| if ( !o.collapsedIcon ) { |
| o.collapsedIcon = $.mobile.getAttrFixed( collapsibleSet[0], "data-" + $.mobile.ns + "collapsed-icon" ); |
| } |
| // Get the preference for expanded icon in the set |
| if ( !o.expandedIcon ) { |
| o.expandedIcon = $.mobile.getAttrFixed( collapsibleSet[0], "data-" + $.mobile.ns + "expanded-icon" ); |
| } |
| // Gets the preference icon position in the set |
| if ( !o.iconPos ) { |
| o.iconPos = $.mobile.getAttrFixed( collapsibleSet[0], "data-" + $.mobile.ns + "iconpos" ); |
| } |
| // Inherit the preference for inset from collapsible-set or set the default value to ensure equalty within a set |
| if ( $.mobile.getAttrFixed( collapsibleSet[0], "data-" + $.mobile.ns + "inset" ) !== undefined ) { |
| o.inset = $.mobile.getAttrFixed( collapsibleSet[0], "data-" + $.mobile.ns + "inset" ); |
| } else { |
| o.inset = true; |
| } |
| // Gets the preference for mini in the set |
| if ( !o.mini ) { |
| o.mini = $.mobile.getAttrFixed( collapsibleSet[0], "data-" + $.mobile.ns + "mini" ); |
| } |
| } else { |
| // get inherited theme if not a set and no theme has been set |
| if ( !o.theme ) { |
| o.theme = $.mobile.getInheritedTheme( $el, "c" ); |
| } |
| } |
| |
| if ( !!o.inset ) { |
| collapsible.addClass( "ui-collapsible-inset" ); |
| } |
| |
| collapsibleContent.addClass( ( o.contentTheme ) ? ( "ui-body-" + o.contentTheme ) : ""); |
| |
| collapsedIcon = $.mobile.getAttrFixed( $el[0], "data-" + $.mobile.ns + "collapsed-icon" ) || o.collapsedIcon || "plus"; |
| expandedIcon = $.mobile.getAttrFixed( $el[0], "data-" + $.mobile.ns + "expanded-icon" ) || o.expandedIcon || "minus"; |
| |
| collapsibleHeading |
| //drop heading in before content |
| .insertBefore( collapsibleContent ) |
| //modify markup & attributes |
| .addClass( "ui-collapsible-heading" ) |
| .append( "<span class='ui-collapsible-heading-status'></span>" ) |
| .wrapInner( "<a href='#' class='ui-collapsible-heading-toggle'></a>" ) |
| .find( "a" ) |
| .first() |
| .buttonMarkup({ |
| shadow: false, |
| corners: false, |
| iconpos: $.mobile.getAttrFixed( $el[0], "data-" + $.mobile.ns + "iconpos" ) || o.iconPos || "left", |
| icon: collapsedIcon, |
| mini: o.mini, |
| theme: o.theme |
| }) |
| .attr( "role", ""); |
| |
| if ( !!o.inset ) { |
| collapsibleHeading |
| .find( "a" ).first().add( ".ui-btn-inner", $el ) |
| .addClass( "ui-corner-top ui-corner-bottom" ); |
| } |
| |
| //events |
| collapsible |
| .bind( "expand collapse", function( event ) { |
| if ( !event.isDefaultPrevented() ) { |
| var $this = $( this ), |
| isCollapse = ( event.type === "collapse" ), |
| contentTheme = o.contentTheme; |
| |
| event.preventDefault(); |
| |
| // Custom event callback |
| if ( o.customEventHandler ) { o.customEventHandler.call( this, isCollapse ) }; |
| |
| collapsibleHeading |
| .toggleClass( "ui-collapsible-heading-collapsed", isCollapse ) |
| .find( ".ui-collapsible-heading-status" ) |
| .text( isCollapse ? o.expandCueText : o.collapseCueText ) |
| .end() |
| .find( ".ui-icon" ) |
| .toggleClass( "ui-icon-" + expandedIcon, !isCollapse ) |
| // logic or cause same icon for expanded/collapsed state would remove the ui-icon-class |
| .toggleClass( "ui-icon-" + collapsedIcon, ( isCollapse || expandedIcon === collapsedIcon ) ) |
| .end() |
| .find( "a" ).first().removeClass( $.mobile.activeBtnClass ); |
| |
| $this.toggleClass( "ui-collapsible-collapsed", isCollapse ); |
| collapsibleContent.toggleClass( "ui-collapsible-content-collapsed", isCollapse ).attr( "aria-hidden", isCollapse ); |
| collapsibleContent.children( "li" ).not( "ui-collapsible-content" ).attr( "tabindex", isCollapse ? "" : "0" ); |
| |
| if ( contentTheme && !!o.inset && ( !collapsibleSet.length || collapsible.jqmData( "collapsible-last" ) ) ) { |
| collapsibleHeading |
| .find( "a" ).first().add( collapsibleHeading.find( ".ui-btn-inner" ) ) |
| .toggleClass( "ui-corner-bottom", isCollapse ); |
| collapsibleContent.toggleClass( "ui-corner-bottom", !isCollapse ); |
| } |
| collapsibleContent.trigger( "updatelayout" ); |
| } |
| }) |
| .trigger( o.collapsed ? "collapse" : "expand" ); |
| |
| collapsibleHeading |
| .bind( "tap", function() { |
| collapsibleHeading.find( "a" ).first().addClass( $.mobile.activeBtnClass ); |
| }) |
| .bind( "vmousecancel", function() { |
| collapsibleHeading.find( "a" ).first().removeClass( $.mobile.activeBtnClass ); |
| }) |
| .bind( "click", function( event ) { |
| |
| var type = collapsibleHeading.is( ".ui-collapsible-heading-collapsed" ) ? "expand" : "collapse"; |
| |
| collapsible.trigger( type ); |
| |
| event.preventDefault(); |
| event.stopPropagation(); |
| }); |
| } |
| }); |
| |
| //delegate auto self-init widgets |
| $.delegateSelfInitWithSingleSelector( $.mobile.collapsible ); |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| $.widget( "mobile.collapsibleset", $.mobile.widget, { |
| options: { |
| initSelector: ":jqmData(role='collapsible-set')" |
| }, |
| _create: function() { |
| var $el = this.element.addClass( "ui-collapsible-set" ), |
| o = this.options; |
| |
| // Inherit the theme from collapsible-set |
| if ( !o.theme ) { |
| o.theme = $.mobile.getInheritedTheme( $el, "c" ); |
| } |
| // Inherit the content-theme from collapsible-set |
| if ( !o.contentTheme ) { |
| o.contentTheme = $.mobile.getAttrFixed( $el[0], "data-" + $.mobile.ns + "content-theme" ); |
| } |
| |
| if ( $.mobile.getAttrFixed( $el[0], "data-" + $.mobile.ns + "inset" ) !== undefined ) { |
| o.inset = $.mobile.getAttrFixed( $el[0], "data-" + $.mobile.ns + "inset" ); |
| } |
| o.inset = o.inset !== undefined ? o.inset : true; |
| |
| // Initialize the collapsible set if it's not already initialized |
| if ( !$el.jqmData( "collapsiblebound" ) ) { |
| $el |
| .jqmData( "collapsiblebound", true ) |
| .bind( "expand collapse", function( event ) { |
| var isCollapse = ( event.type === "collapse" ), |
| collapsible = $( event.target ).closest( ".ui-collapsible" ), |
| widget = collapsible.data( "collapsible" ); |
| if ( collapsible.jqmData( "collapsible-last" ) && !!o.inset ) { |
| collapsible.find( ".ui-collapsible-heading" ).first() |
| .find( "a" ).first() |
| .toggleClass( "ui-corner-bottom", isCollapse ) |
| .find( ".ui-btn-inner" ) |
| .toggleClass( "ui-corner-bottom", isCollapse ); |
| collapsible.find( ".ui-collapsible-content" ).toggleClass( "ui-corner-bottom", !isCollapse ); |
| } |
| }) |
| .bind( "expand", function( event ) { |
| var closestCollapsible = $( event.target ) |
| .closest( ".ui-collapsible" ); |
| if ( closestCollapsible.parent().is( ":jqmData(role='collapsible-set')" ) ) { |
| closestCollapsible |
| .siblings( ".ui-collapsible" ) |
| .trigger( "collapse" ); |
| } |
| }); |
| } |
| }, |
| |
| _init: function() { |
| var $el = this.element, |
| collapsiblesInSet = $el.children( ":jqmData(role='collapsible')" ), |
| expanded = collapsiblesInSet.filter( ":jqmData(collapsed='false')" ); |
| this.refresh(); |
| |
| // Because the corners are handled by the collapsible itself and the default state is collapsed |
| // That was causing https://github.com/jquery/jquery-mobile/issues/4116 |
| expanded.trigger( "expand" ); |
| }, |
| |
| refresh: function() { |
| var $el = this.element, |
| o = this.options, |
| collapsiblesInSet = $el.children( ":jqmData(role='collapsible')" ); |
| |
| $.mobile.collapsible.prototype.enhance( collapsiblesInSet.not( ".ui-collapsible" ) ); |
| |
| // clean up borders |
| if ( !!o.inset ) { |
| collapsiblesInSet.each(function() { |
| $( this ).jqmRemoveData( "collapsible-last" ) |
| .find( ".ui-collapsible-heading" ) |
| .find( "a" ).first() |
| .removeClass( "ui-corner-top ui-corner-bottom" ) |
| .find( ".ui-btn-inner" ) |
| .removeClass( "ui-corner-top ui-corner-bottom" ); |
| }); |
| |
| collapsiblesInSet.first() |
| .find( "a" ) |
| .first() |
| .addClass( "ui-corner-top" ) |
| .find( ".ui-btn-inner" ) |
| .addClass( "ui-corner-top" ); |
| |
| collapsiblesInSet.last() |
| .jqmData( "collapsible-last", true ) |
| .find( "a" ) |
| .first() |
| .addClass( "ui-corner-bottom" ) |
| .find( ".ui-btn-inner" ) |
| .addClass( "ui-corner-bottom" ); |
| } |
| } |
| }); |
| |
| //delegate auto self-init widgets |
| $.delegateSelfInitWithSingleSelector( $.mobile.collapsibleset ); |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| $.widget( "mobile.navbar", $.mobile.widget, { |
| options: { |
| iconpos: "top", |
| grid: null, |
| initSelector: ":jqmData(role='navbar')" |
| }, |
| |
| _create: function() { |
| |
| var $navbar = this.element, |
| $navbtns = $navbar.find( "a" ), |
| iconpos = $navbtns.filter( ":jqmData(icon)" ).length ? |
| this.options.iconpos : undefined; |
| |
| $navbar.addClass( "ui-navbar ui-mini" ) |
| .attr( "role", "navigation" ) |
| .find( "ul" ) |
| .jqmEnhanceable() |
| .grid({ grid: this.options.grid }); |
| |
| $navbtns.buttonMarkup({ |
| corners: false, |
| shadow: false, |
| inline: true, |
| iconpos: iconpos |
| }); |
| |
| $navbar.delegate( "a", "vclick", function( event ) { |
| if ( !$(event.target).hasClass( "ui-disabled" ) ) { |
| $navbtns.removeClass( $.mobile.activeBtnClass ); |
| $( this ).addClass( $.mobile.activeBtnClass ); |
| } |
| }); |
| |
| // Buttons in the navbar with ui-state-persist class should regain their active state before page show |
| $navbar.closest( ".ui-page" ).bind( "pagebeforeshow", function() { |
| $navbtns.filter( ".ui-state-persist" ).addClass( $.mobile.activeBtnClass ); |
| }); |
| } |
| }); |
| |
| //delegate auto self-init widgets |
| $.delegateSelfInitWithSingleSelector( $.mobile.navbar ); |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| //Keeps track of the number of lists per page UID |
| //This allows support for multiple nested list in the same page |
| //https://github.com/jquery/jquery-mobile/issues/1617 |
| var listCountPerPage = {}; |
| |
| $.widget( "mobile.listview", $.mobile.widget, { |
| |
| options: { |
| theme: null, |
| countTheme: "c", |
| headerTheme: "b", |
| dividerTheme: "b", |
| splitIcon: "arrow-r", |
| splitTheme: "b", |
| inset: false, |
| initSelector: ":jqmData(role='listview')" |
| }, |
| |
| _create: function() { |
| var t = this, |
| listviewClasses = ""; |
| |
| listviewClasses += t.options.inset ? " ui-listview-inset ui-corner-all ui-shadow " : ""; |
| |
| // create listview markup |
| t.element.addClass(function( i, orig ) { |
| return orig + " ui-listview " + listviewClasses; |
| }); |
| |
| t.refresh( true ); |
| }, |
| |
| _removeCorners: function( li, which ) { |
| var top = "ui-corner-top ui-corner-tr ui-corner-tl", |
| bot = "ui-corner-bottom ui-corner-br ui-corner-bl"; |
| |
| li = li.add( li.find( ".ui-btn-inner, .ui-li-link-alt, .ui-li-thumb" ) ); |
| |
| if ( which === "top" ) { |
| li.removeClass( top ); |
| } else if ( which === "bottom" ) { |
| li.removeClass( bot ); |
| } else { |
| li.removeClass( top + " " + bot ); |
| } |
| }, |
| |
| _refreshCorners: function( create ) { |
| var $li, |
| $visibleli, |
| $topli, |
| $bottomli; |
| |
| $li = this.element.children( "li" ); |
| // At create time and when autodividers calls refresh the li are not visible yet so we need to rely on .ui-screen-hidden |
| $visibleli = create || $li.filter( ":visible" ).length === 0 ? $li.not( ".ui-screen-hidden" ) : $li.filter( ":visible" ); |
| |
| // ui-li-last is used for setting border-bottom on the last li |
| $li.filter( ".ui-li-last" ).removeClass( "ui-li-last" ); |
| |
| if ( this.options.inset ) { |
| this._removeCorners( $li ); |
| |
| // Select the first visible li element |
| $topli = $visibleli.first() |
| .addClass( "ui-corner-top" ); |
| |
| $topli.add( $topli.find( ".ui-btn-inner" ) |
| .not( ".ui-li-link-alt span:first-child" ) ) |
| .addClass( "ui-corner-top" ) |
| .end() |
| .find( ".ui-li-link-alt, .ui-li-link-alt span:first-child" ) |
| .addClass( "ui-corner-tr" ) |
| .end() |
| .find( ".ui-li-thumb" ) |
| .not( ".ui-li-icon" ) |
| .addClass( "ui-corner-tl" ); |
| |
| // Select the last visible li element |
| $bottomli = $visibleli.last() |
| .addClass( "ui-corner-bottom ui-li-last" ); |
| |
| $bottomli.add( $bottomli.find( ".ui-btn-inner" ) ) |
| .find( ".ui-li-link-alt" ) |
| .addClass( "ui-corner-br" ) |
| .end() |
| .find( ".ui-li-thumb" ) |
| .not( ".ui-li-icon" ) |
| .addClass( "ui-corner-bl" ); |
| } else { |
| $visibleli.last().addClass( "ui-li-last" ); |
| } |
| if ( !create ) { |
| this.element.trigger( "updatelayout" ); |
| } |
| }, |
| |
| // This is a generic utility method for finding the first |
| // node with a given nodeName. It uses basic DOM traversal |
| // to be fast and is meant to be a substitute for simple |
| // $.fn.closest() and $.fn.children() calls on a single |
| // element. Note that callers must pass both the lowerCase |
| // and upperCase version of the nodeName they are looking for. |
| // The main reason for this is that this function will be |
| // called many times and we want to avoid having to lowercase |
| // the nodeName from the element every time to ensure we have |
| // a match. Note that this function lives here for now, but may |
| // be moved into $.mobile if other components need a similar method. |
| _findFirstElementByTagName: function( ele, nextProp, lcName, ucName ) { |
| var dict = {}; |
| dict[ lcName ] = dict[ ucName ] = true; |
| while ( ele ) { |
| if ( dict[ ele.nodeName ] ) { |
| return ele; |
| } |
| ele = ele[ nextProp ]; |
| } |
| return null; |
| }, |
| _getChildrenByTagName: function( ele, lcName, ucName ) { |
| var results = [], |
| dict = {}; |
| dict[ lcName ] = dict[ ucName ] = true; |
| ele = ele.firstChild; |
| while ( ele ) { |
| if ( dict[ ele.nodeName ] ) { |
| results.push( ele ); |
| } |
| ele = ele.nextSibling; |
| } |
| return $( results ); |
| }, |
| |
| _addThumbClasses: function( containers ) { |
| var i, img, len = containers.length; |
| for ( i = 0; i < len; i++ ) { |
| img = $( this._findFirstElementByTagName( containers[ i ].firstChild, "nextSibling", "img", "IMG" ) ); |
| if ( img.length ) { |
| img.addClass( "ui-li-thumb" ).attr( { |
| "role" : "", |
| "aria-label" : "icon" |
| }); |
| $( this._findFirstElementByTagName( img[ 0 ].parentNode, "parentNode", "li", "LI" ) ).addClass( img.is( ".ui-li-icon" ) ? "ui-li-has-icon" : "ui-li-has-thumb" ); |
| } |
| } |
| }, |
| |
| _addCheckboxRadioClasses: function( containers ) |
| { |
| var i, inputAttr, len = containers.length; |
| for ( i = 0; i < len; i++ ) { |
| inputAttr = $( containers[ i ] ).find( "input" ); |
| if ( inputAttr.attr( "type" ) == "checkbox" ) { |
| $( containers[ i ] ).addClass( "ui-li-has-checkbox" ); |
| } else if ( inputAttr.attr( "type" ) == "radio" ) { |
| $( containers[ i ] ).addClass( "ui-li-has-radio" ); |
| } |
| } |
| }, |
| |
| _addRightBtnClasses: function( containers ) |
| { |
| var i, btnAttr, len = containers.length; |
| for ( i = 0; i < len; i++ ) { |
| btnAttr = $( containers[ i ] ).find( ":jqmData(role='button'),input[type='button'],select:jqmData(role='slider')" ); |
| if ( btnAttr.length ) { |
| if ( btnAttr.jqmData( "style" ) == "circle" ) { |
| $( containers[ i ] ).addClass( "ui-li-has-right-circle-btn" ); |
| } else { |
| $( containers[ i ] ).addClass( "ui-li-has-right-btn" ); |
| } |
| } |
| } |
| }, |
| |
| refresh: function( create ) { |
| this.parentPage = this.element.closest( ".ui-page" ); |
| this._createSubPages(); |
| |
| var o = this.options, |
| $list = this.element, |
| self = this, |
| dividertheme = $.mobile.getAttrFixed( $list[0], "data-" + $.mobile.ns + "dividertheme" ) || o.dividerTheme, |
| listsplittheme = $.mobile.getAttrFixed( $list[0], "data-" + $.mobile.ns + "splittheme" ), |
| listspliticon = $.mobile.getAttrFixed( $list[0], "data-" + $.mobile.ns + "spliticon" ), |
| li = this._getChildrenByTagName( $list[ 0 ], "li", "LI" ), |
| ol = !!$.nodeName( $list[ 0 ], "ol" ), |
| jsCount = !$.support.cssPseudoElement, |
| start = $list.attr( "start" ), |
| itemClassDict = {}, |
| item, itemClass, itemTheme, |
| a, last, splittheme, counter, startCount, newStartCount, countParent, icon, imgParents, img, linkIcon; |
| |
| if ( ol && jsCount ) { |
| $list.find( ".ui-li-dec" ).remove(); |
| } |
| |
| if ( ol ) { |
| // Check if a start attribute has been set while taking a value of 0 into account |
| if ( start || start === 0 ) { |
| if ( !jsCount ) { |
| startCount = parseFloat( start ) - 1; |
| $list.css( "counter-reset", "listnumbering " + startCount ); |
| } else { |
| counter = parseFloat( start ); |
| } |
| } else if ( jsCount ) { |
| counter = 1; |
| } |
| } |
| |
| if ( !o.theme ) { |
| o.theme = $.mobile.getInheritedTheme( this.element, "c" ); |
| } |
| |
| for ( var pos = 0, numli = li.length; pos < numli; pos++ ) { |
| item = li.eq( pos ); |
| itemClass = "ui-li"; |
| |
| // If we're creating the element, we update it regardless |
| if ( create || !item.hasClass( "ui-li" ) ) { |
| itemTheme = $.mobile.getAttrFixed( item[0], "data-" + $.mobile.ns + "theme" ) || o.theme; |
| a = this._getChildrenByTagName( item[ 0 ], "a", "A" ).attr( { |
| "role": "", |
| "tabindex": "0" |
| }); |
| var isDivider = ( $.mobile.getAttrFixed( item[0], "data-" + $.mobile.ns + "role" ) === "list-divider" ); |
| |
| if ( item.hasClass( "ui-li-has-checkbox" ) || item.hasClass( "ui-li-has-radio" ) ) { |
| item.on( "vclick", function ( e ) { |
| var targetItem = $( e.target ); |
| var checkboxradio = targetItem.find( ".ui-checkbox" ); |
| if ( !checkboxradio.length ) { |
| checkboxradio = targetItem.find( ".ui-radio" ); |
| } |
| |
| if ( checkboxradio.length ) { |
| checkboxradio.children( "label" ).trigger( "vclick" ); |
| } |
| }); |
| } |
| |
| if ( a.length && !isDivider ) { |
| icon = $.mobile.getAttrFixed( item[0], "data-" + $.mobile.ns + "icon" ); |
| |
| /* Remove auto populated right-arrow button. */ |
| if ( icon === undefined ) { |
| icon = false; |
| } |
| |
| item.buttonMarkup({ |
| wrapperEls: "div", |
| shadow: false, |
| corners: false, |
| iconpos: "right", |
| icon: a.length > 1 || icon === false ? false : icon || "arrow-r", |
| theme: itemTheme |
| }); |
| |
| if ( ( icon !== false ) && ( a.length === 1 ) ) { |
| item.addClass( "ui-li-has-arrow" ); |
| } |
| |
| a.first().removeClass( "ui-link" ).addClass( "ui-link-inherit" ); |
| |
| if ( a.length > 1 ) { |
| itemClass += " ui-li-has-alt"; |
| |
| last = a.last(); |
| splittheme = listsplittheme || $.mobile.getAttrFixed( last[0], "data-" + $.mobile.ns + "theme" ) || o.splitTheme; |
| linkIcon = $.mobile.getAttrFixed( last[0], "data-" + $.mobile.ns + "icon" ); |
| |
| last.appendTo( item ) |
| .attr( "title", last.getEncodedText() ) |
| .addClass( "ui-li-link-alt" ) |
| .empty() |
| .buttonMarkup({ |
| shadow: false, |
| corners: false, |
| theme: itemTheme, |
| icon: false, |
| iconpos: "notext" |
| }) |
| .find( ".ui-btn-inner" ) |
| .append( |
| $( document.createElement( "span" ) ).buttonMarkup({ |
| shadow: true, |
| corners: true, |
| theme: splittheme, |
| iconpos: "notext", |
| // link icon overrides list item icon overrides ul element overrides options |
| icon: linkIcon || icon || listspliticon || o.splitIcon |
| }) |
| ); |
| } |
| } else if ( isDivider ) { |
| |
| itemClass += " ui-li-divider ui-bar-" + dividertheme; |
| item.attr( { "role": "heading", "tabindex": "0" } ); |
| |
| if ( ol ) { |
| //reset counter when a divider heading is encountered |
| if ( start || start === 0 ) { |
| if ( !jsCount ) { |
| newStartCount = parseFloat( start ) - 1; |
| item.css( "counter-reset", "listnumbering " + newStartCount ); |
| } else { |
| counter = parseFloat( start ); |
| } |
| } else if ( jsCount ) { |
| counter = 1; |
| } |
| } |
| |
| } else { |
| itemClass += " ui-li-static ui-btn-up-" + itemTheme; |
| item.attr( "tabindex", "0" ); |
| } |
| } |
| |
| if ( ol && jsCount && itemClass.indexOf( "ui-li-divider" ) < 0 ) { |
| countParent = itemClass.indexOf( "ui-li-static" ) > 0 ? item : item.find( ".ui-link-inherit" ); |
| |
| countParent.addClass( "ui-li-jsnumbering" ) |
| .prepend( "<span class='ui-li-dec'>" + ( counter++ ) + ". </span>" ); |
| } |
| |
| // Instead of setting item class directly on the list item and its |
| // btn-inner at this point in time, push the item into a dictionary |
| // that tells us what class to set on it so we can do this after this |
| // processing loop is finished. |
| |
| if ( !itemClassDict[ itemClass ] ) { |
| itemClassDict[ itemClass ] = []; |
| } |
| |
| itemClassDict[ itemClass ].push( item[ 0 ] ); |
| } |
| |
| // Set the appropriate listview item classes on each list item |
| // and their btn-inner elements. The main reason we didn't do this |
| // in the for-loop above is because we can eliminate per-item function overhead |
| // by calling addClass() and children() once or twice afterwards. This |
| // can give us a significant boost on platforms like WP7.5. |
| |
| for ( itemClass in itemClassDict ) { |
| $( itemClassDict[ itemClass ] ).addClass( itemClass ).children( ".ui-btn-inner" ).addClass( itemClass ); |
| } |
| |
| $list.find( "h1, h2, h3, h4, h5, h6" ).addClass( "ui-li-heading" ) |
| .end() |
| |
| .find( "p, dl" ).addClass( "ui-li-desc" ) |
| .end() |
| |
| .find( ".ui-li-aside" ).each(function() { |
| var $this = $( this ); |
| $this.prependTo( $this.parent() ); //shift aside to front for css float |
| }) |
| .end() |
| |
| .find( ".ui-li-count" ).each(function() { |
| $( this ).closest( "li" ).addClass( "ui-li-has-count" ); |
| }).addClass( "ui-btn-up-" + ( $.mobile.getAttrFixed( $list[0], "data-" + $.mobile.ns + "counttheme" ) || this.options.countTheme) + " ui-btn-corner-all" ); |
| |
| // The idea here is to look at the first image in the list item |
| // itself, and any .ui-link-inherit element it may contain, so we |
| // can place the appropriate classes on the image and list item. |
| // Note that we used to use something like: |
| // |
| // li.find(">img:eq(0), .ui-link-inherit>img:eq(0)").each( ... ); |
| // |
| // But executing a find() like that on Windows Phone 7.5 took a |
| // really long time. Walking things manually with the code below |
| // allows the 400 listview item page to load in about 3 seconds as |
| // opposed to 30 seconds. |
| |
| this._addThumbClasses( li ); |
| this._addThumbClasses( $list.find( ".ui-link-inherit" ) ); |
| |
| this._addCheckboxRadioClasses( li ); |
| this._addCheckboxRadioClasses( $list.find( ".ui-link-inherit" ) ); |
| |
| this._addRightBtnClasses( li ); |
| this._addRightBtnClasses( $list.find( ".ui-link-inherit" ) ); |
| |
| this._refreshCorners( create ); |
| |
| // autodividers binds to this to redraw dividers after the listview refresh |
| this._trigger( "afterrefresh" ); |
| }, |
| |
| //create a string for ID/subpage url creation |
| _idStringEscape: function( str ) { |
| return str.replace(/[^a-zA-Z0-9]/g, '-'); |
| }, |
| |
| _createSubPages: function() { |
| var parentList = this.element, |
| parentPage = parentList.closest( ".ui-page" ), |
| parentUrl = parentPage.jqmData( "url" ), |
| parentId = parentUrl || parentPage[ 0 ][ $.expando ], |
| parentListId = parentList.attr( "id" ), |
| o = this.options, |
| dns = "data-" + $.mobile.ns, |
| self = this, |
| persistentFooterID = parentPage.find( ":jqmData(role='footer')" ).jqmData( "id" ), |
| hasSubPages; |
| |
| if ( typeof listCountPerPage[ parentId ] === "undefined" ) { |
| listCountPerPage[ parentId ] = -1; |
| } |
| |
| parentListId = parentListId || ++listCountPerPage[ parentId ]; |
| |
| $( parentList.find( "li>ul, li>ol" ).toArray().reverse() ).each(function( i ) { |
| var self = this, |
| list = $( this ), |
| listId = list.attr( "id" ) || parentListId + "-" + i, |
| parent = list.parent(), |
| nodeElsFull = $( list.prevAll().toArray().reverse() ), |
| nodeEls = nodeElsFull.length ? nodeElsFull : $( "<span>" + $.trim(parent.contents()[ 0 ].nodeValue) + "</span>" ), |
| title = nodeEls.first().getEncodedText(),//url limits to first 30 chars of text |
| id = ( parentUrl || "" ) + "&" + $.mobile.subPageUrlKey + "=" + listId, |
| theme = $.mobile.getAttrFixed( list[0], "data-" + $.mobile.ns + "theme" ) || o.theme, |
| countTheme = $.mobile.getAttrFixed( list[0], "data-" + $.mobile.ns + "counttheme" ) || $.mobile.getAttrFixed( parentList[0], "data-" + $.mobile.ns + "counttheme" ) || o.countTheme, |
| newPage, anchor; |
| |
| //define hasSubPages for use in later removal |
| hasSubPages = true; |
| |
| newPage = list.detach() |
| .wrap( "<div " + dns + "role='page' " + dns + "url='" + id + "' " + dns + "theme='" + theme + "' " + dns + "count-theme='" + countTheme + "'><div " + dns + "role='content'></div></div>" ) |
| .parent() |
| .before( "<div " + dns + "role='header' " + dns + "theme='" + o.headerTheme + "'><div class='ui-title'>" + title + "</div></div>" ) |
| .after( persistentFooterID ? $( "<div " + dns + "role='footer' " + dns + "id='"+ persistentFooterID +"'>" ) : "" ) |
| .parent() |
| .appendTo( $.mobile.pageContainer ); |
| |
| newPage.page(); |
| |
| anchor = parent.find( 'a:first' ); |
| |
| if ( !anchor.length ) { |
| anchor = $( "<a/>" ).html( nodeEls || title ).prependTo( parent.empty() ); |
| } |
| |
| anchor.attr( "href", "#" + id ); |
| |
| }).listview(); |
| |
| // on pagehide, remove any nested pages along with the parent page, as long as they aren't active |
| // and aren't embedded |
| if ( hasSubPages && |
| parentPage.is( ":jqmData(external-page='true')" ) && |
| parentPage.data( "page" ).options.domCache === false ) { |
| |
| var newRemove = function( e, ui ) { |
| var nextPage = ui.nextPage, npURL, |
| prEvent = new $.Event( "pageremove" ); |
| |
| if ( ui.nextPage ) { |
| npURL = nextPage.jqmData( "url" ); |
| if ( npURL.indexOf( parentUrl + "&" + $.mobile.subPageUrlKey ) !== 0 ) { |
| self.childPages().remove(); |
| parentPage.trigger( prEvent ); |
| if ( !prEvent.isDefaultPrevented() ) { |
| parentPage.removeWithDependents(); |
| } |
| } |
| } |
| }; |
| |
| // unbind the original page remove and replace with our specialized version |
| parentPage |
| .unbind( "pagehide.remove" ) |
| .bind( "pagehide.remove", newRemove); |
| } |
| }, |
| |
| addItem : function( listitem , idx ) { |
| var $item = $(listitem), |
| $li, |
| _self = this; |
| |
| $li = _self.element.children( 'li' ); |
| $item.css( { 'opacity' : 0, |
| 'display' : 'none' } ); |
| if( $li.length == 0 |
| || $li.length <= idx) |
| { |
| $( _self.element ).append( $item ); |
| } else { |
| $( $li.get( idx ) ).before( $item ); |
| } |
| $(_self.element).trigger("create") |
| .listview( 'refresh' ); |
| |
| $item.css( 'min-height' , '0px' ); |
| |
| $item.slideDown( 'fast' , function( ){ |
| $item.addClass("addli"); |
| $item.css( { 'opacity' : 1 } ); |
| } ); |
| }, |
| |
| removeItem : function( idx ) { |
| var $item, |
| $li, |
| _self = this; |
| |
| $li = _self.element.children( 'li' ); |
| if( $li.length <= 0 || |
| $li.length < idx ) { |
| return ; |
| } |
| $item = $( $li.get( idx ) ); |
| $item.addClass("removeli"); |
| $item.slideUp('normal', |
| function( ) { |
| $(this).remove(); |
| }); |
| }, |
| |
| // TODO sort out a better way to track sub pages of the listview this is brittle |
| childPages: function() { |
| var parentUrl = this.parentPage.jqmData( "url" ); |
| |
| return $( ":jqmData(url^='"+ parentUrl + "&" + $.mobile.subPageUrlKey + "')" ); |
| } |
| }); |
| |
| //delegate auto self-init widgets |
| $.delegateSelfInitWithSingleSelector( $.mobile.listview ); |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| $.mobile.listview.prototype.options.autodividers = false; |
| $.mobile.listview.prototype.options.autodividersSelector = function( elt ) { |
| // look for the text in the given element |
| var text = elt.text() || null; |
| |
| if ( !text ) { |
| return null; |
| } |
| |
| // create the text for the divider (first uppercased letter) |
| text = text.trim().slice( 0, 1 ).toUpperCase(); |
| |
| return text; |
| }; |
| |
| $.mobile.$document.delegate( "ul,ol", "listviewcreate", function() { |
| |
| var list = $( this ), |
| listview = list.data( "listview" ); |
| |
| if ( !listview || !listview.options.autodividers ) { |
| return; |
| } |
| |
| var replaceDividers = function () { |
| list.find( "li:jqmData(role='list-divider')" ).remove(); |
| |
| var lis = list.find( 'li' ), |
| lastDividerText = null, li, dividerText; |
| |
| for ( var i = 0; i < lis.length ; i++ ) { |
| li = lis[i]; |
| dividerText = listview.options.autodividersSelector( $( li ) ); |
| |
| if ( dividerText && lastDividerText !== dividerText ) { |
| var divider = document.createElement( 'li' ); |
| divider.appendChild( document.createTextNode( dividerText ) ); |
| divider.setAttribute( 'data-' + $.mobile.ns + 'role', 'list-divider' ); |
| li.parentNode.insertBefore( divider, li ); |
| } |
| |
| lastDividerText = dividerText; |
| } |
| }; |
| |
| var afterListviewRefresh = function () { |
| list.unbind( 'listviewafterrefresh', afterListviewRefresh ); |
| replaceDividers(); |
| listview.refresh(); |
| list.bind( 'listviewafterrefresh', afterListviewRefresh ); |
| }; |
| |
| afterListviewRefresh(); |
| }); |
| |
| })( jQuery ); |
| |
| /* |
| * "checkboxradio" plugin |
| */ |
| |
| (function( $, undefined ) { |
| |
| $.widget( "mobile.checkboxradio", $.mobile.widget, { |
| options: { |
| theme: null, |
| initSelector: "input[type='checkbox'],input[type='radio']" |
| }, |
| _create: function() { |
| var self = this, |
| input = this.element, |
| inheritAttr = function( input, dataAttr ) { |
| return input.jqmData( dataAttr ) || input.closest( "form, fieldset" ).jqmData( dataAttr ); |
| }, |
| // NOTE: Windows Phone could not find the label through a selector |
| // filter works though. |
| parentLabel = $( input ).closest( "label" ), |
| label = parentLabel.length ? parentLabel : ( input[0].id ? $( input ).closest( "form, fieldset, :jqmData(role='page'), :jqmData(role='dialog')" ).find( "label" ).filter( "[for='" + input[0].id + "']" ) : [ ] ), |
| inputtype = input[0].type, |
| mini = inheritAttr( input, "mini" ), |
| checkedState = inputtype + "-on", |
| uncheckedState = inputtype + "-off", |
| icon = input.parents( ":jqmData(type='horizontal')" ).length ? undefined : uncheckedState, |
| iconpos = inheritAttr( input, "iconpos" ), |
| activeBtn = icon ? "" : " " + $.mobile.activeBtnClass, |
| checkedClass = "ui-" + checkedState + activeBtn, |
| uncheckedClass = "ui-" + uncheckedState, |
| checkedicon = "ui-icon-" + checkedState, |
| uncheckedicon = "ui-icon-" + uncheckedState, |
| ariaCheckedAttr = ""; |
| |
| if ( inputtype !== "checkbox" && inputtype !== "radio" ) { |
| return; |
| } |
| |
| ariaCheckedAttr = ( inputtype === "checkbox" ) ? "aria-checked" : "aria-selected"; |
| |
| // Support fake label |
| if ( label.length == 0 ) { |
| label = $( "<label for='" + input[ 0 ].id + |
| "'></label>" ); |
| } |
| |
| // Expose for other methods |
| $.extend( this, { |
| label: label, |
| inputtype: inputtype, |
| checkedClass: checkedClass, |
| uncheckedClass: uncheckedClass, |
| checkedicon: checkedicon, |
| uncheckedicon: uncheckedicon, |
| ariaCheckedAttr : ariaCheckedAttr |
| }); |
| |
| // If there's no selected theme check the data attr |
| if ( !this.options.theme ) { |
| this.options.theme = $.mobile.getInheritedTheme( this.element, "c" ); |
| } |
| |
| label.buttonMarkup({ |
| theme: this.options.theme, |
| icon: icon, |
| shadow: false, |
| mini: mini, |
| iconpos: iconpos |
| }); |
| |
| // Wrap the input + label in a div |
| var wrapper = document.createElement('div'); |
| wrapper.className = 'ui-' + inputtype; |
| wrapper.setAttribute( "role", inputtype ); |
| |
| if ( input.hasClass( "favorite" ) ) { |
| wrapper.className += ' favorite'; |
| } |
| |
| input.add( label ).wrapAll( wrapper ); |
| |
| label.bind({ |
| vmouseover: function( event ) { |
| if ( $( this ).parent().is( ".ui-disabled" ) ) { |
| event.stopPropagation(); |
| } |
| }, |
| |
| vclick: function( event ) { |
| if ( input.is( ":disabled" ) ) { |
| event.preventDefault(); |
| return; |
| } |
| |
| self._cacheVals(); |
| |
| input.prop( "checked", inputtype === "radio" && true || !input.prop( "checked" ) ); |
| |
| // trigger click handler's bound directly to the input as a substitute for |
| // how label clicks behave normally in the browsers |
| // TODO: it would be nice to let the browser's handle the clicks and pass them |
| // through to the associate input. we can swallow that click at the parent |
| // wrapper element level |
| input.triggerHandler( 'click' ); |
| |
| // Input set for common radio buttons will contain all the radio |
| // buttons, but will not for checkboxes. clearing the checked status |
| // of other radios ensures the active button state is applied properly |
| self._getInputSet().not( input ).prop( "checked", false ); |
| |
| self._updateAll(); |
| return false; |
| } |
| }); |
| |
| input |
| .bind({ |
| vmousedown: function() { |
| self._cacheVals(); |
| }, |
| |
| vclick: function() { |
| var $this = $( this ); |
| |
| // Adds checked attribute to checked input when keyboard is used |
| if ( $this.is( ":checked" ) ) { |
| |
| $this.prop( "checked", true); |
| self._getInputSet().not( $this ).prop( "checked", false ); |
| } else { |
| |
| $this.prop( "checked", false ); |
| } |
| |
| self._updateAll(); |
| }, |
| |
| focus: function() { |
| label.addClass( $.mobile.focusClass ); |
| }, |
| |
| blur: function() { |
| label.removeClass( $.mobile.focusClass ); |
| } |
| }); |
| |
| this.refresh(); |
| }, |
| |
| _cacheVals: function() { |
| this._getInputSet().each(function() { |
| $( this ).jqmData( "cacheVal", this.checked ); |
| }); |
| }, |
| |
| //returns either a set of radios with the same name attribute, or a single checkbox |
| _getInputSet: function() { |
| if ( this.inputtype === "checkbox" ) { |
| return this.element; |
| } |
| |
| return this.element.closest( "form, fieldset, :jqmData(role='page'), :jqmData(role='dialog')" ) |
| .find( "input[name='" + this.element[0].name + "'][type='" + this.inputtype + "']" ); |
| }, |
| |
| _updateAll: function() { |
| var self = this; |
| |
| this._getInputSet().each(function() { |
| var $this = $( this ); |
| |
| if ( this.checked || self.inputtype === "checkbox" ) { |
| $this.trigger( "change" ); |
| } |
| $this.focus(); |
| }) |
| .checkboxradio( "refresh" ); |
| }, |
| |
| refresh: function() { |
| var input = this.element[0], |
| label = this.label, |
| wrapper = input.parentNode, |
| icon = label.find( ".ui-icon" ); |
| |
| if ( input.checked ) { |
| label.addClass( this.checkedClass ).removeClass( this.uncheckedClass ); |
| icon.addClass( this.checkedicon ).removeClass( this.uncheckedicon ); |
| wrapper.setAttribute( this.ariaCheckedAttr, true ); |
| } else { |
| label.removeClass( this.checkedClass ).addClass( this.uncheckedClass ); |
| icon.removeClass( this.checkedicon ).addClass( this.uncheckedicon ); |
| wrapper.setAttribute( this.ariaCheckedAttr, false ); |
| } |
| |
| if ( input.disabled ) { |
| this.disable(); |
| } else { |
| this.enable(); |
| } |
| }, |
| |
| disable: function() { |
| this.element.prop( "disabled", true ).parent().addClass( "ui-disabled" ); |
| }, |
| |
| enable: function() { |
| this.element.prop( "disabled", false ).parent().removeClass( "ui-disabled" ); |
| } |
| }); |
| |
| //auto self-init widgets |
| $.mobile.$document.bind( "pagecreate create", function( e ) { |
| $.mobile.checkboxradio.prototype.enhanceWithin( e.target, true ); |
| }); |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| $.widget( "mobile.button", $.mobile.widget, { |
| options: { |
| theme: null, |
| icon: null, |
| iconpos: null, |
| corners: true, |
| shadow: true, |
| iconshadow: true, |
| initSelector: "button, [type='button'], [type='submit'], [type='reset']" |
| }, |
| _create: function() { |
| var $el = this.element, |
| $button, |
| o = this.options, |
| type, |
| name, |
| inline = o.inline || $.mobile.getAttrFixed( $el[0], "data-" + $.mobile.ns + "inline" ), |
| mini = o.mini || $.mobile.getAttrFixed( $el[0], "data-" + $.mobile.ns + "mini" ), |
| classes = "", |
| $buttonPlaceholder; |
| |
| // if this is a link, check if it's been enhanced and, if not, use the right function |
| if ( $el[ 0 ].tagName === "A" ) { |
| if ( !$el.hasClass( "ui-btn" ) ) { |
| $el.buttonMarkup(); |
| } |
| |
| return; |
| } |
| |
| // get the inherited theme |
| // TODO centralize for all widgets |
| if ( !this.options.theme ) { |
| this.options.theme = $.mobile.getInheritedTheme( this.element, "c" ); |
| } |
| |
| // TODO: Post 1.1--once we have time to test thoroughly--any classes manually applied to the original element should be carried over to the enhanced element, with an `-enhanced` suffix. See https://github.com/jquery/jquery-mobile/issues/3577 |
| /* if ( $el[0].className.length ) { |
| classes = $el[0].className; |
| } */ |
| if ( !!~$el[0].className.indexOf( "ui-btn-left" ) ) { |
| classes = "ui-btn-left"; |
| } |
| |
| if ( !!~$el[0].className.indexOf( "ui-btn-right" ) ) { |
| classes = "ui-btn-right"; |
| } |
| |
| if ( $el.attr( "type" ) === "submit" || $el.attr( "type" ) === "reset" ) { |
| classes ? classes += " ui-submit" : classes = "ui-submit"; |
| } |
| $( "label[for='" + $el.attr( "id" ) + "']" ).addClass( "ui-submit" ); |
| |
| // Add ARIA role |
| this.button = $( "<div></div>" ) |
| [ $el.html() ? "html" : "text" ]( $el.html() || $el.val() ) |
| .insertBefore( $el ) |
| .buttonMarkup({ |
| theme: o.theme, |
| icon: o.icon, |
| iconpos: o.iconpos, |
| inline: inline, |
| corners: o.corners, |
| shadow: o.shadow, |
| iconshadow: o.iconshadow, |
| mini: mini |
| }) |
| .addClass( classes ) |
| .append( $el.addClass( "ui-btn-hidden" ) ); |
| |
| $button = this.button; |
| type = $el.attr( "type" ); |
| name = $el.attr( "name" ); |
| |
| // Add hidden input during submit if input type="submit" has a name. |
| if ( type !== "button" && type !== "reset" && name ) { |
| $el.bind( "vclick", function() { |
| // Add hidden input if it doesn't already exist. |
| if ( $buttonPlaceholder === undefined ) { |
| $buttonPlaceholder = $( "<input>", { |
| type: "hidden", |
| name: $el.attr( "name" ), |
| value: $el.attr( "value" ) |
| }).insertBefore( $el ); |
| |
| // Bind to doc to remove after submit handling |
| $.mobile.$document.one( "submit", function() { |
| $buttonPlaceholder.remove(); |
| |
| // reset the local var so that the hidden input |
| // will be re-added on subsequent clicks |
| $buttonPlaceholder = undefined; |
| }); |
| } |
| }); |
| } |
| |
| $el.bind({ |
| focus: function() { |
| $button.addClass( $.mobile.focusClass ); |
| }, |
| |
| blur: function() { |
| $button.removeClass( $.mobile.focusClass ); |
| } |
| }); |
| |
| this.refresh(); |
| }, |
| |
| enable: function() { |
| this.element.attr( "disabled", false ); |
| this.button.removeClass( "ui-disabled" ).attr( "aria-disabled", false ); |
| return this._setOption( "disabled", false ); |
| }, |
| |
| disable: function() { |
| this.element.attr( "disabled", true ); |
| this.button.addClass( "ui-disabled" ).attr( "aria-disabled", true ); |
| return this._setOption( "disabled", true ); |
| }, |
| |
| refresh: function() { |
| var $el = this.element; |
| |
| if ( $el.prop("disabled") ) { |
| this.disable(); |
| } else { |
| this.enable(); |
| } |
| |
| // Grab the button's text element from its implementation-independent data item |
| $( this.button.data( 'buttonElements' ).text )[ $el.html() ? "html" : "text" ]( $el.html() || $el.val() ); |
| } |
| }); |
| |
| //auto self-init widgets |
| $.mobile.$document.bind( "pagecreate create", function( e ) { |
| $.mobile.button.prototype.enhanceWithin( e.target, true ); |
| }); |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| $.fn.controlgroup = function( options ) { |
| function flipClasses( els, flCorners ) { |
| els.removeClass( "ui-btn-corner-all ui-corner-top ui-corner-bottom ui-corner-left ui-corner-right ui-controlgroup-last ui-shadow" ) |
| .eq( 0 ).addClass( flCorners[ 0 ] ) |
| .end() |
| .last().addClass( flCorners[ 1 ] ).addClass( "ui-controlgroup-last" ); |
| } |
| |
| return this.each(function() { |
| var $el = $( this ), |
| o = $.extend({ |
| direction: $.mobile.getAttrFixed( $el[0], "data-"+ $.mobile.ns + "type" ) || "vertical", |
| shadow: false, |
| excludeInvisible: true, |
| mini: $.mobile.getAttrFixed( $el[0], "data-"+ $.mobile.ns + "mini" ) |
| }, options ), |
| grouplegend = $el.children( "legend" ), |
| groupheading = $el.children( ".ui-controlgroup-label" ), |
| groupcontrols = $el.children( ".ui-controlgroup-controls" ), |
| flCorners = o.direction === "horizontal" ? [ "ui-corner-left", "ui-corner-right" ] : [ "ui-corner-top", "ui-corner-bottom" ], |
| type = $el.find( "input" ).first().attr( "type" ); |
| |
| // First unwrap the controls if the controlgroup was already enhanced |
| if ( groupcontrols.length ) { |
| groupcontrols.contents().unwrap(); |
| } |
| $el.wrapInner( "<div class='ui-controlgroup-controls'></div>" ); |
| |
| if ( grouplegend.length ) { |
| // Replace legend with more stylable replacement div |
| $( "<div role='heading' class='ui-controlgroup-label'>" + grouplegend.html() + "</div>" ).insertBefore( $el.children( 0 ) ); |
| grouplegend.remove(); |
| } else if ( groupheading.length ) { |
| // Just move the heading if the controlgroup was already enhanced |
| $el.prepend( groupheading ); |
| } |
| |
| $el.addClass( "ui-corner-all ui-controlgroup ui-controlgroup-" + o.direction ); |
| |
| flipClasses( $el.find( ".ui-btn" + ( o.excludeInvisible ? ":visible" : "" ) ).not( '.ui-slider-handle' ), flCorners ); |
| flipClasses( $el.find( ".ui-btn-inner" ), flCorners ); |
| |
| if ( o.shadow ) { |
| $el.addClass( "ui-shadow" ); |
| } |
| |
| if ( o.mini ) { |
| $el.addClass( "ui-mini" ); |
| } |
| |
| }); |
| }; |
| |
| // The pagecreate handler for controlgroup is in jquery.mobile.init because of the soft-dependency on the wrapped widgets |
| |
| })(jQuery); |
| |
| (function( $, undefined ) { |
| |
| $.mobile.$document.bind( "pagecreate create", function( e ) { |
| |
| //links within content areas, tests included with page |
| $( e.target ) |
| .find( "a" ) |
| .jqmEnhanceable() |
| .not( ".ui-btn, .ui-link-inherit, :jqmData(role='none'), :jqmData(role='nojs')" ) |
| .addClass( "ui-link" ); |
| |
| }); |
| |
| })( jQuery ); |
| |
| |
| (function( $, undefined ) { |
| |
| function fitSegmentInsideSegment( winSize, segSize, offset, desired ) { |
| var ret = desired; |
| |
| if ( winSize < segSize ) { |
| // Center segment if it's bigger than the window |
| ret = offset + ( winSize - segSize ) / 2; |
| } else { |
| // Otherwise center it at the desired coordinate while keeping it completely inside the window |
| ret = Math.min( Math.max( offset, desired - segSize / 2 ), offset + winSize - segSize ); |
| } |
| |
| return ret; |
| } |
| |
| function windowCoords() { |
| var $win = $.mobile.$window; |
| |
| return { |
| x: $win.scrollLeft(), |
| y: $win.scrollTop(), |
| cx: ( window.innerWidth || $win.width() ), |
| cy: ( window.innerHeight || $win.height() ) |
| }; |
| } |
| |
| $.widget( "mobile.popup", $.mobile.widget, { |
| options: { |
| theme: null, |
| overlayTheme: null, |
| shadow: true, |
| corners: true, |
| transition: "pop", |
| positionTo: "origin", |
| tolerance: null, |
| initSelector: ":jqmData(role='popup')", |
| closeLinkSelector: "a:jqmData(rel='back')", |
| closeLinkEvents: "click.popup", |
| navigateEvents: "navigate.popup", |
| closeEvents: "navigate.popup pagebeforechange.popup", |
| isHardwarePopup: false, |
| // NOTE Windows Phone 7 has a scroll position caching issue that |
| // requires us to disable popup history management by default |
| // https://github.com/jquery/jquery-mobile/issues/4784 |
| // |
| // NOTE this option is modified in _create! |
| history: false |
| }, |
| |
| _eatEventAndClose: function( e ) { |
| e.preventDefault(); |
| e.stopImmediatePropagation(); |
| this.close(); |
| return false; |
| }, |
| |
| // Make sure the screen size is increased beyond the page height if the popup's causes the document to increase in height |
| _resizeScreen: function() { |
| var popupHeight = this._ui.container.outerHeight( true ); |
| |
| this._ui.screen.removeAttr( "style" ); |
| if ( popupHeight > this._ui.screen.height() ) { |
| this._ui.screen.height( popupHeight ); |
| } |
| }, |
| |
| _handleWindowKeyUp: function( e ) { |
| if ( this._isOpen && e.keyCode === $.mobile.keyCode.ESCAPE ) { |
| return this._eatEventAndClose( e ); |
| } |
| }, |
| |
| _maybeRefreshTimeout: function() { |
| var winCoords = windowCoords(); |
| |
| if ( this._resizeData ) { |
| if ( winCoords.x === this._resizeData.winCoords.x && |
| winCoords.y === this._resizeData.winCoords.y && |
| winCoords.cx === this._resizeData.winCoords.cx && |
| winCoords.cy === this._resizeData.winCoords.cy ) { |
| // timeout not refreshed |
| return false; |
| } else { |
| // clear existing timeout - it will be refreshed below |
| clearTimeout( this._resizeData.timeoutId ); |
| } |
| } |
| |
| this._resizeData = { |
| timeoutId: setTimeout( $.proxy( this, "_resizeTimeout" ), 200 ), |
| winCoords: winCoords |
| }; |
| |
| return true; |
| }, |
| |
| _resizeTimeout: function() { |
| if ( !this._maybeRefreshTimeout() && this.positionTo === "window" && this._isOpen ) { |
| // effectively rapid-open the popup while leaving the screen intact |
| this._trigger( "beforeposition" ); |
| this._ui.container |
| .removeClass( "ui-selectmenu-hidden" ) |
| .offset( this._placementCoords( this._desiredCoords( undefined, undefined, "window" ) ) ); |
| |
| this._resizeScreen(); |
| this._resizeData = null; |
| this._orientationchangeInProgress = false; |
| } |
| }, |
| |
| _handleWindowResize: function( e ) { |
| if ( this._isOpen ) { |
| // Context popup close when Window resize event |
| if( this.positionTo !== "window" ) { |
| this.close(); |
| return false; |
| } |
| this._maybeRefreshTimeout(); |
| } |
| }, |
| |
| _handleWindowOrientationchange: function( e ) { |
| |
| if ( !this._orientationchangeInProgress ) { |
| // effectively rapid-close the popup while leaving the screen intact |
| this._ui.container |
| .addClass( "ui-selectmenu-hidden" ) |
| .removeAttr( "style" ); |
| |
| this._orientationchangeInProgress = true; |
| } |
| }, |
| |
| _create: function() { |
| var ui = { |
| screen: $( "<div class='ui-screen-hidden ui-popup-screen'></div>" ), |
| placeholder: $( "<div style='display: none;'><!-- placeholder --></div>" ), |
| container: $( "<div class='ui-popup-container ui-selectmenu-hidden'></div>" ), |
| arrow : $("<div class='ui-arrow'></div>") |
| }, |
| thisPage = this.element.closest( ".ui-page" ), |
| myId = this.element.attr( "id" ), |
| self = this; |
| |
| // We need to adjust the history option to be false if there's no AJAX nav. |
| // We can't do it in the option declarations because those are run before |
| // it is determined whether there shall be AJAX nav. |
| this.options.history = this.options.history && $.mobile.ajaxEnabled && $.mobile.hashListeningEnabled; |
| |
| if ( thisPage.length === 0 ) { |
| thisPage = $( "body" ); |
| } |
| |
| // define the container for navigation event bindings |
| // TODO this would be nice at the the mobile widget level |
| this.options.container = this.options.container || $.mobile.pageContainer; |
| |
| // Apply the proto |
| thisPage.append( ui.screen ); |
| ui.container.insertAfter( ui.screen ); |
| // Leave a placeholder where the element used to be |
| ui.placeholder.insertAfter( this.element ); |
| if ( myId ) { |
| ui.screen.attr( "id", myId + "-screen" ); |
| ui.container.attr( "id", myId + "-popup" ); |
| ui.placeholder.html( "<!-- placeholder for " + myId + " -->" ); |
| } |
| ui.container.append( this.element ); |
| ui.container.append( ui.arrow ); |
| // Add class to popup element |
| this.element.addClass( "ui-popup" ); |
| |
| // Define instance variables |
| $.extend( this, { |
| _page: thisPage, |
| _ui: ui, |
| _fallbackTransition: "", |
| _currentTransition: false, |
| _prereqs: null, |
| _isOpen: false, |
| _tolerance: null, |
| _resizeData: null, |
| _orientationchangeInProgress: false, |
| _globalHandlers: [ |
| { |
| src: $.mobile.$window, |
| handler: { |
| orientationchange: $.proxy( this, "_handleWindowOrientationchange" ), |
| resize: $.proxy( this, "_handleWindowResize" ), |
| keyup: $.proxy( this, "_handleWindowKeyUp" ) |
| } |
| } |
| ] |
| }); |
| |
| $.each( this.options, function( key, value ) { |
| // Cause initial options to be applied by their handler by temporarily setting the option to undefined |
| // - the handler then sets it to the initial value |
| self.options[ key ] = undefined; |
| self._setOption( key, value, true ); |
| }); |
| |
| ui.screen.bind( "vclick", $.proxy( this, "_eatEventAndClose" ) ); |
| |
| $.each( this._globalHandlers, function( idx, value ) { |
| value.src.bind( value.handler ); |
| }); |
| }, |
| |
| _applyTheme: function( dst, theme, prefix ) { |
| var classes = ( dst.attr( "class" ) || "").split( " " ), |
| alreadyAdded = true, |
| currentTheme = null, |
| matches, |
| themeStr = String( theme ); |
| |
| while ( classes.length > 0 ) { |
| currentTheme = classes.pop(); |
| matches = ( new RegExp( "^ui-" + prefix + "-([a-z])$" ) ).exec( currentTheme ); |
| if ( matches && matches.length > 1 ) { |
| currentTheme = matches[ 1 ]; |
| break; |
| } else { |
| currentTheme = null; |
| } |
| } |
| |
| if ( theme !== currentTheme ) { |
| dst.removeClass( "ui-" + prefix + "-" + currentTheme ); |
| if ( ! ( theme === null || theme === "none" ) ) { |
| dst.addClass( "ui-" + prefix + "-" + themeStr ); |
| } |
| } |
| }, |
| |
| _setTheme: function( value ) { |
| this._applyTheme( this.element, value, "body" ); |
| }, |
| |
| _setOverlayTheme: function( value ) { |
| this._applyTheme( this._ui.screen, value, "overlay" ); |
| |
| if ( this._isOpen ) { |
| this._ui.screen.addClass( "in" ); |
| } |
| }, |
| |
| _setShadow: function( value ) { |
| this.element.toggleClass( "ui-overlay-shadow", value ); |
| }, |
| |
| _setCorners: function( value ) { |
| this.element.toggleClass( "ui-corner-all", value ); |
| }, |
| |
| _applyTransition: function( value ) { |
| this._ui.container.removeClass( this._fallbackTransition ); |
| if ( value && value !== "none" ) { |
| this._fallbackTransition = $.mobile._maybeDegradeTransition( value ); |
| this._ui.container.addClass( this._fallbackTransition ); |
| } |
| }, |
| |
| _setTransition: function( value ) { |
| if ( !this._currentTransition ) { |
| this._applyTransition( value ); |
| } |
| }, |
| |
| _setTolerance: function( value ) { |
| var tol = { t: 5, r: 5, b: 5, l: 5 }; |
| |
| if ( value ) { |
| var ar = String( value ).split( "," ); |
| |
| $.each( ar, function( idx, val ) { ar[ idx ] = parseInt( val, 10 ); } ); |
| |
| switch( ar.length ) { |
| // All values are to be the same |
| case 1: |
| if ( !isNaN( ar[ 0 ] ) ) { |
| tol.t = tol.r = tol.b = tol.l = ar[ 0 ]; |
| } |
| break; |
| |
| // The first value denotes top/bottom tolerance, and the second value denotes left/right tolerance |
| case 2: |
| if ( !isNaN( ar[ 0 ] ) ) { |
| tol.t = tol.b = ar[ 0 ]; |
| } |
| if ( !isNaN( ar[ 1 ] ) ) { |
| tol.l = tol.r = ar[ 1 ]; |
| } |
| break; |
| |
| // The array contains values in the order top, right, bottom, left |
| case 4: |
| if ( !isNaN( ar[ 0 ] ) ) { |
| tol.t = ar[ 0 ]; |
| } |
| if ( !isNaN( ar[ 1 ] ) ) { |
| tol.r = ar[ 1 ]; |
| } |
| if ( !isNaN( ar[ 2 ] ) ) { |
| tol.b = ar[ 2 ]; |
| } |
| if ( !isNaN( ar[ 3 ] ) ) { |
| tol.l = ar[ 3 ]; |
| } |
| break; |
| |
| default: |
| break; |
| } |
| } |
| |
| this._tolerance = tol; |
| }, |
| |
| _setOption: function( key, value ) { |
| var exclusions, setter = "_set" + key.charAt( 0 ).toUpperCase() + key.slice( 1 ); |
| |
| if ( this[ setter ] !== undefined ) { |
| this[ setter ]( value ); |
| } |
| |
| // TODO REMOVE FOR 1.2.1 by moving them out to a default options object |
| exclusions = [ |
| "initSelector", |
| "closeLinkSelector", |
| "closeLinkEvents", |
| "navigateEvents", |
| "closeEvents", |
| "history", |
| "container" |
| ]; |
| |
| $.mobile.widget.prototype._setOption.apply( this, arguments ); |
| if ( $.inArray( key, exclusions ) === -1 ) { |
| // Record the option change in the options and in the DOM data-* attributes |
| this.element.attr( "data-" + ( $.mobile.ns || "" ) + ( key.replace( /([A-Z])/, "-$1" ).toLowerCase() ), value ); |
| } |
| }, |
| |
| // Try and center the overlay over the given coordinates |
| _placementCoords: function( desired ) { |
| // rectangle within which the popup must fit |
| var |
| winCoords = windowCoords(), |
| rc = { |
| x: this._tolerance.l, |
| y: winCoords.y + this._tolerance.t, |
| cx: winCoords.cx - this._tolerance.l - this._tolerance.r, |
| cy: winCoords.cy - this._tolerance.t - this._tolerance.b |
| }, |
| menuSize, ret, |
| linkOffset = $(this.link).offset(), |
| positionOffsets = [], |
| correctionValue = [0,0], |
| arrayIdx; |
| |
| // Clamp the width of the menu before grabbing its size |
| this._ui.container.css( "max-width", rc.cx ); |
| menuSize = { |
| cx: this._ui.container.outerWidth( true ), |
| cy: this._ui.container.outerHeight( true ) |
| }; |
| |
| // Center the menu over the desired coordinates, while not going outside |
| // the window tolerances. This will center wrt. the window if the popup is too large. |
| ret = { |
| x: fitSegmentInsideSegment( rc.cx, menuSize.cx, rc.x, desired.x ), |
| y: fitSegmentInsideSegment( rc.cy, menuSize.cy, rc.y, desired.y ) |
| }; |
| |
| // Make sure the top of the menu is visible |
| ret.y = Math.max( 0, ret.y ); |
| |
| // If the height of the menu is smaller than the height of the document |
| // align the bottom with the bottom of the document |
| |
| // fix for $( document ).height() bug in core 1.7.2. |
| var docEl = document.documentElement, docBody = document.body, |
| docHeight = Math.max( docEl.clientHeight, docBody.scrollHeight, docBody.offsetHeight, docEl.scrollHeight, docEl.offsetHeight ); |
| |
| ret.y -= Math.min( ret.y, Math.max( 0, ret.y + menuSize.cy - docHeight ) ); |
| |
| if ( this.positionTo !== "origin" ) { |
| return { left: ret.x, top: ret.y , arrowleft: 0 , arrowtop: 0}; |
| } else if( this.options.isHardwarePopup ) { |
| return { left: this._tolerance.l, top: $(window).height() - menuSize.cy - this._tolerance.b, arrowleft: 0 , arrowtop: 0 }; |
| } |
| |
| positionOffsets = [ linkOffset.left, |
| linkOffset.top, |
| docEl.clientHeight - ( linkOffset.top + $(this.link).height() ), |
| docEl.clientWidth - ( linkOffset.left + $(this.link).width() )]; |
| arrayIdx = positionOffsets.indexOf(Math.max.apply(window,positionOffsets)); |
| |
| switch( arrayIdx ) |
| { |
| case 0: |
| correctionValue = [ -$(this.link).width() , 0]; |
| arrowtop = ( linkOffset.top - ret.y ) + ( $(this.link).height() / 2 ) - parseInt( $(this._ui.arrow).css("border-width") ) ; |
| arrowleft = menuSize.cx; |
| $(this._ui.arrow).attr( "class", "" ) |
| .addClass( "ui-arrow left" ) |
| break; |
| case 1: |
| correctionValue = [ 0 , -(ret.y + menuSize.cy - linkOffset.top)]; |
| arrowtop = menuSize.cy - 2; |
| arrowleft = (linkOffset.left - ret.x + correctionValue[0]) + ( $(this.link).width() / 2 ) - parseInt( $(this._ui.arrow).css("border-width") ) / 2; |
| $(this._ui.arrow).attr( "class", "" ) |
| .addClass( "ui-arrow bottom" ); |
| break; |
| case 2: |
| correctionValue = [ 0 , ( linkOffset.top + $(this.link).height() - ret.y ) ]; |
| arrowtop = - parseInt( $(this._ui.arrow).css("border-width") ) * 2 + 1; |
| arrowleft = (linkOffset.left - ret.x + correctionValue[0]) + ( $(this.link).width() / 2 ) - parseInt( $(this._ui.arrow).css("border-width") ) / 2; |
| $(this._ui.arrow).attr( "class", "" ) |
| .addClass("ui-arrow top"); |
| break; |
| case 3: |
| correctionValue = [ ( menuSize.cx < $(this.link).width() ) ? ( $(this.link).width() / 2 ) + ( menuSize.cx / 2) : $(this.link).width() , 0]; |
| arrowtop = ( linkOffset.top - ret.y ) + ( $(this.link).height() / 2 ) - parseInt( $(this._ui.arrow).css("border-width") ) ; |
| arrowleft = - parseInt( $(this._ui.arrow).css("border-width") ) * 2; |
| $(this._ui.arrow).attr( "class", "" ) |
| .addClass("ui-arrow right"); |
| break; |
| } |
| |
| return { left: ret.x + correctionValue[0], top: ret.y + correctionValue[1] , arrowleft: arrowleft , arrowtop: arrowtop }; |
| }, |
| |
| _createPrereqs: function( screenPrereq, containerPrereq, whenDone ) { |
| var self = this, prereqs; |
| |
| // It is important to maintain both the local variable prereqs and self._prereqs. The local variable remains in |
| // the closure of the functions which call the callbacks passed in. The comparison between the local variable and |
| // self._prereqs is necessary, because once a function has been passed to .animationComplete() it will be called |
| // next time an animation completes, even if that's not the animation whose end the function was supposed to catch |
| // (for example, if an abort happens during the opening animation, the .animationComplete handler is not called for |
| // that animation anymore, but the handler remains attached, so it is called the next time the popup is opened |
| // - making it stale. Comparing the local variable prereqs to the widget-level variable self._prereqs ensures that |
| // callbacks triggered by a stale .animationComplete will be ignored. |
| |
| prereqs = { |
| screen: $.Deferred(), |
| container: $.Deferred() |
| }; |
| |
| prereqs.screen.then( function() { |
| if ( prereqs === self._prereqs ) { |
| screenPrereq(); |
| } |
| }); |
| |
| prereqs.container.then( function() { |
| if ( prereqs === self._prereqs ) { |
| containerPrereq(); |
| } |
| }); |
| |
| $.when( prereqs.screen, prereqs.container ).done( function() { |
| if ( prereqs === self._prereqs ) { |
| self._prereqs = null; |
| whenDone(); |
| } |
| }); |
| |
| self._prereqs = prereqs; |
| }, |
| |
| _animate: function( args ) { |
| // NOTE before removing the default animation of the screen |
| // this had an animate callback that would relove the deferred |
| // now the deferred is resolved immediately |
| // TODO remove the dependency on the screen deferred |
| this._ui.screen |
| .removeClass( args.classToRemove ) |
| .addClass( args.screenClassToAdd ); |
| |
| args.prereqs.screen.resolve(); |
| |
| if ( args.transition && args.transition !== "none" ) { |
| if ( args.applyTransition ) { |
| this._applyTransition( args.transition ); |
| } |
| this._ui.container |
| .animationComplete( $.proxy( args.prereqs.container, "resolve" ) ) |
| .addClass( args.containerClassToAdd ) |
| .removeClass( args.classToRemove ); |
| } else { |
| this._ui.container.removeClass( args.classToRemove ); |
| args.prereqs.container.resolve(); |
| } |
| }, |
| |
| // The desired coordinates passed in will be returned untouched if no reference element can be identified via |
| // desiredPosition.positionTo. Nevertheless, this function ensures that its return value always contains valid |
| // x and y coordinates by specifying the center middle of the window if the coordinates are absent. |
| _desiredCoords: function( x, y, positionTo ) { |
| var dst = null, offset, winCoords = windowCoords(); |
| |
| self.positionTo = positionTo; |
| |
| // Establish which element will serve as the reference |
| if ( positionTo && positionTo !== "origin" ) { |
| if ( positionTo === "window" ) { |
| x = winCoords.cx / 2 + winCoords.x; |
| y = winCoords.cy / 2 + winCoords.y; |
| } else { |
| try { |
| dst = $( positionTo ); |
| } catch( e ) { |
| dst = null; |
| } |
| if ( dst ) { |
| dst.filter( ":visible" ); |
| if ( dst.length === 0 ) { |
| dst = null; |
| } |
| } |
| } |
| } |
| |
| // If an element was found, center over it |
| if ( dst ) { |
| offset = dst.offset(); |
| x = offset.left + dst.outerWidth() / 2; |
| y = offset.top + dst.outerHeight() / 2; |
| } |
| |
| // Make sure x and y are valid numbers - center over the window |
| if ( $.type( x ) !== "number" || isNaN( x ) ) { |
| x = winCoords.cx / 2 + winCoords.x; |
| } |
| if ( $.type( y ) !== "number" || isNaN( y ) ) { |
| y = winCoords.cy / 2 + winCoords.y; |
| } |
| |
| return { x: x, y: y }; |
| }, |
| |
| _reposition: function() { |
| var self = this, |
| coords; |
| |
| if( self._isOpen |
| && self.link |
| && self.positionTo !== "window") { |
| coords = self._placementCoords( self._desiredCoords( $(self.link).offset().left + $(self.link).outerWidth() /2 , $(self.link).offset().top + $(self.link).outerHeight() /2 , self.positionTo || self.options.positionTo || "origin" ) ); |
| self._ui.container |
| .offset( { top : coords.top } ); |
| } |
| }, |
| |
| _openPrereqsComplete: function() { |
| var self = this; |
| |
| self._ui.container.addClass( "ui-popup-active" ); |
| self._isOpen = true; |
| self._resizeScreen(); |
| |
| // Android appears to trigger the animation complete before the popup |
| // is visible. Allowing the stack to unwind before applying focus prevents |
| // the "blue flash" of element focus in android 4.0 |
| setTimeout(function(){ |
| self._ui.container.attr( "tabindex", "0" ).focus(); |
| self._trigger( "afteropen" ); |
| self._reposition(); |
| }); |
| }, |
| |
| _open: function( options ) { |
| var coords, transition, |
| androidBlacklist = ( function() { |
| var w = window, |
| ua = navigator.userAgent, |
| // Rendering engine is Webkit, and capture major version |
| wkmatch = ua.match( /AppleWebKit\/([0-9\.]+)/ ), |
| wkversion = !!wkmatch && wkmatch[ 1 ], |
| androidmatch = ua.match( /Android (\d+(?:\.\d+))/ ), |
| andversion = !!androidmatch && androidmatch[ 1 ], |
| chromematch = ua.indexOf( "Chrome" ) > -1; |
| |
| // Platform is Android, WebKit version is greater than 534.13 ( Android 3.2.1 ) and not Chrome. |
| if( androidmatch !== null && andversion === "4.0" && wkversion && wkversion > 534.13 && !chromematch ) { |
| return true; |
| } |
| return false; |
| }()); |
| |
| // Make sure options is defined |
| options = ( options || {} ); |
| |
| // Copy out the transition, because we may be overwriting it later and we don't want to pass that change back to the caller |
| transition = options.transition || this.options.transition; |
| |
| // Give applications a chance to modify the contents of the container before it appears |
| this._trigger( "beforeposition" ); |
| |
| coords = this._placementCoords( this._desiredCoords( options.x, options.y, options.positionTo || this.options.positionTo || "origin" ) ); |
| |
| // Count down to triggering "popupafteropen" - we have two prerequisites: |
| // 1. The popup window animation completes (container()) |
| // 2. The screen opacity animation completes (screen()) |
| this._createPrereqs( |
| $.noop, |
| $.noop, |
| $.proxy( this, "_openPrereqsComplete" ) ); |
| |
| if ( transition ) { |
| this._currentTransition = transition; |
| this._applyTransition( transition ); |
| } else { |
| transition = this.options.transition; |
| } |
| |
| if ( !this.options.theme ) { |
| this._setTheme( this._page.jqmData( "theme" ) || $.mobile.getInheritedTheme( this._page, "c" ) ); |
| } |
| |
| this._ui.screen.removeClass( "ui-screen-hidden" ); |
| |
| this._ui.container |
| .removeClass( "ui-selectmenu-hidden" ) |
| .offset( coords ); |
| this._ui.arrow.css( { top : coords.arrowtop, left : coords.arrowleft } ); |
| if ( this.options.overlayTheme && androidBlacklist ) { |
| /* TODO: |
| The native browser on Android 4.0.X ("Ice Cream Sandwich") suffers from an issue where the popup overlay appears to be z-indexed |
| above the popup itself when certain other styles exist on the same page -- namely, any element set to `position: fixed` and certain |
| types of input. These issues are reminiscent of previously uncovered bugs in older versions of Android's native browser: |
| https://github.com/scottjehl/Device-Bugs/issues/3 |
| |
| This fix closes the following bugs ( I use "closes" with reluctance, and stress that this issue should be revisited as soon as possible ): |
| |
| https://github.com/jquery/jquery-mobile/issues/4816 |
| https://github.com/jquery/jquery-mobile/issues/4844 |
| https://github.com/jquery/jquery-mobile/issues/4874 |
| */ |
| |
| // TODO sort out why this._page isn't working |
| this.element.closest( ".ui-page" ).addClass( "ui-popup-open" ); |
| } |
| this._animate({ |
| additionalCondition: true, |
| transition: transition, |
| classToRemove: "", |
| screenClassToAdd: "in", |
| containerClassToAdd: "in", |
| applyTransition: false, |
| prereqs: this._prereqs |
| }); |
| }, |
| |
| _closePrereqScreen: function() { |
| this._ui.screen |
| .removeClass( "out" ) |
| .addClass( "ui-screen-hidden" ); |
| }, |
| |
| _closePrereqContainer: function() { |
| this._ui.container |
| .removeClass( "reverse out" ) |
| .addClass( "ui-selectmenu-hidden" ) |
| .removeAttr( "style" ); |
| }, |
| |
| _closePrereqsDone: function() { |
| var self = this, opts = self.options; |
| |
| self._ui.container.removeAttr( "tabindex" ); |
| |
| // remove nav bindings if they are still present |
| opts.container.unbind( opts.closeEvents ); |
| |
| // unbind click handlers added when history is disabled |
| self.element.undelegate( opts.closeLinkSelector, opts.closeLinkEvents ); |
| |
| // remove the global mutex for popups |
| $.mobile.popup.active = undefined; |
| |
| // alert users that the popup is closed |
| self._trigger( "afterclose" ); |
| }, |
| |
| _close: function( immediate ) { |
| this._ui.container.removeClass( "ui-popup-active" ); |
| this._page.removeClass( "ui-popup-open" ); |
| |
| this._isOpen = false; |
| |
| // IME hide when popup is closed |
| this.element.find("input").blur(); |
| |
| // Count down to triggering "popupafterclose" - we have two prerequisites: |
| // 1. The popup window reverse animation completes (container()) |
| // 2. The screen opacity animation completes (screen()) |
| this._createPrereqs( |
| $.proxy( this, "_closePrereqScreen" ), |
| $.proxy( this, "_closePrereqContainer" ), |
| $.proxy( this, "_closePrereqsDone" ) ); |
| |
| this._animate( { |
| additionalCondition: this._ui.screen.hasClass( "in" ), |
| transition: ( immediate ? "none" : ( this._currentTransition || this.options.transition ) ), |
| classToRemove: "in", |
| screenClassToAdd: "out", |
| containerClassToAdd: "reverse out", |
| applyTransition: true, |
| prereqs: this._prereqs |
| }); |
| }, |
| |
| _destroy: function() { |
| var self = this; |
| |
| // hide and remove bindings |
| self._close(); |
| |
| // Put the element back to where the placeholder was and remove the "ui-popup" class |
| self._setTheme( "none" ); |
| self.element |
| .insertAfter( self._ui.placeholder ) |
| .removeClass( "ui-popup ui-overlay-shadow ui-corner-all" ); |
| self._ui.screen.remove(); |
| self._ui.container.remove(); |
| self._ui.placeholder.remove(); |
| |
| // Unbind handlers that were bound to elements outside self.element (the window, in self case) |
| // window history back is call "_destroy method" |
| // if this method is called, all kind of window resize event has been unbind |
| /* |
| $.each( self._globalHandlers, function( idx, oneSrc ) { |
| $.each( oneSrc.handler, function( eventType, handler ) { |
| oneSrc.src.unbind( eventType, handler ); |
| }); |
| }); |
| */ |
| }, |
| |
| _closePopup: function( e, data ) { |
| var parsedDst, toUrl; |
| |
| if ( e.type === "pagebeforechange" && data ) { |
| if ( typeof data.toPage === "string" ) { |
| parsedDst = data.toPage; |
| } else { |
| parsedDst = data.toPage.jqmData( "url" ); |
| } |
| parsedDst = $.mobile.path.parseUrl( parsedDst ); |
| toUrl = parsedDst.pathname + parsedDst.search + parsedDst.hash; |
| |
| if ( this._myUrl !== toUrl ) { |
| this.options.container.unbind( this.options.closeEvents ); |
| this._close( true ); |
| } else { |
| this._close(); |
| } |
| return; // skip normal close |
| } |
| |
| this._close(); |
| }, |
| |
| // any navigation event after a popup is opened should close the popup |
| // NOTE the pagebeforechange is bound to catch navigation events that don't |
| // alter the url (eg, dialogs from popups) |
| _bindContainerClose: function() { |
| var self = this; |
| |
| self.options.container |
| .one( self.options.closeEvents, $.proxy( self, "_closePopup" ) ); |
| }, |
| |
| // TODO no clear deliniation of what should be here and |
| // what should be in _open. Seems to be "visual" vs "history" for now |
| open: function( options ) { |
| var self = this, opts = this.options, url, hashkey, activePage, currentIsDialog, hasHash, urlHistory; |
| // self.link = ( $(event.target).attr('data-role') === 'button') ? event.target : $(event.target).closest('[data-role="button"]')[0]; |
| // make sure open is idempotent |
| if( $.mobile.popup.active ) { |
| return; |
| } |
| // set the global popup mutex |
| $.mobile.popup.active = this; |
| if( !options ) { |
| options = []; |
| } |
| |
| if ( !options.link ) { |
| if ( !event ) { |
| self.positionTo = "window"; |
| } else { |
| self.link = ( $(event.target).closest('a')[0] || $(event.target).closest('div')[0] ); |
| } |
| } else { |
| self.link = options.link; |
| } |
| if ( event ) { |
| self.positionTo = ( options != null && options.positionTo != null ) ? options.positionTo : "origin"; |
| } |
| |
| if ( $(self.link).jqmData("position-to") !== "window" |
| && self.positionTo !== "window" ) { |
| |
| $(self.element).addClass("ui-ctxpopup"); |
| $(self._ui.container).removeClass("ui-popup-container") |
| .addClass("ui-ctxpopup-container"); |
| |
| if( self.positionTo !== "origin" ) { |
| $(self._ui.arrow).hide(); |
| } else { |
| $(self._ui.arrow).show(); |
| } |
| } else { |
| $(self._ui.arrow).hide(); |
| // apply opacity back screen |
| this._setOverlayTheme( "dim" ); |
| } |
| if( !options.x |
| && self.positionTo === "origin" |
| && self.link ) { |
| options.x = $(self.link).offset().left + $(self.link).outerWidth() / 2; |
| } |
| if( !options.y |
| && self.positionTo === "origin" |
| && self.link ) { |
| options.y = $(self.link).offset().top + $(self.link).outerHeight() / 2; |
| } |
| |
| // Hadeware key style popup |
| if( $(self.element).hasClass( "ui-ctxpopup-optionmenu" ) ){ |
| self.options.isHardwarePopup = true; |
| $( self._ui.arrow).hide(); |
| } |
| |
| // if history alteration is disabled close on navigate events |
| // and leave the url as is |
| if( !( opts.history ) ) { |
| self._open( options ); |
| self._bindContainerClose(); |
| |
| // When histoy is disabled we have to grab the data-rel |
| // back link clicks so we can close the popup instead of |
| // relying on history to do it for us |
| self.element |
| .delegate( opts.closeLinkSelector, opts.closeLinkEvents, function( e ) { |
| self._close(); |
| |
| // NOTE prevent the browser and navigation handlers from |
| // working with the link's rel=back. This may cause |
| // issues for developers expecting the event to bubble |
| return false; |
| }); |
| |
| return; |
| } |
| |
| // cache some values for min/readability |
| hashkey = $.mobile.dialogHashKey; |
| activePage = $.mobile.activePage; |
| currentIsDialog = activePage.is( ".ui-dialog" ); |
| // Set active page url |
| this._myUrl = url = urlHistory.getActive().url; |
| // |
| url = $.mobile.urlHistory.getActive().url; |
| hasHash = ( url.indexOf( hashkey ) > -1 ) && !currentIsDialog; |
| urlHistory = $.mobile.urlHistory; |
| |
| if ( hasHash ) { |
| self._open( options ); |
| self._bindContainerClose(); |
| return; |
| } |
| |
| // if the current url has no dialog hash key proceed as normal |
| // otherwise, if the page is a dialog simply tack on the hash key |
| if ( url.indexOf( hashkey ) === -1 && !currentIsDialog ){ |
| url = url + hashkey; |
| } else { |
| url = $.mobile.path.parseLocation().hash + hashkey; |
| } |
| |
| // Tack on an extra hashkey if this is the first page and we've just reconstructed the initial hash |
| if ( urlHistory.activeIndex === 0 && url === urlHistory.initialDst ) { |
| url += hashkey; |
| } |
| |
| // swallow the the initial navigation event, and bind for the next |
| opts.container.one( opts.navigateEvents, function( e ) { |
| e.preventDefault(); |
| self._open( options ); |
| self._bindContainerClose(); |
| }); |
| |
| urlHistory.ignoreNextHashChange = currentIsDialog; |
| |
| // Gotta love methods with 1mm args :( |
| urlHistory.addNew( url, undefined, undefined, undefined, "dialog" ); |
| |
| // set the new url with (or without) the new dialog hash key |
| $.mobile.path.set( url ); |
| }, |
| |
| close: function() { |
| // make sure close is idempotent |
| if( !$.mobile.popup.active ){ |
| return; |
| } |
| |
| if( this.options.history ) { |
| $.mobile.back(); |
| } else { |
| this._close(); |
| } |
| } |
| }); |
| |
| |
| // TODO this can be moved inside the widget |
| $.mobile.popup.handleLink = function( $link ) { |
| var closestPage = $link.closest( ":jqmData(role='page')" ), |
| scope = ( ( closestPage.length === 0 ) ? $( "body" ) : closestPage ), |
| // NOTE make sure to get only the hash, ie7 (wp7) return the absolute href |
| // in this case ruining the element selection |
| popup = $( $.mobile.path.parseUrl($link.attr( "href" )).hash, scope[0] ), |
| offset; |
| |
| if ( popup.data( "popup" ) ) { |
| offset = $link.offset(); |
| popup.popup( "open", { |
| x: offset.left + $link.outerWidth() / 2, |
| y: offset.top + $link.outerHeight() / 2, |
| transition: $.mobile.getAttrFixed( $link[0], "data-" + $.mobile.ns + "transition" ), |
| positionTo: $.mobile.getAttrFixed( $link[0], "data-" + $.mobile.ns + "position-to" ), |
| link: $link |
| }); |
| } |
| |
| //remove after delay |
| setTimeout( function() { |
| $link.removeClass( $.mobile.activeBtnClass ); |
| }, 300 ); |
| }; |
| |
| // TODO move inside _create |
| $.mobile.$document.bind( "pagebeforechange", function( e, data ) { |
| if ( data.options.role === "popup" ) { |
| $.mobile.popup.handleLink( data.options.link ); |
| e.preventDefault(); |
| } |
| }); |
| |
| //delegate self-init widgets |
| $.delegateSelfInitWithSingleSelector( $.mobile.popup, true ); |
| |
| })( jQuery ); |
| |
| (function( $ ) { |
| var meta = $( "meta[name=viewport]" ), |
| initialContent = meta.attr( "content" ), |
| disabledZoom = initialContent + ",maximum-scale=1, user-scalable=no", |
| enabledZoom = initialContent + ",maximum-scale=10, user-scalable=yes", |
| disabledInitially = /(user-scalable[\s]*=[\s]*no)|(maximum-scale[\s]*=[\s]*1)[$,\s]/.test( initialContent ); |
| |
| $.mobile.zoom = $.extend( {}, { |
| enabled: !disabledInitially, |
| locked: false, |
| disable: function( lock ) { |
| if ( !disabledInitially && !$.mobile.zoom.locked ) { |
| meta.attr( "content", disabledZoom ); |
| $.mobile.zoom.enabled = false; |
| $.mobile.zoom.locked = lock || false; |
| } |
| }, |
| enable: function( unlock ) { |
| if ( !disabledInitially && ( !$.mobile.zoom.locked || unlock === true ) ) { |
| meta.attr( "content", enabledZoom ); |
| $.mobile.zoom.enabled = true; |
| $.mobile.zoom.locked = false; |
| } |
| }, |
| restore: function() { |
| if ( !disabledInitially ) { |
| meta.attr( "content", initialContent ); |
| $.mobile.zoom.enabled = true; |
| } |
| } |
| }); |
| |
| }( jQuery )); |
| |
| (function( $, undefined ) { |
| |
| $.widget( "mobile.textinput", $.mobile.widget, { |
| options: { |
| theme: null, |
| // This option defaults to true on iOS devices. |
| preventFocusZoom: /iPhone|iPad|iPod/.test( navigator.platform ) && navigator.userAgent.indexOf( "AppleWebKit" ) > -1, |
| initSelector: "input[type='text'], input[type='number'], :jqmData(type='number'), input[type='password'], input[type='email'], input[type='url'], input[type='tel'], textarea, input[type='time'], input[type='date'], input[type='month'], input[type='week'], input[type='datetime'], input[type='datetime-local'], input[type='color'], input:not([type])", |
| disabled: false |
| }, |
| |
| _create: function() { |
| |
| var self = this, |
| input = this.element, |
| o = this.options, |
| theme = o.theme || $.mobile.getInheritedTheme( this.element, "c" ), |
| themeclass = " ui-body-" + theme, |
| mini = $.mobile.getAttrFixed( input[0], "data-" + $.mobile.ns + "mini" ) === true, |
| miniclass = mini ? " ui-mini" : "", |
| focusedEl, clearbtn; |
| |
| function toggleClear() { |
| setTimeout( function() { |
| clearbtn.toggleClass( "ui-input-clear-hidden", !input.val() ); |
| }, 0 ); |
| } |
| |
| $( "label[for='" + input.attr( "id" ) + "']" ).addClass( "ui-input-text" ); |
| |
| focusedEl = input.addClass("ui-input-text ui-body-"+ theme ); |
| |
| switch ( input.attr( "type" ) ) { |
| case "text": |
| case "password": |
| case "number": |
| case "email": |
| case "url": |
| case "tel": |
| input.attr( { "role" : "textbox", "aria-label" : "Keyboard opened" } ); |
| break; |
| default: |
| if ( input.prop( "tagName" ).toLowerCase() === "textarea" ) { |
| input.attr( { "role" : "textbox", "aria-label" : "Keyboard opened" } ); |
| } |
| } |
| |
| // XXX: Temporary workaround for issue 785 (Apple bug 8910589). |
| // Turn off autocorrect and autocomplete on non-iOS 5 devices |
| // since the popup they use can't be dismissed by the user. Note |
| // that we test for the presence of the feature by looking for |
| // the autocorrect property on the input element. We currently |
| // have no test for iOS 5 or newer so we're temporarily using |
| // the touchOverflow support flag for jQM 1.0. Yes, I feel dirty. - jblas |
| if ( typeof input[0].autocorrect !== "undefined" && !$.support.touchOverflow ) { |
| // Set the attribute instead of the property just in case there |
| // is code that attempts to make modifications via HTML. |
| input[0].setAttribute( "autocorrect", "off" ); |
| input[0].setAttribute( "autocomplete", "off" ); |
| } |
| |
| input.focus(function() { |
| focusedEl.addClass( $.mobile.focusClass ); |
| }) |
| .blur(function() { |
| focusedEl.removeClass( $.mobile.focusClass ); |
| }) |
| // In many situations, iOS will zoom into the select upon tap, this prevents that from happening |
| .bind( "focus", function() { |
| if ( o.preventFocusZoom ) { |
| $.mobile.zoom.disable( true ); |
| } |
| }) |
| .bind( "blur", function() { |
| if ( o.preventFocusZoom ) { |
| $.mobile.zoom.enable( true ); |
| } |
| }); |
| |
| // Autogrow |
| if ( input.is( "textarea" ) ) { |
| var extraLineHeight = 15, |
| keyupTimeoutBuffer = 100, |
| keyupTimeout; |
| |
| this._keyup = function() { |
| var scrollHeight = input[ 0 ].scrollHeight, |
| clientHeight = input[ 0 ].clientHeight; |
| |
| if ( clientHeight < scrollHeight && window.innerHeight / 2 > scrollHeight ) { |
| input.height(scrollHeight + extraLineHeight); |
| } |
| }; |
| |
| input.keyup(function() { |
| clearTimeout( keyupTimeout ); |
| keyupTimeout = setTimeout( self._keyup, keyupTimeoutBuffer ); |
| }); |
| |
| // binding to pagechange here ensures that for pages loaded via |
| // ajax the height is recalculated without user input |
| this._on( $.mobile.$document, {"pagechange": "_keyup" }); |
| |
| // Issue 509: the browser is not providing scrollHeight properly until the styles load |
| if ( $.trim( input.val() ) ) { |
| // bind to the window load to make sure the height is calculated based on BOTH |
| // the DOM and CSS |
| this._on( $.mobile.$window, {"load": "_keyup"}); |
| } |
| } |
| if ( input.attr( "disabled" ) ) { |
| this.disable(); |
| } |
| }, |
| |
| disable: function() { |
| var $el; |
| if ( this.element.attr( "disabled", true ) ) { |
| $el = this.element; |
| } else { |
| return; |
| } |
| $el.addClass( "ui-disabled" ); |
| return this._setOption( "disabled", true ); |
| }, |
| |
| enable: function() { |
| var $el; |
| |
| // TODO using more than one line of code is acceptable ;) |
| if ( this.element.attr( "disabled", false ) ) { |
| $el = this.element; |
| } else { |
| return; |
| } |
| $el.removeClass( "ui-disabled" ); |
| return this._setOption( "disabled", false ); |
| } |
| }); |
| |
| //auto self-init widgets |
| $.mobile.$document.bind( "pagecreate create", function( e ) { |
| $.mobile.textinput.prototype.enhanceWithin( e.target, true ); |
| }); |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| $.mobile.listview.prototype.options.filter = false; |
| $.mobile.listview.prototype.options.filterPlaceholder = ""; |
| $.mobile.listview.prototype.options.filterTheme = "c"; |
| // TODO rename callback/deprecate and default to the item itself as the first argument |
| var defaultFilterCallback = function( text, searchValue, item ) { |
| return text.toString().toLowerCase().indexOf( searchValue ) === -1; |
| }; |
| |
| $.mobile.listview.prototype.options.filterCallback = defaultFilterCallback; |
| |
| $.mobile.$document.delegate( ":jqmData(role='listview')", "listviewcreate", function() { |
| |
| var list = $( this ), |
| listview = list.data( "listview" ); |
| |
| if ( !listview.options.filter ) { |
| return; |
| } |
| |
| var wrapper = $( "<form>", { |
| "class": "ui-listview-filter ui-bar-" + listview.options.filterTheme, |
| "role": "search" |
| }), |
| search = $( "<input>", { |
| placeholder: listview.options.filterPlaceholder |
| }) |
| .attr( "data-" + $.mobile.ns + "type", "search" ) |
| .jqmData( "lastval", "" ) |
| .bind( "keyup change", function() { |
| |
| var $this = $( this ), |
| val = this.value.toLowerCase(), |
| listItems = null, |
| lastval = $this.jqmData( "lastval" ) + "", |
| childItems = false, |
| itemtext = "", |
| item, |
| // Check if a custom filter callback applies |
| isCustomFilterCallback = listview.options.filterCallback !== defaultFilterCallback; |
| |
| listview._trigger( "beforefilter", "beforefilter", { input: this } ); |
| |
| // Change val as lastval for next execution |
| $this.jqmData( "lastval" , val ); |
| if ( isCustomFilterCallback || val.length < lastval.length || val.indexOf( lastval ) !== 0 ) { |
| |
| // Custom filter callback applies or removed chars or pasted something totally different, check all items |
| listItems = list.children(); |
| } else { |
| |
| // Only chars added, not removed, only use visible subset |
| listItems = list.children( ":not(.ui-screen-hidden)" ); |
| } |
| |
| if ( val ) { |
| |
| // This handles hiding regular rows without the text we search for |
| // and any list dividers without regular rows shown under it |
| |
| for ( var i = listItems.length - 1; i >= 0; i-- ) { |
| item = $( listItems[ i ] ); |
| itemtext = $.mobile.getAttrFixed( item[0], "data-" + $.mobile.ns + "filtertext" ) || item.text(); |
| |
| if ( item.is( "li:jqmData(role=list-divider)" ) ) { |
| |
| item.toggleClass( "ui-filter-hidequeue" , !childItems ); |
| |
| // New bucket! |
| childItems = false; |
| |
| } else if ( listview.options.filterCallback( itemtext, val, item ) ) { |
| |
| //mark to be hidden |
| item.toggleClass( "ui-filter-hidequeue" , true ); |
| } else { |
| |
| // There's a shown item in the bucket |
| childItems = true; |
| } |
| } |
| |
| // Show items, not marked to be hidden |
| listItems |
| .filter( ":not(.ui-filter-hidequeue)" ) |
| .toggleClass( "ui-screen-hidden", false ); |
| |
| // Hide items, marked to be hidden |
| listItems |
| .filter( ".ui-filter-hidequeue" ) |
| .toggleClass( "ui-screen-hidden", true ) |
| .toggleClass( "ui-filter-hidequeue", false ); |
| |
| } else { |
| |
| //filtervalue is empty => show all |
| listItems.toggleClass( "ui-screen-hidden", false ); |
| } |
| listview._refreshCorners(); |
| }) |
| .appendTo( wrapper ) |
| .textinput(); |
| |
| if ( listview.options.inset ) { |
| wrapper.addClass( "ui-listview-filter-inset" ); |
| } |
| |
| wrapper.bind( "submit", function() { |
| return false; |
| }) |
| .insertBefore( list ); |
| }); |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| $.widget( "mobile.slider", $.mobile.widget, { |
| widgetEventPrefix: "slide", |
| |
| options: { |
| theme: null, |
| trackTheme: null, |
| disabled: false, |
| initSelector: "input[type='range'], :jqmData(type='range'), :jqmData(role='slider')", |
| mini: false |
| }, |
| |
| _create: function() { |
| |
| // TODO: Each of these should have comments explain what they're for |
| var self = this, |
| |
| control = this.element, |
| |
| parentTheme = $.mobile.getInheritedTheme( control, "c" ), |
| |
| theme = this.options.theme || parentTheme, |
| |
| trackTheme = this.options.trackTheme || parentTheme, |
| |
| cType = control[ 0 ].nodeName.toLowerCase(), |
| |
| selectClass = ( cType === "select" ) ? "ui-slider-switch" : "", |
| |
| controlID = control.attr( "id" ), |
| |
| $label = $( "[for='" + controlID + "']" ), |
| |
| labelID = $label.attr( "id" ) || controlID + "-label", |
| |
| label = $label.attr( "id", labelID ), |
| |
| val = function() { |
| return cType === "input" ? parseFloat( control.val() ) : control[0].selectedIndex; |
| }, |
| |
| min = cType === "input" ? parseFloat( control.attr( "min" ) ) : 0, |
| |
| max = cType === "input" ? parseFloat( control.attr( "max" ) ) : control.find( "option" ).length-1, |
| |
| step = window.parseFloat( control.attr( "step" ) || 1 ), |
| |
| inlineClass = ( this.options.inline || $.mobile.getAttrFixed( control[0], "data-" + $.mobile.ns + "inline" ) === true ) ? " ui-slider-inline" : "", |
| |
| miniClass = ( this.options.mini || $.mobile.getAttrFixed( control[0], "data-" + $.mobile.ns + "min" ) ) ? " ui-slider-mini" : "", |
| |
| |
| domHandle = document.createElement( 'a' ), |
| handle = $( domHandle ), |
| domSlider = document.createElement( 'div' ), |
| slider = $( domSlider ), |
| |
| valuebg = $.mobile.getAttrFixed( control[0], "data-" + $.mobile.ns + "highlight" ) !== false && cType !== "select" ? (function() { |
| var bg = document.createElement('div'); |
| bg.className = 'ui-slider-bg ' + $.mobile.activeBtnClass + ' ui-btn-corner-all'; |
| return $( bg ).prependTo( slider ); |
| })() : false, |
| |
| options; |
| |
| this._type = cType; |
| |
| domHandle.setAttribute( 'href', "#" ); |
| domSlider.setAttribute('role','application'); |
| domSlider.className = ['ui-slider ',selectClass," ui-btn-down-",trackTheme,' ui-btn-corner-all', inlineClass, miniClass].join( "" ); |
| domHandle.className = 'ui-slider-handle'; |
| domSlider.appendChild( domHandle ); |
| if ( $( control ).find( "option" ).length && $( control ).find( "option" ).text() === "" ) { |
| $( domSlider ).addClass( "ui-toggle-switch" ); |
| } |
| if( val() === "1" ) { |
| $( domHandle ).addClass( "ui-toggle-on" ); |
| } else { |
| $( domHandle ).addClass( "ui-toggle-off" ); |
| } |
| handle.buttonMarkup({ corners: true, theme: theme, shadow: true }) |
| .attr({ |
| "role": "slider", |
| "aria-valuemin": min, |
| "aria-valuemax": max, |
| "aria-valuenow": val(), |
| "aria-valuetext": val(), |
| "title": val(), |
| "aria-labelledby": labelID |
| }); |
| |
| $.extend( this, { |
| slider: slider, |
| handle: handle, |
| valuebg: valuebg, |
| dragging: false, |
| beforeStart: null, |
| userModified: false, |
| mouseMoved: false |
| }); |
| |
| if ( cType === "select" ) { |
| var wrapper = document.createElement('div'); |
| wrapper.className = 'ui-slider-inneroffset'; |
| |
| for ( var j = 0,length = domSlider.childNodes.length;j < length;j++ ) { |
| wrapper.appendChild( domSlider.childNodes[j] ); |
| } |
| |
| domSlider.appendChild( wrapper ); |
| |
| // slider.wrapInner( "<div class='ui-slider-inneroffset'></div>" ); |
| |
| // make the handle move with a smooth transition |
| handle.addClass( "ui-slider-handle-snapping" ); |
| |
| options = control.find( "option" ); |
| |
| for ( var i = 0, optionsCount = options.length; i < optionsCount; i++ ) { |
| var side = !i ? "b" : "a", |
| sliderTheme = !i ? " ui-btn-down-" + trackTheme : ( " " + $.mobile.activeBtnClass ), |
| sliderLabel = document.createElement( 'div' ), |
| sliderImg = document.createElement( 'span' ); |
| |
| sliderImg.className = ['ui-slider-label ui-slider-label-',side,sliderTheme," ui-btn-corner-all"].join( "" ); |
| sliderImg.setAttribute('role','img'); |
| sliderImg.appendChild( document.createTextNode( options[i].innerHTML ) ); |
| $( sliderImg ).html() ? $( sliderImg ).html( $( sliderImg ).text() ) : $( sliderImg ).html(); |
| $(sliderImg).prependTo( slider ); |
| } |
| |
| self._labels = $( ".ui-slider-label", slider ); |
| |
| } |
| |
| label.addClass( "ui-slider" ); |
| |
| // monitor the input for updated values |
| control.addClass( cType === "input" ? "ui-slider-input" : "ui-slider-switch" ) |
| .change(function() { |
| // if the user dragged the handle, the "change" event was triggered from inside refresh(); don't call refresh() again |
| if ( !self.mouseMoved ) { |
| self.refresh( val(), true ); |
| } |
| }) |
| .keyup(function() { // necessary? |
| self.refresh( val(), true, true ); |
| }) |
| .blur(function() { |
| self.refresh( val(), true ); |
| }); |
| |
| this._preventDocumentDrag = function( event ) { |
| // NOTE: we don't do this in refresh because we still want to |
| // support programmatic alteration of disabled inputs |
| var et = $(event.target); |
| if ( self.dragging && !self.options.disabled && ( ( et.parents( ".ui-slider" ).is( ".ui-toggle-switch" ) && et.parents( ".ui-slider-handle" ).is( ".ui-btn-hover-s" ) ) || ( !$( self.element ).siblings( ".ui-slider" ).is( ".ui-toggle-switch"))) ) { |
| |
| // self.mouseMoved must be updated before refresh() because it will be used in the control "change" event |
| self.mouseMoved = true; |
| |
| if ( cType === "select" ) { |
| // make the handle move in sync with the mouse |
| handle.removeClass( "ui-slider-handle-snapping" ); |
| } |
| |
| self.refresh( event ); |
| |
| // only after refresh() you can calculate self.userModified |
| self.userModified = self.beforeStart !== control[0].selectedIndex; |
| return false; |
| } |
| } |
| |
| this._on( $.mobile.$document, { "vmousemove": this._preventDocumentDrag }); |
| |
| // it appears the clicking the up and down buttons in chrome on |
| // range/number inputs doesn't trigger a change until the field is |
| // blurred. Here we check thif the value has changed and refresh |
| control.bind( "vmouseup", $.proxy( self._checkedRefresh, self)); |
| |
| slider.bind( "vmousedown", function( event ) { |
| // NOTE: we don't do this in refresh because we still want to |
| // support programmatic alteration of disabled inputs |
| if ( self.options.disabled ) { |
| return false; |
| } |
| |
| self.dragging = true; |
| self.userModified = false; |
| self.mouseMoved = false; |
| |
| if ( cType === "select" ) { |
| self.beforeStart = control[0].selectedIndex; |
| } |
| |
| self.refresh( event ); |
| self._trigger( "start" ); |
| return false; |
| }) |
| .bind( "vclick", false ); |
| |
| this._sliderMouseUp = function() { |
| if ( self.dragging ) { |
| self.dragging = false; |
| |
| if ( cType === "select") { |
| // make the handle move with a smooth transition |
| handle.addClass( "ui-slider-handle-snapping" ); |
| |
| if ( self.mouseMoved ) { |
| // this is a drag, change the value only if user dragged enough |
| if ( self.userModified ) { |
| self.refresh( self.beforeStart === 0 ? 1 : 0 ); |
| } |
| else { |
| self.refresh( self.beforeStart ); |
| } |
| } |
| else { |
| // this is just a click, change the value |
| self.refresh( self.beforeStart === 0 ? 1 : 0 ); |
| } |
| } |
| |
| self.mouseMoved = false; |
| self._trigger( "stop" ); |
| return false; |
| } |
| }; |
| |
| this._on( slider.add( document ), { "vmouseup": this._sliderMouseUp }); |
| slider.insertAfter( control ); |
| |
| // Only add focus class to toggle switch, sliders get it automatically from ui-btn |
| if ( cType === 'select' ) { |
| this.handle.bind({ |
| focus: function() { |
| slider.addClass( $.mobile.focusClass ); |
| }, |
| |
| blur: function() { |
| slider.removeClass( $.mobile.focusClass ); |
| } |
| }); |
| } |
| |
| this.handle.bind({ |
| // NOTE force focus on handle |
| vmousedown: function() { |
| $( this ).focus(); |
| }, |
| |
| vclick: false, |
| |
| keydown: function( event ) { |
| var index = val(); |
| |
| if ( self.options.disabled ) { |
| return; |
| } |
| |
| // In all cases prevent the default and mark the handle as active |
| switch ( event.keyCode ) { |
| case $.mobile.keyCode.HOME: |
| case $.mobile.keyCode.END: |
| case $.mobile.keyCode.PAGE_UP: |
| case $.mobile.keyCode.PAGE_DOWN: |
| case $.mobile.keyCode.UP: |
| case $.mobile.keyCode.RIGHT: |
| case $.mobile.keyCode.DOWN: |
| case $.mobile.keyCode.LEFT: |
| event.preventDefault(); |
| |
| if ( !self._keySliding ) { |
| self._keySliding = true; |
| $( this ).addClass( "ui-state-active" ); |
| } |
| break; |
| } |
| |
| // move the slider according to the keypress |
| switch ( event.keyCode ) { |
| case $.mobile.keyCode.HOME: |
| self.refresh( min ); |
| break; |
| case $.mobile.keyCode.END: |
| self.refresh( max ); |
| break; |
| case $.mobile.keyCode.PAGE_UP: |
| case $.mobile.keyCode.UP: |
| case $.mobile.keyCode.RIGHT: |
| self.refresh( index + step ); |
| break; |
| case $.mobile.keyCode.PAGE_DOWN: |
| case $.mobile.keyCode.DOWN: |
| case $.mobile.keyCode.LEFT: |
| self.refresh( index - step ); |
| break; |
| } |
| }, // remove active mark |
| |
| keyup: function( event ) { |
| if ( self._keySliding ) { |
| self._keySliding = false; |
| $( this ).removeClass( "ui-state-active" ); |
| } |
| } |
| }); |
| |
| this.refresh( undefined, undefined, true ); |
| }, |
| |
| _checkedRefresh: function() { |
| if( this.value != this._value() ){ |
| this.refresh( this._value() ); |
| } |
| }, |
| |
| _value: function() { |
| return this._type === "input" ? |
| parseFloat( this.element.val() ) : this.element[0].selectedIndex; |
| }, |
| |
| refresh: function( val, isfromControl, preventInputUpdate ) { |
| |
| // NOTE: we don't return here because we want to support programmatic |
| // alteration of the input value, which should still update the slider |
| if ( this.options.disabled || this.element.attr('disabled')) { |
| this.disable(); |
| } |
| |
| // set the stored value for comparison later |
| this.value = this._value(); |
| |
| var control = this.element, percent, |
| cType = control[0].nodeName.toLowerCase(), |
| min = cType === "input" ? parseFloat( control.attr( "min" ) ) : 0, |
| max = cType === "input" ? parseFloat( control.attr( "max" ) ) : control.find( "option" ).length - 1, |
| step = ( cType === "input" && parseFloat( control.attr( "step" ) ) > 0 ) ? parseFloat( control.attr( "step" ) ) : 1; |
| |
| if ( typeof val === "object" ) { |
| var data = val, |
| // a slight tolerance helped get to the ends of the slider |
| tol = 8; |
| if ( !this.dragging || |
| data.pageX < this.slider.offset().left - tol || |
| data.pageX > this.slider.offset().left + this.slider.width() + tol ) { |
| return; |
| } |
| percent = Math.round( ( ( data.pageX - this.slider.offset().left ) / this.slider.width() ) * 100 ); |
| } else { |
| if ( val == null ) { |
| val = cType === "input" ? parseFloat( control.val() || 0 ) : control[0].selectedIndex; |
| } |
| percent = ( parseFloat( val ) - min ) / ( max - min ) * 100; |
| } |
| |
| if ( isNaN( percent ) ) { |
| return; |
| } |
| |
| if ( percent < 0 ) { |
| percent = 0; |
| } |
| |
| if ( percent > 100 ) { |
| percent = 100; |
| } |
| |
| var newval = ( percent / 100 ) * ( max - min ) + min; |
| |
| //from jQuery UI slider, the following source will round to the nearest step |
| var valModStep = ( newval - min ) % step; |
| var alignValue = newval - valModStep; |
| |
| if ( Math.abs( valModStep ) * 2 >= step ) { |
| alignValue += ( valModStep > 0 ) ? step : ( -step ); |
| } |
| // Since JavaScript has problems with large floats, round |
| // the final value to 5 digits after the decimal point (see jQueryUI: #4124) |
| newval = parseFloat( alignValue.toFixed(5) ); |
| |
| if ( newval < min ) { |
| newval = min; |
| } |
| |
| if ( newval > max ) { |
| newval = max; |
| } |
| |
| this.handle.css( "left", percent + "%" ); |
| this.handle.attr( { |
| "aria-valuenow": cType === "input" ? newval : control.find( "option" ).eq( newval ).attr( "value" ), |
| "aria-valuetext": cType === "input" ? newval : control.find( "option" ).eq( newval ).getEncodedText(), |
| title: cType === "input" ? newval : control.find( "option" ).eq( newval ).getEncodedText() |
| }); |
| |
| if( percent === 100 && this.handle.hasClass( "ui-slider-handle-snapping" ) ) { |
| this.handle.removeClass( "ui-toggle-off" ); |
| this.handle.addClass( "ui-toggle-on" ); |
| } else if ( percent === 0 && this.handle.hasClass( "ui-slider-handle-snapping" ) ) { |
| this.handle.removeClass( "ui-toggle-on" ); |
| this.handle.addClass( "ui-toggle-off" ); |
| } |
| |
| if ( this.valuebg ) { |
| this.valuebg.css( "width", percent + "%" ); |
| } |
| |
| // drag the label widths |
| if ( this._labels ) { |
| var handlePercent = this.handle.width() / this.slider.width() * 100, |
| aPercent = percent && handlePercent + ( 100 - handlePercent ) * percent / 100, |
| bPercent = percent === 100 ? 0 : Math.min( handlePercent + 100 - aPercent, 100 ); |
| |
| this._labels.each(function() { |
| var ab = $( this ).is( ".ui-slider-label-a" ); |
| $( this ).width( ( ab ? aPercent : bPercent ) + "%" ); |
| }); |
| } |
| |
| if ( !preventInputUpdate ) { |
| var valueChanged = false; |
| |
| // update control"s value |
| if ( cType === "input" ) { |
| valueChanged = control.val() !== newval; |
| control.val( newval ); |
| } else { |
| valueChanged = control[ 0 ].selectedIndex !== newval; |
| control[ 0 ].selectedIndex = newval; |
| } |
| if ( !isfromControl && valueChanged ) { |
| control.trigger( "change" ); |
| } |
| } |
| }, |
| |
| enable: function() { |
| this.element.attr( "disabled", false ); |
| this.slider.removeClass( "ui-disabled" ).attr( "aria-disabled", false ); |
| return this._setOption( "disabled", false ); |
| }, |
| |
| disable: function() { |
| this.element.attr( "disabled", true ); |
| this.slider.addClass( "ui-disabled" ).attr( "aria-disabled", true ); |
| return this._setOption( "disabled", true ); |
| } |
| |
| }); |
| |
| //auto self-init widgets |
| $.mobile.$document.bind( "pagecreate create", function( e ) { |
| $.mobile.slider.prototype.enhanceWithin( e.target, true ); |
| }); |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| $.widget( "mobile.selectmenu", $.mobile.widget, { |
| options: { |
| theme: null, |
| disabled: false, |
| icon: "arrow-d", |
| iconpos: "right", |
| inline: false, |
| corners: true, |
| shadow: true, |
| iconshadow: true, |
| overlayTheme: "a", |
| hidePlaceholderMenuItems: true, |
| closeText: "Close", |
| nativeMenu: true, |
| // This option defaults to true on iOS devices. |
| preventFocusZoom: /iPhone|iPad|iPod/.test( navigator.platform ) && navigator.userAgent.indexOf( "AppleWebKit" ) > -1, |
| initSelector: "select:not( :jqmData(role='slider') )", |
| mini: false |
| }, |
| |
| _button: function() { |
| return $( "<div/>" ); |
| }, |
| |
| _setDisabled: function( value ) { |
| this.element.attr( "disabled", value ); |
| this.button.attr( "aria-disabled", value ); |
| return this._setOption( "disabled", value ); |
| }, |
| |
| _focusButton : function() { |
| var self = this; |
| |
| setTimeout( function() { |
| self.button.focus(); |
| }, 40); |
| }, |
| |
| _selectOptions: function() { |
| return this.select.find( "option" ); |
| }, |
| |
| // setup items that are generally necessary for select menu extension |
| _preExtension: function() { |
| var classes = ""; |
| // TODO: Post 1.1--once we have time to test thoroughly--any classes manually applied to the original element should be carried over to the enhanced element, with an `-enhanced` suffix. See https://github.com/jquery/jquery-mobile/issues/3577 |
| /* if ( $el[0].className.length ) { |
| classes = $el[0].className; |
| } */ |
| if ( !!~this.element[0].className.indexOf( "ui-btn-left" ) ) { |
| classes = " ui-btn-left"; |
| } |
| |
| if ( !!~this.element[0].className.indexOf( "ui-btn-right" ) ) { |
| classes = " ui-btn-right"; |
| } |
| |
| this.select = this.element.wrap( "<div class='ui-select" + classes + "'>" ); |
| this.selectID = this.select.attr( "id" ); |
| this.label = $( "label[for='"+ this.selectID +"']" ).addClass( "ui-select" ); |
| this.isMultiple = this.select[ 0 ].multiple; |
| if ( !this.options.theme ) { |
| this.options.theme = $.mobile.getInheritedTheme( this.select, "c" ); |
| } |
| }, |
| |
| _create: function() { |
| this._preExtension(); |
| |
| // Allows for extension of the native select for custom selects and other plugins |
| // see select.custom for example extension |
| // TODO explore plugin registration |
| this._trigger( "beforeCreate" ); |
| |
| this.button = this._button(); |
| |
| var self = this, |
| |
| options = this.options, |
| |
| inline = options.inline || $.mobile.getAttrFixed( this.select[0], "data-" + $.mobile.ns + "inline" ), |
| mini = options.mini || $.mobile.getAttrFixed( this.select[0], "data-" + $.mobile.ns + "mini" ), |
| iconpos = options.icon ? ( options.iconpos || $.mobile.getAttrFixed( this.select[0], "data-" + $.mobile.ns + "iconpos" ) ) : false, |
| |
| // IE throws an exception at options.item() function when |
| // there is no selected item |
| // select first in this case |
| selectedIndex = this.select[ 0 ].selectedIndex === -1 ? 0 : this.select[ 0 ].selectedIndex, |
| |
| // TODO values buttonId and menuId are undefined here |
| button = this.button |
| .insertBefore( this.select ) |
| .buttonMarkup( { |
| theme: options.theme, |
| icon: options.icon, |
| iconpos: iconpos, |
| inline: inline, |
| corners: options.corners, |
| shadow: options.shadow, |
| iconshadow: options.iconshadow, |
| mini: mini |
| }); |
| |
| this.setButtonText(); |
| |
| // TIZEN fix: The TIZEN buttonMarkup patch adds 'ui-btn-icon-only' class to the ui-btn-inner. |
| // It makes the text not to be shown, so the class must be removed. (Like JQM) |
| button.children('.ui-btn-inner').removeClass('ui-btn-icon-only'); |
| |
| // Opera does not properly support opacity on select elements |
| // In Mini, it hides the element, but not its text |
| // On the desktop,it seems to do the opposite |
| // for these reasons, using the nativeMenu option results in a full native select in Opera |
| if ( options.nativeMenu && window.opera && window.opera.version ) { |
| button.addClass( "ui-select-nativeonly" ); |
| } |
| |
| // Add counter for multi selects |
| if ( this.isMultiple ) { |
| this.buttonCount = $( "<span>" ) |
| .addClass( "ui-li-count ui-btn-up-c ui-btn-corner-all" ) |
| .hide() |
| .appendTo( button.addClass('ui-li-has-count') ); |
| } |
| |
| // Disable if specified |
| if ( options.disabled || this.element.attr('disabled')) { |
| this.disable(); |
| } |
| |
| // Events on native select |
| this.select.change(function() { |
| self.refresh(); |
| }); |
| |
| this.build(); |
| }, |
| |
| build: function() { |
| var self = this; |
| |
| this.select |
| .appendTo( self.button ) |
| .bind( "vmousedown", function() { |
| // Add active class to button |
| self.button.addClass( $.mobile.activeBtnClass ); |
| }) |
| .bind( "focus", function() { |
| self.button.addClass( $.mobile.focusClass ); |
| }) |
| .bind( "blur", function() { |
| self.button.removeClass( $.mobile.focusClass ); |
| }) |
| .bind( "focus vmouseover", function() { |
| self.button.trigger( "vmouseover" ); |
| }) |
| .bind( "vmousemove", function() { |
| // Remove active class on scroll/touchmove |
| self.button.removeClass( $.mobile.activeBtnClass ); |
| }) |
| .bind( "change blur vmouseout", function() { |
| self.button.trigger( "vmouseout" ) |
| .removeClass( $.mobile.activeBtnClass ); |
| }) |
| .bind( "change blur", function() { |
| self.button.removeClass( "ui-btn-down-" + self.options.theme ); |
| }); |
| |
| // In many situations, iOS will zoom into the select upon tap, this prevents that from happening |
| self.button.bind( "vmousedown", function() { |
| if ( self.options.preventFocusZoom ) { |
| $.mobile.zoom.disable( true ); |
| } |
| }).bind( "mouseup", function() { |
| if ( self.options.preventFocusZoom ) { |
| setTimeout(function() { |
| $.mobile.zoom.enable( true ); |
| }, 0); |
| } |
| }); |
| }, |
| |
| selected: function() { |
| return this._selectOptions().filter( ":selected" ); |
| }, |
| |
| selectedIndices: function() { |
| var self = this; |
| |
| return this.selected().map(function() { |
| return self._selectOptions().index( this ); |
| }).get(); |
| }, |
| |
| setButtonText: function() { |
| var self = this, |
| selected = this.selected(), |
| text = this.placeholder, |
| span = $( document.createElement( "span" ) ); |
| |
| this.button.find( ".ui-btn-text" ).html(function() { |
| if ( selected.length ) { |
| text = selected.map(function() { |
| return $( this ).text(); |
| }).get().join( ", " ); |
| } else { |
| text = self.placeholder; |
| } |
| |
| // TODO possibly aggregate multiple select option classes |
| return span.text( text ) |
| .addClass( self.select.attr( "class" ) ) |
| .addClass( selected.attr( "class" ) ); |
| }); |
| }, |
| |
| setButtonCount: function() { |
| var selected = this.selected(); |
| |
| // multiple count inside button |
| if ( this.isMultiple ) { |
| this.buttonCount[ selected.length > 1 ? "show" : "hide" ]().text( selected.length ); |
| } |
| }, |
| |
| refresh: function() { |
| this.setButtonText(); |
| this.setButtonCount(); |
| }, |
| |
| // open and close preserved in native selects |
| // to simplify users code when looping over selects |
| open: $.noop, |
| close: $.noop, |
| |
| disable: function() { |
| this._setDisabled( true ); |
| this.button.addClass( "ui-disabled" ); |
| }, |
| |
| enable: function() { |
| this._setDisabled( false ); |
| this.button.removeClass( "ui-disabled" ); |
| } |
| }); |
| |
| //auto self-init widgets |
| $.mobile.$document.bind( "pagecreate create", function( e ) { |
| $.mobile.selectmenu.prototype.enhanceWithin( e.target, true ); |
| }); |
| })( jQuery ); |
| |
| /* |
| * custom "selectmenu" plugin |
| */ |
| |
| (function( $, undefined ) { |
| var extendSelect = function( widget ) { |
| |
| var select = widget.select, |
| selectID = widget.selectID, |
| label = widget.label, |
| thisPage = widget.select.closest( ".ui-page" ), |
| selectOptions = widget._selectOptions(), |
| isMultiple = widget.isMultiple = widget.select[ 0 ].multiple, |
| buttonId = selectID + "-button", |
| menuId = selectID + "-menu", |
| menuPage = $( "<div data-" + $.mobile.ns + "role='dialog' data-" +$.mobile.ns + "theme='"+ widget.options.theme +"' data-" +$.mobile.ns + "overlay-theme='"+ widget.options.overlayTheme +"'>" + |
| "<div data-" + $.mobile.ns + "role='header'>" + |
| "<div class='ui-title'>" + label.getEncodedText() + "</div>"+ |
| "</div>"+ |
| "<div data-" + $.mobile.ns + "role='content'></div>"+ |
| "</div>" ), |
| |
| listbox = $( "<div>", { "class": "ui-selectmenu" } ).insertAfter( widget.select ).popup( { theme: "a" } ), |
| |
| list = $( "<ul>", { |
| "class": "ui-selectmenu-list", |
| "id": menuId, |
| "role": "listbox", |
| "aria-labelledby": buttonId |
| }).attr( "data-" + $.mobile.ns + "theme", widget.options.theme ).appendTo( listbox ), |
| |
| header = $( "<div>", { |
| "class": "ui-header ui-bar-" + widget.options.theme |
| }).prependTo( listbox ), |
| |
| headerTitle = $( "<h1>", { |
| "class": "ui-title" |
| }).appendTo( header ), |
| |
| menuPageContent, |
| menuPageClose, |
| headerClose; |
| |
| if ( widget.isMultiple ) { |
| headerClose = $( "<a>", { |
| "text": widget.options.closeText, |
| "href": "#", |
| "class": "ui-btn-left" |
| }).attr( "data-" + $.mobile.ns + "iconpos", "notext" ).attr( "data-" + $.mobile.ns + "icon", "delete" ).appendTo( header ).buttonMarkup(); |
| } |
| |
| $.extend( widget, { |
| select: widget.select, |
| selectID: selectID, |
| buttonId: buttonId, |
| menuId: menuId, |
| thisPage: thisPage, |
| menuPage: menuPage, |
| label: label, |
| selectOptions: selectOptions, |
| isMultiple: isMultiple, |
| theme: widget.options.theme, |
| listbox: listbox, |
| list: list, |
| header: header, |
| headerTitle: headerTitle, |
| headerClose: headerClose, |
| menuPageContent: menuPageContent, |
| menuPageClose: menuPageClose, |
| placeholder: "", |
| |
| build: function() { |
| var self = this; |
| |
| // Create list from select, update state |
| self.refresh(); |
| |
| self.select.attr( "tabindex", "-1" ).focus(function() { |
| $( this ).blur(); |
| self.button.focus(); |
| }); |
| |
| // Button events |
| self.button.bind( "vclick keydown" , function( event ) { |
| if (event.type === "vclick" || |
| event.keyCode && (event.keyCode === $.mobile.keyCode.ENTER || |
| event.keyCode === $.mobile.keyCode.SPACE)) { |
| |
| self.open(); |
| event.preventDefault(); |
| } |
| }); |
| |
| // Events for list items |
| self.list.attr( "role", "listbox" ) |
| .bind( "focusin", function( e ) { |
| $( e.target ) |
| .attr( "tabindex", "0" ) |
| .trigger( "vmouseover" ); |
| |
| }) |
| .bind( "focusout", function( e ) { |
| $( e.target ) |
| .attr( "tabindex", "-1" ) |
| .trigger( "vmouseout" ); |
| }) |
| .delegate( "li:not(.ui-disabled, .ui-li-divider)", "click", function( event ) { |
| |
| // index of option tag to be selected |
| var oldIndex = self.select[ 0 ].selectedIndex, |
| newIndex = self.list.find( "li:not(.ui-li-divider)" ).index( this ), |
| option = self._selectOptions().eq( newIndex )[ 0 ]; |
| |
| // toggle selected status on the tag for multi selects |
| option.selected = self.isMultiple ? !option.selected : true; |
| |
| // toggle checkbox class for multiple selects |
| if ( self.isMultiple ) { |
| $( this ).find( ".ui-icon" ) |
| .toggleClass( "ui-icon-checkbox-on", option.selected ) |
| .toggleClass( "ui-icon-checkbox-off", !option.selected ); |
| } |
| |
| // trigger change if value changed |
| if ( self.isMultiple || oldIndex !== newIndex ) { |
| self.select.trigger( "change" ); |
| } |
| |
| // hide custom select for single selects only - otherwise focus clicked item |
| // We need to grab the clicked item the hard way, because the list may have been rebuilt |
| if ( self.isMultiple ) { |
| self.list.find( "li:not(.ui-li-divider)" ).eq( newIndex ) |
| .addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus(); |
| } |
| else { |
| self.close(); |
| } |
| |
| event.preventDefault(); |
| }) |
| .keydown(function( event ) { //keyboard events for menu items |
| var target = $( event.target ), |
| li = target.closest( "li" ), |
| prev, next; |
| |
| // switch logic based on which key was pressed |
| switch ( event.keyCode ) { |
| // up or left arrow keys |
| case 38: |
| prev = li.prev().not( ".ui-selectmenu-placeholder" ); |
| |
| if ( prev.is( ".ui-li-divider" ) ) { |
| prev = prev.prev(); |
| } |
| |
| // if there's a previous option, focus it |
| if ( prev.length ) { |
| target |
| .blur() |
| .attr( "tabindex", "-1" ); |
| |
| prev.addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus(); |
| } |
| |
| return false; |
| // down or right arrow keys |
| case 40: |
| next = li.next(); |
| |
| if ( next.is( ".ui-li-divider" ) ) { |
| next = next.next(); |
| } |
| |
| // if there's a next option, focus it |
| if ( next.length ) { |
| target |
| .blur() |
| .attr( "tabindex", "-1" ); |
| |
| next.addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus(); |
| } |
| |
| return false; |
| // If enter or space is pressed, trigger click |
| case 13: |
| case 32: |
| target.trigger( "click" ); |
| |
| return false; |
| } |
| }); |
| |
| // button refocus ensures proper height calculation |
| // by removing the inline style and ensuring page inclusion |
| self.menuPage.bind( "pagehide", function() { |
| self.list.appendTo( self.listbox ); |
| self._focusButton(); |
| |
| // TODO centralize page removal binding / handling in the page plugin. |
| // Suggestion from @jblas to do refcounting |
| // |
| // TODO extremely confusing dependency on the open method where the pagehide.remove |
| // bindings are stripped to prevent the parent page from disappearing. The way |
| // we're keeping pages in the DOM right now sucks |
| // |
| // rebind the page remove that was unbound in the open function |
| // to allow for the parent page removal from actions other than the use |
| // of a dialog sized custom select |
| // |
| // doing this here provides for the back button on the custom select dialog |
| $.mobile._bindPageRemove.call( self.thisPage ); |
| }); |
| |
| // Events on the popup |
| self.listbox.bind( "popupafterclose", function( event ) { |
| self.close(); |
| }); |
| |
| // Close button on small overlays |
| if ( self.isMultiple ) { |
| self.headerClose.click(function() { |
| if ( self.menuType === "overlay" ) { |
| self.close(); |
| return false; |
| } |
| }); |
| } |
| |
| // track this dependency so that when the parent page |
| // is removed on pagehide it will also remove the menupage |
| self.thisPage.addDependents( this.menuPage ); |
| }, |
| |
| _isRebuildRequired: function() { |
| var list = this.list.find( "li" ), |
| options = this._selectOptions(); |
| |
| // TODO exceedingly naive method to determine difference |
| // ignores value changes etc in favor of a forcedRebuild |
| // from the user in the refresh method |
| return options.text() !== list.text(); |
| }, |
| |
| selected: function() { |
| return this._selectOptions().filter( ":selected:not( :jqmData(placeholder='true') )" ); |
| }, |
| |
| refresh: function( forceRebuild , foo ) { |
| var self = this, |
| select = this.element, |
| isMultiple = this.isMultiple, |
| indicies; |
| |
| if ( forceRebuild || this._isRebuildRequired() ) { |
| self._buildList(); |
| } |
| |
| indicies = this.selectedIndices(); |
| |
| self.setButtonText(); |
| self.setButtonCount(); |
| |
| self.list.find( "li:not(.ui-li-divider)" ) |
| .removeClass( $.mobile.activeBtnClass ) |
| .attr( "aria-selected", false ) |
| .each(function( i ) { |
| |
| if ( $.inArray( i, indicies ) > -1 ) { |
| var item = $( this ); |
| |
| // Aria selected attr |
| item.attr( "aria-selected", true ); |
| |
| // Multiple selects: add the "on" checkbox state to the icon |
| if ( self.isMultiple ) { |
| item.find( ".ui-icon" ).removeClass( "ui-icon-checkbox-off" ).addClass( "ui-icon-checkbox-on" ); |
| } else { |
| if ( item.is( ".ui-selectmenu-placeholder" ) ) { |
| item.next().addClass( $.mobile.activeBtnClass ); |
| } else { |
| item.addClass( $.mobile.activeBtnClass ); |
| } |
| } |
| } |
| }); |
| }, |
| |
| close: function() { |
| if ( this.options.disabled || !this.isOpen ) { |
| return; |
| } |
| |
| var self = this; |
| |
| if ( self.menuType === "page" ) { |
| // doesn't solve the possible issue with calling change page |
| // where the objects don't define data urls which prevents dialog key |
| // stripping - changePage has incoming refactor |
| $.mobile.back(); |
| } else { |
| self.listbox.popup( "close" ); |
| self.list.appendTo( self.listbox ); |
| self._focusButton(); |
| } |
| |
| // allow the dialog to be closed again |
| self.isOpen = false; |
| }, |
| |
| open: function() { |
| if ( this.options.disabled ) { |
| return; |
| } |
| |
| var self = this, |
| $window = $.mobile.$window, |
| selfListParent = self.list.parent(), |
| menuHeight = selfListParent.outerHeight(), |
| menuWidth = selfListParent.outerWidth(), |
| activePage = $( "." + $.mobile.activePageClass ), |
| scrollTop = $window.scrollTop(), |
| btnOffset = self.button.offset().top, |
| screenHeight = $window.height(), |
| screenWidth = $window.width(); |
| |
| //add active class to button |
| self.button.addClass( $.mobile.activeBtnClass ); |
| |
| //remove after delay |
| setTimeout( function() { |
| self.button.removeClass( $.mobile.activeBtnClass ); |
| }, 300); |
| |
| function focusMenuItem() { |
| var selector = self.list.find( "." + $.mobile.activeBtnClass + " a" ); |
| if ( selector.length === 0 ) { |
| selector = self.list.find( "li.ui-btn:not( :jqmData(placeholder='true') ) a" ); |
| } |
| selector.first().focus().closest( "li" ).addClass( "ui-btn-down-" + widget.options.theme ); |
| } |
| |
| if ( menuHeight > screenHeight - 80 || !$.support.scrollTop ) { |
| |
| self.menuPage.appendTo( $.mobile.pageContainer ).page(); |
| self.menuPageContent = menuPage.find( ".ui-content" ); |
| self.menuPageClose = menuPage.find( ".ui-header a" ); |
| |
| // prevent the parent page from being removed from the DOM, |
| // otherwise the results of selecting a list item in the dialog |
| // fall into a black hole |
| self.thisPage.unbind( "pagehide.remove" ); |
| |
| //for WebOS/Opera Mini (set lastscroll using button offset) |
| if ( scrollTop === 0 && btnOffset > screenHeight ) { |
| self.thisPage.one( "pagehide", function() { |
| $( this ).jqmData( "lastScroll", btnOffset ); |
| }); |
| } |
| |
| self.menuPage.one( "pageshow", function() { |
| focusMenuItem(); |
| self.isOpen = true; |
| }); |
| |
| self.menuType = "page"; |
| self.menuPageContent.append( self.list ); |
| self.menuPage.find("div .ui-title").text(self.label.text()); |
| $.mobile.changePage( self.menuPage, { |
| transition: $.mobile.defaultDialogTransition |
| }); |
| } else { |
| self.menuType = "overlay"; |
| |
| self.listbox |
| .one( "popupafteropen", focusMenuItem ) |
| .popup( "open", { |
| x: self.button.offset().left + self.button.outerWidth() / 2, |
| y: self.button.offset().top + self.button.outerHeight() / 2 |
| }); |
| |
| // duplicate with value set in page show for dialog sized selects |
| self.isOpen = true; |
| } |
| }, |
| |
| _buildList: function() { |
| var self = this, |
| o = this.options, |
| placeholder = this.placeholder, |
| needPlaceholder = true, |
| optgroups = [], |
| lis = [], |
| dataIcon = self.isMultiple ? "checkbox-off" : "false"; |
| |
| self.list.empty().filter( ".ui-listview" ).listview( "destroy" ); |
| |
| var $options = self.select.find( "option" ), |
| numOptions = $options.length, |
| select = this.select[ 0 ], |
| dataPrefix = 'data-' + $.mobile.ns, |
| dataIndexAttr = dataPrefix + 'option-index', |
| dataIconAttr = dataPrefix + 'icon', |
| dataRoleAttr = dataPrefix + 'role', |
| dataPlaceholderAttr = dataPrefix + 'placeholder', |
| fragment = document.createDocumentFragment(), |
| isPlaceholderItem = false, |
| optGroup; |
| |
| for (var i = 0; i < numOptions;i++, isPlaceholderItem = false) { |
| var option = $options[i], |
| $option = $( option ), |
| parent = option.parentNode, |
| text = $option.text(), |
| anchor = document.createElement( 'a' ), |
| classes = []; |
| |
| anchor.setAttribute( 'href', '#' ); |
| anchor.appendChild( document.createTextNode( text ) ); |
| |
| // Are we inside an optgroup? |
| if ( parent !== select && parent.nodeName.toLowerCase() === "optgroup" ) { |
| var optLabel = parent.getAttribute( 'label' ); |
| if ( optLabel !== optGroup ) { |
| var divider = document.createElement( 'li' ); |
| divider.setAttribute( dataRoleAttr, 'list-divider' ); |
| divider.setAttribute( 'role', 'option' ); |
| divider.setAttribute( 'tabindex', '-1' ); |
| divider.appendChild( document.createTextNode( optLabel ) ); |
| fragment.appendChild( divider ); |
| optGroup = optLabel; |
| } |
| } |
| |
| if ( needPlaceholder && ( !option.getAttribute( "value" ) || text.length === 0 || $option.jqmData( "placeholder" ) ) ) { |
| needPlaceholder = false; |
| isPlaceholderItem = true; |
| |
| // If we have identified a placeholder, mark it retroactively in the select as well |
| option.setAttribute( dataPlaceholderAttr, true ); |
| if ( o.hidePlaceholderMenuItems ) { |
| classes.push( "ui-selectmenu-placeholder" ); |
| } |
| if (!placeholder) { |
| placeholder = self.placeholder = text; |
| } |
| } |
| |
| var item = document.createElement('li'); |
| if ( option.disabled ) { |
| classes.push( "ui-disabled" ); |
| item.setAttribute('aria-disabled',true); |
| } |
| item.setAttribute( dataIndexAttr,i ); |
| item.setAttribute( dataIconAttr, dataIcon ); |
| if ( isPlaceholderItem ) { |
| item.setAttribute( dataPlaceholderAttr, true ); |
| } |
| item.className = classes.join( " " ); |
| item.setAttribute( 'role', 'option' ); |
| anchor.setAttribute( 'tabindex', '-1' ); |
| item.appendChild( anchor ); |
| fragment.appendChild( item ); |
| } |
| |
| self.list[0].appendChild( fragment ); |
| |
| // Hide header if it's not a multiselect and there's no placeholder |
| if ( !this.isMultiple && !placeholder.length ) { |
| this.header.hide(); |
| } else { |
| this.headerTitle.text( this.placeholder ); |
| } |
| |
| // Now populated, create listview |
| self.list.listview(); |
| }, |
| |
| _button: function() { |
| return $( "<a>", { |
| "href": "#", |
| "role": "button", |
| // TODO value is undefined at creation |
| "id": this.buttonId, |
| "aria-haspopup": "true", |
| |
| // TODO value is undefined at creation |
| "aria-owns": this.menuId |
| }); |
| } |
| }); |
| }; |
| |
| // issue #3894 - core doesn't trigger events on disabled delegates |
| $.mobile.$document.bind( "selectmenubeforecreate", function( event ) { |
| var selectmenuWidget = $( event.target ).data( "selectmenu" ); |
| |
| if ( !selectmenuWidget.options.nativeMenu && |
| selectmenuWidget.element.parents( ":jqmData(role='popup')" ).length === 0 ) { |
| extendSelect( selectmenuWidget ); |
| } |
| }); |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| |
| $.widget( "mobile.fixedtoolbar", $.mobile.widget, { |
| options: { |
| visibleOnPageShow: true, |
| disablePageZoom: true, |
| transition: "slide", //can be none, fade, slide (slide maps to slideup or slidedown) |
| fullscreen: false, |
| tapToggle: true, |
| tapToggleBlacklist: "a, button, input, select, textarea, .ui-header-fixed, .ui-footer-fixed, .ui-popup", |
| hideDuringFocus: "input, textarea, select", |
| updatePagePadding: true, |
| trackPersistentToolbars: true, |
| |
| // Browser detection! Weeee, here we go... |
| // Unfortunately, position:fixed is costly, not to mention probably impossible, to feature-detect accurately. |
| // Some tests exist, but they currently return false results in critical devices and browsers, which could lead to a broken experience. |
| // Testing fixed positioning is also pretty obtrusive to page load, requiring injected elements and scrolling the window |
| // The following function serves to rule out some popular browsers with known fixed-positioning issues |
| // This is a plugin option like any other, so feel free to improve or overwrite it |
| supportBlacklist: function() { |
| var w = window, |
| ua = navigator.userAgent, |
| platform = navigator.platform, |
| // Rendering engine is Webkit, and capture major version |
| wkmatch = ua.match( /AppleWebKit\/([0-9]+)/ ), |
| wkversion = !!wkmatch && wkmatch[ 1 ], |
| ffmatch = ua.match( /Fennec\/([0-9]+)/ ), |
| ffversion = !!ffmatch && ffmatch[ 1 ], |
| operammobilematch = ua.match( /Opera Mobi\/([0-9]+)/ ), |
| omversion = !!operammobilematch && operammobilematch[ 1 ]; |
| |
| if( |
| // iOS 4.3 and older : Platform is iPhone/Pad/Touch and Webkit version is less than 534 (ios5) |
| ( ( platform.indexOf( "iPhone" ) > -1 || platform.indexOf( "iPad" ) > -1 || platform.indexOf( "iPod" ) > -1 ) && wkversion && wkversion < 534 ) || |
| // Opera Mini |
| ( w.operamini && ({}).toString.call( w.operamini ) === "[object OperaMini]" ) || |
| ( operammobilematch && omversion < 7458 ) || |
| //Android lte 2.1: Platform is Android and Webkit version is less than 533 (Android 2.2) |
| ( ua.indexOf( "Android" ) > -1 && wkversion && wkversion < 533 ) || |
| // Firefox Mobile before 6.0 - |
| ( ffversion && ffversion < 6 ) || |
| // WebOS less than 3 |
| ( "palmGetResource" in window && wkversion && wkversion < 534 ) || |
| // MeeGo |
| ( ua.indexOf( "MeeGo" ) > -1 && ua.indexOf( "NokiaBrowser/8.5.0" ) > -1 ) ) { |
| return true; |
| } |
| |
| return false; |
| }, |
| initSelector: ":jqmData(position='dummy')" |
| }, |
| |
| _create: function() { |
| |
| var self = this, |
| o = self.options, |
| $el = self.element, |
| tbtype = $el.is( ":jqmData(role='header')" ) ? "header" : "footer", |
| $page = $el.closest( ".ui-page" ); |
| |
| // Feature detecting support for |
| if ( o.supportBlacklist() ) { |
| self.destroy(); |
| return; |
| } |
| |
| $el.addClass( "ui-"+ tbtype +"-fixed" ); |
| |
| // "fullscreen" overlay positioning |
| if ( o.fullscreen ) { |
| $el.addClass( "ui-"+ tbtype +"-fullscreen" ); |
| $page.addClass( "ui-page-" + tbtype + "-fullscreen" ); |
| } |
| // If not fullscreen, add class to page to set top or bottom padding |
| else{ |
| $page.addClass( "ui-page-" + tbtype + "-fixed" ); |
| } |
| |
| self._addTransitionClass(); |
| self._bindPageEvents(); |
| self._bindToggleHandlers(); |
| }, |
| |
| _addTransitionClass: function() { |
| var tclass = this.options.transition; |
| |
| if ( tclass && tclass !== "none" ) { |
| // use appropriate slide for header or footer |
| if ( tclass === "slide" ) { |
| tclass = this.element.is( ".ui-header" ) ? "slidedown" : "slideup"; |
| } |
| |
| this.element.addClass( tclass ); |
| } |
| }, |
| |
| _bindPageEvents: function() { |
| var self = this, |
| o = self.options, |
| $el = self.element; |
| |
| //page event bindings |
| // Fixed toolbars require page zoom to be disabled, otherwise usability issues crop up |
| // This method is meant to disable zoom while a fixed-positioned toolbar page is visible |
| $el.closest( ".ui-page" ) |
| .bind( "pagebeforeshow", function() { |
| if ( o.disablePageZoom ) { |
| $.mobile.zoom.disable( true ); |
| } |
| if ( !o.visibleOnPageShow ) { |
| self.hide( true ); |
| } |
| } ) |
| .bind( "webkitAnimationStart animationstart updatelayout", function() { |
| var thisPage = this; |
| if ( o.updatePagePadding ) { |
| self.updatePagePadding( thisPage ); |
| } |
| }) |
| .bind( "pageshow", function() { |
| var thisPage = this; |
| self.updatePagePadding( thisPage ); |
| if ( o.updatePagePadding ) { |
| $.mobile.$window.bind( "throttledresize." + self.widgetName, function() { |
| self.updatePagePadding( thisPage ); |
| }); |
| } |
| }) |
| .bind( "pagebeforehide", function( e, ui ) { |
| if ( o.disablePageZoom ) { |
| $.mobile.zoom.enable( true ); |
| } |
| if ( o.updatePagePadding ) { |
| $.mobile.$window.unbind( "throttledresize." + self.widgetName ); |
| } |
| |
| if ( o.trackPersistentToolbars ) { |
| var thisFooter = $( ".ui-footer-fixed:jqmData(id)", this ), |
| thisHeader = $( ".ui-header-fixed:jqmData(id)", this ), |
| nextFooter = thisFooter.length && ui.nextPage && $( ".ui-footer-fixed:jqmData(id='" + thisFooter.jqmData( "id" ) + "')", ui.nextPage ) || $(), |
| nextHeader = thisHeader.length && ui.nextPage && $( ".ui-header-fixed:jqmData(id='" + thisHeader.jqmData( "id" ) + "')", ui.nextPage ) || $(); |
| |
| if ( nextFooter.length || nextHeader.length ) { |
| |
| nextFooter.add( nextHeader ).appendTo( $.mobile.pageContainer ); |
| |
| ui.nextPage.one( "pageshow", function() { |
| nextFooter.add( nextHeader ).appendTo( this ); |
| }); |
| } |
| } |
| }); |
| }, |
| |
| _visible: true, |
| |
| // This will set the content element's top or bottom padding equal to the toolbar's height |
| updatePagePadding: function( tbPage ) { |
| var $el = this.element, |
| header = $el.is( ".ui-header" ); |
| |
| // This behavior only applies to "fixed", not "fullscreen" |
| if ( this.options.fullscreen ) { return; } |
| |
| tbPage = tbPage || $el.closest( ".ui-page" ); |
| $( tbPage ).css( "padding-" + ( header ? "top" : "bottom" ), $el.outerHeight() ); |
| }, |
| |
| _useTransition: function( notransition ) { |
| var $win = $.mobile.$window, |
| $el = this.element, |
| scroll = $win.scrollTop(), |
| elHeight = $el.height(), |
| pHeight = $el.closest( ".ui-page" ).height(), |
| viewportHeight = $.mobile.getScreenHeight(), |
| tbtype = $el.is( ":jqmData(role='header')" ) ? "header" : "footer"; |
| |
| return !notransition && |
| ( this.options.transition && this.options.transition !== "none" && |
| ( |
| ( tbtype === "header" && !this.options.fullscreen && scroll > elHeight ) || |
| ( tbtype === "footer" && !this.options.fullscreen && scroll + viewportHeight < pHeight - elHeight ) |
| ) || this.options.fullscreen |
| ); |
| }, |
| |
| show: function( notransition ) { |
| var hideClass = "ui-fixed-hidden", |
| $el = this.element; |
| |
| if ( this._useTransition( notransition ) ) { |
| $el |
| .removeClass( "out " + hideClass ) |
| .addClass( "in" ); |
| } |
| else { |
| $el.removeClass( hideClass ); |
| } |
| this._visible = true; |
| }, |
| |
| hide: function( notransition ) { |
| var hideClass = "ui-fixed-hidden", |
| $el = this.element, |
| // if it's a slide transition, our new transitions need the reverse class as well to slide outward |
| outclass = "out" + ( this.options.transition === "slide" ? " reverse" : "" ); |
| |
| if( this._useTransition( notransition ) ) { |
| $el |
| .addClass( outclass ) |
| .removeClass( "in" ) |
| .animationComplete(function() { |
| $el.addClass( hideClass ).removeClass( outclass ); |
| }); |
| } |
| else { |
| $el.addClass( hideClass ).removeClass( outclass ); |
| } |
| this._visible = false; |
| }, |
| |
| toggle: function() { |
| this[ this._visible ? "hide" : "show" ](); |
| }, |
| |
| _bindToggleHandlers: function() { |
| var self = this, |
| o = self.options, |
| $el = self.element; |
| |
| // tap toggle |
| $el.closest( ".ui-page" ) |
| .bind( "vclick", function( e ) { |
| if ( o.tapToggle && !$( e.target ).closest( o.tapToggleBlacklist ).length ) { |
| self.toggle(); |
| } |
| }) |
| .bind( "focusin focusout", function( e ) { |
| if ( screen.width < 500 && $( e.target ).is( o.hideDuringFocus ) && !$( e.target ).closest( ".ui-header-fixed, .ui-footer-fixed" ).length ) { |
| self[ ( e.type === "focusin" && self._visible ) ? "hide" : "show" ](); |
| } |
| }); |
| }, |
| |
| destroy: function() { |
| this.element.removeClass( "ui-header-fixed ui-footer-fixed ui-header-fullscreen ui-footer-fullscreen in out fade slidedown slideup ui-fixed-hidden" ); |
| this.element.closest( ".ui-page" ).removeClass( "ui-page-header-fixed ui-page-footer-fixed ui-page-header-fullscreen ui-page-footer-fullscreen" ); |
| } |
| |
| }); |
| |
| //auto self-init widgets |
| $.mobile.$document |
| .bind( "pagecreate create", function( e ) { |
| |
| // DEPRECATED in 1.1: support for data-fullscreen=true|false on the page element. |
| // This line ensures it still works, but we recommend moving the attribute to the toolbars themselves. |
| if ( $( e.target ).jqmData( "fullscreen" ) ) { |
| $( $.mobile.fixedtoolbar.prototype.options.initSelector, e.target ).not( ":jqmData(fullscreen)" ).jqmData( "fullscreen", true ); |
| } |
| |
| $.mobile.fixedtoolbar.prototype.enhanceWithin( e.target ); |
| }); |
| |
| })( jQuery ); |
| |
| (function( $, window ) { |
| |
| // This fix addresses an iOS bug, so return early if the UA claims it's something else. |
| if ( !(/iPhone|iPad|iPod/.test( navigator.platform ) && navigator.userAgent.indexOf( "AppleWebKit" ) > -1 ) ) { |
| return; |
| } |
| |
| var zoom = $.mobile.zoom, |
| evt, x, y, z, aig; |
| |
| function checkTilt( e ) { |
| evt = e.originalEvent; |
| aig = evt.accelerationIncludingGravity; |
| |
| x = Math.abs( aig.x ); |
| y = Math.abs( aig.y ); |
| z = Math.abs( aig.z ); |
| |
| // If portrait orientation and in one of the danger zones |
| if ( !window.orientation && ( x > 7 || ( ( z > 6 && y < 8 || z < 8 && y > 6 ) && x > 5 ) ) ) { |
| if ( zoom.enabled ) { |
| zoom.disable(); |
| } |
| } else if ( !zoom.enabled ) { |
| zoom.enable(); |
| } |
| } |
| |
| $.mobile.$window |
| .bind( "orientationchange.iosorientationfix", zoom.enable ) |
| .bind( "devicemotion.iosorientationfix", checkTilt ); |
| |
| }( jQuery, this )); |
| |
| (function( $, window, undefined ) { |
| var $html = $( "html" ), |
| $head = $( "head" ), |
| $window = $.mobile.$window; |
| |
| //remove initial build class (only present on first pageshow) |
| function hideRenderingClass() { |
| $html.removeClass( "ui-mobile-rendering" ); |
| } |
| |
| // trigger mobileinit event - useful hook for configuring $.mobile settings before they're used |
| $( window.document ).trigger( "mobileinit" ); |
| |
| // support conditions |
| // if device support condition(s) aren't met, leave things as they are -> a basic, usable experience, |
| // otherwise, proceed with the enhancements |
| if ( !$.mobile.gradeA() ) { |
| return; |
| } |
| |
| // override ajaxEnabled on platforms that have known conflicts with hash history updates |
| // or generally work better browsing in regular http for full page refreshes (BB5, Opera Mini) |
| if ( $.mobile.ajaxBlacklist ) { |
| $.mobile.ajaxEnabled = false; |
| } |
| |
| // Add mobile, initial load "rendering" classes to docEl |
| $html.addClass( "ui-mobile ui-mobile-rendering" ); |
| |
| // This is a fallback. If anything goes wrong (JS errors, etc), or events don't fire, |
| // this ensures the rendering class is removed after 5 seconds, so content is visible and accessible |
| setTimeout( hideRenderingClass, 5000 ); |
| |
| $.extend( $.mobile, { |
| addEventBlocker: function () { |
| $html.addClass( "ui-blocker" ); |
| $html.bind( "touchstart touchend vclick mousedown mouseup click", function () { |
| return false; |
| } ); |
| }, |
| |
| removeEventBlocker: function () { |
| $html.removeClass( "ui-blocker" ); |
| $html.unbind( "touchstart touchend vclick mousedown mouseup click" ); |
| }, |
| |
| // find and enhance the pages in the dom and transition to the first page. |
| initializePage: function() { |
| // find present pages |
| var $pages = $( ":jqmData(role='page'), :jqmData(role='dialog')" ), |
| hash = $.mobile.path.parseLocation().hash.replace("#", ""), |
| hashPage = document.getElementById( hash ); |
| |
| // if no pages are found, create one with body's inner html |
| if ( !$pages.length ) { |
| $pages = $( "body" ).wrapInner( "<div data-" + $.mobile.ns + "role='page'></div>" ).children( 0 ); |
| } |
| |
| // add dialogs, set data-url attrs |
| $pages.each(function() { |
| var $this = $( this ); |
| |
| // unless the data url is already set set it to the pathname |
| if ( !$this[0].getAttribute( "data-" + $.mobile.ns + "url" ) ) { |
| $this.attr( "data-" + $.mobile.ns + "url", $this.attr( "id" ) || location.pathname + location.search ); |
| } |
| }); |
| |
| // define first page in dom case one backs out to the directory root (not always the first page visited, but defined as fallback) |
| $.mobile.firstPage = $pages.first(); |
| |
| // define page container |
| $.mobile.pageContainer = $.mobile.firstPage.parent().addClass( "ui-mobile-viewport" ); |
| |
| // alert listeners that the pagecontainer has been determined for binding |
| // to events triggered on it |
| $window.trigger( "pagecontainercreate" ); |
| |
| // cue page loading message |
| $.mobile.showPageLoadingMsg(); |
| $.mobile.addEventBlocker(); |
| |
| //remove initial build class (only present on first pageshow) |
| hideRenderingClass(); |
| |
| // if hashchange listening is disabled, there's no hash deeplink, |
| // the hash is not valid (contains more than one # or does not start with #) |
| // or there is no page with that hash, change to the first page in the DOM |
| // Remember, however, that the hash can also be a path! |
| if ( ! ( $.mobile.hashListeningEnabled && |
| $.mobile.path.isHashValid( location.hash ) && |
| ( $( hashPage ).is( ':jqmData(role="page")' ) || |
| $.mobile.path.isPath( hash ) || |
| hash === $.mobile.dialogHashKey ) ) ) { |
| |
| // Store the initial destination |
| if ( $.mobile.path.isHashValid( location.hash ) ) { |
| $.mobile.urlHistory.initialDst = hash.replace( "#", "" ); |
| } |
| $.mobile.changePage( $.mobile.firstPage, { transition: "none", reverse: true, changeHash: false, fromHashChange: true } ); |
| } |
| // otherwise, trigger a hashchange to load a deeplink |
| else { |
| $window.trigger( "hashchange", [ true ] ); |
| } |
| } |
| }); |
| |
| // initialize events now, after mobileinit has occurred |
| $.mobile.navreadyDeferred.resolve(); |
| |
| // check which scrollTop value should be used by scrolling to 1 immediately at domready |
| // then check what the scroll top is. Android will report 0... others 1 |
| // note that this initial scroll won't hide the address bar. It's just for the check. |
| $(function() { |
| // window.scrollTo( 0, 1 ); |
| |
| // if defaultHomeScroll hasn't been set yet, see if scrollTop is 1 |
| // it should be 1 in most browsers, but android treats 1 as 0 (for hiding addr bar) |
| // so if it's 1, use 0 from now on |
| $.mobile.defaultHomeScroll = ( !$.support.scrollTop || $.mobile.$window.scrollTop() === 1 ) ? 0 : 1; |
| |
| |
| // TODO: Implement a proper registration mechanism with dependency handling in order to not have exceptions like the one below |
| //auto self-init widgets for those widgets that have a soft dependency on others |
| if ( $.fn.controlgroup ) { |
| $.mobile.$document.bind( "pagecreate create", function( e ) { |
| $( ":jqmData(role='controlgroup')", e.target ) |
| .jqmEnhanceable() |
| .controlgroup({ excludeInvisible: false }); |
| }); |
| } |
| |
| //dom-ready inits |
| if ( $.mobile.autoInitializePage ) { |
| $.mobile.initializePage(); |
| } |
| |
| // window load event |
| // hide iOS browser chrome on load |
| $window.load( $.mobile.silentScroll ); |
| |
| if ( !$.support.cssPointerEvents ) { |
| // IE and Opera don't support CSS pointer-events: none that we use to disable link-based buttons |
| // by adding the 'ui-disabled' class to them. Using a JavaScript workaround for those browser. |
| // https://github.com/jquery/jquery-mobile/issues/3558 |
| |
| $.mobile.$document.delegate( ".ui-disabled", "vclick", |
| function( e ) { |
| e.preventDefault(); |
| e.stopImmediatePropagation(); |
| } |
| ); |
| } |
| }); |
| }( jQuery, this )); |
| |
| })); |
| |
| /*! |
| * Globalize |
| * |
| * http://github.com/jquery/globalize |
| * |
| * Copyright Software Freedom Conservancy, Inc. |
| * Dual licensed under the MIT or GPL Version 2 licenses. |
| * http://jquery.org/license |
| */ |
| |
| (function( window, undefined ) { |
| |
| var Globalize, |
| // private variables |
| regexHex, |
| regexInfinity, |
| regexParseFloat, |
| regexTrim, |
| // private JavaScript utility functions |
| arrayIndexOf, |
| endsWith, |
| extend, |
| isArray, |
| isFunction, |
| isObject, |
| startsWith, |
| trim, |
| truncate, |
| zeroPad, |
| // private Globalization utility functions |
| appendPreOrPostMatch, |
| expandFormat, |
| formatDate, |
| formatNumber, |
| getTokenRegExp, |
| getEra, |
| getEraYear, |
| parseExact, |
| parseNegativePattern; |
| |
| // Global variable (Globalize) or CommonJS module (globalize) |
| Globalize = function( cultureSelector ) { |
| return new Globalize.prototype.init( cultureSelector ); |
| }; |
| |
| if ( typeof require !== "undefined" |
| && typeof exports !== "undefined" |
| && typeof module !== "undefined" ) { |
| // Assume CommonJS |
| module.exports = Globalize; |
| } else { |
| // Export as global variable |
| window.Globalize = Globalize; |
| } |
| |
| Globalize.cultures = {}; |
| |
| Globalize.prototype = { |
| constructor: Globalize, |
| init: function( cultureSelector ) { |
| this.cultures = Globalize.cultures; |
| this.cultureSelector = cultureSelector; |
| |
| return this; |
| } |
| }; |
| Globalize.prototype.init.prototype = Globalize.prototype; |
| |
| // 1. When defining a culture, all fields are required except the ones stated as optional. |
| // 2. Each culture should have a ".calendars" object with at least one calendar named "standard" |
| // which serves as the default calendar in use by that culture. |
| // 3. Each culture should have a ".calendar" object which is the current calendar being used, |
| // it may be dynamically changed at any time to one of the calendars in ".calendars". |
| Globalize.cultures[ "default" ] = { |
| // A unique name for the culture in the form <language code>-<country/region code> |
| name: "en", |
| // the name of the culture in the english language |
| englishName: "English", |
| // the name of the culture in its own language |
| nativeName: "English", |
| // whether the culture uses right-to-left text |
| isRTL: false, |
| // "language" is used for so-called "specific" cultures. |
| // For example, the culture "es-CL" means "Spanish, in Chili". |
| // It represents the Spanish-speaking culture as it is in Chili, |
| // which might have different formatting rules or even translations |
| // than Spanish in Spain. A "neutral" culture is one that is not |
| // specific to a region. For example, the culture "es" is the generic |
| // Spanish culture, which may be a more generalized version of the language |
| // that may or may not be what a specific culture expects. |
| // For a specific culture like "es-CL", the "language" field refers to the |
| // neutral, generic culture information for the language it is using. |
| // This is not always a simple matter of the string before the dash. |
| // For example, the "zh-Hans" culture is netural (Simplified Chinese). |
| // And the "zh-SG" culture is Simplified Chinese in Singapore, whose lanugage |
| // field is "zh-CHS", not "zh". |
| // This field should be used to navigate from a specific culture to it's |
| // more general, neutral culture. If a culture is already as general as it |
| // can get, the language may refer to itself. |
| language: "en", |
| // numberFormat defines general number formatting rules, like the digits in |
| // each grouping, the group separator, and how negative numbers are displayed. |
| numberFormat: { |
| // [negativePattern] |
| // Note, numberFormat.pattern has no "positivePattern" unlike percent and currency, |
| // but is still defined as an array for consistency with them. |
| // negativePattern: one of "(n)|-n|- n|n-|n -" |
| pattern: [ "-n" ], |
| // number of decimal places normally shown |
| decimals: 2, |
| // string that separates number groups, as in 1,000,000 |
| ",": ",", |
| // string that separates a number from the fractional portion, as in 1.99 |
| ".": ".", |
| // array of numbers indicating the size of each number group. |
| // TODO: more detailed description and example |
| groupSizes: [ 3 ], |
| // symbol used for positive numbers |
| "+": "+", |
| // symbol used for negative numbers |
| "-": "-", |
| // symbol used for NaN (Not-A-Number) |
| NaN: "NaN", |
| // symbol used for Negative Infinity |
| negativeInfinity: "-Infinity", |
| // symbol used for Positive Infinity |
| positiveInfinity: "Infinity", |
| percent: { |
| // [negativePattern, positivePattern] |
| // negativePattern: one of "-n %|-n%|-%n|%-n|%n-|n-%|n%-|-% n|n %-|% n-|% -n|n- %" |
| // positivePattern: one of "n %|n%|%n|% n" |
| pattern: [ "-n %", "n %" ], |
| // number of decimal places normally shown |
| decimals: 2, |
| // array of numbers indicating the size of each number group. |
| // TODO: more detailed description and example |
| groupSizes: [ 3 ], |
| // string that separates number groups, as in 1,000,000 |
| ",": ",", |
| // string that separates a number from the fractional portion, as in 1.99 |
| ".": ".", |
| // symbol used to represent a percentage |
| symbol: "%" |
| }, |
| currency: { |
| // [negativePattern, positivePattern] |
| // negativePattern: one of "($n)|-$n|$-n|$n-|(n$)|-n$|n-$|n$-|-n $|-$ n|n $-|$ n-|$ -n|n- $|($ n)|(n $)" |
| // positivePattern: one of "$n|n$|$ n|n $" |
| pattern: [ "($n)", "$n" ], |
| // number of decimal places normally shown |
| decimals: 2, |
| // array of numbers indicating the size of each number group. |
| // TODO: more detailed description and example |
| groupSizes: [ 3 ], |
| // string that separates number groups, as in 1,000,000 |
| ",": ",", |
| // string that separates a number from the fractional portion, as in 1.99 |
| ".": ".", |
| // symbol used to represent currency |
| symbol: "$" |
| } |
| }, |
| // calendars defines all the possible calendars used by this culture. |
| // There should be at least one defined with name "standard", and is the default |
| // calendar used by the culture. |
| // A calendar contains information about how dates are formatted, information about |
| // the calendar's eras, a standard set of the date formats, |
| // translations for day and month names, and if the calendar is not based on the Gregorian |
| // calendar, conversion functions to and from the Gregorian calendar. |
| calendars: { |
| standard: { |
| // name that identifies the type of calendar this is |
| name: "Gregorian_USEnglish", |
| // separator of parts of a date (e.g. "/" in 11/05/1955) |
| "/": "/", |
| // separator of parts of a time (e.g. ":" in 05:44 PM) |
| ":": ":", |
| // the first day of the week (0 = Sunday, 1 = Monday, etc) |
| firstDay: 0, |
| days: { |
| // full day names |
| names: [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ], |
| // abbreviated day names |
| namesAbbr: [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ], |
| // shortest day names |
| namesShort: [ "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" ] |
| }, |
| months: { |
| // full month names (13 months for lunar calendards -- 13th month should be "" if not lunar) |
| names: [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", "" ], |
| // abbreviated month names |
| namesAbbr: [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "" ] |
| }, |
| // AM and PM designators in one of these forms: |
| // The usual view, and the upper and lower case versions |
| // [ standard, lowercase, uppercase ] |
| // The culture does not use AM or PM (likely all standard date formats use 24 hour time) |
| // null |
| AM: [ "AM", "am", "AM" ], |
| PM: [ "PM", "pm", "PM" ], |
| eras: [ |
| // eras in reverse chronological order. |
| // name: the name of the era in this culture (e.g. A.D., C.E.) |
| // start: when the era starts in ticks (gregorian, gmt), null if it is the earliest supported era. |
| // offset: offset in years from gregorian calendar |
| { |
| "name": "A.D.", |
| "start": null, |
| "offset": 0 |
| } |
| ], |
| // when a two digit year is given, it will never be parsed as a four digit |
| // year greater than this year (in the appropriate era for the culture) |
| // Set it as a full year (e.g. 2029) or use an offset format starting from |
| // the current year: "+19" would correspond to 2029 if the current year 2010. |
| twoDigitYearMax: 2029, |
| // set of predefined date and time patterns used by the culture |
| // these represent the format someone in this culture would expect |
| // to see given the portions of the date that are shown. |
| patterns: { |
| // short date pattern |
| d: "M/d/yyyy", |
| // long date pattern |
| D: "dddd, MMMM dd, yyyy", |
| // short time pattern |
| t: "h:mm tt", |
| // long time pattern |
| T: "h:mm:ss tt", |
| // long date, short time pattern |
| f: "dddd, MMMM dd, yyyy h:mm tt", |
| // long date, long time pattern |
| F: "dddd, MMMM dd, yyyy h:mm:ss tt", |
| // month/day pattern |
| M: "MMMM dd", |
| // month/year pattern |
| Y: "yyyy MMMM", |
| // S is a sortable format that does not vary by culture |
| S: "yyyy\u0027-\u0027MM\u0027-\u0027dd\u0027T\u0027HH\u0027:\u0027mm\u0027:\u0027ss" |
| } |
| // optional fields for each calendar: |
| /* |
| monthsGenitive: |
| Same as months but used when the day preceeds the month. |
| Omit if the culture has no genitive distinction in month names. |
| For an explaination of genitive months, see http://blogs.msdn.com/michkap/archive/2004/12/25/332259.aspx |
| convert: |
| Allows for the support of non-gregorian based calendars. This convert object is used to |
| to convert a date to and from a gregorian calendar date to handle parsing and formatting. |
| The two functions: |
| fromGregorian( date ) |
| Given the date as a parameter, return an array with parts [ year, month, day ] |
| corresponding to the non-gregorian based year, month, and day for the calendar. |
| toGregorian( year, month, day ) |
| Given the non-gregorian year, month, and day, return a new Date() object |
| set to the corresponding date in the gregorian calendar. |
| */ |
| } |
| }, |
| // For localized strings |
| messages: {} |
| }; |
| |
| Globalize.cultures[ "default" ].calendar = Globalize.cultures[ "default" ].calendars.standard; |
| |
| Globalize.cultures[ "en" ] = Globalize.cultures[ "default" ]; |
| |
| Globalize.cultureSelector = "en"; |
| |
| // |
| // private variables |
| // |
| |
| regexHex = /^0x[a-f0-9]+$/i; |
| regexInfinity = /^[+-]?infinity$/i; |
| regexParseFloat = /^[+-]?\d*\.?\d*(e[+-]?\d+)?$/; |
| regexTrim = /^\s+|\s+$/g; |
| |
| // |
| // private JavaScript utility functions |
| // |
| |
| arrayIndexOf = function( array, item ) { |
| if ( array.indexOf ) { |
| return array.indexOf( item ); |
| } |
| for ( var i = 0, length = array.length; i < length; i++ ) { |
| if ( array[i] === item ) { |
| return i; |
| } |
| } |
| return -1; |
| }; |
| |
| endsWith = function( value, pattern ) { |
| return value.substr( value.length - pattern.length ) === pattern; |
| }; |
| |
| extend = function( deep ) { |
| var options, name, src, copy, copyIsArray, clone, |
| target = arguments[0] || {}, |
| i = 1, |
| length = arguments.length, |
| deep = false; |
| |
| // Handle a deep copy situation |
| if ( typeof target === "boolean" ) { |
| deep = target; |
| target = arguments[1] || {}; |
| // skip the boolean and the target |
| i = 2; |
| } |
| |
| // Handle case when target is a string or something (possible in deep copy) |
| if ( typeof target !== "object" && !isFunction(target) ) { |
| target = {}; |
| } |
| |
| for ( ; i < length; i++ ) { |
| // Only deal with non-null/undefined values |
| if ( (options = arguments[ i ]) != null ) { |
| // Extend the base object |
| for ( name in options ) { |
| src = target[ name ]; |
| copy = options[ name ]; |
| |
| // Prevent never-ending loop |
| if ( target === copy ) { |
| continue; |
| } |
| |
| // Recurse if we're merging plain objects or arrays |
| if ( deep && copy && ( isObject(copy) || (copyIsArray = isArray(copy)) ) ) { |
| if ( copyIsArray ) { |
| copyIsArray = false; |
| clone = src && isArray(src) ? src : []; |
| |
| } else { |
| clone = src && isObject(src) ? src : {}; |
| } |
| |
| // Never move original objects, clone them |
| target[ name ] = extend( deep, clone, copy ); |
| |
| // Don't bring in undefined values |
| } else if ( copy !== undefined ) { |
| target[ name ] = copy; |
| } |
| } |
| } |
| } |
| |
| // Return the modified object |
| return target; |
| }; |
| |
| isArray = Array.isArray || function( obj ) { |
| return Object.prototype.toString.call( obj ) === "[object Array]"; |
| }; |
| |
| isFunction = function( obj ) { |
| return Object.prototype.toString.call( obj ) === "[object Function]"; |
| }; |
| |
| isObject = function( obj ) { |
| return Object.prototype.toString.call( obj ) === "[object Object]"; |
| }; |
| |
| startsWith = function( value, pattern ) { |
| return value.indexOf( pattern ) === 0; |
| }; |
| |
| trim = function( value ) { |
| return ( value + "" ).replace( regexTrim, "" ); |
| }; |
| |
| truncate = function( value ) { |
| if ( isNaN( value ) ) { |
| return NaN; |
| } |
| return Math[ value < 0 ? "ceil" : "floor" ]( value ); |
| }; |
| |
| zeroPad = function( str, count, left ) { |
| var l; |
| for ( l = str.length; l < count; l += 1 ) { |
| str = ( left ? ("0" + str) : (str + "0") ); |
| } |
| return str; |
| }; |
| |
| // |
| // private Globalization utility functions |
| // |
| |
| appendPreOrPostMatch = function( preMatch, strings ) { |
| // appends pre- and post- token match strings while removing escaped characters. |
| // Returns a single quote count which is used to determine if the token occurs |
| // in a string literal. |
| var quoteCount = 0, |
| escaped = false; |
| for ( var i = 0, il = preMatch.length; i < il; i++ ) { |
| var c = preMatch.charAt( i ); |
| switch ( c ) { |
| case "\'": |
| if ( escaped ) { |
| strings.push( "\'" ); |
| } |
| else { |
| quoteCount++; |
| } |
| escaped = false; |
| break; |
| case "\\": |
| if ( escaped ) { |
| strings.push( "\\" ); |
| } |
| escaped = !escaped; |
| break; |
| default: |
| strings.push( c ); |
| escaped = false; |
| break; |
| } |
| } |
| return quoteCount; |
| }; |
| |
| expandFormat = function( cal, format ) { |
| // expands unspecified or single character date formats into the full pattern. |
| format = format || "F"; |
| var pattern, |
| patterns = cal.patterns, |
| len = format.length; |
| if ( len === 1 ) { |
| pattern = patterns[ format ]; |
| if ( !pattern ) { |
| throw "Invalid date format string \'" + format + "\'."; |
| } |
| format = pattern; |
| } |
| else if ( len === 2 && format.charAt(0) === "%" ) { |
| // %X escape format -- intended as a custom format string that is only one character, not a built-in format. |
| format = format.charAt( 1 ); |
| } |
| return format; |
| }; |
| |
| formatDate = function( value, format, culture ) { |
| var cal = culture.calendar, |
| convert = cal.convert; |
| |
| if ( !format || !format.length || format === "i" ) { |
| var ret; |
| if ( culture && culture.name.length ) { |
| if ( convert ) { |
| // non-gregorian calendar, so we cannot use built-in toLocaleString() |
| ret = formatDate( value, cal.patterns.F, culture ); |
| } |
| else { |
| var eraDate = new Date( value.getTime() ), |
| era = getEra( value, cal.eras ); |
| eraDate.setFullYear( getEraYear(value, cal, era) ); |
| ret = eraDate.toLocaleString(); |
| } |
| } |
| else { |
| ret = value.toString(); |
| } |
| return ret; |
| } |
| |
| var eras = cal.eras, |
| sortable = format === "s"; |
| format = expandFormat( cal, format ); |
| |
| // Start with an empty string |
| ret = []; |
| var hour, |
| zeros = [ "0", "00", "000" ], |
| foundDay, |
| checkedDay, |
| dayPartRegExp = /([^d]|^)(d|dd)([^d]|$)/g, |
| quoteCount = 0, |
| tokenRegExp = getTokenRegExp(), |
| converted; |
| |
| function padZeros( num, c ) { |
| var r, s = num + ""; |
| if ( c > 1 && s.length < c ) { |
| r = ( zeros[c - 2] + s); |
| return r.substr( r.length - c, c ); |
| } |
| else { |
| r = s; |
| } |
| return r; |
| } |
| |
| function hasDay() { |
| if ( foundDay || checkedDay ) { |
| return foundDay; |
| } |
| foundDay = dayPartRegExp.test( format ); |
| checkedDay = true; |
| return foundDay; |
| } |
| |
| function getPart( date, part ) { |
| if ( converted ) { |
| return converted[ part ]; |
| } |
| switch ( part ) { |
| case 0: return date.getFullYear(); |
| case 1: return date.getMonth(); |
| case 2: return date.getDate(); |
| } |
| } |
| |
| if ( !sortable && convert ) { |
| converted = convert.fromGregorian( value ); |
| } |
| |
| for ( ; ; ) { |
| // Save the current index |
| var index = tokenRegExp.lastIndex, |
| // Look for the next pattern |
| ar = tokenRegExp.exec( format ); |
| |
| // Append the text before the pattern (or the end of the string if not found) |
| var preMatch = format.slice( index, ar ? ar.index : format.length ); |
| quoteCount += appendPreOrPostMatch( preMatch, ret ); |
| |
| if ( !ar ) { |
| break; |
| } |
| |
| // do not replace any matches that occur inside a string literal. |
| if ( quoteCount % 2 ) { |
| ret.push( ar[0] ); |
| continue; |
| } |
| |
| var current = ar[ 0 ], |
| clength = current.length; |
| |
| switch ( current ) { |
| case "ddd": |
| //Day of the week, as a three-letter abbreviation |
| case "dddd": |
| // Day of the week, using the full name |
| var names = ( clength === 3 ) ? cal.days.namesAbbr : cal.days.names; |
| ret.push( names[value.getDay()] ); |
| break; |
| case "d": |
| // Day of month, without leading zero for single-digit days |
| case "dd": |
| // Day of month, with leading zero for single-digit days |
| foundDay = true; |
| ret.push( |
| padZeros( getPart(value, 2), clength ) |
| ); |
| break; |
| case "MMM": |
| // Month, as a three-letter abbreviation |
| case "MMMM": |
| // Month, using the full name |
| var part = getPart( value, 1 ); |
| ret.push( |
| ( cal.monthsGenitive && hasDay() ) |
| ? |
| cal.monthsGenitive[ clength === 3 ? "namesAbbr" : "names" ][ part ] |
| : |
| cal.months[ clength === 3 ? "namesAbbr" : "names" ][ part ] |
| ); |
| break; |
| case "M": |
| // Month, as digits, with no leading zero for single-digit months |
| case "MM": |
| // Month, as digits, with leading zero for single-digit months |
| ret.push( |
| padZeros( getPart(value, 1) + 1, clength ) |
| ); |
| break; |
| case "y": |
| // Year, as two digits, but with no leading zero for years less than 10 |
| case "yy": |
| // Year, as two digits, with leading zero for years less than 10 |
| case "yyyy": |
| // Year represented by four full digits |
| part = converted ? converted[ 0 ] : getEraYear( value, cal, getEra(value, eras), sortable ); |
| if ( clength < 4 ) { |
| part = part % 100; |
| } |
| ret.push( |
| padZeros( part, clength ) |
| ); |
| break; |
| case "h": |
| // Hours with no leading zero for single-digit hours, using 12-hour clock |
| case "hh": |
| // Hours with leading zero for single-digit hours, using 12-hour clock |
| hour = value.getHours() % 12; |
| if ( hour === 0 ) hour = 12; |
| ret.push( |
| padZeros( hour, clength ) |
| ); |
| break; |
| case "H": |
| // Hours with no leading zero for single-digit hours, using 24-hour clock |
| case "HH": |
| // Hours with leading zero for single-digit hours, using 24-hour clock |
| ret.push( |
| padZeros( value.getHours(), clength ) |
| ); |
| break; |
| case "m": |
| // Minutes with no leading zero for single-digit minutes |
| case "mm": |
| // Minutes with leading zero for single-digit minutes |
| ret.push( |
| padZeros( value.getMinutes(), clength ) |
| ); |
| break; |
| case "s": |
| // Seconds with no leading zero for single-digit seconds |
| case "ss": |
| // Seconds with leading zero for single-digit seconds |
| ret.push( |
| padZeros( value.getSeconds(), clength ) |
| ); |
| break; |
| case "t": |
| // One character am/pm indicator ("a" or "p") |
| case "tt": |
| // Multicharacter am/pm indicator |
| part = value.getHours() < 12 ? ( cal.AM ? cal.AM[0] : " " ) : ( cal.PM ? cal.PM[0] : " " ); |
| ret.push( clength === 1 ? part.charAt(0) : part ); |
| break; |
| case "f": |
| // Deciseconds |
| case "ff": |
| // Centiseconds |
| case "fff": |
| // Milliseconds |
| ret.push( |
| padZeros( value.getMilliseconds(), 3 ).substr( 0, clength ) |
| ); |
| break; |
| case "z": |
| // Time zone offset, no leading zero |
| case "zz": |
| // Time zone offset with leading zero |
| hour = value.getTimezoneOffset() / 60; |
| ret.push( |
| ( hour <= 0 ? "+" : "-" ) + padZeros( Math.floor(Math.abs(hour)), clength ) |
| ); |
| break; |
| case "zzz": |
| // Time zone offset with leading zero |
| hour = value.getTimezoneOffset() / 60; |
| ret.push( |
| ( hour <= 0 ? "+" : "-" ) + padZeros( Math.floor(Math.abs(hour)), 2 ) |
| // Hard coded ":" separator, rather than using cal.TimeSeparator |
| // Repeated here for consistency, plus ":" was already assumed in date parsing. |
| + ":" + padZeros( Math.abs(value.getTimezoneOffset() % 60), 2 ) |
| ); |
| break; |
| case "g": |
| case "gg": |
| if ( cal.eras ) { |
| ret.push( |
| cal.eras[ getEra(value, eras) ].name |
| ); |
| } |
| break; |
| case "/": |
| ret.push( cal["/"] ); |
| break; |
| default: |
| throw "Invalid date format pattern \'" + current + "\'."; |
| break; |
| } |
| } |
| return ret.join( "" ); |
| }; |
| |
| // formatNumber |
| (function() { |
| var expandNumber; |
| |
| expandNumber = function( number, precision, formatInfo ) { |
| var groupSizes = formatInfo.groupSizes, |
| curSize = groupSizes[ 0 ], |
| curGroupIndex = 1, |
| factor = Math.pow( 10, precision ), |
| rounded = Math.round( number * factor ) / factor; |
| |
| if ( !isFinite(rounded) ) { |
| rounded = number; |
| } |
| number = rounded; |
| |
| var numberString = number+"", |
| right = "", |
| split = numberString.split( /e/i ), |
| exponent = split.length > 1 ? parseInt( split[1], 10 ) : 0; |
| numberString = split[ 0 ]; |
| split = numberString.split( "." ); |
| numberString = split[ 0 ]; |
| right = split.length > 1 ? split[ 1 ] : ""; |
| |
| var l; |
| if ( exponent > 0 ) { |
| right = zeroPad( right, exponent, false ); |
| numberString += right.slice( 0, exponent ); |
| right = right.substr( exponent ); |
| } |
| else if ( exponent < 0 ) { |
| exponent = -exponent; |
| numberString = zeroPad( numberString, exponent + 1 ); |
| right = numberString.slice( -exponent, numberString.length ) + right; |
| numberString = numberString.slice( 0, -exponent ); |
| } |
| |
| if ( precision > 0 ) { |
| right = formatInfo[ "." ] + |
| ( (right.length > precision) ? right.slice(0, precision) : zeroPad(right, precision) ); |
| } |
| else { |
| right = ""; |
| } |
| |
| var stringIndex = numberString.length - 1, |
| sep = formatInfo[ "," ], |
| ret = ""; |
| |
| while ( stringIndex >= 0 ) { |
| if ( curSize === 0 || curSize > stringIndex ) { |
| return numberString.slice( 0, stringIndex + 1 ) + ( ret.length ? (sep + ret + right) : right ); |
| } |
| ret = numberString.slice( stringIndex - curSize + 1, stringIndex + 1 ) + ( ret.length ? (sep + ret) : "" ); |
| |
| stringIndex -= curSize; |
| |
| if ( curGroupIndex < groupSizes.length ) { |
| curSize = groupSizes[ curGroupIndex ]; |
| curGroupIndex++; |
| } |
| } |
| |
| return numberString.slice( 0, stringIndex + 1 ) + sep + ret + right; |
| }; |
| |
| formatNumber = function( value, format, culture ) { |
| if ( !isFinite(value) ) { |
| if ( value === Infinity ) { |
| return culture.numberFormat.positiveInfinity; |
| } |
| if ( value === -Infinity ) { |
| return culture.numberFormat.negativeInfinity; |
| } |
| return culture.numberFormat.NaN; |
| } |
| if ( !format || format === "i" ) { |
| return culture.name.length ? value.toLocaleString() : value.toString(); |
| } |
| format = format || "D"; |
| |
| var nf = culture.numberFormat, |
| number = Math.abs( value ), |
| precision = -1, |
| pattern; |
| if ( format.length > 1 ) precision = parseInt( format.slice(1), 10 ); |
| |
| var current = format.charAt( 0 ).toUpperCase(), |
| formatInfo; |
| |
| switch ( current ) { |
| case "D": |
| pattern = "n"; |
| number = truncate( number ); |
| if ( precision !== -1 ) { |
| number = zeroPad( "" + number, precision, true ); |
| } |
| if ( value < 0 ) number = "-" + number; |
| break; |
| case "N": |
| formatInfo = nf; |
| // fall through |
| case "C": |
| formatInfo = formatInfo || nf.currency; |
| // fall through |
| case "P": |
| formatInfo = formatInfo || nf.percent; |
| pattern = value < 0 ? formatInfo.pattern[ 0 ] : ( formatInfo.pattern[1] || "n" ); |
| if ( precision === -1 ) precision = formatInfo.decimals; |
| number = expandNumber( number * (current === "P" ? 100 : 1), precision, formatInfo ); |
| break; |
| default: |
| throw "Bad number format specifier: " + current; |
| } |
| |
| var patternParts = /n|\$|-|%/g, |
| ret = ""; |
| for ( ; ; ) { |
| var index = patternParts.lastIndex, |
| ar = patternParts.exec( pattern ); |
| |
| ret += pattern.slice( index, ar ? ar.index : pattern.length ); |
| |
| if ( !ar ) { |
| break; |
| } |
| |
| switch ( ar[0] ) { |
| case "n": |
| ret += number; |
| break; |
| case "$": |
| ret += nf.currency.symbol; |
| break; |
| case "-": |
| // don't make 0 negative |
| if ( /[1-9]/.test(number) ) { |
| ret += nf[ "-" ]; |
| } |
| break; |
| case "%": |
| ret += nf.percent.symbol; |
| break; |
| } |
| } |
| |
| return ret; |
| }; |
| |
| }()); |
| |
| getTokenRegExp = function() { |
| // regular expression for matching date and time tokens in format strings. |
| return /\/|dddd|ddd|dd|d|MMMM|MMM|MM|M|yyyy|yy|y|hh|h|HH|H|mm|m|ss|s|tt|t|fff|ff|f|zzz|zz|z|gg|g/g; |
| }; |
| |
| getEra = function( date, eras ) { |
| if ( !eras ) return 0; |
| var start, ticks = date.getTime(); |
| for ( var i = 0, l = eras.length; i < l; i++ ) { |
| start = eras[ i ].start; |
| if ( start === null || ticks >= start ) { |
| return i; |
| } |
| } |
| return 0; |
| }; |
| |
| getEraYear = function( date, cal, era, sortable ) { |
| var year = date.getFullYear(); |
| if ( !sortable && cal.eras ) { |
| // convert normal gregorian year to era-shifted gregorian |
| // year by subtracting the era offset |
| year -= cal.eras[ era ].offset; |
| } |
| return year; |
| }; |
| |
| // parseExact |
| (function() { |
| var expandYear, |
| getDayIndex, |
| getMonthIndex, |
| getParseRegExp, |
| outOfRange, |
| toUpper, |
| toUpperArray; |
| |
| expandYear = function( cal, year ) { |
| // expands 2-digit year into 4 digits. |
| if ( year < 100 ) { |
| var now = new Date(), |
| era = getEra( now ), |
| curr = getEraYear( now, cal, era ), |
| twoDigitYearMax = cal.twoDigitYearMax; |
| twoDigitYearMax = typeof twoDigitYearMax === "string" ? new Date().getFullYear() % 100 + parseInt( twoDigitYearMax, 10 ) : twoDigitYearMax; |
| year += curr - ( curr % 100 ); |
| if ( year > twoDigitYearMax ) { |
| year -= 100; |
| } |
| } |
| return year; |
| }; |
| |
| getDayIndex = function ( cal, value, abbr ) { |
| var ret, |
| days = cal.days, |
| upperDays = cal._upperDays; |
| if ( !upperDays ) { |
| cal._upperDays = upperDays = [ |
| toUpperArray( days.names ), |
| toUpperArray( days.namesAbbr ), |
| toUpperArray( days.namesShort ) |
| ]; |
| } |
| value = toUpper( value ); |
| if ( abbr ) { |
| ret = arrayIndexOf( upperDays[1], value ); |
| if ( ret === -1 ) { |
| ret = arrayIndexOf( upperDays[2], value ); |
| } |
| } |
| else { |
| ret = arrayIndexOf( upperDays[0], value ); |
| } |
| return ret; |
| }; |
| |
| getMonthIndex = function( cal, value, abbr ) { |
| var months = cal.months, |
| monthsGen = cal.monthsGenitive || cal.months, |
| upperMonths = cal._upperMonths, |
| upperMonthsGen = cal._upperMonthsGen; |
| if ( !upperMonths ) { |
| cal._upperMonths = upperMonths = [ |
| toUpperArray( months.names ), |
| toUpperArray( months.namesAbbr ) |
| ]; |
| cal._upperMonthsGen = upperMonthsGen = [ |
| toUpperArray( monthsGen.names ), |
| toUpperArray( monthsGen.namesAbbr ) |
| ]; |
| } |
| value = toUpper( value ); |
| var i = arrayIndexOf( abbr ? upperMonths[1] : upperMonths[0], value ); |
| if ( i < 0 ) { |
| i = arrayIndexOf( abbr ? upperMonthsGen[1] : upperMonthsGen[0], value ); |
| } |
| return i; |
| }; |
| |
| getParseRegExp = function( cal, format ) { |
| // converts a format string into a regular expression with groups that |
| // can be used to extract date fields from a date string. |
| // check for a cached parse regex. |
| var re = cal._parseRegExp; |
| if ( !re ) { |
| cal._parseRegExp = re = {}; |
| } |
| else { |
| var reFormat = re[ format ]; |
| if ( reFormat ) { |
| return reFormat; |
| } |
| } |
| |
| // expand single digit formats, then escape regular expression characters. |
| var expFormat = expandFormat( cal, format ).replace( /([\^\$\.\*\+\?\|\[\]\(\)\{\}])/g, "\\\\$1" ), |
| regexp = [ "^" ], |
| groups = [], |
| index = 0, |
| quoteCount = 0, |
| tokenRegExp = getTokenRegExp(), |
| match; |
| |
| // iterate through each date token found. |
| while ( (match = tokenRegExp.exec(expFormat)) !== null ) { |
| var preMatch = expFormat.slice( index, match.index ); |
| index = tokenRegExp.lastIndex; |
| |
| // don't replace any matches that occur inside a string literal. |
| quoteCount += appendPreOrPostMatch( preMatch, regexp ); |
| if ( quoteCount % 2 ) { |
| regexp.push( match[0] ); |
| continue; |
| } |
| |
| // add a regex group for the token. |
| var m = match[ 0 ], |
| len = m.length, |
| add; |
| switch ( m ) { |
| case "dddd": case "ddd": |
| case "MMMM": case "MMM": |
| case "gg": case "g": |
| add = "(\\D+)"; |
| break; |
| case "tt": case "t": |
| add = "(\\D*)"; |
| break; |
| case "yyyy": |
| case "fff": |
| case "ff": |
| case "f": |
| add = "(\\d{" + len + "})"; |
| break; |
| case "dd": case "d": |
| case "MM": case "M": |
| case "yy": case "y": |
| case "HH": case "H": |
| case "hh": case "h": |
| case "mm": case "m": |
| case "ss": case "s": |
| add = "(\\d\\d?)"; |
| break; |
| case "zzz": |
| add = "([+-]?\\d\\d?:\\d{2})"; |
| break; |
| case "zz": case "z": |
| add = "([+-]?\\d\\d?)"; |
| break; |
| case "/": |
| add = "(\\" + cal[ "/" ] + ")"; |
| break; |
| default: |
| throw "Invalid date format pattern \'" + m + "\'."; |
| break; |
| } |
| if ( add ) { |
| regexp.push( add ); |
| } |
| groups.push( match[0] ); |
| } |
| appendPreOrPostMatch( expFormat.slice(index), regexp ); |
| regexp.push( "$" ); |
| |
| // allow whitespace to differ when matching formats. |
| var regexpStr = regexp.join( "" ).replace( /\s+/g, "\\s+" ), |
| parseRegExp = { "regExp": regexpStr, "groups": groups }; |
| |
| // cache the regex for this format. |
| return re[ format ] = parseRegExp; |
| }; |
| |
| outOfRange = function( value, low, high ) { |
| return value < low || value > high; |
| }; |
| |
| toUpper = function( value ) { |
| // "he-IL" has non-breaking space in weekday names. |
| return value.split( "\u00A0" ).join( " " ).toUpperCase(); |
| }; |
| |
| toUpperArray = function( arr ) { |
| var results = []; |
| for ( var i = 0, l = arr.length; i < l; i++ ) { |
| results[ i ] = toUpper( arr[i] ); |
| } |
| return results; |
| }; |
| |
| parseExact = function( value, format, culture ) { |
| // try to parse the date string by matching against the format string |
| // while using the specified culture for date field names. |
| value = trim( value ); |
| var cal = culture.calendar, |
| // convert date formats into regular expressions with groupings. |
| // use the regexp to determine the input format and extract the date fields. |
| parseInfo = getParseRegExp( cal, format ), |
| match = new RegExp( parseInfo.regExp ).exec( value ); |
| if ( match === null ) { |
| return null; |
| } |
| // found a date format that matches the input. |
| var groups = parseInfo.groups, |
| era = null, year = null, month = null, date = null, weekDay = null, |
| hour = 0, hourOffset, min = 0, sec = 0, msec = 0, tzMinOffset = null, |
| pmHour = false; |
| // iterate the format groups to extract and set the date fields. |
| for ( var j = 0, jl = groups.length; j < jl; j++ ) { |
| var matchGroup = match[ j + 1 ]; |
| if ( matchGroup ) { |
| var current = groups[ j ], |
| clength = current.length, |
| matchInt = parseInt( matchGroup, 10 ); |
| switch ( current ) { |
| case "dd": case "d": |
| // Day of month. |
| date = matchInt; |
| // check that date is generally in valid range, also checking overflow below. |
| if ( outOfRange(date, 1, 31) ) return null; |
| break; |
| case "MMM": case "MMMM": |
| month = getMonthIndex( cal, matchGroup, clength === 3 ); |
| if ( outOfRange(month, 0, 11) ) return null; |
| break; |
| case "M": case "MM": |
| // Month. |
| month = matchInt - 1; |
| if ( outOfRange(month, 0, 11) ) return null; |
| break; |
| case "y": case "yy": |
| case "yyyy": |
| year = clength < 4 ? expandYear( cal, matchInt ) : matchInt; |
| if ( outOfRange(year, 0, 9999) ) return null; |
| break; |
| case "h": case "hh": |
| // Hours (12-hour clock). |
| hour = matchInt; |
| if ( hour === 12 ) hour = 0; |
| if ( outOfRange(hour, 0, 11) ) return null; |
| break; |
| case "H": case "HH": |
| // Hours (24-hour clock). |
| hour = matchInt; |
| if ( outOfRange(hour, 0, 23) ) return null; |
| break; |
| case "m": case "mm": |
| // Minutes. |
| min = matchInt; |
| if ( outOfRange(min, 0, 59) ) return null; |
| break; |
| case "s": case "ss": |
| // Seconds. |
| sec = matchInt; |
| if ( outOfRange(sec, 0, 59) ) return null; |
| break; |
| case "tt": case "t": |
| // AM/PM designator. |
| // see if it is standard, upper, or lower case PM. If not, ensure it is at least one of |
| // the AM tokens. If not, fail the parse for this format. |
| pmHour = cal.PM && ( matchGroup === cal.PM[0] || matchGroup === cal.PM[1] || matchGroup === cal.PM[2] ); |
| if ( |
| !pmHour && ( |
| !cal.AM || ( matchGroup !== cal.AM[0] && matchGroup !== cal.AM[1] && matchGroup !== cal.AM[2] ) |
| ) |
| ) return null; |
| break; |
| case "f": |
| // Deciseconds. |
| case "ff": |
| // Centiseconds. |
| case "fff": |
| // Milliseconds. |
| msec = matchInt * Math.pow( 10, 3 - clength ); |
| if ( outOfRange(msec, 0, 999) ) return null; |
| break; |
| case "ddd": |
| // Day of week. |
| case "dddd": |
| // Day of week. |
| weekDay = getDayIndex( cal, matchGroup, clength === 3 ); |
| if ( outOfRange(weekDay, 0, 6) ) return null; |
| break; |
| case "zzz": |
| // Time zone offset in +/- hours:min. |
| var offsets = matchGroup.split( /:/ ); |
| if ( offsets.length !== 2 ) return null; |
| hourOffset = parseInt( offsets[0], 10 ); |
| if ( outOfRange(hourOffset, -12, 13) ) return null; |
| var minOffset = parseInt( offsets[1], 10 ); |
| if ( outOfRange(minOffset, 0, 59) ) return null; |
| tzMinOffset = ( hourOffset * 60 ) + ( startsWith(matchGroup, "-") ? -minOffset : minOffset ); |
| break; |
| case "z": case "zz": |
| // Time zone offset in +/- hours. |
| hourOffset = matchInt; |
| if ( outOfRange(hourOffset, -12, 13) ) return null; |
| tzMinOffset = hourOffset * 60; |
| break; |
| case "g": case "gg": |
| var eraName = matchGroup; |
| if ( !eraName || !cal.eras ) return null; |
| eraName = trim( eraName.toLowerCase() ); |
| for ( var i = 0, l = cal.eras.length; i < l; i++ ) { |
| if ( eraName === cal.eras[i].name.toLowerCase() ) { |
| era = i; |
| break; |
| } |
| } |
| // could not find an era with that name |
| if ( era === null ) return null; |
| break; |
| } |
| } |
| } |
| var result = new Date(), defaultYear, convert = cal.convert; |
| defaultYear = convert ? convert.fromGregorian( result )[ 0 ] : result.getFullYear(); |
| if ( year === null ) { |
| year = defaultYear; |
| } |
| else if ( cal.eras ) { |
| // year must be shifted to normal gregorian year |
| // but not if year was not specified, its already normal gregorian |
| // per the main if clause above. |
| year += cal.eras[( era || 0 )].offset; |
| } |
| // set default day and month to 1 and January, so if unspecified, these are the defaults |
| // instead of the current day/month. |
| if ( month === null ) { |
| month = 0; |
| } |
| if ( date === null ) { |
| date = 1; |
| } |
| // now have year, month, and date, but in the culture's calendar. |
| // convert to gregorian if necessary |
| if ( convert ) { |
| result = convert.toGregorian( year, month, date ); |
| // conversion failed, must be an invalid match |
| if ( result === null ) return null; |
| } |
| else { |
| // have to set year, month and date together to avoid overflow based on current date. |
| result.setFullYear( year, month, date ); |
| // check to see if date overflowed for specified month (only checked 1-31 above). |
| if ( result.getDate() !== date ) return null; |
| // invalid day of week. |
| if ( weekDay !== null && result.getDay() !== weekDay ) { |
| return null; |
| } |
| } |
| // if pm designator token was found make sure the hours fit the 24-hour clock. |
| if ( pmHour && hour < 12 ) { |
| hour += 12; |
| } |
| result.setHours( hour, min, sec, msec ); |
| if ( tzMinOffset !== null ) { |
| // adjust timezone to utc before applying local offset. |
| var adjustedMin = result.getMinutes() - ( tzMinOffset + result.getTimezoneOffset() ); |
| // Safari limits hours and minutes to the range of -127 to 127. We need to use setHours |
| // to ensure both these fields will not exceed this range. adjustedMin will range |
| // somewhere between -1440 and 1500, so we only need to split this into hours. |
| result.setHours( result.getHours() + parseInt(adjustedMin / 60, 10), adjustedMin % 60 ); |
| } |
| return result; |
| }; |
| }()); |
| |
| parseNegativePattern = function( value, nf, negativePattern ) { |
| var neg = nf[ "-" ], |
| pos = nf[ "+" ], |
| ret; |
| switch ( negativePattern ) { |
| case "n -": |
| neg = " " + neg; |
| pos = " " + pos; |
| // fall through |
| case "n-": |
| if ( endsWith(value, neg) ) { |
| ret = [ "-", value.substr(0, value.length - neg.length) ]; |
| } |
| else if ( endsWith(value, pos) ) { |
| ret = [ "+", value.substr(0, value.length - pos.length) ]; |
| } |
| break; |
| case "- n": |
| neg += " "; |
| pos += " "; |
| // fall through |
| case "-n": |
| if ( startsWith(value, neg) ) { |
| ret = [ "-", value.substr(neg.length) ]; |
| } |
| else if ( startsWith(value, pos) ) { |
| ret = [ "+", value.substr(pos.length) ]; |
| } |
| break; |
| case "(n)": |
| if ( startsWith(value, "(") && endsWith(value, ")") ) { |
| ret = [ "-", value.substr(1, value.length - 2) ]; |
| } |
| break; |
| } |
| return ret || [ "", value ]; |
| }; |
| |
| // |
| // public instance functions |
| // |
| |
| Globalize.prototype.findClosestCulture = function( cultureSelector ) { |
| return Globalize.findClosestCulture.call( this, cultureSelector ); |
| }; |
| |
| Globalize.prototype.format = function( value, format, cultureSelector ) { |
| return Globalize.format.call( this, value, format, cultureSelector ); |
| }; |
| |
| Globalize.prototype.localize = function( key, cultureSelector ) { |
| return Globalize.localize.call( this, key, cultureSelector ); |
| }; |
| |
| Globalize.prototype.parseInt = function( value, radix, cultureSelector ) { |
| return Globalize.parseInt.call( this, value, radix, cultureSelector ); |
| }; |
| |
| Globalize.prototype.parseFloat = function( value, radix, cultureSelector ) { |
| return Globalize.parseFloat.call( this, value, radix, cultureSelector ); |
| }; |
| |
| Globalize.prototype.culture = function( cultureSelector ) { |
| return Globalize.culture.call( this, cultureSelector ); |
| }; |
| |
| // |
| // public singleton functions |
| // |
| |
| Globalize.addCultureInfo = function( cultureName, baseCultureName, info ) { |
| |
| var base = {}, |
| isNew = false; |
| |
| if ( typeof cultureName !== "string" ) { |
| // cultureName argument is optional string. If not specified, assume info is first |
| // and only argument. Specified info deep-extends current culture. |
| info = cultureName; |
| cultureName = this.culture().name; |
| base = this.cultures[ cultureName ]; |
| } else if ( typeof baseCultureName !== "string" ) { |
| // baseCultureName argument is optional string. If not specified, assume info is second |
| // argument. Specified info deep-extends specified culture. |
| // If specified culture does not exist, create by deep-extending default |
| info = baseCultureName; |
| isNew = ( this.cultures[ cultureName ] == null ); |
| base = this.cultures[ cultureName ] || this.cultures[ "default" ]; |
| } else { |
| // cultureName and baseCultureName specified. Assume a new culture is being created |
| // by deep-extending an specified base culture |
| isNew = true; |
| base = this.cultures[ baseCultureName ]; |
| } |
| |
| this.cultures[ cultureName ] = extend(true, {}, |
| base, |
| info |
| ); |
| // Make the standard calendar the current culture if it's a new culture |
| if ( isNew ) { |
| this.cultures[ cultureName ].calendar = this.cultures[ cultureName ].calendars.standard; |
| } |
| }; |
| |
| Globalize.findClosestCulture = function( name ) { |
| var match; |
| if ( !name ) { |
| return this.findClosestCulture( this.cultureSelector ) || this.cultures[ "default" ]; |
| } |
| if ( typeof name === "string" ) { |
| name = name.split( "," ); |
| } |
| if ( isArray(name) ) { |
| var lang, |
| cultures = this.cultures, |
| list = name, |
| i, l = list.length, |
| prioritized = []; |
| for ( i = 0; i < l; i++ ) { |
| name = trim( list[i] ); |
| var pri, parts = name.split( ";" ); |
| lang = trim( parts[0] ); |
| if ( parts.length === 1 ) { |
| pri = 1; |
| } |
| else { |
| name = trim( parts[1] ); |
| if ( name.indexOf("q=") === 0 ) { |
| name = name.substr( 2 ); |
| pri = parseFloat( name ); |
| pri = isNaN( pri ) ? 0 : pri; |
| } |
| else { |
| pri = 1; |
| } |
| } |
| prioritized.push({ lang: lang, pri: pri }); |
| } |
| prioritized.sort(function( a, b ) { |
| return a.pri < b.pri ? 1 : -1; |
| }); |
| |
| // exact match |
| for ( i = 0; i < l; i++ ) { |
| lang = prioritized[ i ].lang; |
| match = cultures[ lang ]; |
| if ( match ) { |
| return match; |
| } |
| } |
| |
| // neutral language match |
| for ( i = 0; i < l; i++ ) { |
| lang = prioritized[ i ].lang; |
| do { |
| var index = lang.lastIndexOf( "-" ); |
| if ( index === -1 ) { |
| break; |
| } |
| // strip off the last part. e.g. en-US => en |
| lang = lang.substr( 0, index ); |
| match = cultures[ lang ]; |
| if ( match ) { |
| return match; |
| } |
| } |
| while ( 1 ); |
| } |
| |
| // last resort: match first culture using that language |
| for ( i = 0; i < l; i++ ) { |
| lang = prioritized[ i ].lang; |
| for ( var cultureKey in cultures ) { |
| var culture = cultures[ cultureKey ]; |
| if ( culture.language == lang ) { |
| return culture; |
| } |
| } |
| } |
| } |
| else if ( typeof name === "object" ) { |
| return name; |
| } |
| return match || null; |
| }; |
| |
| Globalize.format = function( value, format, cultureSelector ) { |
| culture = this.findClosestCulture( cultureSelector ); |
| if ( value instanceof Date ) { |
| value = formatDate( value, format, culture ); |
| } |
| else if ( typeof value === "number" ) { |
| value = formatNumber( value, format, culture ); |
| } |
| return value; |
| }; |
| |
| Globalize.localize = function( key, cultureSelector ) { |
| return this.findClosestCulture( cultureSelector ).messages[ key ] || |
| this.cultures[ "default" ].messages[ key ]; |
| }; |
| |
| Globalize.parseDate = function( value, formats, culture ) { |
| culture = this.findClosestCulture( culture ); |
| |
| var date, prop, patterns; |
| if ( formats ) { |
| if ( typeof formats === "string" ) { |
| formats = [ formats ]; |
| } |
| if ( formats.length ) { |
| for ( var i = 0, l = formats.length; i < l; i++ ) { |
| var format = formats[ i ]; |
| if ( format ) { |
| date = parseExact( value, format, culture ); |
| if ( date ) { |
| break; |
| } |
| } |
| } |
| } |
| } else { |
| patterns = culture.calendar.patterns; |
| for ( prop in patterns ) { |
| date = parseExact( value, patterns[prop], culture ); |
| if ( date ) { |
| break; |
| } |
| } |
| } |
| |
| return date || null; |
| }; |
| |
| Globalize.parseInt = function( value, radix, cultureSelector ) { |
| return truncate( Globalize.parseFloat(value, radix, cultureSelector) ); |
| }; |
| |
| Globalize.parseFloat = function( value, radix, cultureSelector ) { |
| // radix argument is optional |
| if ( typeof radix !== "number" ) { |
| cultureSelector = radix; |
| radix = 10; |
| } |
| |
| var culture = this.findClosestCulture( cultureSelector ); |
| var ret = NaN, |
| nf = culture.numberFormat; |
| |
| if ( value.indexOf(culture.numberFormat.currency.symbol) > -1 ) { |
| // remove currency symbol |
| value = value.replace( culture.numberFormat.currency.symbol, "" ); |
| // replace decimal seperator |
| value = value.replace( culture.numberFormat.currency["."], culture.numberFormat["."] ); |
| } |
| |
| // trim leading and trailing whitespace |
| value = trim( value ); |
| |
| // allow infinity or hexidecimal |
| if ( regexInfinity.test(value) ) { |
| ret = parseFloat( value ); |
| } |
| else if ( !radix && regexHex.test(value) ) { |
| ret = parseInt( value, 16 ); |
| } |
| else { |
| |
| // determine sign and number |
| var signInfo = parseNegativePattern( value, nf, nf.pattern[0] ), |
| sign = signInfo[ 0 ], |
| num = signInfo[ 1 ]; |
| |
| // #44 - try parsing as "(n)" |
| if ( sign === "" && nf.pattern[0] !== "(n)" ) { |
| signInfo = parseNegativePattern( value, nf, "(n)" ); |
| sign = signInfo[ 0 ]; |
| num = signInfo[ 1 ]; |
| } |
| |
| // try parsing as "-n" |
| if ( sign === "" && nf.pattern[0] !== "-n" ) { |
| signInfo = parseNegativePattern( value, nf, "-n" ); |
| sign = signInfo[ 0 ]; |
| num = signInfo[ 1 ]; |
| } |
| |
| sign = sign || "+"; |
| |
| // determine exponent and number |
| var exponent, |
| intAndFraction, |
| exponentPos = num.indexOf( "e" ); |
| if ( exponentPos < 0 ) exponentPos = num.indexOf( "E" ); |
| if ( exponentPos < 0 ) { |
| intAndFraction = num; |
| exponent = null; |
| } |
| else { |
| intAndFraction = num.substr( 0, exponentPos ); |
| exponent = num.substr( exponentPos + 1 ); |
| } |
| // determine decimal position |
| var integer, |
| fraction, |
| decSep = nf[ "." ], |
| decimalPos = intAndFraction.indexOf( decSep ); |
| if ( decimalPos < 0 ) { |
| integer = intAndFraction; |
| fraction = null; |
| } |
| else { |
| integer = intAndFraction.substr( 0, decimalPos ); |
| fraction = intAndFraction.substr( decimalPos + decSep.length ); |
| } |
| // handle groups (e.g. 1,000,000) |
| var groupSep = nf[ "," ]; |
| integer = integer.split( groupSep ).join( "" ); |
| var altGroupSep = groupSep.replace( /\u00A0/g, " " ); |
| if ( groupSep !== altGroupSep ) { |
| integer = integer.split( altGroupSep ).join( "" ); |
| } |
| // build a natively parsable number string |
| var p = sign + integer; |
| if ( fraction !== null ) { |
| p += "." + fraction; |
| } |
| if ( exponent !== null ) { |
| // exponent itself may have a number patternd |
| var expSignInfo = parseNegativePattern( exponent, nf, "-n" ); |
| p += "e" + ( expSignInfo[0] || "+" ) + expSignInfo[ 1 ]; |
| } |
| if ( regexParseFloat.test(p) ) { |
| ret = parseFloat( p ); |
| } |
| } |
| return ret; |
| }; |
| |
| Globalize.culture = function( cultureSelector ) { |
| // setter |
| if ( typeof cultureSelector !== "undefined" ) { |
| this.cultureSelector = cultureSelector; |
| } |
| // getter |
| return this.findClosestCulture( cultureSelector ) || this.culture[ "default" ]; |
| }; |
| |
| window.Globalize = Globalize; |
| |
| if ( typeof define === "function" ) { |
| define( "globalize/globalize", [], function () { return Globalize; } ); |
| } |
| |
| }( window )); |
| |
| /** |
| * @fileoverview gl-matrix - High performance matrix and vector operations for WebGL |
| * @author Brandon Jones |
| * @author Colin MacKenzie IV |
| * @version 1.3.7 |
| */ |
| |
| /* |
| * Copyright (c) 2012 Brandon Jones, Colin MacKenzie IV |
| * |
| * This software is provided 'as-is', without any express or implied |
| * warranty. In no event will the authors be held liable for any damages |
| * arising from the use of this software. |
| * |
| * Permission is granted to anyone to use this software for any purpose, |
| * including commercial applications, and to alter it and redistribute it |
| * freely, subject to the following restrictions: |
| * |
| * 1. The origin of this software must not be misrepresented; you must not |
| * claim that you wrote the original software. If you use this software |
| * in a product, an acknowledgment in the product documentation would be |
| * appreciated but is not required. |
| * |
| * 2. Altered source versions must be plainly marked as such, and must not |
| * be misrepresented as being the original software. |
| * |
| * 3. This notice may not be removed or altered from any source |
| * distribution. |
| */ |
| |
| // Updated to use a modification of the "returnExportsGlobal" pattern from https://github.com/umdjs/umd |
| |
| (function (root, factory) { |
| if (typeof exports === 'object') { |
| // Node. Does not work with strict CommonJS, but |
| // only CommonJS-like enviroments that support module.exports, |
| // like Node. |
| module.exports = factory(global); |
| } else if (typeof define === 'function' && define.amd) { |
| // AMD. Register as an anonymous module. |
| define([], function () { |
| return factory(root); |
| }); |
| } else { |
| // Specific initialization for TIZEN Web UI Framework |
| root.initGlMatrix = function ( targetRoot ) { |
| factory( targetRoot || root ); |
| }; |
| } |
| }(this, function (root) { |
| "use strict"; |
| |
| // Tweak to your liking |
| var FLOAT_EPSILON = 0.000001; |
| |
| var glMath = {}; |
| (function() { |
| if (typeof(Float32Array) != 'undefined') { |
| var y = new Float32Array(1); |
| var i = new Int32Array(y.buffer); |
| |
| /** |
| * Fast way to calculate the inverse square root, |
| * see http://jsperf.com/inverse-square-root/5 |
| * |
| * If typed arrays are not available, a slower |
| * implementation will be used. |
| * |
| * @param {Number} number the number |
| * @returns {Number} Inverse square root |
| */ |
| glMath.invsqrt = function(number) { |
| var x2 = number * 0.5; |
| y[0] = number; |
| var threehalfs = 1.5; |
| |
| i[0] = 0x5f3759df - (i[0] >> 1); |
| |
| var number2 = y[0]; |
| |
| return number2 * (threehalfs - (x2 * number2 * number2)); |
| }; |
| } else { |
| glMath.invsqrt = function(number) { return 1.0 / Math.sqrt(number); }; |
| } |
| })(); |
| |
| /** |
| * @class System-specific optimal array type |
| * @name MatrixArray |
| */ |
| var MatrixArray = null; |
| |
| // explicitly sets and returns the type of array to use within glMatrix |
| function setMatrixArrayType(type) { |
| MatrixArray = type; |
| return MatrixArray; |
| } |
| |
| // auto-detects and returns the best type of array to use within glMatrix, falling |
| // back to Array if typed arrays are unsupported |
| function determineMatrixArrayType() { |
| MatrixArray = (typeof Float32Array !== 'undefined') ? Float32Array : Array; |
| return MatrixArray; |
| } |
| |
| determineMatrixArrayType(); |
| |
| /** |
| * @class 3 Dimensional Vector |
| * @name vec3 |
| */ |
| var vec3 = {}; |
| |
| /** |
| * Creates a new instance of a vec3 using the default array type |
| * Any javascript array-like objects containing at least 3 numeric elements can serve as a vec3 |
| * |
| * @param {vec3} [vec] vec3 containing values to initialize with |
| * |
| * @returns {vec3} New vec3 |
| */ |
| vec3.create = function (vec) { |
| var dest = new MatrixArray(3); |
| |
| if (vec) { |
| dest[0] = vec[0]; |
| dest[1] = vec[1]; |
| dest[2] = vec[2]; |
| } else { |
| dest[0] = dest[1] = dest[2] = 0; |
| } |
| |
| return dest; |
| }; |
| |
| /** |
| * Creates a new instance of a vec3, initializing it with the given arguments |
| * |
| * @param {number} x X value |
| * @param {number} y Y value |
| * @param {number} z Z value |
| |
| * @returns {vec3} New vec3 |
| */ |
| vec3.createFrom = function (x, y, z) { |
| var dest = new MatrixArray(3); |
| |
| dest[0] = x; |
| dest[1] = y; |
| dest[2] = z; |
| |
| return dest; |
| }; |
| |
| /** |
| * Copies the values of one vec3 to another |
| * |
| * @param {vec3} vec vec3 containing values to copy |
| * @param {vec3} dest vec3 receiving copied values |
| * |
| * @returns {vec3} dest |
| */ |
| vec3.set = function (vec, dest) { |
| dest[0] = vec[0]; |
| dest[1] = vec[1]; |
| dest[2] = vec[2]; |
| |
| return dest; |
| }; |
| |
| /** |
| * Compares two vectors for equality within a certain margin of error |
| * |
| * @param {vec3} a First vector |
| * @param {vec3} b Second vector |
| * |
| * @returns {Boolean} True if a is equivalent to b |
| */ |
| vec3.equal = function (a, b) { |
| return a === b || ( |
| Math.abs(a[0] - b[0]) < FLOAT_EPSILON && |
| Math.abs(a[1] - b[1]) < FLOAT_EPSILON && |
| Math.abs(a[2] - b[2]) < FLOAT_EPSILON |
| ); |
| }; |
| |
| /** |
| * Performs a vector addition |
| * |
| * @param {vec3} vec First operand |
| * @param {vec3} vec2 Second operand |
| * @param {vec3} [dest] vec3 receiving operation result. If not specified result is written to vec |
| * |
| * @returns {vec3} dest if specified, vec otherwise |
| */ |
| vec3.add = function (vec, vec2, dest) { |
| if (!dest || vec === dest) { |
| vec[0] += vec2[0]; |
| vec[1] += vec2[1]; |
| vec[2] += vec2[2]; |
| return vec; |
| } |
| |
| dest[0] = vec[0] + vec2[0]; |
| dest[1] = vec[1] + vec2[1]; |
| dest[2] = vec[2] + vec2[2]; |
| return dest; |
| }; |
| |
| /** |
| * Performs a vector subtraction |
| * |
| * @param {vec3} vec First operand |
| * @param {vec3} vec2 Second operand |
| * @param {vec3} [dest] vec3 receiving operation result. If not specified result is written to vec |
| * |
| * @returns {vec3} dest if specified, vec otherwise |
| */ |
| vec3.subtract = function (vec, vec2, dest) { |
| if (!dest || vec === dest) { |
| vec[0] -= vec2[0]; |
| vec[1] -= vec2[1]; |
| vec[2] -= vec2[2]; |
| return vec; |
| } |
| |
| dest[0] = vec[0] - vec2[0]; |
| dest[1] = vec[1] - vec2[1]; |
| dest[2] = vec[2] - vec2[2]; |
| return dest; |
| }; |
| |
| /** |
| * Performs a vector multiplication |
| * |
| * @param {vec3} vec First operand |
| * @param {vec3} vec2 Second operand |
| * @param {vec3} [dest] vec3 receiving operation result. If not specified result is written to vec |
| * |
| * @returns {vec3} dest if specified, vec otherwise |
| */ |
| vec3.multiply = function (vec, vec2, dest) { |
| if (!dest || vec === dest) { |
| vec[0] *= vec2[0]; |
| vec[1] *= vec2[1]; |
| vec[2] *= vec2[2]; |
| return vec; |
| } |
| |
| dest[0] = vec[0] * vec2[0]; |
| dest[1] = vec[1] * vec2[1]; |
| dest[2] = vec[2] * vec2[2]; |
| return dest; |
| }; |
| |
| /** |
| * Negates the components of a vec3 |
| * |
| * @param {vec3} vec vec3 to negate |
| * @param {vec3} [dest] vec3 receiving operation result. If not specified result is written to vec |
| * |
| * @returns {vec3} dest if specified, vec otherwise |
| */ |
| vec3.negate = function (vec, dest) { |
| if (!dest) { dest = vec; } |
| |
| dest[0] = -vec[0]; |
| dest[1] = -vec[1]; |
| dest[2] = -vec[2]; |
| return dest; |
| }; |
| |
| /** |
| * Multiplies the components of a vec3 by a scalar value |
| * |
| * @param {vec3} vec vec3 to scale |
| * @param {number} val Value to scale by |
| * @param {vec3} [dest] vec3 receiving operation result. If not specified result is written to vec |
| * |
| * @returns {vec3} dest if specified, vec otherwise |
| */ |
| vec3.scale = function (vec, val, dest) { |
| if (!dest || vec === dest) { |
| vec[0] *= val; |
| vec[1] *= val; |
| vec[2] *= val; |
| return vec; |
| } |
| |
| dest[0] = vec[0] * val; |
| dest[1] = vec[1] * val; |
| dest[2] = vec[2] * val; |
| return dest; |
| }; |
| |
| /** |
| * Generates a unit vector of the same direction as the provided vec3 |
| * If vector length is 0, returns [0, 0, 0] |
| * |
| * @param {vec3} vec vec3 to normalize |
| * @param {vec3} [dest] vec3 receiving operation result. If not specified result is written to vec |
| * |
| * @returns {vec3} dest if specified, vec otherwise |
| */ |
| vec3.normalize = function (vec, dest) { |
| if (!dest) { dest = vec; } |
| |
| var x = vec[0], y = vec[1], z = vec[2], |
| len = Math.sqrt(x * x + y * y + z * z); |
| |
| if (!len) { |
| dest[0] = 0; |
| dest[1] = 0; |
| dest[2] = 0; |
| return dest; |
| } else if (len === 1) { |
| dest[0] = x; |
| dest[1] = y; |
| dest[2] = z; |
| return dest; |
| } |
| |
| len = 1 / len; |
| dest[0] = x * len; |
| dest[1] = y * len; |
| dest[2] = z * len; |
| return dest; |
| }; |
| |
| /** |
| * Generates the cross product of two vec3s |
| * |
| * @param {vec3} vec First operand |
| * @param {vec3} vec2 Second operand |
| * @param {vec3} [dest] vec3 receiving operation result. If not specified result is written to vec |
| * |
| * @returns {vec3} dest if specified, vec otherwise |
| */ |
| vec3.cross = function (vec, vec2, dest) { |
| if (!dest) { dest = vec; } |
| |
| var x = vec[0], y = vec[1], z = vec[2], |
| x2 = vec2[0], y2 = vec2[1], z2 = vec2[2]; |
| |
| dest[0] = y * z2 - z * y2; |
| dest[1] = z * x2 - x * z2; |
| dest[2] = x * y2 - y * x2; |
| return dest; |
| }; |
| |
| /** |
| * Caclulates the length of a vec3 |
| * |
| * @param {vec3} vec vec3 to calculate length of |
| * |
| * @returns {number} Length of vec |
| */ |
| vec3.length = function (vec) { |
| var x = vec[0], y = vec[1], z = vec[2]; |
| return Math.sqrt(x * x + y * y + z * z); |
| }; |
| |
| /** |
| * Caclulates the squared length of a vec3 |
| * |
| * @param {vec3} vec vec3 to calculate squared length of |
| * |
| * @returns {number} Squared Length of vec |
| */ |
| vec3.squaredLength = function (vec) { |
| var x = vec[0], y = vec[1], z = vec[2]; |
| return x * x + y * y + z * z; |
| }; |
| |
| /** |
| * Caclulates the dot product of two vec3s |
| * |
| * @param {vec3} vec First operand |
| * @param {vec3} vec2 Second operand |
| * |
| * @returns {number} Dot product of vec and vec2 |
| */ |
| vec3.dot = function (vec, vec2) { |
| return vec[0] * vec2[0] + vec[1] * vec2[1] + vec[2] * vec2[2]; |
| }; |
| |
| /** |
| * Generates a unit vector pointing from one vector to another |
| * |
| * @param {vec3} vec Origin vec3 |
| * @param {vec3} vec2 vec3 to point to |
| * @param {vec3} [dest] vec3 receiving operation result. If not specified result is written to vec |
| * |
| * @returns {vec3} dest if specified, vec otherwise |
| */ |
| vec3.direction = function (vec, vec2, dest) { |
| if (!dest) { dest = vec; } |
| |
| var x = vec[0] - vec2[0], |
| y = vec[1] - vec2[1], |
| z = vec[2] - vec2[2], |
| len = Math.sqrt(x * x + y * y + z * z); |
| |
| if (!len) { |
| dest[0] = 0; |
| dest[1] = 0; |
| dest[2] = 0; |
| return dest; |
| } |
| |
| len = 1 / len; |
| dest[0] = x * len; |
| dest[1] = y * len; |
| dest[2] = z * len; |
| return dest; |
| }; |
| |
| /** |
| * Performs a linear interpolation between two vec3 |
| * |
| * @param {vec3} vec First vector |
| * @param {vec3} vec2 Second vector |
| * @param {number} lerp Interpolation amount between the two inputs |
| * @param {vec3} [dest] vec3 receiving operation result. If not specified result is written to vec |
| * |
| * @returns {vec3} dest if specified, vec otherwise |
| */ |
| vec3.lerp = function (vec, vec2, lerp, dest) { |
| if (!dest) { dest = vec; } |
| |
| dest[0] = vec[0] + lerp * (vec2[0] - vec[0]); |
| dest[1] = vec[1] + lerp * (vec2[1] - vec[1]); |
| dest[2] = vec[2] + lerp * (vec2[2] - vec[2]); |
| |
| return dest; |
| }; |
| |
| /** |
| * Calculates the euclidian distance between two vec3 |
| * |
| * Params: |
| * @param {vec3} vec First vector |
| * @param {vec3} vec2 Second vector |
| * |
| * @returns {number} Distance between vec and vec2 |
| */ |
| vec3.dist = function (vec, vec2) { |
| var x = vec2[0] - vec[0], |
| y = vec2[1] - vec[1], |
| z = vec2[2] - vec[2]; |
| |
| return Math.sqrt(x*x + y*y + z*z); |
| }; |
| |
| // Pre-allocated to prevent unecessary garbage collection |
| var unprojectMat = null; |
| var unprojectVec = new MatrixArray(4); |
| /** |
| * Projects the specified vec3 from screen space into object space |
| * Based on the <a href="http://webcvs.freedesktop.org/mesa/Mesa/src/glu/mesa/project.c?revision=1.4&view=markup">Mesa gluUnProject implementation</a> |
| * |
| * @param {vec3} vec Screen-space vector to project |
| * @param {mat4} view View matrix |
| * @param {mat4} proj Projection matrix |
| * @param {vec4} viewport Viewport as given to gl.viewport [x, y, width, height] |
| * @param {vec3} [dest] vec3 receiving unprojected result. If not specified result is written to vec |
| * |
| * @returns {vec3} dest if specified, vec otherwise |
| */ |
| vec3.unproject = function (vec, view, proj, viewport, dest) { |
| if (!dest) { dest = vec; } |
| |
| if(!unprojectMat) { |
| unprojectMat = mat4.create(); |
| } |
| |
| var m = unprojectMat; |
| var v = unprojectVec; |
| |
| v[0] = (vec[0] - viewport[0]) * 2.0 / viewport[2] - 1.0; |
| v[1] = (vec[1] - viewport[1]) * 2.0 / viewport[3] - 1.0; |
| v[2] = 2.0 * vec[2] - 1.0; |
| v[3] = 1.0; |
| |
| mat4.multiply(proj, view, m); |
| if(!mat4.inverse(m)) { return null; } |
| |
| mat4.multiplyVec4(m, v); |
| if(v[3] === 0.0) { return null; } |
| |
| dest[0] = v[0] / v[3]; |
| dest[1] = v[1] / v[3]; |
| dest[2] = v[2] / v[3]; |
| |
| return dest; |
| }; |
| |
| var xUnitVec3 = vec3.createFrom(1,0,0); |
| var yUnitVec3 = vec3.createFrom(0,1,0); |
| var zUnitVec3 = vec3.createFrom(0,0,1); |
| |
| var tmpvec3 = vec3.create(); |
| /** |
| * Generates a quaternion of rotation between two given normalized vectors |
| * |
| * @param {vec3} a Normalized source vector |
| * @param {vec3} b Normalized target vector |
| * @param {quat4} [dest] quat4 receiving operation result. |
| * |
| * @returns {quat4} dest if specified, a new quat4 otherwise |
| */ |
| vec3.rotationTo = function (a, b, dest) { |
| if (!dest) { dest = quat4.create(); } |
| |
| var d = vec3.dot(a, b); |
| var axis = tmpvec3; |
| if (d >= 1.0) { |
| quat4.set(identityQuat4, dest); |
| } else if (d < (0.000001 - 1.0)) { |
| vec3.cross(xUnitVec3, a, axis); |
| if (vec3.length(axis) < 0.000001) |
| vec3.cross(yUnitVec3, a, axis); |
| if (vec3.length(axis) < 0.000001) |
| vec3.cross(zUnitVec3, a, axis); |
| vec3.normalize(axis); |
| quat4.fromAngleAxis(Math.PI, axis, dest); |
| } else { |
| var s = Math.sqrt((1.0 + d) * 2.0); |
| var sInv = 1.0 / s; |
| vec3.cross(a, b, axis); |
| dest[0] = axis[0] * sInv; |
| dest[1] = axis[1] * sInv; |
| dest[2] = axis[2] * sInv; |
| dest[3] = s * 0.5; |
| quat4.normalize(dest); |
| } |
| if (dest[3] > 1.0) dest[3] = 1.0; |
| else if (dest[3] < -1.0) dest[3] = -1.0; |
| return dest; |
| }; |
| |
| /** |
| * Returns a string representation of a vector |
| * |
| * @param {vec3} vec Vector to represent as a string |
| * |
| * @returns {string} String representation of vec |
| */ |
| vec3.str = function (vec) { |
| return '[' + vec[0] + ', ' + vec[1] + ', ' + vec[2] + ']'; |
| }; |
| |
| /** |
| * @class 3x3 Matrix |
| * @name mat3 |
| */ |
| var mat3 = {}; |
| |
| /** |
| * Creates a new instance of a mat3 using the default array type |
| * Any javascript array-like object containing at least 9 numeric elements can serve as a mat3 |
| * |
| * @param {mat3} [mat] mat3 containing values to initialize with |
| * |
| * @returns {mat3} New mat3 |
| */ |
| mat3.create = function (mat) { |
| var dest = new MatrixArray(9); |
| |
| if (mat) { |
| dest[0] = mat[0]; |
| dest[1] = mat[1]; |
| dest[2] = mat[2]; |
| dest[3] = mat[3]; |
| dest[4] = mat[4]; |
| dest[5] = mat[5]; |
| dest[6] = mat[6]; |
| dest[7] = mat[7]; |
| dest[8] = mat[8]; |
| } else { |
| dest[0] = dest[1] = |
| dest[2] = dest[3] = |
| dest[4] = dest[5] = |
| dest[6] = dest[7] = |
| dest[8] = 0; |
| } |
| |
| return dest; |
| }; |
| |
| /** |
| * Creates a new instance of a mat3, initializing it with the given arguments |
| * |
| * @param {number} m00 |
| * @param {number} m01 |
| * @param {number} m02 |
| * @param {number} m10 |
| * @param {number} m11 |
| * @param {number} m12 |
| * @param {number} m20 |
| * @param {number} m21 |
| * @param {number} m22 |
| |
| * @returns {mat3} New mat3 |
| */ |
| mat3.createFrom = function (m00, m01, m02, m10, m11, m12, m20, m21, m22) { |
| var dest = new MatrixArray(9); |
| |
| dest[0] = m00; |
| dest[1] = m01; |
| dest[2] = m02; |
| dest[3] = m10; |
| dest[4] = m11; |
| dest[5] = m12; |
| dest[6] = m20; |
| dest[7] = m21; |
| dest[8] = m22; |
| |
| return dest; |
| }; |
| |
| /** |
| * Calculates the determinant of a mat3 |
| * |
| * @param {mat3} mat mat3 to calculate determinant of |
| * |
| * @returns {Number} determinant of mat |
| */ |
| mat3.determinant = function (mat) { |
| var a00 = mat[0], a01 = mat[1], a02 = mat[2], |
| a10 = mat[3], a11 = mat[4], a12 = mat[5], |
| a20 = mat[6], a21 = mat[7], a22 = mat[8]; |
| |
| return a00 * (a22 * a11 - a12 * a21) + a01 * (-a22 * a10 + a12 * a20) + a02 * (a21 * a10 - a11 * a20); |
| }; |
| |
| /** |
| * Calculates the inverse matrix of a mat3 |
| * |
| * @param {mat3} mat mat3 to calculate inverse of |
| * @param {mat3} [dest] mat3 receiving inverse matrix. If not specified result is written to mat |
| * |
| * @param {mat3} dest is specified, mat otherwise, null if matrix cannot be inverted |
| */ |
| mat3.inverse = function (mat, dest) { |
| var a00 = mat[0], a01 = mat[1], a02 = mat[2], |
| a10 = mat[3], a11 = mat[4], a12 = mat[5], |
| a20 = mat[6], a21 = mat[7], a22 = mat[8], |
| |
| b01 = a22 * a11 - a12 * a21, |
| b11 = -a22 * a10 + a12 * a20, |
| b21 = a21 * a10 - a11 * a20, |
| |
| d = a00 * b01 + a01 * b11 + a02 * b21, |
| id; |
| |
| if (!d) { return null; } |
| id = 1 / d; |
| |
| if (!dest) { dest = mat3.create(); } |
| |
| dest[0] = b01 * id; |
| dest[1] = (-a22 * a01 + a02 * a21) * id; |
| dest[2] = (a12 * a01 - a02 * a11) * id; |
| dest[3] = b11 * id; |
| dest[4] = (a22 * a00 - a02 * a20) * id; |
| dest[5] = (-a12 * a00 + a02 * a10) * id; |
| dest[6] = b21 * id; |
| dest[7] = (-a21 * a00 + a01 * a20) * id; |
| dest[8] = (a11 * a00 - a01 * a10) * id; |
| return dest; |
| }; |
| |
| /** |
| * Performs a matrix multiplication |
| * |
| * @param {mat3} mat First operand |
| * @param {mat3} mat2 Second operand |
| * @param {mat3} [dest] mat3 receiving operation result. If not specified result is written to mat |
| * |
| * @returns {mat3} dest if specified, mat otherwise |
| */ |
| mat3.multiply = function (mat, mat2, dest) { |
| if (!dest) { dest = mat; } |
| |
| |
| // Cache the matrix values (makes for huge speed increases!) |
| var a00 = mat[0], a01 = mat[1], a02 = mat[2], |
| a10 = mat[3], a11 = mat[4], a12 = mat[5], |
| a20 = mat[6], a21 = mat[7], a22 = mat[8], |
| |
| b00 = mat2[0], b01 = mat2[1], b02 = mat2[2], |
| b10 = mat2[3], b11 = mat2[4], b12 = mat2[5], |
| b20 = mat2[6], b21 = mat2[7], b22 = mat2[8]; |
| |
| dest[0] = b00 * a00 + b01 * a10 + b02 * a20; |
| dest[1] = b00 * a01 + b01 * a11 + b02 * a21; |
| dest[2] = b00 * a02 + b01 * a12 + b02 * a22; |
| |
| dest[3] = b10 * a00 + b11 * a10 + b12 * a20; |
| dest[4] = b10 * a01 + b11 * a11 + b12 * a21; |
| dest[5] = b10 * a02 + b11 * a12 + b12 * a22; |
| |
| dest[6] = b20 * a00 + b21 * a10 + b22 * a20; |
| dest[7] = b20 * a01 + b21 * a11 + b22 * a21; |
| dest[8] = b20 * a02 + b21 * a12 + b22 * a22; |
| |
| return dest; |
| }; |
| |
| /** |
| * Transforms the vec2 according to the given mat3. |
| * |
| * @param {mat3} matrix mat3 to multiply against |
| * @param {vec2} vec the vector to multiply |
| * @param {vec2} [dest] an optional receiving vector. If not given, vec is used. |
| * |
| * @returns {vec2} The multiplication result |
| **/ |
| mat3.multiplyVec2 = function(matrix, vec, dest) { |
| if (!dest) dest = vec; |
| var x = vec[0], y = vec[1]; |
| dest[0] = x * matrix[0] + y * matrix[3] + matrix[6]; |
| dest[1] = x * matrix[1] + y * matrix[4] + matrix[7]; |
| return dest; |
| }; |
| |
| /** |
| * Transforms the vec3 according to the given mat3 |
| * |
| * @param {mat3} matrix mat3 to multiply against |
| * @param {vec3} vec the vector to multiply |
| * @param {vec3} [dest] an optional receiving vector. If not given, vec is used. |
| * |
| * @returns {vec3} The multiplication result |
| **/ |
| mat3.multiplyVec3 = function(matrix, vec, dest) { |
| if (!dest) dest = vec; |
| var x = vec[0], y = vec[1], z = vec[2]; |
| dest[0] = x * matrix[0] + y * matrix[3] + z * matrix[6]; |
| dest[1] = x * matrix[1] + y * matrix[4] + z * matrix[7]; |
| dest[2] = x * matrix[2] + y * matrix[5] + z * matrix[8]; |
| |
| return dest; |
| }; |
| |
| /** |
| * Copies the values of one mat3 to another |
| * |
| * @param {mat3} mat mat3 containing values to copy |
| * @param {mat3} dest mat3 receiving copied values |
| * |
| * @returns {mat3} dest |
| */ |
| mat3.set = function (mat, dest) { |
| dest[0] = mat[0]; |
| dest[1] = mat[1]; |
| dest[2] = mat[2]; |
| dest[3] = mat[3]; |
| dest[4] = mat[4]; |
| dest[5] = mat[5]; |
| dest[6] = mat[6]; |
| dest[7] = mat[7]; |
| dest[8] = mat[8]; |
| return dest; |
| }; |
| |
| /** |
| * Compares two matrices for equality within a certain margin of error |
| * |
| * @param {mat3} a First matrix |
| * @param {mat3} b Second matrix |
| * |
| * @returns {Boolean} True if a is equivalent to b |
| */ |
| mat3.equal = function (a, b) { |
| return a === b || ( |
| Math.abs(a[0] - b[0]) < FLOAT_EPSILON && |
| Math.abs(a[1] - b[1]) < FLOAT_EPSILON && |
| Math.abs(a[2] - b[2]) < FLOAT_EPSILON && |
| Math.abs(a[3] - b[3]) < FLOAT_EPSILON && |
| Math.abs(a[4] - b[4]) < FLOAT_EPSILON && |
| Math.abs(a[5] - b[5]) < FLOAT_EPSILON && |
| Math.abs(a[6] - b[6]) < FLOAT_EPSILON && |
| Math.abs(a[7] - b[7]) < FLOAT_EPSILON && |
| Math.abs(a[8] - b[8]) < FLOAT_EPSILON |
| ); |
| }; |
| |
| /** |
| * Sets a mat3 to an identity matrix |
| * |
| * @param {mat3} dest mat3 to set |
| * |
| * @returns dest if specified, otherwise a new mat3 |
| */ |
| mat3.identity = function (dest) { |
| if (!dest) { dest = mat3.create(); } |
| dest[0] = 1; |
| dest[1] = 0; |
| dest[2] = 0; |
| dest[3] = 0; |
| dest[4] = 1; |
| dest[5] = 0; |
| dest[6] = 0; |
| dest[7] = 0; |
| dest[8] = 1; |
| return dest; |
| }; |
| |
| /** |
| * Transposes a mat3 (flips the values over the diagonal) |
| * |
| * Params: |
| * @param {mat3} mat mat3 to transpose |
| * @param {mat3} [dest] mat3 receiving transposed values. If not specified result is written to mat |
| * |
| * @returns {mat3} dest is specified, mat otherwise |
| */ |
| mat3.transpose = function (mat, dest) { |
| // If we are transposing ourselves we can skip a few steps but have to cache some values |
| if (!dest || mat === dest) { |
| var a01 = mat[1], a02 = mat[2], |
| a12 = mat[5]; |
| |
| mat[1] = mat[3]; |
| mat[2] = mat[6]; |
| mat[3] = a01; |
| mat[5] = mat[7]; |
| mat[6] = a02; |
| mat[7] = a12; |
| return mat; |
| } |
| |
| dest[0] = mat[0]; |
| dest[1] = mat[3]; |
| dest[2] = mat[6]; |
| dest[3] = mat[1]; |
| dest[4] = mat[4]; |
| dest[5] = mat[7]; |
| dest[6] = mat[2]; |
| dest[7] = mat[5]; |
| dest[8] = mat[8]; |
| return dest; |
| }; |
| |
| /** |
| * Copies the elements of a mat3 into the upper 3x3 elements of a mat4 |
| * |
| * @param {mat3} mat mat3 containing values to copy |
| * @param {mat4} [dest] mat4 receiving copied values |
| * |
| * @returns {mat4} dest if specified, a new mat4 otherwise |
| */ |
| mat3.toMat4 = function (mat, dest) { |
| if (!dest) { dest = mat4.create(); } |
| |
| dest[15] = 1; |
| dest[14] = 0; |
| dest[13] = 0; |
| dest[12] = 0; |
| |
| dest[11] = 0; |
| dest[10] = mat[8]; |
| dest[9] = mat[7]; |
| dest[8] = mat[6]; |
| |
| dest[7] = 0; |
| dest[6] = mat[5]; |
| dest[5] = mat[4]; |
| dest[4] = mat[3]; |
| |
| dest[3] = 0; |
| dest[2] = mat[2]; |
| dest[1] = mat[1]; |
| dest[0] = mat[0]; |
| |
| return dest; |
| }; |
| |
| /** |
| * Returns a string representation of a mat3 |
| * |
| * @param {mat3} mat mat3 to represent as a string |
| * |
| * @param {string} String representation of mat |
| */ |
| mat3.str = function (mat) { |
| return '[' + mat[0] + ', ' + mat[1] + ', ' + mat[2] + |
| ', ' + mat[3] + ', ' + mat[4] + ', ' + mat[5] + |
| ', ' + mat[6] + ', ' + mat[7] + ', ' + mat[8] + ']'; |
| }; |
| |
| /** |
| * @class 4x4 Matrix |
| * @name mat4 |
| */ |
| var mat4 = {}; |
| |
| /** |
| * Creates a new instance of a mat4 using the default array type |
| * Any javascript array-like object containing at least 16 numeric elements can serve as a mat4 |
| * |
| * @param {mat4} [mat] mat4 containing values to initialize with |
| * |
| * @returns {mat4} New mat4 |
| */ |
| mat4.create = function (mat) { |
| var dest = new MatrixArray(16); |
| |
| if (mat) { |
| dest[0] = mat[0]; |
| dest[1] = mat[1]; |
| dest[2] = mat[2]; |
| dest[3] = mat[3]; |
| dest[4] = mat[4]; |
| dest[5] = mat[5]; |
| dest[6] = mat[6]; |
| dest[7] = mat[7]; |
| dest[8] = mat[8]; |
| dest[9] = mat[9]; |
| dest[10] = mat[10]; |
| dest[11] = mat[11]; |
| dest[12] = mat[12]; |
| dest[13] = mat[13]; |
| dest[14] = mat[14]; |
| dest[15] = mat[15]; |
| } |
| |
| return dest; |
| }; |
| |
| /** |
| * Creates a new instance of a mat4, initializing it with the given arguments |
| * |
| * @param {number} m00 |
| * @param {number} m01 |
| * @param {number} m02 |
| * @param {number} m03 |
| * @param {number} m10 |
| * @param {number} m11 |
| * @param {number} m12 |
| * @param {number} m13 |
| * @param {number} m20 |
| * @param {number} m21 |
| * @param {number} m22 |
| * @param {number} m23 |
| * @param {number} m30 |
| * @param {number} m31 |
| * @param {number} m32 |
| * @param {number} m33 |
| |
| * @returns {mat4} New mat4 |
| */ |
| mat4.createFrom = function (m00, m01, m02, m03, m10, m11, m12, m13, m20, m21, m22, m23, m30, m31, m32, m33) { |
| var dest = new MatrixArray(16); |
| |
| dest[0] = m00; |
| dest[1] = m01; |
| dest[2] = m02; |
| dest[3] = m03; |
| dest[4] = m10; |
| dest[5] = m11; |
| dest[6] = m12; |
| dest[7] = m13; |
| dest[8] = m20; |
| dest[9] = m21; |
| dest[10] = m22; |
| dest[11] = m23; |
| dest[12] = m30; |
| dest[13] = m31; |
| dest[14] = m32; |
| dest[15] = m33; |
| |
| return dest; |
| }; |
| |
| /** |
| * Copies the values of one mat4 to another |
| * |
| * @param {mat4} mat mat4 containing values to copy |
| * @param {mat4} dest mat4 receiving copied values |
| * |
| * @returns {mat4} dest |
| */ |
| mat4.set = function (mat, dest) { |
| dest[0] = mat[0]; |
| dest[1] = mat[1]; |
| dest[2] = mat[2]; |
| dest[3] = mat[3]; |
| dest[4] = mat[4]; |
| dest[5] = mat[5]; |
| dest[6] = mat[6]; |
| dest[7] = mat[7]; |
| dest[8] = mat[8]; |
| dest[9] = mat[9]; |
| dest[10] = mat[10]; |
| dest[11] = mat[11]; |
| dest[12] = mat[12]; |
| dest[13] = mat[13]; |
| dest[14] = mat[14]; |
| dest[15] = mat[15]; |
| return dest; |
| }; |
| |
| /** |
| * Compares two matrices for equality within a certain margin of error |
| * |
| * @param {mat4} a First matrix |
| * @param {mat4} b Second matrix |
| * |
| * @returns {Boolean} True if a is equivalent to b |
| */ |
| mat4.equal = function (a, b) { |
| return a === b || ( |
| Math.abs(a[0] - b[0]) < FLOAT_EPSILON && |
| Math.abs(a[1] - b[1]) < FLOAT_EPSILON && |
| Math.abs(a[2] - b[2]) < FLOAT_EPSILON && |
| Math.abs(a[3] - b[3]) < FLOAT_EPSILON && |
| Math.abs(a[4] - b[4]) < FLOAT_EPSILON && |
| Math.abs(a[5] - b[5]) < FLOAT_EPSILON && |
| Math.abs(a[6] - b[6]) < FLOAT_EPSILON && |
| Math.abs(a[7] - b[7]) < FLOAT_EPSILON && |
| Math.abs(a[8] - b[8]) < FLOAT_EPSILON && |
| Math.abs(a[9] - b[9]) < FLOAT_EPSILON && |
| Math.abs(a[10] - b[10]) < FLOAT_EPSILON && |
| Math.abs(a[11] - b[11]) < FLOAT_EPSILON && |
| Math.abs(a[12] - b[12]) < FLOAT_EPSILON && |
| Math.abs(a[13] - b[13]) < FLOAT_EPSILON && |
| Math.abs(a[14] - b[14]) < FLOAT_EPSILON && |
| Math.abs(a[15] - b[15]) < FLOAT_EPSILON |
| ); |
| }; |
| |
| /** |
| * Sets a mat4 to an identity matrix |
| * |
| * @param {mat4} dest mat4 to set |
| * |
| * @returns {mat4} dest |
| */ |
| mat4.identity = function (dest) { |
| if (!dest) { dest = mat4.create(); } |
| dest[0] = 1; |
| dest[1] = 0; |
| dest[2] = 0; |
| dest[3] = 0; |
| dest[4] = 0; |
| dest[5] = 1; |
| dest[6] = 0; |
| dest[7] = 0; |
| dest[8] = 0; |
| dest[9] = 0; |
| dest[10] = 1; |
| dest[11] = 0; |
| dest[12] = 0; |
| dest[13] = 0; |
| dest[14] = 0; |
| dest[15] = 1; |
| return dest; |
| }; |
| |
| /** |
| * Transposes a mat4 (flips the values over the diagonal) |
| * |
| * @param {mat4} mat mat4 to transpose |
| * @param {mat4} [dest] mat4 receiving transposed values. If not specified result is written to mat |
| * |
| * @param {mat4} dest is specified, mat otherwise |
| */ |
| mat4.transpose = function (mat, dest) { |
| // If we are transposing ourselves we can skip a few steps but have to cache some values |
| if (!dest || mat === dest) { |
| var a01 = mat[1], a02 = mat[2], a03 = mat[3], |
| a12 = mat[6], a13 = mat[7], |
| a23 = mat[11]; |
| |
| mat[1] = mat[4]; |
| mat[2] = mat[8]; |
| mat[3] = mat[12]; |
| mat[4] = a01; |
| mat[6] = mat[9]; |
| mat[7] = mat[13]; |
| mat[8] = a02; |
| mat[9] = a12; |
| mat[11] = mat[14]; |
| mat[12] = a03; |
| mat[13] = a13; |
| mat[14] = a23; |
| return mat; |
| } |
| |
| dest[0] = mat[0]; |
| dest[1] = mat[4]; |
| dest[2] = mat[8]; |
| dest[3] = mat[12]; |
| dest[4] = mat[1]; |
| dest[5] = mat[5]; |
| dest[6] = mat[9]; |
| dest[7] = mat[13]; |
| dest[8] = mat[2]; |
| dest[9] = mat[6]; |
| dest[10] = mat[10]; |
| dest[11] = mat[14]; |
| dest[12] = mat[3]; |
| dest[13] = mat[7]; |
| dest[14] = mat[11]; |
| dest[15] = mat[15]; |
| return dest; |
| }; |
| |
| /** |
| * Calculates the determinant of a mat4 |
| * |
| * @param {mat4} mat mat4 to calculate determinant of |
| * |
| * @returns {number} determinant of mat |
| */ |
| mat4.determinant = function (mat) { |
| // Cache the matrix values (makes for huge speed increases!) |
| var a00 = mat[0], a01 = mat[1], a02 = mat[2], a03 = mat[3], |
| a10 = mat[4], a11 = mat[5], a12 = mat[6], a13 = mat[7], |
| a20 = mat[8], a21 = mat[9], a22 = mat[10], a23 = mat[11], |
| a30 = mat[12], a31 = mat[13], a32 = mat[14], a33 = mat[15]; |
| |
| return (a30 * a21 * a12 * a03 - a20 * a31 * a12 * a03 - a30 * a11 * a22 * a03 + a10 * a31 * a22 * a03 + |
| a20 * a11 * a32 * a03 - a10 * a21 * a32 * a03 - a30 * a21 * a02 * a13 + a20 * a31 * a02 * a13 + |
| a30 * a01 * a22 * a13 - a00 * a31 * a22 * a13 - a20 * a01 * a32 * a13 + a00 * a21 * a32 * a13 + |
| a30 * a11 * a02 * a23 - a10 * a31 * a02 * a23 - a30 * a01 * a12 * a23 + a00 * a31 * a12 * a23 + |
| a10 * a01 * a32 * a23 - a00 * a11 * a32 * a23 - a20 * a11 * a02 * a33 + a10 * a21 * a02 * a33 + |
| a20 * a01 * a12 * a33 - a00 * a21 * a12 * a33 - a10 * a01 * a22 * a33 + a00 * a11 * a22 * a33); |
| }; |
| |
| /** |
| * Calculates the inverse matrix of a mat4 |
| * |
| * @param {mat4} mat mat4 to calculate inverse of |
| * @param {mat4} [dest] mat4 receiving inverse matrix. If not specified result is written to mat |
| * |
| * @param {mat4} dest is specified, mat otherwise, null if matrix cannot be inverted |
| */ |
| mat4.inverse = function (mat, dest) { |
| if (!dest) { dest = mat; } |
| |
| // Cache the matrix values (makes for huge speed increases!) |
| var a00 = mat[0], a01 = mat[1], a02 = mat[2], a03 = mat[3], |
| a10 = mat[4], a11 = mat[5], a12 = mat[6], a13 = mat[7], |
| a20 = mat[8], a21 = mat[9], a22 = mat[10], a23 = mat[11], |
| a30 = mat[12], a31 = mat[13], a32 = mat[14], a33 = mat[15], |
| |
| b00 = a00 * a11 - a01 * a10, |
| b01 = a00 * a12 - a02 * a10, |
| b02 = a00 * a13 - a03 * a10, |
| b03 = a01 * a12 - a02 * a11, |
| b04 = a01 * a13 - a03 * a11, |
| b05 = a02 * a13 - a03 * a12, |
| b06 = a20 * a31 - a21 * a30, |
| b07 = a20 * a32 - a22 * a30, |
| b08 = a20 * a33 - a23 * a30, |
| b09 = a21 * a32 - a22 * a31, |
| b10 = a21 * a33 - a23 * a31, |
| b11 = a22 * a33 - a23 * a32, |
| |
| d = (b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06), |
| invDet; |
| |
| // Calculate the determinant |
| if (!d) { return null; } |
| invDet = 1 / d; |
| |
| dest[0] = (a11 * b11 - a12 * b10 + a13 * b09) * invDet; |
| dest[1] = (-a01 * b11 + a02 * b10 - a03 * b09) * invDet; |
| dest[2] = (a31 * b05 - a32 * b04 + a33 * b03) * invDet; |
| dest[3] = (-a21 * b05 + a22 * b04 - a23 * b03) * invDet; |
| dest[4] = (-a10 * b11 + a12 * b08 - a13 * b07) * invDet; |
| dest[5] = (a00 * b11 - a02 * b08 + a03 * b07) * invDet; |
| dest[6] = (-a30 * b05 + a32 * b02 - a33 * b01) * invDet; |
| dest[7] = (a20 * b05 - a22 * b02 + a23 * b01) * invDet; |
| dest[8] = (a10 * b10 - a11 * b08 + a13 * b06) * invDet; |
| dest[9] = (-a00 * b10 + a01 * b08 - a03 * b06) * invDet; |
| dest[10] = (a30 * b04 - a31 * b02 + a33 * b00) * invDet; |
| dest[11] = (-a20 * b04 + a21 * b02 - a23 * b00) * invDet; |
| dest[12] = (-a10 * b09 + a11 * b07 - a12 * b06) * invDet; |
| dest[13] = (a00 * b09 - a01 * b07 + a02 * b06) * invDet; |
| dest[14] = (-a30 * b03 + a31 * b01 - a32 * b00) * invDet; |
| dest[15] = (a20 * b03 - a21 * b01 + a22 * b00) * invDet; |
| |
| return dest; |
| }; |
| |
| /** |
| * Copies the upper 3x3 elements of a mat4 into another mat4 |
| * |
| * @param {mat4} mat mat4 containing values to copy |
| * @param {mat4} [dest] mat4 receiving copied values |
| * |
| * @returns {mat4} dest is specified, a new mat4 otherwise |
| */ |
| mat4.toRotationMat = function (mat, dest) { |
| if (!dest) { dest = mat4.create(); } |
| |
| dest[0] = mat[0]; |
| dest[1] = mat[1]; |
| dest[2] = mat[2]; |
| dest[3] = mat[3]; |
| dest[4] = mat[4]; |
| dest[5] = mat[5]; |
| dest[6] = mat[6]; |
| dest[7] = mat[7]; |
| dest[8] = mat[8]; |
| dest[9] = mat[9]; |
| dest[10] = mat[10]; |
| dest[11] = mat[11]; |
| dest[12] = 0; |
| dest[13] = 0; |
| dest[14] = 0; |
| dest[15] = 1; |
| |
| return dest; |
| }; |
| |
| /** |
| * Copies the upper 3x3 elements of a mat4 into a mat3 |
| * |
| * @param {mat4} mat mat4 containing values to copy |
| * @param {mat3} [dest] mat3 receiving copied values |
| * |
| * @returns {mat3} dest is specified, a new mat3 otherwise |
| */ |
| mat4.toMat3 = function (mat, dest) { |
| if (!dest) { dest = mat3.create(); } |
| |
| dest[0] = mat[0]; |
| dest[1] = mat[1]; |
| dest[2] = mat[2]; |
| dest[3] = mat[4]; |
| dest[4] = mat[5]; |
| dest[5] = mat[6]; |
| dest[6] = mat[8]; |
| dest[7] = mat[9]; |
| dest[8] = mat[10]; |
| |
| return dest; |
| }; |
| |
| /** |
| * Calculates the inverse of the upper 3x3 elements of a mat4 and copies the result into a mat3 |
| * The resulting matrix is useful for calculating transformed normals |
| * |
| * Params: |
| * @param {mat4} mat mat4 containing values to invert and copy |
| * @param {mat3} [dest] mat3 receiving values |
| * |
| * @returns {mat3} dest is specified, a new mat3 otherwise, null if the matrix cannot be inverted |
| */ |
| mat4.toInverseMat3 = function (mat, dest) { |
| // Cache the matrix values (makes for huge speed increases!) |
| var a00 = mat[0], a01 = mat[1], a02 = mat[2], |
| a10 = mat[4], a11 = mat[5], a12 = mat[6], |
| a20 = mat[8], a21 = mat[9], a22 = mat[10], |
| |
| b01 = a22 * a11 - a12 * a21, |
| b11 = -a22 * a10 + a12 * a20, |
| b21 = a21 * a10 - a11 * a20, |
| |
| d = a00 * b01 + a01 * b11 + a02 * b21, |
| id; |
| |
| if (!d) { return null; } |
| id = 1 / d; |
| |
| if (!dest) { dest = mat3.create(); } |
| |
| dest[0] = b01 * id; |
| dest[1] = (-a22 * a01 + a02 * a21) * id; |
| dest[2] = (a12 * a01 - a02 * a11) * id; |
| dest[3] = b11 * id; |
| dest[4] = (a22 * a00 - a02 * a20) * id; |
| dest[5] = (-a12 * a00 + a02 * a10) * id; |
| dest[6] = b21 * id; |
| dest[7] = (-a21 * a00 + a01 * a20) * id; |
| dest[8] = (a11 * a00 - a01 * a10) * id; |
| |
| return dest; |
| }; |
| |
| /** |
| * Performs a matrix multiplication |
| * |
| * @param {mat4} mat First operand |
| * @param {mat4} mat2 Second operand |
| * @param {mat4} [dest] mat4 receiving operation result. If not specified result is written to mat |
| * |
| * @returns {mat4} dest if specified, mat otherwise |
| */ |
| mat4.multiply = function (mat, mat2, dest) { |
| if (!dest) { dest = mat; } |
| |
| // Cache the matrix values (makes for huge speed increases!) |
| var a00 = mat[ 0], a01 = mat[ 1], a02 = mat[ 2], a03 = mat[3]; |
| var a10 = mat[ 4], a11 = mat[ 5], a12 = mat[ 6], a13 = mat[7]; |
| var a20 = mat[ 8], a21 = mat[ 9], a22 = mat[10], a23 = mat[11]; |
| var a30 = mat[12], a31 = mat[13], a32 = mat[14], a33 = mat[15]; |
| |
| // Cache only the current line of the second matrix |
| var b0 = mat2[0], b1 = mat2[1], b2 = mat2[2], b3 = mat2[3]; |
| dest[0] = b0*a00 + b1*a10 + b2*a20 + b3*a30; |
| dest[1] = b0*a01 + b1*a11 + b2*a21 + b3*a31; |
| dest[2] = b0*a02 + b1*a12 + b2*a22 + b3*a32; |
| dest[3] = b0*a03 + b1*a13 + b2*a23 + b3*a33; |
| |
| b0 = mat2[4]; |
| b1 = mat2[5]; |
| b2 = mat2[6]; |
| b3 = mat2[7]; |
| dest[4] = b0*a00 + b1*a10 + b2*a20 + b3*a30; |
| dest[5] = b0*a01 + b1*a11 + b2*a21 + b3*a31; |
| dest[6] = b0*a02 + b1*a12 + b2*a22 + b3*a32; |
| dest[7] = b0*a03 + b1*a13 + b2*a23 + b3*a33; |
| |
| b0 = mat2[8]; |
| b1 = mat2[9]; |
| b2 = mat2[10]; |
| b3 = mat2[11]; |
| dest[8] = b0*a00 + b1*a10 + b2*a20 + b3*a30; |
| dest[9] = b0*a01 + b1*a11 + b2*a21 + b3*a31; |
| dest[10] = b0*a02 + b1*a12 + b2*a22 + b3*a32; |
| dest[11] = b0*a03 + b1*a13 + b2*a23 + b3*a33; |
| |
| b0 = mat2[12]; |
| b1 = mat2[13]; |
| b2 = mat2[14]; |
| b3 = mat2[15]; |
| dest[12] = b0*a00 + b1*a10 + b2*a20 + b3*a30; |
| dest[13] = b0*a01 + b1*a11 + b2*a21 + b3*a31; |
| dest[14] = b0*a02 + b1*a12 + b2*a22 + b3*a32; |
| dest[15] = b0*a03 + b1*a13 + b2*a23 + b3*a33; |
| |
| return dest; |
| }; |
| |
| /** |
| * Transforms a vec3 with the given matrix |
| * 4th vector component is implicitly '1' |
| * |
| * @param {mat4} mat mat4 to transform the vector with |
| * @param {vec3} vec vec3 to transform |
| * @param {vec3} [dest] vec3 receiving operation result. If not specified result is written to vec |
| * |
| * @returns {vec3} dest if specified, vec otherwise |
| */ |
| mat4.multiplyVec3 = function (mat, vec, dest) { |
| if (!dest) { dest = vec; } |
| |
| var x = vec[0], y = vec[1], z = vec[2]; |
| |
| dest[0] = mat[0] * x + mat[4] * y + mat[8] * z + mat[12]; |
| dest[1] = mat[1] * x + mat[5] * y + mat[9] * z + mat[13]; |
| dest[2] = mat[2] * x + mat[6] * y + mat[10] * z + mat[14]; |
| |
| return dest; |
| }; |
| |
| /** |
| * Transforms a vec4 with the given matrix |
| * |
| * @param {mat4} mat mat4 to transform the vector with |
| * @param {vec4} vec vec4 to transform |
| * @param {vec4} [dest] vec4 receiving operation result. If not specified result is written to vec |
| * |
| * @returns {vec4} dest if specified, vec otherwise |
| */ |
| mat4.multiplyVec4 = function (mat, vec, dest) { |
| if (!dest) { dest = vec; } |
| |
| var x = vec[0], y = vec[1], z = vec[2], w = vec[3]; |
| |
| dest[0] = mat[0] * x + mat[4] * y + mat[8] * z + mat[12] * w; |
| dest[1] = mat[1] * x + mat[5] * y + mat[9] * z + mat[13] * w; |
| dest[2] = mat[2] * x + mat[6] * y + mat[10] * z + mat[14] * w; |
| dest[3] = mat[3] * x + mat[7] * y + mat[11] * z + mat[15] * w; |
| |
| return dest; |
| }; |
| |
| /** |
| * Translates a matrix by the given vector |
| * |
| * @param {mat4} mat mat4 to translate |
| * @param {vec3} vec vec3 specifying the translation |
| * @param {mat4} [dest] mat4 receiving operation result. If not specified result is written to mat |
| * |
| * @returns {mat4} dest if specified, mat otherwise |
| */ |
| mat4.translate = function (mat, vec, dest) { |
| var x = vec[0], y = vec[1], z = vec[2], |
| a00, a01, a02, a03, |
| a10, a11, a12, a13, |
| a20, a21, a22, a23; |
| |
| if (!dest || mat === dest) { |
| mat[12] = mat[0] * x + mat[4] * y + mat[8] * z + mat[12]; |
| mat[13] = mat[1] * x + mat[5] * y + mat[9] * z + mat[13]; |
| mat[14] = mat[2] * x + mat[6] * y + mat[10] * z + mat[14]; |
| mat[15] = mat[3] * x + mat[7] * y + mat[11] * z + mat[15]; |
| return mat; |
| } |
| |
| a00 = mat[0]; a01 = mat[1]; a02 = mat[2]; a03 = mat[3]; |
| a10 = mat[4]; a11 = mat[5]; a12 = mat[6]; a13 = mat[7]; |
| a20 = mat[8]; a21 = mat[9]; a22 = mat[10]; a23 = mat[11]; |
| |
| dest[0] = a00; dest[1] = a01; dest[2] = a02; dest[3] = a03; |
| dest[4] = a10; dest[5] = a11; dest[6] = a12; dest[7] = a13; |
| dest[8] = a20; dest[9] = a21; dest[10] = a22; dest[11] = a23; |
| |
| dest[12] = a00 * x + a10 * y + a20 * z + mat[12]; |
| dest[13] = a01 * x + a11 * y + a21 * z + mat[13]; |
| dest[14] = a02 * x + a12 * y + a22 * z + mat[14]; |
| dest[15] = a03 * x + a13 * y + a23 * z + mat[15]; |
| return dest; |
| }; |
| |
| /** |
| * Scales a matrix by the given vector |
| * |
| * @param {mat4} mat mat4 to scale |
| * @param {vec3} vec vec3 specifying the scale for each axis |
| * @param {mat4} [dest] mat4 receiving operation result. If not specified result is written to mat |
| * |
| * @param {mat4} dest if specified, mat otherwise |
| */ |
| mat4.scale = function (mat, vec, dest) { |
| var x = vec[0], y = vec[1], z = vec[2]; |
| |
| if (!dest || mat === dest) { |
| mat[0] *= x; |
| mat[1] *= x; |
| mat[2] *= x; |
| mat[3] *= x; |
| mat[4] *= y; |
| mat[5] *= y; |
| mat[6] *= y; |
| mat[7] *= y; |
| mat[8] *= z; |
| mat[9] *= z; |
| mat[10] *= z; |
| mat[11] *= z; |
| return mat; |
| } |
| |
| dest[0] = mat[0] * x; |
| dest[1] = mat[1] * x; |
| dest[2] = mat[2] * x; |
| dest[3] = mat[3] * x; |
| dest[4] = mat[4] * y; |
| dest[5] = mat[5] * y; |
| dest[6] = mat[6] * y; |
| dest[7] = mat[7] * y; |
| dest[8] = mat[8] * z; |
| dest[9] = mat[9] * z; |
| dest[10] = mat[10] * z; |
| dest[11] = mat[11] * z; |
| dest[12] = mat[12]; |
| dest[13] = mat[13]; |
| dest[14] = mat[14]; |
| dest[15] = mat[15]; |
| return dest; |
| }; |
| |
| /** |
| * Rotates a matrix by the given angle around the specified axis |
| * If rotating around a primary axis (X,Y,Z) one of the specialized rotation functions should be used instead for performance |
| * |
| * @param {mat4} mat mat4 to rotate |
| * @param {number} angle Angle (in radians) to rotate |
| * @param {vec3} axis vec3 representing the axis to rotate around |
| * @param {mat4} [dest] mat4 receiving operation result. If not specified result is written to mat |
| * |
| * @returns {mat4} dest if specified, mat otherwise |
| */ |
| mat4.rotate = function (mat, angle, axis, dest) { |
| var x = axis[0], y = axis[1], z = axis[2], |
| len = Math.sqrt(x * x + y * y + z * z), |
| s, c, t, |
| a00, a01, a02, a03, |
| a10, a11, a12, a13, |
| a20, a21, a22, a23, |
| b00, b01, b02, |
| b10, b11, b12, |
| b20, b21, b22; |
| |
| if (!len) { return null; } |
| if (len !== 1) { |
| len = 1 / len; |
| x *= len; |
| y *= len; |
| z *= len; |
| } |
| |
| s = Math.sin(angle); |
| c = Math.cos(angle); |
| t = 1 - c; |
| |
| a00 = mat[0]; a01 = mat[1]; a02 = mat[2]; a03 = mat[3]; |
| a10 = mat[4]; a11 = mat[5]; a12 = mat[6]; a13 = mat[7]; |
| a20 = mat[8]; a21 = mat[9]; a22 = mat[10]; a23 = mat[11]; |
| |
| // Construct the elements of the rotation matrix |
| b00 = x * x * t + c; b01 = y * x * t + z * s; b02 = z * x * t - y * s; |
| b10 = x * y * t - z * s; b11 = y * y * t + c; b12 = z * y * t + x * s; |
| b20 = x * z * t + y * s; b21 = y * z * t - x * s; b22 = z * z * t + c; |
| |
| if (!dest) { |
| dest = mat; |
| } else if (mat !== dest) { // If the source and destination differ, copy the unchanged last row |
| dest[12] = mat[12]; |
| dest[13] = mat[13]; |
| dest[14] = mat[14]; |
| dest[15] = mat[15]; |
| } |
| |
| // Perform rotation-specific matrix multiplication |
| dest[0] = a00 * b00 + a10 * b01 + a20 * b02; |
| dest[1] = a01 * b00 + a11 * b01 + a21 * b02; |
| dest[2] = a02 * b00 + a12 * b01 + a22 * b02; |
| dest[3] = a03 * b00 + a13 * b01 + a23 * b02; |
| |
| dest[4] = a00 * b10 + a10 * b11 + a20 * b12; |
| dest[5] = a01 * b10 + a11 * b11 + a21 * b12; |
| dest[6] = a02 * b10 + a12 * b11 + a22 * b12; |
| dest[7] = a03 * b10 + a13 * b11 + a23 * b12; |
| |
| dest[8] = a00 * b20 + a10 * b21 + a20 * b22; |
| dest[9] = a01 * b20 + a11 * b21 + a21 * b22; |
| dest[10] = a02 * b20 + a12 * b21 + a22 * b22; |
| dest[11] = a03 * b20 + a13 * b21 + a23 * b22; |
| return dest; |
| }; |
| |
| /** |
| * Rotates a matrix by the given angle around the X axis |
| * |
| * @param {mat4} mat mat4 to rotate |
| * @param {number} angle Angle (in radians) to rotate |
| * @param {mat4} [dest] mat4 receiving operation result. If not specified result is written to mat |
| * |
| * @returns {mat4} dest if specified, mat otherwise |
| */ |
| mat4.rotateX = function (mat, angle, dest) { |
| var s = Math.sin(angle), |
| c = Math.cos(angle), |
| a10 = mat[4], |
| a11 = mat[5], |
| a12 = mat[6], |
| a13 = mat[7], |
| a20 = mat[8], |
| a21 = mat[9], |
| a22 = mat[10], |
| a23 = mat[11]; |
| |
| if (!dest) { |
| dest = mat; |
| } else if (mat !== dest) { // If the source and destination differ, copy the unchanged rows |
| dest[0] = mat[0]; |
| dest[1] = mat[1]; |
| dest[2] = mat[2]; |
| dest[3] = mat[3]; |
| |
| dest[12] = mat[12]; |
| dest[13] = mat[13]; |
| dest[14] = mat[14]; |
| dest[15] = mat[15]; |
| } |
| |
| // Perform axis-specific matrix multiplication |
| dest[4] = a10 * c + a20 * s; |
| dest[5] = a11 * c + a21 * s; |
| dest[6] = a12 * c + a22 * s; |
| dest[7] = a13 * c + a23 * s; |
| |
| dest[8] = a10 * -s + a20 * c; |
| dest[9] = a11 * -s + a21 * c; |
| dest[10] = a12 * -s + a22 * c; |
| dest[11] = a13 * -s + a23 * c; |
| return dest; |
| }; |
| |
| /** |
| * Rotates a matrix by the given angle around the Y axis |
| * |
| * @param {mat4} mat mat4 to rotate |
| * @param {number} angle Angle (in radians) to rotate |
| * @param {mat4} [dest] mat4 receiving operation result. If not specified result is written to mat |
| * |
| * @returns {mat4} dest if specified, mat otherwise |
| */ |
| mat4.rotateY = function (mat, angle, dest) { |
| var s = Math.sin(angle), |
| c = Math.cos(angle), |
| a00 = mat[0], |
| a01 = mat[1], |
| a02 = mat[2], |
| a03 = mat[3], |
| a20 = mat[8], |
| a21 = mat[9], |
| a22 = mat[10], |
| a23 = mat[11]; |
| |
| if (!dest) { |
| dest = mat; |
| } else if (mat !== dest) { // If the source and destination differ, copy the unchanged rows |
| dest[4] = mat[4]; |
| dest[5] = mat[5]; |
| dest[6] = mat[6]; |
| dest[7] = mat[7]; |
| |
| dest[12] = mat[12]; |
| dest[13] = mat[13]; |
| dest[14] = mat[14]; |
| dest[15] = mat[15]; |
| } |
| |
| // Perform axis-specific matrix multiplication |
| dest[0] = a00 * c + a20 * -s; |
| dest[1] = a01 * c + a21 * -s; |
| dest[2] = a02 * c + a22 * -s; |
| dest[3] = a03 * c + a23 * -s; |
| |
| dest[8] = a00 * s + a20 * c; |
| dest[9] = a01 * s + a21 * c; |
| dest[10] = a02 * s + a22 * c; |
| dest[11] = a03 * s + a23 * c; |
| return dest; |
| }; |
| |
| /** |
| * Rotates a matrix by the given angle around the Z axis |
| * |
| * @param {mat4} mat mat4 to rotate |
| * @param {number} angle Angle (in radians) to rotate |
| * @param {mat4} [dest] mat4 receiving operation result. If not specified result is written to mat |
| * |
| * @returns {mat4} dest if specified, mat otherwise |
| */ |
| mat4.rotateZ = function (mat, angle, dest) { |
| var s = Math.sin(angle), |
| c = Math.cos(angle), |
| a00 = mat[0], |
| a01 = mat[1], |
| a02 = mat[2], |
| a03 = mat[3], |
| a10 = mat[4], |
| a11 = mat[5], |
| a12 = mat[6], |
| a13 = mat[7]; |
| |
| if (!dest) { |
| dest = mat; |
| } else if (mat !== dest) { // If the source and destination differ, copy the unchanged last row |
| dest[8] = mat[8]; |
| dest[9] = mat[9]; |
| dest[10] = mat[10]; |
| dest[11] = mat[11]; |
| |
| dest[12] = mat[12]; |
| dest[13] = mat[13]; |
| dest[14] = mat[14]; |
| dest[15] = mat[15]; |
| } |
| |
| // Perform axis-specific matrix multiplication |
| dest[0] = a00 * c + a10 * s; |
| dest[1] = a01 * c + a11 * s; |
| dest[2] = a02 * c + a12 * s; |
| dest[3] = a03 * c + a13 * s; |
| |
| dest[4] = a00 * -s + a10 * c; |
| dest[5] = a01 * -s + a11 * c; |
| dest[6] = a02 * -s + a12 * c; |
| dest[7] = a03 * -s + a13 * c; |
| |
| return dest; |
| }; |
| |
| /** |
| * Generates a frustum matrix with the given bounds |
| * |
| * @param {number} left Left bound of the frustum |
| * @param {number} right Right bound of the frustum |
| * @param {number} bottom Bottom bound of the frustum |
| * @param {number} top Top bound of the frustum |
| * @param {number} near Near bound of the frustum |
| * @param {number} far Far bound of the frustum |
| * @param {mat4} [dest] mat4 frustum matrix will be written into |
| * |
| * @returns {mat4} dest if specified, a new mat4 otherwise |
| */ |
| mat4.frustum = function (left, right, bottom, top, near, far, dest) { |
| if (!dest) { dest = mat4.create(); } |
| var rl = (right - left), |
| tb = (top - bottom), |
| fn = (far - near); |
| dest[0] = (near * 2) / rl; |
| dest[1] = 0; |
| dest[2] = 0; |
| dest[3] = 0; |
| dest[4] = 0; |
| dest[5] = (near * 2) / tb; |
| dest[6] = 0; |
| dest[7] = 0; |
| dest[8] = (right + left) / rl; |
| dest[9] = (top + bottom) / tb; |
| dest[10] = -(far + near) / fn; |
| dest[11] = -1; |
| dest[12] = 0; |
| dest[13] = 0; |
| dest[14] = -(far * near * 2) / fn; |
| dest[15] = 0; |
| return dest; |
| }; |
| |
| /** |
| * Generates a perspective projection matrix with the given bounds |
| * |
| * @param {number} fovy Vertical field of view |
| * @param {number} aspect Aspect ratio. typically viewport width/height |
| * @param {number} near Near bound of the frustum |
| * @param {number} far Far bound of the frustum |
| * @param {mat4} [dest] mat4 frustum matrix will be written into |
| * |
| * @returns {mat4} dest if specified, a new mat4 otherwise |
| */ |
| mat4.perspective = function (fovy, aspect, near, far, dest) { |
| var top = near * Math.tan(fovy * Math.PI / 360.0), |
| right = top * aspect; |
| return mat4.frustum(-right, right, -top, top, near, far, dest); |
| }; |
| |
| /** |
| * Generates a orthogonal projection matrix with the given bounds |
| * |
| * @param {number} left Left bound of the frustum |
| * @param {number} right Right bound of the frustum |
| * @param {number} bottom Bottom bound of the frustum |
| * @param {number} top Top bound of the frustum |
| * @param {number} near Near bound of the frustum |
| * @param {number} far Far bound of the frustum |
| * @param {mat4} [dest] mat4 frustum matrix will be written into |
| * |
| * @returns {mat4} dest if specified, a new mat4 otherwise |
| */ |
| mat4.ortho = function (left, right, bottom, top, near, far, dest) { |
| if (!dest) { dest = mat4.create(); } |
| var rl = (right - left), |
| tb = (top - bottom), |
| fn = (far - near); |
| dest[0] = 2 / rl; |
| dest[1] = 0; |
| dest[2] = 0; |
| dest[3] = 0; |
| dest[4] = 0; |
| dest[5] = 2 / tb; |
| dest[6] = 0; |
| dest[7] = 0; |
| dest[8] = 0; |
| dest[9] = 0; |
| dest[10] = -2 / fn; |
| dest[11] = 0; |
| dest[12] = -(left + right) / rl; |
| dest[13] = -(top + bottom) / tb; |
| dest[14] = -(far + near) / fn; |
| dest[15] = 1; |
| return dest; |
| }; |
| |
| /** |
| * Generates a look-at matrix with the given eye position, focal point, and up axis |
| * |
| * @param {vec3} eye Position of the viewer |
| * @param {vec3} center Point the viewer is looking at |
| * @param {vec3} up vec3 pointing "up" |
| * @param {mat4} [dest] mat4 frustum matrix will be written into |
| * |
| * @returns {mat4} dest if specified, a new mat4 otherwise |
| */ |
| mat4.lookAt = function (eye, center, up, dest) { |
| if (!dest) { dest = mat4.create(); } |
| |
| var x0, x1, x2, y0, y1, y2, z0, z1, z2, len, |
| eyex = eye[0], |
| eyey = eye[1], |
| eyez = eye[2], |
| upx = up[0], |
| upy = up[1], |
| upz = up[2], |
| centerx = center[0], |
| centery = center[1], |
| centerz = center[2]; |
| |
| if (eyex === centerx && eyey === centery && eyez === centerz) { |
| return mat4.identity(dest); |
| } |
| |
| //vec3.direction(eye, center, z); |
| z0 = eyex - centerx; |
| z1 = eyey - centery; |
| z2 = eyez - centerz; |
| |
| // normalize (no check needed for 0 because of early return) |
| len = 1 / Math.sqrt(z0 * z0 + z1 * z1 + z2 * z2); |
| z0 *= len; |
| z1 *= len; |
| z2 *= len; |
| |
| //vec3.normalize(vec3.cross(up, z, x)); |
| x0 = upy * z2 - upz * z1; |
| x1 = upz * z0 - upx * z2; |
| x2 = upx * z1 - upy * z0; |
| len = Math.sqrt(x0 * x0 + x1 * x1 + x2 * x2); |
| if (!len) { |
| x0 = 0; |
| x1 = 0; |
| x2 = 0; |
| } else { |
| len = 1 / len; |
| x0 *= len; |
| x1 *= len; |
| x2 *= len; |
| } |
| |
| //vec3.normalize(vec3.cross(z, x, y)); |
| y0 = z1 * x2 - z2 * x1; |
| y1 = z2 * x0 - z0 * x2; |
| y2 = z0 * x1 - z1 * x0; |
| |
| len = Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2); |
| if (!len) { |
| y0 = 0; |
| y1 = 0; |
| y2 = 0; |
| } else { |
| len = 1 / len; |
| y0 *= len; |
| y1 *= len; |
| y2 *= len; |
| } |
| |
| dest[0] = x0; |
| dest[1] = y0; |
| dest[2] = z0; |
| dest[3] = 0; |
| dest[4] = x1; |
| dest[5] = y1; |
| dest[6] = z1; |
| dest[7] = 0; |
| dest[8] = x2; |
| dest[9] = y2; |
| dest[10] = z2; |
| dest[11] = 0; |
| dest[12] = -(x0 * eyex + x1 * eyey + x2 * eyez); |
| dest[13] = -(y0 * eyex + y1 * eyey + y2 * eyez); |
| dest[14] = -(z0 * eyex + z1 * eyey + z2 * eyez); |
| dest[15] = 1; |
| |
| return dest; |
| }; |
| |
| /** |
| * Creates a matrix from a quaternion rotation and vector translation |
| * This is equivalent to (but much faster than): |
| * |
| * mat4.identity(dest); |
| * mat4.translate(dest, vec); |
| * var quatMat = mat4.create(); |
| * quat4.toMat4(quat, quatMat); |
| * mat4.multiply(dest, quatMat); |
| * |
| * @param {quat4} quat Rotation quaternion |
| * @param {vec3} vec Translation vector |
| * @param {mat4} [dest] mat4 receiving operation result. If not specified result is written to a new mat4 |
| * |
| * @returns {mat4} dest if specified, a new mat4 otherwise |
| */ |
| mat4.fromRotationTranslation = function (quat, vec, dest) { |
| if (!dest) { dest = mat4.create(); } |
| |
| // Quaternion math |
| var x = quat[0], y = quat[1], z = quat[2], w = quat[3], |
| x2 = x + x, |
| y2 = y + y, |
| z2 = z + z, |
| |
| xx = x * x2, |
| xy = x * y2, |
| xz = x * z2, |
| yy = y * y2, |
| yz = y * z2, |
| zz = z * z2, |
| wx = w * x2, |
| wy = w * y2, |
| wz = w * z2; |
| |
| dest[0] = 1 - (yy + zz); |
| dest[1] = xy + wz; |
| dest[2] = xz - wy; |
| dest[3] = 0; |
| dest[4] = xy - wz; |
| dest[5] = 1 - (xx + zz); |
| dest[6] = yz + wx; |
| dest[7] = 0; |
| dest[8] = xz + wy; |
| dest[9] = yz - wx; |
| dest[10] = 1 - (xx + yy); |
| dest[11] = 0; |
| dest[12] = vec[0]; |
| dest[13] = vec[1]; |
| dest[14] = vec[2]; |
| dest[15] = 1; |
| |
| return dest; |
| }; |
| |
| /** |
| * Returns a string representation of a mat4 |
| * |
| * @param {mat4} mat mat4 to represent as a string |
| * |
| * @returns {string} String representation of mat |
| */ |
| mat4.str = function (mat) { |
| return '[' + mat[0] + ', ' + mat[1] + ', ' + mat[2] + ', ' + mat[3] + |
| ', ' + mat[4] + ', ' + mat[5] + ', ' + mat[6] + ', ' + mat[7] + |
| ', ' + mat[8] + ', ' + mat[9] + ', ' + mat[10] + ', ' + mat[11] + |
| ', ' + mat[12] + ', ' + mat[13] + ', ' + mat[14] + ', ' + mat[15] + ']'; |
| }; |
| |
| /** |
| * @class Quaternion |
| * @name quat4 |
| */ |
| var quat4 = {}; |
| |
| /** |
| * Creates a new instance of a quat4 using the default array type |
| * Any javascript array containing at least 4 numeric elements can serve as a quat4 |
| * |
| * @param {quat4} [quat] quat4 containing values to initialize with |
| * |
| * @returns {quat4} New quat4 |
| */ |
| quat4.create = function (quat) { |
| var dest = new MatrixArray(4); |
| |
| if (quat) { |
| dest[0] = quat[0]; |
| dest[1] = quat[1]; |
| dest[2] = quat[2]; |
| dest[3] = quat[3]; |
| } else { |
| dest[0] = dest[1] = dest[2] = dest[3] = 0; |
| } |
| |
| return dest; |
| }; |
| |
| /** |
| * Creates a new instance of a quat4, initializing it with the given arguments |
| * |
| * @param {number} x X value |
| * @param {number} y Y value |
| * @param {number} z Z value |
| * @param {number} w W value |
| |
| * @returns {quat4} New quat4 |
| */ |
| quat4.createFrom = function (x, y, z, w) { |
| var dest = new MatrixArray(4); |
| |
| dest[0] = x; |
| dest[1] = y; |
| dest[2] = z; |
| dest[3] = w; |
| |
| return dest; |
| }; |
| |
| /** |
| * Copies the values of one quat4 to another |
| * |
| * @param {quat4} quat quat4 containing values to copy |
| * @param {quat4} dest quat4 receiving copied values |
| * |
| * @returns {quat4} dest |
| */ |
| quat4.set = function (quat, dest) { |
| dest[0] = quat[0]; |
| dest[1] = quat[1]; |
| dest[2] = quat[2]; |
| dest[3] = quat[3]; |
| |
| return dest; |
| }; |
| |
| /** |
| * Compares two quaternions for equality within a certain margin of error |
| * |
| * @param {quat4} a First vector |
| * @param {quat4} b Second vector |
| * |
| * @returns {Boolean} True if a is equivalent to b |
| */ |
| quat4.equal = function (a, b) { |
| return a === b || ( |
| Math.abs(a[0] - b[0]) < FLOAT_EPSILON && |
| Math.abs(a[1] - b[1]) < FLOAT_EPSILON && |
| Math.abs(a[2] - b[2]) < FLOAT_EPSILON && |
| Math.abs(a[3] - b[3]) < FLOAT_EPSILON |
| ); |
| }; |
| |
| /** |
| * Creates a new identity Quat4 |
| * |
| * @param {quat4} [dest] quat4 receiving copied values |
| * |
| * @returns {quat4} dest is specified, new quat4 otherwise |
| */ |
| quat4.identity = function (dest) { |
| if (!dest) { dest = quat4.create(); } |
| dest[0] = 0; |
| dest[1] = 0; |
| dest[2] = 0; |
| dest[3] = 1; |
| return dest; |
| }; |
| |
| var identityQuat4 = quat4.identity(); |
| |
| /** |
| * Calculates the W component of a quat4 from the X, Y, and Z components. |
| * Assumes that quaternion is 1 unit in length. |
| * Any existing W component will be ignored. |
| * |
| * @param {quat4} quat quat4 to calculate W component of |
| * @param {quat4} [dest] quat4 receiving calculated values. If not specified result is written to quat |
| * |
| * @returns {quat4} dest if specified, quat otherwise |
| */ |
| quat4.calculateW = function (quat, dest) { |
| var x = quat[0], y = quat[1], z = quat[2]; |
| |
| if (!dest || quat === dest) { |
| quat[3] = -Math.sqrt(Math.abs(1.0 - x * x - y * y - z * z)); |
| return quat; |
| } |
| dest[0] = x; |
| dest[1] = y; |
| dest[2] = z; |
| dest[3] = -Math.sqrt(Math.abs(1.0 - x * x - y * y - z * z)); |
| return dest; |
| }; |
| |
| /** |
| * Calculates the dot product of two quaternions |
| * |
| * @param {quat4} quat First operand |
| * @param {quat4} quat2 Second operand |
| * |
| * @return {number} Dot product of quat and quat2 |
| */ |
| quat4.dot = function(quat, quat2){ |
| return quat[0]*quat2[0] + quat[1]*quat2[1] + quat[2]*quat2[2] + quat[3]*quat2[3]; |
| }; |
| |
| /** |
| * Calculates the inverse of a quat4 |
| * |
| * @param {quat4} quat quat4 to calculate inverse of |
| * @param {quat4} [dest] quat4 receiving inverse values. If not specified result is written to quat |
| * |
| * @returns {quat4} dest if specified, quat otherwise |
| */ |
| quat4.inverse = function(quat, dest) { |
| var q0 = quat[0], q1 = quat[1], q2 = quat[2], q3 = quat[3], |
| dot = q0*q0 + q1*q1 + q2*q2 + q3*q3, |
| invDot = dot ? 1.0/dot : 0; |
| |
| // TODO: Would be faster to return [0,0,0,0] immediately if dot == 0 |
| |
| if(!dest || quat === dest) { |
| quat[0] *= -invDot; |
| quat[1] *= -invDot; |
| quat[2] *= -invDot; |
| quat[3] *= invDot; |
| return quat; |
| } |
| dest[0] = -quat[0]*invDot; |
| dest[1] = -quat[1]*invDot; |
| dest[2] = -quat[2]*invDot; |
| dest[3] = quat[3]*invDot; |
| return dest; |
| }; |
| |
| |
| /** |
| * Calculates the conjugate of a quat4 |
| * If the quaternion is normalized, this function is faster than quat4.inverse and produces the same result. |
| * |
| * @param {quat4} quat quat4 to calculate conjugate of |
| * @param {quat4} [dest] quat4 receiving conjugate values. If not specified result is written to quat |
| * |
| * @returns {quat4} dest if specified, quat otherwise |
| */ |
| quat4.conjugate = function (quat, dest) { |
| if (!dest || quat === dest) { |
| quat[0] *= -1; |
| quat[1] *= -1; |
| quat[2] *= -1; |
| return quat; |
| } |
| dest[0] = -quat[0]; |
| dest[1] = -quat[1]; |
| dest[2] = -quat[2]; |
| dest[3] = quat[3]; |
| return dest; |
| }; |
| |
| /** |
| * Calculates the length of a quat4 |
| * |
| * Params: |
| * @param {quat4} quat quat4 to calculate length of |
| * |
| * @returns Length of quat |
| */ |
| quat4.length = function (quat) { |
| var x = quat[0], y = quat[1], z = quat[2], w = quat[3]; |
| return Math.sqrt(x * x + y * y + z * z + w * w); |
| }; |
| |
| /** |
| * Generates a unit quaternion of the same direction as the provided quat4 |
| * If quaternion length is 0, returns [0, 0, 0, 0] |
| * |
| * @param {quat4} quat quat4 to normalize |
| * @param {quat4} [dest] quat4 receiving operation result. If not specified result is written to quat |
| * |
| * @returns {quat4} dest if specified, quat otherwise |
| */ |
| quat4.normalize = function (quat, dest) { |
| if (!dest) { dest = quat; } |
| |
| var x = quat[0], y = quat[1], z = quat[2], w = quat[3], |
| len = Math.sqrt(x * x + y * y + z * z + w * w); |
| if (len === 0) { |
| dest[0] = 0; |
| dest[1] = 0; |
| dest[2] = 0; |
| dest[3] = 0; |
| return dest; |
| } |
| len = 1 / len; |
| dest[0] = x * len; |
| dest[1] = y * len; |
| dest[2] = z * len; |
| dest[3] = w * len; |
| |
| return dest; |
| }; |
| |
| /** |
| * Performs quaternion addition |
| * |
| * @param {quat4} quat First operand |
| * @param {quat4} quat2 Second operand |
| * @param {quat4} [dest] quat4 receiving operation result. If not specified result is written to quat |
| * |
| * @returns {quat4} dest if specified, quat otherwise |
| */ |
| quat4.add = function (quat, quat2, dest) { |
| if(!dest || quat === dest) { |
| quat[0] += quat2[0]; |
| quat[1] += quat2[1]; |
| quat[2] += quat2[2]; |
| quat[3] += quat2[3]; |
| return quat; |
| } |
| dest[0] = quat[0]+quat2[0]; |
| dest[1] = quat[1]+quat2[1]; |
| dest[2] = quat[2]+quat2[2]; |
| dest[3] = quat[3]+quat2[3]; |
| return dest; |
| }; |
| |
| /** |
| * Performs a quaternion multiplication |
| * |
| * @param {quat4} quat First operand |
| * @param {quat4} quat2 Second operand |
| * @param {quat4} [dest] quat4 receiving operation result. If not specified result is written to quat |
| * |
| * @returns {quat4} dest if specified, quat otherwise |
| */ |
| quat4.multiply = function (quat, quat2, dest) { |
| if (!dest) { dest = quat; } |
| |
| var qax = quat[0], qay = quat[1], qaz = quat[2], qaw = quat[3], |
| qbx = quat2[0], qby = quat2[1], qbz = quat2[2], qbw = quat2[3]; |
| |
| dest[0] = qax * qbw + qaw * qbx + qay * qbz - qaz * qby; |
| dest[1] = qay * qbw + qaw * qby + qaz * qbx - qax * qbz; |
| dest[2] = qaz * qbw + qaw * qbz + qax * qby - qay * qbx; |
| dest[3] = qaw * qbw - qax * qbx - qay * qby - qaz * qbz; |
| |
| return dest; |
| }; |
| |
| /** |
| * Transforms a vec3 with the given quaternion |
| * |
| * @param {quat4} quat quat4 to transform the vector with |
| * @param {vec3} vec vec3 to transform |
| * @param {vec3} [dest] vec3 receiving operation result. If not specified result is written to vec |
| * |
| * @returns dest if specified, vec otherwise |
| */ |
| quat4.multiplyVec3 = function (quat, vec, dest) { |
| if (!dest) { dest = vec; } |
| |
| var x = vec[0], y = vec[1], z = vec[2], |
| qx = quat[0], qy = quat[1], qz = quat[2], qw = quat[3], |
| |
| // calculate quat * vec |
| ix = qw * x + qy * z - qz * y, |
| iy = qw * y + qz * x - qx * z, |
| iz = qw * z + qx * y - qy * x, |
| iw = -qx * x - qy * y - qz * z; |
| |
| // calculate result * inverse quat |
| dest[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy; |
| dest[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz; |
| dest[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx; |
| |
| return dest; |
| }; |
| |
| /** |
| * Multiplies the components of a quaternion by a scalar value |
| * |
| * @param {quat4} quat to scale |
| * @param {number} val Value to scale by |
| * @param {quat4} [dest] quat4 receiving operation result. If not specified result is written to quat |
| * |
| * @returns {quat4} dest if specified, quat otherwise |
| */ |
| quat4.scale = function (quat, val, dest) { |
| if(!dest || quat === dest) { |
| quat[0] *= val; |
| quat[1] *= val; |
| quat[2] *= val; |
| quat[3] *= val; |
| return quat; |
| } |
| dest[0] = quat[0]*val; |
| dest[1] = quat[1]*val; |
| dest[2] = quat[2]*val; |
| dest[3] = quat[3]*val; |
| return dest; |
| }; |
| |
| /** |
| * Calculates a 3x3 matrix from the given quat4 |
| * |
| * @param {quat4} quat quat4 to create matrix from |
| * @param {mat3} [dest] mat3 receiving operation result |
| * |
| * @returns {mat3} dest if specified, a new mat3 otherwise |
| */ |
| quat4.toMat3 = function (quat, dest) { |
| if (!dest) { dest = mat3.create(); } |
| |
| var x = quat[0], y = quat[1], z = quat[2], w = quat[3], |
| x2 = x + x, |
| y2 = y + y, |
| z2 = z + z, |
| |
| xx = x * x2, |
| xy = x * y2, |
| xz = x * z2, |
| yy = y * y2, |
| yz = y * z2, |
| zz = z * z2, |
| wx = w * x2, |
| wy = w * y2, |
| wz = w * z2; |
| |
| dest[0] = 1 - (yy + zz); |
| dest[1] = xy + wz; |
| dest[2] = xz - wy; |
| |
| dest[3] = xy - wz; |
| dest[4] = 1 - (xx + zz); |
| dest[5] = yz + wx; |
| |
| dest[6] = xz + wy; |
| dest[7] = yz - wx; |
| dest[8] = 1 - (xx + yy); |
| |
| return dest; |
| }; |
| |
| /** |
| * Calculates a 4x4 matrix from the given quat4 |
| * |
| * @param {quat4} quat quat4 to create matrix from |
| * @param {mat4} [dest] mat4 receiving operation result |
| * |
| * @returns {mat4} dest if specified, a new mat4 otherwise |
| */ |
| quat4.toMat4 = function (quat, dest) { |
| if (!dest) { dest = mat4.create(); } |
| |
| var x = quat[0], y = quat[1], z = quat[2], w = quat[3], |
| x2 = x + x, |
| y2 = y + y, |
| z2 = z + z, |
| |
| xx = x * x2, |
| xy = x * y2, |
| xz = x * z2, |
| yy = y * y2, |
| yz = y * z2, |
| zz = z * z2, |
| wx = w * x2, |
| wy = w * y2, |
| wz = w * z2; |
| |
| dest[0] = 1 - (yy + zz); |
| dest[1] = xy + wz; |
| dest[2] = xz - wy; |
| dest[3] = 0; |
| |
| dest[4] = xy - wz; |
| dest[5] = 1 - (xx + zz); |
| dest[6] = yz + wx; |
| dest[7] = 0; |
| |
| dest[8] = xz + wy; |
| dest[9] = yz - wx; |
| dest[10] = 1 - (xx + yy); |
| dest[11] = 0; |
| |
| dest[12] = 0; |
| dest[13] = 0; |
| dest[14] = 0; |
| dest[15] = 1; |
| |
| return dest; |
| }; |
| |
| /** |
| * Performs a spherical linear interpolation between two quat4 |
| * |
| * @param {quat4} quat First quaternion |
| * @param {quat4} quat2 Second quaternion |
| * @param {number} slerp Interpolation amount between the two inputs |
| * @param {quat4} [dest] quat4 receiving operation result. If not specified result is written to quat |
| * |
| * @returns {quat4} dest if specified, quat otherwise |
| */ |
| quat4.slerp = function (quat, quat2, slerp, dest) { |
| if (!dest) { dest = quat; } |
| |
| var cosHalfTheta = quat[0] * quat2[0] + quat[1] * quat2[1] + quat[2] * quat2[2] + quat[3] * quat2[3], |
| halfTheta, |
| sinHalfTheta, |
| ratioA, |
| ratioB; |
| |
| if (Math.abs(cosHalfTheta) >= 1.0) { |
| if (dest !== quat) { |
| dest[0] = quat[0]; |
| dest[1] = quat[1]; |
| dest[2] = quat[2]; |
| dest[3] = quat[3]; |
| } |
| return dest; |
| } |
| |
| halfTheta = Math.acos(cosHalfTheta); |
| sinHalfTheta = Math.sqrt(1.0 - cosHalfTheta * cosHalfTheta); |
| |
| if (Math.abs(sinHalfTheta) < 0.001) { |
| dest[0] = (quat[0] * 0.5 + quat2[0] * 0.5); |
| dest[1] = (quat[1] * 0.5 + quat2[1] * 0.5); |
| dest[2] = (quat[2] * 0.5 + quat2[2] * 0.5); |
| dest[3] = (quat[3] * 0.5 + quat2[3] * 0.5); |
| return dest; |
| } |
| |
| ratioA = Math.sin((1 - slerp) * halfTheta) / sinHalfTheta; |
| ratioB = Math.sin(slerp * halfTheta) / sinHalfTheta; |
| |
| dest[0] = (quat[0] * ratioA + quat2[0] * ratioB); |
| dest[1] = (quat[1] * ratioA + quat2[1] * ratioB); |
| dest[2] = (quat[2] * ratioA + quat2[2] * ratioB); |
| dest[3] = (quat[3] * ratioA + quat2[3] * ratioB); |
| |
| return dest; |
| }; |
| |
| /** |
| * Creates a quaternion from the given 3x3 rotation matrix. |
| * If dest is omitted, a new quaternion will be created. |
| * |
| * @param {mat3} mat the rotation matrix |
| * @param {quat4} [dest] an optional receiving quaternion |
| * |
| * @returns {quat4} the quaternion constructed from the rotation matrix |
| * |
| */ |
| quat4.fromRotationMatrix = function(mat, dest) { |
| if (!dest) dest = quat4.create(); |
| |
| // Algorithm in Ken Shoemake's article in 1987 SIGGRAPH course notes |
| // article "Quaternion Calculus and Fast Animation". |
| |
| var fTrace = mat[0] + mat[4] + mat[8]; |
| var fRoot; |
| |
| if ( fTrace > 0.0 ) { |
| // |w| > 1/2, may as well choose w > 1/2 |
| fRoot = Math.sqrt(fTrace + 1.0); // 2w |
| dest[3] = 0.5 * fRoot; |
| fRoot = 0.5/fRoot; // 1/(4w) |
| dest[0] = (mat[7]-mat[5])*fRoot; |
| dest[1] = (mat[2]-mat[6])*fRoot; |
| dest[2] = (mat[3]-mat[1])*fRoot; |
| } else { |
| // |w| <= 1/2 |
| var s_iNext = quat4.fromRotationMatrix.s_iNext = quat4.fromRotationMatrix.s_iNext || [1,2,0]; |
| var i = 0; |
| if ( mat[4] > mat[0] ) |
| i = 1; |
| if ( mat[8] > mat[i*3+i] ) |
| i = 2; |
| var j = s_iNext[i]; |
| var k = s_iNext[j]; |
| |
| fRoot = Math.sqrt(mat[i*3+i]-mat[j*3+j]-mat[k*3+k] + 1.0); |
| dest[i] = 0.5 * fRoot; |
| fRoot = 0.5 / fRoot; |
| dest[3] = (mat[k*3+j] - mat[j*3+k]) * fRoot; |
| dest[j] = (mat[j*3+i] + mat[i*3+j]) * fRoot; |
| dest[k] = (mat[k*3+i] + mat[i*3+k]) * fRoot; |
| } |
| |
| return dest; |
| }; |
| |
| /** |
| * Alias. See the description for quat4.fromRotationMatrix(). |
| */ |
| mat3.toQuat4 = quat4.fromRotationMatrix; |
| |
| (function() { |
| var mat = mat3.create(); |
| |
| /** |
| * Creates a quaternion from the 3 given vectors. They must be perpendicular |
| * to one another and represent the X, Y and Z axes. |
| * |
| * If dest is omitted, a new quat4 will be created. |
| * |
| * Example: The default OpenGL orientation has a view vector [0, 0, -1], |
| * right vector [1, 0, 0], and up vector [0, 1, 0]. A quaternion representing |
| * this orientation could be constructed with: |
| * |
| * quat = quat4.fromAxes([0, 0, -1], [1, 0, 0], [0, 1, 0], quat4.create()); |
| * |
| * @param {vec3} view the view vector, or direction the object is pointing in |
| * @param {vec3} right the right vector, or direction to the "right" of the object |
| * @param {vec3} up the up vector, or direction towards the object's "up" |
| * @param {quat4} [dest] an optional receiving quat4 |
| * |
| * @returns {quat4} dest |
| **/ |
| quat4.fromAxes = function(view, right, up, dest) { |
| mat[0] = right[0]; |
| mat[3] = right[1]; |
| mat[6] = right[2]; |
| |
| mat[1] = up[0]; |
| mat[4] = up[1]; |
| mat[7] = up[2]; |
| |
| mat[2] = view[0]; |
| mat[5] = view[1]; |
| mat[8] = view[2]; |
| |
| return quat4.fromRotationMatrix(mat, dest); |
| }; |
| })(); |
| |
| /** |
| * Sets a quat4 to the Identity and returns it. |
| * |
| * @param {quat4} [dest] quat4 to set. If omitted, a |
| * new quat4 will be created. |
| * |
| * @returns {quat4} dest |
| */ |
| quat4.identity = function(dest) { |
| if (!dest) dest = quat4.create(); |
| dest[0] = 0; |
| dest[1] = 0; |
| dest[2] = 0; |
| dest[3] = 1; |
| return dest; |
| }; |
| |
| /** |
| * Sets a quat4 from the given angle and rotation axis, |
| * then returns it. If dest is not given, a new quat4 is created. |
| * |
| * @param {Number} angle the angle in radians |
| * @param {vec3} axis the axis around which to rotate |
| * @param {quat4} [dest] the optional quat4 to store the result |
| * |
| * @returns {quat4} dest |
| **/ |
| quat4.fromAngleAxis = function(angle, axis, dest) { |
| // The quaternion representing the rotation is |
| // q = cos(A/2)+sin(A/2)*(x*i+y*j+z*k) |
| if (!dest) dest = quat4.create(); |
| |
| var half = angle * 0.5; |
| var s = Math.sin(half); |
| dest[3] = Math.cos(half); |
| dest[0] = s * axis[0]; |
| dest[1] = s * axis[1]; |
| dest[2] = s * axis[2]; |
| |
| return dest; |
| }; |
| |
| /** |
| * Stores the angle and axis in a vec4, where the XYZ components represent |
| * the axis and the W (4th) component is the angle in radians. |
| * |
| * If dest is not given, src will be modified in place and returned, after |
| * which it should not be considered not a quaternion (just an axis and angle). |
| * |
| * @param {quat4} quat the quaternion whose angle and axis to store |
| * @param {vec4} [dest] the optional vec4 to receive the data |
| * |
| * @returns {vec4} dest |
| */ |
| quat4.toAngleAxis = function(src, dest) { |
| if (!dest) dest = src; |
| // The quaternion representing the rotation is |
| // q = cos(A/2)+sin(A/2)*(x*i+y*j+z*k) |
| |
| var sqrlen = src[0]*src[0]+src[1]*src[1]+src[2]*src[2]; |
| if (sqrlen > 0) |
| { |
| dest[3] = 2 * Math.acos(src[3]); |
| var invlen = glMath.invsqrt(sqrlen); |
| dest[0] = src[0]*invlen; |
| dest[1] = src[1]*invlen; |
| dest[2] = src[2]*invlen; |
| } else { |
| // angle is 0 (mod 2*pi), so any axis will do |
| dest[3] = 0; |
| dest[0] = 1; |
| dest[1] = 0; |
| dest[2] = 0; |
| } |
| |
| return dest; |
| }; |
| |
| /** |
| * Returns a string representation of a quaternion |
| * |
| * @param {quat4} quat quat4 to represent as a string |
| * |
| * @returns {string} String representation of quat |
| */ |
| quat4.str = function (quat) { |
| return '[' + quat[0] + ', ' + quat[1] + ', ' + quat[2] + ', ' + quat[3] + ']'; |
| }; |
| |
| /** |
| * @class 2 Dimensional Vector |
| * @name vec2 |
| */ |
| var vec2 = {}; |
| |
| /** |
| * Creates a new vec2, initializing it from vec if vec |
| * is given. |
| * |
| * @param {vec2} [vec] the vector's initial contents |
| * @returns {vec2} a new 2D vector |
| */ |
| vec2.create = function(vec) { |
| var dest = new MatrixArray(2); |
| |
| if (vec) { |
| dest[0] = vec[0]; |
| dest[1] = vec[1]; |
| } else { |
| dest[0] = 0; |
| dest[1] = 0; |
| } |
| return dest; |
| }; |
| |
| /** |
| * Creates a new instance of a vec2, initializing it with the given arguments |
| * |
| * @param {number} x X value |
| * @param {number} y Y value |
| |
| * @returns {vec2} New vec2 |
| */ |
| vec2.createFrom = function (x, y) { |
| var dest = new MatrixArray(2); |
| |
| dest[0] = x; |
| dest[1] = y; |
| |
| return dest; |
| }; |
| |
| /** |
| * Adds the vec2's together. If dest is given, the result |
| * is stored there. Otherwise, the result is stored in vecB. |
| * |
| * @param {vec2} vecA the first operand |
| * @param {vec2} vecB the second operand |
| * @param {vec2} [dest] the optional receiving vector |
| * @returns {vec2} dest |
| */ |
| vec2.add = function(vecA, vecB, dest) { |
| if (!dest) dest = vecB; |
| dest[0] = vecA[0] + vecB[0]; |
| dest[1] = vecA[1] + vecB[1]; |
| return dest; |
| }; |
| |
| /** |
| * Subtracts vecB from vecA. If dest is given, the result |
| * is stored there. Otherwise, the result is stored in vecB. |
| * |
| * @param {vec2} vecA the first operand |
| * @param {vec2} vecB the second operand |
| * @param {vec2} [dest] the optional receiving vector |
| * @returns {vec2} dest |
| */ |
| vec2.subtract = function(vecA, vecB, dest) { |
| if (!dest) dest = vecB; |
| dest[0] = vecA[0] - vecB[0]; |
| dest[1] = vecA[1] - vecB[1]; |
| return dest; |
| }; |
| |
| /** |
| * Multiplies vecA with vecB. If dest is given, the result |
| * is stored there. Otherwise, the result is stored in vecB. |
| * |
| * @param {vec2} vecA the first operand |
| * @param {vec2} vecB the second operand |
| * @param {vec2} [dest] the optional receiving vector |
| * @returns {vec2} dest |
| */ |
| vec2.multiply = function(vecA, vecB, dest) { |
| if (!dest) dest = vecB; |
| dest[0] = vecA[0] * vecB[0]; |
| dest[1] = vecA[1] * vecB[1]; |
| return dest; |
| }; |
| |
| /** |
| * Divides vecA by vecB. If dest is given, the result |
| * is stored there. Otherwise, the result is stored in vecB. |
| * |
| * @param {vec2} vecA the first operand |
| * @param {vec2} vecB the second operand |
| * @param {vec2} [dest] the optional receiving vector |
| * @returns {vec2} dest |
| */ |
| vec2.divide = function(vecA, vecB, dest) { |
| if (!dest) dest = vecB; |
| dest[0] = vecA[0] / vecB[0]; |
| dest[1] = vecA[1] / vecB[1]; |
| return dest; |
| }; |
| |
| /** |
| * Scales vecA by some scalar number. If dest is given, the result |
| * is stored there. Otherwise, the result is stored in vecA. |
| * |
| * This is the same as multiplying each component of vecA |
| * by the given scalar. |
| * |
| * @param {vec2} vecA the vector to be scaled |
| * @param {Number} scalar the amount to scale the vector by |
| * @param {vec2} [dest] the optional receiving vector |
| * @returns {vec2} dest |
| */ |
| vec2.scale = function(vecA, scalar, dest) { |
| if (!dest) dest = vecA; |
| dest[0] = vecA[0] * scalar; |
| dest[1] = vecA[1] * scalar; |
| return dest; |
| }; |
| |
| /** |
| * Calculates the euclidian distance between two vec2 |
| * |
| * Params: |
| * @param {vec2} vecA First vector |
| * @param {vec2} vecB Second vector |
| * |
| * @returns {number} Distance between vecA and vecB |
| */ |
| vec2.dist = function (vecA, vecB) { |
| var x = vecB[0] - vecA[0], |
| y = vecB[1] - vecA[1]; |
| return Math.sqrt(x*x + y*y); |
| }; |
| |
| /** |
| * Copies the values of one vec2 to another |
| * |
| * @param {vec2} vec vec2 containing values to copy |
| * @param {vec2} dest vec2 receiving copied values |
| * |
| * @returns {vec2} dest |
| */ |
| vec2.set = function (vec, dest) { |
| dest[0] = vec[0]; |
| dest[1] = vec[1]; |
| return dest; |
| }; |
| |
| /** |
| * Compares two vectors for equality within a certain margin of error |
| * |
| * @param {vec2} a First vector |
| * @param {vec2} b Second vector |
| * |
| * @returns {Boolean} True if a is equivalent to b |
| */ |
| vec2.equal = function (a, b) { |
| return a === b || ( |
| Math.abs(a[0] - b[0]) < FLOAT_EPSILON && |
| Math.abs(a[1] - b[1]) < FLOAT_EPSILON |
| ); |
| }; |
| |
| /** |
| * Negates the components of a vec2 |
| * |
| * @param {vec2} vec vec2 to negate |
| * @param {vec2} [dest] vec2 receiving operation result. If not specified result is written to vec |
| * |
| * @returns {vec2} dest if specified, vec otherwise |
| */ |
| vec2.negate = function (vec, dest) { |
| if (!dest) { dest = vec; } |
| dest[0] = -vec[0]; |
| dest[1] = -vec[1]; |
| return dest; |
| }; |
| |
| /** |
| * Normlize a vec2 |
| * |
| * @param {vec2} vec vec2 to normalize |
| * @param {vec2} [dest] vec2 receiving operation result. If not specified result is written to vec |
| * |
| * @returns {vec2} dest if specified, vec otherwise |
| */ |
| vec2.normalize = function (vec, dest) { |
| if (!dest) { dest = vec; } |
| var mag = vec[0] * vec[0] + vec[1] * vec[1]; |
| if (mag > 0) { |
| mag = Math.sqrt(mag); |
| dest[0] = vec[0] / mag; |
| dest[1] = vec[1] / mag; |
| } else { |
| dest[0] = dest[1] = 0; |
| } |
| return dest; |
| }; |
| |
| /** |
| * Computes the cross product of two vec2's. Note that the cross product must by definition |
| * produce a 3D vector. If a dest vector is given, it will contain the resultant 3D vector. |
| * Otherwise, a scalar number will be returned, representing the vector's Z coordinate, since |
| * its X and Y must always equal 0. |
| * |
| * Examples: |
| * var crossResult = vec3.create(); |
| * vec2.cross([1, 2], [3, 4], crossResult); |
| * //=> [0, 0, -2] |
| * |
| * vec2.cross([1, 2], [3, 4]); |
| * //=> -2 |
| * |
| * See http://stackoverflow.com/questions/243945/calculating-a-2d-vectors-cross-product |
| * for some interesting facts. |
| * |
| * @param {vec2} vecA left operand |
| * @param {vec2} vecB right operand |
| * @param {vec2} [dest] optional vec2 receiving result. If not specified a scalar is returned |
| * |
| */ |
| vec2.cross = function (vecA, vecB, dest) { |
| var z = vecA[0] * vecB[1] - vecA[1] * vecB[0]; |
| if (!dest) return z; |
| dest[0] = dest[1] = 0; |
| dest[2] = z; |
| return dest; |
| }; |
| |
| /** |
| * Caclulates the length of a vec2 |
| * |
| * @param {vec2} vec vec2 to calculate length of |
| * |
| * @returns {Number} Length of vec |
| */ |
| vec2.length = function (vec) { |
| var x = vec[0], y = vec[1]; |
| return Math.sqrt(x * x + y * y); |
| }; |
| |
| /** |
| * Caclulates the squared length of a vec2 |
| * |
| * @param {vec2} vec vec2 to calculate squared length of |
| * |
| * @returns {Number} Squared Length of vec |
| */ |
| vec2.squaredLength = function (vec) { |
| var x = vec[0], y = vec[1]; |
| return x * x + y * y; |
| }; |
| |
| /** |
| * Caclulates the dot product of two vec2s |
| * |
| * @param {vec2} vecA First operand |
| * @param {vec2} vecB Second operand |
| * |
| * @returns {Number} Dot product of vecA and vecB |
| */ |
| vec2.dot = function (vecA, vecB) { |
| return vecA[0] * vecB[0] + vecA[1] * vecB[1]; |
| }; |
| |
| /** |
| * Generates a 2D unit vector pointing from one vector to another |
| * |
| * @param {vec2} vecA Origin vec2 |
| * @param {vec2} vecB vec2 to point to |
| * @param {vec2} [dest] vec2 receiving operation result. If not specified result is written to vecA |
| * |
| * @returns {vec2} dest if specified, vecA otherwise |
| */ |
| vec2.direction = function (vecA, vecB, dest) { |
| if (!dest) { dest = vecA; } |
| |
| var x = vecA[0] - vecB[0], |
| y = vecA[1] - vecB[1], |
| len = x * x + y * y; |
| |
| if (!len) { |
| dest[0] = 0; |
| dest[1] = 0; |
| dest[2] = 0; |
| return dest; |
| } |
| |
| len = 1 / Math.sqrt(len); |
| dest[0] = x * len; |
| dest[1] = y * len; |
| return dest; |
| }; |
| |
| /** |
| * Performs a linear interpolation between two vec2 |
| * |
| * @param {vec2} vecA First vector |
| * @param {vec2} vecB Second vector |
| * @param {Number} lerp Interpolation amount between the two inputs |
| * @param {vec2} [dest] vec2 receiving operation result. If not specified result is written to vecA |
| * |
| * @returns {vec2} dest if specified, vecA otherwise |
| */ |
| vec2.lerp = function (vecA, vecB, lerp, dest) { |
| if (!dest) { dest = vecA; } |
| dest[0] = vecA[0] + lerp * (vecB[0] - vecA[0]); |
| dest[1] = vecA[1] + lerp * (vecB[1] - vecA[1]); |
| return dest; |
| }; |
| |
| /** |
| * Returns a string representation of a vector |
| * |
| * @param {vec2} vec Vector to represent as a string |
| * |
| * @returns {String} String representation of vec |
| */ |
| vec2.str = function (vec) { |
| return '[' + vec[0] + ', ' + vec[1] + ']'; |
| }; |
| |
| /** |
| * @class 2x2 Matrix |
| * @name mat2 |
| */ |
| var mat2 = {}; |
| |
| /** |
| * Creates a new 2x2 matrix. If src is given, the new matrix |
| * is initialized to those values. |
| * |
| * @param {mat2} [src] the seed values for the new matrix, if any |
| * @returns {mat2} a new matrix |
| */ |
| mat2.create = function(src) { |
| var dest = new MatrixArray(4); |
| |
| if (src) { |
| dest[0] = src[0]; |
| dest[1] = src[1]; |
| dest[2] = src[2]; |
| dest[3] = src[3]; |
| } else { |
| dest[0] = dest[1] = dest[2] = dest[3] = 0; |
| } |
| return dest; |
| }; |
| |
| /** |
| * Creates a new instance of a mat2, initializing it with the given arguments |
| * |
| * @param {number} m00 |
| * @param {number} m01 |
| * @param {number} m10 |
| * @param {number} m11 |
| |
| * @returns {mat2} New mat2 |
| */ |
| mat2.createFrom = function (m00, m01, m10, m11) { |
| var dest = new MatrixArray(4); |
| |
| dest[0] = m00; |
| dest[1] = m01; |
| dest[2] = m10; |
| dest[3] = m11; |
| |
| return dest; |
| }; |
| |
| /** |
| * Copies the values of one mat2 to another |
| * |
| * @param {mat2} mat mat2 containing values to copy |
| * @param {mat2} dest mat2 receiving copied values |
| * |
| * @returns {mat2} dest |
| */ |
| mat2.set = function (mat, dest) { |
| dest[0] = mat[0]; |
| dest[1] = mat[1]; |
| dest[2] = mat[2]; |
| dest[3] = mat[3]; |
| return dest; |
| }; |
| |
| /** |
| * Compares two matrices for equality within a certain margin of error |
| * |
| * @param {mat2} a First matrix |
| * @param {mat2} b Second matrix |
| * |
| * @returns {Boolean} True if a is equivalent to b |
| */ |
| mat2.equal = function (a, b) { |
| return a === b || ( |
| Math.abs(a[0] - b[0]) < FLOAT_EPSILON && |
| Math.abs(a[1] - b[1]) < FLOAT_EPSILON && |
| Math.abs(a[2] - b[2]) < FLOAT_EPSILON && |
| Math.abs(a[3] - b[3]) < FLOAT_EPSILON |
| ); |
| }; |
| |
| /** |
| * Sets a mat2 to an identity matrix |
| * |
| * @param {mat2} [dest] mat2 to set. If omitted a new one will be created. |
| * |
| * @returns {mat2} dest |
| */ |
| mat2.identity = function (dest) { |
| if (!dest) { dest = mat2.create(); } |
| dest[0] = 1; |
| dest[1] = 0; |
| dest[2] = 0; |
| dest[3] = 1; |
| return dest; |
| }; |
| |
| /** |
| * Transposes a mat2 (flips the values over the diagonal) |
| * |
| * @param {mat2} mat mat2 to transpose |
| * @param {mat2} [dest] mat2 receiving transposed values. If not specified result is written to mat |
| * |
| * @param {mat2} dest if specified, mat otherwise |
| */ |
| mat2.transpose = function (mat, dest) { |
| // If we are transposing ourselves we can skip a few steps but have to cache some values |
| if (!dest || mat === dest) { |
| var a00 = mat[1]; |
| mat[1] = mat[2]; |
| mat[2] = a00; |
| return mat; |
| } |
| |
| dest[0] = mat[0]; |
| dest[1] = mat[2]; |
| dest[2] = mat[1]; |
| dest[3] = mat[3]; |
| return dest; |
| }; |
| |
| /** |
| * Calculates the determinant of a mat2 |
| * |
| * @param {mat2} mat mat2 to calculate determinant of |
| * |
| * @returns {Number} determinant of mat |
| */ |
| mat2.determinant = function (mat) { |
| return mat[0] * mat[3] - mat[2] * mat[1]; |
| }; |
| |
| /** |
| * Calculates the inverse matrix of a mat2 |
| * |
| * @param {mat2} mat mat2 to calculate inverse of |
| * @param {mat2} [dest] mat2 receiving inverse matrix. If not specified result is written to mat |
| * |
| * @param {mat2} dest is specified, mat otherwise, null if matrix cannot be inverted |
| */ |
| mat2.inverse = function (mat, dest) { |
| if (!dest) { dest = mat; } |
| var a0 = mat[0], a1 = mat[1], a2 = mat[2], a3 = mat[3]; |
| var det = a0 * a3 - a2 * a1; |
| if (!det) return null; |
| |
| det = 1.0 / det; |
| dest[0] = a3 * det; |
| dest[1] = -a1 * det; |
| dest[2] = -a2 * det; |
| dest[3] = a0 * det; |
| return dest; |
| }; |
| |
| /** |
| * Performs a matrix multiplication |
| * |
| * @param {mat2} matA First operand |
| * @param {mat2} matB Second operand |
| * @param {mat2} [dest] mat2 receiving operation result. If not specified result is written to matA |
| * |
| * @returns {mat2} dest if specified, matA otherwise |
| */ |
| mat2.multiply = function (matA, matB, dest) { |
| if (!dest) { dest = matA; } |
| var a11 = matA[0], |
| a12 = matA[1], |
| a21 = matA[2], |
| a22 = matA[3]; |
| dest[0] = a11 * matB[0] + a12 * matB[2]; |
| dest[1] = a11 * matB[1] + a12 * matB[3]; |
| dest[2] = a21 * matB[0] + a22 * matB[2]; |
| dest[3] = a21 * matB[1] + a22 * matB[3]; |
| return dest; |
| }; |
| |
| /** |
| * Rotates a 2x2 matrix by an angle |
| * |
| * @param {mat2} mat The matrix to rotate |
| * @param {Number} angle The angle in radians |
| * @param {mat2} [dest] Optional mat2 receiving the result. If omitted mat will be used. |
| * |
| * @returns {mat2} dest if specified, mat otherwise |
| */ |
| mat2.rotate = function (mat, angle, dest) { |
| if (!dest) { dest = mat; } |
| var a11 = mat[0], |
| a12 = mat[1], |
| a21 = mat[2], |
| a22 = mat[3], |
| s = Math.sin(angle), |
| c = Math.cos(angle); |
| dest[0] = a11 * c + a12 * s; |
| dest[1] = a11 * -s + a12 * c; |
| dest[2] = a21 * c + a22 * s; |
| dest[3] = a21 * -s + a22 * c; |
| return dest; |
| }; |
| |
| /** |
| * Multiplies the vec2 by the given 2x2 matrix |
| * |
| * @param {mat2} matrix the 2x2 matrix to multiply against |
| * @param {vec2} vec the vector to multiply |
| * @param {vec2} [dest] an optional receiving vector. If not given, vec is used. |
| * |
| * @returns {vec2} The multiplication result |
| **/ |
| mat2.multiplyVec2 = function(matrix, vec, dest) { |
| if (!dest) dest = vec; |
| var x = vec[0], y = vec[1]; |
| dest[0] = x * matrix[0] + y * matrix[1]; |
| dest[1] = x * matrix[2] + y * matrix[3]; |
| return dest; |
| }; |
| |
| /** |
| * Scales the mat2 by the dimensions in the given vec2 |
| * |
| * @param {mat2} matrix the 2x2 matrix to scale |
| * @param {vec2} vec the vector containing the dimensions to scale by |
| * @param {vec2} [dest] an optional receiving mat2. If not given, matrix is used. |
| * |
| * @returns {mat2} dest if specified, matrix otherwise |
| **/ |
| mat2.scale = function(matrix, vec, dest) { |
| if (!dest) { dest = matrix; } |
| var a11 = matrix[0], |
| a12 = matrix[1], |
| a21 = matrix[2], |
| a22 = matrix[3], |
| b11 = vec[0], |
| b22 = vec[1]; |
| dest[0] = a11 * b11; |
| dest[1] = a12 * b22; |
| dest[2] = a21 * b11; |
| dest[3] = a22 * b22; |
| return dest; |
| }; |
| |
| /** |
| * Returns a string representation of a mat2 |
| * |
| * @param {mat2} mat mat2 to represent as a string |
| * |
| * @param {String} String representation of mat |
| */ |
| mat2.str = function (mat) { |
| return '[' + mat[0] + ', ' + mat[1] + ', ' + mat[2] + ', ' + mat[3] + ']'; |
| }; |
| |
| /** |
| * @class 4 Dimensional Vector |
| * @name vec4 |
| */ |
| var vec4 = {}; |
| |
| /** |
| * Creates a new vec4, initializing it from vec if vec |
| * is given. |
| * |
| * @param {vec4} [vec] the vector's initial contents |
| * @returns {vec4} a new 2D vector |
| */ |
| vec4.create = function(vec) { |
| var dest = new MatrixArray(4); |
| |
| if (vec) { |
| dest[0] = vec[0]; |
| dest[1] = vec[1]; |
| dest[2] = vec[2]; |
| dest[3] = vec[3]; |
| } else { |
| dest[0] = 0; |
| dest[1] = 0; |
| dest[2] = 0; |
| dest[3] = 0; |
| } |
| return dest; |
| }; |
| |
| /** |
| * Creates a new instance of a vec4, initializing it with the given arguments |
| * |
| * @param {number} x X value |
| * @param {number} y Y value |
| * @param {number} z Z value |
| * @param {number} w W value |
| |
| * @returns {vec4} New vec4 |
| */ |
| vec4.createFrom = function (x, y, z, w) { |
| var dest = new MatrixArray(4); |
| |
| dest[0] = x; |
| dest[1] = y; |
| dest[2] = z; |
| dest[3] = w; |
| |
| return dest; |
| }; |
| |
| /** |
| * Adds the vec4's together. If dest is given, the result |
| * is stored there. Otherwise, the result is stored in vecB. |
| * |
| * @param {vec4} vecA the first operand |
| * @param {vec4} vecB the second operand |
| * @param {vec4} [dest] the optional receiving vector |
| * @returns {vec4} dest |
| */ |
| vec4.add = function(vecA, vecB, dest) { |
| if (!dest) dest = vecB; |
| dest[0] = vecA[0] + vecB[0]; |
| dest[1] = vecA[1] + vecB[1]; |
| dest[2] = vecA[2] + vecB[2]; |
| dest[3] = vecA[3] + vecB[3]; |
| return dest; |
| }; |
| |
| /** |
| * Subtracts vecB from vecA. If dest is given, the result |
| * is stored there. Otherwise, the result is stored in vecB. |
| * |
| * @param {vec4} vecA the first operand |
| * @param {vec4} vecB the second operand |
| * @param {vec4} [dest] the optional receiving vector |
| * @returns {vec4} dest |
| */ |
| vec4.subtract = function(vecA, vecB, dest) { |
| if (!dest) dest = vecB; |
| dest[0] = vecA[0] - vecB[0]; |
| dest[1] = vecA[1] - vecB[1]; |
| dest[2] = vecA[2] - vecB[2]; |
| dest[3] = vecA[3] - vecB[3]; |
| return dest; |
| }; |
| |
| /** |
| * Multiplies vecA with vecB. If dest is given, the result |
| * is stored there. Otherwise, the result is stored in vecB. |
| * |
| * @param {vec4} vecA the first operand |
| * @param {vec4} vecB the second operand |
| * @param {vec4} [dest] the optional receiving vector |
| * @returns {vec4} dest |
| */ |
| vec4.multiply = function(vecA, vecB, dest) { |
| if (!dest) dest = vecB; |
| dest[0] = vecA[0] * vecB[0]; |
| dest[1] = vecA[1] * vecB[1]; |
| dest[2] = vecA[2] * vecB[2]; |
| dest[3] = vecA[3] * vecB[3]; |
| return dest; |
| }; |
| |
| /** |
| * Divides vecA by vecB. If dest is given, the result |
| * is stored there. Otherwise, the result is stored in vecB. |
| * |
| * @param {vec4} vecA the first operand |
| * @param {vec4} vecB the second operand |
| * @param {vec4} [dest] the optional receiving vector |
| * @returns {vec4} dest |
| */ |
| vec4.divide = function(vecA, vecB, dest) { |
| if (!dest) dest = vecB; |
| dest[0] = vecA[0] / vecB[0]; |
| dest[1] = vecA[1] / vecB[1]; |
| dest[2] = vecA[2] / vecB[2]; |
| dest[3] = vecA[3] / vecB[3]; |
| return dest; |
| }; |
| |
| /** |
| * Scales vecA by some scalar number. If dest is given, the result |
| * is stored there. Otherwise, the result is stored in vecA. |
| * |
| * This is the same as multiplying each component of vecA |
| * by the given scalar. |
| * |
| * @param {vec4} vecA the vector to be scaled |
| * @param {Number} scalar the amount to scale the vector by |
| * @param {vec4} [dest] the optional receiving vector |
| * @returns {vec4} dest |
| */ |
| vec4.scale = function(vecA, scalar, dest) { |
| if (!dest) dest = vecA; |
| dest[0] = vecA[0] * scalar; |
| dest[1] = vecA[1] * scalar; |
| dest[2] = vecA[2] * scalar; |
| dest[3] = vecA[3] * scalar; |
| return dest; |
| }; |
| |
| /** |
| * Copies the values of one vec4 to another |
| * |
| * @param {vec4} vec vec4 containing values to copy |
| * @param {vec4} dest vec4 receiving copied values |
| * |
| * @returns {vec4} dest |
| */ |
| vec4.set = function (vec, dest) { |
| dest[0] = vec[0]; |
| dest[1] = vec[1]; |
| dest[2] = vec[2]; |
| dest[3] = vec[3]; |
| return dest; |
| }; |
| |
| /** |
| * Compares two vectors for equality within a certain margin of error |
| * |
| * @param {vec4} a First vector |
| * @param {vec4} b Second vector |
| * |
| * @returns {Boolean} True if a is equivalent to b |
| */ |
| vec4.equal = function (a, b) { |
| return a === b || ( |
| Math.abs(a[0] - b[0]) < FLOAT_EPSILON && |
| Math.abs(a[1] - b[1]) < FLOAT_EPSILON && |
| Math.abs(a[2] - b[2]) < FLOAT_EPSILON && |
| Math.abs(a[3] - b[3]) < FLOAT_EPSILON |
| ); |
| }; |
| |
| /** |
| * Negates the components of a vec4 |
| * |
| * @param {vec4} vec vec4 to negate |
| * @param {vec4} [dest] vec4 receiving operation result. If not specified result is written to vec |
| * |
| * @returns {vec4} dest if specified, vec otherwise |
| */ |
| vec4.negate = function (vec, dest) { |
| if (!dest) { dest = vec; } |
| dest[0] = -vec[0]; |
| dest[1] = -vec[1]; |
| dest[2] = -vec[2]; |
| dest[3] = -vec[3]; |
| return dest; |
| }; |
| |
| /** |
| * Caclulates the length of a vec2 |
| * |
| * @param {vec2} vec vec2 to calculate length of |
| * |
| * @returns {Number} Length of vec |
| */ |
| vec4.length = function (vec) { |
| var x = vec[0], y = vec[1], z = vec[2], w = vec[3]; |
| return Math.sqrt(x * x + y * y + z * z + w * w); |
| }; |
| |
| /** |
| * Caclulates the squared length of a vec4 |
| * |
| * @param {vec4} vec vec4 to calculate squared length of |
| * |
| * @returns {Number} Squared Length of vec |
| */ |
| vec4.squaredLength = function (vec) { |
| var x = vec[0], y = vec[1], z = vec[2], w = vec[3]; |
| return x * x + y * y + z * z + w * w; |
| }; |
| |
| /** |
| * Performs a linear interpolation between two vec4 |
| * |
| * @param {vec4} vecA First vector |
| * @param {vec4} vecB Second vector |
| * @param {Number} lerp Interpolation amount between the two inputs |
| * @param {vec4} [dest] vec4 receiving operation result. If not specified result is written to vecA |
| * |
| * @returns {vec4} dest if specified, vecA otherwise |
| */ |
| vec4.lerp = function (vecA, vecB, lerp, dest) { |
| if (!dest) { dest = vecA; } |
| dest[0] = vecA[0] + lerp * (vecB[0] - vecA[0]); |
| dest[1] = vecA[1] + lerp * (vecB[1] - vecA[1]); |
| dest[2] = vecA[2] + lerp * (vecB[2] - vecA[2]); |
| dest[3] = vecA[3] + lerp * (vecB[3] - vecA[3]); |
| return dest; |
| }; |
| |
| /** |
| * Returns a string representation of a vector |
| * |
| * @param {vec4} vec Vector to represent as a string |
| * |
| * @returns {String} String representation of vec |
| */ |
| vec4.str = function (vec) { |
| return '[' + vec[0] + ', ' + vec[1] + ', ' + vec[2] + ', ' + vec[3] + ']'; |
| }; |
| |
| /* |
| * Exports |
| */ |
| |
| if(root) { |
| root.glMatrixArrayType = MatrixArray; |
| root.MatrixArray = MatrixArray; |
| root.setMatrixArrayType = setMatrixArrayType; |
| root.determineMatrixArrayType = determineMatrixArrayType; |
| root.glMath = glMath; |
| root.vec2 = vec2; |
| root.vec3 = vec3; |
| root.vec4 = vec4; |
| root.mat2 = mat2; |
| root.mat3 = mat3; |
| root.mat4 = mat4; |
| root.quat4 = quat4; |
| } |
| |
| return { |
| glMatrixArrayType: MatrixArray, |
| MatrixArray: MatrixArray, |
| setMatrixArrayType: setMatrixArrayType, |
| determineMatrixArrayType: determineMatrixArrayType, |
| glMath: glMath, |
| vec2: vec2, |
| vec3: vec3, |
| vec4: vec4, |
| mat2: mat2, |
| mat3: mat3, |
| mat4: mat4, |
| quat4: quat4 |
| }; |
| })); |
| |