agenda-libre-ruby/public/assets/application-224c876758d5674...

77576 lines
2.1 MiB

/*!
* jQuery JavaScript Library v1.12.4
* http://jquery.com/
*
* Includes Sizzle.js
* http://sizzlejs.com/
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license
* http://jquery.org/license
*
* Date: 2016-05-20T17:17Z
*/
(function( global, factory ) {
if ( typeof module === "object" && typeof module.exports === "object" ) {
// For CommonJS and CommonJS-like environments where a proper `window`
// is present, execute the factory and get jQuery.
// For environments that do not have a `window` with a `document`
// (such as Node.js), expose a factory as module.exports.
// This accentuates the need for the creation of a real `window`.
// e.g. var jQuery = require("jquery")(window);
// See ticket #14549 for more info.
module.exports = global.document ?
factory( global, true ) :
function( w ) {
if ( !w.document ) {
throw new Error( "jQuery requires a window with a document" );
}
return factory( w );
};
} else {
factory( global );
}
// Pass this if window is not defined yet
}(typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
// Support: Firefox 18+
// Can't be in strict mode, several libs including ASP.NET trace
// the stack via arguments.caller.callee and Firefox dies if
// you try to trace through "use strict" call chains. (#13335)
//"use strict";
var deletedIds = [];
var document = window.document;
var slice = deletedIds.slice;
var concat = deletedIds.concat;
var push = deletedIds.push;
var indexOf = deletedIds.indexOf;
var class2type = {};
var toString = class2type.toString;
var hasOwn = class2type.hasOwnProperty;
var support = {};
var
version = "1.12.4",
// Define a local copy of jQuery
jQuery = function( selector, context ) {
// The jQuery object is actually just the init constructor 'enhanced'
// Need init if jQuery is called (just allow error to be thrown if not included)
return new jQuery.fn.init( selector, context );
},
// Support: Android<4.1, IE<9
// Make sure we trim BOM and NBSP
rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,
// Matches dashed string for camelizing
rmsPrefix = /^-ms-/,
rdashAlpha = /-([\da-z])/gi,
// Used by jQuery.camelCase as callback to replace()
fcamelCase = function( all, letter ) {
return letter.toUpperCase();
};
jQuery.fn = jQuery.prototype = {
// The current version of jQuery being used
jquery: version,
constructor: jQuery,
// Start with an empty selector
selector: "",
// The default length of a jQuery object is 0
length: 0,
toArray: function() {
return slice.call( this );
},
// Get the Nth element in the matched element set OR
// Get the whole matched element set as a clean array
get: function( num ) {
return num != null ?
// Return just the one element from the set
( num < 0 ? this[ num + this.length ] : this[ num ] ) :
// Return all the elements in a clean array
slice.call( this );
},
// Take an array of elements and push it onto the stack
// (returning the new matched element set)
pushStack: function( elems ) {
// Build a new jQuery matched element set
var ret = jQuery.merge( this.constructor(), elems );
// Add the old object onto the stack (as a reference)
ret.prevObject = this;
ret.context = this.context;
// Return the newly-formed element set
return ret;
},
// Execute a callback for every element in the matched set.
each: function( callback ) {
return jQuery.each( this, callback );
},
map: function( callback ) {
return this.pushStack( jQuery.map( this, function( elem, i ) {
return callback.call( elem, i, elem );
} ) );
},
slice: function() {
return this.pushStack( slice.apply( this, arguments ) );
},
first: function() {
return this.eq( 0 );
},
last: function() {
return this.eq( -1 );
},
eq: function( i ) {
var len = this.length,
j = +i + ( i < 0 ? len : 0 );
return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] );
},
end: function() {
return this.prevObject || this.constructor();
},
// For internal use only.
// Behaves like an Array's method, not like a jQuery method.
push: push,
sort: deletedIds.sort,
splice: deletedIds.splice
};
jQuery.extend = jQuery.fn.extend = function() {
var src, copyIsArray, copy, name, options, clone,
target = arguments[ 0 ] || {},
i = 1,
length = arguments.length,
deep = false;
// Handle a deep copy situation
if ( typeof target === "boolean" ) {
deep = target;
// skip the boolean and the target
target = arguments[ i ] || {};
i++;
}
// Handle case when target is a string or something (possible in deep copy)
if ( typeof target !== "object" && !jQuery.isFunction( target ) ) {
target = {};
}
// extend jQuery itself if only one argument is passed
if ( i === length ) {
target = this;
i--;
}
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 && ( jQuery.isPlainObject( copy ) ||
( copyIsArray = jQuery.isArray( copy ) ) ) ) {
if ( copyIsArray ) {
copyIsArray = false;
clone = src && jQuery.isArray( src ) ? src : [];
} else {
clone = src && jQuery.isPlainObject( src ) ? src : {};
}
// Never move original objects, clone them
target[ name ] = jQuery.extend( deep, clone, copy );
// Don't bring in undefined values
} else if ( copy !== undefined ) {
target[ name ] = copy;
}
}
}
}
// Return the modified object
return target;
};
jQuery.extend( {
// Unique for each copy of jQuery on the page
expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ),
// Assume jQuery is ready without the ready module
isReady: true,
error: function( msg ) {
throw new Error( msg );
},
noop: function() {},
// See test/unit/core.js for details concerning isFunction.
// Since version 1.3, DOM methods and functions like alert
// aren't supported. They return false on IE (#2968).
isFunction: function( obj ) {
return jQuery.type( obj ) === "function";
},
isArray: Array.isArray || function( obj ) {
return jQuery.type( obj ) === "array";
},
isWindow: function( obj ) {
/* jshint eqeqeq: false */
return obj != null && obj == obj.window;
},
isNumeric: function( obj ) {
// parseFloat NaNs numeric-cast false positives (null|true|false|"")
// ...but misinterprets leading-number strings, particularly hex literals ("0x...")
// subtraction forces infinities to NaN
// adding 1 corrects loss of precision from parseFloat (#15100)
var realStringObj = obj && obj.toString();
return !jQuery.isArray( obj ) && ( realStringObj - parseFloat( realStringObj ) + 1 ) >= 0;
},
isEmptyObject: function( obj ) {
var name;
for ( name in obj ) {
return false;
}
return true;
},
isPlainObject: function( obj ) {
var key;
// Must be an Object.
// Because of IE, we also have to check the presence of the constructor property.
// Make sure that DOM nodes and window objects don't pass through, as well
if ( !obj || jQuery.type( obj ) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
return false;
}
try {
// Not own constructor property must be Object
if ( obj.constructor &&
!hasOwn.call( obj, "constructor" ) &&
!hasOwn.call( obj.constructor.prototype, "isPrototypeOf" ) ) {
return false;
}
} catch ( e ) {
// IE8,9 Will throw exceptions on certain host objects #9897
return false;
}
// Support: IE<9
// Handle iteration over inherited properties before own properties.
if ( !support.ownFirst ) {
for ( key in obj ) {
return hasOwn.call( obj, key );
}
}
// Own properties are enumerated firstly, so to speed up,
// if last one is own, then all properties are own.
for ( key in obj ) {}
return key === undefined || hasOwn.call( obj, key );
},
type: function( obj ) {
if ( obj == null ) {
return obj + "";
}
return typeof obj === "object" || typeof obj === "function" ?
class2type[ toString.call( obj ) ] || "object" :
typeof obj;
},
// Workarounds based on findings by Jim Driscoll
// http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context
globalEval: function( data ) {
if ( data && jQuery.trim( data ) ) {
// We use execScript on Internet Explorer
// We use an anonymous function so that context is window
// rather than jQuery in Firefox
( window.execScript || function( data ) {
window[ "eval" ].call( window, data ); // jscs:ignore requireDotNotation
} )( data );
}
},
// Convert dashed to camelCase; used by the css and data modules
// Microsoft forgot to hump their vendor prefix (#9572)
camelCase: function( string ) {
return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
},
nodeName: function( elem, name ) {
return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
},
each: function( obj, callback ) {
var length, i = 0;
if ( isArrayLike( obj ) ) {
length = obj.length;
for ( ; i < length; i++ ) {
if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
break;
}
}
} else {
for ( i in obj ) {
if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
break;
}
}
}
return obj;
},
// Support: Android<4.1, IE<9
trim: function( text ) {
return text == null ?
"" :
( text + "" ).replace( rtrim, "" );
},
// results is for internal usage only
makeArray: function( arr, results ) {
var ret = results || [];
if ( arr != null ) {
if ( isArrayLike( Object( arr ) ) ) {
jQuery.merge( ret,
typeof arr === "string" ?
[ arr ] : arr
);
} else {
push.call( ret, arr );
}
}
return ret;
},
inArray: function( elem, arr, i ) {
var len;
if ( arr ) {
if ( indexOf ) {
return indexOf.call( arr, elem, i );
}
len = arr.length;
i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;
for ( ; i < len; i++ ) {
// Skip accessing in sparse arrays
if ( i in arr && arr[ i ] === elem ) {
return i;
}
}
}
return -1;
},
merge: function( first, second ) {
var len = +second.length,
j = 0,
i = first.length;
while ( j < len ) {
first[ i++ ] = second[ j++ ];
}
// Support: IE<9
// Workaround casting of .length to NaN on otherwise arraylike objects (e.g., NodeLists)
if ( len !== len ) {
while ( second[ j ] !== undefined ) {
first[ i++ ] = second[ j++ ];
}
}
first.length = i;
return first;
},
grep: function( elems, callback, invert ) {
var callbackInverse,
matches = [],
i = 0,
length = elems.length,
callbackExpect = !invert;
// Go through the array, only saving the items
// that pass the validator function
for ( ; i < length; i++ ) {
callbackInverse = !callback( elems[ i ], i );
if ( callbackInverse !== callbackExpect ) {
matches.push( elems[ i ] );
}
}
return matches;
},
// arg is for internal usage only
map: function( elems, callback, arg ) {
var length, value,
i = 0,
ret = [];
// Go through the array, translating each of the items to their new values
if ( isArrayLike( elems ) ) {
length = elems.length;
for ( ; i < length; i++ ) {
value = callback( elems[ i ], i, arg );
if ( value != null ) {
ret.push( value );
}
}
// Go through every key on the object,
} else {
for ( i in elems ) {
value = callback( elems[ i ], i, arg );
if ( value != null ) {
ret.push( value );
}
}
}
// Flatten any nested arrays
return concat.apply( [], ret );
},
// A global GUID counter for objects
guid: 1,
// Bind a function to a context, optionally partially applying any
// arguments.
proxy: function( fn, context ) {
var args, proxy, tmp;
if ( typeof context === "string" ) {
tmp = fn[ context ];
context = fn;
fn = tmp;
}
// Quick check to determine if target is callable, in the spec
// this throws a TypeError, but we will just return undefined.
if ( !jQuery.isFunction( fn ) ) {
return undefined;
}
// Simulated bind
args = slice.call( arguments, 2 );
proxy = function() {
return fn.apply( context || this, args.concat( slice.call( arguments ) ) );
};
// Set the guid of unique handler to the same of original handler, so it can be removed
proxy.guid = fn.guid = fn.guid || jQuery.guid++;
return proxy;
},
now: function() {
return +( new Date() );
},
// jQuery.support is not used in Core but other projects attach their
// properties to it so it needs to exist.
support: support
} );
// JSHint would error on this code due to the Symbol not being defined in ES5.
// Defining this global in .jshintrc would create a danger of using the global
// unguarded in another place, it seems safer to just disable JSHint for these
// three lines.
/* jshint ignore: start */
if ( typeof Symbol === "function" ) {
jQuery.fn[ Symbol.iterator ] = deletedIds[ Symbol.iterator ];
}
/* jshint ignore: end */
// Populate the class2type map
jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ),
function( i, name ) {
class2type[ "[object " + name + "]" ] = name.toLowerCase();
} );
function isArrayLike( obj ) {
// Support: iOS 8.2 (not reproducible in simulator)
// `in` check used to prevent JIT error (gh-2145)
// hasOwn isn't used here due to false negatives
// regarding Nodelist length in IE
var length = !!obj && "length" in obj && obj.length,
type = jQuery.type( obj );
if ( type === "function" || jQuery.isWindow( obj ) ) {
return false;
}
return type === "array" || length === 0 ||
typeof length === "number" && length > 0 && ( length - 1 ) in obj;
}
var Sizzle =
/*!
* Sizzle CSS Selector Engine v2.2.1
* http://sizzlejs.com/
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license
* http://jquery.org/license
*
* Date: 2015-10-17
*/
(function( window ) {
var i,
support,
Expr,
getText,
isXML,
tokenize,
compile,
select,
outermostContext,
sortInput,
hasDuplicate,
// Local document vars
setDocument,
document,
docElem,
documentIsHTML,
rbuggyQSA,
rbuggyMatches,
matches,
contains,
// Instance-specific data
expando = "sizzle" + 1 * new Date(),
preferredDoc = window.document,
dirruns = 0,
done = 0,
classCache = createCache(),
tokenCache = createCache(),
compilerCache = createCache(),
sortOrder = function( a, b ) {
if ( a === b ) {
hasDuplicate = true;
}
return 0;
},
// General-purpose constants
MAX_NEGATIVE = 1 << 31,
// Instance methods
hasOwn = ({}).hasOwnProperty,
arr = [],
pop = arr.pop,
push_native = arr.push,
push = arr.push,
slice = arr.slice,
// Use a stripped-down indexOf as it's faster than native
// http://jsperf.com/thor-indexof-vs-for/5
indexOf = function( list, elem ) {
var i = 0,
len = list.length;
for ( ; i < len; i++ ) {
if ( list[i] === elem ) {
return i;
}
}
return -1;
},
booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",
// Regular expressions
// http://www.w3.org/TR/css3-selectors/#whitespace
whitespace = "[\\x20\\t\\r\\n\\f]",
// http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
identifier = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",
// Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors
attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace +
// Operator (capture 2)
"*([*^$|!~]?=)" + whitespace +
// "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]"
"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace +
"*\\]",
pseudos = ":(" + identifier + ")(?:\\((" +
// To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:
// 1. quoted (capture 3; capture 4 or capture 5)
"('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" +
// 2. simple (capture 6)
"((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" +
// 3. anything else (capture 2)
".*" +
")\\)|)",
// Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
rwhitespace = new RegExp( whitespace + "+", "g" ),
rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ),
rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ),
rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ),
rpseudo = new RegExp( pseudos ),
ridentifier = new RegExp( "^" + identifier + "$" ),
matchExpr = {
"ID": new RegExp( "^#(" + identifier + ")" ),
"CLASS": new RegExp( "^\\.(" + identifier + ")" ),
"TAG": new RegExp( "^(" + identifier + "|[*])" ),
"ATTR": new RegExp( "^" + attributes ),
"PSEUDO": new RegExp( "^" + pseudos ),
"CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace +
"*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace +
"*(\\d+)|))" + whitespace + "*\\)|)", "i" ),
"bool": new RegExp( "^(?:" + booleans + ")$", "i" ),
// For use in libraries implementing .is()
// We use this for POS matching in `select`
"needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" +
whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" )
},
rinputs = /^(?:input|select|textarea|button)$/i,
rheader = /^h\d$/i,
rnative = /^[^{]+\{\s*\[native \w/,
// Easily-parseable/retrievable ID or TAG or CLASS selectors
rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,
rsibling = /[+~]/,
rescape = /'|\\/g,
// CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ),
funescape = function( _, escaped, escapedWhitespace ) {
var high = "0x" + escaped - 0x10000;
// NaN means non-codepoint
// Support: Firefox<24
// Workaround erroneous numeric interpretation of +"0x"
return high !== high || escapedWhitespace ?
escaped :
high < 0 ?
// BMP codepoint
String.fromCharCode( high + 0x10000 ) :
// Supplemental Plane codepoint (surrogate pair)
String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
},
// Used for iframes
// See setDocument()
// Removing the function wrapper causes a "Permission Denied"
// error in IE
unloadHandler = function() {
setDocument();
};
// Optimize for push.apply( _, NodeList )
try {
push.apply(
(arr = slice.call( preferredDoc.childNodes )),
preferredDoc.childNodes
);
// Support: Android<4.0
// Detect silently failing push.apply
arr[ preferredDoc.childNodes.length ].nodeType;
} catch ( e ) {
push = { apply: arr.length ?
// Leverage slice if possible
function( target, els ) {
push_native.apply( target, slice.call(els) );
} :
// Support: IE<9
// Otherwise append directly
function( target, els ) {
var j = target.length,
i = 0;
// Can't trust NodeList.length
while ( (target[j++] = els[i++]) ) {}
target.length = j - 1;
}
};
}
function Sizzle( selector, context, results, seed ) {
var m, i, elem, nid, nidselect, match, groups, newSelector,
newContext = context && context.ownerDocument,
// nodeType defaults to 9, since context defaults to document
nodeType = context ? context.nodeType : 9;
results = results || [];
// Return early from calls with invalid selector or context
if ( typeof selector !== "string" || !selector ||
nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) {
return results;
}
// Try to shortcut find operations (as opposed to filters) in HTML documents
if ( !seed ) {
if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {
setDocument( context );
}
context = context || document;
if ( documentIsHTML ) {
// If the selector is sufficiently simple, try using a "get*By*" DOM method
// (excepting DocumentFragment context, where the methods don't exist)
if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) {
// ID selector
if ( (m = match[1]) ) {
// Document context
if ( nodeType === 9 ) {
if ( (elem = context.getElementById( m )) ) {
// Support: IE, Opera, Webkit
// TODO: identify versions
// getElementById can match elements by name instead of ID
if ( elem.id === m ) {
results.push( elem );
return results;
}
} else {
return results;
}
// Element context
} else {
// Support: IE, Opera, Webkit
// TODO: identify versions
// getElementById can match elements by name instead of ID
if ( newContext && (elem = newContext.getElementById( m )) &&
contains( context, elem ) &&
elem.id === m ) {
results.push( elem );
return results;
}
}
// Type selector
} else if ( match[2] ) {
push.apply( results, context.getElementsByTagName( selector ) );
return results;
// Class selector
} else if ( (m = match[3]) && support.getElementsByClassName &&
context.getElementsByClassName ) {
push.apply( results, context.getElementsByClassName( m ) );
return results;
}
}
// Take advantage of querySelectorAll
if ( support.qsa &&
!compilerCache[ selector + " " ] &&
(!rbuggyQSA || !rbuggyQSA.test( selector )) ) {
if ( nodeType !== 1 ) {
newContext = context;
newSelector = selector;
// qSA looks outside Element context, which is not what we want
// Thanks to Andrew Dupont for this workaround technique
// Support: IE <=8
// Exclude object elements
} else if ( context.nodeName.toLowerCase() !== "object" ) {
// Capture the context ID, setting it first if necessary
if ( (nid = context.getAttribute( "id" )) ) {
nid = nid.replace( rescape, "\\$&" );
} else {
context.setAttribute( "id", (nid = expando) );
}
// Prefix every selector in the list
groups = tokenize( selector );
i = groups.length;
nidselect = ridentifier.test( nid ) ? "#" + nid : "[id='" + nid + "']";
while ( i-- ) {
groups[i] = nidselect + " " + toSelector( groups[i] );
}
newSelector = groups.join( "," );
// Expand context for sibling selectors
newContext = rsibling.test( selector ) && testContext( context.parentNode ) ||
context;
}
if ( newSelector ) {
try {
push.apply( results,
newContext.querySelectorAll( newSelector )
);
return results;
} catch ( qsaError ) {
} finally {
if ( nid === expando ) {
context.removeAttribute( "id" );
}
}
}
}
}
}
// All others
return select( selector.replace( rtrim, "$1" ), context, results, seed );
}
/**
* Create key-value caches of limited size
* @returns {function(string, object)} Returns the Object data after storing it on itself with
* property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)
* deleting the oldest entry
*/
function createCache() {
var keys = [];
function cache( key, value ) {
// Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
if ( keys.push( key + " " ) > Expr.cacheLength ) {
// Only keep the most recent entries
delete cache[ keys.shift() ];
}
return (cache[ key + " " ] = value);
}
return cache;
}
/**
* Mark a function for special use by Sizzle
* @param {Function} fn The function to mark
*/
function markFunction( fn ) {
fn[ expando ] = true;
return fn;
}
/**
* Support testing using an element
* @param {Function} fn Passed the created div and expects a boolean result
*/
function assert( fn ) {
var div = document.createElement("div");
try {
return !!fn( div );
} catch (e) {
return false;
} finally {
// Remove from its parent by default
if ( div.parentNode ) {
div.parentNode.removeChild( div );
}
// release memory in IE
div = null;
}
}
/**
* Adds the same handler for all of the specified attrs
* @param {String} attrs Pipe-separated list of attributes
* @param {Function} handler The method that will be applied
*/
function addHandle( attrs, handler ) {
var arr = attrs.split("|"),
i = arr.length;
while ( i-- ) {
Expr.attrHandle[ arr[i] ] = handler;
}
}
/**
* Checks document order of two siblings
* @param {Element} a
* @param {Element} b
* @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b
*/
function siblingCheck( a, b ) {
var cur = b && a,
diff = cur && a.nodeType === 1 && b.nodeType === 1 &&
( ~b.sourceIndex || MAX_NEGATIVE ) -
( ~a.sourceIndex || MAX_NEGATIVE );
// Use IE sourceIndex if available on both nodes
if ( diff ) {
return diff;
}
// Check if b follows a
if ( cur ) {
while ( (cur = cur.nextSibling) ) {
if ( cur === b ) {
return -1;
}
}
}
return a ? 1 : -1;
}
/**
* Returns a function to use in pseudos for input types
* @param {String} type
*/
function createInputPseudo( type ) {
return function( elem ) {
var name = elem.nodeName.toLowerCase();
return name === "input" && elem.type === type;
};
}
/**
* Returns a function to use in pseudos for buttons
* @param {String} type
*/
function createButtonPseudo( type ) {
return function( elem ) {
var name = elem.nodeName.toLowerCase();
return (name === "input" || name === "button") && elem.type === type;
};
}
/**
* Returns a function to use in pseudos for positionals
* @param {Function} fn
*/
function createPositionalPseudo( fn ) {
return markFunction(function( argument ) {
argument = +argument;
return markFunction(function( seed, matches ) {
var j,
matchIndexes = fn( [], seed.length, argument ),
i = matchIndexes.length;
// Match elements found at the specified indexes
while ( i-- ) {
if ( seed[ (j = matchIndexes[i]) ] ) {
seed[j] = !(matches[j] = seed[j]);
}
}
});
});
}
/**
* Checks a node for validity as a Sizzle context
* @param {Element|Object=} context
* @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value
*/
function testContext( context ) {
return context && typeof context.getElementsByTagName !== "undefined" && context;
}
// Expose support vars for convenience
support = Sizzle.support = {};
/**
* Detects XML nodes
* @param {Element|Object} elem An element or a document
* @returns {Boolean} True iff elem is a non-HTML XML node
*/
isXML = Sizzle.isXML = function( elem ) {
// documentElement is verified for cases where it doesn't yet exist
// (such as loading iframes in IE - #4833)
var documentElement = elem && (elem.ownerDocument || elem).documentElement;
return documentElement ? documentElement.nodeName !== "HTML" : false;
};
/**
* Sets document-related variables once based on the current document
* @param {Element|Object} [doc] An element or document object to use to set the document
* @returns {Object} Returns the current document
*/
setDocument = Sizzle.setDocument = function( node ) {
var hasCompare, parent,
doc = node ? node.ownerDocument || node : preferredDoc;
// Return early if doc is invalid or already selected
if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {
return document;
}
// Update global variables
document = doc;
docElem = document.documentElement;
documentIsHTML = !isXML( document );
// Support: IE 9-11, Edge
// Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936)
if ( (parent = document.defaultView) && parent.top !== parent ) {
// Support: IE 11
if ( parent.addEventListener ) {
parent.addEventListener( "unload", unloadHandler, false );
// Support: IE 9 - 10 only
} else if ( parent.attachEvent ) {
parent.attachEvent( "onunload", unloadHandler );
}
}
/* Attributes
---------------------------------------------------------------------- */
// Support: IE<8
// Verify that getAttribute really returns attributes and not properties
// (excepting IE8 booleans)
support.attributes = assert(function( div ) {
div.className = "i";
return !div.getAttribute("className");
});
/* getElement(s)By*
---------------------------------------------------------------------- */
// Check if getElementsByTagName("*") returns only elements
support.getElementsByTagName = assert(function( div ) {
div.appendChild( document.createComment("") );
return !div.getElementsByTagName("*").length;
});
// Support: IE<9
support.getElementsByClassName = rnative.test( document.getElementsByClassName );
// Support: IE<10
// Check if getElementById returns elements by name
// The broken getElementById methods don't pick up programatically-set names,
// so use a roundabout getElementsByName test
support.getById = assert(function( div ) {
docElem.appendChild( div ).id = expando;
return !document.getElementsByName || !document.getElementsByName( expando ).length;
});
// ID find and filter
if ( support.getById ) {
Expr.find["ID"] = function( id, context ) {
if ( typeof context.getElementById !== "undefined" && documentIsHTML ) {
var m = context.getElementById( id );
return m ? [ m ] : [];
}
};
Expr.filter["ID"] = function( id ) {
var attrId = id.replace( runescape, funescape );
return function( elem ) {
return elem.getAttribute("id") === attrId;
};
};
} else {
// Support: IE6/7
// getElementById is not reliable as a find shortcut
delete Expr.find["ID"];
Expr.filter["ID"] = function( id ) {
var attrId = id.replace( runescape, funescape );
return function( elem ) {
var node = typeof elem.getAttributeNode !== "undefined" &&
elem.getAttributeNode("id");
return node && node.value === attrId;
};
};
}
// Tag
Expr.find["TAG"] = support.getElementsByTagName ?
function( tag, context ) {
if ( typeof context.getElementsByTagName !== "undefined" ) {
return context.getElementsByTagName( tag );
// DocumentFragment nodes don't have gEBTN
} else if ( support.qsa ) {
return context.querySelectorAll( tag );
}
} :
function( tag, context ) {
var elem,
tmp = [],
i = 0,
// By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too
results = context.getElementsByTagName( tag );
// Filter out possible comments
if ( tag === "*" ) {
while ( (elem = results[i++]) ) {
if ( elem.nodeType === 1 ) {
tmp.push( elem );
}
}
return tmp;
}
return results;
};
// Class
Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) {
if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) {
return context.getElementsByClassName( className );
}
};
/* QSA/matchesSelector
---------------------------------------------------------------------- */
// QSA and matchesSelector support
// matchesSelector(:active) reports false when true (IE9/Opera 11.5)
rbuggyMatches = [];
// qSa(:focus) reports false when true (Chrome 21)
// We allow this because of a bug in IE8/9 that throws an error
// whenever `document.activeElement` is accessed on an iframe
// So, we allow :focus to pass through QSA all the time to avoid the IE error
// See http://bugs.jquery.com/ticket/13378
rbuggyQSA = [];
if ( (support.qsa = rnative.test( document.querySelectorAll )) ) {
// Build QSA regex
// Regex strategy adopted from Diego Perini
assert(function( div ) {
// Select is set to empty string on purpose
// This is to test IE's treatment of not explicitly
// setting a boolean content attribute,
// since its presence should be enough
// http://bugs.jquery.com/ticket/12359
docElem.appendChild( div ).innerHTML = "<a id='" + expando + "'></a>" +
"<select id='" + expando + "-\r\\' msallowcapture=''>" +
"<option selected=''></option></select>";
// Support: IE8, Opera 11-12.16
// Nothing should be selected when empty strings follow ^= or $= or *=
// The test attribute must be unknown in Opera but "safe" for WinRT
// http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section
if ( div.querySelectorAll("[msallowcapture^='']").length ) {
rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" );
}
// Support: IE8
// Boolean attributes and "value" are not treated correctly
if ( !div.querySelectorAll("[selected]").length ) {
rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" );
}
// Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+
if ( !div.querySelectorAll( "[id~=" + expando + "-]" ).length ) {
rbuggyQSA.push("~=");
}
// Webkit/Opera - :checked should return selected option elements
// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
// IE8 throws error here and will not see later tests
if ( !div.querySelectorAll(":checked").length ) {
rbuggyQSA.push(":checked");
}
// Support: Safari 8+, iOS 8+
// https://bugs.webkit.org/show_bug.cgi?id=136851
// In-page `selector#id sibing-combinator selector` fails
if ( !div.querySelectorAll( "a#" + expando + "+*" ).length ) {
rbuggyQSA.push(".#.+[+~]");
}
});
assert(function( div ) {
// Support: Windows 8 Native Apps
// The type and name attributes are restricted during .innerHTML assignment
var input = document.createElement("input");
input.setAttribute( "type", "hidden" );
div.appendChild( input ).setAttribute( "name", "D" );
// Support: IE8
// Enforce case-sensitivity of name attribute
if ( div.querySelectorAll("[name=d]").length ) {
rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" );
}
// FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
// IE8 throws error here and will not see later tests
if ( !div.querySelectorAll(":enabled").length ) {
rbuggyQSA.push( ":enabled", ":disabled" );
}
// Opera 10-11 does not throw on post-comma invalid pseudos
div.querySelectorAll("*,:x");
rbuggyQSA.push(",.*:");
});
}
if ( (support.matchesSelector = rnative.test( (matches = docElem.matches ||
docElem.webkitMatchesSelector ||
docElem.mozMatchesSelector ||
docElem.oMatchesSelector ||
docElem.msMatchesSelector) )) ) {
assert(function( div ) {
// Check to see if it's possible to do matchesSelector
// on a disconnected node (IE 9)
support.disconnectedMatch = matches.call( div, "div" );
// This should fail with an exception
// Gecko does not error, returns false instead
matches.call( div, "[s!='']:x" );
rbuggyMatches.push( "!=", pseudos );
});
}
rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") );
rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") );
/* Contains
---------------------------------------------------------------------- */
hasCompare = rnative.test( docElem.compareDocumentPosition );
// Element contains another
// Purposefully self-exclusive
// As in, an element does not contain itself
contains = hasCompare || rnative.test( docElem.contains ) ?
function( a, b ) {
var adown = a.nodeType === 9 ? a.documentElement : a,
bup = b && b.parentNode;
return a === bup || !!( bup && bup.nodeType === 1 && (
adown.contains ?
adown.contains( bup ) :
a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
));
} :
function( a, b ) {
if ( b ) {
while ( (b = b.parentNode) ) {
if ( b === a ) {
return true;
}
}
}
return false;
};
/* Sorting
---------------------------------------------------------------------- */
// Document order sorting
sortOrder = hasCompare ?
function( a, b ) {
// Flag for duplicate removal
if ( a === b ) {
hasDuplicate = true;
return 0;
}
// Sort on method existence if only one input has compareDocumentPosition
var compare = !a.compareDocumentPosition - !b.compareDocumentPosition;
if ( compare ) {
return compare;
}
// Calculate position if both inputs belong to the same document
compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ?
a.compareDocumentPosition( b ) :
// Otherwise we know they are disconnected
1;
// Disconnected nodes
if ( compare & 1 ||
(!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) {
// Choose the first element that is related to our preferred document
if ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) {
return -1;
}
if ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) {
return 1;
}
// Maintain original order
return sortInput ?
( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
0;
}
return compare & 4 ? -1 : 1;
} :
function( a, b ) {
// Exit early if the nodes are identical
if ( a === b ) {
hasDuplicate = true;
return 0;
}
var cur,
i = 0,
aup = a.parentNode,
bup = b.parentNode,
ap = [ a ],
bp = [ b ];
// Parentless nodes are either documents or disconnected
if ( !aup || !bup ) {
return a === document ? -1 :
b === document ? 1 :
aup ? -1 :
bup ? 1 :
sortInput ?
( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
0;
// If the nodes are siblings, we can do a quick check
} else if ( aup === bup ) {
return siblingCheck( a, b );
}
// Otherwise we need full lists of their ancestors for comparison
cur = a;
while ( (cur = cur.parentNode) ) {
ap.unshift( cur );
}
cur = b;
while ( (cur = cur.parentNode) ) {
bp.unshift( cur );
}
// Walk down the tree looking for a discrepancy
while ( ap[i] === bp[i] ) {
i++;
}
return i ?
// Do a sibling check if the nodes have a common ancestor
siblingCheck( ap[i], bp[i] ) :
// Otherwise nodes in our document sort first
ap[i] === preferredDoc ? -1 :
bp[i] === preferredDoc ? 1 :
0;
};
return document;
};
Sizzle.matches = function( expr, elements ) {
return Sizzle( expr, null, null, elements );
};
Sizzle.matchesSelector = function( elem, expr ) {
// Set document vars if needed
if ( ( elem.ownerDocument || elem ) !== document ) {
setDocument( elem );
}
// Make sure that attribute selectors are quoted
expr = expr.replace( rattributeQuotes, "='$1']" );
if ( support.matchesSelector && documentIsHTML &&
!compilerCache[ expr + " " ] &&
( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&
( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) {
try {
var ret = matches.call( elem, expr );
// IE 9's matchesSelector returns false on disconnected nodes
if ( ret || support.disconnectedMatch ||
// As well, disconnected nodes are said to be in a document
// fragment in IE 9
elem.document && elem.document.nodeType !== 11 ) {
return ret;
}
} catch (e) {}
}
return Sizzle( expr, document, null, [ elem ] ).length > 0;
};
Sizzle.contains = function( context, elem ) {
// Set document vars if needed
if ( ( context.ownerDocument || context ) !== document ) {
setDocument( context );
}
return contains( context, elem );
};
Sizzle.attr = function( elem, name ) {
// Set document vars if needed
if ( ( elem.ownerDocument || elem ) !== document ) {
setDocument( elem );
}
var fn = Expr.attrHandle[ name.toLowerCase() ],
// Don't get fooled by Object.prototype properties (jQuery #13807)
val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?
fn( elem, name, !documentIsHTML ) :
undefined;
return val !== undefined ?
val :
support.attributes || !documentIsHTML ?
elem.getAttribute( name ) :
(val = elem.getAttributeNode(name)) && val.specified ?
val.value :
null;
};
Sizzle.error = function( msg ) {
throw new Error( "Syntax error, unrecognized expression: " + msg );
};
/**
* Document sorting and removing duplicates
* @param {ArrayLike} results
*/
Sizzle.uniqueSort = function( results ) {
var elem,
duplicates = [],
j = 0,
i = 0;
// Unless we *know* we can detect duplicates, assume their presence
hasDuplicate = !support.detectDuplicates;
sortInput = !support.sortStable && results.slice( 0 );
results.sort( sortOrder );
if ( hasDuplicate ) {
while ( (elem = results[i++]) ) {
if ( elem === results[ i ] ) {
j = duplicates.push( i );
}
}
while ( j-- ) {
results.splice( duplicates[ j ], 1 );
}
}
// Clear input after sorting to release objects
// See https://github.com/jquery/sizzle/pull/225
sortInput = null;
return results;
};
/**
* Utility function for retrieving the text value of an array of DOM nodes
* @param {Array|Element} elem
*/
getText = Sizzle.getText = function( elem ) {
var node,
ret = "",
i = 0,
nodeType = elem.nodeType;
if ( !nodeType ) {
// If no nodeType, this is expected to be an array
while ( (node = elem[i++]) ) {
// Do not traverse comment nodes
ret += getText( node );
}
} else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
// Use textContent for elements
// innerText usage removed for consistency of new lines (jQuery #11153)
if ( typeof elem.textContent === "string" ) {
return elem.textContent;
} else {
// Traverse its children
for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
ret += getText( elem );
}
}
} else if ( nodeType === 3 || nodeType === 4 ) {
return elem.nodeValue;
}
// Do not include comment or processing instruction nodes
return ret;
};
Expr = Sizzle.selectors = {
// Can be adjusted by the user
cacheLength: 50,
createPseudo: markFunction,
match: matchExpr,
attrHandle: {},
find: {},
relative: {
">": { dir: "parentNode", first: true },
" ": { dir: "parentNode" },
"+": { dir: "previousSibling", first: true },
"~": { dir: "previousSibling" }
},
preFilter: {
"ATTR": function( match ) {
match[1] = match[1].replace( runescape, funescape );
// Move the given value to match[3] whether quoted or unquoted
match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape );
if ( match[2] === "~=" ) {
match[3] = " " + match[3] + " ";
}
return match.slice( 0, 4 );
},
"CHILD": function( match ) {
/* matches from matchExpr["CHILD"]
1 type (only|nth|...)
2 what (child|of-type)
3 argument (even|odd|\d*|\d*n([+-]\d+)?|...)
4 xn-component of xn+y argument ([+-]?\d*n|)
5 sign of xn-component
6 x of xn-component
7 sign of y-component
8 y of y-component
*/
match[1] = match[1].toLowerCase();
if ( match[1].slice( 0, 3 ) === "nth" ) {
// nth-* requires argument
if ( !match[3] ) {
Sizzle.error( match[0] );
}
// numeric x and y parameters for Expr.filter.CHILD
// remember that false/true cast respectively to 0/1
match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) );
match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" );
// other types prohibit arguments
} else if ( match[3] ) {
Sizzle.error( match[0] );
}
return match;
},
"PSEUDO": function( match ) {
var excess,
unquoted = !match[6] && match[2];
if ( matchExpr["CHILD"].test( match[0] ) ) {
return null;
}
// Accept quoted arguments as-is
if ( match[3] ) {
match[2] = match[4] || match[5] || "";
// Strip excess characters from unquoted arguments
} else if ( unquoted && rpseudo.test( unquoted ) &&
// Get excess from tokenize (recursively)
(excess = tokenize( unquoted, true )) &&
// advance to the next closing parenthesis
(excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) {
// excess is a negative index
match[0] = match[0].slice( 0, excess );
match[2] = unquoted.slice( 0, excess );
}
// Return only captures needed by the pseudo filter method (type and argument)
return match.slice( 0, 3 );
}
},
filter: {
"TAG": function( nodeNameSelector ) {
var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();
return nodeNameSelector === "*" ?
function() { return true; } :
function( elem ) {
return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
};
},
"CLASS": function( className ) {
var pattern = classCache[ className + " " ];
return pattern ||
(pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) &&
classCache( className, function( elem ) {
return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" );
});
},
"ATTR": function( name, operator, check ) {
return function( elem ) {
var result = Sizzle.attr( elem, name );
if ( result == null ) {
return operator === "!=";
}
if ( !operator ) {
return true;
}
result += "";
return operator === "=" ? result === check :
operator === "!=" ? result !== check :
operator === "^=" ? check && result.indexOf( check ) === 0 :
operator === "*=" ? check && result.indexOf( check ) > -1 :
operator === "$=" ? check && result.slice( -check.length ) === check :
operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 :
operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" :
false;
};
},
"CHILD": function( type, what, argument, first, last ) {
var simple = type.slice( 0, 3 ) !== "nth",
forward = type.slice( -4 ) !== "last",
ofType = what === "of-type";
return first === 1 && last === 0 ?
// Shortcut for :nth-*(n)
function( elem ) {
return !!elem.parentNode;
} :
function( elem, context, xml ) {
var cache, uniqueCache, outerCache, node, nodeIndex, start,
dir = simple !== forward ? "nextSibling" : "previousSibling",
parent = elem.parentNode,
name = ofType && elem.nodeName.toLowerCase(),
useCache = !xml && !ofType,
diff = false;
if ( parent ) {
// :(first|last|only)-(child|of-type)
if ( simple ) {
while ( dir ) {
node = elem;
while ( (node = node[ dir ]) ) {
if ( ofType ?
node.nodeName.toLowerCase() === name :
node.nodeType === 1 ) {
return false;
}
}
// Reverse direction for :only-* (if we haven't yet done so)
start = dir = type === "only" && !start && "nextSibling";
}
return true;
}
start = [ forward ? parent.firstChild : parent.lastChild ];
// non-xml :nth-child(...) stores cache data on `parent`
if ( forward && useCache ) {
// Seek `elem` from a previously-cached index
// ...in a gzip-friendly way
node = parent;
outerCache = node[ expando ] || (node[ expando ] = {});
// Support: IE <9 only
// Defend against cloned attroperties (jQuery gh-1709)
uniqueCache = outerCache[ node.uniqueID ] ||
(outerCache[ node.uniqueID ] = {});
cache = uniqueCache[ type ] || [];
nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];
diff = nodeIndex && cache[ 2 ];
node = nodeIndex && parent.childNodes[ nodeIndex ];
while ( (node = ++nodeIndex && node && node[ dir ] ||
// Fallback to seeking `elem` from the start
(diff = nodeIndex = 0) || start.pop()) ) {
// When found, cache indexes on `parent` and break
if ( node.nodeType === 1 && ++diff && node === elem ) {
uniqueCache[ type ] = [ dirruns, nodeIndex, diff ];
break;
}
}
} else {
// Use previously-cached element index if available
if ( useCache ) {
// ...in a gzip-friendly way
node = elem;
outerCache = node[ expando ] || (node[ expando ] = {});
// Support: IE <9 only
// Defend against cloned attroperties (jQuery gh-1709)
uniqueCache = outerCache[ node.uniqueID ] ||
(outerCache[ node.uniqueID ] = {});
cache = uniqueCache[ type ] || [];
nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];
diff = nodeIndex;
}
// xml :nth-child(...)
// or :nth-last-child(...) or :nth(-last)?-of-type(...)
if ( diff === false ) {
// Use the same loop as above to seek `elem` from the start
while ( (node = ++nodeIndex && node && node[ dir ] ||
(diff = nodeIndex = 0) || start.pop()) ) {
if ( ( ofType ?
node.nodeName.toLowerCase() === name :
node.nodeType === 1 ) &&
++diff ) {
// Cache the index of each encountered element
if ( useCache ) {
outerCache = node[ expando ] || (node[ expando ] = {});
// Support: IE <9 only
// Defend against cloned attroperties (jQuery gh-1709)
uniqueCache = outerCache[ node.uniqueID ] ||
(outerCache[ node.uniqueID ] = {});
uniqueCache[ type ] = [ dirruns, diff ];
}
if ( node === elem ) {
break;
}
}
}
}
}
// Incorporate the offset, then check against cycle size
diff -= last;
return diff === first || ( diff % first === 0 && diff / first >= 0 );
}
};
},
"PSEUDO": function( pseudo, argument ) {
// pseudo-class names are case-insensitive
// http://www.w3.org/TR/selectors/#pseudo-classes
// Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
// Remember that setFilters inherits from pseudos
var args,
fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||
Sizzle.error( "unsupported pseudo: " + pseudo );
// The user may use createPseudo to indicate that
// arguments are needed to create the filter function
// just as Sizzle does
if ( fn[ expando ] ) {
return fn( argument );
}
// But maintain support for old signatures
if ( fn.length > 1 ) {
args = [ pseudo, pseudo, "", argument ];
return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?
markFunction(function( seed, matches ) {
var idx,
matched = fn( seed, argument ),
i = matched.length;
while ( i-- ) {
idx = indexOf( seed, matched[i] );
seed[ idx ] = !( matches[ idx ] = matched[i] );
}
}) :
function( elem ) {
return fn( elem, 0, args );
};
}
return fn;
}
},
pseudos: {
// Potentially complex pseudos
"not": markFunction(function( selector ) {
// Trim the selector passed to compile
// to avoid treating leading and trailing
// spaces as combinators
var input = [],
results = [],
matcher = compile( selector.replace( rtrim, "$1" ) );
return matcher[ expando ] ?
markFunction(function( seed, matches, context, xml ) {
var elem,
unmatched = matcher( seed, null, xml, [] ),
i = seed.length;
// Match elements unmatched by `matcher`
while ( i-- ) {
if ( (elem = unmatched[i]) ) {
seed[i] = !(matches[i] = elem);
}
}
}) :
function( elem, context, xml ) {
input[0] = elem;
matcher( input, null, xml, results );
// Don't keep the element (issue #299)
input[0] = null;
return !results.pop();
};
}),
"has": markFunction(function( selector ) {
return function( elem ) {
return Sizzle( selector, elem ).length > 0;
};
}),
"contains": markFunction(function( text ) {
text = text.replace( runescape, funescape );
return function( elem ) {
return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;
};
}),
// "Whether an element is represented by a :lang() selector
// is based solely on the element's language value
// being equal to the identifier C,
// or beginning with the identifier C immediately followed by "-".
// The matching of C against the element's language value is performed case-insensitively.
// The identifier C does not have to be a valid language name."
// http://www.w3.org/TR/selectors/#lang-pseudo
"lang": markFunction( function( lang ) {
// lang value must be a valid identifier
if ( !ridentifier.test(lang || "") ) {
Sizzle.error( "unsupported lang: " + lang );
}
lang = lang.replace( runescape, funescape ).toLowerCase();
return function( elem ) {
var elemLang;
do {
if ( (elemLang = documentIsHTML ?
elem.lang :
elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) {
elemLang = elemLang.toLowerCase();
return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0;
}
} while ( (elem = elem.parentNode) && elem.nodeType === 1 );
return false;
};
}),
// Miscellaneous
"target": function( elem ) {
var hash = window.location && window.location.hash;
return hash && hash.slice( 1 ) === elem.id;
},
"root": function( elem ) {
return elem === docElem;
},
"focus": function( elem ) {
return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);
},
// Boolean properties
"enabled": function( elem ) {
return elem.disabled === false;
},
"disabled": function( elem ) {
return elem.disabled === true;
},
"checked": function( elem ) {
// In CSS3, :checked should return both checked and selected elements
// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
var nodeName = elem.nodeName.toLowerCase();
return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected);
},
"selected": function( elem ) {
// Accessing this property makes selected-by-default
// options in Safari work properly
if ( elem.parentNode ) {
elem.parentNode.selectedIndex;
}
return elem.selected === true;
},
// Contents
"empty": function( elem ) {
// http://www.w3.org/TR/selectors/#empty-pseudo
// :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),
// but not by others (comment: 8; processing instruction: 7; etc.)
// nodeType < 6 works because attributes (2) do not appear as children
for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
if ( elem.nodeType < 6 ) {
return false;
}
}
return true;
},
"parent": function( elem ) {
return !Expr.pseudos["empty"]( elem );
},
// Element/input types
"header": function( elem ) {
return rheader.test( elem.nodeName );
},
"input": function( elem ) {
return rinputs.test( elem.nodeName );
},
"button": function( elem ) {
var name = elem.nodeName.toLowerCase();
return name === "input" && elem.type === "button" || name === "button";
},
"text": function( elem ) {
var attr;
return elem.nodeName.toLowerCase() === "input" &&
elem.type === "text" &&
// Support: IE<8
// New HTML5 attribute values (e.g., "search") appear with elem.type === "text"
( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" );
},
// Position-in-collection
"first": createPositionalPseudo(function() {
return [ 0 ];
}),
"last": createPositionalPseudo(function( matchIndexes, length ) {
return [ length - 1 ];
}),
"eq": createPositionalPseudo(function( matchIndexes, length, argument ) {
return [ argument < 0 ? argument + length : argument ];
}),
"even": createPositionalPseudo(function( matchIndexes, length ) {
var i = 0;
for ( ; i < length; i += 2 ) {
matchIndexes.push( i );
}
return matchIndexes;
}),
"odd": createPositionalPseudo(function( matchIndexes, length ) {
var i = 1;
for ( ; i < length; i += 2 ) {
matchIndexes.push( i );
}
return matchIndexes;
}),
"lt": createPositionalPseudo(function( matchIndexes, length, argument ) {
var i = argument < 0 ? argument + length : argument;
for ( ; --i >= 0; ) {
matchIndexes.push( i );
}
return matchIndexes;
}),
"gt": createPositionalPseudo(function( matchIndexes, length, argument ) {
var i = argument < 0 ? argument + length : argument;
for ( ; ++i < length; ) {
matchIndexes.push( i );
}
return matchIndexes;
})
}
};
Expr.pseudos["nth"] = Expr.pseudos["eq"];
// Add button/input type pseudos
for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {
Expr.pseudos[ i ] = createInputPseudo( i );
}
for ( i in { submit: true, reset: true } ) {
Expr.pseudos[ i ] = createButtonPseudo( i );
}
// Easy API for creating new setFilters
function setFilters() {}
setFilters.prototype = Expr.filters = Expr.pseudos;
Expr.setFilters = new setFilters();
tokenize = Sizzle.tokenize = function( selector, parseOnly ) {
var matched, match, tokens, type,
soFar, groups, preFilters,
cached = tokenCache[ selector + " " ];
if ( cached ) {
return parseOnly ? 0 : cached.slice( 0 );
}
soFar = selector;
groups = [];
preFilters = Expr.preFilter;
while ( soFar ) {
// Comma and first run
if ( !matched || (match = rcomma.exec( soFar )) ) {
if ( match ) {
// Don't consume trailing commas as valid
soFar = soFar.slice( match[0].length ) || soFar;
}
groups.push( (tokens = []) );
}
matched = false;
// Combinators
if ( (match = rcombinators.exec( soFar )) ) {
matched = match.shift();
tokens.push({
value: matched,
// Cast descendant combinators to space
type: match[0].replace( rtrim, " " )
});
soFar = soFar.slice( matched.length );
}
// Filters
for ( type in Expr.filter ) {
if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||
(match = preFilters[ type ]( match ))) ) {
matched = match.shift();
tokens.push({
value: matched,
type: type,
matches: match
});
soFar = soFar.slice( matched.length );
}
}
if ( !matched ) {
break;
}
}
// Return the length of the invalid excess
// if we're just parsing
// Otherwise, throw an error or return tokens
return parseOnly ?
soFar.length :
soFar ?
Sizzle.error( selector ) :
// Cache the tokens
tokenCache( selector, groups ).slice( 0 );
};
function toSelector( tokens ) {
var i = 0,
len = tokens.length,
selector = "";
for ( ; i < len; i++ ) {
selector += tokens[i].value;
}
return selector;
}
function addCombinator( matcher, combinator, base ) {
var dir = combinator.dir,
checkNonElements = base && dir === "parentNode",
doneName = done++;
return combinator.first ?
// Check against closest ancestor/preceding element
function( elem, context, xml ) {
while ( (elem = elem[ dir ]) ) {
if ( elem.nodeType === 1 || checkNonElements ) {
return matcher( elem, context, xml );
}
}
} :
// Check against all ancestor/preceding elements
function( elem, context, xml ) {
var oldCache, uniqueCache, outerCache,
newCache = [ dirruns, doneName ];
// We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching
if ( xml ) {
while ( (elem = elem[ dir ]) ) {
if ( elem.nodeType === 1 || checkNonElements ) {
if ( matcher( elem, context, xml ) ) {
return true;
}
}
}
} else {
while ( (elem = elem[ dir ]) ) {
if ( elem.nodeType === 1 || checkNonElements ) {
outerCache = elem[ expando ] || (elem[ expando ] = {});
// Support: IE <9 only
// Defend against cloned attroperties (jQuery gh-1709)
uniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {});
if ( (oldCache = uniqueCache[ dir ]) &&
oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) {
// Assign to newCache so results back-propagate to previous elements
return (newCache[ 2 ] = oldCache[ 2 ]);
} else {
// Reuse newcache so results back-propagate to previous elements
uniqueCache[ dir ] = newCache;
// A match means we're done; a fail means we have to keep checking
if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) {
return true;
}
}
}
}
}
};
}
function elementMatcher( matchers ) {
return matchers.length > 1 ?
function( elem, context, xml ) {
var i = matchers.length;
while ( i-- ) {
if ( !matchers[i]( elem, context, xml ) ) {
return false;
}
}
return true;
} :
matchers[0];
}
function multipleContexts( selector, contexts, results ) {
var i = 0,
len = contexts.length;
for ( ; i < len; i++ ) {
Sizzle( selector, contexts[i], results );
}
return results;
}
function condense( unmatched, map, filter, context, xml ) {
var elem,
newUnmatched = [],
i = 0,
len = unmatched.length,
mapped = map != null;
for ( ; i < len; i++ ) {
if ( (elem = unmatched[i]) ) {
if ( !filter || filter( elem, context, xml ) ) {
newUnmatched.push( elem );
if ( mapped ) {
map.push( i );
}
}
}
}
return newUnmatched;
}
function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {
if ( postFilter && !postFilter[ expando ] ) {
postFilter = setMatcher( postFilter );
}
if ( postFinder && !postFinder[ expando ] ) {
postFinder = setMatcher( postFinder, postSelector );
}
return markFunction(function( seed, results, context, xml ) {
var temp, i, elem,
preMap = [],
postMap = [],
preexisting = results.length,
// Get initial elements from seed or context
elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ),
// Prefilter to get matcher input, preserving a map for seed-results synchronization
matcherIn = preFilter && ( seed || !selector ) ?
condense( elems, preMap, preFilter, context, xml ) :
elems,
matcherOut = matcher ?
// If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,
postFinder || ( seed ? preFilter : preexisting || postFilter ) ?
// ...intermediate processing is necessary
[] :
// ...otherwise use results directly
results :
matcherIn;
// Find primary matches
if ( matcher ) {
matcher( matcherIn, matcherOut, context, xml );
}
// Apply postFilter
if ( postFilter ) {
temp = condense( matcherOut, postMap );
postFilter( temp, [], context, xml );
// Un-match failing elements by moving them back to matcherIn
i = temp.length;
while ( i-- ) {
if ( (elem = temp[i]) ) {
matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);
}
}
}
if ( seed ) {
if ( postFinder || preFilter ) {
if ( postFinder ) {
// Get the final matcherOut by condensing this intermediate into postFinder contexts
temp = [];
i = matcherOut.length;
while ( i-- ) {
if ( (elem = matcherOut[i]) ) {
// Restore matcherIn since elem is not yet a final match
temp.push( (matcherIn[i] = elem) );
}
}
postFinder( null, (matcherOut = []), temp, xml );
}
// Move matched elements from seed to results to keep them synchronized
i = matcherOut.length;
while ( i-- ) {
if ( (elem = matcherOut[i]) &&
(temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) {
seed[temp] = !(results[temp] = elem);
}
}
}
// Add elements to results, through postFinder if defined
} else {
matcherOut = condense(
matcherOut === results ?
matcherOut.splice( preexisting, matcherOut.length ) :
matcherOut
);
if ( postFinder ) {
postFinder( null, results, matcherOut, xml );
} else {
push.apply( results, matcherOut );
}
}
});
}
function matcherFromTokens( tokens ) {
var checkContext, matcher, j,
len = tokens.length,
leadingRelative = Expr.relative[ tokens[0].type ],
implicitRelative = leadingRelative || Expr.relative[" "],
i = leadingRelative ? 1 : 0,
// The foundational matcher ensures that elements are reachable from top-level context(s)
matchContext = addCombinator( function( elem ) {
return elem === checkContext;
}, implicitRelative, true ),
matchAnyContext = addCombinator( function( elem ) {
return indexOf( checkContext, elem ) > -1;
}, implicitRelative, true ),
matchers = [ function( elem, context, xml ) {
var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
(checkContext = context).nodeType ?
matchContext( elem, context, xml ) :
matchAnyContext( elem, context, xml ) );
// Avoid hanging onto element (issue #299)
checkContext = null;
return ret;
} ];
for ( ; i < len; i++ ) {
if ( (matcher = Expr.relative[ tokens[i].type ]) ) {
matchers = [ addCombinator(elementMatcher( matchers ), matcher) ];
} else {
matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );
// Return special upon seeing a positional matcher
if ( matcher[ expando ] ) {
// Find the next relative operator (if any) for proper handling
j = ++i;
for ( ; j < len; j++ ) {
if ( Expr.relative[ tokens[j].type ] ) {
break;
}
}
return setMatcher(
i > 1 && elementMatcher( matchers ),
i > 1 && toSelector(
// If the preceding token was a descendant combinator, insert an implicit any-element `*`
tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" })
).replace( rtrim, "$1" ),
matcher,
i < j && matcherFromTokens( tokens.slice( i, j ) ),
j < len && matcherFromTokens( (tokens = tokens.slice( j )) ),
j < len && toSelector( tokens )
);
}
matchers.push( matcher );
}
}
return elementMatcher( matchers );
}
function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
var bySet = setMatchers.length > 0,
byElement = elementMatchers.length > 0,
superMatcher = function( seed, context, xml, results, outermost ) {
var elem, j, matcher,
matchedCount = 0,
i = "0",
unmatched = seed && [],
setMatched = [],
contextBackup = outermostContext,
// We must always have either seed elements or outermost context
elems = seed || byElement && Expr.find["TAG"]( "*", outermost ),
// Use integer dirruns iff this is the outermost matcher
dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1),
len = elems.length;
if ( outermost ) {
outermostContext = context === document || context || outermost;
}
// Add elements passing elementMatchers directly to results
// Support: IE<9, Safari
// Tolerate NodeList properties (IE: "length"; Safari: <number>) matching elements by id
for ( ; i !== len && (elem = elems[i]) != null; i++ ) {
if ( byElement && elem ) {
j = 0;
if ( !context && elem.ownerDocument !== document ) {
setDocument( elem );
xml = !documentIsHTML;
}
while ( (matcher = elementMatchers[j++]) ) {
if ( matcher( elem, context || document, xml) ) {
results.push( elem );
break;
}
}
if ( outermost ) {
dirruns = dirrunsUnique;
}
}
// Track unmatched elements for set filters
if ( bySet ) {
// They will have gone through all possible matchers
if ( (elem = !matcher && elem) ) {
matchedCount--;
}
// Lengthen the array for every element, matched or not
if ( seed ) {
unmatched.push( elem );
}
}
}
// `i` is now the count of elements visited above, and adding it to `matchedCount`
// makes the latter nonnegative.
matchedCount += i;
// Apply set filters to unmatched elements
// NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount`
// equals `i`), unless we didn't visit _any_ elements in the above loop because we have
// no element matchers and no seed.
// Incrementing an initially-string "0" `i` allows `i` to remain a string only in that
// case, which will result in a "00" `matchedCount` that differs from `i` but is also
// numerically zero.
if ( bySet && i !== matchedCount ) {
j = 0;
while ( (matcher = setMatchers[j++]) ) {
matcher( unmatched, setMatched, context, xml );
}
if ( seed ) {
// Reintegrate element matches to eliminate the need for sorting
if ( matchedCount > 0 ) {
while ( i-- ) {
if ( !(unmatched[i] || setMatched[i]) ) {
setMatched[i] = pop.call( results );
}
}
}
// Discard index placeholder values to get only actual matches
setMatched = condense( setMatched );
}
// Add matches to results
push.apply( results, setMatched );
// Seedless set matches succeeding multiple successful matchers stipulate sorting
if ( outermost && !seed && setMatched.length > 0 &&
( matchedCount + setMatchers.length ) > 1 ) {
Sizzle.uniqueSort( results );
}
}
// Override manipulation of globals by nested matchers
if ( outermost ) {
dirruns = dirrunsUnique;
outermostContext = contextBackup;
}
return unmatched;
};
return bySet ?
markFunction( superMatcher ) :
superMatcher;
}
compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {
var i,
setMatchers = [],
elementMatchers = [],
cached = compilerCache[ selector + " " ];
if ( !cached ) {
// Generate a function of recursive functions that can be used to check each element
if ( !match ) {
match = tokenize( selector );
}
i = match.length;
while ( i-- ) {
cached = matcherFromTokens( match[i] );
if ( cached[ expando ] ) {
setMatchers.push( cached );
} else {
elementMatchers.push( cached );
}
}
// Cache the compiled function
cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );
// Save selector and tokenization
cached.selector = selector;
}
return cached;
};
/**
* A low-level selection function that works with Sizzle's compiled
* selector functions
* @param {String|Function} selector A selector or a pre-compiled
* selector function built with Sizzle.compile
* @param {Element} context
* @param {Array} [results]
* @param {Array} [seed] A set of elements to match against
*/
select = Sizzle.select = function( selector, context, results, seed ) {
var i, tokens, token, type, find,
compiled = typeof selector === "function" && selector,
match = !seed && tokenize( (selector = compiled.selector || selector) );
results = results || [];
// Try to minimize operations if there is only one selector in the list and no seed
// (the latter of which guarantees us context)
if ( match.length === 1 ) {
// Reduce context if the leading compound selector is an ID
tokens = match[0] = match[0].slice( 0 );
if ( tokens.length > 2 && (token = tokens[0]).type === "ID" &&
support.getById && context.nodeType === 9 && documentIsHTML &&
Expr.relative[ tokens[1].type ] ) {
context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];
if ( !context ) {
return results;
// Precompiled matchers will still verify ancestry, so step up a level
} else if ( compiled ) {
context = context.parentNode;
}
selector = selector.slice( tokens.shift().value.length );
}
// Fetch a seed set for right-to-left matching
i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length;
while ( i-- ) {
token = tokens[i];
// Abort if we hit a combinator
if ( Expr.relative[ (type = token.type) ] ) {
break;
}
if ( (find = Expr.find[ type ]) ) {
// Search, expanding context for leading sibling combinators
if ( (seed = find(
token.matches[0].replace( runescape, funescape ),
rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context
)) ) {
// If seed is empty or no tokens remain, we can return early
tokens.splice( i, 1 );
selector = seed.length && toSelector( tokens );
if ( !selector ) {
push.apply( results, seed );
return results;
}
break;
}
}
}
}
// Compile and execute a filtering function if one is not provided
// Provide `match` to avoid retokenization if we modified the selector above
( compiled || compile( selector, match ) )(
seed,
context,
!documentIsHTML,
results,
!context || rsibling.test( selector ) && testContext( context.parentNode ) || context
);
return results;
};
// One-time assignments
// Sort stability
support.sortStable = expando.split("").sort( sortOrder ).join("") === expando;
// Support: Chrome 14-35+
// Always assume duplicates if they aren't passed to the comparison function
support.detectDuplicates = !!hasDuplicate;
// Initialize against the default document
setDocument();
// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)
// Detached nodes confoundingly follow *each other*
support.sortDetached = assert(function( div1 ) {
// Should return 1, but returns 4 (following)
return div1.compareDocumentPosition( document.createElement("div") ) & 1;
});
// Support: IE<8
// Prevent attribute/property "interpolation"
// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
if ( !assert(function( div ) {
div.innerHTML = "<a href='#'></a>";
return div.firstChild.getAttribute("href") === "#" ;
}) ) {
addHandle( "type|href|height|width", function( elem, name, isXML ) {
if ( !isXML ) {
return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 );
}
});
}
// Support: IE<9
// Use defaultValue in place of getAttribute("value")
if ( !support.attributes || !assert(function( div ) {
div.innerHTML = "<input/>";
div.firstChild.setAttribute( "value", "" );
return div.firstChild.getAttribute( "value" ) === "";
}) ) {
addHandle( "value", function( elem, name, isXML ) {
if ( !isXML && elem.nodeName.toLowerCase() === "input" ) {
return elem.defaultValue;
}
});
}
// Support: IE<9
// Use getAttributeNode to fetch booleans when getAttribute lies
if ( !assert(function( div ) {
return div.getAttribute("disabled") == null;
}) ) {
addHandle( booleans, function( elem, name, isXML ) {
var val;
if ( !isXML ) {
return elem[ name ] === true ? name.toLowerCase() :
(val = elem.getAttributeNode( name )) && val.specified ?
val.value :
null;
}
});
}
return Sizzle;
})( window );
jQuery.find = Sizzle;
jQuery.expr = Sizzle.selectors;
jQuery.expr[ ":" ] = jQuery.expr.pseudos;
jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort;
jQuery.text = Sizzle.getText;
jQuery.isXMLDoc = Sizzle.isXML;
jQuery.contains = Sizzle.contains;
var dir = function( elem, dir, until ) {
var matched = [],
truncate = until !== undefined;
while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) {
if ( elem.nodeType === 1 ) {
if ( truncate && jQuery( elem ).is( until ) ) {
break;
}
matched.push( elem );
}
}
return matched;
};
var siblings = function( n, elem ) {
var matched = [];
for ( ; n; n = n.nextSibling ) {
if ( n.nodeType === 1 && n !== elem ) {
matched.push( n );
}
}
return matched;
};
var rneedsContext = jQuery.expr.match.needsContext;
var rsingleTag = ( /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/ );
var risSimple = /^.[^:#\[\.,]*$/;
// Implement the identical functionality for filter and not
function winnow( elements, qualifier, not ) {
if ( jQuery.isFunction( qualifier ) ) {
return jQuery.grep( elements, function( elem, i ) {
/* jshint -W018 */
return !!qualifier.call( elem, i, elem ) !== not;
} );
}
if ( qualifier.nodeType ) {
return jQuery.grep( elements, function( elem ) {
return ( elem === qualifier ) !== not;
} );
}
if ( typeof qualifier === "string" ) {
if ( risSimple.test( qualifier ) ) {
return jQuery.filter( qualifier, elements, not );
}
qualifier = jQuery.filter( qualifier, elements );
}
return jQuery.grep( elements, function( elem ) {
return ( jQuery.inArray( elem, qualifier ) > -1 ) !== not;
} );
}
jQuery.filter = function( expr, elems, not ) {
var elem = elems[ 0 ];
if ( not ) {
expr = ":not(" + expr + ")";
}
return elems.length === 1 && elem.nodeType === 1 ?
jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] :
jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {
return elem.nodeType === 1;
} ) );
};
jQuery.fn.extend( {
find: function( selector ) {
var i,
ret = [],
self = this,
len = self.length;
if ( typeof selector !== "string" ) {
return this.pushStack( jQuery( selector ).filter( function() {
for ( i = 0; i < len; i++ ) {
if ( jQuery.contains( self[ i ], this ) ) {
return true;
}
}
} ) );
}
for ( i = 0; i < len; i++ ) {
jQuery.find( selector, self[ i ], ret );
}
// Needed because $( selector, context ) becomes $( context ).find( selector )
ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret );
ret.selector = this.selector ? this.selector + " " + selector : selector;
return ret;
},
filter: function( selector ) {
return this.pushStack( winnow( this, selector || [], false ) );
},
not: function( selector ) {
return this.pushStack( winnow( this, selector || [], true ) );
},
is: function( selector ) {
return !!winnow(
this,
// If this is a positional/relative selector, check membership in the returned set
// so $("p:first").is("p:last") won't return true for a doc with two "p".
typeof selector === "string" && rneedsContext.test( selector ) ?
jQuery( selector ) :
selector || [],
false
).length;
}
} );
// Initialize a jQuery object
// A central reference to the root jQuery(document)
var rootjQuery,
// A simple way to check for HTML strings
// Prioritize #id over <tag> to avoid XSS via location.hash (#9521)
// Strict HTML recognition (#11290: must start with <)
rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,
init = jQuery.fn.init = function( selector, context, root ) {
var match, elem;
// HANDLE: $(""), $(null), $(undefined), $(false)
if ( !selector ) {
return this;
}
// init accepts an alternate rootjQuery
// so migrate can support jQuery.sub (gh-2101)
root = root || rootjQuery;
// Handle HTML strings
if ( typeof selector === "string" ) {
if ( selector.charAt( 0 ) === "<" &&
selector.charAt( selector.length - 1 ) === ">" &&
selector.length >= 3 ) {
// Assume that strings that start and end with <> are HTML and skip the regex check
match = [ null, selector, null ];
} else {
match = rquickExpr.exec( selector );
}
// Match html or make sure no context is specified for #id
if ( match && ( match[ 1 ] || !context ) ) {
// HANDLE: $(html) -> $(array)
if ( match[ 1 ] ) {
context = context instanceof jQuery ? context[ 0 ] : context;
// scripts is true for back-compat
// Intentionally let the error be thrown if parseHTML is not present
jQuery.merge( this, jQuery.parseHTML(
match[ 1 ],
context && context.nodeType ? context.ownerDocument || context : document,
true
) );
// HANDLE: $(html, props)
if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) {
for ( match in context ) {
// Properties of context are called as methods if possible
if ( jQuery.isFunction( this[ match ] ) ) {
this[ match ]( context[ match ] );
// ...and otherwise set as attributes
} else {
this.attr( match, context[ match ] );
}
}
}
return this;
// HANDLE: $(#id)
} else {
elem = document.getElementById( match[ 2 ] );
// Check parentNode to catch when Blackberry 4.6 returns
// nodes that are no longer in the document #6963
if ( elem && elem.parentNode ) {
// Handle the case where IE and Opera return items
// by name instead of ID
if ( elem.id !== match[ 2 ] ) {
return rootjQuery.find( selector );
}
// Otherwise, we inject the element directly into the jQuery object
this.length = 1;
this[ 0 ] = elem;
}
this.context = document;
this.selector = selector;
return this;
}
// HANDLE: $(expr, $(...))
} else if ( !context || context.jquery ) {
return ( context || root ).find( selector );
// HANDLE: $(expr, context)
// (which is just equivalent to: $(context).find(expr)
} else {
return this.constructor( context ).find( selector );
}
// HANDLE: $(DOMElement)
} else if ( selector.nodeType ) {
this.context = this[ 0 ] = selector;
this.length = 1;
return this;
// HANDLE: $(function)
// Shortcut for document ready
} else if ( jQuery.isFunction( selector ) ) {
return typeof root.ready !== "undefined" ?
root.ready( selector ) :
// Execute immediately if ready is not present
selector( jQuery );
}
if ( selector.selector !== undefined ) {
this.selector = selector.selector;
this.context = selector.context;
}
return jQuery.makeArray( selector, this );
};
// Give the init function the jQuery prototype for later instantiation
init.prototype = jQuery.fn;
// Initialize central reference
rootjQuery = jQuery( document );
var rparentsprev = /^(?:parents|prev(?:Until|All))/,
// methods guaranteed to produce a unique set when starting from a unique set
guaranteedUnique = {
children: true,
contents: true,
next: true,
prev: true
};
jQuery.fn.extend( {
has: function( target ) {
var i,
targets = jQuery( target, this ),
len = targets.length;
return this.filter( function() {
for ( i = 0; i < len; i++ ) {
if ( jQuery.contains( this, targets[ i ] ) ) {
return true;
}
}
} );
},
closest: function( selectors, context ) {
var cur,
i = 0,
l = this.length,
matched = [],
pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ?
jQuery( selectors, context || this.context ) :
0;
for ( ; i < l; i++ ) {
for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) {
// Always skip document fragments
if ( cur.nodeType < 11 && ( pos ?
pos.index( cur ) > -1 :
// Don't pass non-elements to Sizzle
cur.nodeType === 1 &&
jQuery.find.matchesSelector( cur, selectors ) ) ) {
matched.push( cur );
break;
}
}
}
return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched );
},
// Determine the position of an element within
// the matched set of elements
index: function( elem ) {
// No argument, return index in parent
if ( !elem ) {
return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1;
}
// index in selector
if ( typeof elem === "string" ) {
return jQuery.inArray( this[ 0 ], jQuery( elem ) );
}
// Locate the position of the desired element
return jQuery.inArray(
// If it receives a jQuery object, the first element is used
elem.jquery ? elem[ 0 ] : elem, this );
},
add: function( selector, context ) {
return this.pushStack(
jQuery.uniqueSort(
jQuery.merge( this.get(), jQuery( selector, context ) )
)
);
},
addBack: function( selector ) {
return this.add( selector == null ?
this.prevObject : this.prevObject.filter( selector )
);
}
} );
function sibling( cur, dir ) {
do {
cur = cur[ dir ];
} while ( cur && cur.nodeType !== 1 );
return cur;
}
jQuery.each( {
parent: function( elem ) {
var parent = elem.parentNode;
return parent && parent.nodeType !== 11 ? parent : null;
},
parents: function( elem ) {
return dir( elem, "parentNode" );
},
parentsUntil: function( elem, i, until ) {
return dir( elem, "parentNode", until );
},
next: function( elem ) {
return sibling( elem, "nextSibling" );
},
prev: function( elem ) {
return sibling( elem, "previousSibling" );
},
nextAll: function( elem ) {
return dir( elem, "nextSibling" );
},
prevAll: function( elem ) {
return dir( elem, "previousSibling" );
},
nextUntil: function( elem, i, until ) {
return dir( elem, "nextSibling", until );
},
prevUntil: function( elem, i, until ) {
return dir( elem, "previousSibling", until );
},
siblings: function( elem ) {
return siblings( ( elem.parentNode || {} ).firstChild, elem );
},
children: function( elem ) {
return siblings( elem.firstChild );
},
contents: function( elem ) {
return jQuery.nodeName( elem, "iframe" ) ?
elem.contentDocument || elem.contentWindow.document :
jQuery.merge( [], elem.childNodes );
}
}, function( name, fn ) {
jQuery.fn[ name ] = function( until, selector ) {
var ret = jQuery.map( this, fn, until );
if ( name.slice( -5 ) !== "Until" ) {
selector = until;
}
if ( selector && typeof selector === "string" ) {
ret = jQuery.filter( selector, ret );
}
if ( this.length > 1 ) {
// Remove duplicates
if ( !guaranteedUnique[ name ] ) {
ret = jQuery.uniqueSort( ret );
}
// Reverse order for parents* and prev-derivatives
if ( rparentsprev.test( name ) ) {
ret = ret.reverse();
}
}
return this.pushStack( ret );
};
} );
var rnotwhite = ( /\S+/g );
// Convert String-formatted options into Object-formatted ones
function createOptions( options ) {
var object = {};
jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) {
object[ flag ] = true;
} );
return object;
}
/*
* Create a callback list using the following parameters:
*
* options: an optional list of space-separated options that will change how
* the callback list behaves or a more traditional option object
*
* By default a callback list will act like an event callback list and can be
* "fired" multiple times.
*
* Possible options:
*
* once: will ensure the callback list can only be fired once (like a Deferred)
*
* memory: will keep track of previous values and will call any callback added
* after the list has been fired right away with the latest "memorized"
* values (like a Deferred)
*
* unique: will ensure a callback can only be added once (no duplicate in the list)
*
* stopOnFalse: interrupt callings when a callback returns false
*
*/
jQuery.Callbacks = function( options ) {
// Convert options from String-formatted to Object-formatted if needed
// (we check in cache first)
options = typeof options === "string" ?
createOptions( options ) :
jQuery.extend( {}, options );
var // Flag to know if list is currently firing
firing,
// Last fire value for non-forgettable lists
memory,
// Flag to know if list was already fired
fired,
// Flag to prevent firing
locked,
// Actual callback list
list = [],
// Queue of execution data for repeatable lists
queue = [],
// Index of currently firing callback (modified by add/remove as needed)
firingIndex = -1,
// Fire callbacks
fire = function() {
// Enforce single-firing
locked = options.once;
// Execute callbacks for all pending executions,
// respecting firingIndex overrides and runtime changes
fired = firing = true;
for ( ; queue.length; firingIndex = -1 ) {
memory = queue.shift();
while ( ++firingIndex < list.length ) {
// Run callback and check for early termination
if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false &&
options.stopOnFalse ) {
// Jump to end and forget the data so .add doesn't re-fire
firingIndex = list.length;
memory = false;
}
}
}
// Forget the data if we're done with it
if ( !options.memory ) {
memory = false;
}
firing = false;
// Clean up if we're done firing for good
if ( locked ) {
// Keep an empty list if we have data for future add calls
if ( memory ) {
list = [];
// Otherwise, this object is spent
} else {
list = "";
}
}
},
// Actual Callbacks object
self = {
// Add a callback or a collection of callbacks to the list
add: function() {
if ( list ) {
// If we have memory from a past run, we should fire after adding
if ( memory && !firing ) {
firingIndex = list.length - 1;
queue.push( memory );
}
( function add( args ) {
jQuery.each( args, function( _, arg ) {
if ( jQuery.isFunction( arg ) ) {
if ( !options.unique || !self.has( arg ) ) {
list.push( arg );
}
} else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) {
// Inspect recursively
add( arg );
}
} );
} )( arguments );
if ( memory && !firing ) {
fire();
}
}
return this;
},
// Remove a callback from the list
remove: function() {
jQuery.each( arguments, function( _, arg ) {
var index;
while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
list.splice( index, 1 );
// Handle firing indexes
if ( index <= firingIndex ) {
firingIndex--;
}
}
} );
return this;
},
// Check if a given callback is in the list.
// If no argument is given, return whether or not list has callbacks attached.
has: function( fn ) {
return fn ?
jQuery.inArray( fn, list ) > -1 :
list.length > 0;
},
// Remove all callbacks from the list
empty: function() {
if ( list ) {
list = [];
}
return this;
},
// Disable .fire and .add
// Abort any current/pending executions
// Clear all callbacks and values
disable: function() {
locked = queue = [];
list = memory = "";
return this;
},
disabled: function() {
return !list;
},
// Disable .fire
// Also disable .add unless we have memory (since it would have no effect)
// Abort any pending executions
lock: function() {
locked = true;
if ( !memory ) {
self.disable();
}
return this;
},
locked: function() {
return !!locked;
},
// Call all callbacks with the given context and arguments
fireWith: function( context, args ) {
if ( !locked ) {
args = args || [];
args = [ context, args.slice ? args.slice() : args ];
queue.push( args );
if ( !firing ) {
fire();
}
}
return this;
},
// Call all the callbacks with the given arguments
fire: function() {
self.fireWith( this, arguments );
return this;
},
// To know if the callbacks have already been called at least once
fired: function() {
return !!fired;
}
};
return self;
};
jQuery.extend( {
Deferred: function( func ) {
var tuples = [
// action, add listener, listener list, final state
[ "resolve", "done", jQuery.Callbacks( "once memory" ), "resolved" ],
[ "reject", "fail", jQuery.Callbacks( "once memory" ), "rejected" ],
[ "notify", "progress", jQuery.Callbacks( "memory" ) ]
],
state = "pending",
promise = {
state: function() {
return state;
},
always: function() {
deferred.done( arguments ).fail( arguments );
return this;
},
then: function( /* fnDone, fnFail, fnProgress */ ) {
var fns = arguments;
return jQuery.Deferred( function( newDefer ) {
jQuery.each( tuples, function( i, tuple ) {
var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];
// deferred[ done | fail | progress ] for forwarding actions to newDefer
deferred[ tuple[ 1 ] ]( function() {
var returned = fn && fn.apply( this, arguments );
if ( returned && jQuery.isFunction( returned.promise ) ) {
returned.promise()
.progress( newDefer.notify )
.done( newDefer.resolve )
.fail( newDefer.reject );
} else {
newDefer[ tuple[ 0 ] + "With" ](
this === promise ? newDefer.promise() : this,
fn ? [ returned ] : arguments
);
}
} );
} );
fns = null;
} ).promise();
},
// Get a promise for this deferred
// If obj is provided, the promise aspect is added to the object
promise: function( obj ) {
return obj != null ? jQuery.extend( obj, promise ) : promise;
}
},
deferred = {};
// Keep pipe for back-compat
promise.pipe = promise.then;
// Add list-specific methods
jQuery.each( tuples, function( i, tuple ) {
var list = tuple[ 2 ],
stateString = tuple[ 3 ];
// promise[ done | fail | progress ] = list.add
promise[ tuple[ 1 ] ] = list.add;
// Handle state
if ( stateString ) {
list.add( function() {
// state = [ resolved | rejected ]
state = stateString;
// [ reject_list | resolve_list ].disable; progress_list.lock
}, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
}
// deferred[ resolve | reject | notify ]
deferred[ tuple[ 0 ] ] = function() {
deferred[ tuple[ 0 ] + "With" ]( this === deferred ? promise : this, arguments );
return this;
};
deferred[ tuple[ 0 ] + "With" ] = list.fireWith;
} );
// Make the deferred a promise
promise.promise( deferred );
// Call given func if any
if ( func ) {
func.call( deferred, deferred );
}
// All done!
return deferred;
},
// Deferred helper
when: function( subordinate /* , ..., subordinateN */ ) {
var i = 0,
resolveValues = slice.call( arguments ),
length = resolveValues.length,
// the count of uncompleted subordinates
remaining = length !== 1 ||
( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0,
// the master Deferred.
// If resolveValues consist of only a single Deferred, just use that.
deferred = remaining === 1 ? subordinate : jQuery.Deferred(),
// Update function for both resolve and progress values
updateFunc = function( i, contexts, values ) {
return function( value ) {
contexts[ i ] = this;
values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;
if ( values === progressValues ) {
deferred.notifyWith( contexts, values );
} else if ( !( --remaining ) ) {
deferred.resolveWith( contexts, values );
}
};
},
progressValues, progressContexts, resolveContexts;
// add listeners to Deferred subordinates; treat others as resolved
if ( length > 1 ) {
progressValues = new Array( length );
progressContexts = new Array( length );
resolveContexts = new Array( length );
for ( ; i < length; i++ ) {
if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) {
resolveValues[ i ].promise()
.progress( updateFunc( i, progressContexts, progressValues ) )
.done( updateFunc( i, resolveContexts, resolveValues ) )
.fail( deferred.reject );
} else {
--remaining;
}
}
}
// if we're not waiting on anything, resolve the master
if ( !remaining ) {
deferred.resolveWith( resolveContexts, resolveValues );
}
return deferred.promise();
}
} );
// The deferred used on DOM ready
var readyList;
jQuery.fn.ready = function( fn ) {
// Add the callback
jQuery.ready.promise().done( fn );
return this;
};
jQuery.extend( {
// Is the DOM ready to be used? Set to true once it occurs.
isReady: false,
// A counter to track how many items to wait for before
// the ready event fires. See #6781
readyWait: 1,
// Hold (or release) the ready event
holdReady: function( hold ) {
if ( hold ) {
jQuery.readyWait++;
} else {
jQuery.ready( true );
}
},
// Handle when the DOM is ready
ready: function( wait ) {
// Abort if there are pending holds or we're already ready
if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {
return;
}
// Remember that the DOM is ready
jQuery.isReady = true;
// If a normal DOM Ready event fired, decrement, and wait if need be
if ( wait !== true && --jQuery.readyWait > 0 ) {
return;
}
// If there are functions bound, to execute
readyList.resolveWith( document, [ jQuery ] );
// Trigger any bound ready events
if ( jQuery.fn.triggerHandler ) {
jQuery( document ).triggerHandler( "ready" );
jQuery( document ).off( "ready" );
}
}
} );
/**
* Clean-up method for dom ready events
*/
function detach() {
if ( document.addEventListener ) {
document.removeEventListener( "DOMContentLoaded", completed );
window.removeEventListener( "load", completed );
} else {
document.detachEvent( "onreadystatechange", completed );
window.detachEvent( "onload", completed );
}
}
/**
* The ready event handler and self cleanup method
*/
function completed() {
// readyState === "complete" is good enough for us to call the dom ready in oldIE
if ( document.addEventListener ||
window.event.type === "load" ||
document.readyState === "complete" ) {
detach();
jQuery.ready();
}
}
jQuery.ready.promise = function( obj ) {
if ( !readyList ) {
readyList = jQuery.Deferred();
// Catch cases where $(document).ready() is called
// after the browser event has already occurred.
// Support: IE6-10
// Older IE sometimes signals "interactive" too soon
if ( document.readyState === "complete" ||
( document.readyState !== "loading" && !document.documentElement.doScroll ) ) {
// Handle it asynchronously to allow scripts the opportunity to delay ready
window.setTimeout( jQuery.ready );
// Standards-based browsers support DOMContentLoaded
} else if ( document.addEventListener ) {
// Use the handy event callback
document.addEventListener( "DOMContentLoaded", completed );
// A fallback to window.onload, that will always work
window.addEventListener( "load", completed );
// If IE event model is used
} else {
// Ensure firing before onload, maybe late but safe also for iframes
document.attachEvent( "onreadystatechange", completed );
// A fallback to window.onload, that will always work
window.attachEvent( "onload", completed );
// If IE and not a frame
// continually check to see if the document is ready
var top = false;
try {
top = window.frameElement == null && document.documentElement;
} catch ( e ) {}
if ( top && top.doScroll ) {
( function doScrollCheck() {
if ( !jQuery.isReady ) {
try {
// Use the trick by Diego Perini
// http://javascript.nwbox.com/IEContentLoaded/
top.doScroll( "left" );
} catch ( e ) {
return window.setTimeout( doScrollCheck, 50 );
}
// detach all dom ready events
detach();
// and execute any waiting functions
jQuery.ready();
}
} )();
}
}
}
return readyList.promise( obj );
};
// Kick off the DOM ready check even if the user does not
jQuery.ready.promise();
// Support: IE<9
// Iteration over object's inherited properties before its own
var i;
for ( i in jQuery( support ) ) {
break;
}
support.ownFirst = i === "0";
// Note: most support tests are defined in their respective modules.
// false until the test is run
support.inlineBlockNeedsLayout = false;
// Execute ASAP in case we need to set body.style.zoom
jQuery( function() {
// Minified: var a,b,c,d
var val, div, body, container;
body = document.getElementsByTagName( "body" )[ 0 ];
if ( !body || !body.style ) {
// Return for frameset docs that don't have a body
return;
}
// Setup
div = document.createElement( "div" );
container = document.createElement( "div" );
container.style.cssText = "position:absolute;border:0;width:0;height:0;top:0;left:-9999px";
body.appendChild( container ).appendChild( div );
if ( typeof div.style.zoom !== "undefined" ) {
// Support: IE<8
// Check if natively block-level elements act like inline-block
// elements when setting their display to 'inline' and giving
// them layout
div.style.cssText = "display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1";
support.inlineBlockNeedsLayout = val = div.offsetWidth === 3;
if ( val ) {
// Prevent IE 6 from affecting layout for positioned elements #11048
// Prevent IE from shrinking the body in IE 7 mode #12869
// Support: IE<8
body.style.zoom = 1;
}
}
body.removeChild( container );
} );
( function() {
var div = document.createElement( "div" );
// Support: IE<9
support.deleteExpando = true;
try {
delete div.test;
} catch ( e ) {
support.deleteExpando = false;
}
// Null elements to avoid leaks in IE.
div = null;
} )();
var acceptData = function( elem ) {
var noData = jQuery.noData[ ( elem.nodeName + " " ).toLowerCase() ],
nodeType = +elem.nodeType || 1;
// Do not set data on non-element DOM nodes because it will not be cleared (#8335).
return nodeType !== 1 && nodeType !== 9 ?
false :
// Nodes accept data unless otherwise specified; rejection can be conditional
!noData || noData !== true && elem.getAttribute( "classid" ) === noData;
};
var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,
rmultiDash = /([A-Z])/g;
function dataAttr( elem, key, data ) {
// If nothing was found internally, try to fetch any
// data from the HTML5 data-* attribute
if ( data === undefined && elem.nodeType === 1 ) {
var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase();
data = elem.getAttribute( name );
if ( typeof data === "string" ) {
try {
data = data === "true" ? true :
data === "false" ? false :
data === "null" ? null :
// Only convert to a number if it doesn't change the string
+data + "" === data ? +data :
rbrace.test( data ) ? jQuery.parseJSON( data ) :
data;
} catch ( e ) {}
// Make sure we set the data so it isn't changed later
jQuery.data( elem, key, data );
} else {
data = undefined;
}
}
return data;
}
// checks a cache object for emptiness
function isEmptyDataObject( obj ) {
var name;
for ( name in obj ) {
// if the public data object is empty, the private is still empty
if ( name === "data" && jQuery.isEmptyObject( obj[ name ] ) ) {
continue;
}
if ( name !== "toJSON" ) {
return false;
}
}
return true;
}
function internalData( elem, name, data, pvt /* Internal Use Only */ ) {
if ( !acceptData( elem ) ) {
return;
}
var ret, thisCache,
internalKey = jQuery.expando,
// We have to handle DOM nodes and JS objects differently because IE6-7
// can't GC object references properly across the DOM-JS boundary
isNode = elem.nodeType,
// Only DOM nodes need the global jQuery cache; JS object data is
// attached directly to the object so GC can occur automatically
cache = isNode ? jQuery.cache : elem,
// Only defining an ID for JS objects if its cache already exists allows
// the code to shortcut on the same path as a DOM node with no cache
id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey;
// Avoid doing any more work than we need to when trying to get data on an
// object that has no data at all
if ( ( !id || !cache[ id ] || ( !pvt && !cache[ id ].data ) ) &&
data === undefined && typeof name === "string" ) {
return;
}
if ( !id ) {
// Only DOM nodes need a new unique ID for each element since their data
// ends up in the global cache
if ( isNode ) {
id = elem[ internalKey ] = deletedIds.pop() || jQuery.guid++;
} else {
id = internalKey;
}
}
if ( !cache[ id ] ) {
// Avoid exposing jQuery metadata on plain JS objects when the object
// is serialized using JSON.stringify
cache[ id ] = isNode ? {} : { toJSON: jQuery.noop };
}
// An object can be passed to jQuery.data instead of a key/value pair; this gets
// shallow copied over onto the existing cache
if ( typeof name === "object" || typeof name === "function" ) {
if ( pvt ) {
cache[ id ] = jQuery.extend( cache[ id ], name );
} else {
cache[ id ].data = jQuery.extend( cache[ id ].data, name );
}
}
thisCache = cache[ id ];
// jQuery data() is stored in a separate object inside the object's internal data
// cache in order to avoid key collisions between internal data and user-defined
// data.
if ( !pvt ) {
if ( !thisCache.data ) {
thisCache.data = {};
}
thisCache = thisCache.data;
}
if ( data !== undefined ) {
thisCache[ jQuery.camelCase( name ) ] = data;
}
// Check for both converted-to-camel and non-converted data property names
// If a data property was specified
if ( typeof name === "string" ) {
// First Try to find as-is property data
ret = thisCache[ name ];
// Test for null|undefined property data
if ( ret == null ) {
// Try to find the camelCased property
ret = thisCache[ jQuery.camelCase( name ) ];
}
} else {
ret = thisCache;
}
return ret;
}
function internalRemoveData( elem, name, pvt ) {
if ( !acceptData( elem ) ) {
return;
}
var thisCache, i,
isNode = elem.nodeType,
// See jQuery.data for more information
cache = isNode ? jQuery.cache : elem,
id = isNode ? elem[ jQuery.expando ] : jQuery.expando;
// If there is already no cache entry for this object, there is no
// purpose in continuing
if ( !cache[ id ] ) {
return;
}
if ( name ) {
thisCache = pvt ? cache[ id ] : cache[ id ].data;
if ( thisCache ) {
// Support array or space separated string names for data keys
if ( !jQuery.isArray( name ) ) {
// try the string as a key before any manipulation
if ( name in thisCache ) {
name = [ name ];
} else {
// split the camel cased version by spaces unless a key with the spaces exists
name = jQuery.camelCase( name );
if ( name in thisCache ) {
name = [ name ];
} else {
name = name.split( " " );
}
}
} else {
// If "name" is an array of keys...
// When data is initially created, via ("key", "val") signature,
// keys will be converted to camelCase.
// Since there is no way to tell _how_ a key was added, remove
// both plain key and camelCase key. #12786
// This will only penalize the array argument path.
name = name.concat( jQuery.map( name, jQuery.camelCase ) );
}
i = name.length;
while ( i-- ) {
delete thisCache[ name[ i ] ];
}
// If there is no data left in the cache, we want to continue
// and let the cache object itself get destroyed
if ( pvt ? !isEmptyDataObject( thisCache ) : !jQuery.isEmptyObject( thisCache ) ) {
return;
}
}
}
// See jQuery.data for more information
if ( !pvt ) {
delete cache[ id ].data;
// Don't destroy the parent cache unless the internal data object
// had been the only thing left in it
if ( !isEmptyDataObject( cache[ id ] ) ) {
return;
}
}
// Destroy the cache
if ( isNode ) {
jQuery.cleanData( [ elem ], true );
// Use delete when supported for expandos or `cache` is not a window per isWindow (#10080)
/* jshint eqeqeq: false */
} else if ( support.deleteExpando || cache != cache.window ) {
/* jshint eqeqeq: true */
delete cache[ id ];
// When all else fails, undefined
} else {
cache[ id ] = undefined;
}
}
jQuery.extend( {
cache: {},
// The following elements (space-suffixed to avoid Object.prototype collisions)
// throw uncatchable exceptions if you attempt to set expando properties
noData: {
"applet ": true,
"embed ": true,
// ...but Flash objects (which have this classid) *can* handle expandos
"object ": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
},
hasData: function( elem ) {
elem = elem.nodeType ? jQuery.cache[ elem[ jQuery.expando ] ] : elem[ jQuery.expando ];
return !!elem && !isEmptyDataObject( elem );
},
data: function( elem, name, data ) {
return internalData( elem, name, data );
},
removeData: function( elem, name ) {
return internalRemoveData( elem, name );
},
// For internal use only.
_data: function( elem, name, data ) {
return internalData( elem, name, data, true );
},
_removeData: function( elem, name ) {
return internalRemoveData( elem, name, true );
}
} );
jQuery.fn.extend( {
data: function( key, value ) {
var i, name, data,
elem = this[ 0 ],
attrs = elem && elem.attributes;
// Special expections of .data basically thwart jQuery.access,
// so implement the relevant behavior ourselves
// Gets all values
if ( key === undefined ) {
if ( this.length ) {
data = jQuery.data( elem );
if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) {
i = attrs.length;
while ( i-- ) {
// Support: IE11+
// The attrs elements can be null (#14894)
if ( attrs[ i ] ) {
name = attrs[ i ].name;
if ( name.indexOf( "data-" ) === 0 ) {
name = jQuery.camelCase( name.slice( 5 ) );
dataAttr( elem, name, data[ name ] );
}
}
}
jQuery._data( elem, "parsedAttrs", true );
}
}
return data;
}
// Sets multiple values
if ( typeof key === "object" ) {
return this.each( function() {
jQuery.data( this, key );
} );
}
return arguments.length > 1 ?
// Sets one value
this.each( function() {
jQuery.data( this, key, value );
} ) :
// Gets one value
// Try to fetch any internally stored data first
elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : undefined;
},
removeData: function( key ) {
return this.each( function() {
jQuery.removeData( this, key );
} );
}
} );
jQuery.extend( {
queue: function( elem, type, data ) {
var queue;
if ( elem ) {
type = ( type || "fx" ) + "queue";
queue = jQuery._data( elem, type );
// Speed up dequeue by getting out quickly if this is just a lookup
if ( data ) {
if ( !queue || jQuery.isArray( data ) ) {
queue = jQuery._data( elem, type, jQuery.makeArray( data ) );
} else {
queue.push( data );
}
}
return queue || [];
}
},
dequeue: function( elem, type ) {
type = type || "fx";
var queue = jQuery.queue( elem, type ),
startLength = queue.length,
fn = queue.shift(),
hooks = jQuery._queueHooks( elem, type ),
next = function() {
jQuery.dequeue( elem, type );
};
// If the fx queue is dequeued, always remove the progress sentinel
if ( fn === "inprogress" ) {
fn = queue.shift();
startLength--;
}
if ( fn ) {
// Add a progress sentinel to prevent the fx queue from being
// automatically dequeued
if ( type === "fx" ) {
queue.unshift( "inprogress" );
}
// clear up the last queue stop function
delete hooks.stop;
fn.call( elem, next, hooks );
}
if ( !startLength && hooks ) {
hooks.empty.fire();
}
},
// not intended for public consumption - generates a queueHooks object,
// or returns the current one
_queueHooks: function( elem, type ) {
var key = type + "queueHooks";
return jQuery._data( elem, key ) || jQuery._data( elem, key, {
empty: jQuery.Callbacks( "once memory" ).add( function() {
jQuery._removeData( elem, type + "queue" );
jQuery._removeData( elem, key );
} )
} );
}
} );
jQuery.fn.extend( {
queue: function( type, data ) {
var setter = 2;
if ( typeof type !== "string" ) {
data = type;
type = "fx";
setter--;
}
if ( arguments.length < setter ) {
return jQuery.queue( this[ 0 ], type );
}
return data === undefined ?
this :
this.each( function() {
var queue = jQuery.queue( this, type, data );
// ensure a hooks for this queue
jQuery._queueHooks( this, type );
if ( type === "fx" && queue[ 0 ] !== "inprogress" ) {
jQuery.dequeue( this, type );
}
} );
},
dequeue: function( type ) {
return this.each( function() {
jQuery.dequeue( this, type );
} );
},
clearQueue: function( type ) {
return this.queue( type || "fx", [] );
},
// Get a promise resolved when queues of a certain type
// are emptied (fx is the type by default)
promise: function( type, obj ) {
var tmp,
count = 1,
defer = jQuery.Deferred(),
elements = this,
i = this.length,
resolve = function() {
if ( !( --count ) ) {
defer.resolveWith( elements, [ elements ] );
}
};
if ( typeof type !== "string" ) {
obj = type;
type = undefined;
}
type = type || "fx";
while ( i-- ) {
tmp = jQuery._data( elements[ i ], type + "queueHooks" );
if ( tmp && tmp.empty ) {
count++;
tmp.empty.add( resolve );
}
}
resolve();
return defer.promise( obj );
}
} );
( function() {
var shrinkWrapBlocksVal;
support.shrinkWrapBlocks = function() {
if ( shrinkWrapBlocksVal != null ) {
return shrinkWrapBlocksVal;
}
// Will be changed later if needed.
shrinkWrapBlocksVal = false;
// Minified: var b,c,d
var div, body, container;
body = document.getElementsByTagName( "body" )[ 0 ];
if ( !body || !body.style ) {
// Test fired too early or in an unsupported environment, exit.
return;
}
// Setup
div = document.createElement( "div" );
container = document.createElement( "div" );
container.style.cssText = "position:absolute;border:0;width:0;height:0;top:0;left:-9999px";
body.appendChild( container ).appendChild( div );
// Support: IE6
// Check if elements with layout shrink-wrap their children
if ( typeof div.style.zoom !== "undefined" ) {
// Reset CSS: box-sizing; display; margin; border
div.style.cssText =
// Support: Firefox<29, Android 2.3
// Vendor-prefix box-sizing
"-webkit-box-sizing:content-box;-moz-box-sizing:content-box;" +
"box-sizing:content-box;display:block;margin:0;border:0;" +
"padding:1px;width:1px;zoom:1";
div.appendChild( document.createElement( "div" ) ).style.width = "5px";
shrinkWrapBlocksVal = div.offsetWidth !== 3;
}
body.removeChild( container );
return shrinkWrapBlocksVal;
};
} )();
var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source;
var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" );
var cssExpand = [ "Top", "Right", "Bottom", "Left" ];
var isHidden = function( elem, el ) {
// isHidden might be called from jQuery#filter function;
// in that case, element will be second argument
elem = el || elem;
return jQuery.css( elem, "display" ) === "none" ||
!jQuery.contains( elem.ownerDocument, elem );
};
function adjustCSS( elem, prop, valueParts, tween ) {
var adjusted,
scale = 1,
maxIterations = 20,
currentValue = tween ?
function() { return tween.cur(); } :
function() { return jQuery.css( elem, prop, "" ); },
initial = currentValue(),
unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ),
// Starting value computation is required for potential unit mismatches
initialInUnit = ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) &&
rcssNum.exec( jQuery.css( elem, prop ) );
if ( initialInUnit && initialInUnit[ 3 ] !== unit ) {
// Trust units reported by jQuery.css
unit = unit || initialInUnit[ 3 ];
// Make sure we update the tween properties later on
valueParts = valueParts || [];
// Iteratively approximate from a nonzero starting point
initialInUnit = +initial || 1;
do {
// If previous iteration zeroed out, double until we get *something*.
// Use string for doubling so we don't accidentally see scale as unchanged below
scale = scale || ".5";
// Adjust and apply
initialInUnit = initialInUnit / scale;
jQuery.style( elem, prop, initialInUnit + unit );
// Update scale, tolerating zero or NaN from tween.cur()
// Break the loop if scale is unchanged or perfect, or if we've just had enough.
} while (
scale !== ( scale = currentValue() / initial ) && scale !== 1 && --maxIterations
);
}
if ( valueParts ) {
initialInUnit = +initialInUnit || +initial || 0;
// Apply relative offset (+=/-=) if specified
adjusted = valueParts[ 1 ] ?
initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] :
+valueParts[ 2 ];
if ( tween ) {
tween.unit = unit;
tween.start = initialInUnit;
tween.end = adjusted;
}
}
return adjusted;
}
// Multifunctional method to get and set values of a collection
// The value/s can optionally be executed if it's a function
var access = function( elems, fn, key, value, chainable, emptyGet, raw ) {
var i = 0,
length = elems.length,
bulk = key == null;
// Sets many values
if ( jQuery.type( key ) === "object" ) {
chainable = true;
for ( i in key ) {
access( elems, fn, i, key[ i ], true, emptyGet, raw );
}
// Sets one value
} else if ( value !== undefined ) {
chainable = true;
if ( !jQuery.isFunction( value ) ) {
raw = true;
}
if ( bulk ) {
// Bulk operations run against the entire set
if ( raw ) {
fn.call( elems, value );
fn = null;
// ...except when executing function values
} else {
bulk = fn;
fn = function( elem, key, value ) {
return bulk.call( jQuery( elem ), value );
};
}
}
if ( fn ) {
for ( ; i < length; i++ ) {
fn(
elems[ i ],
key,
raw ? value : value.call( elems[ i ], i, fn( elems[ i ], key ) )
);
}
}
}
return chainable ?
elems :
// Gets
bulk ?
fn.call( elems ) :
length ? fn( elems[ 0 ], key ) : emptyGet;
};
var rcheckableType = ( /^(?:checkbox|radio)$/i );
var rtagName = ( /<([\w:-]+)/ );
var rscriptType = ( /^$|\/(?:java|ecma)script/i );
var rleadingWhitespace = ( /^\s+/ );
var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|" +
"details|dialog|figcaption|figure|footer|header|hgroup|main|" +
"mark|meter|nav|output|picture|progress|section|summary|template|time|video";
function createSafeFragment( document ) {
var list = nodeNames.split( "|" ),
safeFrag = document.createDocumentFragment();
if ( safeFrag.createElement ) {
while ( list.length ) {
safeFrag.createElement(
list.pop()
);
}
}
return safeFrag;
}
( function() {
var div = document.createElement( "div" ),
fragment = document.createDocumentFragment(),
input = document.createElement( "input" );
// Setup
div.innerHTML = " <link/><table></table><a href='/a'>a</a><input type='checkbox'/>";
// IE strips leading whitespace when .innerHTML is used
support.leadingWhitespace = div.firstChild.nodeType === 3;
// Make sure that tbody elements aren't automatically inserted
// IE will insert them into empty tables
support.tbody = !div.getElementsByTagName( "tbody" ).length;
// Make sure that link elements get serialized correctly by innerHTML
// This requires a wrapper element in IE
support.htmlSerialize = !!div.getElementsByTagName( "link" ).length;
// Makes sure cloning an html5 element does not cause problems
// Where outerHTML is undefined, this still works
support.html5Clone =
document.createElement( "nav" ).cloneNode( true ).outerHTML !== "<:nav></:nav>";
// Check if a disconnected checkbox will retain its checked
// value of true after appended to the DOM (IE6/7)
input.type = "checkbox";
input.checked = true;
fragment.appendChild( input );
support.appendChecked = input.checked;
// Make sure textarea (and checkbox) defaultValue is properly cloned
// Support: IE6-IE11+
div.innerHTML = "<textarea>x</textarea>";
support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue;
// #11217 - WebKit loses check when the name is after the checked attribute
fragment.appendChild( div );
// Support: Windows Web Apps (WWA)
// `name` and `type` must use .setAttribute for WWA (#14901)
input = document.createElement( "input" );
input.setAttribute( "type", "radio" );
input.setAttribute( "checked", "checked" );
input.setAttribute( "name", "t" );
div.appendChild( input );
// Support: Safari 5.1, iOS 5.1, Android 4.x, Android 2.3
// old WebKit doesn't clone checked state correctly in fragments
support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked;
// Support: IE<9
// Cloned elements keep attachEvent handlers, we use addEventListener on IE9+
support.noCloneEvent = !!div.addEventListener;
// Support: IE<9
// Since attributes and properties are the same in IE,
// cleanData must set properties to undefined rather than use removeAttribute
div[ jQuery.expando ] = 1;
support.attributes = !div.getAttribute( jQuery.expando );
} )();
// We have to close these tags to support XHTML (#13200)
var wrapMap = {
option: [ 1, "<select multiple='multiple'>", "</select>" ],
legend: [ 1, "<fieldset>", "</fieldset>" ],
area: [ 1, "<map>", "</map>" ],
// Support: IE8
param: [ 1, "<object>", "</object>" ],
thead: [ 1, "<table>", "</table>" ],
tr: [ 2, "<table><tbody>", "</tbody></table>" ],
col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
// IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags,
// unless wrapped in a div with non-breaking characters in front of it.
_default: support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X<div>", "</div>" ]
};
// Support: IE8-IE9
wrapMap.optgroup = wrapMap.option;
wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
wrapMap.th = wrapMap.td;
function getAll( context, tag ) {
var elems, elem,
i = 0,
found = typeof context.getElementsByTagName !== "undefined" ?
context.getElementsByTagName( tag || "*" ) :
typeof context.querySelectorAll !== "undefined" ?
context.querySelectorAll( tag || "*" ) :
undefined;
if ( !found ) {
for ( found = [], elems = context.childNodes || context;
( elem = elems[ i ] ) != null;
i++
) {
if ( !tag || jQuery.nodeName( elem, tag ) ) {
found.push( elem );
} else {
jQuery.merge( found, getAll( elem, tag ) );
}
}
}
return tag === undefined || tag && jQuery.nodeName( context, tag ) ?
jQuery.merge( [ context ], found ) :
found;
}
// Mark scripts as having already been evaluated
function setGlobalEval( elems, refElements ) {
var elem,
i = 0;
for ( ; ( elem = elems[ i ] ) != null; i++ ) {
jQuery._data(
elem,
"globalEval",
!refElements || jQuery._data( refElements[ i ], "globalEval" )
);
}
}
var rhtml = /<|&#?\w+;/,
rtbody = /<tbody/i;
function fixDefaultChecked( elem ) {
if ( rcheckableType.test( elem.type ) ) {
elem.defaultChecked = elem.checked;
}
}
function buildFragment( elems, context, scripts, selection, ignored ) {
var j, elem, contains,
tmp, tag, tbody, wrap,
l = elems.length,
// Ensure a safe fragment
safe = createSafeFragment( context ),
nodes = [],
i = 0;
for ( ; i < l; i++ ) {
elem = elems[ i ];
if ( elem || elem === 0 ) {
// Add nodes directly
if ( jQuery.type( elem ) === "object" ) {
jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );
// Convert non-html into a text node
} else if ( !rhtml.test( elem ) ) {
nodes.push( context.createTextNode( elem ) );
// Convert html into DOM nodes
} else {
tmp = tmp || safe.appendChild( context.createElement( "div" ) );
// Deserialize a standard representation
tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase();
wrap = wrapMap[ tag ] || wrapMap._default;
tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ];
// Descend through wrappers to the right content
j = wrap[ 0 ];
while ( j-- ) {
tmp = tmp.lastChild;
}
// Manually add leading whitespace removed by IE
if ( !support.leadingWhitespace && rleadingWhitespace.test( elem ) ) {
nodes.push( context.createTextNode( rleadingWhitespace.exec( elem )[ 0 ] ) );
}
// Remove IE's autoinserted <tbody> from table fragments
if ( !support.tbody ) {
// String was a <table>, *may* have spurious <tbody>
elem = tag === "table" && !rtbody.test( elem ) ?
tmp.firstChild :
// String was a bare <thead> or <tfoot>
wrap[ 1 ] === "<table>" && !rtbody.test( elem ) ?
tmp :
0;
j = elem && elem.childNodes.length;
while ( j-- ) {
if ( jQuery.nodeName( ( tbody = elem.childNodes[ j ] ), "tbody" ) &&
!tbody.childNodes.length ) {
elem.removeChild( tbody );
}
}
}
jQuery.merge( nodes, tmp.childNodes );
// Fix #12392 for WebKit and IE > 9
tmp.textContent = "";
// Fix #12392 for oldIE
while ( tmp.firstChild ) {
tmp.removeChild( tmp.firstChild );
}
// Remember the top-level container for proper cleanup
tmp = safe.lastChild;
}
}
}
// Fix #11356: Clear elements from fragment
if ( tmp ) {
safe.removeChild( tmp );
}
// Reset defaultChecked for any radios and checkboxes
// about to be appended to the DOM in IE 6/7 (#8060)
if ( !support.appendChecked ) {
jQuery.grep( getAll( nodes, "input" ), fixDefaultChecked );
}
i = 0;
while ( ( elem = nodes[ i++ ] ) ) {
// Skip elements already in the context collection (trac-4087)
if ( selection && jQuery.inArray( elem, selection ) > -1 ) {
if ( ignored ) {
ignored.push( elem );
}
continue;
}
contains = jQuery.contains( elem.ownerDocument, elem );
// Append to fragment
tmp = getAll( safe.appendChild( elem ), "script" );
// Preserve script evaluation history
if ( contains ) {
setGlobalEval( tmp );
}
// Capture executables
if ( scripts ) {
j = 0;
while ( ( elem = tmp[ j++ ] ) ) {
if ( rscriptType.test( elem.type || "" ) ) {
scripts.push( elem );
}
}
}
}
tmp = null;
return safe;
}
( function() {
var i, eventName,
div = document.createElement( "div" );
// Support: IE<9 (lack submit/change bubble), Firefox (lack focus(in | out) events)
for ( i in { submit: true, change: true, focusin: true } ) {
eventName = "on" + i;
if ( !( support[ i ] = eventName in window ) ) {
// Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP)
div.setAttribute( eventName, "t" );
support[ i ] = div.attributes[ eventName ].expando === false;
}
}
// Null elements to avoid leaks in IE.
div = null;
} )();
var rformElems = /^(?:input|select|textarea)$/i,
rkeyEvent = /^key/,
rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/,
rfocusMorph = /^(?:focusinfocus|focusoutblur)$/,
rtypenamespace = /^([^.]*)(?:\.(.+)|)/;
function returnTrue() {
return true;
}
function returnFalse() {
return false;
}
// Support: IE9
// See #13393 for more info
function safeActiveElement() {
try {
return document.activeElement;
} catch ( err ) { }
}
function on( elem, types, selector, data, fn, one ) {
var origFn, type;
// Types can be a map of types/handlers
if ( typeof types === "object" ) {
// ( types-Object, selector, data )
if ( typeof selector !== "string" ) {
// ( types-Object, data )
data = data || selector;
selector = undefined;
}
for ( type in types ) {
on( elem, type, selector, data, types[ type ], one );
}
return elem;
}
if ( data == null && fn == null ) {
// ( types, fn )
fn = selector;
data = selector = undefined;
} else if ( fn == null ) {
if ( typeof selector === "string" ) {
// ( types, selector, fn )
fn = data;
data = undefined;
} else {
// ( types, data, fn )
fn = data;
data = selector;
selector = undefined;
}
}
if ( fn === false ) {
fn = returnFalse;
} else if ( !fn ) {
return elem;
}
if ( one === 1 ) {
origFn = fn;
fn = function( event ) {
// Can use an empty set, since event contains the info
jQuery().off( event );
return origFn.apply( this, arguments );
};
// Use same guid so caller can remove using origFn
fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
}
return elem.each( function() {
jQuery.event.add( this, types, fn, data, selector );
} );
}
/*
* Helper functions for managing events -- not part of the public interface.
* Props to Dean Edwards' addEvent library for many of the ideas.
*/
jQuery.event = {
global: {},
add: function( elem, types, handler, data, selector ) {
var tmp, events, t, handleObjIn,
special, eventHandle, handleObj,
handlers, type, namespaces, origType,
elemData = jQuery._data( elem );
// Don't attach events to noData or text/comment nodes (but allow plain objects)
if ( !elemData ) {
return;
}
// Caller can pass in an object of custom data in lieu of the handler
if ( handler.handler ) {
handleObjIn = handler;
handler = handleObjIn.handler;
selector = handleObjIn.selector;
}
// Make sure that the handler has a unique ID, used to find/remove it later
if ( !handler.guid ) {
handler.guid = jQuery.guid++;
}
// Init the element's event structure and main handler, if this is the first
if ( !( events = elemData.events ) ) {
events = elemData.events = {};
}
if ( !( eventHandle = elemData.handle ) ) {
eventHandle = elemData.handle = function( e ) {
// Discard the second event of a jQuery.event.trigger() and
// when an event is called after a page has unloaded
return typeof jQuery !== "undefined" &&
( !e || jQuery.event.triggered !== e.type ) ?
jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :
undefined;
};
// Add elem as a property of the handle fn to prevent a memory leak
// with IE non-native events
eventHandle.elem = elem;
}
// Handle multiple events separated by a space
types = ( types || "" ).match( rnotwhite ) || [ "" ];
t = types.length;
while ( t-- ) {
tmp = rtypenamespace.exec( types[ t ] ) || [];
type = origType = tmp[ 1 ];
namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort();
// There *must* be a type, no attaching namespace-only handlers
if ( !type ) {
continue;
}
// If event changes its type, use the special event handlers for the changed type
special = jQuery.event.special[ type ] || {};
// If selector defined, determine special event api type, otherwise given type
type = ( selector ? special.delegateType : special.bindType ) || type;
// Update special based on newly reset type
special = jQuery.event.special[ type ] || {};
// handleObj is passed to all event handlers
handleObj = jQuery.extend( {
type: type,
origType: origType,
data: data,
handler: handler,
guid: handler.guid,
selector: selector,
needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
namespace: namespaces.join( "." )
}, handleObjIn );
// Init the event handler queue if we're the first
if ( !( handlers = events[ type ] ) ) {
handlers = events[ type ] = [];
handlers.delegateCount = 0;
// Only use addEventListener/attachEvent if the special events handler returns false
if ( !special.setup ||
special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
// Bind the global event handler to the element
if ( elem.addEventListener ) {
elem.addEventListener( type, eventHandle, false );
} else if ( elem.attachEvent ) {
elem.attachEvent( "on" + type, eventHandle );
}
}
}
if ( special.add ) {
special.add.call( elem, handleObj );
if ( !handleObj.handler.guid ) {
handleObj.handler.guid = handler.guid;
}
}
// Add to the element's handler list, delegates in front
if ( selector ) {
handlers.splice( handlers.delegateCount++, 0, handleObj );
} else {
handlers.push( handleObj );
}
// Keep track of which events have ever been used, for event optimization
jQuery.event.global[ type ] = true;
}
// Nullify elem to prevent memory leaks in IE
elem = null;
},
// Detach an event or set of events from an element
remove: function( elem, types, handler, selector, mappedTypes ) {
var j, handleObj, tmp,
origCount, t, events,
special, handlers, type,
namespaces, origType,
elemData = jQuery.hasData( elem ) && jQuery._data( elem );
if ( !elemData || !( events = elemData.events ) ) {
return;
}
// Once for each type.namespace in types; type may be omitted
types = ( types || "" ).match( rnotwhite ) || [ "" ];
t = types.length;
while ( t-- ) {
tmp = rtypenamespace.exec( types[ t ] ) || [];
type = origType = tmp[ 1 ];
namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort();
// Unbind all events (on this namespace, if provided) for the element
if ( !type ) {
for ( type in events ) {
jQuery.event.remove( elem, type + types[ t ], handler, selector, true );
}
continue;
}
special = jQuery.event.special[ type ] || {};
type = ( selector ? special.delegateType : special.bindType ) || type;
handlers = events[ type ] || [];
tmp = tmp[ 2 ] &&
new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" );
// Remove matching events
origCount = j = handlers.length;
while ( j-- ) {
handleObj = handlers[ j ];
if ( ( mappedTypes || origType === handleObj.origType ) &&
( !handler || handler.guid === handleObj.guid ) &&
( !tmp || tmp.test( handleObj.namespace ) ) &&
( !selector || selector === handleObj.selector ||
selector === "**" && handleObj.selector ) ) {
handlers.splice( j, 1 );
if ( handleObj.selector ) {
handlers.delegateCount--;
}
if ( special.remove ) {
special.remove.call( elem, handleObj );
}
}
}
// Remove generic event handler if we removed something and no more handlers exist
// (avoids potential for endless recursion during removal of special event handlers)
if ( origCount && !handlers.length ) {
if ( !special.teardown ||
special.teardown.call( elem, namespaces, elemData.handle ) === false ) {
jQuery.removeEvent( elem, type, elemData.handle );
}
delete events[ type ];
}
}
// Remove the expando if it's no longer used
if ( jQuery.isEmptyObject( events ) ) {
delete elemData.handle;
// removeData also checks for emptiness and clears the expando if empty
// so use it instead of delete
jQuery._removeData( elem, "events" );
}
},
trigger: function( event, data, elem, onlyHandlers ) {
var handle, ontype, cur,
bubbleType, special, tmp, i,
eventPath = [ elem || document ],
type = hasOwn.call( event, "type" ) ? event.type : event,
namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : [];
cur = tmp = elem = elem || document;
// Don't do events on text and comment nodes
if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
return;
}
// focus/blur morphs to focusin/out; ensure we're not firing them right now
if ( rfocusMorph.test( type + jQuery.event.triggered ) ) {
return;
}
if ( type.indexOf( "." ) > -1 ) {
// Namespaced trigger; create a regexp to match event type in handle()
namespaces = type.split( "." );
type = namespaces.shift();
namespaces.sort();
}
ontype = type.indexOf( ":" ) < 0 && "on" + type;
// Caller can pass in a jQuery.Event object, Object, or just an event type string
event = event[ jQuery.expando ] ?
event :
new jQuery.Event( type, typeof event === "object" && event );
// Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true)
event.isTrigger = onlyHandlers ? 2 : 3;
event.namespace = namespaces.join( "." );
event.rnamespace = event.namespace ?
new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) :
null;
// Clean up the event in case it is being reused
event.result = undefined;
if ( !event.target ) {
event.target = elem;
}
// Clone any incoming data and prepend the event, creating the handler arg list
data = data == null ?
[ event ] :
jQuery.makeArray( data, [ event ] );
// Allow special events to draw outside the lines
special = jQuery.event.special[ type ] || {};
if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {
return;
}
// Determine event propagation path in advance, per W3C events spec (#9951)
// Bubble up to document, then to window; watch for a global ownerDocument var (#9724)
if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {
bubbleType = special.delegateType || type;
if ( !rfocusMorph.test( bubbleType + type ) ) {
cur = cur.parentNode;
}
for ( ; cur; cur = cur.parentNode ) {
eventPath.push( cur );
tmp = cur;
}
// Only add window if we got to document (e.g., not plain obj or detached DOM)
if ( tmp === ( elem.ownerDocument || document ) ) {
eventPath.push( tmp.defaultView || tmp.parentWindow || window );
}
}
// Fire handlers on the event path
i = 0;
while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) {
event.type = i > 1 ?
bubbleType :
special.bindType || type;
// jQuery handler
handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] &&
jQuery._data( cur, "handle" );
if ( handle ) {
handle.apply( cur, data );
}
// Native handler
handle = ontype && cur[ ontype ];
if ( handle && handle.apply && acceptData( cur ) ) {
event.result = handle.apply( cur, data );
if ( event.result === false ) {
event.preventDefault();
}
}
}
event.type = type;
// If nobody prevented the default action, do it now
if ( !onlyHandlers && !event.isDefaultPrevented() ) {
if (
( !special._default ||
special._default.apply( eventPath.pop(), data ) === false
) && acceptData( elem )
) {
// Call a native DOM method on the target with the same name name as the event.
// Can't use an .isFunction() check here because IE6/7 fails that test.
// Don't do default actions on window, that's where global variables be (#6170)
if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) {
// Don't re-trigger an onFOO event when we call its FOO() method
tmp = elem[ ontype ];
if ( tmp ) {
elem[ ontype ] = null;
}
// Prevent re-triggering of the same event, since we already bubbled it above
jQuery.event.triggered = type;
try {
elem[ type ]();
} catch ( e ) {
// IE<9 dies on focus/blur to hidden element (#1486,#12518)
// only reproducible on winXP IE8 native, not IE9 in IE8 mode
}
jQuery.event.triggered = undefined;
if ( tmp ) {
elem[ ontype ] = tmp;
}
}
}
}
return event.result;
},
dispatch: function( event ) {
// Make a writable jQuery.Event from the native event object
event = jQuery.event.fix( event );
var i, j, ret, matched, handleObj,
handlerQueue = [],
args = slice.call( arguments ),
handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [],
special = jQuery.event.special[ event.type ] || {};
// Use the fix-ed jQuery.Event rather than the (read-only) native event
args[ 0 ] = event;
event.delegateTarget = this;
// Call the preDispatch hook for the mapped type, and let it bail if desired
if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {
return;
}
// Determine handlers
handlerQueue = jQuery.event.handlers.call( this, event, handlers );
// Run delegates first; they may want to stop propagation beneath us
i = 0;
while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) {
event.currentTarget = matched.elem;
j = 0;
while ( ( handleObj = matched.handlers[ j++ ] ) &&
!event.isImmediatePropagationStopped() ) {
// Triggered event must either 1) have no namespace, or 2) have namespace(s)
// a subset or equal to those in the bound event (both can have no namespace).
if ( !event.rnamespace || event.rnamespace.test( handleObj.namespace ) ) {
event.handleObj = handleObj;
event.data = handleObj.data;
ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle ||
handleObj.handler ).apply( matched.elem, args );
if ( ret !== undefined ) {
if ( ( event.result = ret ) === false ) {
event.preventDefault();
event.stopPropagation();
}
}
}
}
}
// Call the postDispatch hook for the mapped type
if ( special.postDispatch ) {
special.postDispatch.call( this, event );
}
return event.result;
},
handlers: function( event, handlers ) {
var i, matches, sel, handleObj,
handlerQueue = [],
delegateCount = handlers.delegateCount,
cur = event.target;
// Support (at least): Chrome, IE9
// Find delegate handlers
// Black-hole SVG <use> instance trees (#13180)
//
// Support: Firefox<=42+
// Avoid non-left-click in FF but don't block IE radio events (#3861, gh-2343)
if ( delegateCount && cur.nodeType &&
( event.type !== "click" || isNaN( event.button ) || event.button < 1 ) ) {
/* jshint eqeqeq: false */
for ( ; cur != this; cur = cur.parentNode || this ) {
/* jshint eqeqeq: true */
// Don't check non-elements (#13208)
// Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)
if ( cur.nodeType === 1 && ( cur.disabled !== true || event.type !== "click" ) ) {
matches = [];
for ( i = 0; i < delegateCount; i++ ) {
handleObj = handlers[ i ];
// Don't conflict with Object.prototype properties (#13203)
sel = handleObj.selector + " ";
if ( matches[ sel ] === undefined ) {
matches[ sel ] = handleObj.needsContext ?
jQuery( sel, this ).index( cur ) > -1 :
jQuery.find( sel, this, null, [ cur ] ).length;
}
if ( matches[ sel ] ) {
matches.push( handleObj );
}
}
if ( matches.length ) {
handlerQueue.push( { elem: cur, handlers: matches } );
}
}
}
}
// Add the remaining (directly-bound) handlers
if ( delegateCount < handlers.length ) {
handlerQueue.push( { elem: this, handlers: handlers.slice( delegateCount ) } );
}
return handlerQueue;
},
fix: function( event ) {
if ( event[ jQuery.expando ] ) {
return event;
}
// Create a writable copy of the event object and normalize some properties
var i, prop, copy,
type = event.type,
originalEvent = event,
fixHook = this.fixHooks[ type ];
if ( !fixHook ) {
this.fixHooks[ type ] = fixHook =
rmouseEvent.test( type ) ? this.mouseHooks :
rkeyEvent.test( type ) ? this.keyHooks :
{};
}
copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;
event = new jQuery.Event( originalEvent );
i = copy.length;
while ( i-- ) {
prop = copy[ i ];
event[ prop ] = originalEvent[ prop ];
}
// Support: IE<9
// Fix target property (#1925)
if ( !event.target ) {
event.target = originalEvent.srcElement || document;
}
// Support: Safari 6-8+
// Target should not be a text node (#504, #13143)
if ( event.target.nodeType === 3 ) {
event.target = event.target.parentNode;
}
// Support: IE<9
// For mouse/key events, metaKey==false if it's undefined (#3368, #11328)
event.metaKey = !!event.metaKey;
return fixHook.filter ? fixHook.filter( event, originalEvent ) : event;
},
// Includes some event props shared by KeyEvent and MouseEvent
props: ( "altKey bubbles cancelable ctrlKey currentTarget detail eventPhase " +
"metaKey relatedTarget shiftKey target timeStamp view which" ).split( " " ),
fixHooks: {},
keyHooks: {
props: "char charCode key keyCode".split( " " ),
filter: function( event, original ) {
// Add which for key events
if ( event.which == null ) {
event.which = original.charCode != null ? original.charCode : original.keyCode;
}
return event;
}
},
mouseHooks: {
props: ( "button buttons clientX clientY fromElement offsetX offsetY " +
"pageX pageY screenX screenY toElement" ).split( " " ),
filter: function( event, original ) {
var body, eventDoc, doc,
button = original.button,
fromElement = original.fromElement;
// Calculate pageX/Y if missing and clientX/Y available
if ( event.pageX == null && original.clientX != null ) {
eventDoc = event.target.ownerDocument || document;
doc = eventDoc.documentElement;
body = eventDoc.body;
event.pageX = original.clientX +
( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
( doc && doc.clientLeft || body && body.clientLeft || 0 );
event.pageY = original.clientY +
( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
( doc && doc.clientTop || body && body.clientTop || 0 );
}
// Add relatedTarget, if necessary
if ( !event.relatedTarget && fromElement ) {
event.relatedTarget = fromElement === event.target ?
original.toElement :
fromElement;
}
// Add which for click: 1 === left; 2 === middle; 3 === right
// Note: button is not normalized, so don't use it
if ( !event.which && button !== undefined ) {
event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );
}
return event;
}
},
special: {
load: {
// Prevent triggered image.load events from bubbling to window.load
noBubble: true
},
focus: {
// Fire native event if possible so blur/focus sequence is correct
trigger: function() {
if ( this !== safeActiveElement() && this.focus ) {
try {
this.focus();
return false;
} catch ( e ) {
// Support: IE<9
// If we error on focus to hidden element (#1486, #12518),
// let .trigger() run the handlers
}
}
},
delegateType: "focusin"
},
blur: {
trigger: function() {
if ( this === safeActiveElement() && this.blur ) {
this.blur();
return false;
}
},
delegateType: "focusout"
},
click: {
// For checkbox, fire native event so checked state will be right
trigger: function() {
if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) {
this.click();
return false;
}
},
// For cross-browser consistency, don't fire native .click() on links
_default: function( event ) {
return jQuery.nodeName( event.target, "a" );
}
},
beforeunload: {
postDispatch: function( event ) {
// Support: Firefox 20+
// Firefox doesn't alert if the returnValue field is not set.
if ( event.result !== undefined && event.originalEvent ) {
event.originalEvent.returnValue = event.result;
}
}
}
},
// Piggyback on a donor event to simulate a different one
simulate: function( type, elem, event ) {
var e = jQuery.extend(
new jQuery.Event(),
event,
{
type: type,
isSimulated: true
// Previously, `originalEvent: {}` was set here, so stopPropagation call
// would not be triggered on donor event, since in our own
// jQuery.event.stopPropagation function we had a check for existence of
// originalEvent.stopPropagation method, so, consequently it would be a noop.
//
// Guard for simulated events was moved to jQuery.event.stopPropagation function
// since `originalEvent` should point to the original event for the
// constancy with other events and for more focused logic
}
);
jQuery.event.trigger( e, null, elem );
if ( e.isDefaultPrevented() ) {
event.preventDefault();
}
}
};
jQuery.removeEvent = document.removeEventListener ?
function( elem, type, handle ) {
// This "if" is needed for plain objects
if ( elem.removeEventListener ) {
elem.removeEventListener( type, handle );
}
} :
function( elem, type, handle ) {
var name = "on" + type;
if ( elem.detachEvent ) {
// #8545, #7054, preventing memory leaks for custom events in IE6-8
// detachEvent needed property on element, by name of that event,
// to properly expose it to GC
if ( typeof elem[ name ] === "undefined" ) {
elem[ name ] = null;
}
elem.detachEvent( name, handle );
}
};
jQuery.Event = function( src, props ) {
// Allow instantiation without the 'new' keyword
if ( !( this instanceof jQuery.Event ) ) {
return new jQuery.Event( src, props );
}
// Event object
if ( src && src.type ) {
this.originalEvent = src;
this.type = src.type;
// Events bubbling up the document may have been marked as prevented
// by a handler lower down the tree; reflect the correct value.
this.isDefaultPrevented = src.defaultPrevented ||
src.defaultPrevented === undefined &&
// Support: IE < 9, Android < 4.0
src.returnValue === false ?
returnTrue :
returnFalse;
// Event type
} else {
this.type = src;
}
// Put explicitly provided properties onto the event object
if ( props ) {
jQuery.extend( this, props );
}
// Create a timestamp if incoming event doesn't have one
this.timeStamp = src && src.timeStamp || jQuery.now();
// Mark it as fixed
this[ jQuery.expando ] = true;
};
// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
jQuery.Event.prototype = {
constructor: jQuery.Event,
isDefaultPrevented: returnFalse,
isPropagationStopped: returnFalse,
isImmediatePropagationStopped: returnFalse,
preventDefault: function() {
var e = this.originalEvent;
this.isDefaultPrevented = returnTrue;
if ( !e ) {
return;
}
// If preventDefault exists, run it on the original event
if ( e.preventDefault ) {
e.preventDefault();
// Support: IE
// Otherwise set the returnValue property of the original event to false
} else {
e.returnValue = false;
}
},
stopPropagation: function() {
var e = this.originalEvent;
this.isPropagationStopped = returnTrue;
if ( !e || this.isSimulated ) {
return;
}
// If stopPropagation exists, run it on the original event
if ( e.stopPropagation ) {
e.stopPropagation();
}
// Support: IE
// Set the cancelBubble property of the original event to true
e.cancelBubble = true;
},
stopImmediatePropagation: function() {
var e = this.originalEvent;
this.isImmediatePropagationStopped = returnTrue;
if ( e && e.stopImmediatePropagation ) {
e.stopImmediatePropagation();
}
this.stopPropagation();
}
};
// Create mouseenter/leave events using mouseover/out and event-time checks
// so that event delegation works in jQuery.
// Do the same for pointerenter/pointerleave and pointerover/pointerout
//
// Support: Safari 7 only
// Safari sends mouseenter too often; see:
// https://code.google.com/p/chromium/issues/detail?id=470258
// for the description of the bug (it existed in older Chrome versions as well).
jQuery.each( {
mouseenter: "mouseover",
mouseleave: "mouseout",
pointerenter: "pointerover",
pointerleave: "pointerout"
}, function( orig, fix ) {
jQuery.event.special[ orig ] = {
delegateType: fix,
bindType: fix,
handle: function( event ) {
var ret,
target = this,
related = event.relatedTarget,
handleObj = event.handleObj;
// For mouseenter/leave call the handler if related is outside the target.
// NB: No relatedTarget if the mouse left/entered the browser window
if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) {
event.type = handleObj.origType;
ret = handleObj.handler.apply( this, arguments );
event.type = fix;
}
return ret;
}
};
} );
// IE submit delegation
if ( !support.submit ) {
jQuery.event.special.submit = {
setup: function() {
// Only need this for delegated form submit events
if ( jQuery.nodeName( this, "form" ) ) {
return false;
}
// Lazy-add a submit handler when a descendant form may potentially be submitted
jQuery.event.add( this, "click._submit keypress._submit", function( e ) {
// Node name check avoids a VML-related crash in IE (#9807)
var elem = e.target,
form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ?
// Support: IE <=8
// We use jQuery.prop instead of elem.form
// to allow fixing the IE8 delegated submit issue (gh-2332)
// by 3rd party polyfills/workarounds.
jQuery.prop( elem, "form" ) :
undefined;
if ( form && !jQuery._data( form, "submit" ) ) {
jQuery.event.add( form, "submit._submit", function( event ) {
event._submitBubble = true;
} );
jQuery._data( form, "submit", true );
}
} );
// return undefined since we don't need an event listener
},
postDispatch: function( event ) {
// If form was submitted by the user, bubble the event up the tree
if ( event._submitBubble ) {
delete event._submitBubble;
if ( this.parentNode && !event.isTrigger ) {
jQuery.event.simulate( "submit", this.parentNode, event );
}
}
},
teardown: function() {
// Only need this for delegated form submit events
if ( jQuery.nodeName( this, "form" ) ) {
return false;
}
// Remove delegated handlers; cleanData eventually reaps submit handlers attached above
jQuery.event.remove( this, "._submit" );
}
};
}
// IE change delegation and checkbox/radio fix
if ( !support.change ) {
jQuery.event.special.change = {
setup: function() {
if ( rformElems.test( this.nodeName ) ) {
// IE doesn't fire change on a check/radio until blur; trigger it on click
// after a propertychange. Eat the blur-change in special.change.handle.
// This still fires onchange a second time for check/radio after blur.
if ( this.type === "checkbox" || this.type === "radio" ) {
jQuery.event.add( this, "propertychange._change", function( event ) {
if ( event.originalEvent.propertyName === "checked" ) {
this._justChanged = true;
}
} );
jQuery.event.add( this, "click._change", function( event ) {
if ( this._justChanged && !event.isTrigger ) {
this._justChanged = false;
}
// Allow triggered, simulated change events (#11500)
jQuery.event.simulate( "change", this, event );
} );
}
return false;
}
// Delegated event; lazy-add a change handler on descendant inputs
jQuery.event.add( this, "beforeactivate._change", function( e ) {
var elem = e.target;
if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "change" ) ) {
jQuery.event.add( elem, "change._change", function( event ) {
if ( this.parentNode && !event.isSimulated && !event.isTrigger ) {
jQuery.event.simulate( "change", this.parentNode, event );
}
} );
jQuery._data( elem, "change", true );
}
} );
},
handle: function( event ) {
var elem = event.target;
// Swallow native change events from checkbox/radio, we already triggered them above
if ( this !== elem || event.isSimulated || event.isTrigger ||
( elem.type !== "radio" && elem.type !== "checkbox" ) ) {
return event.handleObj.handler.apply( this, arguments );
}
},
teardown: function() {
jQuery.event.remove( this, "._change" );
return !rformElems.test( this.nodeName );
}
};
}
// Support: Firefox
// Firefox doesn't have focus(in | out) events
// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787
//
// Support: Chrome, Safari
// focus(in | out) events fire after focus & blur events,
// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order
// Related ticket - https://code.google.com/p/chromium/issues/detail?id=449857
if ( !support.focusin ) {
jQuery.each( { focus: "focusin", blur: "focusout" }, function( orig, fix ) {
// Attach a single capturing handler on the document while someone wants focusin/focusout
var handler = function( event ) {
jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) );
};
jQuery.event.special[ fix ] = {
setup: function() {
var doc = this.ownerDocument || this,
attaches = jQuery._data( doc, fix );
if ( !attaches ) {
doc.addEventListener( orig, handler, true );
}
jQuery._data( doc, fix, ( attaches || 0 ) + 1 );
},
teardown: function() {
var doc = this.ownerDocument || this,
attaches = jQuery._data( doc, fix ) - 1;
if ( !attaches ) {
doc.removeEventListener( orig, handler, true );
jQuery._removeData( doc, fix );
} else {
jQuery._data( doc, fix, attaches );
}
}
};
} );
}
jQuery.fn.extend( {
on: function( types, selector, data, fn ) {
return on( this, types, selector, data, fn );
},
one: function( types, selector, data, fn ) {
return on( this, types, selector, data, fn, 1 );
},
off: function( types, selector, fn ) {
var handleObj, type;
if ( types && types.preventDefault && types.handleObj ) {
// ( event ) dispatched jQuery.Event
handleObj = types.handleObj;
jQuery( types.delegateTarget ).off(
handleObj.namespace ?
handleObj.origType + "." + handleObj.namespace :
handleObj.origType,
handleObj.selector,
handleObj.handler
);
return this;
}
if ( typeof types === "object" ) {
// ( types-object [, selector] )
for ( type in types ) {
this.off( type, selector, types[ type ] );
}
return this;
}
if ( selector === false || typeof selector === "function" ) {
// ( types [, fn] )
fn = selector;
selector = undefined;
}
if ( fn === false ) {
fn = returnFalse;
}
return this.each( function() {
jQuery.event.remove( this, types, fn, selector );
} );
},
trigger: function( type, data ) {
return this.each( function() {
jQuery.event.trigger( type, data, this );
} );
},
triggerHandler: function( type, data ) {
var elem = this[ 0 ];
if ( elem ) {
return jQuery.event.trigger( type, data, elem, true );
}
}
} );
var rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g,
rnoshimcache = new RegExp( "<(?:" + nodeNames + ")[\\s/>]", "i" ),
rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,
// Support: IE 10-11, Edge 10240+
// In IE/Edge using regex groups here causes severe slowdowns.
// See https://connect.microsoft.com/IE/feedback/details/1736512/
rnoInnerhtml = /<script|<style|<link/i,
// checked="checked" or checked
rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
rscriptTypeMasked = /^true\/(.*)/,
rcleanScript = /^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,
safeFragment = createSafeFragment( document ),
fragmentDiv = safeFragment.appendChild( document.createElement( "div" ) );
// Support: IE<8
// Manipulating tables requires a tbody
function manipulationTarget( elem, content ) {
return jQuery.nodeName( elem, "table" ) &&
jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ?
elem.getElementsByTagName( "tbody" )[ 0 ] ||
elem.appendChild( elem.ownerDocument.createElement( "tbody" ) ) :
elem;
}
// Replace/restore the type attribute of script elements for safe DOM manipulation
function disableScript( elem ) {
elem.type = ( jQuery.find.attr( elem, "type" ) !== null ) + "/" + elem.type;
return elem;
}
function restoreScript( elem ) {
var match = rscriptTypeMasked.exec( elem.type );
if ( match ) {
elem.type = match[ 1 ];
} else {
elem.removeAttribute( "type" );
}
return elem;
}
function cloneCopyEvent( src, dest ) {
if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) {
return;
}
var type, i, l,
oldData = jQuery._data( src ),
curData = jQuery._data( dest, oldData ),
events = oldData.events;
if ( events ) {
delete curData.handle;
curData.events = {};
for ( type in events ) {
for ( i = 0, l = events[ type ].length; i < l; i++ ) {
jQuery.event.add( dest, type, events[ type ][ i ] );
}
}
}
// make the cloned public data object a copy from the original
if ( curData.data ) {
curData.data = jQuery.extend( {}, curData.data );
}
}
function fixCloneNodeIssues( src, dest ) {
var nodeName, e, data;
// We do not need to do anything for non-Elements
if ( dest.nodeType !== 1 ) {
return;
}
nodeName = dest.nodeName.toLowerCase();
// IE6-8 copies events bound via attachEvent when using cloneNode.
if ( !support.noCloneEvent && dest[ jQuery.expando ] ) {
data = jQuery._data( dest );
for ( e in data.events ) {
jQuery.removeEvent( dest, e, data.handle );
}
// Event data gets referenced instead of copied if the expando gets copied too
dest.removeAttribute( jQuery.expando );
}
// IE blanks contents when cloning scripts, and tries to evaluate newly-set text
if ( nodeName === "script" && dest.text !== src.text ) {
disableScript( dest ).text = src.text;
restoreScript( dest );
// IE6-10 improperly clones children of object elements using classid.
// IE10 throws NoModificationAllowedError if parent is null, #12132.
} else if ( nodeName === "object" ) {
if ( dest.parentNode ) {
dest.outerHTML = src.outerHTML;
}
// This path appears unavoidable for IE9. When cloning an object
// element in IE9, the outerHTML strategy above is not sufficient.
// If the src has innerHTML and the destination does not,
// copy the src.innerHTML into the dest.innerHTML. #10324
if ( support.html5Clone && ( src.innerHTML && !jQuery.trim( dest.innerHTML ) ) ) {
dest.innerHTML = src.innerHTML;
}
} else if ( nodeName === "input" && rcheckableType.test( src.type ) ) {
// IE6-8 fails to persist the checked state of a cloned checkbox
// or radio button. Worse, IE6-7 fail to give the cloned element
// a checked appearance if the defaultChecked value isn't also set
dest.defaultChecked = dest.checked = src.checked;
// IE6-7 get confused and end up setting the value of a cloned
// checkbox/radio button to an empty string instead of "on"
if ( dest.value !== src.value ) {
dest.value = src.value;
}
// IE6-8 fails to return the selected option to the default selected
// state when cloning options
} else if ( nodeName === "option" ) {
dest.defaultSelected = dest.selected = src.defaultSelected;
// IE6-8 fails to set the defaultValue to the correct value when
// cloning other types of input fields
} else if ( nodeName === "input" || nodeName === "textarea" ) {
dest.defaultValue = src.defaultValue;
}
}
function domManip( collection, args, callback, ignored ) {
// Flatten any nested arrays
args = concat.apply( [], args );
var first, node, hasScripts,
scripts, doc, fragment,
i = 0,
l = collection.length,
iNoClone = l - 1,
value = args[ 0 ],
isFunction = jQuery.isFunction( value );
// We can't cloneNode fragments that contain checked, in WebKit
if ( isFunction ||
( l > 1 && typeof value === "string" &&
!support.checkClone && rchecked.test( value ) ) ) {
return collection.each( function( index ) {
var self = collection.eq( index );
if ( isFunction ) {
args[ 0 ] = value.call( this, index, self.html() );
}
domManip( self, args, callback, ignored );
} );
}
if ( l ) {
fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored );
first = fragment.firstChild;
if ( fragment.childNodes.length === 1 ) {
fragment = first;
}
// Require either new content or an interest in ignored elements to invoke the callback
if ( first || ignored ) {
scripts = jQuery.map( getAll( fragment, "script" ), disableScript );
hasScripts = scripts.length;
// Use the original fragment for the last item
// instead of the first because it can end up
// being emptied incorrectly in certain situations (#8070).
for ( ; i < l; i++ ) {
node = fragment;
if ( i !== iNoClone ) {
node = jQuery.clone( node, true, true );
// Keep references to cloned scripts for later restoration
if ( hasScripts ) {
// Support: Android<4.1, PhantomJS<2
// push.apply(_, arraylike) throws on ancient WebKit
jQuery.merge( scripts, getAll( node, "script" ) );
}
}
callback.call( collection[ i ], node, i );
}
if ( hasScripts ) {
doc = scripts[ scripts.length - 1 ].ownerDocument;
// Reenable scripts
jQuery.map( scripts, restoreScript );
// Evaluate executable scripts on first document insertion
for ( i = 0; i < hasScripts; i++ ) {
node = scripts[ i ];
if ( rscriptType.test( node.type || "" ) &&
!jQuery._data( node, "globalEval" ) &&
jQuery.contains( doc, node ) ) {
if ( node.src ) {
// Optional AJAX dependency, but won't run scripts if not present
if ( jQuery._evalUrl ) {
jQuery._evalUrl( node.src );
}
} else {
jQuery.globalEval(
( node.text || node.textContent || node.innerHTML || "" )
.replace( rcleanScript, "" )
);
}
}
}
}
// Fix #11809: Avoid leaking memory
fragment = first = null;
}
}
return collection;
}
function remove( elem, selector, keepData ) {
var node,
elems = selector ? jQuery.filter( selector, elem ) : elem,
i = 0;
for ( ; ( node = elems[ i ] ) != null; i++ ) {
if ( !keepData && node.nodeType === 1 ) {
jQuery.cleanData( getAll( node ) );
}
if ( node.parentNode ) {
if ( keepData && jQuery.contains( node.ownerDocument, node ) ) {
setGlobalEval( getAll( node, "script" ) );
}
node.parentNode.removeChild( node );
}
}
return elem;
}
jQuery.extend( {
htmlPrefilter: function( html ) {
return html.replace( rxhtmlTag, "<$1></$2>" );
},
clone: function( elem, dataAndEvents, deepDataAndEvents ) {
var destElements, node, clone, i, srcElements,
inPage = jQuery.contains( elem.ownerDocument, elem );
if ( support.html5Clone || jQuery.isXMLDoc( elem ) ||
!rnoshimcache.test( "<" + elem.nodeName + ">" ) ) {
clone = elem.cloneNode( true );
// IE<=8 does not properly clone detached, unknown element nodes
} else {
fragmentDiv.innerHTML = elem.outerHTML;
fragmentDiv.removeChild( clone = fragmentDiv.firstChild );
}
if ( ( !support.noCloneEvent || !support.noCloneChecked ) &&
( elem.nodeType === 1 || elem.nodeType === 11 ) && !jQuery.isXMLDoc( elem ) ) {
// We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2
destElements = getAll( clone );
srcElements = getAll( elem );
// Fix all IE cloning issues
for ( i = 0; ( node = srcElements[ i ] ) != null; ++i ) {
// Ensure that the destination node is not null; Fixes #9587
if ( destElements[ i ] ) {
fixCloneNodeIssues( node, destElements[ i ] );
}
}
}
// Copy the events from the original to the clone
if ( dataAndEvents ) {
if ( deepDataAndEvents ) {
srcElements = srcElements || getAll( elem );
destElements = destElements || getAll( clone );
for ( i = 0; ( node = srcElements[ i ] ) != null; i++ ) {
cloneCopyEvent( node, destElements[ i ] );
}
} else {
cloneCopyEvent( elem, clone );
}
}
// Preserve script evaluation history
destElements = getAll( clone, "script" );
if ( destElements.length > 0 ) {
setGlobalEval( destElements, !inPage && getAll( elem, "script" ) );
}
destElements = srcElements = node = null;
// Return the cloned set
return clone;
},
cleanData: function( elems, /* internal */ forceAcceptData ) {
var elem, type, id, data,
i = 0,
internalKey = jQuery.expando,
cache = jQuery.cache,
attributes = support.attributes,
special = jQuery.event.special;
for ( ; ( elem = elems[ i ] ) != null; i++ ) {
if ( forceAcceptData || acceptData( elem ) ) {
id = elem[ internalKey ];
data = id && cache[ id ];
if ( data ) {
if ( data.events ) {
for ( type in data.events ) {
if ( special[ type ] ) {
jQuery.event.remove( elem, type );
// This is a shortcut to avoid jQuery.event.remove's overhead
} else {
jQuery.removeEvent( elem, type, data.handle );
}
}
}
// Remove cache only if it was not already removed by jQuery.event.remove
if ( cache[ id ] ) {
delete cache[ id ];
// Support: IE<9
// IE does not allow us to delete expando properties from nodes
// IE creates expando attributes along with the property
// IE does not have a removeAttribute function on Document nodes
if ( !attributes && typeof elem.removeAttribute !== "undefined" ) {
elem.removeAttribute( internalKey );
// Webkit & Blink performance suffers when deleting properties
// from DOM nodes, so set to undefined instead
// https://code.google.com/p/chromium/issues/detail?id=378607
} else {
elem[ internalKey ] = undefined;
}
deletedIds.push( id );
}
}
}
}
}
} );
jQuery.fn.extend( {
// Keep domManip exposed until 3.0 (gh-2225)
domManip: domManip,
detach: function( selector ) {
return remove( this, selector, true );
},
remove: function( selector ) {
return remove( this, selector );
},
text: function( value ) {
return access( this, function( value ) {
return value === undefined ?
jQuery.text( this ) :
this.empty().append(
( this[ 0 ] && this[ 0 ].ownerDocument || document ).createTextNode( value )
);
}, null, value, arguments.length );
},
append: function() {
return domManip( this, arguments, function( elem ) {
if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
var target = manipulationTarget( this, elem );
target.appendChild( elem );
}
} );
},
prepend: function() {
return domManip( this, arguments, function( elem ) {
if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
var target = manipulationTarget( this, elem );
target.insertBefore( elem, target.firstChild );
}
} );
},
before: function() {
return domManip( this, arguments, function( elem ) {
if ( this.parentNode ) {
this.parentNode.insertBefore( elem, this );
}
} );
},
after: function() {
return domManip( this, arguments, function( elem ) {
if ( this.parentNode ) {
this.parentNode.insertBefore( elem, this.nextSibling );
}
} );
},
empty: function() {
var elem,
i = 0;
for ( ; ( elem = this[ i ] ) != null; i++ ) {
// Remove element nodes and prevent memory leaks
if ( elem.nodeType === 1 ) {
jQuery.cleanData( getAll( elem, false ) );
}
// Remove any remaining nodes
while ( elem.firstChild ) {
elem.removeChild( elem.firstChild );
}
// If this is a select, ensure that it displays empty (#12336)
// Support: IE<9
if ( elem.options && jQuery.nodeName( elem, "select" ) ) {
elem.options.length = 0;
}
}
return this;
},
clone: function( dataAndEvents, deepDataAndEvents ) {
dataAndEvents = dataAndEvents == null ? false : dataAndEvents;
deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;
return this.map( function() {
return jQuery.clone( this, dataAndEvents, deepDataAndEvents );
} );
},
html: function( value ) {
return access( this, function( value ) {
var elem = this[ 0 ] || {},
i = 0,
l = this.length;
if ( value === undefined ) {
return elem.nodeType === 1 ?
elem.innerHTML.replace( rinlinejQuery, "" ) :
undefined;
}
// See if we can take a shortcut and just use innerHTML
if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
( support.htmlSerialize || !rnoshimcache.test( value ) ) &&
( support.leadingWhitespace || !rleadingWhitespace.test( value ) ) &&
!wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) {
value = jQuery.htmlPrefilter( value );
try {
for ( ; i < l; i++ ) {
// Remove element nodes and prevent memory leaks
elem = this[ i ] || {};
if ( elem.nodeType === 1 ) {
jQuery.cleanData( getAll( elem, false ) );
elem.innerHTML = value;
}
}
elem = 0;
// If using innerHTML throws an exception, use the fallback method
} catch ( e ) {}
}
if ( elem ) {
this.empty().append( value );
}
}, null, value, arguments.length );
},
replaceWith: function() {
var ignored = [];
// Make the changes, replacing each non-ignored context element with the new content
return domManip( this, arguments, function( elem ) {
var parent = this.parentNode;
if ( jQuery.inArray( this, ignored ) < 0 ) {
jQuery.cleanData( getAll( this ) );
if ( parent ) {
parent.replaceChild( elem, this );
}
}
// Force callback invocation
}, ignored );
}
} );
jQuery.each( {
appendTo: "append",
prependTo: "prepend",
insertBefore: "before",
insertAfter: "after",
replaceAll: "replaceWith"
}, function( name, original ) {
jQuery.fn[ name ] = function( selector ) {
var elems,
i = 0,
ret = [],
insert = jQuery( selector ),
last = insert.length - 1;
for ( ; i <= last; i++ ) {
elems = i === last ? this : this.clone( true );
jQuery( insert[ i ] )[ original ]( elems );
// Modern browsers can apply jQuery collections as arrays, but oldIE needs a .get()
push.apply( ret, elems.get() );
}
return this.pushStack( ret );
};
} );
var iframe,
elemdisplay = {
// Support: Firefox
// We have to pre-define these values for FF (#10227)
HTML: "block",
BODY: "block"
};
/**
* Retrieve the actual display of a element
* @param {String} name nodeName of the element
* @param {Object} doc Document object
*/
// Called only from within defaultDisplay
function actualDisplay( name, doc ) {
var elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ),
display = jQuery.css( elem[ 0 ], "display" );
// We don't have any data stored on the element,
// so use "detach" method as fast way to get rid of the element
elem.detach();
return display;
}
/**
* Try to determine the default display value of an element
* @param {String} nodeName
*/
function defaultDisplay( nodeName ) {
var doc = document,
display = elemdisplay[ nodeName ];
if ( !display ) {
display = actualDisplay( nodeName, doc );
// If the simple way fails, read from inside an iframe
if ( display === "none" || !display ) {
// Use the already-created iframe if possible
iframe = ( iframe || jQuery( "<iframe frameborder='0' width='0' height='0'/>" ) )
.appendTo( doc.documentElement );
// Always write a new HTML skeleton so Webkit and Firefox don't choke on reuse
doc = ( iframe[ 0 ].contentWindow || iframe[ 0 ].contentDocument ).document;
// Support: IE
doc.write();
doc.close();
display = actualDisplay( nodeName, doc );
iframe.detach();
}
// Store the correct default display
elemdisplay[ nodeName ] = display;
}
return display;
}
var rmargin = ( /^margin/ );
var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" );
var swap = function( elem, options, callback, args ) {
var ret, name,
old = {};
// Remember the old values, and insert the new ones
for ( name in options ) {
old[ name ] = elem.style[ name ];
elem.style[ name ] = options[ name ];
}
ret = callback.apply( elem, args || [] );
// Revert the old values
for ( name in options ) {
elem.style[ name ] = old[ name ];
}
return ret;
};
var documentElement = document.documentElement;
( function() {
var pixelPositionVal, pixelMarginRightVal, boxSizingReliableVal,
reliableHiddenOffsetsVal, reliableMarginRightVal, reliableMarginLeftVal,
container = document.createElement( "div" ),
div = document.createElement( "div" );
// Finish early in limited (non-browser) environments
if ( !div.style ) {
return;
}
div.style.cssText = "float:left;opacity:.5";
// Support: IE<9
// Make sure that element opacity exists (as opposed to filter)
support.opacity = div.style.opacity === "0.5";
// Verify style float existence
// (IE uses styleFloat instead of cssFloat)
support.cssFloat = !!div.style.cssFloat;
div.style.backgroundClip = "content-box";
div.cloneNode( true ).style.backgroundClip = "";
support.clearCloneStyle = div.style.backgroundClip === "content-box";
container = document.createElement( "div" );
container.style.cssText = "border:0;width:8px;height:0;top:0;left:-9999px;" +
"padding:0;margin-top:1px;position:absolute";
div.innerHTML = "";
container.appendChild( div );
// Support: Firefox<29, Android 2.3
// Vendor-prefix box-sizing
support.boxSizing = div.style.boxSizing === "" || div.style.MozBoxSizing === "" ||
div.style.WebkitBoxSizing === "";
jQuery.extend( support, {
reliableHiddenOffsets: function() {
if ( pixelPositionVal == null ) {
computeStyleTests();
}
return reliableHiddenOffsetsVal;
},
boxSizingReliable: function() {
// We're checking for pixelPositionVal here instead of boxSizingReliableVal
// since that compresses better and they're computed together anyway.
if ( pixelPositionVal == null ) {
computeStyleTests();
}
return boxSizingReliableVal;
},
pixelMarginRight: function() {
// Support: Android 4.0-4.3
if ( pixelPositionVal == null ) {
computeStyleTests();
}
return pixelMarginRightVal;
},
pixelPosition: function() {
if ( pixelPositionVal == null ) {
computeStyleTests();
}
return pixelPositionVal;
},
reliableMarginRight: function() {
// Support: Android 2.3
if ( pixelPositionVal == null ) {
computeStyleTests();
}
return reliableMarginRightVal;
},
reliableMarginLeft: function() {
// Support: IE <=8 only, Android 4.0 - 4.3 only, Firefox <=3 - 37
if ( pixelPositionVal == null ) {
computeStyleTests();
}
return reliableMarginLeftVal;
}
} );
function computeStyleTests() {
var contents, divStyle,
documentElement = document.documentElement;
// Setup
documentElement.appendChild( container );
div.style.cssText =
// Support: Android 2.3
// Vendor-prefix box-sizing
"-webkit-box-sizing:border-box;box-sizing:border-box;" +
"position:relative;display:block;" +
"margin:auto;border:1px;padding:1px;" +
"top:1%;width:50%";
// Support: IE<9
// Assume reasonable values in the absence of getComputedStyle
pixelPositionVal = boxSizingReliableVal = reliableMarginLeftVal = false;
pixelMarginRightVal = reliableMarginRightVal = true;
// Check for getComputedStyle so that this code is not run in IE<9.
if ( window.getComputedStyle ) {
divStyle = window.getComputedStyle( div );
pixelPositionVal = ( divStyle || {} ).top !== "1%";
reliableMarginLeftVal = ( divStyle || {} ).marginLeft === "2px";
boxSizingReliableVal = ( divStyle || { width: "4px" } ).width === "4px";
// Support: Android 4.0 - 4.3 only
// Some styles come back with percentage values, even though they shouldn't
div.style.marginRight = "50%";
pixelMarginRightVal = ( divStyle || { marginRight: "4px" } ).marginRight === "4px";
// Support: Android 2.3 only
// Div with explicit width and no margin-right incorrectly
// gets computed margin-right based on width of container (#3333)
// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
contents = div.appendChild( document.createElement( "div" ) );
// Reset CSS: box-sizing; display; margin; border; padding
contents.style.cssText = div.style.cssText =
// Support: Android 2.3
// Vendor-prefix box-sizing
"-webkit-box-sizing:content-box;-moz-box-sizing:content-box;" +
"box-sizing:content-box;display:block;margin:0;border:0;padding:0";
contents.style.marginRight = contents.style.width = "0";
div.style.width = "1px";
reliableMarginRightVal =
!parseFloat( ( window.getComputedStyle( contents ) || {} ).marginRight );
div.removeChild( contents );
}
// Support: IE6-8
// First check that getClientRects works as expected
// Check if table cells still have offsetWidth/Height when they are set
// to display:none and there are still other visible table cells in a
// table row; if so, offsetWidth/Height are not reliable for use when
// determining if an element has been hidden directly using
// display:none (it is still safe to use offsets if a parent element is
// hidden; don safety goggles and see bug #4512 for more information).
div.style.display = "none";
reliableHiddenOffsetsVal = div.getClientRects().length === 0;
if ( reliableHiddenOffsetsVal ) {
div.style.display = "";
div.innerHTML = "<table><tr><td></td><td>t</td></tr></table>";
div.childNodes[ 0 ].style.borderCollapse = "separate";
contents = div.getElementsByTagName( "td" );
contents[ 0 ].style.cssText = "margin:0;border:0;padding:0;display:none";
reliableHiddenOffsetsVal = contents[ 0 ].offsetHeight === 0;
if ( reliableHiddenOffsetsVal ) {
contents[ 0 ].style.display = "";
contents[ 1 ].style.display = "none";
reliableHiddenOffsetsVal = contents[ 0 ].offsetHeight === 0;
}
}
// Teardown
documentElement.removeChild( container );
}
} )();
var getStyles, curCSS,
rposition = /^(top|right|bottom|left)$/;
if ( window.getComputedStyle ) {
getStyles = function( elem ) {
// Support: IE<=11+, Firefox<=30+ (#15098, #14150)
// IE throws on elements created in popups
// FF meanwhile throws on frame elements through "defaultView.getComputedStyle"
var view = elem.ownerDocument.defaultView;
if ( !view || !view.opener ) {
view = window;
}
return view.getComputedStyle( elem );
};
curCSS = function( elem, name, computed ) {
var width, minWidth, maxWidth, ret,
style = elem.style;
computed = computed || getStyles( elem );
// getPropertyValue is only needed for .css('filter') in IE9, see #12537
ret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined;
// Support: Opera 12.1x only
// Fall back to style even without computed
// computed is undefined for elems on document fragments
if ( ( ret === "" || ret === undefined ) && !jQuery.contains( elem.ownerDocument, elem ) ) {
ret = jQuery.style( elem, name );
}
if ( computed ) {
// A tribute to the "awesome hack by Dean Edwards"
// Chrome < 17 and Safari 5.0 uses "computed value"
// instead of "used value" for margin-right
// Safari 5.1.7 (at least) returns percentage for a larger set of values,
// but width seems to be reliably pixels
// this is against the CSSOM draft spec:
// http://dev.w3.org/csswg/cssom/#resolved-values
if ( !support.pixelMarginRight() && rnumnonpx.test( ret ) && rmargin.test( name ) ) {
// Remember the original values
width = style.width;
minWidth = style.minWidth;
maxWidth = style.maxWidth;
// Put in the new values to get a computed value out
style.minWidth = style.maxWidth = style.width = ret;
ret = computed.width;
// Revert the changed values
style.width = width;
style.minWidth = minWidth;
style.maxWidth = maxWidth;
}
}
// Support: IE
// IE returns zIndex value as an integer.
return ret === undefined ?
ret :
ret + "";
};
} else if ( documentElement.currentStyle ) {
getStyles = function( elem ) {
return elem.currentStyle;
};
curCSS = function( elem, name, computed ) {
var left, rs, rsLeft, ret,
style = elem.style;
computed = computed || getStyles( elem );
ret = computed ? computed[ name ] : undefined;
// Avoid setting ret to empty string here
// so we don't default to auto
if ( ret == null && style && style[ name ] ) {
ret = style[ name ];
}
// From the awesome hack by Dean Edwards
// http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
// If we're not dealing with a regular pixel number
// but a number that has a weird ending, we need to convert it to pixels
// but not position css attributes, as those are
// proportional to the parent element instead
// and we can't measure the parent instead because it
// might trigger a "stacking dolls" problem
if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) {
// Remember the original values
left = style.left;
rs = elem.runtimeStyle;
rsLeft = rs && rs.left;
// Put in the new values to get a computed value out
if ( rsLeft ) {
rs.left = elem.currentStyle.left;
}
style.left = name === "fontSize" ? "1em" : ret;
ret = style.pixelLeft + "px";
// Revert the changed values
style.left = left;
if ( rsLeft ) {
rs.left = rsLeft;
}
}
// Support: IE
// IE returns zIndex value as an integer.
return ret === undefined ?
ret :
ret + "" || "auto";
};
}
function addGetHookIf( conditionFn, hookFn ) {
// Define the hook, we'll check on the first run if it's really needed.
return {
get: function() {
if ( conditionFn() ) {
// Hook not needed (or it's not possible to use it due
// to missing dependency), remove it.
delete this.get;
return;
}
// Hook needed; redefine it so that the support test is not executed again.
return ( this.get = hookFn ).apply( this, arguments );
}
};
}
var
ralpha = /alpha\([^)]*\)/i,
ropacity = /opacity\s*=\s*([^)]*)/i,
// swappable if display is none or starts with table except
// "table", "table-cell", or "table-caption"
// see here for display values:
// https://developer.mozilla.org/en-US/docs/CSS/display
rdisplayswap = /^(none|table(?!-c[ea]).+)/,
rnumsplit = new RegExp( "^(" + pnum + ")(.*)$", "i" ),
cssShow = { position: "absolute", visibility: "hidden", display: "block" },
cssNormalTransform = {
letterSpacing: "0",
fontWeight: "400"
},
cssPrefixes = [ "Webkit", "O", "Moz", "ms" ],
emptyStyle = document.createElement( "div" ).style;
// return a css property mapped to a potentially vendor prefixed property
function vendorPropName( name ) {
// shortcut for names that are not vendor prefixed
if ( name in emptyStyle ) {
return name;
}
// check for vendor prefixed names
var capName = name.charAt( 0 ).toUpperCase() + name.slice( 1 ),
i = cssPrefixes.length;
while ( i-- ) {
name = cssPrefixes[ i ] + capName;
if ( name in emptyStyle ) {
return name;
}
}
}
function showHide( elements, show ) {
var display, elem, hidden,
values = [],
index = 0,
length = elements.length;
for ( ; index < length; index++ ) {
elem = elements[ index ];
if ( !elem.style ) {
continue;
}
values[ index ] = jQuery._data( elem, "olddisplay" );
display = elem.style.display;
if ( show ) {
// Reset the inline display of this element to learn if it is
// being hidden by cascaded rules or not
if ( !values[ index ] && display === "none" ) {
elem.style.display = "";
}
// Set elements which have been overridden with display: none
// in a stylesheet to whatever the default browser style is
// for such an element
if ( elem.style.display === "" && isHidden( elem ) ) {
values[ index ] =
jQuery._data( elem, "olddisplay", defaultDisplay( elem.nodeName ) );
}
} else {
hidden = isHidden( elem );
if ( display && display !== "none" || !hidden ) {
jQuery._data(
elem,
"olddisplay",
hidden ? display : jQuery.css( elem, "display" )
);
}
}
}
// Set the display of most of the elements in a second loop
// to avoid the constant reflow
for ( index = 0; index < length; index++ ) {
elem = elements[ index ];
if ( !elem.style ) {
continue;
}
if ( !show || elem.style.display === "none" || elem.style.display === "" ) {
elem.style.display = show ? values[ index ] || "" : "none";
}
}
return elements;
}
function setPositiveNumber( elem, value, subtract ) {
var matches = rnumsplit.exec( value );
return matches ?
// Guard against undefined "subtract", e.g., when used as in cssHooks
Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) :
value;
}
function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {
var i = extra === ( isBorderBox ? "border" : "content" ) ?
// If we already have the right measurement, avoid augmentation
4 :
// Otherwise initialize for horizontal or vertical properties
name === "width" ? 1 : 0,
val = 0;
for ( ; i < 4; i += 2 ) {
// both box models exclude margin, so add it if we want it
if ( extra === "margin" ) {
val += jQuery.css( elem, extra + cssExpand[ i ], true, styles );
}
if ( isBorderBox ) {
// border-box includes padding, so remove it if we want content
if ( extra === "content" ) {
val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
}
// at this point, extra isn't border nor margin, so remove border
if ( extra !== "margin" ) {
val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
}
} else {
// at this point, extra isn't content, so add padding
val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
// at this point, extra isn't content nor padding, so add border
if ( extra !== "padding" ) {
val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
}
}
}
return val;
}
function getWidthOrHeight( elem, name, extra ) {
// Start with offset property, which is equivalent to the border-box value
var valueIsBorderBox = true,
val = name === "width" ? elem.offsetWidth : elem.offsetHeight,
styles = getStyles( elem ),
isBorderBox = support.boxSizing &&
jQuery.css( elem, "boxSizing", false, styles ) === "border-box";
// some non-html elements return undefined for offsetWidth, so check for null/undefined
// svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285
// MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668
if ( val <= 0 || val == null ) {
// Fall back to computed then uncomputed css if necessary
val = curCSS( elem, name, styles );
if ( val < 0 || val == null ) {
val = elem.style[ name ];
}
// Computed unit is not pixels. Stop here and return.
if ( rnumnonpx.test( val ) ) {
return val;
}
// we need the check for style in case a browser which returns unreliable values
// for getComputedStyle silently falls back to the reliable elem.style
valueIsBorderBox = isBorderBox &&
( support.boxSizingReliable() || val === elem.style[ name ] );
// Normalize "", auto, and prepare for extra
val = parseFloat( val ) || 0;
}
// use the active box-sizing model to add/subtract irrelevant styles
return ( val +
augmentWidthOrHeight(
elem,
name,
extra || ( isBorderBox ? "border" : "content" ),
valueIsBorderBox,
styles
)
) + "px";
}
jQuery.extend( {
// Add in style property hooks for overriding the default
// behavior of getting and setting a style property
cssHooks: {
opacity: {
get: function( elem, computed ) {
if ( computed ) {
// We should always get a number back from opacity
var ret = curCSS( elem, "opacity" );
return ret === "" ? "1" : ret;
}
}
}
},
// Don't automatically add "px" to these possibly-unitless properties
cssNumber: {
"animationIterationCount": true,
"columnCount": true,
"fillOpacity": true,
"flexGrow": true,
"flexShrink": true,
"fontWeight": true,
"lineHeight": true,
"opacity": true,
"order": true,
"orphans": true,
"widows": true,
"zIndex": true,
"zoom": true
},
// Add in properties whose names you wish to fix before
// setting or getting the value
cssProps: {
// normalize float css property
"float": support.cssFloat ? "cssFloat" : "styleFloat"
},
// Get and set the style property on a DOM Node
style: function( elem, name, value, extra ) {
// Don't set styles on text and comment nodes
if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
return;
}
// Make sure that we're working with the right name
var ret, type, hooks,
origName = jQuery.camelCase( name ),
style = elem.style;
name = jQuery.cssProps[ origName ] ||
( jQuery.cssProps[ origName ] = vendorPropName( origName ) || origName );
// gets hook for the prefixed version
// followed by the unprefixed version
hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
// Check if we're setting a value
if ( value !== undefined ) {
type = typeof value;
// Convert "+=" or "-=" to relative numbers (#7345)
if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) {
value = adjustCSS( elem, name, ret );
// Fixes bug #9237
type = "number";
}
// Make sure that null and NaN values aren't set. See: #7116
if ( value == null || value !== value ) {
return;
}
// If a number was passed in, add the unit (except for certain CSS properties)
if ( type === "number" ) {
value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" );
}
// Fixes #8908, it can be done more correctly by specifing setters in cssHooks,
// but it would mean to define eight
// (for every problematic property) identical functions
if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) {
style[ name ] = "inherit";
}
// If a hook was provided, use that value, otherwise just set the specified value
if ( !hooks || !( "set" in hooks ) ||
( value = hooks.set( elem, value, extra ) ) !== undefined ) {
// Support: IE
// Swallow errors from 'invalid' CSS values (#5509)
try {
style[ name ] = value;
} catch ( e ) {}
}
} else {
// If a hook was provided get the non-computed value from there
if ( hooks && "get" in hooks &&
( ret = hooks.get( elem, false, extra ) ) !== undefined ) {
return ret;
}
// Otherwise just get the value from the style object
return style[ name ];
}
},
css: function( elem, name, extra, styles ) {
var num, val, hooks,
origName = jQuery.camelCase( name );
// Make sure that we're working with the right name
name = jQuery.cssProps[ origName ] ||
( jQuery.cssProps[ origName ] = vendorPropName( origName ) || origName );
// gets hook for the prefixed version
// followed by the unprefixed version
hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
// If a hook was provided get the computed value from there
if ( hooks && "get" in hooks ) {
val = hooks.get( elem, true, extra );
}
// Otherwise, if a way to get the computed value exists, use that
if ( val === undefined ) {
val = curCSS( elem, name, styles );
}
//convert "normal" to computed value
if ( val === "normal" && name in cssNormalTransform ) {
val = cssNormalTransform[ name ];
}
// Return, converting to number if forced or a qualifier was provided and val looks numeric
if ( extra === "" || extra ) {
num = parseFloat( val );
return extra === true || isFinite( num ) ? num || 0 : val;
}
return val;
}
} );
jQuery.each( [ "height", "width" ], function( i, name ) {
jQuery.cssHooks[ name ] = {
get: function( elem, computed, extra ) {
if ( computed ) {
// certain elements can have dimension info if we invisibly show them
// however, it must have a current display style that would benefit from this
return rdisplayswap.test( jQuery.css( elem, "display" ) ) &&
elem.offsetWidth === 0 ?
swap( elem, cssShow, function() {
return getWidthOrHeight( elem, name, extra );
} ) :
getWidthOrHeight( elem, name, extra );
}
},
set: function( elem, value, extra ) {
var styles = extra && getStyles( elem );
return setPositiveNumber( elem, value, extra ?
augmentWidthOrHeight(
elem,
name,
extra,
support.boxSizing &&
jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
styles
) : 0
);
}
};
} );
if ( !support.opacity ) {
jQuery.cssHooks.opacity = {
get: function( elem, computed ) {
// IE uses filters for opacity
return ropacity.test( ( computed && elem.currentStyle ?
elem.currentStyle.filter :
elem.style.filter ) || "" ) ?
( 0.01 * parseFloat( RegExp.$1 ) ) + "" :
computed ? "1" : "";
},
set: function( elem, value ) {
var style = elem.style,
currentStyle = elem.currentStyle,
opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "",
filter = currentStyle && currentStyle.filter || style.filter || "";
// IE has trouble with opacity if it does not have layout
// Force it by setting the zoom level
style.zoom = 1;
// if setting opacity to 1, and no other filters exist -
// attempt to remove filter attribute #6652
// if value === "", then remove inline opacity #12685
if ( ( value >= 1 || value === "" ) &&
jQuery.trim( filter.replace( ralpha, "" ) ) === "" &&
style.removeAttribute ) {
// Setting style.filter to null, "" & " " still leave "filter:" in the cssText
// if "filter:" is present at all, clearType is disabled, we want to avoid this
// style.removeAttribute is IE Only, but so apparently is this code path...
style.removeAttribute( "filter" );
// if there is no filter style applied in a css rule
// or unset inline opacity, we are done
if ( value === "" || currentStyle && !currentStyle.filter ) {
return;
}
}
// otherwise, set new filter values
style.filter = ralpha.test( filter ) ?
filter.replace( ralpha, opacity ) :
filter + " " + opacity;
}
};
}
jQuery.cssHooks.marginRight = addGetHookIf( support.reliableMarginRight,
function( elem, computed ) {
if ( computed ) {
return swap( elem, { "display": "inline-block" },
curCSS, [ elem, "marginRight" ] );
}
}
);
jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft,
function( elem, computed ) {
if ( computed ) {
return (
parseFloat( curCSS( elem, "marginLeft" ) ) ||
// Support: IE<=11+
// Running getBoundingClientRect on a disconnected node in IE throws an error
// Support: IE8 only
// getClientRects() errors on disconnected elems
( jQuery.contains( elem.ownerDocument, elem ) ?
elem.getBoundingClientRect().left -
swap( elem, { marginLeft: 0 }, function() {
return elem.getBoundingClientRect().left;
} ) :
0
)
) + "px";
}
}
);
// These hooks are used by animate to expand properties
jQuery.each( {
margin: "",
padding: "",
border: "Width"
}, function( prefix, suffix ) {
jQuery.cssHooks[ prefix + suffix ] = {
expand: function( value ) {
var i = 0,
expanded = {},
// assumes a single number if not a string
parts = typeof value === "string" ? value.split( " " ) : [ value ];
for ( ; i < 4; i++ ) {
expanded[ prefix + cssExpand[ i ] + suffix ] =
parts[ i ] || parts[ i - 2 ] || parts[ 0 ];
}
return expanded;
}
};
if ( !rmargin.test( prefix ) ) {
jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;
}
} );
jQuery.fn.extend( {
css: function( name, value ) {
return access( this, function( elem, name, value ) {
var styles, len,
map = {},
i = 0;
if ( jQuery.isArray( name ) ) {
styles = getStyles( elem );
len = name.length;
for ( ; i < len; i++ ) {
map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );
}
return map;
}
return value !== undefined ?
jQuery.style( elem, name, value ) :
jQuery.css( elem, name );
}, name, value, arguments.length > 1 );
},
show: function() {
return showHide( this, true );
},
hide: function() {
return showHide( this );
},
toggle: function( state ) {
if ( typeof state === "boolean" ) {
return state ? this.show() : this.hide();
}
return this.each( function() {
if ( isHidden( this ) ) {
jQuery( this ).show();
} else {
jQuery( this ).hide();
}
} );
}
} );
function Tween( elem, options, prop, end, easing ) {
return new Tween.prototype.init( elem, options, prop, end, easing );
}
jQuery.Tween = Tween;
Tween.prototype = {
constructor: Tween,
init: function( elem, options, prop, end, easing, unit ) {
this.elem = elem;
this.prop = prop;
this.easing = easing || jQuery.easing._default;
this.options = options;
this.start = this.now = this.cur();
this.end = end;
this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" );
},
cur: function() {
var hooks = Tween.propHooks[ this.prop ];
return hooks && hooks.get ?
hooks.get( this ) :
Tween.propHooks._default.get( this );
},
run: function( percent ) {
var eased,
hooks = Tween.propHooks[ this.prop ];
if ( this.options.duration ) {
this.pos = eased = jQuery.easing[ this.easing ](
percent, this.options.duration * percent, 0, 1, this.options.duration
);
} else {
this.pos = eased = percent;
}
this.now = ( this.end - this.start ) * eased + this.start;
if ( this.options.step ) {
this.options.step.call( this.elem, this.now, this );
}
if ( hooks && hooks.set ) {
hooks.set( this );
} else {
Tween.propHooks._default.set( this );
}
return this;
}
};
Tween.prototype.init.prototype = Tween.prototype;
Tween.propHooks = {
_default: {
get: function( tween ) {
var result;
// Use a property on the element directly when it is not a DOM element,
// or when there is no matching style property that exists.
if ( tween.elem.nodeType !== 1 ||
tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) {
return tween.elem[ tween.prop ];
}
// passing an empty string as a 3rd parameter to .css will automatically
// attempt a parseFloat and fallback to a string if the parse fails
// so, simple values such as "10px" are parsed to Float.
// complex values such as "rotate(1rad)" are returned as is.
result = jQuery.css( tween.elem, tween.prop, "" );
// Empty strings, null, undefined and "auto" are converted to 0.
return !result || result === "auto" ? 0 : result;
},
set: function( tween ) {
// use step hook for back compat - use cssHook if its there - use .style if its
// available and use plain properties where available
if ( jQuery.fx.step[ tween.prop ] ) {
jQuery.fx.step[ tween.prop ]( tween );
} else if ( tween.elem.nodeType === 1 &&
( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null ||
jQuery.cssHooks[ tween.prop ] ) ) {
jQuery.style( tween.elem, tween.prop, tween.now + tween.unit );
} else {
tween.elem[ tween.prop ] = tween.now;
}
}
}
};
// Support: IE <=9
// Panic based approach to setting things on disconnected nodes
Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {
set: function( tween ) {
if ( tween.elem.nodeType && tween.elem.parentNode ) {
tween.elem[ tween.prop ] = tween.now;
}
}
};
jQuery.easing = {
linear: function( p ) {
return p;
},
swing: function( p ) {
return 0.5 - Math.cos( p * Math.PI ) / 2;
},
_default: "swing"
};
jQuery.fx = Tween.prototype.init;
// Back Compat <1.8 extension point
jQuery.fx.step = {};
var
fxNow, timerId,
rfxtypes = /^(?:toggle|show|hide)$/,
rrun = /queueHooks$/;
// Animations created synchronously will run synchronously
function createFxNow() {
window.setTimeout( function() {
fxNow = undefined;
} );
return ( fxNow = jQuery.now() );
}
// Generate parameters to create a standard animation
function genFx( type, includeWidth ) {
var which,
attrs = { height: type },
i = 0;
// if we include width, step value is 1 to do all cssExpand values,
// if we don't include width, step value is 2 to skip over Left and Right
includeWidth = includeWidth ? 1 : 0;
for ( ; i < 4 ; i += 2 - includeWidth ) {
which = cssExpand[ i ];
attrs[ "margin" + which ] = attrs[ "padding" + which ] = type;
}
if ( includeWidth ) {
attrs.opacity = attrs.width = type;
}
return attrs;
}
function createTween( value, prop, animation ) {
var tween,
collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ),
index = 0,
length = collection.length;
for ( ; index < length; index++ ) {
if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) {
// we're done with this property
return tween;
}
}
}
function defaultPrefilter( elem, props, opts ) {
/* jshint validthis: true */
var prop, value, toggle, tween, hooks, oldfire, display, checkDisplay,
anim = this,
orig = {},
style = elem.style,
hidden = elem.nodeType && isHidden( elem ),
dataShow = jQuery._data( elem, "fxshow" );
// handle queue: false promises
if ( !opts.queue ) {
hooks = jQuery._queueHooks( elem, "fx" );
if ( hooks.unqueued == null ) {
hooks.unqueued = 0;
oldfire = hooks.empty.fire;
hooks.empty.fire = function() {
if ( !hooks.unqueued ) {
oldfire();
}
};
}
hooks.unqueued++;
anim.always( function() {
// doing this makes sure that the complete handler will be called
// before this completes
anim.always( function() {
hooks.unqueued--;
if ( !jQuery.queue( elem, "fx" ).length ) {
hooks.empty.fire();
}
} );
} );
}
// height/width overflow pass
if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) {
// Make sure that nothing sneaks out
// Record all 3 overflow attributes because IE does not
// change the overflow attribute when overflowX and
// overflowY are set to the same value
opts.overflow = [ style.overflow, style.overflowX, style.overflowY ];
// Set display property to inline-block for height/width
// animations on inline elements that are having width/height animated
display = jQuery.css( elem, "display" );
// Test default display if display is currently "none"
checkDisplay = display === "none" ?
jQuery._data( elem, "olddisplay" ) || defaultDisplay( elem.nodeName ) : display;
if ( checkDisplay === "inline" && jQuery.css( elem, "float" ) === "none" ) {
// inline-level elements accept inline-block;
// block-level elements need to be inline with layout
if ( !support.inlineBlockNeedsLayout || defaultDisplay( elem.nodeName ) === "inline" ) {
style.display = "inline-block";
} else {
style.zoom = 1;
}
}
}
if ( opts.overflow ) {
style.overflow = "hidden";
if ( !support.shrinkWrapBlocks() ) {
anim.always( function() {
style.overflow = opts.overflow[ 0 ];
style.overflowX = opts.overflow[ 1 ];
style.overflowY = opts.overflow[ 2 ];
} );
}
}
// show/hide pass
for ( prop in props ) {
value = props[ prop ];
if ( rfxtypes.exec( value ) ) {
delete props[ prop ];
toggle = toggle || value === "toggle";
if ( value === ( hidden ? "hide" : "show" ) ) {
// If there is dataShow left over from a stopped hide or show
// and we are going to proceed with show, we should pretend to be hidden
if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) {
hidden = true;
} else {
continue;
}
}
orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop );
// Any non-fx value stops us from restoring the original display value
} else {
display = undefined;
}
}
if ( !jQuery.isEmptyObject( orig ) ) {
if ( dataShow ) {
if ( "hidden" in dataShow ) {
hidden = dataShow.hidden;
}
} else {
dataShow = jQuery._data( elem, "fxshow", {} );
}
// store state if its toggle - enables .stop().toggle() to "reverse"
if ( toggle ) {
dataShow.hidden = !hidden;
}
if ( hidden ) {
jQuery( elem ).show();
} else {
anim.done( function() {
jQuery( elem ).hide();
} );
}
anim.done( function() {
var prop;
jQuery._removeData( elem, "fxshow" );
for ( prop in orig ) {
jQuery.style( elem, prop, orig[ prop ] );
}
} );
for ( prop in orig ) {
tween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim );
if ( !( prop in dataShow ) ) {
dataShow[ prop ] = tween.start;
if ( hidden ) {
tween.end = tween.start;
tween.start = prop === "width" || prop === "height" ? 1 : 0;
}
}
}
// If this is a noop like .hide().hide(), restore an overwritten display value
} else if ( ( display === "none" ? defaultDisplay( elem.nodeName ) : display ) === "inline" ) {
style.display = display;
}
}
function propFilter( props, specialEasing ) {
var index, name, easing, value, hooks;
// camelCase, specialEasing and expand cssHook pass
for ( index in props ) {
name = jQuery.camelCase( index );
easing = specialEasing[ name ];
value = props[ index ];
if ( jQuery.isArray( value ) ) {
easing = value[ 1 ];
value = props[ index ] = value[ 0 ];
}
if ( index !== name ) {
props[ name ] = value;
delete props[ index ];
}
hooks = jQuery.cssHooks[ name ];
if ( hooks && "expand" in hooks ) {
value = hooks.expand( value );
delete props[ name ];
// not quite $.extend, this wont overwrite keys already present.
// also - reusing 'index' from above because we have the correct "name"
for ( index in value ) {
if ( !( index in props ) ) {
props[ index ] = value[ index ];
specialEasing[ index ] = easing;
}
}
} else {
specialEasing[ name ] = easing;
}
}
}
function Animation( elem, properties, options ) {
var result,
stopped,
index = 0,
length = Animation.prefilters.length,
deferred = jQuery.Deferred().always( function() {
// don't match elem in the :animated selector
delete tick.elem;
} ),
tick = function() {
if ( stopped ) {
return false;
}
var currentTime = fxNow || createFxNow(),
remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),
// Support: Android 2.3
// Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497)
temp = remaining / animation.duration || 0,
percent = 1 - temp,
index = 0,
length = animation.tweens.length;
for ( ; index < length ; index++ ) {
animation.tweens[ index ].run( percent );
}
deferred.notifyWith( elem, [ animation, percent, remaining ] );
if ( percent < 1 && length ) {
return remaining;
} else {
deferred.resolveWith( elem, [ animation ] );
return false;
}
},
animation = deferred.promise( {
elem: elem,
props: jQuery.extend( {}, properties ),
opts: jQuery.extend( true, {
specialEasing: {},
easing: jQuery.easing._default
}, options ),
originalProperties: properties,
originalOptions: options,
startTime: fxNow || createFxNow(),
duration: options.duration,
tweens: [],
createTween: function( prop, end ) {
var tween = jQuery.Tween( elem, animation.opts, prop, end,
animation.opts.specialEasing[ prop ] || animation.opts.easing );
animation.tweens.push( tween );
return tween;
},
stop: function( gotoEnd ) {
var index = 0,
// if we are going to the end, we want to run all the tweens
// otherwise we skip this part
length = gotoEnd ? animation.tweens.length : 0;
if ( stopped ) {
return this;
}
stopped = true;
for ( ; index < length ; index++ ) {
animation.tweens[ index ].run( 1 );
}
// resolve when we played the last frame
// otherwise, reject
if ( gotoEnd ) {
deferred.notifyWith( elem, [ animation, 1, 0 ] );
deferred.resolveWith( elem, [ animation, gotoEnd ] );
} else {
deferred.rejectWith( elem, [ animation, gotoEnd ] );
}
return this;
}
} ),
props = animation.props;
propFilter( props, animation.opts.specialEasing );
for ( ; index < length ; index++ ) {
result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts );
if ( result ) {
if ( jQuery.isFunction( result.stop ) ) {
jQuery._queueHooks( animation.elem, animation.opts.queue ).stop =
jQuery.proxy( result.stop, result );
}
return result;
}
}
jQuery.map( props, createTween, animation );
if ( jQuery.isFunction( animation.opts.start ) ) {
animation.opts.start.call( elem, animation );
}
jQuery.fx.timer(
jQuery.extend( tick, {
elem: elem,
anim: animation,
queue: animation.opts.queue
} )
);
// attach callbacks from options
return animation.progress( animation.opts.progress )
.done( animation.opts.done, animation.opts.complete )
.fail( animation.opts.fail )
.always( animation.opts.always );
}
jQuery.Animation = jQuery.extend( Animation, {
tweeners: {
"*": [ function( prop, value ) {
var tween = this.createTween( prop, value );
adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween );
return tween;
} ]
},
tweener: function( props, callback ) {
if ( jQuery.isFunction( props ) ) {
callback = props;
props = [ "*" ];
} else {
props = props.match( rnotwhite );
}
var prop,
index = 0,
length = props.length;
for ( ; index < length ; index++ ) {
prop = props[ index ];
Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || [];
Animation.tweeners[ prop ].unshift( callback );
}
},
prefilters: [ defaultPrefilter ],
prefilter: function( callback, prepend ) {
if ( prepend ) {
Animation.prefilters.unshift( callback );
} else {
Animation.prefilters.push( callback );
}
}
} );
jQuery.speed = function( speed, easing, fn ) {
var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
complete: fn || !fn && easing ||
jQuery.isFunction( speed ) && speed,
duration: speed,
easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing
};
opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
opt.duration in jQuery.fx.speeds ?
jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;
// normalize opt.queue - true/undefined/null -> "fx"
if ( opt.queue == null || opt.queue === true ) {
opt.queue = "fx";
}
// Queueing
opt.old = opt.complete;
opt.complete = function() {
if ( jQuery.isFunction( opt.old ) ) {
opt.old.call( this );
}
if ( opt.queue ) {
jQuery.dequeue( this, opt.queue );
}
};
return opt;
};
jQuery.fn.extend( {
fadeTo: function( speed, to, easing, callback ) {
// show any hidden elements after setting opacity to 0
return this.filter( isHidden ).css( "opacity", 0 ).show()
// animate to the value specified
.end().animate( { opacity: to }, speed, easing, callback );
},
animate: function( prop, speed, easing, callback ) {
var empty = jQuery.isEmptyObject( prop ),
optall = jQuery.speed( speed, easing, callback ),
doAnimation = function() {
// Operate on a copy of prop so per-property easing won't be lost
var anim = Animation( this, jQuery.extend( {}, prop ), optall );
// Empty animations, or finishing resolves immediately
if ( empty || jQuery._data( this, "finish" ) ) {
anim.stop( true );
}
};
doAnimation.finish = doAnimation;
return empty || optall.queue === false ?
this.each( doAnimation ) :
this.queue( optall.queue, doAnimation );
},
stop: function( type, clearQueue, gotoEnd ) {
var stopQueue = function( hooks ) {
var stop = hooks.stop;
delete hooks.stop;
stop( gotoEnd );
};
if ( typeof type !== "string" ) {
gotoEnd = clearQueue;
clearQueue = type;
type = undefined;
}
if ( clearQueue && type !== false ) {
this.queue( type || "fx", [] );
}
return this.each( function() {
var dequeue = true,
index = type != null && type + "queueHooks",
timers = jQuery.timers,
data = jQuery._data( this );
if ( index ) {
if ( data[ index ] && data[ index ].stop ) {
stopQueue( data[ index ] );
}
} else {
for ( index in data ) {
if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {
stopQueue( data[ index ] );
}
}
}
for ( index = timers.length; index--; ) {
if ( timers[ index ].elem === this &&
( type == null || timers[ index ].queue === type ) ) {
timers[ index ].anim.stop( gotoEnd );
dequeue = false;
timers.splice( index, 1 );
}
}
// start the next in the queue if the last step wasn't forced
// timers currently will call their complete callbacks, which will dequeue
// but only if they were gotoEnd
if ( dequeue || !gotoEnd ) {
jQuery.dequeue( this, type );
}
} );
},
finish: function( type ) {
if ( type !== false ) {
type = type || "fx";
}
return this.each( function() {
var index,
data = jQuery._data( this ),
queue = data[ type + "queue" ],
hooks = data[ type + "queueHooks" ],
timers = jQuery.timers,
length = queue ? queue.length : 0;
// enable finishing flag on private data
data.finish = true;
// empty the queue first
jQuery.queue( this, type, [] );
if ( hooks && hooks.stop ) {
hooks.stop.call( this, true );
}
// look for any active animations, and finish them
for ( index = timers.length; index--; ) {
if ( timers[ index ].elem === this && timers[ index ].queue === type ) {
timers[ index ].anim.stop( true );
timers.splice( index, 1 );
}
}
// look for any animations in the old queue and finish them
for ( index = 0; index < length; index++ ) {
if ( queue[ index ] && queue[ index ].finish ) {
queue[ index ].finish.call( this );
}
}
// turn off finishing flag
delete data.finish;
} );
}
} );
jQuery.each( [ "toggle", "show", "hide" ], function( i, name ) {
var cssFn = jQuery.fn[ name ];
jQuery.fn[ name ] = function( speed, easing, callback ) {
return speed == null || typeof speed === "boolean" ?
cssFn.apply( this, arguments ) :
this.animate( genFx( name, true ), speed, easing, callback );
};
} );
// Generate shortcuts for custom animations
jQuery.each( {
slideDown: genFx( "show" ),
slideUp: genFx( "hide" ),
slideToggle: genFx( "toggle" ),
fadeIn: { opacity: "show" },
fadeOut: { opacity: "hide" },
fadeToggle: { opacity: "toggle" }
}, function( name, props ) {
jQuery.fn[ name ] = function( speed, easing, callback ) {
return this.animate( props, speed, easing, callback );
};
} );
jQuery.timers = [];
jQuery.fx.tick = function() {
var timer,
timers = jQuery.timers,
i = 0;
fxNow = jQuery.now();
for ( ; i < timers.length; i++ ) {
timer = timers[ i ];
// Checks the timer has not already been removed
if ( !timer() && timers[ i ] === timer ) {
timers.splice( i--, 1 );
}
}
if ( !timers.length ) {
jQuery.fx.stop();
}
fxNow = undefined;
};
jQuery.fx.timer = function( timer ) {
jQuery.timers.push( timer );
if ( timer() ) {
jQuery.fx.start();
} else {
jQuery.timers.pop();
}
};
jQuery.fx.interval = 13;
jQuery.fx.start = function() {
if ( !timerId ) {
timerId = window.setInterval( jQuery.fx.tick, jQuery.fx.interval );
}
};
jQuery.fx.stop = function() {
window.clearInterval( timerId );
timerId = null;
};
jQuery.fx.speeds = {
slow: 600,
fast: 200,
// Default speed
_default: 400
};
// Based off of the plugin by Clint Helfers, with permission.
// http://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/
jQuery.fn.delay = function( time, type ) {
time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;
type = type || "fx";
return this.queue( type, function( next, hooks ) {
var timeout = window.setTimeout( next, time );
hooks.stop = function() {
window.clearTimeout( timeout );
};
} );
};
( function() {
var a,
input = document.createElement( "input" ),
div = document.createElement( "div" ),
select = document.createElement( "select" ),
opt = select.appendChild( document.createElement( "option" ) );
// Setup
div = document.createElement( "div" );
div.setAttribute( "className", "t" );
div.innerHTML = " <link/><table></table><a href='/a'>a</a><input type='checkbox'/>";
a = div.getElementsByTagName( "a" )[ 0 ];
// Support: Windows Web Apps (WWA)
// `type` must use .setAttribute for WWA (#14901)
input.setAttribute( "type", "checkbox" );
div.appendChild( input );
a = div.getElementsByTagName( "a" )[ 0 ];
// First batch of tests.
a.style.cssText = "top:1px";
// Test setAttribute on camelCase class.
// If it works, we need attrFixes when doing get/setAttribute (ie6/7)
support.getSetAttribute = div.className !== "t";
// Get the style information from getAttribute
// (IE uses .cssText instead)
support.style = /top/.test( a.getAttribute( "style" ) );
// Make sure that URLs aren't manipulated
// (IE normalizes it by default)
support.hrefNormalized = a.getAttribute( "href" ) === "/a";
// Check the default checkbox/radio value ("" on WebKit; "on" elsewhere)
support.checkOn = !!input.value;
// Make sure that a selected-by-default option has a working selected property.
// (WebKit defaults to false instead of true, IE too, if it's in an optgroup)
support.optSelected = opt.selected;
// Tests for enctype support on a form (#6743)
support.enctype = !!document.createElement( "form" ).enctype;
// Make sure that the options inside disabled selects aren't marked as disabled
// (WebKit marks them as disabled)
select.disabled = true;
support.optDisabled = !opt.disabled;
// Support: IE8 only
// Check if we can trust getAttribute("value")
input = document.createElement( "input" );
input.setAttribute( "value", "" );
support.input = input.getAttribute( "value" ) === "";
// Check if an input maintains its value after becoming a radio
input.value = "t";
input.setAttribute( "type", "radio" );
support.radioValue = input.value === "t";
} )();
var rreturn = /\r/g,
rspaces = /[\x20\t\r\n\f]+/g;
jQuery.fn.extend( {
val: function( value ) {
var hooks, ret, isFunction,
elem = this[ 0 ];
if ( !arguments.length ) {
if ( elem ) {
hooks = jQuery.valHooks[ elem.type ] ||
jQuery.valHooks[ elem.nodeName.toLowerCase() ];
if (
hooks &&
"get" in hooks &&
( ret = hooks.get( elem, "value" ) ) !== undefined
) {
return ret;
}
ret = elem.value;
return typeof ret === "string" ?
// handle most common string cases
ret.replace( rreturn, "" ) :
// handle cases where value is null/undef or number
ret == null ? "" : ret;
}
return;
}
isFunction = jQuery.isFunction( value );
return this.each( function( i ) {
var val;
if ( this.nodeType !== 1 ) {
return;
}
if ( isFunction ) {
val = value.call( this, i, jQuery( this ).val() );
} else {
val = value;
}
// Treat null/undefined as ""; convert numbers to string
if ( val == null ) {
val = "";
} else if ( typeof val === "number" ) {
val += "";
} else if ( jQuery.isArray( val ) ) {
val = jQuery.map( val, function( value ) {
return value == null ? "" : value + "";
} );
}
hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];
// If set returns undefined, fall back to normal setting
if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) {
this.value = val;
}
} );
}
} );
jQuery.extend( {
valHooks: {
option: {
get: function( elem ) {
var val = jQuery.find.attr( elem, "value" );
return val != null ?
val :
// Support: IE10-11+
// option.text throws exceptions (#14686, #14858)
// Strip and collapse whitespace
// https://html.spec.whatwg.org/#strip-and-collapse-whitespace
jQuery.trim( jQuery.text( elem ) ).replace( rspaces, " " );
}
},
select: {
get: function( elem ) {
var value, option,
options = elem.options,
index = elem.selectedIndex,
one = elem.type === "select-one" || index < 0,
values = one ? null : [],
max = one ? index + 1 : options.length,
i = index < 0 ?
max :
one ? index : 0;
// Loop through all the selected options
for ( ; i < max; i++ ) {
option = options[ i ];
// oldIE doesn't update selected after form reset (#2551)
if ( ( option.selected || i === index ) &&
// Don't return options that are disabled or in a disabled optgroup
( support.optDisabled ?
!option.disabled :
option.getAttribute( "disabled" ) === null ) &&
( !option.parentNode.disabled ||
!jQuery.nodeName( option.parentNode, "optgroup" ) ) ) {
// Get the specific value for the option
value = jQuery( option ).val();
// We don't need an array for one selects
if ( one ) {
return value;
}
// Multi-Selects return an array
values.push( value );
}
}
return values;
},
set: function( elem, value ) {
var optionSet, option,
options = elem.options,
values = jQuery.makeArray( value ),
i = options.length;
while ( i-- ) {
option = options[ i ];
if ( jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1 ) {
// Support: IE6
// When new option element is added to select box we need to
// force reflow of newly added node in order to workaround delay
// of initialization properties
try {
option.selected = optionSet = true;
} catch ( _ ) {
// Will be executed only in IE6
option.scrollHeight;
}
} else {
option.selected = false;
}
}
// Force browsers to behave consistently when non-matching value is set
if ( !optionSet ) {
elem.selectedIndex = -1;
}
return options;
}
}
}
} );
// Radios and checkboxes getter/setter
jQuery.each( [ "radio", "checkbox" ], function() {
jQuery.valHooks[ this ] = {
set: function( elem, value ) {
if ( jQuery.isArray( value ) ) {
return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 );
}
}
};
if ( !support.checkOn ) {
jQuery.valHooks[ this ].get = function( elem ) {
return elem.getAttribute( "value" ) === null ? "on" : elem.value;
};
}
} );
var nodeHook, boolHook,
attrHandle = jQuery.expr.attrHandle,
ruseDefault = /^(?:checked|selected)$/i,
getSetAttribute = support.getSetAttribute,
getSetInput = support.input;
jQuery.fn.extend( {
attr: function( name, value ) {
return access( this, jQuery.attr, name, value, arguments.length > 1 );
},
removeAttr: function( name ) {
return this.each( function() {
jQuery.removeAttr( this, name );
} );
}
} );
jQuery.extend( {
attr: function( elem, name, value ) {
var ret, hooks,
nType = elem.nodeType;
// Don't get/set attributes on text, comment and attribute nodes
if ( nType === 3 || nType === 8 || nType === 2 ) {
return;
}
// Fallback to prop when attributes are not supported
if ( typeof elem.getAttribute === "undefined" ) {
return jQuery.prop( elem, name, value );
}
// All attributes are lowercase
// Grab necessary hook if one is defined
if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {
name = name.toLowerCase();
hooks = jQuery.attrHooks[ name ] ||
( jQuery.expr.match.bool.test( name ) ? boolHook : nodeHook );
}
if ( value !== undefined ) {
if ( value === null ) {
jQuery.removeAttr( elem, name );
return;
}
if ( hooks && "set" in hooks &&
( ret = hooks.set( elem, value, name ) ) !== undefined ) {
return ret;
}
elem.setAttribute( name, value + "" );
return value;
}
if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {
return ret;
}
ret = jQuery.find.attr( elem, name );
// Non-existent attributes return null, we normalize to undefined
return ret == null ? undefined : ret;
},
attrHooks: {
type: {
set: function( elem, value ) {
if ( !support.radioValue && value === "radio" &&
jQuery.nodeName( elem, "input" ) ) {
// Setting the type on a radio button after the value resets the value in IE8-9
// Reset value to default in case type is set after value during creation
var val = elem.value;
elem.setAttribute( "type", value );
if ( val ) {
elem.value = val;
}
return value;
}
}
}
},
removeAttr: function( elem, value ) {
var name, propName,
i = 0,
attrNames = value && value.match( rnotwhite );
if ( attrNames && elem.nodeType === 1 ) {
while ( ( name = attrNames[ i++ ] ) ) {
propName = jQuery.propFix[ name ] || name;
// Boolean attributes get special treatment (#10870)
if ( jQuery.expr.match.bool.test( name ) ) {
// Set corresponding property to false
if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) {
elem[ propName ] = false;
// Support: IE<9
// Also clear defaultChecked/defaultSelected (if appropriate)
} else {
elem[ jQuery.camelCase( "default-" + name ) ] =
elem[ propName ] = false;
}
// See #9699 for explanation of this approach (setting first, then removal)
} else {
jQuery.attr( elem, name, "" );
}
elem.removeAttribute( getSetAttribute ? name : propName );
}
}
}
} );
// Hooks for boolean attributes
boolHook = {
set: function( elem, value, name ) {
if ( value === false ) {
// Remove boolean attributes when set to false
jQuery.removeAttr( elem, name );
} else if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) {
// IE<8 needs the *property* name
elem.setAttribute( !getSetAttribute && jQuery.propFix[ name ] || name, name );
} else {
// Support: IE<9
// Use defaultChecked and defaultSelected for oldIE
elem[ jQuery.camelCase( "default-" + name ) ] = elem[ name ] = true;
}
return name;
}
};
jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) {
var getter = attrHandle[ name ] || jQuery.find.attr;
if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) {
attrHandle[ name ] = function( elem, name, isXML ) {
var ret, handle;
if ( !isXML ) {
// Avoid an infinite loop by temporarily removing this function from the getter
handle = attrHandle[ name ];
attrHandle[ name ] = ret;
ret = getter( elem, name, isXML ) != null ?
name.toLowerCase() :
null;
attrHandle[ name ] = handle;
}
return ret;
};
} else {
attrHandle[ name ] = function( elem, name, isXML ) {
if ( !isXML ) {
return elem[ jQuery.camelCase( "default-" + name ) ] ?
name.toLowerCase() :
null;
}
};
}
} );
// fix oldIE attroperties
if ( !getSetInput || !getSetAttribute ) {
jQuery.attrHooks.value = {
set: function( elem, value, name ) {
if ( jQuery.nodeName( elem, "input" ) ) {
// Does not return so that setAttribute is also used
elem.defaultValue = value;
} else {
// Use nodeHook if defined (#1954); otherwise setAttribute is fine
return nodeHook && nodeHook.set( elem, value, name );
}
}
};
}
// IE6/7 do not support getting/setting some attributes with get/setAttribute
if ( !getSetAttribute ) {
// Use this for any attribute in IE6/7
// This fixes almost every IE6/7 issue
nodeHook = {
set: function( elem, value, name ) {
// Set the existing or create a new attribute node
var ret = elem.getAttributeNode( name );
if ( !ret ) {
elem.setAttributeNode(
( ret = elem.ownerDocument.createAttribute( name ) )
);
}
ret.value = value += "";
// Break association with cloned elements by also using setAttribute (#9646)
if ( name === "value" || value === elem.getAttribute( name ) ) {
return value;
}
}
};
// Some attributes are constructed with empty-string values when not defined
attrHandle.id = attrHandle.name = attrHandle.coords =
function( elem, name, isXML ) {
var ret;
if ( !isXML ) {
return ( ret = elem.getAttributeNode( name ) ) && ret.value !== "" ?
ret.value :
null;
}
};
// Fixing value retrieval on a button requires this module
jQuery.valHooks.button = {
get: function( elem, name ) {
var ret = elem.getAttributeNode( name );
if ( ret && ret.specified ) {
return ret.value;
}
},
set: nodeHook.set
};
// Set contenteditable to false on removals(#10429)
// Setting to empty string throws an error as an invalid value
jQuery.attrHooks.contenteditable = {
set: function( elem, value, name ) {
nodeHook.set( elem, value === "" ? false : value, name );
}
};
// Set width and height to auto instead of 0 on empty string( Bug #8150 )
// This is for removals
jQuery.each( [ "width", "height" ], function( i, name ) {
jQuery.attrHooks[ name ] = {
set: function( elem, value ) {
if ( value === "" ) {
elem.setAttribute( name, "auto" );
return value;
}
}
};
} );
}
if ( !support.style ) {
jQuery.attrHooks.style = {
get: function( elem ) {
// Return undefined in the case of empty string
// Note: IE uppercases css property names, but if we were to .toLowerCase()
// .cssText, that would destroy case sensitivity in URL's, like in "background"
return elem.style.cssText || undefined;
},
set: function( elem, value ) {
return ( elem.style.cssText = value + "" );
}
};
}
var rfocusable = /^(?:input|select|textarea|button|object)$/i,
rclickable = /^(?:a|area)$/i;
jQuery.fn.extend( {
prop: function( name, value ) {
return access( this, jQuery.prop, name, value, arguments.length > 1 );
},
removeProp: function( name ) {
name = jQuery.propFix[ name ] || name;
return this.each( function() {
// try/catch handles cases where IE balks (such as removing a property on window)
try {
this[ name ] = undefined;
delete this[ name ];
} catch ( e ) {}
} );
}
} );
jQuery.extend( {
prop: function( elem, name, value ) {
var ret, hooks,
nType = elem.nodeType;
// Don't get/set properties on text, comment and attribute nodes
if ( nType === 3 || nType === 8 || nType === 2 ) {
return;
}
if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {
// Fix name and attach hooks
name = jQuery.propFix[ name ] || name;
hooks = jQuery.propHooks[ name ];
}
if ( value !== undefined ) {
if ( hooks && "set" in hooks &&
( ret = hooks.set( elem, value, name ) ) !== undefined ) {
return ret;
}
return ( elem[ name ] = value );
}
if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {
return ret;
}
return elem[ name ];
},
propHooks: {
tabIndex: {
get: function( elem ) {
// elem.tabIndex doesn't always return the
// correct value when it hasn't been explicitly set
// http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
// Use proper attribute retrieval(#12072)
var tabindex = jQuery.find.attr( elem, "tabindex" );
return tabindex ?
parseInt( tabindex, 10 ) :
rfocusable.test( elem.nodeName ) ||
rclickable.test( elem.nodeName ) && elem.href ?
0 :
-1;
}
}
},
propFix: {
"for": "htmlFor",
"class": "className"
}
} );
// Some attributes require a special call on IE
// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
if ( !support.hrefNormalized ) {
// href/src property should get the full normalized URL (#10299/#12915)
jQuery.each( [ "href", "src" ], function( i, name ) {
jQuery.propHooks[ name ] = {
get: function( elem ) {
return elem.getAttribute( name, 4 );
}
};
} );
}
// Support: Safari, IE9+
// Accessing the selectedIndex property
// forces the browser to respect setting selected
// on the option
// The getter ensures a default option is selected
// when in an optgroup
if ( !support.optSelected ) {
jQuery.propHooks.selected = {
get: function( elem ) {
var parent = elem.parentNode;
if ( parent ) {
parent.selectedIndex;
// Make sure that it also works with optgroups, see #5701
if ( parent.parentNode ) {
parent.parentNode.selectedIndex;
}
}
return null;
},
set: function( elem ) {
var parent = elem.parentNode;
if ( parent ) {
parent.selectedIndex;
if ( parent.parentNode ) {
parent.parentNode.selectedIndex;
}
}
}
};
}
jQuery.each( [
"tabIndex",
"readOnly",
"maxLength",
"cellSpacing",
"cellPadding",
"rowSpan",
"colSpan",
"useMap",
"frameBorder",
"contentEditable"
], function() {
jQuery.propFix[ this.toLowerCase() ] = this;
} );
// IE6/7 call enctype encoding
if ( !support.enctype ) {
jQuery.propFix.enctype = "encoding";
}
var rclass = /[\t\r\n\f]/g;
function getClass( elem ) {
return jQuery.attr( elem, "class" ) || "";
}
jQuery.fn.extend( {
addClass: function( value ) {
var classes, elem, cur, curValue, clazz, j, finalValue,
i = 0;
if ( jQuery.isFunction( value ) ) {
return this.each( function( j ) {
jQuery( this ).addClass( value.call( this, j, getClass( this ) ) );
} );
}
if ( typeof value === "string" && value ) {
classes = value.match( rnotwhite ) || [];
while ( ( elem = this[ i++ ] ) ) {
curValue = getClass( elem );
cur = elem.nodeType === 1 &&
( " " + curValue + " " ).replace( rclass, " " );
if ( cur ) {
j = 0;
while ( ( clazz = classes[ j++ ] ) ) {
if ( cur.indexOf( " " + clazz + " " ) < 0 ) {
cur += clazz + " ";
}
}
// only assign if different to avoid unneeded rendering.
finalValue = jQuery.trim( cur );
if ( curValue !== finalValue ) {
jQuery.attr( elem, "class", finalValue );
}
}
}
}
return this;
},
removeClass: function( value ) {
var classes, elem, cur, curValue, clazz, j, finalValue,
i = 0;
if ( jQuery.isFunction( value ) ) {
return this.each( function( j ) {
jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) );
} );
}
if ( !arguments.length ) {
return this.attr( "class", "" );
}
if ( typeof value === "string" && value ) {
classes = value.match( rnotwhite ) || [];
while ( ( elem = this[ i++ ] ) ) {
curValue = getClass( elem );
// This expression is here for better compressibility (see addClass)
cur = elem.nodeType === 1 &&
( " " + curValue + " " ).replace( rclass, " " );
if ( cur ) {
j = 0;
while ( ( clazz = classes[ j++ ] ) ) {
// Remove *all* instances
while ( cur.indexOf( " " + clazz + " " ) > -1 ) {
cur = cur.replace( " " + clazz + " ", " " );
}
}
// Only assign if different to avoid unneeded rendering.
finalValue = jQuery.trim( cur );
if ( curValue !== finalValue ) {
jQuery.attr( elem, "class", finalValue );
}
}
}
}
return this;
},
toggleClass: function( value, stateVal ) {
var type = typeof value;
if ( typeof stateVal === "boolean" && type === "string" ) {
return stateVal ? this.addClass( value ) : this.removeClass( value );
}
if ( jQuery.isFunction( value ) ) {
return this.each( function( i ) {
jQuery( this ).toggleClass(
value.call( this, i, getClass( this ), stateVal ),
stateVal
);
} );
}
return this.each( function() {
var className, i, self, classNames;
if ( type === "string" ) {
// Toggle individual class names
i = 0;
self = jQuery( this );
classNames = value.match( rnotwhite ) || [];
while ( ( className = classNames[ i++ ] ) ) {
// Check each className given, space separated list
if ( self.hasClass( className ) ) {
self.removeClass( className );
} else {
self.addClass( className );
}
}
// Toggle whole class name
} else if ( value === undefined || type === "boolean" ) {
className = getClass( this );
if ( className ) {
// store className if set
jQuery._data( this, "__className__", className );
}
// If the element has a class name or if we're passed "false",
// then remove the whole classname (if there was one, the above saved it).
// Otherwise bring back whatever was previously saved (if anything),
// falling back to the empty string if nothing was stored.
jQuery.attr( this, "class",
className || value === false ?
"" :
jQuery._data( this, "__className__" ) || ""
);
}
} );
},
hasClass: function( selector ) {
var className, elem,
i = 0;
className = " " + selector + " ";
while ( ( elem = this[ i++ ] ) ) {
if ( elem.nodeType === 1 &&
( " " + getClass( elem ) + " " ).replace( rclass, " " )
.indexOf( className ) > -1
) {
return true;
}
}
return false;
}
} );
// Return jQuery for attributes-only inclusion
jQuery.each( ( "blur focus focusin focusout load resize scroll unload click dblclick " +
"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
"change select submit keydown keypress keyup error contextmenu" ).split( " " ),
function( i, name ) {
// Handle event binding
jQuery.fn[ name ] = function( data, fn ) {
return arguments.length > 0 ?
this.on( name, null, data, fn ) :
this.trigger( name );
};
} );
jQuery.fn.extend( {
hover: function( fnOver, fnOut ) {
return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
}
} );
var location = window.location;
var nonce = jQuery.now();
var rquery = ( /\?/ );
var rvalidtokens = /(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;
jQuery.parseJSON = function( data ) {
// Attempt to parse using the native JSON parser first
if ( window.JSON && window.JSON.parse ) {
// Support: Android 2.3
// Workaround failure to string-cast null input
return window.JSON.parse( data + "" );
}
var requireNonComma,
depth = null,
str = jQuery.trim( data + "" );
// Guard against invalid (and possibly dangerous) input by ensuring that nothing remains
// after removing valid tokens
return str && !jQuery.trim( str.replace( rvalidtokens, function( token, comma, open, close ) {
// Force termination if we see a misplaced comma
if ( requireNonComma && comma ) {
depth = 0;
}
// Perform no more replacements after returning to outermost depth
if ( depth === 0 ) {
return token;
}
// Commas must not follow "[", "{", or ","
requireNonComma = open || comma;
// Determine new depth
// array/object open ("[" or "{"): depth += true - false (increment)
// array/object close ("]" or "}"): depth += false - true (decrement)
// other cases ("," or primitive): depth += true - true (numeric cast)
depth += !close - !open;
// Remove this token
return "";
} ) ) ?
( Function( "return " + str ) )() :
jQuery.error( "Invalid JSON: " + data );
};
// Cross-browser xml parsing
jQuery.parseXML = function( data ) {
var xml, tmp;
if ( !data || typeof data !== "string" ) {
return null;
}
try {
if ( window.DOMParser ) { // Standard
tmp = new window.DOMParser();
xml = tmp.parseFromString( data, "text/xml" );
} else { // IE
xml = new window.ActiveXObject( "Microsoft.XMLDOM" );
xml.async = "false";
xml.loadXML( data );
}
} catch ( e ) {
xml = undefined;
}
if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) {
jQuery.error( "Invalid XML: " + data );
}
return xml;
};
var
rhash = /#.*$/,
rts = /([?&])_=[^&]*/,
// IE leaves an \r character at EOL
rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg,
// #7653, #8125, #8152: local protocol detection
rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/,
rnoContent = /^(?:GET|HEAD)$/,
rprotocol = /^\/\//,
rurl = /^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,
/* Prefilters
* 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)
* 2) These are called:
* - BEFORE asking for a transport
* - AFTER param serialization (s.data is a string if s.processData is true)
* 3) key is the dataType
* 4) the catchall symbol "*" can be used
* 5) execution will start with transport dataType and THEN continue down to "*" if needed
*/
prefilters = {},
/* Transports bindings
* 1) key is the dataType
* 2) the catchall symbol "*" can be used
* 3) selection will start with transport dataType and THEN go to "*" if needed
*/
transports = {},
// Avoid comment-prolog char sequence (#10098); must appease lint and evade compression
allTypes = "*/".concat( "*" ),
// Document location
ajaxLocation = location.href,
// Segment location into parts
ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || [];
// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport
function addToPrefiltersOrTransports( structure ) {
// dataTypeExpression is optional and defaults to "*"
return function( dataTypeExpression, func ) {
if ( typeof dataTypeExpression !== "string" ) {
func = dataTypeExpression;
dataTypeExpression = "*";
}
var dataType,
i = 0,
dataTypes = dataTypeExpression.toLowerCase().match( rnotwhite ) || [];
if ( jQuery.isFunction( func ) ) {
// For each dataType in the dataTypeExpression
while ( ( dataType = dataTypes[ i++ ] ) ) {
// Prepend if requested
if ( dataType.charAt( 0 ) === "+" ) {
dataType = dataType.slice( 1 ) || "*";
( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func );
// Otherwise append
} else {
( structure[ dataType ] = structure[ dataType ] || [] ).push( func );
}
}
}
};
}
// Base inspection function for prefilters and transports
function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {
var inspected = {},
seekingTransport = ( structure === transports );
function inspect( dataType ) {
var selected;
inspected[ dataType ] = true;
jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {
var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );
if ( typeof dataTypeOrTransport === "string" &&
!seekingTransport && !inspected[ dataTypeOrTransport ] ) {
options.dataTypes.unshift( dataTypeOrTransport );
inspect( dataTypeOrTransport );
return false;
} else if ( seekingTransport ) {
return !( selected = dataTypeOrTransport );
}
} );
return selected;
}
return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" );
}
// A special extend for ajax options
// that takes "flat" options (not to be deep extended)
// Fixes #9887
function ajaxExtend( target, src ) {
var deep, key,
flatOptions = jQuery.ajaxSettings.flatOptions || {};
for ( key in src ) {
if ( src[ key ] !== undefined ) {
( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ];
}
}
if ( deep ) {
jQuery.extend( true, target, deep );
}
return target;
}
/* Handles responses to an ajax request:
* - finds the right dataType (mediates between content-type and expected dataType)
* - returns the corresponding response
*/
function ajaxHandleResponses( s, jqXHR, responses ) {
var firstDataType, ct, finalDataType, type,
contents = s.contents,
dataTypes = s.dataTypes;
// Remove auto dataType and get content-type in the process
while ( dataTypes[ 0 ] === "*" ) {
dataTypes.shift();
if ( ct === undefined ) {
ct = s.mimeType || jqXHR.getResponseHeader( "Content-Type" );
}
}
// Check if we're dealing with a known content-type
if ( ct ) {
for ( type in contents ) {
if ( contents[ type ] && contents[ type ].test( ct ) ) {
dataTypes.unshift( type );
break;
}
}
}
// Check to see if we have a response for the expected dataType
if ( dataTypes[ 0 ] in responses ) {
finalDataType = dataTypes[ 0 ];
} else {
// Try convertible dataTypes
for ( type in responses ) {
if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[ 0 ] ] ) {
finalDataType = type;
break;
}
if ( !firstDataType ) {
firstDataType = type;
}
}
// Or just use first one
finalDataType = finalDataType || firstDataType;
}
// If we found a dataType
// We add the dataType to the list if needed
// and return the corresponding response
if ( finalDataType ) {
if ( finalDataType !== dataTypes[ 0 ] ) {
dataTypes.unshift( finalDataType );
}
return responses[ finalDataType ];
}
}
/* Chain conversions given the request and the original response
* Also sets the responseXXX fields on the jqXHR instance
*/
function ajaxConvert( s, response, jqXHR, isSuccess ) {
var conv2, current, conv, tmp, prev,
converters = {},
// Work with a copy of dataTypes in case we need to modify it for conversion
dataTypes = s.dataTypes.slice();
// Create converters map with lowercased keys
if ( dataTypes[ 1 ] ) {
for ( conv in s.converters ) {
converters[ conv.toLowerCase() ] = s.converters[ conv ];
}
}
current = dataTypes.shift();
// Convert to each sequential dataType
while ( current ) {
if ( s.responseFields[ current ] ) {
jqXHR[ s.responseFields[ current ] ] = response;
}
// Apply the dataFilter if provided
if ( !prev && isSuccess && s.dataFilter ) {
response = s.dataFilter( response, s.dataType );
}
prev = current;
current = dataTypes.shift();
if ( current ) {
// There's only work to do if current dataType is non-auto
if ( current === "*" ) {
current = prev;
// Convert response if prev dataType is non-auto and differs from current
} else if ( prev !== "*" && prev !== current ) {
// Seek a direct converter
conv = converters[ prev + " " + current ] || converters[ "* " + current ];
// If none found, seek a pair
if ( !conv ) {
for ( conv2 in converters ) {
// If conv2 outputs current
tmp = conv2.split( " " );
if ( tmp[ 1 ] === current ) {
// If prev can be converted to accepted input
conv = converters[ prev + " " + tmp[ 0 ] ] ||
converters[ "* " + tmp[ 0 ] ];
if ( conv ) {
// Condense equivalence converters
if ( conv === true ) {
conv = converters[ conv2 ];
// Otherwise, insert the intermediate dataType
} else if ( converters[ conv2 ] !== true ) {
current = tmp[ 0 ];
dataTypes.unshift( tmp[ 1 ] );
}
break;
}
}
}
}
// Apply converter (if not an equivalence)
if ( conv !== true ) {
// Unless errors are allowed to bubble, catch and return them
if ( conv && s[ "throws" ] ) { // jscs:ignore requireDotNotation
response = conv( response );
} else {
try {
response = conv( response );
} catch ( e ) {
return {
state: "parsererror",
error: conv ? e : "No conversion from " + prev + " to " + current
};
}
}
}
}
}
}
return { state: "success", data: response };
}
jQuery.extend( {
// Counter for holding the number of active queries
active: 0,
// Last-Modified header cache for next request
lastModified: {},
etag: {},
ajaxSettings: {
url: ajaxLocation,
type: "GET",
isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ),
global: true,
processData: true,
async: true,
contentType: "application/x-www-form-urlencoded; charset=UTF-8",
/*
timeout: 0,
data: null,
dataType: null,
username: null,
password: null,
cache: null,
throws: false,
traditional: false,
headers: {},
*/
accepts: {
"*": allTypes,
text: "text/plain",
html: "text/html",
xml: "application/xml, text/xml",
json: "application/json, text/javascript"
},
contents: {
xml: /\bxml\b/,
html: /\bhtml/,
json: /\bjson\b/
},
responseFields: {
xml: "responseXML",
text: "responseText",
json: "responseJSON"
},
// Data converters
// Keys separate source (or catchall "*") and destination types with a single space
converters: {
// Convert anything to text
"* text": String,
// Text to html (true = no transformation)
"text html": true,
// Evaluate text as a json expression
"text json": jQuery.parseJSON,
// Parse text as xml
"text xml": jQuery.parseXML
},
// For options that shouldn't be deep extended:
// you can add your own custom options here if
// and when you create one that shouldn't be
// deep extended (see ajaxExtend)
flatOptions: {
url: true,
context: true
}
},
// Creates a full fledged settings object into target
// with both ajaxSettings and settings fields.
// If target is omitted, writes into ajaxSettings.
ajaxSetup: function( target, settings ) {
return settings ?
// Building a settings object
ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) :
// Extending ajaxSettings
ajaxExtend( jQuery.ajaxSettings, target );
},
ajaxPrefilter: addToPrefiltersOrTransports( prefilters ),
ajaxTransport: addToPrefiltersOrTransports( transports ),
// Main method
ajax: function( url, options ) {
// If url is an object, simulate pre-1.5 signature
if ( typeof url === "object" ) {
options = url;
url = undefined;
}
// Force options to be an object
options = options || {};
var
// Cross-domain detection vars
parts,
// Loop variable
i,
// URL without anti-cache param
cacheURL,
// Response headers as string
responseHeadersString,
// timeout handle
timeoutTimer,
// To know if global events are to be dispatched
fireGlobals,
transport,
// Response headers
responseHeaders,
// Create the final options object
s = jQuery.ajaxSetup( {}, options ),
// Callbacks context
callbackContext = s.context || s,
// Context for global events is callbackContext if it is a DOM node or jQuery collection
globalEventContext = s.context &&
( callbackContext.nodeType || callbackContext.jquery ) ?
jQuery( callbackContext ) :
jQuery.event,
// Deferreds
deferred = jQuery.Deferred(),
completeDeferred = jQuery.Callbacks( "once memory" ),
// Status-dependent callbacks
statusCode = s.statusCode || {},
// Headers (they are sent all at once)
requestHeaders = {},
requestHeadersNames = {},
// The jqXHR state
state = 0,
// Default abort message
strAbort = "canceled",
// Fake xhr
jqXHR = {
readyState: 0,
// Builds headers hashtable if needed
getResponseHeader: function( key ) {
var match;
if ( state === 2 ) {
if ( !responseHeaders ) {
responseHeaders = {};
while ( ( match = rheaders.exec( responseHeadersString ) ) ) {
responseHeaders[ match[ 1 ].toLowerCase() ] = match[ 2 ];
}
}
match = responseHeaders[ key.toLowerCase() ];
}
return match == null ? null : match;
},
// Raw string
getAllResponseHeaders: function() {
return state === 2 ? responseHeadersString : null;
},
// Caches the header
setRequestHeader: function( name, value ) {
var lname = name.toLowerCase();
if ( !state ) {
name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name;
requestHeaders[ name ] = value;
}
return this;
},
// Overrides response content-type header
overrideMimeType: function( type ) {
if ( !state ) {
s.mimeType = type;
}
return this;
},
// Status-dependent callbacks
statusCode: function( map ) {
var code;
if ( map ) {
if ( state < 2 ) {
for ( code in map ) {
// Lazy-add the new callback in a way that preserves old ones
statusCode[ code ] = [ statusCode[ code ], map[ code ] ];
}
} else {
// Execute the appropriate callbacks
jqXHR.always( map[ jqXHR.status ] );
}
}
return this;
},
// Cancel the request
abort: function( statusText ) {
var finalText = statusText || strAbort;
if ( transport ) {
transport.abort( finalText );
}
done( 0, finalText );
return this;
}
};
// Attach deferreds
deferred.promise( jqXHR ).complete = completeDeferred.add;
jqXHR.success = jqXHR.done;
jqXHR.error = jqXHR.fail;
// Remove hash character (#7531: and string promotion)
// Add protocol if not provided (#5866: IE7 issue with protocol-less urls)
// Handle falsy url in the settings object (#10093: consistency with old signature)
// We also use the url parameter if available
s.url = ( ( url || s.url || ajaxLocation ) + "" )
.replace( rhash, "" )
.replace( rprotocol, ajaxLocParts[ 1 ] + "//" );
// Alias method option to type as per ticket #12004
s.type = options.method || options.type || s.method || s.type;
// Extract dataTypes list
s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().match( rnotwhite ) || [ "" ];
// A cross-domain request is in order when we have a protocol:host:port mismatch
if ( s.crossDomain == null ) {
parts = rurl.exec( s.url.toLowerCase() );
s.crossDomain = !!( parts &&
( parts[ 1 ] !== ajaxLocParts[ 1 ] || parts[ 2 ] !== ajaxLocParts[ 2 ] ||
( parts[ 3 ] || ( parts[ 1 ] === "http:" ? "80" : "443" ) ) !==
( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? "80" : "443" ) ) )
);
}
// Convert data if not already a string
if ( s.data && s.processData && typeof s.data !== "string" ) {
s.data = jQuery.param( s.data, s.traditional );
}
// Apply prefilters
inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
// If request was aborted inside a prefilter, stop there
if ( state === 2 ) {
return jqXHR;
}
// We can fire global events as of now if asked to
// Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118)
fireGlobals = jQuery.event && s.global;
// Watch for a new set of requests
if ( fireGlobals && jQuery.active++ === 0 ) {
jQuery.event.trigger( "ajaxStart" );
}
// Uppercase the type
s.type = s.type.toUpperCase();
// Determine if request has content
s.hasContent = !rnoContent.test( s.type );
// Save the URL in case we're toying with the If-Modified-Since
// and/or If-None-Match header later on
cacheURL = s.url;
// More options handling for requests with no content
if ( !s.hasContent ) {
// If data is available, append data to url
if ( s.data ) {
cacheURL = ( s.url += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data );
// #9682: remove data so that it's not used in an eventual retry
delete s.data;
}
// Add anti-cache in url if needed
if ( s.cache === false ) {
s.url = rts.test( cacheURL ) ?
// If there is already a '_' parameter, set its value
cacheURL.replace( rts, "$1_=" + nonce++ ) :
// Otherwise add one to the end
cacheURL + ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + nonce++;
}
}
// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
if ( s.ifModified ) {
if ( jQuery.lastModified[ cacheURL ] ) {
jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] );
}
if ( jQuery.etag[ cacheURL ] ) {
jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] );
}
}
// Set the correct header, if data is being sent
if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {
jqXHR.setRequestHeader( "Content-Type", s.contentType );
}
// Set the Accepts header for the server, depending on the dataType
jqXHR.setRequestHeader(
"Accept",
s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ?
s.accepts[ s.dataTypes[ 0 ] ] +
( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) :
s.accepts[ "*" ]
);
// Check for headers option
for ( i in s.headers ) {
jqXHR.setRequestHeader( i, s.headers[ i ] );
}
// Allow custom headers/mimetypes and early abort
if ( s.beforeSend &&
( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) {
// Abort if not done already and return
return jqXHR.abort();
}
// aborting is no longer a cancellation
strAbort = "abort";
// Install callbacks on deferreds
for ( i in { success: 1, error: 1, complete: 1 } ) {
jqXHR[ i ]( s[ i ] );
}
// Get transport
transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );
// If no transport, we auto-abort
if ( !transport ) {
done( -1, "No Transport" );
} else {
jqXHR.readyState = 1;
// Send global event
if ( fireGlobals ) {
globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] );
}
// If request was aborted inside ajaxSend, stop there
if ( state === 2 ) {
return jqXHR;
}
// Timeout
if ( s.async && s.timeout > 0 ) {
timeoutTimer = window.setTimeout( function() {
jqXHR.abort( "timeout" );
}, s.timeout );
}
try {
state = 1;
transport.send( requestHeaders, done );
} catch ( e ) {
// Propagate exception as error if not done
if ( state < 2 ) {
done( -1, e );
// Simply rethrow otherwise
} else {
throw e;
}
}
}
// Callback for when everything is done
function done( status, nativeStatusText, responses, headers ) {
var isSuccess, success, error, response, modified,
statusText = nativeStatusText;
// Called once
if ( state === 2 ) {
return;
}
// State is "done" now
state = 2;
// Clear timeout if it exists
if ( timeoutTimer ) {
window.clearTimeout( timeoutTimer );
}
// Dereference transport for early garbage collection
// (no matter how long the jqXHR object will be used)
transport = undefined;
// Cache response headers
responseHeadersString = headers || "";
// Set readyState
jqXHR.readyState = status > 0 ? 4 : 0;
// Determine if successful
isSuccess = status >= 200 && status < 300 || status === 304;
// Get response data
if ( responses ) {
response = ajaxHandleResponses( s, jqXHR, responses );
}
// Convert no matter what (that way responseXXX fields are always set)
response = ajaxConvert( s, response, jqXHR, isSuccess );
// If successful, handle type chaining
if ( isSuccess ) {
// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
if ( s.ifModified ) {
modified = jqXHR.getResponseHeader( "Last-Modified" );
if ( modified ) {
jQuery.lastModified[ cacheURL ] = modified;
}
modified = jqXHR.getResponseHeader( "etag" );
if ( modified ) {
jQuery.etag[ cacheURL ] = modified;
}
}
// if no content
if ( status === 204 || s.type === "HEAD" ) {
statusText = "nocontent";
// if not modified
} else if ( status === 304 ) {
statusText = "notmodified";
// If we have data, let's convert it
} else {
statusText = response.state;
success = response.data;
error = response.error;
isSuccess = !error;
}
} else {
// We extract error from statusText
// then normalize statusText and status for non-aborts
error = statusText;
if ( status || !statusText ) {
statusText = "error";
if ( status < 0 ) {
status = 0;
}
}
}
// Set data for the fake xhr object
jqXHR.status = status;
jqXHR.statusText = ( nativeStatusText || statusText ) + "";
// Success/Error
if ( isSuccess ) {
deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
} else {
deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
}
// Status-dependent callbacks
jqXHR.statusCode( statusCode );
statusCode = undefined;
if ( fireGlobals ) {
globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError",
[ jqXHR, s, isSuccess ? success : error ] );
}
// Complete
completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );
if ( fireGlobals ) {
globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] );
// Handle the global AJAX counter
if ( !( --jQuery.active ) ) {
jQuery.event.trigger( "ajaxStop" );
}
}
}
return jqXHR;
},
getJSON: function( url, data, callback ) {
return jQuery.get( url, data, callback, "json" );
},
getScript: function( url, callback ) {
return jQuery.get( url, undefined, callback, "script" );
}
} );
jQuery.each( [ "get", "post" ], function( i, method ) {
jQuery[ method ] = function( url, data, callback, type ) {
// shift arguments if data argument was omitted
if ( jQuery.isFunction( data ) ) {
type = type || callback;
callback = data;
data = undefined;
}
// The url can be an options object (which then must have .url)
return jQuery.ajax( jQuery.extend( {
url: url,
type: method,
dataType: type,
data: data,
success: callback
}, jQuery.isPlainObject( url ) && url ) );
};
} );
jQuery._evalUrl = function( url ) {
return jQuery.ajax( {
url: url,
// Make this explicit, since user can override this through ajaxSetup (#11264)
type: "GET",
dataType: "script",
cache: true,
async: false,
global: false,
"throws": true
} );
};
jQuery.fn.extend( {
wrapAll: function( html ) {
if ( jQuery.isFunction( html ) ) {
return this.each( function( i ) {
jQuery( this ).wrapAll( html.call( this, i ) );
} );
}
if ( this[ 0 ] ) {
// The elements to wrap the target around
var wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true );
if ( this[ 0 ].parentNode ) {
wrap.insertBefore( this[ 0 ] );
}
wrap.map( function() {
var elem = this;
while ( elem.firstChild && elem.firstChild.nodeType === 1 ) {
elem = elem.firstChild;
}
return elem;
} ).append( this );
}
return this;
},
wrapInner: function( html ) {
if ( jQuery.isFunction( html ) ) {
return this.each( function( i ) {
jQuery( this ).wrapInner( html.call( this, i ) );
} );
}
return this.each( function() {
var self = jQuery( this ),
contents = self.contents();
if ( contents.length ) {
contents.wrapAll( html );
} else {
self.append( html );
}
} );
},
wrap: function( html ) {
var isFunction = jQuery.isFunction( html );
return this.each( function( i ) {
jQuery( this ).wrapAll( isFunction ? html.call( this, i ) : html );
} );
},
unwrap: function() {
return this.parent().each( function() {
if ( !jQuery.nodeName( this, "body" ) ) {
jQuery( this ).replaceWith( this.childNodes );
}
} ).end();
}
} );
function getDisplay( elem ) {
return elem.style && elem.style.display || jQuery.css( elem, "display" );
}
function filterHidden( elem ) {
// Disconnected elements are considered hidden
if ( !jQuery.contains( elem.ownerDocument || document, elem ) ) {
return true;
}
while ( elem && elem.nodeType === 1 ) {
if ( getDisplay( elem ) === "none" || elem.type === "hidden" ) {
return true;
}
elem = elem.parentNode;
}
return false;
}
jQuery.expr.filters.hidden = function( elem ) {
// Support: Opera <= 12.12
// Opera reports offsetWidths and offsetHeights less than zero on some elements
return support.reliableHiddenOffsets() ?
( elem.offsetWidth <= 0 && elem.offsetHeight <= 0 &&
!elem.getClientRects().length ) :
filterHidden( elem );
};
jQuery.expr.filters.visible = function( elem ) {
return !jQuery.expr.filters.hidden( elem );
};
var r20 = /%20/g,
rbracket = /\[\]$/,
rCRLF = /\r?\n/g,
rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i,
rsubmittable = /^(?:input|select|textarea|keygen)/i;
function buildParams( prefix, obj, traditional, add ) {
var name;
if ( jQuery.isArray( obj ) ) {
// Serialize array item.
jQuery.each( obj, function( i, v ) {
if ( traditional || rbracket.test( prefix ) ) {
// Treat each array item as a scalar.
add( prefix, v );
} else {
// Item is non-scalar (array or object), encode its numeric index.
buildParams(
prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]",
v,
traditional,
add
);
}
} );
} else if ( !traditional && jQuery.type( obj ) === "object" ) {
// Serialize object item.
for ( name in obj ) {
buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add );
}
} else {
// Serialize scalar item.
add( prefix, obj );
}
}
// Serialize an array of form elements or a set of
// key/values into a query string
jQuery.param = function( a, traditional ) {
var prefix,
s = [],
add = function( key, value ) {
// If value is a function, invoke it and return its value
value = jQuery.isFunction( value ) ? value() : ( value == null ? "" : value );
s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value );
};
// Set traditional to true for jQuery <= 1.3.2 behavior.
if ( traditional === undefined ) {
traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional;
}
// If an array was passed in, assume that it is an array of form elements.
if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {
// Serialize the form elements
jQuery.each( a, function() {
add( this.name, this.value );
} );
} else {
// If traditional, encode the "old" way (the way 1.3.2 or older
// did it), otherwise encode params recursively.
for ( prefix in a ) {
buildParams( prefix, a[ prefix ], traditional, add );
}
}
// Return the resulting serialization
return s.join( "&" ).replace( r20, "+" );
};
jQuery.fn.extend( {
serialize: function() {
return jQuery.param( this.serializeArray() );
},
serializeArray: function() {
return this.map( function() {
// Can add propHook for "elements" to filter or add form elements
var elements = jQuery.prop( this, "elements" );
return elements ? jQuery.makeArray( elements ) : this;
} )
.filter( function() {
var type = this.type;
// Use .is(":disabled") so that fieldset[disabled] works
return this.name && !jQuery( this ).is( ":disabled" ) &&
rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&
( this.checked || !rcheckableType.test( type ) );
} )
.map( function( i, elem ) {
var val = jQuery( this ).val();
return val == null ?
null :
jQuery.isArray( val ) ?
jQuery.map( val, function( val ) {
return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
} ) :
{ name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
} ).get();
}
} );
// Create the request object
// (This is still attached to ajaxSettings for backward compatibility)
jQuery.ajaxSettings.xhr = window.ActiveXObject !== undefined ?
// Support: IE6-IE8
function() {
// XHR cannot access local files, always use ActiveX for that case
if ( this.isLocal ) {
return createActiveXHR();
}
// Support: IE 9-11
// IE seems to error on cross-domain PATCH requests when ActiveX XHR
// is used. In IE 9+ always use the native XHR.
// Note: this condition won't catch Edge as it doesn't define
// document.documentMode but it also doesn't support ActiveX so it won't
// reach this code.
if ( document.documentMode > 8 ) {
return createStandardXHR();
}
// Support: IE<9
// oldIE XHR does not support non-RFC2616 methods (#13240)
// See http://msdn.microsoft.com/en-us/library/ie/ms536648(v=vs.85).aspx
// and http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9
// Although this check for six methods instead of eight
// since IE also does not support "trace" and "connect"
return /^(get|post|head|put|delete|options)$/i.test( this.type ) &&
createStandardXHR() || createActiveXHR();
} :
// For all other browsers, use the standard XMLHttpRequest object
createStandardXHR;
var xhrId = 0,
xhrCallbacks = {},
xhrSupported = jQuery.ajaxSettings.xhr();
// Support: IE<10
// Open requests must be manually aborted on unload (#5280)
// See https://support.microsoft.com/kb/2856746 for more info
if ( window.attachEvent ) {
window.attachEvent( "onunload", function() {
for ( var key in xhrCallbacks ) {
xhrCallbacks[ key ]( undefined, true );
}
} );
}
// Determine support properties
support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported );
xhrSupported = support.ajax = !!xhrSupported;
// Create transport if the browser can provide an xhr
if ( xhrSupported ) {
jQuery.ajaxTransport( function( options ) {
// Cross domain only allowed if supported through XMLHttpRequest
if ( !options.crossDomain || support.cors ) {
var callback;
return {
send: function( headers, complete ) {
var i,
xhr = options.xhr(),
id = ++xhrId;
// Open the socket
xhr.open(
options.type,
options.url,
options.async,
options.username,
options.password
);
// Apply custom fields if provided
if ( options.xhrFields ) {
for ( i in options.xhrFields ) {
xhr[ i ] = options.xhrFields[ i ];
}
}
// Override mime type if needed
if ( options.mimeType && xhr.overrideMimeType ) {
xhr.overrideMimeType( options.mimeType );
}
// X-Requested-With header
// For cross-domain requests, seeing as conditions for a preflight are
// akin to a jigsaw puzzle, we simply never set it to be sure.
// (it can always be set on a per-request basis or even using ajaxSetup)
// For same-domain requests, won't change header if already provided.
if ( !options.crossDomain && !headers[ "X-Requested-With" ] ) {
headers[ "X-Requested-With" ] = "XMLHttpRequest";
}
// Set headers
for ( i in headers ) {
// Support: IE<9
// IE's ActiveXObject throws a 'Type Mismatch' exception when setting
// request header to a null-value.
//
// To keep consistent with other XHR implementations, cast the value
// to string and ignore `undefined`.
if ( headers[ i ] !== undefined ) {
xhr.setRequestHeader( i, headers[ i ] + "" );
}
}
// Do send the request
// This may raise an exception which is actually
// handled in jQuery.ajax (so no try/catch here)
xhr.send( ( options.hasContent && options.data ) || null );
// Listener
callback = function( _, isAbort ) {
var status, statusText, responses;
// Was never called and is aborted or complete
if ( callback && ( isAbort || xhr.readyState === 4 ) ) {
// Clean up
delete xhrCallbacks[ id ];
callback = undefined;
xhr.onreadystatechange = jQuery.noop;
// Abort manually if needed
if ( isAbort ) {
if ( xhr.readyState !== 4 ) {
xhr.abort();
}
} else {
responses = {};
status = xhr.status;
// Support: IE<10
// Accessing binary-data responseText throws an exception
// (#11426)
if ( typeof xhr.responseText === "string" ) {
responses.text = xhr.responseText;
}
// Firefox throws an exception when accessing
// statusText for faulty cross-domain requests
try {
statusText = xhr.statusText;
} catch ( e ) {
// We normalize with Webkit giving an empty statusText
statusText = "";
}
// Filter status for non standard behaviors
// If the request is local and we have data: assume a success
// (success with no data won't get notified, that's the best we
// can do given current implementations)
if ( !status && options.isLocal && !options.crossDomain ) {
status = responses.text ? 200 : 404;
// IE - #1450: sometimes returns 1223 when it should be 204
} else if ( status === 1223 ) {
status = 204;
}
}
}
// Call complete if needed
if ( responses ) {
complete( status, statusText, responses, xhr.getAllResponseHeaders() );
}
};
// Do send the request
// `xhr.send` may raise an exception, but it will be
// handled in jQuery.ajax (so no try/catch here)
if ( !options.async ) {
// If we're in sync mode we fire the callback
callback();
} else if ( xhr.readyState === 4 ) {
// (IE6 & IE7) if it's in cache and has been
// retrieved directly we need to fire the callback
window.setTimeout( callback );
} else {
// Register the callback, but delay it in case `xhr.send` throws
// Add to the list of active xhr callbacks
xhr.onreadystatechange = xhrCallbacks[ id ] = callback;
}
},
abort: function() {
if ( callback ) {
callback( undefined, true );
}
}
};
}
} );
}
// Functions to create xhrs
function createStandardXHR() {
try {
return new window.XMLHttpRequest();
} catch ( e ) {}
}
function createActiveXHR() {
try {
return new window.ActiveXObject( "Microsoft.XMLHTTP" );
} catch ( e ) {}
}
// Install script dataType
jQuery.ajaxSetup( {
accepts: {
script: "text/javascript, application/javascript, " +
"application/ecmascript, application/x-ecmascript"
},
contents: {
script: /\b(?:java|ecma)script\b/
},
converters: {
"text script": function( text ) {
jQuery.globalEval( text );
return text;
}
}
} );
// Handle cache's special case and global
jQuery.ajaxPrefilter( "script", function( s ) {
if ( s.cache === undefined ) {
s.cache = false;
}
if ( s.crossDomain ) {
s.type = "GET";
s.global = false;
}
} );
// Bind script tag hack transport
jQuery.ajaxTransport( "script", function( s ) {
// This transport only deals with cross domain requests
if ( s.crossDomain ) {
var script,
head = document.head || jQuery( "head" )[ 0 ] || document.documentElement;
return {
send: function( _, callback ) {
script = document.createElement( "script" );
script.async = true;
if ( s.scriptCharset ) {
script.charset = s.scriptCharset;
}
script.src = s.url;
// Attach handlers for all browsers
script.onload = script.onreadystatechange = function( _, isAbort ) {
if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {
// Handle memory leak in IE
script.onload = script.onreadystatechange = null;
// Remove the script
if ( script.parentNode ) {
script.parentNode.removeChild( script );
}
// Dereference the script
script = null;
// Callback if not abort
if ( !isAbort ) {
callback( 200, "success" );
}
}
};
// Circumvent IE6 bugs with base elements (#2709 and #4378) by prepending
// Use native DOM manipulation to avoid our domManip AJAX trickery
head.insertBefore( script, head.firstChild );
},
abort: function() {
if ( script ) {
script.onload( undefined, true );
}
}
};
}
} );
var oldCallbacks = [],
rjsonp = /(=)\?(?=&|$)|\?\?/;
// Default jsonp settings
jQuery.ajaxSetup( {
jsonp: "callback",
jsonpCallback: function() {
var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( nonce++ ) );
this[ callback ] = true;
return callback;
}
} );
// Detect, normalize options and install callbacks for jsonp requests
jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
var callbackName, overwritten, responseContainer,
jsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ?
"url" :
typeof s.data === "string" &&
( s.contentType || "" )
.indexOf( "application/x-www-form-urlencoded" ) === 0 &&
rjsonp.test( s.data ) && "data"
);
// Handle iff the expected data type is "jsonp" or we have a parameter to set
if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) {
// Get callback name, remembering preexisting value associated with it
callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ?
s.jsonpCallback() :
s.jsonpCallback;
// Insert callback into url or form data
if ( jsonProp ) {
s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName );
} else if ( s.jsonp !== false ) {
s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName;
}
// Use data converter to retrieve json after script execution
s.converters[ "script json" ] = function() {
if ( !responseContainer ) {
jQuery.error( callbackName + " was not called" );
}
return responseContainer[ 0 ];
};
// force json dataType
s.dataTypes[ 0 ] = "json";
// Install callback
overwritten = window[ callbackName ];
window[ callbackName ] = function() {
responseContainer = arguments;
};
// Clean-up function (fires after converters)
jqXHR.always( function() {
// If previous value didn't exist - remove it
if ( overwritten === undefined ) {
jQuery( window ).removeProp( callbackName );
// Otherwise restore preexisting value
} else {
window[ callbackName ] = overwritten;
}
// Save back as free
if ( s[ callbackName ] ) {
// make sure that re-using the options doesn't screw things around
s.jsonpCallback = originalSettings.jsonpCallback;
// save the callback name for future use
oldCallbacks.push( callbackName );
}
// Call if it was a function and we have a response
if ( responseContainer && jQuery.isFunction( overwritten ) ) {
overwritten( responseContainer[ 0 ] );
}
responseContainer = overwritten = undefined;
} );
// Delegate to script
return "script";
}
} );
// data: string of html
// context (optional): If specified, the fragment will be created in this context,
// defaults to document
// keepScripts (optional): If true, will include scripts passed in the html string
jQuery.parseHTML = function( data, context, keepScripts ) {
if ( !data || typeof data !== "string" ) {
return null;
}
if ( typeof context === "boolean" ) {
keepScripts = context;
context = false;
}
context = context || document;
var parsed = rsingleTag.exec( data ),
scripts = !keepScripts && [];
// Single tag
if ( parsed ) {
return [ context.createElement( parsed[ 1 ] ) ];
}
parsed = buildFragment( [ data ], context, scripts );
if ( scripts && scripts.length ) {
jQuery( scripts ).remove();
}
return jQuery.merge( [], parsed.childNodes );
};
// Keep a copy of the old load method
var _load = jQuery.fn.load;
/**
* Load a url into a page
*/
jQuery.fn.load = function( url, params, callback ) {
if ( typeof url !== "string" && _load ) {
return _load.apply( this, arguments );
}
var selector, type, response,
self = this,
off = url.indexOf( " " );
if ( off > -1 ) {
selector = jQuery.trim( url.slice( off, url.length ) );
url = url.slice( 0, off );
}
// If it's a function
if ( jQuery.isFunction( params ) ) {
// We assume that it's the callback
callback = params;
params = undefined;
// Otherwise, build a param string
} else if ( params && typeof params === "object" ) {
type = "POST";
}
// If we have elements to modify, make the request
if ( self.length > 0 ) {
jQuery.ajax( {
url: url,
// If "type" variable is undefined, then "GET" method will be used.
// Make value of this field explicit since
// user can override it through ajaxSetup method
type: type || "GET",
dataType: "html",
data: params
} ).done( function( responseText ) {
// Save response for use in complete callback
response = arguments;
self.html( selector ?
// If a selector was specified, locate the right elements in a dummy div
// Exclude scripts to avoid IE 'Permission Denied' errors
jQuery( "<div>" ).append( jQuery.parseHTML( responseText ) ).find( selector ) :
// Otherwise use the full result
responseText );
// If the request succeeds, this function gets "data", "status", "jqXHR"
// but they are ignored because response was set above.
// If it fails, this function gets "jqXHR", "status", "error"
} ).always( callback && function( jqXHR, status ) {
self.each( function() {
callback.apply( this, response || [ jqXHR.responseText, status, jqXHR ] );
} );
} );
}
return this;
};
// Attach a bunch of functions for handling common AJAX events
jQuery.each( [
"ajaxStart",
"ajaxStop",
"ajaxComplete",
"ajaxError",
"ajaxSuccess",
"ajaxSend"
], function( i, type ) {
jQuery.fn[ type ] = function( fn ) {
return this.on( type, fn );
};
} );
jQuery.expr.filters.animated = function( elem ) {
return jQuery.grep( jQuery.timers, function( fn ) {
return elem === fn.elem;
} ).length;
};
/**
* Gets a window from an element
*/
function getWindow( elem ) {
return jQuery.isWindow( elem ) ?
elem :
elem.nodeType === 9 ?
elem.defaultView || elem.parentWindow :
false;
}
jQuery.offset = {
setOffset: function( elem, options, i ) {
var curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition,
position = jQuery.css( elem, "position" ),
curElem = jQuery( elem ),
props = {};
// set position first, in-case top/left are set even on static elem
if ( position === "static" ) {
elem.style.position = "relative";
}
curOffset = curElem.offset();
curCSSTop = jQuery.css( elem, "top" );
curCSSLeft = jQuery.css( elem, "left" );
calculatePosition = ( position === "absolute" || position === "fixed" ) &&
jQuery.inArray( "auto", [ curCSSTop, curCSSLeft ] ) > -1;
// need to be able to calculate position if either top or left
// is auto and position is either absolute or fixed
if ( calculatePosition ) {
curPosition = curElem.position();
curTop = curPosition.top;
curLeft = curPosition.left;
} else {
curTop = parseFloat( curCSSTop ) || 0;
curLeft = parseFloat( curCSSLeft ) || 0;
}
if ( jQuery.isFunction( options ) ) {
// Use jQuery.extend here to allow modification of coordinates argument (gh-1848)
options = options.call( elem, i, jQuery.extend( {}, curOffset ) );
}
if ( options.top != null ) {
props.top = ( options.top - curOffset.top ) + curTop;
}
if ( options.left != null ) {
props.left = ( options.left - curOffset.left ) + curLeft;
}
if ( "using" in options ) {
options.using.call( elem, props );
} else {
curElem.css( props );
}
}
};
jQuery.fn.extend( {
offset: function( options ) {
if ( arguments.length ) {
return options === undefined ?
this :
this.each( function( i ) {
jQuery.offset.setOffset( this, options, i );
} );
}
var docElem, win,
box = { top: 0, left: 0 },
elem = this[ 0 ],
doc = elem && elem.ownerDocument;
if ( !doc ) {
return;
}
docElem = doc.documentElement;
// Make sure it's not a disconnected DOM node
if ( !jQuery.contains( docElem, elem ) ) {
return box;
}
// If we don't have gBCR, just use 0,0 rather than error
// BlackBerry 5, iOS 3 (original iPhone)
if ( typeof elem.getBoundingClientRect !== "undefined" ) {
box = elem.getBoundingClientRect();
}
win = getWindow( doc );
return {
top: box.top + ( win.pageYOffset || docElem.scrollTop ) - ( docElem.clientTop || 0 ),
left: box.left + ( win.pageXOffset || docElem.scrollLeft ) - ( docElem.clientLeft || 0 )
};
},
position: function() {
if ( !this[ 0 ] ) {
return;
}
var offsetParent, offset,
parentOffset = { top: 0, left: 0 },
elem = this[ 0 ];
// Fixed elements are offset from window (parentOffset = {top:0, left: 0},
// because it is its only offset parent
if ( jQuery.css( elem, "position" ) === "fixed" ) {
// we assume that getBoundingClientRect is available when computed position is fixed
offset = elem.getBoundingClientRect();
} else {
// Get *real* offsetParent
offsetParent = this.offsetParent();
// Get correct offsets
offset = this.offset();
if ( !jQuery.nodeName( offsetParent[ 0 ], "html" ) ) {
parentOffset = offsetParent.offset();
}
// Add offsetParent borders
parentOffset.top += jQuery.css( offsetParent[ 0 ], "borderTopWidth", true );
parentOffset.left += jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true );
}
// Subtract parent offsets and element margins
// note: when an element has margin: auto the offsetLeft and marginLeft
// are the same in Safari causing offset.left to incorrectly be 0
return {
top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ),
left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true )
};
},
offsetParent: function() {
return this.map( function() {
var offsetParent = this.offsetParent;
while ( offsetParent && ( !jQuery.nodeName( offsetParent, "html" ) &&
jQuery.css( offsetParent, "position" ) === "static" ) ) {
offsetParent = offsetParent.offsetParent;
}
return offsetParent || documentElement;
} );
}
} );
// Create scrollLeft and scrollTop methods
jQuery.each( { scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, function( method, prop ) {
var top = /Y/.test( prop );
jQuery.fn[ method ] = function( val ) {
return access( this, function( elem, method, val ) {
var win = getWindow( elem );
if ( val === undefined ) {
return win ? ( prop in win ) ? win[ prop ] :
win.document.documentElement[ method ] :
elem[ method ];
}
if ( win ) {
win.scrollTo(
!top ? val : jQuery( win ).scrollLeft(),
top ? val : jQuery( win ).scrollTop()
);
} else {
elem[ method ] = val;
}
}, method, val, arguments.length, null );
};
} );
// Support: Safari<7-8+, Chrome<37-44+
// Add the top/left cssHooks using jQuery.fn.position
// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084
// getComputedStyle returns percent when specified for top/left/bottom/right
// rather than make the css module depend on the offset module, we just check for it here
jQuery.each( [ "top", "left" ], function( i, prop ) {
jQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition,
function( elem, computed ) {
if ( computed ) {
computed = curCSS( elem, prop );
// if curCSS returns percentage, fallback to offset
return rnumnonpx.test( computed ) ?
jQuery( elem ).position()[ prop ] + "px" :
computed;
}
}
);
} );
// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods
jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name },
function( defaultExtra, funcName ) {
// margin is only for outerHeight, outerWidth
jQuery.fn[ funcName ] = function( margin, value ) {
var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ),
extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );
return access( this, function( elem, type, value ) {
var doc;
if ( jQuery.isWindow( elem ) ) {
// As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there
// isn't a whole lot we can do. See pull request at this URL for discussion:
// https://github.com/jquery/jquery/pull/764
return elem.document.documentElement[ "client" + name ];
}
// Get document width or height
if ( elem.nodeType === 9 ) {
doc = elem.documentElement;
// Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height],
// whichever is greatest
// unfortunately, this causes bug #3838 in IE6/8 only,
// but there is currently no good, small way to fix it.
return Math.max(
elem.body[ "scroll" + name ], doc[ "scroll" + name ],
elem.body[ "offset" + name ], doc[ "offset" + name ],
doc[ "client" + name ]
);
}
return value === undefined ?
// Get width or height on the element, requesting but not forcing parseFloat
jQuery.css( elem, type, extra ) :
// Set width or height on the element
jQuery.style( elem, type, value, extra );
}, type, chainable ? margin : undefined, chainable, null );
};
} );
} );
jQuery.fn.extend( {
bind: function( types, data, fn ) {
return this.on( types, null, data, fn );
},
unbind: function( types, fn ) {
return this.off( types, null, fn );
},
delegate: function( selector, types, data, fn ) {
return this.on( types, selector, data, fn );
},
undelegate: function( selector, types, fn ) {
// ( namespace ) or ( selector, types [, fn] )
return arguments.length === 1 ?
this.off( selector, "**" ) :
this.off( types, selector || "**", fn );
}
} );
// The number of elements contained in the matched element set
jQuery.fn.size = function() {
return this.length;
};
jQuery.fn.andSelf = jQuery.fn.addBack;
// Register as a named AMD module, since jQuery can be concatenated with other
// files that may use define, but not via a proper concatenation script that
// understands anonymous AMD modules. A named AMD is safest and most robust
// way to register. Lowercase jquery is used because AMD module names are
// derived from file names, and jQuery is normally delivered in a lowercase
// file name. Do this after creating the global so that if an AMD module wants
// to call noConflict to hide this version of jQuery, it will work.
// Note that for maximum portability, libraries that are not jQuery should
// declare themselves as anonymous modules, and avoid setting a global if an
// AMD loader is present. jQuery is a special case. For more information, see
// https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon
if ( typeof define === "function" && define.amd ) {
define( "jquery", [], function() {
return jQuery;
} );
}
var
// Map over jQuery in case of overwrite
_jQuery = window.jQuery,
// Map over the $ in case of overwrite
_$ = window.$;
jQuery.noConflict = function( deep ) {
if ( window.$ === jQuery ) {
window.$ = _$;
}
if ( deep && window.jQuery === jQuery ) {
window.jQuery = _jQuery;
}
return jQuery;
};
// Expose jQuery and $ identifiers, even in
// AMD (#7102#comment:10, https://github.com/jquery/jquery/pull/557)
// and CommonJS for browser emulators (#13566)
if ( !noGlobal ) {
window.jQuery = window.$ = jQuery;
}
return jQuery;
}));
(function($, undefined) {
/**
* Unobtrusive scripting adapter for jQuery
* https://github.com/rails/jquery-ujs
*
* Requires jQuery 1.8.0 or later.
*
* Released under the MIT license
*
*/
// Cut down on the number of issues from people inadvertently including jquery_ujs twice
// by detecting and raising an error when it happens.
'use strict';
if ( $.rails !== undefined ) {
$.error('jquery-ujs has already been loaded!');
}
// Shorthand to make it a little easier to call public rails functions from within rails.js
var rails;
var $document = $(document);
$.rails = rails = {
// Link elements bound by jquery-ujs
linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote]:not([disabled]), a[data-disable-with], a[data-disable]',
// Button elements bound by jquery-ujs
buttonClickSelector: 'button[data-remote]:not([form]):not(form button), button[data-confirm]:not([form]):not(form button)',
// Select elements bound by jquery-ujs
inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]',
// Form elements bound by jquery-ujs
formSubmitSelector: 'form',
// Form input elements bound by jquery-ujs
formInputClickSelector: 'form input[type=submit], form input[type=image], form button[type=submit], form button:not([type]), input[type=submit][form], input[type=image][form], button[type=submit][form], button[form]:not([type])',
// Form input elements disabled during form submission
disableSelector: 'input[data-disable-with]:enabled, button[data-disable-with]:enabled, textarea[data-disable-with]:enabled, input[data-disable]:enabled, button[data-disable]:enabled, textarea[data-disable]:enabled',
// Form input elements re-enabled after form submission
enableSelector: 'input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled, input[data-disable]:disabled, button[data-disable]:disabled, textarea[data-disable]:disabled',
// Form required input elements
requiredInputSelector: 'input[name][required]:not([disabled]), textarea[name][required]:not([disabled])',
// Form file input elements
fileInputSelector: 'input[name][type=file]:not([disabled])',
// Link onClick disable selector with possible reenable after remote submission
linkDisableSelector: 'a[data-disable-with], a[data-disable]',
// Button onClick disable selector with possible reenable after remote submission
buttonDisableSelector: 'button[data-remote][data-disable-with], button[data-remote][data-disable]',
// Up-to-date Cross-Site Request Forgery token
csrfToken: function() {
return $('meta[name=csrf-token]').attr('content');
},
// URL param that must contain the CSRF token
csrfParam: function() {
return $('meta[name=csrf-param]').attr('content');
},
// Make sure that every Ajax request sends the CSRF token
CSRFProtection: function(xhr) {
var token = rails.csrfToken();
if (token) xhr.setRequestHeader('X-CSRF-Token', token);
},
// Make sure that all forms have actual up-to-date tokens (cached forms contain old ones)
refreshCSRFTokens: function(){
$('form input[name="' + rails.csrfParam() + '"]').val(rails.csrfToken());
},
// Triggers an event on an element and returns false if the event result is false
fire: function(obj, name, data) {
var event = $.Event(name);
obj.trigger(event, data);
return event.result !== false;
},
// Default confirm dialog, may be overridden with custom confirm dialog in $.rails.confirm
confirm: function(message) {
return confirm(message);
},
// Default ajax function, may be overridden with custom function in $.rails.ajax
ajax: function(options) {
return $.ajax(options);
},
// Default way to get an element's href. May be overridden at $.rails.href.
href: function(element) {
return element[0].href;
},
// Checks "data-remote" if true to handle the request through a XHR request.
isRemote: function(element) {
return element.data('remote') !== undefined && element.data('remote') !== false;
},
// Submits "remote" forms and links with ajax
handleRemote: function(element) {
var method, url, data, withCredentials, dataType, options;
if (rails.fire(element, 'ajax:before')) {
withCredentials = element.data('with-credentials') || null;
dataType = element.data('type') || ($.ajaxSettings && $.ajaxSettings.dataType);
if (element.is('form')) {
method = element.data('ujs:submit-button-formmethod') || element.attr('method');
url = element.data('ujs:submit-button-formaction') || element.attr('action');
data = $(element[0]).serializeArray();
// memoized value from clicked submit button
var button = element.data('ujs:submit-button');
if (button) {
data.push(button);
element.data('ujs:submit-button', null);
}
element.data('ujs:submit-button-formmethod', null);
element.data('ujs:submit-button-formaction', null);
} else if (element.is(rails.inputChangeSelector)) {
method = element.data('method');
url = element.data('url');
data = element.serialize();
if (element.data('params')) data = data + '&' + element.data('params');
} else if (element.is(rails.buttonClickSelector)) {
method = element.data('method') || 'get';
url = element.data('url');
data = element.serialize();
if (element.data('params')) data = data + '&' + element.data('params');
} else {
method = element.data('method');
url = rails.href(element);
data = element.data('params') || null;
}
options = {
type: method || 'GET', data: data, dataType: dataType,
// stopping the "ajax:beforeSend" event will cancel the ajax request
beforeSend: function(xhr, settings) {
if (settings.dataType === undefined) {
xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script);
}
if (rails.fire(element, 'ajax:beforeSend', [xhr, settings])) {
element.trigger('ajax:send', xhr);
} else {
return false;
}
},
success: function(data, status, xhr) {
element.trigger('ajax:success', [data, status, xhr]);
},
complete: function(xhr, status) {
element.trigger('ajax:complete', [xhr, status]);
},
error: function(xhr, status, error) {
element.trigger('ajax:error', [xhr, status, error]);
},
crossDomain: rails.isCrossDomain(url)
};
// There is no withCredentials for IE6-8 when
// "Enable native XMLHTTP support" is disabled
if (withCredentials) {
options.xhrFields = {
withCredentials: withCredentials
};
}
// Only pass url to `ajax` options if not blank
if (url) { options.url = url; }
return rails.ajax(options);
} else {
return false;
}
},
// Determines if the request is a cross domain request.
isCrossDomain: function(url) {
var originAnchor = document.createElement('a');
originAnchor.href = location.href;
var urlAnchor = document.createElement('a');
try {
urlAnchor.href = url;
// This is a workaround to a IE bug.
urlAnchor.href = urlAnchor.href;
// If URL protocol is false or is a string containing a single colon
// *and* host are false, assume it is not a cross-domain request
// (should only be the case for IE7 and IE compatibility mode).
// Otherwise, evaluate protocol and host of the URL against the origin
// protocol and host.
return !(((!urlAnchor.protocol || urlAnchor.protocol === ':') && !urlAnchor.host) ||
(originAnchor.protocol + '//' + originAnchor.host ===
urlAnchor.protocol + '//' + urlAnchor.host));
} catch (e) {
// If there is an error parsing the URL, assume it is crossDomain.
return true;
}
},
// Handles "data-method" on links such as:
// <a href="/users/5" data-method="delete" rel="nofollow" data-confirm="Are you sure?">Delete</a>
handleMethod: function(link) {
var href = rails.href(link),
method = link.data('method'),
target = link.attr('target'),
csrfToken = rails.csrfToken(),
csrfParam = rails.csrfParam(),
form = $('<form method="post" action="' + href + '"></form>'),
metadataInput = '<input name="_method" value="' + method + '" type="hidden" />';
if (csrfParam !== undefined && csrfToken !== undefined && !rails.isCrossDomain(href)) {
metadataInput += '<input name="' + csrfParam + '" value="' + csrfToken + '" type="hidden" />';
}
if (target) { form.attr('target', target); }
form.hide().append(metadataInput).appendTo('body');
form.submit();
},
// Helper function that returns form elements that match the specified CSS selector
// If form is actually a "form" element this will return associated elements outside the from that have
// the html form attribute set
formElements: function(form, selector) {
return form.is('form') ? $(form[0].elements).filter(selector) : form.find(selector);
},
/* Disables form elements:
- Caches element value in 'ujs:enable-with' data store
- Replaces element text with value of 'data-disable-with' attribute
- Sets disabled property to true
*/
disableFormElements: function(form) {
rails.formElements(form, rails.disableSelector).each(function() {
rails.disableFormElement($(this));
});
},
disableFormElement: function(element) {
var method, replacement;
method = element.is('button') ? 'html' : 'val';
replacement = element.data('disable-with');
if (replacement !== undefined) {
element.data('ujs:enable-with', element[method]());
element[method](replacement);
}
element.prop('disabled', true);
element.data('ujs:disabled', true);
},
/* Re-enables disabled form elements:
- Replaces element text with cached value from 'ujs:enable-with' data store (created in `disableFormElements`)
- Sets disabled property to false
*/
enableFormElements: function(form) {
rails.formElements(form, rails.enableSelector).each(function() {
rails.enableFormElement($(this));
});
},
enableFormElement: function(element) {
var method = element.is('button') ? 'html' : 'val';
if (element.data('ujs:enable-with') !== undefined) {
element[method](element.data('ujs:enable-with'));
element.removeData('ujs:enable-with'); // clean up cache
}
element.prop('disabled', false);
element.removeData('ujs:disabled');
},
/* For 'data-confirm' attribute:
- Fires `confirm` event
- Shows the confirmation dialog
- Fires the `confirm:complete` event
Returns `true` if no function stops the chain and user chose yes; `false` otherwise.
Attaching a handler to the element's `confirm` event that returns a `falsy` value cancels the confirmation dialog.
Attaching a handler to the element's `confirm:complete` event that returns a `falsy` value makes this function
return false. The `confirm:complete` event is fired whether or not the user answered true or false to the dialog.
*/
allowAction: function(element) {
var message = element.data('confirm'),
answer = false, callback;
if (!message) { return true; }
if (rails.fire(element, 'confirm')) {
try {
answer = rails.confirm(message);
} catch (e) {
(console.error || console.log).call(console, e.stack || e);
}
callback = rails.fire(element, 'confirm:complete', [answer]);
}
return answer && callback;
},
// Helper function which checks for blank inputs in a form that match the specified CSS selector
blankInputs: function(form, specifiedSelector, nonBlank) {
var foundInputs = $(),
input,
valueToCheck,
radiosForNameWithNoneSelected,
radioName,
selector = specifiedSelector || 'input,textarea',
requiredInputs = form.find(selector),
checkedRadioButtonNames = {};
requiredInputs.each(function() {
input = $(this);
if (input.is('input[type=radio]')) {
// Don't count unchecked required radio as blank if other radio with same name is checked,
// regardless of whether same-name radio input has required attribute or not. The spec
// states https://www.w3.org/TR/html5/forms.html#the-required-attribute
radioName = input.attr('name');
// Skip if we've already seen the radio with this name.
if (!checkedRadioButtonNames[radioName]) {
// If none checked
if (form.find('input[type=radio]:checked[name="' + radioName + '"]').length === 0) {
radiosForNameWithNoneSelected = form.find(
'input[type=radio][name="' + radioName + '"]');
foundInputs = foundInputs.add(radiosForNameWithNoneSelected);
}
// We only need to check each name once.
checkedRadioButtonNames[radioName] = radioName;
}
} else {
valueToCheck = input.is('input[type=checkbox],input[type=radio]') ? input.is(':checked') : !!input.val();
if (valueToCheck === nonBlank) {
foundInputs = foundInputs.add(input);
}
}
});
return foundInputs.length ? foundInputs : false;
},
// Helper function which checks for non-blank inputs in a form that match the specified CSS selector
nonBlankInputs: function(form, specifiedSelector) {
return rails.blankInputs(form, specifiedSelector, true); // true specifies nonBlank
},
// Helper function, needed to provide consistent behavior in IE
stopEverything: function(e) {
$(e.target).trigger('ujs:everythingStopped');
e.stopImmediatePropagation();
return false;
},
// Replace element's html with the 'data-disable-with' after storing original html
// and prevent clicking on it
disableElement: function(element) {
var replacement = element.data('disable-with');
if (replacement !== undefined) {
element.data('ujs:enable-with', element.html()); // store enabled state
element.html(replacement);
}
element.bind('click.railsDisable', function(e) { // prevent further clicking
return rails.stopEverything(e);
});
element.data('ujs:disabled', true);
},
// Restore element to its original state which was disabled by 'disableElement' above
enableElement: function(element) {
if (element.data('ujs:enable-with') !== undefined) {
element.html(element.data('ujs:enable-with')); // set to old enabled state
element.removeData('ujs:enable-with'); // clean up cache
}
element.unbind('click.railsDisable'); // enable element
element.removeData('ujs:disabled');
}
};
if (rails.fire($document, 'rails:attachBindings')) {
$.ajaxPrefilter(function(options, originalOptions, xhr){ if ( !options.crossDomain ) { rails.CSRFProtection(xhr); }});
// This event works the same as the load event, except that it fires every
// time the page is loaded.
//
// See https://github.com/rails/jquery-ujs/issues/357
// See https://developer.mozilla.org/en-US/docs/Using_Firefox_1.5_caching
$(window).on('pageshow.rails', function () {
$($.rails.enableSelector).each(function () {
var element = $(this);
if (element.data('ujs:disabled')) {
$.rails.enableFormElement(element);
}
});
$($.rails.linkDisableSelector).each(function () {
var element = $(this);
if (element.data('ujs:disabled')) {
$.rails.enableElement(element);
}
});
});
$document.on('ajax:complete', rails.linkDisableSelector, function() {
rails.enableElement($(this));
});
$document.on('ajax:complete', rails.buttonDisableSelector, function() {
rails.enableFormElement($(this));
});
$document.on('click.rails', rails.linkClickSelector, function(e) {
var link = $(this), method = link.data('method'), data = link.data('params'), metaClick = e.metaKey || e.ctrlKey;
if (!rails.allowAction(link)) return rails.stopEverything(e);
if (!metaClick && link.is(rails.linkDisableSelector)) rails.disableElement(link);
if (rails.isRemote(link)) {
if (metaClick && (!method || method === 'GET') && !data) { return true; }
var handleRemote = rails.handleRemote(link);
// Response from rails.handleRemote() will either be false or a deferred object promise.
if (handleRemote === false) {
rails.enableElement(link);
} else {
handleRemote.fail( function() { rails.enableElement(link); } );
}
return false;
} else if (method) {
rails.handleMethod(link);
return false;
}
});
$document.on('click.rails', rails.buttonClickSelector, function(e) {
var button = $(this);
if (!rails.allowAction(button) || !rails.isRemote(button)) return rails.stopEverything(e);
if (button.is(rails.buttonDisableSelector)) rails.disableFormElement(button);
var handleRemote = rails.handleRemote(button);
// Response from rails.handleRemote() will either be false or a deferred object promise.
if (handleRemote === false) {
rails.enableFormElement(button);
} else {
handleRemote.fail( function() { rails.enableFormElement(button); } );
}
return false;
});
$document.on('change.rails', rails.inputChangeSelector, function(e) {
var link = $(this);
if (!rails.allowAction(link) || !rails.isRemote(link)) return rails.stopEverything(e);
rails.handleRemote(link);
return false;
});
$document.on('submit.rails', rails.formSubmitSelector, function(e) {
var form = $(this),
remote = rails.isRemote(form),
blankRequiredInputs,
nonBlankFileInputs;
if (!rails.allowAction(form)) return rails.stopEverything(e);
// Skip other logic when required values are missing or file upload is present
if (form.attr('novalidate') === undefined) {
if (form.data('ujs:formnovalidate-button') === undefined) {
blankRequiredInputs = rails.blankInputs(form, rails.requiredInputSelector, false);
if (blankRequiredInputs && rails.fire(form, 'ajax:aborted:required', [blankRequiredInputs])) {
return rails.stopEverything(e);
}
} else {
// Clear the formnovalidate in case the next button click is not on a formnovalidate button
// Not strictly necessary to do here, since it is also reset on each button click, but just to be certain
form.data('ujs:formnovalidate-button', undefined);
}
}
if (remote) {
nonBlankFileInputs = rails.nonBlankInputs(form, rails.fileInputSelector);
if (nonBlankFileInputs) {
// Slight timeout so that the submit button gets properly serialized
// (make it easy for event handler to serialize form without disabled values)
setTimeout(function(){ rails.disableFormElements(form); }, 13);
var aborted = rails.fire(form, 'ajax:aborted:file', [nonBlankFileInputs]);
// Re-enable form elements if event bindings return false (canceling normal form submission)
if (!aborted) { setTimeout(function(){ rails.enableFormElements(form); }, 13); }
return aborted;
}
rails.handleRemote(form);
return false;
} else {
// Slight timeout so that the submit button gets properly serialized
setTimeout(function(){ rails.disableFormElements(form); }, 13);
}
});
$document.on('click.rails', rails.formInputClickSelector, function(event) {
var button = $(this);
if (!rails.allowAction(button)) return rails.stopEverything(event);
// Register the pressed submit button
var name = button.attr('name'),
data = name ? {name:name, value:button.val()} : null;
var form = button.closest('form');
if (form.length === 0) {
form = $('#' + button.attr('form'));
}
form.data('ujs:submit-button', data);
// Save attributes from button
form.data('ujs:formnovalidate-button', button.attr('formnovalidate'));
form.data('ujs:submit-button-formaction', button.attr('formaction'));
form.data('ujs:submit-button-formmethod', button.attr('formmethod'));
});
$document.on('ajax:send.rails', rails.formSubmitSelector, function(event) {
if (this === event.target) rails.disableFormElements($(this));
});
$document.on('ajax:complete.rails', rails.formSubmitSelector, function(event) {
if (this === event.target) rails.enableFormElements($(this));
});
$(function(){
rails.refreshCSRFTokens();
});
}
})( jQuery );
/**
*
* jquery.sparkline.js
*
* v2.1.1
* (c) Splunk, Inc
* Contact: Gareth Watts (gareth@splunk.com)
* http://omnipotent.net/jquery.sparkline/
*
* Generates inline sparkline charts from data supplied either to the method
* or inline in HTML
*
* Compatible with Internet Explorer 6.0+ and modern browsers equipped with the canvas tag
* (Firefox 2.0+, Safari, Opera, etc)
*
* License: New BSD License
*
* Copyright (c) 2012, Splunk Inc.
* 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 Splunk Inc nor the names of its 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.
*
*
* Usage:
* $(selector).sparkline(values, options)
*
* If values is undefined or set to 'html' then the data values are read from the specified tag:
* <p>Sparkline: <span class="sparkline">1,4,6,6,8,5,3,5</span></p>
* $('.sparkline').sparkline();
* There must be no spaces in the enclosed data set
*
* Otherwise values must be an array of numbers or null values
* <p>Sparkline: <span id="sparkline1">This text replaced if the browser is compatible</span></p>
* $('#sparkline1').sparkline([1,4,6,6,8,5,3,5])
* $('#sparkline2').sparkline([1,4,6,null,null,5,3,5])
*
* Values can also be specified in an HTML comment, or as a values attribute:
* <p>Sparkline: <span class="sparkline"><!--1,4,6,6,8,5,3,5 --></span></p>
* <p>Sparkline: <span class="sparkline" values="1,4,6,6,8,5,3,5"></span></p>
* $('.sparkline').sparkline();
*
* For line charts, x values can also be specified:
* <p>Sparkline: <span class="sparkline">1:1,2.7:4,3.4:6,5:6,6:8,8.7:5,9:3,10:5</span></p>
* $('#sparkline1').sparkline([ [1,1], [2.7,4], [3.4,6], [5,6], [6,8], [8.7,5], [9,3], [10,5] ])
*
* By default, options should be passed in as teh second argument to the sparkline function:
* $('.sparkline').sparkline([1,2,3,4], {type: 'bar'})
*
* Options can also be set by passing them on the tag itself. This feature is disabled by default though
* as there's a slight performance overhead:
* $('.sparkline').sparkline([1,2,3,4], {enableTagOptions: true})
* <p>Sparkline: <span class="sparkline" sparkType="bar" sparkBarColor="red">loading</span></p>
* Prefix all options supplied as tag attribute with "spark" (configurable by setting tagOptionPrefix)
*
* Supported options:
* lineColor - Color of the line used for the chart
* fillColor - Color used to fill in the chart - Set to '' or false for a transparent chart
* width - Width of the chart - Defaults to 3 times the number of values in pixels
* height - Height of the chart - Defaults to the height of the containing element
* chartRangeMin - Specify the minimum value to use for the Y range of the chart - Defaults to the minimum value supplied
* chartRangeMax - Specify the maximum value to use for the Y range of the chart - Defaults to the maximum value supplied
* chartRangeClip - Clip out of range values to the max/min specified by chartRangeMin and chartRangeMax
* chartRangeMinX - Specify the minimum value to use for the X range of the chart - Defaults to the minimum value supplied
* chartRangeMaxX - Specify the maximum value to use for the X range of the chart - Defaults to the maximum value supplied
* composite - If true then don't erase any existing chart attached to the tag, but draw
* another chart over the top - Note that width and height are ignored if an
* existing chart is detected.
* tagValuesAttribute - Name of tag attribute to check for data values - Defaults to 'values'
* enableTagOptions - Whether to check tags for sparkline options
* tagOptionPrefix - Prefix used for options supplied as tag attributes - Defaults to 'spark'
* disableHiddenCheck - If set to true, then the plugin will assume that charts will never be drawn into a
* hidden dom element, avoding a browser reflow
* disableInteraction - If set to true then all mouseover/click interaction behaviour will be disabled,
* making the plugin perform much like it did in 1.x
* disableTooltips - If set to true then tooltips will be disabled - Defaults to false (tooltips enabled)
* disableHighlight - If set to true then highlighting of selected chart elements on mouseover will be disabled
* defaults to false (highlights enabled)
* highlightLighten - Factor to lighten/darken highlighted chart values by - Defaults to 1.4 for a 40% increase
* tooltipContainer - Specify which DOM element the tooltip should be rendered into - defaults to document.body
* tooltipClassname - Optional CSS classname to apply to tooltips - If not specified then a default style will be applied
* tooltipOffsetX - How many pixels away from the mouse pointer to render the tooltip on the X axis
* tooltipOffsetY - How many pixels away from the mouse pointer to render the tooltip on the r axis
* tooltipFormatter - Optional callback that allows you to override the HTML displayed in the tooltip
* callback is given arguments of (sparkline, options, fields)
* tooltipChartTitle - If specified then the tooltip uses the string specified by this setting as a title
* tooltipFormat - A format string or SPFormat object (or an array thereof for multiple entries)
* to control the format of the tooltip
* tooltipPrefix - A string to prepend to each field displayed in a tooltip
* tooltipSuffix - A string to append to each field displayed in a tooltip
* tooltipSkipNull - If true then null values will not have a tooltip displayed (defaults to true)
* tooltipValueLookups - An object or range map to map field values to tooltip strings
* (eg. to map -1 to "Lost", 0 to "Draw", and 1 to "Win")
* numberFormatter - Optional callback for formatting numbers in tooltips
* numberDigitGroupSep - Character to use for group separator in numbers "1,234" - Defaults to ","
* numberDecimalMark - Character to use for the decimal point when formatting numbers - Defaults to "."
* numberDigitGroupCount - Number of digits between group separator - Defaults to 3
*
* There are 7 types of sparkline, selected by supplying a "type" option of 'line' (default),
* 'bar', 'tristate', 'bullet', 'discrete', 'pie' or 'box'
* line - Line chart. Options:
* spotColor - Set to '' to not end each line in a circular spot
* minSpotColor - If set, color of spot at minimum value
* maxSpotColor - If set, color of spot at maximum value
* spotRadius - Radius in pixels
* lineWidth - Width of line in pixels
* normalRangeMin
* normalRangeMax - If set draws a filled horizontal bar between these two values marking the "normal"
* or expected range of values
* normalRangeColor - Color to use for the above bar
* drawNormalOnTop - Draw the normal range above the chart fill color if true
* defaultPixelsPerValue - Defaults to 3 pixels of width for each value in the chart
* highlightSpotColor - The color to use for drawing a highlight spot on mouseover - Set to null to disable
* highlightLineColor - The color to use for drawing a highlight line on mouseover - Set to null to disable
* valueSpots - Specify which points to draw spots on, and in which color. Accepts a range map
*
* bar - Bar chart. Options:
* barColor - Color of bars for postive values
* negBarColor - Color of bars for negative values
* zeroColor - Color of bars with zero values
* nullColor - Color of bars with null values - Defaults to omitting the bar entirely
* barWidth - Width of bars in pixels
* colorMap - Optional mappnig of values to colors to override the *BarColor values above
* can be an Array of values to control the color of individual bars or a range map
* to specify colors for individual ranges of values
* barSpacing - Gap between bars in pixels
* zeroAxis - Centers the y-axis around zero if true
*
* tristate - Charts values of win (>0), lose (<0) or draw (=0)
* posBarColor - Color of win values
* negBarColor - Color of lose values
* zeroBarColor - Color of draw values
* barWidth - Width of bars in pixels
* barSpacing - Gap between bars in pixels
* colorMap - Optional mappnig of values to colors to override the *BarColor values above
* can be an Array of values to control the color of individual bars or a range map
* to specify colors for individual ranges of values
*
* discrete - Options:
* lineHeight - Height of each line in pixels - Defaults to 30% of the graph height
* thesholdValue - Values less than this value will be drawn using thresholdColor instead of lineColor
* thresholdColor
*
* bullet - Values for bullet graphs msut be in the order: target, performance, range1, range2, range3, ...
* options:
* targetColor - The color of the vertical target marker
* targetWidth - The width of the target marker in pixels
* performanceColor - The color of the performance measure horizontal bar
* rangeColors - Colors to use for each qualitative range background color
*
* pie - Pie chart. Options:
* sliceColors - An array of colors to use for pie slices
* offset - Angle in degrees to offset the first slice - Try -90 or +90
* borderWidth - Width of border to draw around the pie chart, in pixels - Defaults to 0 (no border)
* borderColor - Color to use for the pie chart border - Defaults to #000
*
* box - Box plot. Options:
* raw - Set to true to supply pre-computed plot points as values
* values should be: low_outlier, low_whisker, q1, median, q3, high_whisker, high_outlier
* When set to false you can supply any number of values and the box plot will
* be computed for you. Default is false.
* showOutliers - Set to true (default) to display outliers as circles
* outlierIQR - Interquartile range used to determine outliers. Default 1.5
* boxLineColor - Outline color of the box
* boxFillColor - Fill color for the box
* whiskerColor - Line color used for whiskers
* outlierLineColor - Outline color of outlier circles
* outlierFillColor - Fill color of the outlier circles
* spotRadius - Radius of outlier circles
* medianColor - Line color of the median line
* target - Draw a target cross hair at the supplied value (default undefined)
*
*
*
* Examples:
* $('#sparkline1').sparkline(myvalues, { lineColor: '#f00', fillColor: false });
* $('.barsparks').sparkline('html', { type:'bar', height:'40px', barWidth:5 });
* $('#tristate').sparkline([1,1,-1,1,0,0,-1], { type:'tristate' }):
* $('#discrete').sparkline([1,3,4,5,5,3,4,5], { type:'discrete' });
* $('#bullet').sparkline([10,12,12,9,7], { type:'bullet' });
* $('#pie').sparkline([1,1,2], { type:'pie' });
*/
/*jslint regexp: true, browser: true, jquery: true, white: true, nomen: false, plusplus: false, maxerr: 500, indent: 4 */
(function(factory) {
if(typeof define === 'function' && define.amd) {
define(['jquery'], factory);
}
else {
factory(jQuery);
}
}
(function($) {
'use strict';
var UNSET_OPTION = {},
getDefaults, createClass, SPFormat, clipval, quartile, normalizeValue, normalizeValues,
remove, isNumber, all, sum, addCSS, ensureArray, formatNumber, RangeMap,
MouseHandler, Tooltip, barHighlightMixin,
line, bar, tristate, discrete, bullet, pie, box, defaultStyles, initStyles,
VShape, VCanvas_base, VCanvas_canvas, VCanvas_vml, pending, shapeCount = 0;
/**
* Default configuration settings
*/
getDefaults = function () {
return {
// Settings common to most/all chart types
common: {
type: 'line',
lineColor: '#00f',
fillColor: '#cdf',
defaultPixelsPerValue: 3,
width: 'auto',
height: 'auto',
composite: false,
tagValuesAttribute: 'values',
tagOptionsPrefix: 'spark',
enableTagOptions: false,
enableHighlight: true,
highlightLighten: 1.4,
tooltipSkipNull: true,
tooltipPrefix: '',
tooltipSuffix: '',
disableHiddenCheck: false,
numberFormatter: false,
numberDigitGroupCount: 3,
numberDigitGroupSep: ',',
numberDecimalMark: '.',
disableTooltips: false,
disableInteraction: false
},
// Defaults for line charts
line: {
spotColor: '#f80',
highlightSpotColor: '#5f5',
highlightLineColor: '#f22',
spotRadius: 1.5,
minSpotColor: '#f80',
maxSpotColor: '#f80',
lineWidth: 1,
normalRangeMin: undefined,
normalRangeMax: undefined,
normalRangeColor: '#ccc',
drawNormalOnTop: false,
chartRangeMin: undefined,
chartRangeMax: undefined,
chartRangeMinX: undefined,
chartRangeMaxX: undefined,
tooltipFormat: new SPFormat('<span style="color: {{color}}">&#9679;</span> {{prefix}}{{y}}{{suffix}}')
},
// Defaults for bar charts
bar: {
barColor: '#3366cc',
negBarColor: '#f44',
stackedBarColor: ['#3366cc', '#dc3912', '#ff9900', '#109618', '#66aa00',
'#dd4477', '#0099c6', '#990099'],
zeroColor: undefined,
nullColor: undefined,
zeroAxis: true,
barWidth: 4,
barSpacing: 1,
chartRangeMax: undefined,
chartRangeMin: undefined,
chartRangeClip: false,
colorMap: undefined,
tooltipFormat: new SPFormat('<span style="color: {{color}}">&#9679;</span> {{prefix}}{{value}}{{suffix}}')
},
// Defaults for tristate charts
tristate: {
barWidth: 4,
barSpacing: 1,
posBarColor: '#6f6',
negBarColor: '#f44',
zeroBarColor: '#999',
colorMap: {},
tooltipFormat: new SPFormat('<span style="color: {{color}}">&#9679;</span> {{value:map}}'),
tooltipValueLookups: { map: { '-1': 'Loss', '0': 'Draw', '1': 'Win' } }
},
// Defaults for discrete charts
discrete: {
lineHeight: 'auto',
thresholdColor: undefined,
thresholdValue: 0,
chartRangeMax: undefined,
chartRangeMin: undefined,
chartRangeClip: false,
tooltipFormat: new SPFormat('{{prefix}}{{value}}{{suffix}}')
},
// Defaults for bullet charts
bullet: {
targetColor: '#f33',
targetWidth: 3, // width of the target bar in pixels
performanceColor: '#33f',
rangeColors: ['#d3dafe', '#a8b6ff', '#7f94ff'],
base: undefined, // set this to a number to change the base start number
tooltipFormat: new SPFormat('{{fieldkey:fields}} - {{value}}'),
tooltipValueLookups: { fields: {r: 'Range', p: 'Performance', t: 'Target'} }
},
// Defaults for pie charts
pie: {
offset: 0,
sliceColors: ['#3366cc', '#dc3912', '#ff9900', '#109618', '#66aa00',
'#dd4477', '#0099c6', '#990099'],
borderWidth: 0,
borderColor: '#000',
tooltipFormat: new SPFormat('<span style="color: {{color}}">&#9679;</span> {{value}} ({{percent.1}}%)')
},
// Defaults for box plots
box: {
raw: false,
boxLineColor: '#000',
boxFillColor: '#cdf',
whiskerColor: '#000',
outlierLineColor: '#333',
outlierFillColor: '#fff',
medianColor: '#f00',
showOutliers: true,
outlierIQR: 1.5,
spotRadius: 1.5,
target: undefined,
targetColor: '#4a2',
chartRangeMax: undefined,
chartRangeMin: undefined,
tooltipFormat: new SPFormat('{{field:fields}}: {{value}}'),
tooltipFormatFieldlistKey: 'field',
tooltipValueLookups: { fields: { lq: 'Lower Quartile', med: 'Median',
uq: 'Upper Quartile', lo: 'Left Outlier', ro: 'Right Outlier',
lw: 'Left Whisker', rw: 'Right Whisker'} }
}
};
};
// You can have tooltips use a css class other than jqstooltip by specifying tooltipClassname
defaultStyles = '.jqstooltip { ' +
'position: absolute;' +
'left: 0px;' +
'top: 0px;' +
'visibility: hidden;' +
'background: rgb(0, 0, 0) transparent;' +
'background-color: rgba(0,0,0,0.6);' +
'filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000);' +
'-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000)";' +
'color: white;' +
'font: 10px arial, san serif;' +
'text-align: left;' +
'white-space: nowrap;' +
'padding: 5px;' +
'border: 1px solid white;' +
'z-index: 10000;' +
'}' +
'.jqsfield { ' +
'color: white;' +
'font: 10px arial, san serif;' +
'text-align: left;' +
'}';
/**
* Utilities
*/
createClass = function (/* [baseclass, [mixin, ...]], definition */) {
var Class, args;
Class = function () {
this.init.apply(this, arguments);
};
if (arguments.length > 1) {
if (arguments[0]) {
Class.prototype = $.extend(new arguments[0](), arguments[arguments.length - 1]);
Class._super = arguments[0].prototype;
} else {
Class.prototype = arguments[arguments.length - 1];
}
if (arguments.length > 2) {
args = Array.prototype.slice.call(arguments, 1, -1);
args.unshift(Class.prototype);
$.extend.apply($, args);
}
} else {
Class.prototype = arguments[0];
}
Class.prototype.cls = Class;
return Class;
};
/**
* Wraps a format string for tooltips
* {{x}}
* {{x.2}
* {{x:months}}
*/
$.SPFormatClass = SPFormat = createClass({
fre: /\{\{([\w.]+?)(:(.+?))?\}\}/g,
precre: /(\w+)\.(\d+)/,
init: function (format, fclass) {
this.format = format;
this.fclass = fclass;
},
render: function (fieldset, lookups, options) {
var self = this,
fields = fieldset,
match, token, lookupkey, fieldvalue, prec;
return this.format.replace(this.fre, function () {
var lookup;
token = arguments[1];
lookupkey = arguments[3];
match = self.precre.exec(token);
if (match) {
prec = match[2];
token = match[1];
} else {
prec = false;
}
fieldvalue = fields[token];
if (fieldvalue === undefined) {
return '';
}
if (lookupkey && lookups && lookups[lookupkey]) {
lookup = lookups[lookupkey];
if (lookup.get) { // RangeMap
return lookups[lookupkey].get(fieldvalue) || fieldvalue;
} else {
return lookups[lookupkey][fieldvalue] || fieldvalue;
}
}
if (isNumber(fieldvalue)) {
if (options.get('numberFormatter')) {
fieldvalue = options.get('numberFormatter')(fieldvalue);
} else {
fieldvalue = formatNumber(fieldvalue, prec,
options.get('numberDigitGroupCount'),
options.get('numberDigitGroupSep'),
options.get('numberDecimalMark'));
}
}
return fieldvalue;
});
}
});
// convience method to avoid needing the new operator
$.spformat = function(format, fclass) {
return new SPFormat(format, fclass);
};
clipval = function (val, min, max) {
if (val < min) {
return min;
}
if (val > max) {
return max;
}
return val;
};
quartile = function (values, q) {
var vl;
if (q === 2) {
vl = Math.floor(values.length / 2);
return values.length % 2 ? values[vl] : (values[vl-1] + values[vl]) / 2;
} else {
if (values.length % 2 ) { // odd
vl = (values.length * q + q) / 4;
return vl % 1 ? (values[Math.floor(vl)] + values[Math.floor(vl) - 1]) / 2 : values[vl-1];
} else { //even
vl = (values.length * q + 2) / 4;
return vl % 1 ? (values[Math.floor(vl)] + values[Math.floor(vl) - 1]) / 2 : values[vl-1];
}
}
};
normalizeValue = function (val) {
var nf;
switch (val) {
case 'undefined':
val = undefined;
break;
case 'null':
val = null;
break;
case 'true':
val = true;
break;
case 'false':
val = false;
break;
default:
nf = parseFloat(val);
if (val == nf) {
val = nf;
}
}
return val;
};
normalizeValues = function (vals) {
var i, result = [];
for (i = vals.length; i--;) {
result[i] = normalizeValue(vals[i]);
}
return result;
};
remove = function (vals, filter) {
var i, vl, result = [];
for (i = 0, vl = vals.length; i < vl; i++) {
if (vals[i] !== filter) {
result.push(vals[i]);
}
}
return result;
};
isNumber = function (num) {
return !isNaN(parseFloat(num)) && isFinite(num);
};
formatNumber = function (num, prec, groupsize, groupsep, decsep) {
var p, i;
num = (prec === false ? parseFloat(num).toString() : num.toFixed(prec)).split('');
p = (p = $.inArray('.', num)) < 0 ? num.length : p;
if (p < num.length) {
num[p] = decsep;
}
for (i = p - groupsize; i > 0; i -= groupsize) {
num.splice(i, 0, groupsep);
}
return num.join('');
};
// determine if all values of an array match a value
// returns true if the array is empty
all = function (val, arr, ignoreNull) {
var i;
for (i = arr.length; i--; ) {
if (ignoreNull && arr[i] === null) continue;
if (arr[i] !== val) {
return false;
}
}
return true;
};
// sums the numeric values in an array, ignoring other values
sum = function (vals) {
var total = 0, i;
for (i = vals.length; i--;) {
total += typeof vals[i] === 'number' ? vals[i] : 0;
}
return total;
};
ensureArray = function (val) {
return $.isArray(val) ? val : [val];
};
// http://paulirish.com/2008/bookmarklet-inject-new-css-rules/
addCSS = function(css) {
var tag;
//if ('\v' == 'v') /* ie only */ {
if (document.createStyleSheet) {
document.createStyleSheet().cssText = css;
} else {
tag = document.createElement('style');
tag.type = 'text/css';
document.getElementsByTagName('head')[0].appendChild(tag);
tag[(typeof document.body.style.WebkitAppearance == 'string') /* webkit only */ ? 'innerText' : 'innerHTML'] = css;
}
};
// Provide a cross-browser interface to a few simple drawing primitives
$.fn.simpledraw = function (width, height, useExisting, interact) {
var target, mhandler;
if (useExisting && (target = this.data('_jqs_vcanvas'))) {
return target;
}
if (width === undefined) {
width = $(this).innerWidth();
}
if (height === undefined) {
height = $(this).innerHeight();
}
if ($.fn.sparkline.hasCanvas) {
target = new VCanvas_canvas(width, height, this, interact);
} else if ($.fn.sparkline.hasVML) {
target = new VCanvas_vml(width, height, this);
} else {
return false;
}
mhandler = $(this).data('_jqs_mhandler');
if (mhandler) {
mhandler.registerCanvas(target);
}
return target;
};
$.fn.cleardraw = function () {
var target = this.data('_jqs_vcanvas');
if (target) {
target.reset();
}
};
$.RangeMapClass = RangeMap = createClass({
init: function (map) {
var key, range, rangelist = [];
for (key in map) {
if (map.hasOwnProperty(key) && typeof key === 'string' && key.indexOf(':') > -1) {
range = key.split(':');
range[0] = range[0].length === 0 ? -Infinity : parseFloat(range[0]);
range[1] = range[1].length === 0 ? Infinity : parseFloat(range[1]);
range[2] = map[key];
rangelist.push(range);
}
}
this.map = map;
this.rangelist = rangelist || false;
},
get: function (value) {
var rangelist = this.rangelist,
i, range, result;
if ((result = this.map[value]) !== undefined) {
return result;
}
if (rangelist) {
for (i = rangelist.length; i--;) {
range = rangelist[i];
if (range[0] <= value && range[1] >= value) {
return range[2];
}
}
}
return undefined;
}
});
// Convenience function
$.range_map = function(map) {
return new RangeMap(map);
};
MouseHandler = createClass({
init: function (el, options) {
var $el = $(el);
this.$el = $el;
this.options = options;
this.currentPageX = 0;
this.currentPageY = 0;
this.el = el;
this.splist = [];
this.tooltip = null;
this.over = false;
this.displayTooltips = !options.get('disableTooltips');
this.highlightEnabled = !options.get('disableHighlight');
},
registerSparkline: function (sp) {
this.splist.push(sp);
if (this.over) {
this.updateDisplay();
}
},
registerCanvas: function (canvas) {
var $canvas = $(canvas.canvas);
this.canvas = canvas;
this.$canvas = $canvas;
$canvas.mouseenter($.proxy(this.mouseenter, this));
$canvas.mouseleave($.proxy(this.mouseleave, this));
$canvas.click($.proxy(this.mouseclick, this));
},
reset: function (removeTooltip) {
this.splist = [];
if (this.tooltip && removeTooltip) {
this.tooltip.remove();
this.tooltip = undefined;
}
},
mouseclick: function (e) {
var clickEvent = $.Event('sparklineClick');
clickEvent.originalEvent = e;
clickEvent.sparklines = this.splist;
this.$el.trigger(clickEvent);
},
mouseenter: function (e) {
$(document.body).unbind('mousemove.jqs');
$(document.body).bind('mousemove.jqs', $.proxy(this.mousemove, this));
this.over = true;
this.currentPageX = e.pageX;
this.currentPageY = e.pageY;
this.currentEl = e.target;
if (!this.tooltip && this.displayTooltips) {
this.tooltip = new Tooltip(this.options);
this.tooltip.updatePosition(e.pageX, e.pageY);
}
this.updateDisplay();
},
mouseleave: function () {
$(document.body).unbind('mousemove.jqs');
var splist = this.splist,
spcount = splist.length,
needsRefresh = false,
sp, i;
this.over = false;
this.currentEl = null;
if (this.tooltip) {
this.tooltip.remove();
this.tooltip = null;
}
for (i = 0; i < spcount; i++) {
sp = splist[i];
if (sp.clearRegionHighlight()) {
needsRefresh = true;
}
}
if (needsRefresh) {
this.canvas.render();
}
},
mousemove: function (e) {
this.currentPageX = e.pageX;
this.currentPageY = e.pageY;
this.currentEl = e.target;
if (this.tooltip) {
this.tooltip.updatePosition(e.pageX, e.pageY);
}
this.updateDisplay();
},
updateDisplay: function () {
var splist = this.splist,
spcount = splist.length,
needsRefresh = false,
offset = this.$canvas.offset(),
localX = this.currentPageX - offset.left,
localY = this.currentPageY - offset.top,
tooltiphtml, sp, i, result, changeEvent;
if (!this.over) {
return;
}
for (i = 0; i < spcount; i++) {
sp = splist[i];
result = sp.setRegionHighlight(this.currentEl, localX, localY);
if (result) {
needsRefresh = true;
}
}
if (needsRefresh) {
changeEvent = $.Event('sparklineRegionChange');
changeEvent.sparklines = this.splist;
this.$el.trigger(changeEvent);
if (this.tooltip) {
tooltiphtml = '';
for (i = 0; i < spcount; i++) {
sp = splist[i];
tooltiphtml += sp.getCurrentRegionTooltip();
}
this.tooltip.setContent(tooltiphtml);
}
if (!this.disableHighlight) {
this.canvas.render();
}
}
if (result === null) {
this.mouseleave();
}
}
});
Tooltip = createClass({
sizeStyle: 'position: static !important;' +
'display: block !important;' +
'visibility: hidden !important;' +
'float: left !important;',
init: function (options) {
var tooltipClassname = options.get('tooltipClassname', 'jqstooltip'),
sizetipStyle = this.sizeStyle,
offset;
this.container = options.get('tooltipContainer') || document.body;
this.tooltipOffsetX = options.get('tooltipOffsetX', 10);
this.tooltipOffsetY = options.get('tooltipOffsetY', 12);
// remove any previous lingering tooltip
$('#jqssizetip').remove();
$('#jqstooltip').remove();
this.sizetip = $('<div/>', {
id: 'jqssizetip',
style: sizetipStyle,
'class': tooltipClassname
});
this.tooltip = $('<div/>', {
id: 'jqstooltip',
'class': tooltipClassname
}).appendTo(this.container);
// account for the container's location
offset = this.tooltip.offset();
this.offsetLeft = offset.left;
this.offsetTop = offset.top;
this.hidden = true;
$(window).unbind('resize.jqs scroll.jqs');
$(window).bind('resize.jqs scroll.jqs', $.proxy(this.updateWindowDims, this));
this.updateWindowDims();
},
updateWindowDims: function () {
this.scrollTop = $(window).scrollTop();
this.scrollLeft = $(window).scrollLeft();
this.scrollRight = this.scrollLeft + $(window).width();
this.updatePosition();
},
getSize: function (content) {
this.sizetip.html(content).appendTo(this.container);
this.width = this.sizetip.width() + 1;
this.height = this.sizetip.height();
this.sizetip.remove();
},
setContent: function (content) {
if (!content) {
this.tooltip.css('visibility', 'hidden');
this.hidden = true;
return;
}
this.getSize(content);
this.tooltip.html(content)
.css({
'width': this.width,
'height': this.height,
'visibility': 'visible'
});
if (this.hidden) {
this.hidden = false;
this.updatePosition();
}
},
updatePosition: function (x, y) {
if (x === undefined) {
if (this.mousex === undefined) {
return;
}
x = this.mousex - this.offsetLeft;
y = this.mousey - this.offsetTop;
} else {
this.mousex = x = x - this.offsetLeft;
this.mousey = y = y - this.offsetTop;
}
if (!this.height || !this.width || this.hidden) {
return;
}
y -= this.height + this.tooltipOffsetY;
x += this.tooltipOffsetX;
if (y < this.scrollTop) {
y = this.scrollTop;
}
if (x < this.scrollLeft) {
x = this.scrollLeft;
} else if (x + this.width > this.scrollRight) {
x = this.scrollRight - this.width;
}
this.tooltip.css({
'left': x,
'top': y
});
},
remove: function () {
this.tooltip.remove();
this.sizetip.remove();
this.sizetip = this.tooltip = undefined;
$(window).unbind('resize.jqs scroll.jqs');
}
});
initStyles = function() {
addCSS(defaultStyles);
};
$(initStyles);
pending = [];
$.fn.sparkline = function (userValues, userOptions) {
return this.each(function () {
var options = new $.fn.sparkline.options(this, userOptions),
$this = $(this),
render, i;
render = function () {
var values, width, height, tmp, mhandler, sp, vals;
if (userValues === 'html' || userValues === undefined) {
vals = this.getAttribute(options.get('tagValuesAttribute'));
if (vals === undefined || vals === null) {
vals = $this.html();
}
values = vals.replace(/(^\s*<!--)|(-->\s*$)|\s+/g, '').split(',');
} else {
values = userValues;
}
width = options.get('width') === 'auto' ? values.length * options.get('defaultPixelsPerValue') : options.get('width');
if (options.get('height') === 'auto') {
if (!options.get('composite') || !$.data(this, '_jqs_vcanvas')) {
// must be a better way to get the line height
tmp = document.createElement('span');
tmp.innerHTML = 'a';
$this.html(tmp);
height = $(tmp).innerHeight() || $(tmp).height();
$(tmp).remove();
tmp = null;
}
} else {
height = options.get('height');
}
if (!options.get('disableInteraction')) {
mhandler = $.data(this, '_jqs_mhandler');
if (!mhandler) {
mhandler = new MouseHandler(this, options);
$.data(this, '_jqs_mhandler', mhandler);
} else if (!options.get('composite')) {
mhandler.reset();
}
} else {
mhandler = false;
}
if (options.get('composite') && !$.data(this, '_jqs_vcanvas')) {
if (!$.data(this, '_jqs_errnotify')) {
alert('Attempted to attach a composite sparkline to an element with no existing sparkline');
$.data(this, '_jqs_errnotify', true);
}
return;
}
sp = new $.fn.sparkline[options.get('type')](this, values, options, width, height);
sp.render();
if (mhandler) {
mhandler.registerSparkline(sp);
}
};
// jQuery 1.3.0 completely changed the meaning of :hidden :-/
if (($(this).html() && !options.get('disableHiddenCheck') && $(this).is(':hidden')) || ($.fn.jquery < '1.3.0' && $(this).parents().is(':hidden')) || !$(this).parents('body').length) {
if (!options.get('composite') && $.data(this, '_jqs_pending')) {
// remove any existing references to the element
for (i = pending.length; i; i--) {
if (pending[i - 1][0] == this) {
pending.splice(i - 1, 1);
}
}
}
pending.push([this, render]);
$.data(this, '_jqs_pending', true);
} else {
render.call(this);
}
});
};
$.fn.sparkline.defaults = getDefaults();
$.sparkline_display_visible = function () {
var el, i, pl;
var done = [];
for (i = 0, pl = pending.length; i < pl; i++) {
el = pending[i][0];
if ($(el).is(':visible') && !$(el).parents().is(':hidden')) {
pending[i][1].call(el);
$.data(pending[i][0], '_jqs_pending', false);
done.push(i);
} else if (!$(el).closest('html').length && !$.data(el, '_jqs_pending')) {
// element has been inserted and removed from the DOM
// If it was not yet inserted into the dom then the .data request
// will return true.
// removing from the dom causes the data to be removed.
$.data(pending[i][0], '_jqs_pending', false);
done.push(i);
}
}
for (i = done.length; i; i--) {
pending.splice(done[i - 1], 1);
}
};
/**
* User option handler
*/
$.fn.sparkline.options = createClass({
init: function (tag, userOptions) {
var extendedOptions, defaults, base, tagOptionType;
this.userOptions = userOptions = userOptions || {};
this.tag = tag;
this.tagValCache = {};
defaults = $.fn.sparkline.defaults;
base = defaults.common;
this.tagOptionsPrefix = userOptions.enableTagOptions && (userOptions.tagOptionsPrefix || base.tagOptionsPrefix);
tagOptionType = this.getTagSetting('type');
if (tagOptionType === UNSET_OPTION) {
extendedOptions = defaults[userOptions.type || base.type];
} else {
extendedOptions = defaults[tagOptionType];
}
this.mergedOptions = $.extend({}, base, extendedOptions, userOptions);
},
getTagSetting: function (key) {
var prefix = this.tagOptionsPrefix,
val, i, pairs, keyval;
if (prefix === false || prefix === undefined) {
return UNSET_OPTION;
}
if (this.tagValCache.hasOwnProperty(key)) {
val = this.tagValCache.key;
} else {
val = this.tag.getAttribute(prefix + key);
if (val === undefined || val === null) {
val = UNSET_OPTION;
} else if (val.substr(0, 1) === '[') {
val = val.substr(1, val.length - 2).split(',');
for (i = val.length; i--;) {
val[i] = normalizeValue(val[i].replace(/(^\s*)|(\s*$)/g, ''));
}
} else if (val.substr(0, 1) === '{') {
pairs = val.substr(1, val.length - 2).split(',');
val = {};
for (i = pairs.length; i--;) {
keyval = pairs[i].split(':', 2);
val[keyval[0].replace(/(^\s*)|(\s*$)/g, '')] = normalizeValue(keyval[1].replace(/(^\s*)|(\s*$)/g, ''));
}
} else {
val = normalizeValue(val);
}
this.tagValCache.key = val;
}
return val;
},
get: function (key, defaultval) {
var tagOption = this.getTagSetting(key),
result;
if (tagOption !== UNSET_OPTION) {
return tagOption;
}
return (result = this.mergedOptions[key]) === undefined ? defaultval : result;
}
});
$.fn.sparkline._base = createClass({
disabled: false,
init: function (el, values, options, width, height) {
this.el = el;
this.$el = $(el);
this.values = values;
this.options = options;
this.width = width;
this.height = height;
this.currentRegion = undefined;
},
/**
* Setup the canvas
*/
initTarget: function () {
var interactive = !this.options.get('disableInteraction');
if (!(this.target = this.$el.simpledraw(this.width, this.height, this.options.get('composite'), interactive))) {
this.disabled = true;
} else {
this.canvasWidth = this.target.pixelWidth;
this.canvasHeight = this.target.pixelHeight;
}
},
/**
* Actually render the chart to the canvas
*/
render: function () {
if (this.disabled) {
this.el.innerHTML = '';
return false;
}
return true;
},
/**
* Return a region id for a given x/y co-ordinate
*/
getRegion: function (x, y) {
},
/**
* Highlight an item based on the moused-over x,y co-ordinate
*/
setRegionHighlight: function (el, x, y) {
var currentRegion = this.currentRegion,
highlightEnabled = !this.options.get('disableHighlight'),
newRegion;
if (x > this.canvasWidth || y > this.canvasHeight || x < 0 || y < 0) {
return null;
}
newRegion = this.getRegion(el, x, y);
if (currentRegion !== newRegion) {
if (currentRegion !== undefined && highlightEnabled) {
this.removeHighlight();
}
this.currentRegion = newRegion;
if (newRegion !== undefined && highlightEnabled) {
this.renderHighlight();
}
return true;
}
return false;
},
/**
* Reset any currently highlighted item
*/
clearRegionHighlight: function () {
if (this.currentRegion !== undefined) {
this.removeHighlight();
this.currentRegion = undefined;
return true;
}
return false;
},
renderHighlight: function () {
this.changeHighlight(true);
},
removeHighlight: function () {
this.changeHighlight(false);
},
changeHighlight: function (highlight) {},
/**
* Fetch the HTML to display as a tooltip
*/
getCurrentRegionTooltip: function () {
var options = this.options,
header = '',
entries = [],
fields, formats, formatlen, fclass, text, i,
showFields, showFieldsKey, newFields, fv,
formatter, format, fieldlen, j;
if (this.currentRegion === undefined) {
return '';
}
fields = this.getCurrentRegionFields();
formatter = options.get('tooltipFormatter');
if (formatter) {
return formatter(this, options, fields);
}
if (options.get('tooltipChartTitle')) {
header += '<div class="jqs jqstitle">' + options.get('tooltipChartTitle') + '</div>\n';
}
formats = this.options.get('tooltipFormat');
if (!formats) {
return '';
}
if (!$.isArray(formats)) {
formats = [formats];
}
if (!$.isArray(fields)) {
fields = [fields];
}
showFields = this.options.get('tooltipFormatFieldlist');
showFieldsKey = this.options.get('tooltipFormatFieldlistKey');
if (showFields && showFieldsKey) {
// user-selected ordering of fields
newFields = [];
for (i = fields.length; i--;) {
fv = fields[i][showFieldsKey];
if ((j = $.inArray(fv, showFields)) != -1) {
newFields[j] = fields[i];
}
}
fields = newFields;
}
formatlen = formats.length;
fieldlen = fields.length;
for (i = 0; i < formatlen; i++) {
format = formats[i];
if (typeof format === 'string') {
format = new SPFormat(format);
}
fclass = format.fclass || 'jqsfield';
for (j = 0; j < fieldlen; j++) {
if (!fields[j].isNull || !options.get('tooltipSkipNull')) {
$.extend(fields[j], {
prefix: options.get('tooltipPrefix'),
suffix: options.get('tooltipSuffix')
});
text = format.render(fields[j], options.get('tooltipValueLookups'), options);
entries.push('<div class="' + fclass + '">' + text + '</div>');
}
}
}
if (entries.length) {
return header + entries.join('\n');
}
return '';
},
getCurrentRegionFields: function () {},
calcHighlightColor: function (color, options) {
var highlightColor = options.get('highlightColor'),
lighten = options.get('highlightLighten'),
parse, mult, rgbnew, i;
if (highlightColor) {
return highlightColor;
}
if (lighten) {
// extract RGB values
parse = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(color) || /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(color);
if (parse) {
rgbnew = [];
mult = color.length === 4 ? 16 : 1;
for (i = 0; i < 3; i++) {
rgbnew[i] = clipval(Math.round(parseInt(parse[i + 1], 16) * mult * lighten), 0, 255);
}
return 'rgb(' + rgbnew.join(',') + ')';
}
}
return color;
}
});
barHighlightMixin = {
changeHighlight: function (highlight) {
var currentRegion = this.currentRegion,
target = this.target,
shapeids = this.regionShapes[currentRegion],
newShapes;
// will be null if the region value was null
if (shapeids) {
newShapes = this.renderRegion(currentRegion, highlight);
if ($.isArray(newShapes) || $.isArray(shapeids)) {
target.replaceWithShapes(shapeids, newShapes);
this.regionShapes[currentRegion] = $.map(newShapes, function (newShape) {
return newShape.id;
});
} else {
target.replaceWithShape(shapeids, newShapes);
this.regionShapes[currentRegion] = newShapes.id;
}
}
},
render: function () {
var values = this.values,
target = this.target,
regionShapes = this.regionShapes,
shapes, ids, i, j;
if (!this.cls._super.render.call(this)) {
return;
}
for (i = values.length; i--;) {
shapes = this.renderRegion(i);
if (shapes) {
if ($.isArray(shapes)) {
ids = [];
for (j = shapes.length; j--;) {
shapes[j].append();
ids.push(shapes[j].id);
}
regionShapes[i] = ids;
} else {
shapes.append();
regionShapes[i] = shapes.id; // store just the shapeid
}
} else {
// null value
regionShapes[i] = null;
}
}
target.render();
}
};
/**
* Line charts
*/
$.fn.sparkline.line = line = createClass($.fn.sparkline._base, {
type: 'line',
init: function (el, values, options, width, height) {
line._super.init.call(this, el, values, options, width, height);
this.vertices = [];
this.regionMap = [];
this.xvalues = [];
this.yvalues = [];
this.yminmax = [];
this.hightlightSpotId = null;
this.lastShapeId = null;
this.initTarget();
},
getRegion: function (el, x, y) {
var i,
regionMap = this.regionMap; // maps regions to value positions
for (i = regionMap.length; i--;) {
if (regionMap[i] !== null && x >= regionMap[i][0] && x <= regionMap[i][1]) {
return regionMap[i][2];
}
}
return undefined;
},
getCurrentRegionFields: function () {
var currentRegion = this.currentRegion;
return {
isNull: this.yvalues[currentRegion] === null,
x: this.xvalues[currentRegion],
y: this.yvalues[currentRegion],
color: this.options.get('lineColor'),
fillColor: this.options.get('fillColor'),
offset: currentRegion
};
},
renderHighlight: function () {
var currentRegion = this.currentRegion,
target = this.target,
vertex = this.vertices[currentRegion],
options = this.options,
spotRadius = options.get('spotRadius'),
highlightSpotColor = options.get('highlightSpotColor'),
highlightLineColor = options.get('highlightLineColor'),
highlightSpot, highlightLine;
if (!vertex) {
return;
}
if (spotRadius && highlightSpotColor) {
highlightSpot = target.drawCircle(vertex[0], vertex[1],
spotRadius, undefined, highlightSpotColor);
this.highlightSpotId = highlightSpot.id;
target.insertAfterShape(this.lastShapeId, highlightSpot);
}
if (highlightLineColor) {
highlightLine = target.drawLine(vertex[0], this.canvasTop, vertex[0],
this.canvasTop + this.canvasHeight, highlightLineColor);
this.highlightLineId = highlightLine.id;
target.insertAfterShape(this.lastShapeId, highlightLine);
}
},
removeHighlight: function () {
var target = this.target;
if (this.highlightSpotId) {
target.removeShapeId(this.highlightSpotId);
this.highlightSpotId = null;
}
if (this.highlightLineId) {
target.removeShapeId(this.highlightLineId);
this.highlightLineId = null;
}
},
scanValues: function () {
var values = this.values,
valcount = values.length,
xvalues = this.xvalues,
yvalues = this.yvalues,
yminmax = this.yminmax,
i, val, isStr, isArray, sp;
for (i = 0; i < valcount; i++) {
val = values[i];
isStr = typeof(values[i]) === 'string';
isArray = typeof(values[i]) === 'object' && values[i] instanceof Array;
sp = isStr && values[i].split(':');
if (isStr && sp.length === 2) { // x:y
xvalues.push(Number(sp[0]));
yvalues.push(Number(sp[1]));
yminmax.push(Number(sp[1]));
} else if (isArray) {
xvalues.push(val[0]);
yvalues.push(val[1]);
yminmax.push(val[1]);
} else {
xvalues.push(i);
if (values[i] === null || values[i] === 'null') {
yvalues.push(null);
} else {
yvalues.push(Number(val));
yminmax.push(Number(val));
}
}
}
if (this.options.get('xvalues')) {
xvalues = this.options.get('xvalues');
}
this.maxy = this.maxyorg = Math.max.apply(Math, yminmax);
this.miny = this.minyorg = Math.min.apply(Math, yminmax);
this.maxx = Math.max.apply(Math, xvalues);
this.minx = Math.min.apply(Math, xvalues);
this.xvalues = xvalues;
this.yvalues = yvalues;
this.yminmax = yminmax;
},
processRangeOptions: function () {
var options = this.options,
normalRangeMin = options.get('normalRangeMin'),
normalRangeMax = options.get('normalRangeMax');
if (normalRangeMin !== undefined) {
if (normalRangeMin < this.miny) {
this.miny = normalRangeMin;
}
if (normalRangeMax > this.maxy) {
this.maxy = normalRangeMax;
}
}
if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < this.miny)) {
this.miny = options.get('chartRangeMin');
}
if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > this.maxy)) {
this.maxy = options.get('chartRangeMax');
}
if (options.get('chartRangeMinX') !== undefined && (options.get('chartRangeClipX') || options.get('chartRangeMinX') < this.minx)) {
this.minx = options.get('chartRangeMinX');
}
if (options.get('chartRangeMaxX') !== undefined && (options.get('chartRangeClipX') || options.get('chartRangeMaxX') > this.maxx)) {
this.maxx = options.get('chartRangeMaxX');
}
},
drawNormalRange: function (canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey) {
var normalRangeMin = this.options.get('normalRangeMin'),
normalRangeMax = this.options.get('normalRangeMax'),
ytop = canvasTop + Math.round(canvasHeight - (canvasHeight * ((normalRangeMax - this.miny) / rangey))),
height = Math.round((canvasHeight * (normalRangeMax - normalRangeMin)) / rangey);
this.target.drawRect(canvasLeft, ytop, canvasWidth, height, undefined, this.options.get('normalRangeColor')).append();
},
render: function () {
var options = this.options,
target = this.target,
canvasWidth = this.canvasWidth,
canvasHeight = this.canvasHeight,
vertices = this.vertices,
spotRadius = options.get('spotRadius'),
regionMap = this.regionMap,
rangex, rangey, yvallast,
canvasTop, canvasLeft,
vertex, path, paths, x, y, xnext, xpos, xposnext,
last, next, yvalcount, lineShapes, fillShapes, plen,
valueSpots, hlSpotsEnabled, color, xvalues, yvalues, i;
if (!line._super.render.call(this)) {
return;
}
this.scanValues();
this.processRangeOptions();
xvalues = this.xvalues;
yvalues = this.yvalues;
if (!this.yminmax.length || this.yvalues.length < 2) {
// empty or all null valuess
return;
}
canvasTop = canvasLeft = 0;
rangex = this.maxx - this.minx === 0 ? 1 : this.maxx - this.minx;
rangey = this.maxy - this.miny === 0 ? 1 : this.maxy - this.miny;
yvallast = this.yvalues.length - 1;
if (spotRadius && (canvasWidth < (spotRadius * 4) || canvasHeight < (spotRadius * 4))) {
spotRadius = 0;
}
if (spotRadius) {
// adjust the canvas size as required so that spots will fit
hlSpotsEnabled = options.get('highlightSpotColor') && !options.get('disableInteraction');
if (hlSpotsEnabled || options.get('minSpotColor') || (options.get('spotColor') && yvalues[yvallast] === this.miny)) {
canvasHeight -= Math.ceil(spotRadius);
}
if (hlSpotsEnabled || options.get('maxSpotColor') || (options.get('spotColor') && yvalues[yvallast] === this.maxy)) {
canvasHeight -= Math.ceil(spotRadius);
canvasTop += Math.ceil(spotRadius);
}
if (hlSpotsEnabled ||
((options.get('minSpotColor') || options.get('maxSpotColor')) && (yvalues[0] === this.miny || yvalues[0] === this.maxy))) {
canvasLeft += Math.ceil(spotRadius);
canvasWidth -= Math.ceil(spotRadius);
}
if (hlSpotsEnabled || options.get('spotColor') ||
(options.get('minSpotColor') || options.get('maxSpotColor') &&
(yvalues[yvallast] === this.miny || yvalues[yvallast] === this.maxy))) {
canvasWidth -= Math.ceil(spotRadius);
}
}
canvasHeight--;
if (options.get('normalRangeMin') !== undefined && !options.get('drawNormalOnTop')) {
this.drawNormalRange(canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey);
}
path = [];
paths = [path];
last = next = null;
yvalcount = yvalues.length;
for (i = 0; i < yvalcount; i++) {
x = xvalues[i];
xnext = xvalues[i + 1];
y = yvalues[i];
xpos = canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex));
xposnext = i < yvalcount - 1 ? canvasLeft + Math.round((xnext - this.minx) * (canvasWidth / rangex)) : canvasWidth;
next = xpos + ((xposnext - xpos) / 2);
regionMap[i] = [last || 0, next, i];
last = next;
if (y === null) {
if (i) {
if (yvalues[i - 1] !== null) {
path = [];
paths.push(path);
}
vertices.push(null);
}
} else {
if (y < this.miny) {
y = this.miny;
}
if (y > this.maxy) {
y = this.maxy;
}
if (!path.length) {
// previous value was null
path.push([xpos, canvasTop + canvasHeight]);
}
vertex = [xpos, canvasTop + Math.round(canvasHeight - (canvasHeight * ((y - this.miny) / rangey)))];
path.push(vertex);
vertices.push(vertex);
}
}
lineShapes = [];
fillShapes = [];
plen = paths.length;
for (i = 0; i < plen; i++) {
path = paths[i];
if (path.length) {
if (options.get('fillColor')) {
path.push([path[path.length - 1][0], (canvasTop + canvasHeight)]);
fillShapes.push(path.slice(0));
path.pop();
}
// if there's only a single point in this path, then we want to display it
// as a vertical line which means we keep path[0] as is
if (path.length > 2) {
// else we want the first value
path[0] = [path[0][0], path[1][1]];
}
lineShapes.push(path);
}
}
// draw the fill first, then optionally the normal range, then the line on top of that
plen = fillShapes.length;
for (i = 0; i < plen; i++) {
target.drawShape(fillShapes[i],
options.get('fillColor'), options.get('fillColor')).append();
}
if (options.get('normalRangeMin') !== undefined && options.get('drawNormalOnTop')) {
this.drawNormalRange(canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey);
}
plen = lineShapes.length;
for (i = 0; i < plen; i++) {
target.drawShape(lineShapes[i], options.get('lineColor'), undefined,
options.get('lineWidth')).append();
}
if (spotRadius && options.get('valueSpots')) {
valueSpots = options.get('valueSpots');
if (valueSpots.get === undefined) {
valueSpots = new RangeMap(valueSpots);
}
for (i = 0; i < yvalcount; i++) {
color = valueSpots.get(yvalues[i]);
if (color) {
target.drawCircle(canvasLeft + Math.round((xvalues[i] - this.minx) * (canvasWidth / rangex)),
canvasTop + Math.round(canvasHeight - (canvasHeight * ((yvalues[i] - this.miny) / rangey))),
spotRadius, undefined,
color).append();
}
}
}
if (spotRadius && options.get('spotColor') && yvalues[yvallast] !== null) {
target.drawCircle(canvasLeft + Math.round((xvalues[xvalues.length - 1] - this.minx) * (canvasWidth / rangex)),
canvasTop + Math.round(canvasHeight - (canvasHeight * ((yvalues[yvallast] - this.miny) / rangey))),
spotRadius, undefined,
options.get('spotColor')).append();
}
if (this.maxy !== this.minyorg) {
if (spotRadius && options.get('minSpotColor')) {
x = xvalues[$.inArray(this.minyorg, yvalues)];
target.drawCircle(canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)),
canvasTop + Math.round(canvasHeight - (canvasHeight * ((this.minyorg - this.miny) / rangey))),
spotRadius, undefined,
options.get('minSpotColor')).append();
}
if (spotRadius && options.get('maxSpotColor')) {
x = xvalues[$.inArray(this.maxyorg, yvalues)];
target.drawCircle(canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)),
canvasTop + Math.round(canvasHeight - (canvasHeight * ((this.maxyorg - this.miny) / rangey))),
spotRadius, undefined,
options.get('maxSpotColor')).append();
}
}
this.lastShapeId = target.getLastShapeId();
this.canvasTop = canvasTop;
target.render();
}
});
/**
* Bar charts
*/
$.fn.sparkline.bar = bar = createClass($.fn.sparkline._base, barHighlightMixin, {
type: 'bar',
init: function (el, values, options, width, height) {
var barWidth = parseInt(options.get('barWidth'), 10),
barSpacing = parseInt(options.get('barSpacing'), 10),
chartRangeMin = options.get('chartRangeMin'),
chartRangeMax = options.get('chartRangeMax'),
chartRangeClip = options.get('chartRangeClip'),
stackMin = Infinity,
stackMax = -Infinity,
isStackString, groupMin, groupMax, stackRanges,
numValues, i, vlen, range, zeroAxis, xaxisOffset, min, max, clipMin, clipMax,
stacked, vlist, j, slen, svals, val, yoffset, yMaxCalc, canvasHeightEf;
bar._super.init.call(this, el, values, options, width, height);
// scan values to determine whether to stack bars
for (i = 0, vlen = values.length; i < vlen; i++) {
val = values[i];
isStackString = typeof(val) === 'string' && val.indexOf(':') > -1;
if (isStackString || $.isArray(val)) {
stacked = true;
if (isStackString) {
val = values[i] = normalizeValues(val.split(':'));
}
val = remove(val, null); // min/max will treat null as zero
groupMin = Math.min.apply(Math, val);
groupMax = Math.max.apply(Math, val);
if (groupMin < stackMin) {
stackMin = groupMin;
}
if (groupMax > stackMax) {
stackMax = groupMax;
}
}
}
this.stacked = stacked;
this.regionShapes = {};
this.barWidth = barWidth;
this.barSpacing = barSpacing;
this.totalBarWidth = barWidth + barSpacing;
this.width = width = (values.length * barWidth) + ((values.length - 1) * barSpacing);
this.initTarget();
if (chartRangeClip) {
clipMin = chartRangeMin === undefined ? -Infinity : chartRangeMin;
clipMax = chartRangeMax === undefined ? Infinity : chartRangeMax;
}
numValues = [];
stackRanges = stacked ? [] : numValues;
var stackTotals = [];
var stackRangesNeg = [];
for (i = 0, vlen = values.length; i < vlen; i++) {
if (stacked) {
vlist = values[i];
values[i] = svals = [];
stackTotals[i] = 0;
stackRanges[i] = stackRangesNeg[i] = 0;
for (j = 0, slen = vlist.length; j < slen; j++) {
val = svals[j] = chartRangeClip ? clipval(vlist[j], clipMin, clipMax) : vlist[j];
if (val !== null) {
if (val > 0) {
stackTotals[i] += val;
}
if (stackMin < 0 && stackMax > 0) {
if (val < 0) {
stackRangesNeg[i] += Math.abs(val);
} else {
stackRanges[i] += val;
}
} else {
stackRanges[i] += Math.abs(val - (val < 0 ? stackMax : stackMin));
}
numValues.push(val);
}
}
} else {
val = chartRangeClip ? clipval(values[i], clipMin, clipMax) : values[i];
val = values[i] = normalizeValue(val);
if (val !== null) {
numValues.push(val);
}
}
}
this.max = max = Math.max.apply(Math, numValues);
this.min = min = Math.min.apply(Math, numValues);
this.stackMax = stackMax = stacked ? Math.max.apply(Math, stackTotals) : max;
this.stackMin = stackMin = stacked ? Math.min.apply(Math, numValues) : min;
if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < min)) {
min = options.get('chartRangeMin');
}
if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > max)) {
max = options.get('chartRangeMax');
}
this.zeroAxis = zeroAxis = options.get('zeroAxis', true);
if (min <= 0 && max >= 0 && zeroAxis) {
xaxisOffset = 0;
} else if (zeroAxis == false) {
xaxisOffset = min;
} else if (min > 0) {
xaxisOffset = min;
} else {
xaxisOffset = max;
}
this.xaxisOffset = xaxisOffset;
range = stacked ? (Math.max.apply(Math, stackRanges) + Math.max.apply(Math, stackRangesNeg)) : max - min;
// as we plot zero/min values a single pixel line, we add a pixel to all other
// values - Reduce the effective canvas size to suit
this.canvasHeightEf = (zeroAxis && min < 0) ? this.canvasHeight - 2 : this.canvasHeight - 1;
if (min < xaxisOffset) {
yMaxCalc = (stacked && max >= 0) ? stackMax : max;
yoffset = (yMaxCalc - xaxisOffset) / range * this.canvasHeight;
if (yoffset !== Math.ceil(yoffset)) {
this.canvasHeightEf -= 2;
yoffset = Math.ceil(yoffset);
}
} else {
yoffset = this.canvasHeight;
}
this.yoffset = yoffset;
if ($.isArray(options.get('colorMap'))) {
this.colorMapByIndex = options.get('colorMap');
this.colorMapByValue = null;
} else {
this.colorMapByIndex = null;
this.colorMapByValue = options.get('colorMap');
if (this.colorMapByValue && this.colorMapByValue.get === undefined) {
this.colorMapByValue = new RangeMap(this.colorMapByValue);
}
}
this.range = range;
},
getRegion: function (el, x, y) {
var result = Math.floor(x / this.totalBarWidth);
return (result < 0 || result >= this.values.length) ? undefined : result;
},
getCurrentRegionFields: function () {
var currentRegion = this.currentRegion,
values = ensureArray(this.values[currentRegion]),
result = [],
value, i;
for (i = values.length; i--;) {
value = values[i];
result.push({
isNull: value === null,
value: value,
color: this.calcColor(i, value, currentRegion),
offset: currentRegion
});
}
return result;
},
calcColor: function (stacknum, value, valuenum) {
var colorMapByIndex = this.colorMapByIndex,
colorMapByValue = this.colorMapByValue,
options = this.options,
color, newColor;
if (this.stacked) {
color = options.get('stackedBarColor');
} else {
color = (value < 0) ? options.get('negBarColor') : options.get('barColor');
}
if (value === 0 && options.get('zeroColor') !== undefined) {
color = options.get('zeroColor');
}
if (colorMapByValue && (newColor = colorMapByValue.get(value))) {
color = newColor;
} else if (colorMapByIndex && colorMapByIndex.length > valuenum) {
color = colorMapByIndex[valuenum];
}
return $.isArray(color) ? color[stacknum % color.length] : color;
},
/**
* Render bar(s) for a region
*/
renderRegion: function (valuenum, highlight) {
var vals = this.values[valuenum],
options = this.options,
xaxisOffset = this.xaxisOffset,
result = [],
range = this.range,
stacked = this.stacked,
target = this.target,
x = valuenum * this.totalBarWidth,
canvasHeightEf = this.canvasHeightEf,
yoffset = this.yoffset,
y, height, color, isNull, yoffsetNeg, i, valcount, val, minPlotted, allMin;
vals = $.isArray(vals) ? vals : [vals];
valcount = vals.length;
val = vals[0];
isNull = all(null, vals);
allMin = all(xaxisOffset, vals, true);
if (isNull) {
if (options.get('nullColor')) {
color = highlight ? options.get('nullColor') : this.calcHighlightColor(options.get('nullColor'), options);
y = (yoffset > 0) ? yoffset - 1 : yoffset;
return target.drawRect(x, y, this.barWidth - 1, 0, color, color);
} else {
return undefined;
}
}
yoffsetNeg = yoffset;
for (i = 0; i < valcount; i++) {
val = vals[i];
if (stacked && val === xaxisOffset) {
if (!allMin || minPlotted) {
continue;
}
minPlotted = true;
}
if (range > 0) {
height = Math.floor(canvasHeightEf * ((Math.abs(val - xaxisOffset) / range))) + 1;
} else {
height = 1;
}
if (val < xaxisOffset || (val === xaxisOffset && yoffset === 0)) {
y = yoffsetNeg;
yoffsetNeg += height;
} else {
y = yoffset - height;
yoffset -= height;
}
color = this.calcColor(i, val, valuenum);
if (highlight) {
color = this.calcHighlightColor(color, options);
}
result.push(target.drawRect(x, y, this.barWidth - 1, height - 1, color, color));
}
if (result.length === 1) {
return result[0];
}
return result;
}
});
/**
* Tristate charts
*/
$.fn.sparkline.tristate = tristate = createClass($.fn.sparkline._base, barHighlightMixin, {
type: 'tristate',
init: function (el, values, options, width, height) {
var barWidth = parseInt(options.get('barWidth'), 10),
barSpacing = parseInt(options.get('barSpacing'), 10);
tristate._super.init.call(this, el, values, options, width, height);
this.regionShapes = {};
this.barWidth = barWidth;
this.barSpacing = barSpacing;
this.totalBarWidth = barWidth + barSpacing;
this.values = $.map(values, Number);
this.width = width = (values.length * barWidth) + ((values.length - 1) * barSpacing);
if ($.isArray(options.get('colorMap'))) {
this.colorMapByIndex = options.get('colorMap');
this.colorMapByValue = null;
} else {
this.colorMapByIndex = null;
this.colorMapByValue = options.get('colorMap');
if (this.colorMapByValue && this.colorMapByValue.get === undefined) {
this.colorMapByValue = new RangeMap(this.colorMapByValue);
}
}
this.initTarget();
},
getRegion: function (el, x, y) {
return Math.floor(x / this.totalBarWidth);
},
getCurrentRegionFields: function () {
var currentRegion = this.currentRegion;
return {
isNull: this.values[currentRegion] === undefined,
value: this.values[currentRegion],
color: this.calcColor(this.values[currentRegion], currentRegion),
offset: currentRegion
};
},
calcColor: function (value, valuenum) {
var values = this.values,
options = this.options,
colorMapByIndex = this.colorMapByIndex,
colorMapByValue = this.colorMapByValue,
color, newColor;
if (colorMapByValue && (newColor = colorMapByValue.get(value))) {
color = newColor;
} else if (colorMapByIndex && colorMapByIndex.length > valuenum) {
color = colorMapByIndex[valuenum];
} else if (values[valuenum] < 0) {
color = options.get('negBarColor');
} else if (values[valuenum] > 0) {
color = options.get('posBarColor');
} else {
color = options.get('zeroBarColor');
}
return color;
},
renderRegion: function (valuenum, highlight) {
var values = this.values,
options = this.options,
target = this.target,
canvasHeight, height, halfHeight,
x, y, color;
canvasHeight = target.pixelHeight;
halfHeight = Math.round(canvasHeight / 2);
x = valuenum * this.totalBarWidth;
if (values[valuenum] < 0) {
y = halfHeight;
height = halfHeight - 1;
} else if (values[valuenum] > 0) {
y = 0;
height = halfHeight - 1;
} else {
y = halfHeight - 1;
height = 2;
}
color = this.calcColor(values[valuenum], valuenum);
if (color === null) {
return;
}
if (highlight) {
color = this.calcHighlightColor(color, options);
}
return target.drawRect(x, y, this.barWidth - 1, height - 1, color, color);
}
});
/**
* Discrete charts
*/
$.fn.sparkline.discrete = discrete = createClass($.fn.sparkline._base, barHighlightMixin, {
type: 'discrete',
init: function (el, values, options, width, height) {
discrete._super.init.call(this, el, values, options, width, height);
this.regionShapes = {};
this.values = values = $.map(values, Number);
this.min = Math.min.apply(Math, values);
this.max = Math.max.apply(Math, values);
this.range = this.max - this.min;
this.width = width = options.get('width') === 'auto' ? values.length * 2 : this.width;
this.interval = Math.floor(width / values.length);
this.itemWidth = width / values.length;
if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < this.min)) {
this.min = options.get('chartRangeMin');
}
if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > this.max)) {
this.max = options.get('chartRangeMax');
}
this.initTarget();
if (this.target) {
this.lineHeight = options.get('lineHeight') === 'auto' ? Math.round(this.canvasHeight * 0.3) : options.get('lineHeight');
}
},
getRegion: function (el, x, y) {
return Math.floor(x / this.itemWidth);
},
getCurrentRegionFields: function () {
var currentRegion = this.currentRegion;
return {
isNull: this.values[currentRegion] === undefined,
value: this.values[currentRegion],
offset: currentRegion
};
},
renderRegion: function (valuenum, highlight) {
var values = this.values,
options = this.options,
min = this.min,
max = this.max,
range = this.range,
interval = this.interval,
target = this.target,
canvasHeight = this.canvasHeight,
lineHeight = this.lineHeight,
pheight = canvasHeight - lineHeight,
ytop, val, color, x;
val = clipval(values[valuenum], min, max);
x = valuenum * interval;
ytop = Math.round(pheight - pheight * ((val - min) / range));
color = (options.get('thresholdColor') && val < options.get('thresholdValue')) ? options.get('thresholdColor') : options.get('lineColor');
if (highlight) {
color = this.calcHighlightColor(color, options);
}
return target.drawLine(x, ytop, x, ytop + lineHeight, color);
}
});
/**
* Bullet charts
*/
$.fn.sparkline.bullet = bullet = createClass($.fn.sparkline._base, {
type: 'bullet',
init: function (el, values, options, width, height) {
var min, max, vals;
bullet._super.init.call(this, el, values, options, width, height);
// values: target, performance, range1, range2, range3
this.values = values = normalizeValues(values);
// target or performance could be null
vals = values.slice();
vals[0] = vals[0] === null ? vals[2] : vals[0];
vals[1] = values[1] === null ? vals[2] : vals[1];
min = Math.min.apply(Math, values);
max = Math.max.apply(Math, values);
if (options.get('base') === undefined) {
min = min < 0 ? min : 0;
} else {
min = options.get('base');
}
this.min = min;
this.max = max;
this.range = max - min;
this.shapes = {};
this.valueShapes = {};
this.regiondata = {};
this.width = width = options.get('width') === 'auto' ? '4.0em' : width;
this.target = this.$el.simpledraw(width, height, options.get('composite'));
if (!values.length) {
this.disabled = true;
}
this.initTarget();
},
getRegion: function (el, x, y) {
var shapeid = this.target.getShapeAt(el, x, y);
return (shapeid !== undefined && this.shapes[shapeid] !== undefined) ? this.shapes[shapeid] : undefined;
},
getCurrentRegionFields: function () {
var currentRegion = this.currentRegion;
return {
fieldkey: currentRegion.substr(0, 1),
value: this.values[currentRegion.substr(1)],
region: currentRegion
};
},
changeHighlight: function (highlight) {
var currentRegion = this.currentRegion,
shapeid = this.valueShapes[currentRegion],
shape;
delete this.shapes[shapeid];
switch (currentRegion.substr(0, 1)) {
case 'r':
shape = this.renderRange(currentRegion.substr(1), highlight);
break;
case 'p':
shape = this.renderPerformance(highlight);
break;
case 't':
shape = this.renderTarget(highlight);
break;
}
this.valueShapes[currentRegion] = shape.id;
this.shapes[shape.id] = currentRegion;
this.target.replaceWithShape(shapeid, shape);
},
renderRange: function (rn, highlight) {
var rangeval = this.values[rn],
rangewidth = Math.round(this.canvasWidth * ((rangeval - this.min) / this.range)),
color = this.options.get('rangeColors')[rn - 2];
if (highlight) {
color = this.calcHighlightColor(color, this.options);
}
return this.target.drawRect(0, 0, rangewidth - 1, this.canvasHeight - 1, color, color);
},
renderPerformance: function (highlight) {
var perfval = this.values[1],
perfwidth = Math.round(this.canvasWidth * ((perfval - this.min) / this.range)),
color = this.options.get('performanceColor');
if (highlight) {
color = this.calcHighlightColor(color, this.options);
}
return this.target.drawRect(0, Math.round(this.canvasHeight * 0.3), perfwidth - 1,
Math.round(this.canvasHeight * 0.4) - 1, color, color);
},
renderTarget: function (highlight) {
var targetval = this.values[0],
x = Math.round(this.canvasWidth * ((targetval - this.min) / this.range) - (this.options.get('targetWidth') / 2)),
targettop = Math.round(this.canvasHeight * 0.10),
targetheight = this.canvasHeight - (targettop * 2),
color = this.options.get('targetColor');
if (highlight) {
color = this.calcHighlightColor(color, this.options);
}
return this.target.drawRect(x, targettop, this.options.get('targetWidth') - 1, targetheight - 1, color, color);
},
render: function () {
var vlen = this.values.length,
target = this.target,
i, shape;
if (!bullet._super.render.call(this)) {
return;
}
for (i = 2; i < vlen; i++) {
shape = this.renderRange(i).append();
this.shapes[shape.id] = 'r' + i;
this.valueShapes['r' + i] = shape.id;
}
if (this.values[1] !== null) {
shape = this.renderPerformance().append();
this.shapes[shape.id] = 'p1';
this.valueShapes.p1 = shape.id;
}
if (this.values[0] !== null) {
shape = this.renderTarget().append();
this.shapes[shape.id] = 't0';
this.valueShapes.t0 = shape.id;
}
target.render();
}
});
/**
* Pie charts
*/
$.fn.sparkline.pie = pie = createClass($.fn.sparkline._base, {
type: 'pie',
init: function (el, values, options, width, height) {
var total = 0, i;
pie._super.init.call(this, el, values, options, width, height);
this.shapes = {}; // map shape ids to value offsets
this.valueShapes = {}; // maps value offsets to shape ids
this.values = values = $.map(values, Number);
if (options.get('width') === 'auto') {
this.width = this.height;
}
if (values.length > 0) {
for (i = values.length; i--;) {
total += values[i];
}
}
this.total = total;
this.initTarget();
this.radius = Math.floor(Math.min(this.canvasWidth, this.canvasHeight) / 2);
},
getRegion: function (el, x, y) {
var shapeid = this.target.getShapeAt(el, x, y);
return (shapeid !== undefined && this.shapes[shapeid] !== undefined) ? this.shapes[shapeid] : undefined;
},
getCurrentRegionFields: function () {
var currentRegion = this.currentRegion;
return {
isNull: this.values[currentRegion] === undefined,
value: this.values[currentRegion],
percent: this.values[currentRegion] / this.total * 100,
color: this.options.get('sliceColors')[currentRegion % this.options.get('sliceColors').length],
offset: currentRegion
};
},
changeHighlight: function (highlight) {
var currentRegion = this.currentRegion,
newslice = this.renderSlice(currentRegion, highlight),
shapeid = this.valueShapes[currentRegion];
delete this.shapes[shapeid];
this.target.replaceWithShape(shapeid, newslice);
this.valueShapes[currentRegion] = newslice.id;
this.shapes[newslice.id] = currentRegion;
},
renderSlice: function (valuenum, highlight) {
var target = this.target,
options = this.options,
radius = this.radius,
borderWidth = options.get('borderWidth'),
offset = options.get('offset'),
circle = 2 * Math.PI,
values = this.values,
total = this.total,
next = offset ? (2*Math.PI)*(offset/360) : 0,
start, end, i, vlen, color;
vlen = values.length;
for (i = 0; i < vlen; i++) {
start = next;
end = next;
if (total > 0) { // avoid divide by zero
end = next + (circle * (values[i] / total));
}
if (valuenum === i) {
color = options.get('sliceColors')[i % options.get('sliceColors').length];
if (highlight) {
color = this.calcHighlightColor(color, options);
}
return target.drawPieSlice(radius, radius, radius - borderWidth, start, end, undefined, color);
}
next = end;
}
},
render: function () {
var target = this.target,
values = this.values,
options = this.options,
radius = this.radius,
borderWidth = options.get('borderWidth'),
shape, i;
if (!pie._super.render.call(this)) {
return;
}
if (borderWidth) {
target.drawCircle(radius, radius, Math.floor(radius - (borderWidth / 2)),
options.get('borderColor'), undefined, borderWidth).append();
}
for (i = values.length; i--;) {
if (values[i]) { // don't render zero values
shape = this.renderSlice(i).append();
this.valueShapes[i] = shape.id; // store just the shapeid
this.shapes[shape.id] = i;
}
}
target.render();
}
});
/**
* Box plots
*/
$.fn.sparkline.box = box = createClass($.fn.sparkline._base, {
type: 'box',
init: function (el, values, options, width, height) {
box._super.init.call(this, el, values, options, width, height);
this.values = $.map(values, Number);
this.width = options.get('width') === 'auto' ? '4.0em' : width;
this.initTarget();
if (!this.values.length) {
this.disabled = 1;
}
},
/**
* Simulate a single region
*/
getRegion: function () {
return 1;
},
getCurrentRegionFields: function () {
var result = [
{ field: 'lq', value: this.quartiles[0] },
{ field: 'med', value: this.quartiles[1] },
{ field: 'uq', value: this.quartiles[2] }
];
if (this.loutlier !== undefined) {
result.push({ field: 'lo', value: this.loutlier});
}
if (this.routlier !== undefined) {
result.push({ field: 'ro', value: this.routlier});
}
if (this.lwhisker !== undefined) {
result.push({ field: 'lw', value: this.lwhisker});
}
if (this.rwhisker !== undefined) {
result.push({ field: 'rw', value: this.rwhisker});
}
return result;
},
render: function () {
var target = this.target,
values = this.values,
vlen = values.length,
options = this.options,
canvasWidth = this.canvasWidth,
canvasHeight = this.canvasHeight,
minValue = options.get('chartRangeMin') === undefined ? Math.min.apply(Math, values) : options.get('chartRangeMin'),
maxValue = options.get('chartRangeMax') === undefined ? Math.max.apply(Math, values) : options.get('chartRangeMax'),
canvasLeft = 0,
lwhisker, loutlier, iqr, q1, q2, q3, rwhisker, routlier, i,
size, unitSize;
if (!box._super.render.call(this)) {
return;
}
if (options.get('raw')) {
if (options.get('showOutliers') && values.length > 5) {
loutlier = values[0];
lwhisker = values[1];
q1 = values[2];
q2 = values[3];
q3 = values[4];
rwhisker = values[5];
routlier = values[6];
} else {
lwhisker = values[0];
q1 = values[1];
q2 = values[2];
q3 = values[3];
rwhisker = values[4];
}
} else {
values.sort(function (a, b) { return a - b; });
q1 = quartile(values, 1);
q2 = quartile(values, 2);
q3 = quartile(values, 3);
iqr = q3 - q1;
if (options.get('showOutliers')) {
lwhisker = rwhisker = undefined;
for (i = 0; i < vlen; i++) {
if (lwhisker === undefined && values[i] > q1 - (iqr * options.get('outlierIQR'))) {
lwhisker = values[i];
}
if (values[i] < q3 + (iqr * options.get('outlierIQR'))) {
rwhisker = values[i];
}
}
loutlier = values[0];
routlier = values[vlen - 1];
} else {
lwhisker = values[0];
rwhisker = values[vlen - 1];
}
}
this.quartiles = [q1, q2, q3];
this.lwhisker = lwhisker;
this.rwhisker = rwhisker;
this.loutlier = loutlier;
this.routlier = routlier;
unitSize = canvasWidth / (maxValue - minValue + 1);
if (options.get('showOutliers')) {
canvasLeft = Math.ceil(options.get('spotRadius'));
canvasWidth -= 2 * Math.ceil(options.get('spotRadius'));
unitSize = canvasWidth / (maxValue - minValue + 1);
if (loutlier < lwhisker) {
target.drawCircle((loutlier - minValue) * unitSize + canvasLeft,
canvasHeight / 2,
options.get('spotRadius'),
options.get('outlierLineColor'),
options.get('outlierFillColor')).append();
}
if (routlier > rwhisker) {
target.drawCircle((routlier - minValue) * unitSize + canvasLeft,
canvasHeight / 2,
options.get('spotRadius'),
options.get('outlierLineColor'),
options.get('outlierFillColor')).append();
}
}
// box
target.drawRect(
Math.round((q1 - minValue) * unitSize + canvasLeft),
Math.round(canvasHeight * 0.1),
Math.round((q3 - q1) * unitSize),
Math.round(canvasHeight * 0.8),
options.get('boxLineColor'),
options.get('boxFillColor')).append();
// left whisker
target.drawLine(
Math.round((lwhisker - minValue) * unitSize + canvasLeft),
Math.round(canvasHeight / 2),
Math.round((q1 - minValue) * unitSize + canvasLeft),
Math.round(canvasHeight / 2),
options.get('lineColor')).append();
target.drawLine(
Math.round((lwhisker - minValue) * unitSize + canvasLeft),
Math.round(canvasHeight / 4),
Math.round((lwhisker - minValue) * unitSize + canvasLeft),
Math.round(canvasHeight - canvasHeight / 4),
options.get('whiskerColor')).append();
// right whisker
target.drawLine(Math.round((rwhisker - minValue) * unitSize + canvasLeft),
Math.round(canvasHeight / 2),
Math.round((q3 - minValue) * unitSize + canvasLeft),
Math.round(canvasHeight / 2),
options.get('lineColor')).append();
target.drawLine(
Math.round((rwhisker - minValue) * unitSize + canvasLeft),
Math.round(canvasHeight / 4),
Math.round((rwhisker - minValue) * unitSize + canvasLeft),
Math.round(canvasHeight - canvasHeight / 4),
options.get('whiskerColor')).append();
// median line
target.drawLine(
Math.round((q2 - minValue) * unitSize + canvasLeft),
Math.round(canvasHeight * 0.1),
Math.round((q2 - minValue) * unitSize + canvasLeft),
Math.round(canvasHeight * 0.9),
options.get('medianColor')).append();
if (options.get('target')) {
size = Math.ceil(options.get('spotRadius'));
target.drawLine(
Math.round((options.get('target') - minValue) * unitSize + canvasLeft),
Math.round((canvasHeight / 2) - size),
Math.round((options.get('target') - minValue) * unitSize + canvasLeft),
Math.round((canvasHeight / 2) + size),
options.get('targetColor')).append();
target.drawLine(
Math.round((options.get('target') - minValue) * unitSize + canvasLeft - size),
Math.round(canvasHeight / 2),
Math.round((options.get('target') - minValue) * unitSize + canvasLeft + size),
Math.round(canvasHeight / 2),
options.get('targetColor')).append();
}
target.render();
}
});
// Setup a very simple "virtual canvas" to make drawing the few shapes we need easier
// This is accessible as $(foo).simpledraw()
// Detect browser renderer support
(function() {
if (document.namespaces && !document.namespaces.v) {
$.fn.sparkline.hasVML = true;
document.namespaces.add('v', 'urn:schemas-microsoft-com:vml', '#default#VML');
} else {
$.fn.sparkline.hasVML = false;
}
var el = document.createElement('canvas');
$.fn.sparkline.hasCanvas = !!(el.getContext && el.getContext('2d'));
})()
VShape = createClass({
init: function (target, id, type, args) {
this.target = target;
this.id = id;
this.type = type;
this.args = args;
},
append: function () {
this.target.appendShape(this);
return this;
}
});
VCanvas_base = createClass({
_pxregex: /(\d+)(px)?\s*$/i,
init: function (width, height, target) {
if (!width) {
return;
}
this.width = width;
this.height = height;
this.target = target;
this.lastShapeId = null;
if (target[0]) {
target = target[0];
}
$.data(target, '_jqs_vcanvas', this);
},
drawLine: function (x1, y1, x2, y2, lineColor, lineWidth) {
return this.drawShape([[x1, y1], [x2, y2]], lineColor, lineWidth);
},
drawShape: function (path, lineColor, fillColor, lineWidth) {
return this._genShape('Shape', [path, lineColor, fillColor, lineWidth]);
},
drawCircle: function (x, y, radius, lineColor, fillColor, lineWidth) {
return this._genShape('Circle', [x, y, radius, lineColor, fillColor, lineWidth]);
},
drawPieSlice: function (x, y, radius, startAngle, endAngle, lineColor, fillColor) {
return this._genShape('PieSlice', [x, y, radius, startAngle, endAngle, lineColor, fillColor]);
},
drawRect: function (x, y, width, height, lineColor, fillColor) {
return this._genShape('Rect', [x, y, width, height, lineColor, fillColor]);
},
getElement: function () {
return this.canvas;
},
/**
* Return the most recently inserted shape id
*/
getLastShapeId: function () {
return this.lastShapeId;
},
/**
* Clear and reset the canvas
*/
reset: function () {
alert('reset not implemented');
},
_insert: function (el, target) {
$(target).html(el);
},
/**
* Calculate the pixel dimensions of the canvas
*/
_calculatePixelDims: function (width, height, canvas) {
// XXX This should probably be a configurable option
var match;
match = this._pxregex.exec(height);
if (match) {
this.pixelHeight = match[1];
} else {
this.pixelHeight = $(canvas).height();
}
match = this._pxregex.exec(width);
if (match) {
this.pixelWidth = match[1];
} else {
this.pixelWidth = $(canvas).width();
}
},
/**
* Generate a shape object and id for later rendering
*/
_genShape: function (shapetype, shapeargs) {
var id = shapeCount++;
shapeargs.unshift(id);
return new VShape(this, id, shapetype, shapeargs);
},
/**
* Add a shape to the end of the render queue
*/
appendShape: function (shape) {
alert('appendShape not implemented');
},
/**
* Replace one shape with another
*/
replaceWithShape: function (shapeid, shape) {
alert('replaceWithShape not implemented');
},
/**
* Insert one shape after another in the render queue
*/
insertAfterShape: function (shapeid, shape) {
alert('insertAfterShape not implemented');
},
/**
* Remove a shape from the queue
*/
removeShapeId: function (shapeid) {
alert('removeShapeId not implemented');
},
/**
* Find a shape at the specified x/y co-ordinates
*/
getShapeAt: function (el, x, y) {
alert('getShapeAt not implemented');
},
/**
* Render all queued shapes onto the canvas
*/
render: function () {
alert('render not implemented');
}
});
VCanvas_canvas = createClass(VCanvas_base, {
init: function (width, height, target, interact) {
VCanvas_canvas._super.init.call(this, width, height, target);
this.canvas = document.createElement('canvas');
if (target[0]) {
target = target[0];
}
$.data(target, '_jqs_vcanvas', this);
$(this.canvas).css({ display: 'inline-block', width: width, height: height, verticalAlign: 'top' });
this._insert(this.canvas, target);
this._calculatePixelDims(width, height, this.canvas);
this.canvas.width = this.pixelWidth;
this.canvas.height = this.pixelHeight;
this.interact = interact;
this.shapes = {};
this.shapeseq = [];
this.currentTargetShapeId = undefined;
$(this.canvas).css({width: this.pixelWidth, height: this.pixelHeight});
},
_getContext: function (lineColor, fillColor, lineWidth) {
var context = this.canvas.getContext('2d');
if (lineColor !== undefined) {
context.strokeStyle = lineColor;
}
context.lineWidth = lineWidth === undefined ? 1 : lineWidth;
if (fillColor !== undefined) {
context.fillStyle = fillColor;
}
return context;
},
reset: function () {
var context = this._getContext();
context.clearRect(0, 0, this.pixelWidth, this.pixelHeight);
this.shapes = {};
this.shapeseq = [];
this.currentTargetShapeId = undefined;
},
_drawShape: function (shapeid, path, lineColor, fillColor, lineWidth) {
var context = this._getContext(lineColor, fillColor, lineWidth),
i, plen;
context.beginPath();
context.moveTo(path[0][0] + 0.5, path[0][1] + 0.5);
for (i = 1, plen = path.length; i < plen; i++) {
context.lineTo(path[i][0] + 0.5, path[i][1] + 0.5); // the 0.5 offset gives us crisp pixel-width lines
}
if (lineColor !== undefined) {
context.stroke();
}
if (fillColor !== undefined) {
context.fill();
}
if (this.targetX !== undefined && this.targetY !== undefined &&
context.isPointInPath(this.targetX, this.targetY)) {
this.currentTargetShapeId = shapeid;
}
},
_drawCircle: function (shapeid, x, y, radius, lineColor, fillColor, lineWidth) {
var context = this._getContext(lineColor, fillColor, lineWidth);
context.beginPath();
context.arc(x, y, radius, 0, 2 * Math.PI, false);
if (this.targetX !== undefined && this.targetY !== undefined &&
context.isPointInPath(this.targetX, this.targetY)) {
this.currentTargetShapeId = shapeid;
}
if (lineColor !== undefined) {
context.stroke();
}
if (fillColor !== undefined) {
context.fill();
}
},
_drawPieSlice: function (shapeid, x, y, radius, startAngle, endAngle, lineColor, fillColor) {
var context = this._getContext(lineColor, fillColor);
context.beginPath();
context.moveTo(x, y);
context.arc(x, y, radius, startAngle, endAngle, false);
context.lineTo(x, y);
context.closePath();
if (lineColor !== undefined) {
context.stroke();
}
if (fillColor) {
context.fill();
}
if (this.targetX !== undefined && this.targetY !== undefined &&
context.isPointInPath(this.targetX, this.targetY)) {
this.currentTargetShapeId = shapeid;
}
},
_drawRect: function (shapeid, x, y, width, height, lineColor, fillColor) {
return this._drawShape(shapeid, [[x, y], [x + width, y], [x + width, y + height], [x, y + height], [x, y]], lineColor, fillColor);
},
appendShape: function (shape) {
this.shapes[shape.id] = shape;
this.shapeseq.push(shape.id);
this.lastShapeId = shape.id;
return shape.id;
},
replaceWithShape: function (shapeid, shape) {
var shapeseq = this.shapeseq,
i;
this.shapes[shape.id] = shape;
for (i = shapeseq.length; i--;) {
if (shapeseq[i] == shapeid) {
shapeseq[i] = shape.id;
}
}
delete this.shapes[shapeid];
},
replaceWithShapes: function (shapeids, shapes) {
var shapeseq = this.shapeseq,
shapemap = {},
sid, i, first;
for (i = shapeids.length; i--;) {
shapemap[shapeids[i]] = true;
}
for (i = shapeseq.length; i--;) {
sid = shapeseq[i];
if (shapemap[sid]) {
shapeseq.splice(i, 1);
delete this.shapes[sid];
first = i;
}
}
for (i = shapes.length; i--;) {
shapeseq.splice(first, 0, shapes[i].id);
this.shapes[shapes[i].id] = shapes[i];
}
},
insertAfterShape: function (shapeid, shape) {
var shapeseq = this.shapeseq,
i;
for (i = shapeseq.length; i--;) {
if (shapeseq[i] === shapeid) {
shapeseq.splice(i + 1, 0, shape.id);
this.shapes[shape.id] = shape;
return;
}
}
},
removeShapeId: function (shapeid) {
var shapeseq = this.shapeseq,
i;
for (i = shapeseq.length; i--;) {
if (shapeseq[i] === shapeid) {
shapeseq.splice(i, 1);
break;
}
}
delete this.shapes[shapeid];
},
getShapeAt: function (el, x, y) {
this.targetX = x;
this.targetY = y;
this.render();
return this.currentTargetShapeId;
},
render: function () {
var shapeseq = this.shapeseq,
shapes = this.shapes,
shapeCount = shapeseq.length,
context = this._getContext(),
shapeid, shape, i;
context.clearRect(0, 0, this.pixelWidth, this.pixelHeight);
for (i = 0; i < shapeCount; i++) {
shapeid = shapeseq[i];
shape = shapes[shapeid];
this['_draw' + shape.type].apply(this, shape.args);
}
if (!this.interact) {
// not interactive so no need to keep the shapes array
this.shapes = {};
this.shapeseq = [];
}
}
});
VCanvas_vml = createClass(VCanvas_base, {
init: function (width, height, target) {
var groupel;
VCanvas_vml._super.init.call(this, width, height, target);
if (target[0]) {
target = target[0];
}
$.data(target, '_jqs_vcanvas', this);
this.canvas = document.createElement('span');
$(this.canvas).css({ display: 'inline-block', position: 'relative', overflow: 'hidden', width: width, height: height, margin: '0px', padding: '0px', verticalAlign: 'top'});
this._insert(this.canvas, target);
this._calculatePixelDims(width, height, this.canvas);
this.canvas.width = this.pixelWidth;
this.canvas.height = this.pixelHeight;
groupel = '<v:group coordorigin="0 0" coordsize="' + this.pixelWidth + ' ' + this.pixelHeight + '"' +
' style="position:absolute;top:0;left:0;width:' + this.pixelWidth + 'px;height=' + this.pixelHeight + 'px;"></v:group>';
this.canvas.insertAdjacentHTML('beforeEnd', groupel);
this.group = $(this.canvas).children()[0];
this.rendered = false;
this.prerender = '';
},
_drawShape: function (shapeid, path, lineColor, fillColor, lineWidth) {
var vpath = [],
initial, stroke, fill, closed, vel, plen, i;
for (i = 0, plen = path.length; i < plen; i++) {
vpath[i] = '' + (path[i][0]) + ',' + (path[i][1]);
}
initial = vpath.splice(0, 1);
lineWidth = lineWidth === undefined ? 1 : lineWidth;
stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="' + lineWidth + 'px" strokeColor="' + lineColor + '" ';
fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
closed = vpath[0] === vpath[vpath.length - 1] ? 'x ' : '';
vel = '<v:shape coordorigin="0 0" coordsize="' + this.pixelWidth + ' ' + this.pixelHeight + '" ' +
' id="jqsshape' + shapeid + '" ' +
stroke +
fill +
' style="position:absolute;left:0px;top:0px;height:' + this.pixelHeight + 'px;width:' + this.pixelWidth + 'px;padding:0px;margin:0px;" ' +
' path="m ' + initial + ' l ' + vpath.join(', ') + ' ' + closed + 'e">' +
' </v:shape>';
return vel;
},
_drawCircle: function (shapeid, x, y, radius, lineColor, fillColor, lineWidth) {
var stroke, fill, vel;
x -= radius;
y -= radius;
stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="' + lineWidth + 'px" strokeColor="' + lineColor + '" ';
fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
vel = '<v:oval ' +
' id="jqsshape' + shapeid + '" ' +
stroke +
fill +
' style="position:absolute;top:' + y + 'px; left:' + x + 'px; width:' + (radius * 2) + 'px; height:' + (radius * 2) + 'px"></v:oval>';
return vel;
},
_drawPieSlice: function (shapeid, x, y, radius, startAngle, endAngle, lineColor, fillColor) {
var vpath, startx, starty, endx, endy, stroke, fill, vel;
if (startAngle === endAngle) {
return ''; // VML seems to have problem when start angle equals end angle.
}
if ((endAngle - startAngle) === (2 * Math.PI)) {
startAngle = 0.0; // VML seems to have a problem when drawing a full circle that doesn't start 0
endAngle = (2 * Math.PI);
}
startx = x + Math.round(Math.cos(startAngle) * radius);
starty = y + Math.round(Math.sin(startAngle) * radius);
endx = x + Math.round(Math.cos(endAngle) * radius);
endy = y + Math.round(Math.sin(endAngle) * radius);
if (startx === endx && starty === endy) {
if ((endAngle - startAngle) < Math.PI) {
// Prevent very small slices from being mistaken as a whole pie
return '';
}
// essentially going to be the entire circle, so ignore startAngle
startx = endx = x + radius;
starty = endy = y;
}
if (startx === endx && starty === endy && (endAngle - startAngle) < Math.PI) {
return '';
}
vpath = [x - radius, y - radius, x + radius, y + radius, startx, starty, endx, endy];
stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="1px" strokeColor="' + lineColor + '" ';
fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
vel = '<v:shape coordorigin="0 0" coordsize="' + this.pixelWidth + ' ' + this.pixelHeight + '" ' +
' id="jqsshape' + shapeid + '" ' +
stroke +
fill +
' style="position:absolute;left:0px;top:0px;height:' + this.pixelHeight + 'px;width:' + this.pixelWidth + 'px;padding:0px;margin:0px;" ' +
' path="m ' + x + ',' + y + ' wa ' + vpath.join(', ') + ' x e">' +
' </v:shape>';
return vel;
},
_drawRect: function (shapeid, x, y, width, height, lineColor, fillColor) {
return this._drawShape(shapeid, [[x, y], [x, y + height], [x + width, y + height], [x + width, y], [x, y]], lineColor, fillColor);
},
reset: function () {
this.group.innerHTML = '';
},
appendShape: function (shape) {
var vel = this['_draw' + shape.type].apply(this, shape.args);
if (this.rendered) {
this.group.insertAdjacentHTML('beforeEnd', vel);
} else {
this.prerender += vel;
}
this.lastShapeId = shape.id;
return shape.id;
},
replaceWithShape: function (shapeid, shape) {
var existing = $('#jqsshape' + shapeid),
vel = this['_draw' + shape.type].apply(this, shape.args);
existing[0].outerHTML = vel;
},
replaceWithShapes: function (shapeids, shapes) {
// replace the first shapeid with all the new shapes then toast the remaining old shapes
var existing = $('#jqsshape' + shapeids[0]),
replace = '',
slen = shapes.length,
i;
for (i = 0; i < slen; i++) {
replace += this['_draw' + shapes[i].type].apply(this, shapes[i].args);
}
existing[0].outerHTML = replace;
for (i = 1; i < shapeids.length; i++) {
$('#jqsshape' + shapeids[i]).remove();
}
},
insertAfterShape: function (shapeid, shape) {
var existing = $('#jqsshape' + shapeid),
vel = this['_draw' + shape.type].apply(this, shape.args);
existing[0].insertAdjacentHTML('afterEnd', vel);
},
removeShapeId: function (shapeid) {
var existing = $('#jqsshape' + shapeid);
this.group.removeChild(existing[0]);
},
getShapeAt: function (el, x, y) {
var shapeid = el.id.substr(8);
return shapeid;
},
render: function () {
if (!this.rendered) {
// batch the intial render into a single repaint
this.group.innerHTML = this.prerender;
this.rendered = true;
}
}
});
}));
( function( factory ) {
if ( typeof define === "function" && define.amd ) {
// AMD. Register as an anonymous module.
define( [ "jquery" ], factory );
} else {
// Browser globals
factory( jQuery );
}
} ( function( $ ) {
$.ui = $.ui || {};
return $.ui.version = "1.12.1";
} ) );
/*!
* jQuery UI Keycode 1.12.1
* http://jqueryui.com
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*/
//>>label: Keycode
//>>group: Core
//>>description: Provide keycodes as keynames
//>>docs: http://api.jqueryui.com/jQuery.ui.keyCode/
( function( factory ) {
if ( typeof define === "function" && define.amd ) {
// AMD. Register as an anonymous module.
define( [ "jquery", "./version" ], factory );
} else {
// Browser globals
factory( jQuery );
}
} ( function( $ ) {
return $.ui.keyCode = {
BACKSPACE: 8,
COMMA: 188,
DELETE: 46,
DOWN: 40,
END: 35,
ENTER: 13,
ESCAPE: 27,
HOME: 36,
LEFT: 37,
PAGE_DOWN: 34,
PAGE_UP: 33,
PERIOD: 190,
RIGHT: 39,
SPACE: 32,
TAB: 9,
UP: 38
};
} ) );
/*!
* jQuery UI Position 1.12.1
* http://jqueryui.com
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*
* http://api.jqueryui.com/position/
*/
//>>label: Position
//>>group: Core
//>>description: Positions elements relative to other elements.
//>>docs: http://api.jqueryui.com/position/
//>>demos: http://jqueryui.com/position/
( function( factory ) {
if ( typeof define === "function" && define.amd ) {
// AMD. Register as an anonymous module.
define( [ "jquery", "./version" ], factory );
} else {
// Browser globals
factory( jQuery );
}
}( function( $ ) {
( function() {
var cachedScrollbarWidth,
max = Math.max,
abs = Math.abs,
rhorizontal = /left|center|right/,
rvertical = /top|center|bottom/,
roffset = /[\+\-]\d+(\.[\d]+)?%?/,
rposition = /^\w+/,
rpercent = /%$/,
_position = $.fn.position;
function getOffsets( offsets, width, height ) {
return [
parseFloat( offsets[ 0 ] ) * ( rpercent.test( offsets[ 0 ] ) ? width / 100 : 1 ),
parseFloat( offsets[ 1 ] ) * ( rpercent.test( offsets[ 1 ] ) ? height / 100 : 1 )
];
}
function parseCss( element, property ) {
return parseInt( $.css( element, property ), 10 ) || 0;
}
function getDimensions( elem ) {
var raw = elem[ 0 ];
if ( raw.nodeType === 9 ) {
return {
width: elem.width(),
height: elem.height(),
offset: { top: 0, left: 0 }
};
}
if ( $.isWindow( raw ) ) {
return {
width: elem.width(),
height: elem.height(),
offset: { top: elem.scrollTop(), left: elem.scrollLeft() }
};
}
if ( raw.preventDefault ) {
return {
width: 0,
height: 0,
offset: { top: raw.pageY, left: raw.pageX }
};
}
return {
width: elem.outerWidth(),
height: elem.outerHeight(),
offset: elem.offset()
};
}
$.position = {
scrollbarWidth: function() {
if ( cachedScrollbarWidth !== undefined ) {
return cachedScrollbarWidth;
}
var w1, w2,
div = $( "<div " +
"style='display:block;position:absolute;width:50px;height:50px;overflow:hidden;'>" +
"<div style='height:100px;width:auto;'></div></div>" ),
innerDiv = div.children()[ 0 ];
$( "body" ).append( div );
w1 = innerDiv.offsetWidth;
div.css( "overflow", "scroll" );
w2 = innerDiv.offsetWidth;
if ( w1 === w2 ) {
w2 = div[ 0 ].clientWidth;
}
div.remove();
return ( cachedScrollbarWidth = w1 - w2 );
},
getScrollInfo: function( within ) {
var overflowX = within.isWindow || within.isDocument ? "" :
within.element.css( "overflow-x" ),
overflowY = within.isWindow || within.isDocument ? "" :
within.element.css( "overflow-y" ),
hasOverflowX = overflowX === "scroll" ||
( overflowX === "auto" && within.width < within.element[ 0 ].scrollWidth ),
hasOverflowY = overflowY === "scroll" ||
( overflowY === "auto" && within.height < within.element[ 0 ].scrollHeight );
return {
width: hasOverflowY ? $.position.scrollbarWidth() : 0,
height: hasOverflowX ? $.position.scrollbarWidth() : 0
};
},
getWithinInfo: function( element ) {
var withinElement = $( element || window ),
isWindow = $.isWindow( withinElement[ 0 ] ),
isDocument = !!withinElement[ 0 ] && withinElement[ 0 ].nodeType === 9,
hasOffset = !isWindow && !isDocument;
return {
element: withinElement,
isWindow: isWindow,
isDocument: isDocument,
offset: hasOffset ? $( element ).offset() : { left: 0, top: 0 },
scrollLeft: withinElement.scrollLeft(),
scrollTop: withinElement.scrollTop(),
width: withinElement.outerWidth(),
height: withinElement.outerHeight()
};
}
};
$.fn.position = function( options ) {
if ( !options || !options.of ) {
return _position.apply( this, arguments );
}
// Make a copy, we don't want to modify arguments
options = $.extend( {}, options );
var atOffset, targetWidth, targetHeight, targetOffset, basePosition, dimensions,
target = $( options.of ),
within = $.position.getWithinInfo( options.within ),
scrollInfo = $.position.getScrollInfo( within ),
collision = ( options.collision || "flip" ).split( " " ),
offsets = {};
dimensions = getDimensions( target );
if ( target[ 0 ].preventDefault ) {
// Force left top to allow flipping
options.at = "left top";
}
targetWidth = dimensions.width;
targetHeight = dimensions.height;
targetOffset = dimensions.offset;
// Clone to reuse original targetOffset later
basePosition = $.extend( {}, targetOffset );
// Force my and at to have valid horizontal and vertical positions
// if a value is missing or invalid, it will be converted to center
$.each( [ "my", "at" ], function() {
var pos = ( options[ this ] || "" ).split( " " ),
horizontalOffset,
verticalOffset;
if ( pos.length === 1 ) {
pos = rhorizontal.test( pos[ 0 ] ) ?
pos.concat( [ "center" ] ) :
rvertical.test( pos[ 0 ] ) ?
[ "center" ].concat( pos ) :
[ "center", "center" ];
}
pos[ 0 ] = rhorizontal.test( pos[ 0 ] ) ? pos[ 0 ] : "center";
pos[ 1 ] = rvertical.test( pos[ 1 ] ) ? pos[ 1 ] : "center";
// Calculate offsets
horizontalOffset = roffset.exec( pos[ 0 ] );
verticalOffset = roffset.exec( pos[ 1 ] );
offsets[ this ] = [
horizontalOffset ? horizontalOffset[ 0 ] : 0,
verticalOffset ? verticalOffset[ 0 ] : 0
];
// Reduce to just the positions without the offsets
options[ this ] = [
rposition.exec( pos[ 0 ] )[ 0 ],
rposition.exec( pos[ 1 ] )[ 0 ]
];
} );
// Normalize collision option
if ( collision.length === 1 ) {
collision[ 1 ] = collision[ 0 ];
}
if ( options.at[ 0 ] === "right" ) {
basePosition.left += targetWidth;
} else if ( options.at[ 0 ] === "center" ) {
basePosition.left += targetWidth / 2;
}
if ( options.at[ 1 ] === "bottom" ) {
basePosition.top += targetHeight;
} else if ( options.at[ 1 ] === "center" ) {
basePosition.top += targetHeight / 2;
}
atOffset = getOffsets( offsets.at, targetWidth, targetHeight );
basePosition.left += atOffset[ 0 ];
basePosition.top += atOffset[ 1 ];
return this.each( function() {
var collisionPosition, using,
elem = $( this ),
elemWidth = elem.outerWidth(),
elemHeight = elem.outerHeight(),
marginLeft = parseCss( this, "marginLeft" ),
marginTop = parseCss( this, "marginTop" ),
collisionWidth = elemWidth + marginLeft + parseCss( this, "marginRight" ) +
scrollInfo.width,
collisionHeight = elemHeight + marginTop + parseCss( this, "marginBottom" ) +
scrollInfo.height,
position = $.extend( {}, basePosition ),
myOffset = getOffsets( offsets.my, elem.outerWidth(), elem.outerHeight() );
if ( options.my[ 0 ] === "right" ) {
position.left -= elemWidth;
} else if ( options.my[ 0 ] === "center" ) {
position.left -= elemWidth / 2;
}
if ( options.my[ 1 ] === "bottom" ) {
position.top -= elemHeight;
} else if ( options.my[ 1 ] === "center" ) {
position.top -= elemHeight / 2;
}
position.left += myOffset[ 0 ];
position.top += myOffset[ 1 ];
collisionPosition = {
marginLeft: marginLeft,
marginTop: marginTop
};
$.each( [ "left", "top" ], function( i, dir ) {
if ( $.ui.position[ collision[ i ] ] ) {
$.ui.position[ collision[ i ] ][ dir ]( position, {
targetWidth: targetWidth,
targetHeight: targetHeight,
elemWidth: elemWidth,
elemHeight: elemHeight,
collisionPosition: collisionPosition,
collisionWidth: collisionWidth,
collisionHeight: collisionHeight,
offset: [ atOffset[ 0 ] + myOffset[ 0 ], atOffset [ 1 ] + myOffset[ 1 ] ],
my: options.my,
at: options.at,
within: within,
elem: elem
} );
}
} );
if ( options.using ) {
// Adds feedback as second argument to using callback, if present
using = function( props ) {
var left = targetOffset.left - position.left,
right = left + targetWidth - elemWidth,
top = targetOffset.top - position.top,
bottom = top + targetHeight - elemHeight,
feedback = {
target: {
element: target,
left: targetOffset.left,
top: targetOffset.top,
width: targetWidth,
height: targetHeight
},
element: {
element: elem,
left: position.left,
top: position.top,
width: elemWidth,
height: elemHeight
},
horizontal: right < 0 ? "left" : left > 0 ? "right" : "center",
vertical: bottom < 0 ? "top" : top > 0 ? "bottom" : "middle"
};
if ( targetWidth < elemWidth && abs( left + right ) < targetWidth ) {
feedback.horizontal = "center";
}
if ( targetHeight < elemHeight && abs( top + bottom ) < targetHeight ) {
feedback.vertical = "middle";
}
if ( max( abs( left ), abs( right ) ) > max( abs( top ), abs( bottom ) ) ) {
feedback.important = "horizontal";
} else {
feedback.important = "vertical";
}
options.using.call( this, props, feedback );
};
}
elem.offset( $.extend( position, { using: using } ) );
} );
};
$.ui.position = {
fit: {
left: function( position, data ) {
var within = data.within,
withinOffset = within.isWindow ? within.scrollLeft : within.offset.left,
outerWidth = within.width,
collisionPosLeft = position.left - data.collisionPosition.marginLeft,
overLeft = withinOffset - collisionPosLeft,
overRight = collisionPosLeft + data.collisionWidth - outerWidth - withinOffset,
newOverRight;
// Element is wider than within
if ( data.collisionWidth > outerWidth ) {
// Element is initially over the left side of within
if ( overLeft > 0 && overRight <= 0 ) {
newOverRight = position.left + overLeft + data.collisionWidth - outerWidth -
withinOffset;
position.left += overLeft - newOverRight;
// Element is initially over right side of within
} else if ( overRight > 0 && overLeft <= 0 ) {
position.left = withinOffset;
// Element is initially over both left and right sides of within
} else {
if ( overLeft > overRight ) {
position.left = withinOffset + outerWidth - data.collisionWidth;
} else {
position.left = withinOffset;
}
}
// Too far left -> align with left edge
} else if ( overLeft > 0 ) {
position.left += overLeft;
// Too far right -> align with right edge
} else if ( overRight > 0 ) {
position.left -= overRight;
// Adjust based on position and margin
} else {
position.left = max( position.left - collisionPosLeft, position.left );
}
},
top: function( position, data ) {
var within = data.within,
withinOffset = within.isWindow ? within.scrollTop : within.offset.top,
outerHeight = data.within.height,
collisionPosTop = position.top - data.collisionPosition.marginTop,
overTop = withinOffset - collisionPosTop,
overBottom = collisionPosTop + data.collisionHeight - outerHeight - withinOffset,
newOverBottom;
// Element is taller than within
if ( data.collisionHeight > outerHeight ) {
// Element is initially over the top of within
if ( overTop > 0 && overBottom <= 0 ) {
newOverBottom = position.top + overTop + data.collisionHeight - outerHeight -
withinOffset;
position.top += overTop - newOverBottom;
// Element is initially over bottom of within
} else if ( overBottom > 0 && overTop <= 0 ) {
position.top = withinOffset;
// Element is initially over both top and bottom of within
} else {
if ( overTop > overBottom ) {
position.top = withinOffset + outerHeight - data.collisionHeight;
} else {
position.top = withinOffset;
}
}
// Too far up -> align with top
} else if ( overTop > 0 ) {
position.top += overTop;
// Too far down -> align with bottom edge
} else if ( overBottom > 0 ) {
position.top -= overBottom;
// Adjust based on position and margin
} else {
position.top = max( position.top - collisionPosTop, position.top );
}
}
},
flip: {
left: function( position, data ) {
var within = data.within,
withinOffset = within.offset.left + within.scrollLeft,
outerWidth = within.width,
offsetLeft = within.isWindow ? within.scrollLeft : within.offset.left,
collisionPosLeft = position.left - data.collisionPosition.marginLeft,
overLeft = collisionPosLeft - offsetLeft,
overRight = collisionPosLeft + data.collisionWidth - outerWidth - offsetLeft,
myOffset = data.my[ 0 ] === "left" ?
-data.elemWidth :
data.my[ 0 ] === "right" ?
data.elemWidth :
0,
atOffset = data.at[ 0 ] === "left" ?
data.targetWidth :
data.at[ 0 ] === "right" ?
-data.targetWidth :
0,
offset = -2 * data.offset[ 0 ],
newOverRight,
newOverLeft;
if ( overLeft < 0 ) {
newOverRight = position.left + myOffset + atOffset + offset + data.collisionWidth -
outerWidth - withinOffset;
if ( newOverRight < 0 || newOverRight < abs( overLeft ) ) {
position.left += myOffset + atOffset + offset;
}
} else if ( overRight > 0 ) {
newOverLeft = position.left - data.collisionPosition.marginLeft + myOffset +
atOffset + offset - offsetLeft;
if ( newOverLeft > 0 || abs( newOverLeft ) < overRight ) {
position.left += myOffset + atOffset + offset;
}
}
},
top: function( position, data ) {
var within = data.within,
withinOffset = within.offset.top + within.scrollTop,
outerHeight = within.height,
offsetTop = within.isWindow ? within.scrollTop : within.offset.top,
collisionPosTop = position.top - data.collisionPosition.marginTop,
overTop = collisionPosTop - offsetTop,
overBottom = collisionPosTop + data.collisionHeight - outerHeight - offsetTop,
top = data.my[ 1 ] === "top",
myOffset = top ?
-data.elemHeight :
data.my[ 1 ] === "bottom" ?
data.elemHeight :
0,
atOffset = data.at[ 1 ] === "top" ?
data.targetHeight :
data.at[ 1 ] === "bottom" ?
-data.targetHeight :
0,
offset = -2 * data.offset[ 1 ],
newOverTop,
newOverBottom;
if ( overTop < 0 ) {
newOverBottom = position.top + myOffset + atOffset + offset + data.collisionHeight -
outerHeight - withinOffset;
if ( newOverBottom < 0 || newOverBottom < abs( overTop ) ) {
position.top += myOffset + atOffset + offset;
}
} else if ( overBottom > 0 ) {
newOverTop = position.top - data.collisionPosition.marginTop + myOffset + atOffset +
offset - offsetTop;
if ( newOverTop > 0 || abs( newOverTop ) < overBottom ) {
position.top += myOffset + atOffset + offset;
}
}
}
},
flipfit: {
left: function() {
$.ui.position.flip.left.apply( this, arguments );
$.ui.position.fit.left.apply( this, arguments );
},
top: function() {
$.ui.position.flip.top.apply( this, arguments );
$.ui.position.fit.top.apply( this, arguments );
}
}
};
} )();
return $.ui.position;
} ) );
( function( factory ) {
if ( typeof define === "function" && define.amd ) {
// AMD. Register as an anonymous module.
define( [ "jquery", "./version" ], factory );
} else {
// Browser globals
factory( jQuery );
}
} ( function( $ ) {
return $.ui.safeActiveElement = function( document ) {
var activeElement;
// Support: IE 9 only
// IE9 throws an "Unspecified error" accessing document.activeElement from an <iframe>
try {
activeElement = document.activeElement;
} catch ( error ) {
activeElement = document.body;
}
// Support: IE 9 - 11 only
// IE may return null instead of an element
// Interestingly, this only seems to occur when NOT in an iframe
if ( !activeElement ) {
activeElement = document.body;
}
// Support: IE 11 only
// IE11 returns a seemingly empty object in some cases when accessing
// document.activeElement from an <iframe>
if ( !activeElement.nodeName ) {
activeElement = document.body;
}
return activeElement;
};
} ) );
/*!
* jQuery UI Unique ID 1.12.1
* http://jqueryui.com
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*/
//>>label: uniqueId
//>>group: Core
//>>description: Functions to generate and remove uniqueId's
//>>docs: http://api.jqueryui.com/uniqueId/
( function( factory ) {
if ( typeof define === "function" && define.amd ) {
// AMD. Register as an anonymous module.
define( [ "jquery", "./version" ], factory );
} else {
// Browser globals
factory( jQuery );
}
} ( function( $ ) {
return $.fn.extend( {
uniqueId: ( function() {
var uuid = 0;
return function() {
return this.each( function() {
if ( !this.id ) {
this.id = "ui-id-" + ( ++uuid );
}
} );
};
} )(),
removeUniqueId: function() {
return this.each( function() {
if ( /^ui-id-\d+$/.test( this.id ) ) {
$( this ).removeAttr( "id" );
}
} );
}
} );
} ) );
/*!
* jQuery UI Widget 1.12.1
* http://jqueryui.com
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*/
//>>label: Widget
//>>group: Core
//>>description: Provides a factory for creating stateful widgets with a common API.
//>>docs: http://api.jqueryui.com/jQuery.widget/
//>>demos: http://jqueryui.com/widget/
( function( factory ) {
if ( typeof define === "function" && define.amd ) {
// AMD. Register as an anonymous module.
define( [ "jquery", "./version" ], factory );
} else {
// Browser globals
factory( jQuery );
}
}( function( $ ) {
var widgetUuid = 0;
var widgetSlice = Array.prototype.slice;
$.cleanData = ( function( orig ) {
return function( elems ) {
var events, elem, i;
for ( i = 0; ( elem = elems[ i ] ) != null; i++ ) {
try {
// Only trigger remove when necessary to save time
events = $._data( elem, "events" );
if ( events && events.remove ) {
$( elem ).triggerHandler( "remove" );
}
// Http://bugs.jquery.com/ticket/8235
} catch ( e ) {}
}
orig( elems );
};
} )( $.cleanData );
$.widget = function( name, base, prototype ) {
var existingConstructor, constructor, basePrototype;
// ProxiedPrototype allows the provided prototype to remain unmodified
// so that it can be used as a mixin for multiple widgets (#8876)
var proxiedPrototype = {};
var namespace = name.split( "." )[ 0 ];
name = name.split( "." )[ 1 ];
var fullName = namespace + "-" + name;
if ( !prototype ) {
prototype = base;
base = $.Widget;
}
if ( $.isArray( prototype ) ) {
prototype = $.extend.apply( null, [ {} ].concat( prototype ) );
}
// Create selector for plugin
$.expr[ ":" ][ fullName.toLowerCase() ] = 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 ) ) {
proxiedPrototype[ prop ] = value;
return;
}
proxiedPrototype[ prop ] = ( function() {
function _super() {
return base.prototype[ prop ].apply( this, arguments );
}
function _superApply( args ) {
return base.prototype[ prop ].apply( this, args );
}
return function() {
var __super = this._super;
var __superApply = this._superApply;
var 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: existingConstructor ? ( basePrototype.widgetEventPrefix || name ) : name
}, proxiedPrototype, {
constructor: constructor,
namespace: namespace,
widgetName: name,
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 );
return constructor;
};
$.widget.extend = function( target ) {
var input = widgetSlice.call( arguments, 1 );
var inputIndex = 0;
var inputLength = input.length;
var key;
var value;
for ( ; inputIndex < inputLength; inputIndex++ ) {
for ( key in input[ inputIndex ] ) {
value = input[ inputIndex ][ key ];
if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) {
// Clone objects
if ( $.isPlainObject( value ) ) {
target[ key ] = $.isPlainObject( target[ key ] ) ?
$.widget.extend( {}, target[ key ], value ) :
// Don't extend strings, arrays, etc. with objects
$.widget.extend( {}, value );
// Copy everything else by reference
} else {
target[ key ] = value;
}
}
}
}
return target;
};
$.widget.bridge = function( name, object ) {
var fullName = object.prototype.widgetFullName || name;
$.fn[ name ] = function( options ) {
var isMethodCall = typeof options === "string";
var args = widgetSlice.call( arguments, 1 );
var returnValue = this;
if ( isMethodCall ) {
// If this is an empty collection, we need to have the instance method
// return undefined instead of the jQuery instance
if ( !this.length && options === "instance" ) {
returnValue = undefined;
} else {
this.each( function() {
var methodValue;
var instance = $.data( this, fullName );
if ( options === "instance" ) {
returnValue = instance;
return false;
}
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 {
// Allow multiple hashes to be passed on init
if ( args.length ) {
options = $.widget.extend.apply( null, [ options ].concat( args ) );
}
this.each( function() {
var instance = $.data( this, fullName );
if ( instance ) {
instance.option( options || {} );
if ( instance._init ) {
instance._init();
}
} else {
$.data( this, fullName, new object( options, this ) );
}
} );
}
return returnValue;
};
};
$.Widget = function( /* options, element */ ) {};
$.Widget._childConstructors = [];
$.Widget.prototype = {
widgetName: "widget",
widgetEventPrefix: "",
defaultElement: "<div>",
options: {
classes: {},
disabled: false,
// Callbacks
create: null
},
_createWidget: function( options, element ) {
element = $( element || this.defaultElement || this )[ 0 ];
this.element = $( element );
this.uuid = widgetUuid++;
this.eventNamespace = "." + this.widgetName + this.uuid;
this.bindings = $();
this.hoverable = $();
this.focusable = $();
this.classesElementLookup = {};
if ( element !== this ) {
$.data( element, this.widgetFullName, this );
this._on( true, this.element, {
remove: function( event ) {
if ( event.target === element ) {
this.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.options = $.widget.extend( {},
this.options,
this._getCreateOptions(),
options );
this._create();
if ( this.options.disabled ) {
this._setOptionDisabled( this.options.disabled );
}
this._trigger( "create", null, this._getCreateEventData() );
this._init();
},
_getCreateOptions: function() {
return {};
},
_getCreateEventData: $.noop,
_create: $.noop,
_init: $.noop,
destroy: function() {
var that = this;
this._destroy();
$.each( this.classesElementLookup, function( key, value ) {
that._removeClass( value, key );
} );
// We can probably remove the unbind calls in 2.0
// all event bindings should go through this._on()
this.element
.off( this.eventNamespace )
.removeData( this.widgetFullName );
this.widget()
.off( this.eventNamespace )
.removeAttr( "aria-disabled" );
// Clean up events and states
this.bindings.off( this.eventNamespace );
},
_destroy: $.noop,
widget: function() {
return this.element;
},
option: function( key, value ) {
var options = key;
var parts;
var curOption;
var 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 ( arguments.length === 1 ) {
return curOption[ key ] === undefined ? null : curOption[ key ];
}
curOption[ key ] = value;
} else {
if ( arguments.length === 1 ) {
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 ) {
if ( key === "classes" ) {
this._setOptionClasses( value );
}
this.options[ key ] = value;
if ( key === "disabled" ) {
this._setOptionDisabled( value );
}
return this;
},
_setOptionClasses: function( value ) {
var classKey, elements, currentElements;
for ( classKey in value ) {
currentElements = this.classesElementLookup[ classKey ];
if ( value[ classKey ] === this.options.classes[ classKey ] ||
!currentElements ||
!currentElements.length ) {
continue;
}
// We are doing this to create a new jQuery object because the _removeClass() call
// on the next line is going to destroy the reference to the current elements being
// tracked. We need to save a copy of this collection so that we can add the new classes
// below.
elements = $( currentElements.get() );
this._removeClass( currentElements, classKey );
// We don't use _addClass() here, because that uses this.options.classes
// for generating the string of classes. We want to use the value passed in from
// _setOption(), this is the new value of the classes option which was passed to
// _setOption(). We pass this value directly to _classes().
elements.addClass( this._classes( {
element: elements,
keys: classKey,
classes: value,
add: true
} ) );
}
},
_setOptionDisabled: function( value ) {
this._toggleClass( this.widget(), this.widgetFullName + "-disabled", null, !!value );
// If the widget is becoming disabled, then nothing is interactive
if ( value ) {
this._removeClass( this.hoverable, null, "ui-state-hover" );
this._removeClass( this.focusable, null, "ui-state-focus" );
}
},
enable: function() {
return this._setOptions( { disabled: false } );
},
disable: function() {
return this._setOptions( { disabled: true } );
},
_classes: function( options ) {
var full = [];
var that = this;
options = $.extend( {
element: this.element,
classes: this.options.classes || {}
}, options );
function processClassString( classes, checkOption ) {
var current, i;
for ( i = 0; i < classes.length; i++ ) {
current = that.classesElementLookup[ classes[ i ] ] || $();
if ( options.add ) {
current = $( $.unique( current.get().concat( options.element.get() ) ) );
} else {
current = $( current.not( options.element ).get() );
}
that.classesElementLookup[ classes[ i ] ] = current;
full.push( classes[ i ] );
if ( checkOption && options.classes[ classes[ i ] ] ) {
full.push( options.classes[ classes[ i ] ] );
}
}
}
this._on( options.element, {
"remove": "_untrackClassesElement"
} );
if ( options.keys ) {
processClassString( options.keys.match( /\S+/g ) || [], true );
}
if ( options.extra ) {
processClassString( options.extra.match( /\S+/g ) || [] );
}
return full.join( " " );
},
_untrackClassesElement: function( event ) {
var that = this;
$.each( that.classesElementLookup, function( key, value ) {
if ( $.inArray( event.target, value ) !== -1 ) {
that.classesElementLookup[ key ] = $( value.not( event.target ).get() );
}
} );
},
_removeClass: function( element, keys, extra ) {
return this._toggleClass( element, keys, extra, false );
},
_addClass: function( element, keys, extra ) {
return this._toggleClass( element, keys, extra, true );
},
_toggleClass: function( element, keys, extra, add ) {
add = ( typeof add === "boolean" ) ? add : extra;
var shift = ( typeof element === "string" || element === null ),
options = {
extra: shift ? keys : extra,
keys: shift ? element : keys,
element: shift ? this.element : element,
add: add
};
options.element.toggleClass( this._classes( options ), add );
return this;
},
_on: function( suppressDisabledCheck, element, handlers ) {
var delegateElement;
var instance = this;
// No suppressDisabledCheck flag, shuffle arguments
if ( typeof suppressDisabledCheck !== "boolean" ) {
handlers = element;
element = suppressDisabledCheck;
suppressDisabledCheck = false;
}
// No element argument, shuffle and use this.element
if ( !handlers ) {
handlers = element;
element = this.element;
delegateElement = this.widget();
} else {
element = delegateElement = $( element );
this.bindings = this.bindings.add( element );
}
$.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 ( !suppressDisabledCheck &&
( 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*(.*)$/ );
var eventName = match[ 1 ] + instance.eventNamespace;
var selector = match[ 2 ];
if ( selector ) {
delegateElement.on( eventName, selector, handlerProxy );
} else {
element.on( eventName, handlerProxy );
}
} );
},
_off: function( element, eventName ) {
eventName = ( eventName || "" ).split( " " ).join( this.eventNamespace + " " ) +
this.eventNamespace;
element.off( eventName ).off( eventName );
// Clear the stack to avoid memory leaks (#10056)
this.bindings = $( this.bindings.not( element ).get() );
this.focusable = $( this.focusable.not( element ).get() );
this.hoverable = $( this.hoverable.not( element ).get() );
},
_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 ) {
this._addClass( $( event.currentTarget ), null, "ui-state-hover" );
},
mouseleave: function( event ) {
this._removeClass( $( event.currentTarget ), null, "ui-state-hover" );
}
} );
},
_focusable: function( element ) {
this.focusable = this.focusable.add( element );
this._on( element, {
focusin: function( event ) {
this._addClass( $( event.currentTarget ), null, "ui-state-focus" );
},
focusout: function( event ) {
this._removeClass( $( event.currentTarget ), null, "ui-state-focus" );
}
} );
},
_trigger: function( type, event, data ) {
var prop, orig;
var 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;
var 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 ] ) {
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();
} );
}
};
} );
return $.widget;
} ) );
/*!
* jQuery UI Menu 1.12.1
* http://jqueryui.com
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*/
//>>label: Menu
//>>group: Widgets
//>>description: Creates nestable menus.
//>>docs: http://api.jqueryui.com/menu/
//>>demos: http://jqueryui.com/menu/
//>>css.structure: ../../themes/base/core.css
//>>css.structure: ../../themes/base/menu.css
//>>css.theme: ../../themes/base/theme.css
( function( factory ) {
if ( typeof define === "function" && define.amd ) {
// AMD. Register as an anonymous module.
define( [
"jquery",
"../keycode",
"../position",
"../safe-active-element",
"../unique-id",
"../version",
"../widget"
], factory );
} else {
// Browser globals
factory( jQuery );
}
}( function( $ ) {
return $.widget( "ui.menu", {
version: "1.12.1",
defaultElement: "<ul>",
delay: 300,
options: {
icons: {
submenu: "ui-icon-caret-1-e"
},
items: "> *",
menus: "ul",
position: {
my: "left top",
at: "right top"
},
role: "menu",
// Callbacks
blur: null,
focus: null,
select: null
},
_create: function() {
this.activeMenu = this.element;
// Flag used to prevent firing of the click handler
// as the event bubbles up through nested menus
this.mouseHandled = false;
this.element
.uniqueId()
.attr( {
role: this.options.role,
tabIndex: 0
} );
this._addClass( "ui-menu", "ui-widget ui-widget-content" );
this._on( {
// Prevent focus from sticking to links inside menu after clicking
// them (focus should always stay on UL during navigation).
"mousedown .ui-menu-item": function( event ) {
event.preventDefault();
},
"click .ui-menu-item": function( event ) {
var target = $( event.target );
var active = $( $.ui.safeActiveElement( this.document[ 0 ] ) );
if ( !this.mouseHandled && target.not( ".ui-state-disabled" ).length ) {
this.select( event );
// Only set the mouseHandled flag if the event will bubble, see #9469.
if ( !event.isPropagationStopped() ) {
this.mouseHandled = true;
}
// Open submenu on click
if ( target.has( ".ui-menu" ).length ) {
this.expand( event );
} else if ( !this.element.is( ":focus" ) &&
active.closest( ".ui-menu" ).length ) {
// Redirect focus to the menu
this.element.trigger( "focus", [ true ] );
// If the active item is on the top level, let it stay active.
// Otherwise, blur the active item since it is no longer visible.
if ( this.active && this.active.parents( ".ui-menu" ).length === 1 ) {
clearTimeout( this.timer );
}
}
}
},
"mouseenter .ui-menu-item": function( event ) {
// Ignore mouse events while typeahead is active, see #10458.
// Prevents focusing the wrong item when typeahead causes a scroll while the mouse
// is over an item in the menu
if ( this.previousFilter ) {
return;
}
var actualTarget = $( event.target ).closest( ".ui-menu-item" ),
target = $( event.currentTarget );
// Ignore bubbled events on parent items, see #11641
if ( actualTarget[ 0 ] !== target[ 0 ] ) {
return;
}
// Remove ui-state-active class from siblings of the newly focused menu item
// to avoid a jump caused by adjacent elements both having a class with a border
this._removeClass( target.siblings().children( ".ui-state-active" ),
null, "ui-state-active" );
this.focus( event, target );
},
mouseleave: "collapseAll",
"mouseleave .ui-menu": "collapseAll",
focus: function( event, keepActiveItem ) {
// If there's already an active item, keep it active
// If not, activate the first item
var item = this.active || this.element.find( this.options.items ).eq( 0 );
if ( !keepActiveItem ) {
this.focus( event, item );
}
},
blur: function( event ) {
this._delay( function() {
var notContained = !$.contains(
this.element[ 0 ],
$.ui.safeActiveElement( this.document[ 0 ] )
);
if ( notContained ) {
this.collapseAll( event );
}
} );
},
keydown: "_keydown"
} );
this.refresh();
// Clicks outside of a menu collapse any open menus
this._on( this.document, {
click: function( event ) {
if ( this._closeOnDocumentClick( event ) ) {
this.collapseAll( event );
}
// Reset the mouseHandled flag
this.mouseHandled = false;
}
} );
},
_destroy: function() {
var items = this.element.find( ".ui-menu-item" )
.removeAttr( "role aria-disabled" ),
submenus = items.children( ".ui-menu-item-wrapper" )
.removeUniqueId()
.removeAttr( "tabIndex role aria-haspopup" );
// Destroy (sub)menus
this.element
.removeAttr( "aria-activedescendant" )
.find( ".ui-menu" ).addBack()
.removeAttr( "role aria-labelledby aria-expanded aria-hidden aria-disabled " +
"tabIndex" )
.removeUniqueId()
.show();
submenus.children().each( function() {
var elem = $( this );
if ( elem.data( "ui-menu-submenu-caret" ) ) {
elem.remove();
}
} );
},
_keydown: function( event ) {
var match, prev, character, skip,
preventDefault = true;
switch ( event.keyCode ) {
case $.ui.keyCode.PAGE_UP:
this.previousPage( event );
break;
case $.ui.keyCode.PAGE_DOWN:
this.nextPage( event );
break;
case $.ui.keyCode.HOME:
this._move( "first", "first", event );
break;
case $.ui.keyCode.END:
this._move( "last", "last", event );
break;
case $.ui.keyCode.UP:
this.previous( event );
break;
case $.ui.keyCode.DOWN:
this.next( event );
break;
case $.ui.keyCode.LEFT:
this.collapse( event );
break;
case $.ui.keyCode.RIGHT:
if ( this.active && !this.active.is( ".ui-state-disabled" ) ) {
this.expand( event );
}
break;
case $.ui.keyCode.ENTER:
case $.ui.keyCode.SPACE:
this._activate( event );
break;
case $.ui.keyCode.ESCAPE:
this.collapse( event );
break;
default:
preventDefault = false;
prev = this.previousFilter || "";
skip = false;
// Support number pad values
character = event.keyCode >= 96 && event.keyCode <= 105 ?
( event.keyCode - 96 ).toString() : String.fromCharCode( event.keyCode );
clearTimeout( this.filterTimer );
if ( character === prev ) {
skip = true;
} else {
character = prev + character;
}
match = this._filterMenuItems( character );
match = skip && match.index( this.active.next() ) !== -1 ?
this.active.nextAll( ".ui-menu-item" ) :
match;
// If no matches on the current filter, reset to the last character pressed
// to move down the menu to the first item that starts with that character
if ( !match.length ) {
character = String.fromCharCode( event.keyCode );
match = this._filterMenuItems( character );
}
if ( match.length ) {
this.focus( event, match );
this.previousFilter = character;
this.filterTimer = this._delay( function() {
delete this.previousFilter;
}, 1000 );
} else {
delete this.previousFilter;
}
}
if ( preventDefault ) {
event.preventDefault();
}
},
_activate: function( event ) {
if ( this.active && !this.active.is( ".ui-state-disabled" ) ) {
if ( this.active.children( "[aria-haspopup='true']" ).length ) {
this.expand( event );
} else {
this.select( event );
}
}
},
refresh: function() {
var menus, items, newSubmenus, newItems, newWrappers,
that = this,
icon = this.options.icons.submenu,
submenus = this.element.find( this.options.menus );
this._toggleClass( "ui-menu-icons", null, !!this.element.find( ".ui-icon" ).length );
// Initialize nested menus
newSubmenus = submenus.filter( ":not(.ui-menu)" )
.hide()
.attr( {
role: this.options.role,
"aria-hidden": "true",
"aria-expanded": "false"
} )
.each( function() {
var menu = $( this ),
item = menu.prev(),
submenuCaret = $( "<span>" ).data( "ui-menu-submenu-caret", true );
that._addClass( submenuCaret, "ui-menu-icon", "ui-icon " + icon );
item
.attr( "aria-haspopup", "true" )
.prepend( submenuCaret );
menu.attr( "aria-labelledby", item.attr( "id" ) );
} );
this._addClass( newSubmenus, "ui-menu", "ui-widget ui-widget-content ui-front" );
menus = submenus.add( this.element );
items = menus.find( this.options.items );
// Initialize menu-items containing spaces and/or dashes only as dividers
items.not( ".ui-menu-item" ).each( function() {
var item = $( this );
if ( that._isDivider( item ) ) {
that._addClass( item, "ui-menu-divider", "ui-widget-content" );
}
} );
// Don't refresh list items that are already adapted
newItems = items.not( ".ui-menu-item, .ui-menu-divider" );
newWrappers = newItems.children()
.not( ".ui-menu" )
.uniqueId()
.attr( {
tabIndex: -1,
role: this._itemRole()
} );
this._addClass( newItems, "ui-menu-item" )
._addClass( newWrappers, "ui-menu-item-wrapper" );
// Add aria-disabled attribute to any disabled menu item
items.filter( ".ui-state-disabled" ).attr( "aria-disabled", "true" );
// If the active item has been removed, blur the menu
if ( this.active && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) {
this.blur();
}
},
_itemRole: function() {
return {
menu: "menuitem",
listbox: "option"
}[ this.options.role ];
},
_setOption: function( key, value ) {
if ( key === "icons" ) {
var icons = this.element.find( ".ui-menu-icon" );
this._removeClass( icons, null, this.options.icons.submenu )
._addClass( icons, null, value.submenu );
}
this._super( key, value );
},
_setOptionDisabled: function( value ) {
this._super( value );
this.element.attr( "aria-disabled", String( value ) );
this._toggleClass( null, "ui-state-disabled", !!value );
},
focus: function( event, item ) {
var nested, focused, activeParent;
this.blur( event, event && event.type === "focus" );
this._scrollIntoView( item );
this.active = item.first();
focused = this.active.children( ".ui-menu-item-wrapper" );
this._addClass( focused, null, "ui-state-active" );
// Only update aria-activedescendant if there's a role
// otherwise we assume focus is managed elsewhere
if ( this.options.role ) {
this.element.attr( "aria-activedescendant", focused.attr( "id" ) );
}
// Highlight active parent menu item, if any
activeParent = this.active
.parent()
.closest( ".ui-menu-item" )
.children( ".ui-menu-item-wrapper" );
this._addClass( activeParent, null, "ui-state-active" );
if ( event && event.type === "keydown" ) {
this._close();
} else {
this.timer = this._delay( function() {
this._close();
}, this.delay );
}
nested = item.children( ".ui-menu" );
if ( nested.length && event && ( /^mouse/.test( event.type ) ) ) {
this._startOpening( nested );
}
this.activeMenu = item.parent();
this._trigger( "focus", event, { item: item } );
},
_scrollIntoView: function( item ) {
var borderTop, paddingTop, offset, scroll, elementHeight, itemHeight;
if ( this._hasScroll() ) {
borderTop = parseFloat( $.css( this.activeMenu[ 0 ], "borderTopWidth" ) ) || 0;
paddingTop = parseFloat( $.css( this.activeMenu[ 0 ], "paddingTop" ) ) || 0;
offset = item.offset().top - this.activeMenu.offset().top - borderTop - paddingTop;
scroll = this.activeMenu.scrollTop();
elementHeight = this.activeMenu.height();
itemHeight = item.outerHeight();
if ( offset < 0 ) {
this.activeMenu.scrollTop( scroll + offset );
} else if ( offset + itemHeight > elementHeight ) {
this.activeMenu.scrollTop( scroll + offset - elementHeight + itemHeight );
}
}
},
blur: function( event, fromFocus ) {
if ( !fromFocus ) {
clearTimeout( this.timer );
}
if ( !this.active ) {
return;
}
this._removeClass( this.active.children( ".ui-menu-item-wrapper" ),
null, "ui-state-active" );
this._trigger( "blur", event, { item: this.active } );
this.active = null;
},
_startOpening: function( submenu ) {
clearTimeout( this.timer );
// Don't open if already open fixes a Firefox bug that caused a .5 pixel
// shift in the submenu position when mousing over the caret icon
if ( submenu.attr( "aria-hidden" ) !== "true" ) {
return;
}
this.timer = this._delay( function() {
this._close();
this._open( submenu );
}, this.delay );
},
_open: function( submenu ) {
var position = $.extend( {
of: this.active
}, this.options.position );
clearTimeout( this.timer );
this.element.find( ".ui-menu" ).not( submenu.parents( ".ui-menu" ) )
.hide()
.attr( "aria-hidden", "true" );
submenu
.show()
.removeAttr( "aria-hidden" )
.attr( "aria-expanded", "true" )
.position( position );
},
collapseAll: function( event, all ) {
clearTimeout( this.timer );
this.timer = this._delay( function() {
// If we were passed an event, look for the submenu that contains the event
var currentMenu = all ? this.element :
$( event && event.target ).closest( this.element.find( ".ui-menu" ) );
// If we found no valid submenu ancestor, use the main menu to close all
// sub menus anyway
if ( !currentMenu.length ) {
currentMenu = this.element;
}
this._close( currentMenu );
this.blur( event );
// Work around active item staying active after menu is blurred
this._removeClass( currentMenu.find( ".ui-state-active" ), null, "ui-state-active" );
this.activeMenu = currentMenu;
}, this.delay );
},
// With no arguments, closes the currently active menu - if nothing is active
// it closes all menus. If passed an argument, it will search for menus BELOW
_close: function( startMenu ) {
if ( !startMenu ) {
startMenu = this.active ? this.active.parent() : this.element;
}
startMenu.find( ".ui-menu" )
.hide()
.attr( "aria-hidden", "true" )
.attr( "aria-expanded", "false" );
},
_closeOnDocumentClick: function( event ) {
return !$( event.target ).closest( ".ui-menu" ).length;
},
_isDivider: function( item ) {
// Match hyphen, em dash, en dash
return !/[^\-\u2014\u2013\s]/.test( item.text() );
},
collapse: function( event ) {
var newItem = this.active &&
this.active.parent().closest( ".ui-menu-item", this.element );
if ( newItem && newItem.length ) {
this._close();
this.focus( event, newItem );
}
},
expand: function( event ) {
var newItem = this.active &&
this.active
.children( ".ui-menu " )
.find( this.options.items )
.first();
if ( newItem && newItem.length ) {
this._open( newItem.parent() );
// Delay so Firefox will not hide activedescendant change in expanding submenu from AT
this._delay( function() {
this.focus( event, newItem );
} );
}
},
next: function( event ) {
this._move( "next", "first", event );
},
previous: function( event ) {
this._move( "prev", "last", event );
},
isFirstItem: function() {
return this.active && !this.active.prevAll( ".ui-menu-item" ).length;
},
isLastItem: function() {
return this.active && !this.active.nextAll( ".ui-menu-item" ).length;
},
_move: function( direction, filter, event ) {
var next;
if ( this.active ) {
if ( direction === "first" || direction === "last" ) {
next = this.active
[ direction === "first" ? "prevAll" : "nextAll" ]( ".ui-menu-item" )
.eq( -1 );
} else {
next = this.active
[ direction + "All" ]( ".ui-menu-item" )
.eq( 0 );
}
}
if ( !next || !next.length || !this.active ) {
next = this.activeMenu.find( this.options.items )[ filter ]();
}
this.focus( event, next );
},
nextPage: function( event ) {
var item, base, height;
if ( !this.active ) {
this.next( event );
return;
}
if ( this.isLastItem() ) {
return;
}
if ( this._hasScroll() ) {
base = this.active.offset().top;
height = this.element.height();
this.active.nextAll( ".ui-menu-item" ).each( function() {
item = $( this );
return item.offset().top - base - height < 0;
} );
this.focus( event, item );
} else {
this.focus( event, this.activeMenu.find( this.options.items )
[ !this.active ? "first" : "last" ]() );
}
},
previousPage: function( event ) {
var item, base, height;
if ( !this.active ) {
this.next( event );
return;
}
if ( this.isFirstItem() ) {
return;
}
if ( this._hasScroll() ) {
base = this.active.offset().top;
height = this.element.height();
this.active.prevAll( ".ui-menu-item" ).each( function() {
item = $( this );
return item.offset().top - base + height > 0;
} );
this.focus( event, item );
} else {
this.focus( event, this.activeMenu.find( this.options.items ).first() );
}
},
_hasScroll: function() {
return this.element.outerHeight() < this.element.prop( "scrollHeight" );
},
select: function( event ) {
// TODO: It should never be possible to not have an active item at this
// point, but the tests don't trigger mouseenter before click.
this.active = this.active || $( event.target ).closest( ".ui-menu-item" );
var ui = { item: this.active };
if ( !this.active.has( ".ui-menu" ).length ) {
this.collapseAll( event, true );
}
this._trigger( "select", event, ui );
},
_filterMenuItems: function( character ) {
var escapedCharacter = character.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" ),
regex = new RegExp( "^" + escapedCharacter, "i" );
return this.activeMenu
.find( this.options.items )
// Only match on items, not dividers or other content (#10571)
.filter( ".ui-menu-item" )
.filter( function() {
return regex.test(
$.trim( $( this ).children( ".ui-menu-item-wrapper" ).text() ) );
} );
}
} );
} ) );
/*!
* jQuery UI Autocomplete 1.12.1
* http://jqueryui.com
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*/
//>>label: Autocomplete
//>>group: Widgets
//>>description: Lists suggested words as the user is typing.
//>>docs: http://api.jqueryui.com/autocomplete/
//>>demos: http://jqueryui.com/autocomplete/
//>>css.structure: ../../themes/base/core.css
//>>css.structure: ../../themes/base/autocomplete.css
//>>css.theme: ../../themes/base/theme.css
( function( factory ) {
if ( typeof define === "function" && define.amd ) {
// AMD. Register as an anonymous module.
define( [
"jquery",
"./menu",
"../keycode",
"../position",
"../safe-active-element",
"../version",
"../widget"
], factory );
} else {
// Browser globals
factory( jQuery );
}
}( function( $ ) {
$.widget( "ui.autocomplete", {
version: "1.12.1",
defaultElement: "<input>",
options: {
appendTo: null,
autoFocus: false,
delay: 300,
minLength: 1,
position: {
my: "left top",
at: "left bottom",
collision: "none"
},
source: null,
// Callbacks
change: null,
close: null,
focus: null,
open: null,
response: null,
search: null,
select: null
},
requestIndex: 0,
pending: 0,
_create: function() {
// Some browsers only repeat keydown events, not keypress events,
// so we use the suppressKeyPress flag to determine if we've already
// handled the keydown event. #7269
// Unfortunately the code for & in keypress is the same as the up arrow,
// so we use the suppressKeyPressRepeat flag to avoid handling keypress
// events when we know the keydown event was used to modify the
// search term. #7799
var suppressKeyPress, suppressKeyPressRepeat, suppressInput,
nodeName = this.element[ 0 ].nodeName.toLowerCase(),
isTextarea = nodeName === "textarea",
isInput = nodeName === "input";
// Textareas are always multi-line
// Inputs are always single-line, even if inside a contentEditable element
// IE also treats inputs as contentEditable
// All other element types are determined by whether or not they're contentEditable
this.isMultiLine = isTextarea || !isInput && this._isContentEditable( this.element );
this.valueMethod = this.element[ isTextarea || isInput ? "val" : "text" ];
this.isNewMenu = true;
this._addClass( "ui-autocomplete-input" );
this.element.attr( "autocomplete", "off" );
this._on( this.element, {
keydown: function( event ) {
if ( this.element.prop( "readOnly" ) ) {
suppressKeyPress = true;
suppressInput = true;
suppressKeyPressRepeat = true;
return;
}
suppressKeyPress = false;
suppressInput = false;
suppressKeyPressRepeat = false;
var keyCode = $.ui.keyCode;
switch ( event.keyCode ) {
case keyCode.PAGE_UP:
suppressKeyPress = true;
this._move( "previousPage", event );
break;
case keyCode.PAGE_DOWN:
suppressKeyPress = true;
this._move( "nextPage", event );
break;
case keyCode.UP:
suppressKeyPress = true;
this._keyEvent( "previous", event );
break;
case keyCode.DOWN:
suppressKeyPress = true;
this._keyEvent( "next", event );
break;
case keyCode.ENTER:
// when menu is open and has focus
if ( this.menu.active ) {
// #6055 - Opera still allows the keypress to occur
// which causes forms to submit
suppressKeyPress = true;
event.preventDefault();
this.menu.select( event );
}
break;
case keyCode.TAB:
if ( this.menu.active ) {
this.menu.select( event );
}
break;
case keyCode.ESCAPE:
if ( this.menu.element.is( ":visible" ) ) {
if ( !this.isMultiLine ) {
this._value( this.term );
}
this.close( event );
// Different browsers have different default behavior for escape
// Single press can mean undo or clear
// Double press in IE means clear the whole form
event.preventDefault();
}
break;
default:
suppressKeyPressRepeat = true;
// search timeout should be triggered before the input value is changed
this._searchTimeout( event );
break;
}
},
keypress: function( event ) {
if ( suppressKeyPress ) {
suppressKeyPress = false;
if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) {
event.preventDefault();
}
return;
}
if ( suppressKeyPressRepeat ) {
return;
}
// Replicate some key handlers to allow them to repeat in Firefox and Opera
var keyCode = $.ui.keyCode;
switch ( event.keyCode ) {
case keyCode.PAGE_UP:
this._move( "previousPage", event );
break;
case keyCode.PAGE_DOWN:
this._move( "nextPage", event );
break;
case keyCode.UP:
this._keyEvent( "previous", event );
break;
case keyCode.DOWN:
this._keyEvent( "next", event );
break;
}
},
input: function( event ) {
if ( suppressInput ) {
suppressInput = false;
event.preventDefault();
return;
}
this._searchTimeout( event );
},
focus: function() {
this.selectedItem = null;
this.previous = this._value();
},
blur: function( event ) {
if ( this.cancelBlur ) {
delete this.cancelBlur;
return;
}
clearTimeout( this.searching );
this.close( event );
this._change( event );
}
} );
this._initSource();
this.menu = $( "<ul>" )
.appendTo( this._appendTo() )
.menu( {
// disable ARIA support, the live region takes care of that
role: null
} )
.hide()
.menu( "instance" );
this._addClass( this.menu.element, "ui-autocomplete", "ui-front" );
this._on( this.menu.element, {
mousedown: function( event ) {
// prevent moving focus out of the text field
event.preventDefault();
// IE doesn't prevent moving focus even with event.preventDefault()
// so we set a flag to know when we should ignore the blur event
this.cancelBlur = true;
this._delay( function() {
delete this.cancelBlur;
// Support: IE 8 only
// Right clicking a menu item or selecting text from the menu items will
// result in focus moving out of the input. However, we've already received
// and ignored the blur event because of the cancelBlur flag set above. So
// we restore focus to ensure that the menu closes properly based on the user's
// next actions.
if ( this.element[ 0 ] !== $.ui.safeActiveElement( this.document[ 0 ] ) ) {
this.element.trigger( "focus" );
}
} );
},
menufocus: function( event, ui ) {
var label, item;
// support: Firefox
// Prevent accidental activation of menu items in Firefox (#7024 #9118)
if ( this.isNewMenu ) {
this.isNewMenu = false;
if ( event.originalEvent && /^mouse/.test( event.originalEvent.type ) ) {
this.menu.blur();
this.document.one( "mousemove", function() {
$( event.target ).trigger( event.originalEvent );
} );
return;
}
}
item = ui.item.data( "ui-autocomplete-item" );
if ( false !== this._trigger( "focus", event, { item: item } ) ) {
// use value to match what will end up in the input, if it was a key event
if ( event.originalEvent && /^key/.test( event.originalEvent.type ) ) {
this._value( item.value );
}
}
// Announce the value in the liveRegion
label = ui.item.attr( "aria-label" ) || item.value;
if ( label && $.trim( label ).length ) {
this.liveRegion.children().hide();
$( "<div>" ).text( label ).appendTo( this.liveRegion );
}
},
menuselect: function( event, ui ) {
var item = ui.item.data( "ui-autocomplete-item" ),
previous = this.previous;
// Only trigger when focus was lost (click on menu)
if ( this.element[ 0 ] !== $.ui.safeActiveElement( this.document[ 0 ] ) ) {
this.element.trigger( "focus" );
this.previous = previous;
// #6109 - IE triggers two focus events and the second
// is asynchronous, so we need to reset the previous
// term synchronously and asynchronously :-(
this._delay( function() {
this.previous = previous;
this.selectedItem = item;
} );
}
if ( false !== this._trigger( "select", event, { item: item } ) ) {
this._value( item.value );
}
// reset the term after the select event
// this allows custom select handling to work properly
this.term = this._value();
this.close( event );
this.selectedItem = item;
}
} );
this.liveRegion = $( "<div>", {
role: "status",
"aria-live": "assertive",
"aria-relevant": "additions"
} )
.appendTo( this.document[ 0 ].body );
this._addClass( this.liveRegion, null, "ui-helper-hidden-accessible" );
// Turning off autocomplete prevents the browser from remembering the
// value when navigating through history, so we re-enable autocomplete
// if the page is unloaded before the widget is destroyed. #7790
this._on( this.window, {
beforeunload: function() {
this.element.removeAttr( "autocomplete" );
}
} );
},
_destroy: function() {
clearTimeout( this.searching );
this.element.removeAttr( "autocomplete" );
this.menu.element.remove();
this.liveRegion.remove();
},
_setOption: function( key, value ) {
this._super( key, value );
if ( key === "source" ) {
this._initSource();
}
if ( key === "appendTo" ) {
this.menu.element.appendTo( this._appendTo() );
}
if ( key === "disabled" && value && this.xhr ) {
this.xhr.abort();
}
},
_isEventTargetInWidget: function( event ) {
var menuElement = this.menu.element[ 0 ];
return event.target === this.element[ 0 ] ||
event.target === menuElement ||
$.contains( menuElement, event.target );
},
_closeOnClickOutside: function( event ) {
if ( !this._isEventTargetInWidget( event ) ) {
this.close();
}
},
_appendTo: function() {
var element = this.options.appendTo;
if ( element ) {
element = element.jquery || element.nodeType ?
$( element ) :
this.document.find( element ).eq( 0 );
}
if ( !element || !element[ 0 ] ) {
element = this.element.closest( ".ui-front, dialog" );
}
if ( !element.length ) {
element = this.document[ 0 ].body;
}
return element;
},
_initSource: function() {
var array, url,
that = this;
if ( $.isArray( this.options.source ) ) {
array = this.options.source;
this.source = function( request, response ) {
response( $.ui.autocomplete.filter( array, request.term ) );
};
} else if ( typeof this.options.source === "string" ) {
url = this.options.source;
this.source = function( request, response ) {
if ( that.xhr ) {
that.xhr.abort();
}
that.xhr = $.ajax( {
url: url,
data: request,
dataType: "json",
success: function( data ) {
response( data );
},
error: function() {
response( [] );
}
} );
};
} else {
this.source = this.options.source;
}
},
_searchTimeout: function( event ) {
clearTimeout( this.searching );
this.searching = this._delay( function() {
// Search if the value has changed, or if the user retypes the same value (see #7434)
var equalValues = this.term === this._value(),
menuVisible = this.menu.element.is( ":visible" ),
modifierKey = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
if ( !equalValues || ( equalValues && !menuVisible && !modifierKey ) ) {
this.selectedItem = null;
this.search( null, event );
}
}, this.options.delay );
},
search: function( value, event ) {
value = value != null ? value : this._value();
// Always save the actual value, not the one passed as an argument
this.term = this._value();
if ( value.length < this.options.minLength ) {
return this.close( event );
}
if ( this._trigger( "search", event ) === false ) {
return;
}
return this._search( value );
},
_search: function( value ) {
this.pending++;
this._addClass( "ui-autocomplete-loading" );
this.cancelSearch = false;
this.source( { term: value }, this._response() );
},
_response: function() {
var index = ++this.requestIndex;
return $.proxy( function( content ) {
if ( index === this.requestIndex ) {
this.__response( content );
}
this.pending--;
if ( !this.pending ) {
this._removeClass( "ui-autocomplete-loading" );
}
}, this );
},
__response: function( content ) {
if ( content ) {
content = this._normalize( content );
}
this._trigger( "response", null, { content: content } );
if ( !this.options.disabled && content && content.length && !this.cancelSearch ) {
this._suggest( content );
this._trigger( "open" );
} else {
// use ._close() instead of .close() so we don't cancel future searches
this._close();
}
},
close: function( event ) {
this.cancelSearch = true;
this._close( event );
},
_close: function( event ) {
// Remove the handler that closes the menu on outside clicks
this._off( this.document, "mousedown" );
if ( this.menu.element.is( ":visible" ) ) {
this.menu.element.hide();
this.menu.blur();
this.isNewMenu = true;
this._trigger( "close", event );
}
},
_change: function( event ) {
if ( this.previous !== this._value() ) {
this._trigger( "change", event, { item: this.selectedItem } );
}
},
_normalize: function( items ) {
// assume all items have the right format when the first item is complete
if ( items.length && items[ 0 ].label && items[ 0 ].value ) {
return items;
}
return $.map( items, function( item ) {
if ( typeof item === "string" ) {
return {
label: item,
value: item
};
}
return $.extend( {}, item, {
label: item.label || item.value,
value: item.value || item.label
} );
} );
},
_suggest: function( items ) {
var ul = this.menu.element.empty();
this._renderMenu( ul, items );
this.isNewMenu = true;
this.menu.refresh();
// Size and position menu
ul.show();
this._resizeMenu();
ul.position( $.extend( {
of: this.element
}, this.options.position ) );
if ( this.options.autoFocus ) {
this.menu.next();
}
// Listen for interactions outside of the widget (#6642)
this._on( this.document, {
mousedown: "_closeOnClickOutside"
} );
},
_resizeMenu: function() {
var ul = this.menu.element;
ul.outerWidth( Math.max(
// Firefox wraps long text (possibly a rounding bug)
// so we add 1px to avoid the wrapping (#7513)
ul.width( "" ).outerWidth() + 1,
this.element.outerWidth()
) );
},
_renderMenu: function( ul, items ) {
var that = this;
$.each( items, function( index, item ) {
that._renderItemData( ul, item );
} );
},
_renderItemData: function( ul, item ) {
return this._renderItem( ul, item ).data( "ui-autocomplete-item", item );
},
_renderItem: function( ul, item ) {
return $( "<li>" )
.append( $( "<div>" ).text( item.label ) )
.appendTo( ul );
},
_move: function( direction, event ) {
if ( !this.menu.element.is( ":visible" ) ) {
this.search( null, event );
return;
}
if ( this.menu.isFirstItem() && /^previous/.test( direction ) ||
this.menu.isLastItem() && /^next/.test( direction ) ) {
if ( !this.isMultiLine ) {
this._value( this.term );
}
this.menu.blur();
return;
}
this.menu[ direction ]( event );
},
widget: function() {
return this.menu.element;
},
_value: function() {
return this.valueMethod.apply( this.element, arguments );
},
_keyEvent: function( keyEvent, event ) {
if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) {
this._move( keyEvent, event );
// Prevents moving cursor to beginning/end of the text field in some browsers
event.preventDefault();
}
},
// Support: Chrome <=50
// We should be able to just use this.element.prop( "isContentEditable" )
// but hidden elements always report false in Chrome.
// https://code.google.com/p/chromium/issues/detail?id=313082
_isContentEditable: function( element ) {
if ( !element.length ) {
return false;
}
var editable = element.prop( "contentEditable" );
if ( editable === "inherit" ) {
return this._isContentEditable( element.parent() );
}
return editable === "true";
}
} );
$.extend( $.ui.autocomplete, {
escapeRegex: function( value ) {
return value.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" );
},
filter: function( array, term ) {
var matcher = new RegExp( $.ui.autocomplete.escapeRegex( term ), "i" );
return $.grep( array, function( value ) {
return matcher.test( value.label || value.value || value );
} );
}
} );
// Live region extension, adding a `messages` option
// NOTE: This is an experimental API. We are still investigating
// a full solution for string manipulation and internationalization.
$.widget( "ui.autocomplete", $.ui.autocomplete, {
options: {
messages: {
noResults: "No search results.",
results: function( amount ) {
return amount + ( amount > 1 ? " results are" : " result is" ) +
" available, use up and down arrow keys to navigate.";
}
}
},
__response: function( content ) {
var message;
this._superApply( arguments );
if ( this.options.disabled || this.cancelSearch ) {
return;
}
if ( content && content.length ) {
message = this.options.messages.results( content.length );
} else {
message = this.options.messages.noResults;
}
this.liveRegion.children().hide();
$( "<div>" ).text( message ).appendTo( this.liveRegion );
}
} );
return $.ui.autocomplete;
} ) );
/*
jQuery Tags Input Plugin 1.3.3
Copyright (c) 2011 XOXCO, Inc
Documentation for this plugin lives here:
http://xoxco.com/clickable/jquery-tags-input
Licensed under the MIT license:
http://www.opensource.org/licenses/mit-license.php
ben@xoxco.com
*/
(function($) {
var delimiter = new Array();
var tags_callbacks = new Array();
$.fn.doAutosize = function(o){
var minWidth = $(this).data('minwidth'),
maxWidth = $(this).data('maxwidth'),
val = '',
input = $(this),
testSubject = $('#'+$(this).data('tester_id'));
if (val === (val = input.val())) {return;}
// Enter new content into testSubject
var escaped = val.replace(/&/g, '&amp;').replace(/\s/g,' ').replace(/</g, '&lt;').replace(/>/g, '&gt;');
testSubject.html(escaped);
// Calculate new width + whether to change
var testerWidth = testSubject.width(),
newWidth = (testerWidth + o.comfortZone) >= minWidth ? testerWidth + o.comfortZone : minWidth,
currentWidth = input.width(),
isValidWidthChange = (newWidth < currentWidth && newWidth >= minWidth)
|| (newWidth > minWidth && newWidth < maxWidth);
// Animate width
if (isValidWidthChange) {
input.width(newWidth);
}
};
$.fn.resetAutosize = function(options){
// alert(JSON.stringify(options));
var minWidth = $(this).data('minwidth') || options.minInputWidth || $(this).width(),
maxWidth = $(this).data('maxwidth') || options.maxInputWidth || ($(this).closest('.tagsinput').width() - options.inputPadding),
val = '',
input = $(this),
testSubject = $('<tester/>').css({
position: 'absolute',
top: -9999,
left: -9999,
width: 'auto',
fontSize: input.css('fontSize'),
fontFamily: input.css('fontFamily'),
fontWeight: input.css('fontWeight'),
letterSpacing: input.css('letterSpacing'),
whiteSpace: 'nowrap'
}),
testerId = $(this).attr('id')+'_autosize_tester';
if(! $('#'+testerId).length > 0){
testSubject.attr('id', testerId);
testSubject.appendTo('body');
}
input.data('minwidth', minWidth);
input.data('maxwidth', maxWidth);
input.data('tester_id', testerId);
input.css('width', minWidth);
};
$.fn.addTag = function(value,options) {
options = jQuery.extend({focus:false,callback:true},options);
this.each(function() {
var id = $(this).attr('id');
var tagslist = $(this).val().split(delimiter[id]);
if (tagslist[0] == '') {
tagslist = new Array();
}
value = jQuery.trim(value);
if (options.unique) {
var skipTag = $(this).tagExist(value);
if(skipTag == true) {
//Marks fake input as not_valid to let styling it
$('#'+id+'_tag').addClass('not_valid');
}
} else {
var skipTag = false;
}
if (value !='' && skipTag != true) {
$('<span>').addClass('tag').append(
$('<span>').text(value).append('&nbsp;&nbsp;'),
$('<a>', {
href : '#',
title : 'Removing tag',
text : 'x'
}).click(function () {
return $('#' + id).removeTag(escape(value));
})
).insertBefore('#' + id + '_addTag');
tagslist.push(value);
$('#'+id+'_tag').val('');
if (options.focus) {
$('#'+id+'_tag').focus();
} else {
$('#'+id+'_tag').blur();
}
$.fn.tagsInput.updateTagsField(this,tagslist);
if (options.callback && tags_callbacks[id] && tags_callbacks[id]['onAddTag']) {
var f = tags_callbacks[id]['onAddTag'];
f.call(this, value);
}
if(tags_callbacks[id] && tags_callbacks[id]['onChange'])
{
var i = tagslist.length;
var f = tags_callbacks[id]['onChange'];
f.call(this, $(this), tagslist[i-1]);
}
}
});
return false;
};
$.fn.removeTag = function(value) {
value = unescape(value);
this.each(function() {
var id = $(this).attr('id');
var old = $(this).val().split(delimiter[id]);
$('#'+id+'_tagsinput .tag').remove();
str = '';
for (i=0; i< old.length; i++) {
if (old[i]!=value) {
str = str + delimiter[id] +old[i];
}
}
$.fn.tagsInput.importTags(this,str);
if (tags_callbacks[id] && tags_callbacks[id]['onRemoveTag']) {
var f = tags_callbacks[id]['onRemoveTag'];
f.call(this, value);
}
});
return false;
};
$.fn.tagExist = function(val) {
var id = $(this).attr('id');
var tagslist = $(this).val().split(delimiter[id]);
return (jQuery.inArray(val, tagslist) >= 0); //true when tag exists, false when not
};
// clear all existing tags and import new ones from a string
$.fn.importTags = function(str) {
var id = $(this).attr('id');
$('#'+id+'_tagsinput .tag').remove();
$.fn.tagsInput.importTags(this,str);
}
$.fn.tagsInput = function(options) {
var settings = jQuery.extend({
interactive:true,
defaultText:'add a tag',
minChars:0,
width:'300px',
height:'100px',
autocomplete: {selectFirst: false },
hide:true,
delimiter: ',',
unique:true,
removeWithBackspace:true,
placeholderColor:'#666666',
autosize: true,
comfortZone: 20,
inputPadding: 6*2
},options);
var uniqueIdCounter = 0;
this.each(function() {
// If we have already initialized the field, do not do it again
if (typeof $(this).attr('data-tagsinput-init') !== 'undefined') {
return;
}
// Mark the field as having been initialized
$(this).attr('data-tagsinput-init', true);
if (settings.hide) {
$(this).hide();
}
var id = $(this).attr('id');
if (!id || delimiter[$(this).attr('id')]) {
id = $(this).attr('id', 'tags' + new Date().getTime() + (uniqueIdCounter++)).attr('id');
}
var data = jQuery.extend({
pid:id,
real_input: '#'+id,
holder: '#'+id+'_tagsinput',
input_wrapper: '#'+id+'_addTag',
fake_input: '#'+id+'_tag'
},settings);
delimiter[id] = data.delimiter;
if (settings.onAddTag || settings.onRemoveTag || settings.onChange) {
tags_callbacks[id] = new Array();
tags_callbacks[id]['onAddTag'] = settings.onAddTag;
tags_callbacks[id]['onRemoveTag'] = settings.onRemoveTag;
tags_callbacks[id]['onChange'] = settings.onChange;
}
var markup = '<div id="'+id+'_tagsinput" class="tagsinput"><div id="'+id+'_addTag">';
if (settings.interactive) {
markup = markup + '<input id="'+id+'_tag" value="" data-default="'+settings.defaultText+'" />';
}
markup = markup + '</div><div class="tags_clear"></div></div>';
$(markup).insertAfter(this);
$(data.holder).css('width',settings.width);
$(data.holder).css('min-height',settings.height);
$(data.holder).css('height',settings.height);
if ($(data.real_input).val()!='') {
$.fn.tagsInput.importTags($(data.real_input),$(data.real_input).val());
}
if (settings.interactive) {
$(data.fake_input).val($(data.fake_input).attr('data-default'));
$(data.fake_input).css('color',settings.placeholderColor);
$(data.fake_input).resetAutosize(settings);
$(data.holder).bind('click',data,function(event) {
$(event.data.fake_input).focus();
});
$(data.fake_input).bind('focus',data,function(event) {
if ($(event.data.fake_input).val()==$(event.data.fake_input).attr('data-default')) {
$(event.data.fake_input).val('');
}
$(event.data.fake_input).css('color','#000000');
});
if (settings.autocomplete_url != undefined) {
autocomplete_options = {source: settings.autocomplete_url};
for (attrname in settings.autocomplete) {
autocomplete_options[attrname] = settings.autocomplete[attrname];
}
if (jQuery.Autocompleter !== undefined) {
$(data.fake_input).autocomplete(settings.autocomplete_url, settings.autocomplete);
$(data.fake_input).bind('result',data,function(event,data,formatted) {
if (data) {
$('#'+id).addTag(data[0] + "",{focus:true,unique:(settings.unique)});
}
});
} else if (jQuery.ui.autocomplete !== undefined) {
$(data.fake_input).autocomplete(autocomplete_options);
$(data.fake_input).bind('autocompleteselect',data,function(event,ui) {
$(event.data.real_input).addTag(ui.item.value,{focus:true,unique:(settings.unique)});
return false;
});
}
} else {
// if a user tabs out of the field, create a new tag
// this is only available if autocomplete is not used.
$(data.fake_input).bind('blur',data,function(event) {
var d = $(this).attr('data-default');
if ($(event.data.fake_input).val()!='' && $(event.data.fake_input).val()!=d) {
if( (event.data.minChars <= $(event.data.fake_input).val().length) && (!event.data.maxChars || (event.data.maxChars >= $(event.data.fake_input).val().length)) )
$(event.data.real_input).addTag($(event.data.fake_input).val(),{focus:true,unique:(settings.unique)});
} else {
$(event.data.fake_input).val($(event.data.fake_input).attr('data-default'));
$(event.data.fake_input).css('color',settings.placeholderColor);
}
return false;
});
}
// if user types a default delimiter like comma,semicolon and then create a new tag
$(data.fake_input).bind('keypress',data,function(event) {
if (_checkDelimiter(event)) {
event.preventDefault();
if( (event.data.minChars <= $(event.data.fake_input).val().length) && (!event.data.maxChars || (event.data.maxChars >= $(event.data.fake_input).val().length)) )
$(event.data.real_input).addTag($(event.data.fake_input).val(),{focus:true,unique:(settings.unique)});
$(event.data.fake_input).resetAutosize(settings);
return false;
} else if (event.data.autosize) {
$(event.data.fake_input).doAutosize(settings);
}
});
//Delete last tag on backspace
data.removeWithBackspace && $(data.fake_input).bind('keydown', function(event)
{
if(event.keyCode == 8 && $(this).val() == '')
{
event.preventDefault();
var last_tag = $(this).closest('.tagsinput').find('.tag:last').text();
var id = $(this).attr('id').replace(/_tag$/, '');
last_tag = last_tag.replace(/[\s]+x$/, '');
$('#' + id).removeTag(escape(last_tag));
$(this).trigger('focus');
}
});
$(data.fake_input).blur();
//Removes the not_valid class when user changes the value of the fake input
if(data.unique) {
$(data.fake_input).keydown(function(event){
if(event.keyCode == 8 || String.fromCharCode(event.which).match(/\w+|[áéíóúÁÉÍÓÚñÑ,/]+/)) {
$(this).removeClass('not_valid');
}
});
}
} // if settings.interactive
});
return this;
};
$.fn.tagsInput.updateTagsField = function(obj,tagslist) {
var id = $(obj).attr('id');
$(obj).val(tagslist.join(delimiter[id]));
};
$.fn.tagsInput.importTags = function(obj,val) {
$(obj).val('');
var id = $(obj).attr('id');
var tags = val.split(delimiter[id]);
for (i=0; i<tags.length; i++) {
$(obj).addTag(tags[i],{focus:false,callback:false});
}
if(tags_callbacks[id] && tags_callbacks[id]['onChange'])
{
var f = tags_callbacks[id]['onChange'];
f.call(obj, obj, tags[i]);
}
};
/**
* check delimiter Array
* @param event
* @returns {boolean}
* @private
*/
var _checkDelimiter = function(event){
var found = false;
if (event.which == 13) {
return true;
}
if (typeof event.data.delimiter === 'string') {
if (event.which == event.data.delimiter.charCodeAt(0)) {
found = true;
}
} else {
$.each(event.data.delimiter, function(index, delimiter) {
if (event.which == delimiter.charCodeAt(0)) {
found = true;
}
});
}
return found;
}
})(jQuery);
/*
Turbolinks 5.0.0
Copyright © 2016 Basecamp, LLC
*/
(function(){(function(){(function(){this.Turbolinks={supported:function(){return null!=window.history.pushState&&null!=window.requestAnimationFrame}(),visit:function(e,r){return t.controller.visit(e,r)},clearCache:function(){return t.controller.clearCache()}}}).call(this)}).call(this);var t=this.Turbolinks;(function(){(function(){var e,r;t.copyObject=function(t){var e,r,n;r={};for(e in t)n=t[e],r[e]=n;return r},t.closest=function(t,r){return e.call(t,r)},e=function(){var t,e;return t=document.documentElement,null!=(e=t.closest)?e:function(t){var e;for(e=this;e;){if(e.nodeType===Node.ELEMENT_NODE&&r.call(e,t))return e;e=e.parentNode}}}(),t.defer=function(t){return setTimeout(t,1)},t.dispatch=function(t,e){var r,n,o,i,s;return i=null!=e?e:{},s=i.target,r=i.cancelable,n=i.data,o=document.createEvent("Events"),o.initEvent(t,!0,r===!0),o.data=null!=n?n:{},(null!=s?s:document).dispatchEvent(o),o},t.match=function(t,e){return r.call(t,e)},r=function(){var t,e,r,n;return t=document.documentElement,null!=(e=null!=(r=null!=(n=t.matchesSelector)?n:t.webkitMatchesSelector)?r:t.msMatchesSelector)?e:t.mozMatchesSelector}(),t.uuid=function(){var t,e,r;for(r="",t=e=1;36>=e;t=++e)r+=9===t||14===t||19===t||24===t?"-":15===t?"4":20===t?(Math.floor(4*Math.random())+8).toString(16):Math.floor(15*Math.random()).toString(16);return r}}).call(this),function(){t.Location=function(){function t(t){var e,r;null==t&&(t=""),r=document.createElement("a"),r.href=t.toString(),this.absoluteURL=r.href,e=r.hash.length,2>e?this.requestURL=this.absoluteURL:(this.requestURL=this.absoluteURL.slice(0,-e),this.anchor=r.hash.slice(1))}var e,r,n,o;return t.wrap=function(t){return t instanceof this?t:new this(t)},t.prototype.getOrigin=function(){return this.absoluteURL.split("/",3).join("/")},t.prototype.getPath=function(){var t,e;return null!=(t=null!=(e=this.absoluteURL.match(/\/\/[^\/]*(\/[^?;]*)/))?e[1]:void 0)?t:"/"},t.prototype.getPathComponents=function(){return this.getPath().split("/").slice(1)},t.prototype.getLastPathComponent=function(){return this.getPathComponents().slice(-1)[0]},t.prototype.getExtension=function(){var t,e;return null!=(t=null!=(e=this.getLastPathComponent().match(/\.[^.]*$/))?e[0]:void 0)?t:""},t.prototype.isHTML=function(){return this.getExtension().match(/^(?:|\.(?:htm|html|xhtml))$/)},t.prototype.isPrefixedBy=function(t){var e;return e=r(t),this.isEqualTo(t)||o(this.absoluteURL,e)},t.prototype.isEqualTo=function(t){return this.absoluteURL===(null!=t?t.absoluteURL:void 0)},t.prototype.toCacheKey=function(){return this.requestURL},t.prototype.toJSON=function(){return this.absoluteURL},t.prototype.toString=function(){return this.absoluteURL},t.prototype.valueOf=function(){return this.absoluteURL},r=function(t){return e(t.getOrigin()+t.getPath())},e=function(t){return n(t,"/")?t:t+"/"},o=function(t,e){return t.slice(0,e.length)===e},n=function(t,e){return t.slice(-e.length)===e},t}()}.call(this),function(){var e=function(t,e){return function(){return t.apply(e,arguments)}};t.HttpRequest=function(){function r(r,n,o){this.delegate=r,this.requestCanceled=e(this.requestCanceled,this),this.requestTimedOut=e(this.requestTimedOut,this),this.requestFailed=e(this.requestFailed,this),this.requestLoaded=e(this.requestLoaded,this),this.requestProgressed=e(this.requestProgressed,this),this.url=t.Location.wrap(n).requestURL,this.referrer=t.Location.wrap(o).absoluteURL,this.createXHR()}return r.NETWORK_FAILURE=0,r.TIMEOUT_FAILURE=-1,r.timeout=60,r.prototype.send=function(){var t;return this.xhr&&!this.sent?(this.notifyApplicationBeforeRequestStart(),this.setProgress(0),this.xhr.send(),this.sent=!0,"function"==typeof(t=this.delegate).requestStarted?t.requestStarted():void 0):void 0},r.prototype.cancel=function(){return this.xhr&&this.sent?this.xhr.abort():void 0},r.prototype.requestProgressed=function(t){return t.lengthComputable?this.setProgress(t.loaded/t.total):void 0},r.prototype.requestLoaded=function(){return this.endRequest(function(t){return function(){var e;return 200<=(e=t.xhr.status)&&300>e?t.delegate.requestCompletedWithResponse(t.xhr.responseText,t.xhr.getResponseHeader("Turbolinks-Location")):(t.failed=!0,t.delegate.requestFailedWithStatusCode(t.xhr.status,t.xhr.responseText))}}(this))},r.prototype.requestFailed=function(){return this.endRequest(function(t){return function(){return t.failed=!0,t.delegate.requestFailedWithStatusCode(t.constructor.NETWORK_FAILURE)}}(this))},r.prototype.requestTimedOut=function(){return this.endRequest(function(t){return function(){return t.failed=!0,t.delegate.requestFailedWithStatusCode(t.constructor.TIMEOUT_FAILURE)}}(this))},r.prototype.requestCanceled=function(){return this.endRequest()},r.prototype.notifyApplicationBeforeRequestStart=function(){return t.dispatch("turbolinks:request-start",{data:{url:this.url,xhr:this.xhr}})},r.prototype.notifyApplicationAfterRequestEnd=function(){return t.dispatch("turbolinks:request-end",{data:{url:this.url,xhr:this.xhr}})},r.prototype.createXHR=function(){return this.xhr=new XMLHttpRequest,this.xhr.open("GET",this.url,!0),this.xhr.timeout=1e3*this.constructor.timeout,this.xhr.setRequestHeader("Accept","text/html, application/xhtml+xml"),this.xhr.setRequestHeader("Turbolinks-Referrer",this.referrer),this.xhr.onprogress=this.requestProgressed,this.xhr.onload=this.requestLoaded,this.xhr.onerror=this.requestFailed,this.xhr.ontimeout=this.requestTimedOut,this.xhr.onabort=this.requestCanceled},r.prototype.endRequest=function(t){return this.xhr?(this.notifyApplicationAfterRequestEnd(),null!=t&&t.call(this),this.destroy()):void 0},r.prototype.setProgress=function(t){var e;return this.progress=t,"function"==typeof(e=this.delegate).requestProgressed?e.requestProgressed(this.progress):void 0},r.prototype.destroy=function(){var t;return this.setProgress(1),"function"==typeof(t=this.delegate).requestFinished&&t.requestFinished(),this.delegate=null,this.xhr=null},r}()}.call(this),function(){var e=function(t,e){return function(){return t.apply(e,arguments)}};t.ProgressBar=function(){function t(){this.trickle=e(this.trickle,this),this.stylesheetElement=this.createStylesheetElement(),this.progressElement=this.createProgressElement()}var r;return r=300,t.defaultCSS=".turbolinks-progress-bar {\n position: fixed;\n display: block;\n top: 0;\n left: 0;\n height: 3px;\n background: #0076ff;\n z-index: 9999;\n transition: width "+r+"ms ease-out, opacity "+r/2+"ms "+r/2+"ms ease-in;\n transform: translate3d(0, 0, 0);\n}",t.prototype.show=function(){return this.visible?void 0:(this.visible=!0,this.installStylesheetElement(),this.installProgressElement(),this.startTrickling())},t.prototype.hide=function(){return this.visible&&!this.hiding?(this.hiding=!0,this.fadeProgressElement(function(t){return function(){return t.uninstallProgressElement(),t.stopTrickling(),t.visible=!1,t.hiding=!1}}(this))):void 0},t.prototype.setValue=function(t){return this.value=t,this.refresh()},t.prototype.installStylesheetElement=function(){return document.head.insertBefore(this.stylesheetElement,document.head.firstChild)},t.prototype.installProgressElement=function(){return this.progressElement.style.width=0,this.progressElement.style.opacity=1,document.documentElement.insertBefore(this.progressElement,document.body),this.refresh()},t.prototype.fadeProgressElement=function(t){return this.progressElement.style.opacity=0,setTimeout(t,1.5*r)},t.prototype.uninstallProgressElement=function(){return this.progressElement.parentNode?document.documentElement.removeChild(this.progressElement):void 0},t.prototype.startTrickling=function(){return null!=this.trickleInterval?this.trickleInterval:this.trickleInterval=setInterval(this.trickle,r)},t.prototype.stopTrickling=function(){return clearInterval(this.trickleInterval),this.trickleInterval=null},t.prototype.trickle=function(){return this.setValue(this.value+Math.random()/100)},t.prototype.refresh=function(){return requestAnimationFrame(function(t){return function(){return t.progressElement.style.width=10+90*t.value+"%"}}(this))},t.prototype.createStylesheetElement=function(){var t;return t=document.createElement("style"),t.type="text/css",t.textContent=this.constructor.defaultCSS,t},t.prototype.createProgressElement=function(){var t;return t=document.createElement("div"),t.className="turbolinks-progress-bar",t},t}()}.call(this),function(){var e=function(t,e){return function(){return t.apply(e,arguments)}};t.BrowserAdapter=function(){function r(r){this.controller=r,this.showProgressBar=e(this.showProgressBar,this),this.progressBar=new t.ProgressBar}var n,o,i,s;return s=t.HttpRequest,n=s.NETWORK_FAILURE,i=s.TIMEOUT_FAILURE,o=500,r.prototype.visitProposedToLocationWithAction=function(t,e){return this.controller.startVisitToLocationWithAction(t,e)},r.prototype.visitStarted=function(t){return t.issueRequest(),t.changeHistory(),t.loadCachedSnapshot()},r.prototype.visitRequestStarted=function(t){return this.progressBar.setValue(0),t.hasCachedSnapshot()||"restore"!==t.action?this.showProgressBarAfterDelay():this.showProgressBar()},r.prototype.visitRequestProgressed=function(t){return this.progressBar.setValue(t.progress)},r.prototype.visitRequestCompleted=function(t){return t.loadResponse()},r.prototype.visitRequestFailedWithStatusCode=function(t,e){switch(e){case n:case i:return this.reload();default:return t.loadResponse()}},r.prototype.visitRequestFinished=function(t){return this.hideProgressBar()},r.prototype.visitCompleted=function(t){return t.followRedirect()},r.prototype.pageInvalidated=function(){return this.reload()},r.prototype.showProgressBarAfterDelay=function(){return this.progressBarTimeout=setTimeout(this.showProgressBar,o)},r.prototype.showProgressBar=function(){return this.progressBar.show()},r.prototype.hideProgressBar=function(){return this.progressBar.hide(),clearTimeout(this.progressBarTimeout)},r.prototype.reload=function(){return window.location.reload()},r}()}.call(this),function(){var e,r=function(t,e){return function(){return t.apply(e,arguments)}};e=!1,addEventListener("load",function(){return t.defer(function(){return e=!0})},!1),t.History=function(){function n(t){this.delegate=t,this.onPopState=r(this.onPopState,this)}return n.prototype.start=function(){return this.started?void 0:(addEventListener("popstate",this.onPopState,!1),this.started=!0)},n.prototype.stop=function(){return this.started?(removeEventListener("popstate",this.onPopState,!1),this.started=!1):void 0},n.prototype.push=function(e,r){return e=t.Location.wrap(e),this.update("push",e,r)},n.prototype.replace=function(e,r){return e=t.Location.wrap(e),this.update("replace",e,r)},n.prototype.onPopState=function(e){var r,n,o,i;return this.shouldHandlePopState()&&(i=null!=(n=e.state)?n.turbolinks:void 0)?(r=t.Location.wrap(window.location),o=i.restorationIdentifier,this.delegate.historyPoppedToLocationWithRestorationIdentifier(r,o)):void 0},n.prototype.shouldHandlePopState=function(){return e===!0},n.prototype.update=function(t,e,r){var n;return n={turbolinks:{restorationIdentifier:r}},history[t+"State"](n,null,e)},n}()}.call(this),function(){t.Snapshot=function(){function e(t){var e,r;r=t.head,e=t.body,this.head=null!=r?r:document.createElement("head"),this.body=null!=e?e:document.createElement("body")}return e.wrap=function(t){return t instanceof this?t:this.fromHTML(t)},e.fromHTML=function(t){var e;return e=document.createElement("html"),e.innerHTML=t,this.fromElement(e)},e.fromElement=function(t){return new this({head:t.querySelector("head"),body:t.querySelector("body")})},e.prototype.clone=function(){return new e({head:this.head.cloneNode(!0),body:this.body.cloneNode(!0)})},e.prototype.getRootLocation=function(){var e,r;return r=null!=(e=this.getSetting("root"))?e:"/",new t.Location(r)},e.prototype.getCacheControlValue=function(){return this.getSetting("cache-control")},e.prototype.hasAnchor=function(t){try{return null!=this.body.querySelector("[id='"+t+"']")}catch(e){}},e.prototype.isPreviewable=function(){return"no-preview"!==this.getCacheControlValue()},e.prototype.isCacheable=function(){return"no-cache"!==this.getCacheControlValue()},e.prototype.getSetting=function(t){var e,r;return r=this.head.querySelectorAll("meta[name='turbolinks-"+t+"']"),e=r[r.length-1],null!=e?e.getAttribute("content"):void 0},e}()}.call(this),function(){var e=[].slice;t.Renderer=function(){function t(){}var r;return t.render=function(){var t,r,n,o;return n=arguments[0],r=arguments[1],t=3<=arguments.length?e.call(arguments,2):[],o=function(t,e,r){r.prototype=t.prototype;var n=new r,o=t.apply(n,e);return Object(o)===o?o:n}(this,t,function(){}),o.delegate=n,o.render(r),o},t.prototype.renderView=function(t){return this.delegate.viewWillRender(this.newBody),t(),this.delegate.viewRendered(this.newBody)},t.prototype.invalidateView=function(){return this.delegate.viewInvalidated()},t.prototype.createScriptElement=function(t){var e;return"false"===t.getAttribute("data-turbolinks-eval")?t:(e=document.createElement("script"),e.textContent=t.textContent,r(e,t),e)},r=function(t,e){var r,n,o,i,s,a,u;for(i=e.attributes,a=[],r=0,n=i.length;n>r;r++)s=i[r],o=s.name,u=s.value,a.push(t.setAttribute(o,u));return a},t}()}.call(this),function(){t.HeadDetails=function(){function t(t){var e,r,i,s,a,u,c;for(this.element=t,this.elements={},c=this.element.childNodes,s=0,u=c.length;u>s;s++)i=c[s],i.nodeType===Node.ELEMENT_NODE&&(a=i.outerHTML,r=null!=(e=this.elements)[a]?e[a]:e[a]={type:o(i),tracked:n(i),elements:[]},r.elements.push(i))}var e,r,n,o;return t.prototype.hasElementWithKey=function(t){return t in this.elements},t.prototype.getTrackedElementSignature=function(){var t,e;return function(){var r,n;r=this.elements,n=[];for(t in r)e=r[t].tracked,e&&n.push(t);return n}.call(this).join("")},t.prototype.getScriptElementsNotInDetails=function(t){return this.getElementsMatchingTypeNotInDetails("script",t)},t.prototype.getStylesheetElementsNotInDetails=function(t){return this.getElementsMatchingTypeNotInDetails("stylesheet",t)},t.prototype.getElementsMatchingTypeNotInDetails=function(t,e){var r,n,o,i,s,a;o=this.elements,s=[];for(n in o)i=o[n],a=i.type,r=i.elements,a!==t||e.hasElementWithKey(n)||s.push(r[0]);return s},t.prototype.getProvisionalElements=function(){var t,e,r,n,o,i,s;r=[],n=this.elements;for(e in n)o=n[e],s=o.type,i=o.tracked,t=o.elements,null!=s||i?t.length>1&&r.push.apply(r,t.slice(1)):r.push.apply(r,t);return r},o=function(t){return e(t)?"script":r(t)?"stylesheet":void 0},n=function(t){return"reload"===t.getAttribute("data-turbolinks-track")},e=function(t){var e;return e=t.tagName.toLowerCase(),"script"===e},r=function(t){var e;return e=t.tagName.toLowerCase(),"style"===e||"link"===e&&"stylesheet"===t.getAttribute("rel")},t}()}.call(this),function(){var e=function(t,e){function n(){this.constructor=t}for(var o in e)r.call(e,o)&&(t[o]=e[o]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},r={}.hasOwnProperty;t.SnapshotRenderer=function(r){function n(e,r){this.currentSnapshot=e,this.newSnapshot=r,this.currentHeadDetails=new t.HeadDetails(this.currentSnapshot.head),this.newHeadDetails=new t.HeadDetails(this.newSnapshot.head),this.newBody=this.newSnapshot.body}return e(n,r),n.prototype.render=function(t){return this.trackedElementsAreIdentical()?(this.mergeHead(),this.renderView(function(e){return function(){return e.replaceBody(),e.focusFirstAutofocusableElement(),t()}}(this))):this.invalidateView()},n.prototype.mergeHead=function(){return this.copyNewHeadStylesheetElements(),this.copyNewHeadScriptElements(),this.removeCurrentHeadProvisionalElements(),this.copyNewHeadProvisionalElements()},n.prototype.replaceBody=function(){return this.activateBodyScriptElements(),this.importBodyPermanentElements(),this.assignNewBody()},n.prototype.trackedElementsAreIdentical=function(){return this.currentHeadDetails.getTrackedElementSignature()===this.newHeadDetails.getTrackedElementSignature()},n.prototype.copyNewHeadStylesheetElements=function(){var t,e,r,n,o;for(n=this.getNewHeadStylesheetElements(),o=[],e=0,r=n.length;r>e;e++)t=n[e],o.push(document.head.appendChild(t));return o},n.prototype.copyNewHeadScriptElements=function(){var t,e,r,n,o;for(n=this.getNewHeadScriptElements(),o=[],e=0,r=n.length;r>e;e++)t=n[e],o.push(document.head.appendChild(this.createScriptElement(t)));return o},n.prototype.removeCurrentHeadProvisionalElements=function(){var t,e,r,n,o;for(n=this.getCurrentHeadProvisionalElements(),o=[],e=0,r=n.length;r>e;e++)t=n[e],o.push(document.head.removeChild(t));return o},n.prototype.copyNewHeadProvisionalElements=function(){var t,e,r,n,o;for(n=this.getNewHeadProvisionalElements(),o=[],e=0,r=n.length;r>e;e++)t=n[e],o.push(document.head.appendChild(t));return o},n.prototype.importBodyPermanentElements=function(){var t,e,r,n,o,i;for(n=this.getNewBodyPermanentElements(),i=[],e=0,r=n.length;r>e;e++)o=n[e],(t=this.findCurrentBodyPermanentElement(o))?i.push(o.parentNode.replaceChild(t,o)):i.push(void 0);return i},n.prototype.activateBodyScriptElements=function(){var t,e,r,n,o,i;for(n=this.getNewBodyScriptElements(),i=[],e=0,r=n.length;r>e;e++)o=n[e],t=this.createScriptElement(o),i.push(o.parentNode.replaceChild(t,o));return i},n.prototype.assignNewBody=function(){return document.body=this.newBody},n.prototype.focusFirstAutofocusableElement=function(){var t;return null!=(t=this.findFirstAutofocusableElement())?t.focus():void 0},n.prototype.getNewHeadStylesheetElements=function(){return this.newHeadDetails.getStylesheetElementsNotInDetails(this.currentHeadDetails)},n.prototype.getNewHeadScriptElements=function(){return this.newHeadDetails.getScriptElementsNotInDetails(this.currentHeadDetails)},n.prototype.getCurrentHeadProvisionalElements=function(){return this.currentHeadDetails.getProvisionalElements()},n.prototype.getNewHeadProvisionalElements=function(){return this.newHeadDetails.getProvisionalElements()},n.prototype.getNewBodyPermanentElements=function(){return this.newBody.querySelectorAll("[id][data-turbolinks-permanent]")},n.prototype.findCurrentBodyPermanentElement=function(t){return document.body.querySelector("#"+t.id+"[data-turbolinks-permanent]")},n.prototype.getNewBodyScriptElements=function(){return this.newBody.querySelectorAll("script")},n.prototype.findFirstAutofocusableElement=function(){return document.body.querySelector("[autofocus]")},n}(t.Renderer)}.call(this),function(){var e=function(t,e){function n(){this.constructor=t}for(var o in e)r.call(e,o)&&(t[o]=e[o]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},r={}.hasOwnProperty;t.ErrorRenderer=function(t){function r(t){this.html=t}return e(r,t),r.prototype.render=function(t){return this.renderView(function(e){return function(){return e.replaceDocumentHTML(),e.activateBodyScriptElements(),t()}}(this))},r.prototype.replaceDocumentHTML=function(){return document.documentElement.innerHTML=this.html},r.prototype.activateBodyScriptElements=function(){var t,e,r,n,o,i;for(n=this.getScriptElements(),i=[],e=0,r=n.length;r>e;e++)o=n[e],t=this.createScriptElement(o),i.push(o.parentNode.replaceChild(t,o));return i},r.prototype.getScriptElements=function(){return document.documentElement.querySelectorAll("script")},r}(t.Renderer)}.call(this),function(){t.View=function(){function e(t){this.delegate=t,this.element=document.documentElement}return e.prototype.getRootLocation=function(){return this.getSnapshot().getRootLocation()},e.prototype.getSnapshot=function(){return t.Snapshot.fromElement(this.element)},e.prototype.render=function(t,e){var r,n,o;return o=t.snapshot,r=t.error,n=t.isPreview,this.markAsPreview(n),null!=o?this.renderSnapshot(o,e):this.renderError(r,e)},e.prototype.markAsPreview=function(t){return t?this.element.setAttribute("data-turbolinks-preview",""):this.element.removeAttribute("data-turbolinks-preview")},e.prototype.renderSnapshot=function(e,r){return t.SnapshotRenderer.render(this.delegate,r,this.getSnapshot(),t.Snapshot.wrap(e))},e.prototype.renderError=function(e,r){return t.ErrorRenderer.render(this.delegate,r,e)},e}()}.call(this),function(){var e=function(t,e){return function(){return t.apply(e,arguments)}};t.ScrollManager=function(){function t(t){this.delegate=t,this.onScroll=e(this.onScroll,this)}return t.prototype.start=function(){return this.started?void 0:(addEventListener("scroll",this.onScroll,!1),this.onScroll(),this.started=!0)},t.prototype.stop=function(){return this.started?(removeEventListener("scroll",this.onScroll,!1),this.started=!1):void 0},t.prototype.scrollToElement=function(t){return t.scrollIntoView()},t.prototype.scrollToPosition=function(t){var e,r;return e=t.x,r=t.y,window.scrollTo(e,r)},t.prototype.onScroll=function(t){return this.updatePosition({x:window.pageXOffset,y:window.pageYOffset})},t.prototype.updatePosition=function(t){var e;return this.position=t,null!=(e=this.delegate)?e.scrollPositionChanged(this.position):void 0},t}()}.call(this),function(){t.SnapshotCache=function(){function e(t){this.size=t,this.keys=[],this.snapshots={}}var r;return e.prototype.has=function(t){var e;return e=r(t),e in this.snapshots},e.prototype.get=function(t){var e;if(this.has(t))return e=this.read(t),this.touch(t),e},e.prototype.put=function(t,e){return this.write(t,e),this.touch(t),e},e.prototype.read=function(t){var e;return e=r(t),this.snapshots[e]},e.prototype.write=function(t,e){var n;return n=r(t),this.snapshots[n]=e},e.prototype.touch=function(t){var e,n;return n=r(t),e=this.keys.indexOf(n),e>-1&&this.keys.splice(e,1),this.keys.unshift(n),this.trim()},e.prototype.trim=function(){var t,e,r,n,o;for(n=this.keys.splice(this.size),o=[],t=0,r=n.length;r>t;t++)e=n[t],o.push(delete this.snapshots[e]);return o},r=function(e){return t.Location.wrap(e).toCacheKey()},e}()}.call(this),function(){var e=function(t,e){return function(){return t.apply(e,arguments)}};t.Visit=function(){function r(r,n,o){this.controller=r,this.action=o,this.performScroll=e(this.performScroll,this),this.identifier=t.uuid(),this.location=t.Location.wrap(n),this.adapter=this.controller.adapter,this.state="initialized",this.timingMetrics={}}var n;return r.prototype.start=function(){return"initialized"===this.state?(this.recordTimingMetric("visitStart"),this.state="started",this.adapter.visitStarted(this)):void 0},r.prototype.cancel=function(){var t;return"started"===this.state?(null!=(t=this.request)&&t.cancel(),this.cancelRender(),this.state="canceled"):void 0},r.prototype.complete=function(){var t;return"started"===this.state?(this.recordTimingMetric("visitEnd"),this.state="completed","function"==typeof(t=this.adapter).visitCompleted&&t.visitCompleted(this),this.controller.visitCompleted(this)):void 0},r.prototype.fail=function(){var t;return"started"===this.state?(this.state="failed","function"==typeof(t=this.adapter).visitFailed?t.visitFailed(this):void 0):void 0},r.prototype.changeHistory=function(){var t,e;return this.historyChanged?void 0:(t=this.location.isEqualTo(this.referrer)?"replace":this.action,e=n(t),this.controller[e](this.location,this.restorationIdentifier),this.historyChanged=!0)},r.prototype.issueRequest=function(){return this.shouldIssueRequest()&&null==this.request?(this.progress=0,this.request=new t.HttpRequest(this,this.location,this.referrer),this.request.send()):void 0},r.prototype.getCachedSnapshot=function(){var t;return!(t=this.controller.getCachedSnapshotForLocation(this.location))||null!=this.location.anchor&&!t.hasAnchor(this.location.anchor)||"restore"!==this.action&&!t.isPreviewable()?void 0:t},r.prototype.hasCachedSnapshot=function(){return null!=this.getCachedSnapshot()},r.prototype.loadCachedSnapshot=function(){var t,e;return(e=this.getCachedSnapshot())?(t=this.shouldIssueRequest(),this.render(function(){var r;return this.cacheSnapshot(),this.controller.render({snapshot:e,isPreview:t},this.performScroll),"function"==typeof(r=this.adapter).visitRendered&&r.visitRendered(this),t?void 0:this.complete()})):void 0},r.prototype.loadResponse=function(){return null!=this.response?this.render(function(){var t,e;return this.cacheSnapshot(),this.request.failed?(this.controller.render({error:this.response},this.performScroll),"function"==typeof(t=this.adapter).visitRendered&&t.visitRendered(this),this.fail()):(this.controller.render({snapshot:this.response},this.performScroll),"function"==typeof(e=this.adapter).visitRendered&&e.visitRendered(this),this.complete())}):void 0},r.prototype.followRedirect=function(){return this.redirectedToLocation&&!this.followedRedirect?(this.location=this.redirectedToLocation,this.controller.replaceHistoryWithLocationAndRestorationIdentifier(this.redirectedToLocation,this.restorationIdentifier),this.followedRedirect=!0):void 0},r.prototype.requestStarted=function(){var t;return this.recordTimingMetric("requestStart"),"function"==typeof(t=this.adapter).visitRequestStarted?t.visitRequestStarted(this):void 0},r.prototype.requestProgressed=function(t){var e;return this.progress=t,"function"==typeof(e=this.adapter).visitRequestProgressed?e.visitRequestProgressed(this):void 0},r.prototype.requestCompletedWithResponse=function(e,r){return this.response=e,null!=r&&(this.redirectedToLocation=t.Location.wrap(r)),this.adapter.visitRequestCompleted(this)},r.prototype.requestFailedWithStatusCode=function(t,e){return this.response=e,this.adapter.visitRequestFailedWithStatusCode(this,t)},r.prototype.requestFinished=function(){var t;return this.recordTimingMetric("requestEnd"),"function"==typeof(t=this.adapter).visitRequestFinished?t.visitRequestFinished(this):void 0},r.prototype.performScroll=function(){return this.scrolled?void 0:("restore"===this.action?this.scrollToRestoredPosition()||this.scrollToTop():this.scrollToAnchor()||this.scrollToTop(),this.scrolled=!0)},r.prototype.scrollToRestoredPosition=function(){var t,e;return t=null!=(e=this.restorationData)?e.scrollPosition:void 0,null!=t?(this.controller.scrollToPosition(t),!0):void 0},r.prototype.scrollToAnchor=function(){return null!=this.location.anchor?(this.controller.scrollToAnchor(this.location.anchor),!0):void 0},r.prototype.scrollToTop=function(){return this.controller.scrollToPosition({x:0,y:0})},r.prototype.recordTimingMetric=function(t){var e;return null!=(e=this.timingMetrics)[t]?e[t]:e[t]=(new Date).getTime()},r.prototype.getTimingMetrics=function(){return t.copyObject(this.timingMetrics)},n=function(t){switch(t){case"replace":return"replaceHistoryWithLocationAndRestorationIdentifier";case"advance":case"restore":return"pushHistoryWithLocationAndRestorationIdentifier"}},r.prototype.shouldIssueRequest=function(){return"restore"===this.action?!this.hasCachedSnapshot():!0},r.prototype.cacheSnapshot=function(){return this.snapshotCached?void 0:(this.controller.cacheSnapshot(),this.snapshotCached=!0)},r.prototype.render=function(t){return this.cancelRender(),this.frame=requestAnimationFrame(function(e){return function(){return e.frame=null,t.call(e)}}(this))},r.prototype.cancelRender=function(){return this.frame?cancelAnimationFrame(this.frame):void 0},r}()}.call(this),function(){var e=function(t,e){return function(){return t.apply(e,arguments)}};t.Controller=function(){function r(){this.clickBubbled=e(this.clickBubbled,this),this.clickCaptured=e(this.clickCaptured,this),this.pageLoaded=e(this.pageLoaded,this),this.history=new t.History(this),this.view=new t.View(this),this.scrollManager=new t.ScrollManager(this),this.restorationData={},this.clearCache()}return r.prototype.start=function(){return t.supported&&!this.started?(addEventListener("click",this.clickCaptured,!0),addEventListener("DOMContentLoaded",this.pageLoaded,!1),this.scrollManager.start(),this.startHistory(),this.started=!0,this.enabled=!0):void 0},r.prototype.disable=function(){return this.enabled=!1},r.prototype.stop=function(){return this.started?(removeEventListener("click",this.clickCaptured,!0),removeEventListener("DOMContentLoaded",this.pageLoaded,!1),this.scrollManager.stop(),this.stopHistory(),this.started=!1):void 0},r.prototype.clearCache=function(){return this.cache=new t.SnapshotCache(10)},r.prototype.visit=function(e,r){var n,o;return null==r&&(r={}),e=t.Location.wrap(e),this.applicationAllowsVisitingLocation(e)?this.locationIsVisitable(e)?(n=null!=(o=r.action)?o:"advance",this.adapter.visitProposedToLocationWithAction(e,n)):window.location=e:void 0},r.prototype.startVisitToLocationWithAction=function(e,r,n){var o;return t.supported?(o=this.getRestorationDataForIdentifier(n),this.startVisit(e,r,{restorationData:o})):window.location=e},r.prototype.startHistory=function(){return this.location=t.Location.wrap(window.location),this.restorationIdentifier=t.uuid(),this.history.start(),this.history.replace(this.location,this.restorationIdentifier)},r.prototype.stopHistory=function(){return this.history.stop()},r.prototype.pushHistoryWithLocationAndRestorationIdentifier=function(e,r){return this.restorationIdentifier=r,this.location=t.Location.wrap(e),this.history.push(this.location,this.restorationIdentifier)},r.prototype.replaceHistoryWithLocationAndRestorationIdentifier=function(e,r){return this.restorationIdentifier=r,this.location=t.Location.wrap(e),this.history.replace(this.location,this.restorationIdentifier)},r.prototype.historyPoppedToLocationWithRestorationIdentifier=function(e,r){var n;return this.restorationIdentifier=r,this.enabled?(n=this.getRestorationDataForIdentifier(this.restorationIdentifier),this.startVisit(e,"restore",{restorationIdentifier:this.restorationIdentifier,restorationData:n,historyChanged:!0}),this.location=t.Location.wrap(e)):this.adapter.pageInvalidated()},r.prototype.getCachedSnapshotForLocation=function(t){var e;return e=this.cache.get(t),e?e.clone():void 0},r.prototype.shouldCacheSnapshot=function(){return this.view.getSnapshot().isCacheable()},r.prototype.cacheSnapshot=function(){var t;return this.shouldCacheSnapshot()?(this.notifyApplicationBeforeCachingSnapshot(),t=this.view.getSnapshot(),this.cache.put(this.lastRenderedLocation,t.clone())):void 0},r.prototype.scrollToAnchor=function(t){var e;return(e=document.getElementById(t))?this.scrollToElement(e):this.scrollToPosition({x:0,y:0})},r.prototype.scrollToElement=function(t){return this.scrollManager.scrollToElement(t)},r.prototype.scrollToPosition=function(t){return this.scrollManager.scrollToPosition(t)},r.prototype.scrollPositionChanged=function(t){var e;return e=this.getCurrentRestorationData(),e.scrollPosition=t},r.prototype.render=function(t,e){return this.view.render(t,e)},r.prototype.viewInvalidated=function(){return this.adapter.pageInvalidated()},r.prototype.viewWillRender=function(t){return this.notifyApplicationBeforeRender(t)},r.prototype.viewRendered=function(){return this.lastRenderedLocation=this.currentVisit.location,this.notifyApplicationAfterRender()},r.prototype.pageLoaded=function(){return this.lastRenderedLocation=this.location,this.notifyApplicationAfterPageLoad()},r.prototype.clickCaptured=function(){return removeEventListener("click",this.clickBubbled,!1),addEventListener("click",this.clickBubbled,!1)},r.prototype.clickBubbled=function(t){var e,r,n;return this.enabled&&this.clickEventIsSignificant(t)&&(r=this.getVisitableLinkForNode(t.target))&&(n=this.getVisitableLocationForLink(r))&&this.applicationAllowsFollowingLinkToLocation(r,n)?(t.preventDefault(),e=this.getActionForLink(r),this.visit(n,{action:e})):void 0},r.prototype.applicationAllowsFollowingLinkToLocation=function(t,e){var r;return r=this.notifyApplicationAfterClickingLinkToLocation(t,e),!r.defaultPrevented},r.prototype.applicationAllowsVisitingLocation=function(t){var e;return e=this.notifyApplicationBeforeVisitingLocation(t),!e.defaultPrevented},r.prototype.notifyApplicationAfterClickingLinkToLocation=function(e,r){return t.dispatch("turbolinks:click",{target:e,data:{url:r.absoluteURL},cancelable:!0})},r.prototype.notifyApplicationBeforeVisitingLocation=function(e){return t.dispatch("turbolinks:before-visit",{data:{url:e.absoluteURL},cancelable:!0})},r.prototype.notifyApplicationAfterVisitingLocation=function(e){return t.dispatch("turbolinks:visit",{data:{url:e.absoluteURL}})},r.prototype.notifyApplicationBeforeCachingSnapshot=function(){return t.dispatch("turbolinks:before-cache")},r.prototype.notifyApplicationBeforeRender=function(e){return t.dispatch("turbolinks:before-render",{data:{newBody:e}})},r.prototype.notifyApplicationAfterRender=function(){return t.dispatch("turbolinks:render")},r.prototype.notifyApplicationAfterPageLoad=function(e){return null==e&&(e={}),t.dispatch("turbolinks:load",{data:{url:this.location.absoluteURL,timing:e}})},r.prototype.startVisit=function(t,e,r){var n;return null!=(n=this.currentVisit)&&n.cancel(),this.currentVisit=this.createVisit(t,e,r),this.currentVisit.start(),this.notifyApplicationAfterVisitingLocation(t)},r.prototype.createVisit=function(e,r,n){
var o,i,s,a,u;return i=null!=n?n:{},a=i.restorationIdentifier,s=i.restorationData,o=i.historyChanged,u=new t.Visit(this,e,r),u.restorationIdentifier=null!=a?a:t.uuid(),u.restorationData=t.copyObject(s),u.historyChanged=o,u.referrer=this.location,u},r.prototype.visitCompleted=function(t){return this.notifyApplicationAfterPageLoad(t.getTimingMetrics())},r.prototype.clickEventIsSignificant=function(t){return!(t.defaultPrevented||t.target.isContentEditable||t.which>1||t.altKey||t.ctrlKey||t.metaKey||t.shiftKey)},r.prototype.getVisitableLinkForNode=function(e){return this.nodeIsVisitable(e)?t.closest(e,"a[href]:not([target])"):void 0},r.prototype.getVisitableLocationForLink=function(e){var r;return r=new t.Location(e.getAttribute("href")),this.locationIsVisitable(r)?r:void 0},r.prototype.getActionForLink=function(t){var e;return null!=(e=t.getAttribute("data-turbolinks-action"))?e:"advance"},r.prototype.nodeIsVisitable=function(e){var r;return(r=t.closest(e,"[data-turbolinks]"))?"false"!==r.getAttribute("data-turbolinks"):!0},r.prototype.locationIsVisitable=function(t){return t.isPrefixedBy(this.view.getRootLocation())&&t.isHTML()},r.prototype.getCurrentRestorationData=function(){return this.getRestorationDataForIdentifier(this.restorationIdentifier)},r.prototype.getRestorationDataForIdentifier=function(t){var e;return null!=(e=this.restorationData)[t]?e[t]:e[t]={}},r}()}.call(this),function(){var e,r,n;t.start=function(){return r()?(null==t.controller&&(t.controller=e()),t.controller.start()):void 0},r=function(){return null==window.Turbolinks&&(window.Turbolinks=t),n()},e=function(){var e;return e=new t.Controller,e.adapter=new t.BrowserAdapter(e),e},n=function(){return window.Turbolinks===t},n()&&t.start()}.call(this)}).call(this),"object"==typeof module&&module.exports?module.exports=t:"function"==typeof define&&define.amd&&define(t)}).call(this);
window.tinymce = window.tinymce || {
base: '/assets/tinymce',
suffix: ''
};
// 4.5.1 (2016-12-07)
/**
* Compiled inline version. (Library mode)
*/
/*jshint smarttabs:true, undef:true, latedef:true, curly:true, bitwise:true, camelcase:true */
/*globals $code */
(function(exports, undefined) {
"use strict";
var modules = {};
function require(ids, callback) {
var module, defs = [];
for (var i = 0; i < ids.length; ++i) {
module = modules[ids[i]] || resolve(ids[i]);
if (!module) {
throw 'module definition dependecy not found: ' + ids[i];
}
defs.push(module);
}
callback.apply(null, defs);
}
function define(id, dependencies, definition) {
if (typeof id !== 'string') {
throw 'invalid module definition, module id must be defined and be a string';
}
if (dependencies === undefined) {
throw 'invalid module definition, dependencies must be specified';
}
if (definition === undefined) {
throw 'invalid module definition, definition function must be specified';
}
require(dependencies, function() {
modules[id] = definition.apply(null, arguments);
});
}
function defined(id) {
return !!modules[id];
}
function resolve(id) {
var target = exports;
var fragments = id.split(/[.\/]/);
for (var fi = 0; fi < fragments.length; ++fi) {
if (!target[fragments[fi]]) {
return;
}
target = target[fragments[fi]];
}
return target;
}
function expose(ids) {
var i, target, id, fragments, privateModules;
for (i = 0; i < ids.length; i++) {
target = exports;
id = ids[i];
fragments = id.split(/[.\/]/);
for (var fi = 0; fi < fragments.length - 1; ++fi) {
if (target[fragments[fi]] === undefined) {
target[fragments[fi]] = {};
}
target = target[fragments[fi]];
}
target[fragments[fragments.length - 1]] = modules[id];
}
// Expose private modules for unit tests
if (exports.AMDLC_TESTS) {
privateModules = exports.privateModules || {};
for (id in modules) {
privateModules[id] = modules[id];
}
for (i = 0; i < ids.length; i++) {
delete privateModules[ids[i]];
}
exports.privateModules = privateModules;
}
}
// Included from: js/tinymce/classes/geom/Rect.js
/**
* Rect.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Contains various tools for rect/position calculation.
*
* @class tinymce.geom.Rect
*/
define("tinymce/geom/Rect", [
], function() {
"use strict";
var min = Math.min, max = Math.max, round = Math.round;
/**
* Returns the rect positioned based on the relative position name
* to the target rect.
*
* @method relativePosition
* @param {Rect} rect Source rect to modify into a new rect.
* @param {Rect} targetRect Rect to move relative to based on the rel option.
* @param {String} rel Relative position. For example: tr-bl.
*/
function relativePosition(rect, targetRect, rel) {
var x, y, w, h, targetW, targetH;
x = targetRect.x;
y = targetRect.y;
w = rect.w;
h = rect.h;
targetW = targetRect.w;
targetH = targetRect.h;
rel = (rel || '').split('');
if (rel[0] === 'b') {
y += targetH;
}
if (rel[1] === 'r') {
x += targetW;
}
if (rel[0] === 'c') {
y += round(targetH / 2);
}
if (rel[1] === 'c') {
x += round(targetW / 2);
}
if (rel[3] === 'b') {
y -= h;
}
if (rel[4] === 'r') {
x -= w;
}
if (rel[3] === 'c') {
y -= round(h / 2);
}
if (rel[4] === 'c') {
x -= round(w / 2);
}
return create(x, y, w, h);
}
/**
* Tests various positions to get the most suitable one.
*
* @method findBestRelativePosition
* @param {Rect} rect Rect to use as source.
* @param {Rect} targetRect Rect to move relative to.
* @param {Rect} constrainRect Rect to constrain within.
* @param {Array} rels Array of relative positions to test against.
*/
function findBestRelativePosition(rect, targetRect, constrainRect, rels) {
var pos, i;
for (i = 0; i < rels.length; i++) {
pos = relativePosition(rect, targetRect, rels[i]);
if (pos.x >= constrainRect.x && pos.x + pos.w <= constrainRect.w + constrainRect.x &&
pos.y >= constrainRect.y && pos.y + pos.h <= constrainRect.h + constrainRect.y) {
return rels[i];
}
}
return null;
}
/**
* Inflates the rect in all directions.
*
* @method inflate
* @param {Rect} rect Rect to expand.
* @param {Number} w Relative width to expand by.
* @param {Number} h Relative height to expand by.
* @return {Rect} New expanded rect.
*/
function inflate(rect, w, h) {
return create(rect.x - w, rect.y - h, rect.w + w * 2, rect.h + h * 2);
}
/**
* Returns the intersection of the specified rectangles.
*
* @method intersect
* @param {Rect} rect The first rectangle to compare.
* @param {Rect} cropRect The second rectangle to compare.
* @return {Rect} The intersection of the two rectangles or null if they don't intersect.
*/
function intersect(rect, cropRect) {
var x1, y1, x2, y2;
x1 = max(rect.x, cropRect.x);
y1 = max(rect.y, cropRect.y);
x2 = min(rect.x + rect.w, cropRect.x + cropRect.w);
y2 = min(rect.y + rect.h, cropRect.y + cropRect.h);
if (x2 - x1 < 0 || y2 - y1 < 0) {
return null;
}
return create(x1, y1, x2 - x1, y2 - y1);
}
/**
* Returns a rect clamped within the specified clamp rect. This forces the
* rect to be inside the clamp rect.
*
* @method clamp
* @param {Rect} rect Rectangle to force within clamp rect.
* @param {Rect} clampRect Rectable to force within.
* @param {Boolean} fixedSize True/false if size should be fixed.
* @return {Rect} Clamped rect.
*/
function clamp(rect, clampRect, fixedSize) {
var underflowX1, underflowY1, overflowX2, overflowY2,
x1, y1, x2, y2, cx2, cy2;
x1 = rect.x;
y1 = rect.y;
x2 = rect.x + rect.w;
y2 = rect.y + rect.h;
cx2 = clampRect.x + clampRect.w;
cy2 = clampRect.y + clampRect.h;
underflowX1 = max(0, clampRect.x - x1);
underflowY1 = max(0, clampRect.y - y1);
overflowX2 = max(0, x2 - cx2);
overflowY2 = max(0, y2 - cy2);
x1 += underflowX1;
y1 += underflowY1;
if (fixedSize) {
x2 += underflowX1;
y2 += underflowY1;
x1 -= overflowX2;
y1 -= overflowY2;
}
x2 -= overflowX2;
y2 -= overflowY2;
return create(x1, y1, x2 - x1, y2 - y1);
}
/**
* Creates a new rectangle object.
*
* @method create
* @param {Number} x Rectangle x location.
* @param {Number} y Rectangle y location.
* @param {Number} w Rectangle width.
* @param {Number} h Rectangle height.
* @return {Rect} New rectangle object.
*/
function create(x, y, w, h) {
return {x: x, y: y, w: w, h: h};
}
/**
* Creates a new rectangle object form a clientRects object.
*
* @method fromClientRect
* @param {ClientRect} clientRect DOM ClientRect object.
* @return {Rect} New rectangle object.
*/
function fromClientRect(clientRect) {
return create(clientRect.left, clientRect.top, clientRect.width, clientRect.height);
}
return {
inflate: inflate,
relativePosition: relativePosition,
findBestRelativePosition: findBestRelativePosition,
intersect: intersect,
clamp: clamp,
create: create,
fromClientRect: fromClientRect
};
});
// Included from: js/tinymce/classes/util/Promise.js
/**
* Promise.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* Promise polyfill under MIT license: https://github.com/taylorhakes/promise-polyfill
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/* eslint-disable */
/* jshint ignore:start */
/**
* Modifed to be a feature fill and wrapped as tinymce module.
*/
define("tinymce/util/Promise", [], function() {
if (window.Promise) {
return window.Promise;
}
// Use polyfill for setImmediate for performance gains
var asap = Promise.immediateFn || (typeof setImmediate === 'function' && setImmediate) ||
function(fn) { setTimeout(fn, 1); };
// Polyfill for Function.prototype.bind
function bind(fn, thisArg) {
return function() {
fn.apply(thisArg, arguments);
};
}
var isArray = Array.isArray || function(value) { return Object.prototype.toString.call(value) === "[object Array]"; };
function Promise(fn) {
if (typeof this !== 'object') throw new TypeError('Promises must be constructed via new');
if (typeof fn !== 'function') throw new TypeError('not a function');
this._state = null;
this._value = null;
this._deferreds = [];
doResolve(fn, bind(resolve, this), bind(reject, this));
}
function handle(deferred) {
var me = this;
if (this._state === null) {
this._deferreds.push(deferred);
return;
}
asap(function() {
var cb = me._state ? deferred.onFulfilled : deferred.onRejected;
if (cb === null) {
(me._state ? deferred.resolve : deferred.reject)(me._value);
return;
}
var ret;
try {
ret = cb(me._value);
}
catch (e) {
deferred.reject(e);
return;
}
deferred.resolve(ret);
});
}
function resolve(newValue) {
try { //Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure
if (newValue === this) throw new TypeError('A promise cannot be resolved with itself.');
if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
var then = newValue.then;
if (typeof then === 'function') {
doResolve(bind(then, newValue), bind(resolve, this), bind(reject, this));
return;
}
}
this._state = true;
this._value = newValue;
finale.call(this);
} catch (e) { reject.call(this, e); }
}
function reject(newValue) {
this._state = false;
this._value = newValue;
finale.call(this);
}
function finale() {
for (var i = 0, len = this._deferreds.length; i < len; i++) {
handle.call(this, this._deferreds[i]);
}
this._deferreds = null;
}
function Handler(onFulfilled, onRejected, resolve, reject){
this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
this.onRejected = typeof onRejected === 'function' ? onRejected : null;
this.resolve = resolve;
this.reject = reject;
}
/**
* Take a potentially misbehaving resolver function and make sure
* onFulfilled and onRejected are only called once.
*
* Makes no guarantees about asynchrony.
*/
function doResolve(fn, onFulfilled, onRejected) {
var done = false;
try {
fn(function (value) {
if (done) return;
done = true;
onFulfilled(value);
}, function (reason) {
if (done) return;
done = true;
onRejected(reason);
});
} catch (ex) {
if (done) return;
done = true;
onRejected(ex);
}
}
Promise.prototype['catch'] = function (onRejected) {
return this.then(null, onRejected);
};
Promise.prototype.then = function(onFulfilled, onRejected) {
var me = this;
return new Promise(function(resolve, reject) {
handle.call(me, new Handler(onFulfilled, onRejected, resolve, reject));
});
};
Promise.all = function () {
var args = Array.prototype.slice.call(arguments.length === 1 && isArray(arguments[0]) ? arguments[0] : arguments);
return new Promise(function (resolve, reject) {
if (args.length === 0) return resolve([]);
var remaining = args.length;
function res(i, val) {
try {
if (val && (typeof val === 'object' || typeof val === 'function')) {
var then = val.then;
if (typeof then === 'function') {
then.call(val, function (val) { res(i, val); }, reject);
return;
}
}
args[i] = val;
if (--remaining === 0) {
resolve(args);
}
} catch (ex) {
reject(ex);
}
}
for (var i = 0; i < args.length; i++) {
res(i, args[i]);
}
});
};
Promise.resolve = function (value) {
if (value && typeof value === 'object' && value.constructor === Promise) {
return value;
}
return new Promise(function (resolve) {
resolve(value);
});
};
Promise.reject = function (value) {
return new Promise(function (resolve, reject) {
reject(value);
});
};
Promise.race = function (values) {
return new Promise(function (resolve, reject) {
for(var i = 0, len = values.length; i < len; i++) {
values[i].then(resolve, reject);
}
});
};
return Promise;
});
/* jshint ignore:end */
/* eslint-enable */
// Included from: js/tinymce/classes/util/Delay.js
/**
* Delay.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Utility class for working with delayed actions like setTimeout.
*
* @class tinymce.util.Delay
*/
define("tinymce/util/Delay", [
"tinymce/util/Promise"
], function(Promise) {
var requestAnimationFramePromise;
function requestAnimationFrame(callback, element) {
var i, requestAnimationFrameFunc = window.requestAnimationFrame, vendors = ['ms', 'moz', 'webkit'];
function featurefill(callback) {
window.setTimeout(callback, 0);
}
for (i = 0; i < vendors.length && !requestAnimationFrameFunc; i++) {
requestAnimationFrameFunc = window[vendors[i] + 'RequestAnimationFrame'];
}
if (!requestAnimationFrameFunc) {
requestAnimationFrameFunc = featurefill;
}
requestAnimationFrameFunc(callback, element);
}
function wrappedSetTimeout(callback, time) {
if (typeof time != 'number') {
time = 0;
}
return setTimeout(callback, time);
}
function wrappedSetInterval(callback, time) {
if (typeof time != 'number') {
time = 1; // IE 8 needs it to be > 0
}
return setInterval(callback, time);
}
function wrappedClearTimeout(id) {
return clearTimeout(id);
}
function wrappedClearInterval(id) {
return clearInterval(id);
}
function debounce(callback, time) {
var timer, func;
func = function() {
var args = arguments;
clearTimeout(timer);
timer = wrappedSetTimeout(function() {
callback.apply(this, args);
}, time);
};
func.stop = function() {
clearTimeout(timer);
};
return func;
}
return {
/**
* Requests an animation frame and fallbacks to a timeout on older browsers.
*
* @method requestAnimationFrame
* @param {function} callback Callback to execute when a new frame is available.
* @param {DOMElement} element Optional element to scope it to.
*/
requestAnimationFrame: function(callback, element) {
if (requestAnimationFramePromise) {
requestAnimationFramePromise.then(callback);
return;
}
requestAnimationFramePromise = new Promise(function(resolve) {
if (!element) {
element = document.body;
}
requestAnimationFrame(resolve, element);
}).then(callback);
},
/**
* Sets a timer in ms and executes the specified callback when the timer runs out.
*
* @method setTimeout
* @param {function} callback Callback to execute when timer runs out.
* @param {Number} time Optional time to wait before the callback is executed, defaults to 0.
* @return {Number} Timeout id number.
*/
setTimeout: wrappedSetTimeout,
/**
* Sets an interval timer in ms and executes the specified callback at every interval of that time.
*
* @method setInterval
* @param {function} callback Callback to execute when interval time runs out.
* @param {Number} time Optional time to wait before the callback is executed, defaults to 0.
* @return {Number} Timeout id number.
*/
setInterval: wrappedSetInterval,
/**
* Sets an editor timeout it's similar to setTimeout except that it checks if the editor instance is
* still alive when the callback gets executed.
*
* @method setEditorTimeout
* @param {tinymce.Editor} editor Editor instance to check the removed state on.
* @param {function} callback Callback to execute when timer runs out.
* @param {Number} time Optional time to wait before the callback is executed, defaults to 0.
* @return {Number} Timeout id number.
*/
setEditorTimeout: function(editor, callback, time) {
return wrappedSetTimeout(function() {
if (!editor.removed) {
callback();
}
}, time);
},
/**
* Sets an interval timer it's similar to setInterval except that it checks if the editor instance is
* still alive when the callback gets executed.
*
* @method setEditorInterval
* @param {function} callback Callback to execute when interval time runs out.
* @param {Number} time Optional time to wait before the callback is executed, defaults to 0.
* @return {Number} Timeout id number.
*/
setEditorInterval: function(editor, callback, time) {
var timer;
timer = wrappedSetInterval(function() {
if (!editor.removed) {
callback();
} else {
clearInterval(timer);
}
}, time);
return timer;
},
/**
* Creates debounced callback function that only gets executed once within the specified time.
*
* @method debounce
* @param {function} callback Callback to execute when timer finishes.
* @param {Number} time Optional time to wait before the callback is executed, defaults to 0.
* @return {Function} debounced function callback.
*/
debounce: debounce,
// Throttle needs to be debounce due to backwards compatibility.
throttle: debounce,
/**
* Clears an interval timer so it won't execute.
*
* @method clearInterval
* @param {Number} Interval timer id number.
*/
clearInterval: wrappedClearInterval,
/**
* Clears an timeout timer so it won't execute.
*
* @method clearTimeout
* @param {Number} Timeout timer id number.
*/
clearTimeout: wrappedClearTimeout
};
});
// Included from: js/tinymce/classes/Env.js
/**
* Env.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class contains various environment constants like browser versions etc.
* Normally you don't want to sniff specific browser versions but sometimes you have
* to when it's impossible to feature detect. So use this with care.
*
* @class tinymce.Env
* @static
*/
define("tinymce/Env", [], function() {
var nav = navigator, userAgent = nav.userAgent;
var opera, webkit, ie, ie11, ie12, gecko, mac, iDevice, android, fileApi, phone, tablet, windowsPhone;
function matchMediaQuery(query) {
return "matchMedia" in window ? matchMedia(query).matches : false;
}
opera = window.opera && window.opera.buildNumber;
android = /Android/.test(userAgent);
webkit = /WebKit/.test(userAgent);
ie = !webkit && !opera && (/MSIE/gi).test(userAgent) && (/Explorer/gi).test(nav.appName);
ie = ie && /MSIE (\w+)\./.exec(userAgent)[1];
ie11 = userAgent.indexOf('Trident/') != -1 && (userAgent.indexOf('rv:') != -1 || nav.appName.indexOf('Netscape') != -1) ? 11 : false;
ie12 = (userAgent.indexOf('Edge/') != -1 && !ie && !ie11) ? 12 : false;
ie = ie || ie11 || ie12;
gecko = !webkit && !ie11 && /Gecko/.test(userAgent);
mac = userAgent.indexOf('Mac') != -1;
iDevice = /(iPad|iPhone)/.test(userAgent);
fileApi = "FormData" in window && "FileReader" in window && "URL" in window && !!URL.createObjectURL;
phone = matchMediaQuery("only screen and (max-device-width: 480px)") && (android || iDevice);
tablet = matchMediaQuery("only screen and (min-width: 800px)") && (android || iDevice);
windowsPhone = userAgent.indexOf('Windows Phone') != -1;
if (ie12) {
webkit = false;
}
// Is a iPad/iPhone and not on iOS5 sniff the WebKit version since older iOS WebKit versions
// says it has contentEditable support but there is no visible caret.
var contentEditable = !iDevice || fileApi || userAgent.match(/AppleWebKit\/(\d*)/)[1] >= 534;
return {
/**
* Constant that is true if the browser is Opera.
*
* @property opera
* @type Boolean
* @final
*/
opera: opera,
/**
* Constant that is true if the browser is WebKit (Safari/Chrome).
*
* @property webKit
* @type Boolean
* @final
*/
webkit: webkit,
/**
* Constant that is more than zero if the browser is IE.
*
* @property ie
* @type Boolean
* @final
*/
ie: ie,
/**
* Constant that is true if the browser is Gecko.
*
* @property gecko
* @type Boolean
* @final
*/
gecko: gecko,
/**
* Constant that is true if the os is Mac OS.
*
* @property mac
* @type Boolean
* @final
*/
mac: mac,
/**
* Constant that is true if the os is iOS.
*
* @property iOS
* @type Boolean
* @final
*/
iOS: iDevice,
/**
* Constant that is true if the os is android.
*
* @property android
* @type Boolean
* @final
*/
android: android,
/**
* Constant that is true if the browser supports editing.
*
* @property contentEditable
* @type Boolean
* @final
*/
contentEditable: contentEditable,
/**
* Transparent image data url.
*
* @property transparentSrc
* @type Boolean
* @final
*/
transparentSrc: "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
/**
* Returns true/false if the browser can or can't place the caret after a inline block like an image.
*
* @property noCaretAfter
* @type Boolean
* @final
*/
caretAfter: ie != 8,
/**
* Constant that is true if the browser supports native DOM Ranges. IE 9+.
*
* @property range
* @type Boolean
*/
range: window.getSelection && "Range" in window,
/**
* Returns the IE document mode for non IE browsers this will fake IE 10.
*
* @property documentMode
* @type Number
*/
documentMode: ie && !ie12 ? (document.documentMode || 7) : 10,
/**
* Constant that is true if the browser has a modern file api.
*
* @property fileApi
* @type Boolean
*/
fileApi: fileApi,
/**
* Constant that is true if the browser supports contentEditable=false regions.
*
* @property ceFalse
* @type Boolean
*/
ceFalse: (ie === false || ie > 8),
/**
* Constant if CSP mode is possible or not. Meaning we can't use script urls for the iframe.
*/
canHaveCSP: (ie === false || ie > 11),
desktop: !phone && !tablet,
windowsPhone: windowsPhone
};
});
// Included from: js/tinymce/classes/dom/EventUtils.js
/**
* EventUtils.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/*jshint loopfunc:true*/
/*eslint no-loop-func:0 */
/**
* This class wraps the browsers native event logic with more convenient methods.
*
* @class tinymce.dom.EventUtils
*/
define("tinymce/dom/EventUtils", [
"tinymce/util/Delay",
"tinymce/Env"
], function(Delay, Env) {
"use strict";
var eventExpandoPrefix = "mce-data-";
var mouseEventRe = /^(?:mouse|contextmenu)|click/;
var deprecated = {
keyLocation: 1, layerX: 1, layerY: 1, returnValue: 1,
webkitMovementX: 1, webkitMovementY: 1, keyIdentifier: 1
};
/**
* Binds a native event to a callback on the speified target.
*/
function addEvent(target, name, callback, capture) {
if (target.addEventListener) {
target.addEventListener(name, callback, capture || false);
} else if (target.attachEvent) {
target.attachEvent('on' + name, callback);
}
}
/**
* Unbinds a native event callback on the specified target.
*/
function removeEvent(target, name, callback, capture) {
if (target.removeEventListener) {
target.removeEventListener(name, callback, capture || false);
} else if (target.detachEvent) {
target.detachEvent('on' + name, callback);
}
}
/**
* Gets the event target based on shadow dom properties like path and deepPath.
*/
function getTargetFromShadowDom(event, defaultTarget) {
var path, target = defaultTarget;
// When target element is inside Shadow DOM we need to take first element from path
// otherwise we'll get Shadow Root parent, not actual target element
// Normalize target for WebComponents v0 implementation (in Chrome)
path = event.path;
if (path && path.length > 0) {
target = path[0];
}
// Normalize target for WebComponents v1 implementation (standard)
if (event.deepPath) {
path = event.deepPath();
if (path && path.length > 0) {
target = path[0];
}
}
return target;
}
/**
* Normalizes a native event object or just adds the event specific methods on a custom event.
*/
function fix(originalEvent, data) {
var name, event = data || {}, undef;
// Dummy function that gets replaced on the delegation state functions
function returnFalse() {
return false;
}
// Dummy function that gets replaced on the delegation state functions
function returnTrue() {
return true;
}
// Copy all properties from the original event
for (name in originalEvent) {
// layerX/layerY is deprecated in Chrome and produces a warning
if (!deprecated[name]) {
event[name] = originalEvent[name];
}
}
// Normalize target IE uses srcElement
if (!event.target) {
event.target = event.srcElement || document;
}
// Experimental shadow dom support
if (Env.experimentalShadowDom) {
event.target = getTargetFromShadowDom(originalEvent, event.target);
}
// Calculate pageX/Y if missing and clientX/Y available
if (originalEvent && mouseEventRe.test(originalEvent.type) && originalEvent.pageX === undef && originalEvent.clientX !== undef) {
var eventDoc = event.target.ownerDocument || document;
var doc = eventDoc.documentElement;
var body = eventDoc.body;
event.pageX = originalEvent.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) -
(doc && doc.clientLeft || body && body.clientLeft || 0);
event.pageY = originalEvent.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) -
(doc && doc.clientTop || body && body.clientTop || 0);
}
// Add preventDefault method
event.preventDefault = function() {
event.isDefaultPrevented = returnTrue;
// Execute preventDefault on the original event object
if (originalEvent) {
if (originalEvent.preventDefault) {
originalEvent.preventDefault();
} else {
originalEvent.returnValue = false; // IE
}
}
};
// Add stopPropagation
event.stopPropagation = function() {
event.isPropagationStopped = returnTrue;
// Execute stopPropagation on the original event object
if (originalEvent) {
if (originalEvent.stopPropagation) {
originalEvent.stopPropagation();
} else {
originalEvent.cancelBubble = true; // IE
}
}
};
// Add stopImmediatePropagation
event.stopImmediatePropagation = function() {
event.isImmediatePropagationStopped = returnTrue;
event.stopPropagation();
};
// Add event delegation states
if (!event.isDefaultPrevented) {
event.isDefaultPrevented = returnFalse;
event.isPropagationStopped = returnFalse;
event.isImmediatePropagationStopped = returnFalse;
}
// Add missing metaKey for IE 8
if (typeof event.metaKey == 'undefined') {
event.metaKey = false;
}
return event;
}
/**
* Bind a DOMContentLoaded event across browsers and executes the callback once the page DOM is initialized.
* It will also set/check the domLoaded state of the event_utils instance so ready isn't called multiple times.
*/
function bindOnReady(win, callback, eventUtils) {
var doc = win.document, event = {type: 'ready'};
if (eventUtils.domLoaded) {
callback(event);
return;
}
// Gets called when the DOM is ready
function readyHandler() {
if (!eventUtils.domLoaded) {
eventUtils.domLoaded = true;
callback(event);
}
}
function waitForDomLoaded() {
// Check complete or interactive state if there is a body
// element on some iframes IE 8 will produce a null body
if (doc.readyState === "complete" || (doc.readyState === "interactive" && doc.body)) {
removeEvent(doc, "readystatechange", waitForDomLoaded);
readyHandler();
}
}
function tryScroll() {
try {
// If IE is used, use the trick by Diego Perini licensed under MIT by request to the author.
// http://javascript.nwbox.com/IEContentLoaded/
doc.documentElement.doScroll("left");
} catch (ex) {
Delay.setTimeout(tryScroll);
return;
}
readyHandler();
}
// Use W3C method
if (doc.addEventListener) {
if (doc.readyState === "complete") {
readyHandler();
} else {
addEvent(win, 'DOMContentLoaded', readyHandler);
}
} else {
// Use IE method
addEvent(doc, "readystatechange", waitForDomLoaded);
// Wait until we can scroll, when we can the DOM is initialized
if (doc.documentElement.doScroll && win.self === win.top) {
tryScroll();
}
}
// Fallback if any of the above methods should fail for some odd reason
addEvent(win, 'load', readyHandler);
}
/**
* This class enables you to bind/unbind native events to elements and normalize it's behavior across browsers.
*/
function EventUtils() {
var self = this, events = {}, count, expando, hasFocusIn, hasMouseEnterLeave, mouseEnterLeave;
expando = eventExpandoPrefix + (+new Date()).toString(32);
hasMouseEnterLeave = "onmouseenter" in document.documentElement;
hasFocusIn = "onfocusin" in document.documentElement;
mouseEnterLeave = {mouseenter: 'mouseover', mouseleave: 'mouseout'};
count = 1;
// State if the DOMContentLoaded was executed or not
self.domLoaded = false;
self.events = events;
/**
* Executes all event handler callbacks for a specific event.
*
* @private
* @param {Event} evt Event object.
* @param {String} id Expando id value to look for.
*/
function executeHandlers(evt, id) {
var callbackList, i, l, callback, container = events[id];
callbackList = container && container[evt.type];
if (callbackList) {
for (i = 0, l = callbackList.length; i < l; i++) {
callback = callbackList[i];
// Check if callback exists might be removed if a unbind is called inside the callback
if (callback && callback.func.call(callback.scope, evt) === false) {
evt.preventDefault();
}
// Should we stop propagation to immediate listeners
if (evt.isImmediatePropagationStopped()) {
return;
}
}
}
}
/**
* Binds a callback to an event on the specified target.
*
* @method bind
* @param {Object} target Target node/window or custom object.
* @param {String} names Name of the event to bind.
* @param {function} callback Callback function to execute when the event occurs.
* @param {Object} scope Scope to call the callback function on, defaults to target.
* @return {function} Callback function that got bound.
*/
self.bind = function(target, names, callback, scope) {
var id, callbackList, i, name, fakeName, nativeHandler, capture, win = window;
// Native event handler function patches the event and executes the callbacks for the expando
function defaultNativeHandler(evt) {
executeHandlers(fix(evt || win.event), id);
}
// Don't bind to text nodes or comments
if (!target || target.nodeType === 3 || target.nodeType === 8) {
return;
}
// Create or get events id for the target
if (!target[expando]) {
id = count++;
target[expando] = id;
events[id] = {};
} else {
id = target[expando];
}
// Setup the specified scope or use the target as a default
scope = scope || target;
// Split names and bind each event, enables you to bind multiple events with one call
names = names.split(' ');
i = names.length;
while (i--) {
name = names[i];
nativeHandler = defaultNativeHandler;
fakeName = capture = false;
// Use ready instead of DOMContentLoaded
if (name === "DOMContentLoaded") {
name = "ready";
}
// DOM is already ready
if (self.domLoaded && name === "ready" && target.readyState == 'complete') {
callback.call(scope, fix({type: name}));
continue;
}
// Handle mouseenter/mouseleaver
if (!hasMouseEnterLeave) {
fakeName = mouseEnterLeave[name];
if (fakeName) {
nativeHandler = function(evt) {
var current, related;
current = evt.currentTarget;
related = evt.relatedTarget;
// Check if related is inside the current target if it's not then the event should
// be ignored since it's a mouseover/mouseout inside the element
if (related && current.contains) {
// Use contains for performance
related = current.contains(related);
} else {
while (related && related !== current) {
related = related.parentNode;
}
}
// Fire fake event
if (!related) {
evt = fix(evt || win.event);
evt.type = evt.type === 'mouseout' ? 'mouseleave' : 'mouseenter';
evt.target = current;
executeHandlers(evt, id);
}
};
}
}
// Fake bubbling of focusin/focusout
if (!hasFocusIn && (name === "focusin" || name === "focusout")) {
capture = true;
fakeName = name === "focusin" ? "focus" : "blur";
nativeHandler = function(evt) {
evt = fix(evt || win.event);
evt.type = evt.type === 'focus' ? 'focusin' : 'focusout';
executeHandlers(evt, id);
};
}
// Setup callback list and bind native event
callbackList = events[id][name];
if (!callbackList) {
events[id][name] = callbackList = [{func: callback, scope: scope}];
callbackList.fakeName = fakeName;
callbackList.capture = capture;
//callbackList.callback = callback;
// Add the nativeHandler to the callback list so that we can later unbind it
callbackList.nativeHandler = nativeHandler;
// Check if the target has native events support
if (name === "ready") {
bindOnReady(target, nativeHandler, self);
} else {
addEvent(target, fakeName || name, nativeHandler, capture);
}
} else {
if (name === "ready" && self.domLoaded) {
callback({type: name});
} else {
// If it already has an native handler then just push the callback
callbackList.push({func: callback, scope: scope});
}
}
}
target = callbackList = 0; // Clean memory for IE
return callback;
};
/**
* Unbinds the specified event by name, name and callback or all events on the target.
*
* @method unbind
* @param {Object} target Target node/window or custom object.
* @param {String} names Optional event name to unbind.
* @param {function} callback Optional callback function to unbind.
* @return {EventUtils} Event utils instance.
*/
self.unbind = function(target, names, callback) {
var id, callbackList, i, ci, name, eventMap;
// Don't bind to text nodes or comments
if (!target || target.nodeType === 3 || target.nodeType === 8) {
return self;
}
// Unbind event or events if the target has the expando
id = target[expando];
if (id) {
eventMap = events[id];
// Specific callback
if (names) {
names = names.split(' ');
i = names.length;
while (i--) {
name = names[i];
callbackList = eventMap[name];
// Unbind the event if it exists in the map
if (callbackList) {
// Remove specified callback
if (callback) {
ci = callbackList.length;
while (ci--) {
if (callbackList[ci].func === callback) {
var nativeHandler = callbackList.nativeHandler;
var fakeName = callbackList.fakeName, capture = callbackList.capture;
// Clone callbackList since unbind inside a callback would otherwise break the handlers loop
callbackList = callbackList.slice(0, ci).concat(callbackList.slice(ci + 1));
callbackList.nativeHandler = nativeHandler;
callbackList.fakeName = fakeName;
callbackList.capture = capture;
eventMap[name] = callbackList;
}
}
}
// Remove all callbacks if there isn't a specified callback or there is no callbacks left
if (!callback || callbackList.length === 0) {
delete eventMap[name];
removeEvent(target, callbackList.fakeName || name, callbackList.nativeHandler, callbackList.capture);
}
}
}
} else {
// All events for a specific element
for (name in eventMap) {
callbackList = eventMap[name];
removeEvent(target, callbackList.fakeName || name, callbackList.nativeHandler, callbackList.capture);
}
eventMap = {};
}
// Check if object is empty, if it isn't then we won't remove the expando map
for (name in eventMap) {
return self;
}
// Delete event object
delete events[id];
// Remove expando from target
try {
// IE will fail here since it can't delete properties from window
delete target[expando];
} catch (ex) {
// IE will set it to null
target[expando] = null;
}
}
return self;
};
/**
* Fires the specified event on the specified target.
*
* @method fire
* @param {Object} target Target node/window or custom object.
* @param {String} name Event name to fire.
* @param {Object} args Optional arguments to send to the observers.
* @return {EventUtils} Event utils instance.
*/
self.fire = function(target, name, args) {
var id;
// Don't bind to text nodes or comments
if (!target || target.nodeType === 3 || target.nodeType === 8) {
return self;
}
// Build event object by patching the args
args = fix(null, args);
args.type = name;
args.target = target;
do {
// Found an expando that means there is listeners to execute
id = target[expando];
if (id) {
executeHandlers(args, id);
}
// Walk up the DOM
target = target.parentNode || target.ownerDocument || target.defaultView || target.parentWindow;
} while (target && !args.isPropagationStopped());
return self;
};
/**
* Removes all bound event listeners for the specified target. This will also remove any bound
* listeners to child nodes within that target.
*
* @method clean
* @param {Object} target Target node/window object.
* @return {EventUtils} Event utils instance.
*/
self.clean = function(target) {
var i, children, unbind = self.unbind;
// Don't bind to text nodes or comments
if (!target || target.nodeType === 3 || target.nodeType === 8) {
return self;
}
// Unbind any element on the specified target
if (target[expando]) {
unbind(target);
}
// Target doesn't have getElementsByTagName it's probably a window object then use it's document to find the children
if (!target.getElementsByTagName) {
target = target.document;
}
// Remove events from each child element
if (target && target.getElementsByTagName) {
unbind(target);
children = target.getElementsByTagName('*');
i = children.length;
while (i--) {
target = children[i];
if (target[expando]) {
unbind(target);
}
}
}
return self;
};
/**
* Destroys the event object. Call this on IE to remove memory leaks.
*/
self.destroy = function() {
events = {};
};
// Legacy function for canceling events
self.cancel = function(e) {
if (e) {
e.preventDefault();
e.stopImmediatePropagation();
}
return false;
};
}
EventUtils.Event = new EventUtils();
EventUtils.Event.bind(window, 'ready', function() {});
return EventUtils;
});
// Included from: js/tinymce/classes/dom/Sizzle.js
/**
* Sizzle.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*
* @ignore-file
*/
/*jshint bitwise:false, expr:true, noempty:false, sub:true, eqnull:true, latedef:false, maxlen:255 */
/*eslint-disable */
/**
* Sizzle CSS Selector Engine v@VERSION
* http://sizzlejs.com/
*
* Copyright 2008, 2014 jQuery Foundation, Inc. and other contributors
* Released under the MIT license
* http://jquery.org/license
*
* Date: @DATE
*/
define("tinymce/dom/Sizzle", [], function() {
var i,
support,
Expr,
getText,
isXML,
tokenize,
compile,
select,
outermostContext,
sortInput,
hasDuplicate,
// Local document vars
setDocument,
document,
docElem,
documentIsHTML,
rbuggyQSA,
rbuggyMatches,
matches,
contains,
// Instance-specific data
expando = "sizzle" + -(new Date()),
preferredDoc = window.document,
dirruns = 0,
done = 0,
classCache = createCache(),
tokenCache = createCache(),
compilerCache = createCache(),
sortOrder = function( a, b ) {
if ( a === b ) {
hasDuplicate = true;
}
return 0;
},
// General-purpose constants
strundefined = typeof undefined,
MAX_NEGATIVE = 1 << 31,
// Instance methods
hasOwn = ({}).hasOwnProperty,
arr = [],
pop = arr.pop,
push_native = arr.push,
push = arr.push,
slice = arr.slice,
// Use a stripped-down indexOf if we can't use a native one
indexOf = arr.indexOf || function( elem ) {
var i = 0,
len = this.length;
for ( ; i < len; i++ ) {
if ( this[i] === elem ) {
return i;
}
}
return -1;
},
booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",
// Regular expressions
// http://www.w3.org/TR/css3-selectors/#whitespace
whitespace = "[\\x20\\t\\r\\n\\f]",
// http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
identifier = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",
// Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors
attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace +
// Operator (capture 2)
"*([*^$|!~]?=)" + whitespace +
// "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]"
"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace +
"*\\]",
pseudos = ":(" + identifier + ")(?:\\((" +
// To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:
// 1. quoted (capture 3; capture 4 or capture 5)
"('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" +
// 2. simple (capture 6)
"((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" +
// 3. anything else (capture 2)
".*" +
")\\)|)",
// Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ),
rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ),
rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ),
rpseudo = new RegExp( pseudos ),
ridentifier = new RegExp( "^" + identifier + "$" ),
matchExpr = {
"ID": new RegExp( "^#(" + identifier + ")" ),
"CLASS": new RegExp( "^\\.(" + identifier + ")" ),
"TAG": new RegExp( "^(" + identifier + "|[*])" ),
"ATTR": new RegExp( "^" + attributes ),
"PSEUDO": new RegExp( "^" + pseudos ),
"CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace +
"*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace +
"*(\\d+)|))" + whitespace + "*\\)|)", "i" ),
"bool": new RegExp( "^(?:" + booleans + ")$", "i" ),
// For use in libraries implementing .is()
// We use this for POS matching in `select`
"needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" +
whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" )
},
rinputs = /^(?:input|select|textarea|button)$/i,
rheader = /^h\d$/i,
rnative = /^[^{]+\{\s*\[native \w/,
// Easily-parseable/retrievable ID or TAG or CLASS selectors
rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,
rsibling = /[+~]/,
rescape = /'|\\/g,
// CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ),
funescape = function( _, escaped, escapedWhitespace ) {
var high = "0x" + escaped - 0x10000;
// NaN means non-codepoint
// Support: Firefox<24
// Workaround erroneous numeric interpretation of +"0x"
return high !== high || escapedWhitespace ?
escaped :
high < 0 ?
// BMP codepoint
String.fromCharCode( high + 0x10000 ) :
// Supplemental Plane codepoint (surrogate pair)
String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
};
// Optimize for push.apply( _, NodeList )
try {
push.apply(
(arr = slice.call( preferredDoc.childNodes )),
preferredDoc.childNodes
);
// Support: Android<4.0
// Detect silently failing push.apply
arr[ preferredDoc.childNodes.length ].nodeType;
} catch ( e ) {
push = { apply: arr.length ?
// Leverage slice if possible
function( target, els ) {
push_native.apply( target, slice.call(els) );
} :
// Support: IE<9
// Otherwise append directly
function( target, els ) {
var j = target.length,
i = 0;
// Can't trust NodeList.length
while ( (target[j++] = els[i++]) ) {}
target.length = j - 1;
}
};
}
function Sizzle( selector, context, results, seed ) {
var match, elem, m, nodeType,
// QSA vars
i, groups, old, nid, newContext, newSelector;
if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {
setDocument( context );
}
context = context || document;
results = results || [];
if ( !selector || typeof selector !== "string" ) {
return results;
}
if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) {
return [];
}
if ( documentIsHTML && !seed ) {
// Shortcuts
if ( (match = rquickExpr.exec( selector )) ) {
// Speed-up: Sizzle("#ID")
if ( (m = match[1]) ) {
if ( nodeType === 9 ) {
elem = context.getElementById( m );
// Check parentNode to catch when Blackberry 4.6 returns
// nodes that are no longer in the document (jQuery #6963)
if ( elem && elem.parentNode ) {
// Handle the case where IE, Opera, and Webkit return items
// by name instead of ID
if ( elem.id === m ) {
results.push( elem );
return results;
}
} else {
return results;
}
} else {
// Context is not a document
if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) &&
contains( context, elem ) && elem.id === m ) {
results.push( elem );
return results;
}
}
// Speed-up: Sizzle("TAG")
} else if ( match[2] ) {
push.apply( results, context.getElementsByTagName( selector ) );
return results;
// Speed-up: Sizzle(".CLASS")
} else if ( (m = match[3]) && support.getElementsByClassName ) {
push.apply( results, context.getElementsByClassName( m ) );
return results;
}
}
// QSA path
if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) {
nid = old = expando;
newContext = context;
newSelector = nodeType === 9 && selector;
// qSA works strangely on Element-rooted queries
// We can work around this by specifying an extra ID on the root
// and working up from there (Thanks to Andrew Dupont for the technique)
// IE 8 doesn't work on object elements
if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) {
groups = tokenize( selector );
if ( (old = context.getAttribute("id")) ) {
nid = old.replace( rescape, "\\$&" );
} else {
context.setAttribute( "id", nid );
}
nid = "[id='" + nid + "'] ";
i = groups.length;
while ( i-- ) {
groups[i] = nid + toSelector( groups[i] );
}
newContext = rsibling.test( selector ) && testContext( context.parentNode ) || context;
newSelector = groups.join(",");
}
if ( newSelector ) {
try {
push.apply( results,
newContext.querySelectorAll( newSelector )
);
return results;
} catch(qsaError) {
} finally {
if ( !old ) {
context.removeAttribute("id");
}
}
}
}
}
// All others
return select( selector.replace( rtrim, "$1" ), context, results, seed );
}
/**
* Create key-value caches of limited size
* @returns {Function(string, Object)} Returns the Object data after storing it on itself with
* property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)
* deleting the oldest entry
*/
function createCache() {
var keys = [];
function cache( key, value ) {
// Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
if ( keys.push( key + " " ) > Expr.cacheLength ) {
// Only keep the most recent entries
delete cache[ keys.shift() ];
}
return (cache[ key + " " ] = value);
}
return cache;
}
/**
* Mark a function for special use by Sizzle
* @param {Function} fn The function to mark
*/
function markFunction( fn ) {
fn[ expando ] = true;
return fn;
}
/**
* Support testing using an element
* @param {Function} fn Passed the created div and expects a boolean result
*/
function assert( fn ) {
var div = document.createElement("div");
try {
return !!fn( div );
} catch (e) {
return false;
} finally {
// Remove from its parent by default
if ( div.parentNode ) {
div.parentNode.removeChild( div );
}
// release memory in IE
div = null;
}
}
/**
* Adds the same handler for all of the specified attrs
* @param {String} attrs Pipe-separated list of attributes
* @param {Function} handler The method that will be applied
*/
function addHandle( attrs, handler ) {
var arr = attrs.split("|"),
i = attrs.length;
while ( i-- ) {
Expr.attrHandle[ arr[i] ] = handler;
}
}
/**
* Checks document order of two siblings
* @param {Element} a
* @param {Element} b
* @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b
*/
function siblingCheck( a, b ) {
var cur = b && a,
diff = cur && a.nodeType === 1 && b.nodeType === 1 &&
( ~b.sourceIndex || MAX_NEGATIVE ) -
( ~a.sourceIndex || MAX_NEGATIVE );
// Use IE sourceIndex if available on both nodes
if ( diff ) {
return diff;
}
// Check if b follows a
if ( cur ) {
while ( (cur = cur.nextSibling) ) {
if ( cur === b ) {
return -1;
}
}
}
return a ? 1 : -1;
}
/**
* Returns a function to use in pseudos for input types
* @param {String} type
*/
function createInputPseudo( type ) {
return function( elem ) {
var name = elem.nodeName.toLowerCase();
return name === "input" && elem.type === type;
};
}
/**
* Returns a function to use in pseudos for buttons
* @param {String} type
*/
function createButtonPseudo( type ) {
return function( elem ) {
var name = elem.nodeName.toLowerCase();
return (name === "input" || name === "button") && elem.type === type;
};
}
/**
* Returns a function to use in pseudos for positionals
* @param {Function} fn
*/
function createPositionalPseudo( fn ) {
return markFunction(function( argument ) {
argument = +argument;
return markFunction(function( seed, matches ) {
var j,
matchIndexes = fn( [], seed.length, argument ),
i = matchIndexes.length;
// Match elements found at the specified indexes
while ( i-- ) {
if ( seed[ (j = matchIndexes[i]) ] ) {
seed[j] = !(matches[j] = seed[j]);
}
}
});
});
}
/**
* Checks a node for validity as a Sizzle context
* @param {Element|Object=} context
* @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value
*/
function testContext( context ) {
return context && typeof context.getElementsByTagName !== strundefined && context;
}
// Expose support vars for convenience
support = Sizzle.support = {};
/**
* Detects XML nodes
* @param {Element|Object} elem An element or a document
* @returns {Boolean} True iff elem is a non-HTML XML node
*/
isXML = Sizzle.isXML = function( elem ) {
// documentElement is verified for cases where it doesn't yet exist
// (such as loading iframes in IE - #4833)
var documentElement = elem && (elem.ownerDocument || elem).documentElement;
return documentElement ? documentElement.nodeName !== "HTML" : false;
};
/**
* Sets document-related variables once based on the current document
* @param {Element|Object} [doc] An element or document object to use to set the document
* @returns {Object} Returns the current document
*/
setDocument = Sizzle.setDocument = function( node ) {
var hasCompare,
doc = node ? node.ownerDocument || node : preferredDoc,
parent = doc.defaultView;
function getTop(win) {
// Edge throws a lovely Object expected if you try to get top on a detached reference see #2642
try {
return win.top;
} catch (ex) {
// Ignore
}
return null;
}
// If no document and documentElement is available, return
if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {
return document;
}
// Set our document
document = doc;
docElem = doc.documentElement;
// Support tests
documentIsHTML = !isXML( doc );
// Support: IE>8
// If iframe document is assigned to "document" variable and if iframe has been reloaded,
// IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936
// IE6-8 do not support the defaultView property so parent will be undefined
if ( parent && parent !== getTop(parent) ) {
// IE11 does not have attachEvent, so all must suffer
if ( parent.addEventListener ) {
parent.addEventListener( "unload", function() {
setDocument();
}, false );
} else if ( parent.attachEvent ) {
parent.attachEvent( "onunload", function() {
setDocument();
});
}
}
/* Attributes
---------------------------------------------------------------------- */
// Support: IE<8
// Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans)
support.attributes = assert(function( div ) {
div.className = "i";
return !div.getAttribute("className");
});
/* getElement(s)By*
---------------------------------------------------------------------- */
// Check if getElementsByTagName("*") returns only elements
support.getElementsByTagName = assert(function( div ) {
div.appendChild( doc.createComment("") );
return !div.getElementsByTagName("*").length;
});
// Support: IE<9
support.getElementsByClassName = rnative.test( doc.getElementsByClassName );
// Support: IE<10
// Check if getElementById returns elements by name
// The broken getElementById methods don't pick up programatically-set names,
// so use a roundabout getElementsByName test
support.getById = assert(function( div ) {
docElem.appendChild( div ).id = expando;
return !doc.getElementsByName || !doc.getElementsByName( expando ).length;
});
// ID find and filter
if ( support.getById ) {
Expr.find["ID"] = function( id, context ) {
if ( typeof context.getElementById !== strundefined && documentIsHTML ) {
var m = context.getElementById( id );
// Check parentNode to catch when Blackberry 4.6 returns
// nodes that are no longer in the document #6963
return m && m.parentNode ? [ m ] : [];
}
};
Expr.filter["ID"] = function( id ) {
var attrId = id.replace( runescape, funescape );
return function( elem ) {
return elem.getAttribute("id") === attrId;
};
};
} else {
// Support: IE6/7
// getElementById is not reliable as a find shortcut
delete Expr.find["ID"];
Expr.filter["ID"] = function( id ) {
var attrId = id.replace( runescape, funescape );
return function( elem ) {
var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id");
return node && node.value === attrId;
};
};
}
// Tag
Expr.find["TAG"] = support.getElementsByTagName ?
function( tag, context ) {
if ( typeof context.getElementsByTagName !== strundefined ) {
return context.getElementsByTagName( tag );
}
} :
function( tag, context ) {
var elem,
tmp = [],
i = 0,
results = context.getElementsByTagName( tag );
// Filter out possible comments
if ( tag === "*" ) {
while ( (elem = results[i++]) ) {
if ( elem.nodeType === 1 ) {
tmp.push( elem );
}
}
return tmp;
}
return results;
};
// Class
Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) {
if ( documentIsHTML ) {
return context.getElementsByClassName( className );
}
};
/* QSA/matchesSelector
---------------------------------------------------------------------- */
// QSA and matchesSelector support
// matchesSelector(:active) reports false when true (IE9/Opera 11.5)
rbuggyMatches = [];
// qSa(:focus) reports false when true (Chrome 21)
// We allow this because of a bug in IE8/9 that throws an error
// whenever `document.activeElement` is accessed on an iframe
// So, we allow :focus to pass through QSA all the time to avoid the IE error
// See http://bugs.jquery.com/ticket/13378
rbuggyQSA = [];
if ( (support.qsa = rnative.test( doc.querySelectorAll )) ) {
// Build QSA regex
// Regex strategy adopted from Diego Perini
assert(function( div ) {
// Select is set to empty string on purpose
// This is to test IE's treatment of not explicitly
// setting a boolean content attribute,
// since its presence should be enough
// http://bugs.jquery.com/ticket/12359
div.innerHTML = "<select msallowcapture=''><option selected=''></option></select>";
// Support: IE8, Opera 11-12.16
// Nothing should be selected when empty strings follow ^= or $= or *=
// The test attribute must be unknown in Opera but "safe" for WinRT
// http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section
if ( div.querySelectorAll("[msallowcapture^='']").length ) {
rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" );
}
// Support: IE8
// Boolean attributes and "value" are not treated correctly
if ( !div.querySelectorAll("[selected]").length ) {
rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" );
}
// Webkit/Opera - :checked should return selected option elements
// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
// IE8 throws error here and will not see later tests
if ( !div.querySelectorAll(":checked").length ) {
rbuggyQSA.push(":checked");
}
});
assert(function( div ) {
// Support: Windows 8 Native Apps
// The type and name attributes are restricted during .innerHTML assignment
var input = doc.createElement("input");
input.setAttribute( "type", "hidden" );
div.appendChild( input ).setAttribute( "name", "D" );
// Support: IE8
// Enforce case-sensitivity of name attribute
if ( div.querySelectorAll("[name=d]").length ) {
rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" );
}
// FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
// IE8 throws error here and will not see later tests
if ( !div.querySelectorAll(":enabled").length ) {
rbuggyQSA.push( ":enabled", ":disabled" );
}
// Opera 10-11 does not throw on post-comma invalid pseudos
div.querySelectorAll("*,:x");
rbuggyQSA.push(",.*:");
});
}
if ( (support.matchesSelector = rnative.test( (matches = docElem.matches ||
docElem.webkitMatchesSelector ||
docElem.mozMatchesSelector ||
docElem.oMatchesSelector ||
docElem.msMatchesSelector) )) ) {
assert(function( div ) {
// Check to see if it's possible to do matchesSelector
// on a disconnected node (IE 9)
support.disconnectedMatch = matches.call( div, "div" );
// This should fail with an exception
// Gecko does not error, returns false instead
matches.call( div, "[s!='']:x" );
rbuggyMatches.push( "!=", pseudos );
});
}
rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") );
rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") );
/* Contains
---------------------------------------------------------------------- */
hasCompare = rnative.test( docElem.compareDocumentPosition );
// Element contains another
// Purposefully does not implement inclusive descendent
// As in, an element does not contain itself
contains = hasCompare || rnative.test( docElem.contains ) ?
function( a, b ) {
var adown = a.nodeType === 9 ? a.documentElement : a,
bup = b && b.parentNode;
return a === bup || !!( bup && bup.nodeType === 1 && (
adown.contains ?
adown.contains( bup ) :
a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
));
} :
function( a, b ) {
if ( b ) {
while ( (b = b.parentNode) ) {
if ( b === a ) {
return true;
}
}
}
return false;
};
/* Sorting
---------------------------------------------------------------------- */
// Document order sorting
sortOrder = hasCompare ?
function( a, b ) {
// Flag for duplicate removal
if ( a === b ) {
hasDuplicate = true;
return 0;
}
// Sort on method existence if only one input has compareDocumentPosition
var compare = !a.compareDocumentPosition - !b.compareDocumentPosition;
if ( compare ) {
return compare;
}
// Calculate position if both inputs belong to the same document
compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ?
a.compareDocumentPosition( b ) :
// Otherwise we know they are disconnected
1;
// Disconnected nodes
if ( compare & 1 ||
(!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) {
// Choose the first element that is related to our preferred document
if ( a === doc || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) {
return -1;
}
if ( b === doc || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) {
return 1;
}
// Maintain original order
return sortInput ?
( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :
0;
}
return compare & 4 ? -1 : 1;
} :
function( a, b ) {
// Exit early if the nodes are identical
if ( a === b ) {
hasDuplicate = true;
return 0;
}
var cur,
i = 0,
aup = a.parentNode,
bup = b.parentNode,
ap = [ a ],
bp = [ b ];
// Parentless nodes are either documents or disconnected
if ( !aup || !bup ) {
return a === doc ? -1 :
b === doc ? 1 :
aup ? -1 :
bup ? 1 :
sortInput ?
( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :
0;
// If the nodes are siblings, we can do a quick check
} else if ( aup === bup ) {
return siblingCheck( a, b );
}
// Otherwise we need full lists of their ancestors for comparison
cur = a;
while ( (cur = cur.parentNode) ) {
ap.unshift( cur );
}
cur = b;
while ( (cur = cur.parentNode) ) {
bp.unshift( cur );
}
// Walk down the tree looking for a discrepancy
while ( ap[i] === bp[i] ) {
i++;
}
return i ?
// Do a sibling check if the nodes have a common ancestor
siblingCheck( ap[i], bp[i] ) :
// Otherwise nodes in our document sort first
ap[i] === preferredDoc ? -1 :
bp[i] === preferredDoc ? 1 :
0;
};
return doc;
};
Sizzle.matches = function( expr, elements ) {
return Sizzle( expr, null, null, elements );
};
Sizzle.matchesSelector = function( elem, expr ) {
// Set document vars if needed
if ( ( elem.ownerDocument || elem ) !== document ) {
setDocument( elem );
}
// Make sure that attribute selectors are quoted
expr = expr.replace( rattributeQuotes, "='$1']" );
if ( support.matchesSelector && documentIsHTML &&
( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&
( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) {
try {
var ret = matches.call( elem, expr );
// IE 9's matchesSelector returns false on disconnected nodes
if ( ret || support.disconnectedMatch ||
// As well, disconnected nodes are said to be in a document
// fragment in IE 9
elem.document && elem.document.nodeType !== 11 ) {
return ret;
}
} catch(e) {}
}
return Sizzle( expr, document, null, [ elem ] ).length > 0;
};
Sizzle.contains = function( context, elem ) {
// Set document vars if needed
if ( ( context.ownerDocument || context ) !== document ) {
setDocument( context );
}
return contains( context, elem );
};
Sizzle.attr = function( elem, name ) {
// Set document vars if needed
if ( ( elem.ownerDocument || elem ) !== document ) {
setDocument( elem );
}
var fn = Expr.attrHandle[ name.toLowerCase() ],
// Don't get fooled by Object.prototype properties (jQuery #13807)
val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?
fn( elem, name, !documentIsHTML ) :
undefined;
return val !== undefined ?
val :
support.attributes || !documentIsHTML ?
elem.getAttribute( name ) :
(val = elem.getAttributeNode(name)) && val.specified ?
val.value :
null;
};
Sizzle.error = function( msg ) {
throw new Error( "Syntax error, unrecognized expression: " + msg );
};
/**
* Document sorting and removing duplicates
* @param {ArrayLike} results
*/
Sizzle.uniqueSort = function( results ) {
var elem,
duplicates = [],
j = 0,
i = 0;
// Unless we *know* we can detect duplicates, assume their presence
hasDuplicate = !support.detectDuplicates;
sortInput = !support.sortStable && results.slice( 0 );
results.sort( sortOrder );
if ( hasDuplicate ) {
while ( (elem = results[i++]) ) {
if ( elem === results[ i ] ) {
j = duplicates.push( i );
}
}
while ( j-- ) {
results.splice( duplicates[ j ], 1 );
}
}
// Clear input after sorting to release objects
// See https://github.com/jquery/sizzle/pull/225
sortInput = null;
return results;
};
/**
* Utility function for retrieving the text value of an array of DOM nodes
* @param {Array|Element} elem
*/
getText = Sizzle.getText = function( elem ) {
var node,
ret = "",
i = 0,
nodeType = elem.nodeType;
if ( !nodeType ) {
// If no nodeType, this is expected to be an array
while ( (node = elem[i++]) ) {
// Do not traverse comment nodes
ret += getText( node );
}
} else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
// Use textContent for elements
// innerText usage removed for consistency of new lines (jQuery #11153)
if ( typeof elem.textContent === "string" ) {
return elem.textContent;
} else {
// Traverse its children
for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
ret += getText( elem );
}
}
} else if ( nodeType === 3 || nodeType === 4 ) {
return elem.nodeValue;
}
// Do not include comment or processing instruction nodes
return ret;
};
Expr = Sizzle.selectors = {
// Can be adjusted by the user
cacheLength: 50,
createPseudo: markFunction,
match: matchExpr,
attrHandle: {},
find: {},
relative: {
">": { dir: "parentNode", first: true },
" ": { dir: "parentNode" },
"+": { dir: "previousSibling", first: true },
"~": { dir: "previousSibling" }
},
preFilter: {
"ATTR": function( match ) {
match[1] = match[1].replace( runescape, funescape );
// Move the given value to match[3] whether quoted or unquoted
match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape );
if ( match[2] === "~=" ) {
match[3] = " " + match[3] + " ";
}
return match.slice( 0, 4 );
},
"CHILD": function( match ) {
/* matches from matchExpr["CHILD"]
1 type (only|nth|...)
2 what (child|of-type)
3 argument (even|odd|\d*|\d*n([+-]\d+)?|...)
4 xn-component of xn+y argument ([+-]?\d*n|)
5 sign of xn-component
6 x of xn-component
7 sign of y-component
8 y of y-component
*/
match[1] = match[1].toLowerCase();
if ( match[1].slice( 0, 3 ) === "nth" ) {
// nth-* requires argument
if ( !match[3] ) {
Sizzle.error( match[0] );
}
// numeric x and y parameters for Expr.filter.CHILD
// remember that false/true cast respectively to 0/1
match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) );
match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" );
// other types prohibit arguments
} else if ( match[3] ) {
Sizzle.error( match[0] );
}
return match;
},
"PSEUDO": function( match ) {
var excess,
unquoted = !match[6] && match[2];
if ( matchExpr["CHILD"].test( match[0] ) ) {
return null;
}
// Accept quoted arguments as-is
if ( match[3] ) {
match[2] = match[4] || match[5] || "";
// Strip excess characters from unquoted arguments
} else if ( unquoted && rpseudo.test( unquoted ) &&
// Get excess from tokenize (recursively)
(excess = tokenize( unquoted, true )) &&
// advance to the next closing parenthesis
(excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) {
// excess is a negative index
match[0] = match[0].slice( 0, excess );
match[2] = unquoted.slice( 0, excess );
}
// Return only captures needed by the pseudo filter method (type and argument)
return match.slice( 0, 3 );
}
},
filter: {
"TAG": function( nodeNameSelector ) {
var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();
return nodeNameSelector === "*" ?
function() { return true; } :
function( elem ) {
return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
};
},
"CLASS": function( className ) {
var pattern = classCache[ className + " " ];
return pattern ||
(pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) &&
classCache( className, function( elem ) {
return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute("class") || "" );
});
},
"ATTR": function( name, operator, check ) {
return function( elem ) {
var result = Sizzle.attr( elem, name );
if ( result == null ) {
return operator === "!=";
}
if ( !operator ) {
return true;
}
result += "";
return operator === "=" ? result === check :
operator === "!=" ? result !== check :
operator === "^=" ? check && result.indexOf( check ) === 0 :
operator === "*=" ? check && result.indexOf( check ) > -1 :
operator === "$=" ? check && result.slice( -check.length ) === check :
operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 :
operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" :
false;
};
},
"CHILD": function( type, what, argument, first, last ) {
var simple = type.slice( 0, 3 ) !== "nth",
forward = type.slice( -4 ) !== "last",
ofType = what === "of-type";
return first === 1 && last === 0 ?
// Shortcut for :nth-*(n)
function( elem ) {
return !!elem.parentNode;
} :
function( elem, context, xml ) {
var cache, outerCache, node, diff, nodeIndex, start,
dir = simple !== forward ? "nextSibling" : "previousSibling",
parent = elem.parentNode,
name = ofType && elem.nodeName.toLowerCase(),
useCache = !xml && !ofType;
if ( parent ) {
// :(first|last|only)-(child|of-type)
if ( simple ) {
while ( dir ) {
node = elem;
while ( (node = node[ dir ]) ) {
if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) {
return false;
}
}
// Reverse direction for :only-* (if we haven't yet done so)
start = dir = type === "only" && !start && "nextSibling";
}
return true;
}
start = [ forward ? parent.firstChild : parent.lastChild ];
// non-xml :nth-child(...) stores cache data on `parent`
if ( forward && useCache ) {
// Seek `elem` from a previously-cached index
outerCache = parent[ expando ] || (parent[ expando ] = {});
cache = outerCache[ type ] || [];
nodeIndex = cache[0] === dirruns && cache[1];
diff = cache[0] === dirruns && cache[2];
node = nodeIndex && parent.childNodes[ nodeIndex ];
while ( (node = ++nodeIndex && node && node[ dir ] ||
// Fallback to seeking `elem` from the start
(diff = nodeIndex = 0) || start.pop()) ) {
// When found, cache indexes on `parent` and break
if ( node.nodeType === 1 && ++diff && node === elem ) {
outerCache[ type ] = [ dirruns, nodeIndex, diff ];
break;
}
}
// Use previously-cached element index if available
} else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) {
diff = cache[1];
// xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...)
} else {
// Use the same loop as above to seek `elem` from the start
while ( (node = ++nodeIndex && node && node[ dir ] ||
(diff = nodeIndex = 0) || start.pop()) ) {
if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) {
// Cache the index of each encountered element
if ( useCache ) {
(node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ];
}
if ( node === elem ) {
break;
}
}
}
}
// Incorporate the offset, then check against cycle size
diff -= last;
return diff === first || ( diff % first === 0 && diff / first >= 0 );
}
};
},
"PSEUDO": function( pseudo, argument ) {
// pseudo-class names are case-insensitive
// http://www.w3.org/TR/selectors/#pseudo-classes
// Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
// Remember that setFilters inherits from pseudos
var args,
fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||
Sizzle.error( "unsupported pseudo: " + pseudo );
// The user may use createPseudo to indicate that
// arguments are needed to create the filter function
// just as Sizzle does
if ( fn[ expando ] ) {
return fn( argument );
}
// But maintain support for old signatures
if ( fn.length > 1 ) {
args = [ pseudo, pseudo, "", argument ];
return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?
markFunction(function( seed, matches ) {
var idx,
matched = fn( seed, argument ),
i = matched.length;
while ( i-- ) {
idx = indexOf.call( seed, matched[i] );
seed[ idx ] = !( matches[ idx ] = matched[i] );
}
}) :
function( elem ) {
return fn( elem, 0, args );
};
}
return fn;
}
},
pseudos: {
// Potentially complex pseudos
"not": markFunction(function( selector ) {
// Trim the selector passed to compile
// to avoid treating leading and trailing
// spaces as combinators
var input = [],
results = [],
matcher = compile( selector.replace( rtrim, "$1" ) );
return matcher[ expando ] ?
markFunction(function( seed, matches, context, xml ) {
var elem,
unmatched = matcher( seed, null, xml, [] ),
i = seed.length;
// Match elements unmatched by `matcher`
while ( i-- ) {
if ( (elem = unmatched[i]) ) {
seed[i] = !(matches[i] = elem);
}
}
}) :
function( elem, context, xml ) {
input[0] = elem;
matcher( input, null, xml, results );
return !results.pop();
};
}),
"has": markFunction(function( selector ) {
return function( elem ) {
return Sizzle( selector, elem ).length > 0;
};
}),
"contains": markFunction(function( text ) {
text = text.replace( runescape, funescape );
return function( elem ) {
return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;
};
}),
// "Whether an element is represented by a :lang() selector
// is based solely on the element's language value
// being equal to the identifier C,
// or beginning with the identifier C immediately followed by "-".
// The matching of C against the element's language value is performed case-insensitively.
// The identifier C does not have to be a valid language name."
// http://www.w3.org/TR/selectors/#lang-pseudo
"lang": markFunction( function( lang ) {
// lang value must be a valid identifier
if ( !ridentifier.test(lang || "") ) {
Sizzle.error( "unsupported lang: " + lang );
}
lang = lang.replace( runescape, funescape ).toLowerCase();
return function( elem ) {
var elemLang;
do {
if ( (elemLang = documentIsHTML ?
elem.lang :
elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) {
elemLang = elemLang.toLowerCase();
return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0;
}
} while ( (elem = elem.parentNode) && elem.nodeType === 1 );
return false;
};
}),
// Miscellaneous
"target": function( elem ) {
var hash = window.location && window.location.hash;
return hash && hash.slice( 1 ) === elem.id;
},
"root": function( elem ) {
return elem === docElem;
},
"focus": function( elem ) {
return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);
},
// Boolean properties
"enabled": function( elem ) {
return elem.disabled === false;
},
"disabled": function( elem ) {
return elem.disabled === true;
},
"checked": function( elem ) {
// In CSS3, :checked should return both checked and selected elements
// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
var nodeName = elem.nodeName.toLowerCase();
return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected);
},
"selected": function( elem ) {
// Accessing this property makes selected-by-default
// options in Safari work properly
if ( elem.parentNode ) {
elem.parentNode.selectedIndex;
}
return elem.selected === true;
},
// Contents
"empty": function( elem ) {
// http://www.w3.org/TR/selectors/#empty-pseudo
// :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),
// but not by others (comment: 8; processing instruction: 7; etc.)
// nodeType < 6 works because attributes (2) do not appear as children
for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
if ( elem.nodeType < 6 ) {
return false;
}
}
return true;
},
"parent": function( elem ) {
return !Expr.pseudos["empty"]( elem );
},
// Element/input types
"header": function( elem ) {
return rheader.test( elem.nodeName );
},
"input": function( elem ) {
return rinputs.test( elem.nodeName );
},
"button": function( elem ) {
var name = elem.nodeName.toLowerCase();
return name === "input" && elem.type === "button" || name === "button";
},
"text": function( elem ) {
var attr;
return elem.nodeName.toLowerCase() === "input" &&
elem.type === "text" &&
// Support: IE<8
// New HTML5 attribute values (e.g., "search") appear with elem.type === "text"
( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" );
},
// Position-in-collection
"first": createPositionalPseudo(function() {
return [ 0 ];
}),
"last": createPositionalPseudo(function( matchIndexes, length ) {
return [ length - 1 ];
}),
"eq": createPositionalPseudo(function( matchIndexes, length, argument ) {
return [ argument < 0 ? argument + length : argument ];
}),
"even": createPositionalPseudo(function( matchIndexes, length ) {
var i = 0;
for ( ; i < length; i += 2 ) {
matchIndexes.push( i );
}
return matchIndexes;
}),
"odd": createPositionalPseudo(function( matchIndexes, length ) {
var i = 1;
for ( ; i < length; i += 2 ) {
matchIndexes.push( i );
}
return matchIndexes;
}),
"lt": createPositionalPseudo(function( matchIndexes, length, argument ) {
var i = argument < 0 ? argument + length : argument;
for ( ; --i >= 0; ) {
matchIndexes.push( i );
}
return matchIndexes;
}),
"gt": createPositionalPseudo(function( matchIndexes, length, argument ) {
var i = argument < 0 ? argument + length : argument;
for ( ; ++i < length; ) {
matchIndexes.push( i );
}
return matchIndexes;
})
}
};
Expr.pseudos["nth"] = Expr.pseudos["eq"];
// Add button/input type pseudos
for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {
Expr.pseudos[ i ] = createInputPseudo( i );
}
for ( i in { submit: true, reset: true } ) {
Expr.pseudos[ i ] = createButtonPseudo( i );
}
// Easy API for creating new setFilters
function setFilters() {}
setFilters.prototype = Expr.filters = Expr.pseudos;
Expr.setFilters = new setFilters();
tokenize = Sizzle.tokenize = function( selector, parseOnly ) {
var matched, match, tokens, type,
soFar, groups, preFilters,
cached = tokenCache[ selector + " " ];
if ( cached ) {
return parseOnly ? 0 : cached.slice( 0 );
}
soFar = selector;
groups = [];
preFilters = Expr.preFilter;
while ( soFar ) {
// Comma and first run
if ( !matched || (match = rcomma.exec( soFar )) ) {
if ( match ) {
// Don't consume trailing commas as valid
soFar = soFar.slice( match[0].length ) || soFar;
}
groups.push( (tokens = []) );
}
matched = false;
// Combinators
if ( (match = rcombinators.exec( soFar )) ) {
matched = match.shift();
tokens.push({
value: matched,
// Cast descendant combinators to space
type: match[0].replace( rtrim, " " )
});
soFar = soFar.slice( matched.length );
}
// Filters
for ( type in Expr.filter ) {
if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||
(match = preFilters[ type ]( match ))) ) {
matched = match.shift();
tokens.push({
value: matched,
type: type,
matches: match
});
soFar = soFar.slice( matched.length );
}
}
if ( !matched ) {
break;
}
}
// Return the length of the invalid excess
// if we're just parsing
// Otherwise, throw an error or return tokens
return parseOnly ?
soFar.length :
soFar ?
Sizzle.error( selector ) :
// Cache the tokens
tokenCache( selector, groups ).slice( 0 );
};
function toSelector( tokens ) {
var i = 0,
len = tokens.length,
selector = "";
for ( ; i < len; i++ ) {
selector += tokens[i].value;
}
return selector;
}
function addCombinator( matcher, combinator, base ) {
var dir = combinator.dir,
checkNonElements = base && dir === "parentNode",
doneName = done++;
return combinator.first ?
// Check against closest ancestor/preceding element
function( elem, context, xml ) {
while ( (elem = elem[ dir ]) ) {
if ( elem.nodeType === 1 || checkNonElements ) {
return matcher( elem, context, xml );
}
}
} :
// Check against all ancestor/preceding elements
function( elem, context, xml ) {
var oldCache, outerCache,
newCache = [ dirruns, doneName ];
// We can't set arbitrary data on XML nodes, so they don't benefit from dir caching
if ( xml ) {
while ( (elem = elem[ dir ]) ) {
if ( elem.nodeType === 1 || checkNonElements ) {
if ( matcher( elem, context, xml ) ) {
return true;
}
}
}
} else {
while ( (elem = elem[ dir ]) ) {
if ( elem.nodeType === 1 || checkNonElements ) {
outerCache = elem[ expando ] || (elem[ expando ] = {});
if ( (oldCache = outerCache[ dir ]) &&
oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) {
// Assign to newCache so results back-propagate to previous elements
return (newCache[ 2 ] = oldCache[ 2 ]);
} else {
// Reuse newcache so results back-propagate to previous elements
outerCache[ dir ] = newCache;
// A match means we're done; a fail means we have to keep checking
if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) {
return true;
}
}
}
}
}
};
}
function elementMatcher( matchers ) {
return matchers.length > 1 ?
function( elem, context, xml ) {
var i = matchers.length;
while ( i-- ) {
if ( !matchers[i]( elem, context, xml ) ) {
return false;
}
}
return true;
} :
matchers[0];
}
function multipleContexts( selector, contexts, results ) {
var i = 0,
len = contexts.length;
for ( ; i < len; i++ ) {
Sizzle( selector, contexts[i], results );
}
return results;
}
function condense( unmatched, map, filter, context, xml ) {
var elem,
newUnmatched = [],
i = 0,
len = unmatched.length,
mapped = map != null;
for ( ; i < len; i++ ) {
if ( (elem = unmatched[i]) ) {
if ( !filter || filter( elem, context, xml ) ) {
newUnmatched.push( elem );
if ( mapped ) {
map.push( i );
}
}
}
}
return newUnmatched;
}
function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {
if ( postFilter && !postFilter[ expando ] ) {
postFilter = setMatcher( postFilter );
}
if ( postFinder && !postFinder[ expando ] ) {
postFinder = setMatcher( postFinder, postSelector );
}
return markFunction(function( seed, results, context, xml ) {
var temp, i, elem,
preMap = [],
postMap = [],
preexisting = results.length,
// Get initial elements from seed or context
elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ),
// Prefilter to get matcher input, preserving a map for seed-results synchronization
matcherIn = preFilter && ( seed || !selector ) ?
condense( elems, preMap, preFilter, context, xml ) :
elems,
matcherOut = matcher ?
// If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,
postFinder || ( seed ? preFilter : preexisting || postFilter ) ?
// ...intermediate processing is necessary
[] :
// ...otherwise use results directly
results :
matcherIn;
// Find primary matches
if ( matcher ) {
matcher( matcherIn, matcherOut, context, xml );
}
// Apply postFilter
if ( postFilter ) {
temp = condense( matcherOut, postMap );
postFilter( temp, [], context, xml );
// Un-match failing elements by moving them back to matcherIn
i = temp.length;
while ( i-- ) {
if ( (elem = temp[i]) ) {
matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);
}
}
}
if ( seed ) {
if ( postFinder || preFilter ) {
if ( postFinder ) {
// Get the final matcherOut by condensing this intermediate into postFinder contexts
temp = [];
i = matcherOut.length;
while ( i-- ) {
if ( (elem = matcherOut[i]) ) {
// Restore matcherIn since elem is not yet a final match
temp.push( (matcherIn[i] = elem) );
}
}
postFinder( null, (matcherOut = []), temp, xml );
}
// Move matched elements from seed to results to keep them synchronized
i = matcherOut.length;
while ( i-- ) {
if ( (elem = matcherOut[i]) &&
(temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) {
seed[temp] = !(results[temp] = elem);
}
}
}
// Add elements to results, through postFinder if defined
} else {
matcherOut = condense(
matcherOut === results ?
matcherOut.splice( preexisting, matcherOut.length ) :
matcherOut
);
if ( postFinder ) {
postFinder( null, results, matcherOut, xml );
} else {
push.apply( results, matcherOut );
}
}
});
}
function matcherFromTokens( tokens ) {
var checkContext, matcher, j,
len = tokens.length,
leadingRelative = Expr.relative[ tokens[0].type ],
implicitRelative = leadingRelative || Expr.relative[" "],
i = leadingRelative ? 1 : 0,
// The foundational matcher ensures that elements are reachable from top-level context(s)
matchContext = addCombinator( function( elem ) {
return elem === checkContext;
}, implicitRelative, true ),
matchAnyContext = addCombinator( function( elem ) {
return indexOf.call( checkContext, elem ) > -1;
}, implicitRelative, true ),
matchers = [ function( elem, context, xml ) {
return ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
(checkContext = context).nodeType ?
matchContext( elem, context, xml ) :
matchAnyContext( elem, context, xml ) );
} ];
for ( ; i < len; i++ ) {
if ( (matcher = Expr.relative[ tokens[i].type ]) ) {
matchers = [ addCombinator(elementMatcher( matchers ), matcher) ];
} else {
matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );
// Return special upon seeing a positional matcher
if ( matcher[ expando ] ) {
// Find the next relative operator (if any) for proper handling
j = ++i;
for ( ; j < len; j++ ) {
if ( Expr.relative[ tokens[j].type ] ) {
break;
}
}
return setMatcher(
i > 1 && elementMatcher( matchers ),
i > 1 && toSelector(
// If the preceding token was a descendant combinator, insert an implicit any-element `*`
tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" })
).replace( rtrim, "$1" ),
matcher,
i < j && matcherFromTokens( tokens.slice( i, j ) ),
j < len && matcherFromTokens( (tokens = tokens.slice( j )) ),
j < len && toSelector( tokens )
);
}
matchers.push( matcher );
}
}
return elementMatcher( matchers );
}
function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
var bySet = setMatchers.length > 0,
byElement = elementMatchers.length > 0,
superMatcher = function( seed, context, xml, results, outermost ) {
var elem, j, matcher,
matchedCount = 0,
i = "0",
unmatched = seed && [],
setMatched = [],
contextBackup = outermostContext,
// We must always have either seed elements or outermost context
elems = seed || byElement && Expr.find["TAG"]( "*", outermost ),
// Use integer dirruns iff this is the outermost matcher
dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1),
len = elems.length;
if ( outermost ) {
outermostContext = context !== document && context;
}
// Add elements passing elementMatchers directly to results
// Keep `i` a string if there are no elements so `matchedCount` will be "00" below
// Support: IE<9, Safari
// Tolerate NodeList properties (IE: "length"; Safari: <number>) matching elements by id
for ( ; i !== len && (elem = elems[i]) != null; i++ ) {
if ( byElement && elem ) {
j = 0;
while ( (matcher = elementMatchers[j++]) ) {
if ( matcher( elem, context, xml ) ) {
results.push( elem );
break;
}
}
if ( outermost ) {
dirruns = dirrunsUnique;
}
}
// Track unmatched elements for set filters
if ( bySet ) {
// They will have gone through all possible matchers
if ( (elem = !matcher && elem) ) {
matchedCount--;
}
// Lengthen the array for every element, matched or not
if ( seed ) {
unmatched.push( elem );
}
}
}
// Apply set filters to unmatched elements
matchedCount += i;
if ( bySet && i !== matchedCount ) {
j = 0;
while ( (matcher = setMatchers[j++]) ) {
matcher( unmatched, setMatched, context, xml );
}
if ( seed ) {
// Reintegrate element matches to eliminate the need for sorting
if ( matchedCount > 0 ) {
while ( i-- ) {
if ( !(unmatched[i] || setMatched[i]) ) {
setMatched[i] = pop.call( results );
}
}
}
// Discard index placeholder values to get only actual matches
setMatched = condense( setMatched );
}
// Add matches to results
push.apply( results, setMatched );
// Seedless set matches succeeding multiple successful matchers stipulate sorting
if ( outermost && !seed && setMatched.length > 0 &&
( matchedCount + setMatchers.length ) > 1 ) {
Sizzle.uniqueSort( results );
}
}
// Override manipulation of globals by nested matchers
if ( outermost ) {
dirruns = dirrunsUnique;
outermostContext = contextBackup;
}
return unmatched;
};
return bySet ?
markFunction( superMatcher ) :
superMatcher;
}
compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {
var i,
setMatchers = [],
elementMatchers = [],
cached = compilerCache[ selector + " " ];
if ( !cached ) {
// Generate a function of recursive functions that can be used to check each element
if ( !match ) {
match = tokenize( selector );
}
i = match.length;
while ( i-- ) {
cached = matcherFromTokens( match[i] );
if ( cached[ expando ] ) {
setMatchers.push( cached );
} else {
elementMatchers.push( cached );
}
}
// Cache the compiled function
cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );
// Save selector and tokenization
cached.selector = selector;
}
return cached;
};
/**
* A low-level selection function that works with Sizzle's compiled
* selector functions
* @param {String|Function} selector A selector or a pre-compiled
* selector function built with Sizzle.compile
* @param {Element} context
* @param {Array} [results]
* @param {Array} [seed] A set of elements to match against
*/
select = Sizzle.select = function( selector, context, results, seed ) {
var i, tokens, token, type, find,
compiled = typeof selector === "function" && selector,
match = !seed && tokenize( (selector = compiled.selector || selector) );
results = results || [];
// Try to minimize operations if there is no seed and only one group
if ( match.length === 1 ) {
// Take a shortcut and set the context if the root selector is an ID
tokens = match[0] = match[0].slice( 0 );
if ( tokens.length > 2 && (token = tokens[0]).type === "ID" &&
support.getById && context.nodeType === 9 && documentIsHTML &&
Expr.relative[ tokens[1].type ] ) {
context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];
if ( !context ) {
return results;
// Precompiled matchers will still verify ancestry, so step up a level
} else if ( compiled ) {
context = context.parentNode;
}
selector = selector.slice( tokens.shift().value.length );
}
// Fetch a seed set for right-to-left matching
i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length;
while ( i-- ) {
token = tokens[i];
// Abort if we hit a combinator
if ( Expr.relative[ (type = token.type) ] ) {
break;
}
if ( (find = Expr.find[ type ]) ) {
// Search, expanding context for leading sibling combinators
if ( (seed = find(
token.matches[0].replace( runescape, funescape ),
rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context
)) ) {
// If seed is empty or no tokens remain, we can return early
tokens.splice( i, 1 );
selector = seed.length && toSelector( tokens );
if ( !selector ) {
push.apply( results, seed );
return results;
}
break;
}
}
}
}
// Compile and execute a filtering function if one is not provided
// Provide `match` to avoid retokenization if we modified the selector above
( compiled || compile( selector, match ) )(
seed,
context,
!documentIsHTML,
results,
rsibling.test( selector ) && testContext( context.parentNode ) || context
);
return results;
};
// One-time assignments
// Sort stability
support.sortStable = expando.split("").sort( sortOrder ).join("") === expando;
// Support: Chrome 14-35+
// Always assume duplicates if they aren't passed to the comparison function
support.detectDuplicates = !!hasDuplicate;
// Initialize against the default document
setDocument();
// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)
// Detached nodes confoundingly follow *each other*
support.sortDetached = assert(function( div1 ) {
// Should return 1, but returns 4 (following)
return div1.compareDocumentPosition( document.createElement("div") ) & 1;
});
// Support: IE<8
// Prevent attribute/property "interpolation"
// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
if ( !assert(function( div ) {
div.innerHTML = "<a href='#'></a>";
return div.firstChild.getAttribute("href") === "#" ;
}) ) {
addHandle( "type|href|height|width", function( elem, name, isXML ) {
if ( !isXML ) {
return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 );
}
});
}
// Support: IE<9
// Use defaultValue in place of getAttribute("value")
if ( !support.attributes || !assert(function( div ) {
div.innerHTML = "<input/>";
div.firstChild.setAttribute( "value", "" );
return div.firstChild.getAttribute( "value" ) === "";
}) ) {
addHandle( "value", function( elem, name, isXML ) {
if ( !isXML && elem.nodeName.toLowerCase() === "input" ) {
return elem.defaultValue;
}
});
}
// Support: IE<9
// Use getAttributeNode to fetch booleans when getAttribute lies
if ( !assert(function( div ) {
return div.getAttribute("disabled") == null;
}) ) {
addHandle( booleans, function( elem, name, isXML ) {
var val;
if ( !isXML ) {
return elem[ name ] === true ? name.toLowerCase() :
(val = elem.getAttributeNode( name )) && val.specified ?
val.value :
null;
}
});
}
// EXPOSE
return Sizzle;
});
/*eslint-enable */
// Included from: js/tinymce/classes/util/Arr.js
/**
* Arr.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Array utility class.
*
* @private
* @class tinymce.util.Arr
*/
define("tinymce/util/Arr", [], function() {
var isArray = Array.isArray || function(obj) {
return Object.prototype.toString.call(obj) === "[object Array]";
};
function toArray(obj) {
var array = obj, i, l;
if (!isArray(obj)) {
array = [];
for (i = 0, l = obj.length; i < l; i++) {
array[i] = obj[i];
}
}
return array;
}
function each(o, cb, s) {
var n, l;
if (!o) {
return 0;
}
s = s || o;
if (o.length !== undefined) {
// Indexed arrays, needed for Safari
for (n = 0, l = o.length; n < l; n++) {
if (cb.call(s, o[n], n, o) === false) {
return 0;
}
}
} else {
// Hashtables
for (n in o) {
if (o.hasOwnProperty(n)) {
if (cb.call(s, o[n], n, o) === false) {
return 0;
}
}
}
}
return 1;
}
function map(array, callback) {
var out = [];
each(array, function(item, index) {
out.push(callback(item, index, array));
});
return out;
}
function filter(a, f) {
var o = [];
each(a, function(v, index) {
if (!f || f(v, index, a)) {
o.push(v);
}
});
return o;
}
function indexOf(a, v) {
var i, l;
if (a) {
for (i = 0, l = a.length; i < l; i++) {
if (a[i] === v) {
return i;
}
}
}
return -1;
}
function reduce(collection, iteratee, accumulator, thisArg) {
var i = 0;
if (arguments.length < 3) {
accumulator = collection[0];
}
for (; i < collection.length; i++) {
accumulator = iteratee.call(thisArg, accumulator, collection[i], i);
}
return accumulator;
}
function findIndex(array, predicate, thisArg) {
var i, l;
for (i = 0, l = array.length; i < l; i++) {
if (predicate.call(thisArg, array[i], i, array)) {
return i;
}
}
return -1;
}
function find(array, predicate, thisArg) {
var idx = findIndex(array, predicate, thisArg);
if (idx !== -1) {
return array[idx];
}
return undefined;
}
function last(collection) {
return collection[collection.length - 1];
}
return {
isArray: isArray,
toArray: toArray,
each: each,
map: map,
filter: filter,
indexOf: indexOf,
reduce: reduce,
findIndex: findIndex,
find: find,
last: last
};
});
// Included from: js/tinymce/classes/util/Tools.js
/**
* Tools.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class contains various utlity functions. These are also exposed
* directly on the tinymce namespace.
*
* @class tinymce.util.Tools
*/
define("tinymce/util/Tools", [
"tinymce/Env",
"tinymce/util/Arr"
], function(Env, Arr) {
/**
* Removes whitespace from the beginning and end of a string.
*
* @method trim
* @param {String} s String to remove whitespace from.
* @return {String} New string with removed whitespace.
*/
var whiteSpaceRegExp = /^\s*|\s*$/g;
function trim(str) {
return (str === null || str === undefined) ? '' : ("" + str).replace(whiteSpaceRegExp, '');
}
/**
* Checks if a object is of a specific type for example an array.
*
* @method is
* @param {Object} obj Object to check type of.
* @param {string} type Optional type to check for.
* @return {Boolean} true/false if the object is of the specified type.
*/
function is(obj, type) {
if (!type) {
return obj !== undefined;
}
if (type == 'array' && Arr.isArray(obj)) {
return true;
}
return typeof obj == type;
}
/**
* Makes a name/object map out of an array with names.
*
* @method makeMap
* @param {Array/String} items Items to make map out of.
* @param {String} delim Optional delimiter to split string by.
* @param {Object} map Optional map to add items to.
* @return {Object} Name/value map of items.
*/
function makeMap(items, delim, map) {
var i;
items = items || [];
delim = delim || ',';
if (typeof items == "string") {
items = items.split(delim);
}
map = map || {};
i = items.length;
while (i--) {
map[items[i]] = {};
}
return map;
}
/**
* JavaScript does not protect hasOwnProperty method, so it is possible to overwrite it. This is
* object independent version.
*
* @param {Object} obj
* @param {String} prop
* @returns {Boolean}
*/
function hasOwnProperty(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
}
/**
* Creates a class, subclass or static singleton.
* More details on this method can be found in the Wiki.
*
* @method create
* @param {String} s Class name, inheritance and prefix.
* @param {Object} p Collection of methods to add to the class.
* @param {Object} root Optional root object defaults to the global window object.
* @example
* // Creates a basic class
* tinymce.create('tinymce.somepackage.SomeClass', {
* SomeClass: function() {
* // Class constructor
* },
*
* method: function() {
* // Some method
* }
* });
*
* // Creates a basic subclass class
* tinymce.create('tinymce.somepackage.SomeSubClass:tinymce.somepackage.SomeClass', {
* SomeSubClass: function() {
* // Class constructor
* this.parent(); // Call parent constructor
* },
*
* method: function() {
* // Some method
* this.parent(); // Call parent method
* },
*
* 'static': {
* staticMethod: function() {
* // Static method
* }
* }
* });
*
* // Creates a singleton/static class
* tinymce.create('static tinymce.somepackage.SomeSingletonClass', {
* method: function() {
* // Some method
* }
* });
*/
function create(s, p, root) {
var self = this, sp, ns, cn, scn, c, de = 0;
// Parse : <prefix> <class>:<super class>
s = /^((static) )?([\w.]+)(:([\w.]+))?/.exec(s);
cn = s[3].match(/(^|\.)(\w+)$/i)[2]; // Class name
// Create namespace for new class
ns = self.createNS(s[3].replace(/\.\w+$/, ''), root);
// Class already exists
if (ns[cn]) {
return;
}
// Make pure static class
if (s[2] == 'static') {
ns[cn] = p;
if (this.onCreate) {
this.onCreate(s[2], s[3], ns[cn]);
}
return;
}
// Create default constructor
if (!p[cn]) {
p[cn] = function() {};
de = 1;
}
// Add constructor and methods
ns[cn] = p[cn];
self.extend(ns[cn].prototype, p);
// Extend
if (s[5]) {
sp = self.resolve(s[5]).prototype;
scn = s[5].match(/\.(\w+)$/i)[1]; // Class name
// Extend constructor
c = ns[cn];
if (de) {
// Add passthrough constructor
ns[cn] = function() {
return sp[scn].apply(this, arguments);
};
} else {
// Add inherit constructor
ns[cn] = function() {
this.parent = sp[scn];
return c.apply(this, arguments);
};
}
ns[cn].prototype[cn] = ns[cn];
// Add super methods
self.each(sp, function(f, n) {
ns[cn].prototype[n] = sp[n];
});
// Add overridden methods
self.each(p, function(f, n) {
// Extend methods if needed
if (sp[n]) {
ns[cn].prototype[n] = function() {
this.parent = sp[n];
return f.apply(this, arguments);
};
} else {
if (n != cn) {
ns[cn].prototype[n] = f;
}
}
});
}
// Add static methods
/*jshint sub:true*/
/*eslint dot-notation:0*/
self.each(p['static'], function(f, n) {
ns[cn][n] = f;
});
}
function extend(obj, ext) {
var i, l, name, args = arguments, value;
for (i = 1, l = args.length; i < l; i++) {
ext = args[i];
for (name in ext) {
if (ext.hasOwnProperty(name)) {
value = ext[name];
if (value !== undefined) {
obj[name] = value;
}
}
}
}
return obj;
}
/**
* Executed the specified function for each item in a object tree.
*
* @method walk
* @param {Object} o Object tree to walk though.
* @param {function} f Function to call for each item.
* @param {String} n Optional name of collection inside the objects to walk for example childNodes.
* @param {String} s Optional scope to execute the function in.
*/
function walk(o, f, n, s) {
s = s || this;
if (o) {
if (n) {
o = o[n];
}
Arr.each(o, function(o, i) {
if (f.call(s, o, i, n) === false) {
return false;
}
walk(o, f, n, s);
});
}
}
/**
* Creates a namespace on a specific object.
*
* @method createNS
* @param {String} n Namespace to create for example a.b.c.d.
* @param {Object} o Optional object to add namespace to, defaults to window.
* @return {Object} New namespace object the last item in path.
* @example
* // Create some namespace
* tinymce.createNS('tinymce.somepackage.subpackage');
*
* // Add a singleton
* var tinymce.somepackage.subpackage.SomeSingleton = {
* method: function() {
* // Some method
* }
* };
*/
function createNS(n, o) {
var i, v;
o = o || window;
n = n.split('.');
for (i = 0; i < n.length; i++) {
v = n[i];
if (!o[v]) {
o[v] = {};
}
o = o[v];
}
return o;
}
/**
* Resolves a string and returns the object from a specific structure.
*
* @method resolve
* @param {String} n Path to resolve for example a.b.c.d.
* @param {Object} o Optional object to search though, defaults to window.
* @return {Object} Last object in path or null if it couldn't be resolved.
* @example
* // Resolve a path into an object reference
* var obj = tinymce.resolve('a.b.c.d');
*/
function resolve(n, o) {
var i, l;
o = o || window;
n = n.split('.');
for (i = 0, l = n.length; i < l; i++) {
o = o[n[i]];
if (!o) {
break;
}
}
return o;
}
/**
* Splits a string but removes the whitespace before and after each value.
*
* @method explode
* @param {string} s String to split.
* @param {string} d Delimiter to split by.
* @example
* // Split a string into an array with a,b,c
* var arr = tinymce.explode('a, b, c');
*/
function explode(s, d) {
if (!s || is(s, 'array')) {
return s;
}
return Arr.map(s.split(d || ','), trim);
}
function _addCacheSuffix(url) {
var cacheSuffix = Env.cacheSuffix;
if (cacheSuffix) {
url += (url.indexOf('?') === -1 ? '?' : '&') + cacheSuffix;
}
return url;
}
return {
trim: trim,
/**
* Returns true/false if the object is an array or not.
*
* @method isArray
* @param {Object} obj Object to check.
* @return {boolean} true/false state if the object is an array or not.
*/
isArray: Arr.isArray,
is: is,
/**
* Converts the specified object into a real JavaScript array.
*
* @method toArray
* @param {Object} obj Object to convert into array.
* @return {Array} Array object based in input.
*/
toArray: Arr.toArray,
makeMap: makeMap,
/**
* Performs an iteration of all items in a collection such as an object or array. This method will execure the
* callback function for each item in the collection, if the callback returns false the iteration will terminate.
* The callback has the following format: cb(value, key_or_index).
*
* @method each
* @param {Object} o Collection to iterate.
* @param {function} cb Callback function to execute for each item.
* @param {Object} s Optional scope to execute the callback in.
* @example
* // Iterate an array
* tinymce.each([1,2,3], function(v, i) {
* console.debug("Value: " + v + ", Index: " + i);
* });
*
* // Iterate an object
* tinymce.each({a: 1, b: 2, c: 3], function(v, k) {
* console.debug("Value: " + v + ", Key: " + k);
* });
*/
each: Arr.each,
/**
* Creates a new array by the return value of each iteration function call. This enables you to convert
* one array list into another.
*
* @method map
* @param {Array} array Array of items to iterate.
* @param {function} callback Function to call for each item. It's return value will be the new value.
* @return {Array} Array with new values based on function return values.
*/
map: Arr.map,
/**
* Filters out items from the input array by calling the specified function for each item.
* If the function returns false the item will be excluded if it returns true it will be included.
*
* @method grep
* @param {Array} a Array of items to loop though.
* @param {function} f Function to call for each item. Include/exclude depends on it's return value.
* @return {Array} New array with values imported and filtered based in input.
* @example
* // Filter out some items, this will return an array with 4 and 5
* var items = tinymce.grep([1,2,3,4,5], function(v) {return v > 3;});
*/
grep: Arr.filter,
/**
* Returns an index of the item or -1 if item is not present in the array.
*
* @method inArray
* @param {any} item Item to search for.
* @param {Array} arr Array to search in.
* @return {Number} index of the item or -1 if item was not found.
*/
inArray: Arr.indexOf,
hasOwn: hasOwnProperty,
extend: extend,
create: create,
walk: walk,
createNS: createNS,
resolve: resolve,
explode: explode,
_addCacheSuffix: _addCacheSuffix
};
});
// Included from: js/tinymce/classes/dom/DomQuery.js
/**
* DomQuery.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class mimics most of the jQuery API:
*
* This is whats currently implemented:
* - Utility functions
* - DOM traversial
* - DOM manipulation
* - Event binding
*
* This is not currently implemented:
* - Dimension
* - Ajax
* - Animation
* - Advanced chaining
*
* @example
* var $ = tinymce.dom.DomQuery;
* $('p').attr('attr', 'value').addClass('class');
*
* @class tinymce.dom.DomQuery
*/
define("tinymce/dom/DomQuery", [
"tinymce/dom/EventUtils",
"tinymce/dom/Sizzle",
"tinymce/util/Tools",
"tinymce/Env"
], function(EventUtils, Sizzle, Tools, Env) {
var doc = document, push = Array.prototype.push, slice = Array.prototype.slice;
var rquickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/;
var Event = EventUtils.Event, undef;
var skipUniques = Tools.makeMap('children,contents,next,prev');
function isDefined(obj) {
return typeof obj !== 'undefined';
}
function isString(obj) {
return typeof obj === 'string';
}
function isWindow(obj) {
return obj && obj == obj.window;
}
function createFragment(html, fragDoc) {
var frag, node, container;
fragDoc = fragDoc || doc;
container = fragDoc.createElement('div');
frag = fragDoc.createDocumentFragment();
container.innerHTML = html;
while ((node = container.firstChild)) {
frag.appendChild(node);
}
return frag;
}
function domManipulate(targetNodes, sourceItem, callback, reverse) {
var i;
if (isString(sourceItem)) {
sourceItem = createFragment(sourceItem, getElementDocument(targetNodes[0]));
} else if (sourceItem.length && !sourceItem.nodeType) {
sourceItem = DomQuery.makeArray(sourceItem);
if (reverse) {
for (i = sourceItem.length - 1; i >= 0; i--) {
domManipulate(targetNodes, sourceItem[i], callback, reverse);
}
} else {
for (i = 0; i < sourceItem.length; i++) {
domManipulate(targetNodes, sourceItem[i], callback, reverse);
}
}
return targetNodes;
}
if (sourceItem.nodeType) {
i = targetNodes.length;
while (i--) {
callback.call(targetNodes[i], sourceItem);
}
}
return targetNodes;
}
function hasClass(node, className) {
return node && className && (' ' + node.className + ' ').indexOf(' ' + className + ' ') !== -1;
}
function wrap(elements, wrapper, all) {
var lastParent, newWrapper;
wrapper = DomQuery(wrapper)[0];
elements.each(function() {
var self = this;
if (!all || lastParent != self.parentNode) {
lastParent = self.parentNode;
newWrapper = wrapper.cloneNode(false);
self.parentNode.insertBefore(newWrapper, self);
newWrapper.appendChild(self);
} else {
newWrapper.appendChild(self);
}
});
return elements;
}
var numericCssMap = Tools.makeMap('fillOpacity fontWeight lineHeight opacity orphans widows zIndex zoom', ' ');
var booleanMap = Tools.makeMap('checked compact declare defer disabled ismap multiple nohref noshade nowrap readonly selected', ' ');
var propFix = {
'for': 'htmlFor',
'class': 'className',
'readonly': 'readOnly'
};
var cssFix = {
'float': 'cssFloat'
};
var attrHooks = {}, cssHooks = {};
function DomQuery(selector, context) {
/*eslint new-cap:0 */
return new DomQuery.fn.init(selector, context);
}
function inArray(item, array) {
var i;
if (array.indexOf) {
return array.indexOf(item);
}
i = array.length;
while (i--) {
if (array[i] === item) {
return i;
}
}
return -1;
}
var whiteSpaceRegExp = /^\s*|\s*$/g;
function trim(str) {
return (str === null || str === undef) ? '' : ("" + str).replace(whiteSpaceRegExp, '');
}
function each(obj, callback) {
var length, key, i, undef, value;
if (obj) {
length = obj.length;
if (length === undef) {
// Loop object items
for (key in obj) {
if (obj.hasOwnProperty(key)) {
value = obj[key];
if (callback.call(value, key, value) === false) {
break;
}
}
}
} else {
// Loop array items
for (i = 0; i < length; i++) {
value = obj[i];
if (callback.call(value, i, value) === false) {
break;
}
}
}
}
return obj;
}
function grep(array, callback) {
var out = [];
each(array, function(i, item) {
if (callback(item, i)) {
out.push(item);
}
});
return out;
}
function getElementDocument(element) {
if (!element) {
return doc;
}
if (element.nodeType == 9) {
return element;
}
return element.ownerDocument;
}
DomQuery.fn = DomQuery.prototype = {
constructor: DomQuery,
/**
* Selector for the current set.
*
* @property selector
* @type String
*/
selector: "",
/**
* Context used to create the set.
*
* @property context
* @type Element
*/
context: null,
/**
* Number of items in the current set.
*
* @property length
* @type Number
*/
length: 0,
/**
* Constructs a new DomQuery instance with the specified selector or context.
*
* @constructor
* @method init
* @param {String/Array/DomQuery} selector Optional CSS selector/Array or array like object or HTML string.
* @param {Document/Element} context Optional context to search in.
*/
init: function(selector, context) {
var self = this, match, node;
if (!selector) {
return self;
}
if (selector.nodeType) {
self.context = self[0] = selector;
self.length = 1;
return self;
}
if (context && context.nodeType) {
self.context = context;
} else {
if (context) {
return DomQuery(selector).attr(context);
}
self.context = context = document;
}
if (isString(selector)) {
self.selector = selector;
if (selector.charAt(0) === "<" && selector.charAt(selector.length - 1) === ">" && selector.length >= 3) {
match = [null, selector, null];
} else {
match = rquickExpr.exec(selector);
}
if (match) {
if (match[1]) {
node = createFragment(selector, getElementDocument(context)).firstChild;
while (node) {
push.call(self, node);
node = node.nextSibling;
}
} else {
node = getElementDocument(context).getElementById(match[2]);
if (!node) {
return self;
}
if (node.id !== match[2]) {
return self.find(selector);
}
self.length = 1;
self[0] = node;
}
} else {
return DomQuery(context).find(selector);
}
} else {
this.add(selector, false);
}
return self;
},
/**
* Converts the current set to an array.
*
* @method toArray
* @return {Array} Array of all nodes in set.
*/
toArray: function() {
return Tools.toArray(this);
},
/**
* Adds new nodes to the set.
*
* @method add
* @param {Array/tinymce.dom.DomQuery} items Array of all nodes to add to set.
* @param {Boolean} sort Optional sort flag that enables sorting of elements.
* @return {tinymce.dom.DomQuery} New instance with nodes added.
*/
add: function(items, sort) {
var self = this, nodes, i;
if (isString(items)) {
return self.add(DomQuery(items));
}
if (sort !== false) {
nodes = DomQuery.unique(self.toArray().concat(DomQuery.makeArray(items)));
self.length = nodes.length;
for (i = 0; i < nodes.length; i++) {
self[i] = nodes[i];
}
} else {
push.apply(self, DomQuery.makeArray(items));
}
return self;
},
/**
* Sets/gets attributes on the elements in the current set.
*
* @method attr
* @param {String/Object} name Name of attribute to get or an object with attributes to set.
* @param {String} value Optional value to set.
* @return {tinymce.dom.DomQuery/String} Current set or the specified attribute when only the name is specified.
*/
attr: function(name, value) {
var self = this, hook;
if (typeof name === "object") {
each(name, function(name, value) {
self.attr(name, value);
});
} else if (isDefined(value)) {
this.each(function() {
var hook;
if (this.nodeType === 1) {
hook = attrHooks[name];
if (hook && hook.set) {
hook.set(this, value);
return;
}
if (value === null) {
this.removeAttribute(name, 2);
} else {
this.setAttribute(name, value, 2);
}
}
});
} else {
if (self[0] && self[0].nodeType === 1) {
hook = attrHooks[name];
if (hook && hook.get) {
return hook.get(self[0], name);
}
if (booleanMap[name]) {
return self.prop(name) ? name : undef;
}
value = self[0].getAttribute(name, 2);
if (value === null) {
value = undef;
}
}
return value;
}
return self;
},
/**
* Removes attributse on the elements in the current set.
*
* @method removeAttr
* @param {String/Object} name Name of attribute to remove.
* @return {tinymce.dom.DomQuery/String} Current set.
*/
removeAttr: function(name) {
return this.attr(name, null);
},
/**
* Sets/gets properties on the elements in the current set.
*
* @method attr
* @param {String/Object} name Name of property to get or an object with properties to set.
* @param {String} value Optional value to set.
* @return {tinymce.dom.DomQuery/String} Current set or the specified property when only the name is specified.
*/
prop: function(name, value) {
var self = this;
name = propFix[name] || name;
if (typeof name === "object") {
each(name, function(name, value) {
self.prop(name, value);
});
} else if (isDefined(value)) {
this.each(function() {
if (this.nodeType == 1) {
this[name] = value;
}
});
} else {
if (self[0] && self[0].nodeType && name in self[0]) {
return self[0][name];
}
return value;
}
return self;
},
/**
* Sets/gets styles on the elements in the current set.
*
* @method css
* @param {String/Object} name Name of style to get or an object with styles to set.
* @param {String} value Optional value to set.
* @return {tinymce.dom.DomQuery/String} Current set or the specified style when only the name is specified.
*/
css: function(name, value) {
var self = this, elm, hook;
function camel(name) {
return name.replace(/-(\D)/g, function(a, b) {
return b.toUpperCase();
});
}
function dashed(name) {
return name.replace(/[A-Z]/g, function(a) {
return '-' + a;
});
}
if (typeof name === "object") {
each(name, function(name, value) {
self.css(name, value);
});
} else {
if (isDefined(value)) {
name = camel(name);
// Default px suffix on these
if (typeof value === 'number' && !numericCssMap[name]) {
value += 'px';
}
self.each(function() {
var style = this.style;
hook = cssHooks[name];
if (hook && hook.set) {
hook.set(this, value);
return;
}
try {
this.style[cssFix[name] || name] = value;
} catch (ex) {
// Ignore
}
if (value === null || value === '') {
if (style.removeProperty) {
style.removeProperty(dashed(name));
} else {
style.removeAttribute(name);
}
}
});
} else {
elm = self[0];
hook = cssHooks[name];
if (hook && hook.get) {
return hook.get(elm);
}
if (elm.ownerDocument.defaultView) {
try {
return elm.ownerDocument.defaultView.getComputedStyle(elm, null).getPropertyValue(dashed(name));
} catch (ex) {
return undef;
}
} else if (elm.currentStyle) {
return elm.currentStyle[camel(name)];
}
}
}
return self;
},
/**
* Removes all nodes in set from the document.
*
* @method remove
* @return {tinymce.dom.DomQuery} Current set with the removed nodes.
*/
remove: function() {
var self = this, node, i = this.length;
while (i--) {
node = self[i];
Event.clean(node);
if (node.parentNode) {
node.parentNode.removeChild(node);
}
}
return this;
},
/**
* Empties all elements in set.
*
* @method empty
* @return {tinymce.dom.DomQuery} Current set with the empty nodes.
*/
empty: function() {
var self = this, node, i = this.length;
while (i--) {
node = self[i];
while (node.firstChild) {
node.removeChild(node.firstChild);
}
}
return this;
},
/**
* Sets or gets the HTML of the current set or first set node.
*
* @method html
* @param {String} value Optional innerHTML value to set on each element.
* @return {tinymce.dom.DomQuery/String} Current set or the innerHTML of the first element.
*/
html: function(value) {
var self = this, i;
if (isDefined(value)) {
i = self.length;
try {
while (i--) {
self[i].innerHTML = value;
}
} catch (ex) {
// Workaround for "Unknown runtime error" when DIV is added to P on IE
DomQuery(self[i]).empty().append(value);
}
return self;
}
return self[0] ? self[0].innerHTML : '';
},
/**
* Sets or gets the text of the current set or first set node.
*
* @method text
* @param {String} value Optional innerText value to set on each element.
* @return {tinymce.dom.DomQuery/String} Current set or the innerText of the first element.
*/
text: function(value) {
var self = this, i;
if (isDefined(value)) {
i = self.length;
while (i--) {
if ("innerText" in self[i]) {
self[i].innerText = value;
} else {
self[0].textContent = value;
}
}
return self;
}
return self[0] ? (self[0].innerText || self[0].textContent) : '';
},
/**
* Appends the specified node/html or node set to the current set nodes.
*
* @method append
* @param {String/Element/Array/tinymce.dom.DomQuery} content Content to append to each element in set.
* @return {tinymce.dom.DomQuery} Current set.
*/
append: function() {
return domManipulate(this, arguments, function(node) {
// Either element or Shadow Root
if (this.nodeType === 1 || (this.host && this.host.nodeType === 1)) {
this.appendChild(node);
}
});
},
/**
* Prepends the specified node/html or node set to the current set nodes.
*
* @method prepend
* @param {String/Element/Array/tinymce.dom.DomQuery} content Content to prepend to each element in set.
* @return {tinymce.dom.DomQuery} Current set.
*/
prepend: function() {
return domManipulate(this, arguments, function(node) {
// Either element or Shadow Root
if (this.nodeType === 1 || (this.host && this.host.nodeType === 1)) {
this.insertBefore(node, this.firstChild);
}
}, true);
},
/**
* Adds the specified elements before current set nodes.
*
* @method before
* @param {String/Element/Array/tinymce.dom.DomQuery} content Content to add before to each element in set.
* @return {tinymce.dom.DomQuery} Current set.
*/
before: function() {
var self = this;
if (self[0] && self[0].parentNode) {
return domManipulate(self, arguments, function(node) {
this.parentNode.insertBefore(node, this);
});
}
return self;
},
/**
* Adds the specified elements after current set nodes.
*
* @method after
* @param {String/Element/Array/tinymce.dom.DomQuery} content Content to add after to each element in set.
* @return {tinymce.dom.DomQuery} Current set.
*/
after: function() {
var self = this;
if (self[0] && self[0].parentNode) {
return domManipulate(self, arguments, function(node) {
this.parentNode.insertBefore(node, this.nextSibling);
}, true);
}
return self;
},
/**
* Appends the specified set nodes to the specified selector/instance.
*
* @method appendTo
* @param {String/Element/Array/tinymce.dom.DomQuery} val Item to append the current set to.
* @return {tinymce.dom.DomQuery} Current set with the appended nodes.
*/
appendTo: function(val) {
DomQuery(val).append(this);
return this;
},
/**
* Prepends the specified set nodes to the specified selector/instance.
*
* @method prependTo
* @param {String/Element/Array/tinymce.dom.DomQuery} val Item to prepend the current set to.
* @return {tinymce.dom.DomQuery} Current set with the prepended nodes.
*/
prependTo: function(val) {
DomQuery(val).prepend(this);
return this;
},
/**
* Replaces the nodes in set with the specified content.
*
* @method replaceWith
* @param {String/Element/Array/tinymce.dom.DomQuery} content Content to replace nodes with.
* @return {tinymce.dom.DomQuery} Set with replaced nodes.
*/
replaceWith: function(content) {
return this.before(content).remove();
},
/**
* Wraps all elements in set with the specified wrapper.
*
* @method wrap
* @param {String/Element/Array/tinymce.dom.DomQuery} content Content to wrap nodes with.
* @return {tinymce.dom.DomQuery} Set with wrapped nodes.
*/
wrap: function(content) {
return wrap(this, content);
},
/**
* Wraps all nodes in set with the specified wrapper. If the nodes are siblings all of them
* will be wrapped in the same wrapper.
*
* @method wrapAll
* @param {String/Element/Array/tinymce.dom.DomQuery} content Content to wrap nodes with.
* @return {tinymce.dom.DomQuery} Set with wrapped nodes.
*/
wrapAll: function(content) {
return wrap(this, content, true);
},
/**
* Wraps all elements inner contents in set with the specified wrapper.
*
* @method wrapInner
* @param {String/Element/Array/tinymce.dom.DomQuery} content Content to wrap nodes with.
* @return {tinymce.dom.DomQuery} Set with wrapped nodes.
*/
wrapInner: function(content) {
this.each(function() {
DomQuery(this).contents().wrapAll(content);
});
return this;
},
/**
* Unwraps all elements by removing the parent element of each item in set.
*
* @method unwrap
* @return {tinymce.dom.DomQuery} Set with unwrapped nodes.
*/
unwrap: function() {
return this.parent().each(function() {
DomQuery(this).replaceWith(this.childNodes);
});
},
/**
* Clones all nodes in set.
*
* @method clone
* @return {tinymce.dom.DomQuery} Set with cloned nodes.
*/
clone: function() {
var result = [];
this.each(function() {
result.push(this.cloneNode(true));
});
return DomQuery(result);
},
/**
* Adds the specified class name to the current set elements.
*
* @method addClass
* @param {String} className Class name to add.
* @return {tinymce.dom.DomQuery} Current set.
*/
addClass: function(className) {
return this.toggleClass(className, true);
},
/**
* Removes the specified class name to the current set elements.
*
* @method removeClass
* @param {String} className Class name to remove.
* @return {tinymce.dom.DomQuery} Current set.
*/
removeClass: function(className) {
return this.toggleClass(className, false);
},
/**
* Toggles the specified class name on the current set elements.
*
* @method toggleClass
* @param {String} className Class name to add/remove.
* @param {Boolean} state Optional state to toggle on/off.
* @return {tinymce.dom.DomQuery} Current set.
*/
toggleClass: function(className, state) {
var self = this;
// Functions are not supported
if (typeof className != 'string') {
return self;
}
if (className.indexOf(' ') !== -1) {
each(className.split(' '), function() {
self.toggleClass(this, state);
});
} else {
self.each(function(index, node) {
var existingClassName, classState;
classState = hasClass(node, className);
if (classState !== state) {
existingClassName = node.className;
if (classState) {
node.className = trim((" " + existingClassName + " ").replace(' ' + className + ' ', ' '));
} else {
node.className += existingClassName ? ' ' + className : className;
}
}
});
}
return self;
},
/**
* Returns true/false if the first item in set has the specified class.
*
* @method hasClass
* @param {String} className Class name to check for.
* @return {Boolean} True/false if the set has the specified class.
*/
hasClass: function(className) {
return hasClass(this[0], className);
},
/**
* Executes the callback function for each item DomQuery collection. If you return false in the
* callback it will break the loop.
*
* @method each
* @param {function} callback Callback function to execute for each item.
* @return {tinymce.dom.DomQuery} Current set.
*/
each: function(callback) {
return each(this, callback);
},
/**
* Binds an event with callback function to the elements in set.
*
* @method on
* @param {String} name Name of the event to bind.
* @param {function} callback Callback function to execute when the event occurs.
* @return {tinymce.dom.DomQuery} Current set.
*/
on: function(name, callback) {
return this.each(function() {
Event.bind(this, name, callback);
});
},
/**
* Unbinds an event with callback function to the elements in set.
*
* @method off
* @param {String} name Optional name of the event to bind.
* @param {function} callback Optional callback function to execute when the event occurs.
* @return {tinymce.dom.DomQuery} Current set.
*/
off: function(name, callback) {
return this.each(function() {
Event.unbind(this, name, callback);
});
},
/**
* Triggers the specified event by name or event object.
*
* @method trigger
* @param {String/Object} name Name of the event to trigger or event object.
* @return {tinymce.dom.DomQuery} Current set.
*/
trigger: function(name) {
return this.each(function() {
if (typeof name == 'object') {
Event.fire(this, name.type, name);
} else {
Event.fire(this, name);
}
});
},
/**
* Shows all elements in set.
*
* @method show
* @return {tinymce.dom.DomQuery} Current set.
*/
show: function() {
return this.css('display', '');
},
/**
* Hides all elements in set.
*
* @method hide
* @return {tinymce.dom.DomQuery} Current set.
*/
hide: function() {
return this.css('display', 'none');
},
/**
* Slices the current set.
*
* @method slice
* @param {Number} start Start index to slice at.
* @param {Number} end Optional end index to end slice at.
* @return {tinymce.dom.DomQuery} Sliced set.
*/
slice: function() {
return new DomQuery(slice.apply(this, arguments));
},
/**
* Makes the set equal to the specified index.
*
* @method eq
* @param {Number} index Index to set it equal to.
* @return {tinymce.dom.DomQuery} Single item set.
*/
eq: function(index) {
return index === -1 ? this.slice(index) : this.slice(index, +index + 1);
},
/**
* Makes the set equal to first element in set.
*
* @method first
* @return {tinymce.dom.DomQuery} Single item set.
*/
first: function() {
return this.eq(0);
},
/**
* Makes the set equal to last element in set.
*
* @method last
* @return {tinymce.dom.DomQuery} Single item set.
*/
last: function() {
return this.eq(-1);
},
/**
* Finds elements by the specified selector for each element in set.
*
* @method find
* @param {String} selector Selector to find elements by.
* @return {tinymce.dom.DomQuery} Set with matches elements.
*/
find: function(selector) {
var i, l, ret = [];
for (i = 0, l = this.length; i < l; i++) {
DomQuery.find(selector, this[i], ret);
}
return DomQuery(ret);
},
/**
* Filters the current set with the specified selector.
*
* @method filter
* @param {String/function} selector Selector to filter elements by.
* @return {tinymce.dom.DomQuery} Set with filtered elements.
*/
filter: function(selector) {
if (typeof selector == 'function') {
return DomQuery(grep(this.toArray(), function(item, i) {
return selector(i, item);
}));
}
return DomQuery(DomQuery.filter(selector, this.toArray()));
},
/**
* Gets the current node or any parent matching the specified selector.
*
* @method closest
* @param {String/Element/tinymce.dom.DomQuery} selector Selector or element to find.
* @return {tinymce.dom.DomQuery} Set with closest elements.
*/
closest: function(selector) {
var result = [];
if (selector instanceof DomQuery) {
selector = selector[0];
}
this.each(function(i, node) {
while (node) {
if (typeof selector == 'string' && DomQuery(node).is(selector)) {
result.push(node);
break;
} else if (node == selector) {
result.push(node);
break;
}
node = node.parentNode;
}
});
return DomQuery(result);
},
/**
* Returns the offset of the first element in set or sets the top/left css properties of all elements in set.
*
* @method offset
* @param {Object} offset Optional offset object to set on each item.
* @return {Object/tinymce.dom.DomQuery} Returns the first element offset or the current set if you specified an offset.
*/
offset: function(offset) {
var elm, doc, docElm;
var x = 0, y = 0, pos;
if (!offset) {
elm = this[0];
if (elm) {
doc = elm.ownerDocument;
docElm = doc.documentElement;
if (elm.getBoundingClientRect) {
pos = elm.getBoundingClientRect();
x = pos.left + (docElm.scrollLeft || doc.body.scrollLeft) - docElm.clientLeft;
y = pos.top + (docElm.scrollTop || doc.body.scrollTop) - docElm.clientTop;
}
}
return {
left: x,
top: y
};
}
return this.css(offset);
},
push: push,
sort: [].sort,
splice: [].splice
};
// Static members
Tools.extend(DomQuery, {
/**
* Extends the specified object with one or more objects.
*
* @static
* @method extend
* @param {Object} target Target object to extend with new items.
* @param {Object..} object Object to extend the target with.
* @return {Object} Extended input object.
*/
extend: Tools.extend,
/**
* Creates an array out of an array like object.
*
* @static
* @method makeArray
* @param {Object} object Object to convert to array.
* @return {Array} Array produced from object.
*/
makeArray: function(object) {
if (isWindow(object) || object.nodeType) {
return [object];
}
return Tools.toArray(object);
},
/**
* Returns the index of the specified item inside the array.
*
* @static
* @method inArray
* @param {Object} item Item to look for.
* @param {Array} array Array to look for item in.
* @return {Number} Index of the item or -1.
*/
inArray: inArray,
/**
* Returns true/false if the specified object is an array or not.
*
* @static
* @method isArray
* @param {Object} array Object to check if it's an array or not.
* @return {Boolean} True/false if the object is an array.
*/
isArray: Tools.isArray,
/**
* Executes the callback function for each item in array/object. If you return false in the
* callback it will break the loop.
*
* @static
* @method each
* @param {Object} obj Object to iterate.
* @param {function} callback Callback function to execute for each item.
*/
each: each,
/**
* Removes whitespace from the beginning and end of a string.
*
* @static
* @method trim
* @param {String} str String to remove whitespace from.
* @return {String} New string with removed whitespace.
*/
trim: trim,
/**
* Filters out items from the input array by calling the specified function for each item.
* If the function returns false the item will be excluded if it returns true it will be included.
*
* @static
* @method grep
* @param {Array} array Array of items to loop though.
* @param {function} callback Function to call for each item. Include/exclude depends on it's return value.
* @return {Array} New array with values imported and filtered based in input.
* @example
* // Filter out some items, this will return an array with 4 and 5
* var items = DomQuery.grep([1, 2, 3, 4, 5], function(v) {return v > 3;});
*/
grep: grep,
// Sizzle
find: Sizzle,
expr: Sizzle.selectors,
unique: Sizzle.uniqueSort,
text: Sizzle.getText,
contains: Sizzle.contains,
filter: function(expr, elems, not) {
var i = elems.length;
if (not) {
expr = ":not(" + expr + ")";
}
while (i--) {
if (elems[i].nodeType != 1) {
elems.splice(i, 1);
}
}
if (elems.length === 1) {
elems = DomQuery.find.matchesSelector(elems[0], expr) ? [elems[0]] : [];
} else {
elems = DomQuery.find.matches(expr, elems);
}
return elems;
}
});
function dir(el, prop, until) {
var matched = [], cur = el[prop];
if (typeof until != 'string' && until instanceof DomQuery) {
until = until[0];
}
while (cur && cur.nodeType !== 9) {
if (until !== undefined) {
if (cur === until) {
break;
}
if (typeof until == 'string' && DomQuery(cur).is(until)) {
break;
}
}
if (cur.nodeType === 1) {
matched.push(cur);
}
cur = cur[prop];
}
return matched;
}
function sibling(node, siblingName, nodeType, until) {
var result = [];
if (until instanceof DomQuery) {
until = until[0];
}
for (; node; node = node[siblingName]) {
if (nodeType && node.nodeType !== nodeType) {
continue;
}
if (until !== undefined) {
if (node === until) {
break;
}
if (typeof until == 'string' && DomQuery(node).is(until)) {
break;
}
}
result.push(node);
}
return result;
}
function firstSibling(node, siblingName, nodeType) {
for (node = node[siblingName]; node; node = node[siblingName]) {
if (node.nodeType == nodeType) {
return node;
}
}
return null;
}
each({
/**
* Returns a new collection with the parent of each item in current collection matching the optional selector.
*
* @method parent
* @param {Element/tinymce.dom.DomQuery} node Node to match parents against.
* @return {tinymce.dom.DomQuery} New DomQuery instance with all matching parents.
*/
parent: function(node) {
var parent = node.parentNode;
return parent && parent.nodeType !== 11 ? parent : null;
},
/**
* Returns a new collection with the all the parents of each item in current collection matching the optional selector.
*
* @method parents
* @param {Element/tinymce.dom.DomQuery} node Node to match parents against.
* @return {tinymce.dom.DomQuery} New DomQuery instance with all matching parents.
*/
parents: function(node) {
return dir(node, "parentNode");
},
/**
* Returns a new collection with next sibling of each item in current collection matching the optional selector.
*
* @method next
* @param {Element/tinymce.dom.DomQuery} node Node to match the next element against.
* @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
*/
next: function(node) {
return firstSibling(node, 'nextSibling', 1);
},
/**
* Returns a new collection with previous sibling of each item in current collection matching the optional selector.
*
* @method prev
* @param {Element/tinymce.dom.DomQuery} node Node to match the previous element against.
* @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
*/
prev: function(node) {
return firstSibling(node, 'previousSibling', 1);
},
/**
* Returns all child elements matching the optional selector.
*
* @method children
* @param {Element/tinymce.dom.DomQuery} node Node to match the elements against.
* @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
*/
children: function(node) {
return sibling(node.firstChild, 'nextSibling', 1);
},
/**
* Returns all child nodes matching the optional selector.
*
* @method contents
* @param {Element/tinymce.dom.DomQuery} node Node to get the contents of.
* @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
*/
contents: function(node) {
return Tools.toArray((node.nodeName === "iframe" ? node.contentDocument || node.contentWindow.document : node).childNodes);
}
}, function(name, fn) {
DomQuery.fn[name] = function(selector) {
var self = this, result = [];
self.each(function() {
var nodes = fn.call(result, this, selector, result);
if (nodes) {
if (DomQuery.isArray(nodes)) {
result.push.apply(result, nodes);
} else {
result.push(nodes);
}
}
});
// If traversing on multiple elements we might get the same elements twice
if (this.length > 1) {
if (!skipUniques[name]) {
result = DomQuery.unique(result);
}
if (name.indexOf('parents') === 0) {
result = result.reverse();
}
}
result = DomQuery(result);
if (selector) {
return result.filter(selector);
}
return result;
};
});
each({
/**
* Returns a new collection with the all the parents until the matching selector/element
* of each item in current collection matching the optional selector.
*
* @method parentsUntil
* @param {Element/tinymce.dom.DomQuery} node Node to find parent of.
* @param {String/Element/tinymce.dom.DomQuery} until Until the matching selector or element.
* @return {tinymce.dom.DomQuery} New DomQuery instance with all matching parents.
*/
parentsUntil: function(node, until) {
return dir(node, "parentNode", until);
},
/**
* Returns a new collection with all next siblings of each item in current collection matching the optional selector.
*
* @method nextUntil
* @param {Element/tinymce.dom.DomQuery} node Node to find next siblings on.
* @param {String/Element/tinymce.dom.DomQuery} until Until the matching selector or element.
* @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
*/
nextUntil: function(node, until) {
return sibling(node, 'nextSibling', 1, until).slice(1);
},
/**
* Returns a new collection with all previous siblings of each item in current collection matching the optional selector.
*
* @method prevUntil
* @param {Element/tinymce.dom.DomQuery} node Node to find previous siblings on.
* @param {String/Element/tinymce.dom.DomQuery} until Until the matching selector or element.
* @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
*/
prevUntil: function(node, until) {
return sibling(node, 'previousSibling', 1, until).slice(1);
}
}, function(name, fn) {
DomQuery.fn[name] = function(selector, filter) {
var self = this, result = [];
self.each(function() {
var nodes = fn.call(result, this, selector, result);
if (nodes) {
if (DomQuery.isArray(nodes)) {
result.push.apply(result, nodes);
} else {
result.push(nodes);
}
}
});
// If traversing on multiple elements we might get the same elements twice
if (this.length > 1) {
result = DomQuery.unique(result);
if (name.indexOf('parents') === 0 || name === 'prevUntil') {
result = result.reverse();
}
}
result = DomQuery(result);
if (filter) {
return result.filter(filter);
}
return result;
};
});
/**
* Returns true/false if the current set items matches the selector.
*
* @method is
* @param {String} selector Selector to match the elements against.
* @return {Boolean} True/false if the current set matches the selector.
*/
DomQuery.fn.is = function(selector) {
return !!selector && this.filter(selector).length > 0;
};
DomQuery.fn.init.prototype = DomQuery.fn;
DomQuery.overrideDefaults = function(callback) {
var defaults;
function sub(selector, context) {
defaults = defaults || callback();
if (arguments.length === 0) {
selector = defaults.element;
}
if (!context) {
context = defaults.context;
}
return new sub.fn.init(selector, context);
}
DomQuery.extend(sub, this);
return sub;
};
function appendHooks(targetHooks, prop, hooks) {
each(hooks, function(name, func) {
targetHooks[name] = targetHooks[name] || {};
targetHooks[name][prop] = func;
});
}
if (Env.ie && Env.ie < 8) {
appendHooks(attrHooks, 'get', {
maxlength: function(elm) {
var value = elm.maxLength;
if (value === 0x7fffffff) {
return undef;
}
return value;
},
size: function(elm) {
var value = elm.size;
if (value === 20) {
return undef;
}
return value;
},
'class': function(elm) {
return elm.className;
},
style: function(elm) {
var value = elm.style.cssText;
if (value.length === 0) {
return undef;
}
return value;
}
});
appendHooks(attrHooks, 'set', {
'class': function(elm, value) {
elm.className = value;
},
style: function(elm, value) {
elm.style.cssText = value;
}
});
}
if (Env.ie && Env.ie < 9) {
/*jshint sub:true */
/*eslint dot-notation: 0*/
cssFix['float'] = 'styleFloat';
appendHooks(cssHooks, 'set', {
opacity: function(elm, value) {
var style = elm.style;
if (value === null || value === '') {
style.removeAttribute('filter');
} else {
style.zoom = 1;
style.filter = 'alpha(opacity=' + (value * 100) + ')';
}
}
});
}
DomQuery.attrHooks = attrHooks;
DomQuery.cssHooks = cssHooks;
return DomQuery;
});
// Included from: js/tinymce/classes/html/Styles.js
/**
* Styles.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class is used to parse CSS styles it also compresses styles to reduce the output size.
*
* @example
* var Styles = new tinymce.html.Styles({
* url_converter: function(url) {
* return url;
* }
* });
*
* styles = Styles.parse('border: 1px solid red');
* styles.color = 'red';
*
* console.log(new tinymce.html.StyleSerializer().serialize(styles));
*
* @class tinymce.html.Styles
* @version 3.4
*/
define("tinymce/html/Styles", [], function() {
return function(settings, schema) {
/*jshint maxlen:255 */
/*eslint max-len:0 */
var rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi,
urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi,
styleRegExp = /\s*([^:]+):\s*([^;]+);?/g,
trimRightRegExp = /\s+$/,
i, encodingLookup = {}, encodingItems, validStyles, invalidStyles, invisibleChar = '\uFEFF';
settings = settings || {};
if (schema) {
validStyles = schema.getValidStyles();
invalidStyles = schema.getInvalidStyles();
}
encodingItems = ('\\" \\\' \\; \\: ; : ' + invisibleChar).split(' ');
for (i = 0; i < encodingItems.length; i++) {
encodingLookup[encodingItems[i]] = invisibleChar + i;
encodingLookup[invisibleChar + i] = encodingItems[i];
}
function toHex(match, r, g, b) {
function hex(val) {
val = parseInt(val, 10).toString(16);
return val.length > 1 ? val : '0' + val; // 0 -> 00
}
return '#' + hex(r) + hex(g) + hex(b);
}
return {
/**
* Parses the specified RGB color value and returns a hex version of that color.
*
* @method toHex
* @param {String} color RGB string value like rgb(1,2,3)
* @return {String} Hex version of that RGB value like #FF00FF.
*/
toHex: function(color) {
return color.replace(rgbRegExp, toHex);
},
/**
* Parses the specified style value into an object collection. This parser will also
* merge and remove any redundant items that browsers might have added. It will also convert non hex
* colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings.
*
* @method parse
* @param {String} css Style value to parse for example: border:1px solid red;.
* @return {Object} Object representation of that style like {border: '1px solid red'}
*/
parse: function(css) {
var styles = {}, matches, name, value, isEncoded, urlConverter = settings.url_converter;
var urlConverterScope = settings.url_converter_scope || this;
function compress(prefix, suffix, noJoin) {
var top, right, bottom, left;
top = styles[prefix + '-top' + suffix];
if (!top) {
return;
}
right = styles[prefix + '-right' + suffix];
if (!right) {
return;
}
bottom = styles[prefix + '-bottom' + suffix];
if (!bottom) {
return;
}
left = styles[prefix + '-left' + suffix];
if (!left) {
return;
}
var box = [top, right, bottom, left];
i = box.length - 1;
while (i--) {
if (box[i] !== box[i + 1]) {
break;
}
}
if (i > -1 && noJoin) {
return;
}
styles[prefix + suffix] = i == -1 ? box[0] : box.join(' ');
delete styles[prefix + '-top' + suffix];
delete styles[prefix + '-right' + suffix];
delete styles[prefix + '-bottom' + suffix];
delete styles[prefix + '-left' + suffix];
}
/**
* Checks if the specific style can be compressed in other words if all border-width are equal.
*/
function canCompress(key) {
var value = styles[key], i;
if (!value) {
return;
}
value = value.split(' ');
i = value.length;
while (i--) {
if (value[i] !== value[0]) {
return false;
}
}
styles[key] = value[0];
return true;
}
/**
* Compresses multiple styles into one style.
*/
function compress2(target, a, b, c) {
if (!canCompress(a)) {
return;
}
if (!canCompress(b)) {
return;
}
if (!canCompress(c)) {
return;
}
// Compress
styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c];
delete styles[a];
delete styles[b];
delete styles[c];
}
// Encodes the specified string by replacing all \" \' ; : with _<num>
function encode(str) {
isEncoded = true;
return encodingLookup[str];
}
// Decodes the specified string by replacing all _<num> with it's original value \" \' etc
// It will also decode the \" \' if keep_slashes is set to fale or omitted
function decode(str, keep_slashes) {
if (isEncoded) {
str = str.replace(/\uFEFF[0-9]/g, function(str) {
return encodingLookup[str];
});
}
if (!keep_slashes) {
str = str.replace(/\\([\'\";:])/g, "$1");
}
return str;
}
function decodeSingleHexSequence(escSeq) {
return String.fromCharCode(parseInt(escSeq.slice(1), 16));
}
function decodeHexSequences(value) {
return value.replace(/\\[0-9a-f]+/gi, decodeSingleHexSequence);
}
function processUrl(match, url, url2, url3, str, str2) {
str = str || str2;
if (str) {
str = decode(str);
// Force strings into single quote format
return "'" + str.replace(/\'/g, "\\'") + "'";
}
url = decode(url || url2 || url3);
if (!settings.allow_script_urls) {
var scriptUrl = url.replace(/[\s\r\n]+/g, '');
if (/(java|vb)script:/i.test(scriptUrl)) {
return "";
}
if (!settings.allow_svg_data_urls && /^data:image\/svg/i.test(scriptUrl)) {
return "";
}
}
// Convert the URL to relative/absolute depending on config
if (urlConverter) {
url = urlConverter.call(urlConverterScope, url, 'style');
}
// Output new URL format
return "url('" + url.replace(/\'/g, "\\'") + "')";
}
if (css) {
css = css.replace(/[\u0000-\u001F]/g, '');
// Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing
css = css.replace(/\\[\"\';:\uFEFF]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, function(str) {
return str.replace(/[;:]/g, encode);
});
// Parse styles
while ((matches = styleRegExp.exec(css))) {
styleRegExp.lastIndex = matches.index + matches[0].length;
name = matches[1].replace(trimRightRegExp, '').toLowerCase();
value = matches[2].replace(trimRightRegExp, '');
if (name && value) {
// Decode escaped sequences like \65 -> e
name = decodeHexSequences(name);
value = decodeHexSequences(value);
// Skip properties with double quotes and sequences like \" \' in their names
// See 'mXSS Attacks: Attacking well-secured Web-Applications by using innerHTML Mutations'
// https://cure53.de/fp170.pdf
if (name.indexOf(invisibleChar) !== -1 || name.indexOf('"') !== -1) {
continue;
}
// Don't allow behavior name or expression/comments within the values
if (!settings.allow_script_urls && (name == "behavior" || /expression\s*\(|\/\*|\*\//.test(value))) {
continue;
}
// Opera will produce 700 instead of bold in their style values
if (name === 'font-weight' && value === '700') {
value = 'bold';
} else if (name === 'color' || name === 'background-color') { // Lowercase colors like RED
value = value.toLowerCase();
}
// Convert RGB colors to HEX
value = value.replace(rgbRegExp, toHex);
// Convert URLs and force them into url('value') format
value = value.replace(urlOrStrRegExp, processUrl);
styles[name] = isEncoded ? decode(value, true) : value;
}
}
// Compress the styles to reduce it's size for example IE will expand styles
compress("border", "", true);
compress("border", "-width");
compress("border", "-color");
compress("border", "-style");
compress("padding", "");
compress("margin", "");
compress2('border', 'border-width', 'border-style', 'border-color');
// Remove pointless border, IE produces these
if (styles.border === 'medium none') {
delete styles.border;
}
// IE 11 will produce a border-image: none when getting the style attribute from <p style="border: 1px solid red"></p>
// So let us assume it shouldn't be there
if (styles['border-image'] === 'none') {
delete styles['border-image'];
}
}
return styles;
},
/**
* Serializes the specified style object into a string.
*
* @method serialize
* @param {Object} styles Object to serialize as string for example: {border: '1px solid red'}
* @param {String} elementName Optional element name, if specified only the styles that matches the schema will be serialized.
* @return {String} String representation of the style object for example: border: 1px solid red.
*/
serialize: function(styles, elementName) {
var css = '', name, value;
function serializeStyles(name) {
var styleList, i, l, value;
styleList = validStyles[name];
if (styleList) {
for (i = 0, l = styleList.length; i < l; i++) {
name = styleList[i];
value = styles[name];
if (value) {
css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';';
}
}
}
}
function isValid(name, elementName) {
var styleMap;
styleMap = invalidStyles['*'];
if (styleMap && styleMap[name]) {
return false;
}
styleMap = invalidStyles[elementName];
if (styleMap && styleMap[name]) {
return false;
}
return true;
}
// Serialize styles according to schema
if (elementName && validStyles) {
// Serialize global styles and element specific styles
serializeStyles('*');
serializeStyles(elementName);
} else {
// Output the styles in the order they are inside the object
for (name in styles) {
value = styles[name];
if (value && (!invalidStyles || isValid(name, elementName))) {
css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';';
}
}
}
return css;
}
};
};
});
// Included from: js/tinymce/classes/dom/TreeWalker.js
/**
* TreeWalker.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* TreeWalker class enables you to walk the DOM in a linear manner.
*
* @class tinymce.dom.TreeWalker
* @example
* var walker = new tinymce.dom.TreeWalker(startNode);
*
* do {
* console.log(walker.current());
* } while (walker.next());
*/
define("tinymce/dom/TreeWalker", [], function() {
/**
* Constructs a new TreeWalker instance.
*
* @constructor
* @method TreeWalker
* @param {Node} startNode Node to start walking from.
* @param {node} rootNode Optional root node to never walk out of.
*/
return function(startNode, rootNode) {
var node = startNode;
function findSibling(node, startName, siblingName, shallow) {
var sibling, parent;
if (node) {
// Walk into nodes if it has a start
if (!shallow && node[startName]) {
return node[startName];
}
// Return the sibling if it has one
if (node != rootNode) {
sibling = node[siblingName];
if (sibling) {
return sibling;
}
// Walk up the parents to look for siblings
for (parent = node.parentNode; parent && parent != rootNode; parent = parent.parentNode) {
sibling = parent[siblingName];
if (sibling) {
return sibling;
}
}
}
}
}
function findPreviousNode(node, startName, siblingName, shallow) {
var sibling, parent, child;
if (node) {
sibling = node[siblingName];
if (rootNode && sibling === rootNode) {
return;
}
if (sibling) {
if (!shallow) {
// Walk up the parents to look for siblings
for (child = sibling[startName]; child; child = child[startName]) {
if (!child[startName]) {
return child;
}
}
}
return sibling;
}
parent = node.parentNode;
if (parent && parent !== rootNode) {
return parent;
}
}
}
/**
* Returns the current node.
*
* @method current
* @return {Node} Current node where the walker is.
*/
this.current = function() {
return node;
};
/**
* Walks to the next node in tree.
*
* @method next
* @return {Node} Current node where the walker is after moving to the next node.
*/
this.next = function(shallow) {
node = findSibling(node, 'firstChild', 'nextSibling', shallow);
return node;
};
/**
* Walks to the previous node in tree.
*
* @method prev
* @return {Node} Current node where the walker is after moving to the previous node.
*/
this.prev = function(shallow) {
node = findSibling(node, 'lastChild', 'previousSibling', shallow);
return node;
};
this.prev2 = function(shallow) {
node = findPreviousNode(node, 'lastChild', 'previousSibling', shallow);
return node;
};
};
});
// Included from: js/tinymce/classes/dom/Range.js
/**
* Range.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Old IE Range.
*
* @private
* @class tinymce.dom.Range
*/
define("tinymce/dom/Range", [
"tinymce/util/Tools"
], function(Tools) {
// Range constructor
function Range(dom) {
var self = this,
doc = dom.doc,
EXTRACT = 0,
CLONE = 1,
DELETE = 2,
TRUE = true,
FALSE = false,
START_OFFSET = 'startOffset',
START_CONTAINER = 'startContainer',
END_CONTAINER = 'endContainer',
END_OFFSET = 'endOffset',
extend = Tools.extend,
nodeIndex = dom.nodeIndex;
function createDocumentFragment() {
return doc.createDocumentFragment();
}
function setStart(n, o) {
_setEndPoint(TRUE, n, o);
}
function setEnd(n, o) {
_setEndPoint(FALSE, n, o);
}
function setStartBefore(n) {
setStart(n.parentNode, nodeIndex(n));
}
function setStartAfter(n) {
setStart(n.parentNode, nodeIndex(n) + 1);
}
function setEndBefore(n) {
setEnd(n.parentNode, nodeIndex(n));
}
function setEndAfter(n) {
setEnd(n.parentNode, nodeIndex(n) + 1);
}
function collapse(ts) {
if (ts) {
self[END_CONTAINER] = self[START_CONTAINER];
self[END_OFFSET] = self[START_OFFSET];
} else {
self[START_CONTAINER] = self[END_CONTAINER];
self[START_OFFSET] = self[END_OFFSET];
}
self.collapsed = TRUE;
}
function selectNode(n) {
setStartBefore(n);
setEndAfter(n);
}
function selectNodeContents(n) {
setStart(n, 0);
setEnd(n, n.nodeType === 1 ? n.childNodes.length : n.nodeValue.length);
}
function compareBoundaryPoints(h, r) {
var sc = self[START_CONTAINER], so = self[START_OFFSET], ec = self[END_CONTAINER], eo = self[END_OFFSET],
rsc = r.startContainer, rso = r.startOffset, rec = r.endContainer, reo = r.endOffset;
// Check START_TO_START
if (h === 0) {
return _compareBoundaryPoints(sc, so, rsc, rso);
}
// Check START_TO_END
if (h === 1) {
return _compareBoundaryPoints(ec, eo, rsc, rso);
}
// Check END_TO_END
if (h === 2) {
return _compareBoundaryPoints(ec, eo, rec, reo);
}
// Check END_TO_START
if (h === 3) {
return _compareBoundaryPoints(sc, so, rec, reo);
}
}
function deleteContents() {
_traverse(DELETE);
}
function extractContents() {
return _traverse(EXTRACT);
}
function cloneContents() {
return _traverse(CLONE);
}
function insertNode(n) {
var startContainer = this[START_CONTAINER],
startOffset = this[START_OFFSET], nn, o;
// Node is TEXT_NODE or CDATA
if ((startContainer.nodeType === 3 || startContainer.nodeType === 4) && startContainer.nodeValue) {
if (!startOffset) {
// At the start of text
startContainer.parentNode.insertBefore(n, startContainer);
} else if (startOffset >= startContainer.nodeValue.length) {
// At the end of text
dom.insertAfter(n, startContainer);
} else {
// Middle, need to split
nn = startContainer.splitText(startOffset);
startContainer.parentNode.insertBefore(n, nn);
}
} else {
// Insert element node
if (startContainer.childNodes.length > 0) {
o = startContainer.childNodes[startOffset];
}
if (o) {
startContainer.insertBefore(n, o);
} else {
if (startContainer.nodeType == 3) {
dom.insertAfter(n, startContainer);
} else {
startContainer.appendChild(n);
}
}
}
}
function surroundContents(n) {
var f = self.extractContents();
self.insertNode(n);
n.appendChild(f);
self.selectNode(n);
}
function cloneRange() {
return extend(new Range(dom), {
startContainer: self[START_CONTAINER],
startOffset: self[START_OFFSET],
endContainer: self[END_CONTAINER],
endOffset: self[END_OFFSET],
collapsed: self.collapsed,
commonAncestorContainer: self.commonAncestorContainer
});
}
// Private methods
function _getSelectedNode(container, offset) {
var child;
// TEXT_NODE
if (container.nodeType == 3) {
return container;
}
if (offset < 0) {
return container;
}
child = container.firstChild;
while (child && offset > 0) {
--offset;
child = child.nextSibling;
}
if (child) {
return child;
}
return container;
}
function _isCollapsed() {
return (self[START_CONTAINER] == self[END_CONTAINER] && self[START_OFFSET] == self[END_OFFSET]);
}
function _compareBoundaryPoints(containerA, offsetA, containerB, offsetB) {
var c, offsetC, n, cmnRoot, childA, childB;
// In the first case the boundary-points have the same container. A is before B
// if its offset is less than the offset of B, A is equal to B if its offset is
// equal to the offset of B, and A is after B if its offset is greater than the
// offset of B.
if (containerA == containerB) {
if (offsetA == offsetB) {
return 0; // equal
}
if (offsetA < offsetB) {
return -1; // before
}
return 1; // after
}
// In the second case a child node C of the container of A is an ancestor
// container of B. In this case, A is before B if the offset of A is less than or
// equal to the index of the child node C and A is after B otherwise.
c = containerB;
while (c && c.parentNode != containerA) {
c = c.parentNode;
}
if (c) {
offsetC = 0;
n = containerA.firstChild;
while (n != c && offsetC < offsetA) {
offsetC++;
n = n.nextSibling;
}
if (offsetA <= offsetC) {
return -1; // before
}
return 1; // after
}
// In the third case a child node C of the container of B is an ancestor container
// of A. In this case, A is before B if the index of the child node C is less than
// the offset of B and A is after B otherwise.
c = containerA;
while (c && c.parentNode != containerB) {
c = c.parentNode;
}
if (c) {
offsetC = 0;
n = containerB.firstChild;
while (n != c && offsetC < offsetB) {
offsetC++;
n = n.nextSibling;
}
if (offsetC < offsetB) {
return -1; // before
}
return 1; // after
}
// In the fourth case, none of three other cases hold: the containers of A and B
// are siblings or descendants of sibling nodes. In this case, A is before B if
// the container of A is before the container of B in a pre-order traversal of the
// Ranges' context tree and A is after B otherwise.
cmnRoot = dom.findCommonAncestor(containerA, containerB);
childA = containerA;
while (childA && childA.parentNode != cmnRoot) {
childA = childA.parentNode;
}
if (!childA) {
childA = cmnRoot;
}
childB = containerB;
while (childB && childB.parentNode != cmnRoot) {
childB = childB.parentNode;
}
if (!childB) {
childB = cmnRoot;
}
if (childA == childB) {
return 0; // equal
}
n = cmnRoot.firstChild;
while (n) {
if (n == childA) {
return -1; // before
}
if (n == childB) {
return 1; // after
}
n = n.nextSibling;
}
}
function _setEndPoint(st, n, o) {
var ec, sc;
if (st) {
self[START_CONTAINER] = n;
self[START_OFFSET] = o;
} else {
self[END_CONTAINER] = n;
self[END_OFFSET] = o;
}
// If one boundary-point of a Range is set to have a root container
// other than the current one for the Range, the Range is collapsed to
// the new position. This enforces the restriction that both boundary-
// points of a Range must have the same root container.
ec = self[END_CONTAINER];
while (ec.parentNode) {
ec = ec.parentNode;
}
sc = self[START_CONTAINER];
while (sc.parentNode) {
sc = sc.parentNode;
}
if (sc == ec) {
// The start position of a Range is guaranteed to never be after the
// end position. To enforce this restriction, if the start is set to
// be at a position after the end, the Range is collapsed to that
// position.
if (_compareBoundaryPoints(self[START_CONTAINER], self[START_OFFSET], self[END_CONTAINER], self[END_OFFSET]) > 0) {
self.collapse(st);
}
} else {
self.collapse(st);
}
self.collapsed = _isCollapsed();
self.commonAncestorContainer = dom.findCommonAncestor(self[START_CONTAINER], self[END_CONTAINER]);
}
function _traverse(how) {
var c, endContainerDepth = 0, startContainerDepth = 0, p, depthDiff, startNode, endNode, sp, ep;
if (self[START_CONTAINER] == self[END_CONTAINER]) {
return _traverseSameContainer(how);
}
for (c = self[END_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) {
if (p == self[START_CONTAINER]) {
return _traverseCommonStartContainer(c, how);
}
++endContainerDepth;
}
for (c = self[START_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) {
if (p == self[END_CONTAINER]) {
return _traverseCommonEndContainer(c, how);
}
++startContainerDepth;
}
depthDiff = startContainerDepth - endContainerDepth;
startNode = self[START_CONTAINER];
while (depthDiff > 0) {
startNode = startNode.parentNode;
depthDiff--;
}
endNode = self[END_CONTAINER];
while (depthDiff < 0) {
endNode = endNode.parentNode;
depthDiff++;
}
// ascend the ancestor hierarchy until we have a common parent.
for (sp = startNode.parentNode, ep = endNode.parentNode; sp != ep; sp = sp.parentNode, ep = ep.parentNode) {
startNode = sp;
endNode = ep;
}
return _traverseCommonAncestors(startNode, endNode, how);
}
function _traverseSameContainer(how) {
var frag, s, sub, n, cnt, sibling, xferNode, start, len;
if (how != DELETE) {
frag = createDocumentFragment();
}
// If selection is empty, just return the fragment
if (self[START_OFFSET] == self[END_OFFSET]) {
return frag;
}
// Text node needs special case handling
if (self[START_CONTAINER].nodeType == 3) { // TEXT_NODE
// get the substring
s = self[START_CONTAINER].nodeValue;
sub = s.substring(self[START_OFFSET], self[END_OFFSET]);
// set the original text node to its new value
if (how != CLONE) {
n = self[START_CONTAINER];
start = self[START_OFFSET];
len = self[END_OFFSET] - self[START_OFFSET];
if (start === 0 && len >= n.nodeValue.length - 1) {
n.parentNode.removeChild(n);
} else {
n.deleteData(start, len);
}
// Nothing is partially selected, so collapse to start point
self.collapse(TRUE);
}
if (how == DELETE) {
return;
}
if (sub.length > 0) {
frag.appendChild(doc.createTextNode(sub));
}
return frag;
}
// Copy nodes between the start/end offsets.
n = _getSelectedNode(self[START_CONTAINER], self[START_OFFSET]);
cnt = self[END_OFFSET] - self[START_OFFSET];
while (n && cnt > 0) {
sibling = n.nextSibling;
xferNode = _traverseFullySelected(n, how);
if (frag) {
frag.appendChild(xferNode);
}
--cnt;
n = sibling;
}
// Nothing is partially selected, so collapse to start point
if (how != CLONE) {
self.collapse(TRUE);
}
return frag;
}
function _traverseCommonStartContainer(endAncestor, how) {
var frag, n, endIdx, cnt, sibling, xferNode;
if (how != DELETE) {
frag = createDocumentFragment();
}
n = _traverseRightBoundary(endAncestor, how);
if (frag) {
frag.appendChild(n);
}
endIdx = nodeIndex(endAncestor);
cnt = endIdx - self[START_OFFSET];
if (cnt <= 0) {
// Collapse to just before the endAncestor, which
// is partially selected.
if (how != CLONE) {
self.setEndBefore(endAncestor);
self.collapse(FALSE);
}
return frag;
}
n = endAncestor.previousSibling;
while (cnt > 0) {
sibling = n.previousSibling;
xferNode = _traverseFullySelected(n, how);
if (frag) {
frag.insertBefore(xferNode, frag.firstChild);
}
--cnt;
n = sibling;
}
// Collapse to just before the endAncestor, which
// is partially selected.
if (how != CLONE) {
self.setEndBefore(endAncestor);
self.collapse(FALSE);
}
return frag;
}
function _traverseCommonEndContainer(startAncestor, how) {
var frag, startIdx, n, cnt, sibling, xferNode;
if (how != DELETE) {
frag = createDocumentFragment();
}
n = _traverseLeftBoundary(startAncestor, how);
if (frag) {
frag.appendChild(n);
}
startIdx = nodeIndex(startAncestor);
++startIdx; // Because we already traversed it
cnt = self[END_OFFSET] - startIdx;
n = startAncestor.nextSibling;
while (n && cnt > 0) {
sibling = n.nextSibling;
xferNode = _traverseFullySelected(n, how);
if (frag) {
frag.appendChild(xferNode);
}
--cnt;
n = sibling;
}
if (how != CLONE) {
self.setStartAfter(startAncestor);
self.collapse(TRUE);
}
return frag;
}
function _traverseCommonAncestors(startAncestor, endAncestor, how) {
var n, frag, startOffset, endOffset, cnt, sibling, nextSibling;
if (how != DELETE) {
frag = createDocumentFragment();
}
n = _traverseLeftBoundary(startAncestor, how);
if (frag) {
frag.appendChild(n);
}
startOffset = nodeIndex(startAncestor);
endOffset = nodeIndex(endAncestor);
++startOffset;
cnt = endOffset - startOffset;
sibling = startAncestor.nextSibling;
while (cnt > 0) {
nextSibling = sibling.nextSibling;
n = _traverseFullySelected(sibling, how);
if (frag) {
frag.appendChild(n);
}
sibling = nextSibling;
--cnt;
}
n = _traverseRightBoundary(endAncestor, how);
if (frag) {
frag.appendChild(n);
}
if (how != CLONE) {
self.setStartAfter(startAncestor);
self.collapse(TRUE);
}
return frag;
}
function _traverseRightBoundary(root, how) {
var next = _getSelectedNode(self[END_CONTAINER], self[END_OFFSET] - 1), parent, clonedParent;
var prevSibling, clonedChild, clonedGrandParent, isFullySelected = next != self[END_CONTAINER];
if (next == root) {
return _traverseNode(next, isFullySelected, FALSE, how);
}
parent = next.parentNode;
clonedParent = _traverseNode(parent, FALSE, FALSE, how);
while (parent) {
while (next) {
prevSibling = next.previousSibling;
clonedChild = _traverseNode(next, isFullySelected, FALSE, how);
if (how != DELETE) {
clonedParent.insertBefore(clonedChild, clonedParent.firstChild);
}
isFullySelected = TRUE;
next = prevSibling;
}
if (parent == root) {
return clonedParent;
}
next = parent.previousSibling;
parent = parent.parentNode;
clonedGrandParent = _traverseNode(parent, FALSE, FALSE, how);
if (how != DELETE) {
clonedGrandParent.appendChild(clonedParent);
}
clonedParent = clonedGrandParent;
}
}
function _traverseLeftBoundary(root, how) {
var next = _getSelectedNode(self[START_CONTAINER], self[START_OFFSET]), isFullySelected = next != self[START_CONTAINER];
var parent, clonedParent, nextSibling, clonedChild, clonedGrandParent;
if (next == root) {
return _traverseNode(next, isFullySelected, TRUE, how);
}
parent = next.parentNode;
clonedParent = _traverseNode(parent, FALSE, TRUE, how);
while (parent) {
while (next) {
nextSibling = next.nextSibling;
clonedChild = _traverseNode(next, isFullySelected, TRUE, how);
if (how != DELETE) {
clonedParent.appendChild(clonedChild);
}
isFullySelected = TRUE;
next = nextSibling;
}
if (parent == root) {
return clonedParent;
}
next = parent.nextSibling;
parent = parent.parentNode;
clonedGrandParent = _traverseNode(parent, FALSE, TRUE, how);
if (how != DELETE) {
clonedGrandParent.appendChild(clonedParent);
}
clonedParent = clonedGrandParent;
}
}
function _traverseNode(n, isFullySelected, isLeft, how) {
var txtValue, newNodeValue, oldNodeValue, offset, newNode;
if (isFullySelected) {
return _traverseFullySelected(n, how);
}
// TEXT_NODE
if (n.nodeType == 3) {
txtValue = n.nodeValue;
if (isLeft) {
offset = self[START_OFFSET];
newNodeValue = txtValue.substring(offset);
oldNodeValue = txtValue.substring(0, offset);
} else {
offset = self[END_OFFSET];
newNodeValue = txtValue.substring(0, offset);
oldNodeValue = txtValue.substring(offset);
}
if (how != CLONE) {
n.nodeValue = oldNodeValue;
}
if (how == DELETE) {
return;
}
newNode = dom.clone(n, FALSE);
newNode.nodeValue = newNodeValue;
return newNode;
}
if (how == DELETE) {
return;
}
return dom.clone(n, FALSE);
}
function _traverseFullySelected(n, how) {
if (how != DELETE) {
return how == CLONE ? dom.clone(n, TRUE) : n;
}
n.parentNode.removeChild(n);
}
function toStringIE() {
return dom.create('body', null, cloneContents()).outerText;
}
extend(self, {
// Initial states
startContainer: doc,
startOffset: 0,
endContainer: doc,
endOffset: 0,
collapsed: TRUE,
commonAncestorContainer: doc,
// Range constants
START_TO_START: 0,
START_TO_END: 1,
END_TO_END: 2,
END_TO_START: 3,
// Public methods
setStart: setStart,
setEnd: setEnd,
setStartBefore: setStartBefore,
setStartAfter: setStartAfter,
setEndBefore: setEndBefore,
setEndAfter: setEndAfter,
collapse: collapse,
selectNode: selectNode,
selectNodeContents: selectNodeContents,
compareBoundaryPoints: compareBoundaryPoints,
deleteContents: deleteContents,
extractContents: extractContents,
cloneContents: cloneContents,
insertNode: insertNode,
surroundContents: surroundContents,
cloneRange: cloneRange,
toStringIE: toStringIE
});
return self;
}
// Older IE versions doesn't let you override toString by it's constructor so we have to stick it in the prototype
Range.prototype.toString = function() {
return this.toStringIE();
};
return Range;
});
// Included from: js/tinymce/classes/html/Entities.js
/**
* Entities.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/*jshint bitwise:false */
/*eslint no-bitwise:0 */
/**
* Entity encoder class.
*
* @class tinymce.html.Entities
* @static
* @version 3.4
*/
define("tinymce/html/Entities", [
"tinymce/util/Tools"
], function(Tools) {
var makeMap = Tools.makeMap;
var namedEntities, baseEntities, reverseEntities,
attrsCharsRegExp = /[&<>\"\u0060\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
rawCharsRegExp = /[<>&\"\']/g,
entityRegExp = /&#([a-z0-9]+);?|&([a-z0-9]+);/gi,
asciiMap = {
128: "\u20AC", 130: "\u201A", 131: "\u0192", 132: "\u201E", 133: "\u2026", 134: "\u2020",
135: "\u2021", 136: "\u02C6", 137: "\u2030", 138: "\u0160", 139: "\u2039", 140: "\u0152",
142: "\u017D", 145: "\u2018", 146: "\u2019", 147: "\u201C", 148: "\u201D", 149: "\u2022",
150: "\u2013", 151: "\u2014", 152: "\u02DC", 153: "\u2122", 154: "\u0161", 155: "\u203A",
156: "\u0153", 158: "\u017E", 159: "\u0178"
};
// Raw entities
baseEntities = {
'\"': '&quot;', // Needs to be escaped since the YUI compressor would otherwise break the code
"'": '&#39;',
'<': '&lt;',
'>': '&gt;',
'&': '&amp;',
'\u0060': '&#96;'
};
// Reverse lookup table for raw entities
reverseEntities = {
'&lt;': '<',
'&gt;': '>',
'&amp;': '&',
'&quot;': '"',
'&apos;': "'"
};
// Decodes text by using the browser
function nativeDecode(text) {
var elm;
elm = document.createElement("div");
elm.innerHTML = text;
return elm.textContent || elm.innerText || text;
}
// Build a two way lookup table for the entities
function buildEntitiesLookup(items, radix) {
var i, chr, entity, lookup = {};
if (items) {
items = items.split(',');
radix = radix || 10;
// Build entities lookup table
for (i = 0; i < items.length; i += 2) {
chr = String.fromCharCode(parseInt(items[i], radix));
// Only add non base entities
if (!baseEntities[chr]) {
entity = '&' + items[i + 1] + ';';
lookup[chr] = entity;
lookup[entity] = chr;
}
}
return lookup;
}
}
// Unpack entities lookup where the numbers are in radix 32 to reduce the size
namedEntities = buildEntitiesLookup(
'50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' +
'5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' +
'5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' +
'5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' +
'68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' +
'6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' +
'6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' +
'75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' +
'7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' +
'7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' +
'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' +
'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' +
't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' +
'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' +
'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' +
'81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' +
'8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' +
'8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' +
'8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' +
'8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' +
'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' +
'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' +
'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' +
'80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' +
'811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro', 32);
var Entities = {
/**
* Encodes the specified string using raw entities. This means only the required XML base entities will be encoded.
*
* @method encodeRaw
* @param {String} text Text to encode.
* @param {Boolean} attr Optional flag to specify if the text is attribute contents.
* @return {String} Entity encoded text.
*/
encodeRaw: function(text, attr) {
return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
return baseEntities[chr] || chr;
});
},
/**
* Encoded the specified text with both the attributes and text entities. This function will produce larger text contents
* since it doesn't know if the context is within a attribute or text node. This was added for compatibility
* and is exposed as the DOMUtils.encode function.
*
* @method encodeAllRaw
* @param {String} text Text to encode.
* @return {String} Entity encoded text.
*/
encodeAllRaw: function(text) {
return ('' + text).replace(rawCharsRegExp, function(chr) {
return baseEntities[chr] || chr;
});
},
/**
* Encodes the specified string using numeric entities. The core entities will be
* encoded as named ones but all non lower ascii characters will be encoded into numeric entities.
*
* @method encodeNumeric
* @param {String} text Text to encode.
* @param {Boolean} attr Optional flag to specify if the text is attribute contents.
* @return {String} Entity encoded text.
*/
encodeNumeric: function(text, attr) {
return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
// Multi byte sequence convert it to a single entity
if (chr.length > 1) {
return '&#' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';';
}
return baseEntities[chr] || '&#' + chr.charCodeAt(0) + ';';
});
},
/**
* Encodes the specified string using named entities. The core entities will be encoded
* as named ones but all non lower ascii characters will be encoded into named entities.
*
* @method encodeNamed
* @param {String} text Text to encode.
* @param {Boolean} attr Optional flag to specify if the text is attribute contents.
* @param {Object} entities Optional parameter with entities to use.
* @return {String} Entity encoded text.
*/
encodeNamed: function(text, attr, entities) {
entities = entities || namedEntities;
return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
return baseEntities[chr] || entities[chr] || chr;
});
},
/**
* Returns an encode function based on the name(s) and it's optional entities.
*
* @method getEncodeFunc
* @param {String} name Comma separated list of encoders for example named,numeric.
* @param {String} entities Optional parameter with entities to use instead of the built in set.
* @return {function} Encode function to be used.
*/
getEncodeFunc: function(name, entities) {
entities = buildEntitiesLookup(entities) || namedEntities;
function encodeNamedAndNumeric(text, attr) {
return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
return baseEntities[chr] || entities[chr] || '&#' + chr.charCodeAt(0) + ';' || chr;
});
}
function encodeCustomNamed(text, attr) {
return Entities.encodeNamed(text, attr, entities);
}
// Replace + with , to be compatible with previous TinyMCE versions
name = makeMap(name.replace(/\+/g, ','));
// Named and numeric encoder
if (name.named && name.numeric) {
return encodeNamedAndNumeric;
}
// Named encoder
if (name.named) {
// Custom names
if (entities) {
return encodeCustomNamed;
}
return Entities.encodeNamed;
}
// Numeric
if (name.numeric) {
return Entities.encodeNumeric;
}
// Raw encoder
return Entities.encodeRaw;
},
/**
* Decodes the specified string, this will replace entities with raw UTF characters.
*
* @method decode
* @param {String} text Text to entity decode.
* @return {String} Entity decoded string.
*/
decode: function(text) {
return text.replace(entityRegExp, function(all, numeric) {
if (numeric) {
if (numeric.charAt(0).toLowerCase() === 'x') {
numeric = parseInt(numeric.substr(1), 16);
} else {
numeric = parseInt(numeric, 10);
}
// Support upper UTF
if (numeric > 0xFFFF) {
numeric -= 0x10000;
return String.fromCharCode(0xD800 + (numeric >> 10), 0xDC00 + (numeric & 0x3FF));
}
return asciiMap[numeric] || String.fromCharCode(numeric);
}
return reverseEntities[all] || namedEntities[all] || nativeDecode(all);
});
}
};
return Entities;
});
// Included from: js/tinymce/classes/dom/StyleSheetLoader.js
/**
* StyleSheetLoader.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class handles loading of external stylesheets and fires events when these are loaded.
*
* @class tinymce.dom.StyleSheetLoader
* @private
*/
define("tinymce/dom/StyleSheetLoader", [
"tinymce/util/Tools",
"tinymce/util/Delay"
], function(Tools, Delay) {
"use strict";
return function(document, settings) {
var idCount = 0, loadedStates = {}, maxLoadTime;
settings = settings || {};
maxLoadTime = settings.maxLoadTime || 5000;
function appendToHead(node) {
document.getElementsByTagName('head')[0].appendChild(node);
}
/**
* Loads the specified css style sheet file and call the loadedCallback once it's finished loading.
*
* @method load
* @param {String} url Url to be loaded.
* @param {Function} loadedCallback Callback to be executed when loaded.
* @param {Function} errorCallback Callback to be executed when failed loading.
*/
function load(url, loadedCallback, errorCallback) {
var link, style, startTime, state;
function passed() {
var callbacks = state.passed, i = callbacks.length;
while (i--) {
callbacks[i]();
}
state.status = 2;
state.passed = [];
state.failed = [];
}
function failed() {
var callbacks = state.failed, i = callbacks.length;
while (i--) {
callbacks[i]();
}
state.status = 3;
state.passed = [];
state.failed = [];
}
// Sniffs for older WebKit versions that have the link.onload but a broken one
function isOldWebKit() {
var webKitChunks = navigator.userAgent.match(/WebKit\/(\d*)/);
return !!(webKitChunks && webKitChunks[1] < 536);
}
// Calls the waitCallback until the test returns true or the timeout occurs
function wait(testCallback, waitCallback) {
if (!testCallback()) {
// Wait for timeout
if ((new Date().getTime()) - startTime < maxLoadTime) {
Delay.setTimeout(waitCallback);
} else {
failed();
}
}
}
// Workaround for WebKit that doesn't properly support the onload event for link elements
// Or WebKit that fires the onload event before the StyleSheet is added to the document
function waitForWebKitLinkLoaded() {
wait(function() {
var styleSheets = document.styleSheets, styleSheet, i = styleSheets.length, owner;
while (i--) {
styleSheet = styleSheets[i];
owner = styleSheet.ownerNode ? styleSheet.ownerNode : styleSheet.owningElement;
if (owner && owner.id === link.id) {
passed();
return true;
}
}
}, waitForWebKitLinkLoaded);
}
// Workaround for older Geckos that doesn't have any onload event for StyleSheets
function waitForGeckoLinkLoaded() {
wait(function() {
try {
// Accessing the cssRules will throw an exception until the CSS file is loaded
var cssRules = style.sheet.cssRules;
passed();
return !!cssRules;
} catch (ex) {
// Ignore
}
}, waitForGeckoLinkLoaded);
}
url = Tools._addCacheSuffix(url);
if (!loadedStates[url]) {
state = {
passed: [],
failed: []
};
loadedStates[url] = state;
} else {
state = loadedStates[url];
}
if (loadedCallback) {
state.passed.push(loadedCallback);
}
if (errorCallback) {
state.failed.push(errorCallback);
}
// Is loading wait for it to pass
if (state.status == 1) {
return;
}
// Has finished loading and was success
if (state.status == 2) {
passed();
return;
}
// Has finished loading and was a failure
if (state.status == 3) {
failed();
return;
}
// Start loading
state.status = 1;
link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.id = 'u' + (idCount++);
link.async = false;
link.defer = false;
startTime = new Date().getTime();
// Feature detect onload on link element and sniff older webkits since it has an broken onload event
if ("onload" in link && !isOldWebKit()) {
link.onload = waitForWebKitLinkLoaded;
link.onerror = failed;
} else {
// Sniff for old Firefox that doesn't support the onload event on link elements
// TODO: Remove this in the future when everyone uses modern browsers
if (navigator.userAgent.indexOf("Firefox") > 0) {
style = document.createElement('style');
style.textContent = '@import "' + url + '"';
waitForGeckoLinkLoaded();
appendToHead(style);
return;
}
// Use the id owner on older webkits
waitForWebKitLinkLoaded();
}
appendToHead(link);
link.href = url;
}
this.load = load;
};
});
// Included from: js/tinymce/classes/dom/DOMUtils.js
/**
* DOMUtils.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Utility class for various DOM manipulation and retrieval functions.
*
* @class tinymce.dom.DOMUtils
* @example
* // Add a class to an element by id in the page
* tinymce.DOM.addClass('someid', 'someclass');
*
* // Add a class to an element by id inside the editor
* tinymce.activeEditor.dom.addClass('someid', 'someclass');
*/
define("tinymce/dom/DOMUtils", [
"tinymce/dom/Sizzle",
"tinymce/dom/DomQuery",
"tinymce/html/Styles",
"tinymce/dom/EventUtils",
"tinymce/dom/TreeWalker",
"tinymce/dom/Range",
"tinymce/html/Entities",
"tinymce/Env",
"tinymce/util/Tools",
"tinymce/dom/StyleSheetLoader"
], function(Sizzle, $, Styles, EventUtils, TreeWalker, Range, Entities, Env, Tools, StyleSheetLoader) {
// Shorten names
var each = Tools.each, is = Tools.is, grep = Tools.grep, trim = Tools.trim;
var isIE = Env.ie;
var simpleSelectorRe = /^([a-z0-9],?)+$/i;
var whiteSpaceRegExp = /^[ \t\r\n]*$/;
function setupAttrHooks(domUtils, settings) {
var attrHooks = {}, keepValues = settings.keep_values, keepUrlHook;
keepUrlHook = {
set: function($elm, value, name) {
if (settings.url_converter) {
value = settings.url_converter.call(settings.url_converter_scope || domUtils, value, name, $elm[0]);
}
$elm.attr('data-mce-' + name, value).attr(name, value);
},
get: function($elm, name) {
return $elm.attr('data-mce-' + name) || $elm.attr(name);
}
};
attrHooks = {
style: {
set: function($elm, value) {
if (value !== null && typeof value === 'object') {
$elm.css(value);
return;
}
if (keepValues) {
$elm.attr('data-mce-style', value);
}
$elm.attr('style', value);
},
get: function($elm) {
var value = $elm.attr('data-mce-style') || $elm.attr('style');
value = domUtils.serializeStyle(domUtils.parseStyle(value), $elm[0].nodeName);
return value;
}
}
};
if (keepValues) {
attrHooks.href = attrHooks.src = keepUrlHook;
}
return attrHooks;
}
function updateInternalStyleAttr(domUtils, $elm) {
var value = $elm.attr('style');
value = domUtils.serializeStyle(domUtils.parseStyle(value), $elm[0].nodeName);
if (!value) {
value = null;
}
$elm.attr('data-mce-style', value);
}
function nodeIndex(node, normalized) {
var idx = 0, lastNodeType, nodeType;
if (node) {
for (lastNodeType = node.nodeType, node = node.previousSibling; node; node = node.previousSibling) {
nodeType = node.nodeType;
// Normalize text nodes
if (normalized && nodeType == 3) {
if (nodeType == lastNodeType || !node.nodeValue.length) {
continue;
}
}
idx++;
lastNodeType = nodeType;
}
}
return idx;
}
/**
* Constructs a new DOMUtils instance. Consult the Wiki for more details on settings etc for this class.
*
* @constructor
* @method DOMUtils
* @param {Document} doc Document reference to bind the utility class to.
* @param {settings} settings Optional settings collection.
*/
function DOMUtils(doc, settings) {
var self = this, blockElementsMap;
self.doc = doc;
self.win = window;
self.files = {};
self.counter = 0;
self.stdMode = !isIE || doc.documentMode >= 8;
self.boxModel = !isIE || doc.compatMode == "CSS1Compat" || self.stdMode;
self.styleSheetLoader = new StyleSheetLoader(doc);
self.boundEvents = [];
self.settings = settings = settings || {};
self.schema = settings.schema;
self.styles = new Styles({
url_converter: settings.url_converter,
url_converter_scope: settings.url_converter_scope
}, settings.schema);
self.fixDoc(doc);
self.events = settings.ownEvents ? new EventUtils(settings.proxy) : EventUtils.Event;
self.attrHooks = setupAttrHooks(self, settings);
blockElementsMap = settings.schema ? settings.schema.getBlockElements() : {};
self.$ = $.overrideDefaults(function() {
return {
context: doc,
element: self.getRoot()
};
});
/**
* Returns true/false if the specified element is a block element or not.
*
* @method isBlock
* @param {Node/String} node Element/Node to check.
* @return {Boolean} True/False state if the node is a block element or not.
*/
self.isBlock = function(node) {
// Fix for #5446
if (!node) {
return false;
}
// This function is called in module pattern style since it might be executed with the wrong this scope
var type = node.nodeType;
// If it's a node then check the type and use the nodeName
if (type) {
return !!(type === 1 && blockElementsMap[node.nodeName]);
}
return !!blockElementsMap[node];
};
}
DOMUtils.prototype = {
$$: function(elm) {
if (typeof elm == 'string') {
elm = this.get(elm);
}
return this.$(elm);
},
root: null,
fixDoc: function(doc) {
var settings = this.settings, name;
if (isIE && settings.schema) {
// Add missing HTML 4/5 elements to IE
('abbr article aside audio canvas ' +
'details figcaption figure footer ' +
'header hgroup mark menu meter nav ' +
'output progress section summary ' +
'time video').replace(/\w+/g, function(name) {
doc.createElement(name);
});
// Create all custom elements
for (name in settings.schema.getCustomElements()) {
doc.createElement(name);
}
}
},
clone: function(node, deep) {
var self = this, clone, doc;
// TODO: Add feature detection here in the future
if (!isIE || node.nodeType !== 1 || deep) {
return node.cloneNode(deep);
}
doc = self.doc;
// Make a HTML5 safe shallow copy
if (!deep) {
clone = doc.createElement(node.nodeName);
// Copy attribs
each(self.getAttribs(node), function(attr) {
self.setAttrib(clone, attr.nodeName, self.getAttrib(node, attr.nodeName));
});
return clone;
}
return clone.firstChild;
},
/**
* Returns the root node of the document. This is normally the body but might be a DIV. Parents like getParent will not
* go above the point of this root node.
*
* @method getRoot
* @return {Element} Root element for the utility class.
*/
getRoot: function() {
var self = this;
return self.settings.root_element || self.doc.body;
},
/**
* Returns the viewport of the window.
*
* @method getViewPort
* @param {Window} win Optional window to get viewport of.
* @return {Object} Viewport object with fields x, y, w and h.
*/
getViewPort: function(win) {
var doc, rootElm;
win = !win ? this.win : win;
doc = win.document;
rootElm = this.boxModel ? doc.documentElement : doc.body;
// Returns viewport size excluding scrollbars
return {
x: win.pageXOffset || rootElm.scrollLeft,
y: win.pageYOffset || rootElm.scrollTop,
w: win.innerWidth || rootElm.clientWidth,
h: win.innerHeight || rootElm.clientHeight
};
},
/**
* Returns the rectangle for a specific element.
*
* @method getRect
* @param {Element/String} elm Element object or element ID to get rectangle from.
* @return {object} Rectangle for specified element object with x, y, w, h fields.
*/
getRect: function(elm) {
var self = this, pos, size;
elm = self.get(elm);
pos = self.getPos(elm);
size = self.getSize(elm);
return {
x: pos.x, y: pos.y,
w: size.w, h: size.h
};
},
/**
* Returns the size dimensions of the specified element.
*
* @method getSize
* @param {Element/String} elm Element object or element ID to get rectangle from.
* @return {object} Rectangle for specified element object with w, h fields.
*/
getSize: function(elm) {
var self = this, w, h;
elm = self.get(elm);
w = self.getStyle(elm, 'width');
h = self.getStyle(elm, 'height');
// Non pixel value, then force offset/clientWidth
if (w.indexOf('px') === -1) {
w = 0;
}
// Non pixel value, then force offset/clientWidth
if (h.indexOf('px') === -1) {
h = 0;
}
return {
w: parseInt(w, 10) || elm.offsetWidth || elm.clientWidth,
h: parseInt(h, 10) || elm.offsetHeight || elm.clientHeight
};
},
/**
* Returns a node by the specified selector function. This function will
* loop through all parent nodes and call the specified function for each node.
* If the function then returns true indicating that it has found what it was looking for, the loop execution will then end
* and the node it found will be returned.
*
* @method getParent
* @param {Node/String} node DOM node to search parents on or ID string.
* @param {function} selector Selection function or CSS selector to execute on each node.
* @param {Node} root Optional root element, never go below this point.
* @return {Node} DOM Node or null if it wasn't found.
*/
getParent: function(node, selector, root) {
return this.getParents(node, selector, root, false);
},
/**
* Returns a node list of all parents matching the specified selector function or pattern.
* If the function then returns true indicating that it has found what it was looking for and that node will be collected.
*
* @method getParents
* @param {Node/String} node DOM node to search parents on or ID string.
* @param {function} selector Selection function to execute on each node or CSS pattern.
* @param {Node} root Optional root element, never go below this point.
* @return {Array} Array of nodes or null if it wasn't found.
*/
getParents: function(node, selector, root, collect) {
var self = this, selectorVal, result = [];
node = self.get(node);
collect = collect === undefined;
// Default root on inline mode
root = root || (self.getRoot().nodeName != 'BODY' ? self.getRoot().parentNode : null);
// Wrap node name as func
if (is(selector, 'string')) {
selectorVal = selector;
if (selector === '*') {
selector = function(node) {
return node.nodeType == 1;
};
} else {
selector = function(node) {
return self.is(node, selectorVal);
};
}
}
while (node) {
if (node == root || !node.nodeType || node.nodeType === 9) {
break;
}
if (!selector || selector(node)) {
if (collect) {
result.push(node);
} else {
return node;
}
}
node = node.parentNode;
}
return collect ? result : null;
},
/**
* Returns the specified element by ID or the input element if it isn't a string.
*
* @method get
* @param {String/Element} n Element id to look for or element to just pass though.
* @return {Element} Element matching the specified id or null if it wasn't found.
*/
get: function(elm) {
var name;
if (elm && this.doc && typeof elm == 'string') {
name = elm;
elm = this.doc.getElementById(elm);
// IE and Opera returns meta elements when they match the specified input ID, but getElementsByName seems to do the trick
if (elm && elm.id !== name) {
return this.doc.getElementsByName(name)[1];
}
}
return elm;
},
/**
* Returns the next node that matches selector or function
*
* @method getNext
* @param {Node} node Node to find siblings from.
* @param {String/function} selector Selector CSS expression or function.
* @return {Node} Next node item matching the selector or null if it wasn't found.
*/
getNext: function(node, selector) {
return this._findSib(node, selector, 'nextSibling');
},
/**
* Returns the previous node that matches selector or function
*
* @method getPrev
* @param {Node} node Node to find siblings from.
* @param {String/function} selector Selector CSS expression or function.
* @return {Node} Previous node item matching the selector or null if it wasn't found.
*/
getPrev: function(node, selector) {
return this._findSib(node, selector, 'previousSibling');
},
// #ifndef jquery
/**
* Selects specific elements by a CSS level 3 pattern. For example "div#a1 p.test".
* This function is optimized for the most common patterns needed in TinyMCE but it also performs well enough
* on more complex patterns.
*
* @method select
* @param {String} selector CSS level 3 pattern to select/find elements by.
* @param {Object} scope Optional root element/scope element to search in.
* @return {Array} Array with all matched elements.
* @example
* // Adds a class to all paragraphs in the currently active editor
* tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass');
*
* // Adds a class to all spans that have the test class in the currently active editor
* tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('span.test'), 'someclass')
*/
select: function(selector, scope) {
var self = this;
/*eslint new-cap:0 */
return Sizzle(selector, self.get(scope) || self.settings.root_element || self.doc, []);
},
/**
* Returns true/false if the specified element matches the specified css pattern.
*
* @method is
* @param {Node/NodeList} elm DOM node to match or an array of nodes to match.
* @param {String} selector CSS pattern to match the element against.
*/
is: function(elm, selector) {
var i;
// If it isn't an array then try to do some simple selectors instead of Sizzle for to boost performance
if (elm.length === undefined) {
// Simple all selector
if (selector === '*') {
return elm.nodeType == 1;
}
// Simple selector just elements
if (simpleSelectorRe.test(selector)) {
selector = selector.toLowerCase().split(/,/);
elm = elm.nodeName.toLowerCase();
for (i = selector.length - 1; i >= 0; i--) {
if (selector[i] == elm) {
return true;
}
}
return false;
}
}
// Is non element
if (elm.nodeType && elm.nodeType != 1) {
return false;
}
var elms = elm.nodeType ? [elm] : elm;
/*eslint new-cap:0 */
return Sizzle(selector, elms[0].ownerDocument || elms[0], null, elms).length > 0;
},
// #endif
/**
* Adds the specified element to another element or elements.
*
* @method add
* @param {String/Element/Array} parentElm Element id string, DOM node element or array of ids or elements to add to.
* @param {String/Element} name Name of new element to add or existing element to add.
* @param {Object} attrs Optional object collection with arguments to add to the new element(s).
* @param {String} html Optional inner HTML contents to add for each element.
* @param {Boolean} create Optional flag if the element should be created or added.
* @return {Element/Array} Element that got created, or an array of created elements if multiple input elements
* were passed in.
* @example
* // Adds a new paragraph to the end of the active editor
* tinymce.activeEditor.dom.add(tinymce.activeEditor.getBody(), 'p', {title: 'my title'}, 'Some content');
*/
add: function(parentElm, name, attrs, html, create) {
var self = this;
return this.run(parentElm, function(parentElm) {
var newElm;
newElm = is(name, 'string') ? self.doc.createElement(name) : name;
self.setAttribs(newElm, attrs);
if (html) {
if (html.nodeType) {
newElm.appendChild(html);
} else {
self.setHTML(newElm, html);
}
}
return !create ? parentElm.appendChild(newElm) : newElm;
});
},
/**
* Creates a new element.
*
* @method create
* @param {String} name Name of new element.
* @param {Object} attrs Optional object name/value collection with element attributes.
* @param {String} html Optional HTML string to set as inner HTML of the element.
* @return {Element} HTML DOM node element that got created.
* @example
* // Adds an element where the caret/selection is in the active editor
* var el = tinymce.activeEditor.dom.create('div', {id: 'test', 'class': 'myclass'}, 'some content');
* tinymce.activeEditor.selection.setNode(el);
*/
create: function(name, attrs, html) {
return this.add(this.doc.createElement(name), name, attrs, html, 1);
},
/**
* Creates HTML string for element. The element will be closed unless an empty inner HTML string is passed in.
*
* @method createHTML
* @param {String} name Name of new element.
* @param {Object} attrs Optional object name/value collection with element attributes.
* @param {String} html Optional HTML string to set as inner HTML of the element.
* @return {String} String with new HTML element, for example: <a href="#">test</a>.
* @example
* // Creates a html chunk and inserts it at the current selection/caret location
* tinymce.activeEditor.selection.setContent(tinymce.activeEditor.dom.createHTML('a', {href: 'test.html'}, 'some line'));
*/
createHTML: function(name, attrs, html) {
var outHtml = '', key;
outHtml += '<' + name;
for (key in attrs) {
if (attrs.hasOwnProperty(key) && attrs[key] !== null && typeof attrs[key] != 'undefined') {
outHtml += ' ' + key + '="' + this.encode(attrs[key]) + '"';
}
}
// A call to tinymce.is doesn't work for some odd reason on IE9 possible bug inside their JS runtime
if (typeof html != "undefined") {
return outHtml + '>' + html + '</' + name + '>';
}
return outHtml + ' />';
},
/**
* Creates a document fragment out of the specified HTML string.
*
* @method createFragment
* @param {String} html Html string to create fragment from.
* @return {DocumentFragment} Document fragment node.
*/
createFragment: function(html) {
var frag, node, doc = this.doc, container;
container = doc.createElement("div");
frag = doc.createDocumentFragment();
if (html) {
container.innerHTML = html;
}
while ((node = container.firstChild)) {
frag.appendChild(node);
}
return frag;
},
/**
* Removes/deletes the specified element(s) from the DOM.
*
* @method remove
* @param {String/Element/Array} node ID of element or DOM element object or array containing multiple elements/ids.
* @param {Boolean} keepChildren Optional state to keep children or not. If set to true all children will be
* placed at the location of the removed element.
* @return {Element/Array} HTML DOM element that got removed, or an array of removed elements if multiple input elements
* were passed in.
* @example
* // Removes all paragraphs in the active editor
* tinymce.activeEditor.dom.remove(tinymce.activeEditor.dom.select('p'));
*
* // Removes an element by id in the document
* tinymce.DOM.remove('mydiv');
*/
remove: function(node, keepChildren) {
node = this.$$(node);
if (keepChildren) {
node.each(function() {
var child;
while ((child = this.firstChild)) {
if (child.nodeType == 3 && child.data.length === 0) {
this.removeChild(child);
} else {
this.parentNode.insertBefore(child, this);
}
}
}).remove();
} else {
node.remove();
}
return node.length > 1 ? node.toArray() : node[0];
},
/**
* Sets the CSS style value on a HTML element. The name can be a camelcase string
* or the CSS style name like background-color.
*
* @method setStyle
* @param {String/Element/Array} elm HTML element/Array of elements to set CSS style value on.
* @param {String} name Name of the style value to set.
* @param {String} value Value to set on the style.
* @example
* // Sets a style value on all paragraphs in the currently active editor
* tinymce.activeEditor.dom.setStyle(tinymce.activeEditor.dom.select('p'), 'background-color', 'red');
*
* // Sets a style value to an element by id in the current document
* tinymce.DOM.setStyle('mydiv', 'background-color', 'red');
*/
setStyle: function(elm, name, value) {
elm = this.$$(elm).css(name, value);
if (this.settings.update_styles) {
updateInternalStyleAttr(this, elm);
}
},
/**
* Returns the current style or runtime/computed value of an element.
*
* @method getStyle
* @param {String/Element} elm HTML element or element id string to get style from.
* @param {String} name Style name to return.
* @param {Boolean} computed Computed style.
* @return {String} Current style or computed style value of an element.
*/
getStyle: function(elm, name, computed) {
elm = this.$$(elm);
if (computed) {
return elm.css(name);
}
// Camelcase it, if needed
name = name.replace(/-(\D)/g, function(a, b) {
return b.toUpperCase();
});
if (name == 'float') {
name = Env.ie && Env.ie < 12 ? 'styleFloat' : 'cssFloat';
}
return elm[0] && elm[0].style ? elm[0].style[name] : undefined;
},
/**
* Sets multiple styles on the specified element(s).
*
* @method setStyles
* @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set styles on.
* @param {Object} styles Name/Value collection of style items to add to the element(s).
* @example
* // Sets styles on all paragraphs in the currently active editor
* tinymce.activeEditor.dom.setStyles(tinymce.activeEditor.dom.select('p'), {'background-color': 'red', 'color': 'green'});
*
* // Sets styles to an element by id in the current document
* tinymce.DOM.setStyles('mydiv', {'background-color': 'red', 'color': 'green'});
*/
setStyles: function(elm, styles) {
elm = this.$$(elm).css(styles);
if (this.settings.update_styles) {
updateInternalStyleAttr(this, elm);
}
},
/**
* Removes all attributes from an element or elements.
*
* @method removeAllAttribs
* @param {Element/String/Array} e DOM element, element id string or array of elements/ids to remove attributes from.
*/
removeAllAttribs: function(e) {
return this.run(e, function(e) {
var i, attrs = e.attributes;
for (i = attrs.length - 1; i >= 0; i--) {
e.removeAttributeNode(attrs.item(i));
}
});
},
/**
* Sets the specified attribute of an element or elements.
*
* @method setAttrib
* @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set attribute on.
* @param {String} name Name of attribute to set.
* @param {String} value Value to set on the attribute - if this value is falsy like null, 0 or '' it will remove
* the attribute instead.
* @example
* // Sets class attribute on all paragraphs in the active editor
* tinymce.activeEditor.dom.setAttrib(tinymce.activeEditor.dom.select('p'), 'class', 'myclass');
*
* // Sets class attribute on a specific element in the current page
* tinymce.dom.setAttrib('mydiv', 'class', 'myclass');
*/
setAttrib: function(elm, name, value) {
var self = this, originalValue, hook, settings = self.settings;
if (value === '') {
value = null;
}
elm = self.$$(elm);
originalValue = elm.attr(name);
if (!elm.length) {
return;
}
hook = self.attrHooks[name];
if (hook && hook.set) {
hook.set(elm, value, name);
} else {
elm.attr(name, value);
}
if (originalValue != value && settings.onSetAttrib) {
settings.onSetAttrib({
attrElm: elm,
attrName: name,
attrValue: value
});
}
},
/**
* Sets two or more specified attributes of an element or elements.
*
* @method setAttribs
* @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set attributes on.
* @param {Object} attrs Name/Value collection of attribute items to add to the element(s).
* @example
* // Sets class and title attributes on all paragraphs in the active editor
* tinymce.activeEditor.dom.setAttribs(tinymce.activeEditor.dom.select('p'), {'class': 'myclass', title: 'some title'});
*
* // Sets class and title attributes on a specific element in the current page
* tinymce.DOM.setAttribs('mydiv', {'class': 'myclass', title: 'some title'});
*/
setAttribs: function(elm, attrs) {
var self = this;
self.$$(elm).each(function(i, node) {
each(attrs, function(value, name) {
self.setAttrib(node, name, value);
});
});
},
/**
* Returns the specified attribute by name.
*
* @method getAttrib
* @param {String/Element} elm Element string id or DOM element to get attribute from.
* @param {String} name Name of attribute to get.
* @param {String} defaultVal Optional default value to return if the attribute didn't exist.
* @return {String} Attribute value string, default value or null if the attribute wasn't found.
*/
getAttrib: function(elm, name, defaultVal) {
var self = this, hook, value;
elm = self.$$(elm);
if (elm.length) {
hook = self.attrHooks[name];
if (hook && hook.get) {
value = hook.get(elm, name);
} else {
value = elm.attr(name);
}
}
if (typeof value == 'undefined') {
value = defaultVal || '';
}
return value;
},
/**
* Returns the absolute x, y position of a node. The position will be returned in an object with x, y fields.
*
* @method getPos
* @param {Element/String} elm HTML element or element id to get x, y position from.
* @param {Element} rootElm Optional root element to stop calculations at.
* @return {object} Absolute position of the specified element object with x, y fields.
*/
getPos: function(elm, rootElm) {
var self = this, x = 0, y = 0, offsetParent, doc = self.doc, body = doc.body, pos;
elm = self.get(elm);
rootElm = rootElm || body;
if (elm) {
// Use getBoundingClientRect if it exists since it's faster than looping offset nodes
// Fallback to offsetParent calculations if the body isn't static better since it stops at the body root
if (rootElm === body && elm.getBoundingClientRect && $(body).css('position') === 'static') {
pos = elm.getBoundingClientRect();
rootElm = self.boxModel ? doc.documentElement : body;
// Add scroll offsets from documentElement or body since IE with the wrong box model will use d.body and so do WebKit
// Also remove the body/documentelement clientTop/clientLeft on IE 6, 7 since they offset the position
x = pos.left + (doc.documentElement.scrollLeft || body.scrollLeft) - rootElm.clientLeft;
y = pos.top + (doc.documentElement.scrollTop || body.scrollTop) - rootElm.clientTop;
return {x: x, y: y};
}
offsetParent = elm;
while (offsetParent && offsetParent != rootElm && offsetParent.nodeType) {
x += offsetParent.offsetLeft || 0;
y += offsetParent.offsetTop || 0;
offsetParent = offsetParent.offsetParent;
}
offsetParent = elm.parentNode;
while (offsetParent && offsetParent != rootElm && offsetParent.nodeType) {
x -= offsetParent.scrollLeft || 0;
y -= offsetParent.scrollTop || 0;
offsetParent = offsetParent.parentNode;
}
}
return {x: x, y: y};
},
/**
* Parses the specified style value into an object collection. This parser will also
* merge and remove any redundant items that browsers might have added. It will also convert non-hex
* colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings.
*
* @method parseStyle
* @param {String} cssText Style value to parse, for example: border:1px solid red;.
* @return {Object} Object representation of that style, for example: {border: '1px solid red'}
*/
parseStyle: function(cssText) {
return this.styles.parse(cssText);
},
/**
* Serializes the specified style object into a string.
*
* @method serializeStyle
* @param {Object} styles Object to serialize as string, for example: {border: '1px solid red'}
* @param {String} name Optional element name.
* @return {String} String representation of the style object, for example: border: 1px solid red.
*/
serializeStyle: function(styles, name) {
return this.styles.serialize(styles, name);
},
/**
* Adds a style element at the top of the document with the specified cssText content.
*
* @method addStyle
* @param {String} cssText CSS Text style to add to top of head of document.
*/
addStyle: function(cssText) {
var self = this, doc = self.doc, head, styleElm;
// Prevent inline from loading the same styles twice
if (self !== DOMUtils.DOM && doc === document) {
var addedStyles = DOMUtils.DOM.addedStyles;
addedStyles = addedStyles || [];
if (addedStyles[cssText]) {
return;
}
addedStyles[cssText] = true;
DOMUtils.DOM.addedStyles = addedStyles;
}
// Create style element if needed
styleElm = doc.getElementById('mceDefaultStyles');
if (!styleElm) {
styleElm = doc.createElement('style');
styleElm.id = 'mceDefaultStyles';
styleElm.type = 'text/css';
head = doc.getElementsByTagName('head')[0];
if (head.firstChild) {
head.insertBefore(styleElm, head.firstChild);
} else {
head.appendChild(styleElm);
}
}
// Append style data to old or new style element
if (styleElm.styleSheet) {
styleElm.styleSheet.cssText += cssText;
} else {
styleElm.appendChild(doc.createTextNode(cssText));
}
},
/**
* Imports/loads the specified CSS file into the document bound to the class.
*
* @method loadCSS
* @param {String} url URL to CSS file to load.
* @example
* // Loads a CSS file dynamically into the current document
* tinymce.DOM.loadCSS('somepath/some.css');
*
* // Loads a CSS file into the currently active editor instance
* tinymce.activeEditor.dom.loadCSS('somepath/some.css');
*
* // Loads a CSS file into an editor instance by id
* tinymce.get('someid').dom.loadCSS('somepath/some.css');
*
* // Loads multiple CSS files into the current document
* tinymce.DOM.loadCSS('somepath/some.css,somepath/someother.css');
*/
loadCSS: function(url) {
var self = this, doc = self.doc, head;
// Prevent inline from loading the same CSS file twice
if (self !== DOMUtils.DOM && doc === document) {
DOMUtils.DOM.loadCSS(url);
return;
}
if (!url) {
url = '';
}
head = doc.getElementsByTagName('head')[0];
each(url.split(','), function(url) {
var link;
url = Tools._addCacheSuffix(url);
if (self.files[url]) {
return;
}
self.files[url] = true;
link = self.create('link', {rel: 'stylesheet', href: url});
// IE 8 has a bug where dynamically loading stylesheets would produce a 1 item remaining bug
// This fix seems to resolve that issue by recalcing the document once a stylesheet finishes loading
// It's ugly but it seems to work fine.
if (isIE && doc.documentMode && doc.recalc) {
link.onload = function() {
if (doc.recalc) {
doc.recalc();
}
link.onload = null;
};
}
head.appendChild(link);
});
},
/**
* Adds a class to the specified element or elements.
*
* @method addClass
* @param {String/Element/Array} elm Element ID string or DOM element or array with elements or IDs.
* @param {String} cls Class name to add to each element.
* @return {String/Array} String with new class value or array with new class values for all elements.
* @example
* // Adds a class to all paragraphs in the active editor
* tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'myclass');
*
* // Adds a class to a specific element in the current page
* tinymce.DOM.addClass('mydiv', 'myclass');
*/
addClass: function(elm, cls) {
this.$$(elm).addClass(cls);
},
/**
* Removes a class from the specified element or elements.
*
* @method removeClass
* @param {String/Element/Array} elm Element ID string or DOM element or array with elements or IDs.
* @param {String} cls Class name to remove from each element.
* @return {String/Array} String of remaining class name(s), or an array of strings if multiple input elements
* were passed in.
* @example
* // Removes a class from all paragraphs in the active editor
* tinymce.activeEditor.dom.removeClass(tinymce.activeEditor.dom.select('p'), 'myclass');
*
* // Removes a class from a specific element in the current page
* tinymce.DOM.removeClass('mydiv', 'myclass');
*/
removeClass: function(elm, cls) {
this.toggleClass(elm, cls, false);
},
/**
* Returns true if the specified element has the specified class.
*
* @method hasClass
* @param {String/Element} elm HTML element or element id string to check CSS class on.
* @param {String} cls CSS class to check for.
* @return {Boolean} true/false if the specified element has the specified class.
*/
hasClass: function(elm, cls) {
return this.$$(elm).hasClass(cls);
},
/**
* Toggles the specified class on/off.
*
* @method toggleClass
* @param {Element} elm Element to toggle class on.
* @param {[type]} cls Class to toggle on/off.
* @param {[type]} state Optional state to set.
*/
toggleClass: function(elm, cls, state) {
this.$$(elm).toggleClass(cls, state).each(function() {
if (this.className === '') {
$(this).attr('class', null);
}
});
},
/**
* Shows the specified element(s) by ID by setting the "display" style.
*
* @method show
* @param {String/Element/Array} elm ID of DOM element or DOM element or array with elements or IDs to show.
*/
show: function(elm) {
this.$$(elm).show();
},
/**
* Hides the specified element(s) by ID by setting the "display" style.
*
* @method hide
* @param {String/Element/Array} elm ID of DOM element or DOM element or array with elements or IDs to hide.
* @example
* // Hides an element by id in the document
* tinymce.DOM.hide('myid');
*/
hide: function(elm) {
this.$$(elm).hide();
},
/**
* Returns true/false if the element is hidden or not by checking the "display" style.
*
* @method isHidden
* @param {String/Element} elm Id or element to check display state on.
* @return {Boolean} true/false if the element is hidden or not.
*/
isHidden: function(elm) {
return this.$$(elm).css('display') == 'none';
},
/**
* Returns a unique id. This can be useful when generating elements on the fly.
* This method will not check if the element already exists.
*
* @method uniqueId
* @param {String} prefix Optional prefix to add in front of all ids - defaults to "mce_".
* @return {String} Unique id.
*/
uniqueId: function(prefix) {
return (!prefix ? 'mce_' : prefix) + (this.counter++);
},
/**
* Sets the specified HTML content inside the element or elements. The HTML will first be processed. This means
* URLs will get converted, hex color values fixed etc. Check processHTML for details.
*
* @method setHTML
* @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set HTML inside of.
* @param {String} html HTML content to set as inner HTML of the element.
* @example
* // Sets the inner HTML of all paragraphs in the active editor
* tinymce.activeEditor.dom.setHTML(tinymce.activeEditor.dom.select('p'), 'some inner html');
*
* // Sets the inner HTML of an element by id in the document
* tinymce.DOM.setHTML('mydiv', 'some inner html');
*/
setHTML: function(elm, html) {
elm = this.$$(elm);
if (isIE) {
elm.each(function(i, target) {
if (target.canHaveHTML === false) {
return;
}
// Remove all child nodes, IE keeps empty text nodes in DOM
while (target.firstChild) {
target.removeChild(target.firstChild);
}
try {
// IE will remove comments from the beginning
// unless you padd the contents with something
target.innerHTML = '<br>' + html;
target.removeChild(target.firstChild);
} catch (ex) {
// IE sometimes produces an unknown runtime error on innerHTML if it's a div inside a p
$('<div></div>').html('<br>' + html).contents().slice(1).appendTo(target);
}
return html;
});
} else {
elm.html(html);
}
},
/**
* Returns the outer HTML of an element.
*
* @method getOuterHTML
* @param {String/Element} elm Element ID or element object to get outer HTML from.
* @return {String} Outer HTML string.
* @example
* tinymce.DOM.getOuterHTML(editorElement);
* tinymce.activeEditor.getOuterHTML(tinymce.activeEditor.getBody());
*/
getOuterHTML: function(elm) {
elm = this.get(elm);
// Older FF doesn't have outerHTML 3.6 is still used by some orgaizations
return elm.nodeType == 1 && "outerHTML" in elm ? elm.outerHTML : $('<div></div>').append($(elm).clone()).html();
},
/**
* Sets the specified outer HTML on an element or elements.
*
* @method setOuterHTML
* @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set outer HTML on.
* @param {Object} html HTML code to set as outer value for the element.
* @example
* // Sets the outer HTML of all paragraphs in the active editor
* tinymce.activeEditor.dom.setOuterHTML(tinymce.activeEditor.dom.select('p'), '<div>some html</div>');
*
* // Sets the outer HTML of an element by id in the document
* tinymce.DOM.setOuterHTML('mydiv', '<div>some html</div>');
*/
setOuterHTML: function(elm, html) {
var self = this;
self.$$(elm).each(function() {
try {
// Older FF doesn't have outerHTML 3.6 is still used by some organizations
if ("outerHTML" in this) {
this.outerHTML = html;
return;
}
} catch (ex) {
// Ignore
}
// OuterHTML for IE it sometimes produces an "unknown runtime error"
self.remove($(this).html(html), true);
});
},
/**
* Entity decodes a string. This method decodes any HTML entities, such as &aring;.
*
* @method decode
* @param {String} s String to decode entities on.
* @return {String} Entity decoded string.
*/
decode: Entities.decode,
/**
* Entity encodes a string. This method encodes the most common entities, such as <>"&.
*
* @method encode
* @param {String} text String to encode with entities.
* @return {String} Entity encoded string.
*/
encode: Entities.encodeAllRaw,
/**
* Inserts an element after the reference element.
*
* @method insertAfter
* @param {Element} node Element to insert after the reference.
* @param {Element/String/Array} referenceNode Reference element, element id or array of elements to insert after.
* @return {Element/Array} Element that got added or an array with elements.
*/
insertAfter: function(node, referenceNode) {
referenceNode = this.get(referenceNode);
return this.run(node, function(node) {
var parent, nextSibling;
parent = referenceNode.parentNode;
nextSibling = referenceNode.nextSibling;
if (nextSibling) {
parent.insertBefore(node, nextSibling);
} else {
parent.appendChild(node);
}
return node;
});
},
/**
* Replaces the specified element or elements with the new element specified. The new element will
* be cloned if multiple input elements are passed in.
*
* @method replace
* @param {Element} newElm New element to replace old ones with.
* @param {Element/String/Array} oldElm Element DOM node, element id or array of elements or ids to replace.
* @param {Boolean} keepChildren Optional keep children state, if set to true child nodes from the old object will be added
* to new ones.
*/
replace: function(newElm, oldElm, keepChildren) {
var self = this;
return self.run(oldElm, function(oldElm) {
if (is(oldElm, 'array')) {
newElm = newElm.cloneNode(true);
}
if (keepChildren) {
each(grep(oldElm.childNodes), function(node) {
newElm.appendChild(node);
});
}
return oldElm.parentNode.replaceChild(newElm, oldElm);
});
},
/**
* Renames the specified element and keeps its attributes and children.
*
* @method rename
* @param {Element} elm Element to rename.
* @param {String} name Name of the new element.
* @return {Element} New element or the old element if it needed renaming.
*/
rename: function(elm, name) {
var self = this, newElm;
if (elm.nodeName != name.toUpperCase()) {
// Rename block element
newElm = self.create(name);
// Copy attribs to new block
each(self.getAttribs(elm), function(attrNode) {
self.setAttrib(newElm, attrNode.nodeName, self.getAttrib(elm, attrNode.nodeName));
});
// Replace block
self.replace(newElm, elm, 1);
}
return newElm || elm;
},
/**
* Find the common ancestor of two elements. This is a shorter method than using the DOM Range logic.
*
* @method findCommonAncestor
* @param {Element} a Element to find common ancestor of.
* @param {Element} b Element to find common ancestor of.
* @return {Element} Common ancestor element of the two input elements.
*/
findCommonAncestor: function(a, b) {
var ps = a, pe;
while (ps) {
pe = b;
while (pe && ps != pe) {
pe = pe.parentNode;
}
if (ps == pe) {
break;
}
ps = ps.parentNode;
}
if (!ps && a.ownerDocument) {
return a.ownerDocument.documentElement;
}
return ps;
},
/**
* Parses the specified RGB color value and returns a hex version of that color.
*
* @method toHex
* @param {String} rgbVal RGB string value like rgb(1,2,3)
* @return {String} Hex version of that RGB value like #FF00FF.
*/
toHex: function(rgbVal) {
return this.styles.toHex(Tools.trim(rgbVal));
},
/**
* Executes the specified function on the element by id or dom element node or array of elements/id.
*
* @method run
* @param {String/Element/Array} elm ID or DOM element object or array with ids or elements.
* @param {function} func Function to execute for each item.
* @param {Object} scope Optional scope to execute the function in.
* @return {Object/Array} Single object, or an array of objects if multiple input elements were passed in.
*/
run: function(elm, func, scope) {
var self = this, result;
if (typeof elm === 'string') {
elm = self.get(elm);
}
if (!elm) {
return false;
}
scope = scope || this;
if (!elm.nodeType && (elm.length || elm.length === 0)) {
result = [];
each(elm, function(elm, i) {
if (elm) {
if (typeof elm == 'string') {
elm = self.get(elm);
}
result.push(func.call(scope, elm, i));
}
});
return result;
}
return func.call(scope, elm);
},
/**
* Returns a NodeList with attributes for the element.
*
* @method getAttribs
* @param {HTMLElement/string} elm Element node or string id to get attributes from.
* @return {NodeList} NodeList with attributes.
*/
getAttribs: function(elm) {
var attrs;
elm = this.get(elm);
if (!elm) {
return [];
}
if (isIE) {
attrs = [];
// Object will throw exception in IE
if (elm.nodeName == 'OBJECT') {
return elm.attributes;
}
// IE doesn't keep the selected attribute if you clone option elements
if (elm.nodeName === 'OPTION' && this.getAttrib(elm, 'selected')) {
attrs.push({specified: 1, nodeName: 'selected'});
}
// It's crazy that this is faster in IE but it's because it returns all attributes all the time
var attrRegExp = /<\/?[\w:\-]+ ?|=[\"][^\"]+\"|=\'[^\']+\'|=[\w\-]+|>/gi;
elm.cloneNode(false).outerHTML.replace(attrRegExp, '').replace(/[\w:\-]+/gi, function(a) {
attrs.push({specified: 1, nodeName: a});
});
return attrs;
}
return elm.attributes;
},
/**
* Returns true/false if the specified node is to be considered empty or not.
*
* @example
* tinymce.DOM.isEmpty(node, {img: true});
* @method isEmpty
* @param {Object} elements Optional name/value object with elements that are automatically treated as non-empty elements.
* @return {Boolean} true/false if the node is empty or not.
*/
isEmpty: function(node, elements) {
var self = this, i, attributes, type, walker, name, brCount = 0;
node = node.firstChild;
if (node) {
walker = new TreeWalker(node, node.parentNode);
elements = elements || (self.schema ? self.schema.getNonEmptyElements() : null);
do {
type = node.nodeType;
if (type === 1) {
// Ignore bogus elements
var bogusVal = node.getAttribute('data-mce-bogus');
if (bogusVal) {
node = walker.next(bogusVal === 'all');
continue;
}
// Keep empty elements like <img />
name = node.nodeName.toLowerCase();
if (elements && elements[name]) {
// Ignore single BR elements in blocks like <p><br /></p> or <p><span><br /></span></p>
if (name === 'br') {
brCount++;
node = walker.next();
continue;
}
return false;
}
// Keep elements with data-bookmark attributes or name attribute like <a name="1"></a>
attributes = self.getAttribs(node);
i = attributes.length;
while (i--) {
name = attributes[i].nodeName;
if (name === "name" || name === 'data-mce-bookmark') {
return false;
}
}
}
// Keep comment nodes
if (type == 8) {
return false;
}
// Keep non whitespace text nodes
if ((type === 3 && !whiteSpaceRegExp.test(node.nodeValue))) {
return false;
}
node = walker.next();
} while (node);
}
return brCount <= 1;
},
/**
* Creates a new DOM Range object. This will use the native DOM Range API if it's
* available. If it's not, it will fall back to the custom TinyMCE implementation.
*
* @method createRng
* @return {DOMRange} DOM Range object.
* @example
* var rng = tinymce.DOM.createRng();
* alert(rng.startContainer + "," + rng.startOffset);
*/
createRng: function() {
var doc = this.doc;
return doc.createRange ? doc.createRange() : new Range(this);
},
/**
* Returns the index of the specified node within its parent.
*
* @method nodeIndex
* @param {Node} node Node to look for.
* @param {boolean} normalized Optional true/false state if the index is what it would be after a normalization.
* @return {Number} Index of the specified node.
*/
nodeIndex: nodeIndex,
/**
* Splits an element into two new elements and places the specified split
* element or elements between the new ones. For example splitting the paragraph at the bold element in
* this example <p>abc<b>abc</b>123</p> would produce <p>abc</p><b>abc</b><p>123</p>.
*
* @method split
* @param {Element} parentElm Parent element to split.
* @param {Element} splitElm Element to split at.
* @param {Element} replacementElm Optional replacement element to replace the split element with.
* @return {Element} Returns the split element or the replacement element if that is specified.
*/
split: function(parentElm, splitElm, replacementElm) {
var self = this, r = self.createRng(), bef, aft, pa;
// W3C valid browsers tend to leave empty nodes to the left/right side of the contents - this makes sense
// but we don't want that in our code since it serves no purpose for the end user
// For example splitting this html at the bold element:
// <p>text 1<span><b>CHOP</b></span>text 2</p>
// would produce:
// <p>text 1<span></span></p><b>CHOP</b><p><span></span>text 2</p>
// this function will then trim off empty edges and produce:
// <p>text 1</p><b>CHOP</b><p>text 2</p>
function trimNode(node) {
var i, children = node.childNodes, type = node.nodeType;
function surroundedBySpans(node) {
var previousIsSpan = node.previousSibling && node.previousSibling.nodeName == 'SPAN';
var nextIsSpan = node.nextSibling && node.nextSibling.nodeName == 'SPAN';
return previousIsSpan && nextIsSpan;
}
if (type == 1 && node.getAttribute('data-mce-type') == 'bookmark') {
return;
}
for (i = children.length - 1; i >= 0; i--) {
trimNode(children[i]);
}
if (type != 9) {
// Keep non whitespace text nodes
if (type == 3 && node.nodeValue.length > 0) {
// If parent element isn't a block or there isn't any useful contents for example "<p> </p>"
// Also keep text nodes with only spaces if surrounded by spans.
// eg. "<p><span>a</span> <span>b</span></p>" should keep space between a and b
var trimmedLength = trim(node.nodeValue).length;
if (!self.isBlock(node.parentNode) || trimmedLength > 0 || trimmedLength === 0 && surroundedBySpans(node)) {
return;
}
} else if (type == 1) {
// If the only child is a bookmark then move it up
children = node.childNodes;
// TODO fix this complex if
if (children.length == 1 && children[0] && children[0].nodeType == 1 &&
children[0].getAttribute('data-mce-type') == 'bookmark') {
node.parentNode.insertBefore(children[0], node);
}
// Keep non empty elements or img, hr etc
if (children.length || /^(br|hr|input|img)$/i.test(node.nodeName)) {
return;
}
}
self.remove(node);
}
return node;
}
if (parentElm && splitElm) {
// Get before chunk
r.setStart(parentElm.parentNode, self.nodeIndex(parentElm));
r.setEnd(splitElm.parentNode, self.nodeIndex(splitElm));
bef = r.extractContents();
// Get after chunk
r = self.createRng();
r.setStart(splitElm.parentNode, self.nodeIndex(splitElm) + 1);
r.setEnd(parentElm.parentNode, self.nodeIndex(parentElm) + 1);
aft = r.extractContents();
// Insert before chunk
pa = parentElm.parentNode;
pa.insertBefore(trimNode(bef), parentElm);
// Insert middle chunk
if (replacementElm) {
pa.insertBefore(replacementElm, parentElm);
//pa.replaceChild(replacementElm, splitElm);
} else {
pa.insertBefore(splitElm, parentElm);
}
// Insert after chunk
pa.insertBefore(trimNode(aft), parentElm);
self.remove(parentElm);
return replacementElm || splitElm;
}
},
/**
* Adds an event handler to the specified object.
*
* @method bind
* @param {Element/Document/Window/Array} target Target element to bind events to.
* handler to or an array of elements/ids/documents.
* @param {String} name Name of event handler to add, for example: click.
* @param {function} func Function to execute when the event occurs.
* @param {Object} scope Optional scope to execute the function in.
* @return {function} Function callback handler the same as the one passed in.
*/
bind: function(target, name, func, scope) {
var self = this;
if (Tools.isArray(target)) {
var i = target.length;
while (i--) {
target[i] = self.bind(target[i], name, func, scope);
}
return target;
}
// Collect all window/document events bound by editor instance
if (self.settings.collect && (target === self.doc || target === self.win)) {
self.boundEvents.push([target, name, func, scope]);
}
return self.events.bind(target, name, func, scope || self);
},
/**
* Removes the specified event handler by name and function from an element or collection of elements.
*
* @method unbind
* @param {Element/Document/Window/Array} target Target element to unbind events on.
* @param {String} name Event handler name, for example: "click"
* @param {function} func Function to remove.
* @return {bool/Array} Bool state of true if the handler was removed, or an array of states if multiple input elements
* were passed in.
*/
unbind: function(target, name, func) {
var self = this, i;
if (Tools.isArray(target)) {
i = target.length;
while (i--) {
target[i] = self.unbind(target[i], name, func);
}
return target;
}
// Remove any bound events matching the input
if (self.boundEvents && (target === self.doc || target === self.win)) {
i = self.boundEvents.length;
while (i--) {
var item = self.boundEvents[i];
if (target == item[0] && (!name || name == item[1]) && (!func || func == item[2])) {
this.events.unbind(item[0], item[1], item[2]);
}
}
}
return this.events.unbind(target, name, func);
},
/**
* Fires the specified event name with object on target.
*
* @method fire
* @param {Node/Document/Window} target Target element or object to fire event on.
* @param {String} name Name of the event to fire.
* @param {Object} evt Event object to send.
* @return {Event} Event object.
*/
fire: function(target, name, evt) {
return this.events.fire(target, name, evt);
},
// Returns the content editable state of a node
getContentEditable: function(node) {
var contentEditable;
// Check type
if (!node || node.nodeType != 1) {
return null;
}
// Check for fake content editable
contentEditable = node.getAttribute("data-mce-contenteditable");
if (contentEditable && contentEditable !== "inherit") {
return contentEditable;
}
// Check for real content editable
return node.contentEditable !== "inherit" ? node.contentEditable : null;
},
getContentEditableParent: function(node) {
var root = this.getRoot(), state = null;
for (; node && node !== root; node = node.parentNode) {
state = this.getContentEditable(node);
if (state !== null) {
break;
}
}
return state;
},
/**
* Destroys all internal references to the DOM to solve IE leak issues.
*
* @method destroy
*/
destroy: function() {
var self = this;
// Unbind all events bound to window/document by editor instance
if (self.boundEvents) {
var i = self.boundEvents.length;
while (i--) {
var item = self.boundEvents[i];
this.events.unbind(item[0], item[1], item[2]);
}
self.boundEvents = null;
}
// Restore sizzle document to window.document
// Since the current document might be removed producing "Permission denied" on IE see #6325
if (Sizzle.setDocument) {
Sizzle.setDocument();
}
self.win = self.doc = self.root = self.events = self.frag = null;
},
isChildOf: function(node, parent) {
while (node) {
if (parent === node) {
return true;
}
node = node.parentNode;
}
return false;
},
// #ifdef debug
dumpRng: function(r) {
return (
'startContainer: ' + r.startContainer.nodeName +
', startOffset: ' + r.startOffset +
', endContainer: ' + r.endContainer.nodeName +
', endOffset: ' + r.endOffset
);
},
// #endif
_findSib: function(node, selector, name) {
var self = this, func = selector;
if (node) {
// If expression make a function of it using is
if (typeof func == 'string') {
func = function(node) {
return self.is(node, selector);
};
}
// Loop all siblings
for (node = node[name]; node; node = node[name]) {
if (func(node)) {
return node;
}
}
}
return null;
}
};
/**
* Instance of DOMUtils for the current document.
*
* @static
* @property DOM
* @type tinymce.dom.DOMUtils
* @example
* // Example of how to add a class to some element by id
* tinymce.DOM.addClass('someid', 'someclass');
*/
DOMUtils.DOM = new DOMUtils(document);
DOMUtils.nodeIndex = nodeIndex;
return DOMUtils;
});
// Included from: js/tinymce/classes/dom/ScriptLoader.js
/**
* ScriptLoader.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/*globals console*/
/**
* This class handles asynchronous/synchronous loading of JavaScript files it will execute callbacks
* when various items gets loaded. This class is useful to load external JavaScript files.
*
* @class tinymce.dom.ScriptLoader
* @example
* // Load a script from a specific URL using the global script loader
* tinymce.ScriptLoader.load('somescript.js');
*
* // Load a script using a unique instance of the script loader
* var scriptLoader = new tinymce.dom.ScriptLoader();
*
* scriptLoader.load('somescript.js');
*
* // Load multiple scripts
* var scriptLoader = new tinymce.dom.ScriptLoader();
*
* scriptLoader.add('somescript1.js');
* scriptLoader.add('somescript2.js');
* scriptLoader.add('somescript3.js');
*
* scriptLoader.loadQueue(function() {
* alert('All scripts are now loaded.');
* });
*/
define("tinymce/dom/ScriptLoader", [
"tinymce/dom/DOMUtils",
"tinymce/util/Tools"
], function(DOMUtils, Tools) {
var DOM = DOMUtils.DOM;
var each = Tools.each, grep = Tools.grep;
function ScriptLoader() {
var QUEUED = 0,
LOADING = 1,
LOADED = 2,
states = {},
queue = [],
scriptLoadedCallbacks = {},
queueLoadedCallbacks = [],
loading = 0,
undef;
/**
* Loads a specific script directly without adding it to the load queue.
*
* @method load
* @param {String} url Absolute URL to script to add.
* @param {function} callback Optional callback function to execute ones this script gets loaded.
*/
function loadScript(url, callback) {
var dom = DOM, elm, id;
// Execute callback when script is loaded
function done() {
dom.remove(id);
if (elm) {
elm.onreadystatechange = elm.onload = elm = null;
}
callback();
}
function error() {
/*eslint no-console:0 */
// Report the error so it's easier for people to spot loading errors
if (typeof console !== "undefined" && console.log) {
console.log("Failed to load: " + url);
}
// We can't mark it as done if there is a load error since
// A) We don't want to produce 404 errors on the server and
// B) the onerror event won't fire on all browsers.
// done();
}
id = dom.uniqueId();
// Create new script element
elm = document.createElement('script');
elm.id = id;
elm.type = 'text/javascript';
elm.src = Tools._addCacheSuffix(url);
// Seems that onreadystatechange works better on IE 10 onload seems to fire incorrectly
if ("onreadystatechange" in elm) {
elm.onreadystatechange = function() {
if (/loaded|complete/.test(elm.readyState)) {
done();
}
};
} else {
elm.onload = done;
}
// Add onerror event will get fired on some browsers but not all of them
elm.onerror = error;
// Add script to document
(document.getElementsByTagName('head')[0] || document.body).appendChild(elm);
}
/**
* Returns true/false if a script has been loaded or not.
*
* @method isDone
* @param {String} url URL to check for.
* @return {Boolean} true/false if the URL is loaded.
*/
this.isDone = function(url) {
return states[url] == LOADED;
};
/**
* Marks a specific script to be loaded. This can be useful if a script got loaded outside
* the script loader or to skip it from loading some script.
*
* @method markDone
* @param {string} url Absolute URL to the script to mark as loaded.
*/
this.markDone = function(url) {
states[url] = LOADED;
};
/**
* Adds a specific script to the load queue of the script loader.
*
* @method add
* @param {String} url Absolute URL to script to add.
* @param {function} callback Optional callback function to execute ones this script gets loaded.
* @param {Object} scope Optional scope to execute callback in.
*/
this.add = this.load = function(url, callback, scope) {
var state = states[url];
// Add url to load queue
if (state == undef) {
queue.push(url);
states[url] = QUEUED;
}
if (callback) {
// Store away callback for later execution
if (!scriptLoadedCallbacks[url]) {
scriptLoadedCallbacks[url] = [];
}
scriptLoadedCallbacks[url].push({
func: callback,
scope: scope || this
});
}
};
this.remove = function(url) {
delete states[url];
delete scriptLoadedCallbacks[url];
};
/**
* Starts the loading of the queue.
*
* @method loadQueue
* @param {function} callback Optional callback to execute when all queued items are loaded.
* @param {Object} scope Optional scope to execute the callback in.
*/
this.loadQueue = function(callback, scope) {
this.loadScripts(queue, callback, scope);
};
/**
* Loads the specified queue of files and executes the callback ones they are loaded.
* This method is generally not used outside this class but it might be useful in some scenarios.
*
* @method loadScripts
* @param {Array} scripts Array of queue items to load.
* @param {function} callback Optional callback to execute ones all items are loaded.
* @param {Object} scope Optional scope to execute callback in.
*/
this.loadScripts = function(scripts, callback, scope) {
var loadScripts;
function execScriptLoadedCallbacks(url) {
// Execute URL callback functions
each(scriptLoadedCallbacks[url], function(callback) {
callback.func.call(callback.scope);
});
scriptLoadedCallbacks[url] = undef;
}
queueLoadedCallbacks.push({
func: callback,
scope: scope || this
});
loadScripts = function() {
var loadingScripts = grep(scripts);
// Current scripts has been handled
scripts.length = 0;
// Load scripts that needs to be loaded
each(loadingScripts, function(url) {
// Script is already loaded then execute script callbacks directly
if (states[url] == LOADED) {
execScriptLoadedCallbacks(url);
return;
}
// Is script not loading then start loading it
if (states[url] != LOADING) {
states[url] = LOADING;
loading++;
loadScript(url, function() {
states[url] = LOADED;
loading--;
execScriptLoadedCallbacks(url);
// Load more scripts if they where added by the recently loaded script
loadScripts();
});
}
});
// No scripts are currently loading then execute all pending queue loaded callbacks
if (!loading) {
each(queueLoadedCallbacks, function(callback) {
callback.func.call(callback.scope);
});
queueLoadedCallbacks.length = 0;
}
};
loadScripts();
};
}
ScriptLoader.ScriptLoader = new ScriptLoader();
return ScriptLoader;
});
// Included from: js/tinymce/classes/AddOnManager.js
/**
* AddOnManager.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class handles the loading of themes/plugins or other add-ons and their language packs.
*
* @class tinymce.AddOnManager
*/
define("tinymce/AddOnManager", [
"tinymce/dom/ScriptLoader",
"tinymce/util/Tools"
], function(ScriptLoader, Tools) {
var each = Tools.each;
function AddOnManager() {
var self = this;
self.items = [];
self.urls = {};
self.lookup = {};
}
AddOnManager.prototype = {
/**
* Returns the specified add on by the short name.
*
* @method get
* @param {String} name Add-on to look for.
* @return {tinymce.Theme/tinymce.Plugin} Theme or plugin add-on instance or undefined.
*/
get: function(name) {
if (this.lookup[name]) {
return this.lookup[name].instance;
}
return undefined;
},
dependencies: function(name) {
var result;
if (this.lookup[name]) {
result = this.lookup[name].dependencies;
}
return result || [];
},
/**
* Loads a language pack for the specified add-on.
*
* @method requireLangPack
* @param {String} name Short name of the add-on.
* @param {String} languages Optional comma or space separated list of languages to check if it matches the name.
*/
requireLangPack: function(name, languages) {
var language = AddOnManager.language;
if (language && AddOnManager.languageLoad !== false) {
if (languages) {
languages = ',' + languages + ',';
// Load short form sv.js or long form sv_SE.js
if (languages.indexOf(',' + language.substr(0, 2) + ',') != -1) {
language = language.substr(0, 2);
} else if (languages.indexOf(',' + language + ',') == -1) {
return;
}
}
ScriptLoader.ScriptLoader.add(this.urls[name] + '/langs/' + language + '.js');
}
},
/**
* Adds a instance of the add-on by it's short name.
*
* @method add
* @param {String} id Short name/id for the add-on.
* @param {tinymce.Theme/tinymce.Plugin} addOn Theme or plugin to add.
* @return {tinymce.Theme/tinymce.Plugin} The same theme or plugin instance that got passed in.
* @example
* // Create a simple plugin
* tinymce.create('tinymce.plugins.TestPlugin', {
* TestPlugin: function(ed, url) {
* ed.on('click', function(e) {
* ed.windowManager.alert('Hello World!');
* });
* }
* });
*
* // Register plugin using the add method
* tinymce.PluginManager.add('test', tinymce.plugins.TestPlugin);
*
* // Initialize TinyMCE
* tinymce.init({
* ...
* plugins: '-test' // Init the plugin but don't try to load it
* });
*/
add: function(id, addOn, dependencies) {
this.items.push(addOn);
this.lookup[id] = {instance: addOn, dependencies: dependencies};
return addOn;
},
remove: function(name) {
delete this.urls[name];
delete this.lookup[name];
},
createUrl: function(baseUrl, dep) {
if (typeof dep === "object") {
return dep;
}
return {prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix};
},
/**
* Add a set of components that will make up the add-on. Using the url of the add-on name as the base url.
* This should be used in development mode. A new compressor/javascript munger process will ensure that the
* components are put together into the plugin.js file and compressed correctly.
*
* @method addComponents
* @param {String} pluginName name of the plugin to load scripts from (will be used to get the base url for the plugins).
* @param {Array} scripts Array containing the names of the scripts to load.
*/
addComponents: function(pluginName, scripts) {
var pluginUrl = this.urls[pluginName];
each(scripts, function(script) {
ScriptLoader.ScriptLoader.add(pluginUrl + "/" + script);
});
},
/**
* Loads an add-on from a specific url.
*
* @method load
* @param {String} name Short name of the add-on that gets loaded.
* @param {String} addOnUrl URL to the add-on that will get loaded.
* @param {function} callback Optional callback to execute ones the add-on is loaded.
* @param {Object} scope Optional scope to execute the callback in.
* @example
* // Loads a plugin from an external URL
* tinymce.PluginManager.load('myplugin', '/some/dir/someplugin/plugin.js');
*
* // Initialize TinyMCE
* tinymce.init({
* ...
* plugins: '-myplugin' // Don't try to load it again
* });
*/
load: function(name, addOnUrl, callback, scope) {
var self = this, url = addOnUrl;
function loadDependencies() {
var dependencies = self.dependencies(name);
each(dependencies, function(dep) {
var newUrl = self.createUrl(addOnUrl, dep);
self.load(newUrl.resource, newUrl, undefined, undefined);
});
if (callback) {
if (scope) {
callback.call(scope);
} else {
callback.call(ScriptLoader);
}
}
}
if (self.urls[name]) {
return;
}
if (typeof addOnUrl === "object") {
url = addOnUrl.prefix + addOnUrl.resource + addOnUrl.suffix;
}
if (url.indexOf('/') !== 0 && url.indexOf('://') == -1) {
url = AddOnManager.baseURL + '/' + url;
}
self.urls[name] = url.substring(0, url.lastIndexOf('/'));
if (self.lookup[name]) {
loadDependencies();
} else {
ScriptLoader.ScriptLoader.add(url, loadDependencies, scope);
}
}
};
AddOnManager.PluginManager = new AddOnManager();
AddOnManager.ThemeManager = new AddOnManager();
return AddOnManager;
});
/**
* TinyMCE theme class.
*
* @class tinymce.Theme
*/
/**
* This method is responsible for rendering/generating the overall user interface with toolbars, buttons, iframe containers etc.
*
* @method renderUI
* @param {Object} obj Object parameter containing the targetNode DOM node that will be replaced visually with an editor instance.
* @return {Object} an object with items like iframeContainer, editorContainer, sizeContainer, deltaWidth, deltaHeight.
*/
/**
* Plugin base class, this is a pseudo class that describes how a plugin is to be created for TinyMCE. The methods below are all optional.
*
* @class tinymce.Plugin
* @example
* tinymce.PluginManager.add('example', function(editor, url) {
* // Add a button that opens a window
* editor.addButton('example', {
* text: 'My button',
* icon: false,
* onclick: function() {
* // Open window
* editor.windowManager.open({
* title: 'Example plugin',
* body: [
* {type: 'textbox', name: 'title', label: 'Title'}
* ],
* onsubmit: function(e) {
* // Insert content when the window form is submitted
* editor.insertContent('Title: ' + e.data.title);
* }
* });
* }
* });
*
* // Adds a menu item to the tools menu
* editor.addMenuItem('example', {
* text: 'Example plugin',
* context: 'tools',
* onclick: function() {
* // Open window with a specific url
* editor.windowManager.open({
* title: 'TinyMCE site',
* url: 'http://www.tinymce.com',
* width: 800,
* height: 600,
* buttons: [{
* text: 'Close',
* onclick: 'close'
* }]
* });
* }
* });
* });
*/
// Included from: js/tinymce/classes/dom/NodeType.js
/**
* NodeType.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Contains various node validation functions.
*
* @private
* @class tinymce.dom.NodeType
*/
define("tinymce/dom/NodeType", [], function() {
function isNodeType(type) {
return function(node) {
return !!node && node.nodeType == type;
};
}
var isElement = isNodeType(1);
function matchNodeNames(names) {
names = names.toLowerCase().split(' ');
return function(node) {
var i, name;
if (node && node.nodeType) {
name = node.nodeName.toLowerCase();
for (i = 0; i < names.length; i++) {
if (name === names[i]) {
return true;
}
}
}
return false;
};
}
function matchStyleValues(name, values) {
values = values.toLowerCase().split(' ');
return function(node) {
var i, cssValue;
if (isElement(node)) {
for (i = 0; i < values.length; i++) {
cssValue = getComputedStyle(node, null).getPropertyValue(name);
if (cssValue === values[i]) {
return true;
}
}
}
return false;
};
}
function hasPropValue(propName, propValue) {
return function(node) {
return isElement(node) && node[propName] === propValue;
};
}
function hasAttributeValue(attrName, attrValue) {
return function(node) {
return isElement(node) && node.getAttribute(attrName) === attrValue;
};
}
function isBogus(node) {
return isElement(node) && node.hasAttribute('data-mce-bogus');
}
function hasContentEditableState(value) {
return function(node) {
if (isElement(node)) {
if (node.contentEditable === value) {
return true;
}
if (node.getAttribute('data-mce-contenteditable') === value) {
return true;
}
}
return false;
};
}
return {
isText: isNodeType(3),
isElement: isElement,
isComment: isNodeType(8),
isBr: matchNodeNames('br'),
isContentEditableTrue: hasContentEditableState('true'),
isContentEditableFalse: hasContentEditableState('false'),
matchNodeNames: matchNodeNames,
hasPropValue: hasPropValue,
hasAttributeValue: hasAttributeValue,
matchStyleValues: matchStyleValues,
isBogus: isBogus
};
});
// Included from: js/tinymce/classes/text/Zwsp.js
/**
* Zwsp.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Utility functions for working with zero width space
* characters used as character containers etc.
*
* @private
* @class tinymce.text.Zwsp
* @example
* var isZwsp = Zwsp.isZwsp('\uFEFF');
* var abc = Zwsp.trim('a\uFEFFc');
*/
define("tinymce/text/Zwsp", [], function() {
var ZWSP = '\uFEFF';
function isZwsp(chr) {
return chr == ZWSP;
}
function trim(str) {
return str.replace(new RegExp(ZWSP, 'g'), '');
}
return {
isZwsp: isZwsp,
ZWSP: ZWSP,
trim: trim
};
});
// Included from: js/tinymce/classes/caret/CaretContainer.js
/**
* CaretContainer.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This module handles caret containers. A caret container is a node that
* holds the caret for positional purposes.
*
* @private
* @class tinymce.caret.CaretContainer
*/
define("tinymce/caret/CaretContainer", [
"tinymce/dom/NodeType",
"tinymce/text/Zwsp"
], function(NodeType, Zwsp) {
var isElement = NodeType.isElement,
isText = NodeType.isText;
function isCaretContainerBlock(node) {
if (isText(node)) {
node = node.parentNode;
}
return isElement(node) && node.hasAttribute('data-mce-caret');
}
function isCaretContainerInline(node) {
return isText(node) && Zwsp.isZwsp(node.data);
}
function isCaretContainer(node) {
return isCaretContainerBlock(node) || isCaretContainerInline(node);
}
function removeNode(node) {
var parentNode = node.parentNode;
if (parentNode) {
parentNode.removeChild(node);
}
}
function getNodeValue(node) {
try {
return node.nodeValue;
} catch (ex) {
// IE sometimes produces "Invalid argument" on nodes
return "";
}
}
function setNodeValue(node, text) {
if (text.length === 0) {
removeNode(node);
} else {
node.nodeValue = text;
}
}
function insertInline(node, before) {
var doc, sibling, textNode, parentNode;
doc = node.ownerDocument;
textNode = doc.createTextNode(Zwsp.ZWSP);
parentNode = node.parentNode;
if (!before) {
sibling = node.nextSibling;
if (isText(sibling)) {
if (isCaretContainer(sibling)) {
return sibling;
}
if (startsWithCaretContainer(sibling)) {
sibling.splitText(1);
return sibling;
}
}
if (node.nextSibling) {
parentNode.insertBefore(textNode, node.nextSibling);
} else {
parentNode.appendChild(textNode);
}
} else {
sibling = node.previousSibling;
if (isText(sibling)) {
if (isCaretContainer(sibling)) {
return sibling;
}
if (endsWithCaretContainer(sibling)) {
return sibling.splitText(sibling.data.length - 1);
}
}
parentNode.insertBefore(textNode, node);
}
return textNode;
}
function createBogusBr() {
var br = document.createElement('br');
br.setAttribute('data-mce-bogus', '1');
return br;
}
function insertBlock(blockName, node, before) {
var doc, blockNode, parentNode;
doc = node.ownerDocument;
blockNode = doc.createElement(blockName);
blockNode.setAttribute('data-mce-caret', before ? 'before' : 'after');
blockNode.setAttribute('data-mce-bogus', 'all');
blockNode.appendChild(createBogusBr());
parentNode = node.parentNode;
if (!before) {
if (node.nextSibling) {
parentNode.insertBefore(blockNode, node.nextSibling);
} else {
parentNode.appendChild(blockNode);
}
} else {
parentNode.insertBefore(blockNode, node);
}
return blockNode;
}
function hasContent(node) {
return node.firstChild !== node.lastChild || !NodeType.isBr(node.firstChild);
}
function remove(caretContainerNode) {
if (isElement(caretContainerNode) && isCaretContainer(caretContainerNode)) {
if (hasContent(caretContainerNode)) {
caretContainerNode.removeAttribute('data-mce-caret');
} else {
removeNode(caretContainerNode);
}
}
if (isText(caretContainerNode)) {
var text = Zwsp.trim(getNodeValue(caretContainerNode));
setNodeValue(caretContainerNode, text);
}
}
function startsWithCaretContainer(node) {
return isText(node) && node.data[0] == Zwsp.ZWSP;
}
function endsWithCaretContainer(node) {
return isText(node) && node.data[node.data.length - 1] == Zwsp.ZWSP;
}
function trimBogusBr(elm) {
var brs = elm.getElementsByTagName('br');
var lastBr = brs[brs.length - 1];
if (NodeType.isBogus(lastBr)) {
lastBr.parentNode.removeChild(lastBr);
}
}
function showCaretContainerBlock(caretContainer) {
if (caretContainer && caretContainer.hasAttribute('data-mce-caret')) {
trimBogusBr(caretContainer);
caretContainer.removeAttribute('data-mce-caret');
caretContainer.removeAttribute('data-mce-bogus');
caretContainer.removeAttribute('style');
caretContainer.removeAttribute('_moz_abspos');
return caretContainer;
}
return null;
}
return {
isCaretContainer: isCaretContainer,
isCaretContainerBlock: isCaretContainerBlock,
isCaretContainerInline: isCaretContainerInline,
showCaretContainerBlock: showCaretContainerBlock,
insertInline: insertInline,
insertBlock: insertBlock,
hasContent: hasContent,
remove: remove,
startsWithCaretContainer: startsWithCaretContainer,
endsWithCaretContainer: endsWithCaretContainer
};
});
// Included from: js/tinymce/classes/dom/RangeUtils.js
/**
* RangeUtils.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class contains a few utility methods for ranges.
*
* @class tinymce.dom.RangeUtils
*/
define("tinymce/dom/RangeUtils", [
"tinymce/util/Tools",
"tinymce/dom/TreeWalker",
"tinymce/dom/NodeType",
"tinymce/dom/Range",
"tinymce/caret/CaretContainer"
], function(Tools, TreeWalker, NodeType, Range, CaretContainer) {
var each = Tools.each,
isContentEditableTrue = NodeType.isContentEditableTrue,
isContentEditableFalse = NodeType.isContentEditableFalse,
isCaretContainer = CaretContainer.isCaretContainer;
function hasCeProperty(node) {
return isContentEditableTrue(node) || isContentEditableFalse(node);
}
function getEndChild(container, index) {
var childNodes = container.childNodes;
index--;
if (index > childNodes.length - 1) {
index = childNodes.length - 1;
} else if (index < 0) {
index = 0;
}
return childNodes[index] || container;
}
function findParent(node, rootNode, predicate) {
while (node && node !== rootNode) {
if (predicate(node)) {
return node;
}
node = node.parentNode;
}
return null;
}
function hasParent(node, rootNode, predicate) {
return findParent(node, rootNode, predicate) !== null;
}
function isFormatterCaret(node) {
return node.id === '_mce_caret';
}
function isCeFalseCaretContainer(node, rootNode) {
return isCaretContainer(node) && hasParent(node, rootNode, isFormatterCaret) === false;
}
function RangeUtils(dom) {
/**
* Walks the specified range like object and executes the callback for each sibling collection it finds.
*
* @private
* @method walk
* @param {Object} rng Range like object.
* @param {function} callback Callback function to execute for each sibling collection.
*/
this.walk = function(rng, callback) {
var startContainer = rng.startContainer,
startOffset = rng.startOffset,
endContainer = rng.endContainer,
endOffset = rng.endOffset,
ancestor, startPoint,
endPoint, node, parent, siblings, nodes;
// Handle table cell selection the table plugin enables
// you to fake select table cells and perform formatting actions on them
nodes = dom.select('td[data-mce-selected],th[data-mce-selected]');
if (nodes.length > 0) {
each(nodes, function(node) {
callback([node]);
});
return;
}
/**
* Excludes start/end text node if they are out side the range
*
* @private
* @param {Array} nodes Nodes to exclude items from.
* @return {Array} Array with nodes excluding the start/end container if needed.
*/
function exclude(nodes) {
var node;
// First node is excluded
node = nodes[0];
if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) {
nodes.splice(0, 1);
}
// Last node is excluded
node = nodes[nodes.length - 1];
if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) {
nodes.splice(nodes.length - 1, 1);
}
return nodes;
}
/**
* Collects siblings
*
* @private
* @param {Node} node Node to collect siblings from.
* @param {String} name Name of the sibling to check for.
* @param {Node} end_node
* @return {Array} Array of collected siblings.
*/
function collectSiblings(node, name, end_node) {
var siblings = [];
for (; node && node != end_node; node = node[name]) {
siblings.push(node);
}
return siblings;
}
/**
* Find an end point this is the node just before the common ancestor root.
*
* @private
* @param {Node} node Node to start at.
* @param {Node} root Root/ancestor element to stop just before.
* @return {Node} Node just before the root element.
*/
function findEndPoint(node, root) {
do {
if (node.parentNode == root) {
return node;
}
node = node.parentNode;
} while (node);
}
function walkBoundary(start_node, end_node, next) {
var siblingName = next ? 'nextSibling' : 'previousSibling';
for (node = start_node, parent = node.parentNode; node && node != end_node; node = parent) {
parent = node.parentNode;
siblings = collectSiblings(node == start_node ? node : node[siblingName], siblingName);
if (siblings.length) {
if (!next) {
siblings.reverse();
}
callback(exclude(siblings));
}
}
}
// If index based start position then resolve it
if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) {
startContainer = startContainer.childNodes[startOffset];
}
// If index based end position then resolve it
if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) {
endContainer = getEndChild(endContainer, endOffset);
}
// Same container
if (startContainer == endContainer) {
return callback(exclude([startContainer]));
}
// Find common ancestor and end points
ancestor = dom.findCommonAncestor(startContainer, endContainer);
// Process left side
for (node = startContainer; node; node = node.parentNode) {
if (node === endContainer) {
return walkBoundary(startContainer, ancestor, true);
}
if (node === ancestor) {
break;
}
}
// Process right side
for (node = endContainer; node; node = node.parentNode) {
if (node === startContainer) {
return walkBoundary(endContainer, ancestor);
}
if (node === ancestor) {
break;
}
}
// Find start/end point
startPoint = findEndPoint(startContainer, ancestor) || startContainer;
endPoint = findEndPoint(endContainer, ancestor) || endContainer;
// Walk left leaf
walkBoundary(startContainer, startPoint, true);
// Walk the middle from start to end point
siblings = collectSiblings(
startPoint == startContainer ? startPoint : startPoint.nextSibling,
'nextSibling',
endPoint == endContainer ? endPoint.nextSibling : endPoint
);
if (siblings.length) {
callback(exclude(siblings));
}
// Walk right leaf
walkBoundary(endContainer, endPoint);
};
/**
* Splits the specified range at it's start/end points.
*
* @private
* @param {Range/RangeObject} rng Range to split.
* @return {Object} Range position object.
*/
this.split = function(rng) {
var startContainer = rng.startContainer,
startOffset = rng.startOffset,
endContainer = rng.endContainer,
endOffset = rng.endOffset;
function splitText(node, offset) {
return node.splitText(offset);
}
// Handle single text node
if (startContainer == endContainer && startContainer.nodeType == 3) {
if (startOffset > 0 && startOffset < startContainer.nodeValue.length) {
endContainer = splitText(startContainer, startOffset);
startContainer = endContainer.previousSibling;
if (endOffset > startOffset) {
endOffset = endOffset - startOffset;
startContainer = endContainer = splitText(endContainer, endOffset).previousSibling;
endOffset = endContainer.nodeValue.length;
startOffset = 0;
} else {
endOffset = 0;
}
}
} else {
// Split startContainer text node if needed
if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) {
startContainer = splitText(startContainer, startOffset);
startOffset = 0;
}
// Split endContainer text node if needed
if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) {
endContainer = splitText(endContainer, endOffset).previousSibling;
endOffset = endContainer.nodeValue.length;
}
}
return {
startContainer: startContainer,
startOffset: startOffset,
endContainer: endContainer,
endOffset: endOffset
};
};
/**
* Normalizes the specified range by finding the closest best suitable caret location.
*
* @private
* @param {Range} rng Range to normalize.
* @return {Boolean} True/false if the specified range was normalized or not.
*/
this.normalize = function(rng) {
var normalized, collapsed;
function normalizeEndPoint(start) {
var container, offset, walker, body = dom.getRoot(), node, nonEmptyElementsMap;
var directionLeft, isAfterNode;
function isTableCell(node) {
return node && /^(TD|TH|CAPTION)$/.test(node.nodeName);
}
function hasBrBeforeAfter(node, left) {
var walker = new TreeWalker(node, dom.getParent(node.parentNode, dom.isBlock) || body);
while ((node = walker[left ? 'prev' : 'next']())) {
if (node.nodeName === "BR") {
return true;
}
}
}
function hasContentEditableFalseParent(node) {
while (node && node != body) {
if (isContentEditableFalse(node)) {
return true;
}
node = node.parentNode;
}
return false;
}
function isPrevNode(node, name) {
return node.previousSibling && node.previousSibling.nodeName == name;
}
// Walks the dom left/right to find a suitable text node to move the endpoint into
// It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG
function findTextNodeRelative(left, startNode) {
var walker, lastInlineElement, parentBlockContainer;
startNode = startNode || container;
parentBlockContainer = dom.getParent(startNode.parentNode, dom.isBlock) || body;
// Lean left before the BR element if it's the only BR within a block element. Gecko bug: #6680
// This: <p><br>|</p> becomes <p>|<br></p>
if (left && startNode.nodeName == 'BR' && isAfterNode && dom.isEmpty(parentBlockContainer)) {
container = startNode.parentNode;
offset = dom.nodeIndex(startNode);
normalized = true;
return;
}
// Walk left until we hit a text node we can move to or a block/br/img
walker = new TreeWalker(startNode, parentBlockContainer);
while ((node = walker[left ? 'prev' : 'next']())) {
// Break if we hit a non content editable node
if (dom.getContentEditableParent(node) === "false" || isCeFalseCaretContainer(node, dom.getRoot())) {
return;
}
// Found text node that has a length
if (node.nodeType === 3 && node.nodeValue.length > 0) {
container = node;
offset = left ? node.nodeValue.length : 0;
normalized = true;
return;
}
// Break if we find a block or a BR/IMG/INPUT etc
if (dom.isBlock(node) || nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
return;
}
lastInlineElement = node;
}
// Only fetch the last inline element when in caret mode for now
if (collapsed && lastInlineElement) {
container = lastInlineElement;
normalized = true;
offset = 0;
}
}
container = rng[(start ? 'start' : 'end') + 'Container'];
offset = rng[(start ? 'start' : 'end') + 'Offset'];
isAfterNode = container.nodeType == 1 && offset === container.childNodes.length;
nonEmptyElementsMap = dom.schema.getNonEmptyElements();
directionLeft = start;
if (isCaretContainer(container)) {
return;
}
if (container.nodeType == 1 && offset > container.childNodes.length - 1) {
directionLeft = false;
}
// If the container is a document move it to the body element
if (container.nodeType === 9) {
container = dom.getRoot();
offset = 0;
}
// If the container is body try move it into the closest text node or position
if (container === body) {
// If start is before/after a image, table etc
if (directionLeft) {
node = container.childNodes[offset > 0 ? offset - 1 : 0];
if (node) {
if (isCaretContainer(node)) {
return;
}
if (nonEmptyElementsMap[node.nodeName] || node.nodeName == "TABLE") {
return;
}
}
}
// Resolve the index
if (container.hasChildNodes()) {
offset = Math.min(!directionLeft && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1);
container = container.childNodes[offset];
offset = 0;
// Don't normalize non collapsed selections like <p>[a</p><table></table>]
if (!collapsed && container === body.lastChild && container.nodeName === 'TABLE') {
return;
}
if (hasContentEditableFalseParent(container) || isCaretContainer(container)) {
return;
}
// Don't walk into elements that doesn't have any child nodes like a IMG
if (container.hasChildNodes() && !/TABLE/.test(container.nodeName)) {
// Walk the DOM to find a text node to place the caret at or a BR
node = container;
walker = new TreeWalker(container, body);
do {
if (isContentEditableFalse(node) || isCaretContainer(node)) {
normalized = false;
break;
}
// Found a text node use that position
if (node.nodeType === 3 && node.nodeValue.length > 0) {
offset = directionLeft ? 0 : node.nodeValue.length;
container = node;
normalized = true;
break;
}
// Found a BR/IMG element that we can place the caret before
if (nonEmptyElementsMap[node.nodeName.toLowerCase()] && !isTableCell(node)) {
offset = dom.nodeIndex(node);
container = node.parentNode;
// Put caret after image when moving the end point
if (node.nodeName == "IMG" && !directionLeft) {
offset++;
}
normalized = true;
break;
}
} while ((node = (directionLeft ? walker.next() : walker.prev())));
}
}
}
// Lean the caret to the left if possible
if (collapsed) {
// So this: <b>x</b><i>|x</i>
// Becomes: <b>x|</b><i>x</i>
// Seems that only gecko has issues with this
if (container.nodeType === 3 && offset === 0) {
findTextNodeRelative(true);
}
// Lean left into empty inline elements when the caret is before a BR
// So this: <i><b></b><i>|<br></i>
// Becomes: <i><b>|</b><i><br></i>
// Seems that only gecko has issues with this.
// Special edge case for <p><a>x</a>|<br></p> since we don't want <p><a>x|</a><br></p>
if (container.nodeType === 1) {
node = container.childNodes[offset];
// Offset is after the containers last child
// then use the previous child for normalization
if (!node) {
node = container.childNodes[offset - 1];
}
if (node && node.nodeName === 'BR' && !isPrevNode(node, 'A') &&
!hasBrBeforeAfter(node) && !hasBrBeforeAfter(node, true)) {
findTextNodeRelative(true, node);
}
}
}
// Lean the start of the selection right if possible
// So this: x[<b>x]</b>
// Becomes: x<b>[x]</b>
if (directionLeft && !collapsed && container.nodeType === 3 && offset === container.nodeValue.length) {
findTextNodeRelative(false);
}
// Set endpoint if it was normalized
if (normalized) {
rng['set' + (start ? 'Start' : 'End')](container, offset);
}
}
collapsed = rng.collapsed;
normalizeEndPoint(true);
if (!collapsed) {
normalizeEndPoint();
}
// If it was collapsed then make sure it still is
if (normalized && collapsed) {
rng.collapse(true);
}
return normalized;
};
}
/**
* Compares two ranges and checks if they are equal.
*
* @static
* @method compareRanges
* @param {DOMRange} rng1 First range to compare.
* @param {DOMRange} rng2 First range to compare.
* @return {Boolean} true/false if the ranges are equal.
*/
RangeUtils.compareRanges = function(rng1, rng2) {
if (rng1 && rng2) {
// Compare native IE ranges
if (rng1.item || rng1.duplicate) {
// Both are control ranges and the selected element matches
if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0)) {
return true;
}
// Both are text ranges and the range matches
if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1)) {
return true;
}
} else {
// Compare w3c ranges
return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset;
}
}
return false;
};
/**
* Finds the closest selection rect tries to get the range from that.
*/
function findClosestIeRange(clientX, clientY, doc) {
var element, rng, rects;
element = doc.elementFromPoint(clientX, clientY);
rng = doc.body.createTextRange();
if (!element || element.tagName == 'HTML') {
element = doc.body;
}
rng.moveToElementText(element);
rects = Tools.toArray(rng.getClientRects());
rects = rects.sort(function(a, b) {
a = Math.abs(Math.max(a.top - clientY, a.bottom - clientY));
b = Math.abs(Math.max(b.top - clientY, b.bottom - clientY));
return a - b;
});
if (rects.length > 0) {
clientY = (rects[0].bottom + rects[0].top) / 2;
try {
rng.moveToPoint(clientX, clientY);
rng.collapse(true);
return rng;
} catch (ex) {
// At least we tried
}
}
return null;
}
function moveOutOfContentEditableFalse(rng, rootNode) {
var parentElement = rng && rng.parentElement ? rng.parentElement() : null;
return isContentEditableFalse(findParent(parentElement, rootNode, hasCeProperty)) ? null : rng;
}
/**
* Gets the caret range for the given x/y location.
*
* @static
* @method getCaretRangeFromPoint
* @param {Number} clientX X coordinate for range
* @param {Number} clientY Y coordinate for range
* @param {Document} doc Document that x/y are relative to
* @returns {Range} caret range
*/
RangeUtils.getCaretRangeFromPoint = function(clientX, clientY, doc) {
var rng, point;
if (doc.caretPositionFromPoint) {
point = doc.caretPositionFromPoint(clientX, clientY);
rng = doc.createRange();
rng.setStart(point.offsetNode, point.offset);
rng.collapse(true);
} else if (doc.caretRangeFromPoint) {
rng = doc.caretRangeFromPoint(clientX, clientY);
} else if (doc.body.createTextRange) {
rng = doc.body.createTextRange();
try {
rng.moveToPoint(clientX, clientY);
rng.collapse(true);
} catch (ex) {
rng = findClosestIeRange(clientX, clientY, doc);
}
return moveOutOfContentEditableFalse(rng, doc.body);
}
return rng;
};
RangeUtils.getSelectedNode = function(range) {
var startContainer = range.startContainer,
startOffset = range.startOffset;
if (startContainer.hasChildNodes() && range.endOffset == startOffset + 1) {
return startContainer.childNodes[startOffset];
}
return null;
};
RangeUtils.getNode = function(container, offset) {
if (container.nodeType == 1 && container.hasChildNodes()) {
if (offset >= container.childNodes.length) {
offset = container.childNodes.length - 1;
}
container = container.childNodes[offset];
}
return container;
};
return RangeUtils;
});
// Included from: js/tinymce/classes/NodeChange.js
/**
* NodeChange.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class handles the nodechange event dispatching both manual and through selection change events.
*
* @class tinymce.NodeChange
* @private
*/
define("tinymce/NodeChange", [
"tinymce/dom/RangeUtils",
"tinymce/Env",
"tinymce/util/Delay"
], function(RangeUtils, Env, Delay) {
return function(editor) {
var lastRng, lastPath = [];
/**
* Returns true/false if the current element path has been changed or not.
*
* @private
* @return {Boolean} True if the element path is the same false if it's not.
*/
function isSameElementPath(startElm) {
var i, currentPath;
currentPath = editor.$(startElm).parentsUntil(editor.getBody()).add(startElm);
if (currentPath.length === lastPath.length) {
for (i = currentPath.length; i >= 0; i--) {
if (currentPath[i] !== lastPath[i]) {
break;
}
}
if (i === -1) {
lastPath = currentPath;
return true;
}
}
lastPath = currentPath;
return false;
}
// Gecko doesn't support the "selectionchange" event
if (!('onselectionchange' in editor.getDoc())) {
editor.on('NodeChange Click MouseUp KeyUp Focus', function(e) {
var nativeRng, fakeRng;
// Since DOM Ranges mutate on modification
// of the DOM we need to clone it's contents
nativeRng = editor.selection.getRng();
fakeRng = {
startContainer: nativeRng.startContainer,
startOffset: nativeRng.startOffset,
endContainer: nativeRng.endContainer,
endOffset: nativeRng.endOffset
};
// Always treat nodechange as a selectionchange since applying
// formatting to the current range wouldn't update the range but it's parent
if (e.type == 'nodechange' || !RangeUtils.compareRanges(fakeRng, lastRng)) {
editor.fire('SelectionChange');
}
lastRng = fakeRng;
});
}
// IE has a bug where it fires a selectionchange on right click that has a range at the start of the body
// When the contextmenu event fires the selection is located at the right location
editor.on('contextmenu', function() {
editor.fire('SelectionChange');
});
// Selection change is delayed ~200ms on IE when you click inside the current range
editor.on('SelectionChange', function() {
var startElm = editor.selection.getStart(true);
// IE 8 will fire a selectionchange event with an incorrect selection
// when focusing out of table cells. Click inside cell -> toolbar = Invalid SelectionChange event
if (!Env.range && editor.selection.isCollapsed()) {
return;
}
if (!isSameElementPath(startElm) && editor.dom.isChildOf(startElm, editor.getBody())) {
editor.nodeChanged({selectionChange: true});
}
});
// Fire an extra nodeChange on mouseup for compatibility reasons
editor.on('MouseUp', function(e) {
if (!e.isDefaultPrevented()) {
// Delay nodeChanged call for WebKit edge case issue where the range
// isn't updated until after you click outside a selected image
if (editor.selection.getNode().nodeName == 'IMG') {
Delay.setEditorTimeout(editor, function() {
editor.nodeChanged();
});
} else {
editor.nodeChanged();
}
}
});
/**
* Dispatches out a onNodeChange event to all observers. This method should be called when you
* need to update the UI states or element path etc.
*
* @method nodeChanged
* @param {Object} args Optional args to pass to NodeChange event handlers.
*/
this.nodeChanged = function(args) {
var selection = editor.selection, node, parents, root;
// Fix for bug #1896577 it seems that this can not be fired while the editor is loading
if (editor.initialized && selection && !editor.settings.disable_nodechange && !editor.readonly) {
// Get start node
root = editor.getBody();
node = selection.getStart() || root;
// Make sure the node is within the editor root or is the editor root
if (node.ownerDocument != editor.getDoc() || !editor.dom.isChildOf(node, root)) {
node = root;
}
// Edge case for <p>|<img></p>
if (node.nodeName == 'IMG' && selection.isCollapsed()) {
node = node.parentNode;
}
// Get parents and add them to object
parents = [];
editor.dom.getParent(node, function(node) {
if (node === root) {
return true;
}
parents.push(node);
});
args = args || {};
args.element = node;
args.parents = parents;
editor.fire('NodeChange', args);
}
};
};
});
// Included from: js/tinymce/classes/html/Node.js
/**
* Node.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class is a minimalistic implementation of a DOM like node used by the DomParser class.
*
* @example
* var node = new tinymce.html.Node('strong', 1);
* someRoot.append(node);
*
* @class tinymce.html.Node
* @version 3.4
*/
define("tinymce/html/Node", [], function() {
var whiteSpaceRegExp = /^[ \t\r\n]*$/, typeLookup = {
'#text': 3,
'#comment': 8,
'#cdata': 4,
'#pi': 7,
'#doctype': 10,
'#document-fragment': 11
};
// Walks the tree left/right
function walk(node, root_node, prev) {
var sibling, parent, startName = prev ? 'lastChild' : 'firstChild', siblingName = prev ? 'prev' : 'next';
// Walk into nodes if it has a start
if (node[startName]) {
return node[startName];
}
// Return the sibling if it has one
if (node !== root_node) {
sibling = node[siblingName];
if (sibling) {
return sibling;
}
// Walk up the parents to look for siblings
for (parent = node.parent; parent && parent !== root_node; parent = parent.parent) {
sibling = parent[siblingName];
if (sibling) {
return sibling;
}
}
}
}
/**
* Constructs a new Node instance.
*
* @constructor
* @method Node
* @param {String} name Name of the node type.
* @param {Number} type Numeric type representing the node.
*/
function Node(name, type) {
this.name = name;
this.type = type;
if (type === 1) {
this.attributes = [];
this.attributes.map = {};
}
}
Node.prototype = {
/**
* Replaces the current node with the specified one.
*
* @example
* someNode.replace(someNewNode);
*
* @method replace
* @param {tinymce.html.Node} node Node to replace the current node with.
* @return {tinymce.html.Node} The old node that got replaced.
*/
replace: function(node) {
var self = this;
if (node.parent) {
node.remove();
}
self.insert(node, self);
self.remove();
return self;
},
/**
* Gets/sets or removes an attribute by name.
*
* @example
* someNode.attr("name", "value"); // Sets an attribute
* console.log(someNode.attr("name")); // Gets an attribute
* someNode.attr("name", null); // Removes an attribute
*
* @method attr
* @param {String} name Attribute name to set or get.
* @param {String} value Optional value to set.
* @return {String/tinymce.html.Node} String or undefined on a get operation or the current node on a set operation.
*/
attr: function(name, value) {
var self = this, attrs, i, undef;
if (typeof name !== "string") {
for (i in name) {
self.attr(i, name[i]);
}
return self;
}
if ((attrs = self.attributes)) {
if (value !== undef) {
// Remove attribute
if (value === null) {
if (name in attrs.map) {
delete attrs.map[name];
i = attrs.length;
while (i--) {
if (attrs[i].name === name) {
attrs = attrs.splice(i, 1);
return self;
}
}
}
return self;
}
// Set attribute
if (name in attrs.map) {
// Set attribute
i = attrs.length;
while (i--) {
if (attrs[i].name === name) {
attrs[i].value = value;
break;
}
}
} else {
attrs.push({name: name, value: value});
}
attrs.map[name] = value;
return self;
}
return attrs.map[name];
}
},
/**
* Does a shallow clones the node into a new node. It will also exclude id attributes since
* there should only be one id per document.
*
* @example
* var clonedNode = node.clone();
*
* @method clone
* @return {tinymce.html.Node} New copy of the original node.
*/
clone: function() {
var self = this, clone = new Node(self.name, self.type), i, l, selfAttrs, selfAttr, cloneAttrs;
// Clone element attributes
if ((selfAttrs = self.attributes)) {
cloneAttrs = [];
cloneAttrs.map = {};
for (i = 0, l = selfAttrs.length; i < l; i++) {
selfAttr = selfAttrs[i];
// Clone everything except id
if (selfAttr.name !== 'id') {
cloneAttrs[cloneAttrs.length] = {name: selfAttr.name, value: selfAttr.value};
cloneAttrs.map[selfAttr.name] = selfAttr.value;
}
}
clone.attributes = cloneAttrs;
}
clone.value = self.value;
clone.shortEnded = self.shortEnded;
return clone;
},
/**
* Wraps the node in in another node.
*
* @example
* node.wrap(wrapperNode);
*
* @method wrap
*/
wrap: function(wrapper) {
var self = this;
self.parent.insert(wrapper, self);
wrapper.append(self);
return self;
},
/**
* Unwraps the node in other words it removes the node but keeps the children.
*
* @example
* node.unwrap();
*
* @method unwrap
*/
unwrap: function() {
var self = this, node, next;
for (node = self.firstChild; node;) {
next = node.next;
self.insert(node, self, true);
node = next;
}
self.remove();
},
/**
* Removes the node from it's parent.
*
* @example
* node.remove();
*
* @method remove
* @return {tinymce.html.Node} Current node that got removed.
*/
remove: function() {
var self = this, parent = self.parent, next = self.next, prev = self.prev;
if (parent) {
if (parent.firstChild === self) {
parent.firstChild = next;
if (next) {
next.prev = null;
}
} else {
prev.next = next;
}
if (parent.lastChild === self) {
parent.lastChild = prev;
if (prev) {
prev.next = null;
}
} else {
next.prev = prev;
}
self.parent = self.next = self.prev = null;
}
return self;
},
/**
* Appends a new node as a child of the current node.
*
* @example
* node.append(someNode);
*
* @method append
* @param {tinymce.html.Node} node Node to append as a child of the current one.
* @return {tinymce.html.Node} The node that got appended.
*/
append: function(node) {
var self = this, last;
if (node.parent) {
node.remove();
}
last = self.lastChild;
if (last) {
last.next = node;
node.prev = last;
self.lastChild = node;
} else {
self.lastChild = self.firstChild = node;
}
node.parent = self;
return node;
},
/**
* Inserts a node at a specific position as a child of the current node.
*
* @example
* parentNode.insert(newChildNode, oldChildNode);
*
* @method insert
* @param {tinymce.html.Node} node Node to insert as a child of the current node.
* @param {tinymce.html.Node} ref_node Reference node to set node before/after.
* @param {Boolean} before Optional state to insert the node before the reference node.
* @return {tinymce.html.Node} The node that got inserted.
*/
insert: function(node, ref_node, before) {
var parent;
if (node.parent) {
node.remove();
}
parent = ref_node.parent || this;
if (before) {
if (ref_node === parent.firstChild) {
parent.firstChild = node;
} else {
ref_node.prev.next = node;
}
node.prev = ref_node.prev;
node.next = ref_node;
ref_node.prev = node;
} else {
if (ref_node === parent.lastChild) {
parent.lastChild = node;
} else {
ref_node.next.prev = node;
}
node.next = ref_node.next;
node.prev = ref_node;
ref_node.next = node;
}
node.parent = parent;
return node;
},
/**
* Get all children by name.
*
* @method getAll
* @param {String} name Name of the child nodes to collect.
* @return {Array} Array with child nodes matchin the specified name.
*/
getAll: function(name) {
var self = this, node, collection = [];
for (node = self.firstChild; node; node = walk(node, self)) {
if (node.name === name) {
collection.push(node);
}
}
return collection;
},
/**
* Removes all children of the current node.
*
* @method empty
* @return {tinymce.html.Node} The current node that got cleared.
*/
empty: function() {
var self = this, nodes, i, node;
// Remove all children
if (self.firstChild) {
nodes = [];
// Collect the children
for (node = self.firstChild; node; node = walk(node, self)) {
nodes.push(node);
}
// Remove the children
i = nodes.length;
while (i--) {
node = nodes[i];
node.parent = node.firstChild = node.lastChild = node.next = node.prev = null;
}
}
self.firstChild = self.lastChild = null;
return self;
},
/**
* Returns true/false if the node is to be considered empty or not.
*
* @example
* node.isEmpty({img: true});
* @method isEmpty
* @param {Object} elements Name/value object with elements that are automatically treated as non empty elements.
* @return {Boolean} true/false if the node is empty or not.
*/
isEmpty: function(elements) {
var self = this, node = self.firstChild, i, name;
if (node) {
do {
if (node.type === 1) {
// Ignore bogus elements
if (node.attributes.map['data-mce-bogus']) {
continue;
}
// Keep empty elements like <img />
if (elements[node.name]) {
return false;
}
// Keep bookmark nodes and name attribute like <a name="1"></a>
i = node.attributes.length;
while (i--) {
name = node.attributes[i].name;
if (name === "name" || name.indexOf('data-mce-bookmark') === 0) {
return false;
}
}
}
// Keep comments
if (node.type === 8) {
return false;
}
// Keep non whitespace text nodes
if ((node.type === 3 && !whiteSpaceRegExp.test(node.value))) {
return false;
}
} while ((node = walk(node, self)));
}
return true;
},
/**
* Walks to the next or previous node and returns that node or null if it wasn't found.
*
* @method walk
* @param {Boolean} prev Optional previous node state defaults to false.
* @return {tinymce.html.Node} Node that is next to or previous of the current node.
*/
walk: function(prev) {
return walk(this, null, prev);
}
};
/**
* Creates a node of a specific type.
*
* @static
* @method create
* @param {String} name Name of the node type to create for example "b" or "#text".
* @param {Object} attrs Name/value collection of attributes that will be applied to elements.
*/
Node.create = function(name, attrs) {
var node, attrName;
// Create node
node = new Node(name, typeLookup[name] || 1);
// Add attributes if needed
if (attrs) {
for (attrName in attrs) {
node.attr(attrName, attrs[attrName]);
}
}
return node;
};
return Node;
});
// Included from: js/tinymce/classes/html/Schema.js
/**
* Schema.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Schema validator class.
*
* @class tinymce.html.Schema
* @example
* if (tinymce.activeEditor.schema.isValidChild('p', 'span'))
* alert('span is valid child of p.');
*
* if (tinymce.activeEditor.schema.getElementRule('p'))
* alert('P is a valid element.');
*
* @class tinymce.html.Schema
* @version 3.4
*/
define("tinymce/html/Schema", [
"tinymce/util/Tools"
], function(Tools) {
var mapCache = {}, dummyObj = {};
var makeMap = Tools.makeMap, each = Tools.each, extend = Tools.extend, explode = Tools.explode, inArray = Tools.inArray;
function split(items, delim) {
items = Tools.trim(items);
return items ? items.split(delim || ' ') : [];
}
/**
* Builds a schema lookup table
*
* @private
* @param {String} type html4, html5 or html5-strict schema type.
* @return {Object} Schema lookup table.
*/
function compileSchema(type) {
var schema = {}, globalAttributes, blockContent;
var phrasingContent, flowContent, html4BlockContent, html4PhrasingContent;
function add(name, attributes, children) {
var ni, attributesOrder, element;
function arrayToMap(array, obj) {
var map = {}, i, l;
for (i = 0, l = array.length; i < l; i++) {
map[array[i]] = obj || {};
}
return map;
}
children = children || [];
attributes = attributes || "";
if (typeof children === "string") {
children = split(children);
}
name = split(name);
ni = name.length;
while (ni--) {
attributesOrder = split([globalAttributes, attributes].join(' '));
element = {
attributes: arrayToMap(attributesOrder),
attributesOrder: attributesOrder,
children: arrayToMap(children, dummyObj)
};
schema[name[ni]] = element;
}
}
function addAttrs(name, attributes) {
var ni, schemaItem, i, l;
name = split(name);
ni = name.length;
attributes = split(attributes);
while (ni--) {
schemaItem = schema[name[ni]];
for (i = 0, l = attributes.length; i < l; i++) {
schemaItem.attributes[attributes[i]] = {};
schemaItem.attributesOrder.push(attributes[i]);
}
}
}
// Use cached schema
if (mapCache[type]) {
return mapCache[type];
}
// Attributes present on all elements
globalAttributes = "id accesskey class dir lang style tabindex title";
// Event attributes can be opt-in/opt-out
/*eventAttributes = split("onabort onblur oncancel oncanplay oncanplaythrough onchange onclick onclose oncontextmenu oncuechange " +
"ondblclick ondrag ondragend ondragenter ondragleave ondragover ondragstart ondrop ondurationchange onemptied onended " +
"onerror onfocus oninput oninvalid onkeydown onkeypress onkeyup onload onloadeddata onloadedmetadata onloadstart " +
"onmousedown onmousemove onmouseout onmouseover onmouseup onmousewheel onpause onplay onplaying onprogress onratechange " +
"onreset onscroll onseeked onseeking onseeking onselect onshow onstalled onsubmit onsuspend ontimeupdate onvolumechange " +
"onwaiting"
);*/
// Block content elements
blockContent =
"address blockquote div dl fieldset form h1 h2 h3 h4 h5 h6 hr menu ol p pre table ul";
// Phrasing content elements from the HTML5 spec (inline)
phrasingContent =
"a abbr b bdo br button cite code del dfn em embed i iframe img input ins kbd " +
"label map noscript object q s samp script select small span strong sub sup " +
"textarea u var #text #comment"
;
// Add HTML5 items to globalAttributes, blockContent, phrasingContent
if (type != "html4") {
globalAttributes += " contenteditable contextmenu draggable dropzone " +
"hidden spellcheck translate";
blockContent += " article aside details dialog figure header footer hgroup section nav";
phrasingContent += " audio canvas command datalist mark meter output picture " +
"progress time wbr video ruby bdi keygen";
}
// Add HTML4 elements unless it's html5-strict
if (type != "html5-strict") {
globalAttributes += " xml:lang";
html4PhrasingContent = "acronym applet basefont big font strike tt";
phrasingContent = [phrasingContent, html4PhrasingContent].join(' ');
each(split(html4PhrasingContent), function(name) {
add(name, "", phrasingContent);
});
html4BlockContent = "center dir isindex noframes";
blockContent = [blockContent, html4BlockContent].join(' ');
// Flow content elements from the HTML5 spec (block+inline)
flowContent = [blockContent, phrasingContent].join(' ');
each(split(html4BlockContent), function(name) {
add(name, "", flowContent);
});
}
// Flow content elements from the HTML5 spec (block+inline)
flowContent = flowContent || [blockContent, phrasingContent].join(" ");
// HTML4 base schema TODO: Move HTML5 specific attributes to HTML5 specific if statement
// Schema items <element name>, <specific attributes>, <children ..>
add("html", "manifest", "head body");
add("head", "", "base command link meta noscript script style title");
add("title hr noscript br");
add("base", "href target");
add("link", "href rel media hreflang type sizes hreflang");
add("meta", "name http-equiv content charset");
add("style", "media type scoped");
add("script", "src async defer type charset");
add("body", "onafterprint onbeforeprint onbeforeunload onblur onerror onfocus " +
"onhashchange onload onmessage onoffline ononline onpagehide onpageshow " +
"onpopstate onresize onscroll onstorage onunload", flowContent);
add("address dt dd div caption", "", flowContent);
add("h1 h2 h3 h4 h5 h6 pre p abbr code var samp kbd sub sup i b u bdo span legend em strong small s cite dfn", "", phrasingContent);
add("blockquote", "cite", flowContent);
add("ol", "reversed start type", "li");
add("ul", "", "li");
add("li", "value", flowContent);
add("dl", "", "dt dd");
add("a", "href target rel media hreflang type", phrasingContent);
add("q", "cite", phrasingContent);
add("ins del", "cite datetime", flowContent);
add("img", "src sizes srcset alt usemap ismap width height");
add("iframe", "src name width height", flowContent);
add("embed", "src type width height");
add("object", "data type typemustmatch name usemap form width height", [flowContent, "param"].join(' '));
add("param", "name value");
add("map", "name", [flowContent, "area"].join(' '));
add("area", "alt coords shape href target rel media hreflang type");
add("table", "border", "caption colgroup thead tfoot tbody tr" + (type == "html4" ? " col" : ""));
add("colgroup", "span", "col");
add("col", "span");
add("tbody thead tfoot", "", "tr");
add("tr", "", "td th");
add("td", "colspan rowspan headers", flowContent);
add("th", "colspan rowspan headers scope abbr", flowContent);
add("form", "accept-charset action autocomplete enctype method name novalidate target", flowContent);
add("fieldset", "disabled form name", [flowContent, "legend"].join(' '));
add("label", "form for", phrasingContent);
add("input", "accept alt autocomplete checked dirname disabled form formaction formenctype formmethod formnovalidate " +
"formtarget height list max maxlength min multiple name pattern readonly required size src step type value width"
);
add("button", "disabled form formaction formenctype formmethod formnovalidate formtarget name type value",
type == "html4" ? flowContent : phrasingContent);
add("select", "disabled form multiple name required size", "option optgroup");
add("optgroup", "disabled label", "option");
add("option", "disabled label selected value");
add("textarea", "cols dirname disabled form maxlength name readonly required rows wrap");
add("menu", "type label", [flowContent, "li"].join(' '));
add("noscript", "", flowContent);
// Extend with HTML5 elements
if (type != "html4") {
add("wbr");
add("ruby", "", [phrasingContent, "rt rp"].join(' '));
add("figcaption", "", flowContent);
add("mark rt rp summary bdi", "", phrasingContent);
add("canvas", "width height", flowContent);
add("video", "src crossorigin poster preload autoplay mediagroup loop " +
"muted controls width height buffered", [flowContent, "track source"].join(' '));
add("audio", "src crossorigin preload autoplay mediagroup loop muted controls " +
"buffered volume", [flowContent, "track source"].join(' '));
add("picture", "", "img source");
add("source", "src srcset type media sizes");
add("track", "kind src srclang label default");
add("datalist", "", [phrasingContent, "option"].join(' '));
add("article section nav aside header footer", "", flowContent);
add("hgroup", "", "h1 h2 h3 h4 h5 h6");
add("figure", "", [flowContent, "figcaption"].join(' '));
add("time", "datetime", phrasingContent);
add("dialog", "open", flowContent);
add("command", "type label icon disabled checked radiogroup command");
add("output", "for form name", phrasingContent);
add("progress", "value max", phrasingContent);
add("meter", "value min max low high optimum", phrasingContent);
add("details", "open", [flowContent, "summary"].join(' '));
add("keygen", "autofocus challenge disabled form keytype name");
}
// Extend with HTML4 attributes unless it's html5-strict
if (type != "html5-strict") {
addAttrs("script", "language xml:space");
addAttrs("style", "xml:space");
addAttrs("object", "declare classid code codebase codetype archive standby align border hspace vspace");
addAttrs("embed", "align name hspace vspace");
addAttrs("param", "valuetype type");
addAttrs("a", "charset name rev shape coords");
addAttrs("br", "clear");
addAttrs("applet", "codebase archive code object alt name width height align hspace vspace");
addAttrs("img", "name longdesc align border hspace vspace");
addAttrs("iframe", "longdesc frameborder marginwidth marginheight scrolling align");
addAttrs("font basefont", "size color face");
addAttrs("input", "usemap align");
addAttrs("select", "onchange");
addAttrs("textarea");
addAttrs("h1 h2 h3 h4 h5 h6 div p legend caption", "align");
addAttrs("ul", "type compact");
addAttrs("li", "type");
addAttrs("ol dl menu dir", "compact");
addAttrs("pre", "width xml:space");
addAttrs("hr", "align noshade size width");
addAttrs("isindex", "prompt");
addAttrs("table", "summary width frame rules cellspacing cellpadding align bgcolor");
addAttrs("col", "width align char charoff valign");
addAttrs("colgroup", "width align char charoff valign");
addAttrs("thead", "align char charoff valign");
addAttrs("tr", "align char charoff valign bgcolor");
addAttrs("th", "axis align char charoff valign nowrap bgcolor width height");
addAttrs("form", "accept");
addAttrs("td", "abbr axis scope align char charoff valign nowrap bgcolor width height");
addAttrs("tfoot", "align char charoff valign");
addAttrs("tbody", "align char charoff valign");
addAttrs("area", "nohref");
addAttrs("body", "background bgcolor text link vlink alink");
}
// Extend with HTML5 attributes unless it's html4
if (type != "html4") {
addAttrs("input button select textarea", "autofocus");
addAttrs("input textarea", "placeholder");
addAttrs("a", "download");
addAttrs("link script img", "crossorigin");
addAttrs("iframe", "sandbox seamless allowfullscreen"); // Excluded: srcdoc
}
// Special: iframe, ruby, video, audio, label
// Delete children of the same name from it's parent
// For example: form can't have a child of the name form
each(split('a form meter progress dfn'), function(name) {
if (schema[name]) {
delete schema[name].children[name];
}
});
// Delete header, footer, sectioning and heading content descendants
/*each('dt th address', function(name) {
delete schema[name].children[name];
});*/
// Caption can't have tables
delete schema.caption.children.table;
// Delete scripts by default due to possible XSS
delete schema.script;
// TODO: LI:s can only have value if parent is OL
// TODO: Handle transparent elements
// a ins del canvas map
mapCache[type] = schema;
return schema;
}
function compileElementMap(value, mode) {
var styles;
if (value) {
styles = {};
if (typeof value == 'string') {
value = {
'*': value
};
}
// Convert styles into a rule list
each(value, function(value, key) {
styles[key] = styles[key.toUpperCase()] = mode == 'map' ? makeMap(value, /[, ]/) : explode(value, /[, ]/);
});
}
return styles;
}
/**
* Constructs a new Schema instance.
*
* @constructor
* @method Schema
* @param {Object} settings Name/value settings object.
*/
return function(settings) {
var self = this, elements = {}, children = {}, patternElements = [], validStyles, invalidStyles, schemaItems;
var whiteSpaceElementsMap, selfClosingElementsMap, shortEndedElementsMap, boolAttrMap, validClasses;
var blockElementsMap, nonEmptyElementsMap, moveCaretBeforeOnEnterElementsMap, textBlockElementsMap, textInlineElementsMap;
var customElementsMap = {}, specialElements = {};
// Creates an lookup table map object for the specified option or the default value
function createLookupTable(option, default_value, extendWith) {
var value = settings[option];
if (!value) {
// Get cached default map or make it if needed
value = mapCache[option];
if (!value) {
value = makeMap(default_value, ' ', makeMap(default_value.toUpperCase(), ' '));
value = extend(value, extendWith);
mapCache[option] = value;
}
} else {
// Create custom map
value = makeMap(value, /[, ]/, makeMap(value.toUpperCase(), /[, ]/));
}
return value;
}
settings = settings || {};
schemaItems = compileSchema(settings.schema);
// Allow all elements and attributes if verify_html is set to false
if (settings.verify_html === false) {
settings.valid_elements = '*[*]';
}
validStyles = compileElementMap(settings.valid_styles);
invalidStyles = compileElementMap(settings.invalid_styles, 'map');
validClasses = compileElementMap(settings.valid_classes, 'map');
// Setup map objects
whiteSpaceElementsMap = createLookupTable('whitespace_elements', 'pre script noscript style textarea video audio iframe object');
selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li option p td tfoot th thead tr');
shortEndedElementsMap = createLookupTable('short_ended_elements', 'area base basefont br col frame hr img input isindex link ' +
'meta param embed source wbr track');
boolAttrMap = createLookupTable('boolean_attributes', 'checked compact declare defer disabled ismap multiple nohref noresize ' +
'noshade nowrap readonly selected autoplay loop controls');
nonEmptyElementsMap = createLookupTable('non_empty_elements', 'td th iframe video audio object script', shortEndedElementsMap);
moveCaretBeforeOnEnterElementsMap = createLookupTable('move_caret_before_on_enter_elements', 'table', nonEmptyElementsMap);
textBlockElementsMap = createLookupTable('text_block_elements', 'h1 h2 h3 h4 h5 h6 p div address pre form ' +
'blockquote center dir fieldset header footer article section hgroup aside nav figure');
blockElementsMap = createLookupTable('block_elements', 'hr table tbody thead tfoot ' +
'th tr td li ol ul caption dl dt dd noscript menu isindex option ' +
'datalist select optgroup figcaption', textBlockElementsMap);
textInlineElementsMap = createLookupTable('text_inline_elements', 'span strong b em i font strike u var cite ' +
'dfn code mark q sup sub samp');
each((settings.special || 'script noscript style textarea').split(' '), function(name) {
specialElements[name] = new RegExp('<\/' + name + '[^>]*>', 'gi');
});
// Converts a wildcard expression string to a regexp for example *a will become /.*a/.
function patternToRegExp(str) {
return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$');
}
// Parses the specified valid_elements string and adds to the current rules
// This function is a bit hard to read since it's heavily optimized for speed
function addValidElements(validElements) {
var ei, el, ai, al, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder,
prefix, outputName, globalAttributes, globalAttributesOrder, key, value,
elementRuleRegExp = /^([#+\-])?([^\[!\/]+)(?:\/([^\[!]+))?(?:(!?)\[([^\]]+)\])?$/,
attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/,
hasPatternsRegExp = /[*?+]/;
if (validElements) {
// Split valid elements into an array with rules
validElements = split(validElements, ',');
if (elements['@']) {
globalAttributes = elements['@'].attributes;
globalAttributesOrder = elements['@'].attributesOrder;
}
// Loop all rules
for (ei = 0, el = validElements.length; ei < el; ei++) {
// Parse element rule
matches = elementRuleRegExp.exec(validElements[ei]);
if (matches) {
// Setup local names for matches
prefix = matches[1];
elementName = matches[2];
outputName = matches[3];
attrData = matches[5];
// Create new attributes and attributesOrder
attributes = {};
attributesOrder = [];
// Create the new element
element = {
attributes: attributes,
attributesOrder: attributesOrder
};
// Padd empty elements prefix
if (prefix === '#') {
element.paddEmpty = true;
}
// Remove empty elements prefix
if (prefix === '-') {
element.removeEmpty = true;
}
if (matches[4] === '!') {
element.removeEmptyAttrs = true;
}
// Copy attributes from global rule into current rule
if (globalAttributes) {
for (key in globalAttributes) {
attributes[key] = globalAttributes[key];
}
attributesOrder.push.apply(attributesOrder, globalAttributesOrder);
}
// Attributes defined
if (attrData) {
attrData = split(attrData, '|');
for (ai = 0, al = attrData.length; ai < al; ai++) {
matches = attrRuleRegExp.exec(attrData[ai]);
if (matches) {
attr = {};
attrType = matches[1];
attrName = matches[2].replace(/::/g, ':');
prefix = matches[3];
value = matches[4];
// Required
if (attrType === '!') {
element.attributesRequired = element.attributesRequired || [];
element.attributesRequired.push(attrName);
attr.required = true;
}
// Denied from global
if (attrType === '-') {
delete attributes[attrName];
attributesOrder.splice(inArray(attributesOrder, attrName), 1);
continue;
}
// Default value
if (prefix) {
// Default value
if (prefix === '=') {
element.attributesDefault = element.attributesDefault || [];
element.attributesDefault.push({name: attrName, value: value});
attr.defaultValue = value;
}
// Forced value
if (prefix === ':') {
element.attributesForced = element.attributesForced || [];
element.attributesForced.push({name: attrName, value: value});
attr.forcedValue = value;
}
// Required values
if (prefix === '<') {
attr.validValues = makeMap(value, '?');
}
}
// Check for attribute patterns
if (hasPatternsRegExp.test(attrName)) {
element.attributePatterns = element.attributePatterns || [];
attr.pattern = patternToRegExp(attrName);
element.attributePatterns.push(attr);
} else {
// Add attribute to order list if it doesn't already exist
if (!attributes[attrName]) {
attributesOrder.push(attrName);
}
attributes[attrName] = attr;
}
}
}
}
// Global rule, store away these for later usage
if (!globalAttributes && elementName == '@') {
globalAttributes = attributes;
globalAttributesOrder = attributesOrder;
}
// Handle substitute elements such as b/strong
if (outputName) {
element.outputName = elementName;
elements[outputName] = element;
}
// Add pattern or exact element
if (hasPatternsRegExp.test(elementName)) {
element.pattern = patternToRegExp(elementName);
patternElements.push(element);
} else {
elements[elementName] = element;
}
}
}
}
}
function setValidElements(validElements) {
elements = {};
patternElements = [];
addValidElements(validElements);
each(schemaItems, function(element, name) {
children[name] = element.children;
});
}
// Adds custom non HTML elements to the schema
function addCustomElements(customElements) {
var customElementRegExp = /^(~)?(.+)$/;
if (customElements) {
// Flush cached items since we are altering the default maps
mapCache.text_block_elements = mapCache.block_elements = null;
each(split(customElements, ','), function(rule) {
var matches = customElementRegExp.exec(rule),
inline = matches[1] === '~',
cloneName = inline ? 'span' : 'div',
name = matches[2];
children[name] = children[cloneName];
customElementsMap[name] = cloneName;
// If it's not marked as inline then add it to valid block elements
if (!inline) {
blockElementsMap[name.toUpperCase()] = {};
blockElementsMap[name] = {};
}
// Add elements clone if needed
if (!elements[name]) {
var customRule = elements[cloneName];
customRule = extend({}, customRule);
delete customRule.removeEmptyAttrs;
delete customRule.removeEmpty;
elements[name] = customRule;
}
// Add custom elements at span/div positions
each(children, function(element, elmName) {
if (element[cloneName]) {
children[elmName] = element = extend({}, children[elmName]);
element[name] = element[cloneName];
}
});
});
}
}
// Adds valid children to the schema object
function addValidChildren(validChildren) {
var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/;
// Invalidate the schema cache if the schema is mutated
mapCache[settings.schema] = null;
if (validChildren) {
each(split(validChildren, ','), function(rule) {
var matches = childRuleRegExp.exec(rule), parent, prefix;
if (matches) {
prefix = matches[1];
// Add/remove items from default
if (prefix) {
parent = children[matches[2]];
} else {
parent = children[matches[2]] = {'#comment': {}};
}
parent = children[matches[2]];
each(split(matches[3], '|'), function(child) {
if (prefix === '-') {
delete parent[child];
} else {
parent[child] = {};
}
});
}
});
}
}
function getElementRule(name) {
var element = elements[name], i;
// Exact match found
if (element) {
return element;
}
// No exact match then try the patterns
i = patternElements.length;
while (i--) {
element = patternElements[i];
if (element.pattern.test(name)) {
return element;
}
}
}
if (!settings.valid_elements) {
// No valid elements defined then clone the elements from the schema spec
each(schemaItems, function(element, name) {
elements[name] = {
attributes: element.attributes,
attributesOrder: element.attributesOrder
};
children[name] = element.children;
});
// Switch these on HTML4
if (settings.schema != "html5") {
each(split('strong/b em/i'), function(item) {
item = split(item, '/');
elements[item[1]].outputName = item[0];
});
}
// Add default alt attribute for images, removed since alt="" is treated as presentational.
// elements.img.attributesDefault = [{name: 'alt', value: ''}];
// Remove these if they are empty by default
each(split('ol ul sub sup blockquote span font a table tbody tr strong em b i'), function(name) {
if (elements[name]) {
elements[name].removeEmpty = true;
}
});
// Padd these by default
each(split('p h1 h2 h3 h4 h5 h6 th td pre div address caption'), function(name) {
elements[name].paddEmpty = true;
});
// Remove these if they have no attributes
each(split('span'), function(name) {
elements[name].removeEmptyAttrs = true;
});
// Remove these by default
// TODO: Reenable in 4.1
/*each(split('script style'), function(name) {
delete elements[name];
});*/
} else {
setValidElements(settings.valid_elements);
}
addCustomElements(settings.custom_elements);
addValidChildren(settings.valid_children);
addValidElements(settings.extended_valid_elements);
// Todo: Remove this when we fix list handling to be valid
addValidChildren('+ol[ul|ol],+ul[ul|ol]');
// Some elements are not valid by themselves - require parents
each({
dd: 'dl',
dt: 'dl',
li: 'ul ol',
td: 'tr',
th: 'tr',
tr: 'tbody thead tfoot',
tbody: 'table',
thead: 'table',
tfoot: 'table',
legend: 'fieldset',
area: 'map',
param: 'video audio object'
}, function(parents, item) {
if (elements[item]) {
elements[item].parentsRequired = split(parents);
}
});
// Delete invalid elements
if (settings.invalid_elements) {
each(explode(settings.invalid_elements), function(item) {
if (elements[item]) {
delete elements[item];
}
});
}
// If the user didn't allow span only allow internal spans
if (!getElementRule('span')) {
addValidElements('span[!data-mce-type|*]');
}
/**
* Name/value map object with valid parents and children to those parents.
*
* @example
* children = {
* div:{p:{}, h1:{}}
* };
* @field children
* @type Object
*/
self.children = children;
/**
* Name/value map object with valid styles for each element.
*
* @method getValidStyles
* @type Object
*/
self.getValidStyles = function() {
return validStyles;
};
/**
* Name/value map object with valid styles for each element.
*
* @method getInvalidStyles
* @type Object
*/
self.getInvalidStyles = function() {
return invalidStyles;
};
/**
* Name/value map object with valid classes for each element.
*
* @method getValidClasses
* @type Object
*/
self.getValidClasses = function() {
return validClasses;
};
/**
* Returns a map with boolean attributes.
*
* @method getBoolAttrs
* @return {Object} Name/value lookup map for boolean attributes.
*/
self.getBoolAttrs = function() {
return boolAttrMap;
};
/**
* Returns a map with block elements.
*
* @method getBlockElements
* @return {Object} Name/value lookup map for block elements.
*/
self.getBlockElements = function() {
return blockElementsMap;
};
/**
* Returns a map with text block elements. Such as: p,h1-h6,div,address
*
* @method getTextBlockElements
* @return {Object} Name/value lookup map for block elements.
*/
self.getTextBlockElements = function() {
return textBlockElementsMap;
};
/**
* Returns a map of inline text format nodes for example strong/span or ins.
*
* @method getTextInlineElements
* @return {Object} Name/value lookup map for text format elements.
*/
self.getTextInlineElements = function() {
return textInlineElementsMap;
};
/**
* Returns a map with short ended elements such as BR or IMG.
*
* @method getShortEndedElements
* @return {Object} Name/value lookup map for short ended elements.
*/
self.getShortEndedElements = function() {
return shortEndedElementsMap;
};
/**
* Returns a map with self closing tags such as <li>.
*
* @method getSelfClosingElements
* @return {Object} Name/value lookup map for self closing tags elements.
*/
self.getSelfClosingElements = function() {
return selfClosingElementsMap;
};
/**
* Returns a map with elements that should be treated as contents regardless if it has text
* content in them or not such as TD, VIDEO or IMG.
*
* @method getNonEmptyElements
* @return {Object} Name/value lookup map for non empty elements.
*/
self.getNonEmptyElements = function() {
return nonEmptyElementsMap;
};
/**
* Returns a map with elements that the caret should be moved in front of after enter is
* pressed
*
* @method getMoveCaretBeforeOnEnterElements
* @return {Object} Name/value lookup map for elements to place the caret in front of.
*/
self.getMoveCaretBeforeOnEnterElements = function() {
return moveCaretBeforeOnEnterElementsMap;
};
/**
* Returns a map with elements where white space is to be preserved like PRE or SCRIPT.
*
* @method getWhiteSpaceElements
* @return {Object} Name/value lookup map for white space elements.
*/
self.getWhiteSpaceElements = function() {
return whiteSpaceElementsMap;
};
/**
* Returns a map with special elements. These are elements that needs to be parsed
* in a special way such as script, style, textarea etc. The map object values
* are regexps used to find the end of the element.
*
* @method getSpecialElements
* @return {Object} Name/value lookup map for special elements.
*/
self.getSpecialElements = function() {
return specialElements;
};
/**
* Returns true/false if the specified element and it's child is valid or not
* according to the schema.
*
* @method isValidChild
* @param {String} name Element name to check for.
* @param {String} child Element child to verify.
* @return {Boolean} True/false if the element is a valid child of the specified parent.
*/
self.isValidChild = function(name, child) {
var parent = children[name];
return !!(parent && parent[child]);
};
/**
* Returns true/false if the specified element name and optional attribute is
* valid according to the schema.
*
* @method isValid
* @param {String} name Name of element to check.
* @param {String} attr Optional attribute name to check for.
* @return {Boolean} True/false if the element and attribute is valid.
*/
self.isValid = function(name, attr) {
var attrPatterns, i, rule = getElementRule(name);
// Check if it's a valid element
if (rule) {
if (attr) {
// Check if attribute name exists
if (rule.attributes[attr]) {
return true;
}
// Check if attribute matches a regexp pattern
attrPatterns = rule.attributePatterns;
if (attrPatterns) {
i = attrPatterns.length;
while (i--) {
if (attrPatterns[i].pattern.test(name)) {
return true;
}
}
}
} else {
return true;
}
}
// No match
return false;
};
/**
* Returns true/false if the specified element is valid or not
* according to the schema.
*
* @method getElementRule
* @param {String} name Element name to check for.
* @return {Object} Element object or undefined if the element isn't valid.
*/
self.getElementRule = getElementRule;
/**
* Returns an map object of all custom elements.
*
* @method getCustomElements
* @return {Object} Name/value map object of all custom elements.
*/
self.getCustomElements = function() {
return customElementsMap;
};
/**
* Parses a valid elements string and adds it to the schema. The valid elements
* format is for example "element[attr=default|otherattr]".
* Existing rules will be replaced with the ones specified, so this extends the schema.
*
* @method addValidElements
* @param {String} valid_elements String in the valid elements format to be parsed.
*/
self.addValidElements = addValidElements;
/**
* Parses a valid elements string and sets it to the schema. The valid elements
* format is for example "element[attr=default|otherattr]".
* Existing rules will be replaced with the ones specified, so this extends the schema.
*
* @method setValidElements
* @param {String} valid_elements String in the valid elements format to be parsed.
*/
self.setValidElements = setValidElements;
/**
* Adds custom non HTML elements to the schema.
*
* @method addCustomElements
* @param {String} custom_elements Comma separated list of custom elements to add.
*/
self.addCustomElements = addCustomElements;
/**
* Parses a valid children string and adds them to the schema structure. The valid children
* format is for example: "element[child1|child2]".
*
* @method addValidChildren
* @param {String} valid_children Valid children elements string to parse
*/
self.addValidChildren = addValidChildren;
self.elements = elements;
};
});
// Included from: js/tinymce/classes/html/SaxParser.js
/**
* SaxParser.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/*eslint max-depth:[2, 9] */
/**
* This class parses HTML code using pure JavaScript and executes various events for each item it finds. It will
* always execute the events in the right order for tag soup code like <b><p></b></p>. It will also remove elements
* and attributes that doesn't fit the schema if the validate setting is enabled.
*
* @example
* var parser = new tinymce.html.SaxParser({
* validate: true,
*
* comment: function(text) {
* console.log('Comment:', text);
* },
*
* cdata: function(text) {
* console.log('CDATA:', text);
* },
*
* text: function(text, raw) {
* console.log('Text:', text, 'Raw:', raw);
* },
*
* start: function(name, attrs, empty) {
* console.log('Start:', name, attrs, empty);
* },
*
* end: function(name) {
* console.log('End:', name);
* },
*
* pi: function(name, text) {
* console.log('PI:', name, text);
* },
*
* doctype: function(text) {
* console.log('DocType:', text);
* }
* }, schema);
* @class tinymce.html.SaxParser
* @version 3.4
*/
define("tinymce/html/SaxParser", [
"tinymce/html/Schema",
"tinymce/html/Entities",
"tinymce/util/Tools"
], function(Schema, Entities, Tools) {
var each = Tools.each;
/**
* Returns the index of the end tag for a specific start tag. This can be
* used to skip all children of a parent element from being processed.
*
* @private
* @method findEndTag
* @param {tinymce.html.Schema} schema Schema instance to use to match short ended elements.
* @param {String} html HTML string to find the end tag in.
* @param {Number} startIndex Indext to start searching at should be after the start tag.
* @return {Number} Index of the end tag.
*/
function findEndTag(schema, html, startIndex) {
var count = 1, index, matches, tokenRegExp, shortEndedElements;
shortEndedElements = schema.getShortEndedElements();
tokenRegExp = /<([!?\/])?([A-Za-z0-9\-_\:\.]+)((?:\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\/|\s+)>/g;
tokenRegExp.lastIndex = index = startIndex;
while ((matches = tokenRegExp.exec(html))) {
index = tokenRegExp.lastIndex;
if (matches[1] === '/') { // End element
count--;
} else if (!matches[1]) { // Start element
if (matches[2] in shortEndedElements) {
continue;
}
count++;
}
if (count === 0) {
break;
}
}
return index;
}
/**
* Constructs a new SaxParser instance.
*
* @constructor
* @method SaxParser
* @param {Object} settings Name/value collection of settings. comment, cdata, text, start and end are callbacks.
* @param {tinymce.html.Schema} schema HTML Schema class to use when parsing.
*/
function SaxParser(settings, schema) {
var self = this;
function noop() {}
settings = settings || {};
self.schema = schema = schema || new Schema();
if (settings.fix_self_closing !== false) {
settings.fix_self_closing = true;
}
// Add handler functions from settings and setup default handlers
each('comment cdata text start end pi doctype'.split(' '), function(name) {
if (name) {
self[name] = settings[name] || noop;
}
});
/**
* Parses the specified HTML string and executes the callbacks for each item it finds.
*
* @example
* new SaxParser({...}).parse('<b>text</b>');
* @method parse
* @param {String} html Html string to sax parse.
*/
self.parse = function(html) {
var self = this, matches, index = 0, value, endRegExp, stack = [], attrList, i, text, name;
var isInternalElement, removeInternalElements, shortEndedElements, fillAttrsMap, isShortEnded;
var validate, elementRule, isValidElement, attr, attribsValue, validAttributesMap, validAttributePatterns;
var attributesRequired, attributesDefault, attributesForced;
var anyAttributesRequired, selfClosing, tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0;
var decode = Entities.decode, fixSelfClosing, filteredUrlAttrs = Tools.makeMap('src,href,data,background,formaction,poster');
var scriptUriRegExp = /((java|vb)script|mhtml):/i, dataUriRegExp = /^data:/i;
function processEndTag(name) {
var pos, i;
// Find position of parent of the same type
pos = stack.length;
while (pos--) {
if (stack[pos].name === name) {
break;
}
}
// Found parent
if (pos >= 0) {
// Close all the open elements
for (i = stack.length - 1; i >= pos; i--) {
name = stack[i];
if (name.valid) {
self.end(name.name);
}
}
// Remove the open elements from the stack
stack.length = pos;
}
}
function parseAttribute(match, name, value, val2, val3) {
var attrRule, i, trimRegExp = /[\s\u0000-\u001F]+/g;
name = name.toLowerCase();
value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute
// Validate name and value pass through all data- attributes
if (validate && !isInternalElement && name.indexOf('data-') !== 0) {
attrRule = validAttributesMap[name];
// Find rule by pattern matching
if (!attrRule && validAttributePatterns) {
i = validAttributePatterns.length;
while (i--) {
attrRule = validAttributePatterns[i];
if (attrRule.pattern.test(name)) {
break;
}
}
// No rule matched
if (i === -1) {
attrRule = null;
}
}
// No attribute rule found
if (!attrRule) {
return;
}
// Validate value
if (attrRule.validValues && !(value in attrRule.validValues)) {
return;
}
}
// Block any javascript: urls or non image data uris
if (filteredUrlAttrs[name] && !settings.allow_script_urls) {
var uri = value.replace(trimRegExp, '');
try {
// Might throw malformed URI sequence
uri = decodeURIComponent(uri);
} catch (ex) {
// Fallback to non UTF-8 decoder
uri = unescape(uri);
}
if (scriptUriRegExp.test(uri)) {
return;
}
if (!settings.allow_html_data_urls && dataUriRegExp.test(uri) && !/^data:image\//i.test(uri)) {
return;
}
}
// Add attribute to list and map
attrList.map[name] = value;
attrList.push({
name: name,
value: value
});
}
// Precompile RegExps and map objects
tokenRegExp = new RegExp('<(?:' +
'(?:!--([\\w\\W]*?)-->)|' + // Comment
'(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|' + // CDATA
'(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE
'(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI
'(?:\\/([^>]+)>)|' + // End element
'(?:([A-Za-z0-9\\-_\\:\\.]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/|\\s+)>)' + // Start element
')', 'g');
attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:[^\"])*)\")|(?:\'((?:[^\'])*)\')|([^>\s]+)))?/g;
// Setup lookup tables for empty elements and boolean attributes
shortEndedElements = schema.getShortEndedElements();
selfClosing = settings.self_closing_elements || schema.getSelfClosingElements();
fillAttrsMap = schema.getBoolAttrs();
validate = settings.validate;
removeInternalElements = settings.remove_internals;
fixSelfClosing = settings.fix_self_closing;
specialElements = schema.getSpecialElements();
while ((matches = tokenRegExp.exec(html))) {
// Text
if (index < matches.index) {
self.text(decode(html.substr(index, matches.index - index)));
}
if ((value = matches[6])) { // End element
value = value.toLowerCase();
// IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements
if (value.charAt(0) === ':') {
value = value.substr(1);
}
processEndTag(value);
} else if ((value = matches[7])) { // Start element
value = value.toLowerCase();
// IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements
if (value.charAt(0) === ':') {
value = value.substr(1);
}
isShortEnded = value in shortEndedElements;
// Is self closing tag for example an <li> after an open <li>
if (fixSelfClosing && selfClosing[value] && stack.length > 0 && stack[stack.length - 1].name === value) {
processEndTag(value);
}
// Validate element
if (!validate || (elementRule = schema.getElementRule(value))) {
isValidElement = true;
// Grab attributes map and patters when validation is enabled
if (validate) {
validAttributesMap = elementRule.attributes;
validAttributePatterns = elementRule.attributePatterns;
}
// Parse attributes
if ((attribsValue = matches[8])) {
isInternalElement = attribsValue.indexOf('data-mce-type') !== -1; // Check if the element is an internal element
// If the element has internal attributes then remove it if we are told to do so
if (isInternalElement && removeInternalElements) {
isValidElement = false;
}
attrList = [];
attrList.map = {};
attribsValue.replace(attrRegExp, parseAttribute);
} else {
attrList = [];
attrList.map = {};
}
// Process attributes if validation is enabled
if (validate && !isInternalElement) {
attributesRequired = elementRule.attributesRequired;
attributesDefault = elementRule.attributesDefault;
attributesForced = elementRule.attributesForced;
anyAttributesRequired = elementRule.removeEmptyAttrs;
// Check if any attribute exists
if (anyAttributesRequired && !attrList.length) {
isValidElement = false;
}
// Handle forced attributes
if (attributesForced) {
i = attributesForced.length;
while (i--) {
attr = attributesForced[i];
name = attr.name;
attrValue = attr.value;
if (attrValue === '{$uid}') {
attrValue = 'mce_' + idCount++;
}
attrList.map[name] = attrValue;
attrList.push({name: name, value: attrValue});
}
}
// Handle default attributes
if (attributesDefault) {
i = attributesDefault.length;
while (i--) {
attr = attributesDefault[i];
name = attr.name;
if (!(name in attrList.map)) {
attrValue = attr.value;
if (attrValue === '{$uid}') {
attrValue = 'mce_' + idCount++;
}
attrList.map[name] = attrValue;
attrList.push({name: name, value: attrValue});
}
}
}
// Handle required attributes
if (attributesRequired) {
i = attributesRequired.length;
while (i--) {
if (attributesRequired[i] in attrList.map) {
break;
}
}
// None of the required attributes where found
if (i === -1) {
isValidElement = false;
}
}
// Invalidate element if it's marked as bogus
if ((attr = attrList.map['data-mce-bogus'])) {
if (attr === 'all') {
index = findEndTag(schema, html, tokenRegExp.lastIndex);
tokenRegExp.lastIndex = index;
continue;
}
isValidElement = false;
}
}
if (isValidElement) {
self.start(value, attrList, isShortEnded);
}
} else {
isValidElement = false;
}
// Treat script, noscript and style a bit different since they may include code that looks like elements
if ((endRegExp = specialElements[value])) {
endRegExp.lastIndex = index = matches.index + matches[0].length;
if ((matches = endRegExp.exec(html))) {
if (isValidElement) {
text = html.substr(index, matches.index - index);
}
index = matches.index + matches[0].length;
} else {
text = html.substr(index);
index = html.length;
}
if (isValidElement) {
if (text.length > 0) {
self.text(text, true);
}
self.end(value);
}
tokenRegExp.lastIndex = index;
continue;
}
// Push value on to stack
if (!isShortEnded) {
if (!attribsValue || attribsValue.indexOf('/') != attribsValue.length - 1) {
stack.push({name: value, valid: isValidElement});
} else if (isValidElement) {
self.end(value);
}
}
} else if ((value = matches[1])) { // Comment
// Padd comment value to avoid browsers from parsing invalid comments as HTML
if (value.charAt(0) === '>') {
value = ' ' + value;
}
if (!settings.allow_conditional_comments && value.substr(0, 3).toLowerCase() === '[if') {
value = ' ' + value;
}
self.comment(value);
} else if ((value = matches[2])) { // CDATA
self.cdata(value);
} else if ((value = matches[3])) { // DOCTYPE
self.doctype(value);
} else if ((value = matches[4])) { // PI
self.pi(value, matches[5]);
}
index = matches.index + matches[0].length;
}
// Text
if (index < html.length) {
self.text(decode(html.substr(index)));
}
// Close any open elements
for (i = stack.length - 1; i >= 0; i--) {
value = stack[i];
if (value.valid) {
self.end(value.name);
}
}
};
}
SaxParser.findEndTag = findEndTag;
return SaxParser;
});
// Included from: js/tinymce/classes/html/DomParser.js
/**
* DomParser.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class parses HTML code into a DOM like structure of nodes it will remove redundant whitespace and make
* sure that the node tree is valid according to the specified schema.
* So for example: <p>a<p>b</p>c</p> will become <p>a</p><p>b</p><p>c</p>
*
* @example
* var parser = new tinymce.html.DomParser({validate: true}, schema);
* var rootNode = parser.parse('<h1>content</h1>');
*
* @class tinymce.html.DomParser
* @version 3.4
*/
define("tinymce/html/DomParser", [
"tinymce/html/Node",
"tinymce/html/Schema",
"tinymce/html/SaxParser",
"tinymce/util/Tools"
], function(Node, Schema, SaxParser, Tools) {
var makeMap = Tools.makeMap, each = Tools.each, explode = Tools.explode, extend = Tools.extend;
/**
* Constructs a new DomParser instance.
*
* @constructor
* @method DomParser
* @param {Object} settings Name/value collection of settings. comment, cdata, text, start and end are callbacks.
* @param {tinymce.html.Schema} schema HTML Schema class to use when parsing.
*/
return function(settings, schema) {
var self = this, nodeFilters = {}, attributeFilters = [], matchedNodes = {}, matchedAttributes = {};
settings = settings || {};
settings.validate = "validate" in settings ? settings.validate : true;
settings.root_name = settings.root_name || 'body';
self.schema = schema = schema || new Schema();
function fixInvalidChildren(nodes) {
var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i;
var nonEmptyElements, nonSplitableElements, textBlockElements, specialElements, sibling, nextNode;
nonSplitableElements = makeMap('tr,td,th,tbody,thead,tfoot,table');
nonEmptyElements = schema.getNonEmptyElements();
textBlockElements = schema.getTextBlockElements();
specialElements = schema.getSpecialElements();
for (ni = 0; ni < nodes.length; ni++) {
node = nodes[ni];
// Already removed or fixed
if (!node.parent || node.fixed) {
continue;
}
// If the invalid element is a text block and the text block is within a parent LI element
// Then unwrap the first text block and convert other sibling text blocks to LI elements similar to Word/Open Office
if (textBlockElements[node.name] && node.parent.name == 'li') {
// Move sibling text blocks after LI element
sibling = node.next;
while (sibling) {
if (textBlockElements[sibling.name]) {
sibling.name = 'li';
sibling.fixed = true;
node.parent.insert(sibling, node.parent);
} else {
break;
}
sibling = sibling.next;
}
// Unwrap current text block
node.unwrap(node);
continue;
}
// Get list of all parent nodes until we find a valid parent to stick the child into
parents = [node];
for (parent = node.parent; parent && !schema.isValidChild(parent.name, node.name) &&
!nonSplitableElements[parent.name]; parent = parent.parent) {
parents.push(parent);
}
// Found a suitable parent
if (parent && parents.length > 1) {
// Reverse the array since it makes looping easier
parents.reverse();
// Clone the related parent and insert that after the moved node
newParent = currentNode = self.filterNode(parents[0].clone());
// Start cloning and moving children on the left side of the target node
for (i = 0; i < parents.length - 1; i++) {
if (schema.isValidChild(currentNode.name, parents[i].name)) {
tempNode = self.filterNode(parents[i].clone());
currentNode.append(tempNode);
} else {
tempNode = currentNode;
}
for (childNode = parents[i].firstChild; childNode && childNode != parents[i + 1];) {
nextNode = childNode.next;
tempNode.append(childNode);
childNode = nextNode;
}
currentNode = tempNode;
}
if (!newParent.isEmpty(nonEmptyElements)) {
parent.insert(newParent, parents[0], true);
parent.insert(node, newParent);
} else {
parent.insert(node, parents[0], true);
}
// Check if the element is empty by looking through it's contents and special treatment for <p><br /></p>
parent = parents[0];
if (parent.isEmpty(nonEmptyElements) || parent.firstChild === parent.lastChild && parent.firstChild.name === 'br') {
parent.empty().remove();
}
} else if (node.parent) {
// If it's an LI try to find a UL/OL for it or wrap it
if (node.name === 'li') {
sibling = node.prev;
if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) {
sibling.append(node);
continue;
}
sibling = node.next;
if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) {
sibling.insert(node, sibling.firstChild, true);
continue;
}
node.wrap(self.filterNode(new Node('ul', 1)));
continue;
}
// Try wrapping the element in a DIV
if (schema.isValidChild(node.parent.name, 'div') && schema.isValidChild('div', node.name)) {
node.wrap(self.filterNode(new Node('div', 1)));
} else {
// We failed wrapping it, then remove or unwrap it
if (specialElements[node.name]) {
node.empty().remove();
} else {
node.unwrap();
}
}
}
}
}
/**
* Runs the specified node though the element and attributes filters.
*
* @method filterNode
* @param {tinymce.html.Node} Node the node to run filters on.
* @return {tinymce.html.Node} The passed in node.
*/
self.filterNode = function(node) {
var i, name, list;
// Run element filters
if (name in nodeFilters) {
list = matchedNodes[name];
if (list) {
list.push(node);
} else {
matchedNodes[name] = [node];
}
}
// Run attribute filters
i = attributeFilters.length;
while (i--) {
name = attributeFilters[i].name;
if (name in node.attributes.map) {
list = matchedAttributes[name];
if (list) {
list.push(node);
} else {
matchedAttributes[name] = [node];
}
}
}
return node;
};
/**
* Adds a node filter function to the parser, the parser will collect the specified nodes by name
* and then execute the callback ones it has finished parsing the document.
*
* @example
* parser.addNodeFilter('p,h1', function(nodes, name) {
* for (var i = 0; i < nodes.length; i++) {
* console.log(nodes[i].name);
* }
* });
* @method addNodeFilter
* @method {String} name Comma separated list of nodes to collect.
* @param {function} callback Callback function to execute once it has collected nodes.
*/
self.addNodeFilter = function(name, callback) {
each(explode(name), function(name) {
var list = nodeFilters[name];
if (!list) {
nodeFilters[name] = list = [];
}
list.push(callback);
});
};
/**
* Adds a attribute filter function to the parser, the parser will collect nodes that has the specified attributes
* and then execute the callback ones it has finished parsing the document.
*
* @example
* parser.addAttributeFilter('src,href', function(nodes, name) {
* for (var i = 0; i < nodes.length; i++) {
* console.log(nodes[i].name);
* }
* });
* @method addAttributeFilter
* @method {String} name Comma separated list of nodes to collect.
* @param {function} callback Callback function to execute once it has collected nodes.
*/
self.addAttributeFilter = function(name, callback) {
each(explode(name), function(name) {
var i;
for (i = 0; i < attributeFilters.length; i++) {
if (attributeFilters[i].name === name) {
attributeFilters[i].callbacks.push(callback);
return;
}
}
attributeFilters.push({name: name, callbacks: [callback]});
});
};
/**
* Parses the specified HTML string into a DOM like node tree and returns the result.
*
* @example
* var rootNode = new DomParser({...}).parse('<b>text</b>');
* @method parse
* @param {String} html Html string to sax parse.
* @param {Object} args Optional args object that gets passed to all filter functions.
* @return {tinymce.html.Node} Root node containing the tree.
*/
self.parse = function(html, args) {
var parser, rootNode, node, nodes, i, l, fi, fl, list, name, validate;
var blockElements, startWhiteSpaceRegExp, invalidChildren = [], isInWhiteSpacePreservedElement;
var endWhiteSpaceRegExp, allWhiteSpaceRegExp, isAllWhiteSpaceRegExp, whiteSpaceElements;
var children, nonEmptyElements, rootBlockName;
args = args || {};
matchedNodes = {};
matchedAttributes = {};
blockElements = extend(makeMap('script,style,head,html,body,title,meta,param'), schema.getBlockElements());
nonEmptyElements = schema.getNonEmptyElements();
children = schema.children;
validate = settings.validate;
rootBlockName = "forced_root_block" in args ? args.forced_root_block : settings.forced_root_block;
whiteSpaceElements = schema.getWhiteSpaceElements();
startWhiteSpaceRegExp = /^[ \t\r\n]+/;
endWhiteSpaceRegExp = /[ \t\r\n]+$/;
allWhiteSpaceRegExp = /[ \t\r\n]+/g;
isAllWhiteSpaceRegExp = /^[ \t\r\n]+$/;
function addRootBlocks() {
var node = rootNode.firstChild, next, rootBlockNode;
// Removes whitespace at beginning and end of block so:
// <p> x </p> -> <p>x</p>
function trim(rootBlockNode) {
if (rootBlockNode) {
node = rootBlockNode.firstChild;
if (node && node.type == 3) {
node.value = node.value.replace(startWhiteSpaceRegExp, '');
}
node = rootBlockNode.lastChild;
if (node && node.type == 3) {
node.value = node.value.replace(endWhiteSpaceRegExp, '');
}
}
}
// Check if rootBlock is valid within rootNode for example if P is valid in H1 if H1 is the contentEditabe root
if (!schema.isValidChild(rootNode.name, rootBlockName.toLowerCase())) {
return;
}
while (node) {
next = node.next;
if (node.type == 3 || (node.type == 1 && node.name !== 'p' &&
!blockElements[node.name] && !node.attr('data-mce-type'))) {
if (!rootBlockNode) {
// Create a new root block element
rootBlockNode = createNode(rootBlockName, 1);
rootBlockNode.attr(settings.forced_root_block_attrs);
rootNode.insert(rootBlockNode, node);
rootBlockNode.append(node);
} else {
rootBlockNode.append(node);
}
} else {
trim(rootBlockNode);
rootBlockNode = null;
}
node = next;
}
trim(rootBlockNode);
}
function createNode(name, type) {
var node = new Node(name, type), list;
if (name in nodeFilters) {
list = matchedNodes[name];
if (list) {
list.push(node);
} else {
matchedNodes[name] = [node];
}
}
return node;
}
function removeWhitespaceBefore(node) {
var textNode, textNodeNext, textVal, sibling, blockElements = schema.getBlockElements();
for (textNode = node.prev; textNode && textNode.type === 3;) {
textVal = textNode.value.replace(endWhiteSpaceRegExp, '');
// Found a text node with non whitespace then trim that and break
if (textVal.length > 0) {
textNode.value = textVal;
return;
}
textNodeNext = textNode.next;
// Fix for bug #7543 where bogus nodes would produce empty
// text nodes and these would be removed if a nested list was before it
if (textNodeNext) {
if (textNodeNext.type == 3 && textNodeNext.value.length) {
textNode = textNode.prev;
continue;
}
if (!blockElements[textNodeNext.name] && textNodeNext.name != 'script' && textNodeNext.name != 'style') {
textNode = textNode.prev;
continue;
}
}
sibling = textNode.prev;
textNode.remove();
textNode = sibling;
}
}
function cloneAndExcludeBlocks(input) {
var name, output = {};
for (name in input) {
if (name !== 'li' && name != 'p') {
output[name] = input[name];
}
}
return output;
}
parser = new SaxParser({
validate: validate,
allow_script_urls: settings.allow_script_urls,
allow_conditional_comments: settings.allow_conditional_comments,
// Exclude P and LI from DOM parsing since it's treated better by the DOM parser
self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()),
cdata: function(text) {
node.append(createNode('#cdata', 4)).value = text;
},
text: function(text, raw) {
var textNode;
// Trim all redundant whitespace on non white space elements
if (!isInWhiteSpacePreservedElement) {
text = text.replace(allWhiteSpaceRegExp, ' ');
if (node.lastChild && blockElements[node.lastChild.name]) {
text = text.replace(startWhiteSpaceRegExp, '');
}
}
// Do we need to create the node
if (text.length !== 0) {
textNode = createNode('#text', 3);
textNode.raw = !!raw;
node.append(textNode).value = text;
}
},
comment: function(text) {
node.append(createNode('#comment', 8)).value = text;
},
pi: function(name, text) {
node.append(createNode(name, 7)).value = text;
removeWhitespaceBefore(node);
},
doctype: function(text) {
var newNode;
newNode = node.append(createNode('#doctype', 10));
newNode.value = text;
removeWhitespaceBefore(node);
},
start: function(name, attrs, empty) {
var newNode, attrFiltersLen, elementRule, attrName, parent;
elementRule = validate ? schema.getElementRule(name) : {};
if (elementRule) {
newNode = createNode(elementRule.outputName || name, 1);
newNode.attributes = attrs;
newNode.shortEnded = empty;
node.append(newNode);
// Check if node is valid child of the parent node is the child is
// unknown we don't collect it since it's probably a custom element
parent = children[node.name];
if (parent && children[newNode.name] && !parent[newNode.name]) {
invalidChildren.push(newNode);
}
attrFiltersLen = attributeFilters.length;
while (attrFiltersLen--) {
attrName = attributeFilters[attrFiltersLen].name;
if (attrName in attrs.map) {
list = matchedAttributes[attrName];
if (list) {
list.push(newNode);
} else {
matchedAttributes[attrName] = [newNode];
}
}
}
// Trim whitespace before block
if (blockElements[name]) {
removeWhitespaceBefore(newNode);
}
// Change current node if the element wasn't empty i.e not <br /> or <img />
if (!empty) {
node = newNode;
}
// Check if we are inside a whitespace preserved element
if (!isInWhiteSpacePreservedElement && whiteSpaceElements[name]) {
isInWhiteSpacePreservedElement = true;
}
}
},
end: function(name) {
var textNode, elementRule, text, sibling, tempNode;
elementRule = validate ? schema.getElementRule(name) : {};
if (elementRule) {
if (blockElements[name]) {
if (!isInWhiteSpacePreservedElement) {
// Trim whitespace of the first node in a block
textNode = node.firstChild;
if (textNode && textNode.type === 3) {
text = textNode.value.replace(startWhiteSpaceRegExp, '');
// Any characters left after trim or should we remove it
if (text.length > 0) {
textNode.value = text;
textNode = textNode.next;
} else {
sibling = textNode.next;
textNode.remove();
textNode = sibling;
// Remove any pure whitespace siblings
while (textNode && textNode.type === 3) {
text = textNode.value;
sibling = textNode.next;
if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) {
textNode.remove();
textNode = sibling;
}
textNode = sibling;
}
}
}
// Trim whitespace of the last node in a block
textNode = node.lastChild;
if (textNode && textNode.type === 3) {
text = textNode.value.replace(endWhiteSpaceRegExp, '');
// Any characters left after trim or should we remove it
if (text.length > 0) {
textNode.value = text;
textNode = textNode.prev;
} else {
sibling = textNode.prev;
textNode.remove();
textNode = sibling;
// Remove any pure whitespace siblings
while (textNode && textNode.type === 3) {
text = textNode.value;
sibling = textNode.prev;
if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) {
textNode.remove();
textNode = sibling;
}
textNode = sibling;
}
}
}
}
// Trim start white space
// Removed due to: #5424
/*textNode = node.prev;
if (textNode && textNode.type === 3) {
text = textNode.value.replace(startWhiteSpaceRegExp, '');
if (text.length > 0)
textNode.value = text;
else
textNode.remove();
}*/
}
// Check if we exited a whitespace preserved element
if (isInWhiteSpacePreservedElement && whiteSpaceElements[name]) {
isInWhiteSpacePreservedElement = false;
}
// Handle empty nodes
if (elementRule.removeEmpty || elementRule.paddEmpty) {
if (node.isEmpty(nonEmptyElements)) {
if (elementRule.paddEmpty) {
node.empty().append(new Node('#text', '3')).value = '\u00a0';
} else {
// Leave nodes that have a name like <a name="name">
if (!node.attributes.map.name && !node.attributes.map.id) {
tempNode = node.parent;
if (blockElements[node.name]) {
node.empty().remove();
} else {
node.unwrap();
}
node = tempNode;
return;
}
}
}
}
node = node.parent;
}
}
}, schema);
rootNode = node = new Node(args.context || settings.root_name, 11);
parser.parse(html);
// Fix invalid children or report invalid children in a contextual parsing
if (validate && invalidChildren.length) {
if (!args.context) {
fixInvalidChildren(invalidChildren);
} else {
args.invalid = true;
}
}
// Wrap nodes in the root into block elements if the root is body
if (rootBlockName && (rootNode.name == 'body' || args.isRootContent)) {
addRootBlocks();
}
// Run filters only when the contents is valid
if (!args.invalid) {
// Run node filters
for (name in matchedNodes) {
list = nodeFilters[name];
nodes = matchedNodes[name];
// Remove already removed children
fi = nodes.length;
while (fi--) {
if (!nodes[fi].parent) {
nodes.splice(fi, 1);
}
}
for (i = 0, l = list.length; i < l; i++) {
list[i](nodes, name, args);
}
}
// Run attribute filters
for (i = 0, l = attributeFilters.length; i < l; i++) {
list = attributeFilters[i];
if (list.name in matchedAttributes) {
nodes = matchedAttributes[list.name];
// Remove already removed children
fi = nodes.length;
while (fi--) {
if (!nodes[fi].parent) {
nodes.splice(fi, 1);
}
}
for (fi = 0, fl = list.callbacks.length; fi < fl; fi++) {
list.callbacks[fi](nodes, list.name, args);
}
}
}
}
return rootNode;
};
// Remove <br> at end of block elements Gecko and WebKit injects BR elements to
// make it possible to place the caret inside empty blocks. This logic tries to remove
// these elements and keep br elements that where intended to be there intact
if (settings.remove_trailing_brs) {
self.addNodeFilter('br', function(nodes) {
var i, l = nodes.length, node, blockElements = extend({}, schema.getBlockElements());
var nonEmptyElements = schema.getNonEmptyElements(), parent, lastParent, prev, prevName;
var elementRule, textNode;
// Remove brs from body element as well
blockElements.body = 1;
// Must loop forwards since it will otherwise remove all brs in <p>a<br><br><br></p>
for (i = 0; i < l; i++) {
node = nodes[i];
parent = node.parent;
if (blockElements[node.parent.name] && node === parent.lastChild) {
// Loop all nodes to the left of the current node and check for other BR elements
// excluding bookmarks since they are invisible
prev = node.prev;
while (prev) {
prevName = prev.name;
// Ignore bookmarks
if (prevName !== "span" || prev.attr('data-mce-type') !== 'bookmark') {
// Found a non BR element
if (prevName !== "br") {
break;
}
// Found another br it's a <br><br> structure then don't remove anything
if (prevName === 'br') {
node = null;
break;
}
}
prev = prev.prev;
}
if (node) {
node.remove();
// Is the parent to be considered empty after we removed the BR
if (parent.isEmpty(nonEmptyElements)) {
elementRule = schema.getElementRule(parent.name);
// Remove or padd the element depending on schema rule
if (elementRule) {
if (elementRule.removeEmpty) {
parent.remove();
} else if (elementRule.paddEmpty) {
parent.empty().append(new Node('#text', 3)).value = '\u00a0';
}
}
}
}
} else {
// Replaces BR elements inside inline elements like <p><b><i><br></i></b></p>
// so they become <p><b><i>&nbsp;</i></b></p>
lastParent = node;
while (parent && parent.firstChild === lastParent && parent.lastChild === lastParent) {
lastParent = parent;
if (blockElements[parent.name]) {
break;
}
parent = parent.parent;
}
if (lastParent === parent) {
textNode = new Node('#text', 3);
textNode.value = '\u00a0';
node.replace(textNode);
}
}
}
});
}
if (!settings.allow_unsafe_link_target) {
self.addAttributeFilter('href', function(nodes) {
var i = nodes.length, node, rel;
var rules = 'noopener noreferrer';
function addTargetRules(rel) {
rel = removeTargetRules(rel);
return rel ? [rel, rules].join(' ') : rules;
}
function removeTargetRules(rel) {
var regExp = new RegExp('(' + rules.replace(' ', '|') + ')', 'g');
if (rel) {
rel = Tools.trim(rel.replace(regExp, ''));
}
return rel ? rel : null;
}
function toggleTargetRules(rel, isUnsafe) {
return isUnsafe ? addTargetRules(rel) : removeTargetRules(rel);
}
while (i--) {
node = nodes[i];
rel = node.attr('rel');
if (node.name === 'a') {
node.attr('rel', toggleTargetRules(rel, node.attr('target') == '_blank'));
}
}
});
}
// Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included.
if (!settings.allow_html_in_named_anchor) {
self.addAttributeFilter('id,name', function(nodes) {
var i = nodes.length, sibling, prevSibling, parent, node;
while (i--) {
node = nodes[i];
if (node.name === 'a' && node.firstChild && !node.attr('href')) {
parent = node.parent;
// Move children after current node
sibling = node.lastChild;
do {
prevSibling = sibling.prev;
parent.insert(sibling, node);
sibling = prevSibling;
} while (sibling);
}
}
});
}
if (settings.validate && schema.getValidClasses()) {
self.addAttributeFilter('class', function(nodes) {
var i = nodes.length, node, classList, ci, className, classValue;
var validClasses = schema.getValidClasses(), validClassesMap, valid;
while (i--) {
node = nodes[i];
classList = node.attr('class').split(' ');
classValue = '';
for (ci = 0; ci < classList.length; ci++) {
className = classList[ci];
valid = false;
validClassesMap = validClasses['*'];
if (validClassesMap && validClassesMap[className]) {
valid = true;
}
validClassesMap = validClasses[node.name];
if (!valid && validClassesMap && validClassesMap[className]) {
valid = true;
}
if (valid) {
if (classValue) {
classValue += ' ';
}
classValue += className;
}
}
if (!classValue.length) {
classValue = null;
}
node.attr('class', classValue);
}
});
}
};
});
// Included from: js/tinymce/classes/html/Writer.js
/**
* Writer.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class is used to write HTML tags out it can be used with the Serializer or the SaxParser.
*
* @class tinymce.html.Writer
* @example
* var writer = new tinymce.html.Writer({indent: true});
* var parser = new tinymce.html.SaxParser(writer).parse('<p><br></p>');
* console.log(writer.getContent());
*
* @class tinymce.html.Writer
* @version 3.4
*/
define("tinymce/html/Writer", [
"tinymce/html/Entities",
"tinymce/util/Tools"
], function(Entities, Tools) {
var makeMap = Tools.makeMap;
/**
* Constructs a new Writer instance.
*
* @constructor
* @method Writer
* @param {Object} settings Name/value settings object.
*/
return function(settings) {
var html = [], indent, indentBefore, indentAfter, encode, htmlOutput;
settings = settings || {};
indent = settings.indent;
indentBefore = makeMap(settings.indent_before || '');
indentAfter = makeMap(settings.indent_after || '');
encode = Entities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities);
htmlOutput = settings.element_format == "html";
return {
/**
* Writes the a start element such as <p id="a">.
*
* @method start
* @param {String} name Name of the element.
* @param {Array} attrs Optional attribute array or undefined if it hasn't any.
* @param {Boolean} empty Optional empty state if the tag should end like <br />.
*/
start: function(name, attrs, empty) {
var i, l, attr, value;
if (indent && indentBefore[name] && html.length > 0) {
value = html[html.length - 1];
if (value.length > 0 && value !== '\n') {
html.push('\n');
}
}
html.push('<', name);
if (attrs) {
for (i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i];
html.push(' ', attr.name, '="', encode(attr.value, true), '"');
}
}
if (!empty || htmlOutput) {
html[html.length] = '>';
} else {
html[html.length] = ' />';
}
if (empty && indent && indentAfter[name] && html.length > 0) {
value = html[html.length - 1];
if (value.length > 0 && value !== '\n') {
html.push('\n');
}
}
},
/**
* Writes the a end element such as </p>.
*
* @method end
* @param {String} name Name of the element.
*/
end: function(name) {
var value;
/*if (indent && indentBefore[name] && html.length > 0) {
value = html[html.length - 1];
if (value.length > 0 && value !== '\n')
html.push('\n');
}*/
html.push('</', name, '>');
if (indent && indentAfter[name] && html.length > 0) {
value = html[html.length - 1];
if (value.length > 0 && value !== '\n') {
html.push('\n');
}
}
},
/**
* Writes a text node.
*
* @method text
* @param {String} text String to write out.
* @param {Boolean} raw Optional raw state if true the contents wont get encoded.
*/
text: function(text, raw) {
if (text.length > 0) {
html[html.length] = raw ? text : encode(text);
}
},
/**
* Writes a cdata node such as <![CDATA[data]]>.
*
* @method cdata
* @param {String} text String to write out inside the cdata.
*/
cdata: function(text) {
html.push('<![CDATA[', text, ']]>');
},
/**
* Writes a comment node such as <!-- Comment -->.
*
* @method cdata
* @param {String} text String to write out inside the comment.
*/
comment: function(text) {
html.push('<!--', text, '-->');
},
/**
* Writes a PI node such as <?xml attr="value" ?>.
*
* @method pi
* @param {String} name Name of the pi.
* @param {String} text String to write out inside the pi.
*/
pi: function(name, text) {
if (text) {
html.push('<?', name, ' ', encode(text), '?>');
} else {
html.push('<?', name, '?>');
}
if (indent) {
html.push('\n');
}
},
/**
* Writes a doctype node such as <!DOCTYPE data>.
*
* @method doctype
* @param {String} text String to write out inside the doctype.
*/
doctype: function(text) {
html.push('<!DOCTYPE', text, '>', indent ? '\n' : '');
},
/**
* Resets the internal buffer if one wants to reuse the writer.
*
* @method reset
*/
reset: function() {
html.length = 0;
},
/**
* Returns the contents that got serialized.
*
* @method getContent
* @return {String} HTML contents that got written down.
*/
getContent: function() {
return html.join('').replace(/\n$/, '');
}
};
};
});
// Included from: js/tinymce/classes/html/Serializer.js
/**
* Serializer.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class is used to serialize down the DOM tree into a string using a Writer instance.
*
*
* @example
* new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('<p>text</p>'));
* @class tinymce.html.Serializer
* @version 3.4
*/
define("tinymce/html/Serializer", [
"tinymce/html/Writer",
"tinymce/html/Schema"
], function(Writer, Schema) {
/**
* Constructs a new Serializer instance.
*
* @constructor
* @method Serializer
* @param {Object} settings Name/value settings object.
* @param {tinymce.html.Schema} schema Schema instance to use.
*/
return function(settings, schema) {
var self = this, writer = new Writer(settings);
settings = settings || {};
settings.validate = "validate" in settings ? settings.validate : true;
self.schema = schema = schema || new Schema();
self.writer = writer;
/**
* Serializes the specified node into a string.
*
* @example
* new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('<p>text</p>'));
* @method serialize
* @param {tinymce.html.Node} node Node instance to serialize.
* @return {String} String with HTML based on DOM tree.
*/
self.serialize = function(node) {
var handlers, validate;
validate = settings.validate;
handlers = {
// #text
3: function(node) {
writer.text(node.value, node.raw);
},
// #comment
8: function(node) {
writer.comment(node.value);
},
// Processing instruction
7: function(node) {
writer.pi(node.name, node.value);
},
// Doctype
10: function(node) {
writer.doctype(node.value);
},
// CDATA
4: function(node) {
writer.cdata(node.value);
},
// Document fragment
11: function(node) {
if ((node = node.firstChild)) {
do {
walk(node);
} while ((node = node.next));
}
}
};
writer.reset();
function walk(node) {
var handler = handlers[node.type], name, isEmpty, attrs, attrName, attrValue, sortedAttrs, i, l, elementRule;
if (!handler) {
name = node.name;
isEmpty = node.shortEnded;
attrs = node.attributes;
// Sort attributes
if (validate && attrs && attrs.length > 1) {
sortedAttrs = [];
sortedAttrs.map = {};
elementRule = schema.getElementRule(node.name);
if (elementRule) {
for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) {
attrName = elementRule.attributesOrder[i];
if (attrName in attrs.map) {
attrValue = attrs.map[attrName];
sortedAttrs.map[attrName] = attrValue;
sortedAttrs.push({name: attrName, value: attrValue});
}
}
for (i = 0, l = attrs.length; i < l; i++) {
attrName = attrs[i].name;
if (!(attrName in sortedAttrs.map)) {
attrValue = attrs.map[attrName];
sortedAttrs.map[attrName] = attrValue;
sortedAttrs.push({name: attrName, value: attrValue});
}
}
attrs = sortedAttrs;
}
}
writer.start(node.name, attrs, isEmpty);
if (!isEmpty) {
if ((node = node.firstChild)) {
do {
walk(node);
} while ((node = node.next));
}
writer.end(name);
}
} else {
handler(node);
}
}
// Serialize element and treat all non elements as fragments
if (node.type == 1 && !settings.inner) {
walk(node);
} else {
handlers[11](node);
}
return writer.getContent();
};
};
});
// Included from: js/tinymce/classes/dom/Serializer.js
/**
* Serializer.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class is used to serialize DOM trees into a string. Consult the TinyMCE Wiki API for
* more details and examples on how to use this class.
*
* @class tinymce.dom.Serializer
*/
define("tinymce/dom/Serializer", [
"tinymce/dom/DOMUtils",
"tinymce/html/DomParser",
"tinymce/html/SaxParser",
"tinymce/html/Entities",
"tinymce/html/Serializer",
"tinymce/html/Node",
"tinymce/html/Schema",
"tinymce/Env",
"tinymce/util/Tools",
"tinymce/text/Zwsp"
], function(DOMUtils, DomParser, SaxParser, Entities, Serializer, Node, Schema, Env, Tools, Zwsp) {
var each = Tools.each, trim = Tools.trim;
var DOM = DOMUtils.DOM;
/**
* IE 11 has a fantastic bug where it will produce two trailing BR elements to iframe bodies when
* the iframe is hidden by display: none on a parent container. The DOM is actually out of sync
* with innerHTML in this case. It's like IE adds shadow DOM BR elements that appears on innerHTML
* but not as the lastChild of the body. So this fix simply removes the last two
* BR elements at the end of the document.
*
* Example of what happens: <body>text</body> becomes <body>text<br><br></body>
*/
function trimTrailingBr(rootNode) {
var brNode1, brNode2;
function isBr(node) {
return node && node.name === 'br';
}
brNode1 = rootNode.lastChild;
if (isBr(brNode1)) {
brNode2 = brNode1.prev;
if (isBr(brNode2)) {
brNode1.remove();
brNode2.remove();
}
}
}
/**
* Constructs a new DOM serializer class.
*
* @constructor
* @method Serializer
* @param {Object} settings Serializer settings object.
* @param {tinymce.Editor} editor Optional editor to bind events to and get schema/dom from.
*/
return function(settings, editor) {
var dom, schema, htmlParser, tempAttrs = ["data-mce-selected"];
if (editor) {
dom = editor.dom;
schema = editor.schema;
}
function trimHtml(html) {
var trimContentRegExp = new RegExp([
'<span[^>]+data-mce-bogus[^>]+>[\u200B\uFEFF]+<\\/span>', // Trim bogus spans like caret containers
'\\s?(' + tempAttrs.join('|') + ')="[^"]+"' // Trim temporaty data-mce prefixed attributes like data-mce-selected
].join('|'), 'gi');
html = Zwsp.trim(html.replace(trimContentRegExp, ''));
return html;
}
function trimContent(html) {
var content = html;
var bogusAllRegExp = /<(\w+) [^>]*data-mce-bogus="all"[^>]*>/g;
var endTagIndex, index, matchLength, matches, shortEndedElements, schema = editor.schema;
content = trimHtml(content);
shortEndedElements = schema.getShortEndedElements();
// Remove all bogus elements marked with "all"
while ((matches = bogusAllRegExp.exec(content))) {
index = bogusAllRegExp.lastIndex;
matchLength = matches[0].length;
if (shortEndedElements[matches[1]]) {
endTagIndex = index;
} else {
endTagIndex = SaxParser.findEndTag(schema, content, index);
}
content = content.substring(0, index - matchLength) + content.substring(endTagIndex);
bogusAllRegExp.lastIndex = index - matchLength;
}
return trim(content);
}
/**
* Returns a trimmed version of the editor contents to be used for the undo level. This
* will remove any data-mce-bogus="all" marked elements since these are used for UI it will also
* remove the data-mce-selected attributes used for selection of objects and caret containers.
* It will keep all data-mce-bogus="1" elements since these can be used to place the caret etc and will
* be removed by the serialization logic when you save.
*
* @private
* @return {String} HTML contents of the editor excluding some internal bogus elements.
*/
function getTrimmedContent() {
return trimContent(editor.getBody().innerHTML);
}
function addTempAttr(name) {
if (Tools.inArray(tempAttrs, name) === -1) {
htmlParser.addAttributeFilter(name, function(nodes, name) {
var i = nodes.length;
while (i--) {
nodes[i].attr(name, null);
}
});
tempAttrs.push(name);
}
}
// Default DOM and Schema if they are undefined
dom = dom || DOM;
schema = schema || new Schema(settings);
settings.entity_encoding = settings.entity_encoding || 'named';
settings.remove_trailing_brs = "remove_trailing_brs" in settings ? settings.remove_trailing_brs : true;
htmlParser = new DomParser(settings, schema);
// Convert tabindex back to elements when serializing contents
htmlParser.addAttributeFilter('data-mce-tabindex', function(nodes, name) {
var i = nodes.length, node;
while (i--) {
node = nodes[i];
node.attr('tabindex', node.attributes.map['data-mce-tabindex']);
node.attr(name, null);
}
});
// Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed
htmlParser.addAttributeFilter('src,href,style', function(nodes, name) {
var i = nodes.length, node, value, internalName = 'data-mce-' + name;
var urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope, undef;
while (i--) {
node = nodes[i];
value = node.attributes.map[internalName];
if (value !== undef) {
// Set external name to internal value and remove internal
node.attr(name, value.length > 0 ? value : null);
node.attr(internalName, null);
} else {
// No internal attribute found then convert the value we have in the DOM
value = node.attributes.map[name];
if (name === "style") {
value = dom.serializeStyle(dom.parseStyle(value), node.name);
} else if (urlConverter) {
value = urlConverter.call(urlConverterScope, value, name, node.name);
}
node.attr(name, value.length > 0 ? value : null);
}
}
});
// Remove internal classes mceItem<..> or mceSelected
htmlParser.addAttributeFilter('class', function(nodes) {
var i = nodes.length, node, value;
while (i--) {
node = nodes[i];
value = node.attr('class');
if (value) {
value = node.attr('class').replace(/(?:^|\s)mce-item-\w+(?!\S)/g, '');
node.attr('class', value.length > 0 ? value : null);
}
}
});
// Remove bookmark elements
htmlParser.addAttributeFilter('data-mce-type', function(nodes, name, args) {
var i = nodes.length, node;
while (i--) {
node = nodes[i];
if (node.attributes.map['data-mce-type'] === 'bookmark' && !args.cleanup) {
node.remove();
}
}
});
htmlParser.addNodeFilter('noscript', function(nodes) {
var i = nodes.length, node;
while (i--) {
node = nodes[i].firstChild;
if (node) {
node.value = Entities.decode(node.value);
}
}
});
// Force script into CDATA sections and remove the mce- prefix also add comments around styles
htmlParser.addNodeFilter('script,style', function(nodes, name) {
var i = nodes.length, node, value, type;
function trim(value) {
/*jshint maxlen:255 */
/*eslint max-len:0 */
return value.replace(/(<!--\[CDATA\[|\]\]-->)/g, '\n')
.replace(/^[\r\n]*|[\r\n]*$/g, '')
.replace(/^\s*((<!--)?(\s*\/\/)?\s*<!\[CDATA\[|(<!--\s*)?\/\*\s*<!\[CDATA\[\s*\*\/|(\/\/)?\s*<!--|\/\*\s*<!--\s*\*\/)\s*[\r\n]*/gi, '')
.replace(/\s*(\/\*\s*\]\]>\s*\*\/(-->)?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g, '');
}
while (i--) {
node = nodes[i];
value = node.firstChild ? node.firstChild.value : '';
if (name === "script") {
// Remove mce- prefix from script elements and remove default type since the user specified
// a script element without type attribute
type = node.attr('type');
if (type) {
node.attr('type', type == 'mce-no/type' ? null : type.replace(/^mce\-/, ''));
}
if (value.length > 0) {
node.firstChild.value = '// <![CDATA[\n' + trim(value) + '\n// ]]>';
}
} else {
if (value.length > 0) {
node.firstChild.value = '<!--\n' + trim(value) + '\n-->';
}
}
}
});
// Convert comments to cdata and handle protected comments
htmlParser.addNodeFilter('#comment', function(nodes) {
var i = nodes.length, node;
while (i--) {
node = nodes[i];
if (node.value.indexOf('[CDATA[') === 0) {
node.name = '#cdata';
node.type = 4;
node.value = node.value.replace(/^\[CDATA\[|\]\]$/g, '');
} else if (node.value.indexOf('mce:protected ') === 0) {
node.name = "#text";
node.type = 3;
node.raw = true;
node.value = unescape(node.value).substr(14);
}
}
});
htmlParser.addNodeFilter('xml:namespace,input', function(nodes, name) {
var i = nodes.length, node;
while (i--) {
node = nodes[i];
if (node.type === 7) {
node.remove();
} else if (node.type === 1) {
if (name === "input" && !("type" in node.attributes.map)) {
node.attr('type', 'text');
}
}
}
});
// Fix list elements, TODO: Replace this later
if (settings.fix_list_elements) {
htmlParser.addNodeFilter('ul,ol', function(nodes) {
var i = nodes.length, node, parentNode;
while (i--) {
node = nodes[i];
parentNode = node.parent;
if (parentNode.name === 'ul' || parentNode.name === 'ol') {
if (node.prev && node.prev.name === 'li') {
node.prev.append(node);
}
}
}
});
}
// Remove internal data attributes
htmlParser.addAttributeFilter(
'data-mce-src,data-mce-href,data-mce-style,' +
'data-mce-selected,data-mce-expando,' +
'data-mce-type,data-mce-resize',
function(nodes, name) {
var i = nodes.length;
while (i--) {
nodes[i].attr(name, null);
}
}
);
// Return public methods
return {
/**
* Schema instance that was used to when the Serializer was constructed.
*
* @field {tinymce.html.Schema} schema
*/
schema: schema,
/**
* Adds a node filter function to the parser used by the serializer, the parser will collect the specified nodes by name
* and then execute the callback ones it has finished parsing the document.
*
* @example
* parser.addNodeFilter('p,h1', function(nodes, name) {
* for (var i = 0; i < nodes.length; i++) {
* console.log(nodes[i].name);
* }
* });
* @method addNodeFilter
* @method {String} name Comma separated list of nodes to collect.
* @param {function} callback Callback function to execute once it has collected nodes.
*/
addNodeFilter: htmlParser.addNodeFilter,
/**
* Adds a attribute filter function to the parser used by the serializer, the parser will
* collect nodes that has the specified attributes
* and then execute the callback ones it has finished parsing the document.
*
* @example
* parser.addAttributeFilter('src,href', function(nodes, name) {
* for (var i = 0; i < nodes.length; i++) {
* console.log(nodes[i].name);
* }
* });
* @method addAttributeFilter
* @method {String} name Comma separated list of nodes to collect.
* @param {function} callback Callback function to execute once it has collected nodes.
*/
addAttributeFilter: htmlParser.addAttributeFilter,
/**
* Serializes the specified browser DOM node into a HTML string.
*
* @method serialize
* @param {DOMNode} node DOM node to serialize.
* @param {Object} args Arguments option that gets passed to event handlers.
*/
serialize: function(node, args) {
var self = this, impl, doc, oldDoc, htmlSerializer, content, rootNode;
// Explorer won't clone contents of script and style and the
// selected index of select elements are cleared on a clone operation.
if (Env.ie && dom.select('script,style,select,map').length > 0) {
content = node.innerHTML;
node = node.cloneNode(false);
dom.setHTML(node, content);
} else {
node = node.cloneNode(true);
}
// Nodes needs to be attached to something in WebKit/Opera
// This fix will make DOM ranges and make Sizzle happy!
impl = document.implementation;
if (impl.createHTMLDocument) {
// Create an empty HTML document
doc = impl.createHTMLDocument("");
// Add the element or it's children if it's a body element to the new document
each(node.nodeName == 'BODY' ? node.childNodes : [node], function(node) {
doc.body.appendChild(doc.importNode(node, true));
});
// Grab first child or body element for serialization
if (node.nodeName != 'BODY') {
node = doc.body.firstChild;
} else {
node = doc.body;
}
// set the new document in DOMUtils so createElement etc works
oldDoc = dom.doc;
dom.doc = doc;
}
args = args || {};
args.format = args.format || 'html';
// Don't wrap content if we want selected html
if (args.selection) {
args.forced_root_block = '';
}
// Pre process
if (!args.no_events) {
args.node = node;
self.onPreProcess(args);
}
// Parse HTML
rootNode = htmlParser.parse(trim(args.getInner ? node.innerHTML : dom.getOuterHTML(node)), args);
trimTrailingBr(rootNode);
// Serialize HTML
htmlSerializer = new Serializer(settings, schema);
args.content = htmlSerializer.serialize(rootNode);
// Replace all BOM characters for now until we can find a better solution
if (!args.cleanup) {
args.content = Zwsp.trim(args.content);
args.content = args.content.replace(/\uFEFF/g, '');
}
// Post process
if (!args.no_events) {
self.onPostProcess(args);
}
// Restore the old document if it was changed
if (oldDoc) {
dom.doc = oldDoc;
}
args.node = null;
return args.content;
},
/**
* Adds valid elements rules to the serializers schema instance this enables you to specify things
* like what elements should be outputted and what attributes specific elements might have.
* Consult the Wiki for more details on this format.
*
* @method addRules
* @param {String} rules Valid elements rules string to add to schema.
*/
addRules: function(rules) {
schema.addValidElements(rules);
},
/**
* Sets the valid elements rules to the serializers schema instance this enables you to specify things
* like what elements should be outputted and what attributes specific elements might have.
* Consult the Wiki for more details on this format.
*
* @method setRules
* @param {String} rules Valid elements rules string.
*/
setRules: function(rules) {
schema.setValidElements(rules);
},
onPreProcess: function(args) {
if (editor) {
editor.fire('PreProcess', args);
}
},
onPostProcess: function(args) {
if (editor) {
editor.fire('PostProcess', args);
}
},
/**
* Adds a temporary internal attribute these attributes will get removed on undo and
* when getting contents out of the editor.
*
* @method addTempAttr
* @param {String} name string
*/
addTempAttr: addTempAttr,
// Internal
trimHtml: trimHtml,
getTrimmedContent: getTrimmedContent,
trimContent: trimContent
};
};
});
// Included from: js/tinymce/classes/dom/TridentSelection.js
/**
* TridentSelection.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Selection class for old explorer versions. This one fakes the
* native selection object available on modern browsers.
*
* @private
* @class tinymce.dom.TridentSelection
*/
define("tinymce/dom/TridentSelection", [], function() {
function Selection(selection) {
var self = this, dom = selection.dom, FALSE = false;
function getPosition(rng, start) {
var checkRng, startIndex = 0, endIndex, inside,
children, child, offset, index, position = -1, parent;
// Setup test range, collapse it and get the parent
checkRng = rng.duplicate();
checkRng.collapse(start);
parent = checkRng.parentElement();
// Check if the selection is within the right document
if (parent.ownerDocument !== selection.dom.doc) {
return;
}
// IE will report non editable elements as it's parent so look for an editable one
while (parent.contentEditable === "false") {
parent = parent.parentNode;
}
// If parent doesn't have any children then return that we are inside the element
if (!parent.hasChildNodes()) {
return {node: parent, inside: 1};
}
// Setup node list and endIndex
children = parent.children;
endIndex = children.length - 1;
// Perform a binary search for the position
while (startIndex <= endIndex) {
index = Math.floor((startIndex + endIndex) / 2);
// Move selection to node and compare the ranges
child = children[index];
checkRng.moveToElementText(child);
position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng);
// Before/after or an exact match
if (position > 0) {
endIndex = index - 1;
} else if (position < 0) {
startIndex = index + 1;
} else {
return {node: child};
}
}
// Check if child position is before or we didn't find a position
if (position < 0) {
// No element child was found use the parent element and the offset inside that
if (!child) {
checkRng.moveToElementText(parent);
checkRng.collapse(true);
child = parent;
inside = true;
} else {
checkRng.collapse(false);
}
// Walk character by character in text node until we hit the selected range endpoint,
// hit the end of document or parent isn't the right one
// We need to walk char by char since rng.text or rng.htmlText will trim line endings
offset = 0;
while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) {
if (checkRng.move('character', 1) === 0 || parent != checkRng.parentElement()) {
break;
}
offset++;
}
} else {
// Child position is after the selection endpoint
checkRng.collapse(true);
// Walk character by character in text node until we hit the selected range endpoint, hit
// the end of document or parent isn't the right one
offset = 0;
while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) {
if (checkRng.move('character', -1) === 0 || parent != checkRng.parentElement()) {
break;
}
offset++;
}
}
return {node: child, position: position, offset: offset, inside: inside};
}
// Returns a W3C DOM compatible range object by using the IE Range API
function getRange() {
var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark;
// If selection is outside the current document just return an empty range
element = ieRange.item ? ieRange.item(0) : ieRange.parentElement();
if (element.ownerDocument != dom.doc) {
return domRange;
}
collapsed = selection.isCollapsed();
// Handle control selection
if (ieRange.item) {
domRange.setStart(element.parentNode, dom.nodeIndex(element));
domRange.setEnd(domRange.startContainer, domRange.startOffset + 1);
return domRange;
}
function findEndPoint(start) {
var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue;
container = endPoint.node;
offset = endPoint.offset;
if (endPoint.inside && !container.hasChildNodes()) {
domRange[start ? 'setStart' : 'setEnd'](container, 0);
return;
}
if (offset === undef) {
domRange[start ? 'setStartBefore' : 'setEndAfter'](container);
return;
}
if (endPoint.position < 0) {
sibling = endPoint.inside ? container.firstChild : container.nextSibling;
if (!sibling) {
domRange[start ? 'setStartAfter' : 'setEndAfter'](container);
return;
}
if (!offset) {
if (sibling.nodeType == 3) {
domRange[start ? 'setStart' : 'setEnd'](sibling, 0);
} else {
domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling);
}
return;
}
// Find the text node and offset
while (sibling) {
if (sibling.nodeType == 3) {
nodeValue = sibling.nodeValue;
textNodeOffset += nodeValue.length;
// We are at or passed the position we where looking for
if (textNodeOffset >= offset) {
container = sibling;
textNodeOffset -= offset;
textNodeOffset = nodeValue.length - textNodeOffset;
break;
}
}
sibling = sibling.nextSibling;
}
} else {
// Find the text node and offset
sibling = container.previousSibling;
if (!sibling) {
return domRange[start ? 'setStartBefore' : 'setEndBefore'](container);
}
// If there isn't any text to loop then use the first position
if (!offset) {
if (container.nodeType == 3) {
domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length);
} else {
domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling);
}
return;
}
while (sibling) {
if (sibling.nodeType == 3) {
textNodeOffset += sibling.nodeValue.length;
// We are at or passed the position we where looking for
if (textNodeOffset >= offset) {
container = sibling;
textNodeOffset -= offset;
break;
}
}
sibling = sibling.previousSibling;
}
}
domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset);
}
try {
// Find start point
findEndPoint(true);
// Find end point if needed
if (!collapsed) {
findEndPoint();
}
} catch (ex) {
// IE has a nasty bug where text nodes might throw "invalid argument" when you
// access the nodeValue or other properties of text nodes. This seems to happen when
// text nodes are split into two nodes by a delete/backspace call.
// So let us detect and try to fix it.
if (ex.number == -2147024809) {
// Get the current selection
bookmark = self.getBookmark(2);
// Get start element
tmpRange = ieRange.duplicate();
tmpRange.collapse(true);
element = tmpRange.parentElement();
// Get end element
if (!collapsed) {
tmpRange = ieRange.duplicate();
tmpRange.collapse(false);
element2 = tmpRange.parentElement();
element2.innerHTML = element2.innerHTML;
}
// Remove the broken elements
element.innerHTML = element.innerHTML;
// Restore the selection
self.moveToBookmark(bookmark);
// Since the range has moved we need to re-get it
ieRange = selection.getRng();
// Find start point
findEndPoint(true);
// Find end point if needed
if (!collapsed) {
findEndPoint();
}
} else {
throw ex; // Throw other errors
}
}
return domRange;
}
this.getBookmark = function(type) {
var rng = selection.getRng(), bookmark = {};
function getIndexes(node) {
var parent, root, children, i, indexes = [];
parent = node.parentNode;
root = dom.getRoot().parentNode;
while (parent != root && parent.nodeType !== 9) {
children = parent.children;
i = children.length;
while (i--) {
if (node === children[i]) {
indexes.push(i);
break;
}
}
node = parent;
parent = parent.parentNode;
}
return indexes;
}
function getBookmarkEndPoint(start) {
var position;
position = getPosition(rng, start);
if (position) {
return {
position: position.position,
offset: position.offset,
indexes: getIndexes(position.node),
inside: position.inside
};
}
}
// Non ubstructive bookmark
if (type === 2) {
// Handle text selection
if (!rng.item) {
bookmark.start = getBookmarkEndPoint(true);
if (!selection.isCollapsed()) {
bookmark.end = getBookmarkEndPoint();
}
} else {
bookmark.start = {ctrl: true, indexes: getIndexes(rng.item(0))};
}
}
return bookmark;
};
this.moveToBookmark = function(bookmark) {
var rng, body = dom.doc.body;
function resolveIndexes(indexes) {
var node, i, idx, children;
node = dom.getRoot();
for (i = indexes.length - 1; i >= 0; i--) {
children = node.children;
idx = indexes[i];
if (idx <= children.length - 1) {
node = children[idx];
}
}
return node;
}
function setBookmarkEndPoint(start) {
var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef, offset;
if (endPoint) {
moveLeft = endPoint.position > 0;
moveRng = body.createTextRange();
moveRng.moveToElementText(resolveIndexes(endPoint.indexes));
offset = endPoint.offset;
if (offset !== undef) {
moveRng.collapse(endPoint.inside || moveLeft);
moveRng.moveStart('character', moveLeft ? -offset : offset);
} else {
moveRng.collapse(start);
}
rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng);
if (start) {
rng.collapse(true);
}
}
}
if (bookmark.start) {
if (bookmark.start.ctrl) {
rng = body.createControlRange();
rng.addElement(resolveIndexes(bookmark.start.indexes));
rng.select();
} else {
rng = body.createTextRange();
setBookmarkEndPoint(true);
setBookmarkEndPoint();
rng.select();
}
}
};
this.addRange = function(rng) {
var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, sibling,
doc = selection.dom.doc, body = doc.body, nativeRng, ctrlElm;
function setEndPoint(start) {
var container, offset, marker, tmpRng, nodes;
marker = dom.create('a');
container = start ? startContainer : endContainer;
offset = start ? startOffset : endOffset;
tmpRng = ieRng.duplicate();
if (container == doc || container == doc.documentElement) {
container = body;
offset = 0;
}
if (container.nodeType == 3) {
container.parentNode.insertBefore(marker, container);
tmpRng.moveToElementText(marker);
tmpRng.moveStart('character', offset);
dom.remove(marker);
ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
} else {
nodes = container.childNodes;
if (nodes.length) {
if (offset >= nodes.length) {
dom.insertAfter(marker, nodes[nodes.length - 1]);
} else {
container.insertBefore(marker, nodes[offset]);
}
tmpRng.moveToElementText(marker);
} else if (container.canHaveHTML) {
// Empty node selection for example <div>|</div>
// Setting innerHTML with a span marker then remove that marker seems to keep empty block elements open
container.innerHTML = '<span>&#xFEFF;</span>';
marker = container.firstChild;
tmpRng.moveToElementText(marker);
tmpRng.collapse(FALSE); // Collapse false works better than true for some odd reason
}
ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
dom.remove(marker);
}
}
// Setup some shorter versions
startContainer = rng.startContainer;
startOffset = rng.startOffset;
endContainer = rng.endContainer;
endOffset = rng.endOffset;
ieRng = body.createTextRange();
// If single element selection then try making a control selection out of it
if (startContainer == endContainer && startContainer.nodeType == 1) {
// Trick to place the caret inside an empty block element like <p></p>
if (startOffset == endOffset && !startContainer.hasChildNodes()) {
if (startContainer.canHaveHTML) {
// Check if previous sibling is an empty block if it is then we need to render it
// IE would otherwise move the caret into the sibling instead of the empty startContainer see: #5236
// Example this: <p></p><p>|</p> would become this: <p>|</p><p></p>
sibling = startContainer.previousSibling;
if (sibling && !sibling.hasChildNodes() && dom.isBlock(sibling)) {
sibling.innerHTML = '&#xFEFF;';
} else {
sibling = null;
}
startContainer.innerHTML = '<span>&#xFEFF;</span><span>&#xFEFF;</span>';
ieRng.moveToElementText(startContainer.lastChild);
ieRng.select();
dom.doc.selection.clear();
startContainer.innerHTML = '';
if (sibling) {
sibling.innerHTML = '';
}
return;
}
startOffset = dom.nodeIndex(startContainer);
startContainer = startContainer.parentNode;
}
if (startOffset == endOffset - 1) {
try {
ctrlElm = startContainer.childNodes[startOffset];
ctrlRng = body.createControlRange();
ctrlRng.addElement(ctrlElm);
ctrlRng.select();
// Check if the range produced is on the correct element and is a control range
// On IE 8 it will select the parent contentEditable container if you select an inner element see: #5398
nativeRng = selection.getRng();
if (nativeRng.item && ctrlElm === nativeRng.item(0)) {
return;
}
} catch (ex) {
// Ignore
}
}
}
// Set start/end point of selection
setEndPoint(true);
setEndPoint();
// Select the new range and scroll it into view
ieRng.select();
};
// Expose range method
this.getRangeAt = getRange;
}
return Selection;
});
// Included from: js/tinymce/classes/util/VK.js
/**
* VK.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This file exposes a set of the common KeyCodes for use. Please grow it as needed.
*/
define("tinymce/util/VK", [
"tinymce/Env"
], function(Env) {
return {
BACKSPACE: 8,
DELETE: 46,
DOWN: 40,
ENTER: 13,
LEFT: 37,
RIGHT: 39,
SPACEBAR: 32,
TAB: 9,
UP: 38,
modifierPressed: function(e) {
return e.shiftKey || e.ctrlKey || e.altKey || this.metaKeyPressed(e);
},
metaKeyPressed: function(e) {
// Check if ctrl or meta key is pressed. Edge case for AltGr on Windows where it produces ctrlKey+altKey states
return (Env.mac ? e.metaKey : e.ctrlKey && !e.altKey);
}
};
});
// Included from: js/tinymce/classes/dom/ControlSelection.js
/**
* ControlSelection.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class handles control selection of elements. Controls are elements
* that can be resized and needs to be selected as a whole. It adds custom resize handles
* to all browser engines that support properly disabling the built in resize logic.
*
* @class tinymce.dom.ControlSelection
*/
define("tinymce/dom/ControlSelection", [
"tinymce/util/VK",
"tinymce/util/Tools",
"tinymce/util/Delay",
"tinymce/Env",
"tinymce/dom/NodeType"
], function(VK, Tools, Delay, Env, NodeType) {
var isContentEditableFalse = NodeType.isContentEditableFalse;
var isContentEditableTrue = NodeType.isContentEditableTrue;
function getContentEditableRoot(root, node) {
while (node && node != root) {
if (isContentEditableTrue(node) || isContentEditableFalse(node)) {
return node;
}
node = node.parentNode;
}
return null;
}
return function(selection, editor) {
var dom = editor.dom, each = Tools.each;
var selectedElm, selectedElmGhost, resizeHelper, resizeHandles, selectedHandle, lastMouseDownEvent;
var startX, startY, selectedElmX, selectedElmY, startW, startH, ratio, resizeStarted;
var width, height, editableDoc = editor.getDoc(), rootDocument = document, isIE = Env.ie && Env.ie < 11;
var abs = Math.abs, round = Math.round, rootElement = editor.getBody(), startScrollWidth, startScrollHeight;
// Details about each resize handle how to scale etc
resizeHandles = {
// Name: x multiplier, y multiplier, delta size x, delta size y
/*n: [0.5, 0, 0, -1],
e: [1, 0.5, 1, 0],
s: [0.5, 1, 0, 1],
w: [0, 0.5, -1, 0],*/
nw: [0, 0, -1, -1],
ne: [1, 0, 1, -1],
se: [1, 1, 1, 1],
sw: [0, 1, -1, 1]
};
// Add CSS for resize handles, cloned element and selected
var rootClass = '.mce-content-body';
editor.contentStyles.push(
rootClass + ' div.mce-resizehandle {' +
'position: absolute;' +
'border: 1px solid black;' +
'box-sizing: box-sizing;' +
'background: #FFF;' +
'width: 7px;' +
'height: 7px;' +
'z-index: 10000' +
'}' +
rootClass + ' .mce-resizehandle:hover {' +
'background: #000' +
'}' +
rootClass + ' img[data-mce-selected],' + rootClass + ' hr[data-mce-selected] {' +
'outline: 1px solid black;' +
'resize: none' + // Have been talks about implementing this in browsers
'}' +
rootClass + ' .mce-clonedresizable {' +
'position: absolute;' +
(Env.gecko ? '' : 'outline: 1px dashed black;') + // Gecko produces trails while resizing
'opacity: .5;' +
'filter: alpha(opacity=50);' +
'z-index: 10000' +
'}' +
rootClass + ' .mce-resize-helper {' +
'background: #555;' +
'background: rgba(0,0,0,0.75);' +
'border-radius: 3px;' +
'border: 1px;' +
'color: white;' +
'display: none;' +
'font-family: sans-serif;' +
'font-size: 12px;' +
'white-space: nowrap;' +
'line-height: 14px;' +
'margin: 5px 10px;' +
'padding: 5px;' +
'position: absolute;' +
'z-index: 10001' +
'}'
);
function isResizable(elm) {
var selector = editor.settings.object_resizing;
if (selector === false || Env.iOS) {
return false;
}
if (typeof selector != 'string') {
selector = 'table,img,div';
}
if (elm.getAttribute('data-mce-resize') === 'false') {
return false;
}
if (elm == editor.getBody()) {
return false;
}
return editor.dom.is(elm, selector);
}
function resizeGhostElement(e) {
var deltaX, deltaY, proportional;
var resizeHelperX, resizeHelperY;
// Calc new width/height
deltaX = e.screenX - startX;
deltaY = e.screenY - startY;
// Calc new size
width = deltaX * selectedHandle[2] + startW;
height = deltaY * selectedHandle[3] + startH;
// Never scale down lower than 5 pixels
width = width < 5 ? 5 : width;
height = height < 5 ? 5 : height;
if (selectedElm.nodeName == "IMG" && editor.settings.resize_img_proportional !== false) {
proportional = !VK.modifierPressed(e);
} else {
proportional = VK.modifierPressed(e) || (selectedElm.nodeName == "IMG" && selectedHandle[2] * selectedHandle[3] !== 0);
}
// Constrain proportions
if (proportional) {
if (abs(deltaX) > abs(deltaY)) {
height = round(width * ratio);
width = round(height / ratio);
} else {
width = round(height / ratio);
height = round(width * ratio);
}
}
// Update ghost size
dom.setStyles(selectedElmGhost, {
width: width,
height: height
});
// Update resize helper position
resizeHelperX = selectedHandle.startPos.x + deltaX;
resizeHelperY = selectedHandle.startPos.y + deltaY;
resizeHelperX = resizeHelperX > 0 ? resizeHelperX : 0;
resizeHelperY = resizeHelperY > 0 ? resizeHelperY : 0;
dom.setStyles(resizeHelper, {
left: resizeHelperX,
top: resizeHelperY,
display: 'block'
});
resizeHelper.innerHTML = width + ' &times; ' + height;
// Update ghost X position if needed
if (selectedHandle[2] < 0 && selectedElmGhost.clientWidth <= width) {
dom.setStyle(selectedElmGhost, 'left', selectedElmX + (startW - width));
}
// Update ghost Y position if needed
if (selectedHandle[3] < 0 && selectedElmGhost.clientHeight <= height) {
dom.setStyle(selectedElmGhost, 'top', selectedElmY + (startH - height));
}
// Calculate how must overflow we got
deltaX = rootElement.scrollWidth - startScrollWidth;
deltaY = rootElement.scrollHeight - startScrollHeight;
// Re-position the resize helper based on the overflow
if (deltaX + deltaY !== 0) {
dom.setStyles(resizeHelper, {
left: resizeHelperX - deltaX,
top: resizeHelperY - deltaY
});
}
if (!resizeStarted) {
editor.fire('ObjectResizeStart', {target: selectedElm, width: startW, height: startH});
resizeStarted = true;
}
}
function endGhostResize() {
resizeStarted = false;
function setSizeProp(name, value) {
if (value) {
// Resize by using style or attribute
if (selectedElm.style[name] || !editor.schema.isValid(selectedElm.nodeName.toLowerCase(), name)) {
dom.setStyle(selectedElm, name, value);
} else {
dom.setAttrib(selectedElm, name, value);
}
}
}
// Set width/height properties
setSizeProp('width', width);
setSizeProp('height', height);
dom.unbind(editableDoc, 'mousemove', resizeGhostElement);
dom.unbind(editableDoc, 'mouseup', endGhostResize);
if (rootDocument != editableDoc) {
dom.unbind(rootDocument, 'mousemove', resizeGhostElement);
dom.unbind(rootDocument, 'mouseup', endGhostResize);
}
// Remove ghost/helper and update resize handle positions
dom.remove(selectedElmGhost);
dom.remove(resizeHelper);
if (!isIE || selectedElm.nodeName == "TABLE") {
showResizeRect(selectedElm);
}
editor.fire('ObjectResized', {target: selectedElm, width: width, height: height});
dom.setAttrib(selectedElm, 'style', dom.getAttrib(selectedElm, 'style'));
editor.nodeChanged();
}
function showResizeRect(targetElm, mouseDownHandleName, mouseDownEvent) {
var position, targetWidth, targetHeight, e, rect;
hideResizeRect();
unbindResizeHandleEvents();
// Get position and size of target
position = dom.getPos(targetElm, rootElement);
selectedElmX = position.x;
selectedElmY = position.y;
rect = targetElm.getBoundingClientRect(); // Fix for Gecko offsetHeight for table with caption
targetWidth = rect.width || (rect.right - rect.left);
targetHeight = rect.height || (rect.bottom - rect.top);
// Reset width/height if user selects a new image/table
if (selectedElm != targetElm) {
detachResizeStartListener();
selectedElm = targetElm;
width = height = 0;
}
// Makes it possible to disable resizing
e = editor.fire('ObjectSelected', {target: targetElm});
if (isResizable(targetElm) && !e.isDefaultPrevented()) {
each(resizeHandles, function(handle, name) {
var handleElm;
function startDrag(e) {
startX = e.screenX;
startY = e.screenY;
startW = selectedElm.clientWidth;
startH = selectedElm.clientHeight;
ratio = startH / startW;
selectedHandle = handle;
handle.startPos = {
x: targetWidth * handle[0] + selectedElmX,
y: targetHeight * handle[1] + selectedElmY
};
startScrollWidth = rootElement.scrollWidth;
startScrollHeight = rootElement.scrollHeight;
selectedElmGhost = selectedElm.cloneNode(true);
dom.addClass(selectedElmGhost, 'mce-clonedresizable');
dom.setAttrib(selectedElmGhost, 'data-mce-bogus', 'all');
selectedElmGhost.contentEditable = false; // Hides IE move layer cursor
selectedElmGhost.unSelectabe = true;
dom.setStyles(selectedElmGhost, {
left: selectedElmX,
top: selectedElmY,
margin: 0
});
selectedElmGhost.removeAttribute('data-mce-selected');
rootElement.appendChild(selectedElmGhost);
dom.bind(editableDoc, 'mousemove', resizeGhostElement);
dom.bind(editableDoc, 'mouseup', endGhostResize);
if (rootDocument != editableDoc) {
dom.bind(rootDocument, 'mousemove', resizeGhostElement);
dom.bind(rootDocument, 'mouseup', endGhostResize);
}
resizeHelper = dom.add(rootElement, 'div', {
'class': 'mce-resize-helper',
'data-mce-bogus': 'all'
}, startW + ' &times; ' + startH);
}
if (mouseDownHandleName) {
// Drag started by IE native resizestart
if (name == mouseDownHandleName) {
startDrag(mouseDownEvent);
}
return;
}
// Get existing or render resize handle
handleElm = dom.get('mceResizeHandle' + name);
if (handleElm) {
dom.remove(handleElm);
}
handleElm = dom.add(rootElement, 'div', {
id: 'mceResizeHandle' + name,
'data-mce-bogus': 'all',
'class': 'mce-resizehandle',
unselectable: true,
style: 'cursor:' + name + '-resize; margin:0; padding:0'
});
// Hides IE move layer cursor
// If we set it on Chrome we get this wounderful bug: #6725
if (Env.ie) {
handleElm.contentEditable = false;
}
dom.bind(handleElm, 'mousedown', function(e) {
e.stopImmediatePropagation();
e.preventDefault();
startDrag(e);
});
handle.elm = handleElm;
// Position element
dom.setStyles(handleElm, {
left: (targetWidth * handle[0] + selectedElmX) - (handleElm.offsetWidth / 2),
top: (targetHeight * handle[1] + selectedElmY) - (handleElm.offsetHeight / 2)
});
});
} else {
hideResizeRect();
}
selectedElm.setAttribute('data-mce-selected', '1');
}
function hideResizeRect() {
var name, handleElm;
unbindResizeHandleEvents();
if (selectedElm) {
selectedElm.removeAttribute('data-mce-selected');
}
for (name in resizeHandles) {
handleElm = dom.get('mceResizeHandle' + name);
if (handleElm) {
dom.unbind(handleElm);
dom.remove(handleElm);
}
}
}
function updateResizeRect(e) {
var startElm, controlElm;
function isChildOrEqual(node, parent) {
if (node) {
do {
if (node === parent) {
return true;
}
} while ((node = node.parentNode));
}
}
// Ignore all events while resizing or if the editor instance was removed
if (resizeStarted || editor.removed) {
return;
}
// Remove data-mce-selected from all elements since they might have been copied using Ctrl+c/v
each(dom.select('img[data-mce-selected],hr[data-mce-selected]'), function(img) {
img.removeAttribute('data-mce-selected');
});
controlElm = e.type == 'mousedown' ? e.target : selection.getNode();
controlElm = dom.$(controlElm).closest(isIE ? 'table' : 'table,img,hr')[0];
if (isChildOrEqual(controlElm, rootElement)) {
disableGeckoResize();
startElm = selection.getStart(true);
if (isChildOrEqual(startElm, controlElm) && isChildOrEqual(selection.getEnd(true), controlElm)) {
if (!isIE || (controlElm != startElm && startElm.nodeName !== 'IMG')) {
showResizeRect(controlElm);
return;
}
}
}
hideResizeRect();
}
function attachEvent(elm, name, func) {
if (elm && elm.attachEvent) {
elm.attachEvent('on' + name, func);
}
}
function detachEvent(elm, name, func) {
if (elm && elm.detachEvent) {
elm.detachEvent('on' + name, func);
}
}
function resizeNativeStart(e) {
var target = e.srcElement, pos, name, corner, cornerX, cornerY, relativeX, relativeY;
pos = target.getBoundingClientRect();
relativeX = lastMouseDownEvent.clientX - pos.left;
relativeY = lastMouseDownEvent.clientY - pos.top;
// Figure out what corner we are draging on
for (name in resizeHandles) {
corner = resizeHandles[name];
cornerX = target.offsetWidth * corner[0];
cornerY = target.offsetHeight * corner[1];
if (abs(cornerX - relativeX) < 8 && abs(cornerY - relativeY) < 8) {
selectedHandle = corner;
break;
}
}
// Remove native selection and let the magic begin
resizeStarted = true;
editor.fire('ObjectResizeStart', {
target: selectedElm,
width: selectedElm.clientWidth,
height: selectedElm.clientHeight
});
editor.getDoc().selection.empty();
showResizeRect(target, name, lastMouseDownEvent);
}
function preventDefault(e) {
if (e.preventDefault) {
e.preventDefault();
} else {
e.returnValue = false; // IE
}
}
function isWithinContentEditableFalse(elm) {
return isContentEditableFalse(getContentEditableRoot(editor.getBody(), elm));
}
function nativeControlSelect(e) {
var target = e.srcElement;
if (isWithinContentEditableFalse(target)) {
preventDefault(e);
return;
}
if (target != selectedElm) {
editor.fire('ObjectSelected', {target: target});
detachResizeStartListener();
if (target.id.indexOf('mceResizeHandle') === 0) {
e.returnValue = false;
return;
}
if (target.nodeName == 'IMG' || target.nodeName == 'TABLE') {
hideResizeRect();
selectedElm = target;
attachEvent(target, 'resizestart', resizeNativeStart);
}
}
}
function detachResizeStartListener() {
detachEvent(selectedElm, 'resizestart', resizeNativeStart);
}
function unbindResizeHandleEvents() {
for (var name in resizeHandles) {
var handle = resizeHandles[name];
if (handle.elm) {
dom.unbind(handle.elm);
delete handle.elm;
}
}
}
function disableGeckoResize() {
try {
// Disable object resizing on Gecko
editor.getDoc().execCommand('enableObjectResizing', false, false);
} catch (ex) {
// Ignore
}
}
function controlSelect(elm) {
var ctrlRng;
if (!isIE) {
return;
}
ctrlRng = editableDoc.body.createControlRange();
try {
ctrlRng.addElement(elm);
ctrlRng.select();
return true;
} catch (ex) {
// Ignore since the element can't be control selected for example a P tag
}
}
editor.on('init', function() {
if (isIE) {
// Hide the resize rect on resize and reselect the image
editor.on('ObjectResized', function(e) {
if (e.target.nodeName != 'TABLE') {
hideResizeRect();
controlSelect(e.target);
}
});
attachEvent(rootElement, 'controlselect', nativeControlSelect);
editor.on('mousedown', function(e) {
lastMouseDownEvent = e;
});
} else {
disableGeckoResize();
// Sniff sniff, hard to feature detect this stuff
if (Env.ie >= 11) {
// Needs to be mousedown for drag/drop to work on IE 11
// Needs to be click on Edge to properly select images
editor.on('mousedown click', function(e) {
var target = e.target, nodeName = target.nodeName;
if (!resizeStarted && /^(TABLE|IMG|HR)$/.test(nodeName) && !isWithinContentEditableFalse(target)) {
editor.selection.select(target, nodeName == 'TABLE');
// Only fire once since nodeChange is expensive
if (e.type == 'mousedown') {
editor.nodeChanged();
}
}
});
editor.dom.bind(rootElement, 'mscontrolselect', function(e) {
function delayedSelect(node) {
Delay.setEditorTimeout(editor, function() {
editor.selection.select(node);
});
}
if (isWithinContentEditableFalse(e.target)) {
e.preventDefault();
delayedSelect(e.target);
return;
}
if (/^(TABLE|IMG|HR)$/.test(e.target.nodeName)) {
e.preventDefault();
// This moves the selection from being a control selection to a text like selection like in WebKit #6753
// TODO: Fix this the day IE works like other browsers without this nasty native ugly control selections.
if (e.target.tagName == 'IMG') {
delayedSelect(e.target);
}
}
});
}
}
var throttledUpdateResizeRect = Delay.throttle(function(e) {
if (!editor.composing) {
updateResizeRect(e);
}
});
editor.on('nodechange ResizeEditor ResizeWindow drop', throttledUpdateResizeRect);
// Update resize rect while typing in a table
editor.on('keyup compositionend', function(e) {
// Don't update the resize rect while composing since it blows away the IME see: #2710
if (selectedElm && selectedElm.nodeName == "TABLE") {
throttledUpdateResizeRect(e);
}
});
editor.on('hide blur', hideResizeRect);
// Hide rect on focusout since it would float on top of windows otherwise
//editor.on('focusout', hideResizeRect);
});
editor.on('remove', unbindResizeHandleEvents);
function destroy() {
selectedElm = selectedElmGhost = null;
if (isIE) {
detachResizeStartListener();
detachEvent(rootElement, 'controlselect', nativeControlSelect);
}
}
return {
isResizable: isResizable,
showResizeRect: showResizeRect,
hideResizeRect: hideResizeRect,
updateResizeRect: updateResizeRect,
controlSelect: controlSelect,
destroy: destroy
};
};
});
// Included from: js/tinymce/classes/util/Fun.js
/**
* Fun.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Functional utility class.
*
* @private
* @class tinymce.util.Fun
*/
define("tinymce/util/Fun", [], function() {
var slice = [].slice;
function constant(value) {
return function() {
return value;
};
}
function negate(predicate) {
return function(x) {
return !predicate(x);
};
}
function compose(f, g) {
return function(x) {
return f(g(x));
};
}
function or() {
var args = slice.call(arguments);
return function(x) {
for (var i = 0; i < args.length; i++) {
if (args[i](x)) {
return true;
}
}
return false;
};
}
function and() {
var args = slice.call(arguments);
return function(x) {
for (var i = 0; i < args.length; i++) {
if (!args[i](x)) {
return false;
}
}
return true;
};
}
function curry(fn) {
var args = slice.call(arguments);
if (args.length - 1 >= fn.length) {
return fn.apply(this, args.slice(1));
}
return function() {
var tempArgs = args.concat([].slice.call(arguments));
return curry.apply(this, tempArgs);
};
}
function noop() {
}
return {
constant: constant,
negate: negate,
and: and,
or: or,
curry: curry,
compose: compose,
noop: noop
};
});
// Included from: js/tinymce/classes/caret/CaretCandidate.js
/**
* CaretCandidate.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This module contains logic for handling caret candidates. A caret candidate is
* for example text nodes, images, input elements, cE=false elements etc.
*
* @private
* @class tinymce.caret.CaretCandidate
*/
define("tinymce/caret/CaretCandidate", [
"tinymce/dom/NodeType",
"tinymce/util/Arr",
"tinymce/caret/CaretContainer"
], function(NodeType, Arr, CaretContainer) {
var isContentEditableTrue = NodeType.isContentEditableTrue,
isContentEditableFalse = NodeType.isContentEditableFalse,
isBr = NodeType.isBr,
isText = NodeType.isText,
isInvalidTextElement = NodeType.matchNodeNames('script style textarea'),
isAtomicInline = NodeType.matchNodeNames('img input textarea hr iframe video audio object'),
isTable = NodeType.matchNodeNames('table'),
isCaretContainer = CaretContainer.isCaretContainer;
function isCaretCandidate(node) {
if (isCaretContainer(node)) {
return false;
}
if (isText(node)) {
if (isInvalidTextElement(node.parentNode)) {
return false;
}
return true;
}
return isAtomicInline(node) || isBr(node) || isTable(node) || isContentEditableFalse(node);
}
function isInEditable(node, rootNode) {
for (node = node.parentNode; node && node != rootNode; node = node.parentNode) {
if (isContentEditableFalse(node)) {
return false;
}
if (isContentEditableTrue(node)) {
return true;
}
}
return true;
}
function isAtomicContentEditableFalse(node) {
if (!isContentEditableFalse(node)) {
return false;
}
return Arr.reduce(node.getElementsByTagName('*'), function(result, elm) {
return result || isContentEditableTrue(elm);
}, false) !== true;
}
function isAtomic(node) {
return isAtomicInline(node) || isAtomicContentEditableFalse(node);
}
function isEditableCaretCandidate(node, rootNode) {
return isCaretCandidate(node) && isInEditable(node, rootNode);
}
return {
isCaretCandidate: isCaretCandidate,
isInEditable: isInEditable,
isAtomic: isAtomic,
isEditableCaretCandidate: isEditableCaretCandidate
};
});
// Included from: js/tinymce/classes/geom/ClientRect.js
/**
* ClientRect.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Utility functions for working with client rects.
*
* @private
* @class tinymce.geom.ClientRect
*/
define("tinymce/geom/ClientRect", [], function() {
var round = Math.round;
function clone(rect) {
if (!rect) {
return {left: 0, top: 0, bottom: 0, right: 0, width: 0, height: 0};
}
return {
left: round(rect.left),
top: round(rect.top),
bottom: round(rect.bottom),
right: round(rect.right),
width: round(rect.width),
height: round(rect.height)
};
}
function collapse(clientRect, toStart) {
clientRect = clone(clientRect);
if (toStart) {
clientRect.right = clientRect.left;
} else {
clientRect.left = clientRect.left + clientRect.width;
clientRect.right = clientRect.left;
}
clientRect.width = 0;
return clientRect;
}
function isEqual(rect1, rect2) {
return (
rect1.left === rect2.left &&
rect1.top === rect2.top &&
rect1.bottom === rect2.bottom &&
rect1.right === rect2.right
);
}
function isValidOverflow(overflowY, clientRect1, clientRect2) {
return overflowY >= 0 && overflowY <= Math.min(clientRect1.height, clientRect2.height) / 2;
}
function isAbove(clientRect1, clientRect2) {
if (clientRect1.bottom < clientRect2.top) {
return true;
}
if (clientRect1.top > clientRect2.bottom) {
return false;
}
return isValidOverflow(clientRect2.top - clientRect1.bottom, clientRect1, clientRect2);
}
function isBelow(clientRect1, clientRect2) {
if (clientRect1.top > clientRect2.bottom) {
return true;
}
if (clientRect1.bottom < clientRect2.top) {
return false;
}
return isValidOverflow(clientRect2.bottom - clientRect1.top, clientRect1, clientRect2);
}
function isLeft(clientRect1, clientRect2) {
return clientRect1.left < clientRect2.left;
}
function isRight(clientRect1, clientRect2) {
return clientRect1.right > clientRect2.right;
}
function compare(clientRect1, clientRect2) {
if (isAbove(clientRect1, clientRect2)) {
return -1;
}
if (isBelow(clientRect1, clientRect2)) {
return 1;
}
if (isLeft(clientRect1, clientRect2)) {
return -1;
}
if (isRight(clientRect1, clientRect2)) {
return 1;
}
return 0;
}
function containsXY(clientRect, clientX, clientY) {
return (
clientX >= clientRect.left &&
clientX <= clientRect.right &&
clientY >= clientRect.top &&
clientY <= clientRect.bottom
);
}
return {
clone: clone,
collapse: collapse,
isEqual: isEqual,
isAbove: isAbove,
isBelow: isBelow,
isLeft: isLeft,
isRight: isRight,
compare: compare,
containsXY: containsXY
};
});
// Included from: js/tinymce/classes/text/ExtendingChar.js
/**
* ExtendingChar.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class contains logic for detecting extending characters.
*
* @private
* @class tinymce.text.ExtendingChar
* @example
* var isExtending = ExtendingChar.isExtendingChar('a');
*/
define("tinymce/text/ExtendingChar", [], function() {
// Generated from: http://www.unicode.org/Public/UNIDATA/DerivedCoreProperties.txt
// Only includes the characters in that fit into UCS-2 16 bit
var extendingChars = new RegExp(
"[\u0300-\u036F\u0483-\u0487\u0488-\u0489\u0591-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u061A" +
"\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0" +
"\u07EB-\u07F3\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08E3-\u0902\u093A\u093C" +
"\u0941-\u0948\u094D\u0951-\u0957\u0962-\u0963\u0981\u09BC\u09BE\u09C1-\u09C4\u09CD\u09D7\u09E2-\u09E3" +
"\u0A01-\u0A02\u0A3C\u0A41-\u0A42\u0A47-\u0A48\u0A4B-\u0A4D\u0A51\u0A70-\u0A71\u0A75\u0A81-\u0A82\u0ABC" +
"\u0AC1-\u0AC5\u0AC7-\u0AC8\u0ACD\u0AE2-\u0AE3\u0B01\u0B3C\u0B3E\u0B3F\u0B41-\u0B44\u0B4D\u0B56\u0B57" +
"\u0B62-\u0B63\u0B82\u0BBE\u0BC0\u0BCD\u0BD7\u0C00\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55-\u0C56" +
"\u0C62-\u0C63\u0C81\u0CBC\u0CBF\u0CC2\u0CC6\u0CCC-\u0CCD\u0CD5-\u0CD6\u0CE2-\u0CE3\u0D01\u0D3E\u0D41-\u0D44" +
"\u0D4D\u0D57\u0D62-\u0D63\u0DCA\u0DCF\u0DD2-\u0DD4\u0DD6\u0DDF\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9" +
"\u0EBB-\u0EBC\u0EC8-\u0ECD\u0F18-\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86-\u0F87\u0F8D-\u0F97" +
"\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039-\u103A\u103D-\u103E\u1058-\u1059\u105E-\u1060\u1071-\u1074" +
"\u1082\u1085-\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17B4-\u17B5" +
"\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DD\u180B-\u180D\u18A9\u1920-\u1922\u1927-\u1928\u1932\u1939-\u193B\u1A17-\u1A18" +
"\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1ABD\u1ABE\u1B00-\u1B03\u1B34" +
"\u1B36-\u1B3A\u1B3C\u1B42\u1B6B-\u1B73\u1B80-\u1B81\u1BA2-\u1BA5\u1BA8-\u1BA9\u1BAB-\u1BAD\u1BE6\u1BE8-\u1BE9" +
"\u1BED\u1BEF-\u1BF1\u1C2C-\u1C33\u1C36-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8-\u1CF9" +
"\u1DC0-\u1DF5\u1DFC-\u1DFF\u200C-\u200D\u20D0-\u20DC\u20DD-\u20E0\u20E1\u20E2-\u20E4\u20E5-\u20F0\u2CEF-\u2CF1" +
"\u2D7F\u2DE0-\u2DFF\u302A-\u302D\u302E-\u302F\u3099-\u309A\uA66F\uA670-\uA672\uA674-\uA67D\uA69E-\uA69F\uA6F0-\uA6F1" +
"\uA802\uA806\uA80B\uA825-\uA826\uA8C4\uA8E0-\uA8F1\uA926-\uA92D\uA947-\uA951\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC" +
"\uA9E5\uAA29-\uAA2E\uAA31-\uAA32\uAA35-\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7-\uAAB8\uAABE-\uAABF\uAAC1" +
"\uAAEC-\uAAED\uAAF6\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFF9E-\uFF9F]"
);
function isExtendingChar(ch) {
return typeof ch == "string" && ch.charCodeAt(0) >= 768 && extendingChars.test(ch);
}
return {
isExtendingChar: isExtendingChar
};
});
// Included from: js/tinymce/classes/caret/CaretPosition.js
/**
* CaretPosition.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This module contains logic for creating caret positions within a document a caretposition
* is similar to a DOMRange object but it doesn't have two endpoints and is also more lightweight
* since it's now updated live when the DOM changes.
*
* @private
* @class tinymce.caret.CaretPosition
* @example
* var caretPos1 = new CaretPosition(container, offset);
* var caretPos2 = CaretPosition.fromRangeStart(someRange);
*/
define("tinymce/caret/CaretPosition", [
"tinymce/util/Fun",
"tinymce/dom/NodeType",
"tinymce/dom/DOMUtils",
"tinymce/dom/RangeUtils",
"tinymce/caret/CaretCandidate",
"tinymce/geom/ClientRect",
"tinymce/text/ExtendingChar"
], function(Fun, NodeType, DOMUtils, RangeUtils, CaretCandidate, ClientRect, ExtendingChar) {
var isElement = NodeType.isElement,
isCaretCandidate = CaretCandidate.isCaretCandidate,
isBlock = NodeType.matchStyleValues('display', 'block table'),
isFloated = NodeType.matchStyleValues('float', 'left right'),
isValidElementCaretCandidate = Fun.and(isElement, isCaretCandidate, Fun.negate(isFloated)),
isNotPre = Fun.negate(NodeType.matchStyleValues('white-space', 'pre pre-line pre-wrap')),
isText = NodeType.isText,
isBr = NodeType.isBr,
nodeIndex = DOMUtils.nodeIndex,
resolveIndex = RangeUtils.getNode;
function createRange(doc) {
return "createRange" in doc ? doc.createRange() : DOMUtils.DOM.createRng();
}
function isWhiteSpace(chr) {
return chr && /[\r\n\t ]/.test(chr);
}
function isHiddenWhiteSpaceRange(range) {
var container = range.startContainer,
offset = range.startOffset,
text;
if (isWhiteSpace(range.toString()) && isNotPre(container.parentNode)) {
text = container.data;
if (isWhiteSpace(text[offset - 1]) || isWhiteSpace(text[offset + 1])) {
return true;
}
}
return false;
}
function getCaretPositionClientRects(caretPosition) {
var clientRects = [], beforeNode, node;
// Hack for older WebKit versions that doesn't
// support getBoundingClientRect on BR elements
function getBrClientRect(brNode) {
var doc = brNode.ownerDocument,
rng = createRange(doc),
nbsp = doc.createTextNode('\u00a0'),
parentNode = brNode.parentNode,
clientRect;
parentNode.insertBefore(nbsp, brNode);
rng.setStart(nbsp, 0);
rng.setEnd(nbsp, 1);
clientRect = ClientRect.clone(rng.getBoundingClientRect());
parentNode.removeChild(nbsp);
return clientRect;
}
function getBoundingClientRect(item) {
var clientRect, clientRects;
clientRects = item.getClientRects();
if (clientRects.length > 0) {
clientRect = ClientRect.clone(clientRects[0]);
} else {
clientRect = ClientRect.clone(item.getBoundingClientRect());
}
if (isBr(item) && clientRect.left === 0) {
return getBrClientRect(item);
}
return clientRect;
}
function collapseAndInflateWidth(clientRect, toStart) {
clientRect = ClientRect.collapse(clientRect, toStart);
clientRect.width = 1;
clientRect.right = clientRect.left + 1;
return clientRect;
}
function addUniqueAndValidRect(clientRect) {
if (clientRect.height === 0) {
return;
}
if (clientRects.length > 0) {
if (ClientRect.isEqual(clientRect, clientRects[clientRects.length - 1])) {
return;
}
}
clientRects.push(clientRect);
}
function addCharacterOffset(container, offset) {
var range = createRange(container.ownerDocument);
if (offset < container.data.length) {
if (ExtendingChar.isExtendingChar(container.data[offset])) {
return clientRects;
}
// WebKit returns two client rects for a position after an extending
// character a\uxxx|b so expand on "b" and collapse to start of "b" box
if (ExtendingChar.isExtendingChar(container.data[offset - 1])) {
range.setStart(container, offset);
range.setEnd(container, offset + 1);
if (!isHiddenWhiteSpaceRange(range)) {
addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(range), false));
return clientRects;
}
}
}
if (offset > 0) {
range.setStart(container, offset - 1);
range.setEnd(container, offset);
if (!isHiddenWhiteSpaceRange(range)) {
addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(range), false));
}
}
if (offset < container.data.length) {
range.setStart(container, offset);
range.setEnd(container, offset + 1);
if (!isHiddenWhiteSpaceRange(range)) {
addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(range), true));
}
}
}
if (isText(caretPosition.container())) {
addCharacterOffset(caretPosition.container(), caretPosition.offset());
return clientRects;
}
if (isElement(caretPosition.container())) {
if (caretPosition.isAtEnd()) {
node = resolveIndex(caretPosition.container(), caretPosition.offset());
if (isText(node)) {
addCharacterOffset(node, node.data.length);
}
if (isValidElementCaretCandidate(node) && !isBr(node)) {
addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(node), false));
}
} else {
node = resolveIndex(caretPosition.container(), caretPosition.offset());
if (isText(node)) {
addCharacterOffset(node, 0);
}
if (isValidElementCaretCandidate(node) && caretPosition.isAtEnd()) {
addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(node), false));
return clientRects;
}
beforeNode = resolveIndex(caretPosition.container(), caretPosition.offset() - 1);
if (isValidElementCaretCandidate(beforeNode) && !isBr(beforeNode)) {
if (isBlock(beforeNode) || isBlock(node) || !isValidElementCaretCandidate(node)) {
addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(beforeNode), false));
}
}
if (isValidElementCaretCandidate(node)) {
addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(node), true));
}
}
}
return clientRects;
}
/**
* Represents a location within the document by a container and an offset.
*
* @constructor
* @param {Node} container Container node.
* @param {Number} offset Offset within that container node.
* @param {Array} clientRects Optional client rects array for the position.
*/
function CaretPosition(container, offset, clientRects) {
function isAtStart() {
if (isText(container)) {
return offset === 0;
}
return offset === 0;
}
function isAtEnd() {
if (isText(container)) {
return offset >= container.data.length;
}
return offset >= container.childNodes.length;
}
function toRange() {
var range;
range = createRange(container.ownerDocument);
range.setStart(container, offset);
range.setEnd(container, offset);
return range;
}
function getClientRects() {
if (!clientRects) {
clientRects = getCaretPositionClientRects(new CaretPosition(container, offset));
}
return clientRects;
}
function isVisible() {
return getClientRects().length > 0;
}
function isEqual(caretPosition) {
return caretPosition && container === caretPosition.container() && offset === caretPosition.offset();
}
function getNode(before) {
return resolveIndex(container, before ? offset - 1 : offset);
}
return {
/**
* Returns the container node.
*
* @method container
* @return {Node} Container node.
*/
container: Fun.constant(container),
/**
* Returns the offset within the container node.
*
* @method offset
* @return {Number} Offset within the container node.
*/
offset: Fun.constant(offset),
/**
* Returns a range out of a the caret position.
*
* @method toRange
* @return {DOMRange} range for the caret position.
*/
toRange: toRange,
/**
* Returns the client rects for the caret position. Might be multiple rects between
* block elements.
*
* @method getClientRects
* @return {Array} Array of client rects.
*/
getClientRects: getClientRects,
/**
* Returns true if the caret location is visible/displayed on screen.
*
* @method isVisible
* @return {Boolean} true/false if the position is visible or not.
*/
isVisible: isVisible,
/**
* Returns true if the caret location is at the beginning of text node or container.
*
* @method isVisible
* @return {Boolean} true/false if the position is at the beginning.
*/
isAtStart: isAtStart,
/**
* Returns true if the caret location is at the end of text node or container.
*
* @method isVisible
* @return {Boolean} true/false if the position is at the end.
*/
isAtEnd: isAtEnd,
/**
* Compares the caret position to another caret position. This will only compare the
* container and offset not it's visual position.
*
* @method isEqual
* @param {tinymce.caret.CaretPosition} caretPosition Caret position to compare with.
* @return {Boolean} true if the caret positions are equal.
*/
isEqual: isEqual,
/**
* Returns the closest resolved node from a node index. That means if you have an offset after the
* last node in a container it will return that last node.
*
* @method getNode
* @return {Node} Node that is closest to the index.
*/
getNode: getNode
};
}
/**
* Creates a caret position from the start of a range.
*
* @method fromRangeStart
* @param {DOMRange} range DOM Range to create caret position from.
* @return {tinymce.caret.CaretPosition} Caret position from the start of DOM range.
*/
CaretPosition.fromRangeStart = function(range) {
return new CaretPosition(range.startContainer, range.startOffset);
};
/**
* Creates a caret position from the end of a range.
*
* @method fromRangeEnd
* @param {DOMRange} range DOM Range to create caret position from.
* @return {tinymce.caret.CaretPosition} Caret position from the end of DOM range.
*/
CaretPosition.fromRangeEnd = function(range) {
return new CaretPosition(range.endContainer, range.endOffset);
};
/**
* Creates a caret position from a node and places the offset after it.
*
* @method after
* @param {Node} node Node to get caret position from.
* @return {tinymce.caret.CaretPosition} Caret position from the node.
*/
CaretPosition.after = function(node) {
return new CaretPosition(node.parentNode, nodeIndex(node) + 1);
};
/**
* Creates a caret position from a node and places the offset before it.
*
* @method before
* @param {Node} node Node to get caret position from.
* @return {tinymce.caret.CaretPosition} Caret position from the node.
*/
CaretPosition.before = function(node) {
return new CaretPosition(node.parentNode, nodeIndex(node));
};
return CaretPosition;
});
// Included from: js/tinymce/classes/caret/CaretBookmark.js
/**
* CaretBookmark.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This module creates or resolves xpath like string representation of a CaretPositions.
*
* The format is a / separated list of chunks with:
* <element|text()>[index|after|before]
*
* For example:
* p[0]/b[0]/text()[0],1 = <p><b>a|c</b></p>
* p[0]/img[0],before = <p>|<img></p>
* p[0]/img[0],after = <p><img>|</p>
*
* @private
* @static
* @class tinymce.caret.CaretBookmark
* @example
* var bookmark = CaretBookmark.create(rootElm, CaretPosition.before(rootElm.firstChild));
* var caretPosition = CaretBookmark.resolve(bookmark);
*/
define('tinymce/caret/CaretBookmark', [
'tinymce/dom/NodeType',
'tinymce/dom/DOMUtils',
'tinymce/util/Fun',
'tinymce/util/Arr',
'tinymce/caret/CaretPosition'
], function(NodeType, DomUtils, Fun, Arr, CaretPosition) {
var isText = NodeType.isText,
isBogus = NodeType.isBogus,
nodeIndex = DomUtils.nodeIndex;
function normalizedParent(node) {
var parentNode = node.parentNode;
if (isBogus(parentNode)) {
return normalizedParent(parentNode);
}
return parentNode;
}
function getChildNodes(node) {
if (!node) {
return [];
}
return Arr.reduce(node.childNodes, function(result, node) {
if (isBogus(node) && node.nodeName != 'BR') {
result = result.concat(getChildNodes(node));
} else {
result.push(node);
}
return result;
}, []);
}
function normalizedTextOffset(textNode, offset) {
while ((textNode = textNode.previousSibling)) {
if (!isText(textNode)) {
break;
}
offset += textNode.data.length;
}
return offset;
}
function equal(targetValue) {
return function(value) {
return targetValue === value;
};
}
function normalizedNodeIndex(node) {
var nodes, index, numTextFragments;
nodes = getChildNodes(normalizedParent(node));
index = Arr.findIndex(nodes, equal(node), node);
nodes = nodes.slice(0, index + 1);
numTextFragments = Arr.reduce(nodes, function(result, node, i) {
if (isText(node) && isText(nodes[i - 1])) {
result++;
}
return result;
}, 0);
nodes = Arr.filter(nodes, NodeType.matchNodeNames(node.nodeName));
index = Arr.findIndex(nodes, equal(node), node);
return index - numTextFragments;
}
function createPathItem(node) {
var name;
if (isText(node)) {
name = 'text()';
} else {
name = node.nodeName.toLowerCase();
}
return name + '[' + normalizedNodeIndex(node) + ']';
}
function parentsUntil(rootNode, node, predicate) {
var parents = [];
for (node = node.parentNode; node != rootNode; node = node.parentNode) {
if (predicate && predicate(node)) {
break;
}
parents.push(node);
}
return parents;
}
function create(rootNode, caretPosition) {
var container, offset, path = [],
outputOffset, childNodes, parents;
container = caretPosition.container();
offset = caretPosition.offset();
if (isText(container)) {
outputOffset = normalizedTextOffset(container, offset);
} else {
childNodes = container.childNodes;
if (offset >= childNodes.length) {
outputOffset = 'after';
offset = childNodes.length - 1;
} else {
outputOffset = 'before';
}
container = childNodes[offset];
}
path.push(createPathItem(container));
parents = parentsUntil(rootNode, container);
parents = Arr.filter(parents, Fun.negate(NodeType.isBogus));
path = path.concat(Arr.map(parents, function(node) {
return createPathItem(node);
}));
return path.reverse().join('/') + ',' + outputOffset;
}
function resolvePathItem(node, name, index) {
var nodes = getChildNodes(node);
nodes = Arr.filter(nodes, function(node, index) {
return !isText(node) || !isText(nodes[index - 1]);
});
nodes = Arr.filter(nodes, NodeType.matchNodeNames(name));
return nodes[index];
}
function findTextPosition(container, offset) {
var node = container, targetOffset = 0, dataLen;
while (isText(node)) {
dataLen = node.data.length;
if (offset >= targetOffset && offset <= targetOffset + dataLen) {
container = node;
offset = offset - targetOffset;
break;
}
if (!isText(node.nextSibling)) {
container = node;
offset = dataLen;
break;
}
targetOffset += dataLen;
node = node.nextSibling;
}
if (offset > container.data.length) {
offset = container.data.length;
}
return new CaretPosition(container, offset);
}
function resolve(rootNode, path) {
var parts, container, offset;
if (!path) {
return null;
}
parts = path.split(',');
path = parts[0].split('/');
offset = parts.length > 1 ? parts[1] : 'before';
container = Arr.reduce(path, function(result, value) {
value = /([\w\-\(\)]+)\[([0-9]+)\]/.exec(value);
if (!value) {
return null;
}
if (value[1] === 'text()') {
value[1] = '#text';
}
return resolvePathItem(result, value[1], parseInt(value[2], 10));
}, rootNode);
if (!container) {
return null;
}
if (!isText(container)) {
if (offset === 'after') {
offset = nodeIndex(container) + 1;
} else {
offset = nodeIndex(container);
}
return new CaretPosition(container.parentNode, offset);
}
return findTextPosition(container, parseInt(offset, 10));
}
return {
/**
* Create a xpath bookmark location for the specified caret position.
*
* @method create
* @param {Node} rootNode Root node to create bookmark within.
* @param {tinymce.caret.CaretPosition} caretPosition Caret position within the root node.
* @return {String} String xpath like location of caret position.
*/
create: create,
/**
* Resolves a xpath like bookmark location to the a caret position.
*
* @method resolve
* @param {Node} rootNode Root node to resolve xpath bookmark within.
* @param {String} bookmark Bookmark string to resolve.
* @return {tinymce.caret.CaretPosition} Caret position resolved from xpath like bookmark.
*/
resolve: resolve
};
});
// Included from: js/tinymce/classes/dom/BookmarkManager.js
/**
* BookmarkManager.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class handles selection bookmarks.
*
* @class tinymce.dom.BookmarkManager
*/
define("tinymce/dom/BookmarkManager", [
"tinymce/Env",
"tinymce/util/Tools",
"tinymce/caret/CaretContainer",
"tinymce/caret/CaretBookmark",
"tinymce/caret/CaretPosition",
"tinymce/dom/NodeType",
"tinymce/dom/RangeUtils"
], function(Env, Tools, CaretContainer, CaretBookmark, CaretPosition, NodeType, RangeUtils) {
var isContentEditableFalse = NodeType.isContentEditableFalse;
/**
* Constructs a new BookmarkManager instance for a specific selection instance.
*
* @constructor
* @method BookmarkManager
* @param {tinymce.dom.Selection} selection Selection instance to handle bookmarks for.
*/
function BookmarkManager(selection) {
var dom = selection.dom;
/**
* Returns a bookmark location for the current selection. This bookmark object
* can then be used to restore the selection after some content modification to the document.
*
* @method getBookmark
* @param {Number} type Optional state if the bookmark should be simple or not. Default is complex.
* @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization.
* @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection.
* @example
* // Stores a bookmark of the current selection
* var bm = tinymce.activeEditor.selection.getBookmark();
*
* tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content');
*
* // Restore the selection bookmark
* tinymce.activeEditor.selection.moveToBookmark(bm);
*/
this.getBookmark = function(type, normalized) {
var rng, rng2, id, collapsed, name, element, chr = '&#xFEFF;', styles;
function findIndex(name, element) {
var count = 0;
Tools.each(dom.select(name), function(node) {
if (node.getAttribute('data-mce-bogus') === 'all') {
return;
}
if (node == element) {
return false;
}
count++;
});
return count;
}
function normalizeTableCellSelection(rng) {
function moveEndPoint(start) {
var container, offset, childNodes, prefix = start ? 'start' : 'end';
container = rng[prefix + 'Container'];
offset = rng[prefix + 'Offset'];
if (container.nodeType == 1 && container.nodeName == "TR") {
childNodes = container.childNodes;
container = childNodes[Math.min(start ? offset : offset - 1, childNodes.length - 1)];
if (container) {
offset = start ? 0 : container.childNodes.length;
rng['set' + (start ? 'Start' : 'End')](container, offset);
}
}
}
moveEndPoint(true);
moveEndPoint();
return rng;
}
function getLocation(rng) {
var root = dom.getRoot(), bookmark = {};
function getPoint(rng, start) {
var container = rng[start ? 'startContainer' : 'endContainer'],
offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0;
if (container.nodeType == 3) {
if (normalized) {
for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling) {
offset += node.nodeValue.length;
}
}
point.push(offset);
} else {
childNodes = container.childNodes;
if (offset >= childNodes.length && childNodes.length) {
after = 1;
offset = Math.max(0, childNodes.length - 1);
}
point.push(dom.nodeIndex(childNodes[offset], normalized) + after);
}
for (; container && container != root; container = container.parentNode) {
point.push(dom.nodeIndex(container, normalized));
}
return point;
}
bookmark.start = getPoint(rng, true);
if (!selection.isCollapsed()) {
bookmark.end = getPoint(rng);
}
return bookmark;
}
function findAdjacentContentEditableFalseElm(rng) {
function findSibling(node, offset) {
var sibling;
if (NodeType.isElement(node)) {
node = RangeUtils.getNode(node, offset);
if (isContentEditableFalse(node)) {
return node;
}
}
if (CaretContainer.isCaretContainer(node)) {
if (NodeType.isText(node) && CaretContainer.isCaretContainerBlock(node)) {
node = node.parentNode;
}
sibling = node.previousSibling;
if (isContentEditableFalse(sibling)) {
return sibling;
}
sibling = node.nextSibling;
if (isContentEditableFalse(sibling)) {
return sibling;
}
}
}
return findSibling(rng.startContainer, rng.startOffset) || findSibling(rng.endContainer, rng.endOffset);
}
if (type == 2) {
element = selection.getNode();
name = element ? element.nodeName : null;
rng = selection.getRng();
if (isContentEditableFalse(element) || name == 'IMG') {
return {name: name, index: findIndex(name, element)};
}
if (selection.tridentSel) {
return selection.tridentSel.getBookmark(type);
}
element = findAdjacentContentEditableFalseElm(rng);
if (element) {
name = element.tagName;
return {name: name, index: findIndex(name, element)};
}
return getLocation(rng);
}
if (type == 3) {
rng = selection.getRng();
return {
start: CaretBookmark.create(dom.getRoot(), CaretPosition.fromRangeStart(rng)),
end: CaretBookmark.create(dom.getRoot(), CaretPosition.fromRangeEnd(rng))
};
}
// Handle simple range
if (type) {
return {rng: selection.getRng()};
}
rng = selection.getRng();
id = dom.uniqueId();
collapsed = selection.isCollapsed();
styles = 'overflow:hidden;line-height:0px';
// Explorer method
if (rng.duplicate || rng.item) {
// Text selection
if (!rng.item) {
rng2 = rng.duplicate();
try {
// Insert start marker
rng.collapse();
rng.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_start" style="' + styles + '">' + chr + '</span>');
// Insert end marker
if (!collapsed) {
rng2.collapse(false);
// Detect the empty space after block elements in IE and move the
// end back one character <p></p>] becomes <p>]</p>
rng.moveToElementText(rng2.parentElement());
if (rng.compareEndPoints('StartToEnd', rng2) === 0) {
rng2.move('character', -1);
}
rng2.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_end" style="' + styles + '">' + chr + '</span>');
}
} catch (ex) {
// IE might throw unspecified error so lets ignore it
return null;
}
} else {
// Control selection
element = rng.item(0);
name = element.nodeName;
return {name: name, index: findIndex(name, element)};
}
} else {
element = selection.getNode();
name = element.nodeName;
if (name == 'IMG') {
return {name: name, index: findIndex(name, element)};
}
// W3C method
rng2 = normalizeTableCellSelection(rng.cloneRange());
// Insert end marker
if (!collapsed) {
rng2.collapse(false);
rng2.insertNode(dom.create('span', {'data-mce-type': "bookmark", id: id + '_end', style: styles}, chr));
}
rng = normalizeTableCellSelection(rng);
rng.collapse(true);
rng.insertNode(dom.create('span', {'data-mce-type': "bookmark", id: id + '_start', style: styles}, chr));
}
selection.moveToBookmark({id: id, keep: 1});
return {id: id};
};
/**
* Restores the selection to the specified bookmark.
*
* @method moveToBookmark
* @param {Object} bookmark Bookmark to restore selection from.
* @return {Boolean} true/false if it was successful or not.
* @example
* // Stores a bookmark of the current selection
* var bm = tinymce.activeEditor.selection.getBookmark();
*
* tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content');
*
* // Restore the selection bookmark
* tinymce.activeEditor.selection.moveToBookmark(bm);
*/
this.moveToBookmark = function(bookmark) {
var rng, root, startContainer, endContainer, startOffset, endOffset;
function setEndPoint(start) {
var point = bookmark[start ? 'start' : 'end'], i, node, offset, children;
if (point) {
offset = point[0];
// Find container node
for (node = root, i = point.length - 1; i >= 1; i--) {
children = node.childNodes;
if (point[i] > children.length - 1) {
return;
}
node = children[point[i]];
}
// Move text offset to best suitable location
if (node.nodeType === 3) {
offset = Math.min(point[0], node.nodeValue.length);
}
// Move element offset to best suitable location
if (node.nodeType === 1) {
offset = Math.min(point[0], node.childNodes.length);
}
// Set offset within container node
if (start) {
rng.setStart(node, offset);
} else {
rng.setEnd(node, offset);
}
}
return true;
}
function restoreEndPoint(suffix) {
var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep;
if (marker) {
node = marker.parentNode;
if (suffix == 'start') {
if (!keep) {
idx = dom.nodeIndex(marker);
} else {
node = marker.firstChild;
idx = 1;
}
startContainer = endContainer = node;
startOffset = endOffset = idx;
} else {
if (!keep) {
idx = dom.nodeIndex(marker);
} else {
node = marker.firstChild;
idx = 1;
}
endContainer = node;
endOffset = idx;
}
if (!keep) {
prev = marker.previousSibling;
next = marker.nextSibling;
// Remove all marker text nodes
Tools.each(Tools.grep(marker.childNodes), function(node) {
if (node.nodeType == 3) {
node.nodeValue = node.nodeValue.replace(/\uFEFF/g, '');
}
});
// Remove marker but keep children if for example contents where inserted into the marker
// Also remove duplicated instances of the marker for example by a
// split operation or by WebKit auto split on paste feature
while ((marker = dom.get(bookmark.id + '_' + suffix))) {
dom.remove(marker, 1);
}
// If siblings are text nodes then merge them unless it's Opera since it some how removes the node
// and we are sniffing since adding a lot of detection code for a browser with 3% of the market
// isn't worth the effort. Sorry, Opera but it's just a fact
if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !Env.opera) {
idx = prev.nodeValue.length;
prev.appendData(next.nodeValue);
dom.remove(next);
if (suffix == 'start') {
startContainer = endContainer = prev;
startOffset = endOffset = idx;
} else {
endContainer = prev;
endOffset = idx;
}
}
}
}
}
function addBogus(node) {
// Adds a bogus BR element for empty block elements
if (dom.isBlock(node) && !node.innerHTML && !Env.ie) {
node.innerHTML = '<br data-mce-bogus="1" />';
}
return node;
}
function resolveCaretPositionBookmark() {
var rng, pos;
rng = dom.createRng();
pos = CaretBookmark.resolve(dom.getRoot(), bookmark.start);
rng.setStart(pos.container(), pos.offset());
pos = CaretBookmark.resolve(dom.getRoot(), bookmark.end);
rng.setEnd(pos.container(), pos.offset());
return rng;
}
if (bookmark) {
if (Tools.isArray(bookmark.start)) {
rng = dom.createRng();
root = dom.getRoot();
if (selection.tridentSel) {
return selection.tridentSel.moveToBookmark(bookmark);
}
if (setEndPoint(true) && setEndPoint()) {
selection.setRng(rng);
}
} else if (typeof bookmark.start == 'string') {
selection.setRng(resolveCaretPositionBookmark(bookmark));
} else if (bookmark.id) {
// Restore start/end points
restoreEndPoint('start');
restoreEndPoint('end');
if (startContainer) {
rng = dom.createRng();
rng.setStart(addBogus(startContainer), startOffset);
rng.setEnd(addBogus(endContainer), endOffset);
selection.setRng(rng);
}
} else if (bookmark.name) {
selection.select(dom.select(bookmark.name)[bookmark.index]);
} else if (bookmark.rng) {
selection.setRng(bookmark.rng);
}
}
};
}
/**
* Returns true/false if the specified node is a bookmark node or not.
*
* @static
* @method isBookmarkNode
* @param {DOMNode} node DOM Node to check if it's a bookmark node or not.
* @return {Boolean} true/false if the node is a bookmark node or not.
*/
BookmarkManager.isBookmarkNode = function(node) {
return node && node.tagName === 'SPAN' && node.getAttribute('data-mce-type') === 'bookmark';
};
return BookmarkManager;
});
// Included from: js/tinymce/classes/dom/Selection.js
/**
* Selection.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class handles text and control selection it's an crossbrowser utility class.
* Consult the TinyMCE Wiki API for more details and examples on how to use this class.
*
* @class tinymce.dom.Selection
* @example
* // Getting the currently selected node for the active editor
* alert(tinymce.activeEditor.selection.getNode().nodeName);
*/
define("tinymce/dom/Selection", [
"tinymce/dom/TreeWalker",
"tinymce/dom/TridentSelection",
"tinymce/dom/ControlSelection",
"tinymce/dom/RangeUtils",
"tinymce/dom/BookmarkManager",
"tinymce/dom/NodeType",
"tinymce/Env",
"tinymce/util/Tools",
"tinymce/caret/CaretPosition"
], function(TreeWalker, TridentSelection, ControlSelection, RangeUtils, BookmarkManager, NodeType, Env, Tools, CaretPosition) {
var each = Tools.each, trim = Tools.trim;
var isIE = Env.ie;
/**
* Constructs a new selection instance.
*
* @constructor
* @method Selection
* @param {tinymce.dom.DOMUtils} dom DOMUtils object reference.
* @param {Window} win Window to bind the selection object to.
* @param {tinymce.Editor} editor Editor instance of the selection.
* @param {tinymce.dom.Serializer} serializer DOM serialization class to use for getContent.
*/
function Selection(dom, win, serializer, editor) {
var self = this;
self.dom = dom;
self.win = win;
self.serializer = serializer;
self.editor = editor;
self.bookmarkManager = new BookmarkManager(self);
self.controlSelection = new ControlSelection(self, editor);
// No W3C Range support
if (!self.win.getSelection) {
self.tridentSel = new TridentSelection(self);
}
}
Selection.prototype = {
/**
* Move the selection cursor range to the specified node and offset.
* If there is no node specified it will move it to the first suitable location within the body.
*
* @method setCursorLocation
* @param {Node} node Optional node to put the cursor in.
* @param {Number} offset Optional offset from the start of the node to put the cursor at.
*/
setCursorLocation: function(node, offset) {
var self = this, rng = self.dom.createRng();
if (!node) {
self._moveEndPoint(rng, self.editor.getBody(), true);
self.setRng(rng);
} else {
rng.setStart(node, offset);
rng.setEnd(node, offset);
self.setRng(rng);
self.collapse(false);
}
},
/**
* Returns the selected contents using the DOM serializer passed in to this class.
*
* @method getContent
* @param {Object} args Optional settings class with for example output format text or html.
* @return {String} Selected contents in for example HTML format.
* @example
* // Alerts the currently selected contents
* alert(tinymce.activeEditor.selection.getContent());
*
* // Alerts the currently selected contents as plain text
* alert(tinymce.activeEditor.selection.getContent({format: 'text'}));
*/
getContent: function(args) {
var self = this, rng = self.getRng(), tmpElm = self.dom.create("body");
var se = self.getSel(), whiteSpaceBefore, whiteSpaceAfter, fragment;
args = args || {};
whiteSpaceBefore = whiteSpaceAfter = '';
args.get = true;
args.format = args.format || 'html';
args.selection = true;
self.editor.fire('BeforeGetContent', args);
if (args.format == 'text') {
return self.isCollapsed() ? '' : (rng.text || (se.toString ? se.toString() : ''));
}
if (rng.cloneContents) {
fragment = rng.cloneContents();
if (fragment) {
tmpElm.appendChild(fragment);
}
} else if (rng.item !== undefined || rng.htmlText !== undefined) {
// IE will produce invalid markup if elements are present that
// it doesn't understand like custom elements or HTML5 elements.
// Adding a BR in front of the contents and then remoiving it seems to fix it though.
tmpElm.innerHTML = '<br>' + (rng.item ? rng.item(0).outerHTML : rng.htmlText);
tmpElm.removeChild(tmpElm.firstChild);
} else {
tmpElm.innerHTML = rng.toString();
}
// Keep whitespace before and after
if (/^\s/.test(tmpElm.innerHTML)) {
whiteSpaceBefore = ' ';
}
if (/\s+$/.test(tmpElm.innerHTML)) {
whiteSpaceAfter = ' ';
}
args.getInner = true;
args.content = self.isCollapsed() ? '' : whiteSpaceBefore + self.serializer.serialize(tmpElm, args) + whiteSpaceAfter;
self.editor.fire('GetContent', args);
return args.content;
},
/**
* Sets the current selection to the specified content. If any contents is selected it will be replaced
* with the contents passed in to this function. If there is no selection the contents will be inserted
* where the caret is placed in the editor/page.
*
* @method setContent
* @param {String} content HTML contents to set could also be other formats depending on settings.
* @param {Object} args Optional settings object with for example data format.
* @example
* // Inserts some HTML contents at the current selection
* tinymce.activeEditor.selection.setContent('<strong>Some contents</strong>');
*/
setContent: function(content, args) {
var self = this, rng = self.getRng(), caretNode, doc = self.win.document, frag, temp;
args = args || {format: 'html'};
args.set = true;
args.selection = true;
args.content = content;
// Dispatch before set content event
if (!args.no_events) {
self.editor.fire('BeforeSetContent', args);
}
content = args.content;
if (rng.insertNode) {
// Make caret marker since insertNode places the caret in the beginning of text after insert
content += '<span id="__caret">_</span>';
// Delete and insert new node
if (rng.startContainer == doc && rng.endContainer == doc) {
// WebKit will fail if the body is empty since the range is then invalid and it can't insert contents
doc.body.innerHTML = content;
} else {
rng.deleteContents();
if (doc.body.childNodes.length === 0) {
doc.body.innerHTML = content;
} else {
// createContextualFragment doesn't exists in IE 9 DOMRanges
if (rng.createContextualFragment) {
rng.insertNode(rng.createContextualFragment(content));
} else {
// Fake createContextualFragment call in IE 9
frag = doc.createDocumentFragment();
temp = doc.createElement('div');
frag.appendChild(temp);
temp.outerHTML = content;
rng.insertNode(frag);
}
}
}
// Move to caret marker
caretNode = self.dom.get('__caret');
// Make sure we wrap it compleatly, Opera fails with a simple select call
rng = doc.createRange();
rng.setStartBefore(caretNode);
rng.setEndBefore(caretNode);
self.setRng(rng);
// Remove the caret position
self.dom.remove('__caret');
try {
self.setRng(rng);
} catch (ex) {
// Might fail on Opera for some odd reason
}
} else {
if (rng.item) {
// Delete content and get caret text selection
doc.execCommand('Delete', false, null);
rng = self.getRng();
}
// Explorer removes spaces from the beginning of pasted contents
if (/^\s+/.test(content)) {
rng.pasteHTML('<span id="__mce_tmp">_</span>' + content);
self.dom.remove('__mce_tmp');
} else {
rng.pasteHTML(content);
}
}
// Dispatch set content event
if (!args.no_events) {
self.editor.fire('SetContent', args);
}
},
/**
* Returns the start element of a selection range. If the start is in a text
* node the parent element will be returned.
*
* @method getStart
* @param {Boolean} real Optional state to get the real parent when the selection is collapsed not the closest element.
* @return {Element} Start element of selection range.
*/
getStart: function(real) {
var self = this, rng = self.getRng(), startElement, parentElement, checkRng, node;
if (rng.duplicate || rng.item) {
// Control selection, return first item
if (rng.item) {
return rng.item(0);
}
// Get start element
checkRng = rng.duplicate();
checkRng.collapse(1);
startElement = checkRng.parentElement();
if (startElement.ownerDocument !== self.dom.doc) {
startElement = self.dom.getRoot();
}
// Check if range parent is inside the start element, then return the inner parent element
// This will fix issues when a single element is selected, IE would otherwise return the wrong start element
parentElement = node = rng.parentElement();
while ((node = node.parentNode)) {
if (node == startElement) {
startElement = parentElement;
break;
}
}
return startElement;
}
startElement = rng.startContainer;
if (startElement.nodeType == 1 && startElement.hasChildNodes()) {
if (!real || !rng.collapsed) {
startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)];
}
}
if (startElement && startElement.nodeType == 3) {
return startElement.parentNode;
}
return startElement;
},
/**
* Returns the end element of a selection range. If the end is in a text
* node the parent element will be returned.
*
* @method getEnd
* @param {Boolean} real Optional state to get the real parent when the selection is collapsed not the closest element.
* @return {Element} End element of selection range.
*/
getEnd: function(real) {
var self = this, rng = self.getRng(), endElement, endOffset;
if (rng.duplicate || rng.item) {
if (rng.item) {
return rng.item(0);
}
rng = rng.duplicate();
rng.collapse(0);
endElement = rng.parentElement();
if (endElement.ownerDocument !== self.dom.doc) {
endElement = self.dom.getRoot();
}
if (endElement && endElement.nodeName == 'BODY') {
return endElement.lastChild || endElement;
}
return endElement;
}
endElement = rng.endContainer;
endOffset = rng.endOffset;
if (endElement.nodeType == 1 && endElement.hasChildNodes()) {
if (!real || !rng.collapsed) {
endElement = endElement.childNodes[endOffset > 0 ? endOffset - 1 : endOffset];
}
}
if (endElement && endElement.nodeType == 3) {
return endElement.parentNode;
}
return endElement;
},
/**
* Returns a bookmark location for the current selection. This bookmark object
* can then be used to restore the selection after some content modification to the document.
*
* @method getBookmark
* @param {Number} type Optional state if the bookmark should be simple or not. Default is complex.
* @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization.
* @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection.
* @example
* // Stores a bookmark of the current selection
* var bm = tinymce.activeEditor.selection.getBookmark();
*
* tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content');
*
* // Restore the selection bookmark
* tinymce.activeEditor.selection.moveToBookmark(bm);
*/
getBookmark: function(type, normalized) {
return this.bookmarkManager.getBookmark(type, normalized);
},
/**
* Restores the selection to the specified bookmark.
*
* @method moveToBookmark
* @param {Object} bookmark Bookmark to restore selection from.
* @return {Boolean} true/false if it was successful or not.
* @example
* // Stores a bookmark of the current selection
* var bm = tinymce.activeEditor.selection.getBookmark();
*
* tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content');
*
* // Restore the selection bookmark
* tinymce.activeEditor.selection.moveToBookmark(bm);
*/
moveToBookmark: function(bookmark) {
return this.bookmarkManager.moveToBookmark(bookmark);
},
/**
* Selects the specified element. This will place the start and end of the selection range around the element.
*
* @method select
* @param {Element} node HTML DOM element to select.
* @param {Boolean} content Optional bool state if the contents should be selected or not on non IE browser.
* @return {Element} Selected element the same element as the one that got passed in.
* @example
* // Select the first paragraph in the active editor
* tinymce.activeEditor.selection.select(tinymce.activeEditor.dom.select('p')[0]);
*/
select: function(node, content) {
var self = this, dom = self.dom, rng = dom.createRng(), idx;
// Clear stored range set by FocusManager
self.lastFocusBookmark = null;
if (node) {
if (!content && self.controlSelection.controlSelect(node)) {
return;
}
idx = dom.nodeIndex(node);
rng.setStart(node.parentNode, idx);
rng.setEnd(node.parentNode, idx + 1);
// Find first/last text node or BR element
if (content) {
self._moveEndPoint(rng, node, true);
self._moveEndPoint(rng, node);
}
self.setRng(rng);
}
return node;
},
/**
* Returns true/false if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection.
*
* @method isCollapsed
* @return {Boolean} true/false state if the selection range is collapsed or not.
* Collapsed means if it's a caret or a larger selection.
*/
isCollapsed: function() {
var self = this, rng = self.getRng(), sel = self.getSel();
if (!rng || rng.item) {
return false;
}
if (rng.compareEndPoints) {
return rng.compareEndPoints('StartToEnd', rng) === 0;
}
return !sel || rng.collapsed;
},
/**
* Collapse the selection to start or end of range.
*
* @method collapse
* @param {Boolean} toStart Optional boolean state if to collapse to end or not. Defaults to false.
*/
collapse: function(toStart) {
var self = this, rng = self.getRng(), node;
// Control range on IE
if (rng.item) {
node = rng.item(0);
rng = self.win.document.body.createTextRange();
rng.moveToElementText(node);
}
rng.collapse(!!toStart);
self.setRng(rng);
},
/**
* Returns the browsers internal selection object.
*
* @method getSel
* @return {Selection} Internal browser selection object.
*/
getSel: function() {
var win = this.win;
return win.getSelection ? win.getSelection() : win.document.selection;
},
/**
* Returns the browsers internal range object.
*
* @method getRng
* @param {Boolean} w3c Forces a compatible W3C range on IE.
* @return {Range} Internal browser range object.
* @see http://www.quirksmode.org/dom/range_intro.html
* @see http://www.dotvoid.com/2001/03/using-the-range-object-in-mozilla/
*/
getRng: function(w3c) {
var self = this, selection, rng, elm, doc, ieRng, evt;
function tryCompareBoundaryPoints(how, sourceRange, destinationRange) {
try {
return sourceRange.compareBoundaryPoints(how, destinationRange);
} catch (ex) {
// Gecko throws wrong document exception if the range points
// to nodes that where removed from the dom #6690
// Browsers should mutate existing DOMRange instances so that they always point
// to something in the document this is not the case in Gecko works fine in IE/WebKit/Blink
// For performance reasons just return -1
return -1;
}
}
if (!self.win) {
return null;
}
doc = self.win.document;
if (typeof doc === 'undefined' || doc === null) {
return null;
}
// Use last rng passed from FocusManager if it's available this enables
// calls to editor.selection.getStart() to work when caret focus is lost on IE
if (!w3c && self.lastFocusBookmark) {
var bookmark = self.lastFocusBookmark;
// Convert bookmark to range IE 11 fix
if (bookmark.startContainer) {
rng = doc.createRange();
rng.setStart(bookmark.startContainer, bookmark.startOffset);
rng.setEnd(bookmark.endContainer, bookmark.endOffset);
} else {
rng = bookmark;
}
return rng;
}
// Found tridentSel object then we need to use that one
if (w3c && self.tridentSel) {
return self.tridentSel.getRangeAt(0);
}
try {
if ((selection = self.getSel())) {
if (selection.rangeCount > 0) {
rng = selection.getRangeAt(0);
} else {
rng = selection.createRange ? selection.createRange() : doc.createRange();
}
}
} catch (ex) {
// IE throws unspecified error here if TinyMCE is placed in a frame/iframe
}
evt = self.editor.fire('GetSelectionRange', {range: rng});
if (evt.range !== rng) {
return evt.range;
}
// We have W3C ranges and it's IE then fake control selection since IE9 doesn't handle that correctly yet
// IE 11 doesn't support the selection object so we check for that as well
if (isIE && rng && rng.setStart && doc.selection) {
try {
// IE will sometimes throw an exception here
ieRng = doc.selection.createRange();
} catch (ex) {
// Ignore
}
if (ieRng && ieRng.item) {
elm = ieRng.item(0);
rng = doc.createRange();
rng.setStartBefore(elm);
rng.setEndAfter(elm);
}
}
// No range found then create an empty one
// This can occur when the editor is placed in a hidden container element on Gecko
// Or on IE when there was an exception
if (!rng) {
rng = doc.createRange ? doc.createRange() : doc.body.createTextRange();
}
// If range is at start of document then move it to start of body
if (rng.setStart && rng.startContainer.nodeType === 9 && rng.collapsed) {
elm = self.dom.getRoot();
rng.setStart(elm, 0);
rng.setEnd(elm, 0);
}
if (self.selectedRange && self.explicitRange) {
if (tryCompareBoundaryPoints(rng.START_TO_START, rng, self.selectedRange) === 0 &&
tryCompareBoundaryPoints(rng.END_TO_END, rng, self.selectedRange) === 0) {
// Safari, Opera and Chrome only ever select text which causes the range to change.
// This lets us use the originally set range if the selection hasn't been changed by the user.
rng = self.explicitRange;
} else {
self.selectedRange = null;
self.explicitRange = null;
}
}
return rng;
},
/**
* Changes the selection to the specified DOM range.
*
* @method setRng
* @param {Range} rng Range to select.
* @param {Boolean} forward Optional boolean if the selection is forwards or backwards.
*/
setRng: function(rng, forward) {
var self = this, sel, node, evt;
if (!rng) {
return;
}
// Is IE specific range
if (rng.select) {
self.explicitRange = null;
try {
rng.select();
} catch (ex) {
// Needed for some odd IE bug #1843306
}
return;
}
if (!self.tridentSel) {
sel = self.getSel();
evt = self.editor.fire('SetSelectionRange', {range: rng});
rng = evt.range;
if (sel) {
self.explicitRange = rng;
try {
sel.removeAllRanges();
sel.addRange(rng);
} catch (ex) {
// IE might throw errors here if the editor is within a hidden container and selection is changed
}
// Forward is set to false and we have an extend function
if (forward === false && sel.extend) {
sel.collapse(rng.endContainer, rng.endOffset);
sel.extend(rng.startContainer, rng.startOffset);
}
// adding range isn't always successful so we need to check range count otherwise an exception can occur
self.selectedRange = sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
}
// WebKit egde case selecting images works better using setBaseAndExtent
if (!rng.collapsed && rng.startContainer == rng.endContainer && sel.setBaseAndExtent && !Env.ie) {
if (rng.endOffset - rng.startOffset < 2) {
if (rng.startContainer.hasChildNodes()) {
node = rng.startContainer.childNodes[rng.startOffset];
if (node && node.tagName == 'IMG') {
self.getSel().setBaseAndExtent(node, 0, node, 1);
}
}
}
}
self.editor.fire('AfterSetSelectionRange', {range: rng});
} else {
// Is W3C Range fake range on IE
if (rng.cloneRange) {
try {
self.tridentSel.addRange(rng);
} catch (ex) {
//IE9 throws an error here if called before selection is placed in the editor
}
}
}
},
/**
* Sets the current selection to the specified DOM element.
*
* @method setNode
* @param {Element} elm Element to set as the contents of the selection.
* @return {Element} Returns the element that got passed in.
* @example
* // Inserts a DOM node at current selection/caret location
* tinymce.activeEditor.selection.setNode(tinymce.activeEditor.dom.create('img', {src: 'some.gif', title: 'some title'}));
*/
setNode: function(elm) {
var self = this;
self.setContent(self.dom.getOuterHTML(elm));
return elm;
},
/**
* Returns the currently selected element or the common ancestor element for both start and end of the selection.
*
* @method getNode
* @return {Element} Currently selected element or common ancestor element.
* @example
* // Alerts the currently selected elements node name
* alert(tinymce.activeEditor.selection.getNode().nodeName);
*/
getNode: function() {
var self = this, rng = self.getRng(), elm;
var startContainer, endContainer, startOffset, endOffset, root = self.dom.getRoot();
function skipEmptyTextNodes(node, forwards) {
var orig = node;
while (node && node.nodeType === 3 && node.length === 0) {
node = forwards ? node.nextSibling : node.previousSibling;
}
return node || orig;
}
// Range maybe lost after the editor is made visible again
if (!rng) {
return root;
}
startContainer = rng.startContainer;
endContainer = rng.endContainer;
startOffset = rng.startOffset;
endOffset = rng.endOffset;
if (rng.setStart) {
elm = rng.commonAncestorContainer;
// Handle selection a image or other control like element such as anchors
if (!rng.collapsed) {
if (startContainer == endContainer) {
if (endOffset - startOffset < 2) {
if (startContainer.hasChildNodes()) {
elm = startContainer.childNodes[startOffset];
}
}
}
// If the anchor node is a element instead of a text node then return this element
//if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1)
// return sel.anchorNode.childNodes[sel.anchorOffset];
// Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent.
// This happens when you double click an underlined word in FireFox.
if (startContainer.nodeType === 3 && endContainer.nodeType === 3) {
if (startContainer.length === startOffset) {
startContainer = skipEmptyTextNodes(startContainer.nextSibling, true);
} else {
startContainer = startContainer.parentNode;
}
if (endOffset === 0) {
endContainer = skipEmptyTextNodes(endContainer.previousSibling, false);
} else {
endContainer = endContainer.parentNode;
}
if (startContainer && startContainer === endContainer) {
return startContainer;
}
}
}
if (elm && elm.nodeType == 3) {
return elm.parentNode;
}
return elm;
}
elm = rng.item ? rng.item(0) : rng.parentElement();
// IE 7 might return elements outside the iframe
if (elm.ownerDocument !== self.win.document) {
elm = root;
}
return elm;
},
getSelectedBlocks: function(startElm, endElm) {
var self = this, dom = self.dom, node, root, selectedBlocks = [];
root = dom.getRoot();
startElm = dom.getParent(startElm || self.getStart(), dom.isBlock);
endElm = dom.getParent(endElm || self.getEnd(), dom.isBlock);
if (startElm && startElm != root) {
selectedBlocks.push(startElm);
}
if (startElm && endElm && startElm != endElm) {
node = startElm;
var walker = new TreeWalker(startElm, root);
while ((node = walker.next()) && node != endElm) {
if (dom.isBlock(node)) {
selectedBlocks.push(node);
}
}
}
if (endElm && startElm != endElm && endElm != root) {
selectedBlocks.push(endElm);
}
return selectedBlocks;
},
isForward: function() {
var dom = this.dom, sel = this.getSel(), anchorRange, focusRange;
// No support for selection direction then always return true
if (!sel || !sel.anchorNode || !sel.focusNode) {
return true;
}
anchorRange = dom.createRng();
anchorRange.setStart(sel.anchorNode, sel.anchorOffset);
anchorRange.collapse(true);
focusRange = dom.createRng();
focusRange.setStart(sel.focusNode, sel.focusOffset);
focusRange.collapse(true);
return anchorRange.compareBoundaryPoints(anchorRange.START_TO_START, focusRange) <= 0;
},
normalize: function() {
var self = this, rng = self.getRng();
if (Env.range && new RangeUtils(self.dom).normalize(rng)) {
self.setRng(rng, self.isForward());
}
return rng;
},
/**
* Executes callback when the current selection starts/stops matching the specified selector. The current
* state will be passed to the callback as it's first argument.
*
* @method selectorChanged
* @param {String} selector CSS selector to check for.
* @param {function} callback Callback with state and args when the selector is matches or not.
*/
selectorChanged: function(selector, callback) {
var self = this, currentSelectors;
if (!self.selectorChangedData) {
self.selectorChangedData = {};
currentSelectors = {};
self.editor.on('NodeChange', function(e) {
var node = e.element, dom = self.dom, parents = dom.getParents(node, null, dom.getRoot()), matchedSelectors = {};
// Check for new matching selectors
each(self.selectorChangedData, function(callbacks, selector) {
each(parents, function(node) {
if (dom.is(node, selector)) {
if (!currentSelectors[selector]) {
// Execute callbacks
each(callbacks, function(callback) {
callback(true, {node: node, selector: selector, parents: parents});
});
currentSelectors[selector] = callbacks;
}
matchedSelectors[selector] = callbacks;
return false;
}
});
});
// Check if current selectors still match
each(currentSelectors, function(callbacks, selector) {
if (!matchedSelectors[selector]) {
delete currentSelectors[selector];
each(callbacks, function(callback) {
callback(false, {node: node, selector: selector, parents: parents});
});
}
});
});
}
// Add selector listeners
if (!self.selectorChangedData[selector]) {
self.selectorChangedData[selector] = [];
}
self.selectorChangedData[selector].push(callback);
return self;
},
getScrollContainer: function() {
var scrollContainer, node = this.dom.getRoot();
while (node && node.nodeName != 'BODY') {
if (node.scrollHeight > node.clientHeight) {
scrollContainer = node;
break;
}
node = node.parentNode;
}
return scrollContainer;
},
scrollIntoView: function(elm, alignToTop) {
var y, viewPort, self = this, dom = self.dom, root = dom.getRoot(), viewPortY, viewPortH, offsetY = 0;
function getPos(elm) {
var x = 0, y = 0;
var offsetParent = elm;
while (offsetParent && offsetParent.nodeType) {
x += offsetParent.offsetLeft || 0;
y += offsetParent.offsetTop || 0;
offsetParent = offsetParent.offsetParent;
}
return {x: x, y: y};
}
if (!NodeType.isElement(elm)) {
return;
}
if (alignToTop === false) {
offsetY = elm.offsetHeight;
}
if (root.nodeName != 'BODY') {
var scrollContainer = self.getScrollContainer();
if (scrollContainer) {
y = getPos(elm).y - getPos(scrollContainer).y + offsetY;
viewPortH = scrollContainer.clientHeight;
viewPortY = scrollContainer.scrollTop;
if (y < viewPortY || y + 25 > viewPortY + viewPortH) {
scrollContainer.scrollTop = y < viewPortY ? y : y - viewPortH + 25;
}
return;
}
}
viewPort = dom.getViewPort(self.editor.getWin());
y = dom.getPos(elm).y + offsetY;
viewPortY = viewPort.y;
viewPortH = viewPort.h;
if (y < viewPort.y || y + 25 > viewPortY + viewPortH) {
self.editor.getWin().scrollTo(0, y < viewPortY ? y : y - viewPortH + 25);
}
},
placeCaretAt: function(clientX, clientY) {
this.setRng(RangeUtils.getCaretRangeFromPoint(clientX, clientY, this.editor.getDoc()));
},
_moveEndPoint: function(rng, node, start) {
var root = node, walker = new TreeWalker(node, root);
var nonEmptyElementsMap = this.dom.schema.getNonEmptyElements();
do {
// Text node
if (node.nodeType == 3 && trim(node.nodeValue).length !== 0) {
if (start) {
rng.setStart(node, 0);
} else {
rng.setEnd(node, node.nodeValue.length);
}
return;
}
// BR/IMG/INPUT elements but not table cells
if (nonEmptyElementsMap[node.nodeName] && !/^(TD|TH)$/.test(node.nodeName)) {
if (start) {
rng.setStartBefore(node);
} else {
if (node.nodeName == 'BR') {
rng.setEndBefore(node);
} else {
rng.setEndAfter(node);
}
}
return;
}
// Found empty text block old IE can place the selection inside those
if (Env.ie && Env.ie < 11 && this.dom.isBlock(node) && this.dom.isEmpty(node)) {
if (start) {
rng.setStart(node, 0);
} else {
rng.setEnd(node, 0);
}
return;
}
} while ((node = (start ? walker.next() : walker.prev())));
// Failed to find any text node or other suitable location then move to the root of body
if (root.nodeName == 'BODY') {
if (start) {
rng.setStart(root, 0);
} else {
rng.setEnd(root, root.childNodes.length);
}
}
},
getBoundingClientRect: function() {
var rng = this.getRng();
return rng.collapsed ? CaretPosition.fromRangeStart(rng).getClientRects()[0] : rng.getBoundingClientRect();
},
destroy: function() {
this.win = null;
this.controlSelection.destroy();
}
};
return Selection;
});
// Included from: js/tinymce/classes/dom/ElementUtils.js
/**
* ElementUtils.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Utility class for various element specific functions.
*
* @private
* @class tinymce.dom.ElementUtils
*/
define("tinymce/dom/ElementUtils", [
"tinymce/dom/BookmarkManager",
"tinymce/util/Tools"
], function(BookmarkManager, Tools) {
var each = Tools.each;
function ElementUtils(dom) {
/**
* Compares two nodes and checks if it's attributes and styles matches.
* This doesn't compare classes as items since their order is significant.
*
* @method compare
* @param {Node} node1 First node to compare with.
* @param {Node} node2 Second node to compare with.
* @return {boolean} True/false if the nodes are the same or not.
*/
this.compare = function(node1, node2) {
// Not the same name
if (node1.nodeName != node2.nodeName) {
return false;
}
/**
* Returns all the nodes attributes excluding internal ones, styles and classes.
*
* @private
* @param {Node} node Node to get attributes from.
* @return {Object} Name/value object with attributes and attribute values.
*/
function getAttribs(node) {
var attribs = {};
each(dom.getAttribs(node), function(attr) {
var name = attr.nodeName.toLowerCase();
// Don't compare internal attributes or style
if (name.indexOf('_') !== 0 && name !== 'style' && name.indexOf('data-') !== 0) {
attribs[name] = dom.getAttrib(node, name);
}
});
return attribs;
}
/**
* Compares two objects checks if it's key + value exists in the other one.
*
* @private
* @param {Object} obj1 First object to compare.
* @param {Object} obj2 Second object to compare.
* @return {boolean} True/false if the objects matches or not.
*/
function compareObjects(obj1, obj2) {
var value, name;
for (name in obj1) {
// Obj1 has item obj2 doesn't have
if (obj1.hasOwnProperty(name)) {
value = obj2[name];
// Obj2 doesn't have obj1 item
if (typeof value == "undefined") {
return false;
}
// Obj2 item has a different value
if (obj1[name] != value) {
return false;
}
// Delete similar value
delete obj2[name];
}
}
// Check if obj 2 has something obj 1 doesn't have
for (name in obj2) {
// Obj2 has item obj1 doesn't have
if (obj2.hasOwnProperty(name)) {
return false;
}
}
return true;
}
// Attribs are not the same
if (!compareObjects(getAttribs(node1), getAttribs(node2))) {
return false;
}
// Styles are not the same
if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style')))) {
return false;
}
return !BookmarkManager.isBookmarkNode(node1) && !BookmarkManager.isBookmarkNode(node2);
};
}
return ElementUtils;
});
// Included from: js/tinymce/classes/fmt/Preview.js
/**
* Preview.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Internal class for generating previews styles for formats.
*
* Example:
* Preview.getCssText(editor, 'bold');
*
* @private
* @class tinymce.fmt.Preview
*/
define("tinymce/fmt/Preview", [
"tinymce/dom/DOMUtils",
"tinymce/util/Tools",
"tinymce/html/Schema"
], function(DOMUtils, Tools, Schema) {
var each = Tools.each;
var dom = DOMUtils.DOM;
function parsedSelectorToHtml(ancestry, editor) {
var elm, item, fragment;
var schema = editor && editor.schema || new Schema({});
function decorate(elm, item) {
if (item.classes.length) {
dom.addClass(elm, item.classes.join(' '));
}
dom.setAttribs(elm, item.attrs);
}
function createElement(sItem) {
var elm;
item = typeof sItem === 'string' ? {
name: sItem,
classes: [],
attrs: {}
} : sItem;
elm = dom.create(item.name);
decorate(elm, item);
return elm;
}
function getRequiredParent(elm, candidate) {
var name = typeof elm !== 'string' ? elm.nodeName.toLowerCase() : elm;
var elmRule = schema.getElementRule(name);
var parentsRequired = elmRule.parentsRequired;
if (parentsRequired && parentsRequired.length) {
return candidate && Tools.inArray(parentsRequired, candidate) !== -1 ? candidate : parentsRequired[0];
} else {
return false;
}
}
function wrapInHtml(elm, ancestry, siblings) {
var parent, parentCandidate, parentRequired;
var ancestor = ancestry.length && ancestry[0];
var ancestorName = ancestor && ancestor.name;
parentRequired = getRequiredParent(elm, ancestorName);
if (parentRequired) {
if (ancestorName == parentRequired) {
parentCandidate = ancestry[0];
ancestry = ancestry.slice(1);
} else {
parentCandidate = parentRequired;
}
} else if (ancestor) {
parentCandidate = ancestry[0];
ancestry = ancestry.slice(1);
} else if (!siblings) {
return elm;
}
if (parentCandidate) {
parent = createElement(parentCandidate);
parent.appendChild(elm);
}
if (siblings) {
if (!parent) {
// if no more ancestry, wrap in generic div
parent = dom.create('div');
parent.appendChild(elm);
}
Tools.each(siblings, function(sibling) {
var siblingElm = createElement(sibling);
parent.insertBefore(siblingElm, elm);
});
}
return wrapInHtml(parent, ancestry, parentCandidate && parentCandidate.siblings);
}
if (ancestry && ancestry.length) {
item = ancestry[0];
elm = createElement(item);
fragment = dom.create('div');
fragment.appendChild(wrapInHtml(elm, ancestry.slice(1), item.siblings));
return fragment;
} else {
return '';
}
}
function selectorToHtml(selector, editor) {
return parsedSelectorToHtml(parseSelector(selector, editor));
}
function parseSelectorItem(item) {
var tagName;
var obj = {
classes: [],
attrs: {}
};
item = obj.selector = Tools.trim(item);
if (item !== '*') {
// matching IDs, CLASSes, ATTRIBUTES and PSEUDOs
tagName = item.replace(/(?:([#\.]|::?)([\w\-]+)|(\[)([^\]]+)\]?)/g, function($0, $1, $2, $3, $4) {
switch ($1) {
case '#':
obj.attrs.id = $2;
break;
case '.':
obj.classes.push($2);
break;
case ':':
if (Tools.inArray('checked disabled enabled read-only required'.split(' '), $2) !== -1) {
obj.attrs[$2] = $2;
}
break;
}
// atribute matched
if ($3 == '[') {
var m = $4.match(/([\w\-]+)(?:\=\"([^\"]+))?/);
if (m) {
obj.attrs[m[1]] = m[2];
}
}
return '';
});
}
obj.name = tagName || 'div';
return obj;
}
function parseSelector(selector) {
if (!selector || typeof selector !== 'string') {
return [];
}
// take into account only first one
selector = selector.split(/\s*,\s*/)[0];
// tighten
selector = selector.replace(/\s*(~\+|~|\+|>)\s*/g, '$1');
// split either on > or on space, but not the one inside brackets
return Tools.map(selector.split(/(?:>|\s+(?![^\[\]]+\]))/), function(item) {
// process each sibling selector separately
var siblings = Tools.map(item.split(/(?:~\+|~|\+)/), parseSelectorItem);
var obj = siblings.pop(); // the last one is our real target
if (siblings.length) {
obj.siblings = siblings;
}
return obj;
}).reverse();
}
function getCssText(editor, format) {
var name, previewFrag, previewElm, items;
var previewCss = '', parentFontSize, previewStyles;
previewStyles = editor.settings.preview_styles;
// No preview forced
if (previewStyles === false) {
return '';
}
// Default preview
if (typeof previewStyles !== 'string') {
previewStyles = 'font-family font-size font-weight font-style text-decoration ' +
'text-transform color background-color border border-radius outline text-shadow';
}
// Removes any variables since these can't be previewed
function removeVars(val) {
return val.replace(/%(\w+)/g, '');
}
// Create block/inline element to use for preview
if (typeof format == "string") {
format = editor.formatter.get(format);
if (!format) {
return;
}
format = format[0];
}
// Format specific preview override
// TODO: This should probably be further reduced by the previewStyles option
if ('preview' in format) {
previewStyles = format.preview;
if (previewStyles === false) {
return '';
}
}
name = format.block || format.inline || 'span';
items = parseSelector(format.selector);
if (items.length) {
if (!items[0].name) { // e.g. something like ul > .someClass was provided
items[0].name = name;
}
name = format.selector;
previewFrag = parsedSelectorToHtml(items);
} else {
previewFrag = parsedSelectorToHtml([name]);
}
previewElm = dom.select(name, previewFrag)[0] || previewFrag.firstChild;
// Add format styles to preview element
each(format.styles, function(value, name) {
value = removeVars(value);
if (value) {
dom.setStyle(previewElm, name, value);
}
});
// Add attributes to preview element
each(format.attributes, function(value, name) {
value = removeVars(value);
if (value) {
dom.setAttrib(previewElm, name, value);
}
});
// Add classes to preview element
each(format.classes, function(value) {
value = removeVars(value);
if (!dom.hasClass(previewElm, value)) {
dom.addClass(previewElm, value);
}
});
editor.fire('PreviewFormats');
// Add the previewElm outside the visual area
dom.setStyles(previewFrag, {position: 'absolute', left: -0xFFFF});
editor.getBody().appendChild(previewFrag);
// Get parent container font size so we can compute px values out of em/% for older IE:s
parentFontSize = dom.getStyle(editor.getBody(), 'fontSize', true);
parentFontSize = /px$/.test(parentFontSize) ? parseInt(parentFontSize, 10) : 0;
each(previewStyles.split(' '), function(name) {
var value = dom.getStyle(previewElm, name, true);
// If background is transparent then check if the body has a background color we can use
if (name == 'background-color' && /transparent|rgba\s*\([^)]+,\s*0\)/.test(value)) {
value = dom.getStyle(editor.getBody(), name, true);
// Ignore white since it's the default color, not the nicest fix
// TODO: Fix this by detecting runtime style
if (dom.toHex(value).toLowerCase() == '#ffffff') {
return;
}
}
if (name == 'color') {
// Ignore black since it's the default color, not the nicest fix
// TODO: Fix this by detecting runtime style
if (dom.toHex(value).toLowerCase() == '#000000') {
return;
}
}
// Old IE won't calculate the font size so we need to do that manually
if (name == 'font-size') {
if (/em|%$/.test(value)) {
if (parentFontSize === 0) {
return;
}
// Convert font size from em/% to px
value = parseFloat(value, 10) / (/%$/.test(value) ? 100 : 1);
value = (value * parentFontSize) + 'px';
}
}
if (name == "border" && value) {
previewCss += 'padding:0 2px;';
}
previewCss += name + ':' + value + ';';
});
editor.fire('AfterPreviewFormats');
//previewCss += 'line-height:normal';
dom.remove(previewFrag);
return previewCss;
}
return {
getCssText: getCssText,
parseSelector: parseSelector,
selectorToHtml: selectorToHtml
};
});
// Included from: js/tinymce/classes/fmt/Hooks.js
/**
* Hooks.js
*
* Released under LGPL License.
* Copyright (c) 1999-2016 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Internal class for overriding formatting.
*
* @private
* @class tinymce.fmt.Hooks
*/
define("tinymce/fmt/Hooks", [
"tinymce/util/Arr",
"tinymce/dom/NodeType",
"tinymce/dom/DomQuery"
], function(Arr, NodeType, $) {
var postProcessHooks = {}, filter = Arr.filter, each = Arr.each;
function addPostProcessHook(name, hook) {
var hooks = postProcessHooks[name];
if (!hooks) {
postProcessHooks[name] = hooks = [];
}
postProcessHooks[name].push(hook);
}
function postProcess(name, editor) {
each(postProcessHooks[name], function(hook) {
hook(editor);
});
}
addPostProcessHook("pre", function(editor) {
var rng = editor.selection.getRng(), isPre, blocks;
function hasPreSibling(pre) {
return isPre(pre.previousSibling) && Arr.indexOf(blocks, pre.previousSibling) != -1;
}
function joinPre(pre1, pre2) {
$(pre2).remove();
$(pre1).append('<br><br>').append(pre2.childNodes);
}
isPre = NodeType.matchNodeNames('pre');
if (!rng.collapsed) {
blocks = editor.selection.getSelectedBlocks();
each(filter(filter(blocks, isPre), hasPreSibling), function(pre) {
joinPre(pre.previousSibling, pre);
});
}
});
return {
postProcess: postProcess
};
});
// Included from: js/tinymce/classes/Formatter.js
/**
* Formatter.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Text formatter engine class. This class is used to apply formats like bold, italic, font size
* etc to the current selection or specific nodes. This engine was built to replace the browser's
* default formatting logic for execCommand due to its inconsistent and buggy behavior.
*
* @class tinymce.Formatter
* @example
* tinymce.activeEditor.formatter.register('mycustomformat', {
* inline: 'span',
* styles: {color: '#ff0000'}
* });
*
* tinymce.activeEditor.formatter.apply('mycustomformat');
*/
define("tinymce/Formatter", [
"tinymce/dom/TreeWalker",
"tinymce/dom/RangeUtils",
"tinymce/dom/BookmarkManager",
"tinymce/dom/ElementUtils",
"tinymce/util/Tools",
"tinymce/fmt/Preview",
"tinymce/fmt/Hooks"
], function(TreeWalker, RangeUtils, BookmarkManager, ElementUtils, Tools, Preview, Hooks) {
/**
* Constructs a new formatter instance.
*
* @constructor Formatter
* @param {tinymce.Editor} ed Editor instance to construct the formatter engine to.
*/
return function(ed) {
var formats = {},
dom = ed.dom,
selection = ed.selection,
rangeUtils = new RangeUtils(dom),
isValid = ed.schema.isValidChild,
isBlock = dom.isBlock,
forcedRootBlock = ed.settings.forced_root_block,
nodeIndex = dom.nodeIndex,
INVISIBLE_CHAR = '\uFEFF',
MCE_ATTR_RE = /^(src|href|style)$/,
FALSE = false,
TRUE = true,
formatChangeData,
undef,
getContentEditable = dom.getContentEditable,
disableCaretContainer,
markCaretContainersBogus,
isBookmarkNode = BookmarkManager.isBookmarkNode;
var each = Tools.each,
grep = Tools.grep,
walk = Tools.walk,
extend = Tools.extend;
function isTextBlock(name) {
if (name.nodeType) {
name = name.nodeName;
}
return !!ed.schema.getTextBlockElements()[name.toLowerCase()];
}
function isTableCell(node) {
return /^(TH|TD)$/.test(node.nodeName);
}
function isInlineBlock(node) {
return node && /^(IMG)$/.test(node.nodeName);
}
function getParents(node, selector) {
return dom.getParents(node, selector, dom.getRoot());
}
function isCaretNode(node) {
return node.nodeType === 1 && node.id === '_mce_caret';
}
function defaultFormats() {
register({
valigntop: [
{selector: 'td,th', styles: {'verticalAlign': 'top'}}
],
valignmiddle: [
{selector: 'td,th', styles: {'verticalAlign': 'middle'}}
],
valignbottom: [
{selector: 'td,th', styles: {'verticalAlign': 'bottom'}}
],
alignleft: [
{
selector: 'figure.image',
collapsed: false,
classes: 'align-left',
ceFalseOverride: true,
preview: 'font-family font-size'
},
{
selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li',
styles: {
textAlign: 'left'
},
inherit: false,
preview: false,
defaultBlock: 'div'
},
{selector: 'img,table', collapsed: false, styles: {'float': 'left'}, preview: 'font-family font-size'}
],
aligncenter: [
{
selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li',
styles: {
textAlign: 'center'
},
inherit: false,
preview: false,
defaultBlock: 'div'
},
{
selector: 'figure.image',
collapsed: false,
classes: 'align-center',
ceFalseOverride: true,
preview: 'font-family font-size'
},
{
selector: 'img',
collapsed: false,
styles: {
display: 'block',
marginLeft: 'auto',
marginRight: 'auto'
},
preview: false
},
{
selector: 'table',
collapsed: false,
styles: {
marginLeft: 'auto',
marginRight: 'auto'
},
preview: 'font-family font-size'
}
],
alignright: [
{
selector: 'figure.image',
collapsed: false,
classes: 'align-right',
ceFalseOverride: true,
preview: 'font-family font-size'
},
{
selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li',
styles: {
textAlign: 'right'
},
inherit: false,
preview: 'font-family font-size',
defaultBlock: 'div'
},
{
selector: 'img,table',
collapsed: false,
styles: {
'float': 'right'
},
preview: 'font-family font-size'
}
],
alignjustify: [
{
selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li',
styles: {
textAlign: 'justify'
},
inherit: false,
defaultBlock: 'div',
preview: 'font-family font-size'
}
],
bold: [
{inline: 'strong', remove: 'all'},
{inline: 'span', styles: {fontWeight: 'bold'}},
{inline: 'b', remove: 'all'}
],
italic: [
{inline: 'em', remove: 'all'},
{inline: 'span', styles: {fontStyle: 'italic'}},
{inline: 'i', remove: 'all'}
],
underline: [
{inline: 'span', styles: {textDecoration: 'underline'}, exact: true},
{inline: 'u', remove: 'all'}
],
strikethrough: [
{inline: 'span', styles: {textDecoration: 'line-through'}, exact: true},
{inline: 'strike', remove: 'all'}
],
forecolor: {inline: 'span', styles: {color: '%value'}, links: true, remove_similar: true},
hilitecolor: {inline: 'span', styles: {backgroundColor: '%value'}, links: true, remove_similar: true},
fontname: {inline: 'span', styles: {fontFamily: '%value'}},
fontsize: {inline: 'span', styles: {fontSize: '%value'}},
fontsize_class: {inline: 'span', attributes: {'class': '%value'}},
blockquote: {block: 'blockquote', wrapper: 1, remove: 'all'},
subscript: {inline: 'sub'},
superscript: {inline: 'sup'},
code: {inline: 'code'},
link: {inline: 'a', selector: 'a', remove: 'all', split: true, deep: true,
onmatch: function() {
return true;
},
onformat: function(elm, fmt, vars) {
each(vars, function(value, key) {
dom.setAttrib(elm, key, value);
});
}
},
removeformat: [
{
selector: 'b,strong,em,i,font,u,strike,sub,sup,dfn,code,samp,kbd,var,cite,mark,q,del,ins',
remove: 'all',
split: true,
expand: false,
block_expand: true,
deep: true
},
{selector: 'span', attributes: ['style', 'class'], remove: 'empty', split: true, expand: false, deep: true},
{selector: '*', attributes: ['style', 'class'], split: false, expand: false, deep: true}
]
});
// Register default block formats
each('p h1 h2 h3 h4 h5 h6 div address pre div dt dd samp'.split(/\s/), function(name) {
register(name, {block: name, remove: 'all'});
});
// Register user defined formats
register(ed.settings.formats);
}
function addKeyboardShortcuts() {
// Add some inline shortcuts
ed.addShortcut('meta+b', 'bold_desc', 'Bold');
ed.addShortcut('meta+i', 'italic_desc', 'Italic');
ed.addShortcut('meta+u', 'underline_desc', 'Underline');
// BlockFormat shortcuts keys
for (var i = 1; i <= 6; i++) {
ed.addShortcut('access+' + i, '', ['FormatBlock', false, 'h' + i]);
}
ed.addShortcut('access+7', '', ['FormatBlock', false, 'p']);
ed.addShortcut('access+8', '', ['FormatBlock', false, 'div']);
ed.addShortcut('access+9', '', ['FormatBlock', false, 'address']);
}
// Public functions
/**
* Returns the format by name or all formats if no name is specified.
*
* @method get
* @param {String} name Optional name to retrieve by.
* @return {Array/Object} Array/Object with all registered formats or a specific format.
*/
function get(name) {
return name ? formats[name] : formats;
}
/**
* Registers a specific format by name.
*
* @method register
* @param {Object/String} name Name of the format for example "bold".
* @param {Object/Array} format Optional format object or array of format variants
* can only be omitted if the first arg is an object.
*/
function register(name, format) {
if (name) {
if (typeof name !== 'string') {
each(name, function(format, name) {
register(name, format);
});
} else {
// Force format into array and add it to internal collection
format = format.length ? format : [format];
each(format, function(format) {
// Set deep to false by default on selector formats this to avoid removing
// alignment on images inside paragraphs when alignment is changed on paragraphs
if (format.deep === undef) {
format.deep = !format.selector;
}
// Default to true
if (format.split === undef) {
format.split = !format.selector || format.inline;
}
// Default to true
if (format.remove === undef && format.selector && !format.inline) {
format.remove = 'none';
}
// Mark format as a mixed format inline + block level
if (format.selector && format.inline) {
format.mixed = true;
format.block_expand = true;
}
// Split classes if needed
if (typeof format.classes === 'string') {
format.classes = format.classes.split(/\s+/);
}
});
formats[name] = format;
}
}
}
/**
* Unregister a specific format by name.
*
* @method unregister
* @param {String} name Name of the format for example "bold".
*/
function unregister(name) {
if (name && formats[name]) {
delete formats[name];
}
return formats;
}
function matchesUnInheritedFormatSelector(node, name) {
var formatList = get(name);
if (formatList) {
for (var i = 0; i < formatList.length; i++) {
if (formatList[i].inherit === false && dom.is(node, formatList[i].selector)) {
return true;
}
}
}
return false;
}
function getTextDecoration(node) {
var decoration;
ed.dom.getParent(node, function(n) {
decoration = ed.dom.getStyle(n, 'text-decoration');
return decoration && decoration !== 'none';
});
return decoration;
}
function processUnderlineAndColor(node) {
var textDecoration;
if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) {
textDecoration = getTextDecoration(node.parentNode);
if (ed.dom.getStyle(node, 'color') && textDecoration) {
ed.dom.setStyle(node, 'text-decoration', textDecoration);
} else if (ed.dom.getStyle(node, 'text-decoration') === textDecoration) {
ed.dom.setStyle(node, 'text-decoration', null);
}
}
}
/**
* Applies the specified format to the current selection or specified node.
*
* @method apply
* @param {String} name Name of format to apply.
* @param {Object} vars Optional list of variables to replace within format before applying it.
* @param {Node} node Optional node to apply the format to defaults to current selection.
*/
function apply(name, vars, node) {
var formatList = get(name), format = formatList[0], bookmark, rng, isCollapsed = !node && selection.isCollapsed();
function setElementFormat(elm, fmt) {
fmt = fmt || format;
if (elm) {
if (fmt.onformat) {
fmt.onformat(elm, fmt, vars, node);
}
each(fmt.styles, function(value, name) {
dom.setStyle(elm, name, replaceVars(value, vars));
});
// Needed for the WebKit span spam bug
// TODO: Remove this once WebKit/Blink fixes this
if (fmt.styles) {
var styleVal = dom.getAttrib(elm, 'style');
if (styleVal) {
elm.setAttribute('data-mce-style', styleVal);
}
}
each(fmt.attributes, function(value, name) {
dom.setAttrib(elm, name, replaceVars(value, vars));
});
each(fmt.classes, function(value) {
value = replaceVars(value, vars);
if (!dom.hasClass(elm, value)) {
dom.addClass(elm, value);
}
});
}
}
function applyNodeStyle(formatList, node) {
var found = false;
if (!format.selector) {
return false;
}
// Look for matching formats
each(formatList, function(format) {
// Check collapsed state if it exists
if ('collapsed' in format && format.collapsed !== isCollapsed) {
return;
}
if (dom.is(node, format.selector) && !isCaretNode(node)) {
setElementFormat(node, format);
found = true;
return false;
}
});
return found;
}
// This converts: <p>[a</p><p>]b</p> -> <p>[a]</p><p>b</p>
function adjustSelectionToVisibleSelection() {
function findSelectionEnd(start, end) {
var walker = new TreeWalker(end);
for (node = walker.prev2(); node; node = walker.prev2()) {
if (node.nodeType == 3 && node.data.length > 0) {
return node;
}
if (node.childNodes.length > 1 || node == start || node.tagName == 'BR') {
return node;
}
}
}
// Adjust selection so that a end container with a end offset of zero is not included in the selection
// as this isn't visible to the user.
var rng = ed.selection.getRng();
var start = rng.startContainer;
var end = rng.endContainer;
if (start != end && rng.endOffset === 0) {
var newEnd = findSelectionEnd(start, end);
var endOffset = newEnd.nodeType == 3 ? newEnd.data.length : newEnd.childNodes.length;
rng.setEnd(newEnd, endOffset);
}
return rng;
}
function applyRngStyle(rng, bookmark, node_specific) {
var newWrappers = [], wrapName, wrapElm, contentEditable = true;
// Setup wrapper element
wrapName = format.inline || format.block;
wrapElm = dom.create(wrapName);
setElementFormat(wrapElm);
rangeUtils.walk(rng, function(nodes) {
var currentWrapElm;
/**
* Process a list of nodes wrap them.
*/
function process(node) {
var nodeName, parentName, hasContentEditableState, lastContentEditable;
lastContentEditable = contentEditable;
nodeName = node.nodeName.toLowerCase();
parentName = node.parentNode.nodeName.toLowerCase();
// Node has a contentEditable value
if (node.nodeType === 1 && getContentEditable(node)) {
lastContentEditable = contentEditable;
contentEditable = getContentEditable(node) === "true";
hasContentEditableState = true; // We don't want to wrap the container only it's children
}
// Stop wrapping on br elements
if (isEq(nodeName, 'br')) {
currentWrapElm = 0;
// Remove any br elements when we wrap things
if (format.block) {
dom.remove(node);
}
return;
}
// If node is wrapper type
if (format.wrapper && matchNode(node, name, vars)) {
currentWrapElm = 0;
return;
}
// Can we rename the block
// TODO: Break this if up, too complex
if (contentEditable && !hasContentEditableState && format.block &&
!format.wrapper && isTextBlock(nodeName) && isValid(parentName, wrapName)) {
node = dom.rename(node, wrapName);
setElementFormat(node);
newWrappers.push(node);
currentWrapElm = 0;
return;
}
// Handle selector patterns
if (format.selector) {
var found = applyNodeStyle(formatList, node);
// Continue processing if a selector match wasn't found and a inline element is defined
if (!format.inline || found) {
currentWrapElm = 0;
return;
}
}
// Is it valid to wrap this item
// TODO: Break this if up, too complex
if (contentEditable && !hasContentEditableState && isValid(wrapName, nodeName) && isValid(parentName, wrapName) &&
!(!node_specific && node.nodeType === 3 &&
node.nodeValue.length === 1 &&
node.nodeValue.charCodeAt(0) === 65279) &&
!isCaretNode(node) &&
(!format.inline || !isBlock(node))) {
// Start wrapping
if (!currentWrapElm) {
// Wrap the node
currentWrapElm = dom.clone(wrapElm, FALSE);
node.parentNode.insertBefore(currentWrapElm, node);
newWrappers.push(currentWrapElm);
}
currentWrapElm.appendChild(node);
} else {
// Start a new wrapper for possible children
currentWrapElm = 0;
each(grep(node.childNodes), process);
if (hasContentEditableState) {
contentEditable = lastContentEditable; // Restore last contentEditable state from stack
}
// End the last wrapper
currentWrapElm = 0;
}
}
// Process siblings from range
each(nodes, process);
});
// Apply formats to links as well to get the color of the underline to change as well
if (format.links === true) {
each(newWrappers, function(node) {
function process(node) {
if (node.nodeName === 'A') {
setElementFormat(node, format);
}
each(grep(node.childNodes), process);
}
process(node);
});
}
// Cleanup
each(newWrappers, function(node) {
var childCount;
function getChildCount(node) {
var count = 0;
each(node.childNodes, function(node) {
if (!isWhiteSpaceNode(node) && !isBookmarkNode(node)) {
count++;
}
});
return count;
}
function mergeStyles(node) {
var child, clone;
each(node.childNodes, function(node) {
if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) {
child = node;
return FALSE; // break loop
}
});
// If child was found and of the same type as the current node
if (child && !isBookmarkNode(child) && matchName(child, format)) {
clone = dom.clone(child, FALSE);
setElementFormat(clone);
dom.replace(clone, node, TRUE);
dom.remove(child, 1);
}
return clone || node;
}
childCount = getChildCount(node);
// Remove empty nodes but only if there is multiple wrappers and they are not block
// elements so never remove single <h1></h1> since that would remove the
// current empty block element where the caret is at
if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) {
dom.remove(node, 1);
return;
}
if (format.inline || format.wrapper) {
// Merges the current node with it's children of similar type to reduce the number of elements
if (!format.exact && childCount === 1) {
node = mergeStyles(node);
}
// Remove/merge children
each(formatList, function(format) {
// Merge all children of similar type will move styles from child to parent
// this: <span style="color:red"><b><span style="color:red; font-size:10px">text</span></b></span>
// will become: <span style="color:red"><b><span style="font-size:10px">text</span></b></span>
each(dom.select(format.inline, node), function(child) {
if (isBookmarkNode(child)) {
return;
}
removeFormat(format, vars, child, format.exact ? child : null);
});
});
// Remove child if direct parent is of same type
if (matchNode(node.parentNode, name, vars)) {
dom.remove(node, 1);
node = 0;
return TRUE;
}
// Look for parent with similar style format
if (format.merge_with_parents) {
dom.getParent(node.parentNode, function(parent) {
if (matchNode(parent, name, vars)) {
dom.remove(node, 1);
node = 0;
return TRUE;
}
});
}
// Merge next and previous siblings if they are similar <b>text</b><b>text</b> becomes <b>texttext</b>
if (node && format.merge_siblings !== false) {
node = mergeSiblings(getNonWhiteSpaceSibling(node), node);
node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE));
}
}
});
}
if (getContentEditable(selection.getNode()) === "false") {
node = selection.getNode();
for (var i = 0, l = formatList.length; i < l; i++) {
if (formatList[i].ceFalseOverride && dom.is(node, formatList[i].selector)) {
setElementFormat(node, formatList[i]);
return;
}
}
return;
}
if (format) {
if (node) {
if (node.nodeType) {
if (!applyNodeStyle(formatList, node)) {
rng = dom.createRng();
rng.setStartBefore(node);
rng.setEndAfter(node);
applyRngStyle(expandRng(rng, formatList), null, true);
}
} else {
applyRngStyle(node, null, true);
}
} else {
if (!isCollapsed || !format.inline || dom.select('td[data-mce-selected],th[data-mce-selected]').length) {
// Obtain selection node before selection is unselected by applyRngStyle()
var curSelNode = ed.selection.getNode();
// If the formats have a default block and we can't find a parent block then
// start wrapping it with a DIV this is for forced_root_blocks: false
// It's kind of a hack but people should be using the default block type P since all desktop editors work that way
if (!forcedRootBlock && formatList[0].defaultBlock && !dom.getParent(curSelNode, dom.isBlock)) {
apply(formatList[0].defaultBlock);
}
// Apply formatting to selection
ed.selection.setRng(adjustSelectionToVisibleSelection());
bookmark = selection.getBookmark();
applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark);
// Colored nodes should be underlined so that the color of the underline matches the text color.
if (format.styles && (format.styles.color || format.styles.textDecoration)) {
walk(curSelNode, processUnderlineAndColor, 'childNodes');
processUnderlineAndColor(curSelNode);
}
selection.moveToBookmark(bookmark);
moveStart(selection.getRng(TRUE));
ed.nodeChanged();
} else {
performCaretAction('apply', name, vars);
}
}
Hooks.postProcess(name, ed);
}
}
/**
* Removes the specified format from the current selection or specified node.
*
* @method remove
* @param {String} name Name of format to remove.
* @param {Object} vars Optional list of variables to replace within format before removing it.
* @param {Node/Range} node Optional node or DOM range to remove the format from defaults to current selection.
*/
function remove(name, vars, node, similar) {
var formatList = get(name), format = formatList[0], bookmark, rng, contentEditable = true;
// Merges the styles for each node
function process(node) {
var children, i, l, lastContentEditable, hasContentEditableState;
// Node has a contentEditable value
if (node.nodeType === 1 && getContentEditable(node)) {
lastContentEditable = contentEditable;
contentEditable = getContentEditable(node) === "true";
hasContentEditableState = true; // We don't want to wrap the container only it's children
}
// Grab the children first since the nodelist might be changed
children = grep(node.childNodes);
// Process current node
if (contentEditable && !hasContentEditableState) {
for (i = 0, l = formatList.length; i < l; i++) {
if (removeFormat(formatList[i], vars, node, node)) {
break;
}
}
}
// Process the children
if (format.deep) {
if (children.length) {
for (i = 0, l = children.length; i < l; i++) {
process(children[i]);
}
if (hasContentEditableState) {
contentEditable = lastContentEditable; // Restore last contentEditable state from stack
}
}
}
}
function findFormatRoot(container) {
var formatRoot;
// Find format root
each(getParents(container.parentNode).reverse(), function(parent) {
var format;
// Find format root element
if (!formatRoot && parent.id != '_start' && parent.id != '_end') {
// Is the node matching the format we are looking for
format = matchNode(parent, name, vars, similar);
if (format && format.split !== false) {
formatRoot = parent;
}
}
});
return formatRoot;
}
function wrapAndSplit(formatRoot, container, target, split) {
var parent, clone, lastClone, firstClone, i, formatRootParent;
// Format root found then clone formats and split it
if (formatRoot) {
formatRootParent = formatRoot.parentNode;
for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) {
clone = dom.clone(parent, FALSE);
for (i = 0; i < formatList.length; i++) {
if (removeFormat(formatList[i], vars, clone, clone)) {
clone = 0;
break;
}
}
// Build wrapper node
if (clone) {
if (lastClone) {
clone.appendChild(lastClone);
}
if (!firstClone) {
firstClone = clone;
}
lastClone = clone;
}
}
// Never split block elements if the format is mixed
if (split && (!format.mixed || !isBlock(formatRoot))) {
container = dom.split(formatRoot, container);
}
// Wrap container in cloned formats
if (lastClone) {
target.parentNode.insertBefore(lastClone, target);
firstClone.appendChild(target);
}
}
return container;
}
function splitToFormatRoot(container) {
return wrapAndSplit(findFormatRoot(container), container, container, true);
}
function unwrap(start) {
var node = dom.get(start ? '_start' : '_end'),
out = node[start ? 'firstChild' : 'lastChild'];
// If the end is placed within the start the result will be removed
// So this checks if the out node is a bookmark node if it is it
// checks for another more suitable node
if (isBookmarkNode(out)) {
out = out[start ? 'firstChild' : 'lastChild'];
}
// Since dom.remove removes empty text nodes then we need to try to find a better node
if (out.nodeType == 3 && out.data.length === 0) {
out = start ? node.previousSibling || node.nextSibling : node.nextSibling || node.previousSibling;
}
dom.remove(node, true);
return out;
}
function removeRngStyle(rng) {
var startContainer, endContainer;
var commonAncestorContainer = rng.commonAncestorContainer;
rng = expandRng(rng, formatList, TRUE);
if (format.split) {
startContainer = getContainer(rng, TRUE);
endContainer = getContainer(rng);
if (startContainer != endContainer) {
// WebKit will render the table incorrectly if we wrap a TH or TD in a SPAN
// so let's see if we can use the first child instead
// This will happen if you triple click a table cell and use remove formatting
if (/^(TR|TH|TD)$/.test(startContainer.nodeName) && startContainer.firstChild) {
if (startContainer.nodeName == "TR") {
startContainer = startContainer.firstChild.firstChild || startContainer;
} else {
startContainer = startContainer.firstChild || startContainer;
}
}
// Try to adjust endContainer as well if cells on the same row were selected - bug #6410
if (commonAncestorContainer &&
/^T(HEAD|BODY|FOOT|R)$/.test(commonAncestorContainer.nodeName) &&
isTableCell(endContainer) && endContainer.firstChild) {
endContainer = endContainer.firstChild || endContainer;
}
if (dom.isChildOf(startContainer, endContainer) && !isBlock(endContainer) &&
!isTableCell(startContainer) && !isTableCell(endContainer)) {
startContainer = wrap(startContainer, 'span', {id: '_start', 'data-mce-type': 'bookmark'});
splitToFormatRoot(startContainer);
startContainer = unwrap(TRUE);
return;
}
// Wrap start/end nodes in span element since these might be cloned/moved
startContainer = wrap(startContainer, 'span', {id: '_start', 'data-mce-type': 'bookmark'});
endContainer = wrap(endContainer, 'span', {id: '_end', 'data-mce-type': 'bookmark'});
// Split start/end
splitToFormatRoot(startContainer);
splitToFormatRoot(endContainer);
// Unwrap start/end to get real elements again
startContainer = unwrap(TRUE);
endContainer = unwrap();
} else {
startContainer = endContainer = splitToFormatRoot(startContainer);
}
// Update range positions since they might have changed after the split operations
rng.startContainer = startContainer.parentNode ? startContainer.parentNode : startContainer;
rng.startOffset = nodeIndex(startContainer);
rng.endContainer = endContainer.parentNode ? endContainer.parentNode : endContainer;
rng.endOffset = nodeIndex(endContainer) + 1;
}
// Remove items between start/end
rangeUtils.walk(rng, function(nodes) {
each(nodes, function(node) {
process(node);
// Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined.
if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' &&
node.parentNode && getTextDecoration(node.parentNode) === 'underline') {
removeFormat({
'deep': false,
'exact': true,
'inline': 'span',
'styles': {
'textDecoration': 'underline'
}
}, null, node);
}
});
});
}
// Handle node
if (node) {
if (node.nodeType) {
rng = dom.createRng();
rng.setStartBefore(node);
rng.setEndAfter(node);
removeRngStyle(rng);
} else {
removeRngStyle(node);
}
return;
}
if (getContentEditable(selection.getNode()) === "false") {
node = selection.getNode();
for (var i = 0, l = formatList.length; i < l; i++) {
if (formatList[i].ceFalseOverride) {
if (removeFormat(formatList[i], vars, node, node)) {
break;
}
}
}
return;
}
if (!selection.isCollapsed() || !format.inline || dom.select('td[data-mce-selected],th[data-mce-selected]').length) {
bookmark = selection.getBookmark();
removeRngStyle(selection.getRng(TRUE));
selection.moveToBookmark(bookmark);
// Check if start element still has formatting then we are at: "<b>text|</b>text"
// and need to move the start into the next text node
if (format.inline && match(name, vars, selection.getStart())) {
moveStart(selection.getRng(true));
}
ed.nodeChanged();
} else {
performCaretAction('remove', name, vars, similar);
}
}
/**
* Toggles the specified format on/off.
*
* @method toggle
* @param {String} name Name of format to apply/remove.
* @param {Object} vars Optional list of variables to replace within format before applying/removing it.
* @param {Node} node Optional node to apply the format to or remove from. Defaults to current selection.
*/
function toggle(name, vars, node) {
var fmt = get(name);
if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0].toggle)) {
remove(name, vars, node);
} else {
apply(name, vars, node);
}
}
/**
* Return true/false if the specified node has the specified format.
*
* @method matchNode
* @param {Node} node Node to check the format on.
* @param {String} name Format name to check.
* @param {Object} vars Optional list of variables to replace before checking it.
* @param {Boolean} similar Match format that has similar properties.
* @return {Object} Returns the format object it matches or undefined if it doesn't match.
*/
function matchNode(node, name, vars, similar) {
var formatList = get(name), format, i, classes;
function matchItems(node, format, item_name) {
var key, value, items = format[item_name], i;
// Custom match
if (format.onmatch) {
return format.onmatch(node, format, item_name);
}
// Check all items
if (items) {
// Non indexed object
if (items.length === undef) {
for (key in items) {
if (items.hasOwnProperty(key)) {
if (item_name === 'attributes') {
value = dom.getAttrib(node, key);
} else {
value = getStyle(node, key);
}
if (similar && !value && !format.exact) {
return;
}
if ((!similar || format.exact) && !isEq(value, normalizeStyleValue(replaceVars(items[key], vars), key))) {
return;
}
}
}
} else {
// Only one match needed for indexed arrays
for (i = 0; i < items.length; i++) {
if (item_name === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i])) {
return format;
}
}
}
}
return format;
}
if (formatList && node) {
// Check each format in list
for (i = 0; i < formatList.length; i++) {
format = formatList[i];
// Name name, attributes, styles and classes
if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) {
// Match classes
if ((classes = format.classes)) {
for (i = 0; i < classes.length; i++) {
if (!dom.hasClass(node, classes[i])) {
return;
}
}
}
return format;
}
}
}
}
/**
* Matches the current selection or specified node against the specified format name.
*
* @method match
* @param {String} name Name of format to match.
* @param {Object} vars Optional list of variables to replace before checking it.
* @param {Node} node Optional node to check.
* @return {boolean} true/false if the specified selection/node matches the format.
*/
function match(name, vars, node) {
var startNode;
function matchParents(node) {
var root = dom.getRoot();
if (node === root) {
return false;
}
// Find first node with similar format settings
node = dom.getParent(node, function(node) {
if (matchesUnInheritedFormatSelector(node, name)) {
return true;
}
return node.parentNode === root || !!matchNode(node, name, vars, true);
});
// Do an exact check on the similar format element
return matchNode(node, name, vars);
}
// Check specified node
if (node) {
return matchParents(node);
}
// Check selected node
node = selection.getNode();
if (matchParents(node)) {
return TRUE;
}
// Check start node if it's different
startNode = selection.getStart();
if (startNode != node) {
if (matchParents(startNode)) {
return TRUE;
}
}
return FALSE;
}
/**
* Matches the current selection against the array of formats and returns a new array with matching formats.
*
* @method matchAll
* @param {Array} names Name of format to match.
* @param {Object} vars Optional list of variables to replace before checking it.
* @return {Array} Array with matched formats.
*/
function matchAll(names, vars) {
var startElement, matchedFormatNames = [], checkedMap = {};
// Check start of selection for formats
startElement = selection.getStart();
dom.getParent(startElement, function(node) {
var i, name;
for (i = 0; i < names.length; i++) {
name = names[i];
if (!checkedMap[name] && matchNode(node, name, vars)) {
checkedMap[name] = true;
matchedFormatNames.push(name);
}
}
}, dom.getRoot());
return matchedFormatNames;
}
/**
* Returns true/false if the specified format can be applied to the current selection or not. It
* will currently only check the state for selector formats, it returns true on all other format types.
*
* @method canApply
* @param {String} name Name of format to check.
* @return {boolean} true/false if the specified format can be applied to the current selection/node.
*/
function canApply(name) {
var formatList = get(name), startNode, parents, i, x, selector;
if (formatList) {
startNode = selection.getStart();
parents = getParents(startNode);
for (x = formatList.length - 1; x >= 0; x--) {
selector = formatList[x].selector;
// Format is not selector based then always return TRUE
// Is it has a defaultBlock then it's likely it can be applied for example align on a non block element line
if (!selector || formatList[x].defaultBlock) {
return TRUE;
}
for (i = parents.length - 1; i >= 0; i--) {
if (dom.is(parents[i], selector)) {
return TRUE;
}
}
}
}
return FALSE;
}
/**
* Executes the specified callback when the current selection matches the formats or not.
*
* @method formatChanged
* @param {String} formats Comma separated list of formats to check for.
* @param {function} callback Callback with state and args when the format is changed/toggled on/off.
* @param {Boolean} similar True/false state if the match should handle similar or exact formats.
*/
function formatChanged(formats, callback, similar) {
var currentFormats;
// Setup format node change logic
if (!formatChangeData) {
formatChangeData = {};
currentFormats = {};
ed.on('NodeChange', function(e) {
var parents = getParents(e.element), matchedFormats = {};
// Ignore bogus nodes like the <a> tag created by moveStart()
parents = Tools.grep(parents, function(node) {
return node.nodeType == 1 && !node.getAttribute('data-mce-bogus');
});
// Check for new formats
each(formatChangeData, function(callbacks, format) {
each(parents, function(node) {
if (matchNode(node, format, {}, callbacks.similar)) {
if (!currentFormats[format]) {
// Execute callbacks
each(callbacks, function(callback) {
callback(true, {node: node, format: format, parents: parents});
});
currentFormats[format] = callbacks;
}
matchedFormats[format] = callbacks;
return false;
}
if (matchesUnInheritedFormatSelector(node, format)) {
return false;
}
});
});
// Check if current formats still match
each(currentFormats, function(callbacks, format) {
if (!matchedFormats[format]) {
delete currentFormats[format];
each(callbacks, function(callback) {
callback(false, {node: e.element, format: format, parents: parents});
});
}
});
});
}
// Add format listeners
each(formats.split(','), function(format) {
if (!formatChangeData[format]) {
formatChangeData[format] = [];
formatChangeData[format].similar = similar;
}
formatChangeData[format].push(callback);
});
return this;
}
/**
* Returns a preview css text for the specified format.
*
* @method getCssText
* @param {String/Object} format Format to generate preview css text for.
* @return {String} Css text for the specified format.
* @example
* var cssText1 = editor.formatter.getCssText('bold');
* var cssText2 = editor.formatter.getCssText({inline: 'b'});
*/
function getCssText(format) {
return Preview.getCssText(ed, format);
}
// Expose to public
extend(this, {
get: get,
register: register,
unregister: unregister,
apply: apply,
remove: remove,
toggle: toggle,
match: match,
matchAll: matchAll,
matchNode: matchNode,
canApply: canApply,
formatChanged: formatChanged,
getCssText: getCssText
});
// Initialize
defaultFormats();
addKeyboardShortcuts();
ed.on('BeforeGetContent', function(e) {
if (markCaretContainersBogus && e.format != 'raw') {
markCaretContainersBogus();
}
});
ed.on('mouseup keydown', function(e) {
if (disableCaretContainer) {
disableCaretContainer(e);
}
});
// Private functions
/**
* Checks if the specified nodes name matches the format inline/block or selector.
*
* @private
* @param {Node} node Node to match against the specified format.
* @param {Object} format Format object o match with.
* @return {boolean} true/false if the format matches.
*/
function matchName(node, format) {
// Check for inline match
if (isEq(node, format.inline)) {
return TRUE;
}
// Check for block match
if (isEq(node, format.block)) {
return TRUE;
}
// Check for selector match
if (format.selector) {
return node.nodeType == 1 && dom.is(node, format.selector);
}
}
/**
* Compares two string/nodes regardless of their case.
*
* @private
* @param {String/Node} str1 Node or string to compare.
* @param {String/Node} str2 Node or string to compare.
* @return {boolean} True/false if they match.
*/
function isEq(str1, str2) {
str1 = str1 || '';
str2 = str2 || '';
str1 = '' + (str1.nodeName || str1);
str2 = '' + (str2.nodeName || str2);
return str1.toLowerCase() == str2.toLowerCase();
}
/**
* Returns the style by name on the specified node. This method modifies the style
* contents to make it more easy to match. This will resolve a few browser issues.
*
* @private
* @param {Node} node to get style from.
* @param {String} name Style name to get.
* @return {String} Style item value.
*/
function getStyle(node, name) {
return normalizeStyleValue(dom.getStyle(node, name), name);
}
/**
* Normalize style value by name. This method modifies the style contents
* to make it more easy to match. This will resolve a few browser issues.
*
* @private
* @param {String} value Value to get style from.
* @param {String} name Style name to get.
* @return {String} Style item value.
*/
function normalizeStyleValue(value, name) {
// Force the format to hex
if (name == 'color' || name == 'backgroundColor') {
value = dom.toHex(value);
}
// Opera will return bold as 700
if (name == 'fontWeight' && value == 700) {
value = 'bold';
}
// Normalize fontFamily so "'Font name', Font" becomes: "Font name,Font"
if (name == 'fontFamily') {
value = value.replace(/[\'\"]/g, '').replace(/,\s+/g, ',');
}
return '' + value;
}
/**
* Replaces variables in the value. The variable format is %var.
*
* @private
* @param {String} value Value to replace variables in.
* @param {Object} vars Name/value array with variables to replace.
* @return {String} New value with replaced variables.
*/
function replaceVars(value, vars) {
if (typeof value != "string") {
value = value(vars);
} else if (vars) {
value = value.replace(/%(\w+)/g, function(str, name) {
return vars[name] || str;
});
}
return value;
}
function isWhiteSpaceNode(node) {
return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue);
}
function wrap(node, name, attrs) {
var wrapper = dom.create(name, attrs);
node.parentNode.insertBefore(wrapper, node);
wrapper.appendChild(node);
return wrapper;
}
/**
* Expands the specified range like object to depending on format.
*
* For example on block formats it will move the start/end position
* to the beginning of the current block.
*
* @private
* @param {Object} rng Range like object.
* @param {Array} format Array with formats to expand by.
* @param {Boolean} remove
* @return {Object} Expanded range like object.
*/
function expandRng(rng, format, remove) {
var lastIdx, leaf, endPoint,
startContainer = rng.startContainer,
startOffset = rng.startOffset,
endContainer = rng.endContainer,
endOffset = rng.endOffset;
// This function walks up the tree if there is no siblings before/after the node
function findParentContainer(start) {
var container, parent, sibling, siblingName, root;
container = parent = start ? startContainer : endContainer;
siblingName = start ? 'previousSibling' : 'nextSibling';
root = dom.getRoot();
function isBogusBr(node) {
return node.nodeName == "BR" && node.getAttribute('data-mce-bogus') && !node.nextSibling;
}
// If it's a text node and the offset is inside the text
if (container.nodeType == 3 && !isWhiteSpaceNode(container)) {
if (start ? startOffset > 0 : endOffset < container.nodeValue.length) {
return container;
}
}
/*eslint no-constant-condition:0 */
while (true) {
// Stop expanding on block elements
if (!format[0].block_expand && isBlock(parent)) {
return parent;
}
// Walk left/right
for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) {
if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling) && !isBogusBr(sibling)) {
return parent;
}
}
// Check if we can move up are we at root level or body level
if (parent == root || parent.parentNode == root) {
container = parent;
break;
}
parent = parent.parentNode;
}
return container;
}
// This function walks down the tree to find the leaf at the selection.
// The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node.
function findLeaf(node, offset) {
if (offset === undef) {
offset = node.nodeType === 3 ? node.length : node.childNodes.length;
}
while (node && node.hasChildNodes()) {
node = node.childNodes[offset];
if (node) {
offset = node.nodeType === 3 ? node.length : node.childNodes.length;
}
}
return {node: node, offset: offset};
}
// If index based start position then resolve it
if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) {
lastIdx = startContainer.childNodes.length - 1;
startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset];
if (startContainer.nodeType == 3) {
startOffset = 0;
}
}
// If index based end position then resolve it
if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) {
lastIdx = endContainer.childNodes.length - 1;
endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1];
if (endContainer.nodeType == 3) {
endOffset = endContainer.nodeValue.length;
}
}
// Expands the node to the closes contentEditable false element if it exists
function findParentContentEditable(node) {
var parent = node;
while (parent) {
if (parent.nodeType === 1 && getContentEditable(parent)) {
return getContentEditable(parent) === "false" ? parent : node;
}
parent = parent.parentNode;
}
return node;
}
function findWordEndPoint(container, offset, start) {
var walker, node, pos, lastTextNode;
function findSpace(node, offset) {
var pos, pos2, str = node.nodeValue;
if (typeof offset == "undefined") {
offset = start ? str.length : 0;
}
if (start) {
pos = str.lastIndexOf(' ', offset);
pos2 = str.lastIndexOf('\u00a0', offset);
pos = pos > pos2 ? pos : pos2;
// Include the space on remove to avoid tag soup
if (pos !== -1 && !remove) {
pos++;
}
} else {
pos = str.indexOf(' ', offset);
pos2 = str.indexOf('\u00a0', offset);
pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2;
}
return pos;
}
if (container.nodeType === 3) {
pos = findSpace(container, offset);
if (pos !== -1) {
return {container: container, offset: pos};
}
lastTextNode = container;
}
// Walk the nodes inside the block
walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody());
while ((node = walker[start ? 'prev' : 'next']())) {
if (node.nodeType === 3) {
lastTextNode = node;
pos = findSpace(node);
if (pos !== -1) {
return {container: node, offset: pos};
}
} else if (isBlock(node)) {
break;
}
}
if (lastTextNode) {
if (start) {
offset = 0;
} else {
offset = lastTextNode.length;
}
return {container: lastTextNode, offset: offset};
}
}
function findSelectorEndPoint(container, sibling_name) {
var parents, i, y, curFormat;
if (container.nodeType == 3 && container.nodeValue.length === 0 && container[sibling_name]) {
container = container[sibling_name];
}
parents = getParents(container);
for (i = 0; i < parents.length; i++) {
for (y = 0; y < format.length; y++) {
curFormat = format[y];
// If collapsed state is set then skip formats that doesn't match that
if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) {
continue;
}
if (dom.is(parents[i], curFormat.selector)) {
return parents[i];
}
}
}
return container;
}
function findBlockEndPoint(container, sibling_name) {
var node, root = dom.getRoot();
// Expand to block of similar type
if (!format[0].wrapper) {
node = dom.getParent(container, format[0].block, root);
}
// Expand to first wrappable block element or any block element
if (!node) {
node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, function(node) {
// Fixes #6183 where it would expand to editable parent element in inline mode
return node != root && isTextBlock(node);
});
}
// Exclude inner lists from wrapping
if (node && format[0].wrapper) {
node = getParents(node, 'ul,ol').reverse()[0] || node;
}
// Didn't find a block element look for first/last wrappable element
if (!node) {
node = container;
while (node[sibling_name] && !isBlock(node[sibling_name])) {
node = node[sibling_name];
// Break on BR but include it will be removed later on
// we can't remove it now since we need to check if it can be wrapped
if (isEq(node, 'br')) {
break;
}
}
}
return node || container;
}
// Expand to closest contentEditable element
startContainer = findParentContentEditable(startContainer);
endContainer = findParentContentEditable(endContainer);
// Exclude bookmark nodes if possible
if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) {
startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode;
startContainer = startContainer.nextSibling || startContainer;
if (startContainer.nodeType == 3) {
startOffset = 0;
}
}
if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) {
endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode;
endContainer = endContainer.previousSibling || endContainer;
if (endContainer.nodeType == 3) {
endOffset = endContainer.length;
}
}
if (format[0].inline) {
if (rng.collapsed) {
// Expand left to closest word boundary
endPoint = findWordEndPoint(startContainer, startOffset, true);
if (endPoint) {
startContainer = endPoint.container;
startOffset = endPoint.offset;
}
// Expand right to closest word boundary
endPoint = findWordEndPoint(endContainer, endOffset);
if (endPoint) {
endContainer = endPoint.container;
endOffset = endPoint.offset;
}
}
// Avoid applying formatting to a trailing space.
leaf = findLeaf(endContainer, endOffset);
if (leaf.node) {
while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) {
leaf = findLeaf(leaf.node.previousSibling);
}
if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 &&
leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') {
if (leaf.offset > 1) {
endContainer = leaf.node;
endContainer.splitText(leaf.offset - 1);
}
}
}
}
// Move start/end point up the tree if the leaves are sharp and if we are in different containers
// Example * becomes !: !<p><b><i>*text</i><i>text*</i></b></p>!
// This will reduce the number of wrapper elements that needs to be created
// Move start point up the tree
if (format[0].inline || format[0].block_expand) {
if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) {
startContainer = findParentContainer(true);
}
if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) {
endContainer = findParentContainer();
}
}
// Expand start/end container to matching selector
if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) {
// Find new startContainer/endContainer if there is better one
startContainer = findSelectorEndPoint(startContainer, 'previousSibling');
endContainer = findSelectorEndPoint(endContainer, 'nextSibling');
}
// Expand start/end container to matching block element or text node
if (format[0].block || format[0].selector) {
// Find new startContainer/endContainer if there is better one
startContainer = findBlockEndPoint(startContainer, 'previousSibling');
endContainer = findBlockEndPoint(endContainer, 'nextSibling');
// Non block element then try to expand up the leaf
if (format[0].block) {
if (!isBlock(startContainer)) {
startContainer = findParentContainer(true);
}
if (!isBlock(endContainer)) {
endContainer = findParentContainer();
}
}
}
// Setup index for startContainer
if (startContainer.nodeType == 1) {
startOffset = nodeIndex(startContainer);
startContainer = startContainer.parentNode;
}
// Setup index for endContainer
if (endContainer.nodeType == 1) {
endOffset = nodeIndex(endContainer) + 1;
endContainer = endContainer.parentNode;
}
// Return new range like object
return {
startContainer: startContainer,
startOffset: startOffset,
endContainer: endContainer,
endOffset: endOffset
};
}
function isColorFormatAndAnchor(node, format) {
return format.links && node.tagName == 'A';
}
/**
* Removes the specified format for the specified node. It will also remove the node if it doesn't have
* any attributes if the format specifies it to do so.
*
* @private
* @param {Object} format Format object with items to remove from node.
* @param {Object} vars Name/value object with variables to apply to format.
* @param {Node} node Node to remove the format styles on.
* @param {Node} compare_node Optional compare node, if specified the styles will be compared to that node.
* @return {Boolean} True/false if the node was removed or not.
*/
function removeFormat(format, vars, node, compare_node) {
var i, attrs, stylesModified;
// Check if node matches format
if (!matchName(node, format) && !isColorFormatAndAnchor(node, format)) {
return FALSE;
}
// Should we compare with format attribs and styles
if (format.remove != 'all') {
// Remove styles
each(format.styles, function(value, name) {
value = normalizeStyleValue(replaceVars(value, vars), name);
// Indexed array
if (typeof name === 'number') {
name = value;
compare_node = 0;
}
if (format.remove_similar || (!compare_node || isEq(getStyle(compare_node, name), value))) {
dom.setStyle(node, name, '');
}
stylesModified = 1;
});
// Remove style attribute if it's empty
if (stylesModified && dom.getAttrib(node, 'style') === '') {
node.removeAttribute('style');
node.removeAttribute('data-mce-style');
}
// Remove attributes
each(format.attributes, function(value, name) {
var valueOut;
value = replaceVars(value, vars);
// Indexed array
if (typeof name === 'number') {
name = value;
compare_node = 0;
}
if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) {
// Keep internal classes
if (name == 'class') {
value = dom.getAttrib(node, name);
if (value) {
// Build new class value where everything is removed except the internal prefixed classes
valueOut = '';
each(value.split(/\s+/), function(cls) {
if (/mce\-\w+/.test(cls)) {
valueOut += (valueOut ? ' ' : '') + cls;
}
});
// We got some internal classes left
if (valueOut) {
dom.setAttrib(node, name, valueOut);
return;
}
}
}
// IE6 has a bug where the attribute doesn't get removed correctly
if (name == "class") {
node.removeAttribute('className');
}
// Remove mce prefixed attributes
if (MCE_ATTR_RE.test(name)) {
node.removeAttribute('data-mce-' + name);
}
node.removeAttribute(name);
}
});
// Remove classes
each(format.classes, function(value) {
value = replaceVars(value, vars);
if (!compare_node || dom.hasClass(compare_node, value)) {
dom.removeClass(node, value);
}
});
// Check for non internal attributes
attrs = dom.getAttribs(node);
for (i = 0; i < attrs.length; i++) {
var attrName = attrs[i].nodeName;
if (attrName.indexOf('_') !== 0 && attrName.indexOf('data-') !== 0) {
return FALSE;
}
}
}
// Remove the inline child if it's empty for example <b> or <span>
if (format.remove != 'none') {
removeNode(node, format);
return TRUE;
}
}
/**
* Removes the node and wrap it's children in paragraphs before doing so or
* appends BR elements to the beginning/end of the block element if forcedRootBlocks is disabled.
*
* If the div in the node below gets removed:
* text<div>text</div>text
*
* Output becomes:
* text<div><br />text<br /></div>text
*
* So when the div is removed the result is:
* text<br />text<br />text
*
* @private
* @param {Node} node Node to remove + apply BR/P elements to.
* @param {Object} format Format rule.
* @return {Node} Input node.
*/
function removeNode(node, format) {
var parentNode = node.parentNode, rootBlockElm;
function find(node, next, inc) {
node = getNonWhiteSpaceSibling(node, next, inc);
return !node || (node.nodeName == 'BR' || isBlock(node));
}
if (format.block) {
if (!forcedRootBlock) {
// Append BR elements if needed before we remove the block
if (isBlock(node) && !isBlock(parentNode)) {
if (!find(node, FALSE) && !find(node.firstChild, TRUE, 1)) {
node.insertBefore(dom.create('br'), node.firstChild);
}
if (!find(node, TRUE) && !find(node.lastChild, FALSE, 1)) {
node.appendChild(dom.create('br'));
}
}
} else {
// Wrap the block in a forcedRootBlock if we are at the root of document
if (parentNode == dom.getRoot()) {
if (!format.list_block || !isEq(node, format.list_block)) {
each(grep(node.childNodes), function(node) {
if (isValid(forcedRootBlock, node.nodeName.toLowerCase())) {
if (!rootBlockElm) {
rootBlockElm = wrap(node, forcedRootBlock);
dom.setAttribs(rootBlockElm, ed.settings.forced_root_block_attrs);
} else {
rootBlockElm.appendChild(node);
}
} else {
rootBlockElm = 0;
}
});
}
}
}
}
// Never remove nodes that isn't the specified inline element if a selector is specified too
if (format.selector && format.inline && !isEq(format.inline, node)) {
return;
}
dom.remove(node, 1);
}
/**
* Returns the next/previous non whitespace node.
*
* @private
* @param {Node} node Node to start at.
* @param {boolean} next (Optional) Include next or previous node defaults to previous.
* @param {boolean} inc (Optional) Include the current node in checking. Defaults to false.
* @return {Node} Next or previous node or undefined if it wasn't found.
*/
function getNonWhiteSpaceSibling(node, next, inc) {
if (node) {
next = next ? 'nextSibling' : 'previousSibling';
for (node = inc ? node : node[next]; node; node = node[next]) {
if (node.nodeType == 1 || !isWhiteSpaceNode(node)) {
return node;
}
}
}
}
/**
* Merges the next/previous sibling element if they match.
*
* @private
* @param {Node} prev Previous node to compare/merge.
* @param {Node} next Next node to compare/merge.
* @return {Node} Next node if we didn't merge and prev node if we did.
*/
function mergeSiblings(prev, next) {
var sibling, tmpSibling, elementUtils = new ElementUtils(dom);
function findElementSibling(node, sibling_name) {
for (sibling = node; sibling; sibling = sibling[sibling_name]) {
if (sibling.nodeType == 3 && sibling.nodeValue.length !== 0) {
return node;
}
if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) {
return sibling;
}
}
return node;
}
// Check if next/prev exists and that they are elements
if (prev && next) {
// If previous sibling is empty then jump over it
prev = findElementSibling(prev, 'previousSibling');
next = findElementSibling(next, 'nextSibling');
// Compare next and previous nodes
if (elementUtils.compare(prev, next)) {
// Append nodes between
for (sibling = prev.nextSibling; sibling && sibling != next;) {
tmpSibling = sibling;
sibling = sibling.nextSibling;
prev.appendChild(tmpSibling);
}
// Remove next node
dom.remove(next);
// Move children into prev node
each(grep(next.childNodes), function(node) {
prev.appendChild(node);
});
return prev;
}
}
return next;
}
function getContainer(rng, start) {
var container, offset, lastIdx;
container = rng[start ? 'startContainer' : 'endContainer'];
offset = rng[start ? 'startOffset' : 'endOffset'];
if (container.nodeType == 1) {
lastIdx = container.childNodes.length - 1;
if (!start && offset) {
offset--;
}
container = container.childNodes[offset > lastIdx ? lastIdx : offset];
}
// If start text node is excluded then walk to the next node
if (container.nodeType === 3 && start && offset >= container.nodeValue.length) {
container = new TreeWalker(container, ed.getBody()).next() || container;
}
// If end text node is excluded then walk to the previous node
if (container.nodeType === 3 && !start && offset === 0) {
container = new TreeWalker(container, ed.getBody()).prev() || container;
}
return container;
}
function performCaretAction(type, name, vars, similar) {
var caretContainerId = '_mce_caret', debug = ed.settings.caret_debug;
// Creates a caret container bogus element
function createCaretContainer(fill) {
var caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true, style: debug ? 'color:red' : ''});
if (fill) {
caretContainer.appendChild(ed.getDoc().createTextNode(INVISIBLE_CHAR));
}
return caretContainer;
}
function isCaretContainerEmpty(node, nodes) {
while (node) {
if ((node.nodeType === 3 && node.nodeValue !== INVISIBLE_CHAR) || node.childNodes.length > 1) {
return false;
}
// Collect nodes
if (nodes && node.nodeType === 1) {
nodes.push(node);
}
node = node.firstChild;
}
return true;
}
// Returns any parent caret container element
function getParentCaretContainer(node) {
while (node) {
if (node.id === caretContainerId) {
return node;
}
node = node.parentNode;
}
}
// Finds the first text node in the specified node
function findFirstTextNode(node) {
var walker;
if (node) {
walker = new TreeWalker(node, node);
for (node = walker.current(); node; node = walker.next()) {
if (node.nodeType === 3) {
return node;
}
}
}
}
// Removes the caret container for the specified node or all on the current document
function removeCaretContainer(node, move_caret) {
var child, rng;
if (!node) {
node = getParentCaretContainer(selection.getStart());
if (!node) {
while ((node = dom.get(caretContainerId))) {
removeCaretContainer(node, false);
}
}
} else {
rng = selection.getRng(true);
if (isCaretContainerEmpty(node)) {
if (move_caret !== false) {
rng.setStartBefore(node);
rng.setEndBefore(node);
}
dom.remove(node);
} else {
child = findFirstTextNode(node);
if (child.nodeValue.charAt(0) === INVISIBLE_CHAR) {
child.deleteData(0, 1);
// Fix for bug #6976
if (rng.startContainer == child && rng.startOffset > 0) {
rng.setStart(child, rng.startOffset - 1);
}
if (rng.endContainer == child && rng.endOffset > 0) {
rng.setEnd(child, rng.endOffset - 1);
}
}
dom.remove(node, 1);
}
selection.setRng(rng);
}
}
// Applies formatting to the caret position
function applyCaretFormat() {
var rng, caretContainer, textNode, offset, bookmark, container, text;
rng = selection.getRng(true);
offset = rng.startOffset;
container = rng.startContainer;
text = container.nodeValue;
caretContainer = getParentCaretContainer(selection.getStart());
if (caretContainer) {
textNode = findFirstTextNode(caretContainer);
}
// Expand to word is caret is in the middle of a text node and the char before/after is a alpha numeric character
if (text && offset > 0 && offset < text.length && /\w/.test(text.charAt(offset)) && /\w/.test(text.charAt(offset - 1))) {
// Get bookmark of caret position
bookmark = selection.getBookmark();
// Collapse bookmark range (WebKit)
rng.collapse(true);
// Expand the range to the closest word and split it at those points
rng = expandRng(rng, get(name));
rng = rangeUtils.split(rng);
// Apply the format to the range
apply(name, vars, rng);
// Move selection back to caret position
selection.moveToBookmark(bookmark);
} else {
if (!caretContainer || textNode.nodeValue !== INVISIBLE_CHAR) {
caretContainer = createCaretContainer(true);
textNode = caretContainer.firstChild;
rng.insertNode(caretContainer);
offset = 1;
apply(name, vars, caretContainer);
} else {
apply(name, vars, caretContainer);
}
// Move selection to text node
selection.setCursorLocation(textNode, offset);
}
}
function removeCaretFormat() {
var rng = selection.getRng(true), container, offset, bookmark,
hasContentAfter, node, formatNode, parents = [], i, caretContainer;
container = rng.startContainer;
offset = rng.startOffset;
node = container;
if (container.nodeType == 3) {
if (offset != container.nodeValue.length) {
hasContentAfter = true;
}
node = node.parentNode;
}
while (node) {
if (matchNode(node, name, vars, similar)) {
formatNode = node;
break;
}
if (node.nextSibling) {
hasContentAfter = true;
}
parents.push(node);
node = node.parentNode;
}
// Node doesn't have the specified format
if (!formatNode) {
return;
}
// Is there contents after the caret then remove the format on the element
if (hasContentAfter) {
// Get bookmark of caret position
bookmark = selection.getBookmark();
// Collapse bookmark range (WebKit)
rng.collapse(true);
// Expand the range to the closest word and split it at those points
rng = expandRng(rng, get(name), true);
rng = rangeUtils.split(rng);
// Remove the format from the range
remove(name, vars, rng);
// Move selection back to caret position
selection.moveToBookmark(bookmark);
} else {
caretContainer = createCaretContainer();
node = caretContainer;
for (i = parents.length - 1; i >= 0; i--) {
node.appendChild(dom.clone(parents[i], false));
node = node.firstChild;
}
// Insert invisible character into inner most format element
node.appendChild(dom.doc.createTextNode(INVISIBLE_CHAR));
node = node.firstChild;
var block = dom.getParent(formatNode, isTextBlock);
if (block && dom.isEmpty(block)) {
// Replace formatNode with caretContainer when removing format from empty block like <p><b>|</b></p>
formatNode.parentNode.replaceChild(caretContainer, formatNode);
} else {
// Insert caret container after the formatted node
dom.insertAfter(caretContainer, formatNode);
}
// Move selection to text node
selection.setCursorLocation(node, 1);
// If the formatNode is empty, we can remove it safely.
if (dom.isEmpty(formatNode)) {
dom.remove(formatNode);
}
}
}
// Checks if the parent caret container node isn't empty if that is the case it
// will remove the bogus state on all children that isn't empty
function unmarkBogusCaretParents() {
var caretContainer;
caretContainer = getParentCaretContainer(selection.getStart());
if (caretContainer && !dom.isEmpty(caretContainer)) {
walk(caretContainer, function(node) {
if (node.nodeType == 1 && node.id !== caretContainerId && !dom.isEmpty(node)) {
dom.setAttrib(node, 'data-mce-bogus', null);
}
}, 'childNodes');
}
}
// Only bind the caret events once
if (!ed._hasCaretEvents) {
// Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements
markCaretContainersBogus = function() {
var nodes = [], i;
if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) {
// Mark children
i = nodes.length;
while (i--) {
dom.setAttrib(nodes[i], 'data-mce-bogus', '1');
}
}
};
disableCaretContainer = function(e) {
var keyCode = e.keyCode;
removeCaretContainer();
// Remove caret container if it's empty
if (keyCode == 8 && selection.isCollapsed() && selection.getStart().innerHTML == INVISIBLE_CHAR) {
removeCaretContainer(getParentCaretContainer(selection.getStart()));
}
// Remove caret container on keydown and it's left/right arrow keys
if (keyCode == 37 || keyCode == 39) {
removeCaretContainer(getParentCaretContainer(selection.getStart()));
}
unmarkBogusCaretParents();
};
// Remove bogus state if they got filled by contents using editor.selection.setContent
ed.on('SetContent', function(e) {
if (e.selection) {
unmarkBogusCaretParents();
}
});
ed._hasCaretEvents = true;
}
// Do apply or remove caret format
if (type == "apply") {
applyCaretFormat();
} else {
removeCaretFormat();
}
}
/**
* Moves the start to the first suitable text node.
*/
function moveStart(rng) {
var container = rng.startContainer,
offset = rng.startOffset, isAtEndOfText,
walker, node, nodes, tmpNode;
if (rng.startContainer == rng.endContainer) {
if (isInlineBlock(rng.startContainer.childNodes[rng.startOffset])) {
return;
}
}
// Convert text node into index if possible
if (container.nodeType == 3 && offset >= container.nodeValue.length) {
// Get the parent container location and walk from there
offset = nodeIndex(container);
container = container.parentNode;
isAtEndOfText = true;
}
// Move startContainer/startOffset in to a suitable node
if (container.nodeType == 1) {
nodes = container.childNodes;
container = nodes[Math.min(offset, nodes.length - 1)];
walker = new TreeWalker(container, dom.getParent(container, dom.isBlock));
// If offset is at end of the parent node walk to the next one
if (offset > nodes.length - 1 || isAtEndOfText) {
walker.next();
}
for (node = walker.current(); node; node = walker.next()) {
if (node.nodeType == 3 && !isWhiteSpaceNode(node)) {
// IE has a "neat" feature where it moves the start node into the closest element
// we can avoid this by inserting an element before it and then remove it after we set the selection
tmpNode = dom.create('a', {'data-mce-bogus': 'all'}, INVISIBLE_CHAR);
node.parentNode.insertBefore(tmpNode, node);
// Set selection and remove tmpNode
rng.setStart(node, 0);
selection.setRng(rng);
dom.remove(tmpNode);
return;
}
}
}
}
};
});
// Included from: js/tinymce/classes/undo/Diff.js
/**
* Diff.js
*
* Released under LGPL License.
* Copyright (c) 1999-2016 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* JS Implementation of the O(ND) Difference Algorithm by Eugene W. Myers.
*
* @class tinymce.undo.Diff
* @private
*/
define("tinymce/undo/Diff", [
], function () {
var KEEP = 0, INSERT = 1, DELETE = 2;
var diff = function (left, right) {
var size = left.length + right.length + 2;
var vDown = new Array(size);
var vUp = new Array(size);
var snake = function (start, end, diag) {
return {
start: start,
end: end,
diag: diag
};
};
var buildScript = function (start1, end1, start2, end2, script) {
var middle = getMiddleSnake(start1, end1, start2, end2);
if (middle === null || middle.start === end1 && middle.diag === end1 - end2 ||
middle.end === start1 && middle.diag === start1 - start2) {
var i = start1;
var j = start2;
while (i < end1 || j < end2) {
if (i < end1 && j < end2 && left[i] === right[j]) {
script.push([KEEP, left[i]]);
++i;
++j;
} else {
if (end1 - start1 > end2 - start2) {
script.push([DELETE, left[i]]);
++i;
} else {
script.push([INSERT, right[j]]);
++j;
}
}
}
} else {
buildScript(start1, middle.start, start2, middle.start - middle.diag, script);
for (var i2 = middle.start; i2 < middle.end; ++i2) {
script.push([KEEP, left[i2]]);
}
buildScript(middle.end, end1, middle.end - middle.diag, end2, script);
}
};
var buildSnake = function (start, diag, end1, end2) {
var end = start;
while (end - diag < end2 && end < end1 && left[end] === right[end - diag]) {
++end;
}
return snake(start, end, diag);
};
var getMiddleSnake = function (start1, end1, start2, end2) {
// Myers Algorithm
// Initialisations
var m = end1 - start1;
var n = end2 - start2;
if (m === 0 || n === 0) {
return null;
}
var delta = m - n;
var sum = n + m;
var offset = (sum % 2 === 0 ? sum : sum + 1) / 2;
vDown[1 + offset] = start1;
vUp[1 + offset] = end1 + 1;
for (var d = 0; d <= offset; ++d) {
// Down
for (var k = -d; k <= d; k += 2) {
// First step
var i = k + offset;
if (k === -d || k != d && vDown[i - 1] < vDown[i + 1]) {
vDown[i] = vDown[i + 1];
} else {
vDown[i] = vDown[i - 1] + 1;
}
var x = vDown[i];
var y = x - start1 + start2 - k;
while (x < end1 && y < end2 && left[x] === right[y]) {
vDown[i] = ++x;
++y;
}
// Second step
if (delta % 2 != 0 && delta - d <= k && k <= delta + d) {
if (vUp[i - delta] <= vDown[i]) {
return buildSnake(vUp[i - delta], k + start1 - start2, end1, end2);
}
}
}
// Up
for (k = delta - d; k <= delta + d; k += 2) {
// First step
i = k + offset - delta;
if (k === delta - d || k != delta + d && vUp[i + 1] <= vUp[i - 1]) {
vUp[i] = vUp[i + 1] - 1;
} else {
vUp[i] = vUp[i - 1];
}
x = vUp[i] - 1;
y = x - start1 + start2 - k;
while (x >= start1 && y >= start2 && left[x] === right[y]) {
vUp[i] = x--;
y--;
}
// Second step
if (delta % 2 === 0 && -d <= k && k <= d) {
if (vUp[i] <= vDown[i + delta]) {
return buildSnake(vUp[i], k + start1 - start2, end1, end2);
}
}
}
}
};
var script = [];
buildScript(0, left.length, 0, right.length, script);
return script;
};
return {
KEEP: KEEP,
DELETE: DELETE,
INSERT: INSERT,
diff: diff
};
});
// Included from: js/tinymce/classes/undo/Fragments.js
/**
* Fragments.js
*
* Released under LGPL License.
* Copyright (c) 1999-2016 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This module reads and applies html fragments from/to dom nodes.
*
* @class tinymce.undo.Fragments
* @private
*/
define("tinymce/undo/Fragments", [
"tinymce/util/Arr",
"tinymce/html/Entities",
"tinymce/undo/Diff"
], function (Arr, Entities, Diff) {
var getOuterHtml = function (elm) {
if (elm.nodeType === 1) {
return elm.outerHTML;
} else if (elm.nodeType === 3) {
return Entities.encodeRaw(elm.data, false);
} else if (elm.nodeType === 8) {
return '<!--' + elm.data + '-->';
}
return '';
};
var createFragment = function(html) {
var frag, node, container;
container = document.createElement("div");
frag = document.createDocumentFragment();
if (html) {
container.innerHTML = html;
}
while ((node = container.firstChild)) {
frag.appendChild(node);
}
return frag;
};
var insertAt = function (elm, html, index) {
var fragment = createFragment(html);
if (elm.hasChildNodes() && index < elm.childNodes.length) {
var target = elm.childNodes[index];
target.parentNode.insertBefore(fragment, target);
} else {
elm.appendChild(fragment);
}
};
var removeAt = function (elm, index) {
if (elm.hasChildNodes() && index < elm.childNodes.length) {
var target = elm.childNodes[index];
target.parentNode.removeChild(target);
}
};
var applyDiff = function (diff, elm) {
var index = 0;
Arr.each(diff, function (action) {
if (action[0] === Diff.KEEP) {
index++;
} else if (action[0] === Diff.INSERT) {
insertAt(elm, action[1], index);
index++;
} else if (action[0] === Diff.DELETE) {
removeAt(elm, index);
}
});
};
var read = function (elm) {
return Arr.map(elm.childNodes, getOuterHtml);
};
var write = function (fragments, elm) {
var currentFragments = Arr.map(elm.childNodes, getOuterHtml);
applyDiff(Diff.diff(currentFragments, fragments), elm);
return elm;
};
return {
read: read,
write: write
};
});
// Included from: js/tinymce/classes/undo/Levels.js
/**
* Levels.js
*
* Released under LGPL License.
* Copyright (c) 1999-2016 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This module handles getting/setting undo levels to/from editor instances.
*
* @class tinymce.undo.Levels
* @private
*/
define("tinymce/undo/Levels", [
"tinymce/util/Arr",
"tinymce/undo/Fragments"
], function (Arr, Fragments) {
var hasIframes = function (html) {
return html.indexOf('</iframe>') !== -1;
};
var createFragmentedLevel = function (fragments) {
return {
type: 'fragmented',
fragments: fragments,
content: '',
bookmark: null,
beforeBookmark: null
};
};
var createCompleteLevel = function (content) {
return {
type: 'complete',
fragments: null,
content: content,
bookmark: null,
beforeBookmark: null
};
};
var createFromEditor = function (editor) {
var fragments, content;
fragments = Fragments.read(editor.getBody());
content = Arr.map(fragments, function (html) {
return editor.serializer.trimContent(html);
}).join('');
return hasIframes(content) ? createFragmentedLevel(fragments) : createCompleteLevel(content);
};
var applyToEditor = function (editor, level, before) {
if (level.type === 'fragmented') {
Fragments.write(level.fragments, editor.getBody());
} else {
editor.setContent(level.content, {format: 'raw'});
}
editor.selection.moveToBookmark(before ? level.beforeBookmark : level.bookmark);
};
var getLevelContent = function (level) {
return level.type === 'fragmented' ? level.fragments.join('') : level.content;
};
var isEq = function (level1, level2) {
return getLevelContent(level1) === getLevelContent(level2);
};
return {
createFragmentedLevel: createFragmentedLevel,
createCompleteLevel: createCompleteLevel,
createFromEditor: createFromEditor,
applyToEditor: applyToEditor,
isEq: isEq
};
});
// Included from: js/tinymce/classes/UndoManager.js
/**
* UndoManager.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class handles the undo/redo history levels for the editor. Since the built-in undo/redo has major drawbacks a custom one was needed.
*
* @class tinymce.UndoManager
*/
define("tinymce/UndoManager", [
"tinymce/util/VK",
"tinymce/util/Tools",
"tinymce/undo/Levels",
"tinymce/Env"
], function(VK, Tools, Levels, Env) {
return function(editor) {
var self = this, index = 0, data = [], beforeBookmark, isFirstTypedCharacter, locks = 0;
function setDirty(state) {
editor.setDirty(state);
}
function addNonTypingUndoLevel(e) {
self.typing = false;
self.add({}, e);
}
function endTyping() {
if (self.typing) {
self.typing = false;
self.add();
}
}
// Add initial undo level when the editor is initialized
editor.on('init', function() {
self.add();
});
// Get position before an execCommand is processed
editor.on('BeforeExecCommand', function(e) {
var cmd = e.command;
if (cmd !== 'Undo' && cmd !== 'Redo' && cmd !== 'mceRepaint') {
endTyping();
self.beforeChange();
}
});
// Add undo level after an execCommand call was made
editor.on('ExecCommand', function(e) {
var cmd = e.command;
if (cmd !== 'Undo' && cmd !== 'Redo' && cmd !== 'mceRepaint') {
addNonTypingUndoLevel(e);
}
});
editor.on('ObjectResizeStart Cut', function() {
self.beforeChange();
});
editor.on('SaveContent ObjectResized blur', addNonTypingUndoLevel);
editor.on('DragEnd', addNonTypingUndoLevel);
editor.on('KeyUp', function(e) {
var keyCode = e.keyCode;
// If key is prevented then don't add undo level
// This would happen on keyboard shortcuts for example
if (e.isDefaultPrevented()) {
return;
}
if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode === 45 || e.ctrlKey) {
addNonTypingUndoLevel();
editor.nodeChanged();
}
if (keyCode === 46 || keyCode === 8 || (Env.mac && (keyCode === 91 || keyCode === 93))) {
editor.nodeChanged();
}
// Fire a TypingUndo event on the first character entered
if (isFirstTypedCharacter && self.typing) {
// Make it dirty if the content was changed after typing the first character
if (!editor.isDirty()) {
setDirty(data[0] && !Levels.isEq(Levels.createFromEditor(editor), data[0]));
// Fire initial change event
if (editor.isDirty()) {
editor.fire('change', {level: data[0], lastLevel: null});
}
}
editor.fire('TypingUndo');
isFirstTypedCharacter = false;
editor.nodeChanged();
}
});
editor.on('KeyDown', function(e) {
var keyCode = e.keyCode;
// If key is prevented then don't add undo level
// This would happen on keyboard shortcuts for example
if (e.isDefaultPrevented()) {
return;
}
// Is character position keys left,right,up,down,home,end,pgdown,pgup,enter
if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode === 45) {
if (self.typing) {
addNonTypingUndoLevel(e);
}
return;
}
// If key isn't Ctrl+Alt/AltGr
var modKey = (e.ctrlKey && !e.altKey) || e.metaKey;
if ((keyCode < 16 || keyCode > 20) && keyCode !== 224 && keyCode !== 91 && !self.typing && !modKey) {
self.beforeChange();
self.typing = true;
self.add({}, e);
isFirstTypedCharacter = true;
}
});
editor.on('MouseDown', function(e) {
if (self.typing) {
addNonTypingUndoLevel(e);
}
});
// Add keyboard shortcuts for undo/redo keys
editor.addShortcut('meta+z', '', 'Undo');
editor.addShortcut('meta+y,meta+shift+z', '', 'Redo');
editor.on('AddUndo Undo Redo ClearUndos', function(e) {
if (!e.isDefaultPrevented()) {
editor.nodeChanged();
}
});
/*eslint consistent-this:0 */
self = {
// Explode for debugging reasons
data: data,
/**
* State if the user is currently typing or not. This will add a typing operation into one undo
* level instead of one new level for each keystroke.
*
* @field {Boolean} typing
*/
typing: false,
/**
* Stores away a bookmark to be used when performing an undo action so that the selection is before
* the change has been made.
*
* @method beforeChange
*/
beforeChange: function() {
if (!locks) {
beforeBookmark = editor.selection.getBookmark(2, true);
}
},
/**
* Adds a new undo level/snapshot to the undo list.
*
* @method add
* @param {Object} level Optional undo level object to add.
* @param {DOMEvent} event Optional event responsible for the creation of the undo level.
* @return {Object} Undo level that got added or null it a level wasn't needed.
*/
add: function(level, event) {
var i, settings = editor.settings, lastLevel, currentLevel;
currentLevel = Levels.createFromEditor(editor);
level = level || {};
level = Tools.extend(level, currentLevel);
if (locks || editor.removed) {
return null;
}
lastLevel = data[index];
if (editor.fire('BeforeAddUndo', {level: level, lastLevel: lastLevel, originalEvent: event}).isDefaultPrevented()) {
return null;
}
// Add undo level if needed
if (lastLevel && Levels.isEq(lastLevel, level)) {
return null;
}
// Set before bookmark on previous level
if (data[index]) {
data[index].beforeBookmark = beforeBookmark;
}
// Time to compress
if (settings.custom_undo_redo_levels) {
if (data.length > settings.custom_undo_redo_levels) {
for (i = 0; i < data.length - 1; i++) {
data[i] = data[i + 1];
}
data.length--;
index = data.length;
}
}
// Get a non intrusive normalized bookmark
level.bookmark = editor.selection.getBookmark(2, true);
// Crop array if needed
if (index < data.length - 1) {
data.length = index + 1;
}
data.push(level);
index = data.length - 1;
var args = {level: level, lastLevel: lastLevel, originalEvent: event};
editor.fire('AddUndo', args);
if (index > 0) {
setDirty(true);
editor.fire('change', args);
}
return level;
},
/**
* Undoes the last action.
*
* @method undo
* @return {Object} Undo level or null if no undo was performed.
*/
undo: function() {
var level;
if (self.typing) {
self.add();
self.typing = false;
}
if (index > 0) {
level = data[--index];
Levels.applyToEditor(editor, level, true);
setDirty(true);
editor.fire('undo', {level: level});
}
return level;
},
/**
* Redoes the last action.
*
* @method redo
* @return {Object} Redo level or null if no redo was performed.
*/
redo: function() {
var level;
if (index < data.length - 1) {
level = data[++index];
Levels.applyToEditor(editor, level, false);
setDirty(true);
editor.fire('redo', {level: level});
}
return level;
},
/**
* Removes all undo levels.
*
* @method clear
*/
clear: function() {
data = [];
index = 0;
self.typing = false;
self.data = data;
editor.fire('ClearUndos');
},
/**
* Returns true/false if the undo manager has any undo levels.
*
* @method hasUndo
* @return {Boolean} true/false if the undo manager has any undo levels.
*/
hasUndo: function() {
// Has undo levels or typing and content isn't the same as the initial level
return index > 0 || (self.typing && data[0] && !Levels.isEq(Levels.createFromEditor(editor), data[0]));
},
/**
* Returns true/false if the undo manager has any redo levels.
*
* @method hasRedo
* @return {Boolean} true/false if the undo manager has any redo levels.
*/
hasRedo: function() {
return index < data.length - 1 && !self.typing;
},
/**
* Executes the specified mutator function as an undo transaction. The selection
* before the modification will be stored to the undo stack and if the DOM changes
* it will add a new undo level. Any methods within the translation that adds undo levels will
* be ignored. So a translation can include calls to execCommand or editor.insertContent.
*
* @method transact
* @param {function} callback Function that gets executed and has dom manipulation logic in it.
* @return {Object} Undo level that got added or null it a level wasn't needed.
*/
transact: function(callback) {
endTyping();
self.beforeChange();
try {
locks++;
callback();
} finally {
locks--;
}
return self.add();
},
/**
* Adds an extra "hidden" undo level by first applying the first mutation and store that to the undo stack
* then roll back that change and do the second mutation on top of the stack. This will produce an extra
* undo level that the user doesn't see until they undo.
*
* @method extra
* @param {function} callback1 Function that does mutation but gets stored as a "hidden" extra undo level.
* @param {function} callback2 Function that does mutation but gets displayed to the user.
*/
extra: function (callback1, callback2) {
var lastLevel, bookmark;
if (self.transact(callback1)) {
bookmark = data[index].bookmark;
lastLevel = data[index - 1];
Levels.applyToEditor(editor, lastLevel, true);
if (self.transact(callback2)) {
data[index - 1].beforeBookmark = bookmark;
}
}
}
};
return self;
};
});
// Included from: js/tinymce/classes/EnterKey.js
/**
* EnterKey.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Contains logic for handling the enter key to split/generate block elements.
*
* @private
* @class tinymce.EnterKey
*/
define("tinymce/EnterKey", [
"tinymce/dom/TreeWalker",
"tinymce/dom/RangeUtils",
"tinymce/caret/CaretContainer",
"tinymce/Env"
], function(TreeWalker, RangeUtils, CaretContainer, Env) {
var isIE = Env.ie && Env.ie < 11;
return function(editor) {
var dom = editor.dom, selection = editor.selection, settings = editor.settings;
var undoManager = editor.undoManager, schema = editor.schema, nonEmptyElementsMap = schema.getNonEmptyElements(),
moveCaretBeforeOnEnterElementsMap = schema.getMoveCaretBeforeOnEnterElements();
function handleEnterKey(evt) {
var rng, tmpRng, editableRoot, container, offset, parentBlock, documentMode, shiftKey,
newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName, isAfterLastNodeInContainer;
// Returns true if the block can be split into two blocks or not
function canSplitBlock(node) {
return node &&
dom.isBlock(node) &&
!/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) &&
!/^(fixed|absolute)/i.test(node.style.position) &&
dom.getContentEditable(node) !== "true";
}
function isTableCell(node) {
return node && /^(TD|TH|CAPTION)$/.test(node.nodeName);
}
// Renders empty block on IE
function renderBlockOnIE(block) {
var oldRng;
if (dom.isBlock(block)) {
oldRng = selection.getRng();
block.appendChild(dom.create('span', null, '\u00a0'));
selection.select(block);
block.lastChild.outerHTML = '';
selection.setRng(oldRng);
}
}
// Remove the first empty inline element of the block so this: <p><b><em></em></b>x</p> becomes this: <p>x</p>
function trimInlineElementsOnLeftSideOfBlock(block) {
var node = block, firstChilds = [], i;
if (!node) {
return;
}
// Find inner most first child ex: <p><i><b>*</b></i></p>
while ((node = node.firstChild)) {
if (dom.isBlock(node)) {
return;
}
if (node.nodeType == 1 && !nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
firstChilds.push(node);
}
}
i = firstChilds.length;
while (i--) {
node = firstChilds[i];
if (!node.hasChildNodes() || (node.firstChild == node.lastChild && node.firstChild.nodeValue === '')) {
dom.remove(node);
} else {
// Remove <a> </a> see #5381
if (node.nodeName == "A" && (node.innerText || node.textContent) === ' ') {
dom.remove(node);
}
}
}
}
// Moves the caret to a suitable position within the root for example in the first non
// pure whitespace text node or before an image
function moveToCaretPosition(root) {
var walker, node, rng, lastNode = root, tempElm;
function firstNonWhiteSpaceNodeSibling(node) {
while (node) {
if (node.nodeType == 1 || (node.nodeType == 3 && node.data && /[\r\n\s]/.test(node.data))) {
return node;
}
node = node.nextSibling;
}
}
if (!root) {
return;
}
// Old IE versions doesn't properly render blocks with br elements in them
// For example <p><br></p> wont be rendered correctly in a contentEditable area
// until you remove the br producing <p></p>
if (Env.ie && Env.ie < 9 && parentBlock && parentBlock.firstChild) {
if (parentBlock.firstChild == parentBlock.lastChild && parentBlock.firstChild.tagName == 'BR') {
dom.remove(parentBlock.firstChild);
}
}
if (/^(LI|DT|DD)$/.test(root.nodeName)) {
var firstChild = firstNonWhiteSpaceNodeSibling(root.firstChild);
if (firstChild && /^(UL|OL|DL)$/.test(firstChild.nodeName)) {
root.insertBefore(dom.doc.createTextNode('\u00a0'), root.firstChild);
}
}
rng = dom.createRng();
// Normalize whitespace to remove empty text nodes. Fix for: #6904
// Gecko will be able to place the caret in empty text nodes but it won't render propery
// Older IE versions will sometimes crash so for now ignore all IE versions
if (!Env.ie) {
root.normalize();
}
if (root.hasChildNodes()) {
walker = new TreeWalker(root, root);
while ((node = walker.current())) {
if (node.nodeType == 3) {
rng.setStart(node, 0);
rng.setEnd(node, 0);
break;
}
if (moveCaretBeforeOnEnterElementsMap[node.nodeName.toLowerCase()]) {
rng.setStartBefore(node);
rng.setEndBefore(node);
break;
}
lastNode = node;
node = walker.next();
}
if (!node) {
rng.setStart(lastNode, 0);
rng.setEnd(lastNode, 0);
}
} else {
if (root.nodeName == 'BR') {
if (root.nextSibling && dom.isBlock(root.nextSibling)) {
// Trick on older IE versions to render the caret before the BR between two lists
if (!documentMode || documentMode < 9) {
tempElm = dom.create('br');
root.parentNode.insertBefore(tempElm, root);
}
rng.setStartBefore(root);
rng.setEndBefore(root);
} else {
rng.setStartAfter(root);
rng.setEndAfter(root);
}
} else {
rng.setStart(root, 0);
rng.setEnd(root, 0);
}
}
selection.setRng(rng);
// Remove tempElm created for old IE:s
dom.remove(tempElm);
selection.scrollIntoView(root);
}
function setForcedBlockAttrs(node) {
var forcedRootBlockName = settings.forced_root_block;
if (forcedRootBlockName && forcedRootBlockName.toLowerCase() === node.tagName.toLowerCase()) {
dom.setAttribs(node, settings.forced_root_block_attrs);
}
}
function emptyBlock(elm) {
// BR is needed in empty blocks on non IE browsers
elm.innerHTML = !isIE ? '<br data-mce-bogus="1">' : '';
}
// Creates a new block element by cloning the current one or creating a new one if the name is specified
// This function will also copy any text formatting from the parent block and add it to the new one
function createNewBlock(name) {
var node = container, block, clonedNode, caretNode, textInlineElements = schema.getTextInlineElements();
if (name || parentBlockName == "TABLE") {
block = dom.create(name || newBlockName);
setForcedBlockAttrs(block);
} else {
block = parentBlock.cloneNode(false);
}
caretNode = block;
// Clone any parent styles
if (settings.keep_styles !== false) {
do {
if (textInlineElements[node.nodeName]) {
// Never clone a caret containers
if (node.id == '_mce_caret') {
continue;
}
clonedNode = node.cloneNode(false);
dom.setAttrib(clonedNode, 'id', ''); // Remove ID since it needs to be document unique
if (block.hasChildNodes()) {
clonedNode.appendChild(block.firstChild);
block.appendChild(clonedNode);
} else {
caretNode = clonedNode;
block.appendChild(clonedNode);
}
}
} while ((node = node.parentNode) && node != editableRoot);
}
// BR is needed in empty blocks on non IE browsers
if (!isIE) {
caretNode.innerHTML = '<br data-mce-bogus="1">';
}
return block;
}
// Returns true/false if the caret is at the start/end of the parent block element
function isCaretAtStartOrEndOfBlock(start) {
var walker, node, name;
// Caret is in the middle of a text node like "a|b"
if (container.nodeType == 3 && (start ? offset > 0 : offset < container.nodeValue.length)) {
return false;
}
// If after the last element in block node edge case for #5091
if (container.parentNode == parentBlock && isAfterLastNodeInContainer && !start) {
return true;
}
// If the caret if before the first element in parentBlock
if (start && container.nodeType == 1 && container == parentBlock.firstChild) {
return true;
}
// Caret can be before/after a table
if (container.nodeName === "TABLE" || (container.previousSibling && container.previousSibling.nodeName == "TABLE")) {
return (isAfterLastNodeInContainer && !start) || (!isAfterLastNodeInContainer && start);
}
// Walk the DOM and look for text nodes or non empty elements
walker = new TreeWalker(container, parentBlock);
// If caret is in beginning or end of a text block then jump to the next/previous node
if (container.nodeType == 3) {
if (start && offset === 0) {
walker.prev();
} else if (!start && offset == container.nodeValue.length) {
walker.next();
}
}
while ((node = walker.current())) {
if (node.nodeType === 1) {
// Ignore bogus elements
if (!node.getAttribute('data-mce-bogus')) {
// Keep empty elements like <img /> <input /> but not trailing br:s like <p>text|<br></p>
name = node.nodeName.toLowerCase();
if (nonEmptyElementsMap[name] && name !== 'br') {
return false;
}
}
} else if (node.nodeType === 3 && !/^[ \t\r\n]*$/.test(node.nodeValue)) {
return false;
}
if (start) {
walker.prev();
} else {
walker.next();
}
}
return true;
}
// Wraps any text nodes or inline elements in the specified forced root block name
function wrapSelfAndSiblingsInDefaultBlock(container, offset) {
var newBlock, parentBlock, startNode, node, next, rootBlockName, blockName = newBlockName || 'P';
// Not in a block element or in a table cell or caption
parentBlock = dom.getParent(container, dom.isBlock);
if (!parentBlock || !canSplitBlock(parentBlock)) {
parentBlock = parentBlock || editableRoot;
if (parentBlock == editor.getBody() || isTableCell(parentBlock)) {
rootBlockName = parentBlock.nodeName.toLowerCase();
} else {
rootBlockName = parentBlock.parentNode.nodeName.toLowerCase();
}
if (!parentBlock.hasChildNodes()) {
newBlock = dom.create(blockName);
setForcedBlockAttrs(newBlock);
parentBlock.appendChild(newBlock);
rng.setStart(newBlock, 0);
rng.setEnd(newBlock, 0);
return newBlock;
}
// Find parent that is the first child of parentBlock
node = container;
while (node.parentNode != parentBlock) {
node = node.parentNode;
}
// Loop left to find start node start wrapping at
while (node && !dom.isBlock(node)) {
startNode = node;
node = node.previousSibling;
}
if (startNode && schema.isValidChild(rootBlockName, blockName.toLowerCase())) {
newBlock = dom.create(blockName);
setForcedBlockAttrs(newBlock);
startNode.parentNode.insertBefore(newBlock, startNode);
// Start wrapping until we hit a block
node = startNode;
while (node && !dom.isBlock(node)) {
next = node.nextSibling;
newBlock.appendChild(node);
node = next;
}
// Restore range to it's past location
rng.setStart(container, offset);
rng.setEnd(container, offset);
}
}
return container;
}
// Inserts a block or br before/after or in the middle of a split list of the LI is empty
function handleEmptyListItem() {
function isFirstOrLastLi(first) {
var node = containerBlock[first ? 'firstChild' : 'lastChild'];
// Find first/last element since there might be whitespace there
while (node) {
if (node.nodeType == 1) {
break;
}
node = node[first ? 'nextSibling' : 'previousSibling'];
}
return node === parentBlock;
}
function getContainerBlock() {
var containerBlockParent = containerBlock.parentNode;
if (/^(LI|DT|DD)$/.test(containerBlockParent.nodeName)) {
return containerBlockParent;
}
return containerBlock;
}
if (containerBlock == editor.getBody()) {
return;
}
// Check if we are in an nested list
var containerBlockParentName = containerBlock.parentNode.nodeName;
if (/^(OL|UL|LI)$/.test(containerBlockParentName)) {
newBlockName = 'LI';
}
newBlock = newBlockName ? createNewBlock(newBlockName) : dom.create('BR');
if (isFirstOrLastLi(true) && isFirstOrLastLi()) {
if (containerBlockParentName == 'LI') {
// Nested list is inside a LI
dom.insertAfter(newBlock, getContainerBlock());
} else {
// Is first and last list item then replace the OL/UL with a text block
dom.replace(newBlock, containerBlock);
}
} else if (isFirstOrLastLi(true)) {
if (containerBlockParentName == 'LI') {
// List nested in an LI then move the list to a new sibling LI
dom.insertAfter(newBlock, getContainerBlock());
newBlock.appendChild(dom.doc.createTextNode(' ')); // Needed for IE so the caret can be placed
newBlock.appendChild(containerBlock);
} else {
// First LI in list then remove LI and add text block before list
containerBlock.parentNode.insertBefore(newBlock, containerBlock);
}
} else if (isFirstOrLastLi()) {
// Last LI in list then remove LI and add text block after list
dom.insertAfter(newBlock, getContainerBlock());
renderBlockOnIE(newBlock);
} else {
// Middle LI in list the split the list and insert a text block in the middle
// Extract after fragment and insert it after the current block
containerBlock = getContainerBlock();
tmpRng = rng.cloneRange();
tmpRng.setStartAfter(parentBlock);
tmpRng.setEndAfter(containerBlock);
fragment = tmpRng.extractContents();
if (newBlockName == 'LI' && fragment.firstChild.nodeName == 'LI') {
newBlock = fragment.firstChild;
dom.insertAfter(fragment, containerBlock);
} else {
dom.insertAfter(fragment, containerBlock);
dom.insertAfter(newBlock, containerBlock);
}
}
dom.remove(parentBlock);
moveToCaretPosition(newBlock);
undoManager.add();
}
// Inserts a BR element if the forced_root_block option is set to false or empty string
function insertBr() {
editor.execCommand("InsertLineBreak", false, evt);
}
// Trims any linebreaks at the beginning of node user for example when pressing enter in a PRE element
function trimLeadingLineBreaks(node) {
do {
if (node.nodeType === 3) {
node.nodeValue = node.nodeValue.replace(/^[\r\n]+/, '');
}
node = node.firstChild;
} while (node);
}
function getEditableRoot(node) {
var root = dom.getRoot(), parent, editableRoot;
// Get all parents until we hit a non editable parent or the root
parent = node;
while (parent !== root && dom.getContentEditable(parent) !== "false") {
if (dom.getContentEditable(parent) === "true") {
editableRoot = parent;
}
parent = parent.parentNode;
}
return parent !== root ? editableRoot : root;
}
// Adds a BR at the end of blocks that only contains an IMG or INPUT since
// these might be floated and then they won't expand the block
function addBrToBlockIfNeeded(block) {
var lastChild;
// IE will render the blocks correctly other browsers needs a BR
if (!isIE) {
block.normalize(); // Remove empty text nodes that got left behind by the extract
// Check if the block is empty or contains a floated last child
lastChild = block.lastChild;
if (!lastChild || (/^(left|right)$/gi.test(dom.getStyle(lastChild, 'float', true)))) {
dom.add(block, 'br');
}
}
}
function insertNewBlockAfter() {
// If the caret is at the end of a header we produce a P tag after it similar to Word unless we are in a hgroup
if (/^(H[1-6]|PRE|FIGURE)$/.test(parentBlockName) && containerBlockName != 'HGROUP') {
newBlock = createNewBlock(newBlockName);
} else {
newBlock = createNewBlock();
}
// Split the current container block element if enter is pressed inside an empty inner block element
if (settings.end_container_on_empty_block && canSplitBlock(containerBlock) && dom.isEmpty(parentBlock)) {
// Split container block for example a BLOCKQUOTE at the current blockParent location for example a P
newBlock = dom.split(containerBlock, parentBlock);
} else {
dom.insertAfter(newBlock, parentBlock);
}
moveToCaretPosition(newBlock);
}
rng = selection.getRng(true);
// Event is blocked by some other handler for example the lists plugin
if (evt.isDefaultPrevented()) {
return;
}
// Delete any selected contents
if (!rng.collapsed) {
editor.execCommand('Delete');
return;
}
// Setup range items and newBlockName
new RangeUtils(dom).normalize(rng);
container = rng.startContainer;
offset = rng.startOffset;
newBlockName = (settings.force_p_newlines ? 'p' : '') || settings.forced_root_block;
newBlockName = newBlockName ? newBlockName.toUpperCase() : '';
documentMode = dom.doc.documentMode;
shiftKey = evt.shiftKey;
// Resolve node index
if (container.nodeType == 1 && container.hasChildNodes()) {
isAfterLastNodeInContainer = offset > container.childNodes.length - 1;
container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container;
if (isAfterLastNodeInContainer && container.nodeType == 3) {
offset = container.nodeValue.length;
} else {
offset = 0;
}
}
// Get editable root node, normally the body element but sometimes a div or span
editableRoot = getEditableRoot(container);
// If there is no editable root then enter is done inside a contentEditable false element
if (!editableRoot) {
return;
}
undoManager.beforeChange();
// If editable root isn't block nor the root of the editor
if (!dom.isBlock(editableRoot) && editableRoot != dom.getRoot()) {
if (!newBlockName || shiftKey) {
insertBr();
}
return;
}
// Wrap the current node and it's sibling in a default block if it's needed.
// for example this <td>text|<b>text2</b></td> will become this <td><p>text|<b>text2</p></b></td>
// This won't happen if root blocks are disabled or the shiftKey is pressed
if ((newBlockName && !shiftKey) || (!newBlockName && shiftKey)) {
container = wrapSelfAndSiblingsInDefaultBlock(container, offset);
}
// Find parent block and setup empty block paddings
parentBlock = dom.getParent(container, dom.isBlock);
containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null;
// Setup block names
parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5
containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5
// Enter inside block contained within a LI then split or insert before/after LI
if (containerBlockName == 'LI' && !evt.ctrlKey) {
parentBlock = containerBlock;
parentBlockName = containerBlockName;
}
if (editor.undoManager.typing) {
editor.undoManager.typing = false;
editor.undoManager.add();
}
// Handle enter in list item
if (/^(LI|DT|DD)$/.test(parentBlockName)) {
if (!newBlockName && shiftKey) {
insertBr();
return;
}
// Handle enter inside an empty list item
if (dom.isEmpty(parentBlock)) {
handleEmptyListItem();
return;
}
}
// Don't split PRE tags but insert a BR instead easier when writing code samples etc
if (parentBlockName == 'PRE' && settings.br_in_pre !== false) {
if (!shiftKey) {
insertBr();
return;
}
} else {
// If no root block is configured then insert a BR by default or if the shiftKey is pressed
if ((!newBlockName && !shiftKey && parentBlockName != 'LI') || (newBlockName && shiftKey)) {
insertBr();
return;
}
}
// If parent block is root then never insert new blocks
if (newBlockName && parentBlock === editor.getBody()) {
return;
}
// Default block name if it's not configured
newBlockName = newBlockName || 'P';
// Insert new block before/after the parent block depending on caret location
if (CaretContainer.isCaretContainerBlock(parentBlock)) {
newBlock = CaretContainer.showCaretContainerBlock(parentBlock);
} else if (isCaretAtStartOrEndOfBlock()) {
insertNewBlockAfter();
} else if (isCaretAtStartOrEndOfBlock(true)) {
// Insert new block before
newBlock = parentBlock.parentNode.insertBefore(createNewBlock(), parentBlock);
renderBlockOnIE(newBlock);
moveToCaretPosition(parentBlock);
} else {
// Extract after fragment and insert it after the current block
tmpRng = rng.cloneRange();
tmpRng.setEndAfter(parentBlock);
fragment = tmpRng.extractContents();
trimLeadingLineBreaks(fragment);
newBlock = fragment.firstChild;
dom.insertAfter(fragment, parentBlock);
trimInlineElementsOnLeftSideOfBlock(newBlock);
addBrToBlockIfNeeded(parentBlock);
if (dom.isEmpty(parentBlock)) {
emptyBlock(parentBlock);
}
newBlock.normalize();
// New block might become empty if it's <p><b>a |</b></p>
if (dom.isEmpty(newBlock)) {
dom.remove(newBlock);
insertNewBlockAfter();
} else {
moveToCaretPosition(newBlock);
}
}
dom.setAttrib(newBlock, 'id', ''); // Remove ID since it needs to be document unique
// Allow custom handling of new blocks
editor.fire('NewBlock', {newBlock: newBlock});
undoManager.typing = false;
undoManager.add();
}
editor.on('keydown', function(evt) {
if (evt.keyCode == 13) {
if (handleEnterKey(evt) !== false) {
evt.preventDefault();
}
}
});
};
});
// Included from: js/tinymce/classes/ForceBlocks.js
/**
* ForceBlocks.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Makes sure that everything gets wrapped in paragraphs.
*
* @private
* @class tinymce.ForceBlocks
*/
define("tinymce/ForceBlocks", [], function() {
return function(editor) {
var settings = editor.settings, dom = editor.dom, selection = editor.selection;
var schema = editor.schema, blockElements = schema.getBlockElements();
function addRootBlocks() {
var node = selection.getStart(), rootNode = editor.getBody(), rng;
var startContainer, startOffset, endContainer, endOffset, rootBlockNode;
var tempNode, offset = -0xFFFFFF, wrapped, restoreSelection;
var tmpRng, rootNodeName, forcedRootBlock;
forcedRootBlock = settings.forced_root_block;
if (!node || node.nodeType !== 1 || !forcedRootBlock) {
return;
}
// Check if node is wrapped in block
while (node && node != rootNode) {
if (blockElements[node.nodeName]) {
return;
}
node = node.parentNode;
}
// Get current selection
rng = selection.getRng();
if (rng.setStart) {
startContainer = rng.startContainer;
startOffset = rng.startOffset;
endContainer = rng.endContainer;
endOffset = rng.endOffset;
try {
restoreSelection = editor.getDoc().activeElement === rootNode;
} catch (ex) {
// IE throws unspecified error here sometimes
}
} else {
// Force control range into text range
if (rng.item) {
node = rng.item(0);
rng = editor.getDoc().body.createTextRange();
rng.moveToElementText(node);
}
restoreSelection = rng.parentElement().ownerDocument === editor.getDoc();
tmpRng = rng.duplicate();
tmpRng.collapse(true);
startOffset = tmpRng.move('character', offset) * -1;
if (!tmpRng.collapsed) {
tmpRng = rng.duplicate();
tmpRng.collapse(false);
endOffset = (tmpRng.move('character', offset) * -1) - startOffset;
}
}
// Wrap non block elements and text nodes
node = rootNode.firstChild;
rootNodeName = rootNode.nodeName.toLowerCase();
while (node) {
// TODO: Break this up, too complex
if (((node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName]))) &&
schema.isValidChild(rootNodeName, forcedRootBlock.toLowerCase())) {
// Remove empty text nodes
if (node.nodeType === 3 && node.nodeValue.length === 0) {
tempNode = node;
node = node.nextSibling;
dom.remove(tempNode);
continue;
}
if (!rootBlockNode) {
rootBlockNode = dom.create(forcedRootBlock, editor.settings.forced_root_block_attrs);
node.parentNode.insertBefore(rootBlockNode, node);
wrapped = true;
}
tempNode = node;
node = node.nextSibling;
rootBlockNode.appendChild(tempNode);
} else {
rootBlockNode = null;
node = node.nextSibling;
}
}
if (wrapped && restoreSelection) {
if (rng.setStart) {
rng.setStart(startContainer, startOffset);
rng.setEnd(endContainer, endOffset);
selection.setRng(rng);
} else {
// Only select if the previous selection was inside the document to prevent auto focus in quirks mode
try {
rng = editor.getDoc().body.createTextRange();
rng.moveToElementText(rootNode);
rng.collapse(true);
rng.moveStart('character', startOffset);
if (endOffset > 0) {
rng.moveEnd('character', endOffset);
}
rng.select();
} catch (ex) {
// Ignore
}
}
editor.nodeChanged();
}
}
// Force root blocks
if (settings.forced_root_block) {
editor.on('NodeChange', addRootBlocks);
}
};
});
// Included from: js/tinymce/classes/caret/CaretUtils.js
/**
* CaretUtils.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Utility functions shared by the caret logic.
*
* @private
* @class tinymce.caret.CaretUtils
*/
define("tinymce/caret/CaretUtils", [
"tinymce/util/Fun",
"tinymce/dom/TreeWalker",
"tinymce/dom/NodeType",
"tinymce/caret/CaretPosition",
"tinymce/caret/CaretContainer",
"tinymce/caret/CaretCandidate"
], function(Fun, TreeWalker, NodeType, CaretPosition, CaretContainer, CaretCandidate) {
var isContentEditableTrue = NodeType.isContentEditableTrue,
isContentEditableFalse = NodeType.isContentEditableFalse,
isBlockLike = NodeType.matchStyleValues('display', 'block table table-cell table-caption'),
isCaretContainer = CaretContainer.isCaretContainer,
isCaretContainerBlock = CaretContainer.isCaretContainerBlock,
curry = Fun.curry,
isElement = NodeType.isElement,
isCaretCandidate = CaretCandidate.isCaretCandidate;
function isForwards(direction) {
return direction > 0;
}
function isBackwards(direction) {
return direction < 0;
}
function skipCaretContainers(walk, shallow) {
var node;
while ((node = walk(shallow))) {
if (!isCaretContainerBlock(node)) {
return node;
}
}
return null;
}
function findNode(node, direction, predicateFn, rootNode, shallow) {
var walker = new TreeWalker(node, rootNode);
if (isBackwards(direction)) {
if (isContentEditableFalse(node) || isCaretContainerBlock(node)) {
node = skipCaretContainers(walker.prev, true);
if (predicateFn(node)) {
return node;
}
}
while ((node = skipCaretContainers(walker.prev, shallow))) {
if (predicateFn(node)) {
return node;
}
}
}
if (isForwards(direction)) {
if (isContentEditableFalse(node) || isCaretContainerBlock(node)) {
node = skipCaretContainers(walker.next, true);
if (predicateFn(node)) {
return node;
}
}
while ((node = skipCaretContainers(walker.next, shallow))) {
if (predicateFn(node)) {
return node;
}
}
}
return null;
}
function getEditingHost(node, rootNode) {
for (node = node.parentNode; node && node != rootNode; node = node.parentNode) {
if (isContentEditableTrue(node)) {
return node;
}
}
return rootNode;
}
function getParentBlock(node, rootNode) {
while (node && node != rootNode) {
if (isBlockLike(node)) {
return node;
}
node = node.parentNode;
}
return null;
}
function isInSameBlock(caretPosition1, caretPosition2, rootNode) {
return getParentBlock(caretPosition1.container(), rootNode) == getParentBlock(caretPosition2.container(), rootNode);
}
function isInSameEditingHost(caretPosition1, caretPosition2, rootNode) {
return getEditingHost(caretPosition1.container(), rootNode) == getEditingHost(caretPosition2.container(), rootNode);
}
function getChildNodeAtRelativeOffset(relativeOffset, caretPosition) {
var container, offset;
if (!caretPosition) {
return null;
}
container = caretPosition.container();
offset = caretPosition.offset();
if (!isElement(container)) {
return null;
}
return container.childNodes[offset + relativeOffset];
}
function beforeAfter(before, node) {
var range = node.ownerDocument.createRange();
if (before) {
range.setStartBefore(node);
range.setEndBefore(node);
} else {
range.setStartAfter(node);
range.setEndAfter(node);
}
return range;
}
function isNodesInSameBlock(rootNode, node1, node2) {
return getParentBlock(node1, rootNode) == getParentBlock(node2, rootNode);
}
function lean(left, rootNode, node) {
var sibling, siblingName;
if (left) {
siblingName = 'previousSibling';
} else {
siblingName = 'nextSibling';
}
while (node && node != rootNode) {
sibling = node[siblingName];
if (isCaretContainer(sibling)) {
sibling = sibling[siblingName];
}
if (isContentEditableFalse(sibling)) {
if (isNodesInSameBlock(rootNode, sibling, node)) {
return sibling;
}
break;
}
if (isCaretCandidate(sibling)) {
break;
}
node = node.parentNode;
}
return null;
}
var before = curry(beforeAfter, true);
var after = curry(beforeAfter, false);
function normalizeRange(direction, rootNode, range) {
var node, container, offset, location;
var leanLeft = curry(lean, true, rootNode);
var leanRight = curry(lean, false, rootNode);
container = range.startContainer;
offset = range.startOffset;
if (CaretContainer.isCaretContainerBlock(container)) {
if (!isElement(container)) {
container = container.parentNode;
}
location = container.getAttribute('data-mce-caret');
if (location == 'before') {
node = container.nextSibling;
if (isContentEditableFalse(node)) {
return before(node);
}
}
if (location == 'after') {
node = container.previousSibling;
if (isContentEditableFalse(node)) {
return after(node);
}
}
}
if (!range.collapsed) {
return range;
}
if (NodeType.isText(container)) {
if (isCaretContainer(container)) {
if (direction === 1) {
node = leanRight(container);
if (node) {
return before(node);
}
node = leanLeft(container);
if (node) {
return after(node);
}
}
if (direction === -1) {
node = leanLeft(container);
if (node) {
return after(node);
}
node = leanRight(container);
if (node) {
return before(node);
}
}
return range;
}
if (CaretContainer.endsWithCaretContainer(container) && offset >= container.data.length - 1) {
if (direction === 1) {
node = leanRight(container);
if (node) {
return before(node);
}
}
return range;
}
if (CaretContainer.startsWithCaretContainer(container) && offset <= 1) {
if (direction === -1) {
node = leanLeft(container);
if (node) {
return after(node);
}
}
return range;
}
if (offset === container.data.length) {
node = leanRight(container);
if (node) {
return before(node);
}
return range;
}
if (offset === 0) {
node = leanLeft(container);
if (node) {
return after(node);
}
return range;
}
}
return range;
}
function isNextToContentEditableFalse(relativeOffset, caretPosition) {
return isContentEditableFalse(getChildNodeAtRelativeOffset(relativeOffset, caretPosition));
}
return {
isForwards: isForwards,
isBackwards: isBackwards,
findNode: findNode,
getEditingHost: getEditingHost,
getParentBlock: getParentBlock,
isInSameBlock: isInSameBlock,
isInSameEditingHost: isInSameEditingHost,
isBeforeContentEditableFalse: curry(isNextToContentEditableFalse, 0),
isAfterContentEditableFalse: curry(isNextToContentEditableFalse, -1),
normalizeRange: normalizeRange
};
});
// Included from: js/tinymce/classes/caret/CaretWalker.js
/**
* CaretWalker.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This module contains logic for moving around a virtual caret in logical order within a DOM element.
*
* It ignores the most obvious invalid caret locations such as within a script element or within a
* contentEditable=false element but it will return locations that isn't possible to render visually.
*
* @private
* @class tinymce.caret.CaretWalker
* @example
* var caretWalker = new CaretWalker(rootElm);
*
* var prevLogicalCaretPosition = caretWalker.prev(CaretPosition.fromRangeStart(range));
* var nextLogicalCaretPosition = caretWalker.next(CaretPosition.fromRangeEnd(range));
*/
define("tinymce/caret/CaretWalker", [
"tinymce/dom/NodeType",
"tinymce/caret/CaretCandidate",
"tinymce/caret/CaretPosition",
"tinymce/caret/CaretUtils",
"tinymce/util/Arr",
"tinymce/util/Fun"
], function(NodeType, CaretCandidate, CaretPosition, CaretUtils, Arr, Fun) {
var isContentEditableFalse = NodeType.isContentEditableFalse,
isText = NodeType.isText,
isElement = NodeType.isElement,
isBr = NodeType.isBr,
isForwards = CaretUtils.isForwards,
isBackwards = CaretUtils.isBackwards,
isCaretCandidate = CaretCandidate.isCaretCandidate,
isAtomic = CaretCandidate.isAtomic,
isEditableCaretCandidate = CaretCandidate.isEditableCaretCandidate;
function getParents(node, rootNode) {
var parents = [];
while (node && node != rootNode) {
parents.push(node);
node = node.parentNode;
}
return parents;
}
function nodeAtIndex(container, offset) {
if (container.hasChildNodes() && offset < container.childNodes.length) {
return container.childNodes[offset];
}
return null;
}
function getCaretCandidatePosition(direction, node) {
if (isForwards(direction)) {
if (isCaretCandidate(node.previousSibling) && !isText(node.previousSibling)) {
return CaretPosition.before(node);
}
if (isText(node)) {
return CaretPosition(node, 0);
}
}
if (isBackwards(direction)) {
if (isCaretCandidate(node.nextSibling) && !isText(node.nextSibling)) {
return CaretPosition.after(node);
}
if (isText(node)) {
return CaretPosition(node, node.data.length);
}
}
if (isBackwards(direction)) {
if (isBr(node)) {
return CaretPosition.before(node);
}
return CaretPosition.after(node);
}
return CaretPosition.before(node);
}
// Jumps over BR elements <p>|<br></p><p>a</p> -> <p><br></p><p>|a</p>
function isBrBeforeBlock(node, rootNode) {
var next;
if (!NodeType.isBr(node)) {
return false;
}
next = findCaretPosition(1, CaretPosition.after(node), rootNode);
if (!next) {
return false;
}
return !CaretUtils.isInSameBlock(CaretPosition.before(node), CaretPosition.before(next), rootNode);
}
function findCaretPosition(direction, startCaretPosition, rootNode) {
var container, offset, node, nextNode, innerNode,
rootContentEditableFalseElm, caretPosition;
if (!isElement(rootNode) || !startCaretPosition) {
return null;
}
caretPosition = startCaretPosition;
container = caretPosition.container();
offset = caretPosition.offset();
if (isText(container)) {
if (isBackwards(direction) && offset > 0) {
return CaretPosition(container, --offset);
}
if (isForwards(direction) && offset < container.length) {
return CaretPosition(container, ++offset);
}
node = container;
} else {
if (isBackwards(direction) && offset > 0) {
nextNode = nodeAtIndex(container, offset - 1);
if (isCaretCandidate(nextNode)) {
if (!isAtomic(nextNode)) {
innerNode = CaretUtils.findNode(nextNode, direction, isEditableCaretCandidate, nextNode);
if (innerNode) {
if (isText(innerNode)) {
return CaretPosition(innerNode, innerNode.data.length);
}
return CaretPosition.after(innerNode);
}
}
if (isText(nextNode)) {
return CaretPosition(nextNode, nextNode.data.length);
}
return CaretPosition.before(nextNode);
}
}
if (isForwards(direction) && offset < container.childNodes.length) {
nextNode = nodeAtIndex(container, offset);
if (isCaretCandidate(nextNode)) {
if (isBrBeforeBlock(nextNode, rootNode)) {
return findCaretPosition(direction, CaretPosition.after(nextNode), rootNode);
}
if (!isAtomic(nextNode)) {
innerNode = CaretUtils.findNode(nextNode, direction, isEditableCaretCandidate, nextNode);
if (innerNode) {
if (isText(innerNode)) {
return CaretPosition(innerNode, 0);
}
return CaretPosition.before(innerNode);
}
}
if (isText(nextNode)) {
return CaretPosition(nextNode, 0);
}
return CaretPosition.after(nextNode);
}
}
node = caretPosition.getNode();
}
if ((isForwards(direction) && caretPosition.isAtEnd()) || (isBackwards(direction) && caretPosition.isAtStart())) {
node = CaretUtils.findNode(node, direction, Fun.constant(true), rootNode, true);
if (isEditableCaretCandidate(node)) {
return getCaretCandidatePosition(direction, node);
}
}
nextNode = CaretUtils.findNode(node, direction, isEditableCaretCandidate, rootNode);
rootContentEditableFalseElm = Arr.last(Arr.filter(getParents(container, rootNode), isContentEditableFalse));
if (rootContentEditableFalseElm && (!nextNode || !rootContentEditableFalseElm.contains(nextNode))) {
if (isForwards(direction)) {
caretPosition = CaretPosition.after(rootContentEditableFalseElm);
} else {
caretPosition = CaretPosition.before(rootContentEditableFalseElm);
}
return caretPosition;
}
if (nextNode) {
return getCaretCandidatePosition(direction, nextNode);
}
return null;
}
return function(rootNode) {
return {
/**
* Returns the next logical caret position from the specificed input
* caretPoisiton or null if there isn't any more positions left for example
* at the end specified root element.
*
* @method next
* @param {tinymce.caret.CaretPosition} caretPosition Caret position to start from.
* @return {tinymce.caret.CaretPosition} CaretPosition or null if no position was found.
*/
next: function(caretPosition) {
return findCaretPosition(1, caretPosition, rootNode);
},
/**
* Returns the previous logical caret position from the specificed input
* caretPoisiton or null if there isn't any more positions left for example
* at the end specified root element.
*
* @method prev
* @param {tinymce.caret.CaretPosition} caretPosition Caret position to start from.
* @return {tinymce.caret.CaretPosition} CaretPosition or null if no position was found.
*/
prev: function(caretPosition) {
return findCaretPosition(-1, caretPosition, rootNode);
}
};
};
});
// Included from: js/tinymce/classes/InsertList.js
/**
* InsertList.js
*
* Released under LGPL License.
* Copyright (c) 1999-2016 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Handles inserts of lists into the editor instance.
*
* @class tinymce.InsertList
* @private
*/
define("tinymce/InsertList", [
"tinymce/util/Tools",
"tinymce/caret/CaretWalker",
"tinymce/caret/CaretPosition"
], function(Tools, CaretWalker, CaretPosition) {
var isListFragment = function(fragment) {
var firstChild = fragment.firstChild;
var lastChild = fragment.lastChild;
// Skip meta since it's likely <meta><ul>..</ul>
if (firstChild && firstChild.name === 'meta') {
firstChild = firstChild.next;
}
// Skip mce_marker since it's likely <ul>..</ul><span id="mce_marker"></span>
if (lastChild && lastChild.attr('id') === 'mce_marker') {
lastChild = lastChild.prev;
}
if (!firstChild || firstChild !== lastChild) {
return false;
}
return firstChild.name === 'ul' || firstChild.name === 'ol';
};
var cleanupDomFragment = function (domFragment) {
var firstChild = domFragment.firstChild;
var lastChild = domFragment.lastChild;
// TODO: remove the meta tag from paste logic
if (firstChild && firstChild.nodeName === 'META') {
firstChild.parentNode.removeChild(firstChild);
}
if (lastChild && lastChild.id === 'mce_marker') {
lastChild.parentNode.removeChild(lastChild);
}
return domFragment;
};
var toDomFragment = function(dom, serializer, fragment) {
var html = serializer.serialize(fragment);
var domFragment = dom.createFragment(html);
return cleanupDomFragment(domFragment);
};
var listItems = function(elm) {
return Tools.grep(elm.childNodes, function(child) {
return child.nodeName === 'LI';
});
};
var isEmpty = function (elm) {
return !elm.firstChild;
};
var trimListItems = function(elms) {
return elms.length > 0 && isEmpty(elms[elms.length - 1]) ? elms.slice(0, -1) : elms;
};
var getParentLi = function(dom, node) {
var parentBlock = dom.getParent(node, dom.isBlock);
return parentBlock && parentBlock.nodeName === 'LI' ? parentBlock : null;
};
var isParentBlockLi = function(dom, node) {
return !!getParentLi(dom, node);
};
var getSplit = function(parentNode, rng) {
var beforeRng = rng.cloneRange();
var afterRng = rng.cloneRange();
beforeRng.setStartBefore(parentNode);
afterRng.setEndAfter(parentNode);
return [
beforeRng.cloneContents(),
afterRng.cloneContents()
];
};
var findFirstIn = function(node, rootNode) {
var caretPos = CaretPosition.before(node);
var caretWalker = new CaretWalker(rootNode);
var newCaretPos = caretWalker.next(caretPos);
return newCaretPos ? newCaretPos.toRange() : null;
};
var findLastOf = function(node, rootNode) {
var caretPos = CaretPosition.after(node);
var caretWalker = new CaretWalker(rootNode);
var newCaretPos = caretWalker.prev(caretPos);
return newCaretPos ? newCaretPos.toRange() : null;
};
var insertMiddle = function(target, elms, rootNode, rng) {
var parts = getSplit(target, rng);
var parentElm = target.parentNode;
parentElm.insertBefore(parts[0], target);
Tools.each(elms, function(li) {
parentElm.insertBefore(li, target);
});
parentElm.insertBefore(parts[1], target);
parentElm.removeChild(target);
return findLastOf(elms[elms.length - 1], rootNode);
};
var insertBefore = function(target, elms, rootNode) {
var parentElm = target.parentNode;
Tools.each(elms, function(elm) {
parentElm.insertBefore(elm, target);
});
return findFirstIn(target, rootNode);
};
var insertAfter = function(target, elms, rootNode, dom) {
dom.insertAfter(elms.reverse(), target);
return findLastOf(elms[0], rootNode);
};
var insertAtCaret = function(serializer, dom, rng, fragment) {
var domFragment = toDomFragment(dom, serializer, fragment);
var liTarget = getParentLi(dom, rng.startContainer);
var liElms = trimListItems(listItems(domFragment.firstChild));
var BEGINNING = 1, END = 2;
var rootNode = dom.getRoot();
var isAt = function(location) {
var caretPos = CaretPosition.fromRangeStart(rng);
var caretWalker = new CaretWalker(dom.getRoot());
var newPos = location === BEGINNING ? caretWalker.prev(caretPos) : caretWalker.next(caretPos);
return newPos ? getParentLi(dom, newPos.getNode()) !== liTarget : true;
};
if (isAt(BEGINNING)) {
return insertBefore(liTarget, liElms, rootNode);
} else if (isAt(END)) {
return insertAfter(liTarget, liElms, rootNode, dom);
}
return insertMiddle(liTarget, liElms, rootNode, rng);
};
return {
isListFragment: isListFragment,
insertAtCaret: insertAtCaret,
isParentBlockLi: isParentBlockLi,
trimListItems: trimListItems,
listItems: listItems
};
});
// Included from: js/tinymce/classes/InsertContent.js
/**
* InsertContent.js
*
* Released under LGPL License.
* Copyright (c) 1999-2016 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Handles inserts of contents into the editor instance.
*
* @class tinymce.InsertContent
* @private
*/
define("tinymce/InsertContent", [
"tinymce/Env",
"tinymce/util/Tools",
"tinymce/html/Serializer",
"tinymce/caret/CaretWalker",
"tinymce/caret/CaretPosition",
"tinymce/dom/ElementUtils",
"tinymce/dom/NodeType",
"tinymce/InsertList"
], function(Env, Tools, Serializer, CaretWalker, CaretPosition, ElementUtils, NodeType, InsertList) {
var isTableCell = NodeType.matchNodeNames('td th');
var insertHtmlAtCaret = function(editor, value, details) {
var parser, serializer, parentNode, rootNode, fragment, args;
var marker, rng, node, node2, bookmarkHtml, merge;
var textInlineElements = editor.schema.getTextInlineElements();
var selection = editor.selection, dom = editor.dom;
function trimOrPaddLeftRight(html) {
var rng, container, offset;
rng = selection.getRng(true);
container = rng.startContainer;
offset = rng.startOffset;
function hasSiblingText(siblingName) {
return container[siblingName] && container[siblingName].nodeType == 3;
}
if (container.nodeType == 3) {
if (offset > 0) {
html = html.replace(/^&nbsp;/, ' ');
} else if (!hasSiblingText('previousSibling')) {
html = html.replace(/^ /, '&nbsp;');
}
if (offset < container.length) {
html = html.replace(/&nbsp;(<br>|)$/, ' ');
} else if (!hasSiblingText('nextSibling')) {
html = html.replace(/(&nbsp;| )(<br>|)$/, '&nbsp;');
}
}
return html;
}
// Removes &nbsp; from a [b] c -> a &nbsp;c -> a c
function trimNbspAfterDeleteAndPaddValue() {
var rng, container, offset;
rng = selection.getRng(true);
container = rng.startContainer;
offset = rng.startOffset;
if (container.nodeType == 3 && rng.collapsed) {
if (container.data[offset] === '\u00a0') {
container.deleteData(offset, 1);
if (!/[\u00a0| ]$/.test(value)) {
value += ' ';
}
} else if (container.data[offset - 1] === '\u00a0') {
container.deleteData(offset - 1, 1);
if (!/[\u00a0| ]$/.test(value)) {
value = ' ' + value;
}
}
}
}
function reduceInlineTextElements() {
if (merge) {
var root = editor.getBody(), elementUtils = new ElementUtils(dom);
Tools.each(dom.select('*[data-mce-fragment]'), function(node) {
for (var testNode = node.parentNode; testNode && testNode != root; testNode = testNode.parentNode) {
if (textInlineElements[node.nodeName.toLowerCase()] && elementUtils.compare(testNode, node)) {
dom.remove(node, true);
}
}
});
}
}
function markFragmentElements(fragment) {
var node = fragment;
while ((node = node.walk())) {
if (node.type === 1) {
node.attr('data-mce-fragment', '1');
}
}
}
function umarkFragmentElements(elm) {
Tools.each(elm.getElementsByTagName('*'), function(elm) {
elm.removeAttribute('data-mce-fragment');
});
}
function isPartOfFragment(node) {
return !!node.getAttribute('data-mce-fragment');
}
function canHaveChildren(node) {
return node && !editor.schema.getShortEndedElements()[node.nodeName];
}
function moveSelectionToMarker(marker) {
var parentEditableFalseElm, parentBlock, nextRng;
function getContentEditableFalseParent(node) {
var root = editor.getBody();
for (; node && node !== root; node = node.parentNode) {
if (editor.dom.getContentEditable(node) === 'false') {
return node;
}
}
return null;
}
if (!marker) {
return;
}
selection.scrollIntoView(marker);
// If marker is in cE=false then move selection to that element instead
parentEditableFalseElm = getContentEditableFalseParent(marker);
if (parentEditableFalseElm) {
dom.remove(marker);
selection.select(parentEditableFalseElm);
return;
}
// Move selection before marker and remove it
rng = dom.createRng();
// If previous sibling is a text node set the selection to the end of that node
node = marker.previousSibling;
if (node && node.nodeType == 3) {
rng.setStart(node, node.nodeValue.length);
// TODO: Why can't we normalize on IE
if (!Env.ie) {
node2 = marker.nextSibling;
if (node2 && node2.nodeType == 3) {
node.appendData(node2.data);
node2.parentNode.removeChild(node2);
}
}
} else {
// If the previous sibling isn't a text node or doesn't exist set the selection before the marker node
rng.setStartBefore(marker);
rng.setEndBefore(marker);
}
function findNextCaretRng(rng) {
var caretPos = CaretPosition.fromRangeStart(rng);
var caretWalker = new CaretWalker(editor.getBody());
caretPos = caretWalker.next(caretPos);
if (caretPos) {
return caretPos.toRange();
}
}
// Remove the marker node and set the new range
parentBlock = dom.getParent(marker, dom.isBlock);
dom.remove(marker);
if (parentBlock && dom.isEmpty(parentBlock)) {
editor.$(parentBlock).empty();
rng.setStart(parentBlock, 0);
rng.setEnd(parentBlock, 0);
if (!isTableCell(parentBlock) && !isPartOfFragment(parentBlock) && (nextRng = findNextCaretRng(rng))) {
rng = nextRng;
dom.remove(parentBlock);
} else {
dom.add(parentBlock, dom.create('br', {'data-mce-bogus': '1'}));
}
}
selection.setRng(rng);
}
// Check for whitespace before/after value
if (/^ | $/.test(value)) {
value = trimOrPaddLeftRight(value);
}
// Setup parser and serializer
parser = editor.parser;
merge = details.merge;
serializer = new Serializer({
validate: editor.settings.validate
}, editor.schema);
bookmarkHtml = '<span id="mce_marker" data-mce-type="bookmark">&#xFEFF;&#x200B;</span>';
// Run beforeSetContent handlers on the HTML to be inserted
args = {content: value, format: 'html', selection: true};
editor.fire('BeforeSetContent', args);
value = args.content;
// Add caret at end of contents if it's missing
if (value.indexOf('{$caret}') == -1) {
value += '{$caret}';
}
// Replace the caret marker with a span bookmark element
value = value.replace(/\{\$caret\}/, bookmarkHtml);
// If selection is at <body>|<p></p> then move it into <body><p>|</p>
rng = selection.getRng();
var caretElement = rng.startContainer || (rng.parentElement ? rng.parentElement() : null);
var body = editor.getBody();
if (caretElement === body && selection.isCollapsed()) {
if (dom.isBlock(body.firstChild) && canHaveChildren(body.firstChild) && dom.isEmpty(body.firstChild)) {
rng = dom.createRng();
rng.setStart(body.firstChild, 0);
rng.setEnd(body.firstChild, 0);
selection.setRng(rng);
}
}
// Insert node maker where we will insert the new HTML and get it's parent
if (!selection.isCollapsed()) {
// Fix for #2595 seems that delete removes one extra character on
// WebKit for some odd reason if you double click select a word
editor.selection.setRng(editor.selection.getRng());
editor.getDoc().execCommand('Delete', false, null);
trimNbspAfterDeleteAndPaddValue();
}
parentNode = selection.getNode();
// Parse the fragment within the context of the parent node
var parserArgs = {context: parentNode.nodeName.toLowerCase(), data: details.data};
fragment = parser.parse(value, parserArgs);
// Custom handling of lists
if (details.paste === true && InsertList.isListFragment(fragment) && InsertList.isParentBlockLi(dom, parentNode)) {
rng = InsertList.insertAtCaret(serializer, dom, editor.selection.getRng(true), fragment);
editor.selection.setRng(rng);
editor.fire('SetContent', args);
return;
}
markFragmentElements(fragment);
// Move the caret to a more suitable location
node = fragment.lastChild;
if (node.attr('id') == 'mce_marker') {
marker = node;
for (node = node.prev; node; node = node.walk(true)) {
if (node.type == 3 || !dom.isBlock(node.name)) {
if (editor.schema.isValidChild(node.parent.name, 'span')) {
node.parent.insert(marker, node, node.name === 'br');
}
break;
}
}
}
editor._selectionOverrides.showBlockCaretContainer(parentNode);
// If parser says valid we can insert the contents into that parent
if (!parserArgs.invalid) {
value = serializer.serialize(fragment);
// Check if parent is empty or only has one BR element then set the innerHTML of that parent
node = parentNode.firstChild;
node2 = parentNode.lastChild;
if (!node || (node === node2 && node.nodeName === 'BR')) {
dom.setHTML(parentNode, value);
} else {
selection.setContent(value);
}
} else {
// If the fragment was invalid within that context then we need
// to parse and process the parent it's inserted into
// Insert bookmark node and get the parent
selection.setContent(bookmarkHtml);
parentNode = selection.getNode();
rootNode = editor.getBody();
// Opera will return the document node when selection is in root
if (parentNode.nodeType == 9) {
parentNode = node = rootNode;
} else {
node = parentNode;
}
// Find the ancestor just before the root element
while (node !== rootNode) {
parentNode = node;
node = node.parentNode;
}
// Get the outer/inner HTML depending on if we are in the root and parser and serialize that
value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode);
value = serializer.serialize(
parser.parse(
// Need to replace by using a function since $ in the contents would otherwise be a problem
value.replace(/<span (id="mce_marker"|id=mce_marker).+?<\/span>/i, function() {
return serializer.serialize(fragment);
})
)
);
// Set the inner/outer HTML depending on if we are in the root or not
if (parentNode == rootNode) {
dom.setHTML(rootNode, value);
} else {
dom.setOuterHTML(parentNode, value);
}
}
reduceInlineTextElements();
moveSelectionToMarker(dom.get('mce_marker'));
umarkFragmentElements(editor.getBody());
editor.fire('SetContent', args);
editor.addVisual();
};
var processValue = function (value) {
var details;
if (typeof value !== 'string') {
details = Tools.extend({
paste: value.paste,
data: {
paste: value.paste
}
}, value);
return {
content: value.content,
details: details
};
}
return {
content: value,
details: {}
};
};
var insertAtCaret = function (editor, value) {
var result = processValue(value);
insertHtmlAtCaret(editor, result.content, result.details);
};
return {
insertAtCaret: insertAtCaret
};
});
// Included from: js/tinymce/classes/EditorCommands.js
/**
* EditorCommands.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class enables you to add custom editor commands and it contains
* overrides for native browser commands to address various bugs and issues.
*
* @class tinymce.EditorCommands
*/
define("tinymce/EditorCommands", [
"tinymce/Env",
"tinymce/util/Tools",
"tinymce/dom/RangeUtils",
"tinymce/dom/TreeWalker",
"tinymce/InsertContent"
], function(Env, Tools, RangeUtils, TreeWalker, InsertContent) {
// Added for compression purposes
var each = Tools.each, extend = Tools.extend;
var map = Tools.map, inArray = Tools.inArray, explode = Tools.explode;
var isOldIE = Env.ie && Env.ie < 11;
var TRUE = true, FALSE = false;
return function(editor) {
var dom, selection, formatter,
commands = {state: {}, exec: {}, value: {}},
settings = editor.settings,
bookmark;
editor.on('PreInit', function() {
dom = editor.dom;
selection = editor.selection;
settings = editor.settings;
formatter = editor.formatter;
});
/**
* Executes the specified command.
*
* @method execCommand
* @param {String} command Command to execute.
* @param {Boolean} ui Optional user interface state.
* @param {Object} value Optional value for command.
* @param {Object} args Optional extra arguments to the execCommand.
* @return {Boolean} true/false if the command was found or not.
*/
function execCommand(command, ui, value, args) {
var func, customCommand, state = 0;
if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint)$/.test(command) && (!args || !args.skip_focus)) {
editor.focus();
}
args = editor.fire('BeforeExecCommand', {command: command, ui: ui, value: value});
if (args.isDefaultPrevented()) {
return false;
}
customCommand = command.toLowerCase();
if ((func = commands.exec[customCommand])) {
func(customCommand, ui, value);
editor.fire('ExecCommand', {command: command, ui: ui, value: value});
return true;
}
// Plugin commands
each(editor.plugins, function(p) {
if (p.execCommand && p.execCommand(command, ui, value)) {
editor.fire('ExecCommand', {command: command, ui: ui, value: value});
state = true;
return false;
}
});
if (state) {
return state;
}
// Theme commands
if (editor.theme && editor.theme.execCommand && editor.theme.execCommand(command, ui, value)) {
editor.fire('ExecCommand', {command: command, ui: ui, value: value});
return true;
}
// Browser commands
try {
state = editor.getDoc().execCommand(command, ui, value);
} catch (ex) {
// Ignore old IE errors
}
if (state) {
editor.fire('ExecCommand', {command: command, ui: ui, value: value});
return true;
}
return false;
}
/**
* Queries the current state for a command for example if the current selection is "bold".
*
* @method queryCommandState
* @param {String} command Command to check the state of.
* @return {Boolean/Number} true/false if the selected contents is bold or not, -1 if it's not found.
*/
function queryCommandState(command) {
var func;
// Is hidden then return undefined
if (editor.quirks.isHidden()) {
return;
}
command = command.toLowerCase();
if ((func = commands.state[command])) {
return func(command);
}
// Browser commands
try {
return editor.getDoc().queryCommandState(command);
} catch (ex) {
// Fails sometimes see bug: 1896577
}
return false;
}
/**
* Queries the command value for example the current fontsize.
*
* @method queryCommandValue
* @param {String} command Command to check the value of.
* @return {Object} Command value of false if it's not found.
*/
function queryCommandValue(command) {
var func;
// Is hidden then return undefined
if (editor.quirks.isHidden()) {
return;
}
command = command.toLowerCase();
if ((func = commands.value[command])) {
return func(command);
}
// Browser commands
try {
return editor.getDoc().queryCommandValue(command);
} catch (ex) {
// Fails sometimes see bug: 1896577
}
}
/**
* Adds commands to the command collection.
*
* @method addCommands
* @param {Object} command_list Name/value collection with commands to add, the names can also be comma separated.
* @param {String} type Optional type to add, defaults to exec. Can be value or state as well.
*/
function addCommands(command_list, type) {
type = type || 'exec';
each(command_list, function(callback, command) {
each(command.toLowerCase().split(','), function(command) {
commands[type][command] = callback;
});
});
}
function addCommand(command, callback, scope) {
command = command.toLowerCase();
commands.exec[command] = function(command, ui, value, args) {
return callback.call(scope || editor, ui, value, args);
};
}
/**
* Returns true/false if the command is supported or not.
*
* @method queryCommandSupported
* @param {String} command Command that we check support for.
* @return {Boolean} true/false if the command is supported or not.
*/
function queryCommandSupported(command) {
command = command.toLowerCase();
if (commands.exec[command]) {
return true;
}
// Browser commands
try {
return editor.getDoc().queryCommandSupported(command);
} catch (ex) {
// Fails sometimes see bug: 1896577
}
return false;
}
function addQueryStateHandler(command, callback, scope) {
command = command.toLowerCase();
commands.state[command] = function() {
return callback.call(scope || editor);
};
}
function addQueryValueHandler(command, callback, scope) {
command = command.toLowerCase();
commands.value[command] = function() {
return callback.call(scope || editor);
};
}
function hasCustomCommand(command) {
command = command.toLowerCase();
return !!commands.exec[command];
}
// Expose public methods
extend(this, {
execCommand: execCommand,
queryCommandState: queryCommandState,
queryCommandValue: queryCommandValue,
queryCommandSupported: queryCommandSupported,
addCommands: addCommands,
addCommand: addCommand,
addQueryStateHandler: addQueryStateHandler,
addQueryValueHandler: addQueryValueHandler,
hasCustomCommand: hasCustomCommand
});
// Private methods
function execNativeCommand(command, ui, value) {
if (ui === undefined) {
ui = FALSE;
}
if (value === undefined) {
value = null;
}
return editor.getDoc().execCommand(command, ui, value);
}
function isFormatMatch(name) {
return formatter.match(name);
}
function toggleFormat(name, value) {
formatter.toggle(name, value ? {value: value} : undefined);
editor.nodeChanged();
}
function storeSelection(type) {
bookmark = selection.getBookmark(type);
}
function restoreSelection() {
selection.moveToBookmark(bookmark);
}
// Add execCommand overrides
addCommands({
// Ignore these, added for compatibility
'mceResetDesignMode,mceBeginUndoLevel': function() {},
// Add undo manager logic
'mceEndUndoLevel,mceAddUndoLevel': function() {
editor.undoManager.add();
},
'Cut,Copy,Paste': function(command) {
var doc = editor.getDoc(), failed;
// Try executing the native command
try {
execNativeCommand(command);
} catch (ex) {
// Command failed
failed = TRUE;
}
// Chrome reports the paste command as supported however older IE:s will return false for cut/paste
if (command === 'paste' && !doc.queryCommandEnabled(command)) {
failed = true;
}
// Present alert message about clipboard access not being available
if (failed || !doc.queryCommandSupported(command)) {
var msg = editor.translate(
"Your browser doesn't support direct access to the clipboard. " +
"Please use the Ctrl+X/C/V keyboard shortcuts instead."
);
if (Env.mac) {
msg = msg.replace(/Ctrl\+/g, '\u2318+');
}
editor.notificationManager.open({text: msg, type: 'error'});
}
},
// Override unlink command
unlink: function() {
if (selection.isCollapsed()) {
var elm = editor.dom.getParent(editor.selection.getStart(), 'a');
if (elm) {
editor.dom.remove(elm, true);
}
return;
}
formatter.remove("link");
},
// Override justify commands to use the text formatter engine
'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull,JustifyNone': function(command) {
var align = command.substring(7);
if (align == 'full') {
align = 'justify';
}
// Remove all other alignments first
each('left,center,right,justify'.split(','), function(name) {
if (align != name) {
formatter.remove('align' + name);
}
});
if (align != 'none') {
toggleFormat('align' + align);
}
},
// Override list commands to fix WebKit bug
'InsertUnorderedList,InsertOrderedList': function(command) {
var listElm, listParent;
execNativeCommand(command);
// WebKit produces lists within block elements so we need to split them
// we will replace the native list creation logic to custom logic later on
// TODO: Remove this when the list creation logic is removed
listElm = dom.getParent(selection.getNode(), 'ol,ul');
if (listElm) {
listParent = listElm.parentNode;
// If list is within a text block then split that block
if (/^(H[1-6]|P|ADDRESS|PRE)$/.test(listParent.nodeName)) {
storeSelection();
dom.split(listParent, listElm);
restoreSelection();
}
}
},
// Override commands to use the text formatter engine
'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function(command) {
toggleFormat(command);
},
// Override commands to use the text formatter engine
'ForeColor,HiliteColor,FontName': function(command, ui, value) {
toggleFormat(command, value);
},
FontSize: function(command, ui, value) {
var fontClasses, fontSizes;
// Convert font size 1-7 to styles
if (value >= 1 && value <= 7) {
fontSizes = explode(settings.font_size_style_values);
fontClasses = explode(settings.font_size_classes);
if (fontClasses) {
value = fontClasses[value - 1] || value;
} else {
value = fontSizes[value - 1] || value;
}
}
toggleFormat(command, value);
},
RemoveFormat: function(command) {
formatter.remove(command);
},
mceBlockQuote: function() {
toggleFormat('blockquote');
},
FormatBlock: function(command, ui, value) {
return toggleFormat(value || 'p');
},
mceCleanup: function() {
var bookmark = selection.getBookmark();
editor.setContent(editor.getContent({cleanup: TRUE}), {cleanup: TRUE});
selection.moveToBookmark(bookmark);
},
mceRemoveNode: function(command, ui, value) {
var node = value || selection.getNode();
// Make sure that the body node isn't removed
if (node != editor.getBody()) {
storeSelection();
editor.dom.remove(node, TRUE);
restoreSelection();
}
},
mceSelectNodeDepth: function(command, ui, value) {
var counter = 0;
dom.getParent(selection.getNode(), function(node) {
if (node.nodeType == 1 && counter++ == value) {
selection.select(node);
return FALSE;
}
}, editor.getBody());
},
mceSelectNode: function(command, ui, value) {
selection.select(value);
},
mceInsertContent: function(command, ui, value) {
InsertContent.insertAtCaret(editor, value);
},
mceInsertRawHTML: function(command, ui, value) {
selection.setContent('tiny_mce_marker');
editor.setContent(
editor.getContent().replace(/tiny_mce_marker/g, function() {
return value;
})
);
},
mceToggleFormat: function(command, ui, value) {
toggleFormat(value);
},
mceSetContent: function(command, ui, value) {
editor.setContent(value);
},
'Indent,Outdent': function(command) {
var intentValue, indentUnit, value;
// Setup indent level
intentValue = settings.indentation;
indentUnit = /[a-z%]+$/i.exec(intentValue);
intentValue = parseInt(intentValue, 10);
if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) {
// If forced_root_blocks is set to false we don't have a block to indent so lets create a div
if (!settings.forced_root_block && !dom.getParent(selection.getNode(), dom.isBlock)) {
formatter.apply('div');
}
each(selection.getSelectedBlocks(), function(element) {
if (dom.getContentEditable(element) === "false") {
return;
}
if (element.nodeName !== "LI") {
var indentStyleName = editor.getParam('indent_use_margin', false) ? 'margin' : 'padding';
indentStyleName = element.nodeName === 'TABLE' ? 'margin' : indentStyleName;
indentStyleName += dom.getStyle(element, 'direction', true) == 'rtl' ? 'Right' : 'Left';
if (command == 'outdent') {
value = Math.max(0, parseInt(element.style[indentStyleName] || 0, 10) - intentValue);
dom.setStyle(element, indentStyleName, value ? value + indentUnit : '');
} else {
value = (parseInt(element.style[indentStyleName] || 0, 10) + intentValue) + indentUnit;
dom.setStyle(element, indentStyleName, value);
}
}
});
} else {
execNativeCommand(command);
}
},
mceRepaint: function() {
},
InsertHorizontalRule: function() {
editor.execCommand('mceInsertContent', false, '<hr />');
},
mceToggleVisualAid: function() {
editor.hasVisual = !editor.hasVisual;
editor.addVisual();
},
mceReplaceContent: function(command, ui, value) {
editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({format: 'text'})));
},
mceInsertLink: function(command, ui, value) {
var anchor;
if (typeof value == 'string') {
value = {href: value};
}
anchor = dom.getParent(selection.getNode(), 'a');
// Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here.
value.href = value.href.replace(' ', '%20');
// Remove existing links if there could be child links or that the href isn't specified
if (!anchor || !value.href) {
formatter.remove('link');
}
// Apply new link to selection
if (value.href) {
formatter.apply('link', value, anchor);
}
},
selectAll: function() {
var root = dom.getRoot(), rng;
if (selection.getRng().setStart) {
rng = dom.createRng();
rng.setStart(root, 0);
rng.setEnd(root, root.childNodes.length);
selection.setRng(rng);
} else {
// IE will render it's own root level block elements and sometimes
// even put font elements in them when the user starts typing. So we need to
// move the selection to a more suitable element from this:
// <body>|<p></p></body> to this: <body><p>|</p></body>
rng = selection.getRng();
if (!rng.item) {
rng.moveToElementText(root);
rng.select();
}
}
},
"delete": function() {
execNativeCommand("Delete");
// Check if body is empty after the delete call if so then set the contents
// to an empty string and move the caret to any block produced by that operation
// this fixes the issue with root blocks not being properly produced after a delete call on IE
var body = editor.getBody();
if (dom.isEmpty(body)) {
editor.setContent('');
if (body.firstChild && dom.isBlock(body.firstChild)) {
editor.selection.setCursorLocation(body.firstChild, 0);
} else {
editor.selection.setCursorLocation(body, 0);
}
}
},
mceNewDocument: function() {
editor.setContent('');
},
InsertLineBreak: function(command, ui, value) {
// We load the current event in from EnterKey.js when appropriate to heed
// certain event-specific variations such as ctrl-enter in a list
var evt = value;
var brElm, extraBr, marker;
var rng = selection.getRng(true);
new RangeUtils(dom).normalize(rng);
var offset = rng.startOffset;
var container = rng.startContainer;
// Resolve node index
if (container.nodeType == 1 && container.hasChildNodes()) {
var isAfterLastNodeInContainer = offset > container.childNodes.length - 1;
container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container;
if (isAfterLastNodeInContainer && container.nodeType == 3) {
offset = container.nodeValue.length;
} else {
offset = 0;
}
}
var parentBlock = dom.getParent(container, dom.isBlock);
var parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5
var containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null;
var containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5
// Enter inside block contained within a LI then split or insert before/after LI
var isControlKey = evt && evt.ctrlKey;
if (containerBlockName == 'LI' && !isControlKey) {
parentBlock = containerBlock;
parentBlockName = containerBlockName;
}
// Walks the parent block to the right and look for BR elements
function hasRightSideContent() {
var walker = new TreeWalker(container, parentBlock), node;
var nonEmptyElementsMap = editor.schema.getNonEmptyElements();
while ((node = walker.next())) {
if (nonEmptyElementsMap[node.nodeName.toLowerCase()] || node.length > 0) {
return true;
}
}
}
if (container && container.nodeType == 3 && offset >= container.nodeValue.length) {
// Insert extra BR element at the end block elements
if (!isOldIE && !hasRightSideContent()) {
brElm = dom.create('br');
rng.insertNode(brElm);
rng.setStartAfter(brElm);
rng.setEndAfter(brElm);
extraBr = true;
}
}
brElm = dom.create('br');
rng.insertNode(brElm);
// Rendering modes below IE8 doesn't display BR elements in PRE unless we have a \n before it
var documentMode = dom.doc.documentMode;
if (isOldIE && parentBlockName == 'PRE' && (!documentMode || documentMode < 8)) {
brElm.parentNode.insertBefore(dom.doc.createTextNode('\r'), brElm);
}
// Insert temp marker and scroll to that
marker = dom.create('span', {}, '&nbsp;');
brElm.parentNode.insertBefore(marker, brElm);
selection.scrollIntoView(marker);
dom.remove(marker);
if (!extraBr) {
rng.setStartAfter(brElm);
rng.setEndAfter(brElm);
} else {
rng.setStartBefore(brElm);
rng.setEndBefore(brElm);
}
selection.setRng(rng);
editor.undoManager.add();
return TRUE;
}
});
// Add queryCommandState overrides
addCommands({
// Override justify commands
'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull': function(command) {
var name = 'align' + command.substring(7);
var nodes = selection.isCollapsed() ? [dom.getParent(selection.getNode(), dom.isBlock)] : selection.getSelectedBlocks();
var matches = map(nodes, function(node) {
return !!formatter.matchNode(node, name);
});
return inArray(matches, TRUE) !== -1;
},
'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function(command) {
return isFormatMatch(command);
},
mceBlockQuote: function() {
return isFormatMatch('blockquote');
},
Outdent: function() {
var node;
if (settings.inline_styles) {
if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) {
return TRUE;
}
if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) {
return TRUE;
}
}
return (
queryCommandState('InsertUnorderedList') ||
queryCommandState('InsertOrderedList') ||
(!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE'))
);
},
'InsertUnorderedList,InsertOrderedList': function(command) {
var list = dom.getParent(selection.getNode(), 'ul,ol');
return list &&
(
command === 'insertunorderedlist' && list.tagName === 'UL' ||
command === 'insertorderedlist' && list.tagName === 'OL'
);
}
}, 'state');
// Add queryCommandValue overrides
addCommands({
'FontSize,FontName': function(command) {
var value = 0, parent;
if ((parent = dom.getParent(selection.getNode(), 'span'))) {
if (command == 'fontsize') {
value = parent.style.fontSize;
} else {
value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase();
}
}
return value;
}
}, 'value');
// Add undo manager logic
addCommands({
Undo: function() {
editor.undoManager.undo();
},
Redo: function() {
editor.undoManager.redo();
}
});
};
});
// Included from: js/tinymce/classes/util/URI.js
/**
* URI.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class handles parsing, modification and serialization of URI/URL strings.
* @class tinymce.util.URI
*/
define("tinymce/util/URI", [
"tinymce/util/Tools"
], function(Tools) {
var each = Tools.each, trim = Tools.trim;
var queryParts = "source protocol authority userInfo user password host port relative path directory file query anchor".split(' ');
var DEFAULT_PORTS = {
'ftp': 21,
'http': 80,
'https': 443,
'mailto': 25
};
/**
* Constructs a new URI instance.
*
* @constructor
* @method URI
* @param {String} url URI string to parse.
* @param {Object} settings Optional settings object.
*/
function URI(url, settings) {
var self = this, baseUri, base_url;
url = trim(url);
settings = self.settings = settings || {};
baseUri = settings.base_uri;
// Strange app protocol that isn't http/https or local anchor
// For example: mailto,skype,tel etc.
if (/^([\w\-]+):([^\/]{2})/i.test(url) || /^\s*#/.test(url)) {
self.source = url;
return;
}
var isProtocolRelative = url.indexOf('//') === 0;
// Absolute path with no host, fake host and protocol
if (url.indexOf('/') === 0 && !isProtocolRelative) {
url = (baseUri ? baseUri.protocol || 'http' : 'http') + '://mce_host' + url;
}
// Relative path http:// or protocol relative //path
if (!/^[\w\-]*:?\/\//.test(url)) {
base_url = settings.base_uri ? settings.base_uri.path : new URI(location.href).directory;
if (settings.base_uri.protocol === "") {
url = '//mce_host' + self.toAbsPath(base_url, url);
} else {
url = /([^#?]*)([#?]?.*)/.exec(url);
url = ((baseUri && baseUri.protocol) || 'http') + '://mce_host' + self.toAbsPath(base_url, url[1]) + url[2];
}
}
// Parse URL (Credits goes to Steave, http://blog.stevenlevithan.com/archives/parseuri)
url = url.replace(/@@/g, '(mce_at)'); // Zope 3 workaround, they use @@something
/*jshint maxlen: 255 */
/*eslint max-len: 0 */
url = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(url);
each(queryParts, function(v, i) {
var part = url[i];
// Zope 3 workaround, they use @@something
if (part) {
part = part.replace(/\(mce_at\)/g, '@@');
}
self[v] = part;
});
if (baseUri) {
if (!self.protocol) {
self.protocol = baseUri.protocol;
}
if (!self.userInfo) {
self.userInfo = baseUri.userInfo;
}
if (!self.port && self.host === 'mce_host') {
self.port = baseUri.port;
}
if (!self.host || self.host === 'mce_host') {
self.host = baseUri.host;
}
self.source = '';
}
if (isProtocolRelative) {
self.protocol = '';
}
//t.path = t.path || '/';
}
URI.prototype = {
/**
* Sets the internal path part of the URI.
*
* @method setPath
* @param {string} path Path string to set.
*/
setPath: function(path) {
var self = this;
path = /^(.*?)\/?(\w+)?$/.exec(path);
// Update path parts
self.path = path[0];
self.directory = path[1];
self.file = path[2];
// Rebuild source
self.source = '';
self.getURI();
},
/**
* Converts the specified URI into a relative URI based on the current URI instance location.
*
* @method toRelative
* @param {String} uri URI to convert into a relative path/URI.
* @return {String} Relative URI from the point specified in the current URI instance.
* @example
* // Converts an absolute URL to an relative URL url will be somedir/somefile.htm
* var url = new tinymce.util.URI('http://www.site.com/dir/').toRelative('http://www.site.com/dir/somedir/somefile.htm');
*/
toRelative: function(uri) {
var self = this, output;
if (uri === "./") {
return uri;
}
uri = new URI(uri, {base_uri: self});
// Not on same domain/port or protocol
if ((uri.host != 'mce_host' && self.host != uri.host && uri.host) || self.port != uri.port ||
(self.protocol != uri.protocol && uri.protocol !== "")) {
return uri.getURI();
}
var tu = self.getURI(), uu = uri.getURI();
// Allow usage of the base_uri when relative_urls = true
if (tu == uu || (tu.charAt(tu.length - 1) == "/" && tu.substr(0, tu.length - 1) == uu)) {
return tu;
}
output = self.toRelPath(self.path, uri.path);
// Add query
if (uri.query) {
output += '?' + uri.query;
}
// Add anchor
if (uri.anchor) {
output += '#' + uri.anchor;
}
return output;
},
/**
* Converts the specified URI into a absolute URI based on the current URI instance location.
*
* @method toAbsolute
* @param {String} uri URI to convert into a relative path/URI.
* @param {Boolean} noHost No host and protocol prefix.
* @return {String} Absolute URI from the point specified in the current URI instance.
* @example
* // Converts an relative URL to an absolute URL url will be http://www.site.com/dir/somedir/somefile.htm
* var url = new tinymce.util.URI('http://www.site.com/dir/').toAbsolute('somedir/somefile.htm');
*/
toAbsolute: function(uri, noHost) {
uri = new URI(uri, {base_uri: this});
return uri.getURI(noHost && this.isSameOrigin(uri));
},
/**
* Determine whether the given URI has the same origin as this URI. Based on RFC-6454.
* Supports default ports for protocols listed in DEFAULT_PORTS. Unsupported protocols will fail safe: they
* won't match, if the port specifications differ.
*
* @method isSameOrigin
* @param {tinymce.util.URI} uri Uri instance to compare.
* @returns {Boolean} True if the origins are the same.
*/
isSameOrigin: function(uri) {
if (this.host == uri.host && this.protocol == uri.protocol) {
if (this.port == uri.port) {
return true;
}
var defaultPort = DEFAULT_PORTS[this.protocol];
if (defaultPort && ((this.port || defaultPort) == (uri.port || defaultPort))) {
return true;
}
}
return false;
},
/**
* Converts a absolute path into a relative path.
*
* @method toRelPath
* @param {String} base Base point to convert the path from.
* @param {String} path Absolute path to convert into a relative path.
*/
toRelPath: function(base, path) {
var items, breakPoint = 0, out = '', i, l;
// Split the paths
base = base.substring(0, base.lastIndexOf('/'));
base = base.split('/');
items = path.split('/');
if (base.length >= items.length) {
for (i = 0, l = base.length; i < l; i++) {
if (i >= items.length || base[i] != items[i]) {
breakPoint = i + 1;
break;
}
}
}
if (base.length < items.length) {
for (i = 0, l = items.length; i < l; i++) {
if (i >= base.length || base[i] != items[i]) {
breakPoint = i + 1;
break;
}
}
}
if (breakPoint === 1) {
return path;
}
for (i = 0, l = base.length - (breakPoint - 1); i < l; i++) {
out += "../";
}
for (i = breakPoint - 1, l = items.length; i < l; i++) {
if (i != breakPoint - 1) {
out += "/" + items[i];
} else {
out += items[i];
}
}
return out;
},
/**
* Converts a relative path into a absolute path.
*
* @method toAbsPath
* @param {String} base Base point to convert the path from.
* @param {String} path Relative path to convert into an absolute path.
*/
toAbsPath: function(base, path) {
var i, nb = 0, o = [], tr, outPath;
// Split paths
tr = /\/$/.test(path) ? '/' : '';
base = base.split('/');
path = path.split('/');
// Remove empty chunks
each(base, function(k) {
if (k) {
o.push(k);
}
});
base = o;
// Merge relURLParts chunks
for (i = path.length - 1, o = []; i >= 0; i--) {
// Ignore empty or .
if (path[i].length === 0 || path[i] === ".") {
continue;
}
// Is parent
if (path[i] === '..') {
nb++;
continue;
}
// Move up
if (nb > 0) {
nb--;
continue;
}
o.push(path[i]);
}
i = base.length - nb;
// If /a/b/c or /
if (i <= 0) {
outPath = o.reverse().join('/');
} else {
outPath = base.slice(0, i).join('/') + '/' + o.reverse().join('/');
}
// Add front / if it's needed
if (outPath.indexOf('/') !== 0) {
outPath = '/' + outPath;
}
// Add traling / if it's needed
if (tr && outPath.lastIndexOf('/') !== outPath.length - 1) {
outPath += tr;
}
return outPath;
},
/**
* Returns the full URI of the internal structure.
*
* @method getURI
* @param {Boolean} noProtoHost Optional no host and protocol part. Defaults to false.
*/
getURI: function(noProtoHost) {
var s, self = this;
// Rebuild source
if (!self.source || noProtoHost) {
s = '';
if (!noProtoHost) {
if (self.protocol) {
s += self.protocol + '://';
} else {
s += '//';
}
if (self.userInfo) {
s += self.userInfo + '@';
}
if (self.host) {
s += self.host;
}
if (self.port) {
s += ':' + self.port;
}
}
if (self.path) {
s += self.path;
}
if (self.query) {
s += '?' + self.query;
}
if (self.anchor) {
s += '#' + self.anchor;
}
self.source = s;
}
return self.source;
}
};
URI.parseDataUri = function(uri) {
var type, matches;
uri = decodeURIComponent(uri).split(',');
matches = /data:([^;]+)/.exec(uri[0]);
if (matches) {
type = matches[1];
}
return {
type: type,
data: uri[1]
};
};
URI.getDocumentBaseUrl = function(loc) {
var baseUrl;
// Pass applewebdata:// and other non web protocols though
if (loc.protocol.indexOf('http') !== 0 && loc.protocol !== 'file:') {
baseUrl = loc.href;
} else {
baseUrl = loc.protocol + '//' + loc.host + loc.pathname;
}
if (/^[^:]+:\/\/\/?[^\/]+\//.test(baseUrl)) {
baseUrl = baseUrl.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, '');
if (!/[\/\\]$/.test(baseUrl)) {
baseUrl += '/';
}
}
return baseUrl;
};
return URI;
});
// Included from: js/tinymce/classes/util/Class.js
/**
* Class.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This utilitiy class is used for easier inheritance.
*
* Features:
* * Exposed super functions: this._super();
* * Mixins
* * Dummy functions
* * Property functions: var value = object.value(); and object.value(newValue);
* * Static functions
* * Defaults settings
*/
define("tinymce/util/Class", [
"tinymce/util/Tools"
], function(Tools) {
var each = Tools.each, extend = Tools.extend;
var extendClass, initializing;
function Class() {
}
// Provides classical inheritance, based on code made by John Resig
Class.extend = extendClass = function(prop) {
var self = this, _super = self.prototype, prototype, name, member;
// The dummy class constructor
function Class() {
var i, mixins, mixin, self = this;
// All construction is actually done in the init method
if (!initializing) {
// Run class constuctor
if (self.init) {
self.init.apply(self, arguments);
}
// Run mixin constructors
mixins = self.Mixins;
if (mixins) {
i = mixins.length;
while (i--) {
mixin = mixins[i];
if (mixin.init) {
mixin.init.apply(self, arguments);
}
}
}
}
}
// Dummy function, needs to be extended in order to provide functionality
function dummy() {
return this;
}
// Creates a overloaded method for the class
// this enables you to use this._super(); to call the super function
function createMethod(name, fn) {
return function() {
var self = this, tmp = self._super, ret;
self._super = _super[name];
ret = fn.apply(self, arguments);
self._super = tmp;
return ret;
};
}
// Instantiate a base class (but only create the instance,
// don't run the init constructor)
initializing = true;
/*eslint new-cap:0 */
prototype = new self();
initializing = false;
// Add mixins
if (prop.Mixins) {
each(prop.Mixins, function(mixin) {
for (var name in mixin) {
if (name !== "init") {
prop[name] = mixin[name];
}
}
});
if (_super.Mixins) {
prop.Mixins = _super.Mixins.concat(prop.Mixins);
}
}
// Generate dummy methods
if (prop.Methods) {
each(prop.Methods.split(','), function(name) {
prop[name] = dummy;
});
}
// Generate property methods
if (prop.Properties) {
each(prop.Properties.split(','), function(name) {
var fieldName = '_' + name;
prop[name] = function(value) {
var self = this, undef;
// Set value
if (value !== undef) {
self[fieldName] = value;
return self;
}
// Get value
return self[fieldName];
};
});
}
// Static functions
if (prop.Statics) {
each(prop.Statics, function(func, name) {
Class[name] = func;
});
}
// Default settings
if (prop.Defaults && _super.Defaults) {
prop.Defaults = extend({}, _super.Defaults, prop.Defaults);
}
// Copy the properties over onto the new prototype
for (name in prop) {
member = prop[name];
if (typeof member == "function" && _super[name]) {
prototype[name] = createMethod(name, member);
} else {
prototype[name] = member;
}
}
// Populate our constructed prototype object
Class.prototype = prototype;
// Enforce the constructor to be what we expect
Class.constructor = Class;
// And make this class extendible
Class.extend = extendClass;
return Class;
};
return Class;
});
// Included from: js/tinymce/classes/util/EventDispatcher.js
/**
* EventDispatcher.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class lets you add/remove and fire events by name on the specified scope. This makes
* it easy to add event listener logic to any class.
*
* @class tinymce.util.EventDispatcher
* @example
* var eventDispatcher = new EventDispatcher();
*
* eventDispatcher.on('click', function() {console.log('data');});
* eventDispatcher.fire('click', {data: 123});
*/
define("tinymce/util/EventDispatcher", [
"tinymce/util/Tools"
], function(Tools) {
var nativeEvents = Tools.makeMap(
"focus blur focusin focusout click dblclick mousedown mouseup mousemove mouseover beforepaste paste cut copy selectionchange " +
"mouseout mouseenter mouseleave wheel keydown keypress keyup input contextmenu dragstart dragend dragover " +
"draggesture dragdrop drop drag submit " +
"compositionstart compositionend compositionupdate touchstart touchmove touchend",
' '
);
function Dispatcher(settings) {
var self = this, scope, bindings = {}, toggleEvent;
function returnFalse() {
return false;
}
function returnTrue() {
return true;
}
settings = settings || {};
scope = settings.scope || self;
toggleEvent = settings.toggleEvent || returnFalse;
/**
* Fires the specified event by name.
*
* @method fire
* @param {String} name Name of the event to fire.
* @param {Object?} args Event arguments.
* @return {Object} Event args instance passed in.
* @example
* instance.fire('event', {...});
*/
function fire(name, args) {
var handlers, i, l, callback;
name = name.toLowerCase();
args = args || {};
args.type = name;
// Setup target is there isn't one
if (!args.target) {
args.target = scope;
}
// Add event delegation methods if they are missing
if (!args.preventDefault) {
// Add preventDefault method
args.preventDefault = function() {
args.isDefaultPrevented = returnTrue;
};
// Add stopPropagation
args.stopPropagation = function() {
args.isPropagationStopped = returnTrue;
};
// Add stopImmediatePropagation
args.stopImmediatePropagation = function() {
args.isImmediatePropagationStopped = returnTrue;
};
// Add event delegation states
args.isDefaultPrevented = returnFalse;
args.isPropagationStopped = returnFalse;
args.isImmediatePropagationStopped = returnFalse;
}
if (settings.beforeFire) {
settings.beforeFire(args);
}
handlers = bindings[name];
if (handlers) {
for (i = 0, l = handlers.length; i < l; i++) {
callback = handlers[i];
// Unbind handlers marked with "once"
if (callback.once) {
off(name, callback.func);
}
// Stop immediate propagation if needed
if (args.isImmediatePropagationStopped()) {
args.stopPropagation();
return args;
}
// If callback returns false then prevent default and stop all propagation
if (callback.func.call(scope, args) === false) {
args.preventDefault();
return args;
}
}
}
return args;
}
/**
* Binds an event listener to a specific event by name.
*
* @method on
* @param {String} name Event name or space separated list of events to bind.
* @param {callback} callback Callback to be executed when the event occurs.
* @param {Boolean} first Optional flag if the event should be prepended. Use this with care.
* @return {Object} Current class instance.
* @example
* instance.on('event', function(e) {
* // Callback logic
* });
*/
function on(name, callback, prepend, extra) {
var handlers, names, i;
if (callback === false) {
callback = returnFalse;
}
if (callback) {
callback = {
func: callback
};
if (extra) {
Tools.extend(callback, extra);
}
names = name.toLowerCase().split(' ');
i = names.length;
while (i--) {
name = names[i];
handlers = bindings[name];
if (!handlers) {
handlers = bindings[name] = [];
toggleEvent(name, true);
}
if (prepend) {
handlers.unshift(callback);
} else {
handlers.push(callback);
}
}
}
return self;
}
/**
* Unbinds an event listener to a specific event by name.
*
* @method off
* @param {String?} name Name of the event to unbind.
* @param {callback?} callback Callback to unbind.
* @return {Object} Current class instance.
* @example
* // Unbind specific callback
* instance.off('event', handler);
*
* // Unbind all listeners by name
* instance.off('event');
*
* // Unbind all events
* instance.off();
*/
function off(name, callback) {
var i, handlers, bindingName, names, hi;
if (name) {
names = name.toLowerCase().split(' ');
i = names.length;
while (i--) {
name = names[i];
handlers = bindings[name];
// Unbind all handlers
if (!name) {
for (bindingName in bindings) {
toggleEvent(bindingName, false);
delete bindings[bindingName];
}
return self;
}
if (handlers) {
// Unbind all by name
if (!callback) {
handlers.length = 0;
} else {
// Unbind specific ones
hi = handlers.length;
while (hi--) {
if (handlers[hi].func === callback) {
handlers = handlers.slice(0, hi).concat(handlers.slice(hi + 1));
bindings[name] = handlers;
}
}
}
if (!handlers.length) {
toggleEvent(name, false);
delete bindings[name];
}
}
}
} else {
for (name in bindings) {
toggleEvent(name, false);
}
bindings = {};
}
return self;
}
/**
* Binds an event listener to a specific event by name
* and automatically unbind the event once the callback fires.
*
* @method once
* @param {String} name Event name or space separated list of events to bind.
* @param {callback} callback Callback to be executed when the event occurs.
* @param {Boolean} first Optional flag if the event should be prepended. Use this with care.
* @return {Object} Current class instance.
* @example
* instance.once('event', function(e) {
* // Callback logic
* });
*/
function once(name, callback, prepend) {
return on(name, callback, prepend, {once: true});
}
/**
* Returns true/false if the dispatcher has a event of the specified name.
*
* @method has
* @param {String} name Name of the event to check for.
* @return {Boolean} true/false if the event exists or not.
*/
function has(name) {
name = name.toLowerCase();
return !(!bindings[name] || bindings[name].length === 0);
}
// Expose
self.fire = fire;
self.on = on;
self.off = off;
self.once = once;
self.has = has;
}
/**
* Returns true/false if the specified event name is a native browser event or not.
*
* @method isNative
* @param {String} name Name to check if it's native.
* @return {Boolean} true/false if the event is native or not.
* @static
*/
Dispatcher.isNative = function(name) {
return !!nativeEvents[name.toLowerCase()];
};
return Dispatcher;
});
// Included from: js/tinymce/classes/data/Binding.js
/**
* Binding.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class gets dynamically extended to provide a binding between two models. This makes it possible to
* sync the state of two properties in two models by a layer of abstraction.
*
* @private
* @class tinymce.data.Binding
*/
define("tinymce/data/Binding", [], function() {
/**
* Constructs a new bidning.
*
* @constructor
* @method Binding
* @param {Object} settings Settings to the binding.
*/
function Binding(settings) {
this.create = settings.create;
}
/**
* Creates a binding for a property on a model.
*
* @method create
* @param {tinymce.data.ObservableObject} model Model to create binding to.
* @param {String} name Name of property to bind.
* @return {tinymce.data.Binding} Binding instance.
*/
Binding.create = function(model, name) {
return new Binding({
create: function(otherModel, otherName) {
var bindings;
function fromSelfToOther(e) {
otherModel.set(otherName, e.value);
}
function fromOtherToSelf(e) {
model.set(name, e.value);
}
otherModel.on('change:' + otherName, fromOtherToSelf);
model.on('change:' + name, fromSelfToOther);
// Keep track of the bindings
bindings = otherModel._bindings;
if (!bindings) {
bindings = otherModel._bindings = [];
otherModel.on('destroy', function() {
var i = bindings.length;
while (i--) {
bindings[i]();
}
});
}
bindings.push(function() {
model.off('change:' + name, fromSelfToOther);
});
return model.get(name);
}
});
};
return Binding;
});
// Included from: js/tinymce/classes/util/Observable.js
/**
* Observable.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This mixin will add event binding logic to classes.
*
* @mixin tinymce.util.Observable
*/
define("tinymce/util/Observable", [
"tinymce/util/EventDispatcher"
], function(EventDispatcher) {
function getEventDispatcher(obj) {
if (!obj._eventDispatcher) {
obj._eventDispatcher = new EventDispatcher({
scope: obj,
toggleEvent: function(name, state) {
if (EventDispatcher.isNative(name) && obj.toggleNativeEvent) {
obj.toggleNativeEvent(name, state);
}
}
});
}
return obj._eventDispatcher;
}
return {
/**
* Fires the specified event by name. Consult the
* <a href="/advanced/events">event reference</a> for more details on each event.
*
* @method fire
* @param {String} name Name of the event to fire.
* @param {Object?} args Event arguments.
* @param {Boolean?} bubble True/false if the event is to be bubbled.
* @return {Object} Event args instance passed in.
* @example
* instance.fire('event', {...});
*/
fire: function(name, args, bubble) {
var self = this;
// Prevent all events except the remove event after the instance has been removed
if (self.removed && name !== "remove") {
return args;
}
args = getEventDispatcher(self).fire(name, args, bubble);
// Bubble event up to parents
if (bubble !== false && self.parent) {
var parent = self.parent();
while (parent && !args.isPropagationStopped()) {
parent.fire(name, args, false);
parent = parent.parent();
}
}
return args;
},
/**
* Binds an event listener to a specific event by name. Consult the
* <a href="/advanced/events">event reference</a> for more details on each event.
*
* @method on
* @param {String} name Event name or space separated list of events to bind.
* @param {callback} callback Callback to be executed when the event occurs.
* @param {Boolean} first Optional flag if the event should be prepended. Use this with care.
* @return {Object} Current class instance.
* @example
* instance.on('event', function(e) {
* // Callback logic
* });
*/
on: function(name, callback, prepend) {
return getEventDispatcher(this).on(name, callback, prepend);
},
/**
* Unbinds an event listener to a specific event by name. Consult the
* <a href="/advanced/events">event reference</a> for more details on each event.
*
* @method off
* @param {String?} name Name of the event to unbind.
* @param {callback?} callback Callback to unbind.
* @return {Object} Current class instance.
* @example
* // Unbind specific callback
* instance.off('event', handler);
*
* // Unbind all listeners by name
* instance.off('event');
*
* // Unbind all events
* instance.off();
*/
off: function(name, callback) {
return getEventDispatcher(this).off(name, callback);
},
/**
* Bind the event callback and once it fires the callback is removed. Consult the
* <a href="/advanced/events">event reference</a> for more details on each event.
*
* @method once
* @param {String} name Name of the event to bind.
* @param {callback} callback Callback to bind only once.
* @return {Object} Current class instance.
*/
once: function(name, callback) {
return getEventDispatcher(this).once(name, callback);
},
/**
* Returns true/false if the object has a event of the specified name.
*
* @method hasEventListeners
* @param {String} name Name of the event to check for.
* @return {Boolean} true/false if the event exists or not.
*/
hasEventListeners: function(name) {
return getEventDispatcher(this).has(name);
}
};
});
// Included from: js/tinymce/classes/data/ObservableObject.js
/**
* ObservableObject.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class is a object that is observable when properties changes a change event gets emitted.
*
* @private
* @class tinymce.data.ObservableObject
*/
define("tinymce/data/ObservableObject", [
"tinymce/data/Binding",
"tinymce/util/Observable",
"tinymce/util/Class",
"tinymce/util/Tools"
], function(Binding, Observable, Class, Tools) {
function isNode(node) {
return node.nodeType > 0;
}
// Todo: Maybe this should be shallow compare since it might be huge object references
function isEqual(a, b) {
var k, checked;
// Strict equals
if (a === b) {
return true;
}
// Compare null
if (a === null || b === null) {
return a === b;
}
// Compare number, boolean, string, undefined
if (typeof a !== "object" || typeof b !== "object") {
return a === b;
}
// Compare arrays
if (Tools.isArray(b)) {
if (a.length !== b.length) {
return false;
}
k = a.length;
while (k--) {
if (!isEqual(a[k], b[k])) {
return false;
}
}
}
// Shallow compare nodes
if (isNode(a) || isNode(b)) {
return a === b;
}
// Compare objects
checked = {};
for (k in b) {
if (!isEqual(a[k], b[k])) {
return false;
}
checked[k] = true;
}
for (k in a) {
if (!checked[k] && !isEqual(a[k], b[k])) {
return false;
}
}
return true;
}
return Class.extend({
Mixins: [Observable],
/**
* Constructs a new observable object instance.
*
* @constructor
* @param {Object} data Initial data for the object.
*/
init: function(data) {
var name, value;
data = data || {};
for (name in data) {
value = data[name];
if (value instanceof Binding) {
data[name] = value.create(this, name);
}
}
this.data = data;
},
/**
* Sets a property on the value this will call
* observers if the value is a change from the current value.
*
* @method set
* @param {String/object} name Name of the property to set or a object of items to set.
* @param {Object} value Value to set for the property.
* @return {tinymce.data.ObservableObject} Observable object instance.
*/
set: function(name, value) {
var key, args, oldValue = this.data[name];
if (value instanceof Binding) {
value = value.create(this, name);
}
if (typeof name === "object") {
for (key in name) {
this.set(key, name[key]);
}
return this;
}
if (!isEqual(oldValue, value)) {
this.data[name] = value;
args = {
target: this,
name: name,
value: value,
oldValue: oldValue
};
this.fire('change:' + name, args);
this.fire('change', args);
}
return this;
},
/**
* Gets a property by name.
*
* @method get
* @param {String} name Name of the property to get.
* @return {Object} Object value of propery.
*/
get: function(name) {
return this.data[name];
},
/**
* Returns true/false if the specified property exists.
*
* @method has
* @param {String} name Name of the property to check for.
* @return {Boolean} true/false if the item exists.
*/
has: function(name) {
return name in this.data;
},
/**
* Returns a dynamic property binding for the specified property name. This makes
* it possible to sync the state of two properties in two ObservableObject instances.
*
* @method bind
* @param {String} name Name of the property to sync with the property it's inserted to.
* @return {tinymce.data.Binding} Data binding instance.
*/
bind: function(name) {
return Binding.create(this, name);
},
/**
* Destroys the observable object and fires the "destroy"
* event and clean up any internal resources.
*
* @method destroy
*/
destroy: function() {
this.fire('destroy');
}
});
});
// Included from: js/tinymce/classes/ui/Selector.js
/**
* Selector.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/*eslint no-nested-ternary:0 */
/**
* Selector engine, enables you to select controls by using CSS like expressions.
* We currently only support basic CSS expressions to reduce the size of the core
* and the ones we support should be enough for most cases.
*
* @example
* Supported expressions:
* element
* element#name
* element.class
* element[attr]
* element[attr*=value]
* element[attr~=value]
* element[attr!=value]
* element[attr^=value]
* element[attr$=value]
* element:<state>
* element:not(<expression>)
* element:first
* element:last
* element:odd
* element:even
* element element
* element > element
*
* @class tinymce.ui.Selector
*/
define("tinymce/ui/Selector", [
"tinymce/util/Class"
], function(Class) {
"use strict";
/**
* Produces an array with a unique set of objects. It will not compare the values
* but the references of the objects.
*
* @private
* @method unqiue
* @param {Array} array Array to make into an array with unique items.
* @return {Array} Array with unique items.
*/
function unique(array) {
var uniqueItems = [], i = array.length, item;
while (i--) {
item = array[i];
if (!item.__checked) {
uniqueItems.push(item);
item.__checked = 1;
}
}
i = uniqueItems.length;
while (i--) {
delete uniqueItems[i].__checked;
}
return uniqueItems;
}
var expression = /^([\w\\*]+)?(?:#([\w\-\\]+))?(?:\.([\w\\\.]+))?(?:\[\@?([\w\\]+)([\^\$\*!~]?=)([\w\\]+)\])?(?:\:(.+))?/i;
/*jshint maxlen:255 */
/*eslint max-len:0 */
var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
whiteSpace = /^\s*|\s*$/g,
Collection;
var Selector = Class.extend({
/**
* Constructs a new Selector instance.
*
* @constructor
* @method init
* @param {String} selector CSS like selector expression.
*/
init: function(selector) {
var match = this.match;
function compileNameFilter(name) {
if (name) {
name = name.toLowerCase();
return function(item) {
return name === '*' || item.type === name;
};
}
}
function compileIdFilter(id) {
if (id) {
return function(item) {
return item._name === id;
};
}
}
function compileClassesFilter(classes) {
if (classes) {
classes = classes.split('.');
return function(item) {
var i = classes.length;
while (i--) {
if (!item.classes.contains(classes[i])) {
return false;
}
}
return true;
};
}
}
function compileAttrFilter(name, cmp, check) {
if (name) {
return function(item) {
var value = item[name] ? item[name]() : '';
return !cmp ? !!check :
cmp === "=" ? value === check :
cmp === "*=" ? value.indexOf(check) >= 0 :
cmp === "~=" ? (" " + value + " ").indexOf(" " + check + " ") >= 0 :
cmp === "!=" ? value != check :
cmp === "^=" ? value.indexOf(check) === 0 :
cmp === "$=" ? value.substr(value.length - check.length) === check :
false;
};
}
}
function compilePsuedoFilter(name) {
var notSelectors;
if (name) {
name = /(?:not\((.+)\))|(.+)/i.exec(name);
if (!name[1]) {
name = name[2];
return function(item, index, length) {
return name === 'first' ? index === 0 :
name === 'last' ? index === length - 1 :
name === 'even' ? index % 2 === 0 :
name === 'odd' ? index % 2 === 1 :
item[name] ? item[name]() :
false;
};
}
// Compile not expression
notSelectors = parseChunks(name[1], []);
return function(item) {
return !match(item, notSelectors);
};
}
}
function compile(selector, filters, direct) {
var parts;
function add(filter) {
if (filter) {
filters.push(filter);
}
}
// Parse expression into parts
parts = expression.exec(selector.replace(whiteSpace, ''));
add(compileNameFilter(parts[1]));
add(compileIdFilter(parts[2]));
add(compileClassesFilter(parts[3]));
add(compileAttrFilter(parts[4], parts[5], parts[6]));
add(compilePsuedoFilter(parts[7]));
// Mark the filter with pseudo for performance
filters.pseudo = !!parts[7];
filters.direct = direct;
return filters;
}
// Parser logic based on Sizzle by John Resig
function parseChunks(selector, selectors) {
var parts = [], extra, matches, i;
do {
chunker.exec("");
matches = chunker.exec(selector);
if (matches) {
selector = matches[3];
parts.push(matches[1]);
if (matches[2]) {
extra = matches[3];
break;
}
}
} while (matches);
if (extra) {
parseChunks(extra, selectors);
}
selector = [];
for (i = 0; i < parts.length; i++) {
if (parts[i] != '>') {
selector.push(compile(parts[i], [], parts[i - 1] === '>'));
}
}
selectors.push(selector);
return selectors;
}
this._selectors = parseChunks(selector, []);
},
/**
* Returns true/false if the selector matches the specified control.
*
* @method match
* @param {tinymce.ui.Control} control Control to match against the selector.
* @param {Array} selectors Optional array of selectors, mostly used internally.
* @return {Boolean} true/false state if the control matches or not.
*/
match: function(control, selectors) {
var i, l, si, sl, selector, fi, fl, filters, index, length, siblings, count, item;
selectors = selectors || this._selectors;
for (i = 0, l = selectors.length; i < l; i++) {
selector = selectors[i];
sl = selector.length;
item = control;
count = 0;
for (si = sl - 1; si >= 0; si--) {
filters = selector[si];
while (item) {
// Find the index and length since a pseudo filter like :first needs it
if (filters.pseudo) {
siblings = item.parent().items();
index = length = siblings.length;
while (index--) {
if (siblings[index] === item) {
break;
}
}
}
for (fi = 0, fl = filters.length; fi < fl; fi++) {
if (!filters[fi](item, index, length)) {
fi = fl + 1;
break;
}
}
if (fi === fl) {
count++;
break;
} else {
// If it didn't match the right most expression then
// break since it's no point looking at the parents
if (si === sl - 1) {
break;
}
}
item = item.parent();
}
}
// If we found all selectors then return true otherwise continue looking
if (count === sl) {
return true;
}
}
return false;
},
/**
* Returns a tinymce.ui.Collection with matches of the specified selector inside the specified container.
*
* @method find
* @param {tinymce.ui.Control} container Container to look for items in.
* @return {tinymce.ui.Collection} Collection with matched elements.
*/
find: function(container) {
var matches = [], i, l, selectors = this._selectors;
function collect(items, selector, index) {
var i, l, fi, fl, item, filters = selector[index];
for (i = 0, l = items.length; i < l; i++) {
item = items[i];
// Run each filter against the item
for (fi = 0, fl = filters.length; fi < fl; fi++) {
if (!filters[fi](item, i, l)) {
fi = fl + 1;
break;
}
}
// All filters matched the item
if (fi === fl) {
// Matched item is on the last expression like: panel toolbar [button]
if (index == selector.length - 1) {
matches.push(item);
} else {
// Collect next expression type
if (item.items) {
collect(item.items(), selector, index + 1);
}
}
} else if (filters.direct) {
return;
}
// Collect child items
if (item.items) {
collect(item.items(), selector, index);
}
}
}
if (container.items) {
for (i = 0, l = selectors.length; i < l; i++) {
collect(container.items(), selectors[i], 0);
}
// Unique the matches if needed
if (l > 1) {
matches = unique(matches);
}
}
// Fix for circular reference
if (!Collection) {
// TODO: Fix me!
Collection = Selector.Collection;
}
return new Collection(matches);
}
});
return Selector;
});
// Included from: js/tinymce/classes/ui/Collection.js
/**
* Collection.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Control collection, this class contains control instances and it enables you to
* perform actions on all the contained items. This is very similar to how jQuery works.
*
* @example
* someCollection.show().disabled(true);
*
* @class tinymce.ui.Collection
*/
define("tinymce/ui/Collection", [
"tinymce/util/Tools",
"tinymce/ui/Selector",
"tinymce/util/Class"
], function(Tools, Selector, Class) {
"use strict";
var Collection, proto, push = Array.prototype.push, slice = Array.prototype.slice;
proto = {
/**
* Current number of contained control instances.
*
* @field length
* @type Number
*/
length: 0,
/**
* Constructor for the collection.
*
* @constructor
* @method init
* @param {Array} items Optional array with items to add.
*/
init: function(items) {
if (items) {
this.add(items);
}
},
/**
* Adds new items to the control collection.
*
* @method add
* @param {Array} items Array if items to add to collection.
* @return {tinymce.ui.Collection} Current collection instance.
*/
add: function(items) {
var self = this;
// Force single item into array
if (!Tools.isArray(items)) {
if (items instanceof Collection) {
self.add(items.toArray());
} else {
push.call(self, items);
}
} else {
push.apply(self, items);
}
return self;
},
/**
* Sets the contents of the collection. This will remove any existing items
* and replace them with the ones specified in the input array.
*
* @method set
* @param {Array} items Array with items to set into the Collection.
* @return {tinymce.ui.Collection} Collection instance.
*/
set: function(items) {
var self = this, len = self.length, i;
self.length = 0;
self.add(items);
// Remove old entries
for (i = self.length; i < len; i++) {
delete self[i];
}
return self;
},
/**
* Filters the collection item based on the specified selector expression or selector function.
*
* @method filter
* @param {String} selector Selector expression to filter items by.
* @return {tinymce.ui.Collection} Collection containing the filtered items.
*/
filter: function(selector) {
var self = this, i, l, matches = [], item, match;
// Compile string into selector expression
if (typeof selector === "string") {
selector = new Selector(selector);
match = function(item) {
return selector.match(item);
};
} else {
// Use selector as matching function
match = selector;
}
for (i = 0, l = self.length; i < l; i++) {
item = self[i];
if (match(item)) {
matches.push(item);
}
}
return new Collection(matches);
},
/**
* Slices the items within the collection.
*
* @method slice
* @param {Number} index Index to slice at.
* @param {Number} len Optional length to slice.
* @return {tinymce.ui.Collection} Current collection.
*/
slice: function() {
return new Collection(slice.apply(this, arguments));
},
/**
* Makes the current collection equal to the specified index.
*
* @method eq
* @param {Number} index Index of the item to set the collection to.
* @return {tinymce.ui.Collection} Current collection.
*/
eq: function(index) {
return index === -1 ? this.slice(index) : this.slice(index, +index + 1);
},
/**
* Executes the specified callback on each item in collection.
*
* @method each
* @param {function} callback Callback to execute for each item in collection.
* @return {tinymce.ui.Collection} Current collection instance.
*/
each: function(callback) {
Tools.each(this, callback);
return this;
},
/**
* Returns an JavaScript array object of the contents inside the collection.
*
* @method toArray
* @return {Array} Array with all items from collection.
*/
toArray: function() {
return Tools.toArray(this);
},
/**
* Finds the index of the specified control or return -1 if it isn't in the collection.
*
* @method indexOf
* @param {Control} ctrl Control instance to look for.
* @return {Number} Index of the specified control or -1.
*/
indexOf: function(ctrl) {
var self = this, i = self.length;
while (i--) {
if (self[i] === ctrl) {
break;
}
}
return i;
},
/**
* Returns a new collection of the contents in reverse order.
*
* @method reverse
* @return {tinymce.ui.Collection} Collection instance with reversed items.
*/
reverse: function() {
return new Collection(Tools.toArray(this).reverse());
},
/**
* Returns true/false if the class exists or not.
*
* @method hasClass
* @param {String} cls Class to check for.
* @return {Boolean} true/false state if the class exists or not.
*/
hasClass: function(cls) {
return this[0] ? this[0].classes.contains(cls) : false;
},
/**
* Sets/gets the specific property on the items in the collection. The same as executing control.<property>(<value>);
*
* @method prop
* @param {String} name Property name to get/set.
* @param {Object} value Optional object value to set.
* @return {tinymce.ui.Collection} Current collection instance or value of the first item on a get operation.
*/
prop: function(name, value) {
var self = this, undef, item;
if (value !== undef) {
self.each(function(item) {
if (item[name]) {
item[name](value);
}
});
return self;
}
item = self[0];
if (item && item[name]) {
return item[name]();
}
},
/**
* Executes the specific function name with optional arguments an all items in collection if it exists.
*
* @example collection.exec("myMethod", arg1, arg2, arg3);
* @method exec
* @param {String} name Name of the function to execute.
* @param {Object} ... Multiple arguments to pass to each function.
* @return {tinymce.ui.Collection} Current collection.
*/
exec: function(name) {
var self = this, args = Tools.toArray(arguments).slice(1);
self.each(function(item) {
if (item[name]) {
item[name].apply(item, args);
}
});
return self;
},
/**
* Remove all items from collection and DOM.
*
* @method remove
* @return {tinymce.ui.Collection} Current collection.
*/
remove: function() {
var i = this.length;
while (i--) {
this[i].remove();
}
return this;
},
/**
* Adds a class to all items in the collection.
*
* @method addClass
* @param {String} cls Class to add to each item.
* @return {tinymce.ui.Collection} Current collection instance.
*/
addClass: function(cls) {
return this.each(function(item) {
item.classes.add(cls);
});
},
/**
* Removes the specified class from all items in collection.
*
* @method removeClass
* @param {String} cls Class to remove from each item.
* @return {tinymce.ui.Collection} Current collection instance.
*/
removeClass: function(cls) {
return this.each(function(item) {
item.classes.remove(cls);
});
}
/**
* Fires the specified event by name and arguments on the control. This will execute all
* bound event handlers.
*
* @method fire
* @param {String} name Name of the event to fire.
* @param {Object} args Optional arguments to pass to the event.
* @return {tinymce.ui.Collection} Current collection instance.
*/
// fire: function(event, args) {}, -- Generated by code below
/**
* Binds a callback to the specified event. This event can both be
* native browser events like "click" or custom ones like PostRender.
*
* The callback function will have two parameters the first one being the control that received the event
* the second one will be the event object either the browsers native event object or a custom JS object.
*
* @method on
* @param {String} name Name of the event to bind. For example "click".
* @param {String/function} callback Callback function to execute ones the event occurs.
* @return {tinymce.ui.Collection} Current collection instance.
*/
// on: function(name, callback) {}, -- Generated by code below
/**
* Unbinds the specified event and optionally a specific callback. If you omit the name
* parameter all event handlers will be removed. If you omit the callback all event handles
* by the specified name will be removed.
*
* @method off
* @param {String} name Optional name for the event to unbind.
* @param {function} callback Optional callback function to unbind.
* @return {tinymce.ui.Collection} Current collection instance.
*/
// off: function(name, callback) {}, -- Generated by code below
/**
* Shows the items in the current collection.
*
* @method show
* @return {tinymce.ui.Collection} Current collection instance.
*/
// show: function() {}, -- Generated by code below
/**
* Hides the items in the current collection.
*
* @method hide
* @return {tinymce.ui.Collection} Current collection instance.
*/
// hide: function() {}, -- Generated by code below
/**
* Sets/gets the text contents of the items in the current collection.
*
* @method text
* @return {tinymce.ui.Collection} Current collection instance or text value of the first item on a get operation.
*/
// text: function(value) {}, -- Generated by code below
/**
* Sets/gets the name contents of the items in the current collection.
*
* @method name
* @return {tinymce.ui.Collection} Current collection instance or name value of the first item on a get operation.
*/
// name: function(value) {}, -- Generated by code below
/**
* Sets/gets the disabled state on the items in the current collection.
*
* @method disabled
* @return {tinymce.ui.Collection} Current collection instance or disabled state of the first item on a get operation.
*/
// disabled: function(state) {}, -- Generated by code below
/**
* Sets/gets the active state on the items in the current collection.
*
* @method active
* @return {tinymce.ui.Collection} Current collection instance or active state of the first item on a get operation.
*/
// active: function(state) {}, -- Generated by code below
/**
* Sets/gets the selected state on the items in the current collection.
*
* @method selected
* @return {tinymce.ui.Collection} Current collection instance or selected state of the first item on a get operation.
*/
// selected: function(state) {}, -- Generated by code below
/**
* Sets/gets the selected state on the items in the current collection.
*
* @method visible
* @return {tinymce.ui.Collection} Current collection instance or visible state of the first item on a get operation.
*/
// visible: function(state) {}, -- Generated by code below
};
// Extend tinymce.ui.Collection prototype with some generated control specific methods
Tools.each('fire on off show hide append prepend before after reflow'.split(' '), function(name) {
proto[name] = function() {
var args = Tools.toArray(arguments);
this.each(function(ctrl) {
if (name in ctrl) {
ctrl[name].apply(ctrl, args);
}
});
return this;
};
});
// Extend tinymce.ui.Collection prototype with some property methods
Tools.each('text name disabled active selected checked visible parent value data'.split(' '), function(name) {
proto[name] = function(value) {
return this.prop(name, value);
};
});
// Create class based on the new prototype
Collection = Class.extend(proto);
// Stick Collection into Selector to prevent circual references
Selector.Collection = Collection;
return Collection;
});
// Included from: js/tinymce/classes/ui/DomUtils.js
/**
* DomUtils.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Private UI DomUtils proxy.
*
* @private
* @class tinymce.ui.DomUtils
*/
define("tinymce/ui/DomUtils", [
"tinymce/Env",
"tinymce/util/Tools",
"tinymce/dom/DOMUtils"
], function(Env, Tools, DOMUtils) {
"use strict";
var count = 0;
var funcs = {
id: function() {
return 'mceu_' + (count++);
},
create: function(name, attrs, children) {
var elm = document.createElement(name);
DOMUtils.DOM.setAttribs(elm, attrs);
if (typeof children === 'string') {
elm.innerHTML = children;
} else {
Tools.each(children, function(child) {
if (child.nodeType) {
elm.appendChild(child);
}
});
}
return elm;
},
createFragment: function(html) {
return DOMUtils.DOM.createFragment(html);
},
getWindowSize: function() {
return DOMUtils.DOM.getViewPort();
},
getSize: function(elm) {
var width, height;
if (elm.getBoundingClientRect) {
var rect = elm.getBoundingClientRect();
width = Math.max(rect.width || (rect.right - rect.left), elm.offsetWidth);
height = Math.max(rect.height || (rect.bottom - rect.bottom), elm.offsetHeight);
} else {
width = elm.offsetWidth;
height = elm.offsetHeight;
}
return {width: width, height: height};
},
getPos: function(elm, root) {
return DOMUtils.DOM.getPos(elm, root || funcs.getContainer());
},
getContainer: function () {
return Env.container ? Env.container : document.body;
},
getViewPort: function(win) {
return DOMUtils.DOM.getViewPort(win);
},
get: function(id) {
return document.getElementById(id);
},
addClass: function(elm, cls) {
return DOMUtils.DOM.addClass(elm, cls);
},
removeClass: function(elm, cls) {
return DOMUtils.DOM.removeClass(elm, cls);
},
hasClass: function(elm, cls) {
return DOMUtils.DOM.hasClass(elm, cls);
},
toggleClass: function(elm, cls, state) {
return DOMUtils.DOM.toggleClass(elm, cls, state);
},
css: function(elm, name, value) {
return DOMUtils.DOM.setStyle(elm, name, value);
},
getRuntimeStyle: function(elm, name) {
return DOMUtils.DOM.getStyle(elm, name, true);
},
on: function(target, name, callback, scope) {
return DOMUtils.DOM.bind(target, name, callback, scope);
},
off: function(target, name, callback) {
return DOMUtils.DOM.unbind(target, name, callback);
},
fire: function(target, name, args) {
return DOMUtils.DOM.fire(target, name, args);
},
innerHtml: function(elm, html) {
// Workaround for <div> in <p> bug on IE 8 #6178
DOMUtils.DOM.setHTML(elm, html);
}
};
return funcs;
});
// Included from: js/tinymce/classes/ui/BoxUtils.js
/**
* BoxUtils.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Utility class for box parsing and measuring.
*
* @private
* @class tinymce.ui.BoxUtils
*/
define("tinymce/ui/BoxUtils", [
], function() {
"use strict";
return {
/**
* Parses the specified box value. A box value contains 1-4 properties in clockwise order.
*
* @method parseBox
* @param {String/Number} value Box value "0 1 2 3" or "0" etc.
* @return {Object} Object with top/right/bottom/left properties.
* @private
*/
parseBox: function(value) {
var len, radix = 10;
if (!value) {
return;
}
if (typeof value === "number") {
value = value || 0;
return {
top: value,
left: value,
bottom: value,
right: value
};
}
value = value.split(' ');
len = value.length;
if (len === 1) {
value[1] = value[2] = value[3] = value[0];
} else if (len === 2) {
value[2] = value[0];
value[3] = value[1];
} else if (len === 3) {
value[3] = value[1];
}
return {
top: parseInt(value[0], radix) || 0,
right: parseInt(value[1], radix) || 0,
bottom: parseInt(value[2], radix) || 0,
left: parseInt(value[3], radix) || 0
};
},
measureBox: function(elm, prefix) {
function getStyle(name) {
var defaultView = document.defaultView;
if (defaultView) {
// Remove camelcase
name = name.replace(/[A-Z]/g, function(a) {
return '-' + a;
});
return defaultView.getComputedStyle(elm, null).getPropertyValue(name);
}
return elm.currentStyle[name];
}
function getSide(name) {
var val = parseFloat(getStyle(name), 10);
return isNaN(val) ? 0 : val;
}
return {
top: getSide(prefix + "TopWidth"),
right: getSide(prefix + "RightWidth"),
bottom: getSide(prefix + "BottomWidth"),
left: getSide(prefix + "LeftWidth")
};
}
};
});
// Included from: js/tinymce/classes/ui/ClassList.js
/**
* ClassList.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Handles adding and removal of classes.
*
* @private
* @class tinymce.ui.ClassList
*/
define("tinymce/ui/ClassList", [
"tinymce/util/Tools"
], function(Tools) {
"use strict";
function noop() {
}
/**
* Constructs a new class list the specified onchange
* callback will be executed when the class list gets modifed.
*
* @constructor ClassList
* @param {function} onchange Onchange callback to be executed.
*/
function ClassList(onchange) {
this.cls = [];
this.cls._map = {};
this.onchange = onchange || noop;
this.prefix = '';
}
Tools.extend(ClassList.prototype, {
/**
* Adds a new class to the class list.
*
* @method add
* @param {String} cls Class to be added.
* @return {tinymce.ui.ClassList} Current class list instance.
*/
add: function(cls) {
if (cls && !this.contains(cls)) {
this.cls._map[cls] = true;
this.cls.push(cls);
this._change();
}
return this;
},
/**
* Removes the specified class from the class list.
*
* @method remove
* @param {String} cls Class to be removed.
* @return {tinymce.ui.ClassList} Current class list instance.
*/
remove: function(cls) {
if (this.contains(cls)) {
for (var i = 0; i < this.cls.length; i++) {
if (this.cls[i] === cls) {
break;
}
}
this.cls.splice(i, 1);
delete this.cls._map[cls];
this._change();
}
return this;
},
/**
* Toggles a class in the class list.
*
* @method toggle
* @param {String} cls Class to be added/removed.
* @param {Boolean} state Optional state if it should be added/removed.
* @return {tinymce.ui.ClassList} Current class list instance.
*/
toggle: function(cls, state) {
var curState = this.contains(cls);
if (curState !== state) {
if (curState) {
this.remove(cls);
} else {
this.add(cls);
}
this._change();
}
return this;
},
/**
* Returns true if the class list has the specified class.
*
* @method contains
* @param {String} cls Class to look for.
* @return {Boolean} true/false if the class exists or not.
*/
contains: function(cls) {
return !!this.cls._map[cls];
},
/**
* Returns a space separated list of classes.
*
* @method toString
* @return {String} Space separated list of classes.
*/
_change: function() {
delete this.clsValue;
this.onchange.call(this);
}
});
// IE 8 compatibility
ClassList.prototype.toString = function() {
var value;
if (this.clsValue) {
return this.clsValue;
}
value = '';
for (var i = 0; i < this.cls.length; i++) {
if (i > 0) {
value += ' ';
}
value += this.prefix + this.cls[i];
}
return value;
};
return ClassList;
});
// Included from: js/tinymce/classes/ui/ReflowQueue.js
/**
* ReflowQueue.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class will automatically reflow controls on the next animation frame within a few milliseconds on older browsers.
* If the user manually reflows then the automatic reflow will be cancelled. This class is used internally when various control states
* changes that triggers a reflow.
*
* @class tinymce.ui.ReflowQueue
* @static
*/
define("tinymce/ui/ReflowQueue", [
"tinymce/util/Delay"
], function(Delay) {
var dirtyCtrls = {}, animationFrameRequested;
return {
/**
* Adds a control to the next automatic reflow call. This is the control that had a state
* change for example if the control was hidden/shown.
*
* @method add
* @param {tinymce.ui.Control} ctrl Control to add to queue.
*/
add: function(ctrl) {
var parent = ctrl.parent();
if (parent) {
if (!parent._layout || parent._layout.isNative()) {
return;
}
if (!dirtyCtrls[parent._id]) {
dirtyCtrls[parent._id] = parent;
}
if (!animationFrameRequested) {
animationFrameRequested = true;
Delay.requestAnimationFrame(function() {
var id, ctrl;
animationFrameRequested = false;
for (id in dirtyCtrls) {
ctrl = dirtyCtrls[id];
if (ctrl.state.get('rendered')) {
ctrl.reflow();
}
}
dirtyCtrls = {};
}, document.body);
}
}
},
/**
* Removes the specified control from the automatic reflow. This will happen when for example the user
* manually triggers a reflow.
*
* @method remove
* @param {tinymce.ui.Control} ctrl Control to remove from queue.
*/
remove: function(ctrl) {
if (dirtyCtrls[ctrl._id]) {
delete dirtyCtrls[ctrl._id];
}
}
};
});
// Included from: js/tinymce/classes/ui/Control.js
/**
* Control.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/*eslint consistent-this:0 */
/**
* This is the base class for all controls and containers. All UI control instances inherit
* from this one as it has the base logic needed by all of them.
*
* @class tinymce.ui.Control
*/
define("tinymce/ui/Control", [
"tinymce/util/Class",
"tinymce/util/Tools",
"tinymce/util/EventDispatcher",
"tinymce/data/ObservableObject",
"tinymce/ui/Collection",
"tinymce/ui/DomUtils",
"tinymce/dom/DomQuery",
"tinymce/ui/BoxUtils",
"tinymce/ui/ClassList",
"tinymce/ui/ReflowQueue"
], function(Class, Tools, EventDispatcher, ObservableObject, Collection, DomUtils, $, BoxUtils, ClassList, ReflowQueue) {
"use strict";
var hasMouseWheelEventSupport = "onmousewheel" in document;
var hasWheelEventSupport = false;
var classPrefix = "mce-";
var Control, idCounter = 0;
var proto = {
Statics: {
classPrefix: classPrefix
},
isRtl: function() {
return Control.rtl;
},
/**
* Class/id prefix to use for all controls.
*
* @final
* @field {String} classPrefix
*/
classPrefix: classPrefix,
/**
* Constructs a new control instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {String} style Style CSS properties to add.
* @setting {String} border Border box values example: 1 1 1 1
* @setting {String} padding Padding box values example: 1 1 1 1
* @setting {String} margin Margin box values example: 1 1 1 1
* @setting {Number} minWidth Minimal width for the control.
* @setting {Number} minHeight Minimal height for the control.
* @setting {String} classes Space separated list of classes to add.
* @setting {String} role WAI-ARIA role to use for control.
* @setting {Boolean} hidden Is the control hidden by default.
* @setting {Boolean} disabled Is the control disabled by default.
* @setting {String} name Name of the control instance.
*/
init: function(settings) {
var self = this, classes, defaultClasses;
function applyClasses(classes) {
var i;
classes = classes.split(' ');
for (i = 0; i < classes.length; i++) {
self.classes.add(classes[i]);
}
}
self.settings = settings = Tools.extend({}, self.Defaults, settings);
// Initial states
self._id = settings.id || ('mceu_' + (idCounter++));
self._aria = {role: settings.role};
self._elmCache = {};
self.$ = $;
self.state = new ObservableObject({
visible: true,
active: false,
disabled: false,
value: ''
});
self.data = new ObservableObject(settings.data);
self.classes = new ClassList(function() {
if (self.state.get('rendered')) {
self.getEl().className = this.toString();
}
});
self.classes.prefix = self.classPrefix;
// Setup classes
classes = settings.classes;
if (classes) {
if (self.Defaults) {
defaultClasses = self.Defaults.classes;
if (defaultClasses && classes != defaultClasses) {
applyClasses(defaultClasses);
}
}
applyClasses(classes);
}
Tools.each('title text name visible disabled active value'.split(' '), function(name) {
if (name in settings) {
self[name](settings[name]);
}
});
self.on('click', function() {
if (self.disabled()) {
return false;
}
});
/**
* Name/value object with settings for the current control.
*
* @field {Object} settings
*/
self.settings = settings;
self.borderBox = BoxUtils.parseBox(settings.border);
self.paddingBox = BoxUtils.parseBox(settings.padding);
self.marginBox = BoxUtils.parseBox(settings.margin);
if (settings.hidden) {
self.hide();
}
},
// Will generate getter/setter methods for these properties
Properties: 'parent,name',
/**
* Returns the root element to render controls into.
*
* @method getContainerElm
* @return {Element} HTML DOM element to render into.
*/
getContainerElm: function() {
return DomUtils.getContainer();
},
/**
* Returns a control instance for the current DOM element.
*
* @method getParentCtrl
* @param {Element} elm HTML dom element to get parent control from.
* @return {tinymce.ui.Control} Control instance or undefined.
*/
getParentCtrl: function(elm) {
var ctrl, lookup = this.getRoot().controlIdLookup;
while (elm && lookup) {
ctrl = lookup[elm.id];
if (ctrl) {
break;
}
elm = elm.parentNode;
}
return ctrl;
},
/**
* Initializes the current controls layout rect.
* This will be executed by the layout managers to determine the
* default minWidth/minHeight etc.
*
* @method initLayoutRect
* @return {Object} Layout rect instance.
*/
initLayoutRect: function() {
var self = this, settings = self.settings, borderBox, layoutRect;
var elm = self.getEl(), width, height, minWidth, minHeight, autoResize;
var startMinWidth, startMinHeight, initialSize;
// Measure the current element
borderBox = self.borderBox = self.borderBox || BoxUtils.measureBox(elm, 'border');
self.paddingBox = self.paddingBox || BoxUtils.measureBox(elm, 'padding');
self.marginBox = self.marginBox || BoxUtils.measureBox(elm, 'margin');
initialSize = DomUtils.getSize(elm);
// Setup minWidth/minHeight and width/height
startMinWidth = settings.minWidth;
startMinHeight = settings.minHeight;
minWidth = startMinWidth || initialSize.width;
minHeight = startMinHeight || initialSize.height;
width = settings.width;
height = settings.height;
autoResize = settings.autoResize;
autoResize = typeof autoResize != "undefined" ? autoResize : !width && !height;
width = width || minWidth;
height = height || minHeight;
var deltaW = borderBox.left + borderBox.right;
var deltaH = borderBox.top + borderBox.bottom;
var maxW = settings.maxWidth || 0xFFFF;
var maxH = settings.maxHeight || 0xFFFF;
// Setup initial layout rect
self._layoutRect = layoutRect = {
x: settings.x || 0,
y: settings.y || 0,
w: width,
h: height,
deltaW: deltaW,
deltaH: deltaH,
contentW: width - deltaW,
contentH: height - deltaH,
innerW: width - deltaW,
innerH: height - deltaH,
startMinWidth: startMinWidth || 0,
startMinHeight: startMinHeight || 0,
minW: Math.min(minWidth, maxW),
minH: Math.min(minHeight, maxH),
maxW: maxW,
maxH: maxH,
autoResize: autoResize,
scrollW: 0
};
self._lastLayoutRect = {};
return layoutRect;
},
/**
* Getter/setter for the current layout rect.
*
* @method layoutRect
* @param {Object} [newRect] Optional new layout rect.
* @return {tinymce.ui.Control/Object} Current control or rect object.
*/
layoutRect: function(newRect) {
var self = this, curRect = self._layoutRect, lastLayoutRect, size, deltaWidth, deltaHeight, undef, repaintControls;
// Initialize default layout rect
if (!curRect) {
curRect = self.initLayoutRect();
}
// Set new rect values
if (newRect) {
// Calc deltas between inner and outer sizes
deltaWidth = curRect.deltaW;
deltaHeight = curRect.deltaH;
// Set x position
if (newRect.x !== undef) {
curRect.x = newRect.x;
}
// Set y position
if (newRect.y !== undef) {
curRect.y = newRect.y;
}
// Set minW
if (newRect.minW !== undef) {
curRect.minW = newRect.minW;
}
// Set minH
if (newRect.minH !== undef) {
curRect.minH = newRect.minH;
}
// Set new width and calculate inner width
size = newRect.w;
if (size !== undef) {
size = size < curRect.minW ? curRect.minW : size;
size = size > curRect.maxW ? curRect.maxW : size;
curRect.w = size;
curRect.innerW = size - deltaWidth;
}
// Set new height and calculate inner height
size = newRect.h;
if (size !== undef) {
size = size < curRect.minH ? curRect.minH : size;
size = size > curRect.maxH ? curRect.maxH : size;
curRect.h = size;
curRect.innerH = size - deltaHeight;
}
// Set new inner width and calculate width
size = newRect.innerW;
if (size !== undef) {
size = size < curRect.minW - deltaWidth ? curRect.minW - deltaWidth : size;
size = size > curRect.maxW - deltaWidth ? curRect.maxW - deltaWidth : size;
curRect.innerW = size;
curRect.w = size + deltaWidth;
}
// Set new height and calculate inner height
size = newRect.innerH;
if (size !== undef) {
size = size < curRect.minH - deltaHeight ? curRect.minH - deltaHeight : size;
size = size > curRect.maxH - deltaHeight ? curRect.maxH - deltaHeight : size;
curRect.innerH = size;
curRect.h = size + deltaHeight;
}
// Set new contentW
if (newRect.contentW !== undef) {
curRect.contentW = newRect.contentW;
}
// Set new contentH
if (newRect.contentH !== undef) {
curRect.contentH = newRect.contentH;
}
// Compare last layout rect with the current one to see if we need to repaint or not
lastLayoutRect = self._lastLayoutRect;
if (lastLayoutRect.x !== curRect.x || lastLayoutRect.y !== curRect.y ||
lastLayoutRect.w !== curRect.w || lastLayoutRect.h !== curRect.h) {
repaintControls = Control.repaintControls;
if (repaintControls) {
if (repaintControls.map && !repaintControls.map[self._id]) {
repaintControls.push(self);
repaintControls.map[self._id] = true;
}
}
lastLayoutRect.x = curRect.x;
lastLayoutRect.y = curRect.y;
lastLayoutRect.w = curRect.w;
lastLayoutRect.h = curRect.h;
}
return self;
}
return curRect;
},
/**
* Repaints the control after a layout operation.
*
* @method repaint
*/
repaint: function() {
var self = this, style, bodyStyle, bodyElm, rect, borderBox;
var borderW, borderH, lastRepaintRect, round, value;
// Use Math.round on all values on IE < 9
round = !document.createRange ? Math.round : function(value) {
return value;
};
style = self.getEl().style;
rect = self._layoutRect;
lastRepaintRect = self._lastRepaintRect || {};
borderBox = self.borderBox;
borderW = borderBox.left + borderBox.right;
borderH = borderBox.top + borderBox.bottom;
if (rect.x !== lastRepaintRect.x) {
style.left = round(rect.x) + 'px';
lastRepaintRect.x = rect.x;
}
if (rect.y !== lastRepaintRect.y) {
style.top = round(rect.y) + 'px';
lastRepaintRect.y = rect.y;
}
if (rect.w !== lastRepaintRect.w) {
value = round(rect.w - borderW);
style.width = (value >= 0 ? value : 0) + 'px';
lastRepaintRect.w = rect.w;
}
if (rect.h !== lastRepaintRect.h) {
value = round(rect.h - borderH);
style.height = (value >= 0 ? value : 0) + 'px';
lastRepaintRect.h = rect.h;
}
// Update body if needed
if (self._hasBody && rect.innerW !== lastRepaintRect.innerW) {
value = round(rect.innerW);
bodyElm = self.getEl('body');
if (bodyElm) {
bodyStyle = bodyElm.style;
bodyStyle.width = (value >= 0 ? value : 0) + 'px';
}
lastRepaintRect.innerW = rect.innerW;
}
if (self._hasBody && rect.innerH !== lastRepaintRect.innerH) {
value = round(rect.innerH);
bodyElm = bodyElm || self.getEl('body');
if (bodyElm) {
bodyStyle = bodyStyle || bodyElm.style;
bodyStyle.height = (value >= 0 ? value : 0) + 'px';
}
lastRepaintRect.innerH = rect.innerH;
}
self._lastRepaintRect = lastRepaintRect;
self.fire('repaint', {}, false);
},
/**
* Updates the controls layout rect by re-measuing it.
*/
updateLayoutRect: function() {
var self = this;
self.parent()._lastRect = null;
DomUtils.css(self.getEl(), {width: '', height: ''});
self._layoutRect = self._lastRepaintRect = self._lastLayoutRect = null;
self.initLayoutRect();
},
/**
* Binds a callback to the specified event. This event can both be
* native browser events like "click" or custom ones like PostRender.
*
* The callback function will be passed a DOM event like object that enables yout do stop propagation.
*
* @method on
* @param {String} name Name of the event to bind. For example "click".
* @param {String/function} callback Callback function to execute ones the event occurs.
* @return {tinymce.ui.Control} Current control object.
*/
on: function(name, callback) {
var self = this;
function resolveCallbackName(name) {
var callback, scope;
if (typeof name != 'string') {
return name;
}
return function(e) {
if (!callback) {
self.parentsAndSelf().each(function(ctrl) {
var callbacks = ctrl.settings.callbacks;
if (callbacks && (callback = callbacks[name])) {
scope = ctrl;
return false;
}
});
}
if (!callback) {
e.action = name;
this.fire('execute', e);
return;
}
return callback.call(scope, e);
};
}
getEventDispatcher(self).on(name, resolveCallbackName(callback));
return self;
},
/**
* Unbinds the specified event and optionally a specific callback. If you omit the name
* parameter all event handlers will be removed. If you omit the callback all event handles
* by the specified name will be removed.
*
* @method off
* @param {String} [name] Name for the event to unbind.
* @param {function} [callback] Callback function to unbind.
* @return {tinymce.ui.Control} Current control object.
*/
off: function(name, callback) {
getEventDispatcher(this).off(name, callback);
return this;
},
/**
* Fires the specified event by name and arguments on the control. This will execute all
* bound event handlers.
*
* @method fire
* @param {String} name Name of the event to fire.
* @param {Object} [args] Arguments to pass to the event.
* @param {Boolean} [bubble] Value to control bubbling. Defaults to true.
* @return {Object} Current arguments object.
*/
fire: function(name, args, bubble) {
var self = this;
args = args || {};
if (!args.control) {
args.control = self;
}
args = getEventDispatcher(self).fire(name, args);
// Bubble event up to parents
if (bubble !== false && self.parent) {
var parent = self.parent();
while (parent && !args.isPropagationStopped()) {
parent.fire(name, args, false);
parent = parent.parent();
}
}
return args;
},
/**
* Returns true/false if the specified event has any listeners.
*
* @method hasEventListeners
* @param {String} name Name of the event to check for.
* @return {Boolean} True/false state if the event has listeners.
*/
hasEventListeners: function(name) {
return getEventDispatcher(this).has(name);
},
/**
* Returns a control collection with all parent controls.
*
* @method parents
* @param {String} selector Optional selector expression to find parents.
* @return {tinymce.ui.Collection} Collection with all parent controls.
*/
parents: function(selector) {
var self = this, ctrl, parents = new Collection();
// Add each parent to collection
for (ctrl = self.parent(); ctrl; ctrl = ctrl.parent()) {
parents.add(ctrl);
}
// Filter away everything that doesn't match the selector
if (selector) {
parents = parents.filter(selector);
}
return parents;
},
/**
* Returns the current control and it's parents.
*
* @method parentsAndSelf
* @param {String} selector Optional selector expression to find parents.
* @return {tinymce.ui.Collection} Collection with all parent controls.
*/
parentsAndSelf: function(selector) {
return new Collection(this).add(this.parents(selector));
},
/**
* Returns the control next to the current control.
*
* @method next
* @return {tinymce.ui.Control} Next control instance.
*/
next: function() {
var parentControls = this.parent().items();
return parentControls[parentControls.indexOf(this) + 1];
},
/**
* Returns the control previous to the current control.
*
* @method prev
* @return {tinymce.ui.Control} Previous control instance.
*/
prev: function() {
var parentControls = this.parent().items();
return parentControls[parentControls.indexOf(this) - 1];
},
/**
* Sets the inner HTML of the control element.
*
* @method innerHtml
* @param {String} html Html string to set as inner html.
* @return {tinymce.ui.Control} Current control object.
*/
innerHtml: function(html) {
this.$el.html(html);
return this;
},
/**
* Returns the control DOM element or sub element.
*
* @method getEl
* @param {String} [suffix] Suffix to get element by.
* @return {Element} HTML DOM element for the current control or it's children.
*/
getEl: function(suffix) {
var id = suffix ? this._id + '-' + suffix : this._id;
if (!this._elmCache[id]) {
this._elmCache[id] = $('#' + id)[0];
}
return this._elmCache[id];
},
/**
* Sets the visible state to true.
*
* @method show
* @return {tinymce.ui.Control} Current control instance.
*/
show: function() {
return this.visible(true);
},
/**
* Sets the visible state to false.
*
* @method hide
* @return {tinymce.ui.Control} Current control instance.
*/
hide: function() {
return this.visible(false);
},
/**
* Focuses the current control.
*
* @method focus
* @return {tinymce.ui.Control} Current control instance.
*/
focus: function() {
try {
this.getEl().focus();
} catch (ex) {
// Ignore IE error
}
return this;
},
/**
* Blurs the current control.
*
* @method blur
* @return {tinymce.ui.Control} Current control instance.
*/
blur: function() {
this.getEl().blur();
return this;
},
/**
* Sets the specified aria property.
*
* @method aria
* @param {String} name Name of the aria property to set.
* @param {String} value Value of the aria property.
* @return {tinymce.ui.Control} Current control instance.
*/
aria: function(name, value) {
var self = this, elm = self.getEl(self.ariaTarget);
if (typeof value === "undefined") {
return self._aria[name];
}
self._aria[name] = value;
if (self.state.get('rendered')) {
elm.setAttribute(name == 'role' ? name : 'aria-' + name, value);
}
return self;
},
/**
* Encodes the specified string with HTML entities. It will also
* translate the string to different languages.
*
* @method encode
* @param {String/Object/Array} text Text to entity encode.
* @param {Boolean} [translate=true] False if the contents shouldn't be translated.
* @return {String} Encoded and possible traslated string.
*/
encode: function(text, translate) {
if (translate !== false) {
text = this.translate(text);
}
return (text || '').replace(/[&<>"]/g, function(match) {
return '&#' + match.charCodeAt(0) + ';';
});
},
/**
* Returns the translated string.
*
* @method translate
* @param {String} text Text to translate.
* @return {String} Translated string or the same as the input.
*/
translate: function(text) {
return Control.translate ? Control.translate(text) : text;
},
/**
* Adds items before the current control.
*
* @method before
* @param {Array/tinymce.ui.Collection} items Array of items to prepend before this control.
* @return {tinymce.ui.Control} Current control instance.
*/
before: function(items) {
var self = this, parent = self.parent();
if (parent) {
parent.insert(items, parent.items().indexOf(self), true);
}
return self;
},
/**
* Adds items after the current control.
*
* @method after
* @param {Array/tinymce.ui.Collection} items Array of items to append after this control.
* @return {tinymce.ui.Control} Current control instance.
*/
after: function(items) {
var self = this, parent = self.parent();
if (parent) {
parent.insert(items, parent.items().indexOf(self));
}
return self;
},
/**
* Removes the current control from DOM and from UI collections.
*
* @method remove
* @return {tinymce.ui.Control} Current control instance.
*/
remove: function() {
var self = this, elm = self.getEl(), parent = self.parent(), newItems, i;
if (self.items) {
var controls = self.items().toArray();
i = controls.length;
while (i--) {
controls[i].remove();
}
}
if (parent && parent.items) {
newItems = [];
parent.items().each(function(item) {
if (item !== self) {
newItems.push(item);
}
});
parent.items().set(newItems);
parent._lastRect = null;
}
if (self._eventsRoot && self._eventsRoot == self) {
$(elm).off();
}
var lookup = self.getRoot().controlIdLookup;
if (lookup) {
delete lookup[self._id];
}
if (elm && elm.parentNode) {
elm.parentNode.removeChild(elm);
}
self.state.set('rendered', false);
self.state.destroy();
self.fire('remove');
return self;
},
/**
* Renders the control before the specified element.
*
* @method renderBefore
* @param {Element} elm Element to render before.
* @return {tinymce.ui.Control} Current control instance.
*/
renderBefore: function(elm) {
$(elm).before(this.renderHtml());
this.postRender();
return this;
},
/**
* Renders the control to the specified element.
*
* @method renderBefore
* @param {Element} elm Element to render to.
* @return {tinymce.ui.Control} Current control instance.
*/
renderTo: function(elm) {
$(elm || this.getContainerElm()).append(this.renderHtml());
this.postRender();
return this;
},
preRender: function() {
},
render: function() {
},
renderHtml: function() {
return '<div id="' + this._id + '" class="' + this.classes + '"></div>';
},
/**
* Post render method. Called after the control has been rendered to the target.
*
* @method postRender
* @return {tinymce.ui.Control} Current control instance.
*/
postRender: function() {
var self = this, settings = self.settings, elm, box, parent, name, parentEventsRoot;
self.$el = $(self.getEl());
self.state.set('rendered', true);
// Bind on<event> settings
for (name in settings) {
if (name.indexOf("on") === 0) {
self.on(name.substr(2), settings[name]);
}
}
if (self._eventsRoot) {
for (parent = self.parent(); !parentEventsRoot && parent; parent = parent.parent()) {
parentEventsRoot = parent._eventsRoot;
}
if (parentEventsRoot) {
for (name in parentEventsRoot._nativeEvents) {
self._nativeEvents[name] = true;
}
}
}
bindPendingEvents(self);
if (settings.style) {
elm = self.getEl();
if (elm) {
elm.setAttribute('style', settings.style);
elm.style.cssText = settings.style;
}
}
if (self.settings.border) {
box = self.borderBox;
self.$el.css({
'border-top-width': box.top,
'border-right-width': box.right,
'border-bottom-width': box.bottom,
'border-left-width': box.left
});
}
// Add instance to lookup
var root = self.getRoot();
if (!root.controlIdLookup) {
root.controlIdLookup = {};
}
root.controlIdLookup[self._id] = self;
for (var key in self._aria) {
self.aria(key, self._aria[key]);
}
if (self.state.get('visible') === false) {
self.getEl().style.display = 'none';
}
self.bindStates();
self.state.on('change:visible', function(e) {
var state = e.value, parentCtrl;
if (self.state.get('rendered')) {
self.getEl().style.display = state === false ? 'none' : '';
// Need to force a reflow here on IE 8
self.getEl().getBoundingClientRect();
}
// Parent container needs to reflow
parentCtrl = self.parent();
if (parentCtrl) {
parentCtrl._lastRect = null;
}
self.fire(state ? 'show' : 'hide');
ReflowQueue.add(self);
});
self.fire('postrender', {}, false);
},
bindStates: function() {
},
/**
* Scrolls the current control into view.
*
* @method scrollIntoView
* @param {String} align Alignment in view top|center|bottom.
* @return {tinymce.ui.Control} Current control instance.
*/
scrollIntoView: function(align) {
function getOffset(elm, rootElm) {
var x, y, parent = elm;
x = y = 0;
while (parent && parent != rootElm && parent.nodeType) {
x += parent.offsetLeft || 0;
y += parent.offsetTop || 0;
parent = parent.offsetParent;
}
return {x: x, y: y};
}
var elm = this.getEl(), parentElm = elm.parentNode;
var x, y, width, height, parentWidth, parentHeight;
var pos = getOffset(elm, parentElm);
x = pos.x;
y = pos.y;
width = elm.offsetWidth;
height = elm.offsetHeight;
parentWidth = parentElm.clientWidth;
parentHeight = parentElm.clientHeight;
if (align == "end") {
x -= parentWidth - width;
y -= parentHeight - height;
} else if (align == "center") {
x -= (parentWidth / 2) - (width / 2);
y -= (parentHeight / 2) - (height / 2);
}
parentElm.scrollLeft = x;
parentElm.scrollTop = y;
return this;
},
getRoot: function() {
var ctrl = this, rootControl, parents = [];
while (ctrl) {
if (ctrl.rootControl) {
rootControl = ctrl.rootControl;
break;
}
parents.push(ctrl);
rootControl = ctrl;
ctrl = ctrl.parent();
}
if (!rootControl) {
rootControl = this;
}
var i = parents.length;
while (i--) {
parents[i].rootControl = rootControl;
}
return rootControl;
},
/**
* Reflows the current control and it's parents.
* This should be used after you for example append children to the current control so
* that the layout managers know that they need to reposition everything.
*
* @example
* container.append({type: 'button', text: 'My button'}).reflow();
*
* @method reflow
* @return {tinymce.ui.Control} Current control instance.
*/
reflow: function() {
ReflowQueue.remove(this);
var parent = this.parent();
if (parent._layout && !parent._layout.isNative()) {
parent.reflow();
}
return this;
}
/**
* Sets/gets the parent container for the control.
*
* @method parent
* @param {tinymce.ui.Container} parent Optional parent to set.
* @return {tinymce.ui.Control} Parent control or the current control on a set action.
*/
// parent: function(parent) {} -- Generated
/**
* Sets/gets the text for the control.
*
* @method text
* @param {String} value Value to set to control.
* @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get.
*/
// text: function(value) {} -- Generated
/**
* Sets/gets the disabled state on the control.
*
* @method disabled
* @param {Boolean} state Value to set to control.
* @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get.
*/
// disabled: function(state) {} -- Generated
/**
* Sets/gets the active for the control.
*
* @method active
* @param {Boolean} state Value to set to control.
* @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get.
*/
// active: function(state) {} -- Generated
/**
* Sets/gets the name for the control.
*
* @method name
* @param {String} value Value to set to control.
* @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get.
*/
// name: function(value) {} -- Generated
/**
* Sets/gets the title for the control.
*
* @method title
* @param {String} value Value to set to control.
* @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get.
*/
// title: function(value) {} -- Generated
/**
* Sets/gets the visible for the control.
*
* @method visible
* @param {Boolean} state Value to set to control.
* @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get.
*/
// visible: function(value) {} -- Generated
};
/**
* Setup state properties.
*/
Tools.each('text title visible disabled active value'.split(' '), function(name) {
proto[name] = function(value) {
if (arguments.length === 0) {
return this.state.get(name);
}
if (typeof value != "undefined") {
this.state.set(name, value);
}
return this;
};
});
Control = Class.extend(proto);
function getEventDispatcher(obj) {
if (!obj._eventDispatcher) {
obj._eventDispatcher = new EventDispatcher({
scope: obj,
toggleEvent: function(name, state) {
if (state && EventDispatcher.isNative(name)) {
if (!obj._nativeEvents) {
obj._nativeEvents = {};
}
obj._nativeEvents[name] = true;
if (obj.state.get('rendered')) {
bindPendingEvents(obj);
}
}
}
});
}
return obj._eventDispatcher;
}
function bindPendingEvents(eventCtrl) {
var i, l, parents, eventRootCtrl, nativeEvents, name;
function delegate(e) {
var control = eventCtrl.getParentCtrl(e.target);
if (control) {
control.fire(e.type, e);
}
}
function mouseLeaveHandler() {
var ctrl = eventRootCtrl._lastHoverCtrl;
if (ctrl) {
ctrl.fire("mouseleave", {target: ctrl.getEl()});
ctrl.parents().each(function(ctrl) {
ctrl.fire("mouseleave", {target: ctrl.getEl()});
});
eventRootCtrl._lastHoverCtrl = null;
}
}
function mouseEnterHandler(e) {
var ctrl = eventCtrl.getParentCtrl(e.target), lastCtrl = eventRootCtrl._lastHoverCtrl, idx = 0, i, parents, lastParents;
// Over on a new control
if (ctrl !== lastCtrl) {
eventRootCtrl._lastHoverCtrl = ctrl;
parents = ctrl.parents().toArray().reverse();
parents.push(ctrl);
if (lastCtrl) {
lastParents = lastCtrl.parents().toArray().reverse();
lastParents.push(lastCtrl);
for (idx = 0; idx < lastParents.length; idx++) {
if (parents[idx] !== lastParents[idx]) {
break;
}
}
for (i = lastParents.length - 1; i >= idx; i--) {
lastCtrl = lastParents[i];
lastCtrl.fire("mouseleave", {
target: lastCtrl.getEl()
});
}
}
for (i = idx; i < parents.length; i++) {
ctrl = parents[i];
ctrl.fire("mouseenter", {
target: ctrl.getEl()
});
}
}
}
function fixWheelEvent(e) {
e.preventDefault();
if (e.type == "mousewheel") {
e.deltaY = -1 / 40 * e.wheelDelta;
if (e.wheelDeltaX) {
e.deltaX = -1 / 40 * e.wheelDeltaX;
}
} else {
e.deltaX = 0;
e.deltaY = e.detail;
}
e = eventCtrl.fire("wheel", e);
}
nativeEvents = eventCtrl._nativeEvents;
if (nativeEvents) {
// Find event root element if it exists
parents = eventCtrl.parents().toArray();
parents.unshift(eventCtrl);
for (i = 0, l = parents.length; !eventRootCtrl && i < l; i++) {
eventRootCtrl = parents[i]._eventsRoot;
}
// Event root wasn't found the use the root control
if (!eventRootCtrl) {
eventRootCtrl = parents[parents.length - 1] || eventCtrl;
}
// Set the eventsRoot property on children that didn't have it
eventCtrl._eventsRoot = eventRootCtrl;
for (l = i, i = 0; i < l; i++) {
parents[i]._eventsRoot = eventRootCtrl;
}
var eventRootDelegates = eventRootCtrl._delegates;
if (!eventRootDelegates) {
eventRootDelegates = eventRootCtrl._delegates = {};
}
// Bind native event delegates
for (name in nativeEvents) {
if (!nativeEvents) {
return false;
}
if (name === "wheel" && !hasWheelEventSupport) {
if (hasMouseWheelEventSupport) {
$(eventCtrl.getEl()).on("mousewheel", fixWheelEvent);
} else {
$(eventCtrl.getEl()).on("DOMMouseScroll", fixWheelEvent);
}
continue;
}
// Special treatment for mousenter/mouseleave since these doesn't bubble
if (name === "mouseenter" || name === "mouseleave") {
// Fake mousenter/mouseleave
if (!eventRootCtrl._hasMouseEnter) {
$(eventRootCtrl.getEl()).on("mouseleave", mouseLeaveHandler).on("mouseover", mouseEnterHandler);
eventRootCtrl._hasMouseEnter = 1;
}
} else if (!eventRootDelegates[name]) {
$(eventRootCtrl.getEl()).on(name, delegate);
eventRootDelegates[name] = true;
}
// Remove the event once it's bound
nativeEvents[name] = false;
}
}
}
return Control;
});
// Included from: js/tinymce/classes/ui/Factory.js
/**
* Factory.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/*global tinymce:true */
/**
* This class is a factory for control instances. This enables you
* to create instances of controls without having to require the UI controls directly.
*
* It also allow you to override or add new control types.
*
* @class tinymce.ui.Factory
*/
define("tinymce/ui/Factory", [], function() {
"use strict";
var types = {}, namespaceInit;
return {
/**
* Adds a new control instance type to the factory.
*
* @method add
* @param {String} type Type name for example "button".
* @param {function} typeClass Class type function.
*/
add: function(type, typeClass) {
types[type.toLowerCase()] = typeClass;
},
/**
* Returns true/false if the specified type exists or not.
*
* @method has
* @param {String} type Type to look for.
* @return {Boolean} true/false if the control by name exists.
*/
has: function(type) {
return !!types[type.toLowerCase()];
},
/**
* Creates a new control instance based on the settings provided. The instance created will be
* based on the specified type property it can also create whole structures of components out of
* the specified JSON object.
*
* @example
* tinymce.ui.Factory.create({
* type: 'button',
* text: 'Hello world!'
* });
*
* @method create
* @param {Object/String} settings Name/Value object with items used to create the type.
* @return {tinymce.ui.Control} Control instance based on the specified type.
*/
create: function(type, settings) {
var ControlType, name, namespace;
// Build type lookup
if (!namespaceInit) {
namespace = tinymce.ui;
for (name in namespace) {
types[name.toLowerCase()] = namespace[name];
}
namespaceInit = true;
}
// If string is specified then use it as the type
if (typeof type == 'string') {
settings = settings || {};
settings.type = type;
} else {
settings = type;
type = settings.type;
}
// Find control type
type = type.toLowerCase();
ControlType = types[type];
// #if debug
if (!ControlType) {
throw new Error("Could not find control by type: " + type);
}
// #endif
ControlType = new ControlType(settings);
ControlType.type = type; // Set the type on the instance, this will be used by the Selector engine
return ControlType;
}
};
});
// Included from: js/tinymce/classes/ui/KeyboardNavigation.js
/**
* KeyboardNavigation.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class handles keyboard navigation of controls and elements.
*
* @class tinymce.ui.KeyboardNavigation
*/
define("tinymce/ui/KeyboardNavigation", [
], function() {
"use strict";
/**
* This class handles all keyboard navigation for WAI-ARIA support. Each root container
* gets an instance of this class.
*
* @constructor
*/
return function(settings) {
var root = settings.root, focusedElement, focusedControl;
function isElement(node) {
return node && node.nodeType === 1;
}
try {
focusedElement = document.activeElement;
} catch (ex) {
// IE sometimes fails to return a proper element
focusedElement = document.body;
}
focusedControl = root.getParentCtrl(focusedElement);
/**
* Returns the currently focused elements wai aria role of the currently
* focused element or specified element.
*
* @private
* @param {Element} elm Optional element to get role from.
* @return {String} Role of specified element.
*/
function getRole(elm) {
elm = elm || focusedElement;
if (isElement(elm)) {
return elm.getAttribute('role');
}
return null;
}
/**
* Returns the wai role of the parent element of the currently
* focused element or specified element.
*
* @private
* @param {Element} elm Optional element to get parent role from.
* @return {String} Role of the first parent that has a role.
*/
function getParentRole(elm) {
var role, parent = elm || focusedElement;
while ((parent = parent.parentNode)) {
if ((role = getRole(parent))) {
return role;
}
}
}
/**
* Returns a wai aria property by name for example aria-selected.
*
* @private
* @param {String} name Name of the aria property to get for example "disabled".
* @return {String} Aria property value.
*/
function getAriaProp(name) {
var elm = focusedElement;
if (isElement(elm)) {
return elm.getAttribute('aria-' + name);
}
}
/**
* Is the element a text input element or not.
*
* @private
* @param {Element} elm Element to check if it's an text input element or not.
* @return {Boolean} True/false if the element is a text element or not.
*/
function isTextInputElement(elm) {
var tagName = elm.tagName.toUpperCase();
// Notice: since type can be "email" etc we don't check the type
// So all input elements gets treated as text input elements
return tagName == "INPUT" || tagName == "TEXTAREA" || tagName == "SELECT";
}
/**
* Returns true/false if the specified element can be focused or not.
*
* @private
* @param {Element} elm DOM element to check if it can be focused or not.
* @return {Boolean} True/false if the element can have focus.
*/
function canFocus(elm) {
if (isTextInputElement(elm) && !elm.hidden) {
return true;
}
if (/^(button|menuitem|checkbox|tab|menuitemcheckbox|option|gridcell|slider)$/.test(getRole(elm))) {
return true;
}
return false;
}
/**
* Returns an array of focusable visible elements within the specified container element.
*
* @private
* @param {Element} elm DOM element to find focusable elements within.
* @return {Array} Array of focusable elements.
*/
function getFocusElements(elm) {
var elements = [];
function collect(elm) {
if (elm.nodeType != 1 || elm.style.display == 'none' || elm.disabled) {
return;
}
if (canFocus(elm)) {
elements.push(elm);
}
for (var i = 0; i < elm.childNodes.length; i++) {
collect(elm.childNodes[i]);
}
}
collect(elm || root.getEl());
return elements;
}
/**
* Returns the navigation root control for the specified control. The navigation root
* is the control that the keyboard navigation gets scoped to for example a menubar or toolbar group.
* It will look for parents of the specified target control or the currently focused control if this option is omitted.
*
* @private
* @param {tinymce.ui.Control} targetControl Optional target control to find root of.
* @return {tinymce.ui.Control} Navigation root control.
*/
function getNavigationRoot(targetControl) {
var navigationRoot, controls;
targetControl = targetControl || focusedControl;
controls = targetControl.parents().toArray();
controls.unshift(targetControl);
for (var i = 0; i < controls.length; i++) {
navigationRoot = controls[i];
if (navigationRoot.settings.ariaRoot) {
break;
}
}
return navigationRoot;
}
/**
* Focuses the first item in the specified targetControl element or the last aria index if the
* navigation root has the ariaRemember option enabled.
*
* @private
* @param {tinymce.ui.Control} targetControl Target control to focus the first item in.
*/
function focusFirst(targetControl) {
var navigationRoot = getNavigationRoot(targetControl);
var focusElements = getFocusElements(navigationRoot.getEl());
if (navigationRoot.settings.ariaRemember && "lastAriaIndex" in navigationRoot) {
moveFocusToIndex(navigationRoot.lastAriaIndex, focusElements);
} else {
moveFocusToIndex(0, focusElements);
}
}
/**
* Moves the focus to the specified index within the elements list.
* This will scope the index to the size of the element list if it changed.
*
* @private
* @param {Number} idx Specified index to move to.
* @param {Array} elements Array with dom elements to move focus within.
* @return {Number} Input index or a changed index if it was out of range.
*/
function moveFocusToIndex(idx, elements) {
if (idx < 0) {
idx = elements.length - 1;
} else if (idx >= elements.length) {
idx = 0;
}
if (elements[idx]) {
elements[idx].focus();
}
return idx;
}
/**
* Moves the focus forwards or backwards.
*
* @private
* @param {Number} dir Direction to move in positive means forward, negative means backwards.
* @param {Array} elements Optional array of elements to move within defaults to the current navigation roots elements.
*/
function moveFocus(dir, elements) {
var idx = -1, navigationRoot = getNavigationRoot();
elements = elements || getFocusElements(navigationRoot.getEl());
for (var i = 0; i < elements.length; i++) {
if (elements[i] === focusedElement) {
idx = i;
}
}
idx += dir;
navigationRoot.lastAriaIndex = moveFocusToIndex(idx, elements);
}
/**
* Moves the focus to the left this is called by the left key.
*
* @private
*/
function left() {
var parentRole = getParentRole();
if (parentRole == "tablist") {
moveFocus(-1, getFocusElements(focusedElement.parentNode));
} else if (focusedControl.parent().submenu) {
cancel();
} else {
moveFocus(-1);
}
}
/**
* Moves the focus to the right this is called by the right key.
*
* @private
*/
function right() {
var role = getRole(), parentRole = getParentRole();
if (parentRole == "tablist") {
moveFocus(1, getFocusElements(focusedElement.parentNode));
} else if (role == "menuitem" && parentRole == "menu" && getAriaProp('haspopup')) {
enter();
} else {
moveFocus(1);
}
}
/**
* Moves the focus to the up this is called by the up key.
*
* @private
*/
function up() {
moveFocus(-1);
}
/**
* Moves the focus to the up this is called by the down key.
*
* @private
*/
function down() {
var role = getRole(), parentRole = getParentRole();
if (role == "menuitem" && parentRole == "menubar") {
enter();
} else if (role == "button" && getAriaProp('haspopup')) {
enter({key: 'down'});
} else {
moveFocus(1);
}
}
/**
* Moves the focus to the next item or previous item depending on shift key.
*
* @private
* @param {DOMEvent} e DOM event object.
*/
function tab(e) {
var parentRole = getParentRole();
if (parentRole == "tablist") {
var elm = getFocusElements(focusedControl.getEl('body'))[0];
if (elm) {
elm.focus();
}
} else {
moveFocus(e.shiftKey ? -1 : 1);
}
}
/**
* Calls the cancel event on the currently focused control. This is normally done using the Esc key.
*
* @private
*/
function cancel() {
focusedControl.fire('cancel');
}
/**
* Calls the click event on the currently focused control. This is normally done using the Enter/Space keys.
*
* @private
* @param {Object} aria Optional aria data to pass along with the enter event.
*/
function enter(aria) {
aria = aria || {};
focusedControl.fire('click', {target: focusedElement, aria: aria});
}
root.on('keydown', function(e) {
function handleNonTabOrEscEvent(e, handler) {
// Ignore non tab keys for text elements
if (isTextInputElement(focusedElement)) {
return;
}
if (getRole(focusedElement) === 'slider') {
return;
}
if (handler(e) !== false) {
e.preventDefault();
}
}
if (e.isDefaultPrevented()) {
return;
}
switch (e.keyCode) {
case 37: // DOM_VK_LEFT
handleNonTabOrEscEvent(e, left);
break;
case 39: // DOM_VK_RIGHT
handleNonTabOrEscEvent(e, right);
break;
case 38: // DOM_VK_UP
handleNonTabOrEscEvent(e, up);
break;
case 40: // DOM_VK_DOWN
handleNonTabOrEscEvent(e, down);
break;
case 27: // DOM_VK_ESCAPE
cancel();
break;
case 14: // DOM_VK_ENTER
case 13: // DOM_VK_RETURN
case 32: // DOM_VK_SPACE
handleNonTabOrEscEvent(e, enter);
break;
case 9: // DOM_VK_TAB
if (tab(e) !== false) {
e.preventDefault();
}
break;
}
});
root.on('focusin', function(e) {
focusedElement = e.target;
focusedControl = e.control;
});
return {
focusFirst: focusFirst
};
};
});
// Included from: js/tinymce/classes/ui/Container.js
/**
* Container.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Container control. This is extended by all controls that can have
* children such as panels etc. You can also use this class directly as an
* generic container instance. The container doesn't have any specific role or style.
*
* @-x-less Container.less
* @class tinymce.ui.Container
* @extends tinymce.ui.Control
*/
define("tinymce/ui/Container", [
"tinymce/ui/Control",
"tinymce/ui/Collection",
"tinymce/ui/Selector",
"tinymce/ui/Factory",
"tinymce/ui/KeyboardNavigation",
"tinymce/util/Tools",
"tinymce/dom/DomQuery",
"tinymce/ui/ClassList",
"tinymce/ui/ReflowQueue"
], function(Control, Collection, Selector, Factory, KeyboardNavigation, Tools, $, ClassList, ReflowQueue) {
"use strict";
var selectorCache = {};
return Control.extend({
/**
* Constructs a new control instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {Array} items Items to add to container in JSON format or control instances.
* @setting {String} layout Layout manager by name to use.
* @setting {Object} defaults Default settings to apply to all items.
*/
init: function(settings) {
var self = this;
self._super(settings);
settings = self.settings;
if (settings.fixed) {
self.state.set('fixed', true);
}
self._items = new Collection();
if (self.isRtl()) {
self.classes.add('rtl');
}
self.bodyClasses = new ClassList(function() {
if (self.state.get('rendered')) {
self.getEl('body').className = this.toString();
}
});
self.bodyClasses.prefix = self.classPrefix;
self.classes.add('container');
self.bodyClasses.add('container-body');
if (settings.containerCls) {
self.classes.add(settings.containerCls);
}
self._layout = Factory.create((settings.layout || '') + 'layout');
if (self.settings.items) {
self.add(self.settings.items);
} else {
self.add(self.render());
}
// TODO: Fix this!
self._hasBody = true;
},
/**
* Returns a collection of child items that the container currently have.
*
* @method items
* @return {tinymce.ui.Collection} Control collection direct child controls.
*/
items: function() {
return this._items;
},
/**
* Find child controls by selector.
*
* @method find
* @param {String} selector Selector CSS pattern to find children by.
* @return {tinymce.ui.Collection} Control collection with child controls.
*/
find: function(selector) {
selector = selectorCache[selector] = selectorCache[selector] || new Selector(selector);
return selector.find(this);
},
/**
* Adds one or many items to the current container. This will create instances of
* the object representations if needed.
*
* @method add
* @param {Array/Object/tinymce.ui.Control} items Array or item that will be added to the container.
* @return {tinymce.ui.Collection} Current collection control.
*/
add: function(items) {
var self = this;
self.items().add(self.create(items)).parent(self);
return self;
},
/**
* Focuses the current container instance. This will look
* for the first control in the container and focus that.
*
* @method focus
* @param {Boolean} keyboard Optional true/false if the focus was a keyboard focus or not.
* @return {tinymce.ui.Collection} Current instance.
*/
focus: function(keyboard) {
var self = this, focusCtrl, keyboardNav, items;
if (keyboard) {
keyboardNav = self.keyboardNav || self.parents().eq(-1)[0].keyboardNav;
if (keyboardNav) {
keyboardNav.focusFirst(self);
return;
}
}
items = self.find('*');
// TODO: Figure out a better way to auto focus alert dialog buttons
if (self.statusbar) {
items.add(self.statusbar.items());
}
items.each(function(ctrl) {
if (ctrl.settings.autofocus) {
focusCtrl = null;
return false;
}
if (ctrl.canFocus) {
focusCtrl = focusCtrl || ctrl;
}
});
if (focusCtrl) {
focusCtrl.focus();
}
return self;
},
/**
* Replaces the specified child control with a new control.
*
* @method replace
* @param {tinymce.ui.Control} oldItem Old item to be replaced.
* @param {tinymce.ui.Control} newItem New item to be inserted.
*/
replace: function(oldItem, newItem) {
var ctrlElm, items = this.items(), i = items.length;
// Replace the item in collection
while (i--) {
if (items[i] === oldItem) {
items[i] = newItem;
break;
}
}
if (i >= 0) {
// Remove new item from DOM
ctrlElm = newItem.getEl();
if (ctrlElm) {
ctrlElm.parentNode.removeChild(ctrlElm);
}
// Remove old item from DOM
ctrlElm = oldItem.getEl();
if (ctrlElm) {
ctrlElm.parentNode.removeChild(ctrlElm);
}
}
// Adopt the item
newItem.parent(this);
},
/**
* Creates the specified items. If any of the items is plain JSON style objects
* it will convert these into real tinymce.ui.Control instances.
*
* @method create
* @param {Array} items Array of items to convert into control instances.
* @return {Array} Array with control instances.
*/
create: function(items) {
var self = this, settings, ctrlItems = [];
// Non array structure, then force it into an array
if (!Tools.isArray(items)) {
items = [items];
}
// Add default type to each child control
Tools.each(items, function(item) {
if (item) {
// Construct item if needed
if (!(item instanceof Control)) {
// Name only then convert it to an object
if (typeof item == "string") {
item = {type: item};
}
// Create control instance based on input settings and default settings
settings = Tools.extend({}, self.settings.defaults, item);
item.type = settings.type = settings.type || item.type || self.settings.defaultType ||
(settings.defaults ? settings.defaults.type : null);
item = Factory.create(settings);
}
ctrlItems.push(item);
}
});
return ctrlItems;
},
/**
* Renders new control instances.
*
* @private
*/
renderNew: function() {
var self = this;
// Render any new items
self.items().each(function(ctrl, index) {
var containerElm;
ctrl.parent(self);
if (!ctrl.state.get('rendered')) {
containerElm = self.getEl('body');
// Insert or append the item
if (containerElm.hasChildNodes() && index <= containerElm.childNodes.length - 1) {
$(containerElm.childNodes[index]).before(ctrl.renderHtml());
} else {
$(containerElm).append(ctrl.renderHtml());
}
ctrl.postRender();
ReflowQueue.add(ctrl);
}
});
self._layout.applyClasses(self.items().filter(':visible'));
self._lastRect = null;
return self;
},
/**
* Appends new instances to the current container.
*
* @method append
* @param {Array/tinymce.ui.Collection} items Array if controls to append.
* @return {tinymce.ui.Container} Current container instance.
*/
append: function(items) {
return this.add(items).renderNew();
},
/**
* Prepends new instances to the current container.
*
* @method prepend
* @param {Array/tinymce.ui.Collection} items Array if controls to prepend.
* @return {tinymce.ui.Container} Current container instance.
*/
prepend: function(items) {
var self = this;
self.items().set(self.create(items).concat(self.items().toArray()));
return self.renderNew();
},
/**
* Inserts an control at a specific index.
*
* @method insert
* @param {Array/tinymce.ui.Collection} items Array if controls to insert.
* @param {Number} index Index to insert controls at.
* @param {Boolean} [before=false] Inserts controls before the index.
*/
insert: function(items, index, before) {
var self = this, curItems, beforeItems, afterItems;
items = self.create(items);
curItems = self.items();
if (!before && index < curItems.length - 1) {
index += 1;
}
if (index >= 0 && index < curItems.length) {
beforeItems = curItems.slice(0, index).toArray();
afterItems = curItems.slice(index).toArray();
curItems.set(beforeItems.concat(items, afterItems));
}
return self.renderNew();
},
/**
* Populates the form fields from the specified JSON data object.
*
* Control items in the form that matches the data will have it's value set.
*
* @method fromJSON
* @param {Object} data JSON data object to set control values by.
* @return {tinymce.ui.Container} Current form instance.
*/
fromJSON: function(data) {
var self = this;
for (var name in data) {
self.find('#' + name).value(data[name]);
}
return self;
},
/**
* Serializes the form into a JSON object by getting all items
* that has a name and a value.
*
* @method toJSON
* @return {Object} JSON object with form data.
*/
toJSON: function() {
var self = this, data = {};
self.find('*').each(function(ctrl) {
var name = ctrl.name(), value = ctrl.value();
if (name && typeof value != "undefined") {
data[name] = value;
}
});
return data;
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, layout = self._layout, role = this.settings.role;
self.preRender();
layout.preRender(self);
return (
'<div id="' + self._id + '" class="' + self.classes + '"' + (role ? ' role="' + this.settings.role + '"' : '') + '>' +
'<div id="' + self._id + '-body" class="' + self.bodyClasses + '">' +
(self.settings.html || '') + layout.renderHtml(self) +
'</div>' +
'</div>'
);
},
/**
* Post render method. Called after the control has been rendered to the target.
*
* @method postRender
* @return {tinymce.ui.Container} Current combobox instance.
*/
postRender: function() {
var self = this, box;
self.items().exec('postRender');
self._super();
self._layout.postRender(self);
self.state.set('rendered', true);
if (self.settings.style) {
self.$el.css(self.settings.style);
}
if (self.settings.border) {
box = self.borderBox;
self.$el.css({
'border-top-width': box.top,
'border-right-width': box.right,
'border-bottom-width': box.bottom,
'border-left-width': box.left
});
}
if (!self.parent()) {
self.keyboardNav = new KeyboardNavigation({
root: self
});
}
return self;
},
/**
* Initializes the current controls layout rect.
* This will be executed by the layout managers to determine the
* default minWidth/minHeight etc.
*
* @method initLayoutRect
* @return {Object} Layout rect instance.
*/
initLayoutRect: function() {
var self = this, layoutRect = self._super();
// Recalc container size by asking layout manager
self._layout.recalc(self);
return layoutRect;
},
/**
* Recalculates the positions of the controls in the current container.
* This is invoked by the reflow method and shouldn't be called directly.
*
* @method recalc
*/
recalc: function() {
var self = this, rect = self._layoutRect, lastRect = self._lastRect;
if (!lastRect || lastRect.w != rect.w || lastRect.h != rect.h) {
self._layout.recalc(self);
rect = self.layoutRect();
self._lastRect = {x: rect.x, y: rect.y, w: rect.w, h: rect.h};
return true;
}
},
/**
* Reflows the current container and it's children and possible parents.
* This should be used after you for example append children to the current control so
* that the layout managers know that they need to reposition everything.
*
* @example
* container.append({type: 'button', text: 'My button'}).reflow();
*
* @method reflow
* @return {tinymce.ui.Container} Current container instance.
*/
reflow: function() {
var i;
ReflowQueue.remove(this);
if (this.visible()) {
Control.repaintControls = [];
Control.repaintControls.map = {};
this.recalc();
i = Control.repaintControls.length;
while (i--) {
Control.repaintControls[i].repaint();
}
// TODO: Fix me!
if (this.settings.layout !== "flow" && this.settings.layout !== "stack") {
this.repaint();
}
Control.repaintControls = [];
}
return this;
}
});
});
// Included from: js/tinymce/classes/ui/DragHelper.js
/**
* DragHelper.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Drag/drop helper class.
*
* @example
* var dragHelper = new tinymce.ui.DragHelper('mydiv', {
* start: function(evt) {
* },
*
* drag: function(evt) {
* },
*
* end: function(evt) {
* }
* });
*
* @class tinymce.ui.DragHelper
*/
define("tinymce/ui/DragHelper", [
"tinymce/dom/DomQuery"
], function($) {
"use strict";
function getDocumentSize(doc) {
var documentElement, body, scrollWidth, clientWidth;
var offsetWidth, scrollHeight, clientHeight, offsetHeight, max = Math.max;
documentElement = doc.documentElement;
body = doc.body;
scrollWidth = max(documentElement.scrollWidth, body.scrollWidth);
clientWidth = max(documentElement.clientWidth, body.clientWidth);
offsetWidth = max(documentElement.offsetWidth, body.offsetWidth);
scrollHeight = max(documentElement.scrollHeight, body.scrollHeight);
clientHeight = max(documentElement.clientHeight, body.clientHeight);
offsetHeight = max(documentElement.offsetHeight, body.offsetHeight);
return {
width: scrollWidth < offsetWidth ? clientWidth : scrollWidth,
height: scrollHeight < offsetHeight ? clientHeight : scrollHeight
};
}
function updateWithTouchData(e) {
var keys, i;
if (e.changedTouches) {
keys = "screenX screenY pageX pageY clientX clientY".split(' ');
for (i = 0; i < keys.length; i++) {
e[keys[i]] = e.changedTouches[0][keys[i]];
}
}
}
return function(id, settings) {
var $eventOverlay, doc = settings.document || document, downButton, start, stop, drag, startX, startY;
settings = settings || {};
function getHandleElm() {
return doc.getElementById(settings.handle || id);
}
start = function(e) {
var docSize = getDocumentSize(doc), handleElm, cursor;
updateWithTouchData(e);
e.preventDefault();
downButton = e.button;
handleElm = getHandleElm();
startX = e.screenX;
startY = e.screenY;
// Grab cursor from handle so we can place it on overlay
if (window.getComputedStyle) {
cursor = window.getComputedStyle(handleElm, null).getPropertyValue("cursor");
} else {
cursor = handleElm.runtimeStyle.cursor;
}
$eventOverlay = $('<div></div>').css({
position: "absolute",
top: 0, left: 0,
width: docSize.width,
height: docSize.height,
zIndex: 0x7FFFFFFF,
opacity: 0.0001,
cursor: cursor
}).appendTo(doc.body);
$(doc).on('mousemove touchmove', drag).on('mouseup touchend', stop);
settings.start(e);
};
drag = function(e) {
updateWithTouchData(e);
if (e.button !== downButton) {
return stop(e);
}
e.deltaX = e.screenX - startX;
e.deltaY = e.screenY - startY;
e.preventDefault();
settings.drag(e);
};
stop = function(e) {
updateWithTouchData(e);
$(doc).off('mousemove touchmove', drag).off('mouseup touchend', stop);
$eventOverlay.remove();
if (settings.stop) {
settings.stop(e);
}
};
/**
* Destroys the drag/drop helper instance.
*
* @method destroy
*/
this.destroy = function() {
$(getHandleElm()).off();
};
$(getHandleElm()).on('mousedown touchstart', start);
};
});
// Included from: js/tinymce/classes/ui/Scrollable.js
/**
* Scrollable.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This mixin makes controls scrollable using custom scrollbars.
*
* @-x-less Scrollable.less
* @mixin tinymce.ui.Scrollable
*/
define("tinymce/ui/Scrollable", [
"tinymce/dom/DomQuery",
"tinymce/ui/DragHelper"
], function($, DragHelper) {
"use strict";
return {
init: function() {
var self = this;
self.on('repaint', self.renderScroll);
},
renderScroll: function() {
var self = this, margin = 2;
function repaintScroll() {
var hasScrollH, hasScrollV, bodyElm;
function repaintAxis(axisName, posName, sizeName, contentSizeName, hasScroll, ax) {
var containerElm, scrollBarElm, scrollThumbElm;
var containerSize, scrollSize, ratio, rect;
var posNameLower, sizeNameLower;
scrollBarElm = self.getEl('scroll' + axisName);
if (scrollBarElm) {
posNameLower = posName.toLowerCase();
sizeNameLower = sizeName.toLowerCase();
$(self.getEl('absend')).css(posNameLower, self.layoutRect()[contentSizeName] - 1);
if (!hasScroll) {
$(scrollBarElm).css('display', 'none');
return;
}
$(scrollBarElm).css('display', 'block');
containerElm = self.getEl('body');
scrollThumbElm = self.getEl('scroll' + axisName + "t");
containerSize = containerElm["client" + sizeName] - (margin * 2);
containerSize -= hasScrollH && hasScrollV ? scrollBarElm["client" + ax] : 0;
scrollSize = containerElm["scroll" + sizeName];
ratio = containerSize / scrollSize;
rect = {};
rect[posNameLower] = containerElm["offset" + posName] + margin;
rect[sizeNameLower] = containerSize;
$(scrollBarElm).css(rect);
rect = {};
rect[posNameLower] = containerElm["scroll" + posName] * ratio;
rect[sizeNameLower] = containerSize * ratio;
$(scrollThumbElm).css(rect);
}
}
bodyElm = self.getEl('body');
hasScrollH = bodyElm.scrollWidth > bodyElm.clientWidth;
hasScrollV = bodyElm.scrollHeight > bodyElm.clientHeight;
repaintAxis("h", "Left", "Width", "contentW", hasScrollH, "Height");
repaintAxis("v", "Top", "Height", "contentH", hasScrollV, "Width");
}
function addScroll() {
function addScrollAxis(axisName, posName, sizeName, deltaPosName, ax) {
var scrollStart, axisId = self._id + '-scroll' + axisName, prefix = self.classPrefix;
$(self.getEl()).append(
'<div id="' + axisId + '" class="' + prefix + 'scrollbar ' + prefix + 'scrollbar-' + axisName + '">' +
'<div id="' + axisId + 't" class="' + prefix + 'scrollbar-thumb"></div>' +
'</div>'
);
self.draghelper = new DragHelper(axisId + 't', {
start: function() {
scrollStart = self.getEl('body')["scroll" + posName];
$('#' + axisId).addClass(prefix + 'active');
},
drag: function(e) {
var ratio, hasScrollH, hasScrollV, containerSize, layoutRect = self.layoutRect();
hasScrollH = layoutRect.contentW > layoutRect.innerW;
hasScrollV = layoutRect.contentH > layoutRect.innerH;
containerSize = self.getEl('body')["client" + sizeName] - (margin * 2);
containerSize -= hasScrollH && hasScrollV ? self.getEl('scroll' + axisName)["client" + ax] : 0;
ratio = containerSize / self.getEl('body')["scroll" + sizeName];
self.getEl('body')["scroll" + posName] = scrollStart + (e["delta" + deltaPosName] / ratio);
},
stop: function() {
$('#' + axisId).removeClass(prefix + 'active');
}
});
}
self.classes.add('scroll');
addScrollAxis("v", "Top", "Height", "Y", "Width");
addScrollAxis("h", "Left", "Width", "X", "Height");
}
if (self.settings.autoScroll) {
if (!self._hasScroll) {
self._hasScroll = true;
addScroll();
self.on('wheel', function(e) {
var bodyEl = self.getEl('body');
bodyEl.scrollLeft += (e.deltaX || 0) * 10;
bodyEl.scrollTop += e.deltaY * 10;
repaintScroll();
});
$(self.getEl('body')).on("scroll", repaintScroll);
}
repaintScroll();
}
}
};
});
// Included from: js/tinymce/classes/ui/Panel.js
/**
* Panel.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new panel.
*
* @-x-less Panel.less
* @class tinymce.ui.Panel
* @extends tinymce.ui.Container
* @mixes tinymce.ui.Scrollable
*/
define("tinymce/ui/Panel", [
"tinymce/ui/Container",
"tinymce/ui/Scrollable"
], function(Container, Scrollable) {
"use strict";
return Container.extend({
Defaults: {
layout: 'fit',
containerCls: 'panel'
},
Mixins: [Scrollable],
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, layout = self._layout, innerHtml = self.settings.html;
self.preRender();
layout.preRender(self);
if (typeof innerHtml == "undefined") {
innerHtml = (
'<div id="' + self._id + '-body" class="' + self.bodyClasses + '">' +
layout.renderHtml(self) +
'</div>'
);
} else {
if (typeof innerHtml == 'function') {
innerHtml = innerHtml.call(self);
}
self._hasBody = false;
}
return (
'<div id="' + self._id + '" class="' + self.classes + '" hidefocus="1" tabindex="-1" role="group">' +
(self._preBodyHtml || '') +
innerHtml +
'</div>'
);
}
});
});
// Included from: js/tinymce/classes/ui/Movable.js
/**
* Movable.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Movable mixin. Makes controls movable absolute and relative to other elements.
*
* @mixin tinymce.ui.Movable
*/
define("tinymce/ui/Movable", [
"tinymce/ui/DomUtils"
], function(DomUtils) {
"use strict";
function calculateRelativePosition(ctrl, targetElm, rel) {
var ctrlElm, pos, x, y, selfW, selfH, targetW, targetH, viewport, size;
viewport = DomUtils.getViewPort();
// Get pos of target
pos = DomUtils.getPos(targetElm);
x = pos.x;
y = pos.y;
if (ctrl.state.get('fixed') && DomUtils.getRuntimeStyle(document.body, 'position') == 'static') {
x -= viewport.x;
y -= viewport.y;
}
// Get size of self
ctrlElm = ctrl.getEl();
size = DomUtils.getSize(ctrlElm);
selfW = size.width;
selfH = size.height;
// Get size of target
size = DomUtils.getSize(targetElm);
targetW = size.width;
targetH = size.height;
// Parse align string
rel = (rel || '').split('');
// Target corners
if (rel[0] === 'b') {
y += targetH;
}
if (rel[1] === 'r') {
x += targetW;
}
if (rel[0] === 'c') {
y += Math.round(targetH / 2);
}
if (rel[1] === 'c') {
x += Math.round(targetW / 2);
}
// Self corners
if (rel[3] === 'b') {
y -= selfH;
}
if (rel[4] === 'r') {
x -= selfW;
}
if (rel[3] === 'c') {
y -= Math.round(selfH / 2);
}
if (rel[4] === 'c') {
x -= Math.round(selfW / 2);
}
return {
x: x,
y: y,
w: selfW,
h: selfH
};
}
return {
/**
* Tests various positions to get the most suitable one.
*
* @method testMoveRel
* @param {DOMElement} elm Element to position against.
* @param {Array} rels Array with relative positions.
* @return {String} Best suitable relative position.
*/
testMoveRel: function(elm, rels) {
var viewPortRect = DomUtils.getViewPort();
for (var i = 0; i < rels.length; i++) {
var pos = calculateRelativePosition(this, elm, rels[i]);
if (this.state.get('fixed')) {
if (pos.x > 0 && pos.x + pos.w < viewPortRect.w && pos.y > 0 && pos.y + pos.h < viewPortRect.h) {
return rels[i];
}
} else {
if (pos.x > viewPortRect.x && pos.x + pos.w < viewPortRect.w + viewPortRect.x &&
pos.y > viewPortRect.y && pos.y + pos.h < viewPortRect.h + viewPortRect.y) {
return rels[i];
}
}
}
return rels[0];
},
/**
* Move relative to the specified element.
*
* @method moveRel
* @param {Element} elm Element to move relative to.
* @param {String} rel Relative mode. For example: br-tl.
* @return {tinymce.ui.Control} Current control instance.
*/
moveRel: function(elm, rel) {
if (typeof rel != 'string') {
rel = this.testMoveRel(elm, rel);
}
var pos = calculateRelativePosition(this, elm, rel);
return this.moveTo(pos.x, pos.y);
},
/**
* Move by a relative x, y values.
*
* @method moveBy
* @param {Number} dx Relative x position.
* @param {Number} dy Relative y position.
* @return {tinymce.ui.Control} Current control instance.
*/
moveBy: function(dx, dy) {
var self = this, rect = self.layoutRect();
self.moveTo(rect.x + dx, rect.y + dy);
return self;
},
/**
* Move to absolute position.
*
* @method moveTo
* @param {Number} x Absolute x position.
* @param {Number} y Absolute y position.
* @return {tinymce.ui.Control} Current control instance.
*/
moveTo: function(x, y) {
var self = this;
// TODO: Move this to some global class
function constrain(value, max, size) {
if (value < 0) {
return 0;
}
if (value + size > max) {
value = max - size;
return value < 0 ? 0 : value;
}
return value;
}
if (self.settings.constrainToViewport) {
var viewPortRect = DomUtils.getViewPort(window);
var layoutRect = self.layoutRect();
x = constrain(x, viewPortRect.w + viewPortRect.x, layoutRect.w);
y = constrain(y, viewPortRect.h + viewPortRect.y, layoutRect.h);
}
if (self.state.get('rendered')) {
self.layoutRect({x: x, y: y}).repaint();
} else {
self.settings.x = x;
self.settings.y = y;
}
self.fire('move', {x: x, y: y});
return self;
}
};
});
// Included from: js/tinymce/classes/ui/Resizable.js
/**
* Resizable.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Resizable mixin. Enables controls to be resized.
*
* @mixin tinymce.ui.Resizable
*/
define("tinymce/ui/Resizable", [
"tinymce/ui/DomUtils"
], function(DomUtils) {
"use strict";
return {
/**
* Resizes the control to contents.
*
* @method resizeToContent
*/
resizeToContent: function() {
this._layoutRect.autoResize = true;
this._lastRect = null;
this.reflow();
},
/**
* Resizes the control to a specific width/height.
*
* @method resizeTo
* @param {Number} w Control width.
* @param {Number} h Control height.
* @return {tinymce.ui.Control} Current control instance.
*/
resizeTo: function(w, h) {
// TODO: Fix hack
if (w <= 1 || h <= 1) {
var rect = DomUtils.getWindowSize();
w = w <= 1 ? w * rect.w : w;
h = h <= 1 ? h * rect.h : h;
}
this._layoutRect.autoResize = false;
return this.layoutRect({minW: w, minH: h, w: w, h: h}).reflow();
},
/**
* Resizes the control to a specific relative width/height.
*
* @method resizeBy
* @param {Number} dw Relative control width.
* @param {Number} dh Relative control height.
* @return {tinymce.ui.Control} Current control instance.
*/
resizeBy: function(dw, dh) {
var self = this, rect = self.layoutRect();
return self.resizeTo(rect.w + dw, rect.h + dh);
}
};
});
// Included from: js/tinymce/classes/ui/FloatPanel.js
/**
* FloatPanel.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class creates a floating panel.
*
* @-x-less FloatPanel.less
* @class tinymce.ui.FloatPanel
* @extends tinymce.ui.Panel
* @mixes tinymce.ui.Movable
* @mixes tinymce.ui.Resizable
*/
define("tinymce/ui/FloatPanel", [
"tinymce/ui/Panel",
"tinymce/ui/Movable",
"tinymce/ui/Resizable",
"tinymce/ui/DomUtils",
"tinymce/dom/DomQuery",
"tinymce/util/Delay"
], function(Panel, Movable, Resizable, DomUtils, $, Delay) {
"use strict";
var documentClickHandler, documentScrollHandler, windowResizeHandler, visiblePanels = [];
var zOrder = [], hasModal;
function isChildOf(ctrl, parent) {
while (ctrl) {
if (ctrl == parent) {
return true;
}
ctrl = ctrl.parent();
}
}
function skipOrHidePanels(e) {
// Hide any float panel when a click/focus out is out side that float panel and the
// float panels direct parent for example a click on a menu button
var i = visiblePanels.length;
while (i--) {
var panel = visiblePanels[i], clickCtrl = panel.getParentCtrl(e.target);
if (panel.settings.autohide) {
if (clickCtrl) {
if (isChildOf(clickCtrl, panel) || panel.parent() === clickCtrl) {
continue;
}
}
e = panel.fire('autohide', {target: e.target});
if (!e.isDefaultPrevented()) {
panel.hide();
}
}
}
}
function bindDocumentClickHandler() {
if (!documentClickHandler) {
documentClickHandler = function(e) {
// Gecko fires click event and in the wrong order on Mac so lets normalize
if (e.button == 2) {
return;
}
skipOrHidePanels(e);
};
$(document).on('click touchstart', documentClickHandler);
}
}
function bindDocumentScrollHandler() {
if (!documentScrollHandler) {
documentScrollHandler = function() {
var i;
i = visiblePanels.length;
while (i--) {
repositionPanel(visiblePanels[i]);
}
};
$(window).on('scroll', documentScrollHandler);
}
}
function bindWindowResizeHandler() {
if (!windowResizeHandler) {
var docElm = document.documentElement, clientWidth = docElm.clientWidth, clientHeight = docElm.clientHeight;
windowResizeHandler = function() {
// Workaround for #7065 IE 7 fires resize events event though the window wasn't resized
if (!document.all || clientWidth != docElm.clientWidth || clientHeight != docElm.clientHeight) {
clientWidth = docElm.clientWidth;
clientHeight = docElm.clientHeight;
FloatPanel.hideAll();
}
};
$(window).on('resize', windowResizeHandler);
}
}
/**
* Repositions the panel to the top of page if the panel is outside of the visual viewport. It will
* also reposition all child panels of the current panel.
*/
function repositionPanel(panel) {
var scrollY = DomUtils.getViewPort().y;
function toggleFixedChildPanels(fixed, deltaY) {
var parent;
for (var i = 0; i < visiblePanels.length; i++) {
if (visiblePanels[i] != panel) {
parent = visiblePanels[i].parent();
while (parent && (parent = parent.parent())) {
if (parent == panel) {
visiblePanels[i].fixed(fixed).moveBy(0, deltaY).repaint();
}
}
}
}
}
if (panel.settings.autofix) {
if (!panel.state.get('fixed')) {
panel._autoFixY = panel.layoutRect().y;
if (panel._autoFixY < scrollY) {
panel.fixed(true).layoutRect({y: 0}).repaint();
toggleFixedChildPanels(true, scrollY - panel._autoFixY);
}
} else {
if (panel._autoFixY > scrollY) {
panel.fixed(false).layoutRect({y: panel._autoFixY}).repaint();
toggleFixedChildPanels(false, panel._autoFixY - scrollY);
}
}
}
}
function addRemove(add, ctrl) {
var i, zIndex = FloatPanel.zIndex || 0xFFFF, topModal;
if (add) {
zOrder.push(ctrl);
} else {
i = zOrder.length;
while (i--) {
if (zOrder[i] === ctrl) {
zOrder.splice(i, 1);
}
}
}
if (zOrder.length) {
for (i = 0; i < zOrder.length; i++) {
if (zOrder[i].modal) {
zIndex++;
topModal = zOrder[i];
}
zOrder[i].getEl().style.zIndex = zIndex;
zOrder[i].zIndex = zIndex;
zIndex++;
}
}
var modalBlockEl = $('#' + ctrl.classPrefix + 'modal-block', ctrl.getContainerElm())[0];
if (topModal) {
$(modalBlockEl).css('z-index', topModal.zIndex - 1);
} else if (modalBlockEl) {
modalBlockEl.parentNode.removeChild(modalBlockEl);
hasModal = false;
}
FloatPanel.currentZIndex = zIndex;
}
var FloatPanel = Panel.extend({
Mixins: [Movable, Resizable],
/**
* Constructs a new control instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {Boolean} autohide Automatically hide the panel.
*/
init: function(settings) {
var self = this;
self._super(settings);
self._eventsRoot = self;
self.classes.add('floatpanel');
// Hide floatpanes on click out side the root button
if (settings.autohide) {
bindDocumentClickHandler();
bindWindowResizeHandler();
visiblePanels.push(self);
}
if (settings.autofix) {
bindDocumentScrollHandler();
self.on('move', function() {
repositionPanel(this);
});
}
self.on('postrender show', function(e) {
if (e.control == self) {
var $modalBlockEl, prefix = self.classPrefix;
if (self.modal && !hasModal) {
$modalBlockEl = $('#' + prefix + 'modal-block', self.getContainerElm());
if (!$modalBlockEl[0]) {
$modalBlockEl = $(
'<div id="' + prefix + 'modal-block" class="' + prefix + 'reset ' + prefix + 'fade"></div>'
).appendTo(self.getContainerElm());
}
Delay.setTimeout(function() {
$modalBlockEl.addClass(prefix + 'in');
$(self.getEl()).addClass(prefix + 'in');
});
hasModal = true;
}
addRemove(true, self);
}
});
self.on('show', function() {
self.parents().each(function(ctrl) {
if (ctrl.state.get('fixed')) {
self.fixed(true);
return false;
}
});
});
if (settings.popover) {
self._preBodyHtml = '<div class="' + self.classPrefix + 'arrow"></div>';
self.classes.add('popover').add('bottom').add(self.isRtl() ? 'end' : 'start');
}
self.aria('label', settings.ariaLabel);
self.aria('labelledby', self._id);
self.aria('describedby', self.describedBy || self._id + '-none');
},
fixed: function(state) {
var self = this;
if (self.state.get('fixed') != state) {
if (self.state.get('rendered')) {
var viewport = DomUtils.getViewPort();
if (state) {
self.layoutRect().y -= viewport.y;
} else {
self.layoutRect().y += viewport.y;
}
}
self.classes.toggle('fixed', state);
self.state.set('fixed', state);
}
return self;
},
/**
* Shows the current float panel.
*
* @method show
* @return {tinymce.ui.FloatPanel} Current floatpanel instance.
*/
show: function() {
var self = this, i, state = self._super();
i = visiblePanels.length;
while (i--) {
if (visiblePanels[i] === self) {
break;
}
}
if (i === -1) {
visiblePanels.push(self);
}
return state;
},
/**
* Hides the current float panel.
*
* @method hide
* @return {tinymce.ui.FloatPanel} Current floatpanel instance.
*/
hide: function() {
removeVisiblePanel(this);
addRemove(false, this);
return this._super();
},
/**
* Hide all visible float panels with he autohide setting enabled. This is for
* manually hiding floating menus or panels.
*
* @method hideAll
*/
hideAll: function() {
FloatPanel.hideAll();
},
/**
* Closes the float panel. This will remove the float panel from page and fire the close event.
*
* @method close
*/
close: function() {
var self = this;
if (!self.fire('close').isDefaultPrevented()) {
self.remove();
addRemove(false, self);
}
return self;
},
/**
* Removes the float panel from page.
*
* @method remove
*/
remove: function() {
removeVisiblePanel(this);
this._super();
},
postRender: function() {
var self = this;
if (self.settings.bodyRole) {
this.getEl('body').setAttribute('role', self.settings.bodyRole);
}
return self._super();
}
});
/**
* Hide all visible float panels with he autohide setting enabled. This is for
* manually hiding floating menus or panels.
*
* @static
* @method hideAll
*/
FloatPanel.hideAll = function() {
var i = visiblePanels.length;
while (i--) {
var panel = visiblePanels[i];
if (panel && panel.settings.autohide) {
panel.hide();
visiblePanels.splice(i, 1);
}
}
};
function removeVisiblePanel(panel) {
var i;
i = visiblePanels.length;
while (i--) {
if (visiblePanels[i] === panel) {
visiblePanels.splice(i, 1);
}
}
i = zOrder.length;
while (i--) {
if (zOrder[i] === panel) {
zOrder.splice(i, 1);
}
}
}
return FloatPanel;
});
// Included from: js/tinymce/classes/ui/Window.js
/**
* Window.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new window.
*
* @-x-less Window.less
* @class tinymce.ui.Window
* @extends tinymce.ui.FloatPanel
*/
define("tinymce/ui/Window", [
"tinymce/ui/FloatPanel",
"tinymce/ui/Panel",
"tinymce/ui/DomUtils",
"tinymce/dom/DomQuery",
"tinymce/ui/DragHelper",
"tinymce/ui/BoxUtils",
"tinymce/Env",
"tinymce/util/Delay"
], function(FloatPanel, Panel, DomUtils, $, DragHelper, BoxUtils, Env, Delay) {
"use strict";
var windows = [], oldMetaValue = '';
function toggleFullScreenState(state) {
var noScaleMetaValue = 'width=device-width,initial-scale=1.0,user-scalable=0,minimum-scale=1.0,maximum-scale=1.0',
viewport = $("meta[name=viewport]")[0],
contentValue;
if (Env.overrideViewPort === false) {
return;
}
if (!viewport) {
viewport = document.createElement('meta');
viewport.setAttribute('name', 'viewport');
document.getElementsByTagName('head')[0].appendChild(viewport);
}
contentValue = viewport.getAttribute('content');
if (contentValue && typeof oldMetaValue != 'undefined') {
oldMetaValue = contentValue;
}
viewport.setAttribute('content', state ? noScaleMetaValue : oldMetaValue);
}
function toggleBodyFullScreenClasses(classPrefix, state) {
if (checkFullscreenWindows() && state === false) {
$([document.documentElement, document.body]).removeClass(classPrefix + 'fullscreen');
}
}
function checkFullscreenWindows() {
for (var i = 0; i < windows.length; i++) {
if (windows[i]._fullscreen) {
return true;
}
}
return false;
}
function handleWindowResize() {
if (!Env.desktop) {
var lastSize = {
w: window.innerWidth,
h: window.innerHeight
};
Delay.setInterval(function() {
var w = window.innerWidth,
h = window.innerHeight;
if (lastSize.w != w || lastSize.h != h) {
lastSize = {
w: w,
h: h
};
$(window).trigger('resize');
}
}, 100);
}
function reposition() {
var i, rect = DomUtils.getWindowSize(), layoutRect;
for (i = 0; i < windows.length; i++) {
layoutRect = windows[i].layoutRect();
windows[i].moveTo(
windows[i].settings.x || Math.max(0, rect.w / 2 - layoutRect.w / 2),
windows[i].settings.y || Math.max(0, rect.h / 2 - layoutRect.h / 2)
);
}
}
$(window).on('resize', reposition);
}
var Window = FloatPanel.extend({
modal: true,
Defaults: {
border: 1,
layout: 'flex',
containerCls: 'panel',
role: 'dialog',
callbacks: {
submit: function() {
this.fire('submit', {data: this.toJSON()});
},
close: function() {
this.close();
}
}
},
/**
* Constructs a instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
*/
init: function(settings) {
var self = this;
self._super(settings);
if (self.isRtl()) {
self.classes.add('rtl');
}
self.classes.add('window');
self.bodyClasses.add('window-body');
self.state.set('fixed', true);
// Create statusbar
if (settings.buttons) {
self.statusbar = new Panel({
layout: 'flex',
border: '1 0 0 0',
spacing: 3,
padding: 10,
align: 'center',
pack: self.isRtl() ? 'start' : 'end',
defaults: {
type: 'button'
},
items: settings.buttons
});
self.statusbar.classes.add('foot');
self.statusbar.parent(self);
}
self.on('click', function(e) {
var closeClass = self.classPrefix + 'close';
if (DomUtils.hasClass(e.target, closeClass) || DomUtils.hasClass(e.target.parentNode, closeClass)) {
self.close();
}
});
self.on('cancel', function() {
self.close();
});
self.aria('describedby', self.describedBy || self._id + '-none');
self.aria('label', settings.title);
self._fullscreen = false;
},
/**
* Recalculates the positions of the controls in the current container.
* This is invoked by the reflow method and shouldn't be called directly.
*
* @method recalc
*/
recalc: function() {
var self = this, statusbar = self.statusbar, layoutRect, width, x, needsRecalc;
if (self._fullscreen) {
self.layoutRect(DomUtils.getWindowSize());
self.layoutRect().contentH = self.layoutRect().innerH;
}
self._super();
layoutRect = self.layoutRect();
// Resize window based on title width
if (self.settings.title && !self._fullscreen) {
width = layoutRect.headerW;
if (width > layoutRect.w) {
x = layoutRect.x - Math.max(0, width / 2);
self.layoutRect({w: width, x: x});
needsRecalc = true;
}
}
// Resize window based on statusbar width
if (statusbar) {
statusbar.layoutRect({w: self.layoutRect().innerW}).recalc();
width = statusbar.layoutRect().minW + layoutRect.deltaW;
if (width > layoutRect.w) {
x = layoutRect.x - Math.max(0, width - layoutRect.w);
self.layoutRect({w: width, x: x});
needsRecalc = true;
}
}
// Recalc body and disable auto resize
if (needsRecalc) {
self.recalc();
}
},
/**
* Initializes the current controls layout rect.
* This will be executed by the layout managers to determine the
* default minWidth/minHeight etc.
*
* @method initLayoutRect
* @return {Object} Layout rect instance.
*/
initLayoutRect: function() {
var self = this, layoutRect = self._super(), deltaH = 0, headEl;
// Reserve vertical space for title
if (self.settings.title && !self._fullscreen) {
headEl = self.getEl('head');
var size = DomUtils.getSize(headEl);
layoutRect.headerW = size.width;
layoutRect.headerH = size.height;
deltaH += layoutRect.headerH;
}
// Reserve vertical space for statusbar
if (self.statusbar) {
deltaH += self.statusbar.layoutRect().h;
}
layoutRect.deltaH += deltaH;
layoutRect.minH += deltaH;
//layoutRect.innerH -= deltaH;
layoutRect.h += deltaH;
var rect = DomUtils.getWindowSize();
layoutRect.x = self.settings.x || Math.max(0, rect.w / 2 - layoutRect.w / 2);
layoutRect.y = self.settings.y || Math.max(0, rect.h / 2 - layoutRect.h / 2);
return layoutRect;
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, layout = self._layout, id = self._id, prefix = self.classPrefix;
var settings = self.settings, headerHtml = '', footerHtml = '', html = settings.html;
self.preRender();
layout.preRender(self);
if (settings.title) {
headerHtml = (
'<div id="' + id + '-head" class="' + prefix + 'window-head">' +
'<div id="' + id + '-title" class="' + prefix + 'title">' + self.encode(settings.title) + '</div>' +
'<div id="' + id + '-dragh" class="' + prefix + 'dragh"></div>' +
'<button type="button" class="' + prefix + 'close" aria-hidden="true">' +
'<i class="mce-ico mce-i-remove"></i>' +
'</button>' +
'</div>'
);
}
if (settings.url) {
html = '<iframe src="' + settings.url + '" tabindex="-1"></iframe>';
}
if (typeof html == "undefined") {
html = layout.renderHtml(self);
}
if (self.statusbar) {
footerHtml = self.statusbar.renderHtml();
}
return (
'<div id="' + id + '" class="' + self.classes + '" hidefocus="1">' +
'<div class="' + self.classPrefix + 'reset" role="application">' +
headerHtml +
'<div id="' + id + '-body" class="' + self.bodyClasses + '">' +
html +
'</div>' +
footerHtml +
'</div>' +
'</div>'
);
},
/**
* Switches the window fullscreen mode.
*
* @method fullscreen
* @param {Boolean} state True/false state.
* @return {tinymce.ui.Window} Current window instance.
*/
fullscreen: function(state) {
var self = this, documentElement = document.documentElement, slowRendering, prefix = self.classPrefix, layoutRect;
if (state != self._fullscreen) {
$(window).on('resize', function() {
var time;
if (self._fullscreen) {
// Time the layout time if it's to slow use a timeout to not hog the CPU
if (!slowRendering) {
time = new Date().getTime();
var rect = DomUtils.getWindowSize();
self.moveTo(0, 0).resizeTo(rect.w, rect.h);
if ((new Date().getTime()) - time > 50) {
slowRendering = true;
}
} else {
if (!self._timer) {
self._timer = Delay.setTimeout(function() {
var rect = DomUtils.getWindowSize();
self.moveTo(0, 0).resizeTo(rect.w, rect.h);
self._timer = 0;
}, 50);
}
}
}
});
layoutRect = self.layoutRect();
self._fullscreen = state;
if (!state) {
self.borderBox = BoxUtils.parseBox(self.settings.border);
self.getEl('head').style.display = '';
layoutRect.deltaH += layoutRect.headerH;
$([documentElement, document.body]).removeClass(prefix + 'fullscreen');
self.classes.remove('fullscreen');
self.moveTo(self._initial.x, self._initial.y).resizeTo(self._initial.w, self._initial.h);
} else {
self._initial = {x: layoutRect.x, y: layoutRect.y, w: layoutRect.w, h: layoutRect.h};
self.borderBox = BoxUtils.parseBox('0');
self.getEl('head').style.display = 'none';
layoutRect.deltaH -= layoutRect.headerH + 2;
$([documentElement, document.body]).addClass(prefix + 'fullscreen');
self.classes.add('fullscreen');
var rect = DomUtils.getWindowSize();
self.moveTo(0, 0).resizeTo(rect.w, rect.h);
}
}
return self.reflow();
},
/**
* Called after the control has been rendered.
*
* @method postRender
*/
postRender: function() {
var self = this, startPos;
setTimeout(function() {
self.classes.add('in');
self.fire('open');
}, 0);
self._super();
if (self.statusbar) {
self.statusbar.postRender();
}
self.focus();
this.dragHelper = new DragHelper(self._id + '-dragh', {
start: function() {
startPos = {
x: self.layoutRect().x,
y: self.layoutRect().y
};
},
drag: function(e) {
self.moveTo(startPos.x + e.deltaX, startPos.y + e.deltaY);
}
});
self.on('submit', function(e) {
if (!e.isDefaultPrevented()) {
self.close();
}
});
windows.push(self);
toggleFullScreenState(true);
},
/**
* Fires a submit event with the serialized form.
*
* @method submit
* @return {Object} Event arguments object.
*/
submit: function() {
return this.fire('submit', {data: this.toJSON()});
},
/**
* Removes the current control from DOM and from UI collections.
*
* @method remove
* @return {tinymce.ui.Control} Current control instance.
*/
remove: function() {
var self = this, i;
self.dragHelper.destroy();
self._super();
if (self.statusbar) {
this.statusbar.remove();
}
toggleBodyFullScreenClasses(self.classPrefix, false);
i = windows.length;
while (i--) {
if (windows[i] === self) {
windows.splice(i, 1);
}
}
toggleFullScreenState(windows.length > 0);
},
/**
* Returns the contentWindow object of the iframe if it exists.
*
* @method getContentWindow
* @return {Window} window object or null.
*/
getContentWindow: function() {
var ifr = this.getEl().getElementsByTagName('iframe')[0];
return ifr ? ifr.contentWindow : null;
}
});
handleWindowResize();
return Window;
});
// Included from: js/tinymce/classes/ui/MessageBox.js
/**
* MessageBox.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class is used to create MessageBoxes like alerts/confirms etc.
*
* @class tinymce.ui.MessageBox
* @extends tinymce.ui.FloatPanel
*/
define("tinymce/ui/MessageBox", [
"tinymce/ui/Window"
], function(Window) {
"use strict";
var MessageBox = Window.extend({
/**
* Constructs a instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
*/
init: function(settings) {
settings = {
border: 1,
padding: 20,
layout: 'flex',
pack: "center",
align: "center",
containerCls: 'panel',
autoScroll: true,
buttons: {type: "button", text: "Ok", action: "ok"},
items: {
type: "label",
multiline: true,
maxWidth: 500,
maxHeight: 200
}
};
this._super(settings);
},
Statics: {
/**
* Ok buttons constant.
*
* @static
* @final
* @field {Number} OK
*/
OK: 1,
/**
* Ok/cancel buttons constant.
*
* @static
* @final
* @field {Number} OK_CANCEL
*/
OK_CANCEL: 2,
/**
* yes/no buttons constant.
*
* @static
* @final
* @field {Number} YES_NO
*/
YES_NO: 3,
/**
* yes/no/cancel buttons constant.
*
* @static
* @final
* @field {Number} YES_NO_CANCEL
*/
YES_NO_CANCEL: 4,
/**
* Constructs a new message box and renders it to the body element.
*
* @static
* @method msgBox
* @param {Object} settings Name/value object with settings.
*/
msgBox: function(settings) {
var buttons, callback = settings.callback || function() {};
function createButton(text, status, primary) {
return {
type: "button",
text: text,
subtype: primary ? 'primary' : '',
onClick: function(e) {
e.control.parents()[1].close();
callback(status);
}
};
}
switch (settings.buttons) {
case MessageBox.OK_CANCEL:
buttons = [
createButton('Ok', true, true),
createButton('Cancel', false)
];
break;
case MessageBox.YES_NO:
case MessageBox.YES_NO_CANCEL:
buttons = [
createButton('Yes', 1, true),
createButton('No', 0)
];
if (settings.buttons == MessageBox.YES_NO_CANCEL) {
buttons.push(createButton('Cancel', -1));
}
break;
default:
buttons = [
createButton('Ok', true, true)
];
break;
}
return new Window({
padding: 20,
x: settings.x,
y: settings.y,
minWidth: 300,
minHeight: 100,
layout: "flex",
pack: "center",
align: "center",
buttons: buttons,
title: settings.title,
role: 'alertdialog',
items: {
type: "label",
multiline: true,
maxWidth: 500,
maxHeight: 200,
text: settings.text
},
onPostRender: function() {
this.aria('describedby', this.items()[0]._id);
},
onClose: settings.onClose,
onCancel: function() {
callback(false);
}
}).renderTo(document.body).reflow();
},
/**
* Creates a new alert dialog.
*
* @method alert
* @param {Object} settings Settings for the alert dialog.
* @param {function} [callback] Callback to execute when the user makes a choice.
*/
alert: function(settings, callback) {
if (typeof settings == "string") {
settings = {text: settings};
}
settings.callback = callback;
return MessageBox.msgBox(settings);
},
/**
* Creates a new confirm dialog.
*
* @method confirm
* @param {Object} settings Settings for the confirm dialog.
* @param {function} [callback] Callback to execute when the user makes a choice.
*/
confirm: function(settings, callback) {
if (typeof settings == "string") {
settings = {text: settings};
}
settings.callback = callback;
settings.buttons = MessageBox.OK_CANCEL;
return MessageBox.msgBox(settings);
}
}
});
return MessageBox;
});
// Included from: js/tinymce/classes/WindowManager.js
/**
* WindowManager.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class handles the creation of native windows and dialogs. This class can be extended to provide for example inline dialogs.
*
* @class tinymce.WindowManager
* @example
* // Opens a new dialog with the file.htm file and the size 320x240
* // It also adds a custom parameter this can be retrieved by using tinyMCEPopup.getWindowArg inside the dialog.
* tinymce.activeEditor.windowManager.open({
* url: 'file.htm',
* width: 320,
* height: 240
* }, {
* custom_param: 1
* });
*
* // Displays an alert box using the active editors window manager instance
* tinymce.activeEditor.windowManager.alert('Hello world!');
*
* // Displays an confirm box and an alert message will be displayed depending on what you choose in the confirm
* tinymce.activeEditor.windowManager.confirm("Do you want to do something", function(s) {
* if (s)
* tinymce.activeEditor.windowManager.alert("Ok");
* else
* tinymce.activeEditor.windowManager.alert("Cancel");
* });
*/
define("tinymce/WindowManager", [
"tinymce/ui/Window",
"tinymce/ui/MessageBox"
], function(Window, MessageBox) {
return function(editor) {
var self = this, windows = [];
function getTopMostWindow() {
if (windows.length) {
return windows[windows.length - 1];
}
}
function fireOpenEvent(win) {
editor.fire('OpenWindow', {
win: win
});
}
function fireCloseEvent(win) {
editor.fire('CloseWindow', {
win: win
});
}
self.windows = windows;
editor.on('remove', function() {
var i = windows.length;
while (i--) {
windows[i].close();
}
});
/**
* Opens a new window.
*
* @method open
* @param {Object} args Optional name/value settings collection contains things like width/height/url etc.
* @param {Object} params Options like title, file, width, height etc.
* @option {String} title Window title.
* @option {String} file URL of the file to open in the window.
* @option {Number} width Width in pixels.
* @option {Number} height Height in pixels.
* @option {Boolean} autoScroll Specifies whether the popup window can have scrollbars if required (i.e. content
* larger than the popup size specified).
*/
self.open = function(args, params) {
var win;
editor.editorManager.setActive(editor);
args.title = args.title || ' ';
// Handle URL
args.url = args.url || args.file; // Legacy
if (args.url) {
args.width = parseInt(args.width || 320, 10);
args.height = parseInt(args.height || 240, 10);
}
// Handle body
if (args.body) {
args.items = {
defaults: args.defaults,
type: args.bodyType || 'form',
items: args.body,
data: args.data,
callbacks: args.commands
};
}
if (!args.url && !args.buttons) {
args.buttons = [
{text: 'Ok', subtype: 'primary', onclick: function() {
win.find('form')[0].submit();
}},
{text: 'Cancel', onclick: function() {
win.close();
}}
];
}
win = new Window(args);
windows.push(win);
win.on('close', function() {
var i = windows.length;
while (i--) {
if (windows[i] === win) {
windows.splice(i, 1);
}
}
if (!windows.length) {
editor.focus();
}
fireCloseEvent(win);
});
// Handle data
if (args.data) {
win.on('postRender', function() {
this.find('*').each(function(ctrl) {
var name = ctrl.name();
if (name in args.data) {
ctrl.value(args.data[name]);
}
});
});
}
// store args and parameters
win.features = args || {};
win.params = params || {};
// Takes a snapshot in the FocusManager of the selection before focus is lost to dialog
if (windows.length === 1) {
editor.nodeChanged();
}
win = win.renderTo().reflow();
fireOpenEvent(win);
return win;
};
/**
* Creates a alert dialog. Please don't use the blocking behavior of this
* native version use the callback method instead then it can be extended.
*
* @method alert
* @param {String} message Text to display in the new alert dialog.
* @param {function} callback Callback function to be executed after the user has selected ok.
* @param {Object} scope Optional scope to execute the callback in.
* @example
* // Displays an alert box using the active editors window manager instance
* tinymce.activeEditor.windowManager.alert('Hello world!');
*/
self.alert = function(message, callback, scope) {
var win;
win = MessageBox.alert(message, function() {
if (callback) {
callback.call(scope || this);
} else {
editor.focus();
}
});
win.on('close', function() {
fireCloseEvent(win);
});
fireOpenEvent(win);
};
/**
* Creates a confirm dialog. Please don't use the blocking behavior of this
* native version use the callback method instead then it can be extended.
*
* @method confirm
* @param {String} message Text to display in the new confirm dialog.
* @param {function} callback Callback function to be executed after the user has selected ok or cancel.
* @param {Object} scope Optional scope to execute the callback in.
* @example
* // Displays an confirm box and an alert message will be displayed depending on what you choose in the confirm
* tinymce.activeEditor.windowManager.confirm("Do you want to do something", function(s) {
* if (s)
* tinymce.activeEditor.windowManager.alert("Ok");
* else
* tinymce.activeEditor.windowManager.alert("Cancel");
* });
*/
self.confirm = function(message, callback, scope) {
var win;
win = MessageBox.confirm(message, function(state) {
callback.call(scope || this, state);
});
win.on('close', function() {
fireCloseEvent(win);
});
fireOpenEvent(win);
};
/**
* Closes the top most window.
*
* @method close
*/
self.close = function() {
if (getTopMostWindow()) {
getTopMostWindow().close();
}
};
/**
* Returns the params of the last window open call. This can be used in iframe based
* dialog to get params passed from the tinymce plugin.
*
* @example
* var dialogArguments = top.tinymce.activeEditor.windowManager.getParams();
*
* @method getParams
* @return {Object} Name/value object with parameters passed from windowManager.open call.
*/
self.getParams = function() {
return getTopMostWindow() ? getTopMostWindow().params : null;
};
/**
* Sets the params of the last opened window.
*
* @method setParams
* @param {Object} params Params object to set for the last opened window.
*/
self.setParams = function(params) {
if (getTopMostWindow()) {
getTopMostWindow().params = params;
}
};
/**
* Returns the currently opened window objects.
*
* @method getWindows
* @return {Array} Array of the currently opened windows.
*/
self.getWindows = function() {
return windows;
};
};
});
// Included from: js/tinymce/classes/ui/Tooltip.js
/**
* Tooltip.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a tooltip instance.
*
* @-x-less ToolTip.less
* @class tinymce.ui.ToolTip
* @extends tinymce.ui.Control
* @mixes tinymce.ui.Movable
*/
define("tinymce/ui/Tooltip", [
"tinymce/ui/Control",
"tinymce/ui/Movable"
], function(Control, Movable) {
return Control.extend({
Mixins: [Movable],
Defaults: {
classes: 'widget tooltip tooltip-n'
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, prefix = self.classPrefix;
return (
'<div id="' + self._id + '" class="' + self.classes + '" role="presentation">' +
'<div class="' + prefix + 'tooltip-arrow"></div>' +
'<div class="' + prefix + 'tooltip-inner">' + self.encode(self.state.get('text')) + '</div>' +
'</div>'
);
},
bindStates: function() {
var self = this;
self.state.on('change:text', function(e) {
self.getEl().lastChild.innerHTML = self.encode(e.value);
});
return self._super();
},
/**
* Repaints the control after a layout operation.
*
* @method repaint
*/
repaint: function() {
var self = this, style, rect;
style = self.getEl().style;
rect = self._layoutRect;
style.left = rect.x + 'px';
style.top = rect.y + 'px';
style.zIndex = 0xFFFF + 0xFFFF;
}
});
});
// Included from: js/tinymce/classes/ui/Widget.js
/**
* Widget.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Widget base class a widget is a control that has a tooltip and some basic states.
*
* @class tinymce.ui.Widget
* @extends tinymce.ui.Control
*/
define("tinymce/ui/Widget", [
"tinymce/ui/Control",
"tinymce/ui/Tooltip"
], function(Control, Tooltip) {
"use strict";
var tooltip;
var Widget = Control.extend({
/**
* Constructs a instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {String} tooltip Tooltip text to display when hovering.
* @setting {Boolean} autofocus True if the control should be focused when rendered.
* @setting {String} text Text to display inside widget.
*/
init: function(settings) {
var self = this;
self._super(settings);
settings = self.settings;
self.canFocus = true;
if (settings.tooltip && Widget.tooltips !== false) {
self.on('mouseenter', function(e) {
var tooltip = self.tooltip().moveTo(-0xFFFF);
if (e.control == self) {
var rel = tooltip.text(settings.tooltip).show().testMoveRel(self.getEl(), ['bc-tc', 'bc-tl', 'bc-tr']);
tooltip.classes.toggle('tooltip-n', rel == 'bc-tc');
tooltip.classes.toggle('tooltip-nw', rel == 'bc-tl');
tooltip.classes.toggle('tooltip-ne', rel == 'bc-tr');
tooltip.moveRel(self.getEl(), rel);
} else {
tooltip.hide();
}
});
self.on('mouseleave mousedown click', function() {
self.tooltip().hide();
});
}
self.aria('label', settings.ariaLabel || settings.tooltip);
},
/**
* Returns the current tooltip instance.
*
* @method tooltip
* @return {tinymce.ui.Tooltip} Tooltip instance.
*/
tooltip: function() {
if (!tooltip) {
tooltip = new Tooltip({type: 'tooltip'});
tooltip.renderTo();
}
return tooltip;
},
/**
* Called after the control has been rendered.
*
* @method postRender
*/
postRender: function() {
var self = this, settings = self.settings;
self._super();
if (!self.parent() && (settings.width || settings.height)) {
self.initLayoutRect();
self.repaint();
}
if (settings.autofocus) {
self.focus();
}
},
bindStates: function() {
var self = this;
function disable(state) {
self.aria('disabled', state);
self.classes.toggle('disabled', state);
}
function active(state) {
self.aria('pressed', state);
self.classes.toggle('active', state);
}
self.state.on('change:disabled', function(e) {
disable(e.value);
});
self.state.on('change:active', function(e) {
active(e.value);
});
if (self.state.get('disabled')) {
disable(true);
}
if (self.state.get('active')) {
active(true);
}
return self._super();
},
/**
* Removes the current control from DOM and from UI collections.
*
* @method remove
* @return {tinymce.ui.Control} Current control instance.
*/
remove: function() {
this._super();
if (tooltip) {
tooltip.remove();
tooltip = null;
}
}
});
return Widget;
});
// Included from: js/tinymce/classes/ui/Progress.js
/**
* Progress.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Progress control.
*
* @-x-less Progress.less
* @class tinymce.ui.Progress
* @extends tinymce.ui.Control
*/
define("tinymce/ui/Progress", [
"tinymce/ui/Widget"
], function(Widget) {
"use strict";
return Widget.extend({
Defaults: {
value: 0
},
init: function(settings) {
var self = this;
self._super(settings);
self.classes.add('progress');
if (!self.settings.filter) {
self.settings.filter = function(value) {
return Math.round(value);
};
}
},
renderHtml: function() {
var self = this, id = self._id, prefix = this.classPrefix;
return (
'<div id="' + id + '" class="' + self.classes + '">' +
'<div class="' + prefix + 'bar-container">' +
'<div class="' + prefix + 'bar"></div>' +
'</div>' +
'<div class="' + prefix + 'text">0%</div>' +
'</div>'
);
},
postRender: function() {
var self = this;
self._super();
self.value(self.settings.value);
return self;
},
bindStates: function() {
var self = this;
function setValue(value) {
value = self.settings.filter(value);
self.getEl().lastChild.innerHTML = value + '%';
self.getEl().firstChild.firstChild.style.width = value + '%';
}
self.state.on('change:value', function(e) {
setValue(e.value);
});
setValue(self.state.get('value'));
return self._super();
}
});
});
// Included from: js/tinymce/classes/ui/Notification.js
/**
* Notification.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a notification instance.
*
* @-x-less Notification.less
* @class tinymce.ui.Notification
* @extends tinymce.ui.Container
* @mixes tinymce.ui.Movable
*/
define("tinymce/ui/Notification", [
"tinymce/ui/Control",
"tinymce/ui/Movable",
"tinymce/ui/Progress",
"tinymce/util/Delay"
], function(Control, Movable, Progress, Delay) {
return Control.extend({
Mixins: [Movable],
Defaults: {
classes: 'widget notification'
},
init: function(settings) {
var self = this;
self._super(settings);
if (settings.text) {
self.text(settings.text);
}
if (settings.icon) {
self.icon = settings.icon;
}
if (settings.color) {
self.color = settings.color;
}
if (settings.type) {
self.classes.add('notification-' + settings.type);
}
if (settings.timeout && (settings.timeout < 0 || settings.timeout > 0) && !settings.closeButton) {
self.closeButton = false;
} else {
self.classes.add('has-close');
self.closeButton = true;
}
if (settings.progressBar) {
self.progressBar = new Progress();
}
self.on('click', function(e) {
if (e.target.className.indexOf(self.classPrefix + 'close') != -1) {
self.close();
}
});
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, prefix = self.classPrefix, icon = '', closeButton = '', progressBar = '', notificationStyle = '';
if (self.icon) {
icon = '<i class="' + prefix + 'ico' + ' ' + prefix + 'i-' + self.icon + '"></i>';
}
if (self.color) {
notificationStyle = ' style="background-color: ' + self.color + '"';
}
if (self.closeButton) {
closeButton = '<button type="button" class="' + prefix + 'close" aria-hidden="true">\u00d7</button>';
}
if (self.progressBar) {
progressBar = self.progressBar.renderHtml();
}
return (
'<div id="' + self._id + '" class="' + self.classes + '"' + notificationStyle + ' role="presentation">' +
icon +
'<div class="' + prefix + 'notification-inner">' + self.state.get('text') + '</div>' +
progressBar +
closeButton +
'</div>'
);
},
postRender: function() {
var self = this;
Delay.setTimeout(function() {
self.$el.addClass(self.classPrefix + 'in');
});
return self._super();
},
bindStates: function() {
var self = this;
self.state.on('change:text', function(e) {
self.getEl().childNodes[1].innerHTML = e.value;
});
if (self.progressBar) {
self.progressBar.bindStates();
}
return self._super();
},
close: function() {
var self = this;
if (!self.fire('close').isDefaultPrevented()) {
self.remove();
}
return self;
},
/**
* Repaints the control after a layout operation.
*
* @method repaint
*/
repaint: function() {
var self = this, style, rect;
style = self.getEl().style;
rect = self._layoutRect;
style.left = rect.x + 'px';
style.top = rect.y + 'px';
// Hardcoded arbitrary z-value because we want the
// notifications under the other windows
style.zIndex = 0xFFFF - 1;
}
});
});
// Included from: js/tinymce/classes/NotificationManager.js
/**
* NotificationManager.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class handles the creation of TinyMCE's notifications.
*
* @class tinymce.notificationManager
* @example
* // Opens a new notification of type "error" with text "An error occurred."
* tinymce.activeEditor.notificationManager.open({
* text: 'An error occurred.',
* type: 'error'
* });
*/
define("tinymce/NotificationManager", [
"tinymce/ui/Notification",
"tinymce/util/Delay",
"tinymce/util/Tools"
], function(Notification, Delay, Tools) {
return function(editor) {
var self = this, notifications = [];
function getLastNotification() {
if (notifications.length) {
return notifications[notifications.length - 1];
}
}
self.notifications = notifications;
function resizeWindowEvent() {
Delay.requestAnimationFrame(function() {
prePositionNotifications();
positionNotifications();
});
}
// Since the viewport will change based on the present notifications, we need to move them all to the
// top left of the viewport to give an accurate size measurement so we can position them later.
function prePositionNotifications() {
for (var i = 0; i < notifications.length; i++) {
notifications[i].moveTo(0, 0);
}
}
function positionNotifications() {
if (notifications.length > 0) {
var firstItem = notifications.slice(0, 1)[0];
var container = editor.inline ? editor.getElement() : editor.getContentAreaContainer();
firstItem.moveRel(container, 'tc-tc');
if (notifications.length > 1) {
for (var i = 1; i < notifications.length; i++) {
notifications[i].moveRel(notifications[i - 1].getEl(), 'bc-tc');
}
}
}
}
editor.on('remove', function() {
var i = notifications.length;
while (i--) {
notifications[i].close();
}
});
editor.on('ResizeEditor', positionNotifications);
editor.on('ResizeWindow', resizeWindowEvent);
/**
* Opens a new notification.
*
* @method open
* @param {Object} args Optional name/value settings collection contains things like timeout/color/message etc.
*/
self.open = function(args) {
// Never open notification if editor has been removed.
if (editor.removed) {
return;
}
var notif;
editor.editorManager.setActive(editor);
var duplicate = findDuplicateMessage(notifications, args);
if (duplicate === null) {
notif = new Notification(args);
notifications.push(notif);
//If we have a timeout value
if (args.timeout > 0) {
notif.timer = setTimeout(function() {
notif.close();
}, args.timeout);
}
notif.on('close', function() {
var i = notifications.length;
if (notif.timer) {
editor.getWin().clearTimeout(notif.timer);
}
while (i--) {
if (notifications[i] === notif) {
notifications.splice(i, 1);
}
}
positionNotifications();
});
notif.renderTo();
positionNotifications();
} else {
notif = duplicate;
}
return notif;
};
/**
* Closes the top most notification.
*
* @method close
*/
self.close = function() {
if (getLastNotification()) {
getLastNotification().close();
}
};
/**
* Returns the currently opened notification objects.
*
* @method getNotifications
* @return {Array} Array of the currently opened notifications.
*/
self.getNotifications = function() {
return notifications;
};
editor.on('SkinLoaded', function() {
var serviceMessage = editor.settings.service_message;
if (serviceMessage) {
editor.notificationManager.open({
text: serviceMessage,
type: 'warning',
timeout: 0,
icon: ''
});
}
});
/**
* Finds any existing notification with the same properties as the new one.
* Returns either the found notification or null.
*
* @param {Notification[]} notificationArray - Array of current notifications
* @param {type: string, } newNotification - New notification object
* @returns {?Notification}
*/
function findDuplicateMessage(notificationArray, newNotification) {
if (!isPlainTextNotification(newNotification)) {
return null;
}
var filteredNotifications = Tools.grep(notificationArray, function (notification) {
return isSameNotification(newNotification, notification);
});
return filteredNotifications.length === 0 ? null : filteredNotifications[0];
}
/**
* Checks if the passed in args object has the same
* type and text properties as the sent in notification.
*
* @param {type: string, text: string} a - New notification args object
* @param {Notification} b - Old notification
* @returns {boolean}
*/
function isSameNotification(a, b) {
return a.type === b.settings.type && a.text === b.settings.text;
}
/**
* Checks that the notification does not have a progressBar
* or timeour property.
*
* @param {Notification} notification - Notification to check
* @returns {boolean}
*/
function isPlainTextNotification(notification) {
return !notification.progressBar && !notification.timeout;
}
//self.positionNotifications = positionNotifications;
};
});
// Included from: js/tinymce/classes/dom/NodePath.js
/**
* NodePath.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Handles paths of nodes within an element.
*
* @private
* @class tinymce.dom.NodePath
*/
define("tinymce/dom/NodePath", [
"tinymce/dom/DOMUtils"
], function(DOMUtils) {
function create(rootNode, targetNode, normalized) {
var path = [];
for (; targetNode && targetNode != rootNode; targetNode = targetNode.parentNode) {
path.push(DOMUtils.nodeIndex(targetNode, normalized));
}
return path;
}
function resolve(rootNode, path) {
var i, node, children;
for (node = rootNode, i = path.length - 1; i >= 0; i--) {
children = node.childNodes;
if (path[i] > children.length - 1) {
return null;
}
node = children[path[i]];
}
return node;
}
return {
create: create,
resolve: resolve
};
});
// Included from: js/tinymce/classes/util/Quirks.js
/**
* Quirks.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*
* @ignore-file
*/
/**
* This file includes fixes for various browser quirks it's made to make it easy to add/remove browser specific fixes.
*
* @private
* @class tinymce.util.Quirks
*/
define("tinymce/util/Quirks", [
"tinymce/util/VK",
"tinymce/dom/RangeUtils",
"tinymce/dom/TreeWalker",
"tinymce/dom/NodePath",
"tinymce/html/Node",
"tinymce/html/Entities",
"tinymce/Env",
"tinymce/util/Tools",
"tinymce/util/Delay",
"tinymce/caret/CaretContainer",
"tinymce/caret/CaretPosition",
"tinymce/caret/CaretWalker"
], function(VK, RangeUtils, TreeWalker, NodePath, Node, Entities, Env, Tools, Delay, CaretContainer, CaretPosition, CaretWalker) {
return function(editor) {
var each = Tools.each, $ = editor.$;
var BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE, dom = editor.dom, selection = editor.selection,
settings = editor.settings, parser = editor.parser, serializer = editor.serializer;
var isGecko = Env.gecko, isIE = Env.ie, isWebKit = Env.webkit;
var mceInternalUrlPrefix = 'data:text/mce-internal,';
var mceInternalDataType = isIE ? 'Text' : 'URL';
/**
* Executes a command with a specific state this can be to enable/disable browser editing features.
*/
function setEditorCommandState(cmd, state) {
try {
editor.getDoc().execCommand(cmd, false, state);
} catch (ex) {
// Ignore
}
}
/**
* Returns current IE document mode.
*/
function getDocumentMode() {
var documentMode = editor.getDoc().documentMode;
return documentMode ? documentMode : 6;
}
/**
* Returns true/false if the event is prevented or not.
*
* @private
* @param {Event} e Event object.
* @return {Boolean} true/false if the event is prevented or not.
*/
function isDefaultPrevented(e) {
return e.isDefaultPrevented();
}
/**
* Sets Text/URL data on the event's dataTransfer object to a special data:text/mce-internal url.
* This is to workaround the inability to set custom contentType on IE and Safari.
* The editor's selected content is encoded into this url so drag and drop between editors will work.
*
* @private
* @param {DragEvent} e Event object
*/
function setMceInternalContent(e) {
var selectionHtml, internalContent;
if (e.dataTransfer) {
if (editor.selection.isCollapsed() && e.target.tagName == 'IMG') {
selection.select(e.target);
}
selectionHtml = editor.selection.getContent();
// Safari/IE doesn't support custom dataTransfer items so we can only use URL and Text
if (selectionHtml.length > 0) {
internalContent = mceInternalUrlPrefix + escape(editor.id) + ',' + escape(selectionHtml);
e.dataTransfer.setData(mceInternalDataType, internalContent);
}
}
}
/**
* Gets content of special data:text/mce-internal url on the event's dataTransfer object.
* This is to workaround the inability to set custom contentType on IE and Safari.
* The editor's selected content is encoded into this url so drag and drop between editors will work.
*
* @private
* @param {DragEvent} e Event object
* @returns {String} mce-internal content
*/
function getMceInternalContent(e) {
var internalContent;
if (e.dataTransfer) {
internalContent = e.dataTransfer.getData(mceInternalDataType);
if (internalContent && internalContent.indexOf(mceInternalUrlPrefix) >= 0) {
internalContent = internalContent.substr(mceInternalUrlPrefix.length).split(',');
return {
id: unescape(internalContent[0]),
html: unescape(internalContent[1])
};
}
}
return null;
}
/**
* Inserts contents using the paste clipboard command if it's available if it isn't it will fallback
* to the core command.
*
* @private
* @param {String} content Content to insert at selection.
*/
function insertClipboardContents(content) {
if (editor.queryCommandSupported('mceInsertClipboardContent')) {
editor.execCommand('mceInsertClipboardContent', false, {content: content});
} else {
editor.execCommand('mceInsertContent', false, content);
}
}
/**
* Fixes a WebKit bug when deleting contents using backspace or delete key.
* WebKit will produce a span element if you delete across two block elements.
*
* Example:
* <h1>a</h1><p>|b</p>
*
* Will produce this on backspace:
* <h1>a<span style="<all runtime styles>">b</span></p>
*
* This fixes the backspace to produce:
* <h1>a|b</p>
*
* See bug: https://bugs.webkit.org/show_bug.cgi?id=45784
*
* This fixes the following delete scenarios:
* 1. Delete by pressing backspace key.
* 2. Delete by pressing delete key.
* 3. Delete by pressing backspace key with ctrl/cmd (Word delete).
* 4. Delete by pressing delete key with ctrl/cmd (Word delete).
* 5. Delete by drag/dropping contents inside the editor.
* 6. Delete by using Cut Ctrl+X/Cmd+X.
* 7. Delete by selecting contents and writing a character.
*
* This code is a ugly hack since writing full custom delete logic for just this bug
* fix seemed like a huge task. I hope we can remove this before the year 2030.
*/
function cleanupStylesWhenDeleting() {
var doc = editor.getDoc(), dom = editor.dom, selection = editor.selection;
var MutationObserver = window.MutationObserver, olderWebKit, dragStartRng;
// Add mini polyfill for older WebKits
// TODO: Remove this when old Safari versions gets updated
if (!MutationObserver) {
olderWebKit = true;
MutationObserver = function() {
var records = [], target;
function nodeInsert(e) {
var target = e.relatedNode || e.target;
records.push({target: target, addedNodes: [target]});
}
function attrModified(e) {
var target = e.relatedNode || e.target;
records.push({target: target, attributeName: e.attrName});
}
this.observe = function(node) {
target = node;
target.addEventListener('DOMSubtreeModified', nodeInsert, false);
target.addEventListener('DOMNodeInsertedIntoDocument', nodeInsert, false);
target.addEventListener('DOMNodeInserted', nodeInsert, false);
target.addEventListener('DOMAttrModified', attrModified, false);
};
this.disconnect = function() {
target.removeEventListener('DOMSubtreeModified', nodeInsert, false);
target.removeEventListener('DOMNodeInsertedIntoDocument', nodeInsert, false);
target.removeEventListener('DOMNodeInserted', nodeInsert, false);
target.removeEventListener('DOMAttrModified', attrModified, false);
};
this.takeRecords = function() {
return records;
};
};
}
function isTrailingBr(node) {
var blockElements = dom.schema.getBlockElements(), rootNode = editor.getBody();
if (node.nodeName != 'BR') {
return false;
}
for (; node != rootNode && !blockElements[node.nodeName]; node = node.parentNode) {
if (node.nextSibling) {
return false;
}
}
return true;
}
function isSiblingsIgnoreWhiteSpace(node1, node2) {
var node;
for (node = node1.nextSibling; node && node != node2; node = node.nextSibling) {
if (node.nodeType == 3 && $.trim(node.data).length === 0) {
continue;
}
if (node !== node2) {
return false;
}
}
return node === node2;
}
function findCaretNode(node, forward, startNode) {
var walker, current, nonEmptyElements;
// Protect against the possibility we are asked to find a caret node relative
// to a node that is no longer in the DOM tree. In this case attempting to
// select on any match leads to a scenario where selection is completely removed
// from the editor. This scenario is met in real world at a minimum on
// WebKit browsers when selecting all and Cmd-X cutting to delete content.
if (!dom.isChildOf(node, editor.getBody())) {
return;
}
nonEmptyElements = dom.schema.getNonEmptyElements();
walker = new TreeWalker(startNode || node, node);
while ((current = walker[forward ? 'next' : 'prev']())) {
if (nonEmptyElements[current.nodeName] && !isTrailingBr(current)) {
return current;
}
if (current.nodeType == 3 && current.data.length > 0) {
return current;
}
}
}
function deleteRangeBetweenTextBlocks(rng) {
var startBlock, endBlock, caretNodeBefore, caretNodeAfter, textBlockElements;
if (rng.collapsed) {
return;
}
startBlock = dom.getParent(RangeUtils.getNode(rng.startContainer, rng.startOffset), dom.isBlock);
endBlock = dom.getParent(RangeUtils.getNode(rng.endContainer, rng.endOffset), dom.isBlock);
textBlockElements = editor.schema.getTextBlockElements();
if (startBlock == endBlock) {
return;
}
if (!textBlockElements[startBlock.nodeName] || !textBlockElements[endBlock.nodeName]) {
return;
}
if (dom.getContentEditable(startBlock) === "false" || dom.getContentEditable(endBlock) === "false") {
return;
}
rng.deleteContents();
caretNodeBefore = findCaretNode(startBlock, false);
caretNodeAfter = findCaretNode(endBlock, true);
if (!dom.isEmpty(endBlock)) {
$(startBlock).append(endBlock.childNodes);
}
$(endBlock).remove();
if (caretNodeBefore) {
if (caretNodeBefore.nodeType == 1) {
if (caretNodeBefore.nodeName == "BR") {
rng.setStartBefore(caretNodeBefore);
rng.setEndBefore(caretNodeBefore);
} else {
rng.setStartAfter(caretNodeBefore);
rng.setEndAfter(caretNodeBefore);
}
} else {
rng.setStart(caretNodeBefore, caretNodeBefore.data.length);
rng.setEnd(caretNodeBefore, caretNodeBefore.data.length);
}
} else if (caretNodeAfter) {
if (caretNodeAfter.nodeType == 1) {
rng.setStartBefore(caretNodeAfter);
rng.setEndBefore(caretNodeAfter);
} else {
rng.setStart(caretNodeAfter, 0);
rng.setEnd(caretNodeAfter, 0);
}
}
selection.setRng(rng);
return true;
}
function expandBetweenBlocks(rng, isForward) {
var caretNode, targetCaretNode, textBlock, targetTextBlock, container, offset;
if (!rng.collapsed) {
return rng;
}
container = rng.startContainer;
offset = rng.startOffset;
if (container.nodeType == 3) {
if (isForward) {
if (offset < container.data.length) {
return rng;
}
} else {
if (offset > 0) {
return rng;
}
}
}
caretNode = RangeUtils.getNode(container, offset);
textBlock = dom.getParent(caretNode, dom.isBlock);
targetCaretNode = findCaretNode(editor.getBody(), isForward, caretNode);
targetTextBlock = dom.getParent(targetCaretNode, dom.isBlock);
var isAfter = container.nodeType === 1 && offset > container.childNodes.length - 1;
if (!caretNode || !targetCaretNode) {
return rng;
}
if (targetTextBlock && textBlock != targetTextBlock) {
if (!isForward) {
if (!isSiblingsIgnoreWhiteSpace(targetTextBlock, textBlock)) {
return rng;
}
if (targetCaretNode.nodeType == 1) {
if (targetCaretNode.nodeName == "BR") {
rng.setStartBefore(targetCaretNode);
} else {
rng.setStartAfter(targetCaretNode);
}
} else {
rng.setStart(targetCaretNode, targetCaretNode.data.length);
}
if (caretNode.nodeType == 1) {
if (isAfter) {
rng.setEndAfter(caretNode);
} else {
rng.setEndBefore(caretNode);
}
} else {
rng.setEndBefore(caretNode);
}
} else {
if (!isSiblingsIgnoreWhiteSpace(textBlock, targetTextBlock)) {
return rng;
}
if (caretNode.nodeType == 1) {
if (caretNode.nodeName == "BR") {
rng.setStartBefore(caretNode);
} else {
rng.setStartAfter(caretNode);
}
} else {
rng.setStart(caretNode, caretNode.data.length);
}
if (targetCaretNode.nodeType == 1) {
rng.setEnd(targetCaretNode, 0);
} else {
rng.setEndBefore(targetCaretNode);
}
}
}
return rng;
}
function handleTextBlockMergeDelete(isForward) {
var rng = selection.getRng();
rng = expandBetweenBlocks(rng, isForward);
if (deleteRangeBetweenTextBlocks(rng)) {
return true;
}
}
/**
* This retains the formatting if the last character is to be deleted.
*
* Backspace on this: <p><b><i>a|</i></b></p> would become <p>|</p> in WebKit.
* With this patch: <p><b><i>|<br></i></b></p>
*/
function handleLastBlockCharacterDelete(isForward, rng) {
var path, blockElm, newBlockElm, clonedBlockElm, sibling,
container, offset, br, currentFormatNodes;
function cloneTextBlockWithFormats(blockElm, node) {
currentFormatNodes = $(node).parents().filter(function(idx, node) {
return !!editor.schema.getTextInlineElements()[node.nodeName];
});
newBlockElm = blockElm.cloneNode(false);
currentFormatNodes = Tools.map(currentFormatNodes, function(formatNode) {
formatNode = formatNode.cloneNode(false);
if (newBlockElm.hasChildNodes()) {
formatNode.appendChild(newBlockElm.firstChild);
newBlockElm.appendChild(formatNode);
} else {
newBlockElm.appendChild(formatNode);
}
newBlockElm.appendChild(formatNode);
return formatNode;
});
if (currentFormatNodes.length) {
br = dom.create('br');
currentFormatNodes[0].appendChild(br);
dom.replace(newBlockElm, blockElm);
rng.setStartBefore(br);
rng.setEndBefore(br);
editor.selection.setRng(rng);
return br;
}
return null;
}
function isTextBlock(node) {
return node && editor.schema.getTextBlockElements()[node.tagName];
}
if (!rng.collapsed) {
return;
}
container = rng.startContainer;
offset = rng.startOffset;
blockElm = dom.getParent(container, dom.isBlock);
if (!isTextBlock(blockElm)) {
return;
}
if (container.nodeType == 1) {
container = container.childNodes[offset];
if (container && container.tagName != 'BR') {
return;
}
if (isForward) {
sibling = blockElm.nextSibling;
} else {
sibling = blockElm.previousSibling;
}
if (dom.isEmpty(blockElm) && isTextBlock(sibling) && dom.isEmpty(sibling)) {
if (cloneTextBlockWithFormats(blockElm, container)) {
dom.remove(sibling);
return true;
}
}
} else if (container.nodeType == 3) {
path = NodePath.create(blockElm, container);
clonedBlockElm = blockElm.cloneNode(true);
container = NodePath.resolve(clonedBlockElm, path);
if (isForward) {
if (offset >= container.data.length) {
return;
}
container.deleteData(offset, 1);
} else {
if (offset <= 0) {
return;
}
container.deleteData(offset - 1, 1);
}
if (dom.isEmpty(clonedBlockElm)) {
return cloneTextBlockWithFormats(blockElm, container);
}
}
}
function customDelete(isForward) {
var mutationObserver, rng, caretElement;
if (handleTextBlockMergeDelete(isForward)) {
return;
}
Tools.each(editor.getBody().getElementsByTagName('*'), function(elm) {
// Mark existing spans
if (elm.tagName == 'SPAN') {
elm.setAttribute('mce-data-marked', 1);
}
// Make sure all elements has a data-mce-style attribute
if (!elm.hasAttribute('data-mce-style') && elm.hasAttribute('style')) {
editor.dom.setAttrib(elm, 'style', editor.dom.getAttrib(elm, 'style'));
}
});
// Observe added nodes and style attribute changes
mutationObserver = new MutationObserver(function() {});
mutationObserver.observe(editor.getDoc(), {
childList: true,
attributes: true,
subtree: true,
attributeFilter: ['style']
});
editor.getDoc().execCommand(isForward ? 'ForwardDelete' : 'Delete', false, null);
rng = editor.selection.getRng();
caretElement = rng.startContainer.parentNode;
Tools.each(mutationObserver.takeRecords(), function(record) {
if (!dom.isChildOf(record.target, editor.getBody())) {
return;
}
// Restore style attribute to previous value
if (record.attributeName == "style") {
var oldValue = record.target.getAttribute('data-mce-style');
if (oldValue) {
record.target.setAttribute("style", oldValue);
} else {
record.target.removeAttribute("style");
}
}
// Remove all spans that aren't marked and retain selection
Tools.each(record.addedNodes, function(node) {
if (node.nodeName == "SPAN" && !node.getAttribute('mce-data-marked')) {
var offset, container;
if (node == caretElement) {
offset = rng.startOffset;
container = node.firstChild;
}
dom.remove(node, true);
if (container) {
rng.setStart(container, offset);
rng.setEnd(container, offset);
editor.selection.setRng(rng);
}
}
});
});
mutationObserver.disconnect();
// Remove any left over marks
Tools.each(editor.dom.select('span[mce-data-marked]'), function(span) {
span.removeAttribute('mce-data-marked');
});
}
function transactCustomDelete(isForward) {
editor.undoManager.transact(function () {
customDelete(isForward);
});
}
editor.on('keydown', function(e) {
var isForward = e.keyCode == DELETE, isMetaOrCtrl = e.ctrlKey || e.metaKey;
if (!isDefaultPrevented(e) && (isForward || e.keyCode == BACKSPACE)) {
var rng = editor.selection.getRng(), container = rng.startContainer, offset = rng.startOffset;
// Shift+Delete is cut
if (isForward && e.shiftKey) {
return;
}
if (handleLastBlockCharacterDelete(isForward, rng)) {
e.preventDefault();
return;
}
// Ignore non meta delete in the where there is text before/after the caret
if (!isMetaOrCtrl && rng.collapsed && container.nodeType == 3) {
if (isForward ? offset < container.data.length : offset > 0) {
return;
}
}
e.preventDefault();
if (isMetaOrCtrl) {
editor.selection.getSel().modify("extend", isForward ? "forward" : "backward", e.metaKey ? "lineboundary" : "word");
}
customDelete(isForward);
}
});
// Handle case where text is deleted by typing over
editor.on('keypress', function(e) {
if (!isDefaultPrevented(e) && !selection.isCollapsed() && e.charCode > 31 && !VK.metaKeyPressed(e)) {
var rng, currentFormatNodes, fragmentNode, blockParent, caretNode, charText;
rng = editor.selection.getRng();
charText = String.fromCharCode(e.charCode);
e.preventDefault();
// Keep track of current format nodes
currentFormatNodes = $(rng.startContainer).parents().filter(function(idx, node) {
return !!editor.schema.getTextInlineElements()[node.nodeName];
});
customDelete(true);
// Check if the browser removed them
currentFormatNodes = currentFormatNodes.filter(function(idx, node) {
return !$.contains(editor.getBody(), node);
});
// Then re-add them
if (currentFormatNodes.length) {
fragmentNode = dom.createFragment();
currentFormatNodes.each(function(idx, formatNode) {
formatNode = formatNode.cloneNode(false);
if (fragmentNode.hasChildNodes()) {
formatNode.appendChild(fragmentNode.firstChild);
fragmentNode.appendChild(formatNode);
} else {
caretNode = formatNode;
fragmentNode.appendChild(formatNode);
}
fragmentNode.appendChild(formatNode);
});
caretNode.appendChild(editor.getDoc().createTextNode(charText));
// Prevent edge case where older WebKit would add an extra BR element
blockParent = dom.getParent(rng.startContainer, dom.isBlock);
if (dom.isEmpty(blockParent)) {
$(blockParent).empty().append(fragmentNode);
} else {
rng.insertNode(fragmentNode);
}
rng.setStart(caretNode.firstChild, 1);
rng.setEnd(caretNode.firstChild, 1);
editor.selection.setRng(rng);
} else {
editor.selection.setContent(charText);
}
}
});
editor.addCommand('Delete', function() {
customDelete();
});
editor.addCommand('ForwardDelete', function() {
customDelete(true);
});
// Older WebKits doesn't properly handle the clipboard so we can't add the rest
if (olderWebKit) {
return;
}
editor.on('dragstart', function(e) {
dragStartRng = selection.getRng();
setMceInternalContent(e);
});
editor.on('drop', function(e) {
if (!isDefaultPrevented(e)) {
var internalContent = getMceInternalContent(e);
if (internalContent) {
e.preventDefault();
// Safari has a weird issue where drag/dropping images sometimes
// produces a green plus icon. When this happens the caretRangeFromPoint
// will return "null" even though the x, y coordinate is correct.
// But if we detach the insert from the drop event we will get a proper range
Delay.setEditorTimeout(editor, function() {
var pointRng = RangeUtils.getCaretRangeFromPoint(e.x, e.y, doc);
if (dragStartRng) {
selection.setRng(dragStartRng);
dragStartRng = null;
transactCustomDelete();
}
selection.setRng(pointRng);
insertClipboardContents(internalContent.html);
});
}
}
});
editor.on('cut', function(e) {
if (!isDefaultPrevented(e) && e.clipboardData && !editor.selection.isCollapsed()) {
e.preventDefault();
e.clipboardData.clearData();
e.clipboardData.setData('text/html', editor.selection.getContent());
e.clipboardData.setData('text/plain', editor.selection.getContent({format: 'text'}));
// Needed delay for https://code.google.com/p/chromium/issues/detail?id=363288#c3
// Nested delete/forwardDelete not allowed on execCommand("cut")
// This is ugly but not sure how to work around it otherwise
Delay.setEditorTimeout(editor, function() {
transactCustomDelete(true);
});
}
});
}
/**
* Makes sure that the editor body becomes empty when backspace or delete is pressed in empty editors.
*
* For example:
* <p><b>|</b></p>
*
* Or:
* <h1>|</h1>
*
* Or:
* [<h1></h1>]
*/
function emptyEditorWhenDeleting() {
function serializeRng(rng) {
var body = dom.create("body");
var contents = rng.cloneContents();
body.appendChild(contents);
return selection.serializer.serialize(body, {format: 'html'});
}
function allContentsSelected(rng) {
if (!rng.setStart) {
if (rng.item) {
return false;
}
var bodyRng = rng.duplicate();
bodyRng.moveToElementText(editor.getBody());
return RangeUtils.compareRanges(rng, bodyRng);
}
var selection = serializeRng(rng);
var allRng = dom.createRng();
allRng.selectNode(editor.getBody());
var allSelection = serializeRng(allRng);
return selection === allSelection;
}
editor.on('keydown', function(e) {
var keyCode = e.keyCode, isCollapsed, body;
// Empty the editor if it's needed for example backspace at <p><b>|</b></p>
if (!isDefaultPrevented(e) && (keyCode == DELETE || keyCode == BACKSPACE)) {
isCollapsed = editor.selection.isCollapsed();
body = editor.getBody();
// Selection is collapsed but the editor isn't empty
if (isCollapsed && !dom.isEmpty(body)) {
return;
}
// Selection isn't collapsed but not all the contents is selected
if (!isCollapsed && !allContentsSelected(editor.selection.getRng())) {
return;
}
// Manually empty the editor
e.preventDefault();
editor.setContent('');
if (body.firstChild && dom.isBlock(body.firstChild)) {
editor.selection.setCursorLocation(body.firstChild, 0);
} else {
editor.selection.setCursorLocation(body, 0);
}
editor.nodeChanged();
}
});
}
/**
* WebKit doesn't select all the nodes in the body when you press Ctrl+A.
* IE selects more than the contents <body>[<p>a</p>]</body> instead of <body><p>[a]</p]</body> see bug #6438
* This selects the whole body so that backspace/delete logic will delete everything
*/
function selectAll() {
editor.shortcuts.add('meta+a', null, 'SelectAll');
}
/**
* WebKit has a weird issue where it some times fails to properly convert keypresses to input method keystrokes.
* The IME on Mac doesn't initialize when it doesn't fire a proper focus event.
*
* This seems to happen when the user manages to click the documentElement element then the window doesn't get proper focus until
* you enter a character into the editor.
*
* It also happens when the first focus in made to the body.
*
* See: https://bugs.webkit.org/show_bug.cgi?id=83566
*/
function inputMethodFocus() {
if (!editor.settings.content_editable) {
// Case 1 IME doesn't initialize if you focus the document
// Disabled since it was interferring with the cE=false logic
// Also coultn't reproduce the issue on Safari 9
/*dom.bind(editor.getDoc(), 'focusin', function() {
selection.setRng(selection.getRng());
});*/
// Case 2 IME doesn't initialize if you click the documentElement it also doesn't properly fire the focusin event
// Needs to be both down/up due to weird rendering bug on Chrome Windows
dom.bind(editor.getDoc(), 'mousedown mouseup', function(e) {
var rng;
if (e.target == editor.getDoc().documentElement) {
rng = selection.getRng();
editor.getBody().focus();
if (e.type == 'mousedown') {
if (CaretContainer.isCaretContainer(rng.startContainer)) {
return;
}
// Edge case for mousedown, drag select and mousedown again within selection on Chrome Windows to render caret
selection.placeCaretAt(e.clientX, e.clientY);
} else {
selection.setRng(rng);
}
}
});
}
}
/**
* Backspacing in FireFox/IE from a paragraph into a horizontal rule results in a floating text node because the
* browser just deletes the paragraph - the browser fails to merge the text node with a horizontal rule so it is
* left there. TinyMCE sees a floating text node and wraps it in a paragraph on the key up event (ForceBlocks.js
* addRootBlocks), meaning the action does nothing. With this code, FireFox/IE matche the behaviour of other
* browsers.
*
* It also fixes a bug on Firefox where it's impossible to delete HR elements.
*/
function removeHrOnBackspace() {
editor.on('keydown', function(e) {
if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) {
// Check if there is any HR elements this is faster since getRng on IE 7 & 8 is slow
if (!editor.getBody().getElementsByTagName('hr').length) {
return;
}
if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) {
var node = selection.getNode();
var previousSibling = node.previousSibling;
if (node.nodeName == 'HR') {
dom.remove(node);
e.preventDefault();
return;
}
if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "hr") {
dom.remove(previousSibling);
e.preventDefault();
}
}
}
});
}
/**
* Firefox 3.x has an issue where the body element won't get proper focus if you click out
* side it's rectangle.
*/
function focusBody() {
// Fix for a focus bug in FF 3.x where the body element
// wouldn't get proper focus if the user clicked on the HTML element
if (!window.Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4
editor.on('mousedown', function(e) {
if (!isDefaultPrevented(e) && e.target.nodeName === "HTML") {
var body = editor.getBody();
// Blur the body it's focused but not correctly focused
body.blur();
// Refocus the body after a little while
Delay.setEditorTimeout(editor, function() {
body.focus();
});
}
});
}
}
/**
* WebKit has a bug where it isn't possible to select image, hr or anchor elements
* by clicking on them so we need to fake that.
*/
function selectControlElements() {
editor.on('click', function(e) {
var target = e.target;
// Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250
// WebKit can't even do simple things like selecting an image
// Needs to be the setBaseAndExtend or it will fail to select floated images
if (/^(IMG|HR)$/.test(target.nodeName) && dom.getContentEditableParent(target) !== "false") {
e.preventDefault();
selection.getSel().setBaseAndExtent(target, 0, target, 1);
editor.nodeChanged();
}
if (target.nodeName == 'A' && dom.hasClass(target, 'mce-item-anchor')) {
e.preventDefault();
selection.select(target);
}
});
}
/**
* Fixes a Gecko bug where the style attribute gets added to the wrong element when deleting between two block elements.
*
* Fixes do backspace/delete on this:
* <p>bla[ck</p><p style="color:red">r]ed</p>
*
* Would become:
* <p>bla|ed</p>
*
* Instead of:
* <p style="color:red">bla|ed</p>
*/
function removeStylesWhenDeletingAcrossBlockElements() {
function getAttributeApplyFunction() {
var template = dom.getAttribs(selection.getStart().cloneNode(false));
return function() {
var target = selection.getStart();
if (target !== editor.getBody()) {
dom.setAttrib(target, "style", null);
each(template, function(attr) {
target.setAttributeNode(attr.cloneNode(true));
});
}
};
}
function isSelectionAcrossElements() {
return !selection.isCollapsed() &&
dom.getParent(selection.getStart(), dom.isBlock) != dom.getParent(selection.getEnd(), dom.isBlock);
}
editor.on('keypress', function(e) {
var applyAttributes;
if (!isDefaultPrevented(e) && (e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) {
applyAttributes = getAttributeApplyFunction();
editor.getDoc().execCommand('delete', false, null);
applyAttributes();
e.preventDefault();
return false;
}
});
dom.bind(editor.getDoc(), 'cut', function(e) {
var applyAttributes;
if (!isDefaultPrevented(e) && isSelectionAcrossElements()) {
applyAttributes = getAttributeApplyFunction();
Delay.setEditorTimeout(editor, function() {
applyAttributes();
});
}
});
}
/**
* Screen readers on IE needs to have the role application set on the body.
*/
function ensureBodyHasRoleApplication() {
document.body.setAttribute("role", "application");
}
/**
* Backspacing into a table behaves differently depending upon browser type.
* Therefore, disable Backspace when cursor immediately follows a table.
*/
function disableBackspaceIntoATable() {
editor.on('keydown', function(e) {
if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) {
if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) {
var previousSibling = selection.getNode().previousSibling;
if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "table") {
e.preventDefault();
return false;
}
}
}
});
}
/**
* Old IE versions can't properly render BR elements in PRE tags white in contentEditable mode. So this
* logic adds a \n before the BR so that it will get rendered.
*/
function addNewLinesBeforeBrInPre() {
// IE8+ rendering mode does the right thing with BR in PRE
if (getDocumentMode() > 7) {
return;
}
// Enable display: none in area and add a specific class that hides all BR elements in PRE to
// avoid the caret from getting stuck at the BR elements while pressing the right arrow key
setEditorCommandState('RespectVisibilityInDesign', true);
editor.contentStyles.push('.mceHideBrInPre pre br {display: none}');
dom.addClass(editor.getBody(), 'mceHideBrInPre');
// Adds a \n before all BR elements in PRE to get them visual
parser.addNodeFilter('pre', function(nodes) {
var i = nodes.length, brNodes, j, brElm, sibling;
while (i--) {
brNodes = nodes[i].getAll('br');
j = brNodes.length;
while (j--) {
brElm = brNodes[j];
// Add \n before BR in PRE elements on older IE:s so the new lines get rendered
sibling = brElm.prev;
if (sibling && sibling.type === 3 && sibling.value.charAt(sibling.value - 1) != '\n') {
sibling.value += '\n';
} else {
brElm.parent.insert(new Node('#text', 3), brElm, true).value = '\n';
}
}
}
});
// Removes any \n before BR elements in PRE since other browsers and in contentEditable=false mode they will be visible
serializer.addNodeFilter('pre', function(nodes) {
var i = nodes.length, brNodes, j, brElm, sibling;
while (i--) {
brNodes = nodes[i].getAll('br');
j = brNodes.length;
while (j--) {
brElm = brNodes[j];
sibling = brElm.prev;
if (sibling && sibling.type == 3) {
sibling.value = sibling.value.replace(/\r?\n$/, '');
}
}
}
});
}
/**
* Moves style width/height to attribute width/height when the user resizes an image on IE.
*/
function removePreSerializedStylesWhenSelectingControls() {
dom.bind(editor.getBody(), 'mouseup', function() {
var value, node = selection.getNode();
// Moved styles to attributes on IMG eements
if (node.nodeName == 'IMG') {
// Convert style width to width attribute
if ((value = dom.getStyle(node, 'width'))) {
dom.setAttrib(node, 'width', value.replace(/[^0-9%]+/g, ''));
dom.setStyle(node, 'width', '');
}
// Convert style height to height attribute
if ((value = dom.getStyle(node, 'height'))) {
dom.setAttrib(node, 'height', value.replace(/[^0-9%]+/g, ''));
dom.setStyle(node, 'height', '');
}
}
});
}
/**
* Removes a blockquote when backspace is pressed at the beginning of it.
*
* For example:
* <blockquote><p>|x</p></blockquote>
*
* Becomes:
* <p>|x</p>
*/
function removeBlockQuoteOnBackSpace() {
// Add block quote deletion handler
editor.on('keydown', function(e) {
var rng, container, offset, root, parent;
if (isDefaultPrevented(e) || e.keyCode != VK.BACKSPACE) {
return;
}
rng = selection.getRng();
container = rng.startContainer;
offset = rng.startOffset;
root = dom.getRoot();
parent = container;
if (!rng.collapsed || offset !== 0) {
return;
}
while (parent && parent.parentNode && parent.parentNode.firstChild == parent && parent.parentNode != root) {
parent = parent.parentNode;
}
// Is the cursor at the beginning of a blockquote?
if (parent.tagName === 'BLOCKQUOTE') {
// Remove the blockquote
editor.formatter.toggle('blockquote', null, parent);
// Move the caret to the beginning of container
rng = dom.createRng();
rng.setStart(container, 0);
rng.setEnd(container, 0);
selection.setRng(rng);
}
});
}
/**
* Sets various Gecko editing options on mouse down and before a execCommand to disable inline table editing that is broken etc.
*/
function setGeckoEditingOptions() {
function setOpts() {
refreshContentEditable();
setEditorCommandState("StyleWithCSS", false);
setEditorCommandState("enableInlineTableEditing", false);
if (!settings.object_resizing) {
setEditorCommandState("enableObjectResizing", false);
}
}
if (!settings.readonly) {
editor.on('BeforeExecCommand MouseDown', setOpts);
}
}
/**
* Fixes a gecko link bug, when a link is placed at the end of block elements there is
* no way to move the caret behind the link. This fix adds a bogus br element after the link.
*
* For example this:
* <p><b><a href="#">x</a></b></p>
*
* Becomes this:
* <p><b><a href="#">x</a></b><br></p>
*/
function addBrAfterLastLinks() {
function fixLinks() {
each(dom.select('a'), function(node) {
var parentNode = node.parentNode, root = dom.getRoot();
if (parentNode.lastChild === node) {
while (parentNode && !dom.isBlock(parentNode)) {
if (parentNode.parentNode.lastChild !== parentNode || parentNode === root) {
return;
}
parentNode = parentNode.parentNode;
}
dom.add(parentNode, 'br', {'data-mce-bogus': 1});
}
});
}
editor.on('SetContent ExecCommand', function(e) {
if (e.type == "setcontent" || e.command === 'mceInsertLink') {
fixLinks();
}
});
}
/**
* WebKit will produce DIV elements here and there by default. But since TinyMCE uses paragraphs by
* default we want to change that behavior.
*/
function setDefaultBlockType() {
if (settings.forced_root_block) {
editor.on('init', function() {
setEditorCommandState('DefaultParagraphSeparator', settings.forced_root_block);
});
}
}
/**
* Deletes the selected image on IE instead of navigating to previous page.
*/
function deleteControlItemOnBackSpace() {
editor.on('keydown', function(e) {
var rng;
if (!isDefaultPrevented(e) && e.keyCode == BACKSPACE) {
rng = editor.getDoc().selection.createRange();
if (rng && rng.item) {
e.preventDefault();
editor.undoManager.beforeChange();
dom.remove(rng.item(0));
editor.undoManager.add();
}
}
});
}
/**
* IE10 doesn't properly render block elements with the right height until you add contents to them.
* This fixes that by adding a padding-right to all empty text block elements.
* See: https://connect.microsoft.com/IE/feedback/details/743881
*/
function renderEmptyBlocksFix() {
var emptyBlocksCSS;
// IE10+
if (getDocumentMode() >= 10) {
emptyBlocksCSS = '';
each('p div h1 h2 h3 h4 h5 h6'.split(' '), function(name, i) {
emptyBlocksCSS += (i > 0 ? ',' : '') + name + ':empty';
});
editor.contentStyles.push(emptyBlocksCSS + '{padding-right: 1px !important}');
}
}
/**
* Old IE versions can't retain contents within noscript elements so this logic will store the contents
* as a attribute and the insert that value as it's raw text when the DOM is serialized.
*/
function keepNoScriptContents() {
if (getDocumentMode() < 9) {
parser.addNodeFilter('noscript', function(nodes) {
var i = nodes.length, node, textNode;
while (i--) {
node = nodes[i];
textNode = node.firstChild;
if (textNode) {
node.attr('data-mce-innertext', textNode.value);
}
}
});
serializer.addNodeFilter('noscript', function(nodes) {
var i = nodes.length, node, textNode, value;
while (i--) {
node = nodes[i];
textNode = nodes[i].firstChild;
if (textNode) {
textNode.value = Entities.decode(textNode.value);
} else {
// Old IE can't retain noscript value so an attribute is used to store it
value = node.attributes.map['data-mce-innertext'];
if (value) {
node.attr('data-mce-innertext', null);
textNode = new Node('#text', 3);
textNode.value = value;
textNode.raw = true;
node.append(textNode);
}
}
}
});
}
}
/**
* IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode.
*/
function fixCaretSelectionOfDocumentElementOnIe() {
var doc = dom.doc, body = doc.body, started, startRng, htmlElm;
// Return range from point or null if it failed
function rngFromPoint(x, y) {
var rng = body.createTextRange();
try {
rng.moveToPoint(x, y);
} catch (ex) {
// IE sometimes throws and exception, so lets just ignore it
rng = null;
}
return rng;
}
// Fires while the selection is changing
function selectionChange(e) {
var pointRng;
// Check if the button is down or not
if (e.button) {
// Create range from mouse position
pointRng = rngFromPoint(e.x, e.y);
if (pointRng) {
// Check if pointRange is before/after selection then change the endPoint
if (pointRng.compareEndPoints('StartToStart', startRng) > 0) {
pointRng.setEndPoint('StartToStart', startRng);
} else {
pointRng.setEndPoint('EndToEnd', startRng);
}
pointRng.select();
}
} else {
endSelection();
}
}
// Removes listeners
function endSelection() {
var rng = doc.selection.createRange();
// If the range is collapsed then use the last start range
if (startRng && !rng.item && rng.compareEndPoints('StartToEnd', rng) === 0) {
startRng.select();
}
dom.unbind(doc, 'mouseup', endSelection);
dom.unbind(doc, 'mousemove', selectionChange);
startRng = started = 0;
}
// Make HTML element unselectable since we are going to handle selection by hand
doc.documentElement.unselectable = true;
// Detect when user selects outside BODY
dom.bind(doc, 'mousedown contextmenu', function(e) {
if (e.target.nodeName === 'HTML') {
if (started) {
endSelection();
}
// Detect vertical scrollbar, since IE will fire a mousedown on the scrollbar and have target set as HTML
htmlElm = doc.documentElement;
if (htmlElm.scrollHeight > htmlElm.clientHeight) {
return;
}
started = 1;
// Setup start position
startRng = rngFromPoint(e.x, e.y);
if (startRng) {
// Listen for selection change events
dom.bind(doc, 'mouseup', endSelection);
dom.bind(doc, 'mousemove', selectionChange);
dom.getRoot().focus();
startRng.select();
}
}
});
}
/**
* Fixes selection issues where the caret can be placed between two inline elements like <b>a</b>|<b>b</b>
* this fix will lean the caret right into the closest inline element.
*/
function normalizeSelection() {
// Normalize selection for example <b>a</b><i>|a</i> becomes <b>a|</b><i>a</i> except for Ctrl+A since it selects everything
editor.on('keyup focusin mouseup', function(e) {
if (e.keyCode != 65 || !VK.metaKeyPressed(e)) {
selection.normalize();
}
}, true);
}
/**
* Forces Gecko to render a broken image icon if it fails to load an image.
*/
function showBrokenImageIcon() {
editor.contentStyles.push(
'img:-moz-broken {' +
'-moz-force-broken-image-icon:1;' +
'min-width:24px;' +
'min-height:24px' +
'}'
);
}
/**
* iOS has a bug where it's impossible to type if the document has a touchstart event
* bound and the user touches the document while having the on screen keyboard visible.
*
* The touch event moves the focus to the parent document while having the caret inside the iframe
* this fix moves the focus back into the iframe document.
*/
function restoreFocusOnKeyDown() {
if (!editor.inline) {
editor.on('keydown', function() {
if (document.activeElement == document.body) {
editor.getWin().focus();
}
});
}
}
/**
* IE 11 has an annoying issue where you can't move focus into the editor
* by clicking on the white area HTML element. We used to be able to to fix this with
* the fixCaretSelectionOfDocumentElementOnIe fix. But since M$ removed the selection
* object it's not possible anymore. So we need to hack in a ungly CSS to force the
* body to be at least 150px. If the user clicks the HTML element out side this 150px region
* we simply move the focus into the first paragraph. Not ideal since you loose the
* positioning of the caret but goot enough for most cases.
*/
function bodyHeight() {
if (!editor.inline) {
editor.contentStyles.push('body {min-height: 150px}');
editor.on('click', function(e) {
var rng;
if (e.target.nodeName == 'HTML') {
// Edge seems to only need focus if we set the range
// the caret will become invisible and moved out of the iframe!!
if (Env.ie > 11) {
editor.getBody().focus();
return;
}
// Need to store away non collapsed ranges since the focus call will mess that up see #7382
rng = editor.selection.getRng();
editor.getBody().focus();
editor.selection.setRng(rng);
editor.selection.normalize();
editor.nodeChanged();
}
});
}
}
/**
* Firefox on Mac OS will move the browser back to the previous page if you press CMD+Left arrow.
* You might then loose all your work so we need to block that behavior and replace it with our own.
*/
function blockCmdArrowNavigation() {
if (Env.mac) {
editor.on('keydown', function(e) {
if (VK.metaKeyPressed(e) && !e.shiftKey && (e.keyCode == 37 || e.keyCode == 39)) {
e.preventDefault();
editor.selection.getSel().modify('move', e.keyCode == 37 ? 'backward' : 'forward', 'lineboundary');
}
});
}
}
/**
* Disables the autolinking in IE 9+ this is then re-enabled by the autolink plugin.
*/
function disableAutoUrlDetect() {
setEditorCommandState("AutoUrlDetect", false);
}
/**
* iOS 7.1 introduced two new bugs:
* 1) It's possible to open links within a contentEditable area by clicking on them.
* 2) If you hold down the finger it will display the link/image touch callout menu.
*/
function tapLinksAndImages() {
editor.on('click', function(e) {
var elm = e.target;
do {
if (elm.tagName === 'A') {
e.preventDefault();
return;
}
} while ((elm = elm.parentNode));
});
editor.contentStyles.push('.mce-content-body {-webkit-touch-callout: none}');
}
/**
* iOS Safari and possible other browsers have a bug where it won't fire
* a click event when a contentEditable is focused. This function fakes click events
* by using touchstart/touchend and measuring the time and distance travelled.
*/
/*
function touchClickEvent() {
editor.on('touchstart', function(e) {
var elm, time, startTouch, changedTouches;
elm = e.target;
time = new Date().getTime();
changedTouches = e.changedTouches;
if (!changedTouches || changedTouches.length > 1) {
return;
}
startTouch = changedTouches[0];
editor.once('touchend', function(e) {
var endTouch = e.changedTouches[0], args;
if (new Date().getTime() - time > 500) {
return;
}
if (Math.abs(startTouch.clientX - endTouch.clientX) > 5) {
return;
}
if (Math.abs(startTouch.clientY - endTouch.clientY) > 5) {
return;
}
args = {
target: elm
};
each('pageX pageY clientX clientY screenX screenY'.split(' '), function(key) {
args[key] = endTouch[key];
});
args = editor.fire('click', args);
if (!args.isDefaultPrevented()) {
// iOS WebKit can't place the caret properly once
// you bind touch events so we need to do this manually
// TODO: Expand to the closest word? Touble tap still works.
editor.selection.placeCaretAt(endTouch.clientX, endTouch.clientY);
editor.nodeChanged();
}
});
});
}
*/
/**
* WebKit has a bug where it will allow forms to be submitted if they are inside a contentEditable element.
* For example this: <form><button></form>
*/
function blockFormSubmitInsideEditor() {
editor.on('init', function() {
editor.dom.bind(editor.getBody(), 'submit', function(e) {
e.preventDefault();
});
});
}
/**
* Sometimes WebKit/Blink generates BR elements with the Apple-interchange-newline class.
*
* Scenario:
* 1) Create a table 2x2.
* 2) Select and copy cells A2-B2.
* 3) Paste and it will add BR element to table cell.
*/
function removeAppleInterchangeBrs() {
parser.addNodeFilter('br', function(nodes) {
var i = nodes.length;
while (i--) {
if (nodes[i].attr('class') == 'Apple-interchange-newline') {
nodes[i].remove();
}
}
});
}
/**
* IE cannot set custom contentType's on drag events, and also does not properly drag/drop between
* editors. This uses a special data:text/mce-internal URL to pass data when drag/drop between editors.
*/
function ieInternalDragAndDrop() {
editor.on('dragstart', function(e) {
setMceInternalContent(e);
});
editor.on('drop', function(e) {
if (!isDefaultPrevented(e)) {
var internalContent = getMceInternalContent(e);
if (internalContent && internalContent.id != editor.id) {
e.preventDefault();
var rng = RangeUtils.getCaretRangeFromPoint(e.x, e.y, editor.getDoc());
selection.setRng(rng);
insertClipboardContents(internalContent.html);
}
}
});
}
function refreshContentEditable() {
// No-op since Mozilla seems to have fixed the caret repaint issues
}
function isHidden() {
var sel;
if (!isGecko) {
return 0;
}
// Weird, wheres that cursor selection?
sel = editor.selection.getSel();
return (!sel || !sel.rangeCount || sel.rangeCount === 0);
}
/**
* Properly empties the editor if all contents is selected and deleted this to
* prevent empty paragraphs from being produced at beginning/end of contents.
*/
function emptyEditorOnDeleteEverything() {
function isEverythingSelected(editor) {
var caretWalker = new CaretWalker(editor.getBody());
var rng = editor.selection.getRng();
var startCaretPos = CaretPosition.fromRangeStart(rng);
var endCaretPos = CaretPosition.fromRangeEnd(rng);
return !editor.selection.isCollapsed() && !caretWalker.prev(startCaretPos) && !caretWalker.next(endCaretPos);
}
// Type over case delete and insert this won't cover typeover with a IME but at least it covers the common case
editor.on('keypress', function (e) {
if (!isDefaultPrevented(e) && !selection.isCollapsed() && e.charCode > 31 && !VK.metaKeyPressed(e)) {
if (isEverythingSelected(editor)) {
e.preventDefault();
editor.setContent(String.fromCharCode(e.charCode));
editor.selection.select(editor.getBody(), true);
editor.selection.collapse(false);
editor.nodeChanged();
}
}
});
editor.on('keydown', function (e) {
var keyCode = e.keyCode;
if (!isDefaultPrevented(e) && (keyCode == DELETE || keyCode == BACKSPACE)) {
if (isEverythingSelected(editor)) {
e.preventDefault();
editor.setContent('');
editor.nodeChanged();
}
}
});
}
// All browsers
removeBlockQuoteOnBackSpace();
emptyEditorWhenDeleting();
// Windows phone will return a range like [body, 0] on mousedown so
// it will always normalize to the wrong location
if (!Env.windowsPhone) {
normalizeSelection();
}
// WebKit
if (isWebKit) {
emptyEditorOnDeleteEverything();
cleanupStylesWhenDeleting();
inputMethodFocus();
selectControlElements();
setDefaultBlockType();
blockFormSubmitInsideEditor();
disableBackspaceIntoATable();
removeAppleInterchangeBrs();
//touchClickEvent();
// iOS
if (Env.iOS) {
restoreFocusOnKeyDown();
bodyHeight();
tapLinksAndImages();
} else {
selectAll();
}
}
// IE
if (isIE && Env.ie < 11) {
removeHrOnBackspace();
ensureBodyHasRoleApplication();
addNewLinesBeforeBrInPre();
removePreSerializedStylesWhenSelectingControls();
deleteControlItemOnBackSpace();
renderEmptyBlocksFix();
keepNoScriptContents();
fixCaretSelectionOfDocumentElementOnIe();
}
if (Env.ie >= 11) {
bodyHeight();
disableBackspaceIntoATable();
}
if (Env.ie) {
selectAll();
disableAutoUrlDetect();
ieInternalDragAndDrop();
}
// Gecko
if (isGecko) {
emptyEditorOnDeleteEverything();
removeHrOnBackspace();
focusBody();
removeStylesWhenDeletingAcrossBlockElements();
setGeckoEditingOptions();
addBrAfterLastLinks();
showBrokenImageIcon();
blockCmdArrowNavigation();
disableBackspaceIntoATable();
}
return {
refreshContentEditable: refreshContentEditable,
isHidden: isHidden
};
};
});
// Included from: js/tinymce/classes/EditorObservable.js
/**
* EditorObservable.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This mixin contains the event logic for the tinymce.Editor class.
*
* @mixin tinymce.EditorObservable
* @extends tinymce.util.Observable
*/
define("tinymce/EditorObservable", [
"tinymce/util/Observable",
"tinymce/dom/DOMUtils",
"tinymce/util/Tools"
], function(Observable, DOMUtils, Tools) {
var DOM = DOMUtils.DOM, customEventRootDelegates;
/**
* Returns the event target so for the specified event. Some events fire
* only on document, some fire on documentElement etc. This also handles the
* custom event root setting where it returns that element instead of the body.
*
* @private
* @param {tinymce.Editor} editor Editor instance to get event target from.
* @param {String} eventName Name of the event for example "click".
* @return {Element/Document} HTML Element or document target to bind on.
*/
function getEventTarget(editor, eventName) {
if (eventName == 'selectionchange') {
return editor.getDoc();
}
// Need to bind mousedown/mouseup etc to document not body in iframe mode
// Since the user might click on the HTML element not the BODY
if (!editor.inline && /^mouse|touch|click|contextmenu|drop|dragover|dragend/.test(eventName)) {
return editor.getDoc().documentElement;
}
// Bind to event root instead of body if it's defined
if (editor.settings.event_root) {
if (!editor.eventRoot) {
editor.eventRoot = DOM.select(editor.settings.event_root)[0];
}
return editor.eventRoot;
}
return editor.getBody();
}
/**
* Binds a event delegate for the specified name this delegate will fire
* the event to the editor dispatcher.
*
* @private
* @param {tinymce.Editor} editor Editor instance to get event target from.
* @param {String} eventName Name of the event for example "click".
*/
function bindEventDelegate(editor, eventName) {
var eventRootElm = getEventTarget(editor, eventName), delegate;
function isListening(editor) {
return !editor.hidden && !editor.readonly;
}
if (!editor.delegates) {
editor.delegates = {};
}
if (editor.delegates[eventName]) {
return;
}
if (editor.settings.event_root) {
if (!customEventRootDelegates) {
customEventRootDelegates = {};
editor.editorManager.on('removeEditor', function() {
var name;
if (!editor.editorManager.activeEditor) {
if (customEventRootDelegates) {
for (name in customEventRootDelegates) {
editor.dom.unbind(getEventTarget(editor, name));
}
customEventRootDelegates = null;
}
}
});
}
if (customEventRootDelegates[eventName]) {
return;
}
delegate = function(e) {
var target = e.target, editors = editor.editorManager.editors, i = editors.length;
while (i--) {
var body = editors[i].getBody();
if (body === target || DOM.isChildOf(target, body)) {
if (isListening(editors[i])) {
editors[i].fire(eventName, e);
}
}
}
};
customEventRootDelegates[eventName] = delegate;
DOM.bind(eventRootElm, eventName, delegate);
} else {
delegate = function(e) {
if (isListening(editor)) {
editor.fire(eventName, e);
}
};
DOM.bind(eventRootElm, eventName, delegate);
editor.delegates[eventName] = delegate;
}
}
var EditorObservable = {
/**
* Bind any pending event delegates. This gets executed after the target body/document is created.
*
* @private
*/
bindPendingEventDelegates: function() {
var self = this;
Tools.each(self._pendingNativeEvents, function(name) {
bindEventDelegate(self, name);
});
},
/**
* Toggles a native event on/off this is called by the EventDispatcher when
* the first native event handler is added and when the last native event handler is removed.
*
* @private
*/
toggleNativeEvent: function(name, state) {
var self = this;
// Never bind focus/blur since the FocusManager fakes those
if (name == "focus" || name == "blur") {
return;
}
if (state) {
if (self.initialized) {
bindEventDelegate(self, name);
} else {
if (!self._pendingNativeEvents) {
self._pendingNativeEvents = [name];
} else {
self._pendingNativeEvents.push(name);
}
}
} else if (self.initialized) {
self.dom.unbind(getEventTarget(self, name), name, self.delegates[name]);
delete self.delegates[name];
}
},
/**
* Unbinds all native event handlers that means delegates, custom events bound using the Events API etc.
*
* @private
*/
unbindAllNativeEvents: function() {
var self = this, name;
if (self.delegates) {
for (name in self.delegates) {
self.dom.unbind(getEventTarget(self, name), name, self.delegates[name]);
}
delete self.delegates;
}
if (!self.inline) {
self.getBody().onload = null;
self.dom.unbind(self.getWin());
self.dom.unbind(self.getDoc());
}
self.dom.unbind(self.getBody());
self.dom.unbind(self.getContainer());
}
};
EditorObservable = Tools.extend({}, Observable, EditorObservable);
return EditorObservable;
});
// Included from: js/tinymce/classes/Mode.js
/**
* Mode.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Mode switcher logic.
*
* @private
* @class tinymce.Mode
*/
define("tinymce/Mode", [], function() {
function setEditorCommandState(editor, cmd, state) {
try {
editor.getDoc().execCommand(cmd, false, state);
} catch (ex) {
// Ignore
}
}
function clickBlocker(editor) {
var target, handler;
target = editor.getBody();
handler = function(e) {
if (editor.dom.getParents(e.target, 'a').length > 0) {
e.preventDefault();
}
};
editor.dom.bind(target, 'click', handler);
return {
unbind: function() {
editor.dom.unbind(target, 'click', handler);
}
};
}
function toggleReadOnly(editor, state) {
if (editor._clickBlocker) {
editor._clickBlocker.unbind();
editor._clickBlocker = null;
}
if (state) {
editor._clickBlocker = clickBlocker(editor);
editor.selection.controlSelection.hideResizeRect();
editor.readonly = true;
editor.getBody().contentEditable = false;
} else {
editor.readonly = false;
editor.getBody().contentEditable = true;
setEditorCommandState(editor, "StyleWithCSS", false);
setEditorCommandState(editor, "enableInlineTableEditing", false);
setEditorCommandState(editor, "enableObjectResizing", false);
editor.focus();
editor.nodeChanged();
}
}
function setMode(editor, mode) {
var currentMode = editor.readonly ? 'readonly' : 'design';
if (mode == currentMode) {
return;
}
if (editor.initialized) {
toggleReadOnly(editor, mode == 'readonly');
} else {
editor.on('init', function() {
toggleReadOnly(editor, mode == 'readonly');
});
}
// Event is NOT preventable
editor.fire('SwitchMode', {mode: mode});
}
return {
setMode: setMode
};
});
// Included from: js/tinymce/classes/Shortcuts.js
/**
* Shortcuts.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Contains all logic for handling of keyboard shortcuts.
*
* @class tinymce.Shortcuts
* @example
* editor.shortcuts.add('ctrl+a', function() {});
* editor.shortcuts.add('meta+a', function() {}); // "meta" maps to Command on Mac and Ctrl on PC
* editor.shortcuts.add('ctrl+alt+a', function() {});
* editor.shortcuts.add('access+a', function() {}); // "access" maps to ctrl+alt on Mac and shift+alt on PC
*/
define("tinymce/Shortcuts", [
"tinymce/util/Tools",
"tinymce/Env"
], function(Tools, Env) {
var each = Tools.each, explode = Tools.explode;
var keyCodeLookup = {
"f9": 120,
"f10": 121,
"f11": 122
};
var modifierNames = Tools.makeMap('alt,ctrl,shift,meta,access');
return function(editor) {
var self = this, shortcuts = {}, pendingPatterns = [];
function parseShortcut(pattern) {
var id, key, shortcut = {};
// Parse modifiers and keys ctrl+alt+b for example
each(explode(pattern, '+'), function(value) {
if (value in modifierNames) {
shortcut[value] = true;
} else {
// Allow numeric keycodes like ctrl+219 for ctrl+[
if (/^[0-9]{2,}$/.test(value)) {
shortcut.keyCode = parseInt(value, 10);
} else {
shortcut.charCode = value.charCodeAt(0);
shortcut.keyCode = keyCodeLookup[value] || value.toUpperCase().charCodeAt(0);
}
}
});
// Generate unique id for modifier combination and set default state for unused modifiers
id = [shortcut.keyCode];
for (key in modifierNames) {
if (shortcut[key]) {
id.push(key);
} else {
shortcut[key] = false;
}
}
shortcut.id = id.join(',');
// Handle special access modifier differently depending on Mac/Win
if (shortcut.access) {
shortcut.alt = true;
if (Env.mac) {
shortcut.ctrl = true;
} else {
shortcut.shift = true;
}
}
// Handle special meta modifier differently depending on Mac/Win
if (shortcut.meta) {
if (Env.mac) {
shortcut.meta = true;
} else {
shortcut.ctrl = true;
shortcut.meta = false;
}
}
return shortcut;
}
function createShortcut(pattern, desc, cmdFunc, scope) {
var shortcuts;
shortcuts = Tools.map(explode(pattern, '>'), parseShortcut);
shortcuts[shortcuts.length - 1] = Tools.extend(shortcuts[shortcuts.length - 1], {
func: cmdFunc,
scope: scope || editor
});
return Tools.extend(shortcuts[0], {
desc: editor.translate(desc),
subpatterns: shortcuts.slice(1)
});
}
function hasModifier(e) {
return e.altKey || e.ctrlKey || e.metaKey;
}
function isFunctionKey(e) {
return e.type === "keydown" && e.keyCode >= 112 && e.keyCode <= 123;
}
function matchShortcut(e, shortcut) {
if (!shortcut) {
return false;
}
if (shortcut.ctrl != e.ctrlKey || shortcut.meta != e.metaKey) {
return false;
}
if (shortcut.alt != e.altKey || shortcut.shift != e.shiftKey) {
return false;
}
if (e.keyCode == shortcut.keyCode || (e.charCode && e.charCode == shortcut.charCode)) {
e.preventDefault();
return true;
}
return false;
}
function executeShortcutAction(shortcut) {
return shortcut.func ? shortcut.func.call(shortcut.scope) : null;
}
editor.on('keyup keypress keydown', function(e) {
if ((hasModifier(e) || isFunctionKey(e)) && !e.isDefaultPrevented()) {
each(shortcuts, function(shortcut) {
if (matchShortcut(e, shortcut)) {
pendingPatterns = shortcut.subpatterns.slice(0);
if (e.type == "keydown") {
executeShortcutAction(shortcut);
}
return true;
}
});
if (matchShortcut(e, pendingPatterns[0])) {
if (pendingPatterns.length === 1) {
if (e.type == "keydown") {
executeShortcutAction(pendingPatterns[0]);
}
}
pendingPatterns.shift();
}
}
});
/**
* Adds a keyboard shortcut for some command or function.
*
* @method add
* @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o.
* @param {String} desc Text description for the command.
* @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed.
* @param {Object} scope Optional scope to execute the function in.
* @return {Boolean} true/false state if the shortcut was added or not.
*/
self.add = function(pattern, desc, cmdFunc, scope) {
var cmd;
cmd = cmdFunc;
if (typeof cmdFunc === 'string') {
cmdFunc = function() {
editor.execCommand(cmd, false, null);
};
} else if (Tools.isArray(cmd)) {
cmdFunc = function() {
editor.execCommand(cmd[0], cmd[1], cmd[2]);
};
}
each(explode(Tools.trim(pattern.toLowerCase())), function(pattern) {
var shortcut = createShortcut(pattern, desc, cmdFunc, scope);
shortcuts[shortcut.id] = shortcut;
});
return true;
};
/**
* Remove a keyboard shortcut by pattern.
*
* @method remove
* @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o.
* @return {Boolean} true/false state if the shortcut was removed or not.
*/
self.remove = function(pattern) {
var shortcut = createShortcut(pattern);
if (shortcuts[shortcut.id]) {
delete shortcuts[shortcut.id];
return true;
}
return false;
};
};
});
// Included from: js/tinymce/classes/file/Uploader.js
/**
* Uploader.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Upload blobs or blob infos to the specified URL or handler.
*
* @private
* @class tinymce.file.Uploader
* @example
* var uploader = new Uploader({
* url: '/upload.php',
* basePath: '/base/path',
* credentials: true,
* handler: function(data, success, failure) {
* ...
* }
* });
*
* uploader.upload(blobInfos).then(function(result) {
* ...
* });
*/
define("tinymce/file/Uploader", [
"tinymce/util/Promise",
"tinymce/util/Tools",
"tinymce/util/Fun"
], function(Promise, Tools, Fun) {
return function(uploadStatus, settings) {
var pendingPromises = {};
function filename(blobInfo) {
var ext, extensions;
extensions = {
'image/jpeg': 'jpg',
'image/jpg': 'jpg',
'image/gif': 'gif',
'image/png': 'png'
};
ext = extensions[blobInfo.blob().type.toLowerCase()] || 'dat';
return blobInfo.filename() + '.' + ext;
}
function pathJoin(path1, path2) {
if (path1) {
return path1.replace(/\/$/, '') + '/' + path2.replace(/^\//, '');
}
return path2;
}
function blobInfoToData(blobInfo) {
return {
id: blobInfo.id,
blob: blobInfo.blob,
base64: blobInfo.base64,
filename: Fun.constant(filename(blobInfo))
};
}
function defaultHandler(blobInfo, success, failure, progress) {
var xhr, formData;
xhr = new XMLHttpRequest();
xhr.open('POST', settings.url);
xhr.withCredentials = settings.credentials;
xhr.upload.onprogress = function(e) {
progress(e.loaded / e.total * 100);
};
xhr.onerror = function() {
failure("Image upload failed due to a XHR Transport error. Code: " + xhr.status);
};
xhr.onload = function() {
var json;
if (xhr.status != 200) {
failure("HTTP Error: " + xhr.status);
return;
}
json = JSON.parse(xhr.responseText);
if (!json || typeof json.location != "string") {
failure("Invalid JSON: " + xhr.responseText);
return;
}
success(pathJoin(settings.basePath, json.location));
};
formData = new FormData();
formData.append('file', blobInfo.blob(), blobInfo.filename());
xhr.send(formData);
}
function noUpload() {
return new Promise(function(resolve) {
resolve([]);
});
}
function handlerSuccess(blobInfo, url) {
return {
url: url,
blobInfo: blobInfo,
status: true
};
}
function handlerFailure(blobInfo, error) {
return {
url: '',
blobInfo: blobInfo,
status: false,
error: error
};
}
function resolvePending(blobUri, result) {
Tools.each(pendingPromises[blobUri], function(resolve) {
resolve(result);
});
delete pendingPromises[blobUri];
}
function uploadBlobInfo(blobInfo, handler, openNotification) {
uploadStatus.markPending(blobInfo.blobUri());
return new Promise(function(resolve) {
var notification, progress;
var noop = function() {
};
try {
var closeNotification = function() {
if (notification) {
notification.close();
progress = noop; // Once it's closed it's closed
}
};
var success = function(url) {
closeNotification();
uploadStatus.markUploaded(blobInfo.blobUri(), url);
resolvePending(blobInfo.blobUri(), handlerSuccess(blobInfo, url));
resolve(handlerSuccess(blobInfo, url));
};
var failure = function() {
closeNotification();
uploadStatus.removeFailed(blobInfo.blobUri());
resolvePending(blobInfo.blobUri(), handlerFailure(blobInfo, failure));
resolve(handlerFailure(blobInfo, failure));
};
progress = function(percent) {
if (percent < 0 || percent > 100) {
return;
}
if (!notification) {
notification = openNotification();
}
notification.progressBar.value(percent);
};
handler(blobInfoToData(blobInfo), success, failure, progress);
} catch (ex) {
resolve(handlerFailure(blobInfo, ex.message));
}
});
}
function isDefaultHandler(handler) {
return handler === defaultHandler;
}
function pendingUploadBlobInfo(blobInfo) {
var blobUri = blobInfo.blobUri();
return new Promise(function(resolve) {
pendingPromises[blobUri] = pendingPromises[blobUri] || [];
pendingPromises[blobUri].push(resolve);
});
}
function uploadBlobs(blobInfos, openNotification) {
blobInfos = Tools.grep(blobInfos, function(blobInfo) {
return !uploadStatus.isUploaded(blobInfo.blobUri());
});
return Promise.all(Tools.map(blobInfos, function(blobInfo) {
return uploadStatus.isPending(blobInfo.blobUri()) ?
pendingUploadBlobInfo(blobInfo) : uploadBlobInfo(blobInfo, settings.handler, openNotification);
}));
}
function upload(blobInfos, openNotification) {
return (!settings.url && isDefaultHandler(settings.handler)) ? noUpload() : uploadBlobs(blobInfos, openNotification);
}
settings = Tools.extend({
credentials: false,
// We are adding a notify argument to this (at the moment, until it doesn't work)
handler: defaultHandler
}, settings);
return {
upload: upload
};
};
});
// Included from: js/tinymce/classes/file/Conversions.js
/**
* Conversions.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Converts blob/uris back and forth.
*
* @private
* @class tinymce.file.Conversions
*/
define("tinymce/file/Conversions", [
"tinymce/util/Promise"
], function(Promise) {
function blobUriToBlob(url) {
return new Promise(function(resolve) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'blob';
xhr.onload = function() {
if (this.status == 200) {
resolve(this.response);
}
};
xhr.send();
});
}
function parseDataUri(uri) {
var type, matches;
uri = decodeURIComponent(uri).split(',');
matches = /data:([^;]+)/.exec(uri[0]);
if (matches) {
type = matches[1];
}
return {
type: type,
data: uri[1]
};
}
function dataUriToBlob(uri) {
return new Promise(function(resolve) {
var str, arr, i;
uri = parseDataUri(uri);
// Might throw error if data isn't proper base64
try {
str = atob(uri.data);
} catch (e) {
resolve(new Blob([]));
return;
}
arr = new Uint8Array(str.length);
for (i = 0; i < arr.length; i++) {
arr[i] = str.charCodeAt(i);
}
resolve(new Blob([arr], {type: uri.type}));
});
}
function uriToBlob(url) {
if (url.indexOf('blob:') === 0) {
return blobUriToBlob(url);
}
if (url.indexOf('data:') === 0) {
return dataUriToBlob(url);
}
return null;
}
function blobToDataUri(blob) {
return new Promise(function(resolve) {
var reader = new FileReader();
reader.onloadend = function() {
resolve(reader.result);
};
reader.readAsDataURL(blob);
});
}
return {
uriToBlob: uriToBlob,
blobToDataUri: blobToDataUri,
parseDataUri: parseDataUri
};
});
// Included from: js/tinymce/classes/file/ImageScanner.js
/**
* ImageScanner.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Finds images with data uris or blob uris. If data uris are found it will convert them into blob uris.
*
* @private
* @class tinymce.file.ImageScanner
*/
define("tinymce/file/ImageScanner", [
"tinymce/util/Promise",
"tinymce/util/Arr",
"tinymce/util/Fun",
"tinymce/file/Conversions",
"tinymce/Env"
], function(Promise, Arr, Fun, Conversions, Env) {
var count = 0;
return function(uploadStatus, blobCache) {
var cachedPromises = {};
function findAll(elm, predicate) {
var images, promises;
function imageToBlobInfo(img, resolve) {
var base64, blobInfo;
if (img.src.indexOf('blob:') === 0) {
blobInfo = blobCache.getByUri(img.src);
if (blobInfo) {
resolve({
image: img,
blobInfo: blobInfo
});
}
return;
}
base64 = Conversions.parseDataUri(img.src).data;
blobInfo = blobCache.findFirst(function(cachedBlobInfo) {
return cachedBlobInfo.base64() === base64;
});
if (blobInfo) {
resolve({
image: img,
blobInfo: blobInfo
});
} else {
Conversions.uriToBlob(img.src).then(function(blob) {
var blobInfoId = 'blobid' + (count++),
blobInfo = blobCache.create(blobInfoId, blob, base64);
blobCache.add(blobInfo);
resolve({
image: img,
blobInfo: blobInfo
});
});
}
}
if (!predicate) {
predicate = Fun.constant(true);
}
images = Arr.filter(elm.getElementsByTagName('img'), function(img) {
var src = img.src;
if (!Env.fileApi) {
return false;
}
if (img.hasAttribute('data-mce-bogus')) {
return false;
}
if (img.hasAttribute('data-mce-placeholder')) {
return false;
}
if (!src || src == Env.transparentSrc) {
return false;
}
if (src.indexOf('blob:') === 0) {
return !uploadStatus.isUploaded(src);
}
if (src.indexOf('data:') === 0) {
return predicate(img);
}
return false;
});
promises = Arr.map(images, function(img) {
var newPromise;
if (cachedPromises[img.src]) {
// Since the cached promise will return the cached image
// We need to wrap it and resolve with the actual image
return new Promise(function(resolve) {
cachedPromises[img.src].then(function(imageInfo) {
resolve({
image: img,
blobInfo: imageInfo.blobInfo
});
});
});
}
newPromise = new Promise(function(resolve) {
imageToBlobInfo(img, resolve);
}).then(function(result) {
delete cachedPromises[result.image.src];
return result;
})['catch'](function(error) {
delete cachedPromises[img.src];
return error;
});
cachedPromises[img.src] = newPromise;
return newPromise;
});
return Promise.all(promises);
}
return {
findAll: findAll
};
};
});
// Included from: js/tinymce/classes/file/BlobCache.js
/**
* BlobCache.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Hold blob info objects where a blob has extra internal information.
*
* @private
* @class tinymce.file.BlobCache
*/
define("tinymce/file/BlobCache", [
"tinymce/util/Arr",
"tinymce/util/Fun"
], function(Arr, Fun) {
return function() {
var cache = [], constant = Fun.constant;
function create(id, blob, base64, filename) {
return {
id: constant(id),
filename: constant(filename || id),
blob: constant(blob),
base64: constant(base64),
blobUri: constant(URL.createObjectURL(blob))
};
}
function add(blobInfo) {
if (!get(blobInfo.id())) {
cache.push(blobInfo);
}
}
function get(id) {
return findFirst(function(cachedBlobInfo) {
return cachedBlobInfo.id() === id;
});
}
function findFirst(predicate) {
return Arr.filter(cache, predicate)[0];
}
function getByUri(blobUri) {
return findFirst(function(blobInfo) {
return blobInfo.blobUri() == blobUri;
});
}
function removeByUri(blobUri) {
cache = Arr.filter(cache, function(blobInfo) {
if (blobInfo.blobUri() === blobUri) {
URL.revokeObjectURL(blobInfo.blobUri());
return false;
}
return true;
});
}
function destroy() {
Arr.each(cache, function(cachedBlobInfo) {
URL.revokeObjectURL(cachedBlobInfo.blobUri());
});
cache = [];
}
return {
create: create,
add: add,
get: get,
getByUri: getByUri,
findFirst: findFirst,
removeByUri: removeByUri,
destroy: destroy
};
};
});
// Included from: js/tinymce/classes/file/UploadStatus.js
/**
* UploadStatus.js
*
* Released under LGPL License.
* Copyright (c) 1999-2016 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Holds the current status of a blob uri, if it's pending or uploaded and what the result urls was.
*
* @private
* @class tinymce.file.UploadStatus
*/
define("tinymce/file/UploadStatus", [
], function() {
return function() {
var PENDING = 1, UPLOADED = 2;
var blobUriStatuses = {};
function createStatus(status, resultUri) {
return {
status: status,
resultUri: resultUri
};
}
function hasBlobUri(blobUri) {
return blobUri in blobUriStatuses;
}
function getResultUri(blobUri) {
var result = blobUriStatuses[blobUri];
return result ? result.resultUri : null;
}
function isPending(blobUri) {
return hasBlobUri(blobUri) ? blobUriStatuses[blobUri].status === PENDING : false;
}
function isUploaded(blobUri) {
return hasBlobUri(blobUri) ? blobUriStatuses[blobUri].status === UPLOADED : false;
}
function markPending(blobUri) {
blobUriStatuses[blobUri] = createStatus(PENDING, null);
}
function markUploaded(blobUri, resultUri) {
blobUriStatuses[blobUri] = createStatus(UPLOADED, resultUri);
}
function removeFailed(blobUri) {
delete blobUriStatuses[blobUri];
}
function destroy() {
blobUriStatuses = {};
}
return {
hasBlobUri: hasBlobUri,
getResultUri: getResultUri,
isPending: isPending,
isUploaded: isUploaded,
markPending: markPending,
markUploaded: markUploaded,
removeFailed: removeFailed,
destroy: destroy
};
};
});
// Included from: js/tinymce/classes/EditorUpload.js
/**
* EditorUpload.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Handles image uploads, updates undo stack and patches over various internal functions.
*
* @private
* @class tinymce.EditorUpload
*/
define("tinymce/EditorUpload", [
"tinymce/util/Arr",
"tinymce/file/Uploader",
"tinymce/file/ImageScanner",
"tinymce/file/BlobCache",
"tinymce/file/UploadStatus"
], function(Arr, Uploader, ImageScanner, BlobCache, UploadStatus) {
return function(editor) {
var blobCache = new BlobCache(), uploader, imageScanner, settings = editor.settings;
var uploadStatus = new UploadStatus();
function aliveGuard(callback) {
return function(result) {
if (editor.selection) {
return callback(result);
}
return [];
};
}
function cacheInvalidator() {
return '?' + (new Date()).getTime();
}
// Replaces strings without regexps to avoid FF regexp to big issue
function replaceString(content, search, replace) {
var index = 0;
do {
index = content.indexOf(search, index);
if (index !== -1) {
content = content.substring(0, index) + replace + content.substr(index + search.length);
index += replace.length - search.length + 1;
}
} while (index !== -1);
return content;
}
function replaceImageUrl(content, targetUrl, replacementUrl) {
content = replaceString(content, 'src="' + targetUrl + '"', 'src="' + replacementUrl + '"');
content = replaceString(content, 'data-mce-src="' + targetUrl + '"', 'data-mce-src="' + replacementUrl + '"');
return content;
}
function replaceUrlInUndoStack(targetUrl, replacementUrl) {
Arr.each(editor.undoManager.data, function(level) {
if (level.type === 'fragmented') {
level.fragments = Arr.map(level.fragments, function (fragment) {
return replaceImageUrl(fragment, targetUrl, replacementUrl);
});
} else {
level.content = replaceImageUrl(level.content, targetUrl, replacementUrl);
}
});
}
function openNotification() {
return editor.notificationManager.open({
text: editor.translate('Image uploading...'),
type: 'info',
timeout: -1,
progressBar: true
});
}
function replaceImageUri(image, resultUri) {
blobCache.removeByUri(image.src);
replaceUrlInUndoStack(image.src, resultUri);
editor.$(image).attr({
src: settings.images_reuse_filename ? resultUri + cacheInvalidator() : resultUri,
'data-mce-src': editor.convertURL(resultUri, 'src')
});
}
function uploadImages(callback) {
if (!uploader) {
uploader = new Uploader(uploadStatus, {
url: settings.images_upload_url,
basePath: settings.images_upload_base_path,
credentials: settings.images_upload_credentials,
handler: settings.images_upload_handler
});
}
return scanForImages().then(aliveGuard(function(imageInfos) {
var blobInfos;
blobInfos = Arr.map(imageInfos, function(imageInfo) {
return imageInfo.blobInfo;
});
return uploader.upload(blobInfos, openNotification).then(aliveGuard(function(result) {
result = Arr.map(result, function(uploadInfo, index) {
var image = imageInfos[index].image;
if (uploadInfo.status && editor.settings.images_replace_blob_uris !== false) {
replaceImageUri(image, uploadInfo.url);
}
return {
element: image,
status: uploadInfo.status
};
});
if (callback) {
callback(result);
}
return result;
}));
}));
}
function uploadImagesAuto(callback) {
if (settings.automatic_uploads !== false) {
return uploadImages(callback);
}
}
function isValidDataUriImage(imgElm) {
return settings.images_dataimg_filter ? settings.images_dataimg_filter(imgElm) : true;
}
function scanForImages() {
if (!imageScanner) {
imageScanner = new ImageScanner(uploadStatus, blobCache);
}
return imageScanner.findAll(editor.getBody(), isValidDataUriImage).then(aliveGuard(function(result) {
Arr.each(result, function(resultItem) {
replaceUrlInUndoStack(resultItem.image.src, resultItem.blobInfo.blobUri());
resultItem.image.src = resultItem.blobInfo.blobUri();
resultItem.image.removeAttribute('data-mce-src');
});
return result;
}));
}
function destroy() {
blobCache.destroy();
uploadStatus.destroy();
imageScanner = uploader = null;
}
function replaceBlobUris(content) {
return content.replace(/src="(blob:[^"]+)"/g, function(match, blobUri) {
var resultUri = uploadStatus.getResultUri(blobUri);
if (resultUri) {
return 'src="' + resultUri + '"';
}
var blobInfo = blobCache.getByUri(blobUri);
if (!blobInfo) {
blobInfo = Arr.reduce(editor.editorManager.editors, function(result, editor) {
return result || editor.editorUpload.blobCache.getByUri(blobUri);
}, null);
}
if (blobInfo) {
return 'src="data:' + blobInfo.blob().type + ';base64,' + blobInfo.base64() + '"';
}
return match;
});
}
editor.on('setContent', function() {
if (editor.settings.automatic_uploads !== false) {
uploadImagesAuto();
} else {
scanForImages();
}
});
editor.on('RawSaveContent', function(e) {
e.content = replaceBlobUris(e.content);
});
editor.on('getContent', function(e) {
if (e.source_view || e.format == 'raw') {
return;
}
e.content = replaceBlobUris(e.content);
});
editor.on('PostRender', function() {
editor.parser.addNodeFilter('img', function(images) {
Arr.each(images, function(img) {
var src = img.attr('src');
if (blobCache.getByUri(src)) {
return;
}
var resultUri = uploadStatus.getResultUri(src);
if (resultUri) {
img.attr('src', resultUri);
}
});
});
});
return {
blobCache: blobCache,
uploadImages: uploadImages,
uploadImagesAuto: uploadImagesAuto,
scanForImages: scanForImages,
destroy: destroy
};
};
});
// Included from: js/tinymce/classes/caret/FakeCaret.js
/**
* FakeCaret.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This module contains logic for rendering a fake visual caret.
*
* @private
* @class tinymce.caret.FakeCaret
*/
define("tinymce/caret/FakeCaret", [
"tinymce/caret/CaretContainer",
"tinymce/caret/CaretPosition",
"tinymce/dom/NodeType",
"tinymce/dom/RangeUtils",
"tinymce/dom/DomQuery",
"tinymce/geom/ClientRect",
"tinymce/util/Delay"
], function(CaretContainer, CaretPosition, NodeType, RangeUtils, $, ClientRect, Delay) {
var isContentEditableFalse = NodeType.isContentEditableFalse;
return function(rootNode, isBlock) {
var cursorInterval, $lastVisualCaret, caretContainerNode;
function getAbsoluteClientRect(node, before) {
var clientRect = ClientRect.collapse(node.getBoundingClientRect(), before),
docElm, scrollX, scrollY, margin, rootRect;
if (rootNode.tagName == 'BODY') {
docElm = rootNode.ownerDocument.documentElement;
scrollX = rootNode.scrollLeft || docElm.scrollLeft;
scrollY = rootNode.scrollTop || docElm.scrollTop;
} else {
rootRect = rootNode.getBoundingClientRect();
scrollX = rootNode.scrollLeft - rootRect.left;
scrollY = rootNode.scrollTop - rootRect.top;
}
clientRect.left += scrollX;
clientRect.right += scrollX;
clientRect.top += scrollY;
clientRect.bottom += scrollY;
clientRect.width = 1;
margin = node.offsetWidth - node.clientWidth;
if (margin > 0) {
if (before) {
margin *= -1;
}
clientRect.left += margin;
clientRect.right += margin;
}
return clientRect;
}
function trimInlineCaretContainers() {
var contentEditableFalseNodes, node, sibling, i, data;
contentEditableFalseNodes = $('*[contentEditable=false]', rootNode);
for (i = 0; i < contentEditableFalseNodes.length; i++) {
node = contentEditableFalseNodes[i];
sibling = node.previousSibling;
if (CaretContainer.endsWithCaretContainer(sibling)) {
data = sibling.data;
if (data.length == 1) {
sibling.parentNode.removeChild(sibling);
} else {
sibling.deleteData(data.length - 1, 1);
}
}
sibling = node.nextSibling;
if (CaretContainer.startsWithCaretContainer(sibling)) {
data = sibling.data;
if (data.length == 1) {
sibling.parentNode.removeChild(sibling);
} else {
sibling.deleteData(0, 1);
}
}
}
return null;
}
function show(before, node) {
var clientRect, rng;
hide();
if (isBlock(node)) {
caretContainerNode = CaretContainer.insertBlock('p', node, before);
clientRect = getAbsoluteClientRect(node, before);
$(caretContainerNode).css('top', clientRect.top);
$lastVisualCaret = $('<div class="mce-visual-caret" data-mce-bogus="all"></div>').css(clientRect).appendTo(rootNode);
if (before) {
$lastVisualCaret.addClass('mce-visual-caret-before');
}
startBlink();
rng = node.ownerDocument.createRange();
rng.setStart(caretContainerNode, 0);
rng.setEnd(caretContainerNode, 0);
} else {
caretContainerNode = CaretContainer.insertInline(node, before);
rng = node.ownerDocument.createRange();
if (isContentEditableFalse(caretContainerNode.nextSibling)) {
rng.setStart(caretContainerNode, 0);
rng.setEnd(caretContainerNode, 0);
} else {
rng.setStart(caretContainerNode, 1);
rng.setEnd(caretContainerNode, 1);
}
return rng;
}
return rng;
}
function hide() {
trimInlineCaretContainers();
if (caretContainerNode) {
CaretContainer.remove(caretContainerNode);
caretContainerNode = null;
}
if ($lastVisualCaret) {
$lastVisualCaret.remove();
$lastVisualCaret = null;
}
clearInterval(cursorInterval);
}
function startBlink() {
cursorInterval = Delay.setInterval(function() {
$('div.mce-visual-caret', rootNode).toggleClass('mce-visual-caret-hidden');
}, 500);
}
function destroy() {
Delay.clearInterval(cursorInterval);
}
function getCss() {
return (
'.mce-visual-caret {' +
'position: absolute;' +
'background-color: black;' +
'background-color: currentcolor;' +
'}' +
'.mce-visual-caret-hidden {' +
'display: none;' +
'}' +
'*[data-mce-caret] {' +
'position: absolute;' +
'left: -1000px;' +
'right: auto;' +
'top: 0;' +
'margin: 0;' +
'padding: 0;' +
'}'
);
}
return {
show: show,
hide: hide,
getCss: getCss,
destroy: destroy
};
};
});
// Included from: js/tinymce/classes/dom/Dimensions.js
/**
* Dimensions.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This module measures nodes and returns client rects. The client rects has an
* extra node property.
*
* @private
* @class tinymce.dom.Dimensions
*/
define("tinymce/dom/Dimensions", [
"tinymce/util/Arr",
"tinymce/dom/NodeType",
"tinymce/geom/ClientRect"
], function(Arr, NodeType, ClientRect) {
function getClientRects(node) {
function toArrayWithNode(clientRects) {
return Arr.map(clientRects, function(clientRect) {
clientRect = ClientRect.clone(clientRect);
clientRect.node = node;
return clientRect;
});
}
if (Arr.isArray(node)) {
return Arr.reduce(node, function(result, node) {
return result.concat(getClientRects(node));
}, []);
}
if (NodeType.isElement(node)) {
return toArrayWithNode(node.getClientRects());
}
if (NodeType.isText(node)) {
var rng = node.ownerDocument.createRange();
rng.setStart(node, 0);
rng.setEnd(node, node.data.length);
return toArrayWithNode(rng.getClientRects());
}
}
return {
/**
* Returns the client rects for a specific node.
*
* @method getClientRects
* @param {Array/DOMNode} node Node or array of nodes to get client rects on.
* @param {Array} Array of client rects with a extra node property.
*/
getClientRects: getClientRects
};
});
// Included from: js/tinymce/classes/caret/LineWalker.js
/**
* LineWalker.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This module lets you walk the document line by line
* returing nodes and client rects for each line.
*
* @private
* @class tinymce.caret.LineWalker
*/
define("tinymce/caret/LineWalker", [
"tinymce/util/Fun",
"tinymce/util/Arr",
"tinymce/dom/Dimensions",
"tinymce/caret/CaretCandidate",
"tinymce/caret/CaretUtils",
"tinymce/caret/CaretWalker",
"tinymce/caret/CaretPosition",
"tinymce/geom/ClientRect"
], function(Fun, Arr, Dimensions, CaretCandidate, CaretUtils, CaretWalker, CaretPosition, ClientRect) {
var curry = Fun.curry;
function findUntil(direction, rootNode, predicateFn, node) {
while ((node = CaretUtils.findNode(node, direction, CaretCandidate.isEditableCaretCandidate, rootNode))) {
if (predicateFn(node)) {
return;
}
}
}
function walkUntil(direction, isAboveFn, isBeflowFn, rootNode, predicateFn, caretPosition) {
var line = 0, node, result = [], targetClientRect;
function add(node) {
var i, clientRect, clientRects;
clientRects = Dimensions.getClientRects(node);
if (direction == -1) {
clientRects = clientRects.reverse();
}
for (i = 0; i < clientRects.length; i++) {
clientRect = clientRects[i];
if (isBeflowFn(clientRect, targetClientRect)) {
continue;
}
if (result.length > 0 && isAboveFn(clientRect, Arr.last(result))) {
line++;
}
clientRect.line = line;
if (predicateFn(clientRect)) {
return true;
}
result.push(clientRect);
}
}
targetClientRect = Arr.last(caretPosition.getClientRects());
if (!targetClientRect) {
return result;
}
node = caretPosition.getNode();
add(node);
findUntil(direction, rootNode, add, node);
return result;
}
function aboveLineNumber(lineNumber, clientRect) {
return clientRect.line > lineNumber;
}
function isLine(lineNumber, clientRect) {
return clientRect.line === lineNumber;
}
var upUntil = curry(walkUntil, -1, ClientRect.isAbove, ClientRect.isBelow);
var downUntil = curry(walkUntil, 1, ClientRect.isBelow, ClientRect.isAbove);
function positionsUntil(direction, rootNode, predicateFn, node) {
var caretWalker = new CaretWalker(rootNode), walkFn, isBelowFn, isAboveFn,
caretPosition, result = [], line = 0, clientRect, targetClientRect;
function getClientRect(caretPosition) {
if (direction == 1) {
return Arr.last(caretPosition.getClientRects());
}
return Arr.last(caretPosition.getClientRects());
}
if (direction == 1) {
walkFn = caretWalker.next;
isBelowFn = ClientRect.isBelow;
isAboveFn = ClientRect.isAbove;
caretPosition = CaretPosition.after(node);
} else {
walkFn = caretWalker.prev;
isBelowFn = ClientRect.isAbove;
isAboveFn = ClientRect.isBelow;
caretPosition = CaretPosition.before(node);
}
targetClientRect = getClientRect(caretPosition);
do {
if (!caretPosition.isVisible()) {
continue;
}
clientRect = getClientRect(caretPosition);
if (isAboveFn(clientRect, targetClientRect)) {
continue;
}
if (result.length > 0 && isBelowFn(clientRect, Arr.last(result))) {
line++;
}
clientRect = ClientRect.clone(clientRect);
clientRect.position = caretPosition;
clientRect.line = line;
if (predicateFn(clientRect)) {
return result;
}
result.push(clientRect);
} while ((caretPosition = walkFn(caretPosition)));
return result;
}
return {
upUntil: upUntil,
downUntil: downUntil,
/**
* Find client rects with line and caret position until the predicate returns true.
*
* @method positionsUntil
* @param {Number} direction Direction forward/backward 1/-1.
* @param {DOMNode} rootNode Root node to walk within.
* @param {function} predicateFn Gets the client rect as it's input.
* @param {DOMNode} node Node to start walking from.
* @return {Array} Array of client rects with line and position properties.
*/
positionsUntil: positionsUntil,
isAboveLine: curry(aboveLineNumber),
isLine: curry(isLine)
};
});
// Included from: js/tinymce/classes/caret/LineUtils.js
/**
* LineUtils.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Utility functions for working with lines.
*
* @private
* @class tinymce.caret.LineUtils
*/
define("tinymce/caret/LineUtils", [
"tinymce/util/Fun",
"tinymce/util/Arr",
"tinymce/dom/NodeType",
"tinymce/dom/Dimensions",
"tinymce/geom/ClientRect",
"tinymce/caret/CaretUtils",
"tinymce/caret/CaretCandidate"
], function(Fun, Arr, NodeType, Dimensions, ClientRect, CaretUtils, CaretCandidate) {
var isContentEditableFalse = NodeType.isContentEditableFalse,
findNode = CaretUtils.findNode,
curry = Fun.curry;
function distanceToRectLeft(clientRect, clientX) {
return Math.abs(clientRect.left - clientX);
}
function distanceToRectRight(clientRect, clientX) {
return Math.abs(clientRect.right - clientX);
}
function findClosestClientRect(clientRects, clientX) {
function isInside(clientX, clientRect) {
return clientX >= clientRect.left && clientX <= clientRect.right;
}
return Arr.reduce(clientRects, function(oldClientRect, clientRect) {
var oldDistance, newDistance;
oldDistance = Math.min(distanceToRectLeft(oldClientRect, clientX), distanceToRectRight(oldClientRect, clientX));
newDistance = Math.min(distanceToRectLeft(clientRect, clientX), distanceToRectRight(clientRect, clientX));
if (isInside(clientX, clientRect)) {
return clientRect;
}
if (isInside(clientX, oldClientRect)) {
return oldClientRect;
}
// cE=false has higher priority
if (newDistance == oldDistance && isContentEditableFalse(clientRect.node)) {
return clientRect;
}
if (newDistance < oldDistance) {
return clientRect;
}
return oldClientRect;
});
}
function walkUntil(direction, rootNode, predicateFn, node) {
while ((node = findNode(node, direction, CaretCandidate.isEditableCaretCandidate, rootNode))) {
if (predicateFn(node)) {
return;
}
}
}
function findLineNodeRects(rootNode, targetNodeRect) {
var clientRects = [];
function collect(checkPosFn, node) {
var lineRects;
lineRects = Arr.filter(Dimensions.getClientRects(node), function(clientRect) {
return !checkPosFn(clientRect, targetNodeRect);
});
clientRects = clientRects.concat(lineRects);
return lineRects.length === 0;
}
clientRects.push(targetNodeRect);
walkUntil(-1, rootNode, curry(collect, ClientRect.isAbove), targetNodeRect.node);
walkUntil(1, rootNode, curry(collect, ClientRect.isBelow), targetNodeRect.node);
return clientRects;
}
function getContentEditableFalseChildren(rootNode) {
return Arr.filter(Arr.toArray(rootNode.getElementsByTagName('*')), isContentEditableFalse);
}
function caretInfo(clientRect, clientX) {
return {
node: clientRect.node,
before: distanceToRectLeft(clientRect, clientX) < distanceToRectRight(clientRect, clientX)
};
}
function closestCaret(rootNode, clientX, clientY) {
var contentEditableFalseNodeRects, closestNodeRect;
contentEditableFalseNodeRects = Dimensions.getClientRects(getContentEditableFalseChildren(rootNode));
contentEditableFalseNodeRects = Arr.filter(contentEditableFalseNodeRects, function(clientRect) {
return clientY >= clientRect.top && clientY <= clientRect.bottom;
});
closestNodeRect = findClosestClientRect(contentEditableFalseNodeRects, clientX);
if (closestNodeRect) {
closestNodeRect = findClosestClientRect(findLineNodeRects(rootNode, closestNodeRect), clientX);
if (closestNodeRect && isContentEditableFalse(closestNodeRect.node)) {
return caretInfo(closestNodeRect, clientX);
}
}
return null;
}
return {
findClosestClientRect: findClosestClientRect,
findLineNodeRects: findLineNodeRects,
closestCaret: closestCaret
};
});
// Included from: js/tinymce/classes/dom/MousePosition.js
/**
* MousePosition.js
*
* Released under LGPL License.
* Copyright (c) 1999-2016 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This module calculates an absolute coordinate inside the editor body for both local and global mouse events.
*
* @private
* @class tinymce.dom.MousePosition
*/
define("tinymce/dom/MousePosition", [
], function() {
var getAbsolutePosition = function (elm) {
var doc, docElem, win, clientRect;
clientRect = elm.getBoundingClientRect();
doc = elm.ownerDocument;
docElem = doc.documentElement;
win = doc.defaultView;
return {
top: clientRect.top + win.pageYOffset - docElem.clientTop,
left: clientRect.left + win.pageXOffset - docElem.clientLeft
};
};
var getBodyPosition = function (editor) {
return editor.inline ? getAbsolutePosition(editor.getBody()) : {left: 0, top: 0};
};
var getScrollPosition = function (editor) {
var body = editor.getBody();
return editor.inline ? {left: body.scrollLeft, top: body.scrollTop} : {left: 0, top: 0};
};
var getBodyScroll = function (editor) {
var body = editor.getBody(), docElm = editor.getDoc().documentElement;
var inlineScroll = {left: body.scrollLeft, top: body.scrollTop};
var iframeScroll = {left: body.scrollLeft || docElm.scrollLeft, top: body.scrollTop || docElm.scrollTop};
return editor.inline ? inlineScroll : iframeScroll;
};
var getMousePosition = function (editor, event) {
if (event.target.ownerDocument !== editor.getDoc()) {
var iframePosition = getAbsolutePosition(editor.getContentAreaContainer());
var scrollPosition = getBodyScroll(editor);
return {
left: event.pageX - iframePosition.left + scrollPosition.left,
top: event.pageY - iframePosition.top + scrollPosition.top
};
}
return {
left: event.pageX,
top: event.pageY
};
};
var calculatePosition = function (bodyPosition, scrollPosition, mousePosition) {
return {
pageX: (mousePosition.left - bodyPosition.left) + scrollPosition.left,
pageY: (mousePosition.top - bodyPosition.top) + scrollPosition.top
};
};
var calc = function (editor, event) {
return calculatePosition(getBodyPosition(editor), getScrollPosition(editor), getMousePosition(editor, event));
};
return {
calc: calc
};
});
// Included from: js/tinymce/classes/DragDropOverrides.js
/**
* DragDropOverrides.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This module contains logic overriding the drag/drop logic of the editor.
*
* @private
* @class tinymce.DragDropOverrides
*/
define("tinymce/DragDropOverrides", [
"tinymce/dom/NodeType",
"tinymce/util/Arr",
"tinymce/util/Fun",
"tinymce/util/Delay",
"tinymce/dom/DOMUtils",
"tinymce/dom/MousePosition"
], function(
NodeType, Arr, Fun, Delay, DOMUtils, MousePosition
) {
var isContentEditableFalse = NodeType.isContentEditableFalse,
isContentEditableTrue = NodeType.isContentEditableTrue;
var isDraggable = function (elm) {
return isContentEditableFalse(elm);
};
var isValidDropTarget = function (editor, targetElement, dragElement) {
if (targetElement === dragElement || editor.dom.isChildOf(targetElement, dragElement)) {
return false;
}
if (isContentEditableFalse(targetElement)) {
return false;
}
return true;
};
var cloneElement = function (elm) {
var cloneElm = elm.cloneNode(true);
cloneElm.removeAttribute('data-mce-selected');
return cloneElm;
};
var createGhost = function (editor, elm, width, height) {
var clonedElm = elm.cloneNode(true);
editor.dom.setStyles(clonedElm, {width: width, height: height});
editor.dom.setAttrib(clonedElm, 'data-mce-selected', null);
var ghostElm = editor.dom.create('div', {
'class': 'mce-drag-container',
'data-mce-bogus': 'all',
unselectable: 'on',
contenteditable: 'false'
});
editor.dom.setStyles(ghostElm, {
position: 'absolute',
opacity: 0.5,
overflow: 'hidden',
border: 0,
padding: 0,
margin: 0,
width: width,
height: height
});
editor.dom.setStyles(clonedElm, {
margin: 0,
boxSizing: 'border-box'
});
ghostElm.appendChild(clonedElm);
return ghostElm;
};
var appendGhostToBody = function (ghostElm, bodyElm) {
if (ghostElm.parentNode !== bodyElm) {
bodyElm.appendChild(ghostElm);
}
};
var moveGhost = function (ghostElm, position, width, height, maxX, maxY) {
var overflowX = 0, overflowY = 0;
ghostElm.style.left = position.pageX + 'px';
ghostElm.style.top = position.pageY + 'px';
if (position.pageX + width > maxX) {
overflowX = (position.pageX + width) - maxX;
}
if (position.pageY + height > maxY) {
overflowY = (position.pageY + height) - maxY;
}
ghostElm.style.width = (width - overflowX) + 'px';
ghostElm.style.height = (height - overflowY) + 'px';
};
var removeElement = function (elm) {
if (elm && elm.parentNode) {
elm.parentNode.removeChild(elm);
}
};
var isLeftMouseButtonPressed = function (e) {
return e.button === 0;
};
var hasDraggableElement = function (state) {
return state.element;
};
var applyRelPos = function (state, position) {
return {
pageX: position.pageX - state.relX,
pageY: position.pageY + 5
};
};
var start = function (state, editor) {
return function (e) {
if (isLeftMouseButtonPressed(e)) {
var ceElm = Arr.find(editor.dom.getParents(e.target), Fun.or(isContentEditableFalse, isContentEditableTrue));
if (isDraggable(ceElm)) {
var elmPos = editor.dom.getPos(ceElm);
var bodyElm = editor.getBody();
var docElm = editor.getDoc().documentElement;
state.element = ceElm;
state.screenX = e.screenX;
state.screenY = e.screenY;
state.maxX = (editor.inline ? bodyElm.scrollWidth : docElm.offsetWidth) - 2;
state.maxY = (editor.inline ? bodyElm.scrollHeight : docElm.offsetHeight) - 2;
state.relX = e.pageX - elmPos.x;
state.relY = e.pageY - elmPos.y;
state.width = ceElm.offsetWidth;
state.height = ceElm.offsetHeight;
state.ghost = createGhost(editor, ceElm, state.width, state.height);
}
}
};
};
var move = function (state, editor) {
// Reduces laggy drag behavior on Gecko
var throttledPlaceCaretAt = Delay.throttle(function (clientX, clientY) {
editor._selectionOverrides.hideFakeCaret();
editor.selection.placeCaretAt(clientX, clientY);
}, 0);
return function (e) {
var movement = Math.max(Math.abs(e.screenX - state.screenX), Math.abs(e.screenY - state.screenY));
if (hasDraggableElement(state) && !state.dragging && movement > 10) {
var args = editor.fire('dragstart', {target: state.element});
if (args.isDefaultPrevented()) {
return;
}
state.dragging = true;
editor.focus();
}
if (state.dragging) {
var targetPos = applyRelPos(state, MousePosition.calc(editor, e));
appendGhostToBody(state.ghost, editor.getBody());
moveGhost(state.ghost, targetPos, state.width, state.height, state.maxX, state.maxY);
throttledPlaceCaretAt(e.clientX, e.clientY);
}
};
};
// Returns the raw element instead of the fake cE=false element
var getRawTarget = function (selection) {
var rng = selection.getSel().getRangeAt(0);
var startContainer = rng.startContainer;
return startContainer.nodeType === 3 ? startContainer.parentNode : startContainer;
};
var drop = function (state, editor) {
return function (e) {
if (state.dragging) {
if (isValidDropTarget(editor, getRawTarget(editor.selection), state.element)) {
var targetClone = cloneElement(state.element);
var args = editor.fire('drop', {
targetClone: targetClone,
clientX: e.clientX,
clientY: e.clientY
});
if (!args.isDefaultPrevented()) {
targetClone = args.targetClone;
editor.undoManager.transact(function() {
removeElement(state.element);
editor.insertContent(editor.dom.getOuterHTML(targetClone));
editor._selectionOverrides.hideFakeCaret();
});
}
}
}
removeDragState(state);
};
};
var stop = function (state, editor) {
return function () {
removeDragState(state);
if (state.dragging) {
editor.fire('dragend');
}
};
};
var removeDragState = function (state) {
state.dragging = false;
state.element = null;
removeElement(state.ghost);
};
var bindFakeDragEvents = function (editor) {
var state = {}, pageDom, dragStartHandler, dragHandler, dropHandler, dragEndHandler, rootDocument;
pageDom = DOMUtils.DOM;
rootDocument = document;
dragStartHandler = start(state, editor);
dragHandler = move(state, editor);
dropHandler = drop(state, editor);
dragEndHandler = stop(state, editor);
editor.on('mousedown', dragStartHandler);
editor.on('mousemove', dragHandler);
editor.on('mouseup', dropHandler);
pageDom.bind(rootDocument, 'mousemove', dragHandler);
pageDom.bind(rootDocument, 'mouseup', dragEndHandler);
editor.on('remove', function () {
pageDom.unbind(rootDocument, 'mousemove', dragHandler);
pageDom.unbind(rootDocument, 'mouseup', dragEndHandler);
});
};
var blockIeDrop = function (editor) {
editor.on('drop', function(e) {
// FF doesn't pass out clientX/clientY for drop since this is for IE we just use null instead
var realTarget = typeof e.clientX !== 'undefined' ? editor.getDoc().elementFromPoint(e.clientX, e.clientY) : null;
if (isContentEditableFalse(realTarget) || isContentEditableFalse(editor.dom.getContentEditableParent(realTarget))) {
e.preventDefault();
}
});
};
var init = function (editor) {
bindFakeDragEvents(editor);
blockIeDrop(editor);
};
return {
init: init
};
});
// Included from: js/tinymce/classes/SelectionOverrides.js
/**
* SelectionOverrides.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This module contains logic overriding the selection with keyboard/mouse
* around contentEditable=false regions.
*
* @example
* // Disable the default cE=false selection
* tinymce.activeEditor.on('ShowCaret BeforeObjectSelected', function(e) {
* e.preventDefault();
* });
*
* @private
* @class tinymce.SelectionOverrides
*/
define("tinymce/SelectionOverrides", [
"tinymce/Env",
"tinymce/caret/CaretWalker",
"tinymce/caret/CaretPosition",
"tinymce/caret/CaretContainer",
"tinymce/caret/CaretUtils",
"tinymce/caret/FakeCaret",
"tinymce/caret/LineWalker",
"tinymce/caret/LineUtils",
"tinymce/dom/NodeType",
"tinymce/dom/RangeUtils",
"tinymce/geom/ClientRect",
"tinymce/util/VK",
"tinymce/util/Fun",
"tinymce/util/Arr",
"tinymce/util/Delay",
"tinymce/DragDropOverrides"
], function(
Env, CaretWalker, CaretPosition, CaretContainer, CaretUtils, FakeCaret, LineWalker,
LineUtils, NodeType, RangeUtils, ClientRect, VK, Fun, Arr, Delay, DragDropOverrides
) {
var curry = Fun.curry,
isContentEditableTrue = NodeType.isContentEditableTrue,
isContentEditableFalse = NodeType.isContentEditableFalse,
isElement = NodeType.isElement,
isAfterContentEditableFalse = CaretUtils.isAfterContentEditableFalse,
isBeforeContentEditableFalse = CaretUtils.isBeforeContentEditableFalse,
getSelectedNode = RangeUtils.getSelectedNode;
function getVisualCaretPosition(walkFn, caretPosition) {
while ((caretPosition = walkFn(caretPosition))) {
if (caretPosition.isVisible()) {
return caretPosition;
}
}
return caretPosition;
}
function SelectionOverrides(editor) {
var rootNode = editor.getBody(), caretWalker = new CaretWalker(rootNode);
var getNextVisualCaretPosition = curry(getVisualCaretPosition, caretWalker.next);
var getPrevVisualCaretPosition = curry(getVisualCaretPosition, caretWalker.prev),
fakeCaret = new FakeCaret(editor.getBody(), isBlock),
realSelectionId = 'sel-' + editor.dom.uniqueId(),
selectedContentEditableNode, $ = editor.$;
function isFakeSelectionElement(elm) {
return editor.dom.hasClass(elm, 'mce-offscreen-selection');
}
function getRealSelectionElement() {
var container = editor.dom.get(realSelectionId);
return container ? container.getElementsByTagName('*')[0] : container;
}
function isBlock(node) {
return editor.dom.isBlock(node);
}
function setRange(range) {
//console.log('setRange', range);
if (range) {
editor.selection.setRng(range);
}
}
function getRange() {
return editor.selection.getRng();
}
function scrollIntoView(node, alignToTop) {
editor.selection.scrollIntoView(node, alignToTop);
}
function showCaret(direction, node, before) {
var e;
e = editor.fire('ShowCaret', {
target: node,
direction: direction,
before: before
});
if (e.isDefaultPrevented()) {
return null;
}
scrollIntoView(node, direction === -1);
return fakeCaret.show(before, node);
}
function selectNode(node) {
var e;
e = editor.fire('BeforeObjectSelected', {target: node});
if (e.isDefaultPrevented()) {
return null;
}
return getNodeRange(node);
}
function getNodeRange(node) {
var rng = node.ownerDocument.createRange();
rng.selectNode(node);
return rng;
}
function isMoveInsideSameBlock(fromCaretPosition, toCaretPosition) {
var inSameBlock = CaretUtils.isInSameBlock(fromCaretPosition, toCaretPosition);
// Handle bogus BR <p>abc|<br></p>
if (!inSameBlock && NodeType.isBr(fromCaretPosition.getNode())) {
return true;
}
return inSameBlock;
}
function getNormalizedRangeEndPoint(direction, range) {
range = CaretUtils.normalizeRange(direction, rootNode, range);
if (direction == -1) {
return CaretPosition.fromRangeStart(range);
}
return CaretPosition.fromRangeEnd(range);
}
function isRangeInCaretContainerBlock(range) {
return CaretContainer.isCaretContainerBlock(range.startContainer);
}
function moveToCeFalseHorizontally(direction, getNextPosFn, isBeforeContentEditableFalseFn, range) {
var node, caretPosition, peekCaretPosition, rangeIsInContainerBlock;
if (!range.collapsed) {
node = getSelectedNode(range);
if (isContentEditableFalse(node)) {
return showCaret(direction, node, direction == -1);
}
}
rangeIsInContainerBlock = isRangeInCaretContainerBlock(range);
caretPosition = getNormalizedRangeEndPoint(direction, range);
if (isBeforeContentEditableFalseFn(caretPosition)) {
return selectNode(caretPosition.getNode(direction == -1));
}
caretPosition = getNextPosFn(caretPosition);
if (!caretPosition) {
if (rangeIsInContainerBlock) {
return range;
}
return null;
}
if (isBeforeContentEditableFalseFn(caretPosition)) {
return showCaret(direction, caretPosition.getNode(direction == -1), direction == 1);
}
// Peek ahead for handling of ab|c<span cE=false> -> abc|<span cE=false>
peekCaretPosition = getNextPosFn(caretPosition);
if (isBeforeContentEditableFalseFn(peekCaretPosition)) {
if (isMoveInsideSameBlock(caretPosition, peekCaretPosition)) {
return showCaret(direction, peekCaretPosition.getNode(direction == -1), direction == 1);
}
}
if (rangeIsInContainerBlock) {
return renderRangeCaret(caretPosition.toRange());
}
return null;
}
function moveToCeFalseVertically(direction, walkerFn, range) {
var caretPosition, linePositions, nextLinePositions,
closestNextLineRect, caretClientRect, clientX,
dist1, dist2, contentEditableFalseNode;
contentEditableFalseNode = getSelectedNode(range);
caretPosition = getNormalizedRangeEndPoint(direction, range);
linePositions = walkerFn(rootNode, LineWalker.isAboveLine(1), caretPosition);
nextLinePositions = Arr.filter(linePositions, LineWalker.isLine(1));
caretClientRect = Arr.last(caretPosition.getClientRects());
if (isBeforeContentEditableFalse(caretPosition)) {
contentEditableFalseNode = caretPosition.getNode();
}
if (isAfterContentEditableFalse(caretPosition)) {
contentEditableFalseNode = caretPosition.getNode(true);
}
if (!caretClientRect) {
return null;
}
clientX = caretClientRect.left;
closestNextLineRect = LineUtils.findClosestClientRect(nextLinePositions, clientX);
if (closestNextLineRect) {
if (isContentEditableFalse(closestNextLineRect.node)) {
dist1 = Math.abs(clientX - closestNextLineRect.left);
dist2 = Math.abs(clientX - closestNextLineRect.right);
return showCaret(direction, closestNextLineRect.node, dist1 < dist2);
}
}
if (contentEditableFalseNode) {
var caretPositions = LineWalker.positionsUntil(direction, rootNode, LineWalker.isAboveLine(1), contentEditableFalseNode);
closestNextLineRect = LineUtils.findClosestClientRect(Arr.filter(caretPositions, LineWalker.isLine(1)), clientX);
if (closestNextLineRect) {
return renderRangeCaret(closestNextLineRect.position.toRange());
}
closestNextLineRect = Arr.last(Arr.filter(caretPositions, LineWalker.isLine(0)));
if (closestNextLineRect) {
return renderRangeCaret(closestNextLineRect.position.toRange());
}
}
}
function exitPreBlock(direction, range) {
var pre, caretPos, newBlock;
function createTextBlock() {
var textBlock = editor.dom.create(editor.settings.forced_root_block);
if (!Env.ie || Env.ie >= 11) {
textBlock.innerHTML = '<br data-mce-bogus="1">';
}
return textBlock;
}
if (range.collapsed && editor.settings.forced_root_block) {
pre = editor.dom.getParent(range.startContainer, 'PRE');
if (!pre) {
return;
}
if (direction == 1) {
caretPos = getNextVisualCaretPosition(CaretPosition.fromRangeStart(range));
} else {
caretPos = getPrevVisualCaretPosition(CaretPosition.fromRangeStart(range));
}
if (!caretPos) {
newBlock = createTextBlock();
if (direction == 1) {
editor.$(pre).after(newBlock);
} else {
editor.$(pre).before(newBlock);
}
editor.selection.select(newBlock, true);
editor.selection.collapse();
}
}
}
function moveH(direction, getNextPosFn, isBeforeContentEditableFalseFn, range) {
var newRange;
newRange = moveToCeFalseHorizontally(direction, getNextPosFn, isBeforeContentEditableFalseFn, range);
if (newRange) {
return newRange;
}
newRange = exitPreBlock(direction, range);
if (newRange) {
return newRange;
}
return null;
}
function moveV(direction, walkerFn, range) {
var newRange;
newRange = moveToCeFalseVertically(direction, walkerFn, range);
if (newRange) {
return newRange;
}
newRange = exitPreBlock(direction, range);
if (newRange) {
return newRange;
}
return null;
}
function getBlockCaretContainer() {
return $('*[data-mce-caret]')[0];
}
function showBlockCaretContainer(blockCaretContainer) {
if (blockCaretContainer.hasAttribute('data-mce-caret')) {
CaretContainer.showCaretContainerBlock(blockCaretContainer);
setRange(getRange()); // Removes control rect on IE
scrollIntoView(blockCaretContainer[0]);
}
}
function renderCaretAtRange(range) {
var caretPosition, ceRoot;
range = CaretUtils.normalizeRange(1, rootNode, range);
caretPosition = CaretPosition.fromRangeStart(range);
if (isContentEditableFalse(caretPosition.getNode())) {
return showCaret(1, caretPosition.getNode(), !caretPosition.isAtEnd());
}
if (isContentEditableFalse(caretPosition.getNode(true))) {
return showCaret(1, caretPosition.getNode(true), false);
}
// TODO: Should render caret before/after depending on where you click on the page forces after now
ceRoot = editor.dom.getParent(caretPosition.getNode(), Fun.or(isContentEditableFalse, isContentEditableTrue));
if (isContentEditableFalse(ceRoot)) {
return showCaret(1, ceRoot, false);
}
return null;
}
function renderRangeCaret(range) {
var caretRange;
if (!range || !range.collapsed) {
return range;
}
caretRange = renderCaretAtRange(range);
if (caretRange) {
return caretRange;
}
return range;
}
function deleteContentEditableNode(node) {
var nextCaretPosition, prevCaretPosition, prevCeFalseElm, nextElement;
if (!isContentEditableFalse(node)) {
return null;
}
if (isContentEditableFalse(node.previousSibling)) {
prevCeFalseElm = node.previousSibling;
}
prevCaretPosition = getPrevVisualCaretPosition(CaretPosition.before(node));
if (!prevCaretPosition) {
nextCaretPosition = getNextVisualCaretPosition(CaretPosition.after(node));
}
if (nextCaretPosition && isElement(nextCaretPosition.getNode())) {
nextElement = nextCaretPosition.getNode();
}
CaretContainer.remove(node.previousSibling);
CaretContainer.remove(node.nextSibling);
editor.dom.remove(node);
if (editor.dom.isEmpty(editor.getBody())) {
editor.setContent('');
editor.focus();
return;
}
if (prevCeFalseElm) {
return CaretPosition.after(prevCeFalseElm).toRange();
}
if (nextElement) {
return CaretPosition.before(nextElement).toRange();
}
if (prevCaretPosition) {
return prevCaretPosition.toRange();
}
if (nextCaretPosition) {
return nextCaretPosition.toRange();
}
return null;
}
function isTextBlock(node) {
var textBlocks = editor.schema.getTextBlockElements();
return node.nodeName in textBlocks;
}
function isEmpty(elm) {
return editor.dom.isEmpty(elm);
}
function mergeTextBlocks(direction, fromCaretPosition, toCaretPosition) {
var dom = editor.dom, fromBlock, toBlock, node, ceTarget;
fromBlock = dom.getParent(fromCaretPosition.getNode(), dom.isBlock);
toBlock = dom.getParent(toCaretPosition.getNode(), dom.isBlock);
if (direction === -1) {
ceTarget = toCaretPosition.getNode(true);
if (isAfterContentEditableFalse(toCaretPosition) && isBlock(ceTarget)) {
if (isTextBlock(fromBlock)) {
if (isEmpty(fromBlock)) {
dom.remove(fromBlock);
}
return CaretPosition.after(ceTarget).toRange();
}
return deleteContentEditableNode(toCaretPosition.getNode(true));
}
} else {
ceTarget = fromCaretPosition.getNode();
if (isBeforeContentEditableFalse(fromCaretPosition) && isBlock(ceTarget)) {
if (isTextBlock(toBlock)) {
if (isEmpty(toBlock)) {
dom.remove(toBlock);
}
return CaretPosition.before(ceTarget).toRange();
}
return deleteContentEditableNode(fromCaretPosition.getNode());
}
}
// Verify that both blocks are text blocks
if (fromBlock === toBlock || !isTextBlock(fromBlock) || !isTextBlock(toBlock)) {
return null;
}
while ((node = fromBlock.firstChild)) {
toBlock.appendChild(node);
}
editor.dom.remove(fromBlock);
return toCaretPosition.toRange();
}
function backspaceDelete(direction, beforeFn, afterFn, range) {
var node, caretPosition, peekCaretPosition, newCaretPosition;
if (!range.collapsed) {
node = getSelectedNode(range);
if (isContentEditableFalse(node)) {
return renderRangeCaret(deleteContentEditableNode(node));
}
}
caretPosition = getNormalizedRangeEndPoint(direction, range);
if (afterFn(caretPosition) && CaretContainer.isCaretContainerBlock(range.startContainer)) {
newCaretPosition = direction == -1 ? caretWalker.prev(caretPosition) : caretWalker.next(caretPosition);
return newCaretPosition ? renderRangeCaret(newCaretPosition.toRange()) : range;
}
if (beforeFn(caretPosition)) {
return renderRangeCaret(deleteContentEditableNode(caretPosition.getNode(direction == -1)));
}
peekCaretPosition = direction == -1 ? caretWalker.prev(caretPosition) : caretWalker.next(caretPosition);
if (beforeFn(peekCaretPosition)) {
if (direction === -1) {
return mergeTextBlocks(direction, caretPosition, peekCaretPosition);
}
return mergeTextBlocks(direction, peekCaretPosition, caretPosition);
}
}
function registerEvents() {
var right = curry(moveH, 1, getNextVisualCaretPosition, isBeforeContentEditableFalse);
var left = curry(moveH, -1, getPrevVisualCaretPosition, isAfterContentEditableFalse);
var deleteForward = curry(backspaceDelete, 1, isBeforeContentEditableFalse, isAfterContentEditableFalse);
var backspace = curry(backspaceDelete, -1, isAfterContentEditableFalse, isBeforeContentEditableFalse);
var up = curry(moveV, -1, LineWalker.upUntil);
var down = curry(moveV, 1, LineWalker.downUntil);
function override(evt, moveFn) {
var range = moveFn(getRange());
if (range && !evt.isDefaultPrevented()) {
evt.preventDefault();
setRange(range);
}
}
function getContentEditableRoot(node) {
var root = editor.getBody();
while (node && node != root) {
if (isContentEditableTrue(node) || isContentEditableFalse(node)) {
return node;
}
node = node.parentNode;
}
return null;
}
function isXYWithinRange(clientX, clientY, range) {
if (range.collapsed) {
return false;
}
return Arr.reduce(range.getClientRects(), function(state, rect) {
return state || ClientRect.containsXY(rect, clientX, clientY);
}, false);
}
// Some browsers (Chrome) lets you place the caret after a cE=false
// Make sure we render the caret container in this case
editor.on('mouseup', function() {
var range = getRange();
if (range.collapsed) {
setRange(renderCaretAtRange(range));
}
});
editor.on('click', function(e) {
var contentEditableRoot;
contentEditableRoot = getContentEditableRoot(e.target);
if (contentEditableRoot) {
// Prevent clicks on links in a cE=false element
if (isContentEditableFalse(contentEditableRoot)) {
e.preventDefault();
editor.focus();
}
// Removes fake selection if a cE=true is clicked within a cE=false like the toc title
if (isContentEditableTrue(contentEditableRoot)) {
if (editor.dom.isChildOf(contentEditableRoot, editor.selection.getNode())) {
removeContentEditableSelection();
}
}
}
});
editor.on('blur NewBlock', function () {
removeContentEditableSelection();
hideFakeCaret();
});
function handleTouchSelect(editor) {
var moved = false;
editor.on('touchstart', function () {
moved = false;
});
editor.on('touchmove', function () {
moved = true;
});
editor.on('touchend', function (e) {
var contentEditableRoot = getContentEditableRoot(e.target);
if (isContentEditableFalse(contentEditableRoot)) {
if (!moved) {
e.preventDefault();
setContentEditableSelection(selectNode(contentEditableRoot));
}
}
});
}
var hasNormalCaretPosition = function (elm) {
var caretWalker = new CaretWalker(elm);
if (!elm.firstChild) {
return false;
}
var startPos = CaretPosition.before(elm.firstChild);
var newPos = caretWalker.next(startPos);
return newPos && !isBeforeContentEditableFalse(newPos) && !isAfterContentEditableFalse(newPos);
};
var isInSameBlock = function (node1, node2) {
var block1 = editor.dom.getParent(node1, editor.dom.isBlock);
var block2 = editor.dom.getParent(node2, editor.dom.isBlock);
return block1 === block2;
};
var isContentKey = function (e) {
if (e.keyCode >= 112 && e.keyCode <= 123) {
return false;
}
return true;
};
// Checks if the target node is in a block and if that block has a caret position better than the
// suggested caretNode this is to prevent the caret from being sucked in towards a cE=false block if
// they are adjacent on the vertical axis
var hasBetterMouseTarget = function (targetNode, caretNode) {
var targetBlock = editor.dom.getParent(targetNode, editor.dom.isBlock);
var caretBlock = editor.dom.getParent(caretNode, editor.dom.isBlock);
return targetBlock && !isInSameBlock(targetBlock, caretBlock) && hasNormalCaretPosition(targetBlock);
};
handleTouchSelect(editor);
editor.on('mousedown', function(e) {
var contentEditableRoot;
contentEditableRoot = getContentEditableRoot(e.target);
if (contentEditableRoot) {
if (isContentEditableFalse(contentEditableRoot)) {
e.preventDefault();
setContentEditableSelection(selectNode(contentEditableRoot));
} else {
if (!isXYWithinRange(e.clientX, e.clientY, editor.selection.getRng())) {
editor.selection.placeCaretAt(e.clientX, e.clientY);
}
}
} else {
// Remove needs to be called here since the mousedown might alter the selection without calling selection.setRng
// and therefore not fire the AfterSetSelectionRange event.
removeContentEditableSelection();
hideFakeCaret();
var caretInfo = LineUtils.closestCaret(rootNode, e.clientX, e.clientY);
if (caretInfo) {
if (!hasBetterMouseTarget(e.target, caretInfo.node)) {
e.preventDefault();
editor.getBody().focus();
setRange(showCaret(1, caretInfo.node, caretInfo.before));
}
}
}
});
editor.on('keydown', function(e) {
if (VK.modifierPressed(e)) {
return;
}
switch (e.keyCode) {
case VK.RIGHT:
override(e, right);
break;
case VK.DOWN:
override(e, down);
break;
case VK.LEFT:
override(e, left);
break;
case VK.UP:
override(e, up);
break;
case VK.DELETE:
override(e, deleteForward);
break;
case VK.BACKSPACE:
override(e, backspace);
break;
default:
if (isContentEditableFalse(editor.selection.getNode()) && isContentKey(e)) {
e.preventDefault();
}
break;
}
});
function paddEmptyContentEditableArea() {
var br, ceRoot = getContentEditableRoot(editor.selection.getNode());
if (isContentEditableTrue(ceRoot) && isBlock(ceRoot) && editor.dom.isEmpty(ceRoot)) {
br = editor.dom.create('br', {"data-mce-bogus": "1"});
editor.$(ceRoot).empty().append(br);
editor.selection.setRng(CaretPosition.before(br).toRange());
}
}
function handleBlockContainer(e) {
var blockCaretContainer = getBlockCaretContainer();
if (!blockCaretContainer) {
return;
}
if (e.type == 'compositionstart') {
e.preventDefault();
e.stopPropagation();
showBlockCaretContainer(blockCaretContainer);
return;
}
if (CaretContainer.hasContent(blockCaretContainer)) {
showBlockCaretContainer(blockCaretContainer);
}
}
function handleEmptyBackspaceDelete(e) {
var prevent;
switch (e.keyCode) {
case VK.DELETE:
prevent = paddEmptyContentEditableArea();
break;
case VK.BACKSPACE:
prevent = paddEmptyContentEditableArea();
break;
}
if (prevent) {
e.preventDefault();
}
}
// Must be added to "top" since undoManager needs to be executed after
editor.on('keyup compositionstart', function(e) {
handleBlockContainer(e);
handleEmptyBackspaceDelete(e);
}, true);
editor.on('cut', function() {
var node = editor.selection.getNode();
if (isContentEditableFalse(node)) {
Delay.setEditorTimeout(editor, function() {
setRange(renderRangeCaret(deleteContentEditableNode(node)));
});
}
});
editor.on('getSelectionRange', function(e) {
var rng = e.range;
if (selectedContentEditableNode) {
if (!selectedContentEditableNode.parentNode) {
selectedContentEditableNode = null;
return;
}
rng = rng.cloneRange();
rng.selectNode(selectedContentEditableNode);
e.range = rng;
}
});
editor.on('setSelectionRange', function(e) {
var rng;
rng = setContentEditableSelection(e.range);
if (rng) {
e.range = rng;
}
});
editor.on('AfterSetSelectionRange', function(e) {
var rng = e.range;
if (!isRangeInCaretContainer(rng)) {
hideFakeCaret();
}
if (!isFakeSelectionElement(rng.startContainer.parentNode)) {
removeContentEditableSelection();
}
});
editor.on('focus', function() {
// Make sure we have a proper fake caret on focus
Delay.setEditorTimeout(editor, function() {
editor.selection.setRng(renderRangeCaret(editor.selection.getRng()));
}, 0);
});
editor.on('copy', function (e) {
var clipboardData = e.clipboardData;
// Make sure we get proper html/text for the fake cE=false selection
// Doesn't work at all on Edge since it doesn't have proper clipboardData support
if (!e.isDefaultPrevented() && e.clipboardData && !Env.ie) {
var realSelectionElement = getRealSelectionElement();
if (realSelectionElement) {
e.preventDefault();
clipboardData.clearData();
clipboardData.setData('text/html', realSelectionElement.outerHTML);
clipboardData.setData('text/plain', realSelectionElement.outerText);
}
}
});
DragDropOverrides.init(editor);
}
function addCss() {
var styles = editor.contentStyles, rootClass = '.mce-content-body';
styles.push(fakeCaret.getCss());
styles.push(
rootClass + ' .mce-offscreen-selection {' +
'position: absolute;' +
'left: -9999999999px;' +
'max-width: 1000000px;' +
'}' +
rootClass + ' *[contentEditable=false] {' +
'cursor: default;' +
'}' +
rootClass + ' *[contentEditable=true] {' +
'cursor: text;' +
'}'
);
}
function isRangeInCaretContainer(rng) {
return CaretContainer.isCaretContainer(rng.startContainer) || CaretContainer.isCaretContainer(rng.endContainer);
}
function setContentEditableSelection(range) {
var node, $ = editor.$, dom = editor.dom, $realSelectionContainer, sel,
startContainer, startOffset, endOffset, e, caretPosition, targetClone, origTargetClone;
if (!range) {
return null;
}
if (range.collapsed) {
if (!isRangeInCaretContainer(range)) {
caretPosition = getNormalizedRangeEndPoint(1, range);
if (isContentEditableFalse(caretPosition.getNode())) {
return showCaret(1, caretPosition.getNode(), !caretPosition.isAtEnd());
}
if (isContentEditableFalse(caretPosition.getNode(true))) {
return showCaret(1, caretPosition.getNode(true), false);
}
}
return null;
}
startContainer = range.startContainer;
startOffset = range.startOffset;
endOffset = range.endOffset;
// Normalizes <span cE=false>[</span>] to [<span cE=false></span>]
if (startContainer.nodeType == 3 && startOffset == 0 && isContentEditableFalse(startContainer.parentNode)) {
startContainer = startContainer.parentNode;
startOffset = dom.nodeIndex(startContainer);
startContainer = startContainer.parentNode;
}
if (startContainer.nodeType != 1) {
return null;
}
if (endOffset == startOffset + 1) {
node = startContainer.childNodes[startOffset];
}
if (!isContentEditableFalse(node)) {
return null;
}
targetClone = origTargetClone = node.cloneNode(true);
e = editor.fire('ObjectSelected', {target: node, targetClone: targetClone});
if (e.isDefaultPrevented()) {
return null;
}
targetClone = e.targetClone;
$realSelectionContainer = $('#' + realSelectionId);
if ($realSelectionContainer.length === 0) {
$realSelectionContainer = $(
'<div data-mce-bogus="all" class="mce-offscreen-selection"></div>'
).attr('id', realSelectionId);
$realSelectionContainer.appendTo(editor.getBody());
}
range = editor.dom.createRng();
// WHY is IE making things so hard! Copy on <i contentEditable="false">x</i> produces: <em>x</em>
// This is a ridiculous hack where we place the selection from a block over the inline element
// so that just the inline element is copied as is and not converted.
if (targetClone === origTargetClone && Env.ie) {
$realSelectionContainer.empty().append('<p style="font-size: 0" data-mce-bogus="all">\u00a0</p>').append(targetClone);
range.setStartAfter($realSelectionContainer[0].firstChild.firstChild);
range.setEndAfter(targetClone);
} else {
$realSelectionContainer.empty().append('\u00a0').append(targetClone).append('\u00a0');
range.setStart($realSelectionContainer[0].firstChild, 1);
range.setEnd($realSelectionContainer[0].lastChild, 0);
}
$realSelectionContainer.css({
top: dom.getPos(node, editor.getBody()).y
});
$realSelectionContainer[0].focus();
sel = editor.selection.getSel();
sel.removeAllRanges();
sel.addRange(range);
editor.$('*[data-mce-selected]').removeAttr('data-mce-selected');
node.setAttribute('data-mce-selected', 1);
selectedContentEditableNode = node;
return range;
}
function removeContentEditableSelection() {
if (selectedContentEditableNode) {
selectedContentEditableNode.removeAttribute('data-mce-selected');
editor.$('#' + realSelectionId).remove();
selectedContentEditableNode = null;
}
}
function destroy() {
fakeCaret.destroy();
selectedContentEditableNode = null;
}
function hideFakeCaret() {
fakeCaret.hide();
}
if (Env.ceFalse) {
registerEvents();
addCss();
}
return {
showBlockCaretContainer: showBlockCaretContainer,
hideFakeCaret: hideFakeCaret,
destroy: destroy
};
}
return SelectionOverrides;
});
// Included from: js/tinymce/classes/util/Uuid.js
/**
* Uuid.js
*
* Released under LGPL License.
* Copyright (c) 1999-2016 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Generates unique ids.
*
* @class tinymce.util.Uuid
* @private
*/
define("tinymce/util/Uuid", [
], function() {
var count = 0;
var seed = function () {
var rnd = function () {
return Math.round(Math.random() * 0xFFFFFFFF).toString(36);
};
var now = new Date().getTime();
return 's' + now.toString(36) + rnd() + rnd() + rnd();
};
var uuid = function (prefix) {
return prefix + (count++) + seed();
};
return {
uuid: uuid
};
});
// Included from: js/tinymce/classes/ui/Sidebar.js
/**
* Sidebar.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This module handle sidebar instances for the editor.
*
* @class tinymce.ui.Sidebar
* @private
*/
define("tinymce/ui/Sidebar", [
], function(
) {
var add = function (editor, name, settings) {
var sidebars = editor.sidebars ? editor.sidebars : [];
sidebars.push({name: name, settings: settings});
editor.sidebars = sidebars;
};
return {
add: add
};
});
// Included from: js/tinymce/classes/Editor.js
/**
* Editor.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/*jshint scripturl:true */
/**
* Include the base event class documentation.
*
* @include ../../../tools/docs/tinymce.Event.js
*/
/**
* This class contains the core logic for a TinyMCE editor.
*
* @class tinymce.Editor
* @mixes tinymce.util.Observable
* @example
* // Add a class to all paragraphs in the editor.
* tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass');
*
* // Gets the current editors selection as text
* tinymce.activeEditor.selection.getContent({format: 'text'});
*
* // Creates a new editor instance
* var ed = new tinymce.Editor('textareaid', {
* some_setting: 1
* }, tinymce.EditorManager);
*
* // Select each item the user clicks on
* ed.on('click', function(e) {
* ed.selection.select(e.target);
* });
*
* ed.render();
*/
define("tinymce/Editor", [
"tinymce/dom/DOMUtils",
"tinymce/dom/DomQuery",
"tinymce/AddOnManager",
"tinymce/NodeChange",
"tinymce/html/Node",
"tinymce/dom/Serializer",
"tinymce/html/Serializer",
"tinymce/dom/Selection",
"tinymce/Formatter",
"tinymce/UndoManager",
"tinymce/EnterKey",
"tinymce/ForceBlocks",
"tinymce/EditorCommands",
"tinymce/util/URI",
"tinymce/dom/ScriptLoader",
"tinymce/dom/EventUtils",
"tinymce/WindowManager",
"tinymce/NotificationManager",
"tinymce/html/Schema",
"tinymce/html/DomParser",
"tinymce/util/Quirks",
"tinymce/Env",
"tinymce/util/Tools",
"tinymce/util/Delay",
"tinymce/EditorObservable",
"tinymce/Mode",
"tinymce/Shortcuts",
"tinymce/EditorUpload",
"tinymce/SelectionOverrides",
"tinymce/util/Uuid",
"tinymce/ui/Sidebar"
], function(
DOMUtils, DomQuery, AddOnManager, NodeChange, Node, DomSerializer, Serializer,
Selection, Formatter, UndoManager, EnterKey, ForceBlocks, EditorCommands,
URI, ScriptLoader, EventUtils, WindowManager, NotificationManager,
Schema, DomParser, Quirks, Env, Tools, Delay, EditorObservable, Mode, Shortcuts, EditorUpload,
SelectionOverrides, Uuid, Sidebar
) {
// Shorten these names
var DOM = DOMUtils.DOM, ThemeManager = AddOnManager.ThemeManager, PluginManager = AddOnManager.PluginManager;
var extend = Tools.extend, each = Tools.each, explode = Tools.explode;
var inArray = Tools.inArray, trim = Tools.trim, resolve = Tools.resolve;
var Event = EventUtils.Event;
var isGecko = Env.gecko, ie = Env.ie;
/**
* Include documentation for all the events.
*
* @include ../../../tools/docs/tinymce.Editor.js
*/
/**
* Constructs a editor instance by id.
*
* @constructor
* @method Editor
* @param {String} id Unique id for the editor.
* @param {Object} settings Settings for the editor.
* @param {tinymce.EditorManager} editorManager EditorManager instance.
*/
function Editor(id, settings, editorManager) {
var self = this, documentBaseUrl, baseUri, defaultSettings;
documentBaseUrl = self.documentBaseUrl = editorManager.documentBaseURL;
baseUri = editorManager.baseURI;
defaultSettings = editorManager.defaultSettings;
/**
* Name/value collection with editor settings.
*
* @property settings
* @type Object
* @example
* // Get the value of the theme setting
* tinymce.activeEditor.windowManager.alert("You are using the " + tinymce.activeEditor.settings.theme + " theme");
*/
settings = extend({
id: id,
theme: 'modern',
delta_width: 0,
delta_height: 0,
popup_css: '',
plugins: '',
document_base_url: documentBaseUrl,
add_form_submit_trigger: true,
submit_patch: true,
add_unload_trigger: true,
convert_urls: true,
relative_urls: true,
remove_script_host: true,
object_resizing: true,
doctype: '<!DOCTYPE html>',
visual: true,
font_size_style_values: 'xx-small,x-small,small,medium,large,x-large,xx-large',
// See: http://www.w3.org/TR/CSS2/fonts.html#propdef-font-size
font_size_legacy_values: 'xx-small,small,medium,large,x-large,xx-large,300%',
forced_root_block: 'p',
hidden_input: true,
padd_empty_editor: true,
render_ui: true,
indentation: '30px',
inline_styles: true,
convert_fonts_to_spans: true,
indent: 'simple',
indent_before: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,' +
'tfoot,tbody,tr,section,article,hgroup,aside,figure,figcaption,option,optgroup,datalist',
indent_after: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,' +
'tfoot,tbody,tr,section,article,hgroup,aside,figure,figcaption,option,optgroup,datalist',
validate: true,
entity_encoding: 'named',
url_converter: self.convertURL,
url_converter_scope: self,
ie7_compat: true
}, defaultSettings, settings);
// Merge external_plugins
if (defaultSettings && defaultSettings.external_plugins && settings.external_plugins) {
settings.external_plugins = extend({}, defaultSettings.external_plugins, settings.external_plugins);
}
self.settings = settings;
AddOnManager.language = settings.language || 'en';
AddOnManager.languageLoad = settings.language_load;
AddOnManager.baseURL = editorManager.baseURL;
/**
* Editor instance id, normally the same as the div/textarea that was replaced.
*
* @property id
* @type String
*/
self.id = settings.id = id;
/**
* State to force the editor to return false on a isDirty call.
*
* @property isNotDirty
* @type Boolean
* @deprecated Use editor.setDirty instead.
*/
self.setDirty(false);
/**
* Name/Value object containing plugin instances.
*
* @property plugins
* @type Object
* @example
* // Execute a method inside a plugin directly
* tinymce.activeEditor.plugins.someplugin.someMethod();
*/
self.plugins = {};
/**
* URI object to document configured for the TinyMCE instance.
*
* @property documentBaseURI
* @type tinymce.util.URI
* @example
* // Get relative URL from the location of document_base_url
* tinymce.activeEditor.documentBaseURI.toRelative('/somedir/somefile.htm');
*
* // Get absolute URL from the location of document_base_url
* tinymce.activeEditor.documentBaseURI.toAbsolute('somefile.htm');
*/
self.documentBaseURI = new URI(settings.document_base_url || documentBaseUrl, {
base_uri: baseUri
});
/**
* URI object to current document that holds the TinyMCE editor instance.
*
* @property baseURI
* @type tinymce.util.URI
* @example
* // Get relative URL from the location of the API
* tinymce.activeEditor.baseURI.toRelative('/somedir/somefile.htm');
*
* // Get absolute URL from the location of the API
* tinymce.activeEditor.baseURI.toAbsolute('somefile.htm');
*/
self.baseURI = baseUri;
/**
* Array with CSS files to load into the iframe.
*
* @property contentCSS
* @type Array
*/
self.contentCSS = [];
/**
* Array of CSS styles to add to head of document when the editor loads.
*
* @property contentStyles
* @type Array
*/
self.contentStyles = [];
// Creates all events like onClick, onSetContent etc see Editor.Events.js for the actual logic
self.shortcuts = new Shortcuts(self);
self.loadedCSS = {};
self.editorCommands = new EditorCommands(self);
self.suffix = editorManager.suffix;
self.editorManager = editorManager;
self.inline = settings.inline;
self.settings.content_editable = self.inline;
if (settings.cache_suffix) {
Env.cacheSuffix = settings.cache_suffix.replace(/^[\?\&]+/, '');
}
if (settings.override_viewport === false) {
Env.overrideViewPort = false;
}
// Call setup
editorManager.fire('SetupEditor', self);
self.execCallback('setup', self);
/**
* Dom query instance with default scope to the editor document and default element is the body of the editor.
*
* @property $
* @type tinymce.dom.DomQuery
* @example
* tinymce.activeEditor.$('p').css('color', 'red');
* tinymce.activeEditor.$().append('<p>new</p>');
*/
self.$ = DomQuery.overrideDefaults(function() {
return {
context: self.inline ? self.getBody() : self.getDoc(),
element: self.getBody()
};
});
}
Editor.prototype = {
/**
* Renders the editor/adds it to the page.
*
* @method render
*/
render: function() {
var self = this, settings = self.settings, id = self.id, suffix = self.suffix;
function readyHandler() {
DOM.unbind(window, 'ready', readyHandler);
self.render();
}
// Page is not loaded yet, wait for it
if (!Event.domLoaded) {
DOM.bind(window, 'ready', readyHandler);
return;
}
// Element not found, then skip initialization
if (!self.getElement()) {
return;
}
// No editable support old iOS versions etc
if (!Env.contentEditable) {
return;
}
// Hide target element early to prevent content flashing
if (!settings.inline) {
self.orgVisibility = self.getElement().style.visibility;
self.getElement().style.visibility = 'hidden';
} else {
self.inline = true;
}
var form = self.getElement().form || DOM.getParent(id, 'form');
if (form) {
self.formElement = form;
// Add hidden input for non input elements inside form elements
if (settings.hidden_input && !/TEXTAREA|INPUT/i.test(self.getElement().nodeName)) {
DOM.insertAfter(DOM.create('input', {type: 'hidden', name: id}), id);
self.hasHiddenInput = true;
}
// Pass submit/reset from form to editor instance
self.formEventDelegate = function(e) {
self.fire(e.type, e);
};
DOM.bind(form, 'submit reset', self.formEventDelegate);
// Reset contents in editor when the form is reset
self.on('reset', function() {
self.setContent(self.startContent, {format: 'raw'});
});
// Check page uses id="submit" or name="submit" for it's submit button
if (settings.submit_patch && !form.submit.nodeType && !form.submit.length && !form._mceOldSubmit) {
form._mceOldSubmit = form.submit;
form.submit = function() {
self.editorManager.triggerSave();
self.setDirty(false);
return form._mceOldSubmit(form);
};
}
}
/**
* Window manager reference, use this to open new windows and dialogs.
*
* @property windowManager
* @type tinymce.WindowManager
* @example
* // Shows an alert message
* tinymce.activeEditor.windowManager.alert('Hello world!');
*
* // Opens a new dialog with the file.htm file and the size 320x240
* // It also adds a custom parameter this can be retrieved by using tinyMCEPopup.getWindowArg inside the dialog.
* tinymce.activeEditor.windowManager.open({
* url: 'file.htm',
* width: 320,
* height: 240
* }, {
* custom_param: 1
* });
*/
self.windowManager = new WindowManager(self);
/**
* Notification manager reference, use this to open new windows and dialogs.
*
* @property notificationManager
* @type tinymce.NotificationManager
* @example
* // Shows a notification info message.
* tinymce.activeEditor.notificationManager.open({text: 'Hello world!', type: 'info'});
*/
self.notificationManager = new NotificationManager(self);
if (settings.encoding == 'xml') {
self.on('GetContent', function(e) {
if (e.save) {
e.content = DOM.encode(e.content);
}
});
}
if (settings.add_form_submit_trigger) {
self.on('submit', function() {
if (self.initialized) {
self.save();
}
});
}
if (settings.add_unload_trigger) {
self._beforeUnload = function() {
if (self.initialized && !self.destroyed && !self.isHidden()) {
self.save({format: 'raw', no_events: true, set_dirty: false});
}
};
self.editorManager.on('BeforeUnload', self._beforeUnload);
}
// Load scripts
function loadScripts() {
var scriptLoader = ScriptLoader.ScriptLoader;
if (settings.language && settings.language != 'en' && !settings.language_url) {
settings.language_url = self.editorManager.baseURL + '/langs/' + settings.language + '.js';
}
if (settings.language_url) {
scriptLoader.add(settings.language_url);
}
if (settings.theme && typeof settings.theme != "function" &&
settings.theme.charAt(0) != '-' && !ThemeManager.urls[settings.theme]) {
var themeUrl = settings.theme_url;
if (themeUrl) {
themeUrl = self.documentBaseURI.toAbsolute(themeUrl);
} else {
themeUrl = 'themes/' + settings.theme + '/theme' + suffix + '.js';
}
ThemeManager.load(settings.theme, themeUrl);
}
if (Tools.isArray(settings.plugins)) {
settings.plugins = settings.plugins.join(' ');
}
each(settings.external_plugins, function(url, name) {
PluginManager.load(name, url);
settings.plugins += ' ' + name;
});
each(settings.plugins.split(/[ ,]/), function(plugin) {
plugin = trim(plugin);
if (plugin && !PluginManager.urls[plugin]) {
if (plugin.charAt(0) == '-') {
plugin = plugin.substr(1, plugin.length);
var dependencies = PluginManager.dependencies(plugin);
each(dependencies, function(dep) {
var defaultSettings = {
prefix: 'plugins/',
resource: dep,
suffix: '/plugin' + suffix + '.js'
};
dep = PluginManager.createUrl(defaultSettings, dep);
PluginManager.load(dep.resource, dep);
});
} else {
PluginManager.load(plugin, {
prefix: 'plugins/',
resource: plugin,
suffix: '/plugin' + suffix + '.js'
});
}
}
});
scriptLoader.loadQueue(function() {
if (!self.removed) {
self.init();
}
});
}
self.editorManager.add(self);
loadScripts();
},
/**
* Initializes the editor this will be called automatically when
* all plugins/themes and language packs are loaded by the rendered method.
* This method will setup the iframe and create the theme and plugin instances.
*
* @method init
*/
init: function() {
var self = this, settings = self.settings, elm = self.getElement();
var w, h, minHeight, n, o, Theme, url, bodyId, bodyClass, re, i, initializedPlugins = [];
self.rtl = settings.rtl_ui || self.editorManager.i18n.rtl;
self.editorManager.i18n.setCode(settings.language);
settings.aria_label = settings.aria_label || DOM.getAttrib(elm, 'aria-label', self.getLang('aria.rich_text_area'));
self.fire('ScriptsLoaded');
/**
* Reference to the theme instance that was used to generate the UI.
*
* @property theme
* @type tinymce.Theme
* @example
* // Executes a method on the theme directly
* tinymce.activeEditor.theme.someMethod();
*/
if (settings.theme) {
if (typeof settings.theme != "function") {
settings.theme = settings.theme.replace(/-/, '');
Theme = ThemeManager.get(settings.theme);
self.theme = new Theme(self, ThemeManager.urls[settings.theme]);
if (self.theme.init) {
self.theme.init(self, ThemeManager.urls[settings.theme] || self.documentBaseUrl.replace(/\/$/, ''), self.$);
}
} else {
self.theme = settings.theme;
}
}
function initPlugin(plugin) {
var Plugin = PluginManager.get(plugin), pluginUrl, pluginInstance;
pluginUrl = PluginManager.urls[plugin] || self.documentBaseUrl.replace(/\/$/, '');
plugin = trim(plugin);
if (Plugin && inArray(initializedPlugins, plugin) === -1) {
each(PluginManager.dependencies(plugin), function(dep) {
initPlugin(dep);
});
if (self.plugins[plugin]) {
return;
}
pluginInstance = new Plugin(self, pluginUrl, self.$);
self.plugins[plugin] = pluginInstance;
if (pluginInstance.init) {
pluginInstance.init(self, pluginUrl);
initializedPlugins.push(plugin);
}
}
}
// Create all plugins
each(settings.plugins.replace(/\-/g, '').split(/[ ,]/), initPlugin);
// Measure box
if (settings.render_ui && self.theme) {
self.orgDisplay = elm.style.display;
if (typeof settings.theme != "function") {
w = settings.width || elm.style.width || elm.offsetWidth;
h = settings.height || elm.style.height || elm.offsetHeight;
minHeight = settings.min_height || 100;
re = /^[0-9\.]+(|px)$/i;
if (re.test('' + w)) {
w = Math.max(parseInt(w, 10), 100);
}
if (re.test('' + h)) {
h = Math.max(parseInt(h, 10), minHeight);
}
// Render UI
o = self.theme.renderUI({
targetNode: elm,
width: w,
height: h,
deltaWidth: settings.delta_width,
deltaHeight: settings.delta_height
});
// Resize editor
if (!settings.content_editable) {
h = (o.iframeHeight || h) + (typeof h == 'number' ? (o.deltaHeight || 0) : '');
if (h < minHeight) {
h = minHeight;
}
}
} else {
o = settings.theme(self, elm);
if (o.editorContainer.nodeType) {
o.editorContainer.id = o.editorContainer.id || self.id + "_parent";
}
if (o.iframeContainer.nodeType) {
o.iframeContainer.id = o.iframeContainer.id || self.id + "_iframecontainer";
}
// Use specified iframe height or the targets offsetHeight
h = o.iframeHeight || elm.offsetHeight;
}
self.editorContainer = o.editorContainer;
}
// Load specified content CSS last
if (settings.content_css) {
each(explode(settings.content_css), function(u) {
self.contentCSS.push(self.documentBaseURI.toAbsolute(u));
});
}
// Load specified content CSS last
if (settings.content_style) {
self.contentStyles.push(settings.content_style);
}
// Content editable mode ends here
if (settings.content_editable) {
elm = n = o = null; // Fix IE leak
return self.initContentBody();
}
self.iframeHTML = settings.doctype + '<html><head>';
// We only need to override paths if we have to
// IE has a bug where it remove site absolute urls to relative ones if this is specified
if (settings.document_base_url != self.documentBaseUrl) {
self.iframeHTML += '<base href="' + self.documentBaseURI.getURI() + '" />';
}
// IE8 doesn't support carets behind images setting ie7_compat would force IE8+ to run in IE7 compat mode.
if (!Env.caretAfter && settings.ie7_compat) {
self.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=7" />';
}
self.iframeHTML += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />';
// Load the CSS by injecting them into the HTML this will reduce "flicker"
// However we can't do that on Chrome since # will scroll to the editor for some odd reason see #2427
if (!/#$/.test(document.location.href)) {
for (i = 0; i < self.contentCSS.length; i++) {
var cssUrl = self.contentCSS[i];
self.iframeHTML += (
'<link type="text/css" ' +
'rel="stylesheet" ' +
'href="' + Tools._addCacheSuffix(cssUrl) + '" />'
);
self.loadedCSS[cssUrl] = true;
}
}
bodyId = settings.body_id || 'tinymce';
if (bodyId.indexOf('=') != -1) {
bodyId = self.getParam('body_id', '', 'hash');
bodyId = bodyId[self.id] || bodyId;
}
bodyClass = settings.body_class || '';
if (bodyClass.indexOf('=') != -1) {
bodyClass = self.getParam('body_class', '', 'hash');
bodyClass = bodyClass[self.id] || '';
}
if (settings.content_security_policy) {
self.iframeHTML += '<meta http-equiv="Content-Security-Policy" content="' + settings.content_security_policy + '" />';
}
self.iframeHTML += '</head><body id="' + bodyId +
'" class="mce-content-body ' + bodyClass +
'" data-id="' + self.id + '"><br></body></html>';
/*eslint no-script-url:0 */
var domainRelaxUrl = 'javascript:(function(){' +
'document.open();document.domain="' + document.domain + '";' +
'var ed = window.parent.tinymce.get("' + self.id + '");document.write(ed.iframeHTML);' +
'document.close();ed.initContentBody(true);})()';
// Domain relaxing is required since the user has messed around with document.domain
if (document.domain != location.hostname) {
// Edge seems to be able to handle domain relaxing
if (Env.ie && Env.ie < 12) {
url = domainRelaxUrl;
}
}
// Create iframe
// TODO: ACC add the appropriate description on this.
var ifr = DOM.create('iframe', {
id: self.id + "_ifr",
//src: url || 'javascript:""', // Workaround for HTTPS warning in IE6/7
frameBorder: '0',
allowTransparency: "true",
title: self.editorManager.translate(
"Rich Text Area. Press ALT-F9 for menu. " +
"Press ALT-F10 for toolbar. Press ALT-0 for help"
),
style: {
width: '100%',
height: h,
display: 'block' // Important for Gecko to render the iframe correctly
}
});
ifr.onload = function() {
ifr.onload = null;
self.fire("load");
};
DOM.setAttrib(ifr, "src", url || 'javascript:""');
self.contentAreaContainer = o.iframeContainer;
self.iframeElement = ifr;
n = DOM.add(o.iframeContainer, ifr);
// Try accessing the document this will fail on IE when document.domain is set to the same as location.hostname
// Then we have to force domain relaxing using the domainRelaxUrl approach very ugly!!
if (ie) {
try {
self.getDoc();
} catch (e) {
n.src = url = domainRelaxUrl;
}
}
if (o.editorContainer) {
DOM.get(o.editorContainer).style.display = self.orgDisplay;
self.hidden = DOM.isHidden(o.editorContainer);
}
self.getElement().style.display = 'none';
DOM.setAttrib(self.id, 'aria-hidden', true);
if (!url) {
self.initContentBody();
}
elm = n = o = null; // Cleanup
},
/**
* This method get called by the init method once the iframe is loaded.
* It will fill the iframe with contents, sets up DOM and selection objects for the iframe.
*
* @method initContentBody
* @private
*/
initContentBody: function(skipWrite) {
var self = this, settings = self.settings, targetElm = self.getElement(), doc = self.getDoc(), body, contentCssText;
// Restore visibility on target element
if (!settings.inline) {
self.getElement().style.visibility = self.orgVisibility;
}
// Setup iframe body
if (!skipWrite && !settings.content_editable) {
doc.open();
doc.write(self.iframeHTML);
doc.close();
}
if (settings.content_editable) {
self.on('remove', function() {
var bodyEl = this.getBody();
DOM.removeClass(bodyEl, 'mce-content-body');
DOM.removeClass(bodyEl, 'mce-edit-focus');
DOM.setAttrib(bodyEl, 'contentEditable', null);
});
DOM.addClass(targetElm, 'mce-content-body');
self.contentDocument = doc = settings.content_document || document;
self.contentWindow = settings.content_window || window;
self.bodyElement = targetElm;
// Prevent leak in IE
settings.content_document = settings.content_window = null;
// TODO: Fix this
settings.root_name = targetElm.nodeName.toLowerCase();
}
// It will not steal focus while setting contentEditable
body = self.getBody();
body.disabled = true;
self.readonly = settings.readonly;
if (!self.readonly) {
if (self.inline && DOM.getStyle(body, 'position', true) == 'static') {
body.style.position = 'relative';
}
body.contentEditable = self.getParam('content_editable_state', true);
}
body.disabled = false;
self.editorUpload = new EditorUpload(self);
/**
* Schema instance, enables you to validate elements and its children.
*
* @property schema
* @type tinymce.html.Schema
*/
self.schema = new Schema(settings);
/**
* DOM instance for the editor.
*
* @property dom
* @type tinymce.dom.DOMUtils
* @example
* // Adds a class to all paragraphs within the editor
* tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass');
*/
self.dom = new DOMUtils(doc, {
keep_values: true,
url_converter: self.convertURL,
url_converter_scope: self,
hex_colors: settings.force_hex_style_colors,
class_filter: settings.class_filter,
update_styles: true,
root_element: self.inline ? self.getBody() : null,
collect: settings.content_editable,
schema: self.schema,
onSetAttrib: function(e) {
self.fire('SetAttrib', e);
}
});
/**
* HTML parser will be used when contents is inserted into the editor.
*
* @property parser
* @type tinymce.html.DomParser
*/
self.parser = new DomParser(settings, self.schema);
// Convert src and href into data-mce-src, data-mce-href and data-mce-style
self.parser.addAttributeFilter('src,href,style,tabindex', function(nodes, name) {
var i = nodes.length, node, dom = self.dom, value, internalName;
while (i--) {
node = nodes[i];
value = node.attr(name);
internalName = 'data-mce-' + name;
// Add internal attribute if we need to we don't on a refresh of the document
if (!node.attributes.map[internalName]) {
// Don't duplicate these since they won't get modified by any browser
if (value.indexOf('data:') === 0 || value.indexOf('blob:') === 0) {
continue;
}
if (name === "style") {
value = dom.serializeStyle(dom.parseStyle(value), node.name);
if (!value.length) {
value = null;
}
node.attr(internalName, value);
node.attr(name, value);
} else if (name === "tabindex") {
node.attr(internalName, value);
node.attr(name, null);
} else {
node.attr(internalName, self.convertURL(value, name, node.name));
}
}
}
});
// Keep scripts from executing
self.parser.addNodeFilter('script', function(nodes) {
var i = nodes.length, node, type;
while (i--) {
node = nodes[i];
type = node.attr('type') || 'no/type';
if (type.indexOf('mce-') !== 0) {
node.attr('type', 'mce-' + type);
}
}
});
self.parser.addNodeFilter('#cdata', function(nodes) {
var i = nodes.length, node;
while (i--) {
node = nodes[i];
node.type = 8;
node.name = '#comment';
node.value = '[CDATA[' + node.value + ']]';
}
});
self.parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', function(nodes) {
var i = nodes.length, node, nonEmptyElements = self.schema.getNonEmptyElements();
while (i--) {
node = nodes[i];
if (node.isEmpty(nonEmptyElements)) {
node.append(new Node('br', 1)).shortEnded = true;
}
}
});
/**
* DOM serializer for the editor. Will be used when contents is extracted from the editor.
*
* @property serializer
* @type tinymce.dom.Serializer
* @example
* // Serializes the first paragraph in the editor into a string
* tinymce.activeEditor.serializer.serialize(tinymce.activeEditor.dom.select('p')[0]);
*/
self.serializer = new DomSerializer(settings, self);
/**
* Selection instance for the editor.
*
* @property selection
* @type tinymce.dom.Selection
* @example
* // Sets some contents to the current selection in the editor
* tinymce.activeEditor.selection.setContent('Some contents');
*
* // Gets the current selection
* alert(tinymce.activeEditor.selection.getContent());
*
* // Selects the first paragraph found
* tinymce.activeEditor.selection.select(tinymce.activeEditor.dom.select('p')[0]);
*/
self.selection = new Selection(self.dom, self.getWin(), self.serializer, self);
/**
* Formatter instance.
*
* @property formatter
* @type tinymce.Formatter
*/
self.formatter = new Formatter(self);
/**
* Undo manager instance, responsible for handling undo levels.
*
* @property undoManager
* @type tinymce.UndoManager
* @example
* // Undoes the last modification to the editor
* tinymce.activeEditor.undoManager.undo();
*/
self.undoManager = new UndoManager(self);
self.forceBlocks = new ForceBlocks(self);
self.enterKey = new EnterKey(self);
self._nodeChangeDispatcher = new NodeChange(self);
self._selectionOverrides = new SelectionOverrides(self);
self.fire('PreInit');
if (!settings.browser_spellcheck && !settings.gecko_spellcheck) {
doc.body.spellcheck = false; // Gecko
DOM.setAttrib(body, "spellcheck", "false");
}
self.quirks = new Quirks(self);
self.fire('PostRender');
if (settings.directionality) {
body.dir = settings.directionality;
}
if (settings.nowrap) {
body.style.whiteSpace = "nowrap";
}
if (settings.protect) {
self.on('BeforeSetContent', function(e) {
each(settings.protect, function(pattern) {
e.content = e.content.replace(pattern, function(str) {
return '<!--mce:protected ' + escape(str) + '-->';
});
});
});
}
self.on('SetContent', function() {
self.addVisual(self.getBody());
});
// Remove empty contents
if (settings.padd_empty_editor) {
self.on('PostProcess', function(e) {
e.content = e.content.replace(/^(<p[^>]*>(&nbsp;|&#160;|\s|\u00a0|)<\/p>[\r\n]*|<br \/>[\r\n]*)$/, '');
});
}
self.load({initial: true, format: 'html'});
self.startContent = self.getContent({format: 'raw'});
/**
* Is set to true after the editor instance has been initialized
*
* @property initialized
* @type Boolean
* @example
* function isEditorInitialized(editor) {
* return editor && editor.initialized;
* }
*/
self.initialized = true;
self.bindPendingEventDelegates();
self.fire('init');
self.focus(true);
self.nodeChanged({initial: true});
self.execCallback('init_instance_callback', self);
self.on('compositionstart compositionend', function(e) {
self.composing = e.type === 'compositionstart';
});
// Add editor specific CSS styles
if (self.contentStyles.length > 0) {
contentCssText = '';
each(self.contentStyles, function(style) {
contentCssText += style + "\r\n";
});
self.dom.addStyle(contentCssText);
}
// Load specified content CSS last
each(self.contentCSS, function(cssUrl) {
if (!self.loadedCSS[cssUrl]) {
self.dom.loadCSS(cssUrl);
self.loadedCSS[cssUrl] = true;
}
});
// Handle auto focus
if (settings.auto_focus) {
Delay.setEditorTimeout(self, function() {
var editor;
if (settings.auto_focus === true) {
editor = self;
} else {
editor = self.editorManager.get(settings.auto_focus);
}
if (!editor.destroyed) {
editor.focus();
}
}, 100);
}
// Clean up references for IE
targetElm = doc = body = null;
},
/**
* Focuses/activates the editor. This will set this editor as the activeEditor in the tinymce collection
* it will also place DOM focus inside the editor.
*
* @method focus
* @param {Boolean} skipFocus Skip DOM focus. Just set is as the active editor.
*/
focus: function(skipFocus) {
var self = this, selection = self.selection, contentEditable = self.settings.content_editable, rng;
var controlElm, doc = self.getDoc(), body = self.getBody(), contentEditableHost;
function getContentEditableHost(node) {
return self.dom.getParent(node, function(node) {
return self.dom.getContentEditable(node) === "true";
});
}
if (!skipFocus) {
// Get selected control element
rng = selection.getRng();
if (rng.item) {
controlElm = rng.item(0);
}
self.quirks.refreshContentEditable();
// Move focus to contentEditable=true child if needed
contentEditableHost = getContentEditableHost(selection.getNode());
if (self.$.contains(body, contentEditableHost)) {
contentEditableHost.focus();
selection.normalize();
self.editorManager.setActive(self);
return;
}
// Focus the window iframe
if (!contentEditable) {
// WebKit needs this call to fire focusin event properly see #5948
// But Opera pre Blink engine will produce an empty selection so skip Opera
if (!Env.opera) {
self.getBody().focus();
}
self.getWin().focus();
}
// Focus the body as well since it's contentEditable
if (isGecko || contentEditable) {
// Check for setActive since it doesn't scroll to the element
if (body.setActive) {
// IE 11 sometimes throws "Invalid function" then fallback to focus
try {
body.setActive();
} catch (ex) {
body.focus();
}
} else {
body.focus();
}
if (contentEditable) {
selection.normalize();
}
}
// Restore selected control element
// This is needed when for example an image is selected within a
// layer a call to focus will then remove the control selection
if (controlElm && controlElm.ownerDocument == doc) {
rng = doc.body.createControlRange();
rng.addElement(controlElm);
rng.select();
}
}
self.editorManager.setActive(self);
},
/**
* Executes a legacy callback. This method is useful to call old 2.x option callbacks.
* There new event model is a better way to add callback so this method might be removed in the future.
*
* @method execCallback
* @param {String} name Name of the callback to execute.
* @return {Object} Return value passed from callback function.
*/
execCallback: function(name) {
var self = this, callback = self.settings[name], scope;
if (!callback) {
return;
}
// Look through lookup
if (self.callbackLookup && (scope = self.callbackLookup[name])) {
callback = scope.func;
scope = scope.scope;
}
if (typeof callback === 'string') {
scope = callback.replace(/\.\w+$/, '');
scope = scope ? resolve(scope) : 0;
callback = resolve(callback);
self.callbackLookup = self.callbackLookup || {};
self.callbackLookup[name] = {func: callback, scope: scope};
}
return callback.apply(scope || self, Array.prototype.slice.call(arguments, 1));
},
/**
* Translates the specified string by replacing variables with language pack items it will also check if there is
* a key matching the input.
*
* @method translate
* @param {String} text String to translate by the language pack data.
* @return {String} Translated string.
*/
translate: function(text) {
var lang = this.settings.language || 'en', i18n = this.editorManager.i18n;
if (!text) {
return '';
}
text = i18n.data[lang + '.' + text] || text.replace(/\{\#([^\}]+)\}/g, function(a, b) {
return i18n.data[lang + '.' + b] || '{#' + b + '}';
});
return this.editorManager.translate(text);
},
/**
* Returns a language pack item by name/key.
*
* @method getLang
* @param {String} name Name/key to get from the language pack.
* @param {String} defaultVal Optional default value to retrieve.
*/
getLang: function(name, defaultVal) {
return (
this.editorManager.i18n.data[(this.settings.language || 'en') + '.' + name] ||
(defaultVal !== undefined ? defaultVal : '{#' + name + '}')
);
},
/**
* Returns a configuration parameter by name.
*
* @method getParam
* @param {String} name Configruation parameter to retrieve.
* @param {String} defaultVal Optional default value to return.
* @param {String} type Optional type parameter.
* @return {String} Configuration parameter value or default value.
* @example
* // Returns a specific config value from the currently active editor
* var someval = tinymce.activeEditor.getParam('myvalue');
*
* // Returns a specific config value from a specific editor instance by id
* var someval2 = tinymce.get('my_editor').getParam('myvalue');
*/
getParam: function(name, defaultVal, type) {
var value = name in this.settings ? this.settings[name] : defaultVal, output;
if (type === 'hash') {
output = {};
if (typeof value === 'string') {
each(value.indexOf('=') > 0 ? value.split(/[;,](?![^=;,]*(?:[;,]|$))/) : value.split(','), function(value) {
value = value.split('=');
if (value.length > 1) {
output[trim(value[0])] = trim(value[1]);
} else {
output[trim(value[0])] = trim(value);
}
});
} else {
output = value;
}
return output;
}
return value;
},
/**
* Dispatches out a onNodeChange event to all observers. This method should be called when you
* need to update the UI states or element path etc.
*
* @method nodeChanged
* @param {Object} args Optional args to pass to NodeChange event handlers.
*/
nodeChanged: function(args) {
this._nodeChangeDispatcher.nodeChanged(args);
},
/**
* Adds a button that later gets created by the theme in the editors toolbars.
*
* @method addButton
* @param {String} name Button name to add.
* @param {Object} settings Settings object with title, cmd etc.
* @example
* // Adds a custom button to the editor that inserts contents when clicked
* tinymce.init({
* ...
*
* toolbar: 'example'
*
* setup: function(ed) {
* ed.addButton('example', {
* title: 'My title',
* image: '../js/tinymce/plugins/example/img/example.gif',
* onclick: function() {
* ed.insertContent('Hello world!!');
* }
* });
* }
* });
*/
addButton: function(name, settings) {
var self = this;
if (settings.cmd) {
settings.onclick = function() {
self.execCommand(settings.cmd);
};
}
if (!settings.text && !settings.icon) {
settings.icon = name;
}
self.buttons = self.buttons || {};
settings.tooltip = settings.tooltip || settings.title;
self.buttons[name] = settings;
},
/**
* Adds a sidebar for the editor instance.
*
* @method addSidebar
* @param {String} name Sidebar name to add.
* @param {Object} settings Settings object with icon, onshow etc.
* @example
* // Adds a custom sidebar that when clicked logs the panel element
* tinymce.init({
* ...
* setup: function(ed) {
* ed.addSidebar('example', {
* tooltip: 'My sidebar',
* icon: 'my-side-bar',
* onshow: function(api) {
* console.log(api.element());
* }
* });
* }
* });
*/
addSidebar: function (name, settings) {
return Sidebar.add(this, name, settings);
},
/**
* Adds a menu item to be used in the menus of the theme. There might be multiple instances
* of this menu item for example it might be used in the main menus of the theme but also in
* the context menu so make sure that it's self contained and supports multiple instances.
*
* @method addMenuItem
* @param {String} name Menu item name to add.
* @param {Object} settings Settings object with title, cmd etc.
* @example
* // Adds a custom menu item to the editor that inserts contents when clicked
* // The context option allows you to add the menu item to an existing default menu
* tinymce.init({
* ...
*
* setup: function(ed) {
* ed.addMenuItem('example', {
* text: 'My menu item',
* context: 'tools',
* onclick: function() {
* ed.insertContent('Hello world!!');
* }
* });
* }
* });
*/
addMenuItem: function(name, settings) {
var self = this;
if (settings.cmd) {
settings.onclick = function() {
self.execCommand(settings.cmd);
};
}
self.menuItems = self.menuItems || {};
self.menuItems[name] = settings;
},
/**
* Adds a contextual toolbar to be rendered when the selector matches.
*
* @method addContextToolbar
* @param {function/string} predicate Predicate that needs to return true if provided strings get converted into CSS predicates.
* @param {String/Array} items String or array with items to add to the context toolbar.
*/
addContextToolbar: function(predicate, items) {
var self = this, selector;
self.contextToolbars = self.contextToolbars || [];
// Convert selector to predicate
if (typeof predicate == "string") {
selector = predicate;
predicate = function(elm) {
return self.dom.is(elm, selector);
};
}
self.contextToolbars.push({
id: Uuid.uuid('mcet'),
predicate: predicate,
items: items
});
},
/**
* Adds a custom command to the editor, you can also override existing commands with this method.
* The command that you add can be executed with execCommand.
*
* @method addCommand
* @param {String} name Command name to add/override.
* @param {addCommandCallback} callback Function to execute when the command occurs.
* @param {Object} scope Optional scope to execute the function in.
* @example
* // Adds a custom command that later can be executed using execCommand
* tinymce.init({
* ...
*
* setup: function(ed) {
* // Register example command
* ed.addCommand('mycommand', function(ui, v) {
* ed.windowManager.alert('Hello world!! Selection: ' + ed.selection.getContent({format: 'text'}));
* });
* }
* });
*/
addCommand: function(name, callback, scope) {
/**
* Callback function that gets called when a command is executed.
*
* @callback addCommandCallback
* @param {Boolean} ui Display UI state true/false.
* @param {Object} value Optional value for command.
* @return {Boolean} True/false state if the command was handled or not.
*/
this.editorCommands.addCommand(name, callback, scope);
},
/**
* Adds a custom query state command to the editor, you can also override existing commands with this method.
* The command that you add can be executed with queryCommandState function.
*
* @method addQueryStateHandler
* @param {String} name Command name to add/override.
* @param {addQueryStateHandlerCallback} callback Function to execute when the command state retrieval occurs.
* @param {Object} scope Optional scope to execute the function in.
*/
addQueryStateHandler: function(name, callback, scope) {
/**
* Callback function that gets called when a queryCommandState is executed.
*
* @callback addQueryStateHandlerCallback
* @return {Boolean} True/false state if the command is enabled or not like is it bold.
*/
this.editorCommands.addQueryStateHandler(name, callback, scope);
},
/**
* Adds a custom query value command to the editor, you can also override existing commands with this method.
* The command that you add can be executed with queryCommandValue function.
*
* @method addQueryValueHandler
* @param {String} name Command name to add/override.
* @param {addQueryValueHandlerCallback} callback Function to execute when the command value retrieval occurs.
* @param {Object} scope Optional scope to execute the function in.
*/
addQueryValueHandler: function(name, callback, scope) {
/**
* Callback function that gets called when a queryCommandValue is executed.
*
* @callback addQueryValueHandlerCallback
* @return {Object} Value of the command or undefined.
*/
this.editorCommands.addQueryValueHandler(name, callback, scope);
},
/**
* Adds a keyboard shortcut for some command or function.
*
* @method addShortcut
* @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o.
* @param {String} desc Text description for the command.
* @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed.
* @param {Object} sc Optional scope to execute the function in.
* @return {Boolean} true/false state if the shortcut was added or not.
*/
addShortcut: function(pattern, desc, cmdFunc, scope) {
this.shortcuts.add(pattern, desc, cmdFunc, scope);
},
/**
* Executes a command on the current instance. These commands can be TinyMCE internal commands prefixed with "mce" or
* they can be build in browser commands such as "Bold". A compleate list of browser commands is available on MSDN or Mozilla.org.
* This function will dispatch the execCommand function on each plugin, theme or the execcommand_callback option if none of these
* return true it will handle the command as a internal browser command.
*
* @method execCommand
* @param {String} cmd Command name to execute, for example mceLink or Bold.
* @param {Boolean} ui True/false state if a UI (dialog) should be presented or not.
* @param {mixed} value Optional command value, this can be anything.
* @param {Object} args Optional arguments object.
*/
execCommand: function(cmd, ui, value, args) {
return this.editorCommands.execCommand(cmd, ui, value, args);
},
/**
* Returns a command specific state, for example if bold is enabled or not.
*
* @method queryCommandState
* @param {string} cmd Command to query state from.
* @return {Boolean} Command specific state, for example if bold is enabled or not.
*/
queryCommandState: function(cmd) {
return this.editorCommands.queryCommandState(cmd);
},
/**
* Returns a command specific value, for example the current font size.
*
* @method queryCommandValue
* @param {string} cmd Command to query value from.
* @return {Object} Command specific value, for example the current font size.
*/
queryCommandValue: function(cmd) {
return this.editorCommands.queryCommandValue(cmd);
},
/**
* Returns true/false if the command is supported or not.
*
* @method queryCommandSupported
* @param {String} cmd Command that we check support for.
* @return {Boolean} true/false if the command is supported or not.
*/
queryCommandSupported: function(cmd) {
return this.editorCommands.queryCommandSupported(cmd);
},
/**
* Shows the editor and hides any textarea/div that the editor is supposed to replace.
*
* @method show
*/
show: function() {
var self = this;
if (self.hidden) {
self.hidden = false;
if (self.inline) {
self.getBody().contentEditable = true;
} else {
DOM.show(self.getContainer());
DOM.hide(self.id);
}
self.load();
self.fire('show');
}
},
/**
* Hides the editor and shows any textarea/div that the editor is supposed to replace.
*
* @method hide
*/
hide: function() {
var self = this, doc = self.getDoc();
if (!self.hidden) {
// Fixed bug where IE has a blinking cursor left from the editor
if (ie && doc && !self.inline) {
doc.execCommand('SelectAll');
}
// We must save before we hide so Safari doesn't crash
self.save();
if (self.inline) {
self.getBody().contentEditable = false;
// Make sure the editor gets blurred
if (self == self.editorManager.focusedEditor) {
self.editorManager.focusedEditor = null;
}
} else {
DOM.hide(self.getContainer());
DOM.setStyle(self.id, 'display', self.orgDisplay);
}
self.hidden = true;
self.fire('hide');
}
},
/**
* Returns true/false if the editor is hidden or not.
*
* @method isHidden
* @return {Boolean} True/false if the editor is hidden or not.
*/
isHidden: function() {
return !!this.hidden;
},
/**
* Sets the progress state, this will display a throbber/progess for the editor.
* This is ideal for asynchronous operations like an AJAX save call.
*
* @method setProgressState
* @param {Boolean} state Boolean state if the progress should be shown or hidden.
* @param {Number} time Optional time to wait before the progress gets shown.
* @return {Boolean} Same as the input state.
* @example
* // Show progress for the active editor
* tinymce.activeEditor.setProgressState(true);
*
* // Hide progress for the active editor
* tinymce.activeEditor.setProgressState(false);
*
* // Show progress after 3 seconds
* tinymce.activeEditor.setProgressState(true, 3000);
*/
setProgressState: function(state, time) {
this.fire('ProgressState', {state: state, time: time});
},
/**
* Loads contents from the textarea or div element that got converted into an editor instance.
* This method will move the contents from that textarea or div into the editor by using setContent
* so all events etc that method has will get dispatched as well.
*
* @method load
* @param {Object} args Optional content object, this gets passed around through the whole load process.
* @return {String} HTML string that got set into the editor.
*/
load: function(args) {
var self = this, elm = self.getElement(), html;
if (elm) {
args = args || {};
args.load = true;
html = self.setContent(elm.value !== undefined ? elm.value : elm.innerHTML, args);
args.element = elm;
if (!args.no_events) {
self.fire('LoadContent', args);
}
args.element = elm = null;
return html;
}
},
/**
* Saves the contents from a editor out to the textarea or div element that got converted into an editor instance.
* This method will move the HTML contents from the editor into that textarea or div by getContent
* so all events etc that method has will get dispatched as well.
*
* @method save
* @param {Object} args Optional content object, this gets passed around through the whole save process.
* @return {String} HTML string that got set into the textarea/div.
*/
save: function(args) {
var self = this, elm = self.getElement(), html, form;
if (!elm || !self.initialized) {
return;
}
args = args || {};
args.save = true;
args.element = elm;
html = args.content = self.getContent(args);
if (!args.no_events) {
self.fire('SaveContent', args);
}
// Always run this internal event
if (args.format == 'raw') {
self.fire('RawSaveContent', args);
}
html = args.content;
if (!/TEXTAREA|INPUT/i.test(elm.nodeName)) {
// Update DIV element when not in inline mode
if (!self.inline) {
elm.innerHTML = html;
}
// Update hidden form element
if ((form = DOM.getParent(self.id, 'form'))) {
each(form.elements, function(elm) {
if (elm.name == self.id) {
elm.value = html;
return false;
}
});
}
} else {
elm.value = html;
}
args.element = elm = null;
if (args.set_dirty !== false) {
self.setDirty(false);
}
return html;
},
/**
* Sets the specified content to the editor instance, this will cleanup the content before it gets set using
* the different cleanup rules options.
*
* @method setContent
* @param {String} content Content to set to editor, normally HTML contents but can be other formats as well.
* @param {Object} args Optional content object, this gets passed around through the whole set process.
* @return {String} HTML string that got set into the editor.
* @example
* // Sets the HTML contents of the activeEditor editor
* tinymce.activeEditor.setContent('<span>some</span> html');
*
* // Sets the raw contents of the activeEditor editor
* tinymce.activeEditor.setContent('<span>some</span> html', {format: 'raw'});
*
* // Sets the content of a specific editor (my_editor in this example)
* tinymce.get('my_editor').setContent(data);
*
* // Sets the bbcode contents of the activeEditor editor if the bbcode plugin was added
* tinymce.activeEditor.setContent('[b]some[/b] html', {format: 'bbcode'});
*/
setContent: function(content, args) {
var self = this, body = self.getBody(), forcedRootBlockName, padd;
// Setup args object
args = args || {};
args.format = args.format || 'html';
args.set = true;
args.content = content;
// Do preprocessing
if (!args.no_events) {
self.fire('BeforeSetContent', args);
}
content = args.content;
// Padd empty content in Gecko and Safari. Commands will otherwise fail on the content
// It will also be impossible to place the caret in the editor unless there is a BR element present
if (content.length === 0 || /^\s+$/.test(content)) {
padd = ie && ie < 11 ? '' : '<br data-mce-bogus="1">';
// Todo: There is a lot more root elements that need special padding
// so separate this and add all of them at some point.
if (body.nodeName == 'TABLE') {
content = '<tr><td>' + padd + '</td></tr>';
} else if (/^(UL|OL)$/.test(body.nodeName)) {
content = '<li>' + padd + '</li>';
}
forcedRootBlockName = self.settings.forced_root_block;
// Check if forcedRootBlock is configured and that the block is a valid child of the body
if (forcedRootBlockName && self.schema.isValidChild(body.nodeName.toLowerCase(), forcedRootBlockName.toLowerCase())) {
// Padd with bogus BR elements on modern browsers and IE 7 and 8 since they don't render empty P tags properly
content = padd;
content = self.dom.createHTML(forcedRootBlockName, self.settings.forced_root_block_attrs, content);
} else if (!ie && !content) {
// We need to add a BR when forced_root_block is disabled on non IE browsers to place the caret
content = '<br data-mce-bogus="1">';
}
self.dom.setHTML(body, content);
self.fire('SetContent', args);
} else {
// Parse and serialize the html
if (args.format !== 'raw') {
content = new Serializer({
validate: self.validate
}, self.schema).serialize(
self.parser.parse(content, {isRootContent: true})
);
}
// Set the new cleaned contents to the editor
args.content = trim(content);
self.dom.setHTML(body, args.content);
// Do post processing
if (!args.no_events) {
self.fire('SetContent', args);
}
// Don't normalize selection if the focused element isn't the body in
// content editable mode since it will steal focus otherwise
/*if (!self.settings.content_editable || document.activeElement === self.getBody()) {
self.selection.normalize();
}*/
}
return args.content;
},
/**
* Gets the content from the editor instance, this will cleanup the content before it gets returned using
* the different cleanup rules options.
*
* @method getContent
* @param {Object} args Optional content object, this gets passed around through the whole get process.
* @return {String} Cleaned content string, normally HTML contents.
* @example
* // Get the HTML contents of the currently active editor
* console.debug(tinymce.activeEditor.getContent());
*
* // Get the raw contents of the currently active editor
* tinymce.activeEditor.getContent({format: 'raw'});
*
* // Get content of a specific editor:
* tinymce.get('content id').getContent()
*/
getContent: function(args) {
var self = this, content, body = self.getBody();
// Setup args object
args = args || {};
args.format = args.format || 'html';
args.get = true;
args.getInner = true;
// Do preprocessing
if (!args.no_events) {
self.fire('BeforeGetContent', args);
}
// Get raw contents or by default the cleaned contents
if (args.format == 'raw') {
content = self.serializer.getTrimmedContent();
} else if (args.format == 'text') {
content = body.innerText || body.textContent;
} else {
content = self.serializer.serialize(body, args);
}
// Trim whitespace in beginning/end of HTML
if (args.format != 'text') {
args.content = trim(content);
} else {
args.content = content;
}
// Do post processing
if (!args.no_events) {
self.fire('GetContent', args);
}
return args.content;
},
/**
* Inserts content at caret position.
*
* @method insertContent
* @param {String} content Content to insert.
* @param {Object} args Optional args to pass to insert call.
*/
insertContent: function(content, args) {
if (args) {
content = extend({content: content}, args);
}
this.execCommand('mceInsertContent', false, content);
},
/**
* Returns true/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents.
*
* The dirty state is automatically set to true if you do modifications to the content in other
* words when new undo levels is created or if you undo/redo to update the contents of the editor. It will also be set
* to false if you call editor.save().
*
* @method isDirty
* @return {Boolean} True/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents.
* @example
* if (tinymce.activeEditor.isDirty())
* alert("You must save your contents.");
*/
isDirty: function() {
return !this.isNotDirty;
},
/**
* Explicitly sets the dirty state. This will fire the dirty event if the editor dirty state is changed from false to true
* by invoking this method.
*
* @method setDirty
* @param {Boolean} state True/false if the editor is considered dirty.
* @example
* function ajaxSave() {
* var editor = tinymce.get('elm1');
*
* // Save contents using some XHR call
* alert(editor.getContent());
*
* editor.setDirty(false); // Force not dirty state
* }
*/
setDirty: function(state) {
var oldState = !this.isNotDirty;
this.isNotDirty = !state;
if (state && state != oldState) {
this.fire('dirty');
}
},
/**
* Sets the editor mode. Mode can be for example "design", "code" or "readonly".
*
* @method setMode
* @param {String} mode Mode to set the editor in.
*/
setMode: function(mode) {
Mode.setMode(this, mode);
},
/**
* Returns the editors container element. The container element wrappes in
* all the elements added to the page for the editor. Such as UI, iframe etc.
*
* @method getContainer
* @return {Element} HTML DOM element for the editor container.
*/
getContainer: function() {
var self = this;
if (!self.container) {
self.container = DOM.get(self.editorContainer || self.id + '_parent');
}
return self.container;
},
/**
* Returns the editors content area container element. The this element is the one who
* holds the iframe or the editable element.
*
* @method getContentAreaContainer
* @return {Element} HTML DOM element for the editor area container.
*/
getContentAreaContainer: function() {
return this.contentAreaContainer;
},
/**
* Returns the target element/textarea that got replaced with a TinyMCE editor instance.
*
* @method getElement
* @return {Element} HTML DOM element for the replaced element.
*/
getElement: function() {
if (!this.targetElm) {
this.targetElm = DOM.get(this.id);
}
return this.targetElm;
},
/**
* Returns the iframes window object.
*
* @method getWin
* @return {Window} Iframe DOM window object.
*/
getWin: function() {
var self = this, elm;
if (!self.contentWindow) {
elm = self.iframeElement;
if (elm) {
self.contentWindow = elm.contentWindow;
}
}
return self.contentWindow;
},
/**
* Returns the iframes document object.
*
* @method getDoc
* @return {Document} Iframe DOM document object.
*/
getDoc: function() {
var self = this, win;
if (!self.contentDocument) {
win = self.getWin();
if (win) {
self.contentDocument = win.document;
}
}
return self.contentDocument;
},
/**
* Returns the root element of the editable area.
* For a non-inline iframe-based editor, returns the iframe's body element.
*
* @method getBody
* @return {Element} The root element of the editable area.
*/
getBody: function() {
var doc = this.getDoc();
return this.bodyElement || (doc ? doc.body : null);
},
/**
* URL converter function this gets executed each time a user adds an img, a or
* any other element that has a URL in it. This will be called both by the DOM and HTML
* manipulation functions.
*
* @method convertURL
* @param {string} url URL to convert.
* @param {string} name Attribute name src, href etc.
* @param {string/HTMLElement} elm Tag name or HTML DOM element depending on HTML or DOM insert.
* @return {string} Converted URL string.
*/
convertURL: function(url, name, elm) {
var self = this, settings = self.settings;
// Use callback instead
if (settings.urlconverter_callback) {
return self.execCallback('urlconverter_callback', url, elm, true, name);
}
// Don't convert link href since thats the CSS files that gets loaded into the editor also skip local file URLs
if (!settings.convert_urls || (elm && elm.nodeName == 'LINK') || url.indexOf('file:') === 0 || url.length === 0) {
return url;
}
// Convert to relative
if (settings.relative_urls) {
return self.documentBaseURI.toRelative(url);
}
// Convert to absolute
url = self.documentBaseURI.toAbsolute(url, settings.remove_script_host);
return url;
},
/**
* Adds visual aid for tables, anchors etc so they can be more easily edited inside the editor.
*
* @method addVisual
* @param {Element} elm Optional root element to loop though to find tables etc that needs the visual aid.
*/
addVisual: function(elm) {
var self = this, settings = self.settings, dom = self.dom, cls;
elm = elm || self.getBody();
if (self.hasVisual === undefined) {
self.hasVisual = settings.visual;
}
each(dom.select('table,a', elm), function(elm) {
var value;
switch (elm.nodeName) {
case 'TABLE':
cls = settings.visual_table_class || 'mce-item-table';
value = dom.getAttrib(elm, 'border');
if ((!value || value == '0') && self.hasVisual) {
dom.addClass(elm, cls);
} else {
dom.removeClass(elm, cls);
}
return;
case 'A':
if (!dom.getAttrib(elm, 'href', false)) {
value = dom.getAttrib(elm, 'name') || elm.id;
cls = settings.visual_anchor_class || 'mce-item-anchor';
if (value && self.hasVisual) {
dom.addClass(elm, cls);
} else {
dom.removeClass(elm, cls);
}
}
return;
}
});
self.fire('VisualAid', {element: elm, hasVisual: self.hasVisual});
},
/**
* Removes the editor from the dom and tinymce collection.
*
* @method remove
*/
remove: function() {
var self = this;
if (!self.removed) {
self.save();
self.removed = 1;
self.unbindAllNativeEvents();
// Remove any hidden input
if (self.hasHiddenInput) {
DOM.remove(self.getElement().nextSibling);
}
if (!self.inline) {
// IE 9 has a bug where the selection stops working if you place the
// caret inside the editor then remove the iframe
if (ie && ie < 10) {
self.getDoc().execCommand('SelectAll', false, null);
}
DOM.setStyle(self.id, 'display', self.orgDisplay);
self.getBody().onload = null; // Prevent #6816
}
self.fire('remove');
self.editorManager.remove(self);
DOM.remove(self.getContainer());
self._selectionOverrides.destroy();
self.editorUpload.destroy();
self.destroy();
}
},
/**
* Destroys the editor instance by removing all events, element references or other resources
* that could leak memory. This method will be called automatically when the page is unloaded
* but you can also call it directly if you know what you are doing.
*
* @method destroy
* @param {Boolean} automatic Optional state if the destroy is an automatic destroy or user called one.
*/
destroy: function(automatic) {
var self = this, form;
// One time is enough
if (self.destroyed) {
return;
}
// If user manually calls destroy and not remove
// Users seems to have logic that calls destroy instead of remove
if (!automatic && !self.removed) {
self.remove();
return;
}
if (!automatic) {
self.editorManager.off('beforeunload', self._beforeUnload);
// Manual destroy
if (self.theme && self.theme.destroy) {
self.theme.destroy();
}
// Destroy controls, selection and dom
self.selection.destroy();
self.dom.destroy();
}
form = self.formElement;
if (form) {
if (form._mceOldSubmit) {
form.submit = form._mceOldSubmit;
form._mceOldSubmit = null;
}
DOM.unbind(form, 'submit reset', self.formEventDelegate);
}
self.contentAreaContainer = self.formElement = self.container = self.editorContainer = null;
self.bodyElement = self.contentDocument = self.contentWindow = null;
self.iframeElement = self.targetElm = null;
if (self.selection) {
self.selection = self.selection.win = self.selection.dom = self.selection.dom.doc = null;
}
self.destroyed = 1;
},
/**
* Uploads all data uri/blob uri images in the editor contents to server.
*
* @method uploadImages
* @param {function} callback Optional callback with images and status for each image.
* @return {tinymce.util.Promise} Promise instance.
*/
uploadImages: function(callback) {
return this.editorUpload.uploadImages(callback);
},
// Internal functions
_scanForImages: function() {
return this.editorUpload.scanForImages();
}
};
extend(Editor.prototype, EditorObservable);
return Editor;
});
// Included from: js/tinymce/classes/util/I18n.js
/**
* I18n.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* I18n class that handles translation of TinyMCE UI.
* Uses po style with csharp style parameters.
*
* @class tinymce.util.I18n
*/
define("tinymce/util/I18n", [
"tinymce/util/Tools"
], function(Tools) {
"use strict";
var data = {}, code = "en";
return {
/**
* Sets the current language code.
*
* @method setCode
* @param {String} newCode Current language code.
*/
setCode: function(newCode) {
if (newCode) {
code = newCode;
this.rtl = this.data[newCode] ? this.data[newCode]._dir === 'rtl' : false;
}
},
/**
* Returns the current language code.
*
* @method getCode
* @return {String} Current language code.
*/
getCode: function() {
return code;
},
/**
* Property gets set to true if a RTL language pack was loaded.
*
* @property rtl
* @type Boolean
*/
rtl: false,
/**
* Adds translations for a specific language code.
*
* @method add
* @param {String} code Language code like sv_SE.
* @param {Array} items Name/value array with English en_US to sv_SE.
*/
add: function(code, items) {
var langData = data[code];
if (!langData) {
data[code] = langData = {};
}
for (var name in items) {
langData[name] = items[name];
}
this.setCode(code);
},
/**
* Translates the specified text.
*
* It has a few formats:
* I18n.translate("Text");
* I18n.translate(["Text {0}/{1}", 0, 1]);
* I18n.translate({raw: "Raw string"});
*
* @method translate
* @param {String/Object/Array} text Text to translate.
* @return {String} String that got translated.
*/
translate: function(text) {
var langData = data[code] || {};
/**
* number - string
* null, undefined and empty string - empty string
* array - comma-delimited string
* object - in [object Object]
* function - in [object Function]
*
* @param obj
* @returns {string}
*/
function toString(obj) {
if (Tools.is(obj, 'function')) {
return Object.prototype.toString.call(obj);
}
return !isEmpty(obj) ? '' + obj : '';
}
function isEmpty(text) {
return text === '' || text === null || Tools.is(text, 'undefined');
}
function getLangData(text) {
// make sure we work on a string and return a string
text = toString(text);
return Tools.hasOwn(langData, text) ? toString(langData[text]) : text;
}
if (isEmpty(text)) {
return '';
}
if (Tools.is(text, 'object') && Tools.hasOwn(text, 'raw')) {
return toString(text.raw);
}
if (Tools.is(text, 'array')) {
var values = text.slice(1);
text = getLangData(text[0]).replace(/\{([0-9]+)\}/g, function($1, $2) {
return Tools.hasOwn(values, $2) ? toString(values[$2]) : $1;
});
}
return getLangData(text).replace(/{context:\w+}$/, '');
},
data: data
};
});
// Included from: js/tinymce/classes/FocusManager.js
/**
* FocusManager.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class manages the focus/blur state of the editor. This class is needed since some
* browsers fire false focus/blur states when the selection is moved to a UI dialog or similar.
*
* This class will fire two events focus and blur on the editor instances that got affected.
* It will also handle the restore of selection when the focus is lost and returned.
*
* @class tinymce.FocusManager
*/
define("tinymce/FocusManager", [
"tinymce/dom/DOMUtils",
"tinymce/util/Delay",
"tinymce/Env"
], function(DOMUtils, Delay, Env) {
var selectionChangeHandler, documentFocusInHandler, documentMouseUpHandler, DOM = DOMUtils.DOM;
/**
* Constructs a new focus manager instance.
*
* @constructor FocusManager
* @param {tinymce.EditorManager} editorManager Editor manager instance to handle focus for.
*/
function FocusManager(editorManager) {
function getActiveElement() {
try {
return document.activeElement;
} catch (ex) {
// IE sometimes fails to get the activeElement when resizing table
// TODO: Investigate this
return document.body;
}
}
// We can't store a real range on IE 11 since it gets mutated so we need to use a bookmark object
// TODO: Move this to a separate range utils class since it's it's logic is present in Selection as well.
function createBookmark(dom, rng) {
if (rng && rng.startContainer) {
// Verify that the range is within the root of the editor
if (!dom.isChildOf(rng.startContainer, dom.getRoot()) || !dom.isChildOf(rng.endContainer, dom.getRoot())) {
return;
}
return {
startContainer: rng.startContainer,
startOffset: rng.startOffset,
endContainer: rng.endContainer,
endOffset: rng.endOffset
};
}
return rng;
}
function bookmarkToRng(editor, bookmark) {
var rng;
if (bookmark.startContainer) {
rng = editor.getDoc().createRange();
rng.setStart(bookmark.startContainer, bookmark.startOffset);
rng.setEnd(bookmark.endContainer, bookmark.endOffset);
} else {
rng = bookmark;
}
return rng;
}
function isUIElement(elm) {
return !!DOM.getParent(elm, FocusManager.isEditorUIElement);
}
function registerEvents(e) {
var editor = e.editor;
editor.on('init', function() {
// Gecko/WebKit has ghost selections in iframes and IE only has one selection per browser tab
if (editor.inline || Env.ie) {
// Use the onbeforedeactivate event when available since it works better see #7023
if ("onbeforedeactivate" in document && Env.ie < 9) {
editor.dom.bind(editor.getBody(), 'beforedeactivate', function(e) {
if (e.target != editor.getBody()) {
return;
}
try {
editor.lastRng = editor.selection.getRng();
} catch (ex) {
// IE throws "Unexcpected call to method or property access" some times so lets ignore it
}
});
} else {
// On other browsers take snapshot on nodechange in inline mode since they have Ghost selections for iframes
editor.on('nodechange mouseup keyup', function(e) {
var node = getActiveElement();
// Only act on manual nodechanges
if (e.type == 'nodechange' && e.selectionChange) {
return;
}
// IE 11 reports active element as iframe not body of iframe
if (node && node.id == editor.id + '_ifr') {
node = editor.getBody();
}
if (editor.dom.isChildOf(node, editor.getBody())) {
editor.lastRng = editor.selection.getRng();
}
});
}
// Handles the issue with WebKit not retaining selection within inline document
// If the user releases the mouse out side the body since a mouse up event wont occur on the body
if (Env.webkit && !selectionChangeHandler) {
selectionChangeHandler = function() {
var activeEditor = editorManager.activeEditor;
if (activeEditor && activeEditor.selection) {
var rng = activeEditor.selection.getRng();
// Store when it's non collapsed
if (rng && !rng.collapsed) {
editor.lastRng = rng;
}
}
};
DOM.bind(document, 'selectionchange', selectionChangeHandler);
}
}
});
editor.on('setcontent', function() {
editor.lastRng = null;
});
// Remove last selection bookmark on mousedown see #6305
editor.on('mousedown', function() {
editor.selection.lastFocusBookmark = null;
});
editor.on('focusin', function() {
var focusedEditor = editorManager.focusedEditor, lastRng;
if (editor.selection.lastFocusBookmark) {
lastRng = bookmarkToRng(editor, editor.selection.lastFocusBookmark);
editor.selection.lastFocusBookmark = null;
editor.selection.setRng(lastRng);
}
if (focusedEditor != editor) {
if (focusedEditor) {
focusedEditor.fire('blur', {focusedEditor: editor});
}
editorManager.setActive(editor);
editorManager.focusedEditor = editor;
editor.fire('focus', {blurredEditor: focusedEditor});
editor.focus(true);
}
editor.lastRng = null;
});
editor.on('focusout', function() {
Delay.setEditorTimeout(editor, function() {
var focusedEditor = editorManager.focusedEditor;
// Still the same editor the blur was outside any editor UI
if (!isUIElement(getActiveElement()) && focusedEditor == editor) {
editor.fire('blur', {focusedEditor: null});
editorManager.focusedEditor = null;
// Make sure selection is valid could be invalid if the editor is blured and removed before the timeout occurs
if (editor.selection) {
editor.selection.lastFocusBookmark = null;
}
}
});
});
// Check if focus is moved to an element outside the active editor by checking if the target node
// isn't within the body of the activeEditor nor a UI element such as a dialog child control
if (!documentFocusInHandler) {
documentFocusInHandler = function(e) {
var activeEditor = editorManager.activeEditor, target;
target = e.target;
if (activeEditor && target.ownerDocument == document) {
// Check to make sure we have a valid selection don't update the bookmark if it's
// a focusin to the body of the editor see #7025
if (activeEditor.selection && target != activeEditor.getBody()) {
activeEditor.selection.lastFocusBookmark = createBookmark(activeEditor.dom, activeEditor.lastRng);
}
// Fire a blur event if the element isn't a UI element
if (target != document.body && !isUIElement(target) && editorManager.focusedEditor == activeEditor) {
activeEditor.fire('blur', {focusedEditor: null});
editorManager.focusedEditor = null;
}
}
};
DOM.bind(document, 'focusin', documentFocusInHandler);
}
// Handle edge case when user starts the selection inside the editor and releases
// the mouse outside the editor producing a new selection. This weird workaround is needed since
// Gecko doesn't have the "selectionchange" event we need to do this. Fixes: #6843
if (editor.inline && !documentMouseUpHandler) {
documentMouseUpHandler = function(e) {
var activeEditor = editorManager.activeEditor, dom = activeEditor.dom;
if (activeEditor.inline && dom && !dom.isChildOf(e.target, activeEditor.getBody())) {
var rng = activeEditor.selection.getRng();
if (!rng.collapsed) {
activeEditor.lastRng = rng;
}
}
};
DOM.bind(document, 'mouseup', documentMouseUpHandler);
}
}
function unregisterDocumentEvents(e) {
if (editorManager.focusedEditor == e.editor) {
editorManager.focusedEditor = null;
}
if (!editorManager.activeEditor) {
DOM.unbind(document, 'selectionchange', selectionChangeHandler);
DOM.unbind(document, 'focusin', documentFocusInHandler);
DOM.unbind(document, 'mouseup', documentMouseUpHandler);
selectionChangeHandler = documentFocusInHandler = documentMouseUpHandler = null;
}
}
editorManager.on('AddEditor', registerEvents);
editorManager.on('RemoveEditor', unregisterDocumentEvents);
}
/**
* Returns true if the specified element is part of the UI for example an button or text input.
*
* @method isEditorUIElement
* @param {Element} elm Element to check if it's part of the UI or not.
* @return {Boolean} True/false state if the element is part of the UI or not.
*/
FocusManager.isEditorUIElement = function(elm) {
// Needs to be converted to string since svg can have focus: #6776
return elm.className.toString().indexOf('mce-') !== -1;
};
return FocusManager;
});
// Included from: js/tinymce/classes/EditorManager.js
/**
* EditorManager.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class used as a factory for manager for tinymce.Editor instances.
*
* @example
* tinymce.EditorManager.init({});
*
* @class tinymce.EditorManager
* @mixes tinymce.util.Observable
* @static
*/
define("tinymce/EditorManager", [
"tinymce/Editor",
"tinymce/dom/DomQuery",
"tinymce/dom/DOMUtils",
"tinymce/util/URI",
"tinymce/Env",
"tinymce/util/Tools",
"tinymce/util/Promise",
"tinymce/util/Observable",
"tinymce/util/I18n",
"tinymce/FocusManager",
"tinymce/AddOnManager"
], function(Editor, $, DOMUtils, URI, Env, Tools, Promise, Observable, I18n, FocusManager, AddOnManager) {
var DOM = DOMUtils.DOM;
var explode = Tools.explode, each = Tools.each, extend = Tools.extend;
var instanceCounter = 0, beforeUnloadDelegate, EditorManager, boundGlobalEvents = false;
function globalEventDelegate(e) {
each(EditorManager.editors, function(editor) {
if (e.type === 'scroll') {
editor.fire('ScrollWindow', e);
} else {
editor.fire('ResizeWindow', e);
}
});
}
function toggleGlobalEvents(editors, state) {
if (state !== boundGlobalEvents) {
if (state) {
$(window).on('resize scroll', globalEventDelegate);
} else {
$(window).off('resize scroll', globalEventDelegate);
}
boundGlobalEvents = state;
}
}
function removeEditorFromList(editor) {
var editors = EditorManager.editors, removedFromList;
delete editors[editor.id];
for (var i = 0; i < editors.length; i++) {
if (editors[i] == editor) {
editors.splice(i, 1);
removedFromList = true;
break;
}
}
// Select another editor since the active one was removed
if (EditorManager.activeEditor == editor) {
EditorManager.activeEditor = editors[0];
}
// Clear focusedEditor if necessary, so that we don't try to blur the destroyed editor
if (EditorManager.focusedEditor == editor) {
EditorManager.focusedEditor = null;
}
return removedFromList;
}
function purgeDestroyedEditor(editor) {
// User has manually destroyed the editor lets clean up the mess
if (editor && editor.initialized && !(editor.getContainer() || editor.getBody()).parentNode) {
removeEditorFromList(editor);
editor.unbindAllNativeEvents();
editor.destroy(true);
editor.removed = true;
editor = null;
}
return editor;
}
EditorManager = {
/**
* Dom query instance.
*
* @property $
* @type tinymce.dom.DomQuery
*/
$: $,
/**
* Major version of TinyMCE build.
*
* @property majorVersion
* @type String
*/
majorVersion: '4',
/**
* Minor version of TinyMCE build.
*
* @property minorVersion
* @type String
*/
minorVersion: '5.1',
/**
* Release date of TinyMCE build.
*
* @property releaseDate
* @type String
*/
releaseDate: '2016-12-07',
/**
* Collection of editor instances.
*
* @property editors
* @type Object
* @example
* for (edId in tinymce.editors)
* tinymce.editors[edId].save();
*/
editors: [],
/**
* Collection of language pack data.
*
* @property i18n
* @type Object
*/
i18n: I18n,
/**
* Currently active editor instance.
*
* @property activeEditor
* @type tinymce.Editor
* @example
* tinyMCE.activeEditor.selection.getContent();
* tinymce.EditorManager.activeEditor.selection.getContent();
*/
activeEditor: null,
setup: function() {
var self = this, baseURL, documentBaseURL, suffix = "", preInit, src;
// Get base URL for the current document
documentBaseURL = URI.getDocumentBaseUrl(document.location);
// Check if the URL is a document based format like: http://site/dir/file and file:///
// leave other formats like applewebdata://... intact
if (/^[^:]+:\/\/\/?[^\/]+\//.test(documentBaseURL)) {
documentBaseURL = documentBaseURL.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, '');
if (!/[\/\\]$/.test(documentBaseURL)) {
documentBaseURL += '/';
}
}
// If tinymce is defined and has a base use that or use the old tinyMCEPreInit
preInit = window.tinymce || window.tinyMCEPreInit;
if (preInit) {
baseURL = preInit.base || preInit.baseURL;
suffix = preInit.suffix;
} else {
// Get base where the tinymce script is located
var scripts = document.getElementsByTagName('script');
for (var i = 0; i < scripts.length; i++) {
src = scripts[i].src;
// Script types supported:
// tinymce.js tinymce.min.js tinymce.dev.js
// tinymce.jquery.js tinymce.jquery.min.js tinymce.jquery.dev.js
// tinymce.full.js tinymce.full.min.js tinymce.full.dev.js
var srcScript = src.substring(src.lastIndexOf('/'));
if (/tinymce(\.full|\.jquery|)(\.min|\.dev|)\.js/.test(src)) {
if (srcScript.indexOf('.min') != -1) {
suffix = '.min';
}
baseURL = src.substring(0, src.lastIndexOf('/'));
break;
}
}
// We didn't find any baseURL by looking at the script elements
// Try to use the document.currentScript as a fallback
if (!baseURL && document.currentScript) {
src = document.currentScript.src;
if (src.indexOf('.min') != -1) {
suffix = '.min';
}
baseURL = src.substring(0, src.lastIndexOf('/'));
}
}
/**
* Base URL where the root directory if TinyMCE is located.
*
* @property baseURL
* @type String
*/
self.baseURL = new URI(documentBaseURL).toAbsolute(baseURL);
/**
* Document base URL where the current document is located.
*
* @property documentBaseURL
* @type String
*/
self.documentBaseURL = documentBaseURL;
/**
* Absolute baseURI for the installation path of TinyMCE.
*
* @property baseURI
* @type tinymce.util.URI
*/
self.baseURI = new URI(self.baseURL);
/**
* Current suffix to add to each plugin/theme that gets loaded for example ".min".
*
* @property suffix
* @type String
*/
self.suffix = suffix;
self.focusManager = new FocusManager(self);
},
/**
* Overrides the default settings for editor instances.
*
* @method overrideDefaults
* @param {Object} defaultSettings Defaults settings object.
*/
overrideDefaults: function(defaultSettings) {
var baseUrl, suffix;
baseUrl = defaultSettings.base_url;
if (baseUrl) {
this.baseURL = new URI(this.documentBaseURL).toAbsolute(baseUrl.replace(/\/+$/, ''));
this.baseURI = new URI(this.baseURL);
}
suffix = defaultSettings.suffix;
if (defaultSettings.suffix) {
this.suffix = suffix;
}
this.defaultSettings = defaultSettings;
var pluginBaseUrls = defaultSettings.plugin_base_urls;
for (var name in pluginBaseUrls) {
AddOnManager.PluginManager.urls[name] = pluginBaseUrls[name];
}
},
/**
* Initializes a set of editors. This method will create editors based on various settings.
*
* @method init
* @param {Object} settings Settings object to be passed to each editor instance.
* @return {tinymce.util.Promise} Promise that gets resolved with an array of editors when all editor instances are initialized.
* @example
* // Initializes a editor using the longer method
* tinymce.EditorManager.init({
* some_settings : 'some value'
* });
*
* // Initializes a editor instance using the shorter version and with a promise
* tinymce.init({
* some_settings : 'some value'
* }).then(function(editors) {
* ...
* });
*/
init: function(settings) {
var self = this, result, invalidInlineTargets;
invalidInlineTargets = Tools.makeMap(
'area base basefont br col frame hr img input isindex link meta param embed source wbr track ' +
'colgroup option tbody tfoot thead tr script noscript style textarea video audio iframe object menu',
' '
);
function isInvalidInlineTarget(settings, elm) {
return settings.inline && elm.tagName.toLowerCase() in invalidInlineTargets;
}
function report(msg, elm) {
// Log in a non test environment
if (window.console && !window.test) {
window.console.log(msg, elm);
}
}
function createId(elm) {
var id = elm.id;
// Use element id, or unique name or generate a unique id
if (!id) {
id = elm.name;
if (id && !DOM.get(id)) {
id = elm.name;
} else {
// Generate unique name
id = DOM.uniqueId();
}
elm.setAttribute('id', id);
}
return id;
}
function execCallback(name) {
var callback = settings[name];
if (!callback) {
return;
}
return callback.apply(self, Array.prototype.slice.call(arguments, 2));
}
function hasClass(elm, className) {
return className.constructor === RegExp ? className.test(elm.className) : DOM.hasClass(elm, className);
}
function findTargets(settings) {
var l, targets = [];
if (settings.types) {
each(settings.types, function(type) {
targets = targets.concat(DOM.select(type.selector));
});
return targets;
} else if (settings.selector) {
return DOM.select(settings.selector);
} else if (settings.target) {
return [settings.target];
}
// Fallback to old setting
switch (settings.mode) {
case "exact":
l = settings.elements || '';
if (l.length > 0) {
each(explode(l), function(id) {
var elm;
if ((elm = DOM.get(id))) {
targets.push(elm);
} else {
each(document.forms, function(f) {
each(f.elements, function(e) {
if (e.name === id) {
id = 'mce_editor_' + instanceCounter++;
DOM.setAttrib(e, 'id', id);
targets.push(e);
}
});
});
}
});
}
break;
case "textareas":
case "specific_textareas":
each(DOM.select('textarea'), function(elm) {
if (settings.editor_deselector && hasClass(elm, settings.editor_deselector)) {
return;
}
if (!settings.editor_selector || hasClass(elm, settings.editor_selector)) {
targets.push(elm);
}
});
break;
}
return targets;
}
var provideResults = function(editors) {
result = editors;
};
function initEditors() {
var initCount = 0, editors = [], targets;
function createEditor(id, settings, targetElm) {
var editor = new Editor(id, settings, self);
editors.push(editor);
editor.on('init', function() {
if (++initCount === targets.length) {
provideResults(editors);
}
});
editor.targetElm = editor.targetElm || targetElm;
editor.render();
}
DOM.unbind(window, 'ready', initEditors);
execCallback('onpageload');
targets = $.unique(findTargets(settings));
// TODO: Deprecate this one
if (settings.types) {
each(settings.types, function(type) {
Tools.each(targets, function(elm) {
if (DOM.is(elm, type.selector)) {
createEditor(createId(elm), extend({}, settings, type), elm);
return false;
}
return true;
});
});
return;
}
Tools.each(targets, function(elm) {
purgeDestroyedEditor(self.get(elm.id));
});
targets = Tools.grep(targets, function(elm) {
return !self.get(elm.id);
});
each(targets, function(elm) {
if (isInvalidInlineTarget(settings, elm)) {
report('Could not initialize inline editor on invalid inline target element', elm);
} else {
createEditor(createId(elm), settings, elm);
}
});
}
self.settings = settings;
DOM.bind(window, 'ready', initEditors);
return new Promise(function(resolve) {
if (result) {
resolve(result);
} else {
provideResults = function(editors) {
resolve(editors);
};
}
});
},
/**
* Returns a editor instance by id.
*
* @method get
* @param {String/Number} id Editor instance id or index to return.
* @return {tinymce.Editor} Editor instance to return.
* @example
* // Adds an onclick event to an editor by id (shorter version)
* tinymce.get('mytextbox').on('click', function(e) {
* ed.windowManager.alert('Hello world!');
* });
*
* // Adds an onclick event to an editor by id (longer version)
* tinymce.EditorManager.get('mytextbox').on('click', function(e) {
* ed.windowManager.alert('Hello world!');
* });
*/
get: function(id) {
if (!arguments.length) {
return this.editors;
}
return id in this.editors ? this.editors[id] : null;
},
/**
* Adds an editor instance to the editor collection. This will also set it as the active editor.
*
* @method add
* @param {tinymce.Editor} editor Editor instance to add to the collection.
* @return {tinymce.Editor} The same instance that got passed in.
*/
add: function(editor) {
var self = this, editors = self.editors;
// Add named and index editor instance
editors[editor.id] = editor;
editors.push(editor);
toggleGlobalEvents(editors, true);
// Doesn't call setActive method since we don't want
// to fire a bunch of activate/deactivate calls while initializing
self.activeEditor = editor;
self.fire('AddEditor', {editor: editor});
if (!beforeUnloadDelegate) {
beforeUnloadDelegate = function() {
self.fire('BeforeUnload');
};
DOM.bind(window, 'beforeunload', beforeUnloadDelegate);
}
return editor;
},
/**
* Creates an editor instance and adds it to the EditorManager collection.
*
* @method createEditor
* @param {String} id Instance id to use for editor.
* @param {Object} settings Editor instance settings.
* @return {tinymce.Editor} Editor instance that got created.
*/
createEditor: function(id, settings) {
return this.add(new Editor(id, settings, this));
},
/**
* Removes a editor or editors form page.
*
* @example
* // Remove all editors bound to divs
* tinymce.remove('div');
*
* // Remove all editors bound to textareas
* tinymce.remove('textarea');
*
* // Remove all editors
* tinymce.remove();
*
* // Remove specific instance by id
* tinymce.remove('#id');
*
* @method remove
* @param {tinymce.Editor/String/Object} [selector] CSS selector or editor instance to remove.
* @return {tinymce.Editor} The editor that got passed in will be return if it was found otherwise null.
*/
remove: function(selector) {
var self = this, i, editors = self.editors, editor;
// Remove all editors
if (!selector) {
for (i = editors.length - 1; i >= 0; i--) {
self.remove(editors[i]);
}
return;
}
// Remove editors by selector
if (typeof selector == "string") {
selector = selector.selector || selector;
each(DOM.select(selector), function(elm) {
editor = editors[elm.id];
if (editor) {
self.remove(editor);
}
});
return;
}
// Remove specific editor
editor = selector;
// Not in the collection
if (!editors[editor.id]) {
return null;
}
if (removeEditorFromList(editor)) {
self.fire('RemoveEditor', {editor: editor});
}
if (!editors.length) {
DOM.unbind(window, 'beforeunload', beforeUnloadDelegate);
}
editor.remove();
toggleGlobalEvents(editors, editors.length > 0);
return editor;
},
/**
* Executes a specific command on the currently active editor.
*
* @method execCommand
* @param {String} cmd Command to perform for example Bold.
* @param {Boolean} ui Optional boolean state if a UI should be presented for the command or not.
* @param {String} value Optional value parameter like for example an URL to a link.
* @return {Boolean} true/false if the command was executed or not.
*/
execCommand: function(cmd, ui, value) {
var self = this, editor = self.get(value);
// Manager commands
switch (cmd) {
case "mceAddEditor":
if (!self.get(value)) {
new Editor(value, self.settings, self).render();
}
return true;
case "mceRemoveEditor":
if (editor) {
editor.remove();
}
return true;
case 'mceToggleEditor':
if (!editor) {
self.execCommand('mceAddEditor', 0, value);
return true;
}
if (editor.isHidden()) {
editor.show();
} else {
editor.hide();
}
return true;
}
// Run command on active editor
if (self.activeEditor) {
return self.activeEditor.execCommand(cmd, ui, value);
}
return false;
},
/**
* Calls the save method on all editor instances in the collection. This can be useful when a form is to be submitted.
*
* @method triggerSave
* @example
* // Saves all contents
* tinyMCE.triggerSave();
*/
triggerSave: function() {
each(this.editors, function(editor) {
editor.save();
});
},
/**
* Adds a language pack, this gets called by the loaded language files like en.js.
*
* @method addI18n
* @param {String} code Optional language code.
* @param {Object} items Name/value object with translations.
*/
addI18n: function(code, items) {
I18n.add(code, items);
},
/**
* Translates the specified string using the language pack items.
*
* @method translate
* @param {String/Array/Object} text String to translate
* @return {String} Translated string.
*/
translate: function(text) {
return I18n.translate(text);
},
/**
* Sets the active editor instance and fires the deactivate/activate events.
*
* @method setActive
* @param {tinymce.Editor} editor Editor instance to set as the active instance.
*/
setActive: function(editor) {
var activeEditor = this.activeEditor;
if (this.activeEditor != editor) {
if (activeEditor) {
activeEditor.fire('deactivate', {relatedTarget: editor});
}
editor.fire('activate', {relatedTarget: activeEditor});
}
this.activeEditor = editor;
}
};
extend(EditorManager, Observable);
EditorManager.setup();
// Export EditorManager as tinymce/tinymce in global namespace
window.tinymce = window.tinyMCE = EditorManager;
return EditorManager;
});
// Included from: js/tinymce/classes/LegacyInput.js
/**
* LegacyInput.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Converts legacy input to modern HTML.
*
* @class tinymce.LegacyInput
* @private
*/
define("tinymce/LegacyInput", [
"tinymce/EditorManager",
"tinymce/util/Tools"
], function(EditorManager, Tools) {
var each = Tools.each, explode = Tools.explode;
EditorManager.on('AddEditor', function(e) {
var editor = e.editor;
editor.on('preInit', function() {
var filters, fontSizes, dom, settings = editor.settings;
function replaceWithSpan(node, styles) {
each(styles, function(value, name) {
if (value) {
dom.setStyle(node, name, value);
}
});
dom.rename(node, 'span');
}
function convert(e) {
dom = editor.dom;
if (settings.convert_fonts_to_spans) {
each(dom.select('font,u,strike', e.node), function(node) {
filters[node.nodeName.toLowerCase()](dom, node);
});
}
}
if (settings.inline_styles) {
fontSizes = explode(settings.font_size_legacy_values);
filters = {
font: function(dom, node) {
replaceWithSpan(node, {
backgroundColor: node.style.backgroundColor,
color: node.color,
fontFamily: node.face,
fontSize: fontSizes[parseInt(node.size, 10) - 1]
});
},
u: function(dom, node) {
// HTML5 allows U element
if (editor.settings.schema === "html4") {
replaceWithSpan(node, {
textDecoration: 'underline'
});
}
},
strike: function(dom, node) {
replaceWithSpan(node, {
textDecoration: 'line-through'
});
}
};
editor.on('PreProcess SetContent', convert);
}
});
});
});
// Included from: js/tinymce/classes/util/XHR.js
/**
* XHR.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class enables you to send XMLHTTPRequests cross browser.
* @class tinymce.util.XHR
* @mixes tinymce.util.Observable
* @static
* @example
* // Sends a low level Ajax request
* tinymce.util.XHR.send({
* url: 'someurl',
* success: function(text) {
* console.debug(text);
* }
* });
*
* // Add custom header to XHR request
* tinymce.util.XHR.on('beforeSend', function(e) {
* e.xhr.setRequestHeader('X-Requested-With', 'Something');
* });
*/
define("tinymce/util/XHR", [
"tinymce/util/Observable",
"tinymce/util/Tools"
], function(Observable, Tools) {
var XHR = {
/**
* Sends a XMLHTTPRequest.
* Consult the Wiki for details on what settings this method takes.
*
* @method send
* @param {Object} settings Object will target URL, callbacks and other info needed to make the request.
*/
send: function(settings) {
var xhr, count = 0;
function ready() {
if (!settings.async || xhr.readyState == 4 || count++ > 10000) {
if (settings.success && count < 10000 && xhr.status == 200) {
settings.success.call(settings.success_scope, '' + xhr.responseText, xhr, settings);
} else if (settings.error) {
settings.error.call(settings.error_scope, count > 10000 ? 'TIMED_OUT' : 'GENERAL', xhr, settings);
}
xhr = null;
} else {
setTimeout(ready, 10);
}
}
// Default settings
settings.scope = settings.scope || this;
settings.success_scope = settings.success_scope || settings.scope;
settings.error_scope = settings.error_scope || settings.scope;
settings.async = settings.async === false ? false : true;
settings.data = settings.data || '';
XHR.fire('beforeInitialize', {settings: settings});
xhr = new XMLHttpRequest();
if (xhr) {
if (xhr.overrideMimeType) {
xhr.overrideMimeType(settings.content_type);
}
xhr.open(settings.type || (settings.data ? 'POST' : 'GET'), settings.url, settings.async);
if (settings.crossDomain) {
xhr.withCredentials = true;
}
if (settings.content_type) {
xhr.setRequestHeader('Content-Type', settings.content_type);
}
if (settings.requestheaders) {
Tools.each(settings.requestheaders, function(header) {
xhr.setRequestHeader(header.key, header.value);
});
}
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr = XHR.fire('beforeSend', {xhr: xhr, settings: settings}).xhr;
xhr.send(settings.data);
// Syncronous request
if (!settings.async) {
return ready();
}
// Wait for response, onReadyStateChange can not be used since it leaks memory in IE
setTimeout(ready, 10);
}
}
};
Tools.extend(XHR, Observable);
return XHR;
});
// Included from: js/tinymce/classes/util/JSON.js
/**
* JSON.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* JSON parser and serializer class.
*
* @class tinymce.util.JSON
* @static
* @example
* // JSON parse a string into an object
* var obj = tinymce.util.JSON.parse(somestring);
*
* // JSON serialize a object into an string
* var str = tinymce.util.JSON.serialize(obj);
*/
define("tinymce/util/JSON", [], function() {
function serialize(o, quote) {
var i, v, t, name;
quote = quote || '"';
if (o === null) {
return 'null';
}
t = typeof o;
if (t == 'string') {
v = '\bb\tt\nn\ff\rr\""\'\'\\\\';
/*eslint no-control-regex:0 */
return quote + o.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g, function(a, b) {
// Make sure single quotes never get encoded inside double quotes for JSON compatibility
if (quote === '"' && a === "'") {
return a;
}
i = v.indexOf(b);
if (i + 1) {
return '\\' + v.charAt(i + 1);
}
a = b.charCodeAt().toString(16);
return '\\u' + '0000'.substring(a.length) + a;
}) + quote;
}
if (t == 'object') {
if (o.hasOwnProperty && Object.prototype.toString.call(o) === '[object Array]') {
for (i = 0, v = '['; i < o.length; i++) {
v += (i > 0 ? ',' : '') + serialize(o[i], quote);
}
return v + ']';
}
v = '{';
for (name in o) {
if (o.hasOwnProperty(name)) {
v += typeof o[name] != 'function' ? (v.length > 1 ? ',' + quote : quote) + name +
quote + ':' + serialize(o[name], quote) : '';
}
}
return v + '}';
}
return '' + o;
}
return {
/**
* Serializes the specified object as a JSON string.
*
* @method serialize
* @param {Object} obj Object to serialize as a JSON string.
* @param {String} quote Optional quote string defaults to ".
* @return {string} JSON string serialized from input.
*/
serialize: serialize,
/**
* Unserializes/parses the specified JSON string into a object.
*
* @method parse
* @param {string} s JSON String to parse into a JavaScript object.
* @return {Object} Object from input JSON string or undefined if it failed.
*/
parse: function(text) {
try {
// Trick uglify JS
return window[String.fromCharCode(101) + 'val']('(' + text + ')');
} catch (ex) {
// Ignore
}
}
/**#@-*/
};
});
// Included from: js/tinymce/classes/util/JSONRequest.js
/**
* JSONRequest.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class enables you to use JSON-RPC to call backend methods.
*
* @class tinymce.util.JSONRequest
* @example
* var json = new tinymce.util.JSONRequest({
* url: 'somebackend.php'
* });
*
* // Send RPC call 1
* json.send({
* method: 'someMethod1',
* params: ['a', 'b'],
* success: function(result) {
* console.dir(result);
* }
* });
*
* // Send RPC call 2
* json.send({
* method: 'someMethod2',
* params: ['a', 'b'],
* success: function(result) {
* console.dir(result);
* }
* });
*/
define("tinymce/util/JSONRequest", [
"tinymce/util/JSON",
"tinymce/util/XHR",
"tinymce/util/Tools"
], function(JSON, XHR, Tools) {
var extend = Tools.extend;
function JSONRequest(settings) {
this.settings = extend({}, settings);
this.count = 0;
}
/**
* Simple helper function to send a JSON-RPC request without the need to initialize an object.
* Consult the Wiki API documentation for more details on what you can pass to this function.
*
* @method sendRPC
* @static
* @param {Object} o Call object where there are three field id, method and params this object should also contain callbacks etc.
*/
JSONRequest.sendRPC = function(o) {
return new JSONRequest().send(o);
};
JSONRequest.prototype = {
/**
* Sends a JSON-RPC call. Consult the Wiki API documentation for more details on what you can pass to this function.
*
* @method send
* @param {Object} args Call object where there are three field id, method and params this object should also contain callbacks etc.
*/
send: function(args) {
var ecb = args.error, scb = args.success;
args = extend(this.settings, args);
args.success = function(c, x) {
c = JSON.parse(c);
if (typeof c == 'undefined') {
c = {
error: 'JSON Parse error.'
};
}
if (c.error) {
ecb.call(args.error_scope || args.scope, c.error, x);
} else {
scb.call(args.success_scope || args.scope, c.result);
}
};
args.error = function(ty, x) {
if (ecb) {
ecb.call(args.error_scope || args.scope, ty, x);
}
};
args.data = JSON.serialize({
id: args.id || 'c' + (this.count++),
method: args.method,
params: args.params
});
// JSON content type for Ruby on rails. Bug: #1883287
args.content_type = 'application/json';
XHR.send(args);
}
};
return JSONRequest;
});
// Included from: js/tinymce/classes/util/JSONP.js
/**
* JSONP.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
define("tinymce/util/JSONP", [
"tinymce/dom/DOMUtils"
], function(DOMUtils) {
return {
callbacks: {},
count: 0,
send: function(settings) {
var self = this, dom = DOMUtils.DOM, count = settings.count !== undefined ? settings.count : self.count;
var id = 'tinymce_jsonp_' + count;
self.callbacks[count] = function(json) {
dom.remove(id);
delete self.callbacks[count];
settings.callback(json);
};
dom.add(dom.doc.body, 'script', {
id: id,
src: settings.url,
type: 'text/javascript'
});
self.count++;
}
};
});
// Included from: js/tinymce/classes/util/LocalStorage.js
/**
* LocalStorage.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class will simulate LocalStorage on IE 7 and return the native version on modern browsers.
* Storage is done using userData on IE 7 and a special serialization format. The format is designed
* to be as small as possible by making sure that the keys and values doesn't need to be encoded. This
* makes it possible to store for example HTML data.
*
* Storage format for userData:
* <base 32 key length>,<key string>,<base 32 value length>,<value>,...
*
* For example this data key1=value1,key2=value2 would be:
* 4,key1,6,value1,4,key2,6,value2
*
* @class tinymce.util.LocalStorage
* @static
* @version 4.0
* @example
* tinymce.util.LocalStorage.setItem('key', 'value');
* var value = tinymce.util.LocalStorage.getItem('key');
*/
define("tinymce/util/LocalStorage", [], function() {
var LocalStorage, storageElm, items, keys, userDataKey, hasOldIEDataSupport;
// Check for native support
try {
if (window.localStorage) {
return localStorage;
}
} catch (ex) {
// Ignore
}
userDataKey = "tinymce";
storageElm = document.documentElement;
hasOldIEDataSupport = !!storageElm.addBehavior;
if (hasOldIEDataSupport) {
storageElm.addBehavior('#default#userData');
}
/**
* Gets the keys names and updates LocalStorage.length property. Since IE7 doesn't have any getters/setters.
*/
function updateKeys() {
keys = [];
for (var key in items) {
keys.push(key);
}
LocalStorage.length = keys.length;
}
/**
* Loads the userData string and parses it into the items structure.
*/
function load() {
var key, data, value, pos = 0;
items = {};
// localStorage can be disabled on WebKit/Gecko so make a dummy storage
if (!hasOldIEDataSupport) {
return;
}
function next(end) {
var value, nextPos;
nextPos = end !== undefined ? pos + end : data.indexOf(',', pos);
if (nextPos === -1 || nextPos > data.length) {
return null;
}
value = data.substring(pos, nextPos);
pos = nextPos + 1;
return value;
}
storageElm.load(userDataKey);
data = storageElm.getAttribute(userDataKey) || '';
do {
var offset = next();
if (offset === null) {
break;
}
key = next(parseInt(offset, 32) || 0);
if (key !== null) {
offset = next();
if (offset === null) {
break;
}
value = next(parseInt(offset, 32) || 0);
if (key) {
items[key] = value;
}
}
} while (key !== null);
updateKeys();
}
/**
* Saves the items structure into a the userData format.
*/
function save() {
var value, data = '';
// localStorage can be disabled on WebKit/Gecko so make a dummy storage
if (!hasOldIEDataSupport) {
return;
}
for (var key in items) {
value = items[key];
data += (data ? ',' : '') + key.length.toString(32) + ',' + key + ',' + value.length.toString(32) + ',' + value;
}
storageElm.setAttribute(userDataKey, data);
try {
storageElm.save(userDataKey);
} catch (ex) {
// Ignore disk full
}
updateKeys();
}
LocalStorage = {
/**
* Length of the number of items in storage.
*
* @property length
* @type Number
* @return {Number} Number of items in storage.
*/
//length:0,
/**
* Returns the key name by index.
*
* @method key
* @param {Number} index Index of key to return.
* @return {String} Key value or null if it wasn't found.
*/
key: function(index) {
return keys[index];
},
/**
* Returns the value if the specified key or null if it wasn't found.
*
* @method getItem
* @param {String} key Key of item to retrieve.
* @return {String} Value of the specified item or null if it wasn't found.
*/
getItem: function(key) {
return key in items ? items[key] : null;
},
/**
* Sets the value of the specified item by it's key.
*
* @method setItem
* @param {String} key Key of the item to set.
* @param {String} value Value of the item to set.
*/
setItem: function(key, value) {
items[key] = "" + value;
save();
},
/**
* Removes the specified item by key.
*
* @method removeItem
* @param {String} key Key of item to remove.
*/
removeItem: function(key) {
delete items[key];
save();
},
/**
* Removes all items.
*
* @method clear
*/
clear: function() {
items = {};
save();
}
};
load();
return LocalStorage;
});
// Included from: js/tinymce/classes/Compat.js
/**
* Compat.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* TinyMCE core class.
*
* @static
* @class tinymce
* @borrow-members tinymce.EditorManager
* @borrow-members tinymce.util.Tools
*/
define("tinymce/Compat", [
"tinymce/dom/DOMUtils",
"tinymce/dom/EventUtils",
"tinymce/dom/ScriptLoader",
"tinymce/AddOnManager",
"tinymce/util/Tools",
"tinymce/Env"
], function(DOMUtils, EventUtils, ScriptLoader, AddOnManager, Tools, Env) {
var tinymce = window.tinymce;
/**
* @property {tinymce.dom.DOMUtils} DOM Global DOM instance.
* @property {tinymce.dom.ScriptLoader} ScriptLoader Global ScriptLoader instance.
* @property {tinymce.AddOnManager} PluginManager Global PluginManager instance.
* @property {tinymce.AddOnManager} ThemeManager Global ThemeManager instance.
*/
tinymce.DOM = DOMUtils.DOM;
tinymce.ScriptLoader = ScriptLoader.ScriptLoader;
tinymce.PluginManager = AddOnManager.PluginManager;
tinymce.ThemeManager = AddOnManager.ThemeManager;
tinymce.dom = tinymce.dom || {};
tinymce.dom.Event = EventUtils.Event;
Tools.each(
'trim isArray is toArray makeMap each map grep inArray extend create walk createNS resolve explode _addCacheSuffix'.split(' '),
function(key) {
tinymce[key] = Tools[key];
}
);
Tools.each('isOpera isWebKit isIE isGecko isMac'.split(' '), function(name) {
tinymce[name] = Env[name.substr(2).toLowerCase()];
});
return {};
});
// Describe the different namespaces
/**
* Root level namespace this contains classes directly related to the TinyMCE editor.
*
* @namespace tinymce
*/
/**
* Contains classes for handling the browsers DOM.
*
* @namespace tinymce.dom
*/
/**
* Contains html parser and serializer logic.
*
* @namespace tinymce.html
*/
/**
* Contains the different UI types such as buttons, listboxes etc.
*
* @namespace tinymce.ui
*/
/**
* Contains various utility classes such as json parser, cookies etc.
*
* @namespace tinymce.util
*/
/**
* Contains modules to handle data binding.
*
* @namespace tinymce.data
*/
// Included from: js/tinymce/classes/ui/Layout.js
/**
* Layout.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Base layout manager class.
*
* @class tinymce.ui.Layout
*/
define("tinymce/ui/Layout", [
"tinymce/util/Class",
"tinymce/util/Tools"
], function(Class, Tools) {
"use strict";
return Class.extend({
Defaults: {
firstControlClass: 'first',
lastControlClass: 'last'
},
/**
* Constructs a layout instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
*/
init: function(settings) {
this.settings = Tools.extend({}, this.Defaults, settings);
},
/**
* This method gets invoked before the layout renders the controls.
*
* @method preRender
* @param {tinymce.ui.Container} container Container instance to preRender.
*/
preRender: function(container) {
container.bodyClasses.add(this.settings.containerClass);
},
/**
* Applies layout classes to the container.
*
* @private
*/
applyClasses: function(items) {
var self = this, settings = self.settings, firstClass, lastClass, firstItem, lastItem;
firstClass = settings.firstControlClass;
lastClass = settings.lastControlClass;
items.each(function(item) {
item.classes.remove(firstClass).remove(lastClass).add(settings.controlClass);
if (item.visible()) {
if (!firstItem) {
firstItem = item;
}
lastItem = item;
}
});
if (firstItem) {
firstItem.classes.add(firstClass);
}
if (lastItem) {
lastItem.classes.add(lastClass);
}
},
/**
* Renders the specified container and any layout specific HTML.
*
* @method renderHtml
* @param {tinymce.ui.Container} container Container to render HTML for.
*/
renderHtml: function(container) {
var self = this, html = '';
self.applyClasses(container.items());
container.items().each(function(item) {
html += item.renderHtml();
});
return html;
},
/**
* Recalculates the positions of the controls in the specified container.
*
* @method recalc
* @param {tinymce.ui.Container} container Container instance to recalc.
*/
recalc: function() {
},
/**
* This method gets invoked after the layout renders the controls.
*
* @method postRender
* @param {tinymce.ui.Container} container Container instance to postRender.
*/
postRender: function() {
},
isNative: function() {
return false;
}
});
});
// Included from: js/tinymce/classes/ui/AbsoluteLayout.js
/**
* AbsoluteLayout.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* LayoutManager for absolute positioning. This layout manager is more of
* a base class for other layouts but can be created and used directly.
*
* @-x-less AbsoluteLayout.less
* @class tinymce.ui.AbsoluteLayout
* @extends tinymce.ui.Layout
*/
define("tinymce/ui/AbsoluteLayout", [
"tinymce/ui/Layout"
], function(Layout) {
"use strict";
return Layout.extend({
Defaults: {
containerClass: 'abs-layout',
controlClass: 'abs-layout-item'
},
/**
* Recalculates the positions of the controls in the specified container.
*
* @method recalc
* @param {tinymce.ui.Container} container Container instance to recalc.
*/
recalc: function(container) {
container.items().filter(':visible').each(function(ctrl) {
var settings = ctrl.settings;
ctrl.layoutRect({
x: settings.x,
y: settings.y,
w: settings.w,
h: settings.h
});
if (ctrl.recalc) {
ctrl.recalc();
}
});
},
/**
* Renders the specified container and any layout specific HTML.
*
* @method renderHtml
* @param {tinymce.ui.Container} container Container to render HTML for.
*/
renderHtml: function(container) {
return '<div id="' + container._id + '-absend" class="' + container.classPrefix + 'abs-end"></div>' + this._super(container);
}
});
});
// Included from: js/tinymce/classes/ui/Button.js
/**
* Button.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class is used to create buttons. You can create them directly or through the Factory.
*
* @example
* // Create and render a button to the body element
* tinymce.ui.Factory.create({
* type: 'button',
* text: 'My button'
* }).renderTo(document.body);
*
* @-x-less Button.less
* @class tinymce.ui.Button
* @extends tinymce.ui.Widget
*/
define("tinymce/ui/Button", [
"tinymce/ui/Widget"
], function(Widget) {
"use strict";
return Widget.extend({
Defaults: {
classes: "widget btn",
role: "button"
},
/**
* Constructs a new button instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {String} size Size of the button small|medium|large.
* @setting {String} image Image to use for icon.
* @setting {String} icon Icon to use for button.
*/
init: function(settings) {
var self = this, size;
self._super(settings);
settings = self.settings;
size = self.settings.size;
self.on('click mousedown', function(e) {
e.preventDefault();
});
self.on('touchstart', function(e) {
self.fire('click', e);
e.preventDefault();
});
if (settings.subtype) {
self.classes.add(settings.subtype);
}
if (size) {
self.classes.add('btn-' + size);
}
if (settings.icon) {
self.icon(settings.icon);
}
},
/**
* Sets/gets the current button icon.
*
* @method icon
* @param {String} [icon] New icon identifier.
* @return {String|tinymce.ui.MenuButton} Current icon or current MenuButton instance.
*/
icon: function(icon) {
if (!arguments.length) {
return this.state.get('icon');
}
this.state.set('icon', icon);
return this;
},
/**
* Repaints the button for example after it's been resizes by a layout engine.
*
* @method repaint
*/
repaint: function() {
var btnElm = this.getEl().firstChild,
btnStyle;
if (btnElm) {
btnStyle = btnElm.style;
btnStyle.width = btnStyle.height = "100%";
}
this._super();
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, id = self._id, prefix = self.classPrefix;
var icon = self.state.get('icon'), image, text = self.state.get('text'), textHtml = '';
image = self.settings.image;
if (image) {
icon = 'none';
// Support for [high dpi, low dpi] image sources
if (typeof image != "string") {
image = window.getSelection ? image[0] : image[1];
}
image = ' style="background-image: url(\'' + image + '\')"';
} else {
image = '';
}
if (text) {
self.classes.add('btn-has-text');
textHtml = '<span class="' + prefix + 'txt">' + self.encode(text) + '</span>';
}
icon = icon ? prefix + 'ico ' + prefix + 'i-' + icon : '';
return (
'<div id="' + id + '" class="' + self.classes + '" tabindex="-1" aria-labelledby="' + id + '">' +
'<button role="presentation" type="button" tabindex="-1">' +
(icon ? '<i class="' + icon + '"' + image + '></i>' : '') +
textHtml +
'</button>' +
'</div>'
);
},
bindStates: function() {
var self = this, $ = self.$, textCls = self.classPrefix + 'txt';
function setButtonText(text) {
var $span = $('span.' + textCls, self.getEl());
if (text) {
if (!$span[0]) {
$('button:first', self.getEl()).append('<span class="' + textCls + '"></span>');
$span = $('span.' + textCls, self.getEl());
}
$span.html(self.encode(text));
} else {
$span.remove();
}
self.classes.toggle('btn-has-text', !!text);
}
self.state.on('change:text', function(e) {
setButtonText(e.value);
});
self.state.on('change:icon', function(e) {
var icon = e.value, prefix = self.classPrefix;
self.settings.icon = icon;
icon = icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : '';
var btnElm = self.getEl().firstChild, iconElm = btnElm.getElementsByTagName('i')[0];
if (icon) {
if (!iconElm || iconElm != btnElm.firstChild) {
iconElm = document.createElement('i');
btnElm.insertBefore(iconElm, btnElm.firstChild);
}
iconElm.className = icon;
} else if (iconElm) {
btnElm.removeChild(iconElm);
}
setButtonText(self.state.get('text'));
});
return self._super();
}
});
});
// Included from: js/tinymce/classes/ui/ButtonGroup.js
/**
* ButtonGroup.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This control enables you to put multiple buttons into a group. This is
* useful when you want to combine similar toolbar buttons into a group.
*
* @example
* // Create and render a buttongroup with two buttons to the body element
* tinymce.ui.Factory.create({
* type: 'buttongroup',
* items: [
* {text: 'Button A'},
* {text: 'Button B'}
* ]
* }).renderTo(document.body);
*
* @-x-less ButtonGroup.less
* @class tinymce.ui.ButtonGroup
* @extends tinymce.ui.Container
*/
define("tinymce/ui/ButtonGroup", [
"tinymce/ui/Container"
], function(Container) {
"use strict";
return Container.extend({
Defaults: {
defaultType: 'button',
role: 'group'
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, layout = self._layout;
self.classes.add('btn-group');
self.preRender();
layout.preRender(self);
return (
'<div id="' + self._id + '" class="' + self.classes + '">' +
'<div id="' + self._id + '-body">' +
(self.settings.html || '') + layout.renderHtml(self) +
'</div>' +
'</div>'
);
}
});
});
// Included from: js/tinymce/classes/ui/Checkbox.js
/**
* Checkbox.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This control creates a custom checkbox.
*
* @example
* // Create and render a checkbox to the body element
* tinymce.ui.Factory.create({
* type: 'checkbox',
* checked: true,
* text: 'My checkbox'
* }).renderTo(document.body);
*
* @-x-less Checkbox.less
* @class tinymce.ui.Checkbox
* @extends tinymce.ui.Widget
*/
define("tinymce/ui/Checkbox", [
"tinymce/ui/Widget"
], function(Widget) {
"use strict";
return Widget.extend({
Defaults: {
classes: "checkbox",
role: "checkbox",
checked: false
},
/**
* Constructs a new Checkbox instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {Boolean} checked True if the checkbox should be checked by default.
*/
init: function(settings) {
var self = this;
self._super(settings);
self.on('click mousedown', function(e) {
e.preventDefault();
});
self.on('click', function(e) {
e.preventDefault();
if (!self.disabled()) {
self.checked(!self.checked());
}
});
self.checked(self.settings.checked);
},
/**
* Getter/setter function for the checked state.
*
* @method checked
* @param {Boolean} [state] State to be set.
* @return {Boolean|tinymce.ui.Checkbox} True/false or checkbox if it's a set operation.
*/
checked: function(state) {
if (!arguments.length) {
return this.state.get('checked');
}
this.state.set('checked', state);
return this;
},
/**
* Getter/setter function for the value state.
*
* @method value
* @param {Boolean} [state] State to be set.
* @return {Boolean|tinymce.ui.Checkbox} True/false or checkbox if it's a set operation.
*/
value: function(state) {
if (!arguments.length) {
return this.checked();
}
return this.checked(state);
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, id = self._id, prefix = self.classPrefix;
return (
'<div id="' + id + '" class="' + self.classes + '" unselectable="on" aria-labelledby="' + id + '-al" tabindex="-1">' +
'<i class="' + prefix + 'ico ' + prefix + 'i-checkbox"></i>' +
'<span id="' + id + '-al" class="' + prefix + 'label">' + self.encode(self.state.get('text')) + '</span>' +
'</div>'
);
},
bindStates: function() {
var self = this;
function checked(state) {
self.classes.toggle("checked", state);
self.aria('checked', state);
}
self.state.on('change:text', function(e) {
self.getEl('al').firstChild.data = self.translate(e.value);
});
self.state.on('change:checked change:value', function(e) {
self.fire('change');
checked(e.value);
});
self.state.on('change:icon', function(e) {
var icon = e.value, prefix = self.classPrefix;
if (typeof icon == 'undefined') {
return self.settings.icon;
}
self.settings.icon = icon;
icon = icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : '';
var btnElm = self.getEl().firstChild, iconElm = btnElm.getElementsByTagName('i')[0];
if (icon) {
if (!iconElm || iconElm != btnElm.firstChild) {
iconElm = document.createElement('i');
btnElm.insertBefore(iconElm, btnElm.firstChild);
}
iconElm.className = icon;
} else if (iconElm) {
btnElm.removeChild(iconElm);
}
});
if (self.state.get('checked')) {
checked(true);
}
return self._super();
}
});
});
// Included from: js/tinymce/classes/ui/ComboBox.js
/**
* ComboBox.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class creates a combobox control. Select box that you select a value from or
* type a value into.
*
* @-x-less ComboBox.less
* @class tinymce.ui.ComboBox
* @extends tinymce.ui.Widget
*/
define("tinymce/ui/ComboBox", [
"tinymce/ui/Widget",
"tinymce/ui/Factory",
"tinymce/ui/DomUtils",
"tinymce/dom/DomQuery",
"tinymce/util/VK",
"tinymce/util/Tools"
], function(Widget, Factory, DomUtils, $, VK, Tools) {
"use strict";
return Widget.extend({
/**
* Constructs a new control instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {String} placeholder Placeholder text to display.
*/
init: function(settings) {
var self = this;
self._super(settings);
settings = self.settings;
self.classes.add('combobox');
self.subinput = true;
self.ariaTarget = 'inp'; // TODO: Figure out a better way
settings.menu = settings.menu || settings.values;
if (settings.menu) {
settings.icon = 'caret';
}
self.on('click', function(e) {
var elm = e.target, root = self.getEl();
if (!$.contains(root, elm) && elm != root) {
return;
}
while (elm && elm != root) {
if (elm.id && elm.id.indexOf('-open') != -1) {
self.fire('action');
if (settings.menu) {
self.showMenu();
if (e.aria) {
self.menu.items()[0].focus();
}
}
}
elm = elm.parentNode;
}
});
// TODO: Rework this
self.on('keydown', function(e) {
var rootControl;
if (e.keyCode == 13 && e.target.nodeName === 'INPUT') {
e.preventDefault();
// Find root control that we can do toJSON on
self.parents().reverse().each(function(ctrl) {
if (ctrl.toJSON) {
rootControl = ctrl;
return false;
}
});
// Fire event on current text box with the serialized data of the whole form
self.fire('submit', {data: rootControl.toJSON()});
}
});
self.on('keyup', function(e) {
if (e.target.nodeName == "INPUT") {
var oldValue = self.state.get('value');
var newValue = e.target.value;
if (newValue !== oldValue) {
self.state.set('value', newValue);
self.fire('autocomplete', e);
}
}
});
self.on('mouseover', function(e) {
var tooltip = self.tooltip().moveTo(-0xFFFF);
if (self.statusLevel() && e.target.className.indexOf(self.classPrefix + 'status') !== -1) {
var statusMessage = self.statusMessage() || 'Ok';
var rel = tooltip.text(statusMessage).show().testMoveRel(e.target, ['bc-tc', 'bc-tl', 'bc-tr']);
tooltip.classes.toggle('tooltip-n', rel == 'bc-tc');
tooltip.classes.toggle('tooltip-nw', rel == 'bc-tl');
tooltip.classes.toggle('tooltip-ne', rel == 'bc-tr');
tooltip.moveRel(e.target, rel);
}
});
},
statusLevel: function (value) {
if (arguments.length > 0) {
this.state.set('statusLevel', value);
}
return this.state.get('statusLevel');
},
statusMessage: function (value) {
if (arguments.length > 0) {
this.state.set('statusMessage', value);
}
return this.state.get('statusMessage');
},
showMenu: function() {
var self = this, settings = self.settings, menu;
if (!self.menu) {
menu = settings.menu || [];
// Is menu array then auto constuct menu control
if (menu.length) {
menu = {
type: 'menu',
items: menu
};
} else {
menu.type = menu.type || 'menu';
}
self.menu = Factory.create(menu).parent(self).renderTo(self.getContainerElm());
self.fire('createmenu');
self.menu.reflow();
self.menu.on('cancel', function(e) {
if (e.control === self.menu) {
self.focus();
}
});
self.menu.on('show hide', function(e) {
e.control.items().each(function(ctrl) {
ctrl.active(ctrl.value() == self.value());
});
}).fire('show');
self.menu.on('select', function(e) {
self.value(e.control.value());
});
self.on('focusin', function(e) {
if (e.target.tagName.toUpperCase() == 'INPUT') {
self.menu.hide();
}
});
self.aria('expanded', true);
}
self.menu.show();
self.menu.layoutRect({w: self.layoutRect().w});
self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']);
},
/**
* Focuses the input area of the control.
*
* @method focus
*/
focus: function() {
this.getEl('inp').focus();
},
/**
* Repaints the control after a layout operation.
*
* @method repaint
*/
repaint: function() {
var self = this, elm = self.getEl(), openElm = self.getEl('open'), rect = self.layoutRect();
var width, lineHeight, innerPadding = 0, inputElm = elm.firstChild;
if (self.statusLevel() && self.statusLevel() !== 'none') {
innerPadding = (
parseInt(DomUtils.getRuntimeStyle(inputElm, 'padding-right'), 10) -
parseInt(DomUtils.getRuntimeStyle(inputElm, 'padding-left'), 10)
);
}
if (openElm) {
width = rect.w - DomUtils.getSize(openElm).width - 10;
} else {
width = rect.w - 10;
}
// Detect old IE 7+8 add lineHeight to align caret vertically in the middle
var doc = document;
if (doc.all && (!doc.documentMode || doc.documentMode <= 8)) {
lineHeight = (self.layoutRect().h - 2) + 'px';
}
$(inputElm).css({
width: width - innerPadding,
lineHeight: lineHeight
});
self._super();
return self;
},
/**
* Post render method. Called after the control has been rendered to the target.
*
* @method postRender
* @return {tinymce.ui.ComboBox} Current combobox instance.
*/
postRender: function() {
var self = this;
$(this.getEl('inp')).on('change', function(e) {
self.state.set('value', e.target.value);
self.fire('change', e);
});
return self._super();
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, id = self._id, settings = self.settings, prefix = self.classPrefix;
var value = self.state.get('value') || '';
var icon, text, openBtnHtml = '', extraAttrs = '', statusHtml = '';
if ("spellcheck" in settings) {
extraAttrs += ' spellcheck="' + settings.spellcheck + '"';
}
if (settings.maxLength) {
extraAttrs += ' maxlength="' + settings.maxLength + '"';
}
if (settings.size) {
extraAttrs += ' size="' + settings.size + '"';
}
if (settings.subtype) {
extraAttrs += ' type="' + settings.subtype + '"';
}
statusHtml = '<i id="' + id + '-status" class="mce-status mce-ico" style="display: none"></i>';
if (self.disabled()) {
extraAttrs += ' disabled="disabled"';
}
icon = settings.icon;
if (icon && icon != 'caret') {
icon = prefix + 'ico ' + prefix + 'i-' + settings.icon;
}
text = self.state.get('text');
if (icon || text) {
openBtnHtml = (
'<div id="' + id + '-open" class="' + prefix + 'btn ' + prefix + 'open" tabIndex="-1" role="button">' +
'<button id="' + id + '-action" type="button" hidefocus="1" tabindex="-1">' +
(icon != 'caret' ? '<i class="' + icon + '"></i>' : '<i class="' + prefix + 'caret"></i>') +
(text ? (icon ? ' ' : '') + text : '') +
'</button>' +
'</div>'
);
self.classes.add('has-open');
}
return (
'<div id="' + id + '" class="' + self.classes + '">' +
'<input id="' + id + '-inp" class="' + prefix + 'textbox" value="' +
self.encode(value, false) + '" hidefocus="1"' + extraAttrs + ' placeholder="' +
self.encode(settings.placeholder) + '" />' +
statusHtml +
openBtnHtml +
'</div>'
);
},
value: function(value) {
if (arguments.length) {
this.state.set('value', value);
return this;
}
// Make sure the real state is in sync
if (this.state.get('rendered')) {
this.state.set('value', this.getEl('inp').value);
}
return this.state.get('value');
},
showAutoComplete: function (items, term) {
var self = this;
if (items.length === 0) {
self.hideMenu();
return;
}
var insert = function (value, title) {
return function () {
self.fire('selectitem', {
title: title,
value: value
});
};
};
if (self.menu) {
self.menu.items().remove();
} else {
self.menu = Factory.create({
type: 'menu',
classes: 'combobox-menu',
layout: 'flow'
}).parent(self).renderTo();
}
Tools.each(items, function (item) {
self.menu.add({
text: item.title,
url: item.previewUrl,
match: term,
classes: 'menu-item-ellipsis',
onclick: insert(item.value, item.title)
});
});
self.menu.renderNew();
self.hideMenu();
self.menu.on('cancel', function(e) {
if (e.control.parent() === self.menu) {
e.stopPropagation();
self.focus();
self.hideMenu();
}
});
self.menu.on('select', function() {
self.focus();
});
var maxW = self.layoutRect().w;
self.menu.layoutRect({w: maxW, minW: 0, maxW: maxW});
self.menu.reflow();
self.menu.show();
self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']);
},
hideMenu: function() {
if (this.menu) {
this.menu.hide();
}
},
bindStates: function() {
var self = this;
self.state.on('change:value', function(e) {
if (self.getEl('inp').value != e.value) {
self.getEl('inp').value = e.value;
}
});
self.state.on('change:disabled', function(e) {
self.getEl('inp').disabled = e.value;
});
self.state.on('change:statusLevel', function(e) {
var statusIconElm = self.getEl('status');
var prefix = self.classPrefix, value = e.value;
DomUtils.css(statusIconElm, 'display', value === 'none' ? 'none' : '');
DomUtils.toggleClass(statusIconElm, prefix + 'i-checkmark', value === 'ok');
DomUtils.toggleClass(statusIconElm, prefix + 'i-warning', value === 'warn');
DomUtils.toggleClass(statusIconElm, prefix + 'i-error', value === 'error');
self.classes.toggle('has-status', value !== 'none');
self.repaint();
});
DomUtils.on(self.getEl('status'), 'mouseleave', function () {
self.tooltip().hide();
});
self.on('cancel', function (e) {
if (self.menu && self.menu.visible()) {
e.stopPropagation();
self.hideMenu();
}
});
var focusIdx = function (idx, menu) {
if (menu && menu.items().length > 0) {
menu.items().eq(idx)[0].focus();
}
};
self.on('keydown', function (e) {
var keyCode = e.keyCode;
if (e.target.nodeName === 'INPUT') {
if (keyCode === VK.DOWN) {
e.preventDefault();
self.fire('autocomplete');
focusIdx(0, self.menu);
} else if (keyCode === VK.UP) {
e.preventDefault();
focusIdx(-1, self.menu);
}
}
});
return self._super();
},
remove: function() {
$(this.getEl('inp')).off();
if (this.menu) {
this.menu.remove();
}
this._super();
}
});
});
// Included from: js/tinymce/classes/ui/ColorBox.js
/**
* ColorBox.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This widget lets you enter colors and browse for colors by pressing the color button. It also displays
* a preview of the current color.
*
* @-x-less ColorBox.less
* @class tinymce.ui.ColorBox
* @extends tinymce.ui.ComboBox
*/
define("tinymce/ui/ColorBox", [
"tinymce/ui/ComboBox"
], function(ComboBox) {
"use strict";
return ComboBox.extend({
/**
* Constructs a new control instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
*/
init: function(settings) {
var self = this;
settings.spellcheck = false;
if (settings.onaction) {
settings.icon = 'none';
}
self._super(settings);
self.classes.add('colorbox');
self.on('change keyup postrender', function() {
self.repaintColor(self.value());
});
},
repaintColor: function(value) {
var openElm = this.getEl('open');
var elm = openElm ? openElm.getElementsByTagName('i')[0] : null;
if (elm) {
try {
elm.style.background = value;
} catch (ex) {
// Ignore
}
}
},
bindStates: function() {
var self = this;
self.state.on('change:value', function(e) {
if (self.state.get('rendered')) {
self.repaintColor(e.value);
}
});
return self._super();
}
});
});
// Included from: js/tinymce/classes/ui/PanelButton.js
/**
* PanelButton.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new panel button.
*
* @class tinymce.ui.PanelButton
* @extends tinymce.ui.Button
*/
define("tinymce/ui/PanelButton", [
"tinymce/ui/Button",
"tinymce/ui/FloatPanel"
], function(Button, FloatPanel) {
"use strict";
return Button.extend({
/**
* Shows the panel for the button.
*
* @method showPanel
*/
showPanel: function() {
var self = this, settings = self.settings;
self.active(true);
if (!self.panel) {
var panelSettings = settings.panel;
// Wrap panel in grid layout if type if specified
// This makes it possible to add forms or other containers directly in the panel option
if (panelSettings.type) {
panelSettings = {
layout: 'grid',
items: panelSettings
};
}
panelSettings.role = panelSettings.role || 'dialog';
panelSettings.popover = true;
panelSettings.autohide = true;
panelSettings.ariaRoot = true;
self.panel = new FloatPanel(panelSettings).on('hide', function() {
self.active(false);
}).on('cancel', function(e) {
e.stopPropagation();
self.focus();
self.hidePanel();
}).parent(self).renderTo(self.getContainerElm());
self.panel.fire('show');
self.panel.reflow();
} else {
self.panel.show();
}
self.panel.moveRel(self.getEl(), settings.popoverAlign || (self.isRtl() ? ['bc-tr', 'bc-tc'] : ['bc-tl', 'bc-tc']));
},
/**
* Hides the panel for the button.
*
* @method hidePanel
*/
hidePanel: function() {
var self = this;
if (self.panel) {
self.panel.hide();
}
},
/**
* Called after the control has been rendered.
*
* @method postRender
*/
postRender: function() {
var self = this;
self.aria('haspopup', true);
self.on('click', function(e) {
if (e.control === self) {
if (self.panel && self.panel.visible()) {
self.hidePanel();
} else {
self.showPanel();
self.panel.focus(!!e.aria);
}
}
});
return self._super();
},
remove: function() {
if (this.panel) {
this.panel.remove();
this.panel = null;
}
return this._super();
}
});
});
// Included from: js/tinymce/classes/ui/ColorButton.js
/**
* ColorButton.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class creates a color button control. This is a split button in which the main
* button has a visual representation of the currently selected color. When clicked
* the caret button displays a color picker, allowing the user to select a new color.
*
* @-x-less ColorButton.less
* @class tinymce.ui.ColorButton
* @extends tinymce.ui.PanelButton
*/
define("tinymce/ui/ColorButton", [
"tinymce/ui/PanelButton",
"tinymce/dom/DOMUtils"
], function(PanelButton, DomUtils) {
"use strict";
var DOM = DomUtils.DOM;
return PanelButton.extend({
/**
* Constructs a new ColorButton instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
*/
init: function(settings) {
this._super(settings);
this.classes.add('colorbutton');
},
/**
* Getter/setter for the current color.
*
* @method color
* @param {String} [color] Color to set.
* @return {String|tinymce.ui.ColorButton} Current color or current instance.
*/
color: function(color) {
if (color) {
this._color = color;
this.getEl('preview').style.backgroundColor = color;
return this;
}
return this._color;
},
/**
* Resets the current color.
*
* @method resetColor
* @return {tinymce.ui.ColorButton} Current instance.
*/
resetColor: function() {
this._color = null;
this.getEl('preview').style.backgroundColor = null;
return this;
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, id = self._id, prefix = self.classPrefix, text = self.state.get('text');
var icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : '';
var image = self.settings.image ? ' style="background-image: url(\'' + self.settings.image + '\')"' : '',
textHtml = '';
if (text) {
self.classes.add('btn-has-text');
textHtml = '<span class="' + prefix + 'txt">' + self.encode(text) + '</span>';
}
return (
'<div id="' + id + '" class="' + self.classes + '" role="button" tabindex="-1" aria-haspopup="true">' +
'<button role="presentation" hidefocus="1" type="button" tabindex="-1">' +
(icon ? '<i class="' + icon + '"' + image + '></i>' : '') +
'<span id="' + id + '-preview" class="' + prefix + 'preview"></span>' +
textHtml +
'</button>' +
'<button type="button" class="' + prefix + 'open" hidefocus="1" tabindex="-1">' +
' <i class="' + prefix + 'caret"></i>' +
'</button>' +
'</div>'
);
},
/**
* Called after the control has been rendered.
*
* @method postRender
*/
postRender: function() {
var self = this, onClickHandler = self.settings.onclick;
self.on('click', function(e) {
if (e.aria && e.aria.key == 'down') {
return;
}
if (e.control == self && !DOM.getParent(e.target, '.' + self.classPrefix + 'open')) {
e.stopImmediatePropagation();
onClickHandler.call(self, e);
}
});
delete self.settings.onclick;
return self._super();
}
});
});
// Included from: js/tinymce/classes/util/Color.js
/**
* Color.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class lets you parse/serialize colors and convert rgb/hsb.
*
* @class tinymce.util.Color
* @example
* var white = new tinymce.util.Color({r: 255, g: 255, b: 255});
* var red = new tinymce.util.Color('#FF0000');
*
* console.log(white.toHex(), red.toHsv());
*/
define("tinymce/util/Color", [], function() {
var min = Math.min, max = Math.max, round = Math.round;
/**
* Constructs a new color instance.
*
* @constructor
* @method Color
* @param {String} value Optional initial value to parse.
*/
function Color(value) {
var self = this, r = 0, g = 0, b = 0;
function rgb2hsv(r, g, b) {
var h, s, v, d, minRGB, maxRGB;
h = 0;
s = 0;
v = 0;
r = r / 255;
g = g / 255;
b = b / 255;
minRGB = min(r, min(g, b));
maxRGB = max(r, max(g, b));
if (minRGB == maxRGB) {
v = minRGB;
return {
h: 0,
s: 0,
v: v * 100
};
}
/*eslint no-nested-ternary:0 */
d = (r == minRGB) ? g - b : ((b == minRGB) ? r - g : b - r);
h = (r == minRGB) ? 3 : ((b == minRGB) ? 1 : 5);
h = 60 * (h - d / (maxRGB - minRGB));
s = (maxRGB - minRGB) / maxRGB;
v = maxRGB;
return {
h: round(h),
s: round(s * 100),
v: round(v * 100)
};
}
function hsvToRgb(hue, saturation, brightness) {
var side, chroma, x, match;
hue = (parseInt(hue, 10) || 0) % 360;
saturation = parseInt(saturation, 10) / 100;
brightness = parseInt(brightness, 10) / 100;
saturation = max(0, min(saturation, 1));
brightness = max(0, min(brightness, 1));
if (saturation === 0) {
r = g = b = round(255 * brightness);
return;
}
side = hue / 60;
chroma = brightness * saturation;
x = chroma * (1 - Math.abs(side % 2 - 1));
match = brightness - chroma;
switch (Math.floor(side)) {
case 0:
r = chroma;
g = x;
b = 0;
break;
case 1:
r = x;
g = chroma;
b = 0;
break;
case 2:
r = 0;
g = chroma;
b = x;
break;
case 3:
r = 0;
g = x;
b = chroma;
break;
case 4:
r = x;
g = 0;
b = chroma;
break;
case 5:
r = chroma;
g = 0;
b = x;
break;
default:
r = g = b = 0;
}
r = round(255 * (r + match));
g = round(255 * (g + match));
b = round(255 * (b + match));
}
/**
* Returns the hex string of the current color. For example: #ff00ff
*
* @method toHex
* @return {String} Hex string of current color.
*/
function toHex() {
function hex(val) {
val = parseInt(val, 10).toString(16);
return val.length > 1 ? val : '0' + val;
}
return '#' + hex(r) + hex(g) + hex(b);
}
/**
* Returns the r, g, b values of the color. Each channel has a range from 0-255.
*
* @method toRgb
* @return {Object} Object with r, g, b fields.
*/
function toRgb() {
return {
r: r,
g: g,
b: b
};
}
/**
* Returns the h, s, v values of the color. Ranges: h=0-360, s=0-100, v=0-100.
*
* @method toHsv
* @return {Object} Object with h, s, v fields.
*/
function toHsv() {
return rgb2hsv(r, g, b);
}
/**
* Parses the specified value and populates the color instance.
*
* Supported format examples:
* * rbg(255,0,0)
* * #ff0000
* * #fff
* * {r: 255, g: 0, b: 0}
* * {h: 360, s: 100, v: 100}
*
* @method parse
* @param {Object/String} value Color value to parse.
* @return {tinymce.util.Color} Current color instance.
*/
function parse(value) {
var matches;
if (typeof value == 'object') {
if ("r" in value) {
r = value.r;
g = value.g;
b = value.b;
} else if ("v" in value) {
hsvToRgb(value.h, value.s, value.v);
}
} else {
if ((matches = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)[^\)]*\)/gi.exec(value))) {
r = parseInt(matches[1], 10);
g = parseInt(matches[2], 10);
b = parseInt(matches[3], 10);
} else if ((matches = /#([0-F]{2})([0-F]{2})([0-F]{2})/gi.exec(value))) {
r = parseInt(matches[1], 16);
g = parseInt(matches[2], 16);
b = parseInt(matches[3], 16);
} else if ((matches = /#([0-F])([0-F])([0-F])/gi.exec(value))) {
r = parseInt(matches[1] + matches[1], 16);
g = parseInt(matches[2] + matches[2], 16);
b = parseInt(matches[3] + matches[3], 16);
}
}
r = r < 0 ? 0 : (r > 255 ? 255 : r);
g = g < 0 ? 0 : (g > 255 ? 255 : g);
b = b < 0 ? 0 : (b > 255 ? 255 : b);
return self;
}
if (value) {
parse(value);
}
self.toRgb = toRgb;
self.toHsv = toHsv;
self.toHex = toHex;
self.parse = parse;
}
return Color;
});
// Included from: js/tinymce/classes/ui/ColorPicker.js
/**
* ColorPicker.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Color picker widget lets you select colors.
*
* @-x-less ColorPicker.less
* @class tinymce.ui.ColorPicker
* @extends tinymce.ui.Widget
*/
define("tinymce/ui/ColorPicker", [
"tinymce/ui/Widget",
"tinymce/ui/DragHelper",
"tinymce/ui/DomUtils",
"tinymce/util/Color"
], function(Widget, DragHelper, DomUtils, Color) {
"use strict";
return Widget.extend({
Defaults: {
classes: "widget colorpicker"
},
/**
* Constructs a new colorpicker instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {String} color Initial color value.
*/
init: function(settings) {
this._super(settings);
},
postRender: function() {
var self = this, color = self.color(), hsv, hueRootElm, huePointElm, svRootElm, svPointElm;
hueRootElm = self.getEl('h');
huePointElm = self.getEl('hp');
svRootElm = self.getEl('sv');
svPointElm = self.getEl('svp');
function getPos(elm, event) {
var pos = DomUtils.getPos(elm), x, y;
x = event.pageX - pos.x;
y = event.pageY - pos.y;
x = Math.max(0, Math.min(x / elm.clientWidth, 1));
y = Math.max(0, Math.min(y / elm.clientHeight, 1));
return {
x: x,
y: y
};
}
function updateColor(hsv, hueUpdate) {
var hue = (360 - hsv.h) / 360;
DomUtils.css(huePointElm, {
top: (hue * 100) + '%'
});
if (!hueUpdate) {
DomUtils.css(svPointElm, {
left: hsv.s + '%',
top: (100 - hsv.v) + '%'
});
}
svRootElm.style.background = new Color({s: 100, v: 100, h: hsv.h}).toHex();
self.color().parse({s: hsv.s, v: hsv.v, h: hsv.h});
}
function updateSaturationAndValue(e) {
var pos;
pos = getPos(svRootElm, e);
hsv.s = pos.x * 100;
hsv.v = (1 - pos.y) * 100;
updateColor(hsv);
self.fire('change');
}
function updateHue(e) {
var pos;
pos = getPos(hueRootElm, e);
hsv = color.toHsv();
hsv.h = (1 - pos.y) * 360;
updateColor(hsv, true);
self.fire('change');
}
self._repaint = function() {
hsv = color.toHsv();
updateColor(hsv);
};
self._super();
self._svdraghelper = new DragHelper(self._id + '-sv', {
start: updateSaturationAndValue,
drag: updateSaturationAndValue
});
self._hdraghelper = new DragHelper(self._id + '-h', {
start: updateHue,
drag: updateHue
});
self._repaint();
},
rgb: function() {
return this.color().toRgb();
},
value: function(value) {
var self = this;
if (arguments.length) {
self.color().parse(value);
if (self._rendered) {
self._repaint();
}
} else {
return self.color().toHex();
}
},
color: function() {
if (!this._color) {
this._color = new Color();
}
return this._color;
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, id = self._id, prefix = self.classPrefix, hueHtml;
var stops = '#ff0000,#ff0080,#ff00ff,#8000ff,#0000ff,#0080ff,#00ffff,#00ff80,#00ff00,#80ff00,#ffff00,#ff8000,#ff0000';
function getOldIeFallbackHtml() {
var i, l, html = '', gradientPrefix, stopsList;
gradientPrefix = 'filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr=';
stopsList = stops.split(',');
for (i = 0, l = stopsList.length - 1; i < l; i++) {
html += (
'<div class="' + prefix + 'colorpicker-h-chunk" style="' +
'height:' + (100 / l) + '%;' +
gradientPrefix + stopsList[i] + ',endColorstr=' + stopsList[i + 1] + ');' +
'-ms-' + gradientPrefix + stopsList[i] + ',endColorstr=' + stopsList[i + 1] + ')' +
'"></div>'
);
}
return html;
}
var gradientCssText = (
'background: -ms-linear-gradient(top,' + stops + ');' +
'background: linear-gradient(to bottom,' + stops + ');'
);
hueHtml = (
'<div id="' + id + '-h" class="' + prefix + 'colorpicker-h" style="' + gradientCssText + '">' +
getOldIeFallbackHtml() +
'<div id="' + id + '-hp" class="' + prefix + 'colorpicker-h-marker"></div>' +
'</div>'
);
return (
'<div id="' + id + '" class="' + self.classes + '">' +
'<div id="' + id + '-sv" class="' + prefix + 'colorpicker-sv">' +
'<div class="' + prefix + 'colorpicker-overlay1">' +
'<div class="' + prefix + 'colorpicker-overlay2">' +
'<div id="' + id + '-svp" class="' + prefix + 'colorpicker-selector1">' +
'<div class="' + prefix + 'colorpicker-selector2"></div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
hueHtml +
'</div>'
);
}
});
});
// Included from: js/tinymce/classes/ui/Path.js
/**
* Path.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new path control.
*
* @-x-less Path.less
* @class tinymce.ui.Path
* @extends tinymce.ui.Widget
*/
define("tinymce/ui/Path", [
"tinymce/ui/Widget"
], function(Widget) {
"use strict";
return Widget.extend({
/**
* Constructs a instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {String} delimiter Delimiter to display between row in path.
*/
init: function(settings) {
var self = this;
if (!settings.delimiter) {
settings.delimiter = '\u00BB';
}
self._super(settings);
self.classes.add('path');
self.canFocus = true;
self.on('click', function(e) {
var index, target = e.target;
if ((index = target.getAttribute('data-index'))) {
self.fire('select', {value: self.row()[index], index: index});
}
});
self.row(self.settings.row);
},
/**
* Focuses the current control.
*
* @method focus
* @return {tinymce.ui.Control} Current control instance.
*/
focus: function() {
var self = this;
self.getEl().firstChild.focus();
return self;
},
/**
* Sets/gets the data to be used for the path.
*
* @method row
* @param {Array} row Array with row name is rendered to path.
*/
row: function(row) {
if (!arguments.length) {
return this.state.get('row');
}
this.state.set('row', row);
return this;
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this;
return (
'<div id="' + self._id + '" class="' + self.classes + '">' +
self._getDataPathHtml(self.state.get('row')) +
'</div>'
);
},
bindStates: function() {
var self = this;
self.state.on('change:row', function(e) {
self.innerHtml(self._getDataPathHtml(e.value));
});
return self._super();
},
_getDataPathHtml: function(data) {
var self = this, parts = data || [], i, l, html = '', prefix = self.classPrefix;
for (i = 0, l = parts.length; i < l; i++) {
html += (
(i > 0 ? '<div class="' + prefix + 'divider" aria-hidden="true"> ' + self.settings.delimiter + ' </div>' : '') +
'<div role="button" class="' + prefix + 'path-item' + (i == l - 1 ? ' ' + prefix + 'last' : '') + '" data-index="' +
i + '" tabindex="-1" id="' + self._id + '-' + i + '" aria-level="' + (i + 1) + '">' + parts[i].name + '</div>'
);
}
if (!html) {
html = '<div class="' + prefix + 'path-item">\u00a0</div>';
}
return html;
}
});
});
// Included from: js/tinymce/classes/ui/ElementPath.js
/**
* ElementPath.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This control creates an path for the current selections parent elements in TinyMCE.
*
* @class tinymce.ui.ElementPath
* @extends tinymce.ui.Path
*/
define("tinymce/ui/ElementPath", [
"tinymce/ui/Path"
], function(Path) {
return Path.extend({
/**
* Post render method. Called after the control has been rendered to the target.
*
* @method postRender
* @return {tinymce.ui.ElementPath} Current combobox instance.
*/
postRender: function() {
var self = this, editor = self.settings.editor;
function isHidden(elm) {
if (elm.nodeType === 1) {
if (elm.nodeName == "BR" || !!elm.getAttribute('data-mce-bogus')) {
return true;
}
if (elm.getAttribute('data-mce-type') === 'bookmark') {
return true;
}
}
return false;
}
if (editor.settings.elementpath !== false) {
self.on('select', function(e) {
editor.focus();
editor.selection.select(this.row()[e.index].element);
editor.nodeChanged();
});
editor.on('nodeChange', function(e) {
var outParents = [], parents = e.parents, i = parents.length;
while (i--) {
if (parents[i].nodeType == 1 && !isHidden(parents[i])) {
var args = editor.fire('ResolveName', {
name: parents[i].nodeName.toLowerCase(),
target: parents[i]
});
if (!args.isDefaultPrevented()) {
outParents.push({name: args.name, element: parents[i]});
}
if (args.isPropagationStopped()) {
break;
}
}
}
self.row(outParents);
});
}
return self._super();
}
});
});
// Included from: js/tinymce/classes/ui/FormItem.js
/**
* FormItem.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class is a container created by the form element with
* a label and control item.
*
* @class tinymce.ui.FormItem
* @extends tinymce.ui.Container
* @setting {String} label Label to display for the form item.
*/
define("tinymce/ui/FormItem", [
"tinymce/ui/Container"
], function(Container) {
"use strict";
return Container.extend({
Defaults: {
layout: 'flex',
align: 'center',
defaults: {
flex: 1
}
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, layout = self._layout, prefix = self.classPrefix;
self.classes.add('formitem');
layout.preRender(self);
return (
'<div id="' + self._id + '" class="' + self.classes + '" hidefocus="1" tabindex="-1">' +
(self.settings.title ? ('<div id="' + self._id + '-title" class="' + prefix + 'title">' +
self.settings.title + '</div>') : '') +
'<div id="' + self._id + '-body" class="' + self.bodyClasses + '">' +
(self.settings.html || '') + layout.renderHtml(self) +
'</div>' +
'</div>'
);
}
});
});
// Included from: js/tinymce/classes/ui/Form.js
/**
* Form.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class creates a form container. A form container has the ability
* to automatically wrap items in tinymce.ui.FormItem instances.
*
* Each FormItem instance is a container for the label and the item.
*
* @example
* tinymce.ui.Factory.create({
* type: 'form',
* items: [
* {type: 'textbox', label: 'My text box'}
* ]
* }).renderTo(document.body);
*
* @class tinymce.ui.Form
* @extends tinymce.ui.Container
*/
define("tinymce/ui/Form", [
"tinymce/ui/Container",
"tinymce/ui/FormItem",
"tinymce/util/Tools"
], function(Container, FormItem, Tools) {
"use strict";
return Container.extend({
Defaults: {
containerCls: 'form',
layout: 'flex',
direction: 'column',
align: 'stretch',
flex: 1,
padding: 20,
labelGap: 30,
spacing: 10,
callbacks: {
submit: function() {
this.submit();
}
}
},
/**
* This method gets invoked before the control is rendered.
*
* @method preRender
*/
preRender: function() {
var self = this, items = self.items();
if (!self.settings.formItemDefaults) {
self.settings.formItemDefaults = {
layout: 'flex',
autoResize: "overflow",
defaults: {flex: 1}
};
}
// Wrap any labeled items in FormItems
items.each(function(ctrl) {
var formItem, label = ctrl.settings.label;
if (label) {
formItem = new FormItem(Tools.extend({
items: {
type: 'label',
id: ctrl._id + '-l',
text: label,
flex: 0,
forId: ctrl._id,
disabled: ctrl.disabled()
}
}, self.settings.formItemDefaults));
formItem.type = 'formitem';
ctrl.aria('labelledby', ctrl._id + '-l');
if (typeof ctrl.settings.flex == "undefined") {
ctrl.settings.flex = 1;
}
self.replace(ctrl, formItem);
formItem.add(ctrl);
}
});
},
/**
* Fires a submit event with the serialized form.
*
* @method submit
* @return {Object} Event arguments object.
*/
submit: function() {
return this.fire('submit', {data: this.toJSON()});
},
/**
* Post render method. Called after the control has been rendered to the target.
*
* @method postRender
* @return {tinymce.ui.ComboBox} Current combobox instance.
*/
postRender: function() {
var self = this;
self._super();
self.fromJSON(self.settings.data);
},
bindStates: function() {
var self = this;
self._super();
function recalcLabels() {
var maxLabelWidth = 0, labels = [], i, labelGap, items;
if (self.settings.labelGapCalc === false) {
return;
}
if (self.settings.labelGapCalc == "children") {
items = self.find('formitem');
} else {
items = self.items();
}
items.filter('formitem').each(function(item) {
var labelCtrl = item.items()[0], labelWidth = labelCtrl.getEl().clientWidth;
maxLabelWidth = labelWidth > maxLabelWidth ? labelWidth : maxLabelWidth;
labels.push(labelCtrl);
});
labelGap = self.settings.labelGap || 0;
i = labels.length;
while (i--) {
labels[i].settings.minWidth = maxLabelWidth + labelGap;
}
}
self.on('show', recalcLabels);
recalcLabels();
}
});
});
// Included from: js/tinymce/classes/ui/FieldSet.js
/**
* FieldSet.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class creates fieldset containers.
*
* @-x-less FieldSet.less
* @class tinymce.ui.FieldSet
* @extends tinymce.ui.Form
*/
define("tinymce/ui/FieldSet", [
"tinymce/ui/Form"
], function(Form) {
"use strict";
return Form.extend({
Defaults: {
containerCls: 'fieldset',
layout: 'flex',
direction: 'column',
align: 'stretch',
flex: 1,
padding: "25 15 5 15",
labelGap: 30,
spacing: 10,
border: 1
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, layout = self._layout, prefix = self.classPrefix;
self.preRender();
layout.preRender(self);
return (
'<fieldset id="' + self._id + '" class="' + self.classes + '" hidefocus="1" tabindex="-1">' +
(self.settings.title ? ('<legend id="' + self._id + '-title" class="' + prefix + 'fieldset-title">' +
self.settings.title + '</legend>') : '') +
'<div id="' + self._id + '-body" class="' + self.bodyClasses + '">' +
(self.settings.html || '') + layout.renderHtml(self) +
'</div>' +
'</fieldset>'
);
}
});
});
// Included from: js/tinymce/classes/content/LinkTargets.js
/**
* LinkTargets.js
*
* Released under LGPL License.
* Copyright (c) 1999-2016 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This module is enables you to get anything that you can link to in a element.
*
* @private
* @class tinymce.content.LinkTargets
*/
define('tinymce/content/LinkTargets', [
'tinymce/dom/DOMUtils',
'tinymce/util/Fun',
'tinymce/util/Arr',
'tinymce/util/Uuid',
'tinymce/util/Tools',
'tinymce/dom/NodeType'
], function(
DOMUtils,
Fun,
Arr,
Uuid,
Tools,
NodeType
) {
var trim = Tools.trim;
var create = function (type, title, url, level, attach) {
return {
type: type,
title: title,
url: url,
level: level,
attach: attach
};
};
var isChildOfContentEditableTrue = function (node) {
while ((node = node.parentNode)) {
var value = node.contentEditable;
if (value && value !== 'inherit') {
return NodeType.isContentEditableTrue(node);
}
}
return false;
};
var select = function (selector, root) {
return DOMUtils.DOM.select(selector, root);
};
var getElementText = function (elm) {
return elm.innerText || elm.textContent;
};
var getOrGenerateId = function (elm) {
return elm.id ? elm.id : Uuid.uuid('h');
};
var isAnchor = function (elm) {
return elm && elm.nodeName === 'A' && (elm.id || elm.name);
};
var isValidAnchor = function (elm) {
return isAnchor(elm) && isEditable(elm);
};
var isHeader = function (elm) {
return elm && /^(H[1-6])$/.test(elm.nodeName);
};
var isEditable = function (elm) {
return isChildOfContentEditableTrue(elm) && !NodeType.isContentEditableFalse(elm);
};
var isValidHeader = function (elm) {
return isHeader(elm) && isEditable(elm);
};
var getLevel = function (elm) {
return isHeader(elm) ? parseInt(elm.nodeName.substr(1), 10) : 0;
};
var headerTarget = function (elm) {
var headerId = getOrGenerateId(elm);
var attach = function () {
elm.id = headerId;
};
return create('header', getElementText(elm), '#' + headerId, getLevel(elm), attach);
};
var anchorTarget = function (elm) {
var anchorId = elm.id || elm.name;
var anchorText = getElementText(elm);
return create('anchor', anchorText ? anchorText : '#' + anchorId, '#' + anchorId, 0, Fun.noop);
};
var getHeaderTargets = function (elms) {
return Arr.map(Arr.filter(elms, isValidHeader), headerTarget);
};
var getAnchorTargets = function (elms) {
return Arr.map(Arr.filter(elms, isValidAnchor), anchorTarget);
};
var getTargetElements = function (elm) {
var elms = select('h1,h2,h3,h4,h5,h6,a:not([href])', elm);
return elms;
};
var hasTitle = function (target) {
return trim(target.title).length > 0;
};
var find = function (elm) {
var elms = getTargetElements(elm);
return Arr.filter(getHeaderTargets(elms).concat(getAnchorTargets(elms)), hasTitle);
};
return {
find: find
};
});
// Included from: js/tinymce/classes/ui/FilePicker.js
/**
* FilePicker.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/*global tinymce:true */
/**
* This class creates a file picker control.
*
* @class tinymce.ui.FilePicker
* @extends tinymce.ui.ComboBox
*/
define("tinymce/ui/FilePicker", [
"tinymce/ui/ComboBox",
"tinymce/util/Tools",
"tinymce/util/Arr",
"tinymce/util/Fun",
"tinymce/util/VK",
"tinymce/content/LinkTargets"
], function(ComboBox, Tools, Arr, Fun, VK, LinkTargets) {
"use strict";
var history = {};
var HISTORY_LENGTH = 5;
var toMenuItem = function (target) {
return {
title: target.title,
value: {
title: {raw: target.title},
url: target.url,
attach: target.attach
}
};
};
var toMenuItems = function (targets) {
return Tools.map(targets, toMenuItem);
};
var staticMenuItem = function (title, url) {
return {
title: title,
value: {
title: title,
url: url,
attach: Fun.noop
}
};
};
var isUniqueUrl = function (url, targets) {
var foundTarget = Arr.find(targets, function (target) {
return target.url === url;
});
return !foundTarget;
};
var getSetting = function (editorSettings, name, defaultValue) {
var value = name in editorSettings ? editorSettings[name] : defaultValue;
return value === false ? null : value;
};
var createMenuItems = function (term, targets, fileType, editorSettings) {
var separator = {title: '-'};
var fromHistoryMenuItems = function (history) {
var uniqueHistory = Arr.filter(history[fileType], function (url) {
return isUniqueUrl(url, targets);
});
return Tools.map(uniqueHistory, function (url) {
return {
title: url,
value: {
title: url,
url: url,
attach: Fun.noop
}
};
});
};
var fromMenuItems = function (type) {
var filteredTargets = Arr.filter(targets, function (target) {
return target.type == type;
});
return toMenuItems(filteredTargets);
};
var anchorMenuItems = function () {
var anchorMenuItems = fromMenuItems('anchor');
var topAnchor = getSetting(editorSettings, 'anchor_top', '#top');
var bottomAchor = getSetting(editorSettings, 'anchor_bottom', '#bottom');
if (topAnchor !== null) {
anchorMenuItems.unshift(staticMenuItem('<top>', topAnchor));
}
if (bottomAchor !== null) {
anchorMenuItems.push(staticMenuItem('<bottom>', bottomAchor));
}
return anchorMenuItems;
};
var join = function (items) {
return Arr.reduce(items, function (a, b) {
var bothEmpty = a.length === 0 || b.length === 0;
return bothEmpty ? a.concat(b) : a.concat(separator, b);
}, []);
};
if (editorSettings.typeahead_urls === false) {
return [];
}
return fileType === 'file' ? join([
filterByQuery(term, fromHistoryMenuItems(history)),
filterByQuery(term, fromMenuItems('header')),
filterByQuery(term, anchorMenuItems())
]) : filterByQuery(term, fromHistoryMenuItems(history));
};
var addToHistory = function (url, fileType) {
var items = history[fileType];
if (!/^https?/.test(url)) {
return;
}
if (items) {
if (Arr.indexOf(items, url) === -1) {
history[fileType] = items.slice(0, HISTORY_LENGTH).concat(url);
}
} else {
history[fileType] = [url];
}
};
var filterByQuery = function (term, menuItems) {
var lowerCaseTerm = term.toLowerCase();
var result = Tools.grep(menuItems, function (item) {
return item.title.toLowerCase().indexOf(lowerCaseTerm) !== -1;
});
return result.length === 1 && result[0].title === term ? [] : result;
};
var getTitle = function (linkDetails) {
var title = linkDetails.title;
return title.raw ? title.raw : title;
};
var setupAutoCompleteHandler = function (ctrl, editorSettings, bodyElm, fileType) {
var autocomplete = function (term) {
var linkTargets = LinkTargets.find(bodyElm);
var menuItems = createMenuItems(term, linkTargets, fileType, editorSettings);
ctrl.showAutoComplete(menuItems, term);
};
ctrl.on('autocomplete', function () {
autocomplete(ctrl.value());
});
ctrl.on('selectitem', function (e) {
var linkDetails = e.value;
ctrl.value(linkDetails.url);
var title = getTitle(linkDetails);
if (fileType === 'image') {
ctrl.fire('change', {meta: {alt: title, attach: linkDetails.attach}});
} else {
ctrl.fire('change', {meta: {text: title, attach: linkDetails.attach}});
}
ctrl.focus();
});
ctrl.on('click', function (e) {
if (ctrl.value().length === 0 && e.target.nodeName === 'INPUT') {
autocomplete('');
}
});
ctrl.on('PostRender', function () {
ctrl.getRoot().on('submit', function (e) {
if (!e.isDefaultPrevented()) {
addToHistory(ctrl.value(), fileType);
}
});
});
};
var statusToUiState = function (result) {
var status = result.status, message = result.message;
if (status === 'valid') {
return {status: 'ok', message: message};
} else if (status === 'unknown') {
return {status: 'warn', message: message};
} else if (status === 'invalid') {
return {status: 'warn', message: message};
} else {
return {status: 'none', message: ''};
}
};
var setupLinkValidatorHandler = function (ctrl, editorSettings, fileType) {
var validatorHandler = editorSettings.filepicker_validator_handler;
if (validatorHandler) {
var validateUrl = function (url) {
if (url.length === 0) {
ctrl.statusLevel('none');
return;
}
validatorHandler({
url: url,
type: fileType
}, function (result) {
var uiState = statusToUiState(result);
ctrl.statusMessage(uiState.message);
ctrl.statusLevel(uiState.status);
});
};
ctrl.state.on('change:value', function (e) {
validateUrl(e.value);
});
}
};
return ComboBox.extend({
/**
* Constructs a new control instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
*/
init: function(settings) {
var self = this, editor = tinymce.activeEditor, editorSettings = editor.settings;
var actionCallback, fileBrowserCallback, fileBrowserCallbackTypes;
var fileType = settings.filetype;
settings.spellcheck = false;
fileBrowserCallbackTypes = editorSettings.file_picker_types || editorSettings.file_browser_callback_types;
if (fileBrowserCallbackTypes) {
fileBrowserCallbackTypes = Tools.makeMap(fileBrowserCallbackTypes, /[, ]/);
}
if (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[fileType]) {
fileBrowserCallback = editorSettings.file_picker_callback;
if (fileBrowserCallback && (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[fileType])) {
actionCallback = function() {
var meta = self.fire('beforecall').meta;
meta = Tools.extend({filetype: fileType}, meta);
// file_picker_callback(callback, currentValue, metaData)
fileBrowserCallback.call(
editor,
function(value, meta) {
self.value(value).fire('change', {meta: meta});
},
self.value(),
meta
);
};
} else {
// Legacy callback: file_picker_callback(id, currentValue, filetype, window)
fileBrowserCallback = editorSettings.file_browser_callback;
if (fileBrowserCallback && (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[fileType])) {
actionCallback = function() {
fileBrowserCallback(
self.getEl('inp').id,
self.value(),
fileType,
window
);
};
}
}
}
if (actionCallback) {
settings.icon = 'browse';
settings.onaction = actionCallback;
}
self._super(settings);
setupAutoCompleteHandler(self, editorSettings, editor.getBody(), fileType);
setupLinkValidatorHandler(self, editorSettings, fileType);
}
});
});
// Included from: js/tinymce/classes/ui/FitLayout.js
/**
* FitLayout.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This layout manager will resize the control to be the size of it's parent container.
* In other words width: 100% and height: 100%.
*
* @-x-less FitLayout.less
* @class tinymce.ui.FitLayout
* @extends tinymce.ui.AbsoluteLayout
*/
define("tinymce/ui/FitLayout", [
"tinymce/ui/AbsoluteLayout"
], function(AbsoluteLayout) {
"use strict";
return AbsoluteLayout.extend({
/**
* Recalculates the positions of the controls in the specified container.
*
* @method recalc
* @param {tinymce.ui.Container} container Container instance to recalc.
*/
recalc: function(container) {
var contLayoutRect = container.layoutRect(), paddingBox = container.paddingBox;
container.items().filter(':visible').each(function(ctrl) {
ctrl.layoutRect({
x: paddingBox.left,
y: paddingBox.top,
w: contLayoutRect.innerW - paddingBox.right - paddingBox.left,
h: contLayoutRect.innerH - paddingBox.top - paddingBox.bottom
});
if (ctrl.recalc) {
ctrl.recalc();
}
});
}
});
});
// Included from: js/tinymce/classes/ui/FlexLayout.js
/**
* FlexLayout.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This layout manager works similar to the CSS flex box.
*
* @setting {String} direction row|row-reverse|column|column-reverse
* @setting {Number} flex A positive-number to flex by.
* @setting {String} align start|end|center|stretch
* @setting {String} pack start|end|justify
*
* @class tinymce.ui.FlexLayout
* @extends tinymce.ui.AbsoluteLayout
*/
define("tinymce/ui/FlexLayout", [
"tinymce/ui/AbsoluteLayout"
], function(AbsoluteLayout) {
"use strict";
return AbsoluteLayout.extend({
/**
* Recalculates the positions of the controls in the specified container.
*
* @method recalc
* @param {tinymce.ui.Container} container Container instance to recalc.
*/
recalc: function(container) {
// A ton of variables, needs to be in the same scope for performance
var i, l, items, contLayoutRect, contPaddingBox, contSettings, align, pack, spacing, totalFlex, availableSpace, direction;
var ctrl, ctrlLayoutRect, ctrlSettings, flex, maxSizeItems = [], size, maxSize, ratio, rect, pos, maxAlignEndPos;
var sizeName, minSizeName, posName, maxSizeName, beforeName, innerSizeName, deltaSizeName, contentSizeName;
var alignAxisName, alignInnerSizeName, alignSizeName, alignMinSizeName, alignBeforeName, alignAfterName;
var alignDeltaSizeName, alignContentSizeName;
var max = Math.max, min = Math.min;
// Get container items, properties and settings
items = container.items().filter(':visible');
contLayoutRect = container.layoutRect();
contPaddingBox = container.paddingBox;
contSettings = container.settings;
direction = container.isRtl() ? (contSettings.direction || 'row-reversed') : contSettings.direction;
align = contSettings.align;
pack = container.isRtl() ? (contSettings.pack || 'end') : contSettings.pack;
spacing = contSettings.spacing || 0;
if (direction == "row-reversed" || direction == "column-reverse") {
items = items.set(items.toArray().reverse());
direction = direction.split('-')[0];
}
// Setup axis variable name for row/column direction since the calculations is the same
if (direction == "column") {
posName = "y";
sizeName = "h";
minSizeName = "minH";
maxSizeName = "maxH";
innerSizeName = "innerH";
beforeName = 'top';
deltaSizeName = "deltaH";
contentSizeName = "contentH";
alignBeforeName = "left";
alignSizeName = "w";
alignAxisName = "x";
alignInnerSizeName = "innerW";
alignMinSizeName = "minW";
alignAfterName = "right";
alignDeltaSizeName = "deltaW";
alignContentSizeName = "contentW";
} else {
posName = "x";
sizeName = "w";
minSizeName = "minW";
maxSizeName = "maxW";
innerSizeName = "innerW";
beforeName = 'left';
deltaSizeName = "deltaW";
contentSizeName = "contentW";
alignBeforeName = "top";
alignSizeName = "h";
alignAxisName = "y";
alignInnerSizeName = "innerH";
alignMinSizeName = "minH";
alignAfterName = "bottom";
alignDeltaSizeName = "deltaH";
alignContentSizeName = "contentH";
}
// Figure out total flex, availableSpace and collect any max size elements
availableSpace = contLayoutRect[innerSizeName] - contPaddingBox[beforeName] - contPaddingBox[beforeName];
maxAlignEndPos = totalFlex = 0;
for (i = 0, l = items.length; i < l; i++) {
ctrl = items[i];
ctrlLayoutRect = ctrl.layoutRect();
ctrlSettings = ctrl.settings;
flex = ctrlSettings.flex;
availableSpace -= (i < l - 1 ? spacing : 0);
if (flex > 0) {
totalFlex += flex;
// Flexed item has a max size then we need to check if we will hit that size
if (ctrlLayoutRect[maxSizeName]) {
maxSizeItems.push(ctrl);
}
ctrlLayoutRect.flex = flex;
}
availableSpace -= ctrlLayoutRect[minSizeName];
// Calculate the align end position to be used to check for overflow/underflow
size = contPaddingBox[alignBeforeName] + ctrlLayoutRect[alignMinSizeName] + contPaddingBox[alignAfterName];
if (size > maxAlignEndPos) {
maxAlignEndPos = size;
}
}
// Calculate minW/minH
rect = {};
if (availableSpace < 0) {
rect[minSizeName] = contLayoutRect[minSizeName] - availableSpace + contLayoutRect[deltaSizeName];
} else {
rect[minSizeName] = contLayoutRect[innerSizeName] - availableSpace + contLayoutRect[deltaSizeName];
}
rect[alignMinSizeName] = maxAlignEndPos + contLayoutRect[alignDeltaSizeName];
rect[contentSizeName] = contLayoutRect[innerSizeName] - availableSpace;
rect[alignContentSizeName] = maxAlignEndPos;
rect.minW = min(rect.minW, contLayoutRect.maxW);
rect.minH = min(rect.minH, contLayoutRect.maxH);
rect.minW = max(rect.minW, contLayoutRect.startMinWidth);
rect.minH = max(rect.minH, contLayoutRect.startMinHeight);
// Resize container container if minSize was changed
if (contLayoutRect.autoResize && (rect.minW != contLayoutRect.minW || rect.minH != contLayoutRect.minH)) {
rect.w = rect.minW;
rect.h = rect.minH;
container.layoutRect(rect);
this.recalc(container);
// Forced recalc for example if items are hidden/shown
if (container._lastRect === null) {
var parentCtrl = container.parent();
if (parentCtrl) {
parentCtrl._lastRect = null;
parentCtrl.recalc();
}
}
return;
}
// Handle max size elements, check if they will become to wide with current options
ratio = availableSpace / totalFlex;
for (i = 0, l = maxSizeItems.length; i < l; i++) {
ctrl = maxSizeItems[i];
ctrlLayoutRect = ctrl.layoutRect();
maxSize = ctrlLayoutRect[maxSizeName];
size = ctrlLayoutRect[minSizeName] + ctrlLayoutRect.flex * ratio;
if (size > maxSize) {
availableSpace -= (ctrlLayoutRect[maxSizeName] - ctrlLayoutRect[minSizeName]);
totalFlex -= ctrlLayoutRect.flex;
ctrlLayoutRect.flex = 0;
ctrlLayoutRect.maxFlexSize = maxSize;
} else {
ctrlLayoutRect.maxFlexSize = 0;
}
}
// Setup new ratio, target layout rect, start position
ratio = availableSpace / totalFlex;
pos = contPaddingBox[beforeName];
rect = {};
// Handle pack setting moves the start position to end, center
if (totalFlex === 0) {
if (pack == "end") {
pos = availableSpace + contPaddingBox[beforeName];
} else if (pack == "center") {
pos = Math.round(
(contLayoutRect[innerSizeName] / 2) - ((contLayoutRect[innerSizeName] - availableSpace) / 2)
) + contPaddingBox[beforeName];
if (pos < 0) {
pos = contPaddingBox[beforeName];
}
} else if (pack == "justify") {
pos = contPaddingBox[beforeName];
spacing = Math.floor(availableSpace / (items.length - 1));
}
}
// Default aligning (start) the other ones needs to be calculated while doing the layout
rect[alignAxisName] = contPaddingBox[alignBeforeName];
// Start laying out controls
for (i = 0, l = items.length; i < l; i++) {
ctrl = items[i];
ctrlLayoutRect = ctrl.layoutRect();
size = ctrlLayoutRect.maxFlexSize || ctrlLayoutRect[minSizeName];
// Align the control on the other axis
if (align === "center") {
rect[alignAxisName] = Math.round((contLayoutRect[alignInnerSizeName] / 2) - (ctrlLayoutRect[alignSizeName] / 2));
} else if (align === "stretch") {
rect[alignSizeName] = max(
ctrlLayoutRect[alignMinSizeName] || 0,
contLayoutRect[alignInnerSizeName] - contPaddingBox[alignBeforeName] - contPaddingBox[alignAfterName]
);
rect[alignAxisName] = contPaddingBox[alignBeforeName];
} else if (align === "end") {
rect[alignAxisName] = contLayoutRect[alignInnerSizeName] - ctrlLayoutRect[alignSizeName] - contPaddingBox.top;
}
// Calculate new size based on flex
if (ctrlLayoutRect.flex > 0) {
size += ctrlLayoutRect.flex * ratio;
}
rect[sizeName] = size;
rect[posName] = pos;
ctrl.layoutRect(rect);
// Recalculate containers
if (ctrl.recalc) {
ctrl.recalc();
}
// Move x/y position
pos += size + spacing;
}
}
});
});
// Included from: js/tinymce/classes/ui/FlowLayout.js
/**
* FlowLayout.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This layout manager will place the controls by using the browsers native layout.
*
* @-x-less FlowLayout.less
* @class tinymce.ui.FlowLayout
* @extends tinymce.ui.Layout
*/
define("tinymce/ui/FlowLayout", [
"tinymce/ui/Layout"
], function(Layout) {
return Layout.extend({
Defaults: {
containerClass: 'flow-layout',
controlClass: 'flow-layout-item',
endClass: 'break'
},
/**
* Recalculates the positions of the controls in the specified container.
*
* @method recalc
* @param {tinymce.ui.Container} container Container instance to recalc.
*/
recalc: function(container) {
container.items().filter(':visible').each(function(ctrl) {
if (ctrl.recalc) {
ctrl.recalc();
}
});
},
isNative: function() {
return true;
}
});
});
// Included from: js/tinymce/classes/ui/FormatControls.js
/**
* FormatControls.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Internal class containing all TinyMCE specific control types such as
* format listboxes, fontlist boxes, toolbar buttons etc.
*
* @class tinymce.ui.FormatControls
*/
define("tinymce/ui/FormatControls", [
"tinymce/ui/Control",
"tinymce/ui/Widget",
"tinymce/ui/FloatPanel",
"tinymce/util/Tools",
"tinymce/util/Arr",
"tinymce/dom/DOMUtils",
"tinymce/EditorManager",
"tinymce/Env"
], function(Control, Widget, FloatPanel, Tools, Arr, DOMUtils, EditorManager, Env) {
var each = Tools.each;
var flatten = function (ar) {
return Arr.reduce(ar, function (result, item) {
return result.concat(item);
}, []);
};
EditorManager.on('AddEditor', function(e) {
var editor = e.editor;
setupRtlMode(editor);
registerControls(editor);
setupContainer(editor);
});
Control.translate = function(text) {
return EditorManager.translate(text);
};
Widget.tooltips = !Env.iOS;
function setupContainer(editor) {
if (editor.settings.ui_container) {
Env.container = DOMUtils.DOM.select(editor.settings.ui_container)[0];
}
}
function setupRtlMode(editor) {
editor.on('ScriptsLoaded', function () {
if (editor.rtl) {
Control.rtl = true;
}
});
}
function registerControls(editor) {
var formatMenu;
function createListBoxChangeHandler(items, formatName) {
return function() {
var self = this;
editor.on('nodeChange', function(e) {
var formatter = editor.formatter;
var value = null;
each(e.parents, function(node) {
each(items, function(item) {
if (formatName) {
if (formatter.matchNode(node, formatName, {value: item.value})) {
value = item.value;
}
} else {
if (formatter.matchNode(node, item.value)) {
value = item.value;
}
}
if (value) {
return false;
}
});
if (value) {
return false;
}
});
self.value(value);
});
};
}
function createFormats(formats) {
formats = formats.replace(/;$/, '').split(';');
var i = formats.length;
while (i--) {
formats[i] = formats[i].split('=');
}
return formats;
}
function createFormatMenu() {
var count = 0, newFormats = [];
var defaultStyleFormats = [
{title: 'Headings', items: [
{title: 'Heading 1', format: 'h1'},
{title: 'Heading 2', format: 'h2'},
{title: 'Heading 3', format: 'h3'},
{title: 'Heading 4', format: 'h4'},
{title: 'Heading 5', format: 'h5'},
{title: 'Heading 6', format: 'h6'}
]},
{title: 'Inline', items: [
{title: 'Bold', icon: 'bold', format: 'bold'},
{title: 'Italic', icon: 'italic', format: 'italic'},
{title: 'Underline', icon: 'underline', format: 'underline'},
{title: 'Strikethrough', icon: 'strikethrough', format: 'strikethrough'},
{title: 'Superscript', icon: 'superscript', format: 'superscript'},
{title: 'Subscript', icon: 'subscript', format: 'subscript'},
{title: 'Code', icon: 'code', format: 'code'}
]},
{title: 'Blocks', items: [
{title: 'Paragraph', format: 'p'},
{title: 'Blockquote', format: 'blockquote'},
{title: 'Div', format: 'div'},
{title: 'Pre', format: 'pre'}
]},
{title: 'Alignment', items: [
{title: 'Left', icon: 'alignleft', format: 'alignleft'},
{title: 'Center', icon: 'aligncenter', format: 'aligncenter'},
{title: 'Right', icon: 'alignright', format: 'alignright'},
{title: 'Justify', icon: 'alignjustify', format: 'alignjustify'}
]}
];
function createMenu(formats) {
var menu = [];
if (!formats) {
return;
}
each(formats, function(format) {
var menuItem = {
text: format.title,
icon: format.icon
};
if (format.items) {
menuItem.menu = createMenu(format.items);
} else {
var formatName = format.format || "custom" + count++;
if (!format.format) {
format.name = formatName;
newFormats.push(format);
}
menuItem.format = formatName;
menuItem.cmd = format.cmd;
}
menu.push(menuItem);
});
return menu;
}
function createStylesMenu() {
var menu;
if (editor.settings.style_formats_merge) {
if (editor.settings.style_formats) {
menu = createMenu(defaultStyleFormats.concat(editor.settings.style_formats));
} else {
menu = createMenu(defaultStyleFormats);
}
} else {
menu = createMenu(editor.settings.style_formats || defaultStyleFormats);
}
return menu;
}
editor.on('init', function() {
each(newFormats, function(format) {
editor.formatter.register(format.name, format);
});
});
return {
type: 'menu',
items: createStylesMenu(),
onPostRender: function(e) {
editor.fire('renderFormatsMenu', {control: e.control});
},
itemDefaults: {
preview: true,
textStyle: function() {
if (this.settings.format) {
return editor.formatter.getCssText(this.settings.format);
}
},
onPostRender: function() {
var self = this;
self.parent().on('show', function() {
var formatName, command;
formatName = self.settings.format;
if (formatName) {
self.disabled(!editor.formatter.canApply(formatName));
self.active(editor.formatter.match(formatName));
}
command = self.settings.cmd;
if (command) {
self.active(editor.queryCommandState(command));
}
});
},
onclick: function() {
if (this.settings.format) {
toggleFormat(this.settings.format);
}
if (this.settings.cmd) {
editor.execCommand(this.settings.cmd);
}
}
}
};
}
formatMenu = createFormatMenu();
function initOnPostRender(name) {
return function() {
var self = this;
// TODO: Fix this
if (editor.formatter) {
editor.formatter.formatChanged(name, function(state) {
self.active(state);
});
} else {
editor.on('init', function() {
editor.formatter.formatChanged(name, function(state) {
self.active(state);
});
});
}
};
}
// Simple format controls <control/format>:<UI text>
each({
bold: 'Bold',
italic: 'Italic',
underline: 'Underline',
strikethrough: 'Strikethrough',
subscript: 'Subscript',
superscript: 'Superscript'
}, function(text, name) {
editor.addButton(name, {
tooltip: text,
onPostRender: initOnPostRender(name),
onclick: function() {
toggleFormat(name);
}
});
});
// Simple command controls <control>:[<UI text>,<Command>]
each({
outdent: ['Decrease indent', 'Outdent'],
indent: ['Increase indent', 'Indent'],
cut: ['Cut', 'Cut'],
copy: ['Copy', 'Copy'],
paste: ['Paste', 'Paste'],
help: ['Help', 'mceHelp'],
selectall: ['Select all', 'SelectAll'],
removeformat: ['Clear formatting', 'RemoveFormat'],
visualaid: ['Visual aids', 'mceToggleVisualAid'],
newdocument: ['New document', 'mceNewDocument']
}, function(item, name) {
editor.addButton(name, {
tooltip: item[0],
cmd: item[1]
});
});
// Simple command controls with format state
each({
blockquote: ['Blockquote', 'mceBlockQuote'],
subscript: ['Subscript', 'Subscript'],
superscript: ['Superscript', 'Superscript'],
alignleft: ['Align left', 'JustifyLeft'],
aligncenter: ['Align center', 'JustifyCenter'],
alignright: ['Align right', 'JustifyRight'],
alignjustify: ['Justify', 'JustifyFull'],
alignnone: ['No alignment', 'JustifyNone']
}, function(item, name) {
editor.addButton(name, {
tooltip: item[0],
cmd: item[1],
onPostRender: initOnPostRender(name)
});
});
function toggleUndoRedoState(type) {
return function() {
var self = this;
type = type == 'redo' ? 'hasRedo' : 'hasUndo';
function checkState() {
return editor.undoManager ? editor.undoManager[type]() : false;
}
self.disabled(!checkState());
editor.on('Undo Redo AddUndo TypingUndo ClearUndos SwitchMode', function() {
self.disabled(editor.readonly || !checkState());
});
};
}
function toggleVisualAidState() {
var self = this;
editor.on('VisualAid', function(e) {
self.active(e.hasVisual);
});
self.active(editor.hasVisual);
}
var trimMenuItems = function (menuItems) {
var outputMenuItems = menuItems;
if (outputMenuItems.length > 0 && outputMenuItems[0].text === '-') {
outputMenuItems = outputMenuItems.slice(1);
}
if (outputMenuItems.length > 0 && outputMenuItems[outputMenuItems.length - 1].text === '-') {
outputMenuItems = outputMenuItems.slice(0, outputMenuItems.length - 1);
}
return outputMenuItems;
};
var createCustomMenuItems = function (names) {
var items, nameList;
if (typeof names === 'string') {
nameList = names.split(' ');
} else if (Tools.isArray(names)) {
return flatten(Tools.map(names, createCustomMenuItems));
}
items = Tools.grep(nameList, function (name) {
return name === '|' || name in editor.menuItems;
});
return Tools.map(items, function (name) {
return name === '|' ? {text: '-'} : editor.menuItems[name];
});
};
var createContextMenuItems = function (context) {
var outputMenuItems = [{text: '-'}];
var menuItems = Tools.grep(editor.menuItems, function (menuItem) {
return menuItem.context === context;
});
Tools.each(menuItems, function (menuItem) {
if (menuItem.separator == 'before') {
outputMenuItems.push({text: '|'});
}
if (menuItem.prependToContext) {
outputMenuItems.unshift(menuItem);
} else {
outputMenuItems.push(menuItem);
}
if (menuItem.separator == 'after') {
outputMenuItems.push({text: '|'});
}
});
return outputMenuItems;
};
var createInsertMenu = function (editorSettings) {
if (editorSettings.insert_button_items) {
return trimMenuItems(createCustomMenuItems(editorSettings.insert_button_items));
} else {
return trimMenuItems(createContextMenuItems('insert'));
}
};
editor.addButton('undo', {
tooltip: 'Undo',
onPostRender: toggleUndoRedoState('undo'),
cmd: 'undo'
});
editor.addButton('redo', {
tooltip: 'Redo',
onPostRender: toggleUndoRedoState('redo'),
cmd: 'redo'
});
editor.addMenuItem('newdocument', {
text: 'New document',
icon: 'newdocument',
cmd: 'mceNewDocument'
});
editor.addMenuItem('undo', {
text: 'Undo',
icon: 'undo',
shortcut: 'Meta+Z',
onPostRender: toggleUndoRedoState('undo'),
cmd: 'undo'
});
editor.addMenuItem('redo', {
text: 'Redo',
icon: 'redo',
shortcut: 'Meta+Y',
onPostRender: toggleUndoRedoState('redo'),
cmd: 'redo'
});
editor.addMenuItem('visualaid', {
text: 'Visual aids',
selectable: true,
onPostRender: toggleVisualAidState,
cmd: 'mceToggleVisualAid'
});
editor.addButton('remove', {
tooltip: 'Remove',
icon: 'remove',
cmd: 'Delete'
});
editor.addButton('insert', {
type: 'menubutton',
icon: 'insert',
menu: [],
oncreatemenu: function () {
this.menu.add(createInsertMenu(editor.settings));
this.menu.renderNew();
}
});
each({
cut: ['Cut', 'Cut', 'Meta+X'],
copy: ['Copy', 'Copy', 'Meta+C'],
paste: ['Paste', 'Paste', 'Meta+V'],
selectall: ['Select all', 'SelectAll', 'Meta+A'],
bold: ['Bold', 'Bold', 'Meta+B'],
italic: ['Italic', 'Italic', 'Meta+I'],
underline: ['Underline', 'Underline'],
strikethrough: ['Strikethrough', 'Strikethrough'],
subscript: ['Subscript', 'Subscript'],
superscript: ['Superscript', 'Superscript'],
removeformat: ['Clear formatting', 'RemoveFormat']
}, function(item, name) {
editor.addMenuItem(name, {
text: item[0],
icon: name,
shortcut: item[2],
cmd: item[1]
});
});
editor.on('mousedown', function() {
FloatPanel.hideAll();
});
function toggleFormat(fmt) {
if (fmt.control) {
fmt = fmt.control.value();
}
if (fmt) {
editor.execCommand('mceToggleFormat', false, fmt);
}
}
function hideMenuObjects(menu) {
var count = menu.length;
Tools.each(menu, function (item) {
if (item.menu) {
item.hidden = hideMenuObjects(item.menu) === 0;
}
var formatName = item.format;
if (formatName) {
item.hidden = !editor.formatter.canApply(formatName);
}
if (item.hidden) {
count--;
}
});
return count;
}
function hideFormatMenuItems(menu) {
var count = menu.items().length;
menu.items().each(function (item) {
if (item.menu) {
item.visible(hideFormatMenuItems(item.menu) > 0);
}
if (!item.menu && item.settings.menu) {
item.visible(hideMenuObjects(item.settings.menu) > 0);
}
var formatName = item.settings.format;
if (formatName) {
item.visible(editor.formatter.canApply(formatName));
}
if (!item.visible()) {
count--;
}
});
return count;
}
editor.addButton('styleselect', {
type: 'menubutton',
text: 'Formats',
menu: formatMenu,
onShowMenu: function () {
if (editor.settings.style_formats_autohide) {
hideFormatMenuItems(this.menu);
}
}
});
editor.addButton('formatselect', function() {
var items = [], blocks = createFormats(editor.settings.block_formats ||
'Paragraph=p;' +
'Heading 1=h1;' +
'Heading 2=h2;' +
'Heading 3=h3;' +
'Heading 4=h4;' +
'Heading 5=h5;' +
'Heading 6=h6;' +
'Preformatted=pre'
);
each(blocks, function(block) {
items.push({
text: block[0],
value: block[1],
textStyle: function() {
return editor.formatter.getCssText(block[1]);
}
});
});
return {
type: 'listbox',
text: blocks[0][0],
values: items,
fixedWidth: true,
onselect: toggleFormat,
onPostRender: createListBoxChangeHandler(items)
};
});
editor.addButton('fontselect', function() {
var defaultFontsFormats =
'Andale Mono=andale mono,monospace;' +
'Arial=arial,helvetica,sans-serif;' +
'Arial Black=arial black,sans-serif;' +
'Book Antiqua=book antiqua,palatino,serif;' +
'Comic Sans MS=comic sans ms,sans-serif;' +
'Courier New=courier new,courier,monospace;' +
'Georgia=georgia,palatino,serif;' +
'Helvetica=helvetica,arial,sans-serif;' +
'Impact=impact,sans-serif;' +
'Symbol=symbol;' +
'Tahoma=tahoma,arial,helvetica,sans-serif;' +
'Terminal=terminal,monaco,monospace;' +
'Times New Roman=times new roman,times,serif;' +
'Trebuchet MS=trebuchet ms,geneva,sans-serif;' +
'Verdana=verdana,geneva,sans-serif;' +
'Webdings=webdings;' +
'Wingdings=wingdings,zapf dingbats';
var items = [], fonts = createFormats(editor.settings.font_formats || defaultFontsFormats);
each(fonts, function(font) {
items.push({
text: {raw: font[0]},
value: font[1],
textStyle: font[1].indexOf('dings') == -1 ? 'font-family:' + font[1] : ''
});
});
return {
type: 'listbox',
text: 'Font Family',
tooltip: 'Font Family',
values: items,
fixedWidth: true,
onPostRender: createListBoxChangeHandler(items, 'fontname'),
onselect: function(e) {
if (e.control.settings.value) {
editor.execCommand('FontName', false, e.control.settings.value);
}
}
};
});
editor.addButton('fontsizeselect', function() {
var items = [], defaultFontsizeFormats = '8pt 10pt 12pt 14pt 18pt 24pt 36pt';
var fontsize_formats = editor.settings.fontsize_formats || defaultFontsizeFormats;
each(fontsize_formats.split(' '), function(item) {
var text = item, value = item;
// Allow text=value font sizes.
var values = item.split('=');
if (values.length > 1) {
text = values[0];
value = values[1];
}
items.push({text: text, value: value});
});
return {
type: 'listbox',
text: 'Font Sizes',
tooltip: 'Font Sizes',
values: items,
fixedWidth: true,
onPostRender: createListBoxChangeHandler(items, 'fontsize'),
onclick: function(e) {
if (e.control.settings.value) {
editor.execCommand('FontSize', false, e.control.settings.value);
}
}
};
});
editor.addMenuItem('formats', {
text: 'Formats',
menu: formatMenu
});
}
});
// Included from: js/tinymce/classes/ui/GridLayout.js
/**
* GridLayout.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This layout manager places controls in a grid.
*
* @setting {Number} spacing Spacing between controls.
* @setting {Number} spacingH Horizontal spacing between controls.
* @setting {Number} spacingV Vertical spacing between controls.
* @setting {Number} columns Number of columns to use.
* @setting {String/Array} alignH start|end|center|stretch or array of values for each column.
* @setting {String/Array} alignV start|end|center|stretch or array of values for each column.
* @setting {String} pack start|end
*
* @class tinymce.ui.GridLayout
* @extends tinymce.ui.AbsoluteLayout
*/
define("tinymce/ui/GridLayout", [
"tinymce/ui/AbsoluteLayout"
], function(AbsoluteLayout) {
"use strict";
return AbsoluteLayout.extend({
/**
* Recalculates the positions of the controls in the specified container.
*
* @method recalc
* @param {tinymce.ui.Container} container Container instance to recalc.
*/
recalc: function(container) {
var settings, rows, cols, items, contLayoutRect, width, height, rect,
ctrlLayoutRect, ctrl, x, y, posX, posY, ctrlSettings, contPaddingBox, align, spacingH, spacingV, alignH, alignV, maxX, maxY,
colWidths = [], rowHeights = [], ctrlMinWidth, ctrlMinHeight, availableWidth, availableHeight, reverseRows, idx;
// Get layout settings
settings = container.settings;
items = container.items().filter(':visible');
contLayoutRect = container.layoutRect();
cols = settings.columns || Math.ceil(Math.sqrt(items.length));
rows = Math.ceil(items.length / cols);
spacingH = settings.spacingH || settings.spacing || 0;
spacingV = settings.spacingV || settings.spacing || 0;
alignH = settings.alignH || settings.align;
alignV = settings.alignV || settings.align;
contPaddingBox = container.paddingBox;
reverseRows = 'reverseRows' in settings ? settings.reverseRows : container.isRtl();
if (alignH && typeof alignH == "string") {
alignH = [alignH];
}
if (alignV && typeof alignV == "string") {
alignV = [alignV];
}
// Zero padd columnWidths
for (x = 0; x < cols; x++) {
colWidths.push(0);
}
// Zero padd rowHeights
for (y = 0; y < rows; y++) {
rowHeights.push(0);
}
// Calculate columnWidths and rowHeights
for (y = 0; y < rows; y++) {
for (x = 0; x < cols; x++) {
ctrl = items[y * cols + x];
// Out of bounds
if (!ctrl) {
break;
}
ctrlLayoutRect = ctrl.layoutRect();
ctrlMinWidth = ctrlLayoutRect.minW;
ctrlMinHeight = ctrlLayoutRect.minH;
colWidths[x] = ctrlMinWidth > colWidths[x] ? ctrlMinWidth : colWidths[x];
rowHeights[y] = ctrlMinHeight > rowHeights[y] ? ctrlMinHeight : rowHeights[y];
}
}
// Calculate maxX
availableWidth = contLayoutRect.innerW - contPaddingBox.left - contPaddingBox.right;
for (maxX = 0, x = 0; x < cols; x++) {
maxX += colWidths[x] + (x > 0 ? spacingH : 0);
availableWidth -= (x > 0 ? spacingH : 0) + colWidths[x];
}
// Calculate maxY
availableHeight = contLayoutRect.innerH - contPaddingBox.top - contPaddingBox.bottom;
for (maxY = 0, y = 0; y < rows; y++) {
maxY += rowHeights[y] + (y > 0 ? spacingV : 0);
availableHeight -= (y > 0 ? spacingV : 0) + rowHeights[y];
}
maxX += contPaddingBox.left + contPaddingBox.right;
maxY += contPaddingBox.top + contPaddingBox.bottom;
// Calculate minW/minH
rect = {};
rect.minW = maxX + (contLayoutRect.w - contLayoutRect.innerW);
rect.minH = maxY + (contLayoutRect.h - contLayoutRect.innerH);
rect.contentW = rect.minW - contLayoutRect.deltaW;
rect.contentH = rect.minH - contLayoutRect.deltaH;
rect.minW = Math.min(rect.minW, contLayoutRect.maxW);
rect.minH = Math.min(rect.minH, contLayoutRect.maxH);
rect.minW = Math.max(rect.minW, contLayoutRect.startMinWidth);
rect.minH = Math.max(rect.minH, contLayoutRect.startMinHeight);
// Resize container container if minSize was changed
if (contLayoutRect.autoResize && (rect.minW != contLayoutRect.minW || rect.minH != contLayoutRect.minH)) {
rect.w = rect.minW;
rect.h = rect.minH;
container.layoutRect(rect);
this.recalc(container);
// Forced recalc for example if items are hidden/shown
if (container._lastRect === null) {
var parentCtrl = container.parent();
if (parentCtrl) {
parentCtrl._lastRect = null;
parentCtrl.recalc();
}
}
return;
}
// Update contentW/contentH so absEnd moves correctly
if (contLayoutRect.autoResize) {
rect = container.layoutRect(rect);
rect.contentW = rect.minW - contLayoutRect.deltaW;
rect.contentH = rect.minH - contLayoutRect.deltaH;
}
var flexV;
if (settings.packV == 'start') {
flexV = 0;
} else {
flexV = availableHeight > 0 ? Math.floor(availableHeight / rows) : 0;
}
// Calculate totalFlex
var totalFlex = 0;
var flexWidths = settings.flexWidths;
if (flexWidths) {
for (x = 0; x < flexWidths.length; x++) {
totalFlex += flexWidths[x];
}
} else {
totalFlex = cols;
}
// Calculate new column widths based on flex values
var ratio = availableWidth / totalFlex;
for (x = 0; x < cols; x++) {
colWidths[x] += flexWidths ? flexWidths[x] * ratio : ratio;
}
// Move/resize controls
posY = contPaddingBox.top;
for (y = 0; y < rows; y++) {
posX = contPaddingBox.left;
height = rowHeights[y] + flexV;
for (x = 0; x < cols; x++) {
if (reverseRows) {
idx = y * cols + cols - 1 - x;
} else {
idx = y * cols + x;
}
ctrl = items[idx];
// No more controls to render then break
if (!ctrl) {
break;
}
// Get control settings and calculate x, y
ctrlSettings = ctrl.settings;
ctrlLayoutRect = ctrl.layoutRect();
width = Math.max(colWidths[x], ctrlLayoutRect.startMinWidth);
ctrlLayoutRect.x = posX;
ctrlLayoutRect.y = posY;
// Align control horizontal
align = ctrlSettings.alignH || (alignH ? (alignH[x] || alignH[0]) : null);
if (align == "center") {
ctrlLayoutRect.x = posX + (width / 2) - (ctrlLayoutRect.w / 2);
} else if (align == "right") {
ctrlLayoutRect.x = posX + width - ctrlLayoutRect.w;
} else if (align == "stretch") {
ctrlLayoutRect.w = width;
}
// Align control vertical
align = ctrlSettings.alignV || (alignV ? (alignV[x] || alignV[0]) : null);
if (align == "center") {
ctrlLayoutRect.y = posY + (height / 2) - (ctrlLayoutRect.h / 2);
} else if (align == "bottom") {
ctrlLayoutRect.y = posY + height - ctrlLayoutRect.h;
} else if (align == "stretch") {
ctrlLayoutRect.h = height;
}
ctrl.layoutRect(ctrlLayoutRect);
posX += width + spacingH;
if (ctrl.recalc) {
ctrl.recalc();
}
}
posY += height + spacingV;
}
}
});
});
// Included from: js/tinymce/classes/ui/Iframe.js
/**
* Iframe.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/*jshint scripturl:true */
/**
* This class creates an iframe.
*
* @setting {String} url Url to open in the iframe.
*
* @-x-less Iframe.less
* @class tinymce.ui.Iframe
* @extends tinymce.ui.Widget
*/
define("tinymce/ui/Iframe", [
"tinymce/ui/Widget",
"tinymce/util/Delay"
], function(Widget, Delay) {
"use strict";
return Widget.extend({
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this;
self.classes.add('iframe');
self.canFocus = false;
/*eslint no-script-url:0 */
return (
'<iframe id="' + self._id + '" class="' + self.classes + '" tabindex="-1" src="' +
(self.settings.url || "javascript:''") + '" frameborder="0"></iframe>'
);
},
/**
* Setter for the iframe source.
*
* @method src
* @param {String} src Source URL for iframe.
*/
src: function(src) {
this.getEl().src = src;
},
/**
* Inner HTML for the iframe.
*
* @method html
* @param {String} html HTML string to set as HTML inside the iframe.
* @param {function} callback Optional callback to execute when the iframe body is filled with contents.
* @return {tinymce.ui.Iframe} Current iframe control.
*/
html: function(html, callback) {
var self = this, body = this.getEl().contentWindow.document.body;
// Wait for iframe to initialize IE 10 takes time
if (!body) {
Delay.setTimeout(function() {
self.html(html);
});
} else {
body.innerHTML = html;
if (callback) {
callback();
}
}
return this;
}
});
});
// Included from: js/tinymce/classes/ui/InfoBox.js
/**
* InfoBox.js
*
* Released under LGPL License.
* Copyright (c) 1999-2016 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* ....
*
* @-x-less InfoBox.less
* @class tinymce.ui.InfoBox
* @extends tinymce.ui.Widget
*/
define("tinymce/ui/InfoBox", [
"tinymce/ui/Widget"
], function(Widget) {
"use strict";
return Widget.extend({
/**
* Constructs a instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {Boolean} multiline Multiline label.
*/
init: function(settings) {
var self = this;
self._super(settings);
self.classes.add('widget').add('infobox');
self.canFocus = false;
},
severity: function(level) {
this.classes.remove('error');
this.classes.remove('warning');
this.classes.remove('success');
this.classes.add(level);
},
help: function(state) {
this.state.set('help', state);
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, prefix = self.classPrefix;
return (
'<div id="' + self._id + '" class="' + self.classes + '">' +
'<div id="' + self._id + '-body">' +
self.encode(self.state.get('text')) +
'<button role="button" tabindex="-1">' +
'<i class="' + prefix + 'ico ' + prefix + 'i-help"></i>' +
'</button>' +
'</div>' +
'</div>'
);
},
bindStates: function() {
var self = this;
self.state.on('change:text', function(e) {
self.getEl('body').firstChild.data = self.encode(e.value);
if (self.state.get('rendered')) {
self.updateLayoutRect();
}
});
self.state.on('change:help', function(e) {
self.classes.toggle('has-help', e.value);
if (self.state.get('rendered')) {
self.updateLayoutRect();
}
});
return self._super();
}
});
});
// Included from: js/tinymce/classes/ui/Label.js
/**
* Label.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class creates a label element. A label is a simple text control
* that can be bound to other controls.
*
* @-x-less Label.less
* @class tinymce.ui.Label
* @extends tinymce.ui.Widget
*/
define("tinymce/ui/Label", [
"tinymce/ui/Widget",
"tinymce/ui/DomUtils"
], function(Widget, DomUtils) {
"use strict";
return Widget.extend({
/**
* Constructs a instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {Boolean} multiline Multiline label.
*/
init: function(settings) {
var self = this;
self._super(settings);
self.classes.add('widget').add('label');
self.canFocus = false;
if (settings.multiline) {
self.classes.add('autoscroll');
}
if (settings.strong) {
self.classes.add('strong');
}
},
/**
* Initializes the current controls layout rect.
* This will be executed by the layout managers to determine the
* default minWidth/minHeight etc.
*
* @method initLayoutRect
* @return {Object} Layout rect instance.
*/
initLayoutRect: function() {
var self = this, layoutRect = self._super();
if (self.settings.multiline) {
var size = DomUtils.getSize(self.getEl());
// Check if the text fits within maxW if not then try word wrapping it
if (size.width > layoutRect.maxW) {
layoutRect.minW = layoutRect.maxW;
self.classes.add('multiline');
}
self.getEl().style.width = layoutRect.minW + 'px';
layoutRect.startMinH = layoutRect.h = layoutRect.minH = Math.min(layoutRect.maxH, DomUtils.getSize(self.getEl()).height);
}
return layoutRect;
},
/**
* Repaints the control after a layout operation.
*
* @method repaint
*/
repaint: function() {
var self = this;
if (!self.settings.multiline) {
self.getEl().style.lineHeight = self.layoutRect().h + 'px';
}
return self._super();
},
severity: function(level) {
this.classes.remove('error');
this.classes.remove('warning');
this.classes.remove('success');
this.classes.add(level);
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, targetCtrl, forName, forId = self.settings.forId;
if (!forId && (forName = self.settings.forName)) {
targetCtrl = self.getRoot().find('#' + forName)[0];
if (targetCtrl) {
forId = targetCtrl._id;
}
}
if (forId) {
return (
'<label id="' + self._id + '" class="' + self.classes + '"' + (forId ? ' for="' + forId + '"' : '') + '>' +
self.encode(self.state.get('text')) +
'</label>'
);
}
return (
'<span id="' + self._id + '" class="' + self.classes + '">' +
self.encode(self.state.get('text')) +
'</span>'
);
},
bindStates: function() {
var self = this;
self.state.on('change:text', function(e) {
self.innerHtml(self.encode(e.value));
if (self.state.get('rendered')) {
self.updateLayoutRect();
}
});
return self._super();
}
});
});
// Included from: js/tinymce/classes/ui/Toolbar.js
/**
* Toolbar.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new toolbar.
*
* @class tinymce.ui.Toolbar
* @extends tinymce.ui.Container
*/
define("tinymce/ui/Toolbar", [
"tinymce/ui/Container"
], function(Container) {
"use strict";
return Container.extend({
Defaults: {
role: 'toolbar',
layout: 'flow'
},
/**
* Constructs a instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
*/
init: function(settings) {
var self = this;
self._super(settings);
self.classes.add('toolbar');
},
/**
* Called after the control has been rendered.
*
* @method postRender
*/
postRender: function() {
var self = this;
self.items().each(function(ctrl) {
ctrl.classes.add('toolbar-item');
});
return self._super();
}
});
});
// Included from: js/tinymce/classes/ui/MenuBar.js
/**
* MenuBar.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new menubar.
*
* @-x-less MenuBar.less
* @class tinymce.ui.MenuBar
* @extends tinymce.ui.Container
*/
define("tinymce/ui/MenuBar", [
"tinymce/ui/Toolbar"
], function(Toolbar) {
"use strict";
return Toolbar.extend({
Defaults: {
role: 'menubar',
containerCls: 'menubar',
ariaRoot: true,
defaults: {
type: 'menubutton'
}
}
});
});
// Included from: js/tinymce/classes/ui/MenuButton.js
/**
* MenuButton.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new menu button.
*
* @-x-less MenuButton.less
* @class tinymce.ui.MenuButton
* @extends tinymce.ui.Button
*/
define("tinymce/ui/MenuButton", [
"tinymce/ui/Button",
"tinymce/ui/Factory",
"tinymce/ui/MenuBar"
], function(Button, Factory, MenuBar) {
"use strict";
// TODO: Maybe add as some global function
function isChildOf(node, parent) {
while (node) {
if (parent === node) {
return true;
}
node = node.parentNode;
}
return false;
}
var MenuButton = Button.extend({
/**
* Constructs a instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
*/
init: function(settings) {
var self = this;
self._renderOpen = true;
self._super(settings);
settings = self.settings;
self.classes.add('menubtn');
if (settings.fixedWidth) {
self.classes.add('fixed-width');
}
self.aria('haspopup', true);
self.state.set('menu', settings.menu || self.render());
},
/**
* Shows the menu for the button.
*
* @method showMenu
*/
showMenu: function() {
var self = this, menu;
if (self.menu && self.menu.visible()) {
return self.hideMenu();
}
if (!self.menu) {
menu = self.state.get('menu') || [];
// Is menu array then auto constuct menu control
if (menu.length) {
menu = {
type: 'menu',
items: menu
};
} else {
menu.type = menu.type || 'menu';
}
if (!menu.renderTo) {
self.menu = Factory.create(menu).parent(self).renderTo();
} else {
self.menu = menu.parent(self).show().renderTo();
}
self.fire('createmenu');
self.menu.reflow();
self.menu.on('cancel', function(e) {
if (e.control.parent() === self.menu) {
e.stopPropagation();
self.focus();
self.hideMenu();
}
});
// Move focus to button when a menu item is selected/clicked
self.menu.on('select', function() {
self.focus();
});
self.menu.on('show hide', function(e) {
if (e.control == self.menu) {
self.activeMenu(e.type == 'show');
}
self.aria('expanded', e.type == 'show');
}).fire('show');
}
self.menu.show();
self.menu.layoutRect({w: self.layoutRect().w});
self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']);
self.fire('showmenu');
},
/**
* Hides the menu for the button.
*
* @method hideMenu
*/
hideMenu: function() {
var self = this;
if (self.menu) {
self.menu.items().each(function(item) {
if (item.hideMenu) {
item.hideMenu();
}
});
self.menu.hide();
}
},
/**
* Sets the active menu state.
*
* @private
*/
activeMenu: function(state) {
this.classes.toggle('active', state);
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, id = self._id, prefix = self.classPrefix;
var icon = self.settings.icon, image, text = self.state.get('text'),
textHtml = '';
image = self.settings.image;
if (image) {
icon = 'none';
// Support for [high dpi, low dpi] image sources
if (typeof image != "string") {
image = window.getSelection ? image[0] : image[1];
}
image = ' style="background-image: url(\'' + image + '\')"';
} else {
image = '';
}
if (text) {
self.classes.add('btn-has-text');
textHtml = '<span class="' + prefix + 'txt">' + self.encode(text) + '</span>';
}
icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : '';
self.aria('role', self.parent() instanceof MenuBar ? 'menuitem' : 'button');
return (
'<div id="' + id + '" class="' + self.classes + '" tabindex="-1" aria-labelledby="' + id + '">' +
'<button id="' + id + '-open" role="presentation" type="button" tabindex="-1">' +
(icon ? '<i class="' + icon + '"' + image + '></i>' : '') +
textHtml +
' <i class="' + prefix + 'caret"></i>' +
'</button>' +
'</div>'
);
},
/**
* Gets invoked after the control has been rendered.
*
* @method postRender
*/
postRender: function() {
var self = this;
self.on('click', function(e) {
if (e.control === self && isChildOf(e.target, self.getEl())) {
self.showMenu();
if (e.aria) {
self.menu.items().filter(':visible')[0].focus();
}
}
});
self.on('mouseenter', function(e) {
var overCtrl = e.control, parent = self.parent(), hasVisibleSiblingMenu;
if (overCtrl && parent && overCtrl instanceof MenuButton && overCtrl.parent() == parent) {
parent.items().filter('MenuButton').each(function(ctrl) {
if (ctrl.hideMenu && ctrl != overCtrl) {
if (ctrl.menu && ctrl.menu.visible()) {
hasVisibleSiblingMenu = true;
}
ctrl.hideMenu();
}
});
if (hasVisibleSiblingMenu) {
overCtrl.focus(); // Fix for: #5887
overCtrl.showMenu();
}
}
});
return self._super();
},
bindStates: function() {
var self = this;
self.state.on('change:menu', function() {
if (self.menu) {
self.menu.remove();
}
self.menu = null;
});
return self._super();
},
/**
* Removes the control and it's menus.
*
* @method remove
*/
remove: function() {
this._super();
if (this.menu) {
this.menu.remove();
}
}
});
return MenuButton;
});
// Included from: js/tinymce/classes/ui/MenuItem.js
/**
* MenuItem.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new menu item.
*
* @-x-less MenuItem.less
* @class tinymce.ui.MenuItem
* @extends tinymce.ui.Control
*/
define("tinymce/ui/MenuItem", [
"tinymce/ui/Widget",
"tinymce/ui/Factory",
"tinymce/Env",
"tinymce/util/Delay"
], function(Widget, Factory, Env, Delay) {
"use strict";
return Widget.extend({
Defaults: {
border: 0,
role: 'menuitem'
},
/**
* Constructs a instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {Boolean} selectable Selectable menu.
* @setting {Array} menu Submenu array with items.
* @setting {String} shortcut Shortcut to display for menu item. Example: Ctrl+X
*/
init: function(settings) {
var self = this, text;
self._super(settings);
settings = self.settings;
self.classes.add('menu-item');
if (settings.menu) {
self.classes.add('menu-item-expand');
}
if (settings.preview) {
self.classes.add('menu-item-preview');
}
text = self.state.get('text');
if (text === '-' || text === '|') {
self.classes.add('menu-item-sep');
self.aria('role', 'separator');
self.state.set('text', '-');
}
if (settings.selectable) {
self.aria('role', 'menuitemcheckbox');
self.classes.add('menu-item-checkbox');
settings.icon = 'selected';
}
if (!settings.preview && !settings.selectable) {
self.classes.add('menu-item-normal');
}
self.on('mousedown', function(e) {
e.preventDefault();
});
if (settings.menu && !settings.ariaHideMenu) {
self.aria('haspopup', true);
}
},
/**
* Returns true/false if the menuitem has sub menu.
*
* @method hasMenus
* @return {Boolean} True/false state if it has submenu.
*/
hasMenus: function() {
return !!this.settings.menu;
},
/**
* Shows the menu for the menu item.
*
* @method showMenu
*/
showMenu: function() {
var self = this, settings = self.settings, menu, parent = self.parent();
parent.items().each(function(ctrl) {
if (ctrl !== self) {
ctrl.hideMenu();
}
});
if (settings.menu) {
menu = self.menu;
if (!menu) {
menu = settings.menu;
// Is menu array then auto constuct menu control
if (menu.length) {
menu = {
type: 'menu',
items: menu
};
} else {
menu.type = menu.type || 'menu';
}
if (parent.settings.itemDefaults) {
menu.itemDefaults = parent.settings.itemDefaults;
}
menu = self.menu = Factory.create(menu).parent(self).renderTo();
menu.reflow();
menu.on('cancel', function(e) {
e.stopPropagation();
self.focus();
menu.hide();
});
menu.on('show hide', function(e) {
if (e.control.items) {
e.control.items().each(function(ctrl) {
ctrl.active(ctrl.settings.selected);
});
}
}).fire('show');
menu.on('hide', function(e) {
if (e.control === menu) {
self.classes.remove('selected');
}
});
menu.submenu = true;
} else {
menu.show();
}
menu._parentMenu = parent;
menu.classes.add('menu-sub');
var rel = menu.testMoveRel(
self.getEl(),
self.isRtl() ? ['tl-tr', 'bl-br', 'tr-tl', 'br-bl'] : ['tr-tl', 'br-bl', 'tl-tr', 'bl-br']
);
menu.moveRel(self.getEl(), rel);
menu.rel = rel;
rel = 'menu-sub-' + rel;
menu.classes.remove(menu._lastRel).add(rel);
menu._lastRel = rel;
self.classes.add('selected');
self.aria('expanded', true);
}
},
/**
* Hides the menu for the menu item.
*
* @method hideMenu
*/
hideMenu: function() {
var self = this;
if (self.menu) {
self.menu.items().each(function(item) {
if (item.hideMenu) {
item.hideMenu();
}
});
self.menu.hide();
self.aria('expanded', false);
}
return self;
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, id = self._id, settings = self.settings, prefix = self.classPrefix, text = self.state.get('text');
var icon = self.settings.icon, image = '', shortcut = settings.shortcut;
var url = self.encode(settings.url), iconHtml = '';
// Converts shortcut format to Mac/PC variants
function convertShortcut(shortcut) {
var i, value, replace = {};
if (Env.mac) {
replace = {
alt: '&#x2325;',
ctrl: '&#x2318;',
shift: '&#x21E7;',
meta: '&#x2318;'
};
} else {
replace = {
meta: 'Ctrl'
};
}
shortcut = shortcut.split('+');
for (i = 0; i < shortcut.length; i++) {
value = replace[shortcut[i].toLowerCase()];
if (value) {
shortcut[i] = value;
}
}
return shortcut.join('+');
}
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function markMatches(text) {
var match = settings.match || '';
return match ? text.replace(new RegExp(escapeRegExp(match), 'gi'), function (match) {
return '!mce~match[' + match + ']mce~match!';
}) : text;
}
function boldMatches(text) {
return text.
replace(new RegExp(escapeRegExp('!mce~match['), 'g'), '<b>').
replace(new RegExp(escapeRegExp(']mce~match!'), 'g'), '</b>');
}
if (icon) {
self.parent().classes.add('menu-has-icons');
}
if (settings.image) {
image = ' style="background-image: url(\'' + settings.image + '\')"';
}
if (shortcut) {
shortcut = convertShortcut(shortcut);
}
icon = prefix + 'ico ' + prefix + 'i-' + (self.settings.icon || 'none');
iconHtml = (text !== '-' ? '<i class="' + icon + '"' + image + '></i>\u00a0' : '');
text = boldMatches(self.encode(markMatches(text)));
url = boldMatches(self.encode(markMatches(url)));
return (
'<div id="' + id + '" class="' + self.classes + '" tabindex="-1">' +
iconHtml +
(text !== '-' ? '<span id="' + id + '-text" class="' + prefix + 'text">' + text + '</span>' : '') +
(shortcut ? '<div id="' + id + '-shortcut" class="' + prefix + 'menu-shortcut">' + shortcut + '</div>' : '') +
(settings.menu ? '<div class="' + prefix + 'caret"></div>' : '') +
(url ? '<div class="' + prefix + 'menu-item-link">' + url + '</div>' : '') +
'</div>'
);
},
/**
* Gets invoked after the control has been rendered.
*
* @method postRender
*/
postRender: function() {
var self = this, settings = self.settings;
var textStyle = settings.textStyle;
if (typeof textStyle == "function") {
textStyle = textStyle.call(this);
}
if (textStyle) {
var textElm = self.getEl('text');
if (textElm) {
textElm.setAttribute('style', textStyle);
}
}
self.on('mouseenter click', function(e) {
if (e.control === self) {
if (!settings.menu && e.type === 'click') {
self.fire('select');
// Edge will crash if you stress it see #2660
Delay.requestAnimationFrame(function() {
self.parent().hideAll();
});
} else {
self.showMenu();
if (e.aria) {
self.menu.focus(true);
}
}
}
});
self._super();
return self;
},
hover: function() {
var self = this;
self.parent().items().each(function(ctrl) {
ctrl.classes.remove('selected');
});
self.classes.toggle('selected', true);
return self;
},
active: function(state) {
if (typeof state != "undefined") {
this.aria('checked', state);
}
return this._super(state);
},
/**
* Removes the control and it's menus.
*
* @method remove
*/
remove: function() {
this._super();
if (this.menu) {
this.menu.remove();
}
}
});
});
// Included from: js/tinymce/classes/ui/Throbber.js
/**
* Throbber.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class enables you to display a Throbber for any element.
*
* @-x-less Throbber.less
* @class tinymce.ui.Throbber
*/
define("tinymce/ui/Throbber", [
"tinymce/dom/DomQuery",
"tinymce/ui/Control",
"tinymce/util/Delay"
], function($, Control, Delay) {
"use strict";
/**
* Constructs a new throbber.
*
* @constructor
* @param {Element} elm DOM Html element to display throbber in.
* @param {Boolean} inline Optional true/false state if the throbber should be appended to end of element for infinite scroll.
*/
return function(elm, inline) {
var self = this, state, classPrefix = Control.classPrefix, timer;
/**
* Shows the throbber.
*
* @method show
* @param {Number} [time] Time to wait before showing.
* @param {function} [callback] Optional callback to execute when the throbber is shown.
* @return {tinymce.ui.Throbber} Current throbber instance.
*/
self.show = function(time, callback) {
function render() {
if (state) {
$(elm).append(
'<div class="' + classPrefix + 'throbber' + (inline ? ' ' + classPrefix + 'throbber-inline' : '') + '"></div>'
);
if (callback) {
callback();
}
}
}
self.hide();
state = true;
if (time) {
timer = Delay.setTimeout(render, time);
} else {
render();
}
return self;
};
/**
* Hides the throbber.
*
* @method hide
* @return {tinymce.ui.Throbber} Current throbber instance.
*/
self.hide = function() {
var child = elm.lastChild;
Delay.clearTimeout(timer);
if (child && child.className.indexOf('throbber') != -1) {
child.parentNode.removeChild(child);
}
state = false;
return self;
};
};
});
// Included from: js/tinymce/classes/ui/Menu.js
/**
* Menu.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new menu.
*
* @-x-less Menu.less
* @class tinymce.ui.Menu
* @extends tinymce.ui.FloatPanel
*/
define("tinymce/ui/Menu", [
"tinymce/ui/FloatPanel",
"tinymce/ui/MenuItem",
"tinymce/ui/Throbber",
"tinymce/util/Tools"
], function(FloatPanel, MenuItem, Throbber, Tools) {
"use strict";
return FloatPanel.extend({
Defaults: {
defaultType: 'menuitem',
border: 1,
layout: 'stack',
role: 'application',
bodyRole: 'menu',
ariaRoot: true
},
/**
* Constructs a instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
*/
init: function(settings) {
var self = this;
settings.autohide = true;
settings.constrainToViewport = true;
if (typeof settings.items === 'function') {
settings.itemsFactory = settings.items;
settings.items = [];
}
if (settings.itemDefaults) {
var items = settings.items, i = items.length;
while (i--) {
items[i] = Tools.extend({}, settings.itemDefaults, items[i]);
}
}
self._super(settings);
self.classes.add('menu');
},
/**
* Repaints the control after a layout operation.
*
* @method repaint
*/
repaint: function() {
this.classes.toggle('menu-align', true);
this._super();
this.getEl().style.height = '';
this.getEl('body').style.height = '';
return this;
},
/**
* Hides/closes the menu.
*
* @method cancel
*/
cancel: function() {
var self = this;
self.hideAll();
self.fire('select');
},
/**
* Loads new items from the factory items function.
*
* @method load
*/
load: function() {
var self = this, time, factory;
function hideThrobber() {
if (self.throbber) {
self.throbber.hide();
self.throbber = null;
}
}
factory = self.settings.itemsFactory;
if (!factory) {
return;
}
if (!self.throbber) {
self.throbber = new Throbber(self.getEl('body'), true);
if (self.items().length === 0) {
self.throbber.show();
self.fire('loading');
} else {
self.throbber.show(100, function() {
self.items().remove();
self.fire('loading');
});
}
self.on('hide close', hideThrobber);
}
self.requestTime = time = new Date().getTime();
self.settings.itemsFactory(function(items) {
if (items.length === 0) {
self.hide();
return;
}
if (self.requestTime !== time) {
return;
}
self.getEl().style.width = '';
self.getEl('body').style.width = '';
hideThrobber();
self.items().remove();
self.getEl('body').innerHTML = '';
self.add(items);
self.renderNew();
self.fire('loaded');
});
},
/**
* Hide menu and all sub menus.
*
* @method hideAll
*/
hideAll: function() {
var self = this;
this.find('menuitem').exec('hideMenu');
return self._super();
},
/**
* Invoked before the menu is rendered.
*
* @method preRender
*/
preRender: function() {
var self = this;
self.items().each(function(ctrl) {
var settings = ctrl.settings;
if (settings.icon || settings.image || settings.selectable) {
self._hasIcons = true;
return false;
}
});
if (self.settings.itemsFactory) {
self.on('postrender', function() {
if (self.settings.itemsFactory) {
self.load();
}
});
}
return self._super();
}
});
});
// Included from: js/tinymce/classes/ui/ListBox.js
/**
* ListBox.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new list box control.
*
* @-x-less ListBox.less
* @class tinymce.ui.ListBox
* @extends tinymce.ui.MenuButton
*/
define("tinymce/ui/ListBox", [
"tinymce/ui/MenuButton",
"tinymce/ui/Menu"
], function(MenuButton, Menu) {
"use strict";
return MenuButton.extend({
/**
* Constructs a instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {Array} values Array with values to add to list box.
*/
init: function(settings) {
var self = this, values, selected, selectedText, lastItemCtrl;
function setSelected(menuValues) {
// Try to find a selected value
for (var i = 0; i < menuValues.length; i++) {
selected = menuValues[i].selected || settings.value === menuValues[i].value;
if (selected) {
selectedText = selectedText || menuValues[i].text;
self.state.set('value', menuValues[i].value);
return true;
}
// If the value has a submenu, try to find the selected values in that menu
if (menuValues[i].menu) {
if (setSelected(menuValues[i].menu)) {
return true;
}
}
}
}
self._super(settings);
settings = self.settings;
self._values = values = settings.values;
if (values) {
if (typeof settings.value != "undefined") {
setSelected(values);
}
// Default with first item
if (!selected && values.length > 0) {
selectedText = values[0].text;
self.state.set('value', values[0].value);
}
self.state.set('menu', values);
}
self.state.set('text', settings.text || selectedText);
self.classes.add('listbox');
self.on('select', function(e) {
var ctrl = e.control;
if (lastItemCtrl) {
e.lastControl = lastItemCtrl;
}
if (settings.multiple) {
ctrl.active(!ctrl.active());
} else {
self.value(e.control.value());
}
lastItemCtrl = ctrl;
});
},
/**
* Getter/setter function for the control value.
*
* @method value
* @param {String} [value] Value to be set.
* @return {Boolean/tinymce.ui.ListBox} Value or self if it's a set operation.
*/
bindStates: function() {
var self = this;
function activateMenuItemsByValue(menu, value) {
if (menu instanceof Menu) {
menu.items().each(function(ctrl) {
if (!ctrl.hasMenus()) {
ctrl.active(ctrl.value() === value);
}
});
}
}
function getSelectedItem(menuValues, value) {
var selectedItem;
if (!menuValues) {
return;
}
for (var i = 0; i < menuValues.length; i++) {
if (menuValues[i].value === value) {
return menuValues[i];
}
if (menuValues[i].menu) {
selectedItem = getSelectedItem(menuValues[i].menu, value);
if (selectedItem) {
return selectedItem;
}
}
}
}
self.on('show', function(e) {
activateMenuItemsByValue(e.control, self.value());
});
self.state.on('change:value', function(e) {
var selectedItem = getSelectedItem(self.state.get('menu'), e.value);
if (selectedItem) {
self.text(selectedItem.text);
} else {
self.text(self.settings.text);
}
});
return self._super();
}
});
});
// Included from: js/tinymce/classes/ui/Radio.js
/**
* Radio.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new radio button.
*
* @-x-less Radio.less
* @class tinymce.ui.Radio
* @extends tinymce.ui.Checkbox
*/
define("tinymce/ui/Radio", [
"tinymce/ui/Checkbox"
], function(Checkbox) {
"use strict";
return Checkbox.extend({
Defaults: {
classes: "radio",
role: "radio"
}
});
});
// Included from: js/tinymce/classes/ui/ResizeHandle.js
/**
* ResizeHandle.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Renders a resize handle that fires ResizeStart, Resize and ResizeEnd events.
*
* @-x-less ResizeHandle.less
* @class tinymce.ui.ResizeHandle
* @extends tinymce.ui.Widget
*/
define("tinymce/ui/ResizeHandle", [
"tinymce/ui/Widget",
"tinymce/ui/DragHelper"
], function(Widget, DragHelper) {
"use strict";
return Widget.extend({
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, prefix = self.classPrefix;
self.classes.add('resizehandle');
if (self.settings.direction == "both") {
self.classes.add('resizehandle-both');
}
self.canFocus = false;
return (
'<div id="' + self._id + '" class="' + self.classes + '">' +
'<i class="' + prefix + 'ico ' + prefix + 'i-resize"></i>' +
'</div>'
);
},
/**
* Called after the control has been rendered.
*
* @method postRender
*/
postRender: function() {
var self = this;
self._super();
self.resizeDragHelper = new DragHelper(this._id, {
start: function() {
self.fire('ResizeStart');
},
drag: function(e) {
if (self.settings.direction != "both") {
e.deltaX = 0;
}
self.fire('Resize', e);
},
stop: function() {
self.fire('ResizeEnd');
}
});
},
remove: function() {
if (this.resizeDragHelper) {
this.resizeDragHelper.destroy();
}
return this._super();
}
});
});
// Included from: js/tinymce/classes/ui/SelectBox.js
/**
* SelectBox.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new select box control.
*
* @-x-less SelectBox.less
* @class tinymce.ui.SelectBox
* @extends tinymce.ui.Widget
*/
define("tinymce/ui/SelectBox", [
"tinymce/ui/Widget"
], function(Widget) {
"use strict";
function createOptions(options) {
var strOptions = '';
if (options) {
for (var i = 0; i < options.length; i++) {
strOptions += '<option value="' + options[i] + '">' + options[i] + '</option>';
}
}
return strOptions;
}
return Widget.extend({
Defaults: {
classes: "selectbox",
role: "selectbox",
options: []
},
/**
* Constructs a instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {Array} options Array with options to add to the select box.
*/
init: function(settings) {
var self = this;
self._super(settings);
if (self.settings.size) {
self.size = self.settings.size;
}
if (self.settings.options) {
self._options = self.settings.options;
}
self.on('keydown', function(e) {
var rootControl;
if (e.keyCode == 13) {
e.preventDefault();
// Find root control that we can do toJSON on
self.parents().reverse().each(function(ctrl) {
if (ctrl.toJSON) {
rootControl = ctrl;
return false;
}
});
// Fire event on current text box with the serialized data of the whole form
self.fire('submit', {data: rootControl.toJSON()});
}
});
},
/**
* Getter/setter function for the options state.
*
* @method options
* @param {Array} [state] State to be set.
* @return {Array|tinymce.ui.SelectBox} Array of string options.
*/
options: function(state) {
if (!arguments.length) {
return this.state.get('options');
}
this.state.set('options', state);
return this;
},
renderHtml: function() {
var self = this, options, size = '';
options = createOptions(self._options);
if (self.size) {
size = ' size = "' + self.size + '"';
}
return (
'<select id="' + self._id + '" class="' + self.classes + '"' + size + '>' +
options +
'</select>'
);
},
bindStates: function() {
var self = this;
self.state.on('change:options', function(e) {
self.getEl().innerHTML = createOptions(e.value);
});
return self._super();
}
});
});
// Included from: js/tinymce/classes/ui/Slider.js
/**
* Slider.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Slider control.
*
* @-x-less Slider.less
* @class tinymce.ui.Slider
* @extends tinymce.ui.Widget
*/
define("tinymce/ui/Slider", [
"tinymce/ui/Widget",
"tinymce/ui/DragHelper",
"tinymce/ui/DomUtils"
], function(Widget, DragHelper, DomUtils) {
"use strict";
function constrain(value, minVal, maxVal) {
if (value < minVal) {
value = minVal;
}
if (value > maxVal) {
value = maxVal;
}
return value;
}
function setAriaProp(el, name, value) {
el.setAttribute('aria-' + name, value);
}
function updateSliderHandle(ctrl, value) {
var maxHandlePos, shortSizeName, sizeName, stylePosName, styleValue, handleEl;
if (ctrl.settings.orientation == "v") {
stylePosName = "top";
sizeName = "height";
shortSizeName = "h";
} else {
stylePosName = "left";
sizeName = "width";
shortSizeName = "w";
}
handleEl = ctrl.getEl('handle');
maxHandlePos = (ctrl.layoutRect()[shortSizeName] || 100) - DomUtils.getSize(handleEl)[sizeName];
styleValue = (maxHandlePos * ((value - ctrl._minValue) / (ctrl._maxValue - ctrl._minValue))) + 'px';
handleEl.style[stylePosName] = styleValue;
handleEl.style.height = ctrl.layoutRect().h + 'px';
setAriaProp(handleEl, 'valuenow', value);
setAriaProp(handleEl, 'valuetext', '' + ctrl.settings.previewFilter(value));
setAriaProp(handleEl, 'valuemin', ctrl._minValue);
setAriaProp(handleEl, 'valuemax', ctrl._maxValue);
}
return Widget.extend({
init: function(settings) {
var self = this;
if (!settings.previewFilter) {
settings.previewFilter = function(value) {
return Math.round(value * 100) / 100.0;
};
}
self._super(settings);
self.classes.add('slider');
if (settings.orientation == "v") {
self.classes.add('vertical');
}
self._minValue = settings.minValue || 0;
self._maxValue = settings.maxValue || 100;
self._initValue = self.state.get('value');
},
renderHtml: function() {
var self = this, id = self._id, prefix = self.classPrefix;
return (
'<div id="' + id + '" class="' + self.classes + '">' +
'<div id="' + id + '-handle" class="' + prefix + 'slider-handle" role="slider" tabindex="-1"></div>' +
'</div>'
);
},
reset: function() {
this.value(this._initValue).repaint();
},
postRender: function() {
var self = this, minValue, maxValue, screenCordName,
stylePosName, sizeName, shortSizeName;
function toFraction(min, max, val) {
return (val + min) / (max - min);
}
function fromFraction(min, max, val) {
return (val * (max - min)) - min;
}
function handleKeyboard(minValue, maxValue) {
function alter(delta) {
var value;
value = self.value();
value = fromFraction(minValue, maxValue, toFraction(minValue, maxValue, value) + (delta * 0.05));
value = constrain(value, minValue, maxValue);
self.value(value);
self.fire('dragstart', {value: value});
self.fire('drag', {value: value});
self.fire('dragend', {value: value});
}
self.on('keydown', function(e) {
switch (e.keyCode) {
case 37:
case 38:
alter(-1);
break;
case 39:
case 40:
alter(1);
break;
}
});
}
function handleDrag(minValue, maxValue, handleEl) {
var startPos, startHandlePos, maxHandlePos, handlePos, value;
self._dragHelper = new DragHelper(self._id, {
handle: self._id + "-handle",
start: function(e) {
startPos = e[screenCordName];
startHandlePos = parseInt(self.getEl('handle').style[stylePosName], 10);
maxHandlePos = (self.layoutRect()[shortSizeName] || 100) - DomUtils.getSize(handleEl)[sizeName];
self.fire('dragstart', {value: value});
},
drag: function(e) {
var delta = e[screenCordName] - startPos;
handlePos = constrain(startHandlePos + delta, 0, maxHandlePos);
handleEl.style[stylePosName] = handlePos + 'px';
value = minValue + (handlePos / maxHandlePos) * (maxValue - minValue);
self.value(value);
self.tooltip().text('' + self.settings.previewFilter(value)).show().moveRel(handleEl, 'bc tc');
self.fire('drag', {value: value});
},
stop: function() {
self.tooltip().hide();
self.fire('dragend', {value: value});
}
});
}
minValue = self._minValue;
maxValue = self._maxValue;
if (self.settings.orientation == "v") {
screenCordName = "screenY";
stylePosName = "top";
sizeName = "height";
shortSizeName = "h";
} else {
screenCordName = "screenX";
stylePosName = "left";
sizeName = "width";
shortSizeName = "w";
}
self._super();
handleKeyboard(minValue, maxValue, self.getEl('handle'));
handleDrag(minValue, maxValue, self.getEl('handle'));
},
repaint: function() {
this._super();
updateSliderHandle(this, this.value());
},
bindStates: function() {
var self = this;
self.state.on('change:value', function(e) {
updateSliderHandle(self, e.value);
});
return self._super();
}
});
});
// Included from: js/tinymce/classes/ui/Spacer.js
/**
* Spacer.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a spacer. This control is used in flex layouts for example.
*
* @-x-less Spacer.less
* @class tinymce.ui.Spacer
* @extends tinymce.ui.Widget
*/
define("tinymce/ui/Spacer", [
"tinymce/ui/Widget"
], function(Widget) {
"use strict";
return Widget.extend({
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this;
self.classes.add('spacer');
self.canFocus = false;
return '<div id="' + self._id + '" class="' + self.classes + '"></div>';
}
});
});
// Included from: js/tinymce/classes/ui/SplitButton.js
/**
* SplitButton.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a split button.
*
* @-x-less SplitButton.less
* @class tinymce.ui.SplitButton
* @extends tinymce.ui.Button
*/
define("tinymce/ui/SplitButton", [
"tinymce/ui/MenuButton",
"tinymce/ui/DomUtils",
"tinymce/dom/DomQuery"
], function(MenuButton, DomUtils, $) {
return MenuButton.extend({
Defaults: {
classes: "widget btn splitbtn",
role: "button"
},
/**
* Repaints the control after a layout operation.
*
* @method repaint
*/
repaint: function() {
var self = this, elm = self.getEl(), rect = self.layoutRect(), mainButtonElm, menuButtonElm;
self._super();
mainButtonElm = elm.firstChild;
menuButtonElm = elm.lastChild;
$(mainButtonElm).css({
width: rect.w - DomUtils.getSize(menuButtonElm).width,
height: rect.h - 2
});
$(menuButtonElm).css({
height: rect.h - 2
});
return self;
},
/**
* Sets the active menu state.
*
* @private
*/
activeMenu: function(state) {
var self = this;
$(self.getEl().lastChild).toggleClass(self.classPrefix + 'active', state);
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, id = self._id, prefix = self.classPrefix, image;
var icon = self.state.get('icon'), text = self.state.get('text'),
textHtml = '';
image = self.settings.image;
if (image) {
icon = 'none';
// Support for [high dpi, low dpi] image sources
if (typeof image != "string") {
image = window.getSelection ? image[0] : image[1];
}
image = ' style="background-image: url(\'' + image + '\')"';
} else {
image = '';
}
icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : '';
if (text) {
self.classes.add('btn-has-text');
textHtml = '<span class="' + prefix + 'txt">' + self.encode(text) + '</span>';
}
return (
'<div id="' + id + '" class="' + self.classes + '" role="button" tabindex="-1">' +
'<button type="button" hidefocus="1" tabindex="-1">' +
(icon ? '<i class="' + icon + '"' + image + '></i>' : '') +
textHtml +
'</button>' +
'<button type="button" class="' + prefix + 'open" hidefocus="1" tabindex="-1">' +
//(icon ? '<i class="' + icon + '"></i>' : '') +
(self._menuBtnText ? (icon ? '\u00a0' : '') + self._menuBtnText : '') +
' <i class="' + prefix + 'caret"></i>' +
'</button>' +
'</div>'
);
},
/**
* Called after the control has been rendered.
*
* @method postRender
*/
postRender: function() {
var self = this, onClickHandler = self.settings.onclick;
self.on('click', function(e) {
var node = e.target;
if (e.control == this) {
// Find clicks that is on the main button
while (node) {
if ((e.aria && e.aria.key != 'down') || (node.nodeName == 'BUTTON' && node.className.indexOf('open') == -1)) {
e.stopImmediatePropagation();
if (onClickHandler) {
onClickHandler.call(this, e);
}
return;
}
node = node.parentNode;
}
}
});
delete self.settings.onclick;
return self._super();
}
});
});
// Included from: js/tinymce/classes/ui/StackLayout.js
/**
* StackLayout.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This layout uses the browsers layout when the items are blocks.
*
* @-x-less StackLayout.less
* @class tinymce.ui.StackLayout
* @extends tinymce.ui.FlowLayout
*/
define("tinymce/ui/StackLayout", [
"tinymce/ui/FlowLayout"
], function(FlowLayout) {
"use strict";
return FlowLayout.extend({
Defaults: {
containerClass: 'stack-layout',
controlClass: 'stack-layout-item',
endClass: 'break'
},
isNative: function() {
return true;
}
});
});
// Included from: js/tinymce/classes/ui/TabPanel.js
/**
* TabPanel.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a tab panel control.
*
* @-x-less TabPanel.less
* @class tinymce.ui.TabPanel
* @extends tinymce.ui.Panel
*
* @setting {Number} activeTab Active tab index.
*/
define("tinymce/ui/TabPanel", [
"tinymce/ui/Panel",
"tinymce/dom/DomQuery",
"tinymce/ui/DomUtils"
], function(Panel, $, DomUtils) {
"use strict";
return Panel.extend({
Defaults: {
layout: 'absolute',
defaults: {
type: 'panel'
}
},
/**
* Activates the specified tab by index.
*
* @method activateTab
* @param {Number} idx Index of the tab to activate.
*/
activateTab: function(idx) {
var activeTabElm;
if (this.activeTabId) {
activeTabElm = this.getEl(this.activeTabId);
$(activeTabElm).removeClass(this.classPrefix + 'active');
activeTabElm.setAttribute('aria-selected', "false");
}
this.activeTabId = 't' + idx;
activeTabElm = this.getEl('t' + idx);
activeTabElm.setAttribute('aria-selected', "true");
$(activeTabElm).addClass(this.classPrefix + 'active');
this.items()[idx].show().fire('showtab');
this.reflow();
this.items().each(function(item, i) {
if (idx != i) {
item.hide();
}
});
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, layout = self._layout, tabsHtml = '', prefix = self.classPrefix;
self.preRender();
layout.preRender(self);
self.items().each(function(ctrl, i) {
var id = self._id + '-t' + i;
ctrl.aria('role', 'tabpanel');
ctrl.aria('labelledby', id);
tabsHtml += (
'<div id="' + id + '" class="' + prefix + 'tab" ' +
'unselectable="on" role="tab" aria-controls="' + ctrl._id + '" aria-selected="false" tabIndex="-1">' +
self.encode(ctrl.settings.title) +
'</div>'
);
});
return (
'<div id="' + self._id + '" class="' + self.classes + '" hidefocus="1" tabindex="-1">' +
'<div id="' + self._id + '-head" class="' + prefix + 'tabs" role="tablist">' +
tabsHtml +
'</div>' +
'<div id="' + self._id + '-body" class="' + self.bodyClasses + '">' +
layout.renderHtml(self) +
'</div>' +
'</div>'
);
},
/**
* Called after the control has been rendered.
*
* @method postRender
*/
postRender: function() {
var self = this;
self._super();
self.settings.activeTab = self.settings.activeTab || 0;
self.activateTab(self.settings.activeTab);
this.on('click', function(e) {
var targetParent = e.target.parentNode;
if (targetParent && targetParent.id == self._id + '-head') {
var i = targetParent.childNodes.length;
while (i--) {
if (targetParent.childNodes[i] == e.target) {
self.activateTab(i);
}
}
}
});
},
/**
* Initializes the current controls layout rect.
* This will be executed by the layout managers to determine the
* default minWidth/minHeight etc.
*
* @method initLayoutRect
* @return {Object} Layout rect instance.
*/
initLayoutRect: function() {
var self = this, rect, minW, minH;
minW = DomUtils.getSize(self.getEl('head')).width;
minW = minW < 0 ? 0 : minW;
minH = 0;
self.items().each(function(item) {
minW = Math.max(minW, item.layoutRect().minW);
minH = Math.max(minH, item.layoutRect().minH);
});
self.items().each(function(ctrl) {
ctrl.settings.x = 0;
ctrl.settings.y = 0;
ctrl.settings.w = minW;
ctrl.settings.h = minH;
ctrl.layoutRect({
x: 0,
y: 0,
w: minW,
h: minH
});
});
var headH = DomUtils.getSize(self.getEl('head')).height;
self.settings.minWidth = minW;
self.settings.minHeight = minH + headH;
rect = self._super();
rect.deltaH += headH;
rect.innerH = rect.h - rect.deltaH;
return rect;
}
});
});
// Included from: js/tinymce/classes/ui/TextBox.js
/**
* TextBox.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Creates a new textbox.
*
* @-x-less TextBox.less
* @class tinymce.ui.TextBox
* @extends tinymce.ui.Widget
*/
define("tinymce/ui/TextBox", [
"tinymce/ui/Widget",
"tinymce/util/Tools",
"tinymce/ui/DomUtils"
], function(Widget, Tools, DomUtils) {
return Widget.extend({
/**
* Constructs a instance with the specified settings.
*
* @constructor
* @param {Object} settings Name/value object with settings.
* @setting {Boolean} multiline True if the textbox is a multiline control.
* @setting {Number} maxLength Max length for the textbox.
* @setting {Number} size Size of the textbox in characters.
*/
init: function(settings) {
var self = this;
self._super(settings);
self.classes.add('textbox');
if (settings.multiline) {
self.classes.add('multiline');
} else {
self.on('keydown', function(e) {
var rootControl;
if (e.keyCode == 13) {
e.preventDefault();
// Find root control that we can do toJSON on
self.parents().reverse().each(function(ctrl) {
if (ctrl.toJSON) {
rootControl = ctrl;
return false;
}
});
// Fire event on current text box with the serialized data of the whole form
self.fire('submit', {data: rootControl.toJSON()});
}
});
self.on('keyup', function(e) {
self.state.set('value', e.target.value);
});
}
},
/**
* Repaints the control after a layout operation.
*
* @method repaint
*/
repaint: function() {
var self = this, style, rect, borderBox, borderW, borderH = 0, lastRepaintRect;
style = self.getEl().style;
rect = self._layoutRect;
lastRepaintRect = self._lastRepaintRect || {};
// Detect old IE 7+8 add lineHeight to align caret vertically in the middle
var doc = document;
if (!self.settings.multiline && doc.all && (!doc.documentMode || doc.documentMode <= 8)) {
style.lineHeight = (rect.h - borderH) + 'px';
}
borderBox = self.borderBox;
borderW = borderBox.left + borderBox.right + 8;
borderH = borderBox.top + borderBox.bottom + (self.settings.multiline ? 8 : 0);
if (rect.x !== lastRepaintRect.x) {
style.left = rect.x + 'px';
lastRepaintRect.x = rect.x;
}
if (rect.y !== lastRepaintRect.y) {
style.top = rect.y + 'px';
lastRepaintRect.y = rect.y;
}
if (rect.w !== lastRepaintRect.w) {
style.width = (rect.w - borderW) + 'px';
lastRepaintRect.w = rect.w;
}
if (rect.h !== lastRepaintRect.h) {
style.height = (rect.h - borderH) + 'px';
lastRepaintRect.h = rect.h;
}
self._lastRepaintRect = lastRepaintRect;
self.fire('repaint', {}, false);
return self;
},
/**
* Renders the control as a HTML string.
*
* @method renderHtml
* @return {String} HTML representing the control.
*/
renderHtml: function() {
var self = this, settings = self.settings, attrs, elm;
attrs = {
id: self._id,
hidefocus: '1'
};
Tools.each([
'rows', 'spellcheck', 'maxLength', 'size', 'readonly', 'min',
'max', 'step', 'list', 'pattern', 'placeholder', 'required', 'multiple'
], function(name) {
attrs[name] = settings[name];
});
if (self.disabled()) {
attrs.disabled = 'disabled';
}
if (settings.subtype) {
attrs.type = settings.subtype;
}
elm = DomUtils.create(settings.multiline ? 'textarea' : 'input', attrs);
elm.value = self.state.get('value');
elm.className = self.classes;
return elm.outerHTML;
},
value: function(value) {
if (arguments.length) {
this.state.set('value', value);
return this;
}
// Make sure the real state is in sync
if (this.state.get('rendered')) {
this.state.set('value', this.getEl().value);
}
return this.state.get('value');
},
/**
* Called after the control has been rendered.
*
* @method postRender
*/
postRender: function() {
var self = this;
self.getEl().value = self.state.get('value');
self._super();
self.$el.on('change', function(e) {
self.state.set('value', e.target.value);
self.fire('change', e);
});
},
bindStates: function() {
var self = this;
self.state.on('change:value', function(e) {
if (self.getEl().value != e.value) {
self.getEl().value = e.value;
}
});
self.state.on('change:disabled', function(e) {
self.getEl().disabled = e.value;
});
return self._super();
},
remove: function() {
this.$el.off();
this._super();
}
});
});
// Included from: js/tinymce/classes/Register.js
/**
* Register.js
*
* Released under LGPL License.
* Copyright (c) 1999-2015 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This registers tinymce in common module loaders.
*
* @private
* @class tinymce.Register
*/
define("tinymce/Register", [
], function() {
/*eslint consistent-this: 0 */
var context = this || window;
var tinymce = function() {
return context.tinymce;
};
if (typeof context.define === "function") {
// Bolt
if (!context.define.amd) {
context.define("ephox/tinymce", [], tinymce);
}
}
if (typeof module === 'object') {
/* global module */
module.exports = window.tinymce;
}
return {};
});
expose(["tinymce/geom/Rect","tinymce/util/Promise","tinymce/util/Delay","tinymce/Env","tinymce/dom/EventUtils","tinymce/dom/Sizzle","tinymce/util/Tools","tinymce/dom/DomQuery","tinymce/html/Styles","tinymce/dom/TreeWalker","tinymce/html/Entities","tinymce/dom/DOMUtils","tinymce/dom/ScriptLoader","tinymce/AddOnManager","tinymce/dom/RangeUtils","tinymce/html/Node","tinymce/html/Schema","tinymce/html/SaxParser","tinymce/html/DomParser","tinymce/html/Writer","tinymce/html/Serializer","tinymce/dom/Serializer","tinymce/util/VK","tinymce/dom/ControlSelection","tinymce/dom/BookmarkManager","tinymce/dom/Selection","tinymce/Formatter","tinymce/UndoManager","tinymce/EditorCommands","tinymce/util/URI","tinymce/util/Class","tinymce/util/EventDispatcher","tinymce/util/Observable","tinymce/ui/Selector","tinymce/ui/Collection","tinymce/ui/ReflowQueue","tinymce/ui/Control","tinymce/ui/Factory","tinymce/ui/KeyboardNavigation","tinymce/ui/Container","tinymce/ui/DragHelper","tinymce/ui/Scrollable","tinymce/ui/Panel","tinymce/ui/Movable","tinymce/ui/Resizable","tinymce/ui/FloatPanel","tinymce/ui/Window","tinymce/ui/MessageBox","tinymce/WindowManager","tinymce/ui/Tooltip","tinymce/ui/Widget","tinymce/ui/Progress","tinymce/ui/Notification","tinymce/NotificationManager","tinymce/EditorObservable","tinymce/Shortcuts","tinymce/Editor","tinymce/util/I18n","tinymce/FocusManager","tinymce/EditorManager","tinymce/util/XHR","tinymce/util/JSON","tinymce/util/JSONRequest","tinymce/util/JSONP","tinymce/util/LocalStorage","tinymce/Compat","tinymce/ui/Layout","tinymce/ui/AbsoluteLayout","tinymce/ui/Button","tinymce/ui/ButtonGroup","tinymce/ui/Checkbox","tinymce/ui/ComboBox","tinymce/ui/ColorBox","tinymce/ui/PanelButton","tinymce/ui/ColorButton","tinymce/util/Color","tinymce/ui/ColorPicker","tinymce/ui/Path","tinymce/ui/ElementPath","tinymce/ui/FormItem","tinymce/ui/Form","tinymce/ui/FieldSet","tinymce/ui/FilePicker","tinymce/ui/FitLayout","tinymce/ui/FlexLayout","tinymce/ui/FlowLayout","tinymce/ui/FormatControls","tinymce/ui/GridLayout","tinymce/ui/Iframe","tinymce/ui/InfoBox","tinymce/ui/Label","tinymce/ui/Toolbar","tinymce/ui/MenuBar","tinymce/ui/MenuButton","tinymce/ui/MenuItem","tinymce/ui/Throbber","tinymce/ui/Menu","tinymce/ui/ListBox","tinymce/ui/Radio","tinymce/ui/ResizeHandle","tinymce/ui/SelectBox","tinymce/ui/Slider","tinymce/ui/Spacer","tinymce/ui/SplitButton","tinymce/ui/StackLayout","tinymce/ui/TabPanel","tinymce/ui/TextBox"]);
})(window);
!function(e){function t(){function t(e){"remove"===e&&this.each(function(e,t){var n=i(t);n&&n.remove()}),this.find("span.mceEditor,div.mceEditor").each(function(e,t){var n=tinymce.get(t.id.replace(/_parent$/,""));n&&n.remove()})}function r(e){var n,r=this;if(null!=e)t.call(r),r.each(function(t,n){var r;(r=tinymce.get(n.id))&&r.setContent(e)});else if(r.length>0&&(n=tinymce.get(r[0].id)))return n.getContent()}function i(e){var t=null;return e&&e.id&&a.tinymce&&(t=tinymce.get(e.id)),t}function o(e){return!!(e&&e.length&&a.tinymce&&e.is(":tinymce"))}var s={};e.each(["text","html","val"],function(t,a){var l=s[a]=e.fn[a],c="text"===a;e.fn[a]=function(t){var a=this;if(!o(a))return l.apply(a,arguments);if(t!==n)return r.call(a.filter(":tinymce"),t),l.apply(a.not(":tinymce"),arguments),a;var s="",u=arguments;return(c?a:a.eq(0)).each(function(t,n){var r=i(n);s+=r?c?r.getContent().replace(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g,""):r.getContent({save:!0}):l.apply(e(n),u)}),s}}),e.each(["append","prepend"],function(t,r){var a=s[r]=e.fn[r],l="prepend"===r;e.fn[r]=function(e){var t=this;return o(t)?e!==n?("string"==typeof e&&t.filter(":tinymce").each(function(t,n){var r=i(n);r&&r.setContent(l?e+r.getContent():r.getContent()+e)}),a.apply(t.not(":tinymce"),arguments),t):void 0:a.apply(t,arguments)}}),e.each(["remove","replaceWith","replaceAll","empty"],function(n,r){var i=s[r]=e.fn[r];e.fn[r]=function(){return t.call(this,r),i.apply(this,arguments)}}),s.attr=e.fn.attr,e.fn.attr=function(t,a){var l=this,c=arguments;if(!t||"value"!==t||!o(l))return a!==n?s.attr.apply(l,c):s.attr.apply(l,c);if(a!==n)return r.call(l.filter(":tinymce"),a),s.attr.apply(l.not(":tinymce"),c),l;var u=l[0],d=i(u);return d?d.getContent({save:!0}):s.attr.apply(e(u),c)}}var n,r,i,o=[],a=window;e.fn.tinymce=function(n){function s(){var r=[],o=0;i||(t(),i=!0),d.each(function(e,t){var i,a=t.id,s=n.oninit;a||(t.id=a=tinymce.DOM.uniqueId()),tinymce.get(a)||(i=new tinymce.Editor(a,n,tinymce.EditorManager),r.push(i),i.on("init",function(){var e,t=s;d.css("visibility",""),s&&++o==r.length&&("string"==typeof t&&(e=t.indexOf(".")===-1?null:tinymce.resolve(t.replace(/\.\w+$/,"")),t=tinymce.resolve(t)),t.apply(e||tinymce,r))}))}),e.each(r,function(e,t){t.render()})}var l,c,u,d=this,f="";if(!d.length)return d;if(!n)return window.tinymce?tinymce.get(d[0].id):null;if(d.css("visibility","hidden"),a.tinymce||r||!(l=n.script_url))1===r?o.push(s):s();else{r=1,c=l.substring(0,l.lastIndexOf("/")),l.indexOf(".min")!=-1&&(f=".min"),a.tinymce=a.tinyMCEPreInit||{base:c,suffix:f},l.indexOf("gzip")!=-1&&(u=n.language||"en",l=l+(/\?/.test(l)?"&":"?")+"js=true&core=true&suffix="+escape(f)+"&themes="+escape(n.theme||"modern")+"&plugins="+escape(n.plugins||"")+"&languages="+(u||""),a.tinyMCE_GZ||(a.tinyMCE_GZ={start:function(){function t(e){tinymce.ScriptLoader.markDone(tinymce.baseURI.toAbsolute(e))}t("langs/"+u+".js"),t("themes/"+n.theme+"/theme"+f+".js"),t("themes/"+n.theme+"/langs/"+u+".js"),e.each(n.plugins.split(","),function(e,n){n&&(t("plugins/"+n+"/plugin"+f+".js"),t("plugins/"+n+"/langs/"+u+".js"))})},end:function(){}}));var h=document.createElement("script");h.type="text/javascript",h.onload=h.onreadystatechange=function(t){t=t||window.event,2===r||"load"!=t.type&&!/complete|loaded/.test(h.readyState)||(tinymce.dom.Event.domLoaded=1,r=2,n.script_loaded&&n.script_loaded(),s(),e.each(o,function(e,t){t()}))},h.src=l,document.body.appendChild(h)}return d},e.extend(e.expr[":"],{tinymce:function(e){var t;return!!(e.id&&"tinymce"in window&&(t=tinymce.get(e.id),t&&t.editorManager===tinymce))}})}(jQuery);
/*!
* Modernizr v2.7.1
* www.modernizr.com
*
* Copyright (c) Faruk Ates, Paul Irish, Alex Sexton
* Available under the BSD and MIT licenses: www.modernizr.com/license/
*/
/*
* Modernizr tests which native CSS3 and HTML5 features are available in
* the current UA and makes the results available to you in two ways:
* as properties on a global Modernizr object, and as classes on the
* <html> element. This information allows you to progressively enhance
* your pages with a granular level of control over the experience.
*
* Modernizr has an optional (not included) conditional resource loader
* called Modernizr.load(), based on Yepnope.js (yepnopejs.com).
* To get a build that includes Modernizr.load(), as well as choosing
* which tests to include, go to www.modernizr.com/download/
*
* Authors Faruk Ates, Paul Irish, Alex Sexton
* Contributors Ryan Seddon, Ben Alman
*/
window.Modernizr = (function( window, document, undefined ) {
var version = '2.7.1',
Modernizr = {},
/*>>cssclasses*/
// option for enabling the HTML classes to be added
enableClasses = true,
/*>>cssclasses*/
docElement = document.documentElement,
/**
* Create our "modernizr" element that we do most feature tests on.
*/
mod = 'modernizr',
modElem = document.createElement(mod),
mStyle = modElem.style,
/**
* Create the input element for various Web Forms feature tests.
*/
inputElem /*>>inputelem*/ = document.createElement('input') /*>>inputelem*/ ,
/*>>smile*/
smile = ':)',
/*>>smile*/
toString = {}.toString,
// TODO :: make the prefixes more granular
/*>>prefixes*/
// List of property values to set for css tests. See ticket #21
prefixes = ' -webkit- -moz- -o- -ms- '.split(' '),
/*>>prefixes*/
/*>>domprefixes*/
// Following spec is to expose vendor-specific style properties as:
// elem.style.WebkitBorderRadius
// and the following would be incorrect:
// elem.style.webkitBorderRadius
// Webkit ghosts their properties in lowercase but Opera & Moz do not.
// Microsoft uses a lowercase `ms` instead of the correct `Ms` in IE8+
// erik.eae.net/archives/2008/03/10/21.48.10/
// More here: github.com/Modernizr/Modernizr/issues/issue/21
omPrefixes = 'Webkit Moz O ms',
cssomPrefixes = omPrefixes.split(' '),
domPrefixes = omPrefixes.toLowerCase().split(' '),
/*>>domprefixes*/
/*>>ns*/
ns = {'svg': 'http://www.w3.org/2000/svg'},
/*>>ns*/
tests = {},
inputs = {},
attrs = {},
classes = [],
slice = classes.slice,
featureName, // used in testing loop
/*>>teststyles*/
// Inject element with style element and some CSS rules
injectElementWithStyles = function( rule, callback, nodes, testnames ) {
var style, ret, node, docOverflow,
div = document.createElement('div'),
// After page load injecting a fake body doesn't work so check if body exists
body = document.body,
// IE6 and 7 won't return offsetWidth or offsetHeight unless it's in the body element, so we fake it.
fakeBody = body || document.createElement('body');
if ( parseInt(nodes, 10) ) {
// In order not to give false positives we create a node for each test
// This also allows the method to scale for unspecified uses
while ( nodes-- ) {
node = document.createElement('div');
node.id = testnames ? testnames[nodes] : mod + (nodes + 1);
div.appendChild(node);
}
}
// <style> elements in IE6-9 are considered 'NoScope' elements and therefore will be removed
// when injected with innerHTML. To get around this you need to prepend the 'NoScope' element
// with a 'scoped' element, in our case the soft-hyphen entity as it won't mess with our measurements.
// msdn.microsoft.com/en-us/library/ms533897%28VS.85%29.aspx
// Documents served as xml will throw if using &shy; so use xml friendly encoded version. See issue #277
style = ['&#173;','<style id="s', mod, '">', rule, '</style>'].join('');
div.id = mod;
// IE6 will false positive on some tests due to the style element inside the test div somehow interfering offsetHeight, so insert it into body or fakebody.
// Opera will act all quirky when injecting elements in documentElement when page is served as xml, needs fakebody too. #270
(body ? div : fakeBody).innerHTML += style;
fakeBody.appendChild(div);
if ( !body ) {
//avoid crashing IE8, if background image is used
fakeBody.style.background = '';
//Safari 5.13/5.1.4 OSX stops loading if ::-webkit-scrollbar is used and scrollbars are visible
fakeBody.style.overflow = 'hidden';
docOverflow = docElement.style.overflow;
docElement.style.overflow = 'hidden';
docElement.appendChild(fakeBody);
}
ret = callback(div, rule);
// If this is done after page load we don't want to remove the body so check if body exists
if ( !body ) {
fakeBody.parentNode.removeChild(fakeBody);
docElement.style.overflow = docOverflow;
} else {
div.parentNode.removeChild(div);
}
return !!ret;
},
/*>>teststyles*/
/*>>mq*/
// adapted from matchMedia polyfill
// by Scott Jehl and Paul Irish
// gist.github.com/786768
testMediaQuery = function( mq ) {
var matchMedia = window.matchMedia || window.msMatchMedia;
if ( matchMedia ) {
return matchMedia(mq).matches;
}
var bool;
injectElementWithStyles('@media ' + mq + ' { #' + mod + ' { position: absolute; } }', function( node ) {
bool = (window.getComputedStyle ?
getComputedStyle(node, null) :
node.currentStyle)['position'] == 'absolute';
});
return bool;
},
/*>>mq*/
/*>>hasevent*/
//
// isEventSupported determines if a given element supports the given event
// kangax.github.com/iseventsupported/
//
// The following results are known incorrects:
// Modernizr.hasEvent("webkitTransitionEnd", elem) // false negative
// Modernizr.hasEvent("textInput") // in Webkit. github.com/Modernizr/Modernizr/issues/333
// ...
isEventSupported = (function() {
var TAGNAMES = {
'select': 'input', 'change': 'input',
'submit': 'form', 'reset': 'form',
'error': 'img', 'load': 'img', 'abort': 'img'
};
function isEventSupported( eventName, element ) {
element = element || document.createElement(TAGNAMES[eventName] || 'div');
eventName = 'on' + eventName;
// When using `setAttribute`, IE skips "unload", WebKit skips "unload" and "resize", whereas `in` "catches" those
var isSupported = eventName in element;
if ( !isSupported ) {
// If it has no `setAttribute` (i.e. doesn't implement Node interface), try generic element
if ( !element.setAttribute ) {
element = document.createElement('div');
}
if ( element.setAttribute && element.removeAttribute ) {
element.setAttribute(eventName, '');
isSupported = is(element[eventName], 'function');
// If property was created, "remove it" (by setting value to `undefined`)
if ( !is(element[eventName], 'undefined') ) {
element[eventName] = undefined;
}
element.removeAttribute(eventName);
}
}
element = null;
return isSupported;
}
return isEventSupported;
})(),
/*>>hasevent*/
// TODO :: Add flag for hasownprop ? didn't last time
// hasOwnProperty shim by kangax needed for Safari 2.0 support
_hasOwnProperty = ({}).hasOwnProperty, hasOwnProp;
if ( !is(_hasOwnProperty, 'undefined') && !is(_hasOwnProperty.call, 'undefined') ) {
hasOwnProp = function (object, property) {
return _hasOwnProperty.call(object, property);
};
}
else {
hasOwnProp = function (object, property) { /* yes, this can give false positives/negatives, but most of the time we don't care about those */
return ((property in object) && is(object.constructor.prototype[property], 'undefined'));
};
}
// Adapted from ES5-shim https://github.com/kriskowal/es5-shim/blob/master/es5-shim.js
// es5.github.com/#x15.3.4.5
if (!Function.prototype.bind) {
Function.prototype.bind = function bind(that) {
var target = this;
if (typeof target != "function") {
throw new TypeError();
}
var args = slice.call(arguments, 1),
bound = function () {
if (this instanceof bound) {
var F = function(){};
F.prototype = target.prototype;
var self = new F();
var result = target.apply(
self,
args.concat(slice.call(arguments))
);
if (Object(result) === result) {
return result;
}
return self;
} else {
return target.apply(
that,
args.concat(slice.call(arguments))
);
}
};
return bound;
};
}
/**
* setCss applies given styles to the Modernizr DOM node.
*/
function setCss( str ) {
mStyle.cssText = str;
}
/**
* setCssAll extrapolates all vendor-specific css strings.
*/
function setCssAll( str1, str2 ) {
return setCss(prefixes.join(str1 + ';') + ( str2 || '' ));
}
/**
* is returns a boolean for if typeof obj is exactly type.
*/
function is( obj, type ) {
return typeof obj === type;
}
/**
* contains returns a boolean for if substr is found within str.
*/
function contains( str, substr ) {
return !!~('' + str).indexOf(substr);
}
/*>>testprop*/
// testProps is a generic CSS / DOM property test.
// In testing support for a given CSS property, it's legit to test:
// `elem.style[styleName] !== undefined`
// If the property is supported it will return an empty string,
// if unsupported it will return undefined.
// We'll take advantage of this quick test and skip setting a style
// on our modernizr element, but instead just testing undefined vs
// empty string.
// Because the testing of the CSS property names (with "-", as
// opposed to the camelCase DOM properties) is non-portable and
// non-standard but works in WebKit and IE (but not Gecko or Opera),
// we explicitly reject properties with dashes so that authors
// developing in WebKit or IE first don't end up with
// browser-specific content by accident.
function testProps( props, prefixed ) {
for ( var i in props ) {
var prop = props[i];
if ( !contains(prop, "-") && mStyle[prop] !== undefined ) {
return prefixed == 'pfx' ? prop : true;
}
}
return false;
}
/*>>testprop*/
// TODO :: add testDOMProps
/**
* testDOMProps is a generic DOM property test; if a browser supports
* a certain property, it won't return undefined for it.
*/
function testDOMProps( props, obj, elem ) {
for ( var i in props ) {
var item = obj[props[i]];
if ( item !== undefined) {
// return the property name as a string
if (elem === false) return props[i];
// let's bind a function
if (is(item, 'function')){
// default to autobind unless override
return item.bind(elem || obj);
}
// return the unbound function or obj or value
return item;
}
}
return false;
}
/*>>testallprops*/
/**
* testPropsAll tests a list of DOM properties we want to check against.
* We specify literally ALL possible (known and/or likely) properties on
* the element including the non-vendor prefixed one, for forward-
* compatibility.
*/
function testPropsAll( prop, prefixed, elem ) {
var ucProp = prop.charAt(0).toUpperCase() + prop.slice(1),
props = (prop + ' ' + cssomPrefixes.join(ucProp + ' ') + ucProp).split(' ');
// did they call .prefixed('boxSizing') or are we just testing a prop?
if(is(prefixed, "string") || is(prefixed, "undefined")) {
return testProps(props, prefixed);
// otherwise, they called .prefixed('requestAnimationFrame', window[, elem])
} else {
props = (prop + ' ' + (domPrefixes).join(ucProp + ' ') + ucProp).split(' ');
return testDOMProps(props, prefixed, elem);
}
}
/*>>testallprops*/
/**
* Tests
* -----
*/
// The *new* flexbox
// dev.w3.org/csswg/css3-flexbox
tests['flexbox'] = function() {
return testPropsAll('flexWrap');
};
// The *old* flexbox
// www.w3.org/TR/2009/WD-css3-flexbox-20090723/
tests['flexboxlegacy'] = function() {
return testPropsAll('boxDirection');
};
// On the S60 and BB Storm, getContext exists, but always returns undefined
// so we actually have to call getContext() to verify
// github.com/Modernizr/Modernizr/issues/issue/97/
tests['canvas'] = function() {
var elem = document.createElement('canvas');
return !!(elem.getContext && elem.getContext('2d'));
};
tests['canvastext'] = function() {
return !!(Modernizr['canvas'] && is(document.createElement('canvas').getContext('2d').fillText, 'function'));
};
// webk.it/70117 is tracking a legit WebGL feature detect proposal
// We do a soft detect which may false positive in order to avoid
// an expensive context creation: bugzil.la/732441
tests['webgl'] = function() {
return !!window.WebGLRenderingContext;
};
/*
* The Modernizr.touch test only indicates if the browser supports
* touch events, which does not necessarily reflect a touchscreen
* device, as evidenced by tablets running Windows 7 or, alas,
* the Palm Pre / WebOS (touch) phones.
*
* Additionally, Chrome (desktop) used to lie about its support on this,
* but that has since been rectified: crbug.com/36415
*
* We also test for Firefox 4 Multitouch Support.
*
* For more info, see: modernizr.github.com/Modernizr/touch.html
*/
tests['touch'] = function() {
var bool;
if(('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch) {
bool = true;
} else {
injectElementWithStyles(['@media (',prefixes.join('touch-enabled),('),mod,')','{#modernizr{top:9px;position:absolute}}'].join(''), function( node ) {
bool = node.offsetTop === 9;
});
}
return bool;
};
// geolocation is often considered a trivial feature detect...
// Turns out, it's quite tricky to get right:
//
// Using !!navigator.geolocation does two things we don't want. It:
// 1. Leaks memory in IE9: github.com/Modernizr/Modernizr/issues/513
// 2. Disables page caching in WebKit: webk.it/43956
//
// Meanwhile, in Firefox < 8, an about:config setting could expose
// a false positive that would throw an exception: bugzil.la/688158
tests['geolocation'] = function() {
return 'geolocation' in navigator;
};
tests['postmessage'] = function() {
return !!window.postMessage;
};
// Chrome incognito mode used to throw an exception when using openDatabase
// It doesn't anymore.
tests['websqldatabase'] = function() {
return !!window.openDatabase;
};
// Vendors had inconsistent prefixing with the experimental Indexed DB:
// - Webkit's implementation is accessible through webkitIndexedDB
// - Firefox shipped moz_indexedDB before FF4b9, but since then has been mozIndexedDB
// For speed, we don't test the legacy (and beta-only) indexedDB
tests['indexedDB'] = function() {
return !!testPropsAll("indexedDB", window);
};
// documentMode logic from YUI to filter out IE8 Compat Mode
// which false positives.
tests['hashchange'] = function() {
return isEventSupported('hashchange', window) && (document.documentMode === undefined || document.documentMode > 7);
};
// Per 1.6:
// This used to be Modernizr.historymanagement but the longer
// name has been deprecated in favor of a shorter and property-matching one.
// The old API is still available in 1.6, but as of 2.0 will throw a warning,
// and in the first release thereafter disappear entirely.
tests['history'] = function() {
return !!(window.history && history.pushState);
};
tests['draganddrop'] = function() {
var div = document.createElement('div');
return ('draggable' in div) || ('ondragstart' in div && 'ondrop' in div);
};
// FF3.6 was EOL'ed on 4/24/12, but the ESR version of FF10
// will be supported until FF19 (2/12/13), at which time, ESR becomes FF17.
// FF10 still uses prefixes, so check for it until then.
// for more ESR info, see: mozilla.org/en-US/firefox/organizations/faq/
tests['websockets'] = function() {
return 'WebSocket' in window || 'MozWebSocket' in window;
};
// css-tricks.com/rgba-browser-support/
tests['rgba'] = function() {
// Set an rgba() color and check the returned value
setCss('background-color:rgba(150,255,150,.5)');
return contains(mStyle.backgroundColor, 'rgba');
};
tests['hsla'] = function() {
// Same as rgba(), in fact, browsers re-map hsla() to rgba() internally,
// except IE9 who retains it as hsla
setCss('background-color:hsla(120,40%,100%,.5)');
return contains(mStyle.backgroundColor, 'rgba') || contains(mStyle.backgroundColor, 'hsla');
};
tests['multiplebgs'] = function() {
// Setting multiple images AND a color on the background shorthand property
// and then querying the style.background property value for the number of
// occurrences of "url(" is a reliable method for detecting ACTUAL support for this!
setCss('background:url(https://),url(https://),red url(https://)');
// If the UA supports multiple backgrounds, there should be three occurrences
// of the string "url(" in the return value for elemStyle.background
return (/(url\s*\(.*?){3}/).test(mStyle.background);
};
// this will false positive in Opera Mini
// github.com/Modernizr/Modernizr/issues/396
tests['backgroundsize'] = function() {
return testPropsAll('backgroundSize');
};
tests['borderimage'] = function() {
return testPropsAll('borderImage');
};
// Super comprehensive table about all the unique implementations of
// border-radius: muddledramblings.com/table-of-css3-border-radius-compliance
tests['borderradius'] = function() {
return testPropsAll('borderRadius');
};
// WebOS unfortunately false positives on this test.
tests['boxshadow'] = function() {
return testPropsAll('boxShadow');
};
// FF3.0 will false positive on this test
tests['textshadow'] = function() {
return document.createElement('div').style.textShadow === '';
};
tests['opacity'] = function() {
// Browsers that actually have CSS Opacity implemented have done so
// according to spec, which means their return values are within the
// range of [0.0,1.0] - including the leading zero.
setCssAll('opacity:.55');
// The non-literal . in this regex is intentional:
// German Chrome returns this value as 0,55
// github.com/Modernizr/Modernizr/issues/#issue/59/comment/516632
return (/^0.55$/).test(mStyle.opacity);
};
// Note, Android < 4 will pass this test, but can only animate
// a single property at a time
// daneden.me/2011/12/putting-up-with-androids-bullshit/
tests['cssanimations'] = function() {
return testPropsAll('animationName');
};
tests['csscolumns'] = function() {
return testPropsAll('columnCount');
};
tests['cssgradients'] = function() {
/**
* For CSS Gradients syntax, please see:
* webkit.org/blog/175/introducing-css-gradients/
* developer.mozilla.org/en/CSS/-moz-linear-gradient
* developer.mozilla.org/en/CSS/-moz-radial-gradient
* dev.w3.org/csswg/css3-images/#gradients-
*/
var str1 = 'background-image:',
str2 = 'gradient(linear,left top,right bottom,from(#9f9),to(white));',
str3 = 'linear-gradient(left top,#9f9, white);';
setCss(
// legacy webkit syntax (FIXME: remove when syntax not in use anymore)
(str1 + '-webkit- '.split(' ').join(str2 + str1) +
// standard syntax // trailing 'background-image:'
prefixes.join(str3 + str1)).slice(0, -str1.length)
);
return contains(mStyle.backgroundImage, 'gradient');
};
tests['cssreflections'] = function() {
return testPropsAll('boxReflect');
};
tests['csstransforms'] = function() {
return !!testPropsAll('transform');
};
tests['csstransforms3d'] = function() {
var ret = !!testPropsAll('perspective');
// Webkit's 3D transforms are passed off to the browser's own graphics renderer.
// It works fine in Safari on Leopard and Snow Leopard, but not in Chrome in
// some conditions. As a result, Webkit typically recognizes the syntax but
// will sometimes throw a false positive, thus we must do a more thorough check:
if ( ret && 'webkitPerspective' in docElement.style ) {
// Webkit allows this media query to succeed only if the feature is enabled.
// `@media (transform-3d),(-webkit-transform-3d){ ... }`
injectElementWithStyles('@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}', function( node, rule ) {
ret = node.offsetLeft === 9 && node.offsetHeight === 3;
});
}
return ret;
};
tests['csstransitions'] = function() {
return testPropsAll('transition');
};
/*>>fontface*/
// @font-face detection routine by Diego Perini
// javascript.nwbox.com/CSSSupport/
// false positives:
// WebOS github.com/Modernizr/Modernizr/issues/342
// WP7 github.com/Modernizr/Modernizr/issues/538
tests['fontface'] = function() {
var bool;
injectElementWithStyles('@font-face {font-family:"font";src:url("https://")}', function( node, rule ) {
var style = document.getElementById('smodernizr'),
sheet = style.sheet || style.styleSheet,
cssText = sheet ? (sheet.cssRules && sheet.cssRules[0] ? sheet.cssRules[0].cssText : sheet.cssText || '') : '';
bool = /src/i.test(cssText) && cssText.indexOf(rule.split(' ')[0]) === 0;
});
return bool;
};
/*>>fontface*/
// CSS generated content detection
tests['generatedcontent'] = function() {
var bool;
injectElementWithStyles(['#',mod,'{font:0/0 a}#',mod,':after{content:"',smile,'";visibility:hidden;font:3px/1 a}'].join(''), function( node ) {
bool = node.offsetHeight >= 3;
});
return bool;
};
// These tests evaluate support of the video/audio elements, as well as
// testing what types of content they support.
//
// We're using the Boolean constructor here, so that we can extend the value
// e.g. Modernizr.video // true
// Modernizr.video.ogg // 'probably'
//
// Codec values from : github.com/NielsLeenheer/html5test/blob/9106a8/index.html#L845
// thx to NielsLeenheer and zcorpan
// Note: in some older browsers, "no" was a return value instead of empty string.
// It was live in FF3.5.0 and 3.5.1, but fixed in 3.5.2
// It was also live in Safari 4.0.0 - 4.0.4, but fixed in 4.0.5
tests['video'] = function() {
var elem = document.createElement('video'),
bool = false;
// IE9 Running on Windows Server SKU can cause an exception to be thrown, bug #224
try {
if ( bool = !!elem.canPlayType ) {
bool = new Boolean(bool);
bool.ogg = elem.canPlayType('video/ogg; codecs="theora"') .replace(/^no$/,'');
// Without QuickTime, this value will be `undefined`. github.com/Modernizr/Modernizr/issues/546
bool.h264 = elem.canPlayType('video/mp4; codecs="avc1.42E01E"') .replace(/^no$/,'');
bool.webm = elem.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/^no$/,'');
}
} catch(e) { }
return bool;
};
tests['audio'] = function() {
var elem = document.createElement('audio'),
bool = false;
try {
if ( bool = !!elem.canPlayType ) {
bool = new Boolean(bool);
bool.ogg = elem.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,'');
bool.mp3 = elem.canPlayType('audio/mpeg;') .replace(/^no$/,'');
// Mimetypes accepted:
// developer.mozilla.org/En/Media_formats_supported_by_the_audio_and_video_elements
// bit.ly/iphoneoscodecs
bool.wav = elem.canPlayType('audio/wav; codecs="1"') .replace(/^no$/,'');
bool.m4a = ( elem.canPlayType('audio/x-m4a;') ||
elem.canPlayType('audio/aac;')) .replace(/^no$/,'');
}
} catch(e) { }
return bool;
};
// In FF4, if disabled, window.localStorage should === null.
// Normally, we could not test that directly and need to do a
// `('localStorage' in window) && ` test first because otherwise Firefox will
// throw bugzil.la/365772 if cookies are disabled
// Also in iOS5 Private Browsing mode, attempting to use localStorage.setItem
// will throw the exception:
// QUOTA_EXCEEDED_ERRROR DOM Exception 22.
// Peculiarly, getItem and removeItem calls do not throw.
// Because we are forced to try/catch this, we'll go aggressive.
// Just FWIW: IE8 Compat mode supports these features completely:
// www.quirksmode.org/dom/html5.html
// But IE8 doesn't support either with local files
tests['localstorage'] = function() {
try {
localStorage.setItem(mod, mod);
localStorage.removeItem(mod);
return true;
} catch(e) {
return false;
}
};
tests['sessionstorage'] = function() {
try {
sessionStorage.setItem(mod, mod);
sessionStorage.removeItem(mod);
return true;
} catch(e) {
return false;
}
};
tests['webworkers'] = function() {
return !!window.Worker;
};
tests['applicationcache'] = function() {
return !!window.applicationCache;
};
// Thanks to Erik Dahlstrom
tests['svg'] = function() {
return !!document.createElementNS && !!document.createElementNS(ns.svg, 'svg').createSVGRect;
};
// specifically for SVG inline in HTML, not within XHTML
// test page: paulirish.com/demo/inline-svg
tests['inlinesvg'] = function() {
var div = document.createElement('div');
div.innerHTML = '<svg/>';
return (div.firstChild && div.firstChild.namespaceURI) == ns.svg;
};
// SVG SMIL animation
tests['smil'] = function() {
return !!document.createElementNS && /SVGAnimate/.test(toString.call(document.createElementNS(ns.svg, 'animate')));
};
// This test is only for clip paths in SVG proper, not clip paths on HTML content
// demo: srufaculty.sru.edu/david.dailey/svg/newstuff/clipPath4.svg
// However read the comments to dig into applying SVG clippaths to HTML content here:
// github.com/Modernizr/Modernizr/issues/213#issuecomment-1149491
tests['svgclippaths'] = function() {
return !!document.createElementNS && /SVGClipPath/.test(toString.call(document.createElementNS(ns.svg, 'clipPath')));
};
/*>>webforms*/
// input features and input types go directly onto the ret object, bypassing the tests loop.
// Hold this guy to execute in a moment.
function webforms() {
/*>>input*/
// Run through HTML5's new input attributes to see if the UA understands any.
// We're using f which is the <input> element created early on
// Mike Taylr has created a comprehensive resource for testing these attributes
// when applied to all input types:
// miketaylr.com/code/input-type-attr.html
// spec: www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#input-type-attr-summary
// Only input placeholder is tested while textarea's placeholder is not.
// Currently Safari 4 and Opera 11 have support only for the input placeholder
// Both tests are available in feature-detects/forms-placeholder.js
Modernizr['input'] = (function( props ) {
for ( var i = 0, len = props.length; i < len; i++ ) {
attrs[ props[i] ] = !!(props[i] in inputElem);
}
if (attrs.list){
// safari false positive's on datalist: webk.it/74252
// see also github.com/Modernizr/Modernizr/issues/146
attrs.list = !!(document.createElement('datalist') && window.HTMLDataListElement);
}
return attrs;
})('autocomplete autofocus list placeholder max min multiple pattern required step'.split(' '));
/*>>input*/
/*>>inputtypes*/
// Run through HTML5's new input types to see if the UA understands any.
// This is put behind the tests runloop because it doesn't return a
// true/false like all the other tests; instead, it returns an object
// containing each input type with its corresponding true/false value
// Big thanks to @miketaylr for the html5 forms expertise. miketaylr.com/
Modernizr['inputtypes'] = (function(props) {
for ( var i = 0, bool, inputElemType, defaultView, len = props.length; i < len; i++ ) {
inputElem.setAttribute('type', inputElemType = props[i]);
bool = inputElem.type !== 'text';
// We first check to see if the type we give it sticks..
// If the type does, we feed it a textual value, which shouldn't be valid.
// If the value doesn't stick, we know there's input sanitization which infers a custom UI
if ( bool ) {
inputElem.value = smile;
inputElem.style.cssText = 'position:absolute;visibility:hidden;';
if ( /^range$/.test(inputElemType) && inputElem.style.WebkitAppearance !== undefined ) {
docElement.appendChild(inputElem);
defaultView = document.defaultView;
// Safari 2-4 allows the smiley as a value, despite making a slider
bool = defaultView.getComputedStyle &&
defaultView.getComputedStyle(inputElem, null).WebkitAppearance !== 'textfield' &&
// Mobile android web browser has false positive, so must
// check the height to see if the widget is actually there.
(inputElem.offsetHeight !== 0);
docElement.removeChild(inputElem);
} else if ( /^(search|tel)$/.test(inputElemType) ){
// Spec doesn't define any special parsing or detectable UI
// behaviors so we pass these through as true
// Interestingly, opera fails the earlier test, so it doesn't
// even make it here.
} else if ( /^(url|email)$/.test(inputElemType) ) {
// Real url and email support comes with prebaked validation.
bool = inputElem.checkValidity && inputElem.checkValidity() === false;
} else {
// If the upgraded input compontent rejects the :) text, we got a winner
bool = inputElem.value != smile;
}
}
inputs[ props[i] ] = !!bool;
}
return inputs;
})('search tel url email datetime date month week time datetime-local number range color'.split(' '));
/*>>inputtypes*/
}
/*>>webforms*/
// End of test definitions
// -----------------------
// Run through all tests and detect their support in the current UA.
// todo: hypothetically we could be doing an array of tests and use a basic loop here.
for ( var feature in tests ) {
if ( hasOwnProp(tests, feature) ) {
// run the test, throw the return value into the Modernizr,
// then based on that boolean, define an appropriate className
// and push it into an array of classes we'll join later.
featureName = feature.toLowerCase();
Modernizr[featureName] = tests[feature]();
classes.push((Modernizr[featureName] ? '' : 'no-') + featureName);
}
}
/*>>webforms*/
// input tests need to run.
Modernizr.input || webforms();
/*>>webforms*/
/**
* addTest allows the user to define their own feature tests
* the result will be added onto the Modernizr object,
* as well as an appropriate className set on the html element
*
* @param feature - String naming the feature
* @param test - Function returning true if feature is supported, false if not
*/
Modernizr.addTest = function ( feature, test ) {
if ( typeof feature == 'object' ) {
for ( var key in feature ) {
if ( hasOwnProp( feature, key ) ) {
Modernizr.addTest( key, feature[ key ] );
}
}
} else {
feature = feature.toLowerCase();
if ( Modernizr[feature] !== undefined ) {
// we're going to quit if you're trying to overwrite an existing test
// if we were to allow it, we'd do this:
// var re = new RegExp("\\b(no-)?" + feature + "\\b");
// docElement.className = docElement.className.replace( re, '' );
// but, no rly, stuff 'em.
return Modernizr;
}
test = typeof test == 'function' ? test() : test;
if (typeof enableClasses !== "undefined" && enableClasses) {
docElement.className += ' ' + (test ? '' : 'no-') + feature;
}
Modernizr[feature] = test;
}
return Modernizr; // allow chaining.
};
// Reset modElem.cssText to nothing to reduce memory footprint.
setCss('');
modElem = inputElem = null;
/*>>shiv*/
/**
* @preserve HTML5 Shiv prev3.7.1 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
*/
;(function(window, document) {
/*jshint evil:true */
/** version */
var version = '3.7.0';
/** Preset options */
var options = window.html5 || {};
/** Used to skip problem elements */
var reSkip = /^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i;
/** Not all elements can be cloned in IE **/
var saveClones = /^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i;
/** Detect whether the browser supports default html5 styles */
var supportsHtml5Styles;
/** Name of the expando, to work with multiple documents or to re-shiv one document */
var expando = '_html5shiv';
/** The id for the the documents expando */
var expanID = 0;
/** Cached data for each document */
var expandoData = {};
/** Detect whether the browser supports unknown elements */
var supportsUnknownElements;
(function() {
try {
var a = document.createElement('a');
a.innerHTML = '<xyz></xyz>';
//if the hidden property is implemented we can assume, that the browser supports basic HTML5 Styles
supportsHtml5Styles = ('hidden' in a);
supportsUnknownElements = a.childNodes.length == 1 || (function() {
// assign a false positive if unable to shiv
(document.createElement)('a');
var frag = document.createDocumentFragment();
return (
typeof frag.cloneNode == 'undefined' ||
typeof frag.createDocumentFragment == 'undefined' ||
typeof frag.createElement == 'undefined'
);
}());
} catch(e) {
// assign a false positive if detection fails => unable to shiv
supportsHtml5Styles = true;
supportsUnknownElements = true;
}
}());
/*--------------------------------------------------------------------------*/
/**
* Creates a style sheet with the given CSS text and adds it to the document.
* @private
* @param {Document} ownerDocument The document.
* @param {String} cssText The CSS text.
* @returns {StyleSheet} The style element.
*/
function addStyleSheet(ownerDocument, cssText) {
var p = ownerDocument.createElement('p'),
parent = ownerDocument.getElementsByTagName('head')[0] || ownerDocument.documentElement;
p.innerHTML = 'x<style>' + cssText + '</style>';
return parent.insertBefore(p.lastChild, parent.firstChild);
}
/**
* Returns the value of `html5.elements` as an array.
* @private
* @returns {Array} An array of shived element node names.
*/
function getElements() {
var elements = html5.elements;
return typeof elements == 'string' ? elements.split(' ') : elements;
}
/**
* Returns the data associated to the given document
* @private
* @param {Document} ownerDocument The document.
* @returns {Object} An object of data.
*/
function getExpandoData(ownerDocument) {
var data = expandoData[ownerDocument[expando]];
if (!data) {
data = {};
expanID++;
ownerDocument[expando] = expanID;
expandoData[expanID] = data;
}
return data;
}
/**
* returns a shived element for the given nodeName and document
* @memberOf html5
* @param {String} nodeName name of the element
* @param {Document} ownerDocument The context document.
* @returns {Object} The shived element.
*/
function createElement(nodeName, ownerDocument, data){
if (!ownerDocument) {
ownerDocument = document;
}
if(supportsUnknownElements){
return ownerDocument.createElement(nodeName);
}
if (!data) {
data = getExpandoData(ownerDocument);
}
var node;
if (data.cache[nodeName]) {
node = data.cache[nodeName].cloneNode();
} else if (saveClones.test(nodeName)) {
node = (data.cache[nodeName] = data.createElem(nodeName)).cloneNode();
} else {
node = data.createElem(nodeName);
}
// Avoid adding some elements to fragments in IE < 9 because
// * Attributes like `name` or `type` cannot be set/changed once an element
// is inserted into a document/fragment
// * Link elements with `src` attributes that are inaccessible, as with
// a 403 response, will cause the tab/window to crash
// * Script elements appended to fragments will execute when their `src`
// or `text` property is set
return node.canHaveChildren && !reSkip.test(nodeName) && !node.tagUrn ? data.frag.appendChild(node) : node;
}
/**
* returns a shived DocumentFragment for the given document
* @memberOf html5
* @param {Document} ownerDocument The context document.
* @returns {Object} The shived DocumentFragment.
*/
function createDocumentFragment(ownerDocument, data){
if (!ownerDocument) {
ownerDocument = document;
}
if(supportsUnknownElements){
return ownerDocument.createDocumentFragment();
}
data = data || getExpandoData(ownerDocument);
var clone = data.frag.cloneNode(),
i = 0,
elems = getElements(),
l = elems.length;
for(;i<l;i++){
clone.createElement(elems[i]);
}
return clone;
}
/**
* Shivs the `createElement` and `createDocumentFragment` methods of the document.
* @private
* @param {Document|DocumentFragment} ownerDocument The document.
* @param {Object} data of the document.
*/
function shivMethods(ownerDocument, data) {
if (!data.cache) {
data.cache = {};
data.createElem = ownerDocument.createElement;
data.createFrag = ownerDocument.createDocumentFragment;
data.frag = data.createFrag();
}
ownerDocument.createElement = function(nodeName) {
//abort shiv
if (!html5.shivMethods) {
return data.createElem(nodeName);
}
return createElement(nodeName, ownerDocument, data);
};
ownerDocument.createDocumentFragment = Function('h,f', 'return function(){' +
'var n=f.cloneNode(),c=n.createElement;' +
'h.shivMethods&&(' +
// unroll the `createElement` calls
getElements().join().replace(/[\w\-]+/g, function(nodeName) {
data.createElem(nodeName);
data.frag.createElement(nodeName);
return 'c("' + nodeName + '")';
}) +
');return n}'
)(html5, data.frag);
}
/*--------------------------------------------------------------------------*/
/**
* Shivs the given document.
* @memberOf html5
* @param {Document} ownerDocument The document to shiv.
* @returns {Document} The shived document.
*/
function shivDocument(ownerDocument) {
if (!ownerDocument) {
ownerDocument = document;
}
var data = getExpandoData(ownerDocument);
if (html5.shivCSS && !supportsHtml5Styles && !data.hasCSS) {
data.hasCSS = !!addStyleSheet(ownerDocument,
// corrects block display not defined in IE6/7/8/9
'article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}' +
// adds styling not present in IE6/7/8/9
'mark{background:#FF0;color:#000}' +
// hides non-rendered elements
'template{display:none}'
);
}
if (!supportsUnknownElements) {
shivMethods(ownerDocument, data);
}
return ownerDocument;
}
/*--------------------------------------------------------------------------*/
/**
* The `html5` object is exposed so that more elements can be shived and
* existing shiving can be detected on iframes.
* @type Object
* @example
*
* // options can be changed before the script is included
* html5 = { 'elements': 'mark section', 'shivCSS': false, 'shivMethods': false };
*/
var html5 = {
/**
* An array or space separated string of node names of the elements to shiv.
* @memberOf html5
* @type Array|String
*/
'elements': options.elements || 'abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video',
/**
* current version of html5shiv
*/
'version': version,
/**
* A flag to indicate that the HTML5 style sheet should be inserted.
* @memberOf html5
* @type Boolean
*/
'shivCSS': (options.shivCSS !== false),
/**
* Is equal to true if a browser supports creating unknown/HTML5 elements
* @memberOf html5
* @type boolean
*/
'supportsUnknownElements': supportsUnknownElements,
/**
* A flag to indicate that the document's `createElement` and `createDocumentFragment`
* methods should be overwritten.
* @memberOf html5
* @type Boolean
*/
'shivMethods': (options.shivMethods !== false),
/**
* A string to describe the type of `html5` object ("default" or "default print").
* @memberOf html5
* @type String
*/
'type': 'default',
// shivs the document according to the specified `html5` object options
'shivDocument': shivDocument,
//creates a shived element
createElement: createElement,
//creates a shived documentFragment
createDocumentFragment: createDocumentFragment
};
/*--------------------------------------------------------------------------*/
// expose html5
window.html5 = html5;
// shiv the document
shivDocument(document);
}(this, document));
/*>>shiv*/
// Assign private properties to the return object with prefix
Modernizr._version = version;
// expose these for the plugin API. Look in the source for how to join() them against your input
/*>>prefixes*/
Modernizr._prefixes = prefixes;
/*>>prefixes*/
/*>>domprefixes*/
Modernizr._domPrefixes = domPrefixes;
Modernizr._cssomPrefixes = cssomPrefixes;
/*>>domprefixes*/
/*>>mq*/
// Modernizr.mq tests a given media query, live against the current state of the window
// A few important notes:
// * If a browser does not support media queries at all (eg. oldIE) the mq() will always return false
// * A max-width or orientation query will be evaluated against the current state, which may change later.
// * You must specify values. Eg. If you are testing support for the min-width media query use:
// Modernizr.mq('(min-width:0)')
// usage:
// Modernizr.mq('only screen and (max-width:768)')
Modernizr.mq = testMediaQuery;
/*>>mq*/
/*>>hasevent*/
// Modernizr.hasEvent() detects support for a given event, with an optional element to test on
// Modernizr.hasEvent('gesturestart', elem)
Modernizr.hasEvent = isEventSupported;
/*>>hasevent*/
/*>>testprop*/
// Modernizr.testProp() investigates whether a given style property is recognized
// Note that the property names must be provided in the camelCase variant.
// Modernizr.testProp('pointerEvents')
Modernizr.testProp = function(prop){
return testProps([prop]);
};
/*>>testprop*/
/*>>testallprops*/
// Modernizr.testAllProps() investigates whether a given style property,
// or any of its vendor-prefixed variants, is recognized
// Note that the property names must be provided in the camelCase variant.
// Modernizr.testAllProps('boxSizing')
Modernizr.testAllProps = testPropsAll;
/*>>testallprops*/
/*>>teststyles*/
// Modernizr.testStyles() allows you to add custom styles to the document and test an element afterwards
// Modernizr.testStyles('#modernizr { position:absolute }', function(elem, rule){ ... })
Modernizr.testStyles = injectElementWithStyles;
/*>>teststyles*/
/*>>prefixed*/
// Modernizr.prefixed() returns the prefixed or nonprefixed property name variant of your input
// Modernizr.prefixed('boxSizing') // 'MozBoxSizing'
// Properties must be passed as dom-style camelcase, rather than `box-sizing` hypentated style.
// Return values will also be the camelCase variant, if you need to translate that to hypenated style use:
//
// str.replace(/([A-Z])/g, function(str,m1){ return '-' + m1.toLowerCase(); }).replace(/^ms-/,'-ms-');
// If you're trying to ascertain which transition end event to bind to, you might do something like...
//
// var transEndEventNames = {
// 'WebkitTransition' : 'webkitTransitionEnd',
// 'MozTransition' : 'transitionend',
// 'OTransition' : 'oTransitionEnd',
// 'msTransition' : 'MSTransitionEnd',
// 'transition' : 'transitionend'
// },
// transEndEventName = transEndEventNames[ Modernizr.prefixed('transition') ];
Modernizr.prefixed = function(prop, obj, elem){
if(!obj) {
return testPropsAll(prop, 'pfx');
} else {
// Testing DOM property e.g. Modernizr.prefixed('requestAnimationFrame', window) // 'mozRequestAnimationFrame'
return testPropsAll(prop, obj, elem);
}
};
/*>>prefixed*/
/*>>cssclasses*/
// Remove "no-js" class from <html> element, if it exists:
docElement.className = docElement.className.replace(/(^|\s)no-js(\s|$)/, '$1$2') +
// Add the new classes to the <html> element.
(enableClasses ? ' js ' + classes.join(' ') : '');
/*>>cssclasses*/
return Modernizr;
})(this, this.document);
/*
Leaflet, a JavaScript library for mobile-friendly interactive maps. http://leafletjs.com
(c) 2010-2013, Vladimir Agafonkin
(c) 2010-2011, CloudMade
*/
(function (window, document, undefined) {
var oldL = window.L,
L = {};
L.version = '0.7.7';
// define Leaflet for Node module pattern loaders, including Browserify
if (typeof module === 'object' && typeof module.exports === 'object') {
module.exports = L;
// define Leaflet as an AMD module
} else if (typeof define === 'function' && define.amd) {
define(L);
}
// define Leaflet as a global L variable, saving the original L to restore later if needed
L.noConflict = function () {
window.L = oldL;
return this;
};
window.L = L;
/*
* L.Util contains various utility functions used throughout Leaflet code.
*/
L.Util = {
extend: function (dest) { // (Object[, Object, ...]) ->
var sources = Array.prototype.slice.call(arguments, 1),
i, j, len, src;
for (j = 0, len = sources.length; j < len; j++) {
src = sources[j] || {};
for (i in src) {
if (src.hasOwnProperty(i)) {
dest[i] = src[i];
}
}
}
return dest;
},
bind: function (fn, obj) { // (Function, Object) -> Function
var args = arguments.length > 2 ? Array.prototype.slice.call(arguments, 2) : null;
return function () {
return fn.apply(obj, args || arguments);
};
},
stamp: (function () {
var lastId = 0,
key = '_leaflet_id';
return function (obj) {
obj[key] = obj[key] || ++lastId;
return obj[key];
};
}()),
invokeEach: function (obj, method, context) {
var i, args;
if (typeof obj === 'object') {
args = Array.prototype.slice.call(arguments, 3);
for (i in obj) {
method.apply(context, [i, obj[i]].concat(args));
}
return true;
}
return false;
},
limitExecByInterval: function (fn, time, context) {
var lock, execOnUnlock;
return function wrapperFn() {
var args = arguments;
if (lock) {
execOnUnlock = true;
return;
}
lock = true;
setTimeout(function () {
lock = false;
if (execOnUnlock) {
wrapperFn.apply(context, args);
execOnUnlock = false;
}
}, time);
fn.apply(context, args);
};
},
falseFn: function () {
return false;
},
formatNum: function (num, digits) {
var pow = Math.pow(10, digits || 5);
return Math.round(num * pow) / pow;
},
trim: function (str) {
return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, '');
},
splitWords: function (str) {
return L.Util.trim(str).split(/\s+/);
},
setOptions: function (obj, options) {
obj.options = L.extend({}, obj.options, options);
return obj.options;
},
getParamString: function (obj, existingUrl, uppercase) {
var params = [];
for (var i in obj) {
params.push(encodeURIComponent(uppercase ? i.toUpperCase() : i) + '=' + encodeURIComponent(obj[i]));
}
return ((!existingUrl || existingUrl.indexOf('?') === -1) ? '?' : '&') + params.join('&');
},
template: function (str, data) {
return str.replace(/\{ *([\w_]+) *\}/g, function (str, key) {
var value = data[key];
if (value === undefined) {
throw new Error('No value provided for variable ' + str);
} else if (typeof value === 'function') {
value = value(data);
}
return value;
});
},
isArray: Array.isArray || function (obj) {
return (Object.prototype.toString.call(obj) === '[object Array]');
},
emptyImageUrl: 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='
};
(function () {
// inspired by http://paulirish.com/2011/requestanimationframe-for-smart-animating/
function getPrefixed(name) {
var i, fn,
prefixes = ['webkit', 'moz', 'o', 'ms'];
for (i = 0; i < prefixes.length && !fn; i++) {
fn = window[prefixes[i] + name];
}
return fn;
}
var lastTime = 0;
function timeoutDefer(fn) {
var time = +new Date(),
timeToCall = Math.max(0, 16 - (time - lastTime));
lastTime = time + timeToCall;
return window.setTimeout(fn, timeToCall);
}
var requestFn = window.requestAnimationFrame ||
getPrefixed('RequestAnimationFrame') || timeoutDefer;
var cancelFn = window.cancelAnimationFrame ||
getPrefixed('CancelAnimationFrame') ||
getPrefixed('CancelRequestAnimationFrame') ||
function (id) { window.clearTimeout(id); };
L.Util.requestAnimFrame = function (fn, context, immediate, element) {
fn = L.bind(fn, context);
if (immediate && requestFn === timeoutDefer) {
fn();
} else {
return requestFn.call(window, fn, element);
}
};
L.Util.cancelAnimFrame = function (id) {
if (id) {
cancelFn.call(window, id);
}
};
}());
// shortcuts for most used utility functions
L.extend = L.Util.extend;
L.bind = L.Util.bind;
L.stamp = L.Util.stamp;
L.setOptions = L.Util.setOptions;
/*
* L.Class powers the OOP facilities of the library.
* Thanks to John Resig and Dean Edwards for inspiration!
*/
L.Class = function () {};
L.Class.extend = function (props) {
// extended class with the new prototype
var NewClass = function () {
// call the constructor
if (this.initialize) {
this.initialize.apply(this, arguments);
}
// call all constructor hooks
if (this._initHooks) {
this.callInitHooks();
}
};
// instantiate class without calling constructor
var F = function () {};
F.prototype = this.prototype;
var proto = new F();
proto.constructor = NewClass;
NewClass.prototype = proto;
//inherit parent's statics
for (var i in this) {
if (this.hasOwnProperty(i) && i !== 'prototype') {
NewClass[i] = this[i];
}
}
// mix static properties into the class
if (props.statics) {
L.extend(NewClass, props.statics);
delete props.statics;
}
// mix includes into the prototype
if (props.includes) {
L.Util.extend.apply(null, [proto].concat(props.includes));
delete props.includes;
}
// merge options
if (props.options && proto.options) {
props.options = L.extend({}, proto.options, props.options);
}
// mix given properties into the prototype
L.extend(proto, props);
proto._initHooks = [];
var parent = this;
// jshint camelcase: false
NewClass.__super__ = parent.prototype;
// add method for calling all hooks
proto.callInitHooks = function () {
if (this._initHooksCalled) { return; }
if (parent.prototype.callInitHooks) {
parent.prototype.callInitHooks.call(this);
}
this._initHooksCalled = true;
for (var i = 0, len = proto._initHooks.length; i < len; i++) {
proto._initHooks[i].call(this);
}
};
return NewClass;
};
// method for adding properties to prototype
L.Class.include = function (props) {
L.extend(this.prototype, props);
};
// merge new default options to the Class
L.Class.mergeOptions = function (options) {
L.extend(this.prototype.options, options);
};
// add a constructor hook
L.Class.addInitHook = function (fn) { // (Function) || (String, args...)
var args = Array.prototype.slice.call(arguments, 1);
var init = typeof fn === 'function' ? fn : function () {
this[fn].apply(this, args);
};
this.prototype._initHooks = this.prototype._initHooks || [];
this.prototype._initHooks.push(init);
};
/*
* L.Mixin.Events is used to add custom events functionality to Leaflet classes.
*/
var eventsKey = '_leaflet_events';
L.Mixin = {};
L.Mixin.Events = {
addEventListener: function (types, fn, context) { // (String, Function[, Object]) or (Object[, Object])
// types can be a map of types/handlers
if (L.Util.invokeEach(types, this.addEventListener, this, fn, context)) { return this; }
var events = this[eventsKey] = this[eventsKey] || {},
contextId = context && context !== this && L.stamp(context),
i, len, event, type, indexKey, indexLenKey, typeIndex;
// types can be a string of space-separated words
types = L.Util.splitWords(types);
for (i = 0, len = types.length; i < len; i++) {
event = {
action: fn,
context: context || this
};
type = types[i];
if (contextId) {
// store listeners of a particular context in a separate hash (if it has an id)
// gives a major performance boost when removing thousands of map layers
indexKey = type + '_idx';
indexLenKey = indexKey + '_len';
typeIndex = events[indexKey] = events[indexKey] || {};
if (!typeIndex[contextId]) {
typeIndex[contextId] = [];
// keep track of the number of keys in the index to quickly check if it's empty
events[indexLenKey] = (events[indexLenKey] || 0) + 1;
}
typeIndex[contextId].push(event);
} else {
events[type] = events[type] || [];
events[type].push(event);
}
}
return this;
},
hasEventListeners: function (type) { // (String) -> Boolean
var events = this[eventsKey];
return !!events && ((type in events && events[type].length > 0) ||
(type + '_idx' in events && events[type + '_idx_len'] > 0));
},
removeEventListener: function (types, fn, context) { // ([String, Function, Object]) or (Object[, Object])
if (!this[eventsKey]) {
return this;
}
if (!types) {
return this.clearAllEventListeners();
}
if (L.Util.invokeEach(types, this.removeEventListener, this, fn, context)) { return this; }
var events = this[eventsKey],
contextId = context && context !== this && L.stamp(context),
i, len, type, listeners, j, indexKey, indexLenKey, typeIndex, removed;
types = L.Util.splitWords(types);
for (i = 0, len = types.length; i < len; i++) {
type = types[i];
indexKey = type + '_idx';
indexLenKey = indexKey + '_len';
typeIndex = events[indexKey];
if (!fn) {
// clear all listeners for a type if function isn't specified
delete events[type];
delete events[indexKey];
delete events[indexLenKey];
} else {
listeners = contextId && typeIndex ? typeIndex[contextId] : events[type];
if (listeners) {
for (j = listeners.length - 1; j >= 0; j--) {
if ((listeners[j].action === fn) && (!context || (listeners[j].context === context))) {
removed = listeners.splice(j, 1);
// set the old action to a no-op, because it is possible
// that the listener is being iterated over as part of a dispatch
removed[0].action = L.Util.falseFn;
}
}
if (context && typeIndex && (listeners.length === 0)) {
delete typeIndex[contextId];
events[indexLenKey]--;
}
}
}
}
return this;
},
clearAllEventListeners: function () {
delete this[eventsKey];
return this;
},
fireEvent: function (type, data) { // (String[, Object])
if (!this.hasEventListeners(type)) {
return this;
}
var event = L.Util.extend({}, data, { type: type, target: this });
var events = this[eventsKey],
listeners, i, len, typeIndex, contextId;
if (events[type]) {
// make sure adding/removing listeners inside other listeners won't cause infinite loop
listeners = events[type].slice();
for (i = 0, len = listeners.length; i < len; i++) {
listeners[i].action.call(listeners[i].context, event);
}
}
// fire event for the context-indexed listeners as well
typeIndex = events[type + '_idx'];
for (contextId in typeIndex) {
listeners = typeIndex[contextId].slice();
if (listeners) {
for (i = 0, len = listeners.length; i < len; i++) {
listeners[i].action.call(listeners[i].context, event);
}
}
}
return this;
},
addOneTimeEventListener: function (types, fn, context) {
if (L.Util.invokeEach(types, this.addOneTimeEventListener, this, fn, context)) { return this; }
var handler = L.bind(function () {
this
.removeEventListener(types, fn, context)
.removeEventListener(types, handler, context);
}, this);
return this
.addEventListener(types, fn, context)
.addEventListener(types, handler, context);
}
};
L.Mixin.Events.on = L.Mixin.Events.addEventListener;
L.Mixin.Events.off = L.Mixin.Events.removeEventListener;
L.Mixin.Events.once = L.Mixin.Events.addOneTimeEventListener;
L.Mixin.Events.fire = L.Mixin.Events.fireEvent;
/*
* L.Browser handles different browser and feature detections for internal Leaflet use.
*/
(function () {
var ie = 'ActiveXObject' in window,
ielt9 = ie && !document.addEventListener,
// terrible browser detection to work around Safari / iOS / Android browser bugs
ua = navigator.userAgent.toLowerCase(),
webkit = ua.indexOf('webkit') !== -1,
chrome = ua.indexOf('chrome') !== -1,
phantomjs = ua.indexOf('phantom') !== -1,
android = ua.indexOf('android') !== -1,
android23 = ua.search('android [23]') !== -1,
gecko = ua.indexOf('gecko') !== -1,
mobile = typeof orientation !== undefined + '',
msPointer = !window.PointerEvent && window.MSPointerEvent,
pointer = (window.PointerEvent && window.navigator.pointerEnabled) ||
msPointer,
retina = ('devicePixelRatio' in window && window.devicePixelRatio > 1) ||
('matchMedia' in window && window.matchMedia('(min-resolution:144dpi)') &&
window.matchMedia('(min-resolution:144dpi)').matches),
doc = document.documentElement,
ie3d = ie && ('transition' in doc.style),
webkit3d = ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) && !android23,
gecko3d = 'MozPerspective' in doc.style,
opera3d = 'OTransition' in doc.style,
any3d = !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d || opera3d) && !phantomjs;
var touch = !window.L_NO_TOUCH && !phantomjs && (pointer || 'ontouchstart' in window ||
(window.DocumentTouch && document instanceof window.DocumentTouch));
L.Browser = {
ie: ie,
ielt9: ielt9,
webkit: webkit,
gecko: gecko && !webkit && !window.opera && !ie,
android: android,
android23: android23,
chrome: chrome,
ie3d: ie3d,
webkit3d: webkit3d,
gecko3d: gecko3d,
opera3d: opera3d,
any3d: any3d,
mobile: mobile,
mobileWebkit: mobile && webkit,
mobileWebkit3d: mobile && webkit3d,
mobileOpera: mobile && window.opera,
touch: touch,
msPointer: msPointer,
pointer: pointer,
retina: retina
};
}());
/*
* L.Point represents a point with x and y coordinates.
*/
L.Point = function (/*Number*/ x, /*Number*/ y, /*Boolean*/ round) {
this.x = (round ? Math.round(x) : x);
this.y = (round ? Math.round(y) : y);
};
L.Point.prototype = {
clone: function () {
return new L.Point(this.x, this.y);
},
// non-destructive, returns a new point
add: function (point) {
return this.clone()._add(L.point(point));
},
// destructive, used directly for performance in situations where it's safe to modify existing point
_add: function (point) {
this.x += point.x;
this.y += point.y;
return this;
},
subtract: function (point) {
return this.clone()._subtract(L.point(point));
},
_subtract: function (point) {
this.x -= point.x;
this.y -= point.y;
return this;
},
divideBy: function (num) {
return this.clone()._divideBy(num);
},
_divideBy: function (num) {
this.x /= num;
this.y /= num;
return this;
},
multiplyBy: function (num) {
return this.clone()._multiplyBy(num);
},
_multiplyBy: function (num) {
this.x *= num;
this.y *= num;
return this;
},
round: function () {
return this.clone()._round();
},
_round: function () {
this.x = Math.round(this.x);
this.y = Math.round(this.y);
return this;
},
floor: function () {
return this.clone()._floor();
},
_floor: function () {
this.x = Math.floor(this.x);
this.y = Math.floor(this.y);
return this;
},
distanceTo: function (point) {
point = L.point(point);
var x = point.x - this.x,
y = point.y - this.y;
return Math.sqrt(x * x + y * y);
},
equals: function (point) {
point = L.point(point);
return point.x === this.x &&
point.y === this.y;
},
contains: function (point) {
point = L.point(point);
return Math.abs(point.x) <= Math.abs(this.x) &&
Math.abs(point.y) <= Math.abs(this.y);
},
toString: function () {
return 'Point(' +
L.Util.formatNum(this.x) + ', ' +
L.Util.formatNum(this.y) + ')';
}
};
L.point = function (x, y, round) {
if (x instanceof L.Point) {
return x;
}
if (L.Util.isArray(x)) {
return new L.Point(x[0], x[1]);
}
if (x === undefined || x === null) {
return x;
}
return new L.Point(x, y, round);
};
/*
* L.Bounds represents a rectangular area on the screen in pixel coordinates.
*/
L.Bounds = function (a, b) { //(Point, Point) or Point[]
if (!a) { return; }
var points = b ? [a, b] : a;
for (var i = 0, len = points.length; i < len; i++) {
this.extend(points[i]);
}
};
L.Bounds.prototype = {
// extend the bounds to contain the given point
extend: function (point) { // (Point)
point = L.point(point);
if (!this.min && !this.max) {
this.min = point.clone();
this.max = point.clone();
} else {
this.min.x = Math.min(point.x, this.min.x);
this.max.x = Math.max(point.x, this.max.x);
this.min.y = Math.min(point.y, this.min.y);
this.max.y = Math.max(point.y, this.max.y);
}
return this;
},
getCenter: function (round) { // (Boolean) -> Point
return new L.Point(
(this.min.x + this.max.x) / 2,
(this.min.y + this.max.y) / 2, round);
},
getBottomLeft: function () { // -> Point
return new L.Point(this.min.x, this.max.y);
},
getTopRight: function () { // -> Point
return new L.Point(this.max.x, this.min.y);
},
getSize: function () {
return this.max.subtract(this.min);
},
contains: function (obj) { // (Bounds) or (Point) -> Boolean
var min, max;
if (typeof obj[0] === 'number' || obj instanceof L.Point) {
obj = L.point(obj);
} else {
obj = L.bounds(obj);
}
if (obj instanceof L.Bounds) {
min = obj.min;
max = obj.max;
} else {
min = max = obj;
}
return (min.x >= this.min.x) &&
(max.x <= this.max.x) &&
(min.y >= this.min.y) &&
(max.y <= this.max.y);
},
intersects: function (bounds) { // (Bounds) -> Boolean
bounds = L.bounds(bounds);
var min = this.min,
max = this.max,
min2 = bounds.min,
max2 = bounds.max,
xIntersects = (max2.x >= min.x) && (min2.x <= max.x),
yIntersects = (max2.y >= min.y) && (min2.y <= max.y);
return xIntersects && yIntersects;
},
isValid: function () {
return !!(this.min && this.max);
}
};
L.bounds = function (a, b) { // (Bounds) or (Point, Point) or (Point[])
if (!a || a instanceof L.Bounds) {
return a;
}
return new L.Bounds(a, b);
};
/*
* L.Transformation is an utility class to perform simple point transformations through a 2d-matrix.
*/
L.Transformation = function (a, b, c, d) {
this._a = a;
this._b = b;
this._c = c;
this._d = d;
};
L.Transformation.prototype = {
transform: function (point, scale) { // (Point, Number) -> Point
return this._transform(point.clone(), scale);
},
// destructive transform (faster)
_transform: function (point, scale) {
scale = scale || 1;
point.x = scale * (this._a * point.x + this._b);
point.y = scale * (this._c * point.y + this._d);
return point;
},
untransform: function (point, scale) {
scale = scale || 1;
return new L.Point(
(point.x / scale - this._b) / this._a,
(point.y / scale - this._d) / this._c);
}
};
/*
* L.DomUtil contains various utility functions for working with DOM.
*/
L.DomUtil = {
get: function (id) {
return (typeof id === 'string' ? document.getElementById(id) : id);
},
getStyle: function (el, style) {
var value = el.style[style];
if (!value && el.currentStyle) {
value = el.currentStyle[style];
}
if ((!value || value === 'auto') && document.defaultView) {
var css = document.defaultView.getComputedStyle(el, null);
value = css ? css[style] : null;
}
return value === 'auto' ? null : value;
},
getViewportOffset: function (element) {
var top = 0,
left = 0,
el = element,
docBody = document.body,
docEl = document.documentElement,
pos;
do {
top += el.offsetTop || 0;
left += el.offsetLeft || 0;
//add borders
top += parseInt(L.DomUtil.getStyle(el, 'borderTopWidth'), 10) || 0;
left += parseInt(L.DomUtil.getStyle(el, 'borderLeftWidth'), 10) || 0;
pos = L.DomUtil.getStyle(el, 'position');
if (el.offsetParent === docBody && pos === 'absolute') { break; }
if (pos === 'fixed') {
top += docBody.scrollTop || docEl.scrollTop || 0;
left += docBody.scrollLeft || docEl.scrollLeft || 0;
break;
}
if (pos === 'relative' && !el.offsetLeft) {
var width = L.DomUtil.getStyle(el, 'width'),
maxWidth = L.DomUtil.getStyle(el, 'max-width'),
r = el.getBoundingClientRect();
if (width !== 'none' || maxWidth !== 'none') {
left += r.left + el.clientLeft;
}
//calculate full y offset since we're breaking out of the loop
top += r.top + (docBody.scrollTop || docEl.scrollTop || 0);
break;
}
el = el.offsetParent;
} while (el);
el = element;
do {
if (el === docBody) { break; }
top -= el.scrollTop || 0;
left -= el.scrollLeft || 0;
el = el.parentNode;
} while (el);
return new L.Point(left, top);
},
documentIsLtr: function () {
if (!L.DomUtil._docIsLtrCached) {
L.DomUtil._docIsLtrCached = true;
L.DomUtil._docIsLtr = L.DomUtil.getStyle(document.body, 'direction') === 'ltr';
}
return L.DomUtil._docIsLtr;
},
create: function (tagName, className, container) {
var el = document.createElement(tagName);
el.className = className;
if (container) {
container.appendChild(el);
}
return el;
},
hasClass: function (el, name) {
if (el.classList !== undefined) {
return el.classList.contains(name);
}
var className = L.DomUtil._getClass(el);
return className.length > 0 && new RegExp('(^|\\s)' + name + '(\\s|$)').test(className);
},
addClass: function (el, name) {
if (el.classList !== undefined) {
var classes = L.Util.splitWords(name);
for (var i = 0, len = classes.length; i < len; i++) {
el.classList.add(classes[i]);
}
} else if (!L.DomUtil.hasClass(el, name)) {
var className = L.DomUtil._getClass(el);
L.DomUtil._setClass(el, (className ? className + ' ' : '') + name);
}
},
removeClass: function (el, name) {
if (el.classList !== undefined) {
el.classList.remove(name);
} else {
L.DomUtil._setClass(el, L.Util.trim((' ' + L.DomUtil._getClass(el) + ' ').replace(' ' + name + ' ', ' ')));
}
},
_setClass: function (el, name) {
if (el.className.baseVal === undefined) {
el.className = name;
} else {
// in case of SVG element
el.className.baseVal = name;
}
},
_getClass: function (el) {
return el.className.baseVal === undefined ? el.className : el.className.baseVal;
},
setOpacity: function (el, value) {
if ('opacity' in el.style) {
el.style.opacity = value;
} else if ('filter' in el.style) {
var filter = false,
filterName = 'DXImageTransform.Microsoft.Alpha';
// filters collection throws an error if we try to retrieve a filter that doesn't exist
try {
filter = el.filters.item(filterName);
} catch (e) {
// don't set opacity to 1 if we haven't already set an opacity,
// it isn't needed and breaks transparent pngs.
if (value === 1) { return; }
}
value = Math.round(value * 100);
if (filter) {
filter.Enabled = (value !== 100);
filter.Opacity = value;
} else {
el.style.filter += ' progid:' + filterName + '(opacity=' + value + ')';
}
}
},
testProp: function (props) {
var style = document.documentElement.style;
for (var i = 0; i < props.length; i++) {
if (props[i] in style) {
return props[i];
}
}
return false;
},
getTranslateString: function (point) {
// on WebKit browsers (Chrome/Safari/iOS Safari/Android) using translate3d instead of translate
// makes animation smoother as it ensures HW accel is used. Firefox 13 doesn't care
// (same speed either way), Opera 12 doesn't support translate3d
var is3d = L.Browser.webkit3d,
open = 'translate' + (is3d ? '3d' : '') + '(',
close = (is3d ? ',0' : '') + ')';
return open + point.x + 'px,' + point.y + 'px' + close;
},
getScaleString: function (scale, origin) {
var preTranslateStr = L.DomUtil.getTranslateString(origin.add(origin.multiplyBy(-1 * scale))),
scaleStr = ' scale(' + scale + ') ';
return preTranslateStr + scaleStr;
},
setPosition: function (el, point, disable3D) { // (HTMLElement, Point[, Boolean])
// jshint camelcase: false
el._leaflet_pos = point;
if (!disable3D && L.Browser.any3d) {
el.style[L.DomUtil.TRANSFORM] = L.DomUtil.getTranslateString(point);
} else {
el.style.left = point.x + 'px';
el.style.top = point.y + 'px';
}
},
getPosition: function (el) {
// this method is only used for elements previously positioned using setPosition,
// so it's safe to cache the position for performance
// jshint camelcase: false
return el._leaflet_pos;
}
};
// prefix style property names
L.DomUtil.TRANSFORM = L.DomUtil.testProp(
['transform', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform']);
// webkitTransition comes first because some browser versions that drop vendor prefix don't do
// the same for the transitionend event, in particular the Android 4.1 stock browser
L.DomUtil.TRANSITION = L.DomUtil.testProp(
['webkitTransition', 'transition', 'OTransition', 'MozTransition', 'msTransition']);
L.DomUtil.TRANSITION_END =
L.DomUtil.TRANSITION === 'webkitTransition' || L.DomUtil.TRANSITION === 'OTransition' ?
L.DomUtil.TRANSITION + 'End' : 'transitionend';
(function () {
if ('onselectstart' in document) {
L.extend(L.DomUtil, {
disableTextSelection: function () {
L.DomEvent.on(window, 'selectstart', L.DomEvent.preventDefault);
},
enableTextSelection: function () {
L.DomEvent.off(window, 'selectstart', L.DomEvent.preventDefault);
}
});
} else {
var userSelectProperty = L.DomUtil.testProp(
['userSelect', 'WebkitUserSelect', 'OUserSelect', 'MozUserSelect', 'msUserSelect']);
L.extend(L.DomUtil, {
disableTextSelection: function () {
if (userSelectProperty) {
var style = document.documentElement.style;
this._userSelect = style[userSelectProperty];
style[userSelectProperty] = 'none';
}
},
enableTextSelection: function () {
if (userSelectProperty) {
document.documentElement.style[userSelectProperty] = this._userSelect;
delete this._userSelect;
}
}
});
}
L.extend(L.DomUtil, {
disableImageDrag: function () {
L.DomEvent.on(window, 'dragstart', L.DomEvent.preventDefault);
},
enableImageDrag: function () {
L.DomEvent.off(window, 'dragstart', L.DomEvent.preventDefault);
}
});
})();
/*
* L.LatLng represents a geographical point with latitude and longitude coordinates.
*/
L.LatLng = function (lat, lng, alt) { // (Number, Number, Number)
lat = parseFloat(lat);
lng = parseFloat(lng);
if (isNaN(lat) || isNaN(lng)) {
throw new Error('Invalid LatLng object: (' + lat + ', ' + lng + ')');
}
this.lat = lat;
this.lng = lng;
if (alt !== undefined) {
this.alt = parseFloat(alt);
}
};
L.extend(L.LatLng, {
DEG_TO_RAD: Math.PI / 180,
RAD_TO_DEG: 180 / Math.PI,
MAX_MARGIN: 1.0E-9 // max margin of error for the "equals" check
});
L.LatLng.prototype = {
equals: function (obj) { // (LatLng) -> Boolean
if (!obj) { return false; }
obj = L.latLng(obj);
var margin = Math.max(
Math.abs(this.lat - obj.lat),
Math.abs(this.lng - obj.lng));
return margin <= L.LatLng.MAX_MARGIN;
},
toString: function (precision) { // (Number) -> String
return 'LatLng(' +
L.Util.formatNum(this.lat, precision) + ', ' +
L.Util.formatNum(this.lng, precision) + ')';
},
// Haversine distance formula, see http://en.wikipedia.org/wiki/Haversine_formula
// TODO move to projection code, LatLng shouldn't know about Earth
distanceTo: function (other) { // (LatLng) -> Number
other = L.latLng(other);
var R = 6378137, // earth radius in meters
d2r = L.LatLng.DEG_TO_RAD,
dLat = (other.lat - this.lat) * d2r,
dLon = (other.lng - this.lng) * d2r,
lat1 = this.lat * d2r,
lat2 = other.lat * d2r,
sin1 = Math.sin(dLat / 2),
sin2 = Math.sin(dLon / 2);
var a = sin1 * sin1 + sin2 * sin2 * Math.cos(lat1) * Math.cos(lat2);
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
},
wrap: function (a, b) { // (Number, Number) -> LatLng
var lng = this.lng;
a = a || -180;
b = b || 180;
lng = (lng + b) % (b - a) + (lng < a || lng === b ? b : a);
return new L.LatLng(this.lat, lng);
}
};
L.latLng = function (a, b) { // (LatLng) or ([Number, Number]) or (Number, Number)
if (a instanceof L.LatLng) {
return a;
}
if (L.Util.isArray(a)) {
if (typeof a[0] === 'number' || typeof a[0] === 'string') {
return new L.LatLng(a[0], a[1], a[2]);
} else {
return null;
}
}
if (a === undefined || a === null) {
return a;
}
if (typeof a === 'object' && 'lat' in a) {
return new L.LatLng(a.lat, 'lng' in a ? a.lng : a.lon);
}
if (b === undefined) {
return null;
}
return new L.LatLng(a, b);
};
/*
* L.LatLngBounds represents a rectangular area on the map in geographical coordinates.
*/
L.LatLngBounds = function (southWest, northEast) { // (LatLng, LatLng) or (LatLng[])
if (!southWest) { return; }
var latlngs = northEast ? [southWest, northEast] : southWest;
for (var i = 0, len = latlngs.length; i < len; i++) {
this.extend(latlngs[i]);
}
};
L.LatLngBounds.prototype = {
// extend the bounds to contain the given point or bounds
extend: function (obj) { // (LatLng) or (LatLngBounds)
if (!obj) { return this; }
var latLng = L.latLng(obj);
if (latLng !== null) {
obj = latLng;
} else {
obj = L.latLngBounds(obj);
}
if (obj instanceof L.LatLng) {
if (!this._southWest && !this._northEast) {
this._southWest = new L.LatLng(obj.lat, obj.lng);
this._northEast = new L.LatLng(obj.lat, obj.lng);
} else {
this._southWest.lat = Math.min(obj.lat, this._southWest.lat);
this._southWest.lng = Math.min(obj.lng, this._southWest.lng);
this._northEast.lat = Math.max(obj.lat, this._northEast.lat);
this._northEast.lng = Math.max(obj.lng, this._northEast.lng);
}
} else if (obj instanceof L.LatLngBounds) {
this.extend(obj._southWest);
this.extend(obj._northEast);
}
return this;
},
// extend the bounds by a percentage
pad: function (bufferRatio) { // (Number) -> LatLngBounds
var sw = this._southWest,
ne = this._northEast,
heightBuffer = Math.abs(sw.lat - ne.lat) * bufferRatio,
widthBuffer = Math.abs(sw.lng - ne.lng) * bufferRatio;
return new L.LatLngBounds(
new L.LatLng(sw.lat - heightBuffer, sw.lng - widthBuffer),
new L.LatLng(ne.lat + heightBuffer, ne.lng + widthBuffer));
},
getCenter: function () { // -> LatLng
return new L.LatLng(
(this._southWest.lat + this._northEast.lat) / 2,
(this._southWest.lng + this._northEast.lng) / 2);
},
getSouthWest: function () {
return this._southWest;
},
getNorthEast: function () {
return this._northEast;
},
getNorthWest: function () {
return new L.LatLng(this.getNorth(), this.getWest());
},
getSouthEast: function () {
return new L.LatLng(this.getSouth(), this.getEast());
},
getWest: function () {
return this._southWest.lng;
},
getSouth: function () {
return this._southWest.lat;
},
getEast: function () {
return this._northEast.lng;
},
getNorth: function () {
return this._northEast.lat;
},
contains: function (obj) { // (LatLngBounds) or (LatLng) -> Boolean
if (typeof obj[0] === 'number' || obj instanceof L.LatLng) {
obj = L.latLng(obj);
} else {
obj = L.latLngBounds(obj);
}
var sw = this._southWest,
ne = this._northEast,
sw2, ne2;
if (obj instanceof L.LatLngBounds) {
sw2 = obj.getSouthWest();
ne2 = obj.getNorthEast();
} else {
sw2 = ne2 = obj;
}
return (sw2.lat >= sw.lat) && (ne2.lat <= ne.lat) &&
(sw2.lng >= sw.lng) && (ne2.lng <= ne.lng);
},
intersects: function (bounds) { // (LatLngBounds)
bounds = L.latLngBounds(bounds);
var sw = this._southWest,
ne = this._northEast,
sw2 = bounds.getSouthWest(),
ne2 = bounds.getNorthEast(),
latIntersects = (ne2.lat >= sw.lat) && (sw2.lat <= ne.lat),
lngIntersects = (ne2.lng >= sw.lng) && (sw2.lng <= ne.lng);
return latIntersects && lngIntersects;
},
toBBoxString: function () {
return [this.getWest(), this.getSouth(), this.getEast(), this.getNorth()].join(',');
},
equals: function (bounds) { // (LatLngBounds)
if (!bounds) { return false; }
bounds = L.latLngBounds(bounds);
return this._southWest.equals(bounds.getSouthWest()) &&
this._northEast.equals(bounds.getNorthEast());
},
isValid: function () {
return !!(this._southWest && this._northEast);
}
};
//TODO International date line?
L.latLngBounds = function (a, b) { // (LatLngBounds) or (LatLng, LatLng)
if (!a || a instanceof L.LatLngBounds) {
return a;
}
return new L.LatLngBounds(a, b);
};
/*
* L.Projection contains various geographical projections used by CRS classes.
*/
L.Projection = {};
/*
* Spherical Mercator is the most popular map projection, used by EPSG:3857 CRS used by default.
*/
L.Projection.SphericalMercator = {
MAX_LATITUDE: 85.0511287798,
project: function (latlng) { // (LatLng) -> Point
var d = L.LatLng.DEG_TO_RAD,
max = this.MAX_LATITUDE,
lat = Math.max(Math.min(max, latlng.lat), -max),
x = latlng.lng * d,
y = lat * d;
y = Math.log(Math.tan((Math.PI / 4) + (y / 2)));
return new L.Point(x, y);
},
unproject: function (point) { // (Point, Boolean) -> LatLng
var d = L.LatLng.RAD_TO_DEG,
lng = point.x * d,
lat = (2 * Math.atan(Math.exp(point.y)) - (Math.PI / 2)) * d;
return new L.LatLng(lat, lng);
}
};
/*
* Simple equirectangular (Plate Carree) projection, used by CRS like EPSG:4326 and Simple.
*/
L.Projection.LonLat = {
project: function (latlng) {
return new L.Point(latlng.lng, latlng.lat);
},
unproject: function (point) {
return new L.LatLng(point.y, point.x);
}
};
/*
* L.CRS is a base object for all defined CRS (Coordinate Reference Systems) in Leaflet.
*/
L.CRS = {
latLngToPoint: function (latlng, zoom) { // (LatLng, Number) -> Point
var projectedPoint = this.projection.project(latlng),
scale = this.scale(zoom);
return this.transformation._transform(projectedPoint, scale);
},
pointToLatLng: function (point, zoom) { // (Point, Number[, Boolean]) -> LatLng
var scale = this.scale(zoom),
untransformedPoint = this.transformation.untransform(point, scale);
return this.projection.unproject(untransformedPoint);
},
project: function (latlng) {
return this.projection.project(latlng);
},
scale: function (zoom) {
return 256 * Math.pow(2, zoom);
},
getSize: function (zoom) {
var s = this.scale(zoom);
return L.point(s, s);
}
};
/*
* A simple CRS that can be used for flat non-Earth maps like panoramas or game maps.
*/
L.CRS.Simple = L.extend({}, L.CRS, {
projection: L.Projection.LonLat,
transformation: new L.Transformation(1, 0, -1, 0),
scale: function (zoom) {
return Math.pow(2, zoom);
}
});
/*
* L.CRS.EPSG3857 (Spherical Mercator) is the most common CRS for web mapping
* and is used by Leaflet by default.
*/
L.CRS.EPSG3857 = L.extend({}, L.CRS, {
code: 'EPSG:3857',
projection: L.Projection.SphericalMercator,
transformation: new L.Transformation(0.5 / Math.PI, 0.5, -0.5 / Math.PI, 0.5),
project: function (latlng) { // (LatLng) -> Point
var projectedPoint = this.projection.project(latlng),
earthRadius = 6378137;
return projectedPoint.multiplyBy(earthRadius);
}
});
L.CRS.EPSG900913 = L.extend({}, L.CRS.EPSG3857, {
code: 'EPSG:900913'
});
/*
* L.CRS.EPSG4326 is a CRS popular among advanced GIS specialists.
*/
L.CRS.EPSG4326 = L.extend({}, L.CRS, {
code: 'EPSG:4326',
projection: L.Projection.LonLat,
transformation: new L.Transformation(1 / 360, 0.5, -1 / 360, 0.5)
});
/*
* L.Map is the central class of the API - it is used to create a map.
*/
L.Map = L.Class.extend({
includes: L.Mixin.Events,
options: {
crs: L.CRS.EPSG3857,
/*
center: LatLng,
zoom: Number,
layers: Array,
*/
fadeAnimation: L.DomUtil.TRANSITION && !L.Browser.android23,
trackResize: true,
markerZoomAnimation: L.DomUtil.TRANSITION && L.Browser.any3d
},
initialize: function (id, options) { // (HTMLElement or String, Object)
options = L.setOptions(this, options);
this._initContainer(id);
this._initLayout();
// hack for https://github.com/Leaflet/Leaflet/issues/1980
this._onResize = L.bind(this._onResize, this);
this._initEvents();
if (options.maxBounds) {
this.setMaxBounds(options.maxBounds);
}
if (options.center && options.zoom !== undefined) {
this.setView(L.latLng(options.center), options.zoom, {reset: true});
}
this._handlers = [];
this._layers = {};
this._zoomBoundLayers = {};
this._tileLayersNum = 0;
this.callInitHooks();
this._addLayers(options.layers);
},
// public methods that modify map state
// replaced by animation-powered implementation in Map.PanAnimation.js
setView: function (center, zoom) {
zoom = zoom === undefined ? this.getZoom() : zoom;
this._resetView(L.latLng(center), this._limitZoom(zoom));
return this;
},
setZoom: function (zoom, options) {
if (!this._loaded) {
this._zoom = this._limitZoom(zoom);
return this;
}
return this.setView(this.getCenter(), zoom, {zoom: options});
},
zoomIn: function (delta, options) {
return this.setZoom(this._zoom + (delta || 1), options);
},
zoomOut: function (delta, options) {
return this.setZoom(this._zoom - (delta || 1), options);
},
setZoomAround: function (latlng, zoom, options) {
var scale = this.getZoomScale(zoom),
viewHalf = this.getSize().divideBy(2),
containerPoint = latlng instanceof L.Point ? latlng : this.latLngToContainerPoint(latlng),
centerOffset = containerPoint.subtract(viewHalf).multiplyBy(1 - 1 / scale),
newCenter = this.containerPointToLatLng(viewHalf.add(centerOffset));
return this.setView(newCenter, zoom, {zoom: options});
},
fitBounds: function (bounds, options) {
options = options || {};
bounds = bounds.getBounds ? bounds.getBounds() : L.latLngBounds(bounds);
var paddingTL = L.point(options.paddingTopLeft || options.padding || [0, 0]),
paddingBR = L.point(options.paddingBottomRight || options.padding || [0, 0]),
zoom = this.getBoundsZoom(bounds, false, paddingTL.add(paddingBR));
zoom = (options.maxZoom) ? Math.min(options.maxZoom, zoom) : zoom;
var paddingOffset = paddingBR.subtract(paddingTL).divideBy(2),
swPoint = this.project(bounds.getSouthWest(), zoom),
nePoint = this.project(bounds.getNorthEast(), zoom),
center = this.unproject(swPoint.add(nePoint).divideBy(2).add(paddingOffset), zoom);
return this.setView(center, zoom, options);
},
fitWorld: function (options) {
return this.fitBounds([[-90, -180], [90, 180]], options);
},
panTo: function (center, options) { // (LatLng)
return this.setView(center, this._zoom, {pan: options});
},
panBy: function (offset) { // (Point)
// replaced with animated panBy in Map.PanAnimation.js
this.fire('movestart');
this._rawPanBy(L.point(offset));
this.fire('move');
return this.fire('moveend');
},
setMaxBounds: function (bounds) {
bounds = L.latLngBounds(bounds);
this.options.maxBounds = bounds;
if (!bounds) {
return this.off('moveend', this._panInsideMaxBounds, this);
}
if (this._loaded) {
this._panInsideMaxBounds();
}
return this.on('moveend', this._panInsideMaxBounds, this);
},
panInsideBounds: function (bounds, options) {
var center = this.getCenter(),
newCenter = this._limitCenter(center, this._zoom, bounds);
if (center.equals(newCenter)) { return this; }
return this.panTo(newCenter, options);
},
addLayer: function (layer) {
// TODO method is too big, refactor
var id = L.stamp(layer);
if (this._layers[id]) { return this; }
this._layers[id] = layer;
// TODO getMaxZoom, getMinZoom in ILayer (instead of options)
if (layer.options && (!isNaN(layer.options.maxZoom) || !isNaN(layer.options.minZoom))) {
this._zoomBoundLayers[id] = layer;
this._updateZoomLevels();
}
// TODO looks ugly, refactor!!!
if (this.options.zoomAnimation && L.TileLayer && (layer instanceof L.TileLayer)) {
this._tileLayersNum++;
this._tileLayersToLoad++;
layer.on('load', this._onTileLayerLoad, this);
}
if (this._loaded) {
this._layerAdd(layer);
}
return this;
},
removeLayer: function (layer) {
var id = L.stamp(layer);
if (!this._layers[id]) { return this; }
if (this._loaded) {
layer.onRemove(this);
}
delete this._layers[id];
if (this._loaded) {
this.fire('layerremove', {layer: layer});
}
if (this._zoomBoundLayers[id]) {
delete this._zoomBoundLayers[id];
this._updateZoomLevels();
}
// TODO looks ugly, refactor
if (this.options.zoomAnimation && L.TileLayer && (layer instanceof L.TileLayer)) {
this._tileLayersNum--;
this._tileLayersToLoad--;
layer.off('load', this._onTileLayerLoad, this);
}
return this;
},
hasLayer: function (layer) {
if (!layer) { return false; }
return (L.stamp(layer) in this._layers);
},
eachLayer: function (method, context) {
for (var i in this._layers) {
method.call(context, this._layers[i]);
}
return this;
},
invalidateSize: function (options) {
if (!this._loaded) { return this; }
options = L.extend({
animate: false,
pan: true
}, options === true ? {animate: true} : options);
var oldSize = this.getSize();
this._sizeChanged = true;
this._initialCenter = null;
var newSize = this.getSize(),
oldCenter = oldSize.divideBy(2).round(),
newCenter = newSize.divideBy(2).round(),
offset = oldCenter.subtract(newCenter);
if (!offset.x && !offset.y) { return this; }
if (options.animate && options.pan) {
this.panBy(offset);
} else {
if (options.pan) {
this._rawPanBy(offset);
}
this.fire('move');
if (options.debounceMoveend) {
clearTimeout(this._sizeTimer);
this._sizeTimer = setTimeout(L.bind(this.fire, this, 'moveend'), 200);
} else {
this.fire('moveend');
}
}
return this.fire('resize', {
oldSize: oldSize,
newSize: newSize
});
},
// TODO handler.addTo
addHandler: function (name, HandlerClass) {
if (!HandlerClass) { return this; }
var handler = this[name] = new HandlerClass(this);
this._handlers.push(handler);
if (this.options[name]) {
handler.enable();
}
return this;
},
remove: function () {
if (this._loaded) {
this.fire('unload');
}
this._initEvents('off');
try {
// throws error in IE6-8
delete this._container._leaflet;
} catch (e) {
this._container._leaflet = undefined;
}
this._clearPanes();
if (this._clearControlPos) {
this._clearControlPos();
}
this._clearHandlers();
return this;
},
// public methods for getting map state
getCenter: function () { // (Boolean) -> LatLng
this._checkIfLoaded();
if (this._initialCenter && !this._moved()) {
return this._initialCenter;
}
return this.layerPointToLatLng(this._getCenterLayerPoint());
},
getZoom: function () {
return this._zoom;
},
getBounds: function () {
var bounds = this.getPixelBounds(),
sw = this.unproject(bounds.getBottomLeft()),
ne = this.unproject(bounds.getTopRight());
return new L.LatLngBounds(sw, ne);
},
getMinZoom: function () {
return this.options.minZoom === undefined ?
(this._layersMinZoom === undefined ? 0 : this._layersMinZoom) :
this.options.minZoom;
},
getMaxZoom: function () {
return this.options.maxZoom === undefined ?
(this._layersMaxZoom === undefined ? Infinity : this._layersMaxZoom) :
this.options.maxZoom;
},
getBoundsZoom: function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number
bounds = L.latLngBounds(bounds);
var zoom = this.getMinZoom() - (inside ? 1 : 0),
maxZoom = this.getMaxZoom(),
size = this.getSize(),
nw = bounds.getNorthWest(),
se = bounds.getSouthEast(),
zoomNotFound = true,
boundsSize;
padding = L.point(padding || [0, 0]);
do {
zoom++;
boundsSize = this.project(se, zoom).subtract(this.project(nw, zoom)).add(padding);
zoomNotFound = !inside ? size.contains(boundsSize) : boundsSize.x < size.x || boundsSize.y < size.y;
} while (zoomNotFound && zoom <= maxZoom);
if (zoomNotFound && inside) {
return null;
}
return inside ? zoom : zoom - 1;
},
getSize: function () {
if (!this._size || this._sizeChanged) {
this._size = new L.Point(
this._container.clientWidth,
this._container.clientHeight);
this._sizeChanged = false;
}
return this._size.clone();
},
getPixelBounds: function () {
var topLeftPoint = this._getTopLeftPoint();
return new L.Bounds(topLeftPoint, topLeftPoint.add(this.getSize()));
},
getPixelOrigin: function () {
this._checkIfLoaded();
return this._initialTopLeftPoint;
},
getPanes: function () {
return this._panes;
},
getContainer: function () {
return this._container;
},
// TODO replace with universal implementation after refactoring projections
getZoomScale: function (toZoom) {
var crs = this.options.crs;
return crs.scale(toZoom) / crs.scale(this._zoom);
},
getScaleZoom: function (scale) {
return this._zoom + (Math.log(scale) / Math.LN2);
},
// conversion methods
project: function (latlng, zoom) { // (LatLng[, Number]) -> Point
zoom = zoom === undefined ? this._zoom : zoom;
return this.options.crs.latLngToPoint(L.latLng(latlng), zoom);
},
unproject: function (point, zoom) { // (Point[, Number]) -> LatLng
zoom = zoom === undefined ? this._zoom : zoom;
return this.options.crs.pointToLatLng(L.point(point), zoom);
},
layerPointToLatLng: function (point) { // (Point)
var projectedPoint = L.point(point).add(this.getPixelOrigin());
return this.unproject(projectedPoint);
},
latLngToLayerPoint: function (latlng) { // (LatLng)
var projectedPoint = this.project(L.latLng(latlng))._round();
return projectedPoint._subtract(this.getPixelOrigin());
},
containerPointToLayerPoint: function (point) { // (Point)
return L.point(point).subtract(this._getMapPanePos());
},
layerPointToContainerPoint: function (point) { // (Point)
return L.point(point).add(this._getMapPanePos());
},
containerPointToLatLng: function (point) {
var layerPoint = this.containerPointToLayerPoint(L.point(point));
return this.layerPointToLatLng(layerPoint);
},
latLngToContainerPoint: function (latlng) {
return this.layerPointToContainerPoint(this.latLngToLayerPoint(L.latLng(latlng)));
},
mouseEventToContainerPoint: function (e) { // (MouseEvent)
return L.DomEvent.getMousePosition(e, this._container);
},
mouseEventToLayerPoint: function (e) { // (MouseEvent)
return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e));
},
mouseEventToLatLng: function (e) { // (MouseEvent)
return this.layerPointToLatLng(this.mouseEventToLayerPoint(e));
},
// map initialization methods
_initContainer: function (id) {
var container = this._container = L.DomUtil.get(id);
if (!container) {
throw new Error('Map container not found.');
} else if (container._leaflet) {
throw new Error('Map container is already initialized.');
}
container._leaflet = true;
},
_initLayout: function () {
var container = this._container;
L.DomUtil.addClass(container, 'leaflet-container' +
(L.Browser.touch ? ' leaflet-touch' : '') +
(L.Browser.retina ? ' leaflet-retina' : '') +
(L.Browser.ielt9 ? ' leaflet-oldie' : '') +
(this.options.fadeAnimation ? ' leaflet-fade-anim' : ''));
var position = L.DomUtil.getStyle(container, 'position');
if (position !== 'absolute' && position !== 'relative' && position !== 'fixed') {
container.style.position = 'relative';
}
this._initPanes();
if (this._initControlPos) {
this._initControlPos();
}
},
_initPanes: function () {
var panes = this._panes = {};
this._mapPane = panes.mapPane = this._createPane('leaflet-map-pane', this._container);
this._tilePane = panes.tilePane = this._createPane('leaflet-tile-pane', this._mapPane);
panes.objectsPane = this._createPane('leaflet-objects-pane', this._mapPane);
panes.shadowPane = this._createPane('leaflet-shadow-pane');
panes.overlayPane = this._createPane('leaflet-overlay-pane');
panes.markerPane = this._createPane('leaflet-marker-pane');
panes.popupPane = this._createPane('leaflet-popup-pane');
var zoomHide = ' leaflet-zoom-hide';
if (!this.options.markerZoomAnimation) {
L.DomUtil.addClass(panes.markerPane, zoomHide);
L.DomUtil.addClass(panes.shadowPane, zoomHide);
L.DomUtil.addClass(panes.popupPane, zoomHide);
}
},
_createPane: function (className, container) {
return L.DomUtil.create('div', className, container || this._panes.objectsPane);
},
_clearPanes: function () {
this._container.removeChild(this._mapPane);
},
_addLayers: function (layers) {
layers = layers ? (L.Util.isArray(layers) ? layers : [layers]) : [];
for (var i = 0, len = layers.length; i < len; i++) {
this.addLayer(layers[i]);
}
},
// private methods that modify map state
_resetView: function (center, zoom, preserveMapOffset, afterZoomAnim) {
var zoomChanged = (this._zoom !== zoom);
if (!afterZoomAnim) {
this.fire('movestart');
if (zoomChanged) {
this.fire('zoomstart');
}
}
this._zoom = zoom;
this._initialCenter = center;
this._initialTopLeftPoint = this._getNewTopLeftPoint(center);
if (!preserveMapOffset) {
L.DomUtil.setPosition(this._mapPane, new L.Point(0, 0));
} else {
this._initialTopLeftPoint._add(this._getMapPanePos());
}
this._tileLayersToLoad = this._tileLayersNum;
var loading = !this._loaded;
this._loaded = true;
this.fire('viewreset', {hard: !preserveMapOffset});
if (loading) {
this.fire('load');
this.eachLayer(this._layerAdd, this);
}
this.fire('move');
if (zoomChanged || afterZoomAnim) {
this.fire('zoomend');
}
this.fire('moveend', {hard: !preserveMapOffset});
},
_rawPanBy: function (offset) {
L.DomUtil.setPosition(this._mapPane, this._getMapPanePos().subtract(offset));
},
_getZoomSpan: function () {
return this.getMaxZoom() - this.getMinZoom();
},
_updateZoomLevels: function () {
var i,
minZoom = Infinity,
maxZoom = -Infinity,
oldZoomSpan = this._getZoomSpan();
for (i in this._zoomBoundLayers) {
var layer = this._zoomBoundLayers[i];
if (!isNaN(layer.options.minZoom)) {
minZoom = Math.min(minZoom, layer.options.minZoom);
}
if (!isNaN(layer.options.maxZoom)) {
maxZoom = Math.max(maxZoom, layer.options.maxZoom);
}
}
if (i === undefined) { // we have no tilelayers
this._layersMaxZoom = this._layersMinZoom = undefined;
} else {
this._layersMaxZoom = maxZoom;
this._layersMinZoom = minZoom;
}
if (oldZoomSpan !== this._getZoomSpan()) {
this.fire('zoomlevelschange');
}
},
_panInsideMaxBounds: function () {
this.panInsideBounds(this.options.maxBounds);
},
_checkIfLoaded: function () {
if (!this._loaded) {
throw new Error('Set map center and zoom first.');
}
},
// map events
_initEvents: function (onOff) {
if (!L.DomEvent) { return; }
onOff = onOff || 'on';
L.DomEvent[onOff](this._container, 'click', this._onMouseClick, this);
var events = ['dblclick', 'mousedown', 'mouseup', 'mouseenter',
'mouseleave', 'mousemove', 'contextmenu'],
i, len;
for (i = 0, len = events.length; i < len; i++) {
L.DomEvent[onOff](this._container, events[i], this._fireMouseEvent, this);
}
if (this.options.trackResize) {
L.DomEvent[onOff](window, 'resize', this._onResize, this);
}
},
_onResize: function () {
L.Util.cancelAnimFrame(this._resizeRequest);
this._resizeRequest = L.Util.requestAnimFrame(
function () { this.invalidateSize({debounceMoveend: true}); }, this, false, this._container);
},
_onMouseClick: function (e) {
if (!this._loaded || (!e._simulated &&
((this.dragging && this.dragging.moved()) ||
(this.boxZoom && this.boxZoom.moved()))) ||
L.DomEvent._skipped(e)) { return; }
this.fire('preclick');
this._fireMouseEvent(e);
},
_fireMouseEvent: function (e) {
if (!this._loaded || L.DomEvent._skipped(e)) { return; }
var type = e.type;
type = (type === 'mouseenter' ? 'mouseover' : (type === 'mouseleave' ? 'mouseout' : type));
if (!this.hasEventListeners(type)) { return; }
if (type === 'contextmenu') {
L.DomEvent.preventDefault(e);
}
var containerPoint = this.mouseEventToContainerPoint(e),
layerPoint = this.containerPointToLayerPoint(containerPoint),
latlng = this.layerPointToLatLng(layerPoint);
this.fire(type, {
latlng: latlng,
layerPoint: layerPoint,
containerPoint: containerPoint,
originalEvent: e
});
},
_onTileLayerLoad: function () {
this._tileLayersToLoad--;
if (this._tileLayersNum && !this._tileLayersToLoad) {
this.fire('tilelayersload');
}
},
_clearHandlers: function () {
for (var i = 0, len = this._handlers.length; i < len; i++) {
this._handlers[i].disable();
}
},
whenReady: function (callback, context) {
if (this._loaded) {
callback.call(context || this, this);
} else {
this.on('load', callback, context);
}
return this;
},
_layerAdd: function (layer) {
layer.onAdd(this);
this.fire('layeradd', {layer: layer});
},
// private methods for getting map state
_getMapPanePos: function () {
return L.DomUtil.getPosition(this._mapPane);
},
_moved: function () {
var pos = this._getMapPanePos();
return pos && !pos.equals([0, 0]);
},
_getTopLeftPoint: function () {
return this.getPixelOrigin().subtract(this._getMapPanePos());
},
_getNewTopLeftPoint: function (center, zoom) {
var viewHalf = this.getSize()._divideBy(2);
// TODO round on display, not calculation to increase precision?
return this.project(center, zoom)._subtract(viewHalf)._round();
},
_latLngToNewLayerPoint: function (latlng, newZoom, newCenter) {
var topLeft = this._getNewTopLeftPoint(newCenter, newZoom).add(this._getMapPanePos());
return this.project(latlng, newZoom)._subtract(topLeft);
},
// layer point of the current center
_getCenterLayerPoint: function () {
return this.containerPointToLayerPoint(this.getSize()._divideBy(2));
},
// offset of the specified place to the current center in pixels
_getCenterOffset: function (latlng) {
return this.latLngToLayerPoint(latlng).subtract(this._getCenterLayerPoint());
},
// adjust center for view to get inside bounds
_limitCenter: function (center, zoom, bounds) {
if (!bounds) { return center; }
var centerPoint = this.project(center, zoom),
viewHalf = this.getSize().divideBy(2),
viewBounds = new L.Bounds(centerPoint.subtract(viewHalf), centerPoint.add(viewHalf)),
offset = this._getBoundsOffset(viewBounds, bounds, zoom);
return this.unproject(centerPoint.add(offset), zoom);
},
// adjust offset for view to get inside bounds
_limitOffset: function (offset, bounds) {
if (!bounds) { return offset; }
var viewBounds = this.getPixelBounds(),
newBounds = new L.Bounds(viewBounds.min.add(offset), viewBounds.max.add(offset));
return offset.add(this._getBoundsOffset(newBounds, bounds));
},
// returns offset needed for pxBounds to get inside maxBounds at a specified zoom
_getBoundsOffset: function (pxBounds, maxBounds, zoom) {
var nwOffset = this.project(maxBounds.getNorthWest(), zoom).subtract(pxBounds.min),
seOffset = this.project(maxBounds.getSouthEast(), zoom).subtract(pxBounds.max),
dx = this._rebound(nwOffset.x, -seOffset.x),
dy = this._rebound(nwOffset.y, -seOffset.y);
return new L.Point(dx, dy);
},
_rebound: function (left, right) {
return left + right > 0 ?
Math.round(left - right) / 2 :
Math.max(0, Math.ceil(left)) - Math.max(0, Math.floor(right));
},
_limitZoom: function (zoom) {
var min = this.getMinZoom(),
max = this.getMaxZoom();
return Math.max(min, Math.min(max, zoom));
}
});
L.map = function (id, options) {
return new L.Map(id, options);
};
/*
* Mercator projection that takes into account that the Earth is not a perfect sphere.
* Less popular than spherical mercator; used by projections like EPSG:3395.
*/
L.Projection.Mercator = {
MAX_LATITUDE: 85.0840591556,
R_MINOR: 6356752.314245179,
R_MAJOR: 6378137,
project: function (latlng) { // (LatLng) -> Point
var d = L.LatLng.DEG_TO_RAD,
max = this.MAX_LATITUDE,
lat = Math.max(Math.min(max, latlng.lat), -max),
r = this.R_MAJOR,
r2 = this.R_MINOR,
x = latlng.lng * d * r,
y = lat * d,
tmp = r2 / r,
eccent = Math.sqrt(1.0 - tmp * tmp),
con = eccent * Math.sin(y);
con = Math.pow((1 - con) / (1 + con), eccent * 0.5);
var ts = Math.tan(0.5 * ((Math.PI * 0.5) - y)) / con;
y = -r * Math.log(ts);
return new L.Point(x, y);
},
unproject: function (point) { // (Point, Boolean) -> LatLng
var d = L.LatLng.RAD_TO_DEG,
r = this.R_MAJOR,
r2 = this.R_MINOR,
lng = point.x * d / r,
tmp = r2 / r,
eccent = Math.sqrt(1 - (tmp * tmp)),
ts = Math.exp(- point.y / r),
phi = (Math.PI / 2) - 2 * Math.atan(ts),
numIter = 15,
tol = 1e-7,
i = numIter,
dphi = 0.1,
con;
while ((Math.abs(dphi) > tol) && (--i > 0)) {
con = eccent * Math.sin(phi);
dphi = (Math.PI / 2) - 2 * Math.atan(ts *
Math.pow((1.0 - con) / (1.0 + con), 0.5 * eccent)) - phi;
phi += dphi;
}
return new L.LatLng(phi * d, lng);
}
};
L.CRS.EPSG3395 = L.extend({}, L.CRS, {
code: 'EPSG:3395',
projection: L.Projection.Mercator,
transformation: (function () {
var m = L.Projection.Mercator,
r = m.R_MAJOR,
scale = 0.5 / (Math.PI * r);
return new L.Transformation(scale, 0.5, -scale, 0.5);
}())
});
/*
* L.TileLayer is used for standard xyz-numbered tile layers.
*/
L.TileLayer = L.Class.extend({
includes: L.Mixin.Events,
options: {
minZoom: 0,
maxZoom: 18,
tileSize: 256,
subdomains: 'abc',
errorTileUrl: '',
attribution: '',
zoomOffset: 0,
opacity: 1,
/*
maxNativeZoom: null,
zIndex: null,
tms: false,
continuousWorld: false,
noWrap: false,
zoomReverse: false,
detectRetina: false,
reuseTiles: false,
bounds: false,
*/
unloadInvisibleTiles: L.Browser.mobile,
updateWhenIdle: L.Browser.mobile
},
initialize: function (url, options) {
options = L.setOptions(this, options);
// detecting retina displays, adjusting tileSize and zoom levels
if (options.detectRetina && L.Browser.retina && options.maxZoom > 0) {
options.tileSize = Math.floor(options.tileSize / 2);
options.zoomOffset++;
if (options.minZoom > 0) {
options.minZoom--;
}
this.options.maxZoom--;
}
if (options.bounds) {
options.bounds = L.latLngBounds(options.bounds);
}
this._url = url;
var subdomains = this.options.subdomains;
if (typeof subdomains === 'string') {
this.options.subdomains = subdomains.split('');
}
},
onAdd: function (map) {
this._map = map;
this._animated = map._zoomAnimated;
// create a container div for tiles
this._initContainer();
// set up events
map.on({
'viewreset': this._reset,
'moveend': this._update
}, this);
if (this._animated) {
map.on({
'zoomanim': this._animateZoom,
'zoomend': this._endZoomAnim
}, this);
}
if (!this.options.updateWhenIdle) {
this._limitedUpdate = L.Util.limitExecByInterval(this._update, 150, this);
map.on('move', this._limitedUpdate, this);
}
this._reset();
this._update();
},
addTo: function (map) {
map.addLayer(this);
return this;
},
onRemove: function (map) {
this._container.parentNode.removeChild(this._container);
map.off({
'viewreset': this._reset,
'moveend': this._update
}, this);
if (this._animated) {
map.off({
'zoomanim': this._animateZoom,
'zoomend': this._endZoomAnim
}, this);
}
if (!this.options.updateWhenIdle) {
map.off('move', this._limitedUpdate, this);
}
this._container = null;
this._map = null;
},
bringToFront: function () {
var pane = this._map._panes.tilePane;
if (this._container) {
pane.appendChild(this._container);
this._setAutoZIndex(pane, Math.max);
}
return this;
},
bringToBack: function () {
var pane = this._map._panes.tilePane;
if (this._container) {
pane.insertBefore(this._container, pane.firstChild);
this._setAutoZIndex(pane, Math.min);
}
return this;
},
getAttribution: function () {
return this.options.attribution;
},
getContainer: function () {
return this._container;
},
setOpacity: function (opacity) {
this.options.opacity = opacity;
if (this._map) {
this._updateOpacity();
}
return this;
},
setZIndex: function (zIndex) {
this.options.zIndex = zIndex;
this._updateZIndex();
return this;
},
setUrl: function (url, noRedraw) {
this._url = url;
if (!noRedraw) {
this.redraw();
}
return this;
},
redraw: function () {
if (this._map) {
this._reset({hard: true});
this._update();
}
return this;
},
_updateZIndex: function () {
if (this._container && this.options.zIndex !== undefined) {
this._container.style.zIndex = this.options.zIndex;
}
},
_setAutoZIndex: function (pane, compare) {
var layers = pane.children,
edgeZIndex = -compare(Infinity, -Infinity), // -Infinity for max, Infinity for min
zIndex, i, len;
for (i = 0, len = layers.length; i < len; i++) {
if (layers[i] !== this._container) {
zIndex = parseInt(layers[i].style.zIndex, 10);
if (!isNaN(zIndex)) {
edgeZIndex = compare(edgeZIndex, zIndex);
}
}
}
this.options.zIndex = this._container.style.zIndex =
(isFinite(edgeZIndex) ? edgeZIndex : 0) + compare(1, -1);
},
_updateOpacity: function () {
var i,
tiles = this._tiles;
if (L.Browser.ielt9) {
for (i in tiles) {
L.DomUtil.setOpacity(tiles[i], this.options.opacity);
}
} else {
L.DomUtil.setOpacity(this._container, this.options.opacity);
}
},
_initContainer: function () {
var tilePane = this._map._panes.tilePane;
if (!this._container) {
this._container = L.DomUtil.create('div', 'leaflet-layer');
this._updateZIndex();
if (this._animated) {
var className = 'leaflet-tile-container';
this._bgBuffer = L.DomUtil.create('div', className, this._container);
this._tileContainer = L.DomUtil.create('div', className, this._container);
} else {
this._tileContainer = this._container;
}
tilePane.appendChild(this._container);
if (this.options.opacity < 1) {
this._updateOpacity();
}
}
},
_reset: function (e) {
for (var key in this._tiles) {
this.fire('tileunload', {tile: this._tiles[key]});
}
this._tiles = {};
this._tilesToLoad = 0;
if (this.options.reuseTiles) {
this._unusedTiles = [];
}
this._tileContainer.innerHTML = '';
if (this._animated && e && e.hard) {
this._clearBgBuffer();
}
this._initContainer();
},
_getTileSize: function () {
var map = this._map,
zoom = map.getZoom() + this.options.zoomOffset,
zoomN = this.options.maxNativeZoom,
tileSize = this.options.tileSize;
if (zoomN && zoom > zoomN) {
tileSize = Math.round(map.getZoomScale(zoom) / map.getZoomScale(zoomN) * tileSize);
}
return tileSize;
},
_update: function () {
if (!this._map) { return; }
var map = this._map,
bounds = map.getPixelBounds(),
zoom = map.getZoom(),
tileSize = this._getTileSize();
if (zoom > this.options.maxZoom || zoom < this.options.minZoom) {
return;
}
var tileBounds = L.bounds(
bounds.min.divideBy(tileSize)._floor(),
bounds.max.divideBy(tileSize)._floor());
this._addTilesFromCenterOut(tileBounds);
if (this.options.unloadInvisibleTiles || this.options.reuseTiles) {
this._removeOtherTiles(tileBounds);
}
},
_addTilesFromCenterOut: function (bounds) {
var queue = [],
center = bounds.getCenter();
var j, i, point;
for (j = bounds.min.y; j <= bounds.max.y; j++) {
for (i = bounds.min.x; i <= bounds.max.x; i++) {
point = new L.Point(i, j);
if (this._tileShouldBeLoaded(point)) {
queue.push(point);
}
}
}
var tilesToLoad = queue.length;
if (tilesToLoad === 0) { return; }
// load tiles in order of their distance to center
queue.sort(function (a, b) {
return a.distanceTo(center) - b.distanceTo(center);
});
var fragment = document.createDocumentFragment();
// if its the first batch of tiles to load
if (!this._tilesToLoad) {
this.fire('loading');
}
this._tilesToLoad += tilesToLoad;
for (i = 0; i < tilesToLoad; i++) {
this._addTile(queue[i], fragment);
}
this._tileContainer.appendChild(fragment);
},
_tileShouldBeLoaded: function (tilePoint) {
if ((tilePoint.x + ':' + tilePoint.y) in this._tiles) {
return false; // already loaded
}
var options = this.options;
if (!options.continuousWorld) {
var limit = this._getWrapTileNum();
// don't load if exceeds world bounds
if ((options.noWrap && (tilePoint.x < 0 || tilePoint.x >= limit.x)) ||
tilePoint.y < 0 || tilePoint.y >= limit.y) { return false; }
}
if (options.bounds) {
var tileSize = this._getTileSize(),
nwPoint = tilePoint.multiplyBy(tileSize),
sePoint = nwPoint.add([tileSize, tileSize]),
nw = this._map.unproject(nwPoint),
se = this._map.unproject(sePoint);
// TODO temporary hack, will be removed after refactoring projections
// https://github.com/Leaflet/Leaflet/issues/1618
if (!options.continuousWorld && !options.noWrap) {
nw = nw.wrap();
se = se.wrap();
}
if (!options.bounds.intersects([nw, se])) { return false; }
}
return true;
},
_removeOtherTiles: function (bounds) {
var kArr, x, y, key;
for (key in this._tiles) {
kArr = key.split(':');
x = parseInt(kArr[0], 10);
y = parseInt(kArr[1], 10);
// remove tile if it's out of bounds
if (x < bounds.min.x || x > bounds.max.x || y < bounds.min.y || y > bounds.max.y) {
this._removeTile(key);
}
}
},
_removeTile: function (key) {
var tile = this._tiles[key];
this.fire('tileunload', {tile: tile, url: tile.src});
if (this.options.reuseTiles) {
L.DomUtil.removeClass(tile, 'leaflet-tile-loaded');
this._unusedTiles.push(tile);
} else if (tile.parentNode === this._tileContainer) {
this._tileContainer.removeChild(tile);
}
// for https://github.com/CloudMade/Leaflet/issues/137
if (!L.Browser.android) {
tile.onload = null;
tile.src = L.Util.emptyImageUrl;
}
delete this._tiles[key];
},
_addTile: function (tilePoint, container) {
var tilePos = this._getTilePos(tilePoint);
// get unused tile - or create a new tile
var tile = this._getTile();
/*
Chrome 20 layouts much faster with top/left (verify with timeline, frames)
Android 4 browser has display issues with top/left and requires transform instead
(other browsers don't currently care) - see debug/hacks/jitter.html for an example
*/
L.DomUtil.setPosition(tile, tilePos, L.Browser.chrome);
this._tiles[tilePoint.x + ':' + tilePoint.y] = tile;
this._loadTile(tile, tilePoint);
if (tile.parentNode !== this._tileContainer) {
container.appendChild(tile);
}
},
_getZoomForUrl: function () {
var options = this.options,
zoom = this._map.getZoom();
if (options.zoomReverse) {
zoom = options.maxZoom - zoom;
}
zoom += options.zoomOffset;
return options.maxNativeZoom ? Math.min(zoom, options.maxNativeZoom) : zoom;
},
_getTilePos: function (tilePoint) {
var origin = this._map.getPixelOrigin(),
tileSize = this._getTileSize();
return tilePoint.multiplyBy(tileSize).subtract(origin);
},
// image-specific code (override to implement e.g. Canvas or SVG tile layer)
getTileUrl: function (tilePoint) {
return L.Util.template(this._url, L.extend({
s: this._getSubdomain(tilePoint),
z: tilePoint.z,
x: tilePoint.x,
y: tilePoint.y
}, this.options));
},
_getWrapTileNum: function () {
var crs = this._map.options.crs,
size = crs.getSize(this._map.getZoom());
return size.divideBy(this._getTileSize())._floor();
},
_adjustTilePoint: function (tilePoint) {
var limit = this._getWrapTileNum();
// wrap tile coordinates
if (!this.options.continuousWorld && !this.options.noWrap) {
tilePoint.x = ((tilePoint.x % limit.x) + limit.x) % limit.x;
}
if (this.options.tms) {
tilePoint.y = limit.y - tilePoint.y - 1;
}
tilePoint.z = this._getZoomForUrl();
},
_getSubdomain: function (tilePoint) {
var index = Math.abs(tilePoint.x + tilePoint.y) % this.options.subdomains.length;
return this.options.subdomains[index];
},
_getTile: function () {
if (this.options.reuseTiles && this._unusedTiles.length > 0) {
var tile = this._unusedTiles.pop();
this._resetTile(tile);
return tile;
}
return this._createTile();
},
// Override if data stored on a tile needs to be cleaned up before reuse
_resetTile: function (/*tile*/) {},
_createTile: function () {
var tile = L.DomUtil.create('img', 'leaflet-tile');
tile.style.width = tile.style.height = this._getTileSize() + 'px';
tile.galleryimg = 'no';
tile.onselectstart = tile.onmousemove = L.Util.falseFn;
if (L.Browser.ielt9 && this.options.opacity !== undefined) {
L.DomUtil.setOpacity(tile, this.options.opacity);
}
// without this hack, tiles disappear after zoom on Chrome for Android
// https://github.com/Leaflet/Leaflet/issues/2078
if (L.Browser.mobileWebkit3d) {
tile.style.WebkitBackfaceVisibility = 'hidden';
}
return tile;
},
_loadTile: function (tile, tilePoint) {
tile._layer = this;
tile.onload = this._tileOnLoad;
tile.onerror = this._tileOnError;
this._adjustTilePoint(tilePoint);
tile.src = this.getTileUrl(tilePoint);
this.fire('tileloadstart', {
tile: tile,
url: tile.src
});
},
_tileLoaded: function () {
this._tilesToLoad--;
if (this._animated) {
L.DomUtil.addClass(this._tileContainer, 'leaflet-zoom-animated');
}
if (!this._tilesToLoad) {
this.fire('load');
if (this._animated) {
// clear scaled tiles after all new tiles are loaded (for performance)
clearTimeout(this._clearBgBufferTimer);
this._clearBgBufferTimer = setTimeout(L.bind(this._clearBgBuffer, this), 500);
}
}
},
_tileOnLoad: function () {
var layer = this._layer;
//Only if we are loading an actual image
if (this.src !== L.Util.emptyImageUrl) {
L.DomUtil.addClass(this, 'leaflet-tile-loaded');
layer.fire('tileload', {
tile: this,
url: this.src
});
}
layer._tileLoaded();
},
_tileOnError: function () {
var layer = this._layer;
layer.fire('tileerror', {
tile: this,
url: this.src
});
var newUrl = layer.options.errorTileUrl;
if (newUrl) {
this.src = newUrl;
}
layer._tileLoaded();
}
});
L.tileLayer = function (url, options) {
return new L.TileLayer(url, options);
};
/*
* L.TileLayer.WMS is used for putting WMS tile layers on the map.
*/
L.TileLayer.WMS = L.TileLayer.extend({
defaultWmsParams: {
service: 'WMS',
request: 'GetMap',
version: '1.1.1',
layers: '',
styles: '',
format: 'image/jpeg',
transparent: false
},
initialize: function (url, options) { // (String, Object)
this._url = url;
var wmsParams = L.extend({}, this.defaultWmsParams),
tileSize = options.tileSize || this.options.tileSize;
if (options.detectRetina && L.Browser.retina) {
wmsParams.width = wmsParams.height = tileSize * 2;
} else {
wmsParams.width = wmsParams.height = tileSize;
}
for (var i in options) {
// all keys that are not TileLayer options go to WMS params
if (!this.options.hasOwnProperty(i) && i !== 'crs') {
wmsParams[i] = options[i];
}
}
this.wmsParams = wmsParams;
L.setOptions(this, options);
},
onAdd: function (map) {
this._crs = this.options.crs || map.options.crs;
this._wmsVersion = parseFloat(this.wmsParams.version);
var projectionKey = this._wmsVersion >= 1.3 ? 'crs' : 'srs';
this.wmsParams[projectionKey] = this._crs.code;
L.TileLayer.prototype.onAdd.call(this, map);
},
getTileUrl: function (tilePoint) { // (Point, Number) -> String
var map = this._map,
tileSize = this.options.tileSize,
nwPoint = tilePoint.multiplyBy(tileSize),
sePoint = nwPoint.add([tileSize, tileSize]),
nw = this._crs.project(map.unproject(nwPoint, tilePoint.z)),
se = this._crs.project(map.unproject(sePoint, tilePoint.z)),
bbox = this._wmsVersion >= 1.3 && this._crs === L.CRS.EPSG4326 ?
[se.y, nw.x, nw.y, se.x].join(',') :
[nw.x, se.y, se.x, nw.y].join(','),
url = L.Util.template(this._url, {s: this._getSubdomain(tilePoint)});
return url + L.Util.getParamString(this.wmsParams, url, true) + '&BBOX=' + bbox;
},
setParams: function (params, noRedraw) {
L.extend(this.wmsParams, params);
if (!noRedraw) {
this.redraw();
}
return this;
}
});
L.tileLayer.wms = function (url, options) {
return new L.TileLayer.WMS(url, options);
};
/*
* L.TileLayer.Canvas is a class that you can use as a base for creating
* dynamically drawn Canvas-based tile layers.
*/
L.TileLayer.Canvas = L.TileLayer.extend({
options: {
async: false
},
initialize: function (options) {
L.setOptions(this, options);
},
redraw: function () {
if (this._map) {
this._reset({hard: true});
this._update();
}
for (var i in this._tiles) {
this._redrawTile(this._tiles[i]);
}
return this;
},
_redrawTile: function (tile) {
this.drawTile(tile, tile._tilePoint, this._map._zoom);
},
_createTile: function () {
var tile = L.DomUtil.create('canvas', 'leaflet-tile');
tile.width = tile.height = this.options.tileSize;
tile.onselectstart = tile.onmousemove = L.Util.falseFn;
return tile;
},
_loadTile: function (tile, tilePoint) {
tile._layer = this;
tile._tilePoint = tilePoint;
this._redrawTile(tile);
if (!this.options.async) {
this.tileDrawn(tile);
}
},
drawTile: function (/*tile, tilePoint*/) {
// override with rendering code
},
tileDrawn: function (tile) {
this._tileOnLoad.call(tile);
}
});
L.tileLayer.canvas = function (options) {
return new L.TileLayer.Canvas(options);
};
/*
* L.ImageOverlay is used to overlay images over the map (to specific geographical bounds).
*/
L.ImageOverlay = L.Class.extend({
includes: L.Mixin.Events,
options: {
opacity: 1
},
initialize: function (url, bounds, options) { // (String, LatLngBounds, Object)
this._url = url;
this._bounds = L.latLngBounds(bounds);
L.setOptions(this, options);
},
onAdd: function (map) {
this._map = map;
if (!this._image) {
this._initImage();
}
map._panes.overlayPane.appendChild(this._image);
map.on('viewreset', this._reset, this);
if (map.options.zoomAnimation && L.Browser.any3d) {
map.on('zoomanim', this._animateZoom, this);
}
this._reset();
},
onRemove: function (map) {
map.getPanes().overlayPane.removeChild(this._image);
map.off('viewreset', this._reset, this);
if (map.options.zoomAnimation) {
map.off('zoomanim', this._animateZoom, this);
}
},
addTo: function (map) {
map.addLayer(this);
return this;
},
setOpacity: function (opacity) {
this.options.opacity = opacity;
this._updateOpacity();
return this;
},
// TODO remove bringToFront/bringToBack duplication from TileLayer/Path
bringToFront: function () {
if (this._image) {
this._map._panes.overlayPane.appendChild(this._image);
}
return this;
},
bringToBack: function () {
var pane = this._map._panes.overlayPane;
if (this._image) {
pane.insertBefore(this._image, pane.firstChild);
}
return this;
},
setUrl: function (url) {
this._url = url;
this._image.src = this._url;
},
getAttribution: function () {
return this.options.attribution;
},
_initImage: function () {
this._image = L.DomUtil.create('img', 'leaflet-image-layer');
if (this._map.options.zoomAnimation && L.Browser.any3d) {
L.DomUtil.addClass(this._image, 'leaflet-zoom-animated');
} else {
L.DomUtil.addClass(this._image, 'leaflet-zoom-hide');
}
this._updateOpacity();
//TODO createImage util method to remove duplication
L.extend(this._image, {
galleryimg: 'no',
onselectstart: L.Util.falseFn,
onmousemove: L.Util.falseFn,
onload: L.bind(this._onImageLoad, this),
src: this._url
});
},
_animateZoom: function (e) {
var map = this._map,
image = this._image,
scale = map.getZoomScale(e.zoom),
nw = this._bounds.getNorthWest(),
se = this._bounds.getSouthEast(),
topLeft = map._latLngToNewLayerPoint(nw, e.zoom, e.center),
size = map._latLngToNewLayerPoint(se, e.zoom, e.center)._subtract(topLeft),
origin = topLeft._add(size._multiplyBy((1 / 2) * (1 - 1 / scale)));
image.style[L.DomUtil.TRANSFORM] =
L.DomUtil.getTranslateString(origin) + ' scale(' + scale + ') ';
},
_reset: function () {
var image = this._image,
topLeft = this._map.latLngToLayerPoint(this._bounds.getNorthWest()),
size = this._map.latLngToLayerPoint(this._bounds.getSouthEast())._subtract(topLeft);
L.DomUtil.setPosition(image, topLeft);
image.style.width = size.x + 'px';
image.style.height = size.y + 'px';
},
_onImageLoad: function () {
this.fire('load');
},
_updateOpacity: function () {
L.DomUtil.setOpacity(this._image, this.options.opacity);
}
});
L.imageOverlay = function (url, bounds, options) {
return new L.ImageOverlay(url, bounds, options);
};
/*
* L.Icon is an image-based icon class that you can use with L.Marker for custom markers.
*/
L.Icon = L.Class.extend({
options: {
/*
iconUrl: (String) (required)
iconRetinaUrl: (String) (optional, used for retina devices if detected)
iconSize: (Point) (can be set through CSS)
iconAnchor: (Point) (centered by default, can be set in CSS with negative margins)
popupAnchor: (Point) (if not specified, popup opens in the anchor point)
shadowUrl: (String) (no shadow by default)
shadowRetinaUrl: (String) (optional, used for retina devices if detected)
shadowSize: (Point)
shadowAnchor: (Point)
*/
className: ''
},
initialize: function (options) {
L.setOptions(this, options);
},
createIcon: function (oldIcon) {
return this._createIcon('icon', oldIcon);
},
createShadow: function (oldIcon) {
return this._createIcon('shadow', oldIcon);
},
_createIcon: function (name, oldIcon) {
var src = this._getIconUrl(name);
if (!src) {
if (name === 'icon') {
throw new Error('iconUrl not set in Icon options (see the docs).');
}
return null;
}
var img;
if (!oldIcon || oldIcon.tagName !== 'IMG') {
img = this._createImg(src);
} else {
img = this._createImg(src, oldIcon);
}
this._setIconStyles(img, name);
return img;
},
_setIconStyles: function (img, name) {
var options = this.options,
size = L.point(options[name + 'Size']),
anchor;
if (name === 'shadow') {
anchor = L.point(options.shadowAnchor || options.iconAnchor);
} else {
anchor = L.point(options.iconAnchor);
}
if (!anchor && size) {
anchor = size.divideBy(2, true);
}
img.className = 'leaflet-marker-' + name + ' ' + options.className;
if (anchor) {
img.style.marginLeft = (-anchor.x) + 'px';
img.style.marginTop = (-anchor.y) + 'px';
}
if (size) {
img.style.width = size.x + 'px';
img.style.height = size.y + 'px';
}
},
_createImg: function (src, el) {
el = el || document.createElement('img');
el.src = src;
return el;
},
_getIconUrl: function (name) {
if (L.Browser.retina && this.options[name + 'RetinaUrl']) {
return this.options[name + 'RetinaUrl'];
}
return this.options[name + 'Url'];
}
});
L.icon = function (options) {
return new L.Icon(options);
};
/*
* L.Icon.Default is the blue marker icon used by default in Leaflet.
*/
L.Icon.Default = L.Icon.extend({
options: {
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
},
_getIconUrl: function (name) {
var key = name + 'Url';
if (this.options[key]) {
return this.options[key];
}
if (L.Browser.retina && name === 'icon') {
return "/assets/marker-icon-2x-454dc479e82b487529b6b93d6a9b29ac69ca7b4f5a9d5fdf8e01871f6d216113.png";
}
if (name == 'shadow') {
return "/assets/marker-shadow-4f340d2d61746333dffe056e074ce1704ae4e47fec5a7de98322fbdbcfcb2b6d.png";
} else {
return "/assets/marker-icon-915e83a6fc798c599e5c9e3f759d6bc065d65151019acd0410d1f4731bcaaf72.png";
}
}
});
L.Icon.Default.imagePath = (function () {
var scripts = document.getElementsByTagName('script'),
leafletRe = /[\/^]leaflet[\-\._]?([\w\-\._]*)\.js\??/;
var i, len, src, matches, path;
for (i = 0, len = scripts.length; i < len; i++) {
src = scripts[i].src;
matches = src.match(leafletRe);
if (matches) {
path = src.split(leafletRe)[0];
return (path ? path + '/' : '');
}
}
}());
/*
* L.Marker is used to display clickable/draggable icons on the map.
*/
L.Marker = L.Class.extend({
includes: L.Mixin.Events,
options: {
icon: new L.Icon.Default(),
title: '',
alt: '',
clickable: true,
draggable: false,
keyboard: true,
zIndexOffset: 0,
opacity: 1,
riseOnHover: false,
riseOffset: 250
},
initialize: function (latlng, options) {
L.setOptions(this, options);
this._latlng = L.latLng(latlng);
},
onAdd: function (map) {
this._map = map;
map.on('viewreset', this.update, this);
this._initIcon();
this.update();
this.fire('add');
if (map.options.zoomAnimation && map.options.markerZoomAnimation) {
map.on('zoomanim', this._animateZoom, this);
}
},
addTo: function (map) {
map.addLayer(this);
return this;
},
onRemove: function (map) {
if (this.dragging) {
this.dragging.disable();
}
this._removeIcon();
this._removeShadow();
this.fire('remove');
map.off({
'viewreset': this.update,
'zoomanim': this._animateZoom
}, this);
this._map = null;
},
getLatLng: function () {
return this._latlng;
},
setLatLng: function (latlng) {
this._latlng = L.latLng(latlng);
this.update();
return this.fire('move', { latlng: this._latlng });
},
setZIndexOffset: function (offset) {
this.options.zIndexOffset = offset;
this.update();
return this;
},
setIcon: function (icon) {
this.options.icon = icon;
if (this._map) {
this._initIcon();
this.update();
}
if (this._popup) {
this.bindPopup(this._popup);
}
return this;
},
update: function () {
if (this._icon) {
this._setPos(this._map.latLngToLayerPoint(this._latlng).round());
}
return this;
},
_initIcon: function () {
var options = this.options,
map = this._map,
animation = (map.options.zoomAnimation && map.options.markerZoomAnimation),
classToAdd = animation ? 'leaflet-zoom-animated' : 'leaflet-zoom-hide';
var icon = options.icon.createIcon(this._icon),
addIcon = false;
// if we're not reusing the icon, remove the old one and init new one
if (icon !== this._icon) {
if (this._icon) {
this._removeIcon();
}
addIcon = true;
if (options.title) {
icon.title = options.title;
}
if (options.alt) {
icon.alt = options.alt;
}
}
L.DomUtil.addClass(icon, classToAdd);
if (options.keyboard) {
icon.tabIndex = '0';
}
this._icon = icon;
this._initInteraction();
if (options.riseOnHover) {
L.DomEvent
.on(icon, 'mouseover', this._bringToFront, this)
.on(icon, 'mouseout', this._resetZIndex, this);
}
var newShadow = options.icon.createShadow(this._shadow),
addShadow = false;
if (newShadow !== this._shadow) {
this._removeShadow();
addShadow = true;
}
if (newShadow) {
L.DomUtil.addClass(newShadow, classToAdd);
}
this._shadow = newShadow;
if (options.opacity < 1) {
this._updateOpacity();
}
var panes = this._map._panes;
if (addIcon) {
panes.markerPane.appendChild(this._icon);
}
if (newShadow && addShadow) {
panes.shadowPane.appendChild(this._shadow);
}
},
_removeIcon: function () {
if (this.options.riseOnHover) {
L.DomEvent
.off(this._icon, 'mouseover', this._bringToFront)
.off(this._icon, 'mouseout', this._resetZIndex);
}
this._map._panes.markerPane.removeChild(this._icon);
this._icon = null;
},
_removeShadow: function () {
if (this._shadow) {
this._map._panes.shadowPane.removeChild(this._shadow);
}
this._shadow = null;
},
_setPos: function (pos) {
L.DomUtil.setPosition(this._icon, pos);
if (this._shadow) {
L.DomUtil.setPosition(this._shadow, pos);
}
this._zIndex = pos.y + this.options.zIndexOffset;
this._resetZIndex();
},
_updateZIndex: function (offset) {
this._icon.style.zIndex = this._zIndex + offset;
},
_animateZoom: function (opt) {
var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center).round();
this._setPos(pos);
},
_initInteraction: function () {
if (!this.options.clickable) { return; }
// TODO refactor into something shared with Map/Path/etc. to DRY it up
var icon = this._icon,
events = ['dblclick', 'mousedown', 'mouseover', 'mouseout', 'contextmenu'];
L.DomUtil.addClass(icon, 'leaflet-clickable');
L.DomEvent.on(icon, 'click', this._onMouseClick, this);
L.DomEvent.on(icon, 'keypress', this._onKeyPress, this);
for (var i = 0; i < events.length; i++) {
L.DomEvent.on(icon, events[i], this._fireMouseEvent, this);
}
if (L.Handler.MarkerDrag) {
this.dragging = new L.Handler.MarkerDrag(this);
if (this.options.draggable) {
this.dragging.enable();
}
}
},
_onMouseClick: function (e) {
var wasDragged = this.dragging && this.dragging.moved();
if (this.hasEventListeners(e.type) || wasDragged) {
L.DomEvent.stopPropagation(e);
}
if (wasDragged) { return; }
if ((!this.dragging || !this.dragging._enabled) && this._map.dragging && this._map.dragging.moved()) { return; }
this.fire(e.type, {
originalEvent: e,
latlng: this._latlng
});
},
_onKeyPress: function (e) {
if (e.keyCode === 13) {
this.fire('click', {
originalEvent: e,
latlng: this._latlng
});
}
},
_fireMouseEvent: function (e) {
this.fire(e.type, {
originalEvent: e,
latlng: this._latlng
});
// TODO proper custom event propagation
// this line will always be called if marker is in a FeatureGroup
if (e.type === 'contextmenu' && this.hasEventListeners(e.type)) {
L.DomEvent.preventDefault(e);
}
if (e.type !== 'mousedown') {
L.DomEvent.stopPropagation(e);
} else {
L.DomEvent.preventDefault(e);
}
},
setOpacity: function (opacity) {
this.options.opacity = opacity;
if (this._map) {
this._updateOpacity();
}
return this;
},
_updateOpacity: function () {
L.DomUtil.setOpacity(this._icon, this.options.opacity);
if (this._shadow) {
L.DomUtil.setOpacity(this._shadow, this.options.opacity);
}
},
_bringToFront: function () {
this._updateZIndex(this.options.riseOffset);
},
_resetZIndex: function () {
this._updateZIndex(0);
}
});
L.marker = function (latlng, options) {
return new L.Marker(latlng, options);
};
/*
* L.DivIcon is a lightweight HTML-based icon class (as opposed to the image-based L.Icon)
* to use with L.Marker.
*/
L.DivIcon = L.Icon.extend({
options: {
iconSize: [12, 12], // also can be set through CSS
/*
iconAnchor: (Point)
popupAnchor: (Point)
html: (String)
bgPos: (Point)
*/
className: 'leaflet-div-icon',
html: false
},
createIcon: function (oldIcon) {
var div = (oldIcon && oldIcon.tagName === 'DIV') ? oldIcon : document.createElement('div'),
options = this.options;
if (options.html !== false) {
div.innerHTML = options.html;
} else {
div.innerHTML = '';
}
if (options.bgPos) {
div.style.backgroundPosition =
(-options.bgPos.x) + 'px ' + (-options.bgPos.y) + 'px';
}
this._setIconStyles(div, 'icon');
return div;
},
createShadow: function () {
return null;
}
});
L.divIcon = function (options) {
return new L.DivIcon(options);
};
/*
* L.Popup is used for displaying popups on the map.
*/
L.Map.mergeOptions({
closePopupOnClick: true
});
L.Popup = L.Class.extend({
includes: L.Mixin.Events,
options: {
minWidth: 50,
maxWidth: 300,
// maxHeight: null,
autoPan: true,
closeButton: true,
offset: [0, 7],
autoPanPadding: [5, 5],
// autoPanPaddingTopLeft: null,
// autoPanPaddingBottomRight: null,
keepInView: false,
className: '',
zoomAnimation: true
},
initialize: function (options, source) {
L.setOptions(this, options);
this._source = source;
this._animated = L.Browser.any3d && this.options.zoomAnimation;
this._isOpen = false;
},
onAdd: function (map) {
this._map = map;
if (!this._container) {
this._initLayout();
}
var animFade = map.options.fadeAnimation;
if (animFade) {
L.DomUtil.setOpacity(this._container, 0);
}
map._panes.popupPane.appendChild(this._container);
map.on(this._getEvents(), this);
this.update();
if (animFade) {
L.DomUtil.setOpacity(this._container, 1);
}
this.fire('open');
map.fire('popupopen', {popup: this});
if (this._source) {
this._source.fire('popupopen', {popup: this});
}
},
addTo: function (map) {
map.addLayer(this);
return this;
},
openOn: function (map) {
map.openPopup(this);
return this;
},
onRemove: function (map) {
map._panes.popupPane.removeChild(this._container);
L.Util.falseFn(this._container.offsetWidth); // force reflow
map.off(this._getEvents(), this);
if (map.options.fadeAnimation) {
L.DomUtil.setOpacity(this._container, 0);
}
this._map = null;
this.fire('close');
map.fire('popupclose', {popup: this});
if (this._source) {
this._source.fire('popupclose', {popup: this});
}
},
getLatLng: function () {
return this._latlng;
},
setLatLng: function (latlng) {
this._latlng = L.latLng(latlng);
if (this._map) {
this._updatePosition();
this._adjustPan();
}
return this;
},
getContent: function () {
return this._content;
},
setContent: function (content) {
this._content = content;
this.update();
return this;
},
update: function () {
if (!this._map) { return; }
this._container.style.visibility = 'hidden';
this._updateContent();
this._updateLayout();
this._updatePosition();
this._container.style.visibility = '';
this._adjustPan();
},
_getEvents: function () {
var events = {
viewreset: this._updatePosition
};
if (this._animated) {
events.zoomanim = this._zoomAnimation;
}
if ('closeOnClick' in this.options ? this.options.closeOnClick : this._map.options.closePopupOnClick) {
events.preclick = this._close;
}
if (this.options.keepInView) {
events.moveend = this._adjustPan;
}
return events;
},
_close: function () {
if (this._map) {
this._map.closePopup(this);
}
},
_initLayout: function () {
var prefix = 'leaflet-popup',
containerClass = prefix + ' ' + this.options.className + ' leaflet-zoom-' +
(this._animated ? 'animated' : 'hide'),
container = this._container = L.DomUtil.create('div', containerClass),
closeButton;
if (this.options.closeButton) {
closeButton = this._closeButton =
L.DomUtil.create('a', prefix + '-close-button', container);
closeButton.href = '#close';
closeButton.innerHTML = '&#215;';
L.DomEvent.disableClickPropagation(closeButton);
L.DomEvent.on(closeButton, 'click', this._onCloseButtonClick, this);
}
var wrapper = this._wrapper =
L.DomUtil.create('div', prefix + '-content-wrapper', container);
L.DomEvent.disableClickPropagation(wrapper);
this._contentNode = L.DomUtil.create('div', prefix + '-content', wrapper);
L.DomEvent.disableScrollPropagation(this._contentNode);
L.DomEvent.on(wrapper, 'contextmenu', L.DomEvent.stopPropagation);
this._tipContainer = L.DomUtil.create('div', prefix + '-tip-container', container);
this._tip = L.DomUtil.create('div', prefix + '-tip', this._tipContainer);
},
_updateContent: function () {
if (!this._content) { return; }
if (typeof this._content === 'string') {
this._contentNode.innerHTML = this._content;
} else {
while (this._contentNode.hasChildNodes()) {
this._contentNode.removeChild(this._contentNode.firstChild);
}
this._contentNode.appendChild(this._content);
}
this.fire('contentupdate');
},
_updateLayout: function () {
var container = this._contentNode,
style = container.style;
style.width = '';
style.whiteSpace = 'nowrap';
var width = container.offsetWidth;
width = Math.min(width, this.options.maxWidth);
width = Math.max(width, this.options.minWidth);
style.width = (width + 1) + 'px';
style.whiteSpace = '';
style.height = '';
var height = container.offsetHeight,
maxHeight = this.options.maxHeight,
scrolledClass = 'leaflet-popup-scrolled';
if (maxHeight && height > maxHeight) {
style.height = maxHeight + 'px';
L.DomUtil.addClass(container, scrolledClass);
} else {
L.DomUtil.removeClass(container, scrolledClass);
}
this._containerWidth = this._container.offsetWidth;
},
_updatePosition: function () {
if (!this._map) { return; }
var pos = this._map.latLngToLayerPoint(this._latlng),
animated = this._animated,
offset = L.point(this.options.offset);
if (animated) {
L.DomUtil.setPosition(this._container, pos);
}
this._containerBottom = -offset.y - (animated ? 0 : pos.y);
this._containerLeft = -Math.round(this._containerWidth / 2) + offset.x + (animated ? 0 : pos.x);
// bottom position the popup in case the height of the popup changes (images loading etc)
this._container.style.bottom = this._containerBottom + 'px';
this._container.style.left = this._containerLeft + 'px';
},
_zoomAnimation: function (opt) {
var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center);
L.DomUtil.setPosition(this._container, pos);
},
_adjustPan: function () {
if (!this.options.autoPan) { return; }
var map = this._map,
containerHeight = this._container.offsetHeight,
containerWidth = this._containerWidth,
layerPos = new L.Point(this._containerLeft, -containerHeight - this._containerBottom);
if (this._animated) {
layerPos._add(L.DomUtil.getPosition(this._container));
}
var containerPos = map.layerPointToContainerPoint(layerPos),
padding = L.point(this.options.autoPanPadding),
paddingTL = L.point(this.options.autoPanPaddingTopLeft || padding),
paddingBR = L.point(this.options.autoPanPaddingBottomRight || padding),
size = map.getSize(),
dx = 0,
dy = 0;
if (containerPos.x + containerWidth + paddingBR.x > size.x) { // right
dx = containerPos.x + containerWidth - size.x + paddingBR.x;
}
if (containerPos.x - dx - paddingTL.x < 0) { // left
dx = containerPos.x - paddingTL.x;
}
if (containerPos.y + containerHeight + paddingBR.y > size.y) { // bottom
dy = containerPos.y + containerHeight - size.y + paddingBR.y;
}
if (containerPos.y - dy - paddingTL.y < 0) { // top
dy = containerPos.y - paddingTL.y;
}
if (dx || dy) {
map
.fire('autopanstart')
.panBy([dx, dy]);
}
},
_onCloseButtonClick: function (e) {
this._close();
L.DomEvent.stop(e);
}
});
L.popup = function (options, source) {
return new L.Popup(options, source);
};
L.Map.include({
openPopup: function (popup, latlng, options) { // (Popup) or (String || HTMLElement, LatLng[, Object])
this.closePopup();
if (!(popup instanceof L.Popup)) {
var content = popup;
popup = new L.Popup(options)
.setLatLng(latlng)
.setContent(content);
}
popup._isOpen = true;
this._popup = popup;
return this.addLayer(popup);
},
closePopup: function (popup) {
if (!popup || popup === this._popup) {
popup = this._popup;
this._popup = null;
}
if (popup) {
this.removeLayer(popup);
popup._isOpen = false;
}
return this;
}
});
/*
* Popup extension to L.Marker, adding popup-related methods.
*/
L.Marker.include({
openPopup: function () {
if (this._popup && this._map && !this._map.hasLayer(this._popup)) {
this._popup.setLatLng(this._latlng);
this._map.openPopup(this._popup);
}
return this;
},
closePopup: function () {
if (this._popup) {
this._popup._close();
}
return this;
},
togglePopup: function () {
if (this._popup) {
if (this._popup._isOpen) {
this.closePopup();
} else {
this.openPopup();
}
}
return this;
},
bindPopup: function (content, options) {
var anchor = L.point(this.options.icon.options.popupAnchor || [0, 0]);
anchor = anchor.add(L.Popup.prototype.options.offset);
if (options && options.offset) {
anchor = anchor.add(options.offset);
}
options = L.extend({offset: anchor}, options);
if (!this._popupHandlersAdded) {
this
.on('click', this.togglePopup, this)
.on('remove', this.closePopup, this)
.on('move', this._movePopup, this);
this._popupHandlersAdded = true;
}
if (content instanceof L.Popup) {
L.setOptions(content, options);
this._popup = content;
content._source = this;
} else {
this._popup = new L.Popup(options, this)
.setContent(content);
}
return this;
},
setPopupContent: function (content) {
if (this._popup) {
this._popup.setContent(content);
}
return this;
},
unbindPopup: function () {
if (this._popup) {
this._popup = null;
this
.off('click', this.togglePopup, this)
.off('remove', this.closePopup, this)
.off('move', this._movePopup, this);
this._popupHandlersAdded = false;
}
return this;
},
getPopup: function () {
return this._popup;
},
_movePopup: function (e) {
this._popup.setLatLng(e.latlng);
}
});
/*
* L.LayerGroup is a class to combine several layers into one so that
* you can manipulate the group (e.g. add/remove it) as one layer.
*/
L.LayerGroup = L.Class.extend({
initialize: function (layers) {
this._layers = {};
var i, len;
if (layers) {
for (i = 0, len = layers.length; i < len; i++) {
this.addLayer(layers[i]);
}
}
},
addLayer: function (layer) {
var id = this.getLayerId(layer);
this._layers[id] = layer;
if (this._map) {
this._map.addLayer(layer);
}
return this;
},
removeLayer: function (layer) {
var id = layer in this._layers ? layer : this.getLayerId(layer);
if (this._map && this._layers[id]) {
this._map.removeLayer(this._layers[id]);
}
delete this._layers[id];
return this;
},
hasLayer: function (layer) {
if (!layer) { return false; }
return (layer in this._layers || this.getLayerId(layer) in this._layers);
},
clearLayers: function () {
this.eachLayer(this.removeLayer, this);
return this;
},
invoke: function (methodName) {
var args = Array.prototype.slice.call(arguments, 1),
i, layer;
for (i in this._layers) {
layer = this._layers[i];
if (layer[methodName]) {
layer[methodName].apply(layer, args);
}
}
return this;
},
onAdd: function (map) {
this._map = map;
this.eachLayer(map.addLayer, map);
},
onRemove: function (map) {
this.eachLayer(map.removeLayer, map);
this._map = null;
},
addTo: function (map) {
map.addLayer(this);
return this;
},
eachLayer: function (method, context) {
for (var i in this._layers) {
method.call(context, this._layers[i]);
}
return this;
},
getLayer: function (id) {
return this._layers[id];
},
getLayers: function () {
var layers = [];
for (var i in this._layers) {
layers.push(this._layers[i]);
}
return layers;
},
setZIndex: function (zIndex) {
return this.invoke('setZIndex', zIndex);
},
getLayerId: function (layer) {
return L.stamp(layer);
}
});
L.layerGroup = function (layers) {
return new L.LayerGroup(layers);
};
/*
* L.FeatureGroup extends L.LayerGroup by introducing mouse events and additional methods
* shared between a group of interactive layers (like vectors or markers).
*/
L.FeatureGroup = L.LayerGroup.extend({
includes: L.Mixin.Events,
statics: {
EVENTS: 'click dblclick mouseover mouseout mousemove contextmenu popupopen popupclose'
},
addLayer: function (layer) {
if (this.hasLayer(layer)) {
return this;
}
if ('on' in layer) {
layer.on(L.FeatureGroup.EVENTS, this._propagateEvent, this);
}
L.LayerGroup.prototype.addLayer.call(this, layer);
if (this._popupContent && layer.bindPopup) {
layer.bindPopup(this._popupContent, this._popupOptions);
}
return this.fire('layeradd', {layer: layer});
},
removeLayer: function (layer) {
if (!this.hasLayer(layer)) {
return this;
}
if (layer in this._layers) {
layer = this._layers[layer];
}
if ('off' in layer) {
layer.off(L.FeatureGroup.EVENTS, this._propagateEvent, this);
}
L.LayerGroup.prototype.removeLayer.call(this, layer);
if (this._popupContent) {
this.invoke('unbindPopup');
}
return this.fire('layerremove', {layer: layer});
},
bindPopup: function (content, options) {
this._popupContent = content;
this._popupOptions = options;
return this.invoke('bindPopup', content, options);
},
openPopup: function (latlng) {
// open popup on the first layer
for (var id in this._layers) {
this._layers[id].openPopup(latlng);
break;
}
return this;
},
setStyle: function (style) {
return this.invoke('setStyle', style);
},
bringToFront: function () {
return this.invoke('bringToFront');
},
bringToBack: function () {
return this.invoke('bringToBack');
},
getBounds: function () {
var bounds = new L.LatLngBounds();
this.eachLayer(function (layer) {
bounds.extend(layer instanceof L.Marker ? layer.getLatLng() : layer.getBounds());
});
return bounds;
},
_propagateEvent: function (e) {
e = L.extend({
layer: e.target,
target: this
}, e);
this.fire(e.type, e);
}
});
L.featureGroup = function (layers) {
return new L.FeatureGroup(layers);
};
/*
* L.Path is a base class for rendering vector paths on a map. Inherited by Polyline, Circle, etc.
*/
L.Path = L.Class.extend({
includes: [L.Mixin.Events],
statics: {
// how much to extend the clip area around the map view
// (relative to its size, e.g. 0.5 is half the screen in each direction)
// set it so that SVG element doesn't exceed 1280px (vectors flicker on dragend if it is)
CLIP_PADDING: (function () {
var max = L.Browser.mobile ? 1280 : 2000,
target = (max / Math.max(window.outerWidth, window.outerHeight) - 1) / 2;
return Math.max(0, Math.min(0.5, target));
})()
},
options: {
stroke: true,
color: '#0033ff',
dashArray: null,
lineCap: null,
lineJoin: null,
weight: 5,
opacity: 0.5,
fill: false,
fillColor: null, //same as color by default
fillOpacity: 0.2,
clickable: true
},
initialize: function (options) {
L.setOptions(this, options);
},
onAdd: function (map) {
this._map = map;
if (!this._container) {
this._initElements();
this._initEvents();
}
this.projectLatlngs();
this._updatePath();
if (this._container) {
this._map._pathRoot.appendChild(this._container);
}
this.fire('add');
map.on({
'viewreset': this.projectLatlngs,
'moveend': this._updatePath
}, this);
},
addTo: function (map) {
map.addLayer(this);
return this;
},
onRemove: function (map) {
map._pathRoot.removeChild(this._container);
// Need to fire remove event before we set _map to null as the event hooks might need the object
this.fire('remove');
this._map = null;
if (L.Browser.vml) {
this._container = null;
this._stroke = null;
this._fill = null;
}
map.off({
'viewreset': this.projectLatlngs,
'moveend': this._updatePath
}, this);
},
projectLatlngs: function () {
// do all projection stuff here
},
setStyle: function (style) {
L.setOptions(this, style);
if (this._container) {
this._updateStyle();
}
return this;
},
redraw: function () {
if (this._map) {
this.projectLatlngs();
this._updatePath();
}
return this;
}
});
L.Map.include({
_updatePathViewport: function () {
var p = L.Path.CLIP_PADDING,
size = this.getSize(),
panePos = L.DomUtil.getPosition(this._mapPane),
min = panePos.multiplyBy(-1)._subtract(size.multiplyBy(p)._round()),
max = min.add(size.multiplyBy(1 + p * 2)._round());
this._pathViewport = new L.Bounds(min, max);
}
});
/*
* Extends L.Path with SVG-specific rendering code.
*/
L.Path.SVG_NS = 'http://www.w3.org/2000/svg';
L.Browser.svg = !!(document.createElementNS && document.createElementNS(L.Path.SVG_NS, 'svg').createSVGRect);
L.Path = L.Path.extend({
statics: {
SVG: L.Browser.svg
},
bringToFront: function () {
var root = this._map._pathRoot,
path = this._container;
if (path && root.lastChild !== path) {
root.appendChild(path);
}
return this;
},
bringToBack: function () {
var root = this._map._pathRoot,
path = this._container,
first = root.firstChild;
if (path && first !== path) {
root.insertBefore(path, first);
}
return this;
},
getPathString: function () {
// form path string here
},
_createElement: function (name) {
return document.createElementNS(L.Path.SVG_NS, name);
},
_initElements: function () {
this._map._initPathRoot();
this._initPath();
this._initStyle();
},
_initPath: function () {
this._container = this._createElement('g');
this._path = this._createElement('path');
if (this.options.className) {
L.DomUtil.addClass(this._path, this.options.className);
}
this._container.appendChild(this._path);
},
_initStyle: function () {
if (this.options.stroke) {
this._path.setAttribute('stroke-linejoin', 'round');
this._path.setAttribute('stroke-linecap', 'round');
}
if (this.options.fill) {
this._path.setAttribute('fill-rule', 'evenodd');
}
if (this.options.pointerEvents) {
this._path.setAttribute('pointer-events', this.options.pointerEvents);
}
if (!this.options.clickable && !this.options.pointerEvents) {
this._path.setAttribute('pointer-events', 'none');
}
this._updateStyle();
},
_updateStyle: function () {
if (this.options.stroke) {
this._path.setAttribute('stroke', this.options.color);
this._path.setAttribute('stroke-opacity', this.options.opacity);
this._path.setAttribute('stroke-width', this.options.weight);
if (this.options.dashArray) {
this._path.setAttribute('stroke-dasharray', this.options.dashArray);
} else {
this._path.removeAttribute('stroke-dasharray');
}
if (this.options.lineCap) {
this._path.setAttribute('stroke-linecap', this.options.lineCap);
}
if (this.options.lineJoin) {
this._path.setAttribute('stroke-linejoin', this.options.lineJoin);
}
} else {
this._path.setAttribute('stroke', 'none');
}
if (this.options.fill) {
this._path.setAttribute('fill', this.options.fillColor || this.options.color);
this._path.setAttribute('fill-opacity', this.options.fillOpacity);
} else {
this._path.setAttribute('fill', 'none');
}
},
_updatePath: function () {
var str = this.getPathString();
if (!str) {
// fix webkit empty string parsing bug
str = 'M0 0';
}
this._path.setAttribute('d', str);
},
// TODO remove duplication with L.Map
_initEvents: function () {
if (this.options.clickable) {
if (L.Browser.svg || !L.Browser.vml) {
L.DomUtil.addClass(this._path, 'leaflet-clickable');
}
L.DomEvent.on(this._container, 'click', this._onMouseClick, this);
var events = ['dblclick', 'mousedown', 'mouseover',
'mouseout', 'mousemove', 'contextmenu'];
for (var i = 0; i < events.length; i++) {
L.DomEvent.on(this._container, events[i], this._fireMouseEvent, this);
}
}
},
_onMouseClick: function (e) {
if (this._map.dragging && this._map.dragging.moved()) { return; }
this._fireMouseEvent(e);
},
_fireMouseEvent: function (e) {
if (!this._map || !this.hasEventListeners(e.type)) { return; }
var map = this._map,
containerPoint = map.mouseEventToContainerPoint(e),
layerPoint = map.containerPointToLayerPoint(containerPoint),
latlng = map.layerPointToLatLng(layerPoint);
this.fire(e.type, {
latlng: latlng,
layerPoint: layerPoint,
containerPoint: containerPoint,
originalEvent: e
});
if (e.type === 'contextmenu') {
L.DomEvent.preventDefault(e);
}
if (e.type !== 'mousemove') {
L.DomEvent.stopPropagation(e);
}
}
});
L.Map.include({
_initPathRoot: function () {
if (!this._pathRoot) {
this._pathRoot = L.Path.prototype._createElement('svg');
this._panes.overlayPane.appendChild(this._pathRoot);
if (this.options.zoomAnimation && L.Browser.any3d) {
L.DomUtil.addClass(this._pathRoot, 'leaflet-zoom-animated');
this.on({
'zoomanim': this._animatePathZoom,
'zoomend': this._endPathZoom
});
} else {
L.DomUtil.addClass(this._pathRoot, 'leaflet-zoom-hide');
}
this.on('moveend', this._updateSvgViewport);
this._updateSvgViewport();
}
},
_animatePathZoom: function (e) {
var scale = this.getZoomScale(e.zoom),
offset = this._getCenterOffset(e.center)._multiplyBy(-scale)._add(this._pathViewport.min);
this._pathRoot.style[L.DomUtil.TRANSFORM] =
L.DomUtil.getTranslateString(offset) + ' scale(' + scale + ') ';
this._pathZooming = true;
},
_endPathZoom: function () {
this._pathZooming = false;
},
_updateSvgViewport: function () {
if (this._pathZooming) {
// Do not update SVGs while a zoom animation is going on otherwise the animation will break.
// When the zoom animation ends we will be updated again anyway
// This fixes the case where you do a momentum move and zoom while the move is still ongoing.
return;
}
this._updatePathViewport();
var vp = this._pathViewport,
min = vp.min,
max = vp.max,
width = max.x - min.x,
height = max.y - min.y,
root = this._pathRoot,
pane = this._panes.overlayPane;
// Hack to make flicker on drag end on mobile webkit less irritating
if (L.Browser.mobileWebkit) {
pane.removeChild(root);
}
L.DomUtil.setPosition(root, min);
root.setAttribute('width', width);
root.setAttribute('height', height);
root.setAttribute('viewBox', [min.x, min.y, width, height].join(' '));
if (L.Browser.mobileWebkit) {
pane.appendChild(root);
}
}
});
/*
* Popup extension to L.Path (polylines, polygons, circles), adding popup-related methods.
*/
L.Path.include({
bindPopup: function (content, options) {
if (content instanceof L.Popup) {
this._popup = content;
} else {
if (!this._popup || options) {
this._popup = new L.Popup(options, this);
}
this._popup.setContent(content);
}
if (!this._popupHandlersAdded) {
this
.on('click', this._openPopup, this)
.on('remove', this.closePopup, this);
this._popupHandlersAdded = true;
}
return this;
},
unbindPopup: function () {
if (this._popup) {
this._popup = null;
this
.off('click', this._openPopup)
.off('remove', this.closePopup);
this._popupHandlersAdded = false;
}
return this;
},
openPopup: function (latlng) {
if (this._popup) {
// open the popup from one of the path's points if not specified
latlng = latlng || this._latlng ||
this._latlngs[Math.floor(this._latlngs.length / 2)];
this._openPopup({latlng: latlng});
}
return this;
},
closePopup: function () {
if (this._popup) {
this._popup._close();
}
return this;
},
_openPopup: function (e) {
this._popup.setLatLng(e.latlng);
this._map.openPopup(this._popup);
}
});
/*
* Vector rendering for IE6-8 through VML.
* Thanks to Dmitry Baranovsky and his Raphael library for inspiration!
*/
L.Browser.vml = !L.Browser.svg && (function () {
try {
var div = document.createElement('div');
div.innerHTML = '<v:shape adj="1"/>';
var shape = div.firstChild;
shape.style.behavior = 'url(#default#VML)';
return shape && (typeof shape.adj === 'object');
} catch (e) {
return false;
}
}());
L.Path = L.Browser.svg || !L.Browser.vml ? L.Path : L.Path.extend({
statics: {
VML: true,
CLIP_PADDING: 0.02
},
_createElement: (function () {
try {
document.namespaces.add('lvml', 'urn:schemas-microsoft-com:vml');
return function (name) {
return document.createElement('<lvml:' + name + ' class="lvml">');
};
} catch (e) {
return function (name) {
return document.createElement(
'<' + name + ' xmlns="urn:schemas-microsoft.com:vml" class="lvml">');
};
}
}()),
_initPath: function () {
var container = this._container = this._createElement('shape');
L.DomUtil.addClass(container, 'leaflet-vml-shape' +
(this.options.className ? ' ' + this.options.className : ''));
if (this.options.clickable) {
L.DomUtil.addClass(container, 'leaflet-clickable');
}
container.coordsize = '1 1';
this._path = this._createElement('path');
container.appendChild(this._path);
this._map._pathRoot.appendChild(container);
},
_initStyle: function () {
this._updateStyle();
},
_updateStyle: function () {
var stroke = this._stroke,
fill = this._fill,
options = this.options,
container = this._container;
container.stroked = options.stroke;
container.filled = options.fill;
if (options.stroke) {
if (!stroke) {
stroke = this._stroke = this._createElement('stroke');
stroke.endcap = 'round';
container.appendChild(stroke);
}
stroke.weight = options.weight + 'px';
stroke.color = options.color;
stroke.opacity = options.opacity;
if (options.dashArray) {
stroke.dashStyle = L.Util.isArray(options.dashArray) ?
options.dashArray.join(' ') :
options.dashArray.replace(/( *, *)/g, ' ');
} else {
stroke.dashStyle = '';
}
if (options.lineCap) {
stroke.endcap = options.lineCap.replace('butt', 'flat');
}
if (options.lineJoin) {
stroke.joinstyle = options.lineJoin;
}
} else if (stroke) {
container.removeChild(stroke);
this._stroke = null;
}
if (options.fill) {
if (!fill) {
fill = this._fill = this._createElement('fill');
container.appendChild(fill);
}
fill.color = options.fillColor || options.color;
fill.opacity = options.fillOpacity;
} else if (fill) {
container.removeChild(fill);
this._fill = null;
}
},
_updatePath: function () {
var style = this._container.style;
style.display = 'none';
this._path.v = this.getPathString() + ' '; // the space fixes IE empty path string bug
style.display = '';
}
});
L.Map.include(L.Browser.svg || !L.Browser.vml ? {} : {
_initPathRoot: function () {
if (this._pathRoot) { return; }
var root = this._pathRoot = document.createElement('div');
root.className = 'leaflet-vml-container';
this._panes.overlayPane.appendChild(root);
this.on('moveend', this._updatePathViewport);
this._updatePathViewport();
}
});
/*
* Vector rendering for all browsers that support canvas.
*/
L.Browser.canvas = (function () {
return !!document.createElement('canvas').getContext;
}());
L.Path = (L.Path.SVG && !window.L_PREFER_CANVAS) || !L.Browser.canvas ? L.Path : L.Path.extend({
statics: {
//CLIP_PADDING: 0.02, // not sure if there's a need to set it to a small value
CANVAS: true,
SVG: false
},
redraw: function () {
if (this._map) {
this.projectLatlngs();
this._requestUpdate();
}
return this;
},
setStyle: function (style) {
L.setOptions(this, style);
if (this._map) {
this._updateStyle();
this._requestUpdate();
}
return this;
},
onRemove: function (map) {
map
.off('viewreset', this.projectLatlngs, this)
.off('moveend', this._updatePath, this);
if (this.options.clickable) {
this._map.off('click', this._onClick, this);
this._map.off('mousemove', this._onMouseMove, this);
}
this._requestUpdate();
this.fire('remove');
this._map = null;
},
_requestUpdate: function () {
if (this._map && !L.Path._updateRequest) {
L.Path._updateRequest = L.Util.requestAnimFrame(this._fireMapMoveEnd, this._map);
}
},
_fireMapMoveEnd: function () {
L.Path._updateRequest = null;
this.fire('moveend');
},
_initElements: function () {
this._map._initPathRoot();
this._ctx = this._map._canvasCtx;
},
_updateStyle: function () {
var options = this.options;
if (options.stroke) {
this._ctx.lineWidth = options.weight;
this._ctx.strokeStyle = options.color;
}
if (options.fill) {
this._ctx.fillStyle = options.fillColor || options.color;
}
if (options.lineCap) {
this._ctx.lineCap = options.lineCap;
}
if (options.lineJoin) {
this._ctx.lineJoin = options.lineJoin;
}
},
_drawPath: function () {
var i, j, len, len2, point, drawMethod;
this._ctx.beginPath();
for (i = 0, len = this._parts.length; i < len; i++) {
for (j = 0, len2 = this._parts[i].length; j < len2; j++) {
point = this._parts[i][j];
drawMethod = (j === 0 ? 'move' : 'line') + 'To';
this._ctx[drawMethod](point.x, point.y);
}
// TODO refactor ugly hack
if (this instanceof L.Polygon) {
this._ctx.closePath();
}
}
},
_checkIfEmpty: function () {
return !this._parts.length;
},
_updatePath: function () {
if (this._checkIfEmpty()) { return; }
var ctx = this._ctx,
options = this.options;
this._drawPath();
ctx.save();
this._updateStyle();
if (options.fill) {
ctx.globalAlpha = options.fillOpacity;
ctx.fill(options.fillRule || 'evenodd');
}
if (options.stroke) {
ctx.globalAlpha = options.opacity;
ctx.stroke();
}
ctx.restore();
// TODO optimization: 1 fill/stroke for all features with equal style instead of 1 for each feature
},
_initEvents: function () {
if (this.options.clickable) {
this._map.on('mousemove', this._onMouseMove, this);
this._map.on('click dblclick contextmenu', this._fireMouseEvent, this);
}
},
_fireMouseEvent: function (e) {
if (this._containsPoint(e.layerPoint)) {
this.fire(e.type, e);
}
},
_onMouseMove: function (e) {
if (!this._map || this._map._animatingZoom) { return; }
// TODO don't do on each move
if (this._containsPoint(e.layerPoint)) {
this._ctx.canvas.style.cursor = 'pointer';
this._mouseInside = true;
this.fire('mouseover', e);
} else if (this._mouseInside) {
this._ctx.canvas.style.cursor = '';
this._mouseInside = false;
this.fire('mouseout', e);
}
}
});
L.Map.include((L.Path.SVG && !window.L_PREFER_CANVAS) || !L.Browser.canvas ? {} : {
_initPathRoot: function () {
var root = this._pathRoot,
ctx;
if (!root) {
root = this._pathRoot = document.createElement('canvas');
root.style.position = 'absolute';
ctx = this._canvasCtx = root.getContext('2d');
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
this._panes.overlayPane.appendChild(root);
if (this.options.zoomAnimation) {
this._pathRoot.className = 'leaflet-zoom-animated';
this.on('zoomanim', this._animatePathZoom);
this.on('zoomend', this._endPathZoom);
}
this.on('moveend', this._updateCanvasViewport);
this._updateCanvasViewport();
}
},
_updateCanvasViewport: function () {
// don't redraw while zooming. See _updateSvgViewport for more details
if (this._pathZooming) { return; }
this._updatePathViewport();
var vp = this._pathViewport,
min = vp.min,
size = vp.max.subtract(min),
root = this._pathRoot;
//TODO check if this works properly on mobile webkit
L.DomUtil.setPosition(root, min);
root.width = size.x;
root.height = size.y;
root.getContext('2d').translate(-min.x, -min.y);
}
});
/*
* L.LineUtil contains different utility functions for line segments
* and polylines (clipping, simplification, distances, etc.)
*/
/*jshint bitwise:false */ // allow bitwise operations for this file
L.LineUtil = {
// Simplify polyline with vertex reduction and Douglas-Peucker simplification.
// Improves rendering performance dramatically by lessening the number of points to draw.
simplify: function (/*Point[]*/ points, /*Number*/ tolerance) {
if (!tolerance || !points.length) {
return points.slice();
}
var sqTolerance = tolerance * tolerance;
// stage 1: vertex reduction
points = this._reducePoints(points, sqTolerance);
// stage 2: Douglas-Peucker simplification
points = this._simplifyDP(points, sqTolerance);
return points;
},
// distance from a point to a segment between two points
pointToSegmentDistance: function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2) {
return Math.sqrt(this._sqClosestPointOnSegment(p, p1, p2, true));
},
closestPointOnSegment: function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2) {
return this._sqClosestPointOnSegment(p, p1, p2);
},
// Douglas-Peucker simplification, see http://en.wikipedia.org/wiki/Douglas-Peucker_algorithm
_simplifyDP: function (points, sqTolerance) {
var len = points.length,
ArrayConstructor = typeof Uint8Array !== undefined + '' ? Uint8Array : Array,
markers = new ArrayConstructor(len);
markers[0] = markers[len - 1] = 1;
this._simplifyDPStep(points, markers, sqTolerance, 0, len - 1);
var i,
newPoints = [];
for (i = 0; i < len; i++) {
if (markers[i]) {
newPoints.push(points[i]);
}
}
return newPoints;
},
_simplifyDPStep: function (points, markers, sqTolerance, first, last) {
var maxSqDist = 0,
index, i, sqDist;
for (i = first + 1; i <= last - 1; i++) {
sqDist = this._sqClosestPointOnSegment(points[i], points[first], points[last], true);
if (sqDist > maxSqDist) {
index = i;
maxSqDist = sqDist;
}
}
if (maxSqDist > sqTolerance) {
markers[index] = 1;
this._simplifyDPStep(points, markers, sqTolerance, first, index);
this._simplifyDPStep(points, markers, sqTolerance, index, last);
}
},
// reduce points that are too close to each other to a single point
_reducePoints: function (points, sqTolerance) {
var reducedPoints = [points[0]];
for (var i = 1, prev = 0, len = points.length; i < len; i++) {
if (this._sqDist(points[i], points[prev]) > sqTolerance) {
reducedPoints.push(points[i]);
prev = i;
}
}
if (prev < len - 1) {
reducedPoints.push(points[len - 1]);
}
return reducedPoints;
},
// Cohen-Sutherland line clipping algorithm.
// Used to avoid rendering parts of a polyline that are not currently visible.
clipSegment: function (a, b, bounds, useLastCode) {
var codeA = useLastCode ? this._lastCode : this._getBitCode(a, bounds),
codeB = this._getBitCode(b, bounds),
codeOut, p, newCode;
// save 2nd code to avoid calculating it on the next segment
this._lastCode = codeB;
while (true) {
// if a,b is inside the clip window (trivial accept)
if (!(codeA | codeB)) {
return [a, b];
// if a,b is outside the clip window (trivial reject)
} else if (codeA & codeB) {
return false;
// other cases
} else {
codeOut = codeA || codeB;
p = this._getEdgeIntersection(a, b, codeOut, bounds);
newCode = this._getBitCode(p, bounds);
if (codeOut === codeA) {
a = p;
codeA = newCode;
} else {
b = p;
codeB = newCode;
}
}
}
},
_getEdgeIntersection: function (a, b, code, bounds) {
var dx = b.x - a.x,
dy = b.y - a.y,
min = bounds.min,
max = bounds.max;
if (code & 8) { // top
return new L.Point(a.x + dx * (max.y - a.y) / dy, max.y);
} else if (code & 4) { // bottom
return new L.Point(a.x + dx * (min.y - a.y) / dy, min.y);
} else if (code & 2) { // right
return new L.Point(max.x, a.y + dy * (max.x - a.x) / dx);
} else if (code & 1) { // left
return new L.Point(min.x, a.y + dy * (min.x - a.x) / dx);
}
},
_getBitCode: function (/*Point*/ p, bounds) {
var code = 0;
if (p.x < bounds.min.x) { // left
code |= 1;
} else if (p.x > bounds.max.x) { // right
code |= 2;
}
if (p.y < bounds.min.y) { // bottom
code |= 4;
} else if (p.y > bounds.max.y) { // top
code |= 8;
}
return code;
},
// square distance (to avoid unnecessary Math.sqrt calls)
_sqDist: function (p1, p2) {
var dx = p2.x - p1.x,
dy = p2.y - p1.y;
return dx * dx + dy * dy;
},
// return closest point on segment or distance to that point
_sqClosestPointOnSegment: function (p, p1, p2, sqDist) {
var x = p1.x,
y = p1.y,
dx = p2.x - x,
dy = p2.y - y,
dot = dx * dx + dy * dy,
t;
if (dot > 0) {
t = ((p.x - x) * dx + (p.y - y) * dy) / dot;
if (t > 1) {
x = p2.x;
y = p2.y;
} else if (t > 0) {
x += dx * t;
y += dy * t;
}
}
dx = p.x - x;
dy = p.y - y;
return sqDist ? dx * dx + dy * dy : new L.Point(x, y);
}
};
/*
* L.Polyline is used to display polylines on a map.
*/
L.Polyline = L.Path.extend({
initialize: function (latlngs, options) {
L.Path.prototype.initialize.call(this, options);
this._latlngs = this._convertLatLngs(latlngs);
},
options: {
// how much to simplify the polyline on each zoom level
// more = better performance and smoother look, less = more accurate
smoothFactor: 1.0,
noClip: false
},
projectLatlngs: function () {
this._originalPoints = [];
for (var i = 0, len = this._latlngs.length; i < len; i++) {
this._originalPoints[i] = this._map.latLngToLayerPoint(this._latlngs[i]);
}
},
getPathString: function () {
for (var i = 0, len = this._parts.length, str = ''; i < len; i++) {
str += this._getPathPartStr(this._parts[i]);
}
return str;
},
getLatLngs: function () {
return this._latlngs;
},
setLatLngs: function (latlngs) {
this._latlngs = this._convertLatLngs(latlngs);
return this.redraw();
},
addLatLng: function (latlng) {
this._latlngs.push(L.latLng(latlng));
return this.redraw();
},
spliceLatLngs: function () { // (Number index, Number howMany)
var removed = [].splice.apply(this._latlngs, arguments);
this._convertLatLngs(this._latlngs, true);
this.redraw();
return removed;
},
closestLayerPoint: function (p) {
var minDistance = Infinity, parts = this._parts, p1, p2, minPoint = null;
for (var j = 0, jLen = parts.length; j < jLen; j++) {
var points = parts[j];
for (var i = 1, len = points.length; i < len; i++) {
p1 = points[i - 1];
p2 = points[i];
var sqDist = L.LineUtil._sqClosestPointOnSegment(p, p1, p2, true);
if (sqDist < minDistance) {
minDistance = sqDist;
minPoint = L.LineUtil._sqClosestPointOnSegment(p, p1, p2);
}
}
}
if (minPoint) {
minPoint.distance = Math.sqrt(minDistance);
}
return minPoint;
},
getBounds: function () {
return new L.LatLngBounds(this.getLatLngs());
},
_convertLatLngs: function (latlngs, overwrite) {
var i, len, target = overwrite ? latlngs : [];
for (i = 0, len = latlngs.length; i < len; i++) {
if (L.Util.isArray(latlngs[i]) && typeof latlngs[i][0] !== 'number') {
return;
}
target[i] = L.latLng(latlngs[i]);
}
return target;
},
_initEvents: function () {
L.Path.prototype._initEvents.call(this);
},
_getPathPartStr: function (points) {
var round = L.Path.VML;
for (var j = 0, len2 = points.length, str = '', p; j < len2; j++) {
p = points[j];
if (round) {
p._round();
}
str += (j ? 'L' : 'M') + p.x + ' ' + p.y;
}
return str;
},
_clipPoints: function () {
var points = this._originalPoints,
len = points.length,
i, k, segment;
if (this.options.noClip) {
this._parts = [points];
return;
}
this._parts = [];
var parts = this._parts,
vp = this._map._pathViewport,
lu = L.LineUtil;
for (i = 0, k = 0; i < len - 1; i++) {
segment = lu.clipSegment(points[i], points[i + 1], vp, i);
if (!segment) {
continue;
}
parts[k] = parts[k] || [];
parts[k].push(segment[0]);
// if segment goes out of screen, or it's the last one, it's the end of the line part
if ((segment[1] !== points[i + 1]) || (i === len - 2)) {
parts[k].push(segment[1]);
k++;
}
}
},
// simplify each clipped part of the polyline
_simplifyPoints: function () {
var parts = this._parts,
lu = L.LineUtil;
for (var i = 0, len = parts.length; i < len; i++) {
parts[i] = lu.simplify(parts[i], this.options.smoothFactor);
}
},
_updatePath: function () {
if (!this._map) { return; }
this._clipPoints();
this._simplifyPoints();
L.Path.prototype._updatePath.call(this);
}
});
L.polyline = function (latlngs, options) {
return new L.Polyline(latlngs, options);
};
/*
* L.PolyUtil contains utility functions for polygons (clipping, etc.).
*/
/*jshint bitwise:false */ // allow bitwise operations here
L.PolyUtil = {};
/*
* Sutherland-Hodgeman polygon clipping algorithm.
* Used to avoid rendering parts of a polygon that are not currently visible.
*/
L.PolyUtil.clipPolygon = function (points, bounds) {
var clippedPoints,
edges = [1, 4, 2, 8],
i, j, k,
a, b,
len, edge, p,
lu = L.LineUtil;
for (i = 0, len = points.length; i < len; i++) {
points[i]._code = lu._getBitCode(points[i], bounds);
}
// for each edge (left, bottom, right, top)
for (k = 0; k < 4; k++) {
edge = edges[k];
clippedPoints = [];
for (i = 0, len = points.length, j = len - 1; i < len; j = i++) {
a = points[i];
b = points[j];
// if a is inside the clip window
if (!(a._code & edge)) {
// if b is outside the clip window (a->b goes out of screen)
if (b._code & edge) {
p = lu._getEdgeIntersection(b, a, edge, bounds);
p._code = lu._getBitCode(p, bounds);
clippedPoints.push(p);
}
clippedPoints.push(a);
// else if b is inside the clip window (a->b enters the screen)
} else if (!(b._code & edge)) {
p = lu._getEdgeIntersection(b, a, edge, bounds);
p._code = lu._getBitCode(p, bounds);
clippedPoints.push(p);
}
}
points = clippedPoints;
}
return points;
};
/*
* L.Polygon is used to display polygons on a map.
*/
L.Polygon = L.Polyline.extend({
options: {
fill: true
},
initialize: function (latlngs, options) {
L.Polyline.prototype.initialize.call(this, latlngs, options);
this._initWithHoles(latlngs);
},
_initWithHoles: function (latlngs) {
var i, len, hole;
if (latlngs && L.Util.isArray(latlngs[0]) && (typeof latlngs[0][0] !== 'number')) {
this._latlngs = this._convertLatLngs(latlngs[0]);
this._holes = latlngs.slice(1);
for (i = 0, len = this._holes.length; i < len; i++) {
hole = this._holes[i] = this._convertLatLngs(this._holes[i]);
if (hole[0].equals(hole[hole.length - 1])) {
hole.pop();
}
}
}
// filter out last point if its equal to the first one
latlngs = this._latlngs;
if (latlngs.length >= 2 && latlngs[0].equals(latlngs[latlngs.length - 1])) {
latlngs.pop();
}
},
projectLatlngs: function () {
L.Polyline.prototype.projectLatlngs.call(this);
// project polygon holes points
// TODO move this logic to Polyline to get rid of duplication
this._holePoints = [];
if (!this._holes) { return; }
var i, j, len, len2;
for (i = 0, len = this._holes.length; i < len; i++) {
this._holePoints[i] = [];
for (j = 0, len2 = this._holes[i].length; j < len2; j++) {
this._holePoints[i][j] = this._map.latLngToLayerPoint(this._holes[i][j]);
}
}
},
setLatLngs: function (latlngs) {
if (latlngs && L.Util.isArray(latlngs[0]) && (typeof latlngs[0][0] !== 'number')) {
this._initWithHoles(latlngs);
return this.redraw();
} else {
return L.Polyline.prototype.setLatLngs.call(this, latlngs);
}
},
_clipPoints: function () {
var points = this._originalPoints,
newParts = [];
this._parts = [points].concat(this._holePoints);
if (this.options.noClip) { return; }
for (var i = 0, len = this._parts.length; i < len; i++) {
var clipped = L.PolyUtil.clipPolygon(this._parts[i], this._map._pathViewport);
if (clipped.length) {
newParts.push(clipped);
}
}
this._parts = newParts;
},
_getPathPartStr: function (points) {
var str = L.Polyline.prototype._getPathPartStr.call(this, points);
return str + (L.Browser.svg ? 'z' : 'x');
}
});
L.polygon = function (latlngs, options) {
return new L.Polygon(latlngs, options);
};
/*
* Contains L.MultiPolyline and L.MultiPolygon layers.
*/
(function () {
function createMulti(Klass) {
return L.FeatureGroup.extend({
initialize: function (latlngs, options) {
this._layers = {};
this._options = options;
this.setLatLngs(latlngs);
},
setLatLngs: function (latlngs) {
var i = 0,
len = latlngs.length;
this.eachLayer(function (layer) {
if (i < len) {
layer.setLatLngs(latlngs[i++]);
} else {
this.removeLayer(layer);
}
}, this);
while (i < len) {
this.addLayer(new Klass(latlngs[i++], this._options));
}
return this;
},
getLatLngs: function () {
var latlngs = [];
this.eachLayer(function (layer) {
latlngs.push(layer.getLatLngs());
});
return latlngs;
}
});
}
L.MultiPolyline = createMulti(L.Polyline);
L.MultiPolygon = createMulti(L.Polygon);
L.multiPolyline = function (latlngs, options) {
return new L.MultiPolyline(latlngs, options);
};
L.multiPolygon = function (latlngs, options) {
return new L.MultiPolygon(latlngs, options);
};
}());
/*
* L.Rectangle extends Polygon and creates a rectangle when passed a LatLngBounds object.
*/
L.Rectangle = L.Polygon.extend({
initialize: function (latLngBounds, options) {
L.Polygon.prototype.initialize.call(this, this._boundsToLatLngs(latLngBounds), options);
},
setBounds: function (latLngBounds) {
this.setLatLngs(this._boundsToLatLngs(latLngBounds));
},
_boundsToLatLngs: function (latLngBounds) {
latLngBounds = L.latLngBounds(latLngBounds);
return [
latLngBounds.getSouthWest(),
latLngBounds.getNorthWest(),
latLngBounds.getNorthEast(),
latLngBounds.getSouthEast()
];
}
});
L.rectangle = function (latLngBounds, options) {
return new L.Rectangle(latLngBounds, options);
};
/*
* L.Circle is a circle overlay (with a certain radius in meters).
*/
L.Circle = L.Path.extend({
initialize: function (latlng, radius, options) {
L.Path.prototype.initialize.call(this, options);
this._latlng = L.latLng(latlng);
this._mRadius = radius;
},
options: {
fill: true
},
setLatLng: function (latlng) {
this._latlng = L.latLng(latlng);
return this.redraw();
},
setRadius: function (radius) {
this._mRadius = radius;
return this.redraw();
},
projectLatlngs: function () {
var lngRadius = this._getLngRadius(),
latlng = this._latlng,
pointLeft = this._map.latLngToLayerPoint([latlng.lat, latlng.lng - lngRadius]);
this._point = this._map.latLngToLayerPoint(latlng);
this._radius = Math.max(this._point.x - pointLeft.x, 1);
},
getBounds: function () {
var lngRadius = this._getLngRadius(),
latRadius = (this._mRadius / 40075017) * 360,
latlng = this._latlng;
return new L.LatLngBounds(
[latlng.lat - latRadius, latlng.lng - lngRadius],
[latlng.lat + latRadius, latlng.lng + lngRadius]);
},
getLatLng: function () {
return this._latlng;
},
getPathString: function () {
var p = this._point,
r = this._radius;
if (this._checkIfEmpty()) {
return '';
}
if (L.Browser.svg) {
return 'M' + p.x + ',' + (p.y - r) +
'A' + r + ',' + r + ',0,1,1,' +
(p.x - 0.1) + ',' + (p.y - r) + ' z';
} else {
p._round();
r = Math.round(r);
return 'AL ' + p.x + ',' + p.y + ' ' + r + ',' + r + ' 0,' + (65535 * 360);
}
},
getRadius: function () {
return this._mRadius;
},
// TODO Earth hardcoded, move into projection code!
_getLatRadius: function () {
return (this._mRadius / 40075017) * 360;
},
_getLngRadius: function () {
return this._getLatRadius() / Math.cos(L.LatLng.DEG_TO_RAD * this._latlng.lat);
},
_checkIfEmpty: function () {
if (!this._map) {
return false;
}
var vp = this._map._pathViewport,
r = this._radius,
p = this._point;
return p.x - r > vp.max.x || p.y - r > vp.max.y ||
p.x + r < vp.min.x || p.y + r < vp.min.y;
}
});
L.circle = function (latlng, radius, options) {
return new L.Circle(latlng, radius, options);
};
/*
* L.CircleMarker is a circle overlay with a permanent pixel radius.
*/
L.CircleMarker = L.Circle.extend({
options: {
radius: 10,
weight: 2
},
initialize: function (latlng, options) {
L.Circle.prototype.initialize.call(this, latlng, null, options);
this._radius = this.options.radius;
},
projectLatlngs: function () {
this._point = this._map.latLngToLayerPoint(this._latlng);
},
_updateStyle : function () {
L.Circle.prototype._updateStyle.call(this);
this.setRadius(this.options.radius);
},
setLatLng: function (latlng) {
L.Circle.prototype.setLatLng.call(this, latlng);
if (this._popup && this._popup._isOpen) {
this._popup.setLatLng(latlng);
}
return this;
},
setRadius: function (radius) {
this.options.radius = this._radius = radius;
return this.redraw();
},
getRadius: function () {
return this._radius;
}
});
L.circleMarker = function (latlng, options) {
return new L.CircleMarker(latlng, options);
};
/*
* Extends L.Polyline to be able to manually detect clicks on Canvas-rendered polylines.
*/
L.Polyline.include(!L.Path.CANVAS ? {} : {
_containsPoint: function (p, closed) {
var i, j, k, len, len2, dist, part,
w = this.options.weight / 2;
if (L.Browser.touch) {
w += 10; // polyline click tolerance on touch devices
}
for (i = 0, len = this._parts.length; i < len; i++) {
part = this._parts[i];
for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) {
if (!closed && (j === 0)) {
continue;
}
dist = L.LineUtil.pointToSegmentDistance(p, part[k], part[j]);
if (dist <= w) {
return true;
}
}
}
return false;
}
});
/*
* Extends L.Polygon to be able to manually detect clicks on Canvas-rendered polygons.
*/
L.Polygon.include(!L.Path.CANVAS ? {} : {
_containsPoint: function (p) {
var inside = false,
part, p1, p2,
i, j, k,
len, len2;
// TODO optimization: check if within bounds first
if (L.Polyline.prototype._containsPoint.call(this, p, true)) {
// click on polygon border
return true;
}
// ray casting algorithm for detecting if point is in polygon
for (i = 0, len = this._parts.length; i < len; i++) {
part = this._parts[i];
for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) {
p1 = part[j];
p2 = part[k];
if (((p1.y > p.y) !== (p2.y > p.y)) &&
(p.x < (p2.x - p1.x) * (p.y - p1.y) / (p2.y - p1.y) + p1.x)) {
inside = !inside;
}
}
}
return inside;
}
});
/*
* Extends L.Circle with Canvas-specific code.
*/
L.Circle.include(!L.Path.CANVAS ? {} : {
_drawPath: function () {
var p = this._point;
this._ctx.beginPath();
this._ctx.arc(p.x, p.y, this._radius, 0, Math.PI * 2, false);
},
_containsPoint: function (p) {
var center = this._point,
w2 = this.options.stroke ? this.options.weight / 2 : 0;
return (p.distanceTo(center) <= this._radius + w2);
}
});
/*
* CircleMarker canvas specific drawing parts.
*/
L.CircleMarker.include(!L.Path.CANVAS ? {} : {
_updateStyle: function () {
L.Path.prototype._updateStyle.call(this);
}
});
/*
* L.GeoJSON turns any GeoJSON data into a Leaflet layer.
*/
L.GeoJSON = L.FeatureGroup.extend({
initialize: function (geojson, options) {
L.setOptions(this, options);
this._layers = {};
if (geojson) {
this.addData(geojson);
}
},
addData: function (geojson) {
var features = L.Util.isArray(geojson) ? geojson : geojson.features,
i, len, feature;
if (features) {
for (i = 0, len = features.length; i < len; i++) {
// Only add this if geometry or geometries are set and not null
feature = features[i];
if (feature.geometries || feature.geometry || feature.features || feature.coordinates) {
this.addData(features[i]);
}
}
return this;
}
var options = this.options;
if (options.filter && !options.filter(geojson)) { return; }
var layer = L.GeoJSON.geometryToLayer(geojson, options.pointToLayer, options.coordsToLatLng, options);
layer.feature = L.GeoJSON.asFeature(geojson);
layer.defaultOptions = layer.options;
this.resetStyle(layer);
if (options.onEachFeature) {
options.onEachFeature(geojson, layer);
}
return this.addLayer(layer);
},
resetStyle: function (layer) {
var style = this.options.style;
if (style) {
// reset any custom styles
L.Util.extend(layer.options, layer.defaultOptions);
this._setLayerStyle(layer, style);
}
},
setStyle: function (style) {
this.eachLayer(function (layer) {
this._setLayerStyle(layer, style);
}, this);
},
_setLayerStyle: function (layer, style) {
if (typeof style === 'function') {
style = style(layer.feature);
}
if (layer.setStyle) {
layer.setStyle(style);
}
}
});
L.extend(L.GeoJSON, {
geometryToLayer: function (geojson, pointToLayer, coordsToLatLng, vectorOptions) {
var geometry = geojson.type === 'Feature' ? geojson.geometry : geojson,
coords = geometry.coordinates,
layers = [],
latlng, latlngs, i, len;
coordsToLatLng = coordsToLatLng || this.coordsToLatLng;
switch (geometry.type) {
case 'Point':
latlng = coordsToLatLng(coords);
return pointToLayer ? pointToLayer(geojson, latlng) : new L.Marker(latlng);
case 'MultiPoint':
for (i = 0, len = coords.length; i < len; i++) {
latlng = coordsToLatLng(coords[i]);
layers.push(pointToLayer ? pointToLayer(geojson, latlng) : new L.Marker(latlng));
}
return new L.FeatureGroup(layers);
case 'LineString':
latlngs = this.coordsToLatLngs(coords, 0, coordsToLatLng);
return new L.Polyline(latlngs, vectorOptions);
case 'Polygon':
if (coords.length === 2 && !coords[1].length) {
throw new Error('Invalid GeoJSON object.');
}
latlngs = this.coordsToLatLngs(coords, 1, coordsToLatLng);
return new L.Polygon(latlngs, vectorOptions);
case 'MultiLineString':
latlngs = this.coordsToLatLngs(coords, 1, coordsToLatLng);
return new L.MultiPolyline(latlngs, vectorOptions);
case 'MultiPolygon':
latlngs = this.coordsToLatLngs(coords, 2, coordsToLatLng);
return new L.MultiPolygon(latlngs, vectorOptions);
case 'GeometryCollection':
for (i = 0, len = geometry.geometries.length; i < len; i++) {
layers.push(this.geometryToLayer({
geometry: geometry.geometries[i],
type: 'Feature',
properties: geojson.properties
}, pointToLayer, coordsToLatLng, vectorOptions));
}
return new L.FeatureGroup(layers);
default:
throw new Error('Invalid GeoJSON object.');
}
},
coordsToLatLng: function (coords) { // (Array[, Boolean]) -> LatLng
return new L.LatLng(coords[1], coords[0], coords[2]);
},
coordsToLatLngs: function (coords, levelsDeep, coordsToLatLng) { // (Array[, Number, Function]) -> Array
var latlng, i, len,
latlngs = [];
for (i = 0, len = coords.length; i < len; i++) {
latlng = levelsDeep ?
this.coordsToLatLngs(coords[i], levelsDeep - 1, coordsToLatLng) :
(coordsToLatLng || this.coordsToLatLng)(coords[i]);
latlngs.push(latlng);
}
return latlngs;
},
latLngToCoords: function (latlng) {
var coords = [latlng.lng, latlng.lat];
if (latlng.alt !== undefined) {
coords.push(latlng.alt);
}
return coords;
},
latLngsToCoords: function (latLngs) {
var coords = [];
for (var i = 0, len = latLngs.length; i < len; i++) {
coords.push(L.GeoJSON.latLngToCoords(latLngs[i]));
}
return coords;
},
getFeature: function (layer, newGeometry) {
return layer.feature ? L.extend({}, layer.feature, {geometry: newGeometry}) : L.GeoJSON.asFeature(newGeometry);
},
asFeature: function (geoJSON) {
if (geoJSON.type === 'Feature') {
return geoJSON;
}
return {
type: 'Feature',
properties: {},
geometry: geoJSON
};
}
});
var PointToGeoJSON = {
toGeoJSON: function () {
return L.GeoJSON.getFeature(this, {
type: 'Point',
coordinates: L.GeoJSON.latLngToCoords(this.getLatLng())
});
}
};
L.Marker.include(PointToGeoJSON);
L.Circle.include(PointToGeoJSON);
L.CircleMarker.include(PointToGeoJSON);
L.Polyline.include({
toGeoJSON: function () {
return L.GeoJSON.getFeature(this, {
type: 'LineString',
coordinates: L.GeoJSON.latLngsToCoords(this.getLatLngs())
});
}
});
L.Polygon.include({
toGeoJSON: function () {
var coords = [L.GeoJSON.latLngsToCoords(this.getLatLngs())],
i, len, hole;
coords[0].push(coords[0][0]);
if (this._holes) {
for (i = 0, len = this._holes.length; i < len; i++) {
hole = L.GeoJSON.latLngsToCoords(this._holes[i]);
hole.push(hole[0]);
coords.push(hole);
}
}
return L.GeoJSON.getFeature(this, {
type: 'Polygon',
coordinates: coords
});
}
});
(function () {
function multiToGeoJSON(type) {
return function () {
var coords = [];
this.eachLayer(function (layer) {
coords.push(layer.toGeoJSON().geometry.coordinates);
});
return L.GeoJSON.getFeature(this, {
type: type,
coordinates: coords
});
};
}
L.MultiPolyline.include({toGeoJSON: multiToGeoJSON('MultiLineString')});
L.MultiPolygon.include({toGeoJSON: multiToGeoJSON('MultiPolygon')});
L.LayerGroup.include({
toGeoJSON: function () {
var geometry = this.feature && this.feature.geometry,
jsons = [],
json;
if (geometry && geometry.type === 'MultiPoint') {
return multiToGeoJSON('MultiPoint').call(this);
}
var isGeometryCollection = geometry && geometry.type === 'GeometryCollection';
this.eachLayer(function (layer) {
if (layer.toGeoJSON) {
json = layer.toGeoJSON();
jsons.push(isGeometryCollection ? json.geometry : L.GeoJSON.asFeature(json));
}
});
if (isGeometryCollection) {
return L.GeoJSON.getFeature(this, {
geometries: jsons,
type: 'GeometryCollection'
});
}
return {
type: 'FeatureCollection',
features: jsons
};
}
});
}());
L.geoJson = function (geojson, options) {
return new L.GeoJSON(geojson, options);
};
/*
* L.DomEvent contains functions for working with DOM events.
*/
L.DomEvent = {
/* inspired by John Resig, Dean Edwards and YUI addEvent implementations */
addListener: function (obj, type, fn, context) { // (HTMLElement, String, Function[, Object])
var id = L.stamp(fn),
key = '_leaflet_' + type + id,
handler, originalHandler, newType;
if (obj[key]) { return this; }
handler = function (e) {
return fn.call(context || obj, e || L.DomEvent._getEvent());
};
if (L.Browser.pointer && type.indexOf('touch') === 0) {
return this.addPointerListener(obj, type, handler, id);
}
if (L.Browser.touch && (type === 'dblclick') && this.addDoubleTapListener) {
this.addDoubleTapListener(obj, handler, id);
}
if ('addEventListener' in obj) {
if (type === 'mousewheel') {
obj.addEventListener('DOMMouseScroll', handler, false);
obj.addEventListener(type, handler, false);
} else if ((type === 'mouseenter') || (type === 'mouseleave')) {
originalHandler = handler;
newType = (type === 'mouseenter' ? 'mouseover' : 'mouseout');
handler = function (e) {
if (!L.DomEvent._checkMouse(obj, e)) { return; }
return originalHandler(e);
};
obj.addEventListener(newType, handler, false);
} else if (type === 'click' && L.Browser.android) {
originalHandler = handler;
handler = function (e) {
return L.DomEvent._filterClick(e, originalHandler);
};
obj.addEventListener(type, handler, false);
} else {
obj.addEventListener(type, handler, false);
}
} else if ('attachEvent' in obj) {
obj.attachEvent('on' + type, handler);
}
obj[key] = handler;
return this;
},
removeListener: function (obj, type, fn) { // (HTMLElement, String, Function)
var id = L.stamp(fn),
key = '_leaflet_' + type + id,
handler = obj[key];
if (!handler) { return this; }
if (L.Browser.pointer && type.indexOf('touch') === 0) {
this.removePointerListener(obj, type, id);
} else if (L.Browser.touch && (type === 'dblclick') && this.removeDoubleTapListener) {
this.removeDoubleTapListener(obj, id);
} else if ('removeEventListener' in obj) {
if (type === 'mousewheel') {
obj.removeEventListener('DOMMouseScroll', handler, false);
obj.removeEventListener(type, handler, false);
} else if ((type === 'mouseenter') || (type === 'mouseleave')) {
obj.removeEventListener((type === 'mouseenter' ? 'mouseover' : 'mouseout'), handler, false);
} else {
obj.removeEventListener(type, handler, false);
}
} else if ('detachEvent' in obj) {
obj.detachEvent('on' + type, handler);
}
obj[key] = null;
return this;
},
stopPropagation: function (e) {
if (e.stopPropagation) {
e.stopPropagation();
} else {
e.cancelBubble = true;
}
L.DomEvent._skipped(e);
return this;
},
disableScrollPropagation: function (el) {
var stop = L.DomEvent.stopPropagation;
return L.DomEvent
.on(el, 'mousewheel', stop)
.on(el, 'MozMousePixelScroll', stop);
},
disableClickPropagation: function (el) {
var stop = L.DomEvent.stopPropagation;
for (var i = L.Draggable.START.length - 1; i >= 0; i--) {
L.DomEvent.on(el, L.Draggable.START[i], stop);
}
return L.DomEvent
.on(el, 'click', L.DomEvent._fakeStop)
.on(el, 'dblclick', stop);
},
preventDefault: function (e) {
if (e.preventDefault) {
e.preventDefault();
} else {
e.returnValue = false;
}
return this;
},
stop: function (e) {
return L.DomEvent
.preventDefault(e)
.stopPropagation(e);
},
getMousePosition: function (e, container) {
if (!container) {
return new L.Point(e.clientX, e.clientY);
}
var rect = container.getBoundingClientRect();
return new L.Point(
e.clientX - rect.left - container.clientLeft,
e.clientY - rect.top - container.clientTop);
},
getWheelDelta: function (e) {
var delta = 0;
if (e.wheelDelta) {
delta = e.wheelDelta / 120;
}
if (e.detail) {
delta = -e.detail / 3;
}
return delta;
},
_skipEvents: {},
_fakeStop: function (e) {
// fakes stopPropagation by setting a special event flag, checked/reset with L.DomEvent._skipped(e)
L.DomEvent._skipEvents[e.type] = true;
},
_skipped: function (e) {
var skipped = this._skipEvents[e.type];
// reset when checking, as it's only used in map container and propagates outside of the map
this._skipEvents[e.type] = false;
return skipped;
},
// check if element really left/entered the event target (for mouseenter/mouseleave)
_checkMouse: function (el, e) {
var related = e.relatedTarget;
if (!related) { return true; }
try {
while (related && (related !== el)) {
related = related.parentNode;
}
} catch (err) {
return false;
}
return (related !== el);
},
_getEvent: function () { // evil magic for IE
/*jshint noarg:false */
var e = window.event;
if (!e) {
var caller = arguments.callee.caller;
while (caller) {
e = caller['arguments'][0];
if (e && window.Event === e.constructor) {
break;
}
caller = caller.caller;
}
}
return e;
},
// this is a horrible workaround for a bug in Android where a single touch triggers two click events
_filterClick: function (e, handler) {
var timeStamp = (e.timeStamp || e.originalEvent.timeStamp),
elapsed = L.DomEvent._lastClick && (timeStamp - L.DomEvent._lastClick);
// are they closer together than 500ms yet more than 100ms?
// Android typically triggers them ~300ms apart while multiple listeners
// on the same event should be triggered far faster;
// or check if click is simulated on the element, and if it is, reject any non-simulated events
if ((elapsed && elapsed > 100 && elapsed < 500) || (e.target._simulatedClick && !e._simulated)) {
L.DomEvent.stop(e);
return;
}
L.DomEvent._lastClick = timeStamp;
return handler(e);
}
};
L.DomEvent.on = L.DomEvent.addListener;
L.DomEvent.off = L.DomEvent.removeListener;
/*
* L.Draggable allows you to add dragging capabilities to any element. Supports mobile devices too.
*/
L.Draggable = L.Class.extend({
includes: L.Mixin.Events,
statics: {
START: L.Browser.touch ? ['touchstart', 'mousedown'] : ['mousedown'],
END: {
mousedown: 'mouseup',
touchstart: 'touchend',
pointerdown: 'touchend',
MSPointerDown: 'touchend'
},
MOVE: {
mousedown: 'mousemove',
touchstart: 'touchmove',
pointerdown: 'touchmove',
MSPointerDown: 'touchmove'
}
},
initialize: function (element, dragStartTarget) {
this._element = element;
this._dragStartTarget = dragStartTarget || element;
},
enable: function () {
if (this._enabled) { return; }
for (var i = L.Draggable.START.length - 1; i >= 0; i--) {
L.DomEvent.on(this._dragStartTarget, L.Draggable.START[i], this._onDown, this);
}
this._enabled = true;
},
disable: function () {
if (!this._enabled) { return; }
for (var i = L.Draggable.START.length - 1; i >= 0; i--) {
L.DomEvent.off(this._dragStartTarget, L.Draggable.START[i], this._onDown, this);
}
this._enabled = false;
this._moved = false;
},
_onDown: function (e) {
this._moved = false;
if (e.shiftKey || ((e.which !== 1) && (e.button !== 1) && !e.touches)) { return; }
L.DomEvent.stopPropagation(e);
if (L.Draggable._disabled) { return; }
L.DomUtil.disableImageDrag();
L.DomUtil.disableTextSelection();
if (this._moving) { return; }
var first = e.touches ? e.touches[0] : e;
this._startPoint = new L.Point(first.clientX, first.clientY);
this._startPos = this._newPos = L.DomUtil.getPosition(this._element);
L.DomEvent
.on(document, L.Draggable.MOVE[e.type], this._onMove, this)
.on(document, L.Draggable.END[e.type], this._onUp, this);
},
_onMove: function (e) {
if (e.touches && e.touches.length > 1) {
this._moved = true;
return;
}
var first = (e.touches && e.touches.length === 1 ? e.touches[0] : e),
newPoint = new L.Point(first.clientX, first.clientY),
offset = newPoint.subtract(this._startPoint);
if (!offset.x && !offset.y) { return; }
if (L.Browser.touch && Math.abs(offset.x) + Math.abs(offset.y) < 3) { return; }
L.DomEvent.preventDefault(e);
if (!this._moved) {
this.fire('dragstart');
this._moved = true;
this._startPos = L.DomUtil.getPosition(this._element).subtract(offset);
L.DomUtil.addClass(document.body, 'leaflet-dragging');
this._lastTarget = e.target || e.srcElement;
L.DomUtil.addClass(this._lastTarget, 'leaflet-drag-target');
}
this._newPos = this._startPos.add(offset);
this._moving = true;
L.Util.cancelAnimFrame(this._animRequest);
this._animRequest = L.Util.requestAnimFrame(this._updatePosition, this, true, this._dragStartTarget);
},
_updatePosition: function () {
this.fire('predrag');
L.DomUtil.setPosition(this._element, this._newPos);
this.fire('drag');
},
_onUp: function () {
L.DomUtil.removeClass(document.body, 'leaflet-dragging');
if (this._lastTarget) {
L.DomUtil.removeClass(this._lastTarget, 'leaflet-drag-target');
this._lastTarget = null;
}
for (var i in L.Draggable.MOVE) {
L.DomEvent
.off(document, L.Draggable.MOVE[i], this._onMove)
.off(document, L.Draggable.END[i], this._onUp);
}
L.DomUtil.enableImageDrag();
L.DomUtil.enableTextSelection();
if (this._moved && this._moving) {
// ensure drag is not fired after dragend
L.Util.cancelAnimFrame(this._animRequest);
this.fire('dragend', {
distance: this._newPos.distanceTo(this._startPos)
});
}
this._moving = false;
}
});
/*
L.Handler is a base class for handler classes that are used internally to inject
interaction features like dragging to classes like Map and Marker.
*/
L.Handler = L.Class.extend({
initialize: function (map) {
this._map = map;
},
enable: function () {
if (this._enabled) { return; }
this._enabled = true;
this.addHooks();
},
disable: function () {
if (!this._enabled) { return; }
this._enabled = false;
this.removeHooks();
},
enabled: function () {
return !!this._enabled;
}
});
/*
* L.Handler.MapDrag is used to make the map draggable (with panning inertia), enabled by default.
*/
L.Map.mergeOptions({
dragging: true,
inertia: !L.Browser.android23,
inertiaDeceleration: 3400, // px/s^2
inertiaMaxSpeed: Infinity, // px/s
inertiaThreshold: L.Browser.touch ? 32 : 18, // ms
easeLinearity: 0.25,
// TODO refactor, move to CRS
worldCopyJump: false
});
L.Map.Drag = L.Handler.extend({
addHooks: function () {
if (!this._draggable) {
var map = this._map;
this._draggable = new L.Draggable(map._mapPane, map._container);
this._draggable.on({
'dragstart': this._onDragStart,
'drag': this._onDrag,
'dragend': this._onDragEnd
}, this);
if (map.options.worldCopyJump) {
this._draggable.on('predrag', this._onPreDrag, this);
map.on('viewreset', this._onViewReset, this);
map.whenReady(this._onViewReset, this);
}
}
this._draggable.enable();
},
removeHooks: function () {
this._draggable.disable();
},
moved: function () {
return this._draggable && this._draggable._moved;
},
_onDragStart: function () {
var map = this._map;
if (map._panAnim) {
map._panAnim.stop();
}
map
.fire('movestart')
.fire('dragstart');
if (map.options.inertia) {
this._positions = [];
this._times = [];
}
},
_onDrag: function () {
if (this._map.options.inertia) {
var time = this._lastTime = +new Date(),
pos = this._lastPos = this._draggable._newPos;
this._positions.push(pos);
this._times.push(time);
if (time - this._times[0] > 200) {
this._positions.shift();
this._times.shift();
}
}
this._map
.fire('move')
.fire('drag');
},
_onViewReset: function () {
// TODO fix hardcoded Earth values
var pxCenter = this._map.getSize()._divideBy(2),
pxWorldCenter = this._map.latLngToLayerPoint([0, 0]);
this._initialWorldOffset = pxWorldCenter.subtract(pxCenter).x;
this._worldWidth = this._map.project([0, 180]).x;
},
_onPreDrag: function () {
// TODO refactor to be able to adjust map pane position after zoom
var worldWidth = this._worldWidth,
halfWidth = Math.round(worldWidth / 2),
dx = this._initialWorldOffset,
x = this._draggable._newPos.x,
newX1 = (x - halfWidth + dx) % worldWidth + halfWidth - dx,
newX2 = (x + halfWidth + dx) % worldWidth - halfWidth - dx,
newX = Math.abs(newX1 + dx) < Math.abs(newX2 + dx) ? newX1 : newX2;
this._draggable._newPos.x = newX;
},
_onDragEnd: function (e) {
var map = this._map,
options = map.options,
delay = +new Date() - this._lastTime,
noInertia = !options.inertia || delay > options.inertiaThreshold || !this._positions[0];
map.fire('dragend', e);
if (noInertia) {
map.fire('moveend');
} else {
var direction = this._lastPos.subtract(this._positions[0]),
duration = (this._lastTime + delay - this._times[0]) / 1000,
ease = options.easeLinearity,
speedVector = direction.multiplyBy(ease / duration),
speed = speedVector.distanceTo([0, 0]),
limitedSpeed = Math.min(options.inertiaMaxSpeed, speed),
limitedSpeedVector = speedVector.multiplyBy(limitedSpeed / speed),
decelerationDuration = limitedSpeed / (options.inertiaDeceleration * ease),
offset = limitedSpeedVector.multiplyBy(-decelerationDuration / 2).round();
if (!offset.x || !offset.y) {
map.fire('moveend');
} else {
offset = map._limitOffset(offset, map.options.maxBounds);
L.Util.requestAnimFrame(function () {
map.panBy(offset, {
duration: decelerationDuration,
easeLinearity: ease,
noMoveStart: true
});
});
}
}
}
});
L.Map.addInitHook('addHandler', 'dragging', L.Map.Drag);
/*
* L.Handler.DoubleClickZoom is used to handle double-click zoom on the map, enabled by default.
*/
L.Map.mergeOptions({
doubleClickZoom: true
});
L.Map.DoubleClickZoom = L.Handler.extend({
addHooks: function () {
this._map.on('dblclick', this._onDoubleClick, this);
},
removeHooks: function () {
this._map.off('dblclick', this._onDoubleClick, this);
},
_onDoubleClick: function (e) {
var map = this._map,
zoom = map.getZoom() + (e.originalEvent.shiftKey ? -1 : 1);
if (map.options.doubleClickZoom === 'center') {
map.setZoom(zoom);
} else {
map.setZoomAround(e.containerPoint, zoom);
}
}
});
L.Map.addInitHook('addHandler', 'doubleClickZoom', L.Map.DoubleClickZoom);
/*
* L.Handler.ScrollWheelZoom is used by L.Map to enable mouse scroll wheel zoom on the map.
*/
L.Map.mergeOptions({
scrollWheelZoom: true
});
L.Map.ScrollWheelZoom = L.Handler.extend({
addHooks: function () {
L.DomEvent.on(this._map._container, 'mousewheel', this._onWheelScroll, this);
L.DomEvent.on(this._map._container, 'MozMousePixelScroll', L.DomEvent.preventDefault);
this._delta = 0;
},
removeHooks: function () {
L.DomEvent.off(this._map._container, 'mousewheel', this._onWheelScroll);
L.DomEvent.off(this._map._container, 'MozMousePixelScroll', L.DomEvent.preventDefault);
},
_onWheelScroll: function (e) {
var delta = L.DomEvent.getWheelDelta(e);
this._delta += delta;
this._lastMousePos = this._map.mouseEventToContainerPoint(e);
if (!this._startTime) {
this._startTime = +new Date();
}
var left = Math.max(40 - (+new Date() - this._startTime), 0);
clearTimeout(this._timer);
this._timer = setTimeout(L.bind(this._performZoom, this), left);
L.DomEvent.preventDefault(e);
L.DomEvent.stopPropagation(e);
},
_performZoom: function () {
var map = this._map,
delta = this._delta,
zoom = map.getZoom();
delta = delta > 0 ? Math.ceil(delta) : Math.floor(delta);
delta = Math.max(Math.min(delta, 4), -4);
delta = map._limitZoom(zoom + delta) - zoom;
this._delta = 0;
this._startTime = null;
if (!delta) { return; }
if (map.options.scrollWheelZoom === 'center') {
map.setZoom(zoom + delta);
} else {
map.setZoomAround(this._lastMousePos, zoom + delta);
}
}
});
L.Map.addInitHook('addHandler', 'scrollWheelZoom', L.Map.ScrollWheelZoom);
/*
* Extends the event handling code with double tap support for mobile browsers.
*/
L.extend(L.DomEvent, {
_touchstart: L.Browser.msPointer ? 'MSPointerDown' : L.Browser.pointer ? 'pointerdown' : 'touchstart',
_touchend: L.Browser.msPointer ? 'MSPointerUp' : L.Browser.pointer ? 'pointerup' : 'touchend',
// inspired by Zepto touch code by Thomas Fuchs
addDoubleTapListener: function (obj, handler, id) {
var last,
doubleTap = false,
delay = 250,
touch,
pre = '_leaflet_',
touchstart = this._touchstart,
touchend = this._touchend,
trackedTouches = [];
function onTouchStart(e) {
var count;
if (L.Browser.pointer) {
trackedTouches.push(e.pointerId);
count = trackedTouches.length;
} else {
count = e.touches.length;
}
if (count > 1) {
return;
}
var now = Date.now(),
delta = now - (last || now);
touch = e.touches ? e.touches[0] : e;
doubleTap = (delta > 0 && delta <= delay);
last = now;
}
function onTouchEnd(e) {
if (L.Browser.pointer) {
var idx = trackedTouches.indexOf(e.pointerId);
if (idx === -1) {
return;
}
trackedTouches.splice(idx, 1);
}
if (doubleTap) {
if (L.Browser.pointer) {
// work around .type being readonly with MSPointer* events
var newTouch = { },
prop;
// jshint forin:false
for (var i in touch) {
prop = touch[i];
if (typeof prop === 'function') {
newTouch[i] = prop.bind(touch);
} else {
newTouch[i] = prop;
}
}
touch = newTouch;
}
touch.type = 'dblclick';
handler(touch);
last = null;
}
}
obj[pre + touchstart + id] = onTouchStart;
obj[pre + touchend + id] = onTouchEnd;
// on pointer we need to listen on the document, otherwise a drag starting on the map and moving off screen
// will not come through to us, so we will lose track of how many touches are ongoing
var endElement = L.Browser.pointer ? document.documentElement : obj;
obj.addEventListener(touchstart, onTouchStart, false);
endElement.addEventListener(touchend, onTouchEnd, false);
if (L.Browser.pointer) {
endElement.addEventListener(L.DomEvent.POINTER_CANCEL, onTouchEnd, false);
}
return this;
},
removeDoubleTapListener: function (obj, id) {
var pre = '_leaflet_';
obj.removeEventListener(this._touchstart, obj[pre + this._touchstart + id], false);
(L.Browser.pointer ? document.documentElement : obj).removeEventListener(
this._touchend, obj[pre + this._touchend + id], false);
if (L.Browser.pointer) {
document.documentElement.removeEventListener(L.DomEvent.POINTER_CANCEL, obj[pre + this._touchend + id],
false);
}
return this;
}
});
/*
* Extends L.DomEvent to provide touch support for Internet Explorer and Windows-based devices.
*/
L.extend(L.DomEvent, {
//static
POINTER_DOWN: L.Browser.msPointer ? 'MSPointerDown' : 'pointerdown',
POINTER_MOVE: L.Browser.msPointer ? 'MSPointerMove' : 'pointermove',
POINTER_UP: L.Browser.msPointer ? 'MSPointerUp' : 'pointerup',
POINTER_CANCEL: L.Browser.msPointer ? 'MSPointerCancel' : 'pointercancel',
_pointers: [],
_pointerDocumentListener: false,
// Provides a touch events wrapper for (ms)pointer events.
// Based on changes by veproza https://github.com/CloudMade/Leaflet/pull/1019
//ref http://www.w3.org/TR/pointerevents/ https://www.w3.org/Bugs/Public/show_bug.cgi?id=22890
addPointerListener: function (obj, type, handler, id) {
switch (type) {
case 'touchstart':
return this.addPointerListenerStart(obj, type, handler, id);
case 'touchend':
return this.addPointerListenerEnd(obj, type, handler, id);
case 'touchmove':
return this.addPointerListenerMove(obj, type, handler, id);
default:
throw 'Unknown touch event type';
}
},
addPointerListenerStart: function (obj, type, handler, id) {
var pre = '_leaflet_',
pointers = this._pointers;
var cb = function (e) {
if (e.pointerType !== 'mouse' && e.pointerType !== e.MSPOINTER_TYPE_MOUSE) {
L.DomEvent.preventDefault(e);
}
var alreadyInArray = false;
for (var i = 0; i < pointers.length; i++) {
if (pointers[i].pointerId === e.pointerId) {
alreadyInArray = true;
break;
}
}
if (!alreadyInArray) {
pointers.push(e);
}
e.touches = pointers.slice();
e.changedTouches = [e];
handler(e);
};
obj[pre + 'touchstart' + id] = cb;
obj.addEventListener(this.POINTER_DOWN, cb, false);
// need to also listen for end events to keep the _pointers list accurate
// this needs to be on the body and never go away
if (!this._pointerDocumentListener) {
var internalCb = function (e) {
for (var i = 0; i < pointers.length; i++) {
if (pointers[i].pointerId === e.pointerId) {
pointers.splice(i, 1);
break;
}
}
};
//We listen on the documentElement as any drags that end by moving the touch off the screen get fired there
document.documentElement.addEventListener(this.POINTER_UP, internalCb, false);
document.documentElement.addEventListener(this.POINTER_CANCEL, internalCb, false);
this._pointerDocumentListener = true;
}
return this;
},
addPointerListenerMove: function (obj, type, handler, id) {
var pre = '_leaflet_',
touches = this._pointers;
function cb(e) {
// don't fire touch moves when mouse isn't down
if ((e.pointerType === e.MSPOINTER_TYPE_MOUSE || e.pointerType === 'mouse') && e.buttons === 0) { return; }
for (var i = 0; i < touches.length; i++) {
if (touches[i].pointerId === e.pointerId) {
touches[i] = e;
break;
}
}
e.touches = touches.slice();
e.changedTouches = [e];
handler(e);
}
obj[pre + 'touchmove' + id] = cb;
obj.addEventListener(this.POINTER_MOVE, cb, false);
return this;
},
addPointerListenerEnd: function (obj, type, handler, id) {
var pre = '_leaflet_',
touches = this._pointers;
var cb = function (e) {
for (var i = 0; i < touches.length; i++) {
if (touches[i].pointerId === e.pointerId) {
touches.splice(i, 1);
break;
}
}
e.touches = touches.slice();
e.changedTouches = [e];
handler(e);
};
obj[pre + 'touchend' + id] = cb;
obj.addEventListener(this.POINTER_UP, cb, false);
obj.addEventListener(this.POINTER_CANCEL, cb, false);
return this;
},
removePointerListener: function (obj, type, id) {
var pre = '_leaflet_',
cb = obj[pre + type + id];
switch (type) {
case 'touchstart':
obj.removeEventListener(this.POINTER_DOWN, cb, false);
break;
case 'touchmove':
obj.removeEventListener(this.POINTER_MOVE, cb, false);
break;
case 'touchend':
obj.removeEventListener(this.POINTER_UP, cb, false);
obj.removeEventListener(this.POINTER_CANCEL, cb, false);
break;
}
return this;
}
});
/*
* L.Handler.TouchZoom is used by L.Map to add pinch zoom on supported mobile browsers.
*/
L.Map.mergeOptions({
touchZoom: L.Browser.touch && !L.Browser.android23,
bounceAtZoomLimits: true
});
L.Map.TouchZoom = L.Handler.extend({
addHooks: function () {
L.DomEvent.on(this._map._container, 'touchstart', this._onTouchStart, this);
},
removeHooks: function () {
L.DomEvent.off(this._map._container, 'touchstart', this._onTouchStart, this);
},
_onTouchStart: function (e) {
var map = this._map;
if (!e.touches || e.touches.length !== 2 || map._animatingZoom || this._zooming) { return; }
var p1 = map.mouseEventToLayerPoint(e.touches[0]),
p2 = map.mouseEventToLayerPoint(e.touches[1]),
viewCenter = map._getCenterLayerPoint();
this._startCenter = p1.add(p2)._divideBy(2);
this._startDist = p1.distanceTo(p2);
this._moved = false;
this._zooming = true;
this._centerOffset = viewCenter.subtract(this._startCenter);
if (map._panAnim) {
map._panAnim.stop();
}
L.DomEvent
.on(document, 'touchmove', this._onTouchMove, this)
.on(document, 'touchend', this._onTouchEnd, this);
L.DomEvent.preventDefault(e);
},
_onTouchMove: function (e) {
var map = this._map;
if (!e.touches || e.touches.length !== 2 || !this._zooming) { return; }
var p1 = map.mouseEventToLayerPoint(e.touches[0]),
p2 = map.mouseEventToLayerPoint(e.touches[1]);
this._scale = p1.distanceTo(p2) / this._startDist;
this._delta = p1._add(p2)._divideBy(2)._subtract(this._startCenter);
if (this._scale === 1) { return; }
if (!map.options.bounceAtZoomLimits) {
if ((map.getZoom() === map.getMinZoom() && this._scale < 1) ||
(map.getZoom() === map.getMaxZoom() && this._scale > 1)) { return; }
}
if (!this._moved) {
L.DomUtil.addClass(map._mapPane, 'leaflet-touching');
map
.fire('movestart')
.fire('zoomstart');
this._moved = true;
}
L.Util.cancelAnimFrame(this._animRequest);
this._animRequest = L.Util.requestAnimFrame(
this._updateOnMove, this, true, this._map._container);
L.DomEvent.preventDefault(e);
},
_updateOnMove: function () {
var map = this._map,
origin = this._getScaleOrigin(),
center = map.layerPointToLatLng(origin),
zoom = map.getScaleZoom(this._scale);
map._animateZoom(center, zoom, this._startCenter, this._scale, this._delta, false, true);
},
_onTouchEnd: function () {
if (!this._moved || !this._zooming) {
this._zooming = false;
return;
}
var map = this._map;
this._zooming = false;
L.DomUtil.removeClass(map._mapPane, 'leaflet-touching');
L.Util.cancelAnimFrame(this._animRequest);
L.DomEvent
.off(document, 'touchmove', this._onTouchMove)
.off(document, 'touchend', this._onTouchEnd);
var origin = this._getScaleOrigin(),
center = map.layerPointToLatLng(origin),
oldZoom = map.getZoom(),
floatZoomDelta = map.getScaleZoom(this._scale) - oldZoom,
roundZoomDelta = (floatZoomDelta > 0 ?
Math.ceil(floatZoomDelta) : Math.floor(floatZoomDelta)),
zoom = map._limitZoom(oldZoom + roundZoomDelta),
scale = map.getZoomScale(zoom) / this._scale;
map._animateZoom(center, zoom, origin, scale);
},
_getScaleOrigin: function () {
var centerOffset = this._centerOffset.subtract(this._delta).divideBy(this._scale);
return this._startCenter.add(centerOffset);
}
});
L.Map.addInitHook('addHandler', 'touchZoom', L.Map.TouchZoom);
/*
* L.Map.Tap is used to enable mobile hacks like quick taps and long hold.
*/
L.Map.mergeOptions({
tap: true,
tapTolerance: 15
});
L.Map.Tap = L.Handler.extend({
addHooks: function () {
L.DomEvent.on(this._map._container, 'touchstart', this._onDown, this);
},
removeHooks: function () {
L.DomEvent.off(this._map._container, 'touchstart', this._onDown, this);
},
_onDown: function (e) {
if (!e.touches) { return; }
L.DomEvent.preventDefault(e);
this._fireClick = true;
// don't simulate click or track longpress if more than 1 touch
if (e.touches.length > 1) {
this._fireClick = false;
clearTimeout(this._holdTimeout);
return;
}
var first = e.touches[0],
el = first.target;
this._startPos = this._newPos = new L.Point(first.clientX, first.clientY);
// if touching a link, highlight it
if (el.tagName && el.tagName.toLowerCase() === 'a') {
L.DomUtil.addClass(el, 'leaflet-active');
}
// simulate long hold but setting a timeout
this._holdTimeout = setTimeout(L.bind(function () {
if (this._isTapValid()) {
this._fireClick = false;
this._onUp();
this._simulateEvent('contextmenu', first);
}
}, this), 1000);
L.DomEvent
.on(document, 'touchmove', this._onMove, this)
.on(document, 'touchend', this._onUp, this);
},
_onUp: function (e) {
clearTimeout(this._holdTimeout);
L.DomEvent
.off(document, 'touchmove', this._onMove, this)
.off(document, 'touchend', this._onUp, this);
if (this._fireClick && e && e.changedTouches) {
var first = e.changedTouches[0],
el = first.target;
if (el && el.tagName && el.tagName.toLowerCase() === 'a') {
L.DomUtil.removeClass(el, 'leaflet-active');
}
// simulate click if the touch didn't move too much
if (this._isTapValid()) {
this._simulateEvent('click', first);
}
}
},
_isTapValid: function () {
return this._newPos.distanceTo(this._startPos) <= this._map.options.tapTolerance;
},
_onMove: function (e) {
var first = e.touches[0];
this._newPos = new L.Point(first.clientX, first.clientY);
},
_simulateEvent: function (type, e) {
var simulatedEvent = document.createEvent('MouseEvents');
simulatedEvent._simulated = true;
e.target._simulatedClick = true;
simulatedEvent.initMouseEvent(
type, true, true, window, 1,
e.screenX, e.screenY,
e.clientX, e.clientY,
false, false, false, false, 0, null);
e.target.dispatchEvent(simulatedEvent);
}
});
if (L.Browser.touch && !L.Browser.pointer) {
L.Map.addInitHook('addHandler', 'tap', L.Map.Tap);
}
/*
* L.Handler.ShiftDragZoom is used to add shift-drag zoom interaction to the map
* (zoom to a selected bounding box), enabled by default.
*/
L.Map.mergeOptions({
boxZoom: true
});
L.Map.BoxZoom = L.Handler.extend({
initialize: function (map) {
this._map = map;
this._container = map._container;
this._pane = map._panes.overlayPane;
this._moved = false;
},
addHooks: function () {
L.DomEvent.on(this._container, 'mousedown', this._onMouseDown, this);
},
removeHooks: function () {
L.DomEvent.off(this._container, 'mousedown', this._onMouseDown);
this._moved = false;
},
moved: function () {
return this._moved;
},
_onMouseDown: function (e) {
this._moved = false;
if (!e.shiftKey || ((e.which !== 1) && (e.button !== 1))) { return false; }
L.DomUtil.disableTextSelection();
L.DomUtil.disableImageDrag();
this._startLayerPoint = this._map.mouseEventToLayerPoint(e);
L.DomEvent
.on(document, 'mousemove', this._onMouseMove, this)
.on(document, 'mouseup', this._onMouseUp, this)
.on(document, 'keydown', this._onKeyDown, this);
},
_onMouseMove: function (e) {
if (!this._moved) {
this._box = L.DomUtil.create('div', 'leaflet-zoom-box', this._pane);
L.DomUtil.setPosition(this._box, this._startLayerPoint);
//TODO refactor: move cursor to styles
this._container.style.cursor = 'crosshair';
this._map.fire('boxzoomstart');
}
var startPoint = this._startLayerPoint,
box = this._box,
layerPoint = this._map.mouseEventToLayerPoint(e),
offset = layerPoint.subtract(startPoint),
newPos = new L.Point(
Math.min(layerPoint.x, startPoint.x),
Math.min(layerPoint.y, startPoint.y));
L.DomUtil.setPosition(box, newPos);
this._moved = true;
// TODO refactor: remove hardcoded 4 pixels
box.style.width = (Math.max(0, Math.abs(offset.x) - 4)) + 'px';
box.style.height = (Math.max(0, Math.abs(offset.y) - 4)) + 'px';
},
_finish: function () {
if (this._moved) {
this._pane.removeChild(this._box);
this._container.style.cursor = '';
}
L.DomUtil.enableTextSelection();
L.DomUtil.enableImageDrag();
L.DomEvent
.off(document, 'mousemove', this._onMouseMove)
.off(document, 'mouseup', this._onMouseUp)
.off(document, 'keydown', this._onKeyDown);
},
_onMouseUp: function (e) {
this._finish();
var map = this._map,
layerPoint = map.mouseEventToLayerPoint(e);
if (this._startLayerPoint.equals(layerPoint)) { return; }
var bounds = new L.LatLngBounds(
map.layerPointToLatLng(this._startLayerPoint),
map.layerPointToLatLng(layerPoint));
map.fitBounds(bounds);
map.fire('boxzoomend', {
boxZoomBounds: bounds
});
},
_onKeyDown: function (e) {
if (e.keyCode === 27) {
this._finish();
}
}
});
L.Map.addInitHook('addHandler', 'boxZoom', L.Map.BoxZoom);
/*
* L.Map.Keyboard is handling keyboard interaction with the map, enabled by default.
*/
L.Map.mergeOptions({
keyboard: true,
keyboardPanOffset: 80,
keyboardZoomOffset: 1
});
L.Map.Keyboard = L.Handler.extend({
keyCodes: {
left: [37],
right: [39],
down: [40],
up: [38],
zoomIn: [187, 107, 61, 171],
zoomOut: [189, 109, 173]
},
initialize: function (map) {
this._map = map;
this._setPanOffset(map.options.keyboardPanOffset);
this._setZoomOffset(map.options.keyboardZoomOffset);
},
addHooks: function () {
var container = this._map._container;
// make the container focusable by tabbing
if (container.tabIndex === -1) {
container.tabIndex = '0';
}
L.DomEvent
.on(container, 'focus', this._onFocus, this)
.on(container, 'blur', this._onBlur, this)
.on(container, 'mousedown', this._onMouseDown, this);
this._map
.on('focus', this._addHooks, this)
.on('blur', this._removeHooks, this);
},
removeHooks: function () {
this._removeHooks();
var container = this._map._container;
L.DomEvent
.off(container, 'focus', this._onFocus, this)
.off(container, 'blur', this._onBlur, this)
.off(container, 'mousedown', this._onMouseDown, this);
this._map
.off('focus', this._addHooks, this)
.off('blur', this._removeHooks, this);
},
_onMouseDown: function () {
if (this._focused) { return; }
var body = document.body,
docEl = document.documentElement,
top = body.scrollTop || docEl.scrollTop,
left = body.scrollLeft || docEl.scrollLeft;
this._map._container.focus();
window.scrollTo(left, top);
},
_onFocus: function () {
this._focused = true;
this._map.fire('focus');
},
_onBlur: function () {
this._focused = false;
this._map.fire('blur');
},
_setPanOffset: function (pan) {
var keys = this._panKeys = {},
codes = this.keyCodes,
i, len;
for (i = 0, len = codes.left.length; i < len; i++) {
keys[codes.left[i]] = [-1 * pan, 0];
}
for (i = 0, len = codes.right.length; i < len; i++) {
keys[codes.right[i]] = [pan, 0];
}
for (i = 0, len = codes.down.length; i < len; i++) {
keys[codes.down[i]] = [0, pan];
}
for (i = 0, len = codes.up.length; i < len; i++) {
keys[codes.up[i]] = [0, -1 * pan];
}
},
_setZoomOffset: function (zoom) {
var keys = this._zoomKeys = {},
codes = this.keyCodes,
i, len;
for (i = 0, len = codes.zoomIn.length; i < len; i++) {
keys[codes.zoomIn[i]] = zoom;
}
for (i = 0, len = codes.zoomOut.length; i < len; i++) {
keys[codes.zoomOut[i]] = -zoom;
}
},
_addHooks: function () {
L.DomEvent.on(document, 'keydown', this._onKeyDown, this);
},
_removeHooks: function () {
L.DomEvent.off(document, 'keydown', this._onKeyDown, this);
},
_onKeyDown: function (e) {
var key = e.keyCode,
map = this._map;
if (key in this._panKeys) {
if (map._panAnim && map._panAnim._inProgress) { return; }
map.panBy(this._panKeys[key]);
if (map.options.maxBounds) {
map.panInsideBounds(map.options.maxBounds);
}
} else if (key in this._zoomKeys) {
map.setZoom(map.getZoom() + this._zoomKeys[key]);
} else {
return;
}
L.DomEvent.stop(e);
}
});
L.Map.addInitHook('addHandler', 'keyboard', L.Map.Keyboard);
/*
* L.Handler.MarkerDrag is used internally by L.Marker to make the markers draggable.
*/
L.Handler.MarkerDrag = L.Handler.extend({
initialize: function (marker) {
this._marker = marker;
},
addHooks: function () {
var icon = this._marker._icon;
if (!this._draggable) {
this._draggable = new L.Draggable(icon, icon);
}
this._draggable
.on('dragstart', this._onDragStart, this)
.on('drag', this._onDrag, this)
.on('dragend', this._onDragEnd, this);
this._draggable.enable();
L.DomUtil.addClass(this._marker._icon, 'leaflet-marker-draggable');
},
removeHooks: function () {
this._draggable
.off('dragstart', this._onDragStart, this)
.off('drag', this._onDrag, this)
.off('dragend', this._onDragEnd, this);
this._draggable.disable();
L.DomUtil.removeClass(this._marker._icon, 'leaflet-marker-draggable');
},
moved: function () {
return this._draggable && this._draggable._moved;
},
_onDragStart: function () {
this._marker
.closePopup()
.fire('movestart')
.fire('dragstart');
},
_onDrag: function () {
var marker = this._marker,
shadow = marker._shadow,
iconPos = L.DomUtil.getPosition(marker._icon),
latlng = marker._map.layerPointToLatLng(iconPos);
// update shadow position
if (shadow) {
L.DomUtil.setPosition(shadow, iconPos);
}
marker._latlng = latlng;
marker
.fire('move', {latlng: latlng})
.fire('drag');
},
_onDragEnd: function (e) {
this._marker
.fire('moveend')
.fire('dragend', e);
}
});
/*
* L.Control is a base class for implementing map controls. Handles positioning.
* All other controls extend from this class.
*/
L.Control = L.Class.extend({
options: {
position: 'topright'
},
initialize: function (options) {
L.setOptions(this, options);
},
getPosition: function () {
return this.options.position;
},
setPosition: function (position) {
var map = this._map;
if (map) {
map.removeControl(this);
}
this.options.position = position;
if (map) {
map.addControl(this);
}
return this;
},
getContainer: function () {
return this._container;
},
addTo: function (map) {
this._map = map;
var container = this._container = this.onAdd(map),
pos = this.getPosition(),
corner = map._controlCorners[pos];
L.DomUtil.addClass(container, 'leaflet-control');
if (pos.indexOf('bottom') !== -1) {
corner.insertBefore(container, corner.firstChild);
} else {
corner.appendChild(container);
}
return this;
},
removeFrom: function (map) {
var pos = this.getPosition(),
corner = map._controlCorners[pos];
corner.removeChild(this._container);
this._map = null;
if (this.onRemove) {
this.onRemove(map);
}
return this;
},
_refocusOnMap: function () {
if (this._map) {
this._map.getContainer().focus();
}
}
});
L.control = function (options) {
return new L.Control(options);
};
// adds control-related methods to L.Map
L.Map.include({
addControl: function (control) {
control.addTo(this);
return this;
},
removeControl: function (control) {
control.removeFrom(this);
return this;
},
_initControlPos: function () {
var corners = this._controlCorners = {},
l = 'leaflet-',
container = this._controlContainer =
L.DomUtil.create('div', l + 'control-container', this._container);
function createCorner(vSide, hSide) {
var className = l + vSide + ' ' + l + hSide;
corners[vSide + hSide] = L.DomUtil.create('div', className, container);
}
createCorner('top', 'left');
createCorner('top', 'right');
createCorner('bottom', 'left');
createCorner('bottom', 'right');
},
_clearControlPos: function () {
this._container.removeChild(this._controlContainer);
}
});
/*
* L.Control.Zoom is used for the default zoom buttons on the map.
*/
L.Control.Zoom = L.Control.extend({
options: {
position: 'topleft',
zoomInText: '+',
zoomInTitle: 'Zoom in',
zoomOutText: '-',
zoomOutTitle: 'Zoom out'
},
onAdd: function (map) {
var zoomName = 'leaflet-control-zoom',
container = L.DomUtil.create('div', zoomName + ' leaflet-bar');
this._map = map;
this._zoomInButton = this._createButton(
this.options.zoomInText, this.options.zoomInTitle,
zoomName + '-in', container, this._zoomIn, this);
this._zoomOutButton = this._createButton(
this.options.zoomOutText, this.options.zoomOutTitle,
zoomName + '-out', container, this._zoomOut, this);
this._updateDisabled();
map.on('zoomend zoomlevelschange', this._updateDisabled, this);
return container;
},
onRemove: function (map) {
map.off('zoomend zoomlevelschange', this._updateDisabled, this);
},
_zoomIn: function (e) {
this._map.zoomIn(e.shiftKey ? 3 : 1);
},
_zoomOut: function (e) {
this._map.zoomOut(e.shiftKey ? 3 : 1);
},
_createButton: function (html, title, className, container, fn, context) {
var link = L.DomUtil.create('a', className, container);
link.innerHTML = html;
link.href = '#';
link.title = title;
var stop = L.DomEvent.stopPropagation;
L.DomEvent
.on(link, 'click', stop)
.on(link, 'mousedown', stop)
.on(link, 'dblclick', stop)
.on(link, 'click', L.DomEvent.preventDefault)
.on(link, 'click', fn, context)
.on(link, 'click', this._refocusOnMap, context);
return link;
},
_updateDisabled: function () {
var map = this._map,
className = 'leaflet-disabled';
L.DomUtil.removeClass(this._zoomInButton, className);
L.DomUtil.removeClass(this._zoomOutButton, className);
if (map._zoom === map.getMinZoom()) {
L.DomUtil.addClass(this._zoomOutButton, className);
}
if (map._zoom === map.getMaxZoom()) {
L.DomUtil.addClass(this._zoomInButton, className);
}
}
});
L.Map.mergeOptions({
zoomControl: true
});
L.Map.addInitHook(function () {
if (this.options.zoomControl) {
this.zoomControl = new L.Control.Zoom();
this.addControl(this.zoomControl);
}
});
L.control.zoom = function (options) {
return new L.Control.Zoom(options);
};
/*
* L.Control.Attribution is used for displaying attribution on the map (added by default).
*/
L.Control.Attribution = L.Control.extend({
options: {
position: 'bottomright',
prefix: '<a href="http://leafletjs.com" title="A JS library for interactive maps">Leaflet</a>'
},
initialize: function (options) {
L.setOptions(this, options);
this._attributions = {};
},
onAdd: function (map) {
this._container = L.DomUtil.create('div', 'leaflet-control-attribution');
L.DomEvent.disableClickPropagation(this._container);
for (var i in map._layers) {
if (map._layers[i].getAttribution) {
this.addAttribution(map._layers[i].getAttribution());
}
}
map
.on('layeradd', this._onLayerAdd, this)
.on('layerremove', this._onLayerRemove, this);
this._update();
return this._container;
},
onRemove: function (map) {
map
.off('layeradd', this._onLayerAdd)
.off('layerremove', this._onLayerRemove);
},
setPrefix: function (prefix) {
this.options.prefix = prefix;
this._update();
return this;
},
addAttribution: function (text) {
if (!text) { return; }
if (!this._attributions[text]) {
this._attributions[text] = 0;
}
this._attributions[text]++;
this._update();
return this;
},
removeAttribution: function (text) {
if (!text) { return; }
if (this._attributions[text]) {
this._attributions[text]--;
this._update();
}
return this;
},
_update: function () {
if (!this._map) { return; }
var attribs = [];
for (var i in this._attributions) {
if (this._attributions[i]) {
attribs.push(i);
}
}
var prefixAndAttribs = [];
if (this.options.prefix) {
prefixAndAttribs.push(this.options.prefix);
}
if (attribs.length) {
prefixAndAttribs.push(attribs.join(', '));
}
this._container.innerHTML = prefixAndAttribs.join(' | ');
},
_onLayerAdd: function (e) {
if (e.layer.getAttribution) {
this.addAttribution(e.layer.getAttribution());
}
},
_onLayerRemove: function (e) {
if (e.layer.getAttribution) {
this.removeAttribution(e.layer.getAttribution());
}
}
});
L.Map.mergeOptions({
attributionControl: true
});
L.Map.addInitHook(function () {
if (this.options.attributionControl) {
this.attributionControl = (new L.Control.Attribution()).addTo(this);
}
});
L.control.attribution = function (options) {
return new L.Control.Attribution(options);
};
/*
* L.Control.Scale is used for displaying metric/imperial scale on the map.
*/
L.Control.Scale = L.Control.extend({
options: {
position: 'bottomleft',
maxWidth: 100,
metric: true,
imperial: true,
updateWhenIdle: false
},
onAdd: function (map) {
this._map = map;
var className = 'leaflet-control-scale',
container = L.DomUtil.create('div', className),
options = this.options;
this._addScales(options, className, container);
map.on(options.updateWhenIdle ? 'moveend' : 'move', this._update, this);
map.whenReady(this._update, this);
return container;
},
onRemove: function (map) {
map.off(this.options.updateWhenIdle ? 'moveend' : 'move', this._update, this);
},
_addScales: function (options, className, container) {
if (options.metric) {
this._mScale = L.DomUtil.create('div', className + '-line', container);
}
if (options.imperial) {
this._iScale = L.DomUtil.create('div', className + '-line', container);
}
},
_update: function () {
var bounds = this._map.getBounds(),
centerLat = bounds.getCenter().lat,
halfWorldMeters = 6378137 * Math.PI * Math.cos(centerLat * Math.PI / 180),
dist = halfWorldMeters * (bounds.getNorthEast().lng - bounds.getSouthWest().lng) / 180,
size = this._map.getSize(),
options = this.options,
maxMeters = 0;
if (size.x > 0) {
maxMeters = dist * (options.maxWidth / size.x);
}
this._updateScales(options, maxMeters);
},
_updateScales: function (options, maxMeters) {
if (options.metric && maxMeters) {
this._updateMetric(maxMeters);
}
if (options.imperial && maxMeters) {
this._updateImperial(maxMeters);
}
},
_updateMetric: function (maxMeters) {
var meters = this._getRoundNum(maxMeters);
this._mScale.style.width = this._getScaleWidth(meters / maxMeters) + 'px';
this._mScale.innerHTML = meters < 1000 ? meters + ' m' : (meters / 1000) + ' km';
},
_updateImperial: function (maxMeters) {
var maxFeet = maxMeters * 3.2808399,
scale = this._iScale,
maxMiles, miles, feet;
if (maxFeet > 5280) {
maxMiles = maxFeet / 5280;
miles = this._getRoundNum(maxMiles);
scale.style.width = this._getScaleWidth(miles / maxMiles) + 'px';
scale.innerHTML = miles + ' mi';
} else {
feet = this._getRoundNum(maxFeet);
scale.style.width = this._getScaleWidth(feet / maxFeet) + 'px';
scale.innerHTML = feet + ' ft';
}
},
_getScaleWidth: function (ratio) {
return Math.round(this.options.maxWidth * ratio) - 10;
},
_getRoundNum: function (num) {
var pow10 = Math.pow(10, (Math.floor(num) + '').length - 1),
d = num / pow10;
d = d >= 10 ? 10 : d >= 5 ? 5 : d >= 3 ? 3 : d >= 2 ? 2 : 1;
return pow10 * d;
}
});
L.control.scale = function (options) {
return new L.Control.Scale(options);
};
/*
* L.Control.Layers is a control to allow users to switch between different layers on the map.
*/
L.Control.Layers = L.Control.extend({
options: {
collapsed: true,
position: 'topright',
autoZIndex: true
},
initialize: function (baseLayers, overlays, options) {
L.setOptions(this, options);
this._layers = {};
this._lastZIndex = 0;
this._handlingClick = false;
for (var i in baseLayers) {
this._addLayer(baseLayers[i], i);
}
for (i in overlays) {
this._addLayer(overlays[i], i, true);
}
},
onAdd: function (map) {
this._initLayout();
this._update();
map
.on('layeradd', this._onLayerChange, this)
.on('layerremove', this._onLayerChange, this);
return this._container;
},
onRemove: function (map) {
map
.off('layeradd', this._onLayerChange, this)
.off('layerremove', this._onLayerChange, this);
},
addBaseLayer: function (layer, name) {
this._addLayer(layer, name);
this._update();
return this;
},
addOverlay: function (layer, name) {
this._addLayer(layer, name, true);
this._update();
return this;
},
removeLayer: function (layer) {
var id = L.stamp(layer);
delete this._layers[id];
this._update();
return this;
},
_initLayout: function () {
var className = 'leaflet-control-layers',
container = this._container = L.DomUtil.create('div', className);
//Makes this work on IE10 Touch devices by stopping it from firing a mouseout event when the touch is released
container.setAttribute('aria-haspopup', true);
if (!L.Browser.touch) {
L.DomEvent
.disableClickPropagation(container)
.disableScrollPropagation(container);
} else {
L.DomEvent.on(container, 'click', L.DomEvent.stopPropagation);
}
var form = this._form = L.DomUtil.create('form', className + '-list');
if (this.options.collapsed) {
if (!L.Browser.android) {
L.DomEvent
.on(container, 'mouseover', this._expand, this)
.on(container, 'mouseout', this._collapse, this);
}
var link = this._layersLink = L.DomUtil.create('a', className + '-toggle', container);
link.href = '#';
link.title = 'Layers';
if (L.Browser.touch) {
L.DomEvent
.on(link, 'click', L.DomEvent.stop)
.on(link, 'click', this._expand, this);
}
else {
L.DomEvent.on(link, 'focus', this._expand, this);
}
//Work around for Firefox android issue https://github.com/Leaflet/Leaflet/issues/2033
L.DomEvent.on(form, 'click', function () {
setTimeout(L.bind(this._onInputClick, this), 0);
}, this);
this._map.on('click', this._collapse, this);
// TODO keyboard accessibility
} else {
this._expand();
}
this._baseLayersList = L.DomUtil.create('div', className + '-base', form);
this._separator = L.DomUtil.create('div', className + '-separator', form);
this._overlaysList = L.DomUtil.create('div', className + '-overlays', form);
container.appendChild(form);
},
_addLayer: function (layer, name, overlay) {
var id = L.stamp(layer);
this._layers[id] = {
layer: layer,
name: name,
overlay: overlay
};
if (this.options.autoZIndex && layer.setZIndex) {
this._lastZIndex++;
layer.setZIndex(this._lastZIndex);
}
},
_update: function () {
if (!this._container) {
return;
}
this._baseLayersList.innerHTML = '';
this._overlaysList.innerHTML = '';
var baseLayersPresent = false,
overlaysPresent = false,
i, obj;
for (i in this._layers) {
obj = this._layers[i];
this._addItem(obj);
overlaysPresent = overlaysPresent || obj.overlay;
baseLayersPresent = baseLayersPresent || !obj.overlay;
}
this._separator.style.display = overlaysPresent && baseLayersPresent ? '' : 'none';
},
_onLayerChange: function (e) {
var obj = this._layers[L.stamp(e.layer)];
if (!obj) { return; }
if (!this._handlingClick) {
this._update();
}
var type = obj.overlay ?
(e.type === 'layeradd' ? 'overlayadd' : 'overlayremove') :
(e.type === 'layeradd' ? 'baselayerchange' : null);
if (type) {
this._map.fire(type, obj);
}
},
// IE7 bugs out if you create a radio dynamically, so you have to do it this hacky way (see http://bit.ly/PqYLBe)
_createRadioElement: function (name, checked) {
var radioHtml = '<input type="radio" class="leaflet-control-layers-selector" name="' + name + '"';
if (checked) {
radioHtml += ' checked="checked"';
}
radioHtml += '/>';
var radioFragment = document.createElement('div');
radioFragment.innerHTML = radioHtml;
return radioFragment.firstChild;
},
_addItem: function (obj) {
var label = document.createElement('label'),
input,
checked = this._map.hasLayer(obj.layer);
if (obj.overlay) {
input = document.createElement('input');
input.type = 'checkbox';
input.className = 'leaflet-control-layers-selector';
input.defaultChecked = checked;
} else {
input = this._createRadioElement('leaflet-base-layers', checked);
}
input.layerId = L.stamp(obj.layer);
L.DomEvent.on(input, 'click', this._onInputClick, this);
var name = document.createElement('span');
name.innerHTML = ' ' + obj.name;
label.appendChild(input);
label.appendChild(name);
var container = obj.overlay ? this._overlaysList : this._baseLayersList;
container.appendChild(label);
return label;
},
_onInputClick: function () {
var i, input, obj,
inputs = this._form.getElementsByTagName('input'),
inputsLen = inputs.length;
this._handlingClick = true;
for (i = 0; i < inputsLen; i++) {
input = inputs[i];
obj = this._layers[input.layerId];
if (input.checked && !this._map.hasLayer(obj.layer)) {
this._map.addLayer(obj.layer);
} else if (!input.checked && this._map.hasLayer(obj.layer)) {
this._map.removeLayer(obj.layer);
}
}
this._handlingClick = false;
this._refocusOnMap();
},
_expand: function () {
L.DomUtil.addClass(this._container, 'leaflet-control-layers-expanded');
},
_collapse: function () {
this._container.className = this._container.className.replace(' leaflet-control-layers-expanded', '');
}
});
L.control.layers = function (baseLayers, overlays, options) {
return new L.Control.Layers(baseLayers, overlays, options);
};
/*
* L.PosAnimation is used by Leaflet internally for pan animations.
*/
L.PosAnimation = L.Class.extend({
includes: L.Mixin.Events,
run: function (el, newPos, duration, easeLinearity) { // (HTMLElement, Point[, Number, Number])
this.stop();
this._el = el;
this._inProgress = true;
this._newPos = newPos;
this.fire('start');
el.style[L.DomUtil.TRANSITION] = 'all ' + (duration || 0.25) +
's cubic-bezier(0,0,' + (easeLinearity || 0.5) + ',1)';
L.DomEvent.on(el, L.DomUtil.TRANSITION_END, this._onTransitionEnd, this);
L.DomUtil.setPosition(el, newPos);
// toggle reflow, Chrome flickers for some reason if you don't do this
L.Util.falseFn(el.offsetWidth);
// there's no native way to track value updates of transitioned properties, so we imitate this
this._stepTimer = setInterval(L.bind(this._onStep, this), 50);
},
stop: function () {
if (!this._inProgress) { return; }
// if we just removed the transition property, the element would jump to its final position,
// so we need to make it stay at the current position
L.DomUtil.setPosition(this._el, this._getPos());
this._onTransitionEnd();
L.Util.falseFn(this._el.offsetWidth); // force reflow in case we are about to start a new animation
},
_onStep: function () {
var stepPos = this._getPos();
if (!stepPos) {
this._onTransitionEnd();
return;
}
// jshint camelcase: false
// make L.DomUtil.getPosition return intermediate position value during animation
this._el._leaflet_pos = stepPos;
this.fire('step');
},
// you can't easily get intermediate values of properties animated with CSS3 Transitions,
// we need to parse computed style (in case of transform it returns matrix string)
_transformRe: /([-+]?(?:\d*\.)?\d+)\D*, ([-+]?(?:\d*\.)?\d+)\D*\)/,
_getPos: function () {
var left, top, matches,
el = this._el,
style = window.getComputedStyle(el);
if (L.Browser.any3d) {
matches = style[L.DomUtil.TRANSFORM].match(this._transformRe);
if (!matches) { return; }
left = parseFloat(matches[1]);
top = parseFloat(matches[2]);
} else {
left = parseFloat(style.left);
top = parseFloat(style.top);
}
return new L.Point(left, top, true);
},
_onTransitionEnd: function () {
L.DomEvent.off(this._el, L.DomUtil.TRANSITION_END, this._onTransitionEnd, this);
if (!this._inProgress) { return; }
this._inProgress = false;
this._el.style[L.DomUtil.TRANSITION] = '';
// jshint camelcase: false
// make sure L.DomUtil.getPosition returns the final position value after animation
this._el._leaflet_pos = this._newPos;
clearInterval(this._stepTimer);
this.fire('step').fire('end');
}
});
/*
* Extends L.Map to handle panning animations.
*/
L.Map.include({
setView: function (center, zoom, options) {
zoom = zoom === undefined ? this._zoom : this._limitZoom(zoom);
center = this._limitCenter(L.latLng(center), zoom, this.options.maxBounds);
options = options || {};
if (this._panAnim) {
this._panAnim.stop();
}
if (this._loaded && !options.reset && options !== true) {
if (options.animate !== undefined) {
options.zoom = L.extend({animate: options.animate}, options.zoom);
options.pan = L.extend({animate: options.animate}, options.pan);
}
// try animating pan or zoom
var animated = (this._zoom !== zoom) ?
this._tryAnimatedZoom && this._tryAnimatedZoom(center, zoom, options.zoom) :
this._tryAnimatedPan(center, options.pan);
if (animated) {
// prevent resize handler call, the view will refresh after animation anyway
clearTimeout(this._sizeTimer);
return this;
}
}
// animation didn't start, just reset the map view
this._resetView(center, zoom);
return this;
},
panBy: function (offset, options) {
offset = L.point(offset).round();
options = options || {};
if (!offset.x && !offset.y) {
return this;
}
if (!this._panAnim) {
this._panAnim = new L.PosAnimation();
this._panAnim.on({
'step': this._onPanTransitionStep,
'end': this._onPanTransitionEnd
}, this);
}
// don't fire movestart if animating inertia
if (!options.noMoveStart) {
this.fire('movestart');
}
// animate pan unless animate: false specified
if (options.animate !== false) {
L.DomUtil.addClass(this._mapPane, 'leaflet-pan-anim');
var newPos = this._getMapPanePos().subtract(offset);
this._panAnim.run(this._mapPane, newPos, options.duration || 0.25, options.easeLinearity);
} else {
this._rawPanBy(offset);
this.fire('move').fire('moveend');
}
return this;
},
_onPanTransitionStep: function () {
this.fire('move');
},
_onPanTransitionEnd: function () {
L.DomUtil.removeClass(this._mapPane, 'leaflet-pan-anim');
this.fire('moveend');
},
_tryAnimatedPan: function (center, options) {
// difference between the new and current centers in pixels
var offset = this._getCenterOffset(center)._floor();
// don't animate too far unless animate: true specified in options
if ((options && options.animate) !== true && !this.getSize().contains(offset)) { return false; }
this.panBy(offset, options);
return true;
}
});
/*
* L.PosAnimation fallback implementation that powers Leaflet pan animations
* in browsers that don't support CSS3 Transitions.
*/
L.PosAnimation = L.DomUtil.TRANSITION ? L.PosAnimation : L.PosAnimation.extend({
run: function (el, newPos, duration, easeLinearity) { // (HTMLElement, Point[, Number, Number])
this.stop();
this._el = el;
this._inProgress = true;
this._duration = duration || 0.25;
this._easeOutPower = 1 / Math.max(easeLinearity || 0.5, 0.2);
this._startPos = L.DomUtil.getPosition(el);
this._offset = newPos.subtract(this._startPos);
this._startTime = +new Date();
this.fire('start');
this._animate();
},
stop: function () {
if (!this._inProgress) { return; }
this._step();
this._complete();
},
_animate: function () {
// animation loop
this._animId = L.Util.requestAnimFrame(this._animate, this);
this._step();
},
_step: function () {
var elapsed = (+new Date()) - this._startTime,
duration = this._duration * 1000;
if (elapsed < duration) {
this._runFrame(this._easeOut(elapsed / duration));
} else {
this._runFrame(1);
this._complete();
}
},
_runFrame: function (progress) {
var pos = this._startPos.add(this._offset.multiplyBy(progress));
L.DomUtil.setPosition(this._el, pos);
this.fire('step');
},
_complete: function () {
L.Util.cancelAnimFrame(this._animId);
this._inProgress = false;
this.fire('end');
},
_easeOut: function (t) {
return 1 - Math.pow(1 - t, this._easeOutPower);
}
});
/*
* Extends L.Map to handle zoom animations.
*/
L.Map.mergeOptions({
zoomAnimation: true,
zoomAnimationThreshold: 4
});
if (L.DomUtil.TRANSITION) {
L.Map.addInitHook(function () {
// don't animate on browsers without hardware-accelerated transitions or old Android/Opera
this._zoomAnimated = this.options.zoomAnimation && L.DomUtil.TRANSITION &&
L.Browser.any3d && !L.Browser.android23 && !L.Browser.mobileOpera;
// zoom transitions run with the same duration for all layers, so if one of transitionend events
// happens after starting zoom animation (propagating to the map pane), we know that it ended globally
if (this._zoomAnimated) {
L.DomEvent.on(this._mapPane, L.DomUtil.TRANSITION_END, this._catchTransitionEnd, this);
}
});
}
L.Map.include(!L.DomUtil.TRANSITION ? {} : {
_catchTransitionEnd: function (e) {
if (this._animatingZoom && e.propertyName.indexOf('transform') >= 0) {
this._onZoomTransitionEnd();
}
},
_nothingToAnimate: function () {
return !this._container.getElementsByClassName('leaflet-zoom-animated').length;
},
_tryAnimatedZoom: function (center, zoom, options) {
if (this._animatingZoom) { return true; }
options = options || {};
// don't animate if disabled, not supported or zoom difference is too large
if (!this._zoomAnimated || options.animate === false || this._nothingToAnimate() ||
Math.abs(zoom - this._zoom) > this.options.zoomAnimationThreshold) { return false; }
// offset is the pixel coords of the zoom origin relative to the current center
var scale = this.getZoomScale(zoom),
offset = this._getCenterOffset(center)._divideBy(1 - 1 / scale),
origin = this._getCenterLayerPoint()._add(offset);
// don't animate if the zoom origin isn't within one screen from the current center, unless forced
if (options.animate !== true && !this.getSize().contains(offset)) { return false; }
this
.fire('movestart')
.fire('zoomstart');
this._animateZoom(center, zoom, origin, scale, null, true);
return true;
},
_animateZoom: function (center, zoom, origin, scale, delta, backwards, forTouchZoom) {
if (!forTouchZoom) {
this._animatingZoom = true;
}
// put transform transition on all layers with leaflet-zoom-animated class
L.DomUtil.addClass(this._mapPane, 'leaflet-zoom-anim');
// remember what center/zoom to set after animation
this._animateToCenter = center;
this._animateToZoom = zoom;
// disable any dragging during animation
if (L.Draggable) {
L.Draggable._disabled = true;
}
L.Util.requestAnimFrame(function () {
this.fire('zoomanim', {
center: center,
zoom: zoom,
origin: origin,
scale: scale,
delta: delta,
backwards: backwards
});
// horrible hack to work around a Chrome bug https://github.com/Leaflet/Leaflet/issues/3689
setTimeout(L.bind(this._onZoomTransitionEnd, this), 250);
}, this);
},
_onZoomTransitionEnd: function () {
if (!this._animatingZoom) { return; }
this._animatingZoom = false;
L.DomUtil.removeClass(this._mapPane, 'leaflet-zoom-anim');
L.Util.requestAnimFrame(function () {
this._resetView(this._animateToCenter, this._animateToZoom, true, true);
if (L.Draggable) {
L.Draggable._disabled = false;
}
}, this);
}
});
/*
Zoom animation logic for L.TileLayer.
*/
L.TileLayer.include({
_animateZoom: function (e) {
if (!this._animating) {
this._animating = true;
this._prepareBgBuffer();
}
var bg = this._bgBuffer,
transform = L.DomUtil.TRANSFORM,
initialTransform = e.delta ? L.DomUtil.getTranslateString(e.delta) : bg.style[transform],
scaleStr = L.DomUtil.getScaleString(e.scale, e.origin);
bg.style[transform] = e.backwards ?
scaleStr + ' ' + initialTransform :
initialTransform + ' ' + scaleStr;
},
_endZoomAnim: function () {
var front = this._tileContainer,
bg = this._bgBuffer;
front.style.visibility = '';
front.parentNode.appendChild(front); // Bring to fore
// force reflow
L.Util.falseFn(bg.offsetWidth);
var zoom = this._map.getZoom();
if (zoom > this.options.maxZoom || zoom < this.options.minZoom) {
this._clearBgBuffer();
}
this._animating = false;
},
_clearBgBuffer: function () {
var map = this._map;
if (map && !map._animatingZoom && !map.touchZoom._zooming) {
this._bgBuffer.innerHTML = '';
this._bgBuffer.style[L.DomUtil.TRANSFORM] = '';
}
},
_prepareBgBuffer: function () {
var front = this._tileContainer,
bg = this._bgBuffer;
// if foreground layer doesn't have many tiles but bg layer does,
// keep the existing bg layer and just zoom it some more
var bgLoaded = this._getLoadedTilesPercentage(bg),
frontLoaded = this._getLoadedTilesPercentage(front);
if (bg && bgLoaded > 0.5 && frontLoaded < 0.5) {
front.style.visibility = 'hidden';
this._stopLoadingImages(front);
return;
}
// prepare the buffer to become the front tile pane
bg.style.visibility = 'hidden';
bg.style[L.DomUtil.TRANSFORM] = '';
// switch out the current layer to be the new bg layer (and vice-versa)
this._tileContainer = bg;
bg = this._bgBuffer = front;
this._stopLoadingImages(bg);
//prevent bg buffer from clearing right after zoom
clearTimeout(this._clearBgBufferTimer);
},
_getLoadedTilesPercentage: function (container) {
var tiles = container.getElementsByTagName('img'),
i, len, count = 0;
for (i = 0, len = tiles.length; i < len; i++) {
if (tiles[i].complete) {
count++;
}
}
return count / len;
},
// stops loading all tiles in the background layer
_stopLoadingImages: function (container) {
var tiles = Array.prototype.slice.call(container.getElementsByTagName('img')),
i, len, tile;
for (i = 0, len = tiles.length; i < len; i++) {
tile = tiles[i];
if (!tile.complete) {
tile.onload = L.Util.falseFn;
tile.onerror = L.Util.falseFn;
tile.src = L.Util.emptyImageUrl;
tile.parentNode.removeChild(tile);
}
}
}
});
/*
* Provides L.Map with convenient shortcuts for using browser geolocation features.
*/
L.Map.include({
_defaultLocateOptions: {
watch: false,
setView: false,
maxZoom: Infinity,
timeout: 10000,
maximumAge: 0,
enableHighAccuracy: false
},
locate: function (/*Object*/ options) {
options = this._locateOptions = L.extend(this._defaultLocateOptions, options);
if (!navigator.geolocation) {
this._handleGeolocationError({
code: 0,
message: 'Geolocation not supported.'
});
return this;
}
var onResponse = L.bind(this._handleGeolocationResponse, this),
onError = L.bind(this._handleGeolocationError, this);
if (options.watch) {
this._locationWatchId =
navigator.geolocation.watchPosition(onResponse, onError, options);
} else {
navigator.geolocation.getCurrentPosition(onResponse, onError, options);
}
return this;
},
stopLocate: function () {
if (navigator.geolocation) {
navigator.geolocation.clearWatch(this._locationWatchId);
}
if (this._locateOptions) {
this._locateOptions.setView = false;
}
return this;
},
_handleGeolocationError: function (error) {
var c = error.code,
message = error.message ||
(c === 1 ? 'permission denied' :
(c === 2 ? 'position unavailable' : 'timeout'));
if (this._locateOptions.setView && !this._loaded) {
this.fitWorld();
}
this.fire('locationerror', {
code: c,
message: 'Geolocation error: ' + message + '.'
});
},
_handleGeolocationResponse: function (pos) {
var lat = pos.coords.latitude,
lng = pos.coords.longitude,
latlng = new L.LatLng(lat, lng),
latAccuracy = 180 * pos.coords.accuracy / 40075017,
lngAccuracy = latAccuracy / Math.cos(L.LatLng.DEG_TO_RAD * lat),
bounds = L.latLngBounds(
[lat - latAccuracy, lng - lngAccuracy],
[lat + latAccuracy, lng + lngAccuracy]),
options = this._locateOptions;
if (options.setView) {
var zoom = Math.min(this.getBoundsZoom(bounds), options.maxZoom);
this.setView(latlng, zoom);
}
var data = {
latlng: latlng,
bounds: bounds,
timestamp: pos.timestamp
};
for (var i in pos.coords) {
if (typeof pos.coords[i] === 'number') {
data[i] = pos.coords[i];
}
}
this.fire('locationfound', data);
}
});
}(window, document));
/*
Leaflet.markercluster, Provides Beautiful Animated Marker Clustering functionality for Leaflet, a JS library for interactive maps.
https://github.com/Leaflet/Leaflet.markercluster
(c) 2012-2013, Dave Leaver, smartrak
*/
!function(t,e){L.MarkerClusterGroup=L.FeatureGroup.extend({options:{maxClusterRadius:80,iconCreateFunction:null,spiderfyOnMaxZoom:!0,showCoverageOnHover:!0,zoomToBoundsOnClick:!0,singleMarkerMode:!1,disableClusteringAtZoom:null,removeOutsideVisibleBounds:!0,animateAddingMarkers:!1,spiderfyDistanceMultiplier:1,chunkedLoading:!1,chunkInterval:200,chunkDelay:50,chunkProgress:null,polygonOptions:{}},initialize:function(t){L.Util.setOptions(this,t),this.options.iconCreateFunction||(this.options.iconCreateFunction=this._defaultIconCreateFunction),this._featureGroup=L.featureGroup(),this._featureGroup.on(L.FeatureGroup.EVENTS,this._propagateEvent,this),this._nonPointGroup=L.featureGroup(),this._nonPointGroup.on(L.FeatureGroup.EVENTS,this._propagateEvent,this),this._inZoomAnimation=0,this._needsClustering=[],this._needsRemoving=[],this._currentShownBounds=null,this._queue=[]},addLayer:function(t){if(t instanceof L.LayerGroup){var e=[];for(var i in t._layers)e.push(t._layers[i]);return this.addLayers(e)}if(!t.getLatLng)return this._nonPointGroup.addLayer(t),this;if(!this._map)return this._needsClustering.push(t),this;if(this.hasLayer(t))return this;this._unspiderfy&&this._unspiderfy(),this._addLayer(t,this._maxZoom);var n=t,s=this._map.getZoom();if(t.__parent)for(;n.__parent._zoom>=s;)n=n.__parent;return this._currentShownBounds.contains(n.getLatLng())&&(this.options.animateAddingMarkers?this._animationAddLayer(t,n):this._animationAddLayerNonAnimated(t,n)),this},removeLayer:function(t){if(t instanceof L.LayerGroup){var e=[];for(var i in t._layers)e.push(t._layers[i]);return this.removeLayers(e)}return t.getLatLng?this._map?t.__parent?(this._unspiderfy&&(this._unspiderfy(),this._unspiderfyLayer(t)),this._removeLayer(t,!0),this._featureGroup.hasLayer(t)&&(this._featureGroup.removeLayer(t),t.setOpacity&&t.setOpacity(1)),this):this:(!this._arraySplice(this._needsClustering,t)&&this.hasLayer(t)&&this._needsRemoving.push(t),this):(this._nonPointGroup.removeLayer(t),this)},addLayers:function(t){var e,i,n,s,r=this._featureGroup,o=this._nonPointGroup,a=this.options.chunkedLoading,h=this.options.chunkInterval,_=this.options.chunkProgress;if(this._map){var u=0,l=(new Date).getTime(),d=L.bind(function(){for(var e=(new Date).getTime();u<t.length;u++){if(a&&0===u%200){var i=(new Date).getTime()-e;if(i>h)break}if(s=t[u],s.getLatLng){if(!this.hasLayer(s)&&(this._addLayer(s,this._maxZoom),s.__parent&&2===s.__parent.getChildCount())){var n=s.__parent.getAllChildMarkers(),p=n[0]===s?n[1]:n[0];r.removeLayer(p)}}else o.addLayer(s)}_&&_(u,t.length,(new Date).getTime()-l),u===t.length?(this._featureGroup.eachLayer(function(t){t instanceof L.MarkerCluster&&t._iconNeedsUpdate&&t._updateIcon()}),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds)):setTimeout(d,this.options.chunkDelay)},this);d()}else{for(e=[],i=0,n=t.length;n>i;i++)s=t[i],s.getLatLng?this.hasLayer(s)||e.push(s):o.addLayer(s);this._needsClustering=this._needsClustering.concat(e)}return this},removeLayers:function(t){var e,i,n,s=this._featureGroup,r=this._nonPointGroup;if(!this._map){for(e=0,i=t.length;i>e;e++)n=t[e],this._arraySplice(this._needsClustering,n),r.removeLayer(n);return this}for(e=0,i=t.length;i>e;e++)n=t[e],n.__parent?(this._removeLayer(n,!0,!0),s.hasLayer(n)&&(s.removeLayer(n),n.setOpacity&&n.setOpacity(1))):r.removeLayer(n);return this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds),s.eachLayer(function(t){t instanceof L.MarkerCluster&&t._updateIcon()}),this},clearLayers:function(){return this._map||(this._needsClustering=[],delete this._gridClusters,delete this._gridUnclustered),this._noanimationUnspiderfy&&this._noanimationUnspiderfy(),this._featureGroup.clearLayers(),this._nonPointGroup.clearLayers(),this.eachLayer(function(t){delete t.__parent}),this._map&&this._generateInitialClusters(),this},getBounds:function(){var t=new L.LatLngBounds;if(this._topClusterLevel)t.extend(this._topClusterLevel._bounds);else for(var e=this._needsClustering.length-1;e>=0;e--)t.extend(this._needsClustering[e].getLatLng());return t.extend(this._nonPointGroup.getBounds()),t},eachLayer:function(t,e){var i,n=this._needsClustering.slice();for(this._topClusterLevel&&this._topClusterLevel.getAllChildMarkers(n),i=n.length-1;i>=0;i--)t.call(e,n[i]);this._nonPointGroup.eachLayer(t,e)},getLayers:function(){var t=[];return this.eachLayer(function(e){t.push(e)}),t},getLayer:function(t){var e=null;return this.eachLayer(function(i){L.stamp(i)===t&&(e=i)}),e},hasLayer:function(t){if(!t)return!1;var e,i=this._needsClustering;for(e=i.length-1;e>=0;e--)if(i[e]===t)return!0;for(i=this._needsRemoving,e=i.length-1;e>=0;e--)if(i[e]===t)return!1;return!(!t.__parent||t.__parent._group!==this)||this._nonPointGroup.hasLayer(t)},zoomToShowLayer:function(t,e){var i=function(){if((t._icon||t.__parent._icon)&&!this._inZoomAnimation)if(this._map.off("moveend",i,this),this.off("animationend",i,this),t._icon)e();else if(t.__parent._icon){var n=function(){this.off("spiderfied",n,this),e()};this.on("spiderfied",n,this),t.__parent.spiderfy()}};t._icon&&this._map.getBounds().contains(t.getLatLng())?e():t.__parent._zoom<this._map.getZoom()?(this._map.on("moveend",i,this),this._map.panTo(t.getLatLng())):(this._map.on("moveend",i,this),this.on("animationend",i,this),this._map.setView(t.getLatLng(),t.__parent._zoom+1),t.__parent.zoomToBounds())},onAdd:function(t){this._map=t;var e,i,n;if(!isFinite(this._map.getMaxZoom()))throw"Map has no maxZoom specified";for(this._featureGroup.onAdd(t),this._nonPointGroup.onAdd(t),this._gridClusters||this._generateInitialClusters(),e=0,i=this._needsRemoving.length;i>e;e++)n=this._needsRemoving[e],this._removeLayer(n,!0);this._needsRemoving=[],this._zoom=this._map.getZoom(),this._currentShownBounds=this._getExpandedVisibleBounds(),this._map.on("zoomend",this._zoomEnd,this),this._map.on("moveend",this._moveEnd,this),this._spiderfierOnAdd&&this._spiderfierOnAdd(),this._bindEvents(),i=this._needsClustering,this._needsClustering=[],this.addLayers(i)},onRemove:function(t){t.off("zoomend",this._zoomEnd,this),t.off("moveend",this._moveEnd,this),this._unbindEvents(),this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim",""),this._spiderfierOnRemove&&this._spiderfierOnRemove(),this._hideCoverage(),this._featureGroup.onRemove(t),this._nonPointGroup.onRemove(t),this._featureGroup.clearLayers(),this._map=null},getVisibleParent:function(t){for(var e=t;e&&!e._icon;)e=e.__parent;return e||null},_arraySplice:function(t,e){for(var i=t.length-1;i>=0;i--)if(t[i]===e)return t.splice(i,1),!0},_removeLayer:function(t,e,i){var n=this._gridClusters,s=this._gridUnclustered,r=this._featureGroup,o=this._map;if(e)for(var a=this._maxZoom;a>=0&&s[a].removeObject(t,o.project(t.getLatLng(),a));a--);var h,_=t.__parent,u=_._markers;for(this._arraySplice(u,t);_&&(_._childCount--,!(_._zoom<0));)e&&_._childCount<=1?(h=_._markers[0]===t?_._markers[1]:_._markers[0],n[_._zoom].removeObject(_,o.project(_._cLatLng,_._zoom)),s[_._zoom].addObject(h,o.project(h.getLatLng(),_._zoom)),this._arraySplice(_.__parent._childClusters,_),_.__parent._markers.push(h),h.__parent=_.__parent,_._icon&&(r.removeLayer(_),i||r.addLayer(h))):(_._recalculateBounds(),i&&_._icon||_._updateIcon()),_=_.__parent;delete t.__parent},_isOrIsParent:function(t,e){for(;e;){if(t===e)return!0;e=e.parentNode}return!1},_propagateEvent:function(t){if(t.layer instanceof L.MarkerCluster){if(t.originalEvent&&this._isOrIsParent(t.layer._icon,t.originalEvent.relatedTarget))return;t.type="cluster"+t.type}this.fire(t.type,t)},_defaultIconCreateFunction:function(t){var e=t.getChildCount(),i=" marker-cluster-";return i+=10>e?"small":100>e?"medium":"large",new L.DivIcon({html:"<div><span>"+e+"</span></div>",className:"marker-cluster"+i,iconSize:new L.Point(40,40)})},_bindEvents:function(){var t=this._map,e=this.options.spiderfyOnMaxZoom,i=this.options.showCoverageOnHover,n=this.options.zoomToBoundsOnClick;(e||n)&&this.on("clusterclick",this._zoomOrSpiderfy,this),i&&(this.on("clustermouseover",this._showCoverage,this),this.on("clustermouseout",this._hideCoverage,this),t.on("zoomend",this._hideCoverage,this))},_zoomOrSpiderfy:function(t){var e=this._map;e.getMaxZoom()===e.getZoom()?this.options.spiderfyOnMaxZoom&&t.layer.spiderfy():this.options.zoomToBoundsOnClick&&t.layer.zoomToBounds(),t.originalEvent&&13===t.originalEvent.keyCode&&e._container.focus()},_showCoverage:function(t){var e=this._map;this._inZoomAnimation||(this._shownPolygon&&e.removeLayer(this._shownPolygon),t.layer.getChildCount()>2&&t.layer!==this._spiderfied&&(this._shownPolygon=new L.Polygon(t.layer.getConvexHull(),this.options.polygonOptions),e.addLayer(this._shownPolygon)))},_hideCoverage:function(){this._shownPolygon&&(this._map.removeLayer(this._shownPolygon),this._shownPolygon=null)},_unbindEvents:function(){var t=this.options.spiderfyOnMaxZoom,e=this.options.showCoverageOnHover,i=this.options.zoomToBoundsOnClick,n=this._map;(t||i)&&this.off("clusterclick",this._zoomOrSpiderfy,this),e&&(this.off("clustermouseover",this._showCoverage,this),this.off("clustermouseout",this._hideCoverage,this),n.off("zoomend",this._hideCoverage,this))},_zoomEnd:function(){this._map&&(this._mergeSplitClusters(),this._zoom=this._map._zoom,this._currentShownBounds=this._getExpandedVisibleBounds())},_moveEnd:function(){if(!this._inZoomAnimation){var t=this._getExpandedVisibleBounds();this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,this._zoom,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._map._zoom,t),this._currentShownBounds=t}},_generateInitialClusters:function(){var t=this._map.getMaxZoom(),e=this.options.maxClusterRadius,i=e;"function"!=typeof e&&(i=function(){return e}),this.options.disableClusteringAtZoom&&(t=this.options.disableClusteringAtZoom-1),this._maxZoom=t,this._gridClusters={},this._gridUnclustered={};for(var n=t;n>=0;n--)this._gridClusters[n]=new L.DistanceGrid(i(n)),this._gridUnclustered[n]=new L.DistanceGrid(i(n));this._topClusterLevel=new L.MarkerCluster(this,-1)},_addLayer:function(t,e){var i,n,s=this._gridClusters,r=this._gridUnclustered;for(this.options.singleMarkerMode&&(t.options.icon=this.options.iconCreateFunction({getChildCount:function(){return 1},getAllChildMarkers:function(){return[t]}}));e>=0;e--){i=this._map.project(t.getLatLng(),e);var o=s[e].getNearObject(i);if(o)return o._addChild(t),t.__parent=o,void 0;if(o=r[e].getNearObject(i)){var a=o.__parent;a&&this._removeLayer(o,!1);var h=new L.MarkerCluster(this,e,o,t);s[e].addObject(h,this._map.project(h._cLatLng,e)),o.__parent=h,t.__parent=h;var _=h;for(n=e-1;n>a._zoom;n--)_=new L.MarkerCluster(this,n,_),s[n].addObject(_,this._map.project(o.getLatLng(),n));for(a._addChild(_),n=e;n>=0&&r[n].removeObject(o,this._map.project(o.getLatLng(),n));n--);return}r[e].addObject(t,i)}this._topClusterLevel._addChild(t),t.__parent=this._topClusterLevel},_enqueue:function(t){this._queue.push(t),this._queueTimeout||(this._queueTimeout=setTimeout(L.bind(this._processQueue,this),300))},_processQueue:function(){for(var t=0;t<this._queue.length;t++)this._queue[t].call(this);this._queue.length=0,clearTimeout(this._queueTimeout),this._queueTimeout=null},_mergeSplitClusters:function(){this._processQueue(),this._zoom<this._map._zoom&&this._currentShownBounds.contains(this._getExpandedVisibleBounds())?(this._animationStart(),this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,this._zoom,this._getExpandedVisibleBounds()),this._animationZoomIn(this._zoom,this._map._zoom)):this._zoom>this._map._zoom?(this._animationStart(),this._animationZoomOut(this._zoom,this._map._zoom)):this._moveEnd()},_getExpandedVisibleBounds:function(){if(!this.options.removeOutsideVisibleBounds)return this.getBounds();var t=this._map,e=t.getBounds(),i=e._southWest,n=e._northEast,s=L.Browser.mobile?0:Math.abs(i.lat-n.lat),r=L.Browser.mobile?0:Math.abs(i.lng-n.lng);return new L.LatLngBounds(new L.LatLng(i.lat-s,i.lng-r,!0),new L.LatLng(n.lat+s,n.lng+r,!0))},_animationAddLayerNonAnimated:function(t,e){if(e===t)this._featureGroup.addLayer(t);else if(2===e._childCount){e._addToMap();var i=e.getAllChildMarkers();this._featureGroup.removeLayer(i[0]),this._featureGroup.removeLayer(i[1])}else e._updateIcon()}}),L.MarkerClusterGroup.include(L.DomUtil.TRANSITION?{_animationStart:function(){this._map._mapPane.className+=" leaflet-cluster-anim",this._inZoomAnimation++},_animationEnd:function(){this._map&&(this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim","")),this._inZoomAnimation--,this.fire("animationend")},_animationZoomIn:function(t,e){var i,n=this._getExpandedVisibleBounds(),s=this._featureGroup;this._topClusterLevel._recursively(n,t,0,function(r){var o,a=r._latlng,h=r._markers;for(n.contains(a)||(a=null),r._isSingleParent()&&t+1===e?(s.removeLayer(r),r._recursivelyAddChildrenToMap(null,e,n)):(r.setOpacity(0),r._recursivelyAddChildrenToMap(a,e,n)),i=h.length-1;i>=0;i--)o=h[i],n.contains(o._latlng)||s.removeLayer(o)}),this._forceLayout(),this._topClusterLevel._recursivelyBecomeVisible(n,e),s.eachLayer(function(t){t instanceof L.MarkerCluster||!t._icon||t.setOpacity(1)}),this._topClusterLevel._recursively(n,t,e,function(t){t._recursivelyRestoreChildPositions(e)}),this._enqueue(function(){this._topClusterLevel._recursively(n,t,0,function(t){s.removeLayer(t),t.setOpacity(1)}),this._animationEnd()})},_animationZoomOut:function(t,e){this._animationZoomOutSingle(this._topClusterLevel,t-1,e),this._topClusterLevel._recursivelyAddChildrenToMap(null,e,this._getExpandedVisibleBounds()),this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,t,this._getExpandedVisibleBounds())},_animationZoomOutSingle:function(t,e,i){var n=this._getExpandedVisibleBounds();t._recursivelyAnimateChildrenInAndAddSelfToMap(n,e+1,i);var s=this;this._forceLayout(),t._recursivelyBecomeVisible(n,i),this._enqueue(function(){if(1===t._childCount){var r=t._markers[0];r.setLatLng(r.getLatLng()),r.setOpacity&&r.setOpacity(1)}else t._recursively(n,i,0,function(t){t._recursivelyRemoveChildrenFromMap(n,e+1)});s._animationEnd()})},_animationAddLayer:function(t,e){var i=this,n=this._featureGroup;n.addLayer(t),e!==t&&(e._childCount>2?(e._updateIcon(),this._forceLayout(),this._animationStart(),t._setPos(this._map.latLngToLayerPoint(e.getLatLng())),t.setOpacity(0),this._enqueue(function(){n.removeLayer(t),t.setOpacity(1),i._animationEnd()})):(this._forceLayout(),i._animationStart(),i._animationZoomOutSingle(e,this._map.getMaxZoom(),this._map.getZoom())))},_forceLayout:function(){L.Util.falseFn(e.body.offsetWidth)}}:{_animationStart:function(){},_animationZoomIn:function(t,e){this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,e,this._getExpandedVisibleBounds()),this.fire("animationend")},_animationZoomOut:function(t,e){this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,e,this._getExpandedVisibleBounds()),this.fire("animationend")},_animationAddLayer:function(t,e){this._animationAddLayerNonAnimated(t,e)}}),L.markerClusterGroup=function(t){return new L.MarkerClusterGroup(t)},L.MarkerCluster=L.Marker.extend({initialize:function(t,e,i,n){L.Marker.prototype.initialize.call(this,i?i._cLatLng||i.getLatLng():new L.LatLng(0,0),{icon:this}),this._group=t,this._zoom=e,this._markers=[],this._childClusters=[],this._childCount=0,this._iconNeedsUpdate=!0,this._bounds=new L.LatLngBounds,i&&this._addChild(i),n&&this._addChild(n)},getAllChildMarkers:function(t){t=t||[];for(var e=this._childClusters.length-1;e>=0;e--)this._childClusters[e].getAllChildMarkers(t);for(var i=this._markers.length-1;i>=0;i--)t.push(this._markers[i]);return t},getChildCount:function(){return this._childCount},zoomToBounds:function(){for(var t,e=this._childClusters.slice(),i=this._group._map,n=i.getBoundsZoom(this._bounds),s=this._zoom+1,r=i.getZoom();e.length>0&&n>s;){s++;var o=[];for(t=0;t<e.length;t++)o=o.concat(e[t]._childClusters);e=o}n>s?this._group._map.setView(this._latlng,s):r>=n?this._group._map.setView(this._latlng,r+1):this._group._map.fitBounds(this._bounds)},getBounds:function(){var t=new L.LatLngBounds;return t.extend(this._bounds),t},_updateIcon:function(){this._iconNeedsUpdate=!0,this._icon&&this.setIcon(this)},createIcon:function(){return this._iconNeedsUpdate&&(this._iconObj=this._group.options.iconCreateFunction(this),this._iconNeedsUpdate=!1),this._iconObj.createIcon()},createShadow:function(){return this._iconObj.createShadow()},_addChild:function(t,e){this._iconNeedsUpdate=!0,this._expandBounds(t),t instanceof L.MarkerCluster?(e||(this._childClusters.push(t),t.__parent=this),this._childCount+=t._childCount):(e||this._markers.push(t),this._childCount++),this.__parent&&this.__parent._addChild(t,!0)},_expandBounds:function(t){var e,i=t._wLatLng||t._latlng;t instanceof L.MarkerCluster?(this._bounds.extend(t._bounds),e=t._childCount):(this._bounds.extend(i),e=1),this._cLatLng||(this._cLatLng=t._cLatLng||i);var n=this._childCount+e;this._wLatLng?(this._wLatLng.lat=(i.lat*e+this._wLatLng.lat*this._childCount)/n,this._wLatLng.lng=(i.lng*e+this._wLatLng.lng*this._childCount)/n):this._latlng=this._wLatLng=new L.LatLng(i.lat,i.lng)},_addToMap:function(t){t&&(this._backupLatlng=this._latlng,this.setLatLng(t)),this._group._featureGroup.addLayer(this)},_recursivelyAnimateChildrenIn:function(t,e,i){this._recursively(t,0,i-1,function(t){var i,n,s=t._markers;for(i=s.length-1;i>=0;i--)n=s[i],n._icon&&(n._setPos(e),n.setOpacity(0))},function(t){var i,n,s=t._childClusters;for(i=s.length-1;i>=0;i--)n=s[i],n._icon&&(n._setPos(e),n.setOpacity(0))})},_recursivelyAnimateChildrenInAndAddSelfToMap:function(t,e,i){this._recursively(t,i,0,function(n){n._recursivelyAnimateChildrenIn(t,n._group._map.latLngToLayerPoint(n.getLatLng()).round(),e),n._isSingleParent()&&e-1===i?(n.setOpacity(1),n._recursivelyRemoveChildrenFromMap(t,e)):n.setOpacity(0),n._addToMap()})},_recursivelyBecomeVisible:function(t,e){this._recursively(t,0,e,null,function(t){t.setOpacity(1)})},_recursivelyAddChildrenToMap:function(t,e,i){this._recursively(i,-1,e,function(n){if(e!==n._zoom)for(var s=n._markers.length-1;s>=0;s--){var r=n._markers[s];i.contains(r._latlng)&&(t&&(r._backupLatlng=r.getLatLng(),r.setLatLng(t),r.setOpacity&&r.setOpacity(0)),n._group._featureGroup.addLayer(r))}},function(e){e._addToMap(t)})},_recursivelyRestoreChildPositions:function(t){for(var e=this._markers.length-1;e>=0;e--){var i=this._markers[e];i._backupLatlng&&(i.setLatLng(i._backupLatlng),delete i._backupLatlng)}if(t-1===this._zoom)for(var n=this._childClusters.length-1;n>=0;n--)this._childClusters[n]._restorePosition();else for(var s=this._childClusters.length-1;s>=0;s--)this._childClusters[s]._recursivelyRestoreChildPositions(t)},_restorePosition:function(){this._backupLatlng&&(this.setLatLng(this._backupLatlng),delete this._backupLatlng)},_recursivelyRemoveChildrenFromMap:function(t,e,i){var n,s;this._recursively(t,-1,e-1,function(t){for(s=t._markers.length-1;s>=0;s--)n=t._markers[s],i&&i.contains(n._latlng)||(t._group._featureGroup.removeLayer(n),n.setOpacity&&n.setOpacity(1))},function(t){for(s=t._childClusters.length-1;s>=0;s--)n=t._childClusters[s],i&&i.contains(n._latlng)||(t._group._featureGroup.removeLayer(n),n.setOpacity&&n.setOpacity(1))})},_recursively:function(t,e,i,n,s){var r,o,a=this._childClusters,h=this._zoom;if(e>h)for(r=a.length-1;r>=0;r--)o=a[r],t.intersects(o._bounds)&&o._recursively(t,e,i,n,s);else if(n&&n(this),s&&this._zoom===i&&s(this),i>h)for(r=a.length-1;r>=0;r--)o=a[r],t.intersects(o._bounds)&&o._recursively(t,e,i,n,s)},_recalculateBounds:function(){var t,e=this._markers,i=this._childClusters;for(this._bounds=new L.LatLngBounds,delete this._wLatLng,t=e.length-1;t>=0;t--)this._expandBounds(e[t]);for(t=i.length-1;t>=0;t--)this._expandBounds(i[t])},_isSingleParent:function(){return this._childClusters.length>0&&this._childClusters[0]._childCount===this._childCount}}),L.DistanceGrid=function(t){this._cellSize=t,this._sqCellSize=t*t,this._grid={},this._objectPoint={}},L.DistanceGrid.prototype={addObject:function(t,e){var i=this._getCoord(e.x),n=this._getCoord(e.y),s=this._grid,r=s[n]=s[n]||{},o=r[i]=r[i]||[],a=L.Util.stamp(t);this._objectPoint[a]=e,o.push(t)},updateObject:function(t,e){this.removeObject(t),this.addObject(t,e)},removeObject:function(t,e){var i,n,s=this._getCoord(e.x),r=this._getCoord(e.y),o=this._grid,a=o[r]=o[r]||{},h=a[s]=a[s]||[];for(delete this._objectPoint[L.Util.stamp(t)],i=0,n=h.length;n>i;i++)if(h[i]===t)return h.splice(i,1),1===n&&delete a[s],!0},eachObject:function(t,e){var i,n,s,r,o,a,h,_=this._grid;for(i in _){o=_[i];for(n in o)for(a=o[n],s=0,r=a.length;r>s;s++)h=t.call(e,a[s]),h&&(s--,r--)}},getNearObject:function(t){var e,i,n,s,r,o,a,h,_=this._getCoord(t.x),u=this._getCoord(t.y),l=this._objectPoint,d=this._sqCellSize,p=null;for(e=u-1;u+1>=e;e++)if(s=this._grid[e])for(i=_-1;_+1>=i;i++)if(r=s[i])for(n=0,o=r.length;o>n;n++)a=r[n],h=this._sqDist(l[L.Util.stamp(a)],t),d>h&&(d=h,p=a);return p},_getCoord:function(t){return Math.floor(t/this._cellSize)},_sqDist:function(t,e){var i=e.x-t.x,n=e.y-t.y;return i*i+n*n}},function(){L.QuickHull={getDistant:function(t,e){var i=e[1].lat-e[0].lat,n=e[0].lng-e[1].lng;return n*(t.lat-e[0].lat)+i*(t.lng-e[0].lng)},findMostDistantPointFromBaseLine:function(t,e){var i,n,s,r=0,o=null,a=[];for(i=e.length-1;i>=0;i--)n=e[i],s=this.getDistant(n,t),s>0&&(a.push(n),s>r&&(r=s,o=n));return{maxPoint:o,newPoints:a}},buildConvexHull:function(t,e){var i=[],n=this.findMostDistantPointFromBaseLine(t,e);return n.maxPoint?(i=i.concat(this.buildConvexHull([t[0],n.maxPoint],n.newPoints)),i=i.concat(this.buildConvexHull([n.maxPoint,t[1]],n.newPoints))):[t[0]]},getConvexHull:function(t){var e,i=!1,n=!1,s=null,r=null;for(e=t.length-1;e>=0;e--){var o=t[e];(i===!1||o.lat>i)&&(s=o,i=o.lat),(n===!1||o.lat<n)&&(r=o,n=o.lat)}var a=[].concat(this.buildConvexHull([r,s],t),this.buildConvexHull([s,r],t));return a}}}(),L.MarkerCluster.include({getConvexHull:function(){var t,e,i=this.getAllChildMarkers(),n=[];for(e=i.length-1;e>=0;e--)t=i[e].getLatLng(),n.push(t);return L.QuickHull.getConvexHull(n)}}),L.MarkerCluster.include({_2PI:2*Math.PI,_circleFootSeparation:25,_circleStartAngle:Math.PI/6,_spiralFootSeparation:28,_spiralLengthStart:11,_spiralLengthFactor:5,_circleSpiralSwitchover:9,spiderfy:function(){if(this._group._spiderfied!==this&&!this._group._inZoomAnimation){var t,e=this.getAllChildMarkers(),i=this._group,n=i._map,s=n.latLngToLayerPoint(this._latlng);this._group._unspiderfy(),this._group._spiderfied=this,e.length>=this._circleSpiralSwitchover?t=this._generatePointsSpiral(e.length,s):(s.y+=10,t=this._generatePointsCircle(e.length,s)),this._animationSpiderfy(e,t)}},unspiderfy:function(t){this._group._inZoomAnimation||(this._animationUnspiderfy(t),this._group._spiderfied=null)},_generatePointsCircle:function(t,e){var i,n,s=this._group.options.spiderfyDistanceMultiplier*this._circleFootSeparation*(2+t),r=s/this._2PI,o=this._2PI/t,a=[];for(a.length=t,i=t-1;i>=0;i--)n=this._circleStartAngle+i*o,a[i]=new L.Point(e.x+r*Math.cos(n),e.y+r*Math.sin(n))._round();return a},_generatePointsSpiral:function(t,e){var i,n=this._group.options.spiderfyDistanceMultiplier*this._spiralLengthStart,s=this._group.options.spiderfyDistanceMultiplier*this._spiralFootSeparation,r=this._group.options.spiderfyDistanceMultiplier*this._spiralLengthFactor,o=0,a=[];for(a.length=t,i=t-1;i>=0;i--)o+=s/n+5e-4*i,a[i]=new L.Point(e.x+n*Math.cos(o),e.y+n*Math.sin(o))._round(),n+=this._2PI*r/o;return a},_noanimationUnspiderfy:function(){var t,e,i=this._group,n=i._map,s=i._featureGroup,r=this.getAllChildMarkers();for(this.setOpacity(1),e=r.length-1;e>=0;e--)t=r[e],s.removeLayer(t),t._preSpiderfyLatlng&&(t.setLatLng(t._preSpiderfyLatlng),delete t._preSpiderfyLatlng),t.setZIndexOffset&&t.setZIndexOffset(0),t._spiderLeg&&(n.removeLayer(t._spiderLeg),delete t._spiderLeg);i._spiderfied=null}}),L.MarkerCluster.include(L.DomUtil.TRANSITION?{SVG_ANIMATION:function(){return e.createElementNS("http://www.w3.org/2000/svg","animate").toString().indexOf("SVGAnimate")>-1}(),_animationSpiderfy:function(t,i){var n,s,r,o,a=this,h=this._group,_=h._map,u=h._featureGroup,l=_.latLngToLayerPoint(this._latlng);for(n=t.length-1;n>=0;n--)s=t[n],s.setOpacity?(s.setZIndexOffset(1e6),s.setOpacity(0),u.addLayer(s),s._setPos(l)):u.addLayer(s);h._forceLayout(),h._animationStart();var d=L.Path.SVG?0:.3,p=L.Path.SVG_NS;for(n=t.length-1;n>=0;n--)if(o=_.layerPointToLatLng(i[n]),s=t[n],s._preSpiderfyLatlng=s._latlng,s.setLatLng(o),s.setOpacity&&s.setOpacity(1),r=new L.Polyline([a._latlng,o],{weight:1.5,color:"#222",opacity:d}),_.addLayer(r),s._spiderLeg=r,L.Path.SVG&&this.SVG_ANIMATION){var c=r._path.getTotalLength();r._path.setAttribute("stroke-dasharray",c+","+c);var m=e.createElementNS(p,"animate");m.setAttribute("attributeName","stroke-dashoffset"),m.setAttribute("begin","indefinite"),m.setAttribute("from",c),m.setAttribute("to",0),m.setAttribute("dur",.25),r._path.appendChild(m),m.beginElement(),m=e.createElementNS(p,"animate"),m.setAttribute("attributeName","stroke-opacity"),m.setAttribute("attributeName","stroke-opacity"),m.setAttribute("begin","indefinite"),m.setAttribute("from",0),m.setAttribute("to",.5),m.setAttribute("dur",.25),r._path.appendChild(m),m.beginElement()}if(a.setOpacity(.3),L.Path.SVG)for(this._group._forceLayout(),n=t.length-1;n>=0;n--)s=t[n]._spiderLeg,s.options.opacity=.5,s._path.setAttribute("stroke-opacity",.5);setTimeout(function(){h._animationEnd(),h.fire("spiderfied")},200)},_animationUnspiderfy:function(t){var e,i,n,s=this._group,r=s._map,o=s._featureGroup,a=t?r._latLngToNewLayerPoint(this._latlng,t.zoom,t.center):r.latLngToLayerPoint(this._latlng),h=this.getAllChildMarkers(),_=L.Path.SVG&&this.SVG_ANIMATION;for(s._animationStart(),this.setOpacity(1),i=h.length-1;i>=0;i--)e=h[i],e._preSpiderfyLatlng&&(e.setLatLng(e._preSpiderfyLatlng),delete e._preSpiderfyLatlng,e.setOpacity?(e._setPos(a),e.setOpacity(0)):o.removeLayer(e),_&&(n=e._spiderLeg._path.childNodes[0],n.setAttribute("to",n.getAttribute("from")),n.setAttribute("from",0),n.beginElement(),n=e._spiderLeg._path.childNodes[1],n.setAttribute("from",.5),n.setAttribute("to",0),n.setAttribute("stroke-opacity",0),n.beginElement(),e._spiderLeg._path.setAttribute("stroke-opacity",0)));setTimeout(function(){var t=0;for(i=h.length-1;i>=0;i--)e=h[i],e._spiderLeg&&t++;for(i=h.length-1;i>=0;i--)e=h[i],e._spiderLeg&&(e.setOpacity&&(e.setOpacity(1),e.setZIndexOffset(0)),t>1&&o.removeLayer(e),r.removeLayer(e._spiderLeg),delete e._spiderLeg);s._animationEnd()},200)}}:{_animationSpiderfy:function(t,e){var i,n,s,r,o=this._group,a=o._map,h=o._featureGroup;for(i=t.length-1;i>=0;i--)r=a.layerPointToLatLng(e[i]),n=t[i],n._preSpiderfyLatlng=n._latlng,n.setLatLng(r),n.setZIndexOffset&&n.setZIndexOffset(1e6),h.addLayer(n),s=new L.Polyline([this._latlng,r],{weight:1.5,color:"#222"}),a.addLayer(s),n._spiderLeg=s;this.setOpacity(.3),o.fire("spiderfied")},_animationUnspiderfy:function(){this._noanimationUnspiderfy()}}),L.MarkerClusterGroup.include({_spiderfied:null,_spiderfierOnAdd:function(){this._map.on("click",this._unspiderfyWrapper,this),this._map.options.zoomAnimation&&this._map.on("zoomstart",this._unspiderfyZoomStart,this),this._map.on("zoomend",this._noanimationUnspiderfy,this),L.Path.SVG&&!L.Browser.touch&&this._map._initPathRoot()},_spiderfierOnRemove:function(){this._map.off("click",this._unspiderfyWrapper,this),this._map.off("zoomstart",this._unspiderfyZoomStart,this),this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._unspiderfy()},_unspiderfyZoomStart:function(){this._map&&this._map.on("zoomanim",this._unspiderfyZoomAnim,this)},_unspiderfyZoomAnim:function(t){L.DomUtil.hasClass(this._map._mapPane,"leaflet-touching")||(this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._unspiderfy(t))},_unspiderfyWrapper:function(){this._unspiderfy()},_unspiderfy:function(t){this._spiderfied&&this._spiderfied.unspiderfy(t)},_noanimationUnspiderfy:function(){this._spiderfied&&this._spiderfied._noanimationUnspiderfy()},_unspiderfyLayer:function(t){t._spiderLeg&&(this._featureGroup.removeLayer(t),t.setOpacity(1),t.setZIndexOffset(0),this._map.removeLayer(t._spiderLeg),delete t._spiderLeg)}})}(window,document);
/*
Leaflet.AwesomeMarkers, a plugin that adds colorful iconic markers for Leaflet, based on the Font Awesome icons
(c) 2012-2013, Lennard Voogdt
http://leafletjs.com
https://github.com/lvoogdt
*/
/*global L*/
(function (window, document, undefined) {
"use strict";
/*
* Leaflet.AwesomeMarkers assumes that you have already included the Leaflet library.
*/
L.AwesomeMarkers = {};
L.AwesomeMarkers.version = '2.0.1';
L.AwesomeMarkers.Icon = L.Icon.extend({
options: {
iconSize: [35, 45],
iconAnchor: [17, 42],
popupAnchor: [1, -32],
shadowAnchor: [10, 12],
shadowSize: [36, 16],
className: 'awesome-marker',
prefix: 'glyphicon',
spinClass: 'fa-spin',
extraClasses: '',
icon: 'home',
markerColor: 'blue',
iconColor: 'white'
},
initialize: function (options) {
options = L.Util.setOptions(this, options);
},
createIcon: function () {
var div = document.createElement('div'),
options = this.options;
if (options.icon) {
div.innerHTML = this._createInner();
}
if (options.bgPos) {
div.style.backgroundPosition =
(-options.bgPos.x) + 'px ' + (-options.bgPos.y) + 'px';
}
this._setIconStyles(div, 'icon-' + options.markerColor);
return div;
},
_createInner: function() {
var iconClass, iconSpinClass = "", iconColorClass = "", iconColorStyle = "", options = this.options;
if(options.icon.slice(0,options.prefix.length+1) === options.prefix + "-") {
iconClass = options.icon;
} else {
iconClass = options.prefix + "-" + options.icon;
}
if(options.spin && typeof options.spinClass === "string") {
iconSpinClass = options.spinClass;
}
if(options.iconColor) {
if(options.iconColor === 'white' || options.iconColor === 'black') {
iconColorClass = "icon-" + options.iconColor;
} else {
iconColorStyle = "style='color: " + options.iconColor + "' ";
}
}
return "<i " + iconColorStyle + "class='" + options.extraClasses + " " + options.prefix + " " + iconClass + " " + iconSpinClass + " " + iconColorClass + "'></i>";
},
_setIconStyles: function (img, name) {
var options = this.options,
size = L.point(options[name === 'shadow' ? 'shadowSize' : 'iconSize']),
anchor;
if (name === 'shadow') {
anchor = L.point(options.shadowAnchor || options.iconAnchor);
} else {
anchor = L.point(options.iconAnchor);
}
if (!anchor && size) {
anchor = size.divideBy(2, true);
}
img.className = 'awesome-marker-' + name + ' ' + options.className;
if (anchor) {
img.style.marginLeft = (-anchor.x) + 'px';
img.style.marginTop = (-anchor.y) + 'px';
}
if (size) {
img.style.width = size.x + 'px';
img.style.height = size.y + 'px';
}
},
createShadow: function () {
var div = document.createElement('div');
this._setIconStyles(div, 'shadow');
return div;
}
});
L.AwesomeMarkers.icon = function (options) {
return new L.AwesomeMarkers.Icon(options);
};
}(this, document));
(function() {
$(document).on('turbolinks:load', function() {
$('#event_start_time').change(function() {
if ($('#event_start_time').val() >= $('#event_end_time').val()) {
return $('#event_end_time').val($('#event_start_time').val());
}
});
$('#event_end_time').change(function() {
if ($('#event_start_time').val() >= $('#event_end_time').val()) {
return $('#event_start_time').val($('#event_end_time').val());
}
});
$('#event_repeat').each(function() {
if ($(this).val() === '0') {
$('.field.rule').hide();
}
return $(this).change(function() {
if ($(this).val() > 0) {
return $('.field.rule').show();
} else {
return $('.field.rule').hide();
}
});
});
return $('#event_tags').each(function() {
var elt;
elt = $(this);
return $.ajax({
url: '/tags.json'
}).done(function(data) {
var tags;
return tags = jQuery.map(data, function(n) {
return n[0];
});
});
});
});
}).call(this);
(function() {
$(document).on('turbolinks:load', function() {
$('body.pages form :input').prop('disabled', false);
return $('body.pages form').submit(function() {
$('input[name=utf8]').prop('disabled', true);
return $(':input', this).filter(function() {
return this.value.length === 0;
}).prop('disabled', true);
});
});
}).call(this);
(function() {
$(document).on('turbolinks:load', function() {});
}).call(this);
(function() {
$(document).on('turbolinks:load', function() {});
}).call(this);
(function() {
var modulo = function(a, b) { return (+a % (b = +b) + b) % b; };
$(document).on('turbolinks:load', function() {
var idx, markerColors;
markerColors = ['blue', 'red', 'darkred', 'orange', 'green', 'darkgreen', 'purple', 'darkpuple', 'cadetblue'];
idx = 0;
$('#map.list').each(function() {
var controls, map;
map = L.map('map');
L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a>'
}).addTo(map);
controls = L.control.layers(null, null, {
collapsed: false
}).addTo(map);
return $('li a', this).each(function() {
var markerColor, text, url;
url = $(this).attr('href');
text = $(this).html();
markerColor = markerColors[modulo(idx++, markerColors.length)];
if (location.search && url.indexOf('?') >= 0) {
url += '&' + location.search.substr(1);
} else {
url += location.search;
}
return $.getJSON(url, function(json) {
var layer;
if (json) {
layer = L.markerClusterGroup({
maxClusterRadius: 30
}).addLayer(L.geoJson(json, {
pointToLayer: function(feature, latlng) {
var marker;
marker = L.AwesomeMarkers.icon({
prefix: 'fa',
icon: feature.properties.icon || 'calendar',
markerColor: markerColor
});
return L.marker(latlng, {
icon: marker
});
},
onEachFeature: function(feature, layer) {
if (feature.properties && feature.properties.popupContent) {
return layer.bindPopup(feature.properties.popupContent);
}
}
}));
map.addLayer(layer);
controls.addOverlay(layer, text + ' - ' + json.length);
if ((/maps\//.test(location.href) || /maps.json/.test(url)) && layer.getBounds()._northEast && layer.getBounds()._southWest) {
return map.fitBounds(layer.getBounds());
}
}
});
});
});
return $('#map.event, #map.orga').each(function() {
var coord, map, marker, markerColor, url;
coord = [$(this).data('latitude'), $(this).data('longitude')];
map = L.map('map').setView([coord[0], coord[1]], 16);
L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a>'
}).addTo(map);
url = $(this).data('url');
markerColor = markerColors[modulo(idx++, markerColors.length)];
if (location.search && url.indexOf('?') >= 0) {
url += '&' + location.search.substr(1);
} else {
url += location.search;
}
marker = L.AwesomeMarkers.icon({
prefix: 'fa',
icon: 'calendar'
});
L.marker([coord[0], coord[1]], {
icon: marker
}).addTo(map);
return $.getJSON(url, function(json) {
var layer;
layer = L.markerClusterGroup({
maxClusterRadius: 30
}).addLayer(L.geoJson(json, {
pointToLayer: function(feature, latlng) {
marker = L.AwesomeMarkers.icon({
prefix: 'fa',
icon: feature.properties.icon || 'calendar',
markerColor: markerColor
});
return L.marker(latlng, {
icon: marker
});
},
onEachFeature: function(feature, layer) {
if (feature.properties && feature.properties.popupContent) {
return layer.bindPopup(feature.properties.popupContent);
}
}
}));
return map.addLayer(layer);
});
});
});
}).call(this);
(function() {
$(document).on('turbolinks:load', function() {
$('body.moderations .field.closer input[type=radio]').click(function() {
return $('body.moderations #event_reason').parent().slideUp();
});
return $('body.moderations .field.opener input[type=radio]').click(function() {
return $('body.moderations #event_reason').parent().slideDown();
});
});
}).call(this);
(function() {
}).call(this);
(function() {
}).call(this);
(function() {
var pager;
pager = true;
$(document).on('turbolinks:load', function() {
$('.pagination .next a').attr('data-remote', true).click(function() {
return $('#loading').fadeIn();
});
if (pager) {
pager = false;
$(document).on('ajax:success', '.pagination .next a', function(event, data, status, xhr) {
var elts, next;
$('#loading').fadeOut();
elts = $('tbody tr', data);
$(this).parents('tfoot').prev().append(elts);
next = $('.pagination .next a', data).attr('href');
if (next != null) {
return $(this).show().data('remote', true).attr('href', next);
} else {
return $(this).parents('.pagination').remove();
}
});
}
if ($('.pagination .next a').size() > 0) {
return $(document).scroll(function() {
if ($(window).scrollTop() === $(document).height() - $(window).height() && $('.pagination .next a').is(':visible')) {
return $('.pagination .next a').hide().click();
}
});
}
});
}).call(this);
(function() {
$(document).on('turbolinks:load', function() {
$('table.list.dates tbody tr').each(function() {
var vals;
vals = $(this).find('td.quantity').map(function() {
var val;
val = $(this).find('a').html().replace(' ', '').trim();
if (val && val !== '') {
return parseInt(val);
} else {
return 0;
}
});
return $(this).find('.sparkline').sparkline(vals, {
width: '5em'
});
});
return $('table.list.dates tfoot').each(function() {
var vals;
vals = $(this).find('th.quantity').map(function() {
return parseInt($(this).html().replace(' ', ''));
});
return $(this).find('.sparkline').sparkline(vals, {
type: 'bar',
height: '3em',
barWidth: '100%',
barColor: '#9CC5EE',
barSpacing: 2
});
});
});
}).call(this);
(function() {
$(document).on('turbolinks:load', function() {
return tinyMCE.init({
schema: 'html5',
menubar: false,
language: 'fr_FR',
selector: 'textarea.description',
content_css: '/assets/application-943a3e70ac370e883a0394cbddf7c60a6ec281cd561d3c6817cfa97974531bc8.css',
entity_encoding: 'raw',
add_unload_trigger: true,
browser_spellcheck: true,
toolbar: [' bold italic strikethrough | bullist numlist outdent indent | alignleft aligncenter alignright alignjustify | link image media insertdatetime charmap table | undo redo | searchreplace | code visualblocks preview fullscreen'],
plugins: 'lists, advlist, autolink, link, image, charmap, paste, print, preview, table, fullscreen, searchreplace, media, insertdatetime, visualblocks, visualchars, wordcount, contextmenu, code'
});
});
$(document).on('turbolinks:before-cache', function() {
return tinymce.remove();
});
}).call(this);
(function() {
}).call(this);
(function() {
$(document).on('turbolinks:load', function() {
if (!Modernizr.testAllProps('forceBrokenImageIcon')) {
$('img.favicon').one('error', function() {
return $(this).css({
visibility: 'hidden'
});
});
}
return $('.field.tags input').tagsInput({
delimiter: [' '],
defaultText: '',
autocomplete_url: '/tags.json'
});
});
}).call(this);