/*! * 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 = "" + ""; // 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: ) 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 = ""; 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 = ""; 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 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 = "
a"; // 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>"; // 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 = ""; 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, "" ], legend: [ 1, "
", "
" ], area: [ 1, "", "" ], // Support: IE8 param: [ 1, "", "" ], thead: [ 1, "", "
" ], tr: [ 2, "", "
" ], col: [ 2, "", "
" ], td: [ 3, "", "
" ], // 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
", "
" ] }; // 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 = / from table fragments if ( !support.tbody ) { // String was a , *may* have spurious elem = tag === "table" && !rtbody.test( elem ) ? tmp.firstChild : // String was a bare or wrap[ 1 ] === "
" && !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 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 = /\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>" ); }, 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( "'; } if (typeof html == "undefined") { html = layout.renderHtml(self); } if (self.statusbar) { footerHtml = self.statusbar.renderHtml(); } return ( '
' + '
' + headerHtml + '
' + html + '
' + footerHtml + '
' + '
' ); }, /** * 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; } ); /** * MessageBox.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.MessageBox', [ "tinymce.core.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; } ); /** * WindowManagerImpl.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.ui.WindowManagerImpl', [ "tinymce.core.ui.Window", "tinymce.core.ui.MessageBox" ], function (Window, MessageBox) { return function (editor) { var open = function (args, params, closeCallback) { var win; 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); win.on('close', function () { closeCallback(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 || {}; win = win.renderTo().reflow(); return win; }; var alert = function (message, choiceCallback, closeCallback) { var win; win = MessageBox.alert(message, function () { choiceCallback(); }); win.on('close', function () { closeCallback(win); }); return win; }; var confirm = function (message, choiceCallback, closeCallback) { var win; win = MessageBox.confirm(message, function (state) { choiceCallback(state); }); win.on('close', function () { closeCallback(win); }); return win; }; var close = function (window) { window.close(); }; var getParams = function (window) { return window.params; }; var setParams = function (window, params) { window.params = params; }; return { open: open, alert: alert, confirm: confirm, close: close, getParams: getParams, setParams: setParams }; }; } ); /** * WindowManager.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.api.WindowManager', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Option', 'tinymce.core.ui.WindowManagerImpl' ], function (Arr, Option, WindowManagerImpl) { return function (editor) { var windows = []; var getImplementation = function () { var theme = editor.theme; return theme.getWindowManagerImpl ? theme.getWindowManagerImpl() : WindowManagerImpl(editor); }; var funcBind = function (scope, f) { return function () { return f ? f.apply(scope, arguments) : undefined; }; }; var fireOpenEvent = function (win) { editor.fire('OpenWindow', { win: win }); }; var fireCloseEvent = function (win) { editor.fire('CloseWindow', { win: win }); }; var addWindow = function (win) { windows.push(win); fireOpenEvent(win); }; var closeWindow = function (win) { Arr.findIndex(windows, function (otherWindow) { return otherWindow === win; }).each(function (index) { // Mutate here since third party might have stored away the window array, consider breaking this api windows.splice(index, 1); fireCloseEvent(win); // Move focus back to editor when the last window is closed if (windows.length === 0) { editor.focus(); } }); }; var getTopWindow = function () { return Option.from(windows[0]); }; var open = function (args, params) { editor.editorManager.setActive(editor); // Takes a snapshot in the FocusManager of the selection before focus is lost to dialog if (windows.length === 0) { editor.nodeChanged(); } var win = getImplementation().open(args, params, closeWindow); addWindow(win); return win; }; var alert = function (message, callback, scope) { var win = getImplementation().alert(message, funcBind(scope ? scope : this, callback), closeWindow); addWindow(win); }; var confirm = function (message, callback, scope) { var win = getImplementation().confirm(message, funcBind(scope ? scope : this, callback), closeWindow); addWindow(win); }; var close = function () { getTopWindow().each(function (win) { getImplementation().close(win); closeWindow(win); }); }; var getParams = function () { return getTopWindow().map(getImplementation().getParams).getOr(null); }; var setParams = function (params) { getTopWindow().each(function (win) { getImplementation().setParams(win, params); }); }; var getWindows = function () { return windows; }; editor.on('remove', function () { Arr.each(windows.slice(0), function (win) { getImplementation().close(win); }); }); return { // Used by the legacy3x compat layer and possible third party // TODO: Deprecate this, and possible switch to a immutable window array for getWindows windows: windows, /** * 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). */ open: open, /** * 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!'); */ alert: alert, /** * 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"); * }); */ confirm: confirm, /** * Closes the top most window. * * @method close */ close: 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. */ getParams: getParams, /** * Sets the params of the last opened window. * * @method setParams * @param {Object} params Params object to set for the last opened window. */ setParams: setParams, /** * Returns the currently opened window objects. * * @method getWindows * @return {Array} Array of the currently opened windows. */ getWindows: getWindows }; }; } ); /** * RangePoint.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.dom.RangePoint', [ 'ephox.katamari.api.Arr', 'tinymce.core.geom.ClientRect' ], function (Arr, ClientRect) { var isXYWithinRange = function (clientX, clientY, range) { if (range.collapsed) { return false; } return Arr.foldl(range.getClientRects(), function (state, rect) { return state || ClientRect.containsXY(rect, clientX, clientY); }, false); }; return { isXYWithinRange: isXYWithinRange }; } ); /** * VK.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.util.VK', [ "tinymce.core.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); } }; } ); /** * ControlSelection.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.dom.ControlSelection', [ 'ephox.katamari.api.Fun', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.search.Selectors', 'tinymce.core.dom.NodeType', 'tinymce.core.dom.RangePoint', 'tinymce.core.Env', 'tinymce.core.util.Delay', 'tinymce.core.util.Tools', 'tinymce.core.util.VK' ], function (Fun, Element, Selectors, NodeType, RangePoint, Env, Delay, Tools, VK) { 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; } var isImage = function (elm) { return elm && elm.nodeName === 'IMG'; }; var isEventOnImageOutsideRange = function (evt, range) { return isImage(evt.target) && !RangePoint.isXYWithinRange(evt.clientX, evt.clientY, range); }; var contextMenuSelectImage = function (editor, evt) { var target = evt.target; if (isEventOnImageOutsideRange(evt, editor.selection.getRng()) && !evt.isDefaultPrevented()) { evt.preventDefault(); editor.selection.select(target); } }; 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 Selectors.is(Element.fromDom(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 + ' × ' + 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 + ' × ' + 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)) { if (e.button !== 2) { 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); editor.on('contextmenu', Fun.curry(contextMenuSelectImage, editor)); // 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 }; }; } ); /** * ScrollIntoView.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.dom.ScrollIntoView', [ 'tinymce.core.dom.NodeType' ], function (NodeType) { var getPos = function (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 }; }; var fireScrollIntoViewEvent = function (editor, elm, alignToTop) { var scrollEvent = { elm: elm, alignToTop: alignToTop }; editor.fire('scrollIntoView', scrollEvent); return scrollEvent.isDefaultPrevented(); }; var scrollIntoView = function (editor, elm, alignToTop) { var y, viewPort, dom = editor.dom, root = dom.getRoot(), viewPortY, viewPortH, offsetY = 0; if (fireScrollIntoViewEvent(editor, elm, alignToTop)) { return; } if (!NodeType.isElement(elm)) { return; } if (alignToTop === false) { offsetY = elm.offsetHeight; } if (root.nodeName !== 'BODY') { var scrollContainer = editor.selection.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(editor.getWin()); y = dom.getPos(elm).y + offsetY; viewPortY = viewPort.y; viewPortH = viewPort.h; if (y < viewPort.y || y + 25 > viewPortY + viewPortH) { editor.getWin().scrollTo(0, y < viewPortY ? y : y - viewPortH + 25); } }; return { scrollIntoView: scrollIntoView }; } ); /** * TridentSelection.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.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
|
// Setting innerHTML with a span marker then remove that marker seems to keep empty block elements open container.innerHTML = ''; 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

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:

|

would become this:

|

sibling = startContainer.previousSibling; if (sibling && !sibling.hasChildNodes() && dom.isBlock(sibling)) { sibling.innerHTML = ''; } else { sibling = null; } startContainer.innerHTML = ''; 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; } ); define( 'ephox.sugar.api.dom.Replication', [ 'ephox.sugar.api.properties.Attr', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.dom.Insert', 'ephox.sugar.api.dom.InsertAll', 'ephox.sugar.api.dom.Remove', 'ephox.sugar.api.search.Traverse' ], function (Attr, Element, Insert, InsertAll, Remove, Traverse) { var clone = function (original, deep) { return Element.fromDom(original.dom().cloneNode(deep)); }; /** Shallow clone - just the tag, no children */ var shallow = function (original) { return clone(original, false); }; /** Deep clone - everything copied including children */ var deep = function (original) { return clone(original, true); }; /** Shallow clone, with a new tag */ var shallowAs = function (original, tag) { var nu = Element.fromTag(tag); var attributes = Attr.clone(original); Attr.setAll(nu, attributes); return nu; }; /** Deep clone, with a new tag */ var copy = function (original, tag) { var nu = shallowAs(original, tag); // NOTE // previously this used serialisation: // nu.dom().innerHTML = original.dom().innerHTML; // // Clone should be equivalent (and faster), but if TD <-> TH toggle breaks, put it back. var cloneChildren = Traverse.children(deep(original)); InsertAll.append(nu, cloneChildren); return nu; }; /** Change the tag name, but keep all children */ var mutate = function (original, tag) { var nu = shallowAs(original, tag); Insert.before(original, nu); var children = Traverse.children(original); InsertAll.append(nu, children); Remove.remove(original); return nu; }; return { shallow: shallow, shallowAs: shallowAs, deep: deep, copy: copy, mutate: mutate }; } ); define( 'ephox.sugar.api.node.Fragment', [ 'ephox.katamari.api.Arr', 'ephox.sugar.api.node.Element', 'global!document' ], function (Arr, Element, document) { var fromElements = function (elements, scope) { var doc = scope || document; var fragment = doc.createDocumentFragment(); Arr.each(elements, function (element) { fragment.appendChild(element.dom()); }); return Element.fromDom(fragment); }; return { fromElements: fromElements }; } ); define( 'ephox.sugar.impl.ClosestOrAncestor', [ 'ephox.katamari.api.Type', 'ephox.katamari.api.Option' ], function (Type, Option) { return function (is, ancestor, scope, a, isRoot) { return is(scope, a) ? Option.some(scope) : Type.isFunction(isRoot) && isRoot(scope) ? Option.none() : ancestor(scope, a, isRoot); }; } ); define( 'ephox.sugar.api.search.PredicateFind', [ 'ephox.katamari.api.Type', 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'ephox.katamari.api.Option', 'ephox.sugar.api.node.Body', 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.node.Element', 'ephox.sugar.impl.ClosestOrAncestor' ], function (Type, Arr, Fun, Option, Body, Compare, Element, ClosestOrAncestor) { var first = function (predicate) { return descendant(Body.body(), predicate); }; var ancestor = function (scope, predicate, isRoot) { var element = scope.dom(); var stop = Type.isFunction(isRoot) ? isRoot : Fun.constant(false); while (element.parentNode) { element = element.parentNode; var el = Element.fromDom(element); if (predicate(el)) return Option.some(el); else if (stop(el)) break; } return Option.none(); }; var closest = function (scope, predicate, isRoot) { // This is required to avoid ClosestOrAncestor passing the predicate to itself var is = function (scope) { return predicate(scope); }; return ClosestOrAncestor(is, ancestor, scope, predicate, isRoot); }; var sibling = function (scope, predicate) { var element = scope.dom(); if (!element.parentNode) return Option.none(); return child(Element.fromDom(element.parentNode), function (x) { return !Compare.eq(scope, x) && predicate(x); }); }; var child = function (scope, predicate) { var result = Arr.find(scope.dom().childNodes, Fun.compose(predicate, Element.fromDom)); return result.map(Element.fromDom); }; var descendant = function (scope, predicate) { var descend = function (element) { for (var i = 0; i < element.childNodes.length; i++) { if (predicate(Element.fromDom(element.childNodes[i]))) return Option.some(Element.fromDom(element.childNodes[i])); var res = descend(element.childNodes[i]); if (res.isSome()) return res; } return Option.none(); }; return descend(scope.dom()); }; return { first: first, ancestor: ancestor, closest: closest, sibling: sibling, child: child, descendant: descendant }; } ); define( 'ephox.sugar.api.search.SelectorFind', [ 'ephox.sugar.api.search.PredicateFind', 'ephox.sugar.api.search.Selectors', 'ephox.sugar.impl.ClosestOrAncestor' ], function (PredicateFind, Selectors, ClosestOrAncestor) { // TODO: An internal SelectorFilter module that doesn't Element.fromDom() everything var first = function (selector) { return Selectors.one(selector); }; var ancestor = function (scope, selector, isRoot) { return PredicateFind.ancestor(scope, function (e) { return Selectors.is(e, selector); }, isRoot); }; var sibling = function (scope, selector) { return PredicateFind.sibling(scope, function (e) { return Selectors.is(e, selector); }); }; var child = function (scope, selector) { return PredicateFind.child(scope, function (e) { return Selectors.is(e, selector); }); }; var descendant = function (scope, selector) { return Selectors.one(selector, scope); }; var closest = function (scope, selector, isRoot) { return ClosestOrAncestor(Selectors.is, ancestor, scope, selector, isRoot); }; return { first: first, ancestor: ancestor, sibling: sibling, child: child, descendant: descendant, closest: closest }; } ); /** * Parents.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.dom.Parents', [ 'ephox.katamari.api.Fun', 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.search.Traverse' ], function (Fun, Compare, Traverse) { var dropLast = function (xs) { return xs.slice(0, -1); }; var parentsUntil = function (startNode, rootElm, predicate) { if (Compare.contains(rootElm, startNode)) { return dropLast(Traverse.parents(startNode, function (elm) { return predicate(elm) || Compare.eq(elm, rootElm); })); } else { return []; } }; var parents = function (startNode, rootElm) { return parentsUntil(startNode, rootElm, Fun.constant(false)); }; var parentsAndSelf = function (startNode, rootElm) { return [startNode].concat(parents(startNode, rootElm)); }; return { parentsUntil: parentsUntil, parents: parents, parentsAndSelf: parentsAndSelf }; } ); define( 'ephox.katamari.api.Options', [ 'ephox.katamari.api.Option' ], function (Option) { /** cat :: [Option a] -> [a] */ var cat = function (arr) { var r = []; var push = function (x) { r.push(x); }; for (var i = 0; i < arr.length; i++) { arr[i].each(push); } return r; }; /** findMap :: ([a], (a, Int -> Option b)) -> Option b */ var findMap = function (arr, f) { for (var i = 0; i < arr.length; i++) { var r = f(arr[i], i); if (r.isSome()) { return r; } } return Option.none(); }; /** * if all elements in arr are 'some', their inner values are passed as arguments to f * f must have arity arr.length */ var liftN = function(arr, f) { var r = []; for (var i = 0; i < arr.length; i++) { var x = arr[i]; if (x.isSome()) { r.push(x.getOrDie()); } else { return Option.none(); } } return Option.some(f.apply(null, r)); }; return { cat: cat, findMap: findMap, liftN: liftN }; } ); /** * SelectionUtils.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.selection.SelectionUtils', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'ephox.katamari.api.Option', 'ephox.katamari.api.Options', 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.node.Node', 'ephox.sugar.api.search.Traverse', 'tinymce.core.dom.NodeType' ], function (Arr, Fun, Option, Options, Compare, Element, Node, Traverse, NodeType) { var getStartNode = function (rng) { var sc = rng.startContainer, so = rng.startOffset; if (NodeType.isText(sc)) { return so === 0 ? Option.some(Element.fromDom(sc)) : Option.none(); } else { return Option.from(sc.childNodes[so]).map(Element.fromDom); } }; var getEndNode = function (rng) { var ec = rng.endContainer, eo = rng.endOffset; if (NodeType.isText(ec)) { return eo === ec.data.length ? Option.some(Element.fromDom(ec)) : Option.none(); } else { return Option.from(ec.childNodes[eo - 1]).map(Element.fromDom); } }; var getFirstChildren = function (node) { return Traverse.firstChild(node).fold( Fun.constant([node]), function (child) { return [node].concat(getFirstChildren(child)); } ); }; var getLastChildren = function (node) { return Traverse.lastChild(node).fold( Fun.constant([node]), function (child) { if (Node.name(child) === 'br') { return Traverse.prevSibling(child).map(function (sibling) { return [node].concat(getLastChildren(sibling)); }).getOr([]); } else { return [node].concat(getLastChildren(child)); } } ); }; var hasAllContentsSelected = function (elm, rng) { return Options.liftN([getStartNode(rng), getEndNode(rng)], function (startNode, endNode) { var start = Arr.find(getFirstChildren(elm), Fun.curry(Compare.eq, startNode)); var end = Arr.find(getLastChildren(elm), Fun.curry(Compare.eq, endNode)); return start.isSome() && end.isSome(); }).getOr(false); }; return { hasAllContentsSelected: hasAllContentsSelected }; } ); /** * SimpleTableModel.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.selection.SimpleTableModel', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Option', 'ephox.katamari.api.Struct', 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.dom.Insert', 'ephox.sugar.api.dom.InsertAll', 'ephox.sugar.api.dom.Replication', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.properties.Attr', 'ephox.sugar.api.search.SelectorFilter' ], function (Arr, Option, Struct, Compare, Insert, InsertAll, Replication, Element, Attr, SelectorFilter) { var tableModel = Struct.immutable('element', 'width', 'rows'); var tableRow = Struct.immutable('element', 'cells'); var cellPosition = Struct.immutable('x', 'y'); var getSpan = function (td, key) { var value = parseInt(Attr.get(td, key), 10); return isNaN(value) ? 1 : value; }; var fillout = function (table, x, y, tr, td) { var rowspan = getSpan(td, 'rowspan'); var colspan = getSpan(td, 'colspan'); var rows = table.rows(); for (var y2 = y; y2 < y + rowspan; y2++) { if (!rows[y2]) { rows[y2] = tableRow(Replication.deep(tr), []); } for (var x2 = x; x2 < x + colspan; x2++) { var cells = rows[y2].cells(); // not filler td:s are purposely not cloned so that we can // find cells in the model by element object references cells[x2] = y2 == y && x2 == x ? td : Replication.shallow(td); } } }; var cellExists = function (table, x, y) { var rows = table.rows(); var cells = rows[y] ? rows[y].cells() : []; return !!cells[x]; }; var skipCellsX = function (table, x, y) { while (cellExists(table, x, y)) { x++; } return x; }; var getWidth = function (rows) { return Arr.foldl(rows, function (acc, row) { return row.cells().length > acc ? row.cells().length : acc; }, 0); }; var findElementPos = function (table, element) { var rows = table.rows(); for (var y = 0; y < rows.length; y++) { var cells = rows[y].cells(); for (var x = 0; x < cells.length; x++) { if (Compare.eq(cells[x], element)) { return Option.some(cellPosition(x, y)); } } } return Option.none(); }; var extractRows = function (table, sx, sy, ex, ey) { var newRows = []; var rows = table.rows(); for (var y = sy; y <= ey; y++) { var cells = rows[y].cells(); var slice = sx < ex ? cells.slice(sx, ex + 1) : cells.slice(ex, sx + 1); newRows.push(tableRow(rows[y].element(), slice)); } return newRows; }; var subTable = function (table, startPos, endPos) { var sx = startPos.x(), sy = startPos.y(); var ex = endPos.x(), ey = endPos.y(); var newRows = sy < ey ? extractRows(table, sx, sy, ex, ey) : extractRows(table, sx, ey, ex, sy); return tableModel(table.element(), getWidth(newRows), newRows); }; var createDomTable = function (table, rows) { var tableElement = Replication.shallow(table.element()); var tableBody = Element.fromTag('tbody'); InsertAll.append(tableBody, rows); Insert.append(tableElement, tableBody); return tableElement; }; var modelRowsToDomRows = function (table) { return Arr.map(table.rows(), function (row) { var cells = Arr.map(row.cells(), function (cell) { var td = Replication.deep(cell); Attr.remove(td, 'colspan'); Attr.remove(td, 'rowspan'); return td; }); var tr = Replication.shallow(row.element()); InsertAll.append(tr, cells); return tr; }); }; var fromDom = function (tableElm) { var table = tableModel(Replication.shallow(tableElm), 0, []); Arr.each(SelectorFilter.descendants(tableElm, 'tr'), function (tr, y) { Arr.each(SelectorFilter.descendants(tr, 'td,th'), function (td, x) { fillout(table, skipCellsX(table, x, y), y, tr, td); }); }); return tableModel(table.element(), getWidth(table.rows()), table.rows()); }; var toDom = function (table) { return createDomTable(table, modelRowsToDomRows(table)); }; var subsection = function (table, startElement, endElement) { return findElementPos(table, startElement).bind(function (startPos) { return findElementPos(table, endElement).map(function (endPos) { return subTable(table, startPos, endPos); }); }); }; return { fromDom: fromDom, toDom: toDom, subsection: subsection }; } ); /** * MultiRange.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.selection.MultiRange', [ 'ephox.katamari.api.Arr', 'ephox.sugar.api.node.Element', 'tinymce.core.dom.RangeUtils' ], function (Arr, Element, RangeUtils) { var getRanges = function (selection) { var ranges = []; for (var i = 0; i < selection.rangeCount; i++) { ranges.push(selection.getRangeAt(i)); } return ranges; }; var getSelectedNodes = function (ranges) { return Arr.bind(ranges, function (range) { var node = RangeUtils.getSelectedNode(range); return node ? [ Element.fromDom(node) ] : []; }); }; var hasMultipleRanges = function (selection) { return getRanges(selection).length > 1; }; return { getRanges: getRanges, getSelectedNodes: getSelectedNodes, hasMultipleRanges: hasMultipleRanges }; } ); /** * TableCellSelection.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.selection.TableCellSelection', [ 'ephox.katamari.api.Arr', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.search.SelectorFilter', 'tinymce.core.dom.ElementType', 'tinymce.core.selection.MultiRange' ], function (Arr, Element, SelectorFilter, ElementType, MultiRange) { var getCellsFromRanges = function (ranges) { return Arr.filter(MultiRange.getSelectedNodes(ranges), ElementType.isTableCell); }; var getCellsFromElement = function (elm) { var selectedCells = SelectorFilter.descendants(elm, 'td[data-mce-selected],th[data-mce-selected]'); return selectedCells; }; var getCellsFromElementOrRanges = function (ranges, element) { var selectedCells = getCellsFromElement(element); var rangeCells = getCellsFromRanges(ranges); return selectedCells.length > 0 ? selectedCells : rangeCells; }; var getCellsFromEditor = function (editor) { return getCellsFromElementOrRanges(MultiRange.getRanges(editor.selection.getSel()), Element.fromDom(editor.getBody())); }; return { getCellsFromRanges: getCellsFromRanges, getCellsFromElement: getCellsFromElement, getCellsFromElementOrRanges: getCellsFromElementOrRanges, getCellsFromEditor: getCellsFromEditor }; } ); /** * FragmentReader.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.selection.FragmentReader', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.dom.Insert', 'ephox.sugar.api.dom.Replication', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.node.Fragment', 'ephox.sugar.api.node.Node', 'ephox.sugar.api.search.SelectorFind', 'ephox.sugar.api.search.Traverse', 'tinymce.core.dom.ElementType', 'tinymce.core.dom.Parents', 'tinymce.core.selection.SelectionUtils', 'tinymce.core.selection.SimpleTableModel', 'tinymce.core.selection.TableCellSelection' ], function (Arr, Fun, Compare, Insert, Replication, Element, Fragment, Node, SelectorFind, Traverse, ElementType, Parents, SelectionUtils, SimpleTableModel, TableCellSelection) { var findParentListContainer = function (parents) { return Arr.find(parents, function (elm) { return Node.name(elm) === 'ul' || Node.name(elm) === 'ol'; }); }; var getFullySelectedListWrappers = function (parents, rng) { return Arr.find(parents, function (elm) { return Node.name(elm) === 'li' && SelectionUtils.hasAllContentsSelected(elm, rng); }).fold( Fun.constant([]), function (li) { return findParentListContainer(parents).map(function (listCont) { return [ Element.fromTag('li'), Element.fromTag(Node.name(listCont)) ]; }).getOr([]); } ); }; var wrap = function (innerElm, elms) { var wrapped = Arr.foldl(elms, function (acc, elm) { Insert.append(elm, acc); return elm; }, innerElm); return elms.length > 0 ? Fragment.fromElements([wrapped]) : wrapped; }; var directListWrappers = function (commonAnchorContainer) { if (ElementType.isListItem(commonAnchorContainer)) { return Traverse.parent(commonAnchorContainer).filter(ElementType.isList).fold( Fun.constant([]), function (listElm) { return [ commonAnchorContainer, listElm ]; } ); } else { return ElementType.isList(commonAnchorContainer) ? [ commonAnchorContainer ] : [ ]; } }; var getWrapElements = function (rootNode, rng) { var commonAnchorContainer = Element.fromDom(rng.commonAncestorContainer); var parents = Parents.parentsAndSelf(commonAnchorContainer, rootNode); var wrapElements = Arr.filter(parents, function (elm) { return ElementType.isInline(elm) || ElementType.isHeading(elm); }); var listWrappers = getFullySelectedListWrappers(parents, rng); var allWrappers = wrapElements.concat(listWrappers.length ? listWrappers : directListWrappers(commonAnchorContainer)); return Arr.map(allWrappers, Replication.shallow); }; var emptyFragment = function () { return Fragment.fromElements([]); }; var getFragmentFromRange = function (rootNode, rng) { return wrap(Element.fromDom(rng.cloneContents()), getWrapElements(rootNode, rng)); }; var getParentTable = function (rootElm, cell) { return SelectorFind.ancestor(cell, 'table', Fun.curry(Compare.eq, rootElm)); }; var getTableFragment = function (rootNode, selectedTableCells) { return getParentTable(rootNode, selectedTableCells[0]).bind(function (tableElm) { var firstCell = selectedTableCells[0]; var lastCell = selectedTableCells[selectedTableCells.length - 1]; var fullTableModel = SimpleTableModel.fromDom(tableElm); return SimpleTableModel.subsection(fullTableModel, firstCell, lastCell).map(function (sectionedTableModel) { return Fragment.fromElements([SimpleTableModel.toDom(sectionedTableModel)]); }); }).getOrThunk(emptyFragment); }; var getSelectionFragment = function (rootNode, ranges) { return ranges.length > 0 && ranges[0].collapsed ? emptyFragment() : getFragmentFromRange(rootNode, ranges[0]); }; var read = function (rootNode, ranges) { var selectedCells = TableCellSelection.getCellsFromElementOrRanges(ranges, rootNode); return selectedCells.length > 0 ? getTableFragment(rootNode, selectedCells) : getSelectionFragment(rootNode, ranges); }; return { read: read }; } ); /** * Selection.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.dom.Selection', [ 'ephox.katamari.api.Arr', 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.node.Element', 'tinymce.core.caret.CaretPosition', 'tinymce.core.dom.BookmarkManager', 'tinymce.core.dom.ControlSelection', 'tinymce.core.dom.NodeType', 'tinymce.core.dom.RangeUtils', 'tinymce.core.dom.ScrollIntoView', 'tinymce.core.dom.TreeWalker', 'tinymce.core.dom.TridentSelection', 'tinymce.core.Env', 'tinymce.core.selection.FragmentReader', 'tinymce.core.selection.MultiRange', 'tinymce.core.text.Zwsp', 'tinymce.core.util.Tools' ], function ( Arr, Compare, Element, CaretPosition, BookmarkManager, ControlSelection, NodeType, RangeUtils, ScrollIntoView, TreeWalker, TridentSelection, Env, FragmentReader, MultiRange, Zwsp, Tools ) { var each = Tools.each, trim = Tools.trim; var isIE = Env.ie; var isAttachedToDom = function (node) { return !!(node && node.ownerDocument) && Compare.contains(Element.fromDom(node.ownerDocument), Element.fromDom(node)); }; var isValidRange = function (rng) { if (!rng) { return false; } else if (rng.select) { // Native IE range still produced by placeCaretAt return true; } else { return isAttachedToDom(rng.startContainer) && isAttachedToDom(rng.endContainer); } }; var eventProcessRanges = function (editor, ranges) { return Arr.map(ranges, function (range) { var evt = editor.fire('GetSelectionRange', { range: range }); return evt.range !== range ? evt.range : range; }); }; /** * 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; var ranges = eventProcessRanges(self.editor, MultiRange.getRanges(this.getSel())); 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() ? '' : Zwsp.trim(rng.text || (se.toString ? se.toString() : '')); } if (rng.cloneContents) { fragment = args.contextual ? FragmentReader.read(Element.fromDom(self.editor.getBody()), ranges).dom() : 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 = '
' + (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('Some contents'); */ 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 += '_'; // 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('_' + 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; 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 } rng = eventProcessRanges(self.editor, [ rng ])[0]; // 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 (!isValidRange(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, forward: forward }); 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 when the image is floated 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') { sel.setBaseAndExtent( rng.startContainer, rng.startOffset, rng.endContainer, rng.endOffset ); // Since the setBaseAndExtent is fixed in more recent Blink versions we // need to detect if it's doing the wrong thing and falling back to the // crazy incorrect behavior api call since that seems to be the only way // to get it to work on Safari WebKit as of 2017-02-23 if (sel.anchorNode !== rng.startContainer || sel.focusNode !== rng.endContainer) { sel.setBaseAndExtent(node, 0, node, 1); } } } } } self.editor.fire('AfterSetSelectionRange', { range: rng, forward: forward }); } 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 (new RangeUtils(self.dom).normalize(rng) && !MultiRange.hasMultipleRanges(self.getSel())) { 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) { ScrollIntoView.scrollIntoView(this.editor, elm, alignToTop); }, 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; } ); /** * Node.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.html.Node', [ ], function () { var whiteSpaceRegExp = /^[ \t\r\n]*$/; var typeLookup = { '#text': 3, '#comment': 8, '#cdata': 4, '#pi': 7, '#doctype': 10, '#document-fragment': 11 }; // Walks the tree left/right function walk(node, rootNode, 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 !== rootNode) { sibling = node[siblingName]; if (sibling) { return sibling; } // Walk up the parents to look for siblings for (parent = node.parent; parent && parent !== rootNode; 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} refNode 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, refNode, before) { var parent; if (node.parent) { node.remove(); } parent = refNode.parent || this; if (before) { if (refNode === parent.firstChild) { parent.firstChild = node; } else { refNode.prev.next = node; } node.prev = refNode.prev; node.next = refNode; refNode.prev = node; } else { if (refNode === parent.lastChild) { parent.lastChild = node; } else { refNode.next.prev = node; } node.next = refNode.next; node.prev = refNode; refNode.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. * @param {Object} whitespace Name/value object with elements that are automatically treated whitespace preservables. * @param {function} predicate Optional predicate that gets called after the other rules determine that the node is empty. Should return true if the node is a content node. * @return {Boolean} true/false if the node is empty or not. */ isEmpty: function (elements, whitespace, predicate) { var self = this, node = self.firstChild, i, name; whitespace = whitespace || {}; if (node) { do { if (node.type === 1) { // Ignore bogus elements if (node.attributes.map['data-mce-bogus']) { continue; } // Keep empty elements like if (elements[node.name]) { return false; } // Keep bookmark nodes and name attribute like 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; } // Keep whitespace preserve elements if (node.type === 3 && node.parent && whitespace[node.parent.name] && whiteSpaceRegExp.test(node.value)) { return false; } // Predicate tells that the node is contents if (predicate && predicate(node)) { 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; } ); /** * SaxParser.js * * Released under LGPL License. * Copyright (c) 1999-2017 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

. 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.core.html.SaxParser', [ "tinymce.core.html.Schema", "tinymce.core.html.Entities", "tinymce.core.util.Tools" ], function (Schema, Entities, Tools) { var each = Tools.each; var isValidPrefixAttrName = function (name) { return name.indexOf('data-') === 0 || name.indexOf('aria-') === 0; }; var trimComments = function (text) { return text.replace(//g, ''); }; /** * 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('text'); * @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, processHtml; 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 && isValidPrefixAttrName(name) === false) { 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; } } // Block data or event attributes on elements marked as internal if (isInternalElement && (name in filteredUrlAttrs || name.indexOf('on') === 0)) { 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 '(?:\\/([A-Za-z][A-Za-z0-9\\-_\\:\\.]*)>)|' + // End element '(?:([A-Za-z][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(); processHtml = html + '>'; while ((matches = tokenRegExp.exec(processHtml))) { // Adds and extra '>' to keep regexps from doing catastrofic backtracking on malformed 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 // Did we consume the extra character then treat it as text // This handles the case with html like this: "text a html.length) { self.text(decode(html.substr(matches.index))); index = matches.index + matches[0].length; continue; } 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
  • after an open
  • 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(trimComments(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; } ); /** * DomParser.js * * Released under LGPL License. * Copyright (c) 1999-2017 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:

    a

    b

    c

    will become

    a

    b

    c

    * * @example * var parser = new tinymce.html.DomParser({validate: true}, schema); * var rootNode = parser.parse('

    content

    '); * * @class tinymce.html.DomParser * @version 3.4 */ define( 'tinymce.core.html.DomParser', [ "tinymce.core.html.Node", "tinymce.core.html.Schema", "tinymce.core.html.SaxParser", "tinymce.core.util.Tools" ], function (Node, Schema, SaxParser, Tools) { var makeMap = Tools.makeMap, each = Tools.each, explode = Tools.explode, extend = Tools.extend; var paddEmptyNode = function (settings, node) { if (settings.padd_empty_with_br) { node.empty().append(new Node('br', '1')).shortEnded = true; } else { node.empty().append(new Node('#text', '3')).value = '\u00a0'; } }; var hasOnlyChild = function (node, name) { return node && node.firstChild === node.lastChild && node.firstChild.name === name; }; var isPadded = function (schema, node) { var rule = schema.getElementRule(node.name); return rule && rule.paddEmpty; }; var isEmpty = function (schema, nonEmptyElements, whitespaceElements, node) { return node.isEmpty(nonEmptyElements, whitespaceElements, function (node) { return isPadded(schema, node); }); }; /** * 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, whitespaceElements, nonSplitableElements, textBlockElements, specialElements, sibling, nextNode; nonSplitableElements = makeMap('tr,td,th,tbody,thead,tfoot,table'); nonEmptyElements = schema.getNonEmptyElements(); whitespaceElements = schema.getWhiteSpaceElements(); 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 (!isEmpty(schema, nonEmptyElements, whitespaceElements, newParent)) { 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


    parent = parents[0]; if (isEmpty(schema, nonEmptyElements, whitespaceElements, parent) || hasOnlyChild(parent, '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('text'); * @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: //

    x

    ->

    x

    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
    or 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 (isEmpty(schema, nonEmptyElements, whiteSpaceElements, node)) { if (elementRule.paddEmpty) { paddEmptyNode(settings, node); } else { // Leave nodes that have a name like 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
    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 whiteSpaceElements = schema.getNonEmptyElements(); var elementRule, textNode; // Remove brs from body element as well blockElements.body = 1; // Must loop forwards since it will otherwise remove all brs in

    a


    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

    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 (isEmpty(schema, nonEmptyElements, whiteSpaceElements, parent)) { 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) { paddEmptyNode(settings, parent); } } } } } else { // Replaces BR elements inside inline elements like


    // so they become

     

    lastParent = node; while (parent && parent.firstChild === lastParent && parent.lastChild === lastParent) { lastParent = parent; if (blockElements[parent.name]) { break; } parent = parent.parent; } if (lastParent === parent && settings.padd_empty_with_br !== true) { textNode = new Node('#text', 3); textNode.value = '\u00a0'; node.replace(textNode); } } } }); } self.addAttributeFilter('href', function (nodes) { var i = nodes.length, node; var appendRel = function (rel) { var parts = rel.split(' ').filter(function (p) { return p.length > 0; }); return parts.concat(['noopener']).sort().join(' '); }; var addNoOpener = function (rel) { var newRel = rel ? Tools.trim(rel) : ''; if (!/\b(noopener)\b/g.test(newRel)) { return appendRel(newRel); } else { return newRel; } }; if (!settings.allow_unsafe_link_target) { while (i--) { node = nodes[i]; if (node.name === 'a' && node.attr('target') === '_blank') { node.attr('rel', addNoOpener(node.attr('rel'))); } } } }); // 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.fix_list_elements) { self.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); } else { var li = new Node('li', 1); li.attr('style', 'list-style-type: none'); node.wrap(li); } } } }); } 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); } }); } }; } ); /** * Writer.js * * Released under LGPL License. * Copyright (c) 1999-2017 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('


    '); * console.log(writer.getContent()); * * @class tinymce.html.Writer * @version 3.4 */ define( 'tinymce.core.html.Writer', [ "tinymce.core.html.Entities", "tinymce.core.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

    . * * @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
    . */ 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

    . * * @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(''); 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 . * * @method cdata * @param {String} text String to write out inside the cdata. */ cdata: function (text) { html.push(''); }, /** * Writes a comment node such as . * * @method cdata * @param {String} text String to write out inside the comment. */ comment: function (text) { html.push(''); }, /** * Writes a PI node such as . * * @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(''); } else { html.push(''); } if (indent) { html.push('\n'); } }, /** * Writes a doctype node such as . * * @method doctype * @param {String} text String to write out inside the doctype. */ doctype: function (text) { html.push('', 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$/, ''); } }; }; } ); /** * Serializer.js * * Released under LGPL License. * Copyright (c) 1999-2017 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('

    text

    ')); * @class tinymce.html.Serializer * @version 3.4 */ define( 'tinymce.core.html.Serializer', [ "tinymce.core.html.Writer", "tinymce.core.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('

    text

    ')); * @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(); }; }; } ); /** * Serializer.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.dom.Serializer', [ "tinymce.core.dom.DOMUtils", "tinymce.core.html.DomParser", "tinymce.core.html.SaxParser", "tinymce.core.html.Entities", "tinymce.core.html.Serializer", "tinymce.core.html.Node", "tinymce.core.html.Schema", "tinymce.core.Env", "tinymce.core.util.Tools", "tinymce.core.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: text becomes text

    */ 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([ ']+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 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(/()/g, '\n') .replace(/^[\r\n]*|[\r\n]*$/g, '') .replace(/^\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 = '// '; } } else { if (value.length > 0) { node.firstChild.value = ''; } } } }); // 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'); } } } }); // 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 content = Zwsp.trim(trim(args.getInner ? node.innerHTML : dom.getOuterHTML(node))); rootNode = htmlParser.parse(content, args); trimTrailingBr(rootNode); // Serialize HTML htmlSerializer = new Serializer(settings, schema); args.content = htmlSerializer.serialize(rootNode); // 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 }; }; } ); /** * DeleteUtils.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.delete.DeleteUtils', [ 'ephox.katamari.api.Option', 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.search.PredicateFind', 'tinymce.core.dom.ElementType' ], function (Option, Compare, Element, PredicateFind, ElementType) { var isBeforeRoot = function (rootNode) { return function (elm) { return Compare.eq(rootNode, Element.fromDom(elm.dom().parentNode)); }; }; var getParentBlock = function (rootNode, elm) { return Compare.contains(rootNode, elm) ? PredicateFind.closest(elm, function (element) { return ElementType.isTextBlock(element) || ElementType.isListItem(element); }, isBeforeRoot(rootNode)) : Option.none(); }; var placeCaretInEmptyBody = function (editor) { var body = editor.getBody(); var node = body.firstChild && editor.dom.isBlock(body.firstChild) ? body.firstChild : body; editor.selection.setCursorLocation(node, 0); }; var paddEmptyBody = function (editor) { if (editor.dom.isEmpty(editor.getBody())) { editor.setContent(''); placeCaretInEmptyBody(editor); } }; return { getParentBlock: getParentBlock, paddEmptyBody: paddEmptyBody }; } ); define( 'ephox.sugar.api.search.SelectorExists', [ 'ephox.sugar.api.search.SelectorFind' ], function (SelectorFind) { var any = function (selector) { return SelectorFind.first(selector).isSome(); }; var ancestor = function (scope, selector, isRoot) { return SelectorFind.ancestor(scope, selector, isRoot).isSome(); }; var sibling = function (scope, selector) { return SelectorFind.sibling(scope, selector).isSome(); }; var child = function (scope, selector) { return SelectorFind.child(scope, selector).isSome(); }; var descendant = function (scope, selector) { return SelectorFind.descendant(scope, selector).isSome(); }; var closest = function (scope, selector, isRoot) { return SelectorFind.closest(scope, selector, isRoot).isSome(); }; return { any: any, ancestor: ancestor, sibling: sibling, child: child, descendant: descendant, closest: closest }; } ); /** * Empty.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.dom.Empty', [ 'ephox.katamari.api.Fun', 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.search.SelectorExists', 'tinymce.core.caret.CaretCandidate', 'tinymce.core.dom.NodeType', 'tinymce.core.dom.TreeWalker' ], function (Fun, Compare, Element, SelectorExists, CaretCandidate, NodeType, TreeWalker) { var hasWhitespacePreserveParent = function (rootNode, node) { var rootElement = Element.fromDom(rootNode); var startNode = Element.fromDom(node); return SelectorExists.ancestor(startNode, 'pre,code', Fun.curry(Compare.eq, rootElement)); }; var isWhitespace = function (rootNode, node) { return NodeType.isText(node) && /^[ \t\r\n]*$/.test(node.data) && hasWhitespacePreserveParent(rootNode, node) === false; }; var isNamedAnchor = function (node) { return NodeType.isElement(node) && node.nodeName === 'A' && node.hasAttribute('name'); }; var isContent = function (rootNode, node) { return (CaretCandidate.isCaretCandidate(node) && isWhitespace(rootNode, node) === false) || isNamedAnchor(node) || isBookmark(node); }; var isBookmark = NodeType.hasAttribute('data-mce-bookmark'); var isBogus = NodeType.hasAttribute('data-mce-bogus'); var isBogusAll = NodeType.hasAttributeValue('data-mce-bogus', 'all'); var isEmptyNode = function (targetNode) { var walker, node, brCount = 0; if (isContent(targetNode, targetNode)) { return false; } else { node = targetNode.firstChild; if (!node) { return true; } walker = new TreeWalker(node, targetNode); do { if (isBogusAll(node)) { node = walker.next(true); continue; } if (isBogus(node)) { node = walker.next(); continue; } if (NodeType.isBr(node)) { brCount++; node = walker.next(); continue; } if (isContent(targetNode, node)) { return false; } node = walker.next(); } while (node); return brCount <= 1; } }; var isEmpty = function (elm) { return isEmptyNode(elm.dom()); }; return { isEmpty: isEmpty }; } ); /** * BlockBoundary.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.delete.BlockBoundary', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'ephox.katamari.api.Option', 'ephox.katamari.api.Options', 'ephox.katamari.api.Struct', 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.node.Node', 'ephox.sugar.api.search.PredicateFind', 'ephox.sugar.api.search.Traverse', 'tinymce.core.caret.CaretFinder', 'tinymce.core.caret.CaretPosition', 'tinymce.core.delete.DeleteUtils', 'tinymce.core.dom.Empty', 'tinymce.core.dom.NodeType' ], function (Arr, Fun, Option, Options, Struct, Compare, Element, Node, PredicateFind, Traverse, CaretFinder, CaretPosition, DeleteUtils, Empty, NodeType) { var BlockPosition = Struct.immutable('block', 'position'); var BlockBoundary = Struct.immutable('from', 'to'); var getBlockPosition = function (rootNode, pos) { var rootElm = Element.fromDom(rootNode); var containerElm = Element.fromDom(pos.container()); return DeleteUtils.getParentBlock(rootElm, containerElm).map(function (block) { return BlockPosition(block, pos); }); }; var isDifferentBlocks = function (blockBoundary) { return Compare.eq(blockBoundary.from().block(), blockBoundary.to().block()) === false; }; var hasSameParent = function (blockBoundary) { return Traverse.parent(blockBoundary.from().block()).bind(function (parent1) { return Traverse.parent(blockBoundary.to().block()).filter(function (parent2) { return Compare.eq(parent1, parent2); }); }).isSome(); }; var isEditable = function (blockBoundary) { return NodeType.isContentEditableFalse(blockBoundary.from().block()) === false && NodeType.isContentEditableFalse(blockBoundary.to().block()) === false; }; var skipLastBr = function (rootNode, forward, blockPosition) { if (NodeType.isBr(blockPosition.position().getNode()) && Empty.isEmpty(blockPosition.block()) === false) { return CaretFinder.positionIn(false, blockPosition.block().dom()).bind(function (lastPositionInBlock) { if (lastPositionInBlock.isEqual(blockPosition.position())) { return CaretFinder.fromPosition(forward, rootNode, lastPositionInBlock).bind(function (to) { return getBlockPosition(rootNode, to); }); } else { return Option.some(blockPosition); } }).getOr(blockPosition); } else { return blockPosition; } }; var readFromRange = function (rootNode, forward, rng) { var fromBlockPos = getBlockPosition(rootNode, CaretPosition.fromRangeStart(rng)); var toBlockPos = fromBlockPos.bind(function (blockPos) { return CaretFinder.fromPosition(forward, rootNode, blockPos.position()).bind(function (to) { return getBlockPosition(rootNode, to).map(function (blockPos) { return skipLastBr(rootNode, forward, blockPos); }); }); }); return Options.liftN([fromBlockPos, toBlockPos], BlockBoundary).filter(function (blockBoundary) { return isDifferentBlocks(blockBoundary) && hasSameParent(blockBoundary) && isEditable(blockBoundary); }); }; var read = function (rootNode, forward, rng) { return rng.collapsed ? readFromRange(rootNode, forward, rng) : Option.none(); }; return { read: read }; } ); /** * MergeBlocks.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.delete.MergeBlocks', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Option', 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.dom.Insert', 'ephox.sugar.api.dom.Remove', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.search.Traverse', 'tinymce.core.caret.CaretFinder', 'tinymce.core.caret.CaretPosition', 'tinymce.core.dom.ElementType', 'tinymce.core.dom.Empty', 'tinymce.core.dom.NodeType', 'tinymce.core.dom.Parents' ], function (Arr, Option, Compare, Insert, Remove, Element, Traverse, CaretFinder, CaretPosition, ElementType, Empty, NodeType, Parents) { var getChildrenUntilBlockBoundary = function (block) { var children = Traverse.children(block); return Arr.findIndex(children, ElementType.isBlock).fold( function () { return children; }, function (index) { return children.slice(0, index); } ); }; var extractChildren = function (block) { var children = getChildrenUntilBlockBoundary(block); Arr.each(children, function (node) { Remove.remove(node); }); return children; }; var trimBr = function (first, block) { CaretFinder.positionIn(first, block.dom()).each(function (position) { var node = position.getNode(); if (NodeType.isBr(node)) { Remove.remove(Element.fromDom(node)); } }); }; var removeEmptyRoot = function (rootNode, block) { var parents = Parents.parentsAndSelf(block, rootNode); return Arr.find(parents.reverse(), Empty.isEmpty).each(Remove.remove); }; var findParentInsertPoint = function (toBlock, block) { var parents = Traverse.parents(block, function (elm) { return Compare.eq(elm, toBlock); }); return Option.from(parents[parents.length - 2]); }; var getInsertionPoint = function (fromBlock, toBlock) { if (Compare.contains(toBlock, fromBlock)) { return Traverse.parent(fromBlock).bind(function (parent) { return Compare.eq(parent, toBlock) ? Option.some(fromBlock) : findParentInsertPoint(toBlock, fromBlock); }); } else { return Option.none(); } }; var mergeBlockInto = function (rootNode, fromBlock, toBlock) { if (Empty.isEmpty(toBlock)) { Remove.remove(toBlock); return CaretFinder.firstPositionIn(fromBlock.dom()); } else { trimBr(true, fromBlock); trimBr(false, toBlock); var children = extractChildren(fromBlock); return getInsertionPoint(fromBlock, toBlock).fold( function () { removeEmptyRoot(rootNode, fromBlock); var position = CaretFinder.lastPositionIn(toBlock.dom()); Arr.each(children, function (node) { Insert.append(toBlock, node); }); return position; }, function (target) { var position = CaretFinder.prevPosition(toBlock.dom(), CaretPosition.before(target.dom())); Arr.each(children, function (node) { Insert.before(target, node); }); removeEmptyRoot(rootNode, fromBlock); return position; } ); } }; var mergeBlocks = function (rootNode, forward, block1, block2) { return forward ? mergeBlockInto(rootNode, block2, block1) : mergeBlockInto(rootNode, block1, block2); }; return { mergeBlocks: mergeBlocks }; } ); /** * BlockBoundaryDelete.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.delete.BlockBoundaryDelete', [ 'ephox.sugar.api.node.Element', 'tinymce.core.delete.BlockBoundary', 'tinymce.core.delete.MergeBlocks' ], function (Element, BlockBoundary, MergeBlocks) { var backspaceDelete = function (editor, forward) { var position, rootNode = Element.fromDom(editor.getBody()); position = BlockBoundary.read(rootNode.dom(), forward, editor.selection.getRng()).bind(function (blockBoundary) { return MergeBlocks.mergeBlocks(rootNode, forward, blockBoundary.from().block(), blockBoundary.to().block()); }); position.each(function (pos) { editor.selection.setRng(pos.toRange()); }); return position.isSome(); }; return { backspaceDelete: backspaceDelete }; } ); /** * BlockRangeDelete.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.delete.BlockRangeDelete', [ 'ephox.katamari.api.Options', 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.node.Element', 'tinymce.core.caret.CaretFinder', 'tinymce.core.caret.CaretPosition', 'tinymce.core.delete.DeleteUtils', 'tinymce.core.delete.MergeBlocks' ], function (Options, Compare, Element, CaretFinder, CaretPosition, DeleteUtils, MergeBlocks) { var deleteRangeMergeBlocks = function (rootNode, selection) { var rng = selection.getRng(); return Options.liftN([ DeleteUtils.getParentBlock(rootNode, Element.fromDom(rng.startContainer)), DeleteUtils.getParentBlock(rootNode, Element.fromDom(rng.endContainer)) ], function (block1, block2) { if (Compare.eq(block1, block2) === false) { rng.deleteContents(); MergeBlocks.mergeBlocks(rootNode, true, block1, block2).each(function (pos) { selection.setRng(pos.toRange()); }); return true; } else { return false; } }).getOr(false); }; var isEverythingSelected = function (rootNode, rng) { var noPrevious = CaretFinder.prevPosition(rootNode.dom(), CaretPosition.fromRangeStart(rng)).isNone(); var noNext = CaretFinder.nextPosition(rootNode.dom(), CaretPosition.fromRangeEnd(rng)).isNone(); return noPrevious && noNext; }; var emptyEditor = function (editor) { editor.setContent(''); editor.selection.setCursorLocation(); return true; }; var deleteRange = function (editor) { var rootNode = Element.fromDom(editor.getBody()); var rng = editor.selection.getRng(); return isEverythingSelected(rootNode, rng) ? emptyEditor(editor) : deleteRangeMergeBlocks(rootNode, editor.selection); }; var backspaceDelete = function (editor, forward) { return editor.selection.isCollapsed() ? false : deleteRange(editor, editor.selection.getRng()); }; return { backspaceDelete: backspaceDelete }; } ); define( 'ephox.katamari.api.Adt', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Obj', 'ephox.katamari.api.Type', 'global!Array', 'global!Error', 'global!console' ], function (Arr, Obj, Type, Array, Error, console) { /* * Generates a church encoded ADT (https://en.wikipedia.org/wiki/Church_encoding) * For syntax and use, look at the test code. */ var generate = function (cases) { // validation if (!Type.isArray(cases)) { throw new Error('cases must be an array'); } if (cases.length === 0) { throw new Error('there must be at least one case'); } var constructors = [ ]; // adt is mutated to add the individual cases var adt = {}; Arr.each(cases, function (acase, count) { var keys = Obj.keys(acase); // validation if (keys.length !== 1) { throw new Error('one and only one name per case'); } var key = keys[0]; var value = acase[key]; // validation if (adt[key] !== undefined) { throw new Error('duplicate key detected:' + key); } else if (key === 'cata') { throw new Error('cannot have a case named cata (sorry)'); } else if (!Type.isArray(value)) { // this implicitly checks if acase is an object throw new Error('case arguments must be an array'); } constructors.push(key); // // constructor for key // adt[key] = function () { var argLength = arguments.length; // validation if (argLength !== value.length) { throw new Error('Wrong number of arguments to case ' + key + '. Expected ' + value.length + ' (' + value + '), got ' + argLength); } // Don't use array slice(arguments), makes the whole function unoptimisable on Chrome var args = new Array(argLength); for (var i = 0; i < args.length; i++) args[i] = arguments[i]; var match = function (branches) { var branchKeys = Obj.keys(branches); if (constructors.length !== branchKeys.length) { throw new Error('Wrong number of arguments to match. Expected: ' + constructors.join(',') + '\nActual: ' + branchKeys.join(',')); } var allReqd = Arr.forall(constructors, function (reqKey) { return Arr.contains(branchKeys, reqKey); }); if (!allReqd) throw new Error('Not all branches were specified when using match. Specified: ' + branchKeys.join(', ') + '\nRequired: ' + constructors.join(', ')); return branches[key].apply(null, args); }; // // the fold function for key // return { fold: function (/* arguments */) { // runtime validation if (arguments.length !== cases.length) { throw new Error('Wrong number of arguments to fold. Expected ' + cases.length + ', got ' + arguments.length); } var target = arguments[count]; return target.apply(null, args); }, match: match, // NOTE: Only for debugging. log: function (label) { console.log(label, { constructors: constructors, constructor: key, params: args }); } }; }; }); return adt; }; return { generate: generate }; } ); /** * CefDeleteAction.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.delete.CefDeleteAction', [ 'ephox.katamari.api.Adt', 'ephox.katamari.api.Option', 'ephox.sugar.api.node.Element', 'tinymce.core.caret.CaretFinder', 'tinymce.core.caret.CaretPosition', 'tinymce.core.caret.CaretUtils', 'tinymce.core.delete.DeleteUtils', 'tinymce.core.dom.Empty', 'tinymce.core.dom.NodeType' ], function (Adt, Option, Element, CaretFinder, CaretPosition, CaretUtils, DeleteUtils, Empty, NodeType) { var DeleteAction = Adt.generate([ { remove: [ 'element' ] }, { moveToElement: [ 'element' ] }, { moveToPosition: [ 'position' ] } ]); var isAtContentEditableBlockCaret = function (forward, from) { var elm = from.getNode(forward === false); var caretLocation = forward ? 'after' : 'before'; return NodeType.isElement(elm) && elm.getAttribute('data-mce-caret') === caretLocation; }; var deleteEmptyBlockOrMoveToCef = function (rootNode, forward, from, to) { var toCefElm = to.getNode(forward === false); return DeleteUtils.getParentBlock(Element.fromDom(rootNode), Element.fromDom(from.getNode())).map(function (blockElm) { return Empty.isEmpty(blockElm) ? DeleteAction.remove(blockElm.dom()) : DeleteAction.moveToElement(toCefElm); }).orThunk(function () { return Option.some(DeleteAction.moveToElement(toCefElm)); }); }; var findCefPosition = function (rootNode, forward, from) { return CaretFinder.fromPosition(forward, rootNode, from).bind(function (to) { if (forward && NodeType.isContentEditableFalse(to.getNode())) { return deleteEmptyBlockOrMoveToCef(rootNode, forward, from, to); } else if (forward === false && NodeType.isContentEditableFalse(to.getNode(true))) { return deleteEmptyBlockOrMoveToCef(rootNode, forward, from, to); } else if (forward && CaretUtils.isAfterContentEditableFalse(from)) { return Option.some(DeleteAction.moveToPosition(to)); } else if (forward === false && CaretUtils.isBeforeContentEditableFalse(from)) { return Option.some(DeleteAction.moveToPosition(to)); } else { return Option.none(); } }); }; var getContentEditableBlockAction = function (forward, elm) { if (forward && NodeType.isContentEditableFalse(elm.nextSibling)) { return Option.some(DeleteAction.moveToElement(elm.nextSibling)); } else if (forward === false && NodeType.isContentEditableFalse(elm.previousSibling)) { return Option.some(DeleteAction.moveToElement(elm.previousSibling)); } else { return Option.none(); } }; var getContentEditableAction = function (rootNode, forward, from) { if (isAtContentEditableBlockCaret(forward, from)) { return getContentEditableBlockAction(forward, from.getNode(forward === false)) .fold( function () { return findCefPosition(rootNode, forward, from); }, Option.some ); } else { return findCefPosition(rootNode, forward, from); } }; var read = function (rootNode, forward, rng) { var normalizedRange = CaretUtils.normalizeRange(forward ? 1 : -1, rootNode, rng); var from = CaretPosition.fromRangeStart(normalizedRange); if (forward === false && CaretUtils.isAfterContentEditableFalse(from)) { return Option.some(DeleteAction.remove(from.getNode(true))); } else if (forward && CaretUtils.isBeforeContentEditableFalse(from)) { return Option.some(DeleteAction.remove(from.getNode())); } else { return getContentEditableAction(rootNode, forward, from); } }; return { read: read }; } ); /** * DeleteElement.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.delete.DeleteElement', [ 'ephox.katamari.api.Fun', 'ephox.katamari.api.Option', 'ephox.katamari.api.Options', 'ephox.sugar.api.dom.Insert', 'ephox.sugar.api.dom.Remove', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.node.Node', 'ephox.sugar.api.search.PredicateFind', 'ephox.sugar.api.search.Traverse', 'tinymce.core.caret.CaretCandidate', 'tinymce.core.caret.CaretFinder', 'tinymce.core.caret.CaretPosition', 'tinymce.core.dom.Empty', 'tinymce.core.dom.NodeType' ], function (Fun, Option, Options, Insert, Remove, Element, Node, PredicateFind, Traverse, CaretCandidate, CaretFinder, CaretPosition, Empty, NodeType) { var needsReposition = function (pos, elm) { var container = pos.container(); var offset = pos.offset(); return CaretPosition.isTextPosition(pos) === false && container === elm.parentNode && offset > CaretPosition.before(elm).offset(); }; var reposition = function (elm, pos) { return needsReposition(pos, elm) ? new CaretPosition(pos.container(), pos.offset() - 1) : pos; }; var beforeOrStartOf = function (node) { return NodeType.isText(node) ? new CaretPosition(node, 0) : CaretPosition.before(node); }; var afterOrEndOf = function (node) { return NodeType.isText(node) ? new CaretPosition(node, node.data.length) : CaretPosition.after(node); }; var getPreviousSiblingCaretPosition = function (elm) { if (CaretCandidate.isCaretCandidate(elm.previousSibling)) { return Option.some(afterOrEndOf(elm.previousSibling)); } else { return elm.previousSibling ? CaretFinder.lastPositionIn(elm.previousSibling) : Option.none(); } }; var getNextSiblingCaretPosition = function (elm) { if (CaretCandidate.isCaretCandidate(elm.nextSibling)) { return Option.some(beforeOrStartOf(elm.nextSibling)); } else { return elm.nextSibling ? CaretFinder.firstPositionIn(elm.nextSibling) : Option.none(); } }; var findCaretPositionBackwardsFromElm = function (rootElement, elm) { var startPosition = CaretPosition.before(elm.previousSibling ? elm.previousSibling : elm.parentNode); return CaretFinder.prevPosition(rootElement, startPosition).fold( function () { return CaretFinder.nextPosition(rootElement, CaretPosition.after(elm)); }, Option.some ); }; var findCaretPositionForwardsFromElm = function (rootElement, elm) { return CaretFinder.nextPosition(rootElement, CaretPosition.after(elm)).fold( function () { return CaretFinder.prevPosition(rootElement, CaretPosition.before(elm)); }, Option.some ); }; var findCaretPositionBackwards = function (rootElement, elm) { return getPreviousSiblingCaretPosition(elm).orThunk(function () { return getNextSiblingCaretPosition(elm); }).orThunk(function () { return findCaretPositionBackwardsFromElm(rootElement, elm); }); }; var findCaretPositionForward = function (rootElement, elm) { return getNextSiblingCaretPosition(elm).orThunk(function () { return getPreviousSiblingCaretPosition(elm); }).orThunk(function () { return findCaretPositionForwardsFromElm(rootElement, elm); }); }; var findCaretPosition = function (forward, rootElement, elm) { return forward ? findCaretPositionForward(rootElement, elm) : findCaretPositionBackwards(rootElement, elm); }; var findCaretPosOutsideElmAfterDelete = function (forward, rootElement, elm) { return findCaretPosition(forward, rootElement, elm).map(Fun.curry(reposition, elm)); }; var setSelection = function (editor, forward, pos) { pos.fold( function () { editor.focus(); }, function (pos) { editor.selection.setRng(pos.toRange(), forward); } ); }; var eqRawNode = function (rawNode) { return function (elm) { return elm.dom() === rawNode; }; }; var isBlock = function (editor, elm) { return elm && editor.schema.getBlockElements().hasOwnProperty(Node.name(elm)); }; var paddEmptyBlock = function (elm) { if (Empty.isEmpty(elm)) { var br = Element.fromHtml('
    '); Remove.empty(elm); Insert.append(elm, br); return Option.some(CaretPosition.before(br.dom())); } else { return Option.none(); } }; // When deleting an element between two text nodes IE 11 doesn't automatically merge the adjacent text nodes var deleteNormalized = function (elm, afterDeletePosOpt) { return Options.liftN([Traverse.prevSibling(elm), Traverse.nextSibling(elm), afterDeletePosOpt], function (prev, next, afterDeletePos) { var offset, prevNode = prev.dom(), nextNode = next.dom(); if (NodeType.isText(prevNode) && NodeType.isText(nextNode)) { offset = prevNode.data.length; prevNode.appendData(nextNode.data); Remove.remove(next); Remove.remove(elm); if (afterDeletePos.container() === nextNode) { return new CaretPosition(prevNode, offset); } else { return afterDeletePos; } } else { Remove.remove(elm); return afterDeletePos; } }).orThunk(function () { Remove.remove(elm); return afterDeletePosOpt; }); }; var deleteElement = function (editor, forward, elm) { var afterDeletePos = findCaretPosOutsideElmAfterDelete(forward, editor.getBody(), elm.dom()); var parentBlock = PredicateFind.ancestor(elm, Fun.curry(isBlock, editor), eqRawNode(editor.getBody())); var normalizedAfterDeletePos = deleteNormalized(elm, afterDeletePos); parentBlock.bind(paddEmptyBlock).fold( function () { setSelection(editor, forward, normalizedAfterDeletePos); }, function (paddPos) { setSelection(editor, forward, Option.some(paddPos)); } ); }; return { deleteElement: deleteElement }; } ); /** * CefDelete.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.delete.CefDelete', [ 'ephox.katamari.api.Arr', 'ephox.sugar.api.dom.Remove', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.search.SelectorFilter', 'tinymce.core.caret.CaretPosition', 'tinymce.core.delete.CefDeleteAction', 'tinymce.core.delete.DeleteElement', 'tinymce.core.delete.DeleteUtils', 'tinymce.core.dom.NodeType' ], function (Arr, Remove, Element, SelectorFilter, CaretPosition, CefDeleteAction, DeleteElement, DeleteUtils, NodeType) { var deleteElement = function (editor, forward) { return function (element) { DeleteElement.deleteElement(editor, forward, Element.fromDom(element)); return true; }; }; var moveToElement = function (editor, forward) { return function (element) { var pos = forward ? CaretPosition.before(element) : CaretPosition.after(element); editor.selection.setRng(pos.toRange()); return true; }; }; var moveToPosition = function (editor) { return function (pos) { editor.selection.setRng(pos.toRange()); return true; }; }; var backspaceDeleteCaret = function (editor, forward) { var result = CefDeleteAction.read(editor.getBody(), forward, editor.selection.getRng()).map(function (deleteAction) { return deleteAction.fold( deleteElement(editor, forward), moveToElement(editor, forward), moveToPosition(editor) ); }); return result.getOr(false); }; var deleteOffscreenSelection = function (rootElement) { Arr.each(SelectorFilter.descendants(rootElement, '.mce-offscreen-selection'), Remove.remove); }; var backspaceDeleteRange = function (editor, forward) { var selectedElement = editor.selection.getNode(); if (NodeType.isContentEditableFalse(selectedElement)) { deleteOffscreenSelection(Element.fromDom(editor.getBody())); DeleteElement.deleteElement(editor, forward, Element.fromDom(editor.selection.getNode())); DeleteUtils.paddEmptyBody(editor); return true; } else { return false; } }; var getContentEditableRoot = function (root, node) { while (node && node !== root) { if (NodeType.isContentEditableTrue(node) || NodeType.isContentEditableFalse(node)) { return node; } node = node.parentNode; } return null; }; var paddEmptyElement = function (editor) { var br, ceRoot = getContentEditableRoot(editor.getBody(), editor.selection.getNode()); if (NodeType.isContentEditableTrue(ceRoot) && editor.dom.isBlock(ceRoot) && editor.dom.isEmpty(ceRoot)) { br = editor.dom.create('br', { "data-mce-bogus": "1" }); editor.dom.setHTML(ceRoot, ''); ceRoot.appendChild(br); editor.selection.setRng(CaretPosition.before(br).toRange()); } return true; }; var backspaceDelete = function (editor, forward) { if (editor.selection.isCollapsed()) { return backspaceDeleteCaret(editor, forward); } else { return backspaceDeleteRange(editor, forward); } }; return { backspaceDelete: backspaceDelete, paddEmptyElement: paddEmptyElement }; } ); /** * CaretContainerInline.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.caret.CaretContainerInline', [ 'ephox.katamari.api.Fun', 'tinymce.core.dom.NodeType', 'tinymce.core.text.Zwsp' ], function (Fun, NodeType, Zwsp) { var isText = NodeType.isText; var startsWithCaretContainer = function (node) { return isText(node) && node.data[0] === Zwsp.ZWSP; }; var endsWithCaretContainer = function (node) { return isText(node) && node.data[node.data.length - 1] === Zwsp.ZWSP; }; var createZwsp = function (node) { return node.ownerDocument.createTextNode(Zwsp.ZWSP); }; var insertBefore = function (node) { if (isText(node.previousSibling)) { if (endsWithCaretContainer(node.previousSibling)) { return node.previousSibling; } else { node.previousSibling.appendData(Zwsp.ZWSP); return node.previousSibling; } } else if (isText(node)) { if (startsWithCaretContainer(node)) { return node; } else { node.insertData(0, Zwsp.ZWSP); return node; } } else { var newNode = createZwsp(node); node.parentNode.insertBefore(newNode, node); return newNode; } }; var insertAfter = function (node) { if (isText(node.nextSibling)) { if (startsWithCaretContainer(node.nextSibling)) { return node.nextSibling; } else { node.nextSibling.insertData(0, Zwsp.ZWSP); return node.nextSibling; } } else if (isText(node)) { if (endsWithCaretContainer(node)) { return node; } else { node.appendData(Zwsp.ZWSP); return node; } } else { var newNode = createZwsp(node); if (node.nextSibling) { node.parentNode.insertBefore(newNode, node.nextSibling); } else { node.parentNode.appendChild(newNode); } return newNode; } }; var insertInline = function (before, node) { return before ? insertBefore(node) : insertAfter(node); }; return { insertInline: insertInline, insertInlineBefore: Fun.curry(insertInline, true), insertInlineAfter: Fun.curry(insertInline, false) }; } ); /** * CaretContainerRemove.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.caret.CaretContainerRemove', [ 'ephox.katamari.api.Arr', 'tinymce.core.caret.CaretContainer', 'tinymce.core.caret.CaretPosition', 'tinymce.core.dom.NodeType', 'tinymce.core.text.Zwsp', 'tinymce.core.util.Tools' ], function (Arr, CaretContainer, CaretPosition, NodeType, Zwsp, Tools) { var isElement = NodeType.isElement; var isText = NodeType.isText; var removeNode = function (node) { var parentNode = node.parentNode; if (parentNode) { parentNode.removeChild(node); } }; var getNodeValue = function (node) { try { return node.nodeValue; } catch (ex) { // IE sometimes produces "Invalid argument" on nodes return ""; } }; var setNodeValue = function (node, text) { if (text.length === 0) { removeNode(node); } else { node.nodeValue = text; } }; var trimCount = function (text) { var trimmedText = Zwsp.trim(text); return { count: text.length - trimmedText.length, text: trimmedText }; }; var removeUnchanged = function (caretContainer, pos) { remove(caretContainer); return pos; }; var removeTextAndReposition = function (caretContainer, pos) { var before = trimCount(caretContainer.data.substr(0, pos.offset())); var after = trimCount(caretContainer.data.substr(pos.offset())); var text = before.text + after.text; if (text.length > 0) { setNodeValue(caretContainer, text); return new CaretPosition(caretContainer, pos.offset() - before.count); } else { return pos; } }; var removeElementAndReposition = function (caretContainer, pos) { var parentNode = pos.container(); var newPosition = Arr.indexOf(parentNode.childNodes, caretContainer).map(function (index) { return index < pos.offset() ? new CaretPosition(parentNode, pos.offset() - 1) : pos; }).getOr(pos); remove(caretContainer); return newPosition; }; var removeTextCaretContainer = function (caretContainer, pos) { return pos.container() === caretContainer ? removeTextAndReposition(caretContainer, pos) : removeUnchanged(caretContainer, pos); }; var removeElementCaretContainer = function (caretContainer, pos) { return pos.container() === caretContainer.parentNode ? removeElementAndReposition(caretContainer, pos) : removeUnchanged(caretContainer, pos); }; var removeAndReposition = function (container, pos) { return CaretPosition.isTextPosition(pos) ? removeTextCaretContainer(container, pos) : removeElementCaretContainer(container, pos); }; var remove = function (caretContainerNode) { if (isElement(caretContainerNode) && CaretContainer.isCaretContainer(caretContainerNode)) { if (CaretContainer.hasContent(caretContainerNode)) { caretContainerNode.removeAttribute('data-mce-caret'); } else { removeNode(caretContainerNode); } } if (isText(caretContainerNode)) { var text = Zwsp.trim(getNodeValue(caretContainerNode)); setNodeValue(caretContainerNode, text); } }; return { removeAndReposition: removeAndReposition, remove: remove }; } ); /** * DefaultSettings.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.EditorSettings', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'ephox.katamari.api.Obj', 'ephox.katamari.api.Option', 'ephox.katamari.api.Strings', 'ephox.katamari.api.Struct', 'ephox.katamari.api.Type', 'ephox.sand.api.PlatformDetection', 'tinymce.core.util.Tools' ], function (Arr, Fun, Obj, Option, Strings, Struct, Type, PlatformDetection, Tools) { var sectionResult = Struct.immutable('sections', 'settings'); var detection = PlatformDetection.detect(); var isTouch = detection.deviceType.isTouch(); var mobilePlugins = [ 'lists', 'autolink', 'autosave' ]; var normalizePlugins = function (plugins) { return Type.isArray(plugins) ? plugins.join(' ') : plugins; }; var filterMobilePlugins = function (plugins) { var trimmedPlugins = Arr.map(normalizePlugins(plugins).split(' '), Strings.trim); return Arr.filter(trimmedPlugins, Fun.curry(Arr.contains, mobilePlugins)).join(' '); }; var extractSections = function (keys, settings) { var result = Obj.bifilter(settings, function (value, key) { return Arr.contains(keys, key); }); return sectionResult(result.t, result.f); }; var getSection = function (sectionResult, name) { var sections = sectionResult.sections(); return sections.hasOwnProperty(name) ? sections[name] : { }; }; var hasSection = function (sectionResult, name) { return sectionResult.sections().hasOwnProperty(name); }; var getDefaultSettings = function (id, documentBaseUrl, editor) { return { 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: '', 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', entity_encoding: 'named', url_converter: editor.convertURL, url_converter_scope: editor, ie7_compat: true }; }; var getExternalPlugins = function (overrideSettings, settings) { var userDefinedExternalPlugins = settings.external_plugins ? settings.external_plugins : { }; if (overrideSettings && overrideSettings.external_plugins) { return Tools.extend({}, overrideSettings.external_plugins, userDefinedExternalPlugins); } else { return userDefinedExternalPlugins; } }; var combineSettings = function (defaultSettings, defaultOverrideSettings, settings) { var sectionResult = extractSections(['mobile'], settings); var plugins = sectionResult.settings().plugins; var extendedSettings = Tools.extend( // Default settings defaultSettings, // tinymce.overrideDefaults settings defaultOverrideSettings, // User settings sectionResult.settings(), // Sections isTouch ? getSection(sectionResult, 'mobile') : { }, // Forced settings { validate: true, content_editable: sectionResult.settings().inline, external_plugins: getExternalPlugins(defaultOverrideSettings, sectionResult.settings()) }, // TODO: Remove this once we fix each plugin with a mobile version isTouch && plugins && hasSection(sectionResult, 'mobile') ? { plugins: filterMobilePlugins(plugins) } : { } ); return extendedSettings; }; var getEditorSettings = function (editor, id, documentBaseUrl, defaultOverrideSettings, settings) { var defaultSettings = getDefaultSettings(id, documentBaseUrl, editor); return combineSettings(defaultSettings, defaultOverrideSettings, settings); }; var get = function (editor, name) { return Option.from(editor.settings[name]); }; var getFiltered = function (predicate, editor, name) { return Option.from(editor.settings[name]).filter(predicate); }; return { getEditorSettings: getEditorSettings, get: get, getString: Fun.curry(getFiltered, Type.isString), // TODO: Remove this once we have proper mobile plugins filterMobilePlugins: filterMobilePlugins }; } ); /** * Bidi.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.text.Bidi', [ ], function () { var strongRtl = /[\u0591-\u07FF\uFB1D-\uFDFF\uFE70-\uFEFC]/; var hasStrongRtl = function (text) { return strongRtl.test(text); }; return { hasStrongRtl: hasStrongRtl }; } ); /** * InlineUtils.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.InlineUtils', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'ephox.katamari.api.Option', 'ephox.katamari.api.Options', 'ephox.katamari.api.Type', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.search.Selectors', 'tinymce.core.caret.CaretContainer', 'tinymce.core.caret.CaretFinder', 'tinymce.core.caret.CaretPosition', 'tinymce.core.caret.CaretUtils', 'tinymce.core.caret.CaretWalker', 'tinymce.core.dom.DOMUtils', 'tinymce.core.dom.NodeType', 'tinymce.core.EditorSettings', 'tinymce.core.text.Bidi' ], function ( Arr, Fun, Option, Options, Type, Element, Selectors, CaretContainer, CaretFinder, CaretPosition, CaretUtils, CaretWalker, DOMUtils, NodeType, EditorSettings, Bidi ) { var isInlineTarget = function (editor, elm) { var selector = EditorSettings.getString(editor, 'inline_boundaries_selector').getOr('a[href],code'); return Selectors.is(Element.fromDom(elm), selector); }; var isRtl = function (element) { return DOMUtils.DOM.getStyle(element, 'direction', true) === 'rtl' || Bidi.hasStrongRtl(element.textContent); }; var findInlineParents = function (isInlineTarget, rootNode, pos) { return Arr.filter(DOMUtils.DOM.getParents(pos.container(), '*', rootNode), isInlineTarget); }; var findRootInline = function (isInlineTarget, rootNode, pos) { var parents = findInlineParents(isInlineTarget, rootNode, pos); return Option.from(parents[parents.length - 1]); }; var hasSameParentBlock = function (rootNode, node1, node2) { var block1 = CaretUtils.getParentBlock(node1, rootNode); var block2 = CaretUtils.getParentBlock(node2, rootNode); return block1 && block1 === block2; }; var isAtZwsp = function (pos) { return CaretContainer.isBeforeInline(pos) || CaretContainer.isAfterInline(pos); }; var normalizePosition = function (forward, pos) { var container = pos.container(), offset = pos.offset(); if (forward) { if (CaretContainer.isCaretContainerInline(container)) { if (NodeType.isText(container.nextSibling)) { return new CaretPosition(container.nextSibling, 0); } else { return CaretPosition.after(container); } } else { return CaretContainer.isBeforeInline(pos) ? new CaretPosition(container, offset + 1) : pos; } } else { if (CaretContainer.isCaretContainerInline(container)) { if (NodeType.isText(container.previousSibling)) { return new CaretPosition(container.previousSibling, container.previousSibling.data.length); } else { return CaretPosition.before(container); } } else { return CaretContainer.isAfterInline(pos) ? new CaretPosition(container, offset - 1) : pos; } } }; var normalizeForwards = Fun.curry(normalizePosition, true); var normalizeBackwards = Fun.curry(normalizePosition, false); return { isInlineTarget: isInlineTarget, findRootInline: findRootInline, isRtl: isRtl, isAtZwsp: isAtZwsp, normalizePosition: normalizePosition, normalizeForwards: normalizeForwards, normalizeBackwards: normalizeBackwards, hasSameParentBlock: hasSameParentBlock }; } ); /** * BoundaryCaret.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.BoundaryCaret', [ 'ephox.katamari.api.Option', 'tinymce.core.caret.CaretContainer', 'tinymce.core.caret.CaretContainerInline', 'tinymce.core.caret.CaretContainerRemove', 'tinymce.core.caret.CaretFinder', 'tinymce.core.caret.CaretPosition', 'tinymce.core.dom.NodeType', 'tinymce.core.keyboard.InlineUtils' ], function (Option, CaretContainer, CaretContainerInline, CaretContainerRemove, CaretFinder, CaretPosition, NodeType, InlineUtils) { var insertInlinePos = function (pos, before) { if (NodeType.isText(pos.container())) { return CaretContainerInline.insertInline(before, pos.container()); } else { return CaretContainerInline.insertInline(before, pos.getNode()); } }; var isPosCaretContainer = function (pos, caret) { var caretNode = caret.get(); return caretNode && pos.container() === caretNode && CaretContainer.isCaretContainerInline(caretNode); }; var renderCaret = function (caret, location) { return location.fold( function (element) { // Before CaretContainerRemove.remove(caret.get()); var text = CaretContainerInline.insertInlineBefore(element); caret.set(text); return Option.some(new CaretPosition(text, text.length - 1)); }, function (element) { // Start return CaretFinder.firstPositionIn(element).map(function (pos) { if (!isPosCaretContainer(pos, caret)) { CaretContainerRemove.remove(caret.get()); var text = insertInlinePos(pos, true); caret.set(text); return new CaretPosition(text, 1); } else { return new CaretPosition(caret.get(), 1); } }); }, function (element) { // End return CaretFinder.lastPositionIn(element).map(function (pos) { if (!isPosCaretContainer(pos, caret)) { CaretContainerRemove.remove(caret.get()); var text = insertInlinePos(pos, false); caret.set(text); return new CaretPosition(text, text.length - 1); } else { return new CaretPosition(caret.get(), caret.get().length - 1); } }); }, function (element) { // After CaretContainerRemove.remove(caret.get()); var text = CaretContainerInline.insertInlineAfter(element); caret.set(text); return Option.some(new CaretPosition(text, 1)); } ); }; return { renderCaret: renderCaret }; } ); /** * LazyEvaluator.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.util.LazyEvaluator', [ 'ephox.katamari.api.Option' ], function (Option) { var evaluateUntil = function (fns, args) { for (var i = 0; i < fns.length; i++) { var result = fns[i].apply(null, args); if (result.isSome()) { return result; } } return Option.none(); }; return { evaluateUntil: evaluateUntil }; } ); /** * BoundaryLocation.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.BoundaryLocation', [ 'ephox.katamari.api.Adt', 'ephox.katamari.api.Fun', 'ephox.katamari.api.Option', 'ephox.katamari.api.Options', 'tinymce.core.caret.CaretContainer', 'tinymce.core.caret.CaretFinder', 'tinymce.core.caret.CaretPosition', 'tinymce.core.caret.CaretUtils', 'tinymce.core.dom.NodeType', 'tinymce.core.keyboard.InlineUtils', 'tinymce.core.util.LazyEvaluator' ], function (Adt, Fun, Option, Options, CaretContainer, CaretFinder, CaretPosition, CaretUtils, NodeType, InlineUtils, LazyEvaluator) { var Location = Adt.generate([ { before: [ 'element' ] }, { start: [ 'element' ] }, { end: [ 'element' ] }, { after: [ 'element' ] } ]); var rescope = function (rootNode, node) { var parentBlock = CaretUtils.getParentBlock(node, rootNode); return parentBlock ? parentBlock : rootNode; }; var before = function (isInlineTarget, rootNode, pos) { var nPos = InlineUtils.normalizeForwards(pos); var scope = rescope(rootNode, nPos.container()); return InlineUtils.findRootInline(isInlineTarget, scope, nPos).fold( function () { return CaretFinder.nextPosition(scope, nPos) .bind(Fun.curry(InlineUtils.findRootInline, isInlineTarget, scope)) .map(function (inline) { return Location.before(inline); }); }, Option.none ); }; var start = function (isInlineTarget, rootNode, pos) { var nPos = InlineUtils.normalizeBackwards(pos); return InlineUtils.findRootInline(isInlineTarget, rootNode, nPos).bind(function (inline) { var prevPos = CaretFinder.prevPosition(inline, nPos); return prevPos.isNone() ? Option.some(Location.start(inline)) : Option.none(); }); }; var end = function (isInlineTarget, rootNode, pos) { var nPos = InlineUtils.normalizeForwards(pos); return InlineUtils.findRootInline(isInlineTarget, rootNode, nPos).bind(function (inline) { var nextPos = CaretFinder.nextPosition(inline, nPos); return nextPos.isNone() ? Option.some(Location.end(inline)) : Option.none(); }); }; var after = function (isInlineTarget, rootNode, pos) { var nPos = InlineUtils.normalizeBackwards(pos); var scope = rescope(rootNode, nPos.container()); return InlineUtils.findRootInline(isInlineTarget, scope, nPos).fold( function () { return CaretFinder.prevPosition(scope, nPos) .bind(Fun.curry(InlineUtils.findRootInline, isInlineTarget, scope)) .map(function (inline) { return Location.after(inline); }); }, Option.none ); }; var isValidLocation = function (location) { return InlineUtils.isRtl(getElement(location)) === false; }; var readLocation = function (isInlineTarget, rootNode, pos) { var location = LazyEvaluator.evaluateUntil([ before, start, end, after ], [isInlineTarget, rootNode, pos]); return location.filter(isValidLocation); }; var getElement = function (location) { return location.fold( Fun.identity, // Before Fun.identity, // Start Fun.identity, // End Fun.identity // After ); }; var getName = function (location) { return location.fold( Fun.constant('before'), // Before Fun.constant('start'), // Start Fun.constant('end'), // End Fun.constant('after') // After ); }; var outside = function (location) { return location.fold( Location.before, // Before Location.before, // Start Location.after, // End Location.after // After ); }; var inside = function (location) { return location.fold( Location.start, // Before Location.start, // Start Location.end, // End Location.end // After ); }; var isEq = function (location1, location2) { return getName(location1) === getName(location2) && getElement(location1) === getElement(location2); }; var betweenInlines = function (forward, isInlineTarget, rootNode, from, to, location) { return Options.liftN([ InlineUtils.findRootInline(isInlineTarget, rootNode, from), InlineUtils.findRootInline(isInlineTarget, rootNode, to) ], function (fromInline, toInline) { if (fromInline !== toInline && InlineUtils.hasSameParentBlock(rootNode, fromInline, toInline)) { // Force after since some browsers normalize and lean left into the closest inline return Location.after(forward ? fromInline : toInline); } else { return location; } }).getOr(location); }; var skipNoMovement = function (fromLocation, toLocation) { return fromLocation.fold( Fun.constant(true), function (fromLocation) { return !isEq(fromLocation, toLocation); } ); }; var findLocationTraverse = function (forward, isInlineTarget, rootNode, fromLocation, pos) { var from = InlineUtils.normalizePosition(forward, pos); var to = CaretFinder.fromPosition(forward, rootNode, from).map(Fun.curry(InlineUtils.normalizePosition, forward)); var location = to.fold( function () { return fromLocation.map(outside); }, function (to) { return readLocation(isInlineTarget, rootNode, to) .map(Fun.curry(betweenInlines, forward, isInlineTarget, rootNode, from, to)) .filter(Fun.curry(skipNoMovement, fromLocation)); } ); return location.filter(isValidLocation); }; var findLocationSimple = function (forward, location) { if (forward) { return location.fold( Fun.compose(Option.some, Location.start), // Before -> Start Option.none, Fun.compose(Option.some, Location.after), // End -> After Option.none ); } else { return location.fold( Option.none, Fun.compose(Option.some, Location.before), // Before <- Start Option.none, Fun.compose(Option.some, Location.end) // End <- After ); } }; var findLocation = function (forward, isInlineTarget, rootNode, pos) { var from = InlineUtils.normalizePosition(forward, pos); var fromLocation = readLocation(isInlineTarget, rootNode, from); return readLocation(isInlineTarget, rootNode, from).bind(Fun.curry(findLocationSimple, forward)).orThunk(function () { return findLocationTraverse(forward, isInlineTarget, rootNode, fromLocation, pos); }); }; return { readLocation: readLocation, findLocation: findLocation, prevLocation: Fun.curry(findLocation, false), nextLocation: Fun.curry(findLocation, true), getElement: getElement, outside: outside, inside: inside }; } ); /** * BoundarySelection.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.BoundarySelection', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Cell', 'ephox.katamari.api.Fun', 'tinymce.core.caret.CaretContainerRemove', 'tinymce.core.caret.CaretPosition', 'tinymce.core.keyboard.BoundaryCaret', 'tinymce.core.keyboard.BoundaryLocation', 'tinymce.core.keyboard.InlineUtils' ], function (Arr, Cell, Fun, CaretContainerRemove, CaretPosition, BoundaryCaret, BoundaryLocation, InlineUtils) { var setCaretPosition = function (editor, pos) { var rng = editor.dom.createRng(); rng.setStart(pos.container(), pos.offset()); rng.setEnd(pos.container(), pos.offset()); editor.selection.setRng(rng); }; var isFeatureEnabled = function (editor) { return editor.settings.inline_boundaries !== false; }; var setSelected = function (state, elm) { if (state) { elm.setAttribute('data-mce-selected', '1'); } else { elm.removeAttribute('data-mce-selected', '1'); } }; var renderCaretLocation = function (editor, caret, location) { return BoundaryCaret.renderCaret(caret, location).map(function (pos) { setCaretPosition(editor, pos); return location; }); }; var findLocation = function (editor, caret, forward) { var rootNode = editor.getBody(); var from = CaretPosition.fromRangeStart(editor.selection.getRng()); var isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor); var location = BoundaryLocation.findLocation(forward, isInlineTarget, rootNode, from); return location.bind(function (location) { return renderCaretLocation(editor, caret, location); }); }; var toggleInlines = function (isInlineTarget, dom, elms) { var selectedInlines = Arr.filter(dom.select('*[data-mce-selected]'), isInlineTarget); var targetInlines = Arr.filter(elms, isInlineTarget); Arr.each(Arr.difference(selectedInlines, targetInlines), Fun.curry(setSelected, false)); Arr.each(Arr.difference(targetInlines, selectedInlines), Fun.curry(setSelected, true)); }; var safeRemoveCaretContainer = function (editor, caret) { if (editor.selection.isCollapsed() && editor.composing !== true && caret.get()) { var pos = CaretPosition.fromRangeStart(editor.selection.getRng()); if (CaretPosition.isTextPosition(pos) && InlineUtils.isAtZwsp(pos) === false) { setCaretPosition(editor, CaretContainerRemove.removeAndReposition(caret.get(), pos)); caret.set(null); } } }; var renderInsideInlineCaret = function (isInlineTarget, editor, caret, elms) { if (editor.selection.isCollapsed()) { var inlines = Arr.filter(elms, isInlineTarget); Arr.each(inlines, function (inline) { var pos = CaretPosition.fromRangeStart(editor.selection.getRng()); BoundaryLocation.readLocation(isInlineTarget, editor.getBody(), pos).bind(function (location) { return renderCaretLocation(editor, caret, location); }); }); } }; var move = function (editor, caret, forward) { return function () { return isFeatureEnabled(editor) ? findLocation(editor, caret, forward).isSome() : false; }; }; var setupSelectedState = function (editor) { var caret = new Cell(null); var isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor); editor.on('NodeChange', function (e) { if (isFeatureEnabled(editor)) { toggleInlines(isInlineTarget, editor.dom, e.parents); safeRemoveCaretContainer(editor, caret); renderInsideInlineCaret(isInlineTarget, editor, caret, e.parents); } }); return caret; }; return { move: move, setupSelectedState: setupSelectedState, setCaretPosition: setCaretPosition }; } ); /** * InlineBoundaryDelete.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.delete.InlineBoundaryDelete', [ 'ephox.katamari.api.Fun', 'ephox.katamari.api.Option', 'ephox.katamari.api.Options', 'ephox.sugar.api.node.Element', 'tinymce.core.caret.CaretContainer', 'tinymce.core.caret.CaretFinder', 'tinymce.core.caret.CaretPosition', 'tinymce.core.caret.CaretUtils', 'tinymce.core.delete.DeleteElement', 'tinymce.core.keyboard.BoundaryCaret', 'tinymce.core.keyboard.BoundaryLocation', 'tinymce.core.keyboard.BoundarySelection', 'tinymce.core.keyboard.InlineUtils' ], function ( Fun, Option, Options, Element, CaretContainer, CaretFinder, CaretPosition, CaretUtils, DeleteElement, BoundaryCaret, BoundaryLocation, BoundarySelection, InlineUtils ) { var isFeatureEnabled = function (editor) { return editor.settings.inline_boundaries !== false; }; var rangeFromPositions = function (from, to) { var range = document.createRange(); range.setStart(from.container(), from.offset()); range.setEnd(to.container(), to.offset()); return range; }; // Checks for delete at |a when there is only one item left except the zwsp caret container nodes var hasOnlyTwoOrLessPositionsLeft = function (elm) { return Options.liftN([ CaretFinder.firstPositionIn(elm), CaretFinder.lastPositionIn(elm) ], function (firstPos, lastPos) { var normalizedFirstPos = InlineUtils.normalizePosition(true, firstPos); var normalizedLastPos = InlineUtils.normalizePosition(false, lastPos); return CaretFinder.nextPosition(elm, normalizedFirstPos).map(function (pos) { return pos.isEqual(normalizedLastPos); }).getOr(true); }).getOr(true); }; var setCaretLocation = function (editor, caret) { return function (location) { return BoundaryCaret.renderCaret(caret, location).map(function (pos) { BoundarySelection.setCaretPosition(editor, pos); return true; }).getOr(false); }; }; var deleteFromTo = function (editor, caret, from, to) { var rootNode = editor.getBody(); var isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor); editor.undoManager.ignore(function () { editor.selection.setRng(rangeFromPositions(from, to)); editor.execCommand('Delete'); BoundaryLocation.readLocation(isInlineTarget, rootNode, CaretPosition.fromRangeStart(editor.selection.getRng())) .map(BoundaryLocation.inside) .map(setCaretLocation(editor, caret)); }); editor.nodeChanged(); }; var rescope = function (rootNode, node) { var parentBlock = CaretUtils.getParentBlock(node, rootNode); return parentBlock ? parentBlock : rootNode; }; var backspaceDeleteCollapsed = function (editor, caret, forward, from) { var rootNode = rescope(editor.getBody(), from.container()); var isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor); var fromLocation = BoundaryLocation.readLocation(isInlineTarget, rootNode, from); return fromLocation.bind(function (location) { if (forward) { return location.fold( Fun.constant(Option.some(BoundaryLocation.inside(location))), // Before Option.none, // Start Fun.constant(Option.some(BoundaryLocation.outside(location))), // End Option.none // After ); } else { return location.fold( Option.none, // Before Fun.constant(Option.some(BoundaryLocation.outside(location))), // Start Option.none, // End Fun.constant(Option.some(BoundaryLocation.inside(location))) // After ); } }) .map(setCaretLocation(editor, caret)) .getOrThunk(function () { var toPosition = CaretFinder.navigate(forward, rootNode, from); var toLocation = toPosition.bind(function (pos) { return BoundaryLocation.readLocation(isInlineTarget, rootNode, pos); }); if (fromLocation.isSome() && toLocation.isSome()) { return InlineUtils.findRootInline(isInlineTarget, rootNode, from).map(function (elm) { if (hasOnlyTwoOrLessPositionsLeft(elm)) { DeleteElement.deleteElement(editor, forward, Element.fromDom(elm)); return true; } else { return false; } }).getOr(false); } else { return toLocation.bind(function (_) { return toPosition.map(function (to) { if (forward) { deleteFromTo(editor, caret, from, to); } else { deleteFromTo(editor, caret, to, from); } return true; }); }).getOr(false); } }); }; var backspaceDelete = function (editor, caret, forward) { if (editor.selection.isCollapsed() && isFeatureEnabled(editor)) { var from = CaretPosition.fromRangeStart(editor.selection.getRng()); return backspaceDeleteCollapsed(editor, caret, forward, from); } return false; }; return { backspaceDelete: backspaceDelete }; } ); define( 'tinymce.core.delete.TableDeleteAction', [ 'ephox.katamari.api.Adt', 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'ephox.katamari.api.Option', 'ephox.katamari.api.Options', 'ephox.katamari.api.Struct', 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.search.SelectorFilter', 'ephox.sugar.api.search.SelectorFind' ], function (Adt, Arr, Fun, Option, Options, Struct, Compare, Element, SelectorFilter, SelectorFind) { var tableCellRng = Struct.immutable('start', 'end'); var tableSelection = Struct.immutable('rng', 'table', 'cells'); var deleteAction = Adt.generate([ { removeTable: [ 'element' ] }, { emptyCells: [ 'cells' ] } ]); var getClosestCell = function (container, isRoot) { return SelectorFind.closest(Element.fromDom(container), 'td,th', isRoot); }; var getClosestTable = function (cell, isRoot) { return SelectorFind.ancestor(cell, 'table', isRoot); }; var isExpandedCellRng = function (cellRng) { return Compare.eq(cellRng.start(), cellRng.end()) === false; }; var getTableFromCellRng = function (cellRng, isRoot) { return getClosestTable(cellRng.start(), isRoot) .bind(function (startParentTable) { return getClosestTable(cellRng.end(), isRoot) .bind(function (endParentTable) { return Compare.eq(startParentTable, endParentTable) ? Option.some(startParentTable) : Option.none(); }); }); }; var getCellRng = function (rng, isRoot) { return Options.liftN([ // get start and end cell getClosestCell(rng.startContainer, isRoot), getClosestCell(rng.endContainer, isRoot) ], tableCellRng) .filter(isExpandedCellRng); }; var getTableSelectionFromCellRng = function (cellRng, isRoot) { return getTableFromCellRng(cellRng, isRoot) .bind(function (table) { var cells = SelectorFilter.descendants(table, 'td,th'); return tableSelection(cellRng, table, cells); }); }; var getTableSelectionFromRng = function (rootNode, rng) { var isRoot = Fun.curry(Compare.eq, rootNode); return getCellRng(rng, isRoot) .map(function (cellRng) { return getTableSelectionFromCellRng(cellRng, isRoot); }); }; var getCellIndex = function (cellArray, cell) { return Arr.findIndex(cellArray, function (x) { return Compare.eq(x, cell); }); }; var getSelectedCells = function (tableSelection) { return Options.liftN([ getCellIndex(tableSelection.cells(), tableSelection.rng().start()), getCellIndex(tableSelection.cells(), tableSelection.rng().end()) ], function (startIndex, endIndex) { return tableSelection.cells().slice(startIndex, endIndex + 1); }); }; var getAction = function (tableSelection) { return getSelectedCells(tableSelection) .bind(function (selected) { var cells = tableSelection.cells(); return selected.length === cells.length ? deleteAction.removeTable(tableSelection.table()) : deleteAction.emptyCells(selected); }); }; var getActionFromCells = function (cells) { return deleteAction.emptyCells(cells); }; var getActionFromRange = function (rootNode, rng) { return getTableSelectionFromRng(rootNode, rng) .map(getAction); }; return { getActionFromRange: getActionFromRange, getActionFromCells: getActionFromCells }; } ); /** * TableDelete.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.delete.TableDelete', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'ephox.sugar.api.node.Element', 'tinymce.core.delete.DeleteElement', 'tinymce.core.delete.TableDeleteAction', 'tinymce.core.dom.PaddingBr', 'tinymce.core.selection.TableCellSelection' ], function (Arr, Fun, Element, DeleteElement, TableDeleteAction, PaddingBr, TableCellSelection) { var emptyCells = function (editor, cells) { Arr.each(cells, PaddingBr.fillWithPaddingBr); editor.selection.setCursorLocation(cells[0].dom(), 0); return true; }; var deleteTableElement = function (editor, table) { DeleteElement.deleteElement(editor, false, table); return true; }; var handleCellRange = function (editor, rootNode, rng) { return TableDeleteAction.getActionFromRange(rootNode, rng) .map(function (action) { return action.fold( Fun.curry(deleteTableElement, editor), Fun.curry(emptyCells, editor) ); }).getOr(false); }; var deleteRange = function (editor) { var rootNode = Element.fromDom(editor.getBody()); var rng = editor.selection.getRng(); var selectedCells = TableCellSelection.getCellsFromEditor(editor); return selectedCells.length !== 0 ? emptyCells(editor, selectedCells) : handleCellRange(editor, rootNode, rng); }; var backspaceDelete = function (editor) { return editor.selection.isCollapsed() ? false : deleteRange(editor); }; return { backspaceDelete: backspaceDelete }; } ); /** * Commands.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.delete.DeleteCommands', [ 'tinymce.core.delete.BlockBoundaryDelete', 'tinymce.core.delete.BlockRangeDelete', 'tinymce.core.delete.CefDelete', 'tinymce.core.delete.DeleteUtils', 'tinymce.core.delete.InlineBoundaryDelete', 'tinymce.core.delete.TableDelete' ], function (BlockBoundaryDelete, BlockRangeDelete, CefDelete, DeleteUtils, BoundaryDelete, TableDelete) { var nativeCommand = function (editor, command) { editor.getDoc().execCommand(command, false, null); }; var deleteCommand = function (editor) { if (CefDelete.backspaceDelete(editor, false)) { return; } else if (BoundaryDelete.backspaceDelete(editor, false)) { return; } else if (BlockBoundaryDelete.backspaceDelete(editor, false)) { return; } else if (TableDelete.backspaceDelete(editor)) { return; } else if (BlockRangeDelete.backspaceDelete(editor, false)) { return; } else { nativeCommand(editor, 'Delete'); DeleteUtils.paddEmptyBody(editor); } }; var forwardDeleteCommand = function (editor) { if (CefDelete.backspaceDelete(editor, true)) { return; } else if (BoundaryDelete.backspaceDelete(editor, true)) { return; } else if (BlockBoundaryDelete.backspaceDelete(editor, true)) { return; } else if (TableDelete.backspaceDelete(editor)) { return; } else if (BlockRangeDelete.backspaceDelete(editor, true)) { return; } else { nativeCommand(editor, 'ForwardDelete'); } }; return { deleteCommand: deleteCommand, forwardDeleteCommand: forwardDeleteCommand }; } ); /** * InsertList.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.InsertList', [ "tinymce.core.util.Tools", "tinymce.core.caret.CaretWalker", "tinymce.core.caret.CaretPosition" ], function (Tools, CaretWalker, CaretPosition) { var hasOnlyOneChild = function (node) { return node.firstChild && node.firstChild === node.lastChild; }; var isPaddingNode = function (node) { return node.name === 'br' || node.value === '\u00a0'; }; var isPaddedEmptyBlock = function (schema, node) { var blockElements = schema.getBlockElements(); return blockElements[node.name] && hasOnlyOneChild(node) && isPaddingNode(node.firstChild); }; var isEmptyFragmentElement = function (schema, node) { var nonEmptyElements = schema.getNonEmptyElements(); return node && (node.isEmpty(nonEmptyElements) || isPaddedEmptyBlock(schema, node)); }; var isListFragment = function (schema, fragment) { var firstChild = fragment.firstChild; var lastChild = fragment.lastChild; // Skip meta since it's likely
      ..
    if (firstChild && firstChild.name === 'meta') { firstChild = firstChild.next; } // Skip mce_marker since it's likely
      ..
    if (lastChild && lastChild.attr('id') === 'mce_marker') { lastChild = lastChild.prev; } // Skip last child if it's an empty block if (isEmptyFragmentElement(schema, lastChild)) { 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 }; } ); /** * InsertContent.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.InsertContent', [ 'ephox.katamari.api.Option', 'ephox.sugar.api.node.Element', 'tinymce.core.caret.CaretPosition', 'tinymce.core.caret.CaretWalker', 'tinymce.core.dom.ElementUtils', 'tinymce.core.dom.NodeType', 'tinymce.core.dom.PaddingBr', 'tinymce.core.dom.RangeNormalizer', 'tinymce.core.Env', 'tinymce.core.html.Serializer', 'tinymce.core.InsertList', 'tinymce.core.util.Tools' ], function (Option, Element, CaretPosition, CaretWalker, ElementUtils, NodeType, PaddingBr, RangeNormalizer, Env, Serializer, InsertList, Tools) { var isTableCell = NodeType.matchNodeNames('td th'); var validInsertion = function (editor, value, parentNode) { // Should never insert content into bogus elements, since these can // be resize handles or similar if (parentNode.getAttribute('data-mce-bogus') === 'all') { parentNode.parentNode.insertBefore(editor.dom.createFragment(value), parentNode); } else { // Check if parent is empty or only has one BR element then set the innerHTML of that parent var node = parentNode.firstChild; var node2 = parentNode.lastChild; if (!node || (node === node2 && node.nodeName === 'BR')) {/// editor.dom.setHTML(parentNode, value); } else { editor.selection.setContent(value); } } }; var trimBrsFromTableCell = function (dom, elm) { Option.from(dom.getParent(elm, 'td,th')).map(Element.fromDom).each(PaddingBr.trimBlockTrailingBr); }; 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(/^ /, ' '); } else if (!hasSiblingText('previousSibling')) { html = html.replace(/^ /, ' '); } if (offset < container.length) { html = html.replace(/ (
    |)$/, ' '); } else if (!hasSiblingText('nextSibling')) { html = html.replace(/( | )(
    |)$/, ' '); } } return html; } // Removes   from a [b] c -> a  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 = '​'; // 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 |

    then move it into

    |

    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(RangeNormalizer.normalize(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(editor.schema, 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); validInsertion(editor, value, parentNode); } 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(//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()); trimBrsFromTableCell(editor.dom, editor.selection.getStart()); 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 }; } ); /** * EditorCommands.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.EditorCommands', [ 'tinymce.core.delete.DeleteCommands', 'tinymce.core.dom.NodeType', 'tinymce.core.dom.RangeUtils', 'tinymce.core.dom.TreeWalker', 'tinymce.core.Env', 'tinymce.core.InsertContent', 'tinymce.core.util.Tools' ], function (DeleteCommands, NodeType, RangeUtils, TreeWalker, Env, InsertContent, Tools) { // 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 (editor.removed) { return; } 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; if (editor.quirks.isHidden() || editor.removed) { 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; if (editor.quirks.isHidden() || editor.removed) { 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} commandList 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(commandList, type) { type = type || 'exec'; each(commandList, 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, '
    '); }, 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) { var editingHost = dom.getParent(selection.getStart(), NodeType.isContentEditableTrue); if (editingHost) { rng = dom.createRng(); rng.selectNodeContents(editingHost); 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: // |

    to this:

    |

    rng = selection.getRng(); if (!rng.item) { rng.moveToElementText(root); rng.select(); } } }, "delete": function () { DeleteCommands.deleteCommand(editor); }, "forwardDelete": function () { DeleteCommands.forwardDeleteCommand(editor); }, 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', {}, ' '); 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(); } }); }; } ); /** * EditorFocus.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.EditorFocus', [ 'ephox.katamari.api.Option', 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.node.Element', 'tinymce.core.caret.CaretFinder', 'tinymce.core.dom.ElementType', 'tinymce.core.dom.RangeUtils', 'tinymce.core.Env' ], function (Option, Compare, Element, CaretFinder, ElementType, RangeUtils, Env) { var getContentEditableHost = function (editor, node) { return editor.dom.getParent(node, function (node) { return editor.dom.getContentEditable(node) === "true"; }); }; var getCollapsedNode = function (rng) { return rng.collapsed ? Option.from(RangeUtils.getNode(rng.startContainer, rng.startOffset)).map(Element.fromDom) : Option.none(); }; var getFocusInElement = function (root, rng) { return getCollapsedNode(rng).bind(function (node) { if (ElementType.isTableSection(node)) { return Option.some(node); } else if (Compare.contains(root, node) === false) { return Option.some(root); } else { return Option.none(); } }); }; var normalizeSelection = function (editor, rng) { getFocusInElement(Element.fromDom(editor.getBody()), rng).bind(function (elm) { return CaretFinder.firstPositionIn(elm.dom()); }).fold( function () { editor.selection.normalize(); }, function (caretPos) { editor.selection.setRng(caretPos.toRange()); } ); }; var focusBody = function (body) { if (body.setActive) { // IE 11 sometimes throws "Invalid function" then fallback to focus // setActive is better since it doesn't scroll to the element being focused try { body.setActive(); } catch (ex) { body.focus(); } } else { body.focus(); } }; var focusEditor = function (editor) { var selection = editor.selection, contentEditable = editor.settings.content_editable, rng; var controlElm, doc = editor.getDoc(), body = editor.getBody(), contentEditableHost; // Get selected control element rng = selection.getRng(); if (rng.item) { controlElm = rng.item(0); } editor.quirks.refreshContentEditable(); // Move focus to contentEditable=true child if needed contentEditableHost = getContentEditableHost(editor, selection.getNode()); if (editor.$.contains(body, contentEditableHost)) { focusBody(contentEditableHost); normalizeSelection(editor, rng); activateEditor(editor); 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) { focusBody(body); } editor.getWin().focus(); } // Focus the body as well since it's contentEditable if (Env.gecko || contentEditable) { // Restore previous selection before focus to prevent Chrome from // jumping to the top of the document in long inline editors if (contentEditable && document.activeElement !== body) { editor.selection.setRng(editor.lastRng); } focusBody(body); normalizeSelection(editor, rng); } // 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(); } activateEditor(editor); }; var activateEditor = function (editor) { editor.editorManager.setActive(editor); }; var focus = function (editor, skipFocus) { if (editor.removed) { return; } skipFocus ? activateEditor(editor) : focusEditor(editor); }; return { focus: focus }; } ); /** * EditorObservable.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.EditorObservable', [ "tinymce.core.util.Observable", "tinymce.core.dom.DOMUtils", "tinymce.core.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, delegate; function isListening(editor) { return !editor.hidden && !editor.readonly; } if (!editor.delegates) { editor.delegates = {}; } if (editor.delegates[eventName] || editor.removed) { return; } eventRootElm = getEventTarget(editor, eventName); 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.get(), 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; } ); /** * ErrorReporter.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * Various error reporting helper functions. * * @class tinymce.ErrorReporter * @private */ define( 'tinymce.core.ErrorReporter', [ 'tinymce.core.AddOnManager' ], function (AddOnManager) { var PluginManager = AddOnManager.PluginManager; var resolvePluginName = function (targetUrl, suffix) { for (var name in PluginManager.urls) { var matchUrl = PluginManager.urls[name] + '/plugin' + suffix + '.js'; if (matchUrl === targetUrl) { return name; } } return null; }; var pluginUrlToMessage = function (editor, url) { var plugin = resolvePluginName(url, editor.suffix); return plugin ? 'Failed to load plugin: ' + plugin + ' from url ' + url : 'Failed to load plugin url: ' + url; }; var displayNotification = function (editor, message) { editor.notificationManager.open({ type: 'error', text: message }); }; var displayError = function (editor, message) { if (editor._skinLoaded) { displayNotification(editor, message); } else { editor.on('SkinLoaded', function () { displayNotification(editor, message); }); } }; var uploadError = function (editor, message) { displayError(editor, 'Failed to upload image: ' + message); }; var pluginLoadError = function (editor, url) { displayError(editor, pluginUrlToMessage(editor, url)); }; var initError = function (message) { var console = window.console; if (console && !window.test) { // Skip test env if (console.error) { console.error.apply(console, arguments); } else { console.log.apply(console, arguments); } } }; return { pluginLoadError: pluginLoadError, uploadError: uploadError, displayError: displayError, initError: initError }; } ); /** * CaretContainerInput.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This module shows the invisble block that the caret is currently in when contents is added to that block. */ define( 'tinymce.core.caret.CaretContainerInput', [ 'ephox.katamari.api.Fun', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.search.SelectorFind', 'tinymce.core.caret.CaretContainer' ], function (Fun, Element, SelectorFind, CaretContainer) { var findBlockCaretContainer = function (editor) { return SelectorFind.descendant(Element.fromDom(editor.getBody()), '*[data-mce-caret]').fold(Fun.constant(null), function (elm) { return elm.dom(); }); }; var removeIeControlRect = function (editor) { editor.selection.setRng(editor.selection.getRng()); }; var showBlockCaretContainer = function (editor, blockCaretContainer) { if (blockCaretContainer.hasAttribute('data-mce-caret')) { CaretContainer.showCaretContainerBlock(blockCaretContainer); removeIeControlRect(editor); editor.selection.scrollIntoView(blockCaretContainer); } }; var handleBlockContainer = function (editor, e) { var blockCaretContainer = findBlockCaretContainer(editor); if (!blockCaretContainer) { return; } if (e.type === 'compositionstart') { e.preventDefault(); e.stopPropagation(); showBlockCaretContainer(blockCaretContainer); return; } if (CaretContainer.hasContent(blockCaretContainer)) { showBlockCaretContainer(editor, blockCaretContainer); } }; var setup = function (editor) { editor.on('keyup compositionstart', Fun.curry(handleBlockContainer, editor)); }; return { setup: setup }; } ); /** * Uploader.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.file.Uploader', [ "tinymce.core.util.Promise", "tinymce.core.util.Tools", "tinymce.core.util.Fun" ], function (Promise, Tools, Fun) { return function (uploadStatus, settings) { var pendingPromises = {}; function pathJoin(path1, path2) { if (path1) { return path1.replace(/\/$/, '') + '/' + path2.replace(/^\//, ''); } return path2; } 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 || xhr.status >= 300) { 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 (error) { closeNotification(); uploadStatus.removeFailed(blobInfo.blobUri()); resolvePending(blobInfo.blobUri(), handlerFailure(blobInfo, error)); resolve(handlerFailure(blobInfo, error)); }; progress = function (percent) { if (percent < 0 || percent > 100) { return; } if (!notification) { notification = openNotification(); } notification.progressBar.value(percent); }; handler(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 }; }; } ); define( 'ephox.sand.api.Window', [ 'ephox.sand.util.Global' ], function (Global) { /****************************************************************************************** * BIG BIG WARNING: Don't put anything other than top-level window functions in here. * * Objects that are technically available as window.X should be in their own module X (e.g. Blob, FileReader, URL). ****************************************************************************************** */ /* * IE10 and above per * https://developer.mozilla.org/en/docs/Web/API/window.requestAnimationFrame */ var requestAnimationFrame = function (callback) { var f = Global.getOrDie('requestAnimationFrame'); f(callback); }; /* * IE10 and above per * https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64.atob */ var atob = function (base64) { var f = Global.getOrDie('atob'); return f(base64); }; return { atob: atob, requestAnimationFrame: requestAnimationFrame }; } ); /** * Conversions.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.file.Conversions', [ 'ephox.sand.api.Window', 'tinymce.core.util.Promise' ], function (Window, Promise) { function blobUriToBlob(url) { return new Promise(function (resolve, reject) { var rejectWithError = function () { reject("Cannot convert " + url + " to Blob. Resource might not exist or is inaccessible."); }; try { var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.responseType = 'blob'; xhr.onload = function () { if (this.status == 200) { resolve(this.response); } else { // IE11 makes it into onload but responds with status 500 rejectWithError(); } }; // Chrome fires an error event instead of the exception // Also there seems to be no way to intercept the message that is logged to the console xhr.onerror = rejectWithError; xhr.send(); } catch (ex) { rejectWithError(); } }); } 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 = Window.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 }; } ); /** * ImageScanner.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.file.ImageScanner', [ "tinymce.core.util.Promise", "tinymce.core.util.Arr", "tinymce.core.util.Fun", "tinymce.core.file.Conversions", "tinymce.core.Env" ], function (Promise, Arr, Fun, Conversions, Env) { var count = 0; var uniqueId = function (prefix) { return (prefix || 'blobid') + (count++); }; var imageToBlobInfo = function (blobCache, img, resolve, reject) { var base64, blobInfo; if (img.src.indexOf('blob:') === 0) { blobInfo = blobCache.getByUri(img.src); if (blobInfo) { resolve({ image: img, blobInfo: blobInfo }); } else { Conversions.uriToBlob(img.src).then(function (blob) { Conversions.blobToDataUri(blob).then(function (dataUri) { base64 = Conversions.parseDataUri(dataUri).data; blobInfo = blobCache.create(uniqueId(), blob, base64); blobCache.add(blobInfo); resolve({ image: img, blobInfo: blobInfo }); }); }, function (err) { reject(err); }); } 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) { blobInfo = blobCache.create(uniqueId(), blob, base64); blobCache.add(blobInfo); resolve({ image: img, blobInfo: blobInfo }); }, function (err) { reject(err); }); } }; var getAllImages = function (elm) { return elm ? elm.getElementsByTagName('img') : []; }; return function (uploadStatus, blobCache) { var cachedPromises = {}; function findAll(elm, predicate) { var images, promises; if (!predicate) { predicate = Fun.constant(true); } images = Arr.filter(getAllImages(elm), 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) { if (typeof imageInfo === 'string') { // error apparently return imageInfo; } resolve({ image: img, blobInfo: imageInfo.blobInfo }); }); }); } newPromise = new Promise(function (resolve, reject) { imageToBlobInfo(blobCache, img, resolve, reject); }).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 }; }; } ); define( 'ephox.sand.api.URL', [ 'ephox.sand.util.Global' ], function (Global) { /* * IE10 and above per * https://developer.mozilla.org/en-US/docs/Web/API/URL.createObjectURL * * Also Safari 6.1+ * Safari 6.0 has 'webkitURL' instead, but doesn't support flexbox so we * aren't supporting it anyway */ var url = function () { return Global.getOrDie('URL'); }; var createObjectURL = function (blob) { return url().createObjectURL(blob); }; var revokeObjectURL = function (u) { url().revokeObjectURL(u); }; return { createObjectURL: createObjectURL, revokeObjectURL: revokeObjectURL }; } ); /** * Uuid.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.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 }; } ); /** * BlobCache.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.file.BlobCache', [ 'ephox.sand.api.URL', 'tinymce.core.util.Arr', 'tinymce.core.util.Fun', 'tinymce.core.util.Uuid' ], function (URL, Arr, Fun, Uuid) { return function () { var cache = [], constant = Fun.constant; function mimeToExt(mime) { var mimes = { 'image/jpeg': 'jpg', 'image/jpg': 'jpg', 'image/gif': 'gif', 'image/png': 'png' }; return mimes[mime.toLowerCase()] || 'dat'; } function create(o, blob, base64, filename) { return typeof o === 'object' ? toBlobInfo(o) : toBlobInfo({ id: o, name: filename, blob: blob, base64: base64 }); } function toBlobInfo(o) { var id, name; if (!o.blob || !o.base64) { throw "blob and base64 representations of the image are required for BlobInfo to be created"; } id = o.id || Uuid.uuid('blobid'); name = o.name || id; return { id: constant(id), name: constant(name), filename: constant(name + '.' + mimeToExt(o.blob.type)), blob: constant(o.blob), base64: constant(o.base64), blobUri: constant(o.blobUri || URL.createObjectURL(o.blob)), uri: constant(o.uri) }; } 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 }; }; } ); /** * UploadStatus.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.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 }; }; } ); /** * EditorUpload.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.EditorUpload', [ "tinymce.core.util.Arr", "tinymce.core.file.Uploader", "tinymce.core.file.ImageScanner", "tinymce.core.file.BlobCache", "tinymce.core.file.UploadStatus", "tinymce.core.ErrorReporter" ], function (Arr, Uploader, ImageScanner, BlobCache, UploadStatus, ErrorReporter) { 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) { var filteredResult = 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); } else if (uploadInfo.error) { ErrorReporter.uploadError(editor, uploadInfo.error); } return { element: image, status: uploadInfo.status }; }); if (callback) { callback(filteredResult); } return filteredResult; })); })); } 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) { result = Arr.filter(result, function (resultItem) { // ImageScanner internally converts images that it finds, but it may fail to do so if image source is inaccessible. // In such case resultItem will contain appropriate text error message, instead of image data. if (typeof resultItem === 'string') { ErrorReporter.displayError(editor, resultItem); return false; } return true; }); 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.get(), function (result, editor) { return result || editor.editorUpload && 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 }; }; } ); /** * ForceBlocks.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ForceBlocks', [ 'ephox.katamari.api.Fun' ], function (Fun) { var addRootBlocks = function (editor) { var settings = editor.settings, dom = editor.dom, selection = editor.selection; var schema = editor.schema, blockElements = schema.getBlockElements(); 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(); } }; var setup = function (editor) { if (editor.settings.forced_root_block) { editor.on('NodeChange', Fun.curry(addRootBlocks, editor)); } }; return { setup: setup }; } ); /** * Dimensions.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.dom.Dimensions', [ "tinymce.core.util.Arr", "tinymce.core.dom.NodeType", "tinymce.core.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 }; } ); /** * LineUtils.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.caret.LineUtils', [ "tinymce.core.util.Fun", "tinymce.core.util.Arr", "tinymce.core.dom.NodeType", "tinymce.core.dom.Dimensions", "tinymce.core.geom.ClientRect", "tinymce.core.caret.CaretUtils", "tinymce.core.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 }; } ); /** * LineWalker.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.caret.LineWalker', [ "tinymce.core.util.Fun", "tinymce.core.util.Arr", "tinymce.core.dom.Dimensions", "tinymce.core.caret.CaretCandidate", "tinymce.core.caret.CaretUtils", "tinymce.core.caret.CaretWalker", "tinymce.core.caret.CaretPosition", "tinymce.core.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) }; } ); /** * CefUtils.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.CefUtils', [ 'tinymce.core.caret.CaretPosition', 'tinymce.core.caret.CaretUtils', 'tinymce.core.dom.NodeType', 'tinymce.core.util.Fun' ], function (CaretPosition, CaretUtils, NodeType, Fun) { var isContentEditableTrue = NodeType.isContentEditableTrue; var isContentEditableFalse = NodeType.isContentEditableFalse; var showCaret = function (direction, editor, node, before) { // TODO: Figure out a better way to handle this dependency return editor._selectionOverrides.showCaret(direction, node, before); }; var getNodeRange = function (node) { var rng = node.ownerDocument.createRange(); rng.selectNode(node); return rng; }; var selectNode = function (editor, node) { var e; e = editor.fire('BeforeObjectSelected', { target: node }); if (e.isDefaultPrevented()) { return null; } return getNodeRange(node); }; var renderCaretAtRange = function (editor, range) { var caretPosition, ceRoot; range = CaretUtils.normalizeRange(1, editor.getBody(), range); caretPosition = CaretPosition.fromRangeStart(range); if (isContentEditableFalse(caretPosition.getNode())) { return showCaret(1, editor, caretPosition.getNode(), !caretPosition.isAtEnd()); } if (isContentEditableFalse(caretPosition.getNode(true))) { return showCaret(1, editor, 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, editor, ceRoot, false); } return null; }; var renderRangeCaret = function (editor, range) { var caretRange; if (!range || !range.collapsed) { return range; } caretRange = renderCaretAtRange(editor, range); if (caretRange) { return caretRange; } return range; }; return { showCaret: showCaret, selectNode: selectNode, renderCaretAtRange: renderCaretAtRange, renderRangeCaret: renderRangeCaret }; } ); /** * CefNavigation.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.CefNavigation', [ 'tinymce.core.caret.CaretContainer', 'tinymce.core.caret.CaretPosition', 'tinymce.core.caret.CaretUtils', 'tinymce.core.caret.CaretWalker', 'tinymce.core.caret.LineUtils', 'tinymce.core.caret.LineWalker', 'tinymce.core.dom.NodeType', 'tinymce.core.dom.RangeUtils', 'tinymce.core.Env', 'tinymce.core.keyboard.CefUtils', 'tinymce.core.util.Arr', 'tinymce.core.util.Fun' ], function (CaretContainer, CaretPosition, CaretUtils, CaretWalker, LineUtils, LineWalker, NodeType, RangeUtils, Env, CefUtils, Arr, Fun) { var isContentEditableFalse = NodeType.isContentEditableFalse; var getSelectedNode = RangeUtils.getSelectedNode; var isAfterContentEditableFalse = CaretUtils.isAfterContentEditableFalse; var isBeforeContentEditableFalse = CaretUtils.isBeforeContentEditableFalse; var getVisualCaretPosition = function (walkFn, caretPosition) { while ((caretPosition = walkFn(caretPosition))) { if (caretPosition.isVisible()) { return caretPosition; } } return caretPosition; }; var isMoveInsideSameBlock = function (fromCaretPosition, toCaretPosition) { var inSameBlock = CaretUtils.isInSameBlock(fromCaretPosition, toCaretPosition); // Handle bogus BR

    abc|

    if (!inSameBlock && NodeType.isBr(fromCaretPosition.getNode())) { return true; } return inSameBlock; }; var isRangeInCaretContainerBlock = function (range) { return CaretContainer.isCaretContainerBlock(range.startContainer); }; var getNormalizedRangeEndPoint = function (direction, rootNode, range) { range = CaretUtils.normalizeRange(direction, rootNode, range); if (direction === -1) { return CaretPosition.fromRangeStart(range); } return CaretPosition.fromRangeEnd(range); }; var moveToCeFalseHorizontally = function (direction, editor, getNextPosFn, isBeforeContentEditableFalseFn, range) { var node, caretPosition, peekCaretPosition, rangeIsInContainerBlock; if (!range.collapsed) { node = getSelectedNode(range); if (isContentEditableFalse(node)) { return CefUtils.showCaret(direction, editor, node, direction === -1); } } rangeIsInContainerBlock = isRangeInCaretContainerBlock(range); caretPosition = getNormalizedRangeEndPoint(direction, editor.getBody(), range); if (isBeforeContentEditableFalseFn(caretPosition)) { return CefUtils.selectNode(editor, caretPosition.getNode(direction === -1)); } caretPosition = getNextPosFn(caretPosition); if (!caretPosition) { if (rangeIsInContainerBlock) { return range; } return null; } if (isBeforeContentEditableFalseFn(caretPosition)) { return CefUtils.showCaret(direction, editor, caretPosition.getNode(direction === -1), direction === 1); } // Peek ahead for handling of ab|c -> abc| peekCaretPosition = getNextPosFn(caretPosition); if (isBeforeContentEditableFalseFn(peekCaretPosition)) { if (isMoveInsideSameBlock(caretPosition, peekCaretPosition)) { return CefUtils.showCaret(direction, editor, peekCaretPosition.getNode(direction === -1), direction === 1); } } if (rangeIsInContainerBlock) { return CefUtils.renderRangeCaret(editor, caretPosition.toRange()); } return null; }; var moveToCeFalseVertically = function (direction, editor, walkerFn, range) { var caretPosition, linePositions, nextLinePositions, closestNextLineRect, caretClientRect, clientX, dist1, dist2, contentEditableFalseNode; contentEditableFalseNode = getSelectedNode(range); caretPosition = getNormalizedRangeEndPoint(direction, editor.getBody(), range); linePositions = walkerFn(editor.getBody(), 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 CefUtils.showCaret(direction, editor, closestNextLineRect.node, dist1 < dist2); } } if (contentEditableFalseNode) { var caretPositions = LineWalker.positionsUntil(direction, editor.getBody(), LineWalker.isAboveLine(1), contentEditableFalseNode); closestNextLineRect = LineUtils.findClosestClientRect(Arr.filter(caretPositions, LineWalker.isLine(1)), clientX); if (closestNextLineRect) { return CefUtils.renderRangeCaret(editor, closestNextLineRect.position.toRange()); } closestNextLineRect = Arr.last(Arr.filter(caretPositions, LineWalker.isLine(0))); if (closestNextLineRect) { return CefUtils.renderRangeCaret(editor, closestNextLineRect.position.toRange()); } } }; var createTextBlock = function (editor) { var textBlock = editor.dom.create(editor.settings.forced_root_block); if (!Env.ie || Env.ie >= 11) { textBlock.innerHTML = '
    '; } return textBlock; }; var exitPreBlock = function (editor, direction, range) { var pre, caretPos, newBlock; var caretWalker = new CaretWalker(editor.getBody()); var getNextVisualCaretPosition = Fun.curry(getVisualCaretPosition, caretWalker.next); var getPrevVisualCaretPosition = Fun.curry(getVisualCaretPosition, caretWalker.prev); 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(editor); if (direction === 1) { editor.$(pre).after(newBlock); } else { editor.$(pre).before(newBlock); } editor.selection.select(newBlock, true); editor.selection.collapse(); } } }; var getHorizontalRange = function (editor, forward) { var caretWalker = new CaretWalker(editor.getBody()); var getNextVisualCaretPosition = Fun.curry(getVisualCaretPosition, caretWalker.next); var getPrevVisualCaretPosition = Fun.curry(getVisualCaretPosition, caretWalker.prev); var newRange, direction = forward ? 1 : -1; var getNextPosFn = forward ? getNextVisualCaretPosition : getPrevVisualCaretPosition; var isBeforeContentEditableFalseFn = forward ? isBeforeContentEditableFalse : isAfterContentEditableFalse; var range = editor.selection.getRng(); newRange = moveToCeFalseHorizontally(direction, editor, getNextPosFn, isBeforeContentEditableFalseFn, range); if (newRange) { return newRange; } newRange = exitPreBlock(editor, direction, range); if (newRange) { return newRange; } return null; }; var getVerticalRange = function (editor, down) { var newRange, direction = down ? 1 : -1; var walkerFn = down ? LineWalker.downUntil : LineWalker.upUntil; var range = editor.selection.getRng(); newRange = moveToCeFalseVertically(direction, editor, walkerFn, range); if (newRange) { return newRange; } newRange = exitPreBlock(editor, direction, range); if (newRange) { return newRange; } return null; }; var moveH = function (editor, forward) { return function () { var newRng = getHorizontalRange(editor, forward); if (newRng) { editor.selection.setRng(newRng); return true; } else { return false; } }; }; var moveV = function (editor, down) { return function () { var newRng = getVerticalRange(editor, down); if (newRng) { editor.selection.setRng(newRng); return true; } else { return false; } }; }; return { moveH: moveH, moveV: moveV }; } ); define( 'ephox.katamari.api.Merger', [ 'ephox.katamari.api.Type', 'global!Array', 'global!Error' ], function (Type, Array, Error) { var shallow = function (old, nu) { return nu; }; var deep = function (old, nu) { var bothObjects = Type.isObject(old) && Type.isObject(nu); return bothObjects ? deepMerge(old, nu) : nu; }; var baseMerge = function (merger) { return function() { // Don't use array slice(arguments), makes the whole function unoptimisable on Chrome var objects = new Array(arguments.length); for (var i = 0; i < objects.length; i++) objects[i] = arguments[i]; if (objects.length === 0) throw new Error('Can\'t merge zero objects'); var ret = {}; for (var j = 0; j < objects.length; j++) { var curObject = objects[j]; for (var key in curObject) if (curObject.hasOwnProperty(key)) { ret[key] = merger(ret[key], curObject[key]); } } return ret; }; }; var deepMerge = baseMerge(deep); var merge = baseMerge(shallow); return { deepMerge: deepMerge, merge: merge }; } ); /** * MatchKeys.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.MatchKeys', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'ephox.katamari.api.Merger' ], function (Arr, Fun, Merger) { var defaultPatterns = function (patterns) { return Arr.map(patterns, function (pattern) { return Merger.merge({ shiftKey: false, altKey: false, ctrlKey: false, metaKey: false, keyCode: 0, action: Fun.noop }, pattern); }); }; var matchesEvent = function (pattern, evt) { return ( evt.keyCode === pattern.keyCode && evt.shiftKey === pattern.shiftKey && evt.altKey === pattern.altKey && evt.ctrlKey === pattern.ctrlKey && evt.metaKey === pattern.metaKey ); }; var match = function (patterns, evt) { return Arr.bind(defaultPatterns(patterns), function (pattern) { return matchesEvent(pattern, evt) ? [pattern] : [ ]; }); }; var action = function (f) { var args = Array.prototype.slice.call(arguments, 1); return function () { return f.apply(null, args); }; }; var execute = function (patterns, evt) { return Arr.find(match(patterns, evt), function (pattern) { return pattern.action(); }); }; return { match: match, action: action, execute: execute }; } ); /** * ArrowKeys.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.ArrowKeys', [ 'tinymce.core.keyboard.BoundarySelection', 'tinymce.core.keyboard.CefNavigation', 'tinymce.core.keyboard.MatchKeys', 'tinymce.core.util.VK' ], function (BoundarySelection, CefNavigation, MatchKeys, VK) { var executeKeydownOverride = function (editor, caret, evt) { MatchKeys.execute([ { keyCode: VK.RIGHT, action: CefNavigation.moveH(editor, true) }, { keyCode: VK.LEFT, action: CefNavigation.moveH(editor, false) }, { keyCode: VK.UP, action: CefNavigation.moveV(editor, false) }, { keyCode: VK.DOWN, action: CefNavigation.moveV(editor, true) }, { keyCode: VK.RIGHT, action: BoundarySelection.move(editor, caret, true) }, { keyCode: VK.LEFT, action: BoundarySelection.move(editor, caret, false) } ], evt).each(function (_) { evt.preventDefault(); }); }; var setup = function (editor, caret) { editor.on('keydown', function (evt) { if (evt.isDefaultPrevented() === false) { executeKeydownOverride(editor, caret, evt); } }); }; return { setup: setup }; } ); /** * DeleteBackspaceKeys.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.DeleteBackspaceKeys', [ 'tinymce.core.delete.BlockBoundaryDelete', 'tinymce.core.delete.BlockRangeDelete', 'tinymce.core.delete.CefDelete', 'tinymce.core.delete.InlineBoundaryDelete', 'tinymce.core.keyboard.MatchKeys', 'tinymce.core.util.VK' ], function (BlockBoundaryDelete, BlockRangeDelete, CefDelete, InlineBoundaryDelete, MatchKeys, VK) { var executeKeydownOverride = function (editor, caret, evt) { MatchKeys.execute([ { keyCode: VK.BACKSPACE, action: MatchKeys.action(CefDelete.backspaceDelete, editor, false) }, { keyCode: VK.DELETE, action: MatchKeys.action(CefDelete.backspaceDelete, editor, true) }, { keyCode: VK.BACKSPACE, action: MatchKeys.action(InlineBoundaryDelete.backspaceDelete, editor, caret, false) }, { keyCode: VK.DELETE, action: MatchKeys.action(InlineBoundaryDelete.backspaceDelete, editor, caret, true) }, { keyCode: VK.BACKSPACE, action: MatchKeys.action(BlockRangeDelete.backspaceDelete, editor, false) }, { keyCode: VK.DELETE, action: MatchKeys.action(BlockRangeDelete.backspaceDelete, editor, true) }, { keyCode: VK.BACKSPACE, action: MatchKeys.action(BlockBoundaryDelete.backspaceDelete, editor, false) }, { keyCode: VK.DELETE, action: MatchKeys.action(BlockBoundaryDelete.backspaceDelete, editor, true) } ], evt).each(function (_) { evt.preventDefault(); }); }; var executeKeyupOverride = function (editor, evt) { MatchKeys.execute([ { keyCode: VK.BACKSPACE, action: MatchKeys.action(CefDelete.paddEmptyElement, editor) }, { keyCode: VK.DELETE, action: MatchKeys.action(CefDelete.paddEmptyElement, editor) } ], evt); }; var setup = function (editor, caret) { editor.on('keydown', function (evt) { if (evt.isDefaultPrevented() === false) { executeKeydownOverride(editor, caret, evt); } }); editor.on('keyup', function (evt) { if (evt.isDefaultPrevented() === false) { executeKeyupOverride(editor, evt); } }); }; return { setup: setup }; } ); /** * InsertNewLine.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.InsertNewLine', [ 'tinymce.core.caret.CaretContainer', 'tinymce.core.dom.NodeType', 'tinymce.core.dom.RangeUtils', 'tinymce.core.dom.TreeWalker', 'tinymce.core.text.Zwsp', 'tinymce.core.util.Tools' ], function (CaretContainer, NodeType, RangeUtils, TreeWalker, Zwsp, Tools) { var isEmptyAnchor = function (elm) { return elm && elm.nodeName === "A" && Tools.trim(Zwsp.trim(elm.innerText || elm.textContent)).length === 0; }; var isTableCell = function (node) { return node && /^(TD|TH|CAPTION)$/.test(node.nodeName); }; var hasFirstChild = function (elm, name) { return elm.firstChild && elm.firstChild.nodeName == name; }; var hasParent = function (elm, parentName) { return elm && elm.parentNode && elm.parentNode.nodeName === parentName; }; var emptyBlock = function (elm) { elm.innerHTML = '
    '; }; var containerAndSiblingName = function (container, nodeName) { return container.nodeName === nodeName || (container.previousSibling && container.previousSibling.nodeName === nodeName); }; var isListBlock = function (elm) { return elm && /^(OL|UL|LI)$/.test(elm.nodeName); }; var isNestedList = function (elm) { return isListBlock(elm) && isListBlock(elm.parentNode); }; // Returns true if the block can be split into two blocks or not var canSplitBlock = function (dom, node) { return node && dom.isBlock(node) && !/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) && !/^(fixed|absolute)/i.test(node.style.position) && dom.getContentEditable(node) !== "true"; }; // Remove the first empty inline element of the block so this:

    x

    becomes this:

    x

    var trimInlineElementsOnLeftSideOfBlock = function (dom, nonEmptyElementsMap, block) { var node = block, firstChilds = [], i; if (!node) { return; } // Find inner most first child ex:

    *

    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 { if (isEmptyAnchor(node)) { dom.remove(node); } } } }; var normalizeZwspOffset = function (start, container, offset) { if (NodeType.isText(container) === false) { return offset; } if (start) { return offset === 1 && container.data.charAt(offset - 1) === Zwsp.ZWSP ? 0 : offset; } else { return offset === container.data.length - 1 && container.data.charAt(offset) === Zwsp.ZWSP ? container.data.length : offset; } }; var includeZwspInRange = function (rng) { var newRng = rng.cloneRange(); newRng.setStart(rng.startContainer, normalizeZwspOffset(true, rng.startContainer, rng.startOffset)); newRng.setEnd(rng.endContainer, normalizeZwspOffset(false, rng.endContainer, rng.endOffset)); return newRng; }; var firstNonWhiteSpaceNodeSibling = function (node) { while (node) { if (node.nodeType === 1 || (node.nodeType === 3 && node.data && /[\r\n\s]/.test(node.data))) { return node; } node = node.nextSibling; } }; // Inserts a BR element if the forced_root_block option is set to false or empty string var insertBr = function (editor, evt) { editor.execCommand("InsertLineBreak", false, evt); }; // Trims any linebreaks at the beginning of node user for example when pressing enter in a PRE element var trimLeadingLineBreaks = function (node) { do { if (node.nodeType === 3) { node.nodeValue = node.nodeValue.replace(/^[\r\n]+/, ''); } node = node.firstChild; } while (node); }; var getEditableRoot = function (dom, 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; }; var setForcedBlockAttrs = function (editor, node) { var forcedRootBlockName = editor.settings.forced_root_block; if (forcedRootBlockName && forcedRootBlockName.toLowerCase() === node.tagName.toLowerCase()) { editor.dom.setAttribs(node, editor.settings.forced_root_block_attrs); } }; // Wraps any text nodes or inline elements in the specified forced root block name var wrapSelfAndSiblingsInDefaultBlock = function (editor, newBlockName, rng, container, offset) { var newBlock, parentBlock, startNode, node, next, rootBlockName, blockName = newBlockName || 'P'; var dom = editor.dom, editableRoot = getEditableRoot(dom, container); // Not in a block element or in a table cell or caption parentBlock = dom.getParent(container, dom.isBlock); if (!parentBlock || !canSplitBlock(dom, 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(editor, 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 && editor.schema.isValidChild(rootBlockName, blockName.toLowerCase())) { newBlock = dom.create(blockName); setForcedBlockAttrs(editor, 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; }; // 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 var addBrToBlockIfNeeded = function (dom, block) { var lastChild; // IE will render the blocks correctly other browsers needs a BR 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'); } }; var getContainerBlock = function (containerBlock) { var containerBlockParent = containerBlock.parentNode; if (/^(LI|DT|DD)$/.test(containerBlockParent.nodeName)) { return containerBlockParent; } return containerBlock; }; var isFirstOrLastLi = function (containerBlock, parentBlock, 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; }; var insert = function (editor, evt) { var tmpRng, editableRoot, container, offset, parentBlock, shiftKey; var newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName, isAfterLastNodeInContainer; var dom = editor.dom, selection = editor.selection, settings = editor.settings; var schema = editor.schema, nonEmptyElementsMap = schema.getNonEmptyElements(); var rng = editor.selection.getRng(); // 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; var moveCaretBeforeOnEnterElementsMap = schema.getMoveCaretBeforeOnEnterElements(); if (!root) { return; } 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(); 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)) { 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); } // 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" || parentBlockName == "HR") { block = dom.create(name || newBlockName); setForcedBlockAttrs(editor, block); } else { block = parentBlock.cloneNode(false); } caretNode = block; if (settings.keep_styles === false) { dom.setAttrib(block, 'style', null); // wipe out any styles that came over with the block dom.setAttrib(block, 'class', null); } else { // Clone any parent styles 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); } emptyBlock(caretNode); 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, normalizedOffset; normalizedOffset = normalizeZwspOffset(start, container, offset); // Caret is in the middle of a text node like "a|b" if (container.nodeType == 3 && (start ? normalizedOffset > 0 : normalizedOffset < 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 or a hr if (containerAndSiblingName(container, 'TABLE') || containerAndSiblingName(container, 'HR')) { 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 && normalizedOffset === 0) { walker.prev(); } else if (!start && normalizedOffset == 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 but not trailing br:s like

    text|

    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; } // Inserts a block or br before/after or in the middle of a split list of the LI is empty function handleEmptyListItem() { if (containerBlock == editor.getBody()) { return; } if (isNestedList(containerBlock)) { newBlockName = 'LI'; } newBlock = newBlockName ? createNewBlock(newBlockName) : dom.create('BR'); if (isFirstOrLastLi(containerBlock, parentBlock, true) && isFirstOrLastLi(containerBlock, parentBlock, false)) { if (hasParent(containerBlock, 'LI')) { // Nested list is inside a LI dom.insertAfter(newBlock, getContainerBlock(containerBlock)); } else { // Is first and last list item then replace the OL/UL with a text block dom.replace(newBlock, containerBlock); } } else if (isFirstOrLastLi(containerBlock, parentBlock, true)) { if (hasParent(containerBlock, 'LI')) { // List nested in an LI then move the list to a new sibling LI dom.insertAfter(newBlock, getContainerBlock(containerBlock)); 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(containerBlock, parentBlock, false)) { // Last LI in list then remove LI and add text block after list dom.insertAfter(newBlock, getContainerBlock(containerBlock)); } 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(containerBlock); tmpRng = rng.cloneRange(); tmpRng.setStartAfter(parentBlock); tmpRng.setEndAfter(containerBlock); fragment = tmpRng.extractContents(); if (newBlockName === 'LI' && hasFirstChild(fragment, 'LI')) { newBlock = fragment.firstChild; dom.insertAfter(fragment, containerBlock); } else { dom.insertAfter(fragment, containerBlock); dom.insertAfter(newBlock, containerBlock); } } dom.remove(parentBlock); moveToCaretPosition(newBlock); } 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(dom, 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); } // 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() : ''; 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(dom, container); // If there is no editable root then enter is done inside a contentEditable false element if (!editableRoot) { return; } // If editable root isn't block nor the root of the editor if (!dom.isBlock(editableRoot) && editableRoot != dom.getRoot()) { if (!newBlockName || shiftKey) { insertBr(editor, evt); } return; } // Wrap the current node and it's sibling in a default block if it's needed. // for example this
  • will become this // This won't happen if root blocks are disabled or the shiftKey is pressed if ((newBlockName && !shiftKey) || (!newBlockName && shiftKey)) { container = wrapSelfAndSiblingsInDefaultBlock(editor, newBlockName, rng, 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; containerBlock = containerBlock.parentNode; parentBlockName = containerBlockName; } // Handle enter in list item if (/^(LI|DT|DD)$/.test(parentBlockName)) { if (!newBlockName && shiftKey) { insertBr(editor, evt); 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(editor, evt); 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(editor, evt); 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); if (dom.isEmpty(parentBlock)) { emptyBlock(parentBlock); } moveToCaretPosition(newBlock); } else if (isCaretAtStartOrEndOfBlock()) { insertNewBlockAfter(); } else if (isCaretAtStartOrEndOfBlock(true)) { // Insert new block before newBlock = parentBlock.parentNode.insertBefore(createNewBlock(), parentBlock); // Adjust caret position if HR containerAndSiblingName(parentBlock, 'HR') ? moveToCaretPosition(newBlock) : moveToCaretPosition(parentBlock); } else { // Extract after fragment and insert it after the current block tmpRng = includeZwspInRange(rng).cloneRange(); tmpRng.setEndAfter(parentBlock); fragment = tmpRng.extractContents(); trimLeadingLineBreaks(fragment); newBlock = fragment.firstChild; dom.insertAfter(fragment, parentBlock); trimInlineElementsOnLeftSideOfBlock(dom, nonEmptyElementsMap, newBlock); addBrToBlockIfNeeded(dom, parentBlock); if (dom.isEmpty(parentBlock)) { emptyBlock(parentBlock); } newBlock.normalize(); // New block might become empty if it's

    a |

    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 }); }; return { insert: insert }; } ); /** * EnterKey.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.EnterKey', [ 'tinymce.core.keyboard.InsertNewLine', 'tinymce.core.util.VK' ], function (InsertNewLine, VK) { var endTypingLevel = function (undoManager) { if (undoManager.typing) { undoManager.typing = false; undoManager.add(); } }; var handleEnterKeyEvent = function (editor, event) { if (event.isDefaultPrevented()) { return; } event.preventDefault(); endTypingLevel(editor.undoManager); editor.undoManager.transact(function () { if (editor.selection.isCollapsed() === false) { editor.execCommand('Delete'); } InsertNewLine.insert(editor, event); }); }; var setup = function (editor) { editor.on('keydown', function (event) { if (event.keyCode === VK.ENTER) { handleEnterKeyEvent(editor, event); } }); }; return { setup: setup }; } ); /** * InsertSpace.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.InsertSpace', [ 'ephox.katamari.api.Fun', 'tinymce.core.caret.CaretPosition', 'tinymce.core.dom.NodeType', 'tinymce.core.keyboard.BoundaryLocation', 'tinymce.core.keyboard.InlineUtils' ], function (Fun, CaretPosition, NodeType, BoundaryLocation, InlineUtils) { var isValidInsertPoint = function (location, caretPosition) { return isAtStartOrEnd(location) && NodeType.isText(caretPosition.container()); }; var insertNbspAtPosition = function (editor, caretPosition) { var container = caretPosition.container(); var offset = caretPosition.offset(); container.insertData(offset, '\u00a0'); editor.selection.setCursorLocation(container, offset + 1); }; var insertAtLocation = function (editor, caretPosition, location) { if (isValidInsertPoint(location, caretPosition)) { insertNbspAtPosition(editor, caretPosition); return true; } else { return false; } }; var insertAtCaret = function (editor) { var isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor); var caretPosition = CaretPosition.fromRangeStart(editor.selection.getRng()); var boundaryLocation = BoundaryLocation.readLocation(isInlineTarget, editor.getBody(), caretPosition); return boundaryLocation.map(Fun.curry(insertAtLocation, editor, caretPosition)).getOr(false); }; var isAtStartOrEnd = function (location) { return location.fold( Fun.constant(false), // Before Fun.constant(true), // Start Fun.constant(true), // End Fun.constant(false) // After ); }; var insertAtSelection = function (editor) { return editor.selection.isCollapsed() ? insertAtCaret(editor) : false; }; return { insertAtSelection: insertAtSelection }; } ); /** * SpaceKey.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.SpaceKey', [ 'tinymce.core.keyboard.InsertSpace', 'tinymce.core.keyboard.MatchKeys', 'tinymce.core.util.VK' ], function (InsertSpace, MatchKeys, VK) { var executeKeydownOverride = function (editor, evt) { MatchKeys.execute([ { keyCode: VK.SPACEBAR, action: MatchKeys.action(InsertSpace.insertAtSelection, editor) } ], evt).each(function (_) { evt.preventDefault(); }); }; var setup = function (editor) { editor.on('keydown', function (evt) { if (evt.isDefaultPrevented() === false) { executeKeydownOverride(editor, evt); } }); }; return { setup: setup }; } ); /** * KeyboardOverrides.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.KeyboardOverrides', [ 'tinymce.core.keyboard.ArrowKeys', 'tinymce.core.keyboard.BoundarySelection', 'tinymce.core.keyboard.DeleteBackspaceKeys', 'tinymce.core.keyboard.EnterKey', 'tinymce.core.keyboard.SpaceKey' ], function (ArrowKeys, BoundarySelection, DeleteBackspaceKeys, EnterKey, SpaceKey) { var setup = function (editor) { var caret = BoundarySelection.setupSelectedState(editor); ArrowKeys.setup(editor, caret); DeleteBackspaceKeys.setup(editor, caret); EnterKey.setup(editor); SpaceKey.setup(editor); }; return { setup: setup }; } ); /** * NodeChange.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.NodeChange', [ "tinymce.core.dom.RangeUtils", "tinymce.core.Env", "tinymce.core.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); // When focusout from after cef element to other input element the startelm can be undefined. // 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 (!startElm || (!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(true) || 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; } // 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); } }; }; } ); /** * MousePosition.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.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 }; } ); /** * DragDropOverrides.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.DragDropOverrides', [ "tinymce.core.dom.NodeType", "tinymce.core.util.Arr", "tinymce.core.util.Fun", "tinymce.core.util.Delay", "tinymce.core.dom.DOMUtils", "tinymce.core.dom.MousePosition" ], function ( NodeType, Arr, Fun, Delay, DOMUtils, MousePosition ) { var isContentEditableFalse = NodeType.isContentEditableFalse, isContentEditableTrue = NodeType.isContentEditableTrue; var isDraggable = function (rootElm, elm) { return isContentEditableFalse(elm) && elm !== rootElm; }; 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(editor.getBody(), 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 }; } ); /** * FakeCaret.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.caret.FakeCaret', [ 'tinymce.core.caret.CaretContainer', 'tinymce.core.caret.CaretContainerRemove', 'tinymce.core.caret.CaretPosition', 'tinymce.core.dom.DomQuery', 'tinymce.core.dom.NodeType', 'tinymce.core.dom.RangeUtils', 'tinymce.core.geom.ClientRect', 'tinymce.core.util.Delay' ], function (CaretContainer, CaretContainerRemove, CaretPosition, DomQuery, NodeType, RangeUtils, ClientRect, Delay) { var isContentEditableFalse = NodeType.isContentEditableFalse; var isTableCell = function (node) { return node && /^(TD|TH)$/i.test(node.nodeName); }; 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 = DomQuery('*[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 (isTableCell(node)) { return null; } if (isBlock(node)) { caretContainerNode = CaretContainer.insertBlock('p', node, before); clientRect = getAbsoluteClientRect(node, before); DomQuery(caretContainerNode).css('top', clientRect.top); $lastVisualCaret = DomQuery('
    ').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) { CaretContainerRemove.remove(caretContainerNode); caretContainerNode = null; } if ($lastVisualCaret) { $lastVisualCaret.remove(); $lastVisualCaret = null; } clearInterval(cursorInterval); } function startBlink() { cursorInterval = Delay.setInterval(function () { DomQuery('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 }; }; } ); /** * SelectionOverrides.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.SelectionOverrides', [ 'ephox.katamari.api.Arr', 'ephox.sugar.api.dom.Remove', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.properties.Attr', 'ephox.sugar.api.search.SelectorFilter', 'ephox.sugar.api.search.SelectorFind', 'tinymce.core.DragDropOverrides', 'tinymce.core.EditorView', 'tinymce.core.Env', 'tinymce.core.caret.CaretContainer', 'tinymce.core.caret.CaretPosition', 'tinymce.core.caret.CaretUtils', 'tinymce.core.caret.CaretWalker', 'tinymce.core.caret.FakeCaret', 'tinymce.core.caret.LineUtils', 'tinymce.core.dom.ElementType', 'tinymce.core.dom.NodeType', 'tinymce.core.dom.RangePoint', 'tinymce.core.keyboard.CefUtils', 'tinymce.core.util.Delay', 'tinymce.core.util.VK' ], function ( Arr, Remove, Element, Attr, SelectorFilter, SelectorFind, DragDropOverrides, EditorView, Env, CaretContainer, CaretPosition, CaretUtils, CaretWalker, FakeCaret, LineUtils, ElementType, NodeType, RangePoint, CefUtils, Delay, VK ) { var isContentEditableTrue = NodeType.isContentEditableTrue, isContentEditableFalse = NodeType.isContentEditableFalse, isAfterContentEditableFalse = CaretUtils.isAfterContentEditableFalse, isBeforeContentEditableFalse = CaretUtils.isBeforeContentEditableFalse; function SelectionOverrides(editor) { var rootNode = editor.getBody(); var fakeCaret = new FakeCaret(editor.getBody(), isBlock), realSelectionId = 'sel-' + editor.dom.uniqueId(), selectedContentEditableNode; 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 getNormalizedRangeEndPoint(direction, range) { range = CaretUtils.normalizeRange(direction, rootNode, range); if (direction == -1) { return CaretPosition.fromRangeStart(range); } return CaretPosition.fromRangeEnd(range); } function showBlockCaretContainer(blockCaretContainer) { if (blockCaretContainer.hasAttribute('data-mce-caret')) { CaretContainer.showCaretContainerBlock(blockCaretContainer); setRange(getRange()); // Removes control rect on IE scrollIntoView(blockCaretContainer[0]); } } function registerEvents() { function getContentEditableRoot(node) { var root = editor.getBody(); while (node && node != root) { if (isContentEditableTrue(node) || isContentEditableFalse(node)) { return node; } node = node.parentNode; } return null; } // 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 (e) { var range = getRange(); if (range.collapsed && EditorView.isXYInContentArea(editor, e.clientX, e.clientY)) { setRange(CefUtils.renderCaretAtRange(editor, 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(CefUtils.selectNode(editor, 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; }; // 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; if (EditorView.isXYInContentArea(editor, e.clientX, e.clientY) === false) { return; } contentEditableRoot = getContentEditableRoot(e.target); if (contentEditableRoot) { if (isContentEditableFalse(contentEditableRoot)) { e.preventDefault(); setContentEditableSelection(CefUtils.selectNode(editor, contentEditableRoot)); } else { removeContentEditableSelection(); // Check that we're not attempting a shift + click select within a contenteditable='true' element if (!(isContentEditableTrue(contentEditableRoot) && e.shiftKey) && !RangePoint.isXYWithinRange(e.clientX, e.clientY, editor.selection.getRng())) { ElementType.isVoid(Element.fromDom(e.target)) ? editor.selection.select(e.target) : 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('keypress', function (e) { if (VK.modifierPressed(e)) { return; } switch (e.keyCode) { default: if (isContentEditableFalse(editor.selection.getNode())) { e.preventDefault(); } break; } }); 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, e.forward); 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(CefUtils.renderRangeCaret(editor, 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 isWithinCaretContainer(node) { return ( CaretContainer.isCaretContainer(node) || CaretContainer.startsWithCaretContainer(node) || CaretContainer.endsWithCaretContainer(node) ); } function isRangeInCaretContainer(rng) { return isWithinCaretContainer(rng.startContainer) || isWithinCaretContainer(rng.endContainer); } function setContentEditableSelection(range, forward) { 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)) { if (forward === false) { caretPosition = getNormalizedRangeEndPoint(-1, range); if (isContentEditableFalse(caretPosition.getNode(true))) { return showCaret(-1, caretPosition.getNode(true), false); } if (isContentEditableFalse(caretPosition.getNode())) { return showCaret(-1, caretPosition.getNode(), !caretPosition.isAtEnd()); } } else { 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 [] to [] 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; } $realSelectionContainer = SelectorFind.descendant(Element.fromDom(editor.getBody()), '#' + realSelectionId).fold( function () { return $([]); }, function (elm) { return $([elm.dom()]); } ); targetClone = e.targetClone; if ($realSelectionContainer.length === 0) { $realSelectionContainer = $( '
    ' ).attr('id', realSelectionId); $realSelectionContainer.appendTo(editor.getBody()); } range = editor.dom.createRng(); // WHY is IE making things so hard! Copy on x produces: x // 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('

    \u00a0

    ').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); Arr.each(SelectorFilter.descendants(Element.fromDom(editor.getBody()), '*[data-mce-selected]'), function (elm) { Attr.remove(elm, 'data-mce-selected'); }); node.setAttribute('data-mce-selected', 1); selectedContentEditableNode = node; hideFakeCaret(); return range; } function removeContentEditableSelection() { if (selectedContentEditableNode) { selectedContentEditableNode.removeAttribute('data-mce-selected'); SelectorFind.descendant(Element.fromDom(editor.getBody()), '#' + realSelectionId).each(Remove.remove); selectedContentEditableNode = null; } } function destroy() { fakeCaret.destroy(); selectedContentEditableNode = null; } function hideFakeCaret() { fakeCaret.hide(); } if (Env.ceFalse) { registerEvents(); addCss(); } return { showCaret: showCaret, showBlockCaretContainer: showBlockCaretContainer, hideFakeCaret: hideFakeCaret, destroy: destroy }; } return SelectionOverrides; } ); /** * Diff.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.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 }; } ); /** * Fragments.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.undo.Fragments', [ "tinymce.core.util.Arr", "tinymce.core.html.Entities", "tinymce.core.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 ''; } 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.filter(Arr.map(elm.childNodes, getOuterHtml), function (item) { return item.length > 0; }); }; 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 }; } ); /** * Levels.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.undo.Levels', [ "tinymce.core.util.Arr", "tinymce.core.undo.Fragments" ], function (Arr, Fragments) { var hasIframes = function (html) { return html.indexOf('') !== -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, trimmedFragments; fragments = Fragments.read(editor.getBody()); trimmedFragments = Arr.map(fragments, function (html) { return editor.serializer.trimContent(html); }); content = trimmedFragments.join(''); return hasIframes(content) ? createFragmentedLevel(trimmedFragments) : 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 !!level1 && !!level2 && getLevelContent(level1) === getLevelContent(level2); }; return { createFragmentedLevel: createFragmentedLevel, createCompleteLevel: createCompleteLevel, createFromEditor: createFromEditor, applyToEditor: applyToEditor, isEq: isEq }; } ); /** * UndoManager.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.UndoManager', [ "tinymce.core.util.VK", "tinymce.core.util.Tools", "tinymce.core.undo.Levels" ], function (VK, Tools, Levels) { return function (editor) { var self = this, index = 0, data = [], beforeBookmark, isFirstTypedCharacter, locks = 0; var isUnlocked = function () { return locks === 0; }; var setTyping = function (typing) { if (isUnlocked()) { self.typing = typing; } }; function setDirty(state) { editor.setDirty(state); } function addNonTypingUndoLevel(e) { setTyping(false); self.add({}, e); } function endTyping() { if (self.typing) { setTyping(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) { editor.nodeChanged(); } // Fire a TypingUndo/Change event on the first character entered if (isFirstTypedCharacter && self.typing && Levels.isEq(Levels.createFromEditor(editor), data[0]) === false) { if (editor.isDirty() === false) { setDirty(true); 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(); setTyping(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 (isUnlocked()) { 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 (isUnlocked() === false || 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; setTyping(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 logic 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(); self.ignore(callback); return self.add(); }, /** * Executes the specified mutator function as an undo transaction. But without adding an undo level. * Any logic within the translation that adds undo levels will be ignored. So a translation can * include calls to execCommand or editor.insertContent. * * @method ignore * @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. */ ignore: function (callback) { try { locks++; callback(); } finally { locks--; } }, /** * 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; }; } ); /** * NodePath.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.dom.NodePath', [ "tinymce.core.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 }; } ); /** * Quirks.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.util.Quirks', [ "tinymce.core.util.VK", "tinymce.core.dom.RangeUtils", "tinymce.core.dom.TreeWalker", "tinymce.core.dom.NodePath", "tinymce.core.html.Node", "tinymce.core.html.Entities", "tinymce.core.Env", "tinymce.core.util.Tools", "tinymce.core.util.Delay", "tinymce.core.caret.CaretContainer", "tinymce.core.caret.CaretPosition", "tinymce.core.caret.CaretWalker" ], function (VK, RangeUtils, TreeWalker, NodePath, Node, Entities, Env, Tools, Delay, CaretContainer, CaretPosition, CaretWalker) { return function (editor) { var each = Tools.each; 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. * @param {Boolean} internal State if the paste is to be considered internal or external. */ function insertClipboardContents(content, internal) { if (editor.queryCommandSupported('mceInsertClipboardContent')) { editor.execCommand('mceInsertClipboardContent', false, { content: content, internal: internal }); } else { editor.execCommand('mceInsertContent', false, content); } } /** * Makes sure that the editor body becomes empty when backspace or delete is pressed in empty editors. * * For example: *

    |

    * * Or: *

    |

    * * Or: * [

    ] */ 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

    |

    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 [

    a

    ] instead of

    [a] 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(); editor.selection.select(target); 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: *

    bla[ck

    r]ed

    * * Would become: *

    bla|ed

    * * Instead of: *

    bla|ed

    */ 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: *

    |x

    * * Becomes: *

    |x

    */ 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: *

    x

    * * Becomes this: *

    x

    */ 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 a|b * this fix will lean the caret right into the closest inline element. */ function normalizeSelection() { // Normalize selection for example a|a becomes a|a except for Ctrl+A since it selects everything editor.on('keyup focusin mouseup', function (e) { if (e.keyCode != 65 || !VK.metaKeyPressed(e)) { // We can't normalize on non collapsed ranges on keyboard events since that would cause // issues with moving the selection over empty paragraphs. See #TINY-1130 if (e.type !== 'keyup' || editor.selection.isCollapsed()) { 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:
    '; } else if (/^(UL|OL)$/.test(body.nodeName)) { content = '
  • ' + padd + '
  • '; } 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 = '
    '; } 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(); if (self.removed) { return ''; } // 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 = Tools.trim(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; } ); /** * FocusManager.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.FocusManager', [ "tinymce.core.dom.DOMUtils", "tinymce.core.util.Delay", "tinymce.core.Env" ], function (DOMUtils, Delay, Env) { var selectionChangeHandler, documentFocusInHandler, documentMouseUpHandler, DOM = DOMUtils.DOM; var isUIElement = function (editor, elm) { var customSelector = editor ? editor.settings.custom_ui_selector : ''; var parent = DOM.getParent(elm, function (elm) { return ( FocusManager.isEditorUIElement(elm) || (customSelector ? editor.dom.is(elm, customSelector) : false) ); }); return parent !== null; }; var isInlineEditor = function (editor) { return editor.inline === true; }; var isElementOursideInlineEditor = function (editor, target) { return isInlineEditor(editor) === false || editor.dom.isChildOf(target, editor.getBody()) === false; }; /** * 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 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(); } }); } } }); 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(editor, 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() && isElementOursideInlineEditor(editor, target)) { 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(activeEditor, 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; }; FocusManager._isUIElement = isUIElement; return FocusManager; } ); /** * LegacyInput.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.LegacyInput', [ "tinymce.core.util.Tools" ], function (Tools) { var each = Tools.each, explode = Tools.explode; var register = function (EditorManager) { 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); } }); }); }; return { register: register }; } ); /** * I18n.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.util.I18n', [ "tinymce.core.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 }; } ); /** * EditorManager.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.EditorManager', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Type', 'tinymce.core.AddOnManager', 'tinymce.core.dom.DomQuery', 'tinymce.core.dom.DOMUtils', 'tinymce.core.Editor', 'tinymce.core.Env', 'tinymce.core.ErrorReporter', 'tinymce.core.FocusManager', 'tinymce.core.LegacyInput', 'tinymce.core.util.I18n', 'tinymce.core.util.Observable', 'tinymce.core.util.Promise', 'tinymce.core.util.Tools', 'tinymce.core.util.URI' ], function (Arr, Type, AddOnManager, DomQuery, DOMUtils, Editor, Env, ErrorReporter, FocusManager, LegacyInput, I18n, Observable, Promise, Tools, URI) { var DOM = DOMUtils.DOM; var explode = Tools.explode, each = Tools.each, extend = Tools.extend; var instanceCounter = 0, beforeUnloadDelegate, EditorManager, boundGlobalEvents = false; var legacyEditors = [], editors = []; var isValidLegacyKey = function (id) { // In theory we could filter out any editor id:s that clash // with array prototype items but that could break existing integrations return id !== 'length'; }; function globalEventDelegate(e) { each(EditorManager.get(), function (editor) { if (e.type === 'scroll') { editor.fire('ScrollWindow', e); } else { editor.fire('ResizeWindow', e); } }); } function toggleGlobalEvents(state) { if (state !== boundGlobalEvents) { if (state) { DomQuery(window).on('resize scroll', globalEventDelegate); } else { DomQuery(window).off('resize scroll', globalEventDelegate); } boundGlobalEvents = state; } } function removeEditorFromList(targetEditor) { var oldEditors = editors; delete legacyEditors[targetEditor.id]; for (var i = 0; i < legacyEditors.length; i++) { if (legacyEditors[i] === targetEditor) { legacyEditors.splice(i, 1); break; } } editors = Arr.filter(editors, function (editor) { return targetEditor !== editor; }); // Select another editor since the active one was removed if (EditorManager.activeEditor === targetEditor) { EditorManager.activeEditor = editors.length > 0 ? editors[0] : null; } // Clear focusedEditor if necessary, so that we don't try to blur the destroyed editor if (EditorManager.focusedEditor === targetEditor) { EditorManager.focusedEditor = null; } return oldEditors.length !== editors.length; } 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 */ $: DomQuery, /** * Major version of TinyMCE build. * * @property majorVersion * @type String */ majorVersion: '4', /** * Minor version of TinyMCE build. * * @property minorVersion * @type String */ minorVersion: '6.7', /** * Release date of TinyMCE build. * * @property releaseDate * @type String */ releaseDate: '2017-09-18', /** * Collection of editor instances. Deprecated use tinymce.get() instead. * * @property editors * @type Object */ editors: legacyEditors, /** * 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, settings: {}, 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 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 (Env.ie && Env.ie < 11) { ErrorReporter.initError( 'TinyMCE does not support the browser you are using. For a list of supported' + ' browsers please see: https://www.tinymce.com/docs/get-started/system-requirements/' ); return []; } 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 = DomQuery.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); }); if (targets.length === 0) { provideResults([]); } else { each(targets, function (elm) { if (isInvalidInlineTarget(settings, elm)) { ErrorReporter.initError('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/Array} Editor instance to return or array of editor instances. * @example * // Adds an onclick event to an editor by id * tinymce.get('mytextbox').on('click', function(e) { * ed.windowManager.alert('Hello world!'); * }); * * // Adds an onclick event to an editor by index * tinymce.get(0).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 === 0) { return editors.slice(0); } else if (Type.isString(id)) { return Arr.find(editors, function (editor) { return editor.id === id; }).getOr(null); } else if (Type.isNumber(id)) { return editors[id] ? editors[id] : null; } else { return 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, existingEditor; // Prevent existing editors from beeing added again this could happen // if a user calls createEditor then render or add multiple times. existingEditor = legacyEditors[editor.id]; if (existingEditor === editor) { return editor; } if (self.get(editor.id) === null) { // Add to legacy editors array, this is what breaks in HTML5 where ID:s with numbers are valid // We can't get rid of this strange object and array at the same time since it seems to be used all over the web if (isValidLegacyKey(editor.id)) { legacyEditors[editor.id] = editor; } legacyEditors.push(editor); editors.push(editor); } toggleGlobalEvents(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, editor; // Remove all editors if (!selector) { for (i = editors.length - 1; i >= 0; i--) { self.remove(editors[i]); } return; } // Remove editors by selector if (Type.isString(selector)) { selector = selector.selector || selector; each(DOM.select(selector), function (elm) { editor = self.get(elm.id); if (editor) { self.remove(editor); } }); return; } // Remove specific editor editor = selector; // Not in the collection if (Type.isNull(self.get(editor.id))) { return null; } if (removeEditorFromList(editor)) { self.fire('RemoveEditor', { editor: editor }); } if (editors.length === 0) { DOM.unbind(window, 'beforeunload', beforeUnloadDelegate); } editor.remove(); toggleGlobalEvents(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(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(); LegacyInput.register(EditorManager); return EditorManager; } ); /** * Rect.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.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 }; } ); /** * Layout.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.Layout', [ "tinymce.core.util.Class", "tinymce.core.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; } }); } ); /** * AbsoluteLayout.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.AbsoluteLayout', [ "tinymce.core.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 '
    ' + this._super(container); } }); } ); /** * Button.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.Button', [ "tinymce.core.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 = '' + self.encode(text) + ''; } icon = icon ? prefix + 'ico ' + prefix + 'i-' + icon : ''; return ( '
    ' + '' + '
    ' ); }, 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 = $('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(); } }); } ); /** * ButtonGroup.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.ButtonGroup', [ "tinymce.core.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 ( '
    ' + '
    ' + (self.settings.html || '') + layout.renderHtml(self) + '
    ' + '
    ' ); } }); } ); /** * Checkbox.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.Checkbox', [ "tinymce.core.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 ( '
    ' + '' + '' + self.encode(self.state.get('text')) + '' + '
    ' ); }, 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(); } }); } ); /** * ComboBox.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.ComboBox', [ "tinymce.core.ui.Widget", "tinymce.core.ui.Factory", "tinymce.core.ui.DomUtils", "tinymce.core.dom.DomQuery", "tinymce.core.util.VK", "tinymce.core.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 = ''; 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 = ( '
    ' + '' + '
    ' ); self.classes.add('has-open'); } return ( '
    ' + '' + statusHtml + openBtnHtml + '
    ' ); }, 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(); } }); } ); /** * ColorBox.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.ColorBox', [ "tinymce.core.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(); } }); } ); /** * PanelButton.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.PanelButton', [ "tinymce.core.ui.Button", "tinymce.core.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(); } var rel = self.panel.testMoveRel(self.getEl(), settings.popoverAlign || (self.isRtl() ? ['bc-tc', 'bc-tl', 'bc-tr'] : ['bc-tc', 'bc-tr', 'bc-tl'])); self.panel.classes.toggle('start', rel === 'bc-tl'); self.panel.classes.toggle('end', rel === 'bc-tr'); self.panel.moveRel(self.getEl(), rel); }, /** * 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(); } }); } ); /** * ColorButton.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.ColorButton', [ "tinymce.core.ui.PanelButton", "tinymce.core.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 = '' + self.encode(text) + ''; } return ( '
    ' + '' + '' + '
    ' ); }, /** * 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(); } }); } ); /** * Color.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.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; } ); /** * ColorPicker.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.ColorPicker', [ "tinymce.core.ui.Widget", "tinymce.core.ui.DragHelper", "tinymce.core.ui.DomUtils", "tinymce.core.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 += ( '
    ' ); } return html; } var gradientCssText = ( 'background: -ms-linear-gradient(top,' + stops + ');' + 'background: linear-gradient(to bottom,' + stops + ');' ); hueHtml = ( '
    ' + getOldIeFallbackHtml() + '
    ' + '
    ' ); return ( '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + hueHtml + '
    ' ); } }); } ); /** * Path.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.Path', [ "tinymce.core.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 ( '
    ' + self._getDataPathHtml(self.state.get('row')) + '
    ' ); }, 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 ? '' : '') + '
    ' + parts[i].name + '
    ' ); } if (!html) { html = '
    \u00a0
    '; } return html; } }); } ); /** * ElementPath.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.ElementPath', [ "tinymce.core.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(); } }); } ); /** * FormItem.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.FormItem', [ "tinymce.core.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 ( '
    ' + (self.settings.title ? ('
    ' + self.settings.title + '
    ') : '') + '
    ' + (self.settings.html || '') + layout.renderHtml(self) + '
    ' + '
    ' ); } }); } ); /** * Form.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.Form', [ "tinymce.core.ui.Container", "tinymce.core.ui.FormItem", "tinymce.core.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(); } }); } ); /** * FieldSet.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.FieldSet', [ "tinymce.core.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 ( '
    ' + (self.settings.title ? ('' + self.settings.title + '') : '') + '
    ' + (self.settings.html || '') + layout.renderHtml(self) + '
    ' + '
    ' ); } }); } ); /** * LinkTargets.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.content.LinkTargets', [ 'ephox.sugar.api.node.Element', 'ephox.sugar.api.search.SelectorFilter', 'tinymce.core.dom.DOMUtils', 'tinymce.core.dom.NodeType', 'tinymce.core.util.Arr', 'tinymce.core.util.Fun', 'tinymce.core.util.Tools', 'tinymce.core.util.Uuid' ], function (Element, SelectorFilter, DOMUtils, NodeType, Arr, Fun, Tools, Uuid) { 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 Arr.map(SelectorFilter.descendants(Element.fromDom(root), selector), function (element) { return element.dom(); }); }; 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 }; } ); /** * FilePicker.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This class creates a file picker control. * * @class tinymce.ui.FilePicker * @extends tinymce.ui.ComboBox */ define( 'tinymce.core.ui.FilePicker', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'global!window', 'tinymce.core.content.LinkTargets', 'tinymce.core.EditorManager', 'tinymce.core.ui.ComboBox', 'tinymce.core.util.Tools' ], function (Arr, Fun, window, LinkTargets, EditorManager, ComboBox, Tools) { "use strict"; var getActiveEditor = function () { return window.tinymce ? window.tinymce.activeEditor : EditorManager.activeEditor; }; 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.exists(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 historyItems = history.hasOwnProperty(fileType) ? history[fileType] : [ ]; var uniqueHistory = Arr.filter(historyItems, 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('', topAnchor)); } if (bottomAchor !== null) { anchorMenuItems.push(staticMenuItem('', bottomAchor)); } return anchorMenuItems; }; var join = function (items) { return Arr.foldl(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 = getActiveEditor(), 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); } }); } ); /** * FitLayout.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.FitLayout', [ "tinymce.core.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(); } }); } }); } ); /** * FlexLayout.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.FlexLayout', [ "tinymce.core.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; } } }); } ); /** * FlowLayout.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.FlowLayout', [ "tinymce.core.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; } }); } ); /** * FontInfo.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * Internal class for computing font size for elements. * * @private * @class tinymce.fmt.FontInfo */ define( 'tinymce.core.fmt.FontInfo', [ 'ephox.katamari.api.Fun', 'ephox.katamari.api.Option', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.node.Node', 'tinymce.core.dom.DOMUtils' ], function (Fun, Option, Element, Node, DOMUtils) { var getSpecifiedFontProp = function (propName, rootElm, elm) { while (elm !== rootElm) { if (elm.style[propName]) { var foundStyle = elm.style[propName]; return foundStyle !== '' ? Option.some(foundStyle) : Option.none(); } elm = elm.parentNode; } return Option.none(); }; var toPt = function (fontSize) { if (/[0-9.]+px$/.test(fontSize)) { return Math.round(parseInt(fontSize, 10) * 72 / 96) + 'pt'; } return fontSize; }; var normalizeFontFamily = function (fontFamily) { // 'Font name', Font -> Font name,Font return fontFamily.replace(/[\'\"]/g, '').replace(/,\s+/g, ','); }; var getComputedFontProp = function (propName, elm) { return Option.from(DOMUtils.DOM.getStyle(elm, propName, true)); }; var getFontProp = function (propName) { return function (rootElm, elm) { return Option.from(elm) .map(Element.fromDom) .filter(Node.isElement) .bind(function (element) { return getSpecifiedFontProp(propName, rootElm, element.dom()) .or(getComputedFontProp(propName, element.dom())); }) .getOr(''); }; }; return { getFontSize: getFontProp('fontSize'), getFontFamily: Fun.compose(normalizeFontFamily, getFontProp('fontFamily')), toPt: toPt }; } ); /** * FormatControls.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.FormatControls', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.search.SelectorFind', 'tinymce.core.dom.DOMUtils', 'tinymce.core.EditorManager', 'tinymce.core.Env', 'tinymce.core.fmt.FontInfo', 'tinymce.core.ui.Control', 'tinymce.core.ui.FloatPanel', 'tinymce.core.ui.Widget', 'tinymce.core.util.Tools' ], function (Arr, Fun, Element, SelectorFind, DOMUtils, EditorManager, Env, FontInfo, Control, FloatPanel, Widget, Tools) { var each = Tools.each; var flatten = function (ar) { return Arr.foldl(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 = SelectorFind.descendant(Element.fromDom(document.body), editor.settings.ui_container).fold(Fun.constant(null), function (elm) { return elm.dom(); }); } } 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 createFontNameListBoxChangeHandler(items) { return function () { var self = this; var getFirstFont = function (fontFamily) { return fontFamily ? fontFamily.split(',')[0] : ''; }; editor.on('init nodeChange', function (e) { var fontFamily, value = null; fontFamily = FontInfo.getFontFamily(editor.getBody(), e.element); each(items, function (item) { if (item.value.toLowerCase() === fontFamily.toLowerCase()) { value = item.value; } }); each(items, function (item) { if (!value && getFirstFont(item.value).toLowerCase() === getFirstFont(fontFamily).toLowerCase()) { value = item.value; } }); self.value(value); if (!value && fontFamily) { self.text(getFirstFont(fontFamily)); } }); }; } function createFontSizeListBoxChangeHandler(items) { return function () { var self = this; editor.on('init nodeChange', function (e) { var px, pt, value = null; px = FontInfo.getFontSize(editor.getBody(), e.element); pt = FontInfo.toPt(px); each(items, function (item) { if (item.value === px) { value = px; } else if (item.value === pt) { value = pt; } }); self.value(value); if (!value) { self.text(pt); } }); }; } 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 : 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 :[,] 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; function checkState() { var typeFn = type == 'redo' ? 'hasRedo' : 'hasUndo'; return editor.undoManager ? editor.undoManager[typeFn]() : 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', 'Meta+U'], 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: createFontNameListBoxChangeHandler(items), 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 fontsizeFormats = editor.settings.fontsize_formats || defaultFontsizeFormats; each(fontsizeFormats.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: createFontSizeListBoxChangeHandler(items), onclick: function (e) { if (e.control.settings.value) { editor.execCommand('FontSize', false, e.control.settings.value); } } }; }); editor.addMenuItem('formats', { text: 'Formats', menu: formatMenu }); } return {}; } ); /** * GridLayout.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.GridLayout', [ "tinymce.core.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; } } }); } ); /** * Iframe.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.Iframe', [ "tinymce.core.ui.Widget", "tinymce.core.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 ( '' ); }, /** * 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; } }); } ); /** * InfoBox.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.InfoBox', [ "tinymce.core.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 ( '
    ' + '
    ' + self.encode(self.state.get('text')) + '' + '
    ' + '
    ' ); }, 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(); } }); } ); /** * Label.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.Label', [ "tinymce.core.ui.Widget", "tinymce.core.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; var text = self.settings.html ? self.settings.html : self.encode(self.state.get('text')); if (!forId && (forName = self.settings.forName)) { targetCtrl = self.getRoot().find('#' + forName)[0]; if (targetCtrl) { forId = targetCtrl._id; } } if (forId) { return ( '' ); } return ( '' + text + '' ); }, 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(); } }); } ); /** * Toolbar.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.Toolbar', [ "tinymce.core.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(); } }); } ); /** * MenuBar.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.MenuBar', [ "tinymce.core.ui.Toolbar" ], function (Toolbar) { "use strict"; return Toolbar.extend({ Defaults: { role: 'menubar', containerCls: 'menubar', ariaRoot: true, defaults: { type: 'menubutton' } } }); } ); /** * MenuButton.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.MenuButton', [ "tinymce.core.ui.Button", "tinymce.core.ui.Factory", "tinymce.core.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 (toggle) { var self = this, menu; if (self.menu && self.menu.visible() && toggle !== false) { 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 = '' + self.encode(text) + ''; } icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : ''; self.aria('role', self.parent() instanceof MenuBar ? 'menuitem' : 'button'); return ( '
    ' + '' + '
    ' ); }, /** * 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.focus(); self.showMenu(!e.aria); 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; } ); /** * MenuItem.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.MenuItem', [ "tinymce.core.ui.Widget", "tinymce.core.ui.Factory", "tinymce.core.Env", "tinymce.core.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: '⌥', ctrl: '⌘', shift: '⇧', meta: '⌘' }; } 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'), ''). replace(new RegExp(escapeRegExp(']mce~match!'), 'g'), ''); } 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 !== '-' ? '\u00a0' : ''); text = boldMatches(self.encode(markMatches(text))); url = boldMatches(self.encode(markMatches(url))); return ( '
    ' + iconHtml + (text !== '-' ? '' + text + '' : '') + (shortcut ? '
    ' + shortcut + '
    ' : '') + (settings.menu ? '
    ' : '') + (url ? '' : '') + '
    ' ); }, /** * 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(); } } }); } ); /** * Throbber.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.Throbber', [ "tinymce.core.dom.DomQuery", "tinymce.core.ui.Control", "tinymce.core.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( '
    ' ); 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; }; }; } ); /** * Menu.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.Menu', [ "tinymce.core.ui.FloatPanel", "tinymce.core.ui.MenuItem", "tinymce.core.ui.Throbber", "tinymce.core.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(); } }); } ); /** * ListBox.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.ListBox', [ "tinymce.core.ui.MenuButton", "tinymce.core.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(); } }); } ); /** * Radio.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.Radio', [ "tinymce.core.ui.Checkbox" ], function (Checkbox) { "use strict"; return Checkbox.extend({ Defaults: { classes: "radio", role: "radio" } }); } ); /** * ResizeHandle.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.ResizeHandle', [ "tinymce.core.ui.Widget", "tinymce.core.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 ( '
    ' + '' + '
    ' ); }, /** * 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(); } }); } ); /** * SelectBox.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.SelectBox', [ "tinymce.core.ui.Widget" ], function (Widget) { "use strict"; function createOptions(options) { var strOptions = ''; if (options) { for (var i = 0; i < options.length; i++) { strOptions += ''; } } 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 ( '' ); }, bindStates: function () { var self = this; self.state.on('change:options', function (e) { self.getEl().innerHTML = createOptions(e.value); }); return self._super(); } }); } ); /** * Slider.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.Slider', [ "tinymce.core.ui.Widget", "tinymce.core.ui.DragHelper", "tinymce.core.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 ( '
    ' + '
    ' + '
    ' ); }, 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(); } }); } ); /** * Spacer.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.Spacer', [ "tinymce.core.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 '
    '; } }); } ); /** * SplitButton.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.SplitButton', [ "tinymce.core.ui.MenuButton", "tinymce.core.ui.DomUtils", "tinymce.core.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 = '' + self.encode(text) + ''; } return ( '
    ' + '' + '' + '
    ' ); }, /** * 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(); } }); } ); /** * StackLayout.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.StackLayout', [ "tinymce.core.ui.FlowLayout" ], function (FlowLayout) { "use strict"; return FlowLayout.extend({ Defaults: { containerClass: 'stack-layout', controlClass: 'stack-layout-item', endClass: 'break' }, isNative: function () { return true; } }); } ); /** * TabPanel.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.TabPanel', [ "tinymce.core.ui.Panel", "tinymce.core.dom.DomQuery", "tinymce.core.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 += ( '' ); }); return ( '
    ' + '
    ' + tabsHtml + '
    ' + '
    ' + layout.renderHtml(self) + '
    ' + '
    ' ); }, /** * 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; } }); } ); /** * TextBox.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.ui.TextBox', [ "tinymce.core.ui.Widget", "tinymce.core.util.Tools", "tinymce.core.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(); } }); } ); defineGlobal("global!RegExp", RegExp); /** * DropZone.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * Creates a new dropzone. * * @-x-less DropZone.less * @class tinymce.ui.DropZone * @extends tinymce.ui.Widget */ define( 'tinymce.core.ui.DropZone', [ 'tinymce.core.ui.Widget', 'tinymce.core.util.Tools', 'tinymce.core.ui.DomUtils', 'global!RegExp' ], function (Widget, Tools, DomUtils, RegExp) { return Widget.extend({ /** * Constructs a instance with the specified settings. * * @constructor * @param {Object} settings Name/value object with settings. * @setting {Boolean} multiple True if the dropzone is a multiple control. * @setting {Number} maxLength Max length for the dropzone. * @setting {Number} size Size of the dropzone in characters. */ init: function (settings) { var self = this; settings = Tools.extend({ height: 100, text: "Drop an image here", multiple: false, accept: null // by default accept any files }, settings); self._super(settings); self.classes.add('dropzone'); if (settings.multiple) { self.classes.add('multiple'); } }, /** * Renders the control as a HTML string. * * @method renderHtml * @return {String} HTML representing the control. */ renderHtml: function () { var self = this, attrs, elm; var cfg = self.settings; attrs = { id: self._id, hidefocus: '1' }; elm = DomUtils.create('div', attrs, '' + this.translate(cfg.text) + ''); if (cfg.height) { DomUtils.css(elm, 'height', cfg.height + 'px'); } if (cfg.width) { DomUtils.css(elm, 'width', cfg.width + 'px'); } elm.className = self.classes; return elm.outerHTML; }, /** * Called after the control has been rendered. * * @method postRender */ postRender: function () { var self = this; var toggleDragClass = function (e) { e.preventDefault(); self.classes.toggle('dragenter'); self.getEl().className = self.classes; }; var filter = function (files) { var accept = self.settings.accept; if (typeof accept !== 'string') { return files; } var re = new RegExp('(' + accept.split(/\s*,\s*/).join('|') + ')$', 'i'); return Tools.grep(files, function (file) { return re.test(file.name); }); }; self._super(); self.$el.on('dragover', function (e) { e.preventDefault(); }); self.$el.on('dragenter', toggleDragClass); self.$el.on('dragleave', toggleDragClass); self.$el.on('drop', function (e) { e.preventDefault(); if (self.state.get('disabled')) { return; } var files = filter(e.dataTransfer.files); self.value = function () { if (!files.length) { return null; } else if (self.settings.multiple) { return files; } else { return files[0]; } }; if (files.length) { self.fire('change', e); } }); }, remove: function () { this.$el.off(); this._super(); } }); } ); /** * BrowseButton.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * Creates a new browse button. * * @-x-less BrowseButton.less * @class tinymce.ui.BrowseButton * @extends tinymce.ui.Widget */ define( 'tinymce.core.ui.BrowseButton', [ 'tinymce.core.ui.Button', 'tinymce.core.util.Tools', 'tinymce.core.ui.DomUtils', 'tinymce.core.dom.DomQuery', 'global!RegExp' ], function (Button, Tools, DomUtils, $, RegExp) { return Button.extend({ /** * Constructs a instance with the specified settings. * * @constructor * @param {Object} settings Name/value object with settings. * @setting {Boolean} multiple True if the dropzone is a multiple control. * @setting {Number} maxLength Max length for the dropzone. * @setting {Number} size Size of the dropzone in characters. */ init: function (settings) { var self = this; settings = Tools.extend({ text: "Browse...", multiple: false, accept: null // by default accept any files }, settings); self._super(settings); self.classes.add('browsebutton'); if (settings.multiple) { self.classes.add('multiple'); } }, /** * Called after the control has been rendered. * * @method postRender */ postRender: function () { var self = this; var input = DomUtils.create('input', { type: 'file', id: self._id + '-browse', accept: self.settings.accept }); self._super(); $(input).on('change', function (e) { var files = e.target.files; self.value = function () { if (!files.length) { return null; } else if (self.settings.multiple) { return files; } else { return files[0]; } }; e.preventDefault(); if (files.length) { self.fire('change', e); } }); // ui.Button prevents default on click, so we shouldn't let the click to propagate up to it $(input).on('click', function (e) { e.stopPropagation(); }); $(self.getEl('button')).on('click', function (e) { e.stopPropagation(); input.click(); }); // in newer browsers input doesn't have to be attached to dom to trigger browser dialog // however older IE11 (< 11.1358.14393.0) still requires this self.getEl().appendChild(input); }, remove: function () { $(this.getEl('button')).off(); $(this.getEl('input')).off(); this._super(); } }); } ); /** * Api.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.ui.Api', [ 'tinymce.core.ui.Selector', 'tinymce.core.ui.Collection', 'tinymce.core.ui.ReflowQueue', 'tinymce.core.ui.Control', 'tinymce.core.ui.Factory', 'tinymce.core.ui.KeyboardNavigation', 'tinymce.core.ui.Container', 'tinymce.core.ui.DragHelper', 'tinymce.core.ui.Scrollable', 'tinymce.core.ui.Panel', 'tinymce.core.ui.Movable', 'tinymce.core.ui.Resizable', 'tinymce.core.ui.FloatPanel', 'tinymce.core.ui.Window', 'tinymce.core.ui.MessageBox', 'tinymce.core.ui.Tooltip', 'tinymce.core.ui.Widget', 'tinymce.core.ui.Progress', 'tinymce.core.ui.Notification', 'tinymce.core.ui.Layout', 'tinymce.core.ui.AbsoluteLayout', 'tinymce.core.ui.Button', 'tinymce.core.ui.ButtonGroup', 'tinymce.core.ui.Checkbox', 'tinymce.core.ui.ComboBox', 'tinymce.core.ui.ColorBox', 'tinymce.core.ui.PanelButton', 'tinymce.core.ui.ColorButton', 'tinymce.core.ui.ColorPicker', 'tinymce.core.ui.Path', 'tinymce.core.ui.ElementPath', 'tinymce.core.ui.FormItem', 'tinymce.core.ui.Form', 'tinymce.core.ui.FieldSet', 'tinymce.core.ui.FilePicker', 'tinymce.core.ui.FitLayout', 'tinymce.core.ui.FlexLayout', 'tinymce.core.ui.FlowLayout', 'tinymce.core.ui.FormatControls', 'tinymce.core.ui.GridLayout', 'tinymce.core.ui.Iframe', 'tinymce.core.ui.InfoBox', 'tinymce.core.ui.Label', 'tinymce.core.ui.Toolbar', 'tinymce.core.ui.MenuBar', 'tinymce.core.ui.MenuButton', 'tinymce.core.ui.MenuItem', 'tinymce.core.ui.Throbber', 'tinymce.core.ui.Menu', 'tinymce.core.ui.ListBox', 'tinymce.core.ui.Radio', 'tinymce.core.ui.ResizeHandle', 'tinymce.core.ui.SelectBox', 'tinymce.core.ui.Slider', 'tinymce.core.ui.Spacer', 'tinymce.core.ui.SplitButton', 'tinymce.core.ui.StackLayout', 'tinymce.core.ui.TabPanel', 'tinymce.core.ui.TextBox', 'tinymce.core.ui.DropZone', 'tinymce.core.ui.BrowseButton' ], function ( Selector, Collection, ReflowQueue, Control, Factory, KeyboardNavigation, Container, DragHelper, Scrollable, Panel, Movable, Resizable, FloatPanel, Window, MessageBox, Tooltip, Widget, Progress, Notification, Layout, AbsoluteLayout, Button, ButtonGroup, Checkbox, ComboBox, ColorBox, PanelButton, ColorButton, ColorPicker, Path, ElementPath, FormItem, Form, FieldSet, FilePicker, FitLayout, FlexLayout, FlowLayout, FormatControls, GridLayout, Iframe, InfoBox, Label, Toolbar, MenuBar, MenuButton, MenuItem, Throbber, Menu, ListBox, Radio, ResizeHandle, SelectBox, Slider, Spacer, SplitButton, StackLayout, TabPanel, TextBox, DropZone, BrowseButton ) { "use strict"; var registerToFactory = function (id, ref) { Factory.add(id.split('.').pop(), ref); }; var expose = function (target, id, ref) { var i, fragments; fragments = id.split(/[.\/]/); for (i = 0; i < fragments.length - 1; ++i) { if (target[fragments[i]] === undefined) { target[fragments[i]] = {}; } target = target[fragments[i]]; } target[fragments[fragments.length - 1]] = ref; registerToFactory(id, ref); }; var appendTo = function (target) { expose(target, 'ui.Selector', Selector); expose(target, 'ui.Collection', Collection); expose(target, 'ui.ReflowQueue', ReflowQueue); expose(target, 'ui.Control', Control); expose(target, 'ui.Factory', Factory); expose(target, 'ui.KeyboardNavigation', KeyboardNavigation); expose(target, 'ui.Container', Container); expose(target, 'ui.DragHelper', DragHelper); expose(target, 'ui.Scrollable', Scrollable); expose(target, 'ui.Panel', Panel); expose(target, 'ui.Movable', Movable); expose(target, 'ui.Resizable', Resizable); expose(target, 'ui.FloatPanel', FloatPanel); expose(target, 'ui.Window', Window); expose(target, 'ui.MessageBox', MessageBox); expose(target, 'ui.Tooltip', Tooltip); expose(target, 'ui.Widget', Widget); expose(target, 'ui.Progress', Progress); expose(target, 'ui.Notification', Notification); expose(target, 'ui.Layout', Layout); expose(target, 'ui.AbsoluteLayout', AbsoluteLayout); expose(target, 'ui.Button', Button); expose(target, 'ui.ButtonGroup', ButtonGroup); expose(target, 'ui.Checkbox', Checkbox); expose(target, 'ui.ComboBox', ComboBox); expose(target, 'ui.ColorBox', ColorBox); expose(target, 'ui.PanelButton', PanelButton); expose(target, 'ui.ColorButton', ColorButton); expose(target, 'ui.ColorPicker', ColorPicker); expose(target, 'ui.Path', Path); expose(target, 'ui.ElementPath', ElementPath); expose(target, 'ui.FormItem', FormItem); expose(target, 'ui.Form', Form); expose(target, 'ui.FieldSet', FieldSet); expose(target, 'ui.FilePicker', FilePicker); expose(target, 'ui.FitLayout', FitLayout); expose(target, 'ui.FlexLayout', FlexLayout); expose(target, 'ui.FlowLayout', FlowLayout); expose(target, 'ui.FormatControls', FormatControls); expose(target, 'ui.GridLayout', GridLayout); expose(target, 'ui.Iframe', Iframe); expose(target, 'ui.InfoBox', InfoBox); expose(target, 'ui.Label', Label); expose(target, 'ui.Toolbar', Toolbar); expose(target, 'ui.MenuBar', MenuBar); expose(target, 'ui.MenuButton', MenuButton); expose(target, 'ui.MenuItem', MenuItem); expose(target, 'ui.Throbber', Throbber); expose(target, 'ui.Menu', Menu); expose(target, 'ui.ListBox', ListBox); expose(target, 'ui.Radio', Radio); expose(target, 'ui.ResizeHandle', ResizeHandle); expose(target, 'ui.SelectBox', SelectBox); expose(target, 'ui.Slider', Slider); expose(target, 'ui.Spacer', Spacer); expose(target, 'ui.SplitButton', SplitButton); expose(target, 'ui.StackLayout', StackLayout); expose(target, 'ui.TabPanel', TabPanel); expose(target, 'ui.TextBox', TextBox); expose(target, 'ui.DropZone', DropZone); expose(target, 'ui.BrowseButton', BrowseButton); expose(target, 'ui.Api', Api); }; var Api = { appendTo: appendTo }; return Api; } ); /** * JSON.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.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 } } /**#@-*/ }; } ); /** * JSONP.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.util.JSONP', [ "tinymce.core.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++; } }; } ); /** * XHR.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.util.XHR', [ "tinymce.core.util.Observable", "tinymce.core.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; } ); /** * JSONRequest.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.util.JSONRequest', [ "tinymce.core.util.JSON", "tinymce.core.util.XHR", "tinymce.core.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; } ); /** * LocalStorage.js * * Released under LGPL License. * Copyright (c) 1999-2017 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: * ,,,,... * * 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.core.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; } ); /** * Tinymce.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.api.Tinymce', [ 'tinymce.core.AddOnManager', 'tinymce.core.api.Formatter', 'tinymce.core.api.NotificationManager', 'tinymce.core.api.WindowManager', 'tinymce.core.dom.BookmarkManager', 'tinymce.core.dom.ControlSelection', 'tinymce.core.dom.DomQuery', 'tinymce.core.dom.DOMUtils', 'tinymce.core.dom.EventUtils', 'tinymce.core.dom.RangeUtils', 'tinymce.core.dom.ScriptLoader', 'tinymce.core.dom.Selection', 'tinymce.core.dom.Serializer', 'tinymce.core.dom.Sizzle', 'tinymce.core.dom.TreeWalker', 'tinymce.core.Editor', 'tinymce.core.EditorCommands', 'tinymce.core.EditorManager', 'tinymce.core.EditorObservable', 'tinymce.core.Env', 'tinymce.core.FocusManager', 'tinymce.core.geom.Rect', 'tinymce.core.html.DomParser', 'tinymce.core.html.Entities', 'tinymce.core.html.Node', 'tinymce.core.html.SaxParser', 'tinymce.core.html.Schema', 'tinymce.core.html.Serializer', 'tinymce.core.html.Styles', 'tinymce.core.html.Writer', 'tinymce.core.Shortcuts', 'tinymce.core.ui.Api', 'tinymce.core.UndoManager', 'tinymce.core.util.Class', 'tinymce.core.util.Color', 'tinymce.core.util.Delay', 'tinymce.core.util.EventDispatcher', 'tinymce.core.util.I18n', 'tinymce.core.util.JSON', 'tinymce.core.util.JSONP', 'tinymce.core.util.JSONRequest', 'tinymce.core.util.LocalStorage', 'tinymce.core.util.Observable', 'tinymce.core.util.Promise', 'tinymce.core.util.Tools', 'tinymce.core.util.URI', 'tinymce.core.util.VK', 'tinymce.core.util.XHR' ], function ( AddOnManager, Formatter, NotificationManager, WindowManager, BookmarkManager, ControlSelection, DomQuery, DOMUtils, EventUtils, RangeUtils, ScriptLoader, Selection, DomSerializer, Sizzle, TreeWalker, Editor, EditorCommands, EditorManager, EditorObservable, Env, FocusManager, Rect, DomParser, Entities, Node, SaxParser, Schema, HtmlSerializer, Styles, Writer, Shortcuts, UiApi, UndoManager, Class, Color, Delay, EventDispatcher, I18n, JSON, JSONP, JSONRequest, LocalStorage, Observable, Promise, Tools, URI, VK, XHR ) { var tinymce = EditorManager; /** * @include ../../../../../../tools/docs/tinymce.js */ var publicApi = { geom: { Rect: Rect }, util: { Promise: Promise, Delay: Delay, Tools: Tools, VK: VK, URI: URI, Class: Class, EventDispatcher: EventDispatcher, Observable: Observable, I18n: I18n, XHR: XHR, JSON: JSON, JSONRequest: JSONRequest, JSONP: JSONP, LocalStorage: LocalStorage, Color: Color }, dom: { EventUtils: EventUtils, Sizzle: Sizzle, DomQuery: DomQuery, TreeWalker: TreeWalker, DOMUtils: DOMUtils, ScriptLoader: ScriptLoader, RangeUtils: RangeUtils, Serializer: DomSerializer, ControlSelection: ControlSelection, BookmarkManager: BookmarkManager, Selection: Selection, Event: EventUtils.Event }, html: { Styles: Styles, Entities: Entities, Node: Node, Schema: Schema, SaxParser: SaxParser, DomParser: DomParser, Writer: Writer, Serializer: HtmlSerializer }, Env: Env, AddOnManager: AddOnManager, Formatter: Formatter, UndoManager: UndoManager, EditorCommands: EditorCommands, WindowManager: WindowManager, NotificationManager: NotificationManager, EditorObservable: EditorObservable, Shortcuts: Shortcuts, Editor: Editor, FocusManager: FocusManager, EditorManager: EditorManager, // Global instances DOM: DOMUtils.DOM, ScriptLoader: ScriptLoader.ScriptLoader, PluginManager: AddOnManager.PluginManager, ThemeManager: AddOnManager.ThemeManager, // Global utility functions trim: Tools.trim, isArray: Tools.isArray, is: Tools.is, toArray: Tools.toArray, makeMap: Tools.makeMap, each: Tools.each, map: Tools.map, grep: Tools.grep, inArray: Tools.inArray, extend: Tools.extend, create: Tools.create, walk: Tools.walk, createNS: Tools.createNS, resolve: Tools.resolve, explode: Tools.explode, _addCacheSuffix: Tools._addCacheSuffix, // Legacy browser detection isOpera: Env.opera, isWebKit: Env.webkit, isIE: Env.ie, isGecko: Env.gecko, isMac: Env.mac }; tinymce = Tools.extend(tinymce, publicApi); UiApi.appendTo(tinymce); return tinymce; } ); /** * Main.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.api.Main', [ 'ephox.katamari.api.Fun', 'tinymce.core.api.Tinymce' ], function (Fun, Tinymce) { /*eslint consistent-this: 0 */ var context = this || window; var exportToModuleLoaders = function (tinymce) { // Bolt if (typeof context.define === "function" && !context.define.amd) { context.define("ephox/tinymce", [], Fun.constant(tinymce)); context.define("tinymce.core.EditorManager", [], Fun.constant(tinymce)); } // CommonJS if (typeof module === 'object') { /* global module */ module.exports = tinymce; } }; var exportToWindowGlobal = function (tinymce) { window.tinymce = tinymce; window.tinyMCE = tinymce; }; return function () { exportToWindowGlobal(Tinymce); exportToModuleLoaders(Tinymce); return Tinymce; }; } ); dem('tinymce.core.api.Main')(); })(); !function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i0&&(c=i().get(d[0].id)))return c.getContent()}function d(a){var b=null;return a&&a.id&&g.tinymce&&(b=i().get(a.id)),b}function e(a){return!!(a&&a.length&&g.tinymce&&a.is(":tinymce"))}var h={};f.each(["text","html","val"],function(a,g){var i=h[g]=f.fn[g],j="text"===g;f.fn[g]=function(a){var g=this;if(!e(g))return i.apply(g,arguments);if(a!==c)return b.call(g.filter(":tinymce"),a),i.apply(g.not(":tinymce"),arguments),g;var h="",k=arguments;return(j?g:g.eq(0)).each(function(a,b){var c=d(b);h+=c?j?c.getContent().replace(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g,""):c.getContent({save:!0}):i.apply(f(b),k)}),h}}),f.each(["append","prepend"],function(a,b){var g=h[b]=f.fn[b],i="prepend"===b;f.fn[b]=function(a){var b=this;return e(b)?a!==c?("string"==typeof a&&b.filter(":tinymce").each(function(b,c){var e=d(c);e&&e.setContent(i?a+e.getContent():e.getContent()+a)}),g.apply(b.not(":tinymce"),arguments),b):void 0:g.apply(b,arguments)}}),f.each(["remove","replaceWith","replaceAll","empty"],function(b,c){var d=h[c]=f.fn[c];f.fn[c]=function(){return a.call(this,c),d.apply(this,arguments)}}),h.attr=f.fn.attr,f.fn.attr=function(a,g){var i=this,j=arguments;if(!a||"value"!==a||!e(i))return g!==c?h.attr.apply(i,j):h.attr.apply(i,j);if(g!==c)return b.call(i.filter(":tinymce"),g),h.attr.apply(i.not(":tinymce"),j),i;var k=i[0],l=d(k);return l?l.getContent({save:!0}):h.attr.apply(f(k),j)}}var c,d,e,f,g,h=[];g=a?a:window,f=g.jQuery;var i=function(){return g.tinymce};f.fn.tinymce=function(a){function c(){var c=[],d=0;e||(b(),e=!0),m.each(function(b,e){var f,g=e.id,h=a.oninit;g||(e.id=g=i().DOM.uniqueId()),i().get(g)||(f=i().createEditor(g,a),c.push(f),f.on("init",function(){var a,b=h;m.css("visibility",""),h&&++d==c.length&&("string"==typeof b&&(a=b.indexOf(".")===-1?null:i().resolve(b.replace(/\.\w+$/,"")),b=i().resolve(b)),b.apply(a||i(),c))}))}),f.each(c,function(a,b){b.render()})}var j,k,l,m=this,n="";if(!m.length)return m;if(!a)return i()?i().get(m[0].id):null;if(m.css("visibility","hidden"),g.tinymce||d||!(j=a.script_url))1===d?h.push(c):c();else{d=1,k=j.substring(0,j.lastIndexOf("/")),j.indexOf(".min")!=-1&&(n=".min"),g.tinymce=g.tinyMCEPreInit||{base:k,suffix:n},j.indexOf("gzip")!=-1&&(l=a.language||"en",j=j+(/\?/.test(j)?"&":"?")+"js=true&core=true&suffix="+escape(n)+"&themes="+escape(a.theme||"modern")+"&plugins="+escape(a.plugins||"")+"&languages="+(l||""),g.tinyMCE_GZ||(g.tinyMCE_GZ={start:function(){function b(a){i().ScriptLoader.markDone(i().baseURI.toAbsolute(a))}b("langs/"+l+".js"),b("themes/"+a.theme+"/theme"+n+".js"),b("themes/"+a.theme+"/langs/"+l+".js"),f.each(a.plugins.split(","),function(a,c){c&&(b("plugins/"+c+"/plugin"+n+".js"),b("plugins/"+c+"/langs/"+l+".js"))})},end:function(){}}));var o=document.createElement("script");o.type="text/javascript",o.onload=o.onreadystatechange=function(b){b=b||window.event,2===d||"load"!=b.type&&!/complete|loaded/.test(o.readyState)||(i().dom.Event.domLoaded=1,d=2,a.script_loaded&&a.script_loaded(),c(),f.each(h,function(a,b){b()}))},o.src=j,document.body.appendChild(o)}return m},f.extend(f.expr[":"],{tinymce:function(a){var b;return!!(a.id&&"tinymce"in g&&(b=i().get(a.id),b&&b.editorManager===i()))}})}}),d("0")()}(); /*! * 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 * 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); } } // '].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 = ''; 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 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 = ''; //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'; 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>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 element, if it exists: docElement.className = docElement.className.replace(/(^|\s)no-js(\s|$)/, '$1$2') + // Add the new classes to the element. (enableClasses ? ' js ' + classes.join(' ') : ''); /*>>cssclasses*/ return Modernizr; })(this, this.document); /* @preserve * Leaflet 1.2.0, a JS library for interactive maps. http://leafletjs.com * (c) 2010-2017 Vladimir Agafonkin, (c) 2010-2011 CloudMade */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (factory((global.L = {}))); }(this, (function (exports) { 'use strict'; var version = "1.2.0"; /* * @namespace Util * * Various utility functions, used by Leaflet internally. */ var freeze = Object.freeze; Object.freeze = function (obj) { return obj; }; // @function extend(dest: Object, src?: Object): Object // Merges the properties of the `src` object (or multiple objects) into `dest` object and returns the latter. Has an `L.extend` shortcut. function extend(dest) { var i, j, len, src; for (j = 1, len = arguments.length; j < len; j++) { src = arguments[j]; for (i in src) { dest[i] = src[i]; } } return dest; } // @function create(proto: Object, properties?: Object): Object // Compatibility polyfill for [Object.create](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/create) var create = Object.create || (function () { function F() {} return function (proto) { F.prototype = proto; return new F(); }; })(); // @function bind(fn: Function, …): Function // Returns a new function bound to the arguments passed, like [Function.prototype.bind](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Function/bind). // Has a `L.bind()` shortcut. function bind(fn, obj) { var slice = Array.prototype.slice; if (fn.bind) { return fn.bind.apply(fn, slice.call(arguments, 1)); } var args = slice.call(arguments, 2); return function () { return fn.apply(obj, args.length ? args.concat(slice.call(arguments)) : arguments); }; } // @property lastId: Number // Last unique ID used by [`stamp()`](#util-stamp) var lastId = 0; // @function stamp(obj: Object): Number // Returns the unique ID of an object, assiging it one if it doesn't have it. function stamp(obj) { /*eslint-disable */ obj._leaflet_id = obj._leaflet_id || ++lastId; return obj._leaflet_id; /*eslint-enable */ } // @function throttle(fn: Function, time: Number, context: Object): Function // Returns a function which executes function `fn` with the given scope `context` // (so that the `this` keyword refers to `context` inside `fn`'s code). The function // `fn` will be called no more than one time per given amount of `time`. The arguments // received by the bound function will be any arguments passed when binding the // function, followed by any arguments passed when invoking the bound function. // Has an `L.throttle` shortcut. function throttle(fn, time, context) { var lock, args, wrapperFn, later; later = function () { // reset lock and call if queued lock = false; if (args) { wrapperFn.apply(context, args); args = false; } }; wrapperFn = function () { if (lock) { // called too soon, queue to call later args = arguments; } else { // call and lock until later fn.apply(context, arguments); setTimeout(later, time); lock = true; } }; return wrapperFn; } // @function wrapNum(num: Number, range: Number[], includeMax?: Boolean): Number // Returns the number `num` modulo `range` in such a way so it lies within // `range[0]` and `range[1]`. The returned value will be always smaller than // `range[1]` unless `includeMax` is set to `true`. function wrapNum(x, range, includeMax) { var max = range[1], min = range[0], d = max - min; return x === max && includeMax ? x : ((x - min) % d + d) % d + min; } // @function falseFn(): Function // Returns a function which always returns `false`. function falseFn() { return false; } // @function formatNum(num: Number, digits?: Number): Number // Returns the number `num` rounded to `digits` decimals, or to 5 decimals by default. function formatNum(num, digits) { var pow = Math.pow(10, digits || 5); return Math.round(num * pow) / pow; } // @function trim(str: String): String // Compatibility polyfill for [String.prototype.trim](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/Trim) function trim(str) { return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, ''); } // @function splitWords(str: String): String[] // Trims and splits the string on whitespace and returns the array of parts. function splitWords(str) { return trim(str).split(/\s+/); } // @function setOptions(obj: Object, options: Object): Object // Merges the given properties to the `options` of the `obj` object, returning the resulting options. See `Class options`. Has an `L.setOptions` shortcut. function setOptions(obj, options) { if (!obj.hasOwnProperty('options')) { obj.options = obj.options ? create(obj.options) : {}; } for (var i in options) { obj.options[i] = options[i]; } return obj.options; } // @function getParamString(obj: Object, existingUrl?: String, uppercase?: Boolean): String // Converts an object into a parameter URL string, e.g. `{a: "foo", b: "bar"}` // translates to `'?a=foo&b=bar'`. If `existingUrl` is set, the parameters will // be appended at the end. If `uppercase` is `true`, the parameter names will // be uppercased (e.g. `'?A=foo&B=bar'`) function getParamString(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('&'); } var templateRe = /\{ *([\w_\-]+) *\}/g; // @function template(str: String, data: Object): String // Simple templating facility, accepts a template string of the form `'Hello {a}, {b}'` // and a data object like `{a: 'foo', b: 'bar'}`, returns evaluated string // `('Hello foo, bar')`. You can also specify functions instead of strings for // data values — they will be evaluated passing `data` as an argument. function template(str, data) { return str.replace(templateRe, 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; }); } // @function isArray(obj): Boolean // Compatibility polyfill for [Array.isArray](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray) var isArray = Array.isArray || function (obj) { return (Object.prototype.toString.call(obj) === '[object Array]'); }; // @function indexOf(array: Array, el: Object): Number // Compatibility polyfill for [Array.prototype.indexOf](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf) function indexOf(array, el) { for (var i = 0; i < array.length; i++) { if (array[i] === el) { return i; } } return -1; } // @property emptyImageUrl: String // Data URI string containing a base64-encoded empty GIF image. // Used as a hack to free memory from unused images on WebKit-powered // mobile devices (by setting image `src` to this string). var emptyImageUrl = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; // inspired by http://paulirish.com/2011/requestanimationframe-for-smart-animating/ function getPrefixed(name) { return window['webkit' + name] || window['moz' + name] || window['ms' + name]; } var lastTime = 0; // fallback for IE 7-8 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); }; // @function requestAnimFrame(fn: Function, context?: Object, immediate?: Boolean): Number // Schedules `fn` to be executed when the browser repaints. `fn` is bound to // `context` if given. When `immediate` is set, `fn` is called immediately if // the browser doesn't have native support for // [`window.requestAnimationFrame`](https://developer.mozilla.org/docs/Web/API/window/requestAnimationFrame), // otherwise it's delayed. Returns a request ID that can be used to cancel the request. function requestAnimFrame(fn, context, immediate) { if (immediate && requestFn === timeoutDefer) { fn.call(context); } else { return requestFn.call(window, bind(fn, context)); } } // @function cancelAnimFrame(id: Number): undefined // Cancels a previous `requestAnimFrame`. See also [window.cancelAnimationFrame](https://developer.mozilla.org/docs/Web/API/window/cancelAnimationFrame). function cancelAnimFrame(id) { if (id) { cancelFn.call(window, id); } } var Util = (Object.freeze || Object)({ freeze: freeze, extend: extend, create: create, bind: bind, lastId: lastId, stamp: stamp, throttle: throttle, wrapNum: wrapNum, falseFn: falseFn, formatNum: formatNum, trim: trim, splitWords: splitWords, setOptions: setOptions, getParamString: getParamString, template: template, isArray: isArray, indexOf: indexOf, emptyImageUrl: emptyImageUrl, requestFn: requestFn, cancelFn: cancelFn, requestAnimFrame: requestAnimFrame, cancelAnimFrame: cancelAnimFrame }); // @class Class // @aka L.Class // @section // @uninheritable // Thanks to John Resig and Dean Edwards for inspiration! function Class() {} Class.extend = function (props) { // @function extend(props: Object): Function // [Extends the current class](#class-inheritance) given the properties to be included. // Returns a Javascript function that is a class constructor (to be called with `new`). var NewClass = function () { // call the constructor if (this.initialize) { this.initialize.apply(this, arguments); } // call all constructor hooks this.callInitHooks(); }; var parentProto = NewClass.__super__ = this.prototype; var proto = create(parentProto); proto.constructor = NewClass; NewClass.prototype = proto; // inherit parent's statics for (var i in this) { if (this.hasOwnProperty(i) && i !== 'prototype' && i !== '__super__') { NewClass[i] = this[i]; } } // mix static properties into the class if (props.statics) { extend(NewClass, props.statics); delete props.statics; } // mix includes into the prototype if (props.includes) { checkDeprecatedMixinEvents(props.includes); extend.apply(null, [proto].concat(props.includes)); delete props.includes; } // merge options if (proto.options) { props.options = extend(create(proto.options), props.options); } // mix given properties into the prototype extend(proto, props); proto._initHooks = []; // add method for calling all hooks proto.callInitHooks = function () { if (this._initHooksCalled) { return; } if (parentProto.callInitHooks) { parentProto.callInitHooks.call(this); } this._initHooksCalled = true; for (var i = 0, len = proto._initHooks.length; i < len; i++) { proto._initHooks[i].call(this); } }; return NewClass; }; // @function include(properties: Object): this // [Includes a mixin](#class-includes) into the current class. Class.include = function (props) { extend(this.prototype, props); return this; }; // @function mergeOptions(options: Object): this // [Merges `options`](#class-options) into the defaults of the class. Class.mergeOptions = function (options) { extend(this.prototype.options, options); return this; }; // @function addInitHook(fn: Function): this // Adds a [constructor hook](#class-constructor-hooks) to the class. 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); return this; }; function checkDeprecatedMixinEvents(includes) { if (!L || !L.Mixin) { return; } includes = isArray(includes) ? includes : [includes]; for (var i = 0; i < includes.length; i++) { if (includes[i] === L.Mixin.Events) { console.warn('Deprecated include of L.Mixin.Events: ' + 'this property will be removed in future releases, ' + 'please inherit from L.Evented instead.', new Error().stack); } } } /* * @class Evented * @aka L.Evented * @inherits Class * * A set of methods shared between event-powered classes (like `Map` and `Marker`). Generally, events allow you to execute some function when something happens with an object (e.g. the user clicks on the map, causing the map to fire `'click'` event). * * @example * * ```js * map.on('click', function(e) { * alert(e.latlng); * } ); * ``` * * Leaflet deals with event listeners by reference, so if you want to add a listener and then remove it, define it as a function: * * ```js * function onClick(e) { ... } * * map.on('click', onClick); * map.off('click', onClick); * ``` */ var Events = { /* @method on(type: String, fn: Function, context?: Object): this * Adds a listener function (`fn`) to a particular event type of the object. You can optionally specify the context of the listener (object the this keyword will point to). You can also pass several space-separated types (e.g. `'click dblclick'`). * * @alternative * @method on(eventMap: Object): this * Adds a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}` */ on: function (types, fn, context) { // types can be a map of types/handlers if (typeof types === 'object') { for (var type in types) { // we don't process space-separated events here for performance; // it's a hot path since Layer uses the on(obj) syntax this._on(type, types[type], fn); } } else { // types can be a string of space-separated words types = splitWords(types); for (var i = 0, len = types.length; i < len; i++) { this._on(types[i], fn, context); } } return this; }, /* @method off(type: String, fn?: Function, context?: Object): this * Removes a previously added listener function. If no function is specified, it will remove all the listeners of that particular event from the object. Note that if you passed a custom context to `on`, you must pass the same context to `off` in order to remove the listener. * * @alternative * @method off(eventMap: Object): this * Removes a set of type/listener pairs. * * @alternative * @method off: this * Removes all listeners to all events on the object. */ off: function (types, fn, context) { if (!types) { // clear all listeners if called without arguments delete this._events; } else if (typeof types === 'object') { for (var type in types) { this._off(type, types[type], fn); } } else { types = splitWords(types); for (var i = 0, len = types.length; i < len; i++) { this._off(types[i], fn, context); } } return this; }, // attach listener (without syntactic sugar now) _on: function (type, fn, context) { this._events = this._events || {}; /* get/init listeners for type */ var typeListeners = this._events[type]; if (!typeListeners) { typeListeners = []; this._events[type] = typeListeners; } if (context === this) { // Less memory footprint. context = undefined; } var newListener = {fn: fn, ctx: context}, listeners = typeListeners; // check if fn already there for (var i = 0, len = listeners.length; i < len; i++) { if (listeners[i].fn === fn && listeners[i].ctx === context) { return; } } listeners.push(newListener); }, _off: function (type, fn, context) { var listeners, i, len; if (!this._events) { return; } listeners = this._events[type]; if (!listeners) { return; } if (!fn) { // Set all removed listeners to noop so they are not called if remove happens in fire for (i = 0, len = listeners.length; i < len; i++) { listeners[i].fn = falseFn; } // clear all listeners for a type if function isn't specified delete this._events[type]; return; } if (context === this) { context = undefined; } if (listeners) { // find fn and remove it for (i = 0, len = listeners.length; i < len; i++) { var l = listeners[i]; if (l.ctx !== context) { continue; } if (l.fn === fn) { // set the removed listener to noop so that's not called if remove happens in fire l.fn = falseFn; if (this._firingCount) { /* copy array in case events are being fired */ this._events[type] = listeners = listeners.slice(); } listeners.splice(i, 1); return; } } } }, // @method fire(type: String, data?: Object, propagate?: Boolean): this // Fires an event of the specified type. You can optionally provide an data // object — the first argument of the listener function will contain its // properties. The event can optionally be propagated to event parents. fire: function (type, data, propagate) { if (!this.listens(type, propagate)) { return this; } var event = extend({}, data, {type: type, target: this}); if (this._events) { var listeners = this._events[type]; if (listeners) { this._firingCount = (this._firingCount + 1) || 1; for (var i = 0, len = listeners.length; i < len; i++) { var l = listeners[i]; l.fn.call(l.ctx || this, event); } this._firingCount--; } } if (propagate) { // propagate the event to parents (set with addEventParent) this._propagateEvent(event); } return this; }, // @method listens(type: String): Boolean // Returns `true` if a particular event type has any listeners attached to it. listens: function (type, propagate) { var listeners = this._events && this._events[type]; if (listeners && listeners.length) { return true; } if (propagate) { // also check parents for listeners if event propagates for (var id in this._eventParents) { if (this._eventParents[id].listens(type, propagate)) { return true; } } } return false; }, // @method once(…): this // Behaves as [`on(…)`](#evented-on), except the listener will only get fired once and then removed. once: function (types, fn, context) { if (typeof types === 'object') { for (var type in types) { this.once(type, types[type], fn); } return this; } var handler = bind(function () { this .off(types, fn, context) .off(types, handler, context); }, this); // add a listener that's executed once and removed after that return this .on(types, fn, context) .on(types, handler, context); }, // @method addEventParent(obj: Evented): this // Adds an event parent - an `Evented` that will receive propagated events addEventParent: function (obj) { this._eventParents = this._eventParents || {}; this._eventParents[stamp(obj)] = obj; return this; }, // @method removeEventParent(obj: Evented): this // Removes an event parent, so it will stop receiving propagated events removeEventParent: function (obj) { if (this._eventParents) { delete this._eventParents[stamp(obj)]; } return this; }, _propagateEvent: function (e) { for (var id in this._eventParents) { this._eventParents[id].fire(e.type, extend({layer: e.target}, e), true); } } }; // aliases; we should ditch those eventually // @method addEventListener(…): this // Alias to [`on(…)`](#evented-on) Events.addEventListener = Events.on; // @method removeEventListener(…): this // Alias to [`off(…)`](#evented-off) // @method clearAllEventListeners(…): this // Alias to [`off()`](#evented-off) Events.removeEventListener = Events.clearAllEventListeners = Events.off; // @method addOneTimeEventListener(…): this // Alias to [`once(…)`](#evented-once) Events.addOneTimeEventListener = Events.once; // @method fireEvent(…): this // Alias to [`fire(…)`](#evented-fire) Events.fireEvent = Events.fire; // @method hasEventListeners(…): Boolean // Alias to [`listens(…)`](#evented-listens) Events.hasEventListeners = Events.listens; var Evented = Class.extend(Events); /* * @class Point * @aka L.Point * * Represents a point with `x` and `y` coordinates in pixels. * * @example * * ```js * var point = L.point(200, 300); * ``` * * All Leaflet methods and options that accept `Point` objects also accept them in a simple Array form (unless noted otherwise), so these lines are equivalent: * * ```js * map.panBy([200, 300]); * map.panBy(L.point(200, 300)); * ``` */ function Point(x, y, round) { // @property x: Number; The `x` coordinate of the point this.x = (round ? Math.round(x) : x); // @property y: Number; The `y` coordinate of the point this.y = (round ? Math.round(y) : y); } Point.prototype = { // @method clone(): Point // Returns a copy of the current point. clone: function () { return new Point(this.x, this.y); }, // @method add(otherPoint: Point): Point // Returns the result of addition of the current and the given points. add: function (point) { // non-destructive, returns a new point return this.clone()._add(toPoint(point)); }, _add: function (point) { // destructive, used directly for performance in situations where it's safe to modify existing point this.x += point.x; this.y += point.y; return this; }, // @method subtract(otherPoint: Point): Point // Returns the result of subtraction of the given point from the current. subtract: function (point) { return this.clone()._subtract(toPoint(point)); }, _subtract: function (point) { this.x -= point.x; this.y -= point.y; return this; }, // @method divideBy(num: Number): Point // Returns the result of division of the current point by the given number. divideBy: function (num) { return this.clone()._divideBy(num); }, _divideBy: function (num) { this.x /= num; this.y /= num; return this; }, // @method multiplyBy(num: Number): Point // Returns the result of multiplication of the current point by the given number. multiplyBy: function (num) { return this.clone()._multiplyBy(num); }, _multiplyBy: function (num) { this.x *= num; this.y *= num; return this; }, // @method scaleBy(scale: Point): Point // Multiply each coordinate of the current point by each coordinate of // `scale`. In linear algebra terms, multiply the point by the // [scaling matrix](https://en.wikipedia.org/wiki/Scaling_%28geometry%29#Matrix_representation) // defined by `scale`. scaleBy: function (point) { return new Point(this.x * point.x, this.y * point.y); }, // @method unscaleBy(scale: Point): Point // Inverse of `scaleBy`. Divide each coordinate of the current point by // each coordinate of `scale`. unscaleBy: function (point) { return new Point(this.x / point.x, this.y / point.y); }, // @method round(): Point // Returns a copy of the current point with rounded coordinates. round: function () { return this.clone()._round(); }, _round: function () { this.x = Math.round(this.x); this.y = Math.round(this.y); return this; }, // @method floor(): Point // Returns a copy of the current point with floored coordinates (rounded down). floor: function () { return this.clone()._floor(); }, _floor: function () { this.x = Math.floor(this.x); this.y = Math.floor(this.y); return this; }, // @method ceil(): Point // Returns a copy of the current point with ceiled coordinates (rounded up). ceil: function () { return this.clone()._ceil(); }, _ceil: function () { this.x = Math.ceil(this.x); this.y = Math.ceil(this.y); return this; }, // @method distanceTo(otherPoint: Point): Number // Returns the cartesian distance between the current and the given points. distanceTo: function (point) { point = toPoint(point); var x = point.x - this.x, y = point.y - this.y; return Math.sqrt(x * x + y * y); }, // @method equals(otherPoint: Point): Boolean // Returns `true` if the given point has the same coordinates. equals: function (point) { point = toPoint(point); return point.x === this.x && point.y === this.y; }, // @method contains(otherPoint: Point): Boolean // Returns `true` if both coordinates of the given point are less than the corresponding current point coordinates (in absolute values). contains: function (point) { point = toPoint(point); return Math.abs(point.x) <= Math.abs(this.x) && Math.abs(point.y) <= Math.abs(this.y); }, // @method toString(): String // Returns a string representation of the point for debugging purposes. toString: function () { return 'Point(' + formatNum(this.x) + ', ' + formatNum(this.y) + ')'; } }; // @factory L.point(x: Number, y: Number, round?: Boolean) // Creates a Point object with the given `x` and `y` coordinates. If optional `round` is set to true, rounds the `x` and `y` values. // @alternative // @factory L.point(coords: Number[]) // Expects an array of the form `[x, y]` instead. // @alternative // @factory L.point(coords: Object) // Expects a plain object of the form `{x: Number, y: Number}` instead. function toPoint(x, y, round) { if (x instanceof Point) { return x; } if (isArray(x)) { return new Point(x[0], x[1]); } if (x === undefined || x === null) { return x; } if (typeof x === 'object' && 'x' in x && 'y' in x) { return new Point(x.x, x.y); } return new Point(x, y, round); } /* * @class Bounds * @aka L.Bounds * * Represents a rectangular area in pixel coordinates. * * @example * * ```js * var p1 = L.point(10, 10), * p2 = L.point(40, 60), * bounds = L.bounds(p1, p2); * ``` * * All Leaflet methods that accept `Bounds` objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this: * * ```js * otherBounds.intersects([[10, 10], [40, 60]]); * ``` */ function Bounds(a, b) { if (!a) { return; } var points = b ? [a, b] : a; for (var i = 0, len = points.length; i < len; i++) { this.extend(points[i]); } } Bounds.prototype = { // @method extend(point: Point): this // Extends the bounds to contain the given point. extend: function (point) { // (Point) point = toPoint(point); // @property min: Point // The top left corner of the rectangle. // @property max: Point // The bottom right corner of the rectangle. 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; }, // @method getCenter(round?: Boolean): Point // Returns the center point of the bounds. getCenter: function (round) { return new Point( (this.min.x + this.max.x) / 2, (this.min.y + this.max.y) / 2, round); }, // @method getBottomLeft(): Point // Returns the bottom-left point of the bounds. getBottomLeft: function () { return new Point(this.min.x, this.max.y); }, // @method getTopRight(): Point // Returns the top-right point of the bounds. getTopRight: function () { // -> Point return new Point(this.max.x, this.min.y); }, // @method getTopLeft(): Point // Returns the top-left point of the bounds (i.e. [`this.min`](#bounds-min)). getTopLeft: function () { return this.min; // left, top }, // @method getBottomRight(): Point // Returns the bottom-right point of the bounds (i.e. [`this.max`](#bounds-max)). getBottomRight: function () { return this.max; // right, bottom }, // @method getSize(): Point // Returns the size of the given bounds getSize: function () { return this.max.subtract(this.min); }, // @method contains(otherBounds: Bounds): Boolean // Returns `true` if the rectangle contains the given one. // @alternative // @method contains(point: Point): Boolean // Returns `true` if the rectangle contains the given point. contains: function (obj) { var min, max; if (typeof obj[0] === 'number' || obj instanceof Point) { obj = toPoint(obj); } else { obj = toBounds(obj); } if (obj instanceof 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); }, // @method intersects(otherBounds: Bounds): Boolean // Returns `true` if the rectangle intersects the given bounds. Two bounds // intersect if they have at least one point in common. intersects: function (bounds) { // (Bounds) -> Boolean bounds = toBounds(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; }, // @method overlaps(otherBounds: Bounds): Boolean // Returns `true` if the rectangle overlaps the given bounds. Two bounds // overlap if their intersection is an area. overlaps: function (bounds) { // (Bounds) -> Boolean bounds = toBounds(bounds); var min = this.min, max = this.max, min2 = bounds.min, max2 = bounds.max, xOverlaps = (max2.x > min.x) && (min2.x < max.x), yOverlaps = (max2.y > min.y) && (min2.y < max.y); return xOverlaps && yOverlaps; }, isValid: function () { return !!(this.min && this.max); } }; // @factory L.bounds(corner1: Point, corner2: Point) // Creates a Bounds object from two corners coordinate pairs. // @alternative // @factory L.bounds(points: Point[]) // Creates a Bounds object from the given array of points. function toBounds(a, b) { if (!a || a instanceof Bounds) { return a; } return new Bounds(a, b); } /* * @class LatLngBounds * @aka L.LatLngBounds * * Represents a rectangular geographical area on a map. * * @example * * ```js * var corner1 = L.latLng(40.712, -74.227), * corner2 = L.latLng(40.774, -74.125), * bounds = L.latLngBounds(corner1, corner2); * ``` * * All Leaflet methods that accept LatLngBounds objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this: * * ```js * map.fitBounds([ * [40.712, -74.227], * [40.774, -74.125] * ]); * ``` * * Caution: if the area crosses the antimeridian (often confused with the International Date Line), you must specify corners _outside_ the [-180, 180] degrees longitude range. */ function LatLngBounds(corner1, corner2) { // (LatLng, LatLng) or (LatLng[]) if (!corner1) { return; } var latlngs = corner2 ? [corner1, corner2] : corner1; for (var i = 0, len = latlngs.length; i < len; i++) { this.extend(latlngs[i]); } } LatLngBounds.prototype = { // @method extend(latlng: LatLng): this // Extend the bounds to contain the given point // @alternative // @method extend(otherBounds: LatLngBounds): this // Extend the bounds to contain the given bounds extend: function (obj) { var sw = this._southWest, ne = this._northEast, sw2, ne2; if (obj instanceof LatLng) { sw2 = obj; ne2 = obj; } else if (obj instanceof LatLngBounds) { sw2 = obj._southWest; ne2 = obj._northEast; if (!sw2 || !ne2) { return this; } } else { return obj ? this.extend(toLatLng(obj) || toLatLngBounds(obj)) : this; } if (!sw && !ne) { this._southWest = new LatLng(sw2.lat, sw2.lng); this._northEast = new LatLng(ne2.lat, ne2.lng); } else { sw.lat = Math.min(sw2.lat, sw.lat); sw.lng = Math.min(sw2.lng, sw.lng); ne.lat = Math.max(ne2.lat, ne.lat); ne.lng = Math.max(ne2.lng, ne.lng); } return this; }, // @method pad(bufferRatio: Number): LatLngBounds // Returns bigger bounds created by extending the current bounds by a given percentage in each direction. pad: function (bufferRatio) { 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 LatLngBounds( new LatLng(sw.lat - heightBuffer, sw.lng - widthBuffer), new LatLng(ne.lat + heightBuffer, ne.lng + widthBuffer)); }, // @method getCenter(): LatLng // Returns the center point of the bounds. getCenter: function () { return new LatLng( (this._southWest.lat + this._northEast.lat) / 2, (this._southWest.lng + this._northEast.lng) / 2); }, // @method getSouthWest(): LatLng // Returns the south-west point of the bounds. getSouthWest: function () { return this._southWest; }, // @method getNorthEast(): LatLng // Returns the north-east point of the bounds. getNorthEast: function () { return this._northEast; }, // @method getNorthWest(): LatLng // Returns the north-west point of the bounds. getNorthWest: function () { return new LatLng(this.getNorth(), this.getWest()); }, // @method getSouthEast(): LatLng // Returns the south-east point of the bounds. getSouthEast: function () { return new LatLng(this.getSouth(), this.getEast()); }, // @method getWest(): Number // Returns the west longitude of the bounds getWest: function () { return this._southWest.lng; }, // @method getSouth(): Number // Returns the south latitude of the bounds getSouth: function () { return this._southWest.lat; }, // @method getEast(): Number // Returns the east longitude of the bounds getEast: function () { return this._northEast.lng; }, // @method getNorth(): Number // Returns the north latitude of the bounds getNorth: function () { return this._northEast.lat; }, // @method contains(otherBounds: LatLngBounds): Boolean // Returns `true` if the rectangle contains the given one. // @alternative // @method contains (latlng: LatLng): Boolean // Returns `true` if the rectangle contains the given point. contains: function (obj) { // (LatLngBounds) or (LatLng) -> Boolean if (typeof obj[0] === 'number' || obj instanceof LatLng || 'lat' in obj) { obj = toLatLng(obj); } else { obj = toLatLngBounds(obj); } var sw = this._southWest, ne = this._northEast, sw2, ne2; if (obj instanceof 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); }, // @method intersects(otherBounds: LatLngBounds): Boolean // Returns `true` if the rectangle intersects the given bounds. Two bounds intersect if they have at least one point in common. intersects: function (bounds) { bounds = toLatLngBounds(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; }, // @method overlaps(otherBounds: Bounds): Boolean // Returns `true` if the rectangle overlaps the given bounds. Two bounds overlap if their intersection is an area. overlaps: function (bounds) { bounds = toLatLngBounds(bounds); var sw = this._southWest, ne = this._northEast, sw2 = bounds.getSouthWest(), ne2 = bounds.getNorthEast(), latOverlaps = (ne2.lat > sw.lat) && (sw2.lat < ne.lat), lngOverlaps = (ne2.lng > sw.lng) && (sw2.lng < ne.lng); return latOverlaps && lngOverlaps; }, // @method toBBoxString(): String // Returns a string with bounding box coordinates in a 'southwest_lng,southwest_lat,northeast_lng,northeast_lat' format. Useful for sending requests to web services that return geo data. toBBoxString: function () { return [this.getWest(), this.getSouth(), this.getEast(), this.getNorth()].join(','); }, // @method equals(otherBounds: LatLngBounds, maxMargin?: Number): Boolean // Returns `true` if the rectangle is equivalent (within a small margin of error) to the given bounds. The margin of error can be overriden by setting `maxMargin` to a small number. equals: function (bounds, maxMargin) { if (!bounds) { return false; } bounds = toLatLngBounds(bounds); return this._southWest.equals(bounds.getSouthWest(), maxMargin) && this._northEast.equals(bounds.getNorthEast(), maxMargin); }, // @method isValid(): Boolean // Returns `true` if the bounds are properly initialized. isValid: function () { return !!(this._southWest && this._northEast); } }; // TODO International date line? // @factory L.latLngBounds(corner1: LatLng, corner2: LatLng) // Creates a `LatLngBounds` object by defining two diagonally opposite corners of the rectangle. // @alternative // @factory L.latLngBounds(latlngs: LatLng[]) // Creates a `LatLngBounds` object defined by the geographical points it contains. Very useful for zooming the map to fit a particular set of locations with [`fitBounds`](#map-fitbounds). function toLatLngBounds(a, b) { if (a instanceof LatLngBounds) { return a; } return new LatLngBounds(a, b); } /* @class LatLng * @aka L.LatLng * * Represents a geographical point with a certain latitude and longitude. * * @example * * ``` * var latlng = L.latLng(50.5, 30.5); * ``` * * All Leaflet methods that accept LatLng objects also accept them in a simple Array form and simple object form (unless noted otherwise), so these lines are equivalent: * * ``` * map.panTo([50, 30]); * map.panTo({lon: 30, lat: 50}); * map.panTo({lat: 50, lng: 30}); * map.panTo(L.latLng(50, 30)); * ``` */ function LatLng(lat, lng, alt) { if (isNaN(lat) || isNaN(lng)) { throw new Error('Invalid LatLng object: (' + lat + ', ' + lng + ')'); } // @property lat: Number // Latitude in degrees this.lat = +lat; // @property lng: Number // Longitude in degrees this.lng = +lng; // @property alt: Number // Altitude in meters (optional) if (alt !== undefined) { this.alt = +alt; } } LatLng.prototype = { // @method equals(otherLatLng: LatLng, maxMargin?: Number): Boolean // Returns `true` if the given `LatLng` point is at the same position (within a small margin of error). The margin of error can be overriden by setting `maxMargin` to a small number. equals: function (obj, maxMargin) { if (!obj) { return false; } obj = toLatLng(obj); var margin = Math.max( Math.abs(this.lat - obj.lat), Math.abs(this.lng - obj.lng)); return margin <= (maxMargin === undefined ? 1.0E-9 : maxMargin); }, // @method toString(): String // Returns a string representation of the point (for debugging purposes). toString: function (precision) { return 'LatLng(' + formatNum(this.lat, precision) + ', ' + formatNum(this.lng, precision) + ')'; }, // @method distanceTo(otherLatLng: LatLng): Number // Returns the distance (in meters) to the given `LatLng` calculated using the [Haversine formula](http://en.wikipedia.org/wiki/Haversine_formula). distanceTo: function (other) { return Earth.distance(this, toLatLng(other)); }, // @method wrap(): LatLng // Returns a new `LatLng` object with the longitude wrapped so it's always between -180 and +180 degrees. wrap: function () { return Earth.wrapLatLng(this); }, // @method toBounds(sizeInMeters: Number): LatLngBounds // Returns a new `LatLngBounds` object in which each boundary is `sizeInMeters/2` meters apart from the `LatLng`. toBounds: function (sizeInMeters) { var latAccuracy = 180 * sizeInMeters / 40075017, lngAccuracy = latAccuracy / Math.cos((Math.PI / 180) * this.lat); return toLatLngBounds( [this.lat - latAccuracy, this.lng - lngAccuracy], [this.lat + latAccuracy, this.lng + lngAccuracy]); }, clone: function () { return new LatLng(this.lat, this.lng, this.alt); } }; // @factory L.latLng(latitude: Number, longitude: Number, altitude?: Number): LatLng // Creates an object representing a geographical point with the given latitude and longitude (and optionally altitude). // @alternative // @factory L.latLng(coords: Array): LatLng // Expects an array of the form `[Number, Number]` or `[Number, Number, Number]` instead. // @alternative // @factory L.latLng(coords: Object): LatLng // Expects an plain object of the form `{lat: Number, lng: Number}` or `{lat: Number, lng: Number, alt: Number}` instead. function toLatLng(a, b, c) { if (a instanceof LatLng) { return a; } if (isArray(a) && typeof a[0] !== 'object') { if (a.length === 3) { return new LatLng(a[0], a[1], a[2]); } if (a.length === 2) { return new LatLng(a[0], a[1]); } return null; } if (a === undefined || a === null) { return a; } if (typeof a === 'object' && 'lat' in a) { return new LatLng(a.lat, 'lng' in a ? a.lng : a.lon, a.alt); } if (b === undefined) { return null; } return new LatLng(a, b, c); } /* * @namespace CRS * @crs L.CRS.Base * Object that defines coordinate reference systems for projecting * geographical points into pixel (screen) coordinates and back (and to * coordinates in other units for [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) services). See * [spatial reference system](http://en.wikipedia.org/wiki/Coordinate_reference_system). * * Leaflet defines the most usual CRSs by default. If you want to use a * CRS not defined by default, take a look at the * [Proj4Leaflet](https://github.com/kartena/Proj4Leaflet) plugin. */ var CRS = { // @method latLngToPoint(latlng: LatLng, zoom: Number): Point // Projects geographical coordinates into pixel coordinates for a given zoom. latLngToPoint: function (latlng, zoom) { var projectedPoint = this.projection.project(latlng), scale = this.scale(zoom); return this.transformation._transform(projectedPoint, scale); }, // @method pointToLatLng(point: Point, zoom: Number): LatLng // The inverse of `latLngToPoint`. Projects pixel coordinates on a given // zoom into geographical coordinates. pointToLatLng: function (point, zoom) { var scale = this.scale(zoom), untransformedPoint = this.transformation.untransform(point, scale); return this.projection.unproject(untransformedPoint); }, // @method project(latlng: LatLng): Point // Projects geographical coordinates into coordinates in units accepted for // this CRS (e.g. meters for EPSG:3857, for passing it to WMS services). project: function (latlng) { return this.projection.project(latlng); }, // @method unproject(point: Point): LatLng // Given a projected coordinate returns the corresponding LatLng. // The inverse of `project`. unproject: function (point) { return this.projection.unproject(point); }, // @method scale(zoom: Number): Number // Returns the scale used when transforming projected coordinates into // pixel coordinates for a particular zoom. For example, it returns // `256 * 2^zoom` for Mercator-based CRS. scale: function (zoom) { return 256 * Math.pow(2, zoom); }, // @method zoom(scale: Number): Number // Inverse of `scale()`, returns the zoom level corresponding to a scale // factor of `scale`. zoom: function (scale) { return Math.log(scale / 256) / Math.LN2; }, // @method getProjectedBounds(zoom: Number): Bounds // Returns the projection's bounds scaled and transformed for the provided `zoom`. getProjectedBounds: function (zoom) { if (this.infinite) { return null; } var b = this.projection.bounds, s = this.scale(zoom), min = this.transformation.transform(b.min, s), max = this.transformation.transform(b.max, s); return new Bounds(min, max); }, // @method distance(latlng1: LatLng, latlng2: LatLng): Number // Returns the distance between two geographical coordinates. // @property code: String // Standard code name of the CRS passed into WMS services (e.g. `'EPSG:3857'`) // // @property wrapLng: Number[] // An array of two numbers defining whether the longitude (horizontal) coordinate // axis wraps around a given range and how. Defaults to `[-180, 180]` in most // geographical CRSs. If `undefined`, the longitude axis does not wrap around. // // @property wrapLat: Number[] // Like `wrapLng`, but for the latitude (vertical) axis. // wrapLng: [min, max], // wrapLat: [min, max], // @property infinite: Boolean // If true, the coordinate space will be unbounded (infinite in both axes) infinite: false, // @method wrapLatLng(latlng: LatLng): LatLng // Returns a `LatLng` where lat and lng has been wrapped according to the // CRS's `wrapLat` and `wrapLng` properties, if they are outside the CRS's bounds. wrapLatLng: function (latlng) { var lng = this.wrapLng ? wrapNum(latlng.lng, this.wrapLng, true) : latlng.lng, lat = this.wrapLat ? wrapNum(latlng.lat, this.wrapLat, true) : latlng.lat, alt = latlng.alt; return new LatLng(lat, lng, alt); }, // @method wrapLatLngBounds(bounds: LatLngBounds): LatLngBounds // Returns a `LatLngBounds` with the same size as the given one, ensuring // that its center is within the CRS's bounds. // Only accepts actual `L.LatLngBounds` instances, not arrays. wrapLatLngBounds: function (bounds) { var center = bounds.getCenter(), newCenter = this.wrapLatLng(center), latShift = center.lat - newCenter.lat, lngShift = center.lng - newCenter.lng; if (latShift === 0 && lngShift === 0) { return bounds; } var sw = bounds.getSouthWest(), ne = bounds.getNorthEast(), newSw = new LatLng(sw.lat - latShift, sw.lng - lngShift), newNe = new LatLng(ne.lat - latShift, ne.lng - lngShift); return new LatLngBounds(newSw, newNe); } }; /* * @namespace CRS * @crs L.CRS.Earth * * Serves as the base for CRS that are global such that they cover the earth. * Can only be used as the base for other CRS and cannot be used directly, * since it does not have a `code`, `projection` or `transformation`. `distance()` returns * meters. */ var Earth = extend({}, CRS, { wrapLng: [-180, 180], // Mean Earth Radius, as recommended for use by // the International Union of Geodesy and Geophysics, // see http://rosettacode.org/wiki/Haversine_formula R: 6371000, // distance between two geographical points using spherical law of cosines approximation distance: function (latlng1, latlng2) { var rad = Math.PI / 180, lat1 = latlng1.lat * rad, lat2 = latlng2.lat * rad, a = Math.sin(lat1) * Math.sin(lat2) + Math.cos(lat1) * Math.cos(lat2) * Math.cos((latlng2.lng - latlng1.lng) * rad); return this.R * Math.acos(Math.min(a, 1)); } }); /* * @namespace Projection * @projection L.Projection.SphericalMercator * * Spherical Mercator projection — the most common projection for online maps, * used by almost all free and commercial tile providers. Assumes that Earth is * a sphere. Used by the `EPSG:3857` CRS. */ var SphericalMercator = { R: 6378137, MAX_LATITUDE: 85.0511287798, project: function (latlng) { var d = Math.PI / 180, max = this.MAX_LATITUDE, lat = Math.max(Math.min(max, latlng.lat), -max), sin = Math.sin(lat * d); return new Point( this.R * latlng.lng * d, this.R * Math.log((1 + sin) / (1 - sin)) / 2); }, unproject: function (point) { var d = 180 / Math.PI; return new LatLng( (2 * Math.atan(Math.exp(point.y / this.R)) - (Math.PI / 2)) * d, point.x * d / this.R); }, bounds: (function () { var d = 6378137 * Math.PI; return new Bounds([-d, -d], [d, d]); })() }; /* * @class Transformation * @aka L.Transformation * * Represents an affine transformation: a set of coefficients `a`, `b`, `c`, `d` * for transforming a point of a form `(x, y)` into `(a*x + b, c*y + d)` and doing * the reverse. Used by Leaflet in its projections code. * * @example * * ```js * var transformation = L.transformation(2, 5, -1, 10), * p = L.point(1, 2), * p2 = transformation.transform(p), // L.point(7, 8) * p3 = transformation.untransform(p2); // L.point(1, 2) * ``` */ // factory new L.Transformation(a: Number, b: Number, c: Number, d: Number) // Creates a `Transformation` object with the given coefficients. function Transformation(a, b, c, d) { if (isArray(a)) { // use array properties this._a = a[0]; this._b = a[1]; this._c = a[2]; this._d = a[3]; return; } this._a = a; this._b = b; this._c = c; this._d = d; } Transformation.prototype = { // @method transform(point: Point, scale?: Number): Point // Returns a transformed point, optionally multiplied by the given scale. // Only accepts actual `L.Point` instances, not arrays. 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; }, // @method untransform(point: Point, scale?: Number): Point // Returns the reverse transformation of the given point, optionally divided // by the given scale. Only accepts actual `L.Point` instances, not arrays. untransform: function (point, scale) { scale = scale || 1; return new Point( (point.x / scale - this._b) / this._a, (point.y / scale - this._d) / this._c); } }; // factory L.transformation(a: Number, b: Number, c: Number, d: Number) // @factory L.transformation(a: Number, b: Number, c: Number, d: Number) // Instantiates a Transformation object with the given coefficients. // @alternative // @factory L.transformation(coefficients: Array): Transformation // Expects an coeficients array of the form // `[a: Number, b: Number, c: Number, d: Number]`. function toTransformation(a, b, c, d) { return new Transformation(a, b, c, d); } /* * @namespace CRS * @crs L.CRS.EPSG3857 * * The most common CRS for online maps, used by almost all free and commercial * tile providers. Uses Spherical Mercator projection. Set in by default in * Map's `crs` option. */ var EPSG3857 = extend({}, Earth, { code: 'EPSG:3857', projection: SphericalMercator, transformation: (function () { var scale = 0.5 / (Math.PI * SphericalMercator.R); return toTransformation(scale, 0.5, -scale, 0.5); }()) }); var EPSG900913 = extend({}, EPSG3857, { code: 'EPSG:900913' }); // @namespace SVG; @section // There are several static functions which can be called without instantiating L.SVG: // @function create(name: String): SVGElement // Returns a instance of [SVGElement](https://developer.mozilla.org/docs/Web/API/SVGElement), // corresponding to the class name passed. For example, using 'line' will return // an instance of [SVGLineElement](https://developer.mozilla.org/docs/Web/API/SVGLineElement). function svgCreate(name) { return document.createElementNS('http://www.w3.org/2000/svg', name); } // @function pointsToPath(rings: Point[], closed: Boolean): String // Generates a SVG path string for multiple rings, with each ring turning // into "M..L..L.." instructions function pointsToPath(rings, closed) { var str = '', i, j, len, len2, points, p; for (i = 0, len = rings.length; i < len; i++) { points = rings[i]; for (j = 0, len2 = points.length; j < len2; j++) { p = points[j]; str += (j ? 'L' : 'M') + p.x + ' ' + p.y; } // closes the ring for polygons; "x" is VML syntax str += closed ? (svg ? 'z' : 'x') : ''; } // SVG complains about empty path strings return str || 'M0 0'; } /* * @namespace Browser * @aka L.Browser * * A namespace with static properties for browser/feature detection used by Leaflet internally. * * @example * * ```js * if (L.Browser.ielt9) { * alert('Upgrade your browser, dude!'); * } * ``` */ var style$1 = document.documentElement.style; // @property ie: Boolean; `true` for all Internet Explorer versions (not Edge). var ie = 'ActiveXObject' in window; // @property ielt9: Boolean; `true` for Internet Explorer versions less than 9. var ielt9 = ie && !document.addEventListener; // @property edge: Boolean; `true` for the Edge web browser. var edge = 'msLaunchUri' in navigator && !('documentMode' in document); // @property webkit: Boolean; // `true` for webkit-based browsers like Chrome and Safari (including mobile versions). var webkit = userAgentContains('webkit'); // @property android: Boolean // `true` for any browser running on an Android platform. var android = userAgentContains('android'); // @property android23: Boolean; `true` for browsers running on Android 2 or Android 3. var android23 = userAgentContains('android 2') || userAgentContains('android 3'); // @property opera: Boolean; `true` for the Opera browser var opera = !!window.opera; // @property chrome: Boolean; `true` for the Chrome browser. var chrome = userAgentContains('chrome'); // @property gecko: Boolean; `true` for gecko-based browsers like Firefox. var gecko = userAgentContains('gecko') && !webkit && !opera && !ie; // @property safari: Boolean; `true` for the Safari browser. var safari = !chrome && userAgentContains('safari'); var phantom = userAgentContains('phantom'); // @property opera12: Boolean // `true` for the Opera browser supporting CSS transforms (version 12 or later). var opera12 = 'OTransition' in style$1; // @property win: Boolean; `true` when the browser is running in a Windows platform var win = navigator.platform.indexOf('Win') === 0; // @property ie3d: Boolean; `true` for all Internet Explorer versions supporting CSS transforms. var ie3d = ie && ('transition' in style$1); // @property webkit3d: Boolean; `true` for webkit-based browsers supporting CSS transforms. var webkit3d = ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) && !android23; // @property gecko3d: Boolean; `true` for gecko-based browsers supporting CSS transforms. var gecko3d = 'MozPerspective' in style$1; // @property any3d: Boolean // `true` for all browsers supporting CSS transforms. var any3d = !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d) && !opera12 && !phantom; // @property mobile: Boolean; `true` for all browsers running in a mobile device. var mobile = typeof orientation !== 'undefined' || userAgentContains('mobile'); // @property mobileWebkit: Boolean; `true` for all webkit-based browsers in a mobile device. var mobileWebkit = mobile && webkit; // @property mobileWebkit3d: Boolean // `true` for all webkit-based browsers in a mobile device supporting CSS transforms. var mobileWebkit3d = mobile && webkit3d; // @property msPointer: Boolean // `true` for browsers implementing the Microsoft touch events model (notably IE10). var msPointer = !window.PointerEvent && window.MSPointerEvent; // @property pointer: Boolean // `true` for all browsers supporting [pointer events](https://msdn.microsoft.com/en-us/library/dn433244%28v=vs.85%29.aspx). var pointer = !!(window.PointerEvent || msPointer); // @property touch: Boolean // `true` for all browsers supporting [touch events](https://developer.mozilla.org/docs/Web/API/Touch_events). // This does not necessarily mean that the browser is running in a computer with // a touchscreen, it only means that the browser is capable of understanding // touch events. var touch = !window.L_NO_TOUCH && (pointer || 'ontouchstart' in window || (window.DocumentTouch && document instanceof window.DocumentTouch)); // @property mobileOpera: Boolean; `true` for the Opera browser in a mobile device. var mobileOpera = mobile && opera; // @property mobileGecko: Boolean // `true` for gecko-based browsers running in a mobile device. var mobileGecko = mobile && gecko; // @property retina: Boolean // `true` for browsers on a high-resolution "retina" screen. var retina = (window.devicePixelRatio || (window.screen.deviceXDPI / window.screen.logicalXDPI)) > 1; // @property canvas: Boolean // `true` when the browser supports [``](https://developer.mozilla.org/docs/Web/API/Canvas_API). var canvas = (function () { return !!document.createElement('canvas').getContext; }()); // @property svg: Boolean // `true` when the browser supports [SVG](https://developer.mozilla.org/docs/Web/SVG). var svg = !!(document.createElementNS && svgCreate('svg').createSVGRect); // @property vml: Boolean // `true` if the browser supports [VML](https://en.wikipedia.org/wiki/Vector_Markup_Language). var vml = !svg && (function () { try { var div = document.createElement('div'); div.innerHTML = ''; var shape = div.firstChild; shape.style.behavior = 'url(#default#VML)'; return shape && (typeof shape.adj === 'object'); } catch (e) { return false; } }()); function userAgentContains(str) { return navigator.userAgent.toLowerCase().indexOf(str) >= 0; } var Browser = (Object.freeze || Object)({ ie: ie, ielt9: ielt9, edge: edge, webkit: webkit, android: android, android23: android23, opera: opera, chrome: chrome, gecko: gecko, safari: safari, phantom: phantom, opera12: opera12, win: win, ie3d: ie3d, webkit3d: webkit3d, gecko3d: gecko3d, any3d: any3d, mobile: mobile, mobileWebkit: mobileWebkit, mobileWebkit3d: mobileWebkit3d, msPointer: msPointer, pointer: pointer, touch: touch, mobileOpera: mobileOpera, mobileGecko: mobileGecko, retina: retina, canvas: canvas, svg: svg, vml: vml }); /* * Extends L.DomEvent to provide touch support for Internet Explorer and Windows-based devices. */ var POINTER_DOWN = msPointer ? 'MSPointerDown' : 'pointerdown'; var POINTER_MOVE = msPointer ? 'MSPointerMove' : 'pointermove'; var POINTER_UP = msPointer ? 'MSPointerUp' : 'pointerup'; var POINTER_CANCEL = msPointer ? 'MSPointerCancel' : 'pointercancel'; var TAG_WHITE_LIST = ['INPUT', 'SELECT', 'OPTION']; var _pointers = {}; var _pointerDocListener = false; // DomEvent.DoubleTap needs to know about this var _pointersCount = 0; // Provides a touch events wrapper for (ms)pointer events. // ref http://www.w3.org/TR/pointerevents/ https://www.w3.org/Bugs/Public/show_bug.cgi?id=22890 function addPointerListener(obj, type, handler, id) { if (type === 'touchstart') { _addPointerStart(obj, handler, id); } else if (type === 'touchmove') { _addPointerMove(obj, handler, id); } else if (type === 'touchend') { _addPointerEnd(obj, handler, id); } return this; } function removePointerListener(obj, type, id) { var handler = obj['_leaflet_' + type + id]; if (type === 'touchstart') { obj.removeEventListener(POINTER_DOWN, handler, false); } else if (type === 'touchmove') { obj.removeEventListener(POINTER_MOVE, handler, false); } else if (type === 'touchend') { obj.removeEventListener(POINTER_UP, handler, false); obj.removeEventListener(POINTER_CANCEL, handler, false); } return this; } function _addPointerStart(obj, handler, id) { var onDown = bind(function (e) { if (e.pointerType !== 'mouse' && e.pointerType !== e.MSPOINTER_TYPE_MOUSE && e.pointerType !== e.MSPOINTER_TYPE_MOUSE) { // In IE11, some touch events needs to fire for form controls, or // the controls will stop working. We keep a whitelist of tag names that // need these events. For other target tags, we prevent default on the event. if (TAG_WHITE_LIST.indexOf(e.target.tagName) < 0) { preventDefault(e); } else { return; } } _handlePointer(e, handler); }); obj['_leaflet_touchstart' + id] = onDown; obj.addEventListener(POINTER_DOWN, onDown, false); // need to keep track of what pointers and how many are active to provide e.touches emulation if (!_pointerDocListener) { // we listen documentElement as any drags that end by moving the touch off the screen get fired there document.documentElement.addEventListener(POINTER_DOWN, _globalPointerDown, true); document.documentElement.addEventListener(POINTER_MOVE, _globalPointerMove, true); document.documentElement.addEventListener(POINTER_UP, _globalPointerUp, true); document.documentElement.addEventListener(POINTER_CANCEL, _globalPointerUp, true); _pointerDocListener = true; } } function _globalPointerDown(e) { _pointers[e.pointerId] = e; _pointersCount++; } function _globalPointerMove(e) { if (_pointers[e.pointerId]) { _pointers[e.pointerId] = e; } } function _globalPointerUp(e) { delete _pointers[e.pointerId]; _pointersCount--; } function _handlePointer(e, handler) { e.touches = []; for (var i in _pointers) { e.touches.push(_pointers[i]); } e.changedTouches = [e]; handler(e); } function _addPointerMove(obj, handler, id) { var onMove = function (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; } _handlePointer(e, handler); }; obj['_leaflet_touchmove' + id] = onMove; obj.addEventListener(POINTER_MOVE, onMove, false); } function _addPointerEnd(obj, handler, id) { var onUp = function (e) { _handlePointer(e, handler); }; obj['_leaflet_touchend' + id] = onUp; obj.addEventListener(POINTER_UP, onUp, false); obj.addEventListener(POINTER_CANCEL, onUp, false); } /* * Extends the event handling code with double tap support for mobile browsers. */ var _touchstart = msPointer ? 'MSPointerDown' : pointer ? 'pointerdown' : 'touchstart'; var _touchend = msPointer ? 'MSPointerUp' : pointer ? 'pointerup' : 'touchend'; var _pre = '_leaflet_'; // inspired by Zepto touch code by Thomas Fuchs function addDoubleTapListener(obj, handler, id) { var last, touch$$1, doubleTap = false, delay = 250; function onTouchStart(e) { var count; if (pointer) { if ((!edge) || e.pointerType === 'mouse') { return; } count = _pointersCount; } else { count = e.touches.length; } if (count > 1) { return; } var now = Date.now(), delta = now - (last || now); touch$$1 = e.touches ? e.touches[0] : e; doubleTap = (delta > 0 && delta <= delay); last = now; } function onTouchEnd(e) { if (doubleTap && !touch$$1.cancelBubble) { if (pointer) { if ((!edge) || e.pointerType === 'mouse') { return; } // work around .type being readonly with MSPointer* events var newTouch = {}, prop, i; for (i in touch$$1) { prop = touch$$1[i]; newTouch[i] = prop && prop.bind ? prop.bind(touch$$1) : prop; } touch$$1 = newTouch; } touch$$1.type = 'dblclick'; handler(touch$$1); last = null; } } obj[_pre + _touchstart + id] = onTouchStart; obj[_pre + _touchend + id] = onTouchEnd; obj[_pre + 'dblclick' + id] = handler; obj.addEventListener(_touchstart, onTouchStart, false); obj.addEventListener(_touchend, onTouchEnd, false); // On some platforms (notably, chrome<55 on win10 + touchscreen + mouse), // the browser doesn't fire touchend/pointerup events but does fire // native dblclicks. See #4127. // Edge 14 also fires native dblclicks, but only for pointerType mouse, see #5180. obj.addEventListener('dblclick', handler, false); return this; } function removeDoubleTapListener(obj, id) { var touchstart = obj[_pre + _touchstart + id], touchend = obj[_pre + _touchend + id], dblclick = obj[_pre + 'dblclick' + id]; obj.removeEventListener(_touchstart, touchstart, false); obj.removeEventListener(_touchend, touchend, false); if (!edge) { obj.removeEventListener('dblclick', dblclick, false); } return this; } /* * @namespace DomEvent * Utility functions to work with the [DOM events](https://developer.mozilla.org/docs/Web/API/Event), used by Leaflet internally. */ // Inspired by John Resig, Dean Edwards and YUI addEvent implementations. // @function on(el: HTMLElement, types: String, fn: Function, context?: Object): this // Adds a listener function (`fn`) to a particular DOM event type of the // element `el`. You can optionally specify the context of the listener // (object the `this` keyword will point to). You can also pass several // space-separated types (e.g. `'click dblclick'`). // @alternative // @function on(el: HTMLElement, eventMap: Object, context?: Object): this // Adds a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}` function on(obj, types, fn, context) { if (typeof types === 'object') { for (var type in types) { addOne(obj, type, types[type], fn); } } else { types = splitWords(types); for (var i = 0, len = types.length; i < len; i++) { addOne(obj, types[i], fn, context); } } return this; } var eventsKey = '_leaflet_events'; // @function off(el: HTMLElement, types: String, fn: Function, context?: Object): this // Removes a previously added listener function. If no function is specified, // it will remove all the listeners of that particular DOM event from the element. // Note that if you passed a custom context to on, you must pass the same // context to `off` in order to remove the listener. // @alternative // @function off(el: HTMLElement, eventMap: Object, context?: Object): this // Removes a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}` // @alternative // @function off(el: HTMLElement): this // Removes all known event listeners function off(obj, types, fn, context) { if (typeof types === 'object') { for (var type in types) { removeOne(obj, type, types[type], fn); } } else if (types) { types = splitWords(types); for (var i = 0, len = types.length; i < len; i++) { removeOne(obj, types[i], fn, context); } } else { for (var j in obj[eventsKey]) { removeOne(obj, j, obj[eventsKey][j]); } delete obj[eventsKey]; } return this; } function addOne(obj, type, fn, context) { var id = type + stamp(fn) + (context ? '_' + stamp(context) : ''); if (obj[eventsKey] && obj[eventsKey][id]) { return this; } var handler = function (e) { return fn.call(context || obj, e || window.event); }; var originalHandler = handler; if (pointer && type.indexOf('touch') === 0) { // Needs DomEvent.Pointer.js addPointerListener(obj, type, handler, id); } else if (touch && (type === 'dblclick') && addDoubleTapListener && !(pointer && chrome)) { // Chrome >55 does not need the synthetic dblclicks from addDoubleTapListener // See #5180 addDoubleTapListener(obj, handler, id); } else if ('addEventListener' in obj) { if (type === 'mousewheel') { obj.addEventListener('onwheel' in obj ? 'wheel' : 'mousewheel', handler, false); } else if ((type === 'mouseenter') || (type === 'mouseleave')) { handler = function (e) { e = e || window.event; if (isExternalTarget(obj, e)) { originalHandler(e); } }; obj.addEventListener(type === 'mouseenter' ? 'mouseover' : 'mouseout', handler, false); } else { if (type === 'click' && android) { handler = function (e) { filterClick(e, originalHandler); }; } obj.addEventListener(type, handler, false); } } else if ('attachEvent' in obj) { obj.attachEvent('on' + type, handler); } obj[eventsKey] = obj[eventsKey] || {}; obj[eventsKey][id] = handler; } function removeOne(obj, type, fn, context) { var id = type + stamp(fn) + (context ? '_' + stamp(context) : ''), handler = obj[eventsKey] && obj[eventsKey][id]; if (!handler) { return this; } if (pointer && type.indexOf('touch') === 0) { removePointerListener(obj, type, id); } else if (touch && (type === 'dblclick') && removeDoubleTapListener) { removeDoubleTapListener(obj, id); } else if ('removeEventListener' in obj) { if (type === 'mousewheel') { obj.removeEventListener('onwheel' in obj ? 'wheel' : 'mousewheel', handler, false); } else { obj.removeEventListener( type === 'mouseenter' ? 'mouseover' : type === 'mouseleave' ? 'mouseout' : type, handler, false); } } else if ('detachEvent' in obj) { obj.detachEvent('on' + type, handler); } obj[eventsKey][id] = null; } // @function stopPropagation(ev: DOMEvent): this // Stop the given event from propagation to parent elements. Used inside the listener functions: // ```js // L.DomEvent.on(div, 'click', function (ev) { // L.DomEvent.stopPropagation(ev); // }); // ``` function stopPropagation(e) { if (e.stopPropagation) { e.stopPropagation(); } else if (e.originalEvent) { // In case of Leaflet event. e.originalEvent._stopped = true; } else { e.cancelBubble = true; } skipped(e); return this; } // @function disableScrollPropagation(el: HTMLElement): this // Adds `stopPropagation` to the element's `'mousewheel'` events (plus browser variants). function disableScrollPropagation(el) { addOne(el, 'mousewheel', stopPropagation); return this; } // @function disableClickPropagation(el: HTMLElement): this // Adds `stopPropagation` to the element's `'click'`, `'doubleclick'`, // `'mousedown'` and `'touchstart'` events (plus browser variants). function disableClickPropagation(el) { on(el, 'mousedown touchstart dblclick', stopPropagation); addOne(el, 'click', fakeStop); return this; } // @function preventDefault(ev: DOMEvent): this // Prevents the default action of the DOM Event `ev` from happening (such as // following a link in the href of the a element, or doing a POST request // with page reload when a `
    ` is submitted). // Use it inside listener functions. function preventDefault(e) { if (e.preventDefault) { e.preventDefault(); } else { e.returnValue = false; } return this; } // @function stop(ev): this // Does `stopPropagation` and `preventDefault` at the same time. function stop(e) { preventDefault(e); stopPropagation(e); return this; } // @function getMousePosition(ev: DOMEvent, container?: HTMLElement): Point // Gets normalized mouse position from a DOM event relative to the // `container` or to the whole page if not specified. function getMousePosition(e, container) { if (!container) { return new Point(e.clientX, e.clientY); } var rect = container.getBoundingClientRect(); return new Point( e.clientX - rect.left - container.clientLeft, e.clientY - rect.top - container.clientTop); } // Chrome on Win scrolls double the pixels as in other platforms (see #4538), // and Firefox scrolls device pixels, not CSS pixels var wheelPxFactor = (win && chrome) ? 2 * window.devicePixelRatio : gecko ? window.devicePixelRatio : 1; // @function getWheelDelta(ev: DOMEvent): Number // Gets normalized wheel delta from a mousewheel DOM event, in vertical // pixels scrolled (negative if scrolling down). // Events from pointing devices without precise scrolling are mapped to // a best guess of 60 pixels. function getWheelDelta(e) { return (edge) ? e.wheelDeltaY / 2 : // Don't trust window-geometry-based delta (e.deltaY && e.deltaMode === 0) ? -e.deltaY / wheelPxFactor : // Pixels (e.deltaY && e.deltaMode === 1) ? -e.deltaY * 20 : // Lines (e.deltaY && e.deltaMode === 2) ? -e.deltaY * 60 : // Pages (e.deltaX || e.deltaZ) ? 0 : // Skip horizontal/depth wheel events e.wheelDelta ? (e.wheelDeltaY || e.wheelDelta) / 2 : // Legacy IE pixels (e.detail && Math.abs(e.detail) < 32765) ? -e.detail * 20 : // Legacy Moz lines e.detail ? e.detail / -32765 * 60 : // Legacy Moz pages 0; } var skipEvents = {}; function fakeStop(e) { // fakes stopPropagation by setting a special event flag, checked/reset with skipped(e) skipEvents[e.type] = true; } function skipped(e) { var events = skipEvents[e.type]; // reset when checking, as it's only used in map container and propagates outside of the map skipEvents[e.type] = false; return events; } // check if element really left/entered the event target (for mouseenter/mouseleave) function isExternalTarget(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); } var lastClick; // this is a horrible workaround for a bug in Android where a single touch triggers two click events function filterClick(e, handler) { var timeStamp = (e.timeStamp || (e.originalEvent && e.originalEvent.timeStamp)), elapsed = lastClick && (timeStamp - 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)) { stop(e); return; } lastClick = timeStamp; handler(e); } var DomEvent = (Object.freeze || Object)({ on: on, off: off, stopPropagation: stopPropagation, disableScrollPropagation: disableScrollPropagation, disableClickPropagation: disableClickPropagation, preventDefault: preventDefault, stop: stop, getMousePosition: getMousePosition, getWheelDelta: getWheelDelta, fakeStop: fakeStop, skipped: skipped, isExternalTarget: isExternalTarget, addListener: on, removeListener: off }); /* * @namespace DomUtil * * Utility functions to work with the [DOM](https://developer.mozilla.org/docs/Web/API/Document_Object_Model) * tree, used by Leaflet internally. * * Most functions expecting or returning a `HTMLElement` also work for * SVG elements. The only difference is that classes refer to CSS classes * in HTML and SVG classes in SVG. */ // @property TRANSFORM: String // Vendor-prefixed transform style name (e.g. `'webkitTransform'` for WebKit). var TRANSFORM = 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 // @property TRANSITION: String // Vendor-prefixed transition style name. var TRANSITION = testProp( ['webkitTransition', 'transition', 'OTransition', 'MozTransition', 'msTransition']); // @property TRANSITION_END: String // Vendor-prefixed transitionend event name. var TRANSITION_END = TRANSITION === 'webkitTransition' || TRANSITION === 'OTransition' ? TRANSITION + 'End' : 'transitionend'; // @function get(id: String|HTMLElement): HTMLElement // Returns an element given its DOM id, or returns the element itself // if it was passed directly. function get(id) { return typeof id === 'string' ? document.getElementById(id) : id; } // @function getStyle(el: HTMLElement, styleAttrib: String): String // Returns the value for a certain style attribute on an element, // including computed values or values set through CSS. function getStyle(el, style) { var value = el.style[style] || (el.currentStyle && 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; } // @function create(tagName: String, className?: String, container?: HTMLElement): HTMLElement // Creates an HTML element with `tagName`, sets its class to `className`, and optionally appends it to `container` element. function create$1(tagName, className, container) { var el = document.createElement(tagName); el.className = className || ''; if (container) { container.appendChild(el); } return el; } // @function remove(el: HTMLElement) // Removes `el` from its parent element function remove(el) { var parent = el.parentNode; if (parent) { parent.removeChild(el); } } // @function empty(el: HTMLElement) // Removes all of `el`'s children elements from `el` function empty(el) { while (el.firstChild) { el.removeChild(el.firstChild); } } // @function toFront(el: HTMLElement) // Makes `el` the last child of its parent, so it renders in front of the other children. function toFront(el) { var parent = el.parentNode; if (parent.lastChild !== el) { parent.appendChild(el); } } // @function toBack(el: HTMLElement) // Makes `el` the first child of its parent, so it renders behind the other children. function toBack(el) { var parent = el.parentNode; if (parent.firstChild !== el) { parent.insertBefore(el, parent.firstChild); } } // @function hasClass(el: HTMLElement, name: String): Boolean // Returns `true` if the element's class attribute contains `name`. function hasClass(el, name) { if (el.classList !== undefined) { return el.classList.contains(name); } var className = getClass(el); return className.length > 0 && new RegExp('(^|\\s)' + name + '(\\s|$)').test(className); } // @function addClass(el: HTMLElement, name: String) // Adds `name` to the element's class attribute. function addClass(el, name) { if (el.classList !== undefined) { var classes = splitWords(name); for (var i = 0, len = classes.length; i < len; i++) { el.classList.add(classes[i]); } } else if (!hasClass(el, name)) { var className = getClass(el); setClass(el, (className ? className + ' ' : '') + name); } } // @function removeClass(el: HTMLElement, name: String) // Removes `name` from the element's class attribute. function removeClass(el, name) { if (el.classList !== undefined) { el.classList.remove(name); } else { setClass(el, trim((' ' + getClass(el) + ' ').replace(' ' + name + ' ', ' '))); } } // @function setClass(el: HTMLElement, name: String) // Sets the element's class. function setClass(el, name) { if (el.className.baseVal === undefined) { el.className = name; } else { // in case of SVG element el.className.baseVal = name; } } // @function getClass(el: HTMLElement): String // Returns the element's class. function getClass(el) { return el.className.baseVal === undefined ? el.className : el.className.baseVal; } // @function setOpacity(el: HTMLElement, opacity: Number) // Set the opacity of an element (including old IE support). // `opacity` must be a number from `0` to `1`. function setOpacity(el, value) { if ('opacity' in el.style) { el.style.opacity = value; } else if ('filter' in el.style) { _setOpacityIE(el, value); } } function _setOpacityIE(el, value) { 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 + ')'; } } // @function testProp(props: String[]): String|false // Goes through the array of style names and returns the first name // that is a valid style name for an element. If no such name is found, // it returns false. Useful for vendor-prefixed styles like `transform`. function testProp(props) { var style = document.documentElement.style; for (var i = 0; i < props.length; i++) { if (props[i] in style) { return props[i]; } } return false; } // @function setTransform(el: HTMLElement, offset: Point, scale?: Number) // Resets the 3D CSS transform of `el` so it is translated by `offset` pixels // and optionally scaled by `scale`. Does not have an effect if the // browser doesn't support 3D CSS transforms. function setTransform(el, offset, scale) { var pos = offset || new Point(0, 0); el.style[TRANSFORM] = (ie3d ? 'translate(' + pos.x + 'px,' + pos.y + 'px)' : 'translate3d(' + pos.x + 'px,' + pos.y + 'px,0)') + (scale ? ' scale(' + scale + ')' : ''); } // @function setPosition(el: HTMLElement, position: Point) // Sets the position of `el` to coordinates specified by `position`, // using CSS translate or top/left positioning depending on the browser // (used by Leaflet internally to position its layers). function setPosition(el, point) { /*eslint-disable */ el._leaflet_pos = point; /*eslint-enable */ if (any3d) { setTransform(el, point); } else { el.style.left = point.x + 'px'; el.style.top = point.y + 'px'; } } // @function getPosition(el: HTMLElement): Point // Returns the coordinates of an element previously positioned with setPosition. function getPosition(el) { // this method is only used for elements previously positioned using setPosition, // so it's safe to cache the position for performance return el._leaflet_pos || new Point(0, 0); } // @function disableTextSelection() // Prevents the user from generating `selectstart` DOM events, usually generated // when the user drags the mouse through a page with text. Used internally // by Leaflet to override the behaviour of any click-and-drag interaction on // the map. Affects drag interactions on the whole document. // @function enableTextSelection() // Cancels the effects of a previous [`L.DomUtil.disableTextSelection`](#domutil-disabletextselection). var disableTextSelection; var enableTextSelection; var _userSelect; if ('onselectstart' in document) { disableTextSelection = function () { on(window, 'selectstart', preventDefault); }; enableTextSelection = function () { off(window, 'selectstart', preventDefault); }; } else { var userSelectProperty = testProp( ['userSelect', 'WebkitUserSelect', 'OUserSelect', 'MozUserSelect', 'msUserSelect']); disableTextSelection = function () { if (userSelectProperty) { var style = document.documentElement.style; _userSelect = style[userSelectProperty]; style[userSelectProperty] = 'none'; } }; enableTextSelection = function () { if (userSelectProperty) { document.documentElement.style[userSelectProperty] = _userSelect; _userSelect = undefined; } }; } // @function disableImageDrag() // As [`L.DomUtil.disableTextSelection`](#domutil-disabletextselection), but // for `dragstart` DOM events, usually generated when the user drags an image. function disableImageDrag() { on(window, 'dragstart', preventDefault); } // @function enableImageDrag() // Cancels the effects of a previous [`L.DomUtil.disableImageDrag`](#domutil-disabletextselection). function enableImageDrag() { off(window, 'dragstart', preventDefault); } var _outlineElement; var _outlineStyle; // @function preventOutline(el: HTMLElement) // Makes the [outline](https://developer.mozilla.org/docs/Web/CSS/outline) // of the element `el` invisible. Used internally by Leaflet to prevent // focusable elements from displaying an outline when the user performs a // drag interaction on them. function preventOutline(element) { while (element.tabIndex === -1) { element = element.parentNode; } if (!element.style) { return; } restoreOutline(); _outlineElement = element; _outlineStyle = element.style.outline; element.style.outline = 'none'; on(window, 'keydown', restoreOutline); } // @function restoreOutline() // Cancels the effects of a previous [`L.DomUtil.preventOutline`](). function restoreOutline() { if (!_outlineElement) { return; } _outlineElement.style.outline = _outlineStyle; _outlineElement = undefined; _outlineStyle = undefined; off(window, 'keydown', restoreOutline); } var DomUtil = (Object.freeze || Object)({ TRANSFORM: TRANSFORM, TRANSITION: TRANSITION, TRANSITION_END: TRANSITION_END, get: get, getStyle: getStyle, create: create$1, remove: remove, empty: empty, toFront: toFront, toBack: toBack, hasClass: hasClass, addClass: addClass, removeClass: removeClass, setClass: setClass, getClass: getClass, setOpacity: setOpacity, testProp: testProp, setTransform: setTransform, setPosition: setPosition, getPosition: getPosition, disableTextSelection: disableTextSelection, enableTextSelection: enableTextSelection, disableImageDrag: disableImageDrag, enableImageDrag: enableImageDrag, preventOutline: preventOutline, restoreOutline: restoreOutline }); /* * @class PosAnimation * @aka L.PosAnimation * @inherits Evented * Used internally for panning animations, utilizing CSS3 Transitions for modern browsers and a timer fallback for IE6-9. * * @example * ```js * var fx = new L.PosAnimation(); * fx.run(el, [300, 500], 0.5); * ``` * * @constructor L.PosAnimation() * Creates a `PosAnimation` object. * */ var PosAnimation = Evented.extend({ // @method run(el: HTMLElement, newPos: Point, duration?: Number, easeLinearity?: Number) // Run an animation of a given element to a new position, optionally setting // duration in seconds (`0.25` by default) and easing linearity factor (3rd // argument of the [cubic bezier curve](http://cubic-bezier.com/#0,0,.5,1), // `0.5` by default). run: function (el, newPos, duration, easeLinearity) { 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 = getPosition(el); this._offset = newPos.subtract(this._startPos); this._startTime = +new Date(); // @event start: Event // Fired when the animation starts this.fire('start'); this._animate(); }, // @method stop() // Stops the animation (if currently running). stop: function () { if (!this._inProgress) { return; } this._step(true); this._complete(); }, _animate: function () { // animation loop this._animId = requestAnimFrame(this._animate, this); this._step(); }, _step: function (round) { var elapsed = (+new Date()) - this._startTime, duration = this._duration * 1000; if (elapsed < duration) { this._runFrame(this._easeOut(elapsed / duration), round); } else { this._runFrame(1); this._complete(); } }, _runFrame: function (progress, round) { var pos = this._startPos.add(this._offset.multiplyBy(progress)); if (round) { pos._round(); } setPosition(this._el, pos); // @event step: Event // Fired continuously during the animation. this.fire('step'); }, _complete: function () { cancelAnimFrame(this._animId); this._inProgress = false; // @event end: Event // Fired when the animation ends. this.fire('end'); }, _easeOut: function (t) { return 1 - Math.pow(1 - t, this._easeOutPower); } }); /* * @class Map * @aka L.Map * @inherits Evented * * The central class of the API — it is used to create a map on a page and manipulate it. * * @example * * ```js * // initialize the map on the "map" div with a given center and zoom * var map = L.map('map', { * center: [51.505, -0.09], * zoom: 13 * }); * ``` * */ var Map = Evented.extend({ options: { // @section Map State Options // @option crs: CRS = L.CRS.EPSG3857 // The [Coordinate Reference System](#crs) to use. Don't change this if you're not // sure what it means. crs: EPSG3857, // @option center: LatLng = undefined // Initial geographic center of the map center: undefined, // @option zoom: Number = undefined // Initial map zoom level zoom: undefined, // @option minZoom: Number = * // Minimum zoom level of the map. // If not specified and at least one `GridLayer` or `TileLayer` is in the map, // the lowest of their `minZoom` options will be used instead. minZoom: undefined, // @option maxZoom: Number = * // Maximum zoom level of the map. // If not specified and at least one `GridLayer` or `TileLayer` is in the map, // the highest of their `maxZoom` options will be used instead. maxZoom: undefined, // @option layers: Layer[] = [] // Array of layers that will be added to the map initially layers: [], // @option maxBounds: LatLngBounds = null // When this option is set, the map restricts the view to the given // geographical bounds, bouncing the user back if the user tries to pan // outside the view. To set the restriction dynamically, use // [`setMaxBounds`](#map-setmaxbounds) method. maxBounds: undefined, // @option renderer: Renderer = * // The default method for drawing vector layers on the map. `L.SVG` // or `L.Canvas` by default depending on browser support. renderer: undefined, // @section Animation Options // @option zoomAnimation: Boolean = true // Whether the map zoom animation is enabled. By default it's enabled // in all browsers that support CSS3 Transitions except Android. zoomAnimation: true, // @option zoomAnimationThreshold: Number = 4 // Won't animate zoom if the zoom difference exceeds this value. zoomAnimationThreshold: 4, // @option fadeAnimation: Boolean = true // Whether the tile fade animation is enabled. By default it's enabled // in all browsers that support CSS3 Transitions except Android. fadeAnimation: true, // @option markerZoomAnimation: Boolean = true // Whether markers animate their zoom with the zoom animation, if disabled // they will disappear for the length of the animation. By default it's // enabled in all browsers that support CSS3 Transitions except Android. markerZoomAnimation: true, // @option transform3DLimit: Number = 2^23 // Defines the maximum size of a CSS translation transform. The default // value should not be changed unless a web browser positions layers in // the wrong place after doing a large `panBy`. transform3DLimit: 8388608, // Precision limit of a 32-bit float // @section Interaction Options // @option zoomSnap: Number = 1 // Forces the map's zoom level to always be a multiple of this, particularly // right after a [`fitBounds()`](#map-fitbounds) or a pinch-zoom. // By default, the zoom level snaps to the nearest integer; lower values // (e.g. `0.5` or `0.1`) allow for greater granularity. A value of `0` // means the zoom level will not be snapped after `fitBounds` or a pinch-zoom. zoomSnap: 1, // @option zoomDelta: Number = 1 // Controls how much the map's zoom level will change after a // [`zoomIn()`](#map-zoomin), [`zoomOut()`](#map-zoomout), pressing `+` // or `-` on the keyboard, or using the [zoom controls](#control-zoom). // Values smaller than `1` (e.g. `0.5`) allow for greater granularity. zoomDelta: 1, // @option trackResize: Boolean = true // Whether the map automatically handles browser window resize to update itself. trackResize: true }, initialize: function (id, options) { // (HTMLElement or String, Object) options = setOptions(this, options); this._initContainer(id); this._initLayout(); // hack for https://github.com/Leaflet/Leaflet/issues/1980 this._onResize = bind(this._onResize, this); this._initEvents(); if (options.maxBounds) { this.setMaxBounds(options.maxBounds); } if (options.zoom !== undefined) { this._zoom = this._limitZoom(options.zoom); } if (options.center && options.zoom !== undefined) { this.setView(toLatLng(options.center), options.zoom, {reset: true}); } this._handlers = []; this._layers = {}; this._zoomBoundLayers = {}; this._sizeChanged = true; this.callInitHooks(); // don't animate on browsers without hardware-accelerated transitions or old Android/Opera this._zoomAnimated = TRANSITION && any3d && !mobileOpera && this.options.zoomAnimation; // 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) { this._createAnimProxy(); on(this._proxy, TRANSITION_END, this._catchTransitionEnd, this); } this._addLayers(this.options.layers); }, // @section Methods for modifying map state // @method setView(center: LatLng, zoom: Number, options?: Zoom/pan options): this // Sets the view of the map (geographical center and zoom) with the given // animation options. setView: function (center, zoom, options) { zoom = zoom === undefined ? this._zoom : this._limitZoom(zoom); center = this._limitCenter(toLatLng(center), zoom, this.options.maxBounds); options = options || {}; this._stop(); if (this._loaded && !options.reset && options !== true) { if (options.animate !== undefined) { options.zoom = extend({animate: options.animate}, options.zoom); options.pan = extend({animate: options.animate, duration: options.duration}, options.pan); } // try animating pan or zoom var moved = (this._zoom !== zoom) ? this._tryAnimatedZoom && this._tryAnimatedZoom(center, zoom, options.zoom) : this._tryAnimatedPan(center, options.pan); if (moved) { // 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; }, // @method setZoom(zoom: Number, options?: Zoom/pan options): this // Sets the zoom of the map. setZoom: function (zoom, options) { if (!this._loaded) { this._zoom = zoom; return this; } return this.setView(this.getCenter(), zoom, {zoom: options}); }, // @method zoomIn(delta?: Number, options?: Zoom options): this // Increases the zoom of the map by `delta` ([`zoomDelta`](#map-zoomdelta) by default). zoomIn: function (delta, options) { delta = delta || (any3d ? this.options.zoomDelta : 1); return this.setZoom(this._zoom + delta, options); }, // @method zoomOut(delta?: Number, options?: Zoom options): this // Decreases the zoom of the map by `delta` ([`zoomDelta`](#map-zoomdelta) by default). zoomOut: function (delta, options) { delta = delta || (any3d ? this.options.zoomDelta : 1); return this.setZoom(this._zoom - delta, options); }, // @method setZoomAround(latlng: LatLng, zoom: Number, options: Zoom options): this // Zooms the map while keeping a specified geographical point on the map // stationary (e.g. used internally for scroll zoom and double-click zoom). // @alternative // @method setZoomAround(offset: Point, zoom: Number, options: Zoom options): this // Zooms the map while keeping a specified pixel on the map (relative to the top-left corner) stationary. setZoomAround: function (latlng, zoom, options) { var scale = this.getZoomScale(zoom), viewHalf = this.getSize().divideBy(2), containerPoint = latlng instanceof 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}); }, _getBoundsCenterZoom: function (bounds, options) { options = options || {}; bounds = bounds.getBounds ? bounds.getBounds() : toLatLngBounds(bounds); var paddingTL = toPoint(options.paddingTopLeft || options.padding || [0, 0]), paddingBR = toPoint(options.paddingBottomRight || options.padding || [0, 0]), zoom = this.getBoundsZoom(bounds, false, paddingTL.add(paddingBR)); zoom = (typeof options.maxZoom === 'number') ? Math.min(options.maxZoom, zoom) : zoom; if (zoom === Infinity) { return { center: bounds.getCenter(), 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 { center: center, zoom: zoom }; }, // @method fitBounds(bounds: LatLngBounds, options?: fitBounds options): this // Sets a map view that contains the given geographical bounds with the // maximum zoom level possible. fitBounds: function (bounds, options) { bounds = toLatLngBounds(bounds); if (!bounds.isValid()) { throw new Error('Bounds are not valid.'); } var target = this._getBoundsCenterZoom(bounds, options); return this.setView(target.center, target.zoom, options); }, // @method fitWorld(options?: fitBounds options): this // Sets a map view that mostly contains the whole world with the maximum // zoom level possible. fitWorld: function (options) { return this.fitBounds([[-90, -180], [90, 180]], options); }, // @method panTo(latlng: LatLng, options?: Pan options): this // Pans the map to a given center. panTo: function (center, options) { // (LatLng) return this.setView(center, this._zoom, {pan: options}); }, // @method panBy(offset: Point, options?: Pan options): this // Pans the map by a given number of pixels (animated). panBy: function (offset, options) { offset = toPoint(offset).round(); options = options || {}; if (!offset.x && !offset.y) { return this.fire('moveend'); } // If we pan too far, Chrome gets issues with tiles // and makes them disappear or appear in the wrong place (slightly offset) #2602 if (options.animate !== true && !this.getSize().contains(offset)) { this._resetView(this.unproject(this.project(this.getCenter()).add(offset)), this.getZoom()); return this; } if (!this._panAnim) { this._panAnim = new 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) { addClass(this._mapPane, 'leaflet-pan-anim'); var newPos = this._getMapPanePos().subtract(offset).round(); this._panAnim.run(this._mapPane, newPos, options.duration || 0.25, options.easeLinearity); } else { this._rawPanBy(offset); this.fire('move').fire('moveend'); } return this; }, // @method flyTo(latlng: LatLng, zoom?: Number, options?: Zoom/pan options): this // Sets the view of the map (geographical center and zoom) performing a smooth // pan-zoom animation. flyTo: function (targetCenter, targetZoom, options) { options = options || {}; if (options.animate === false || !any3d) { return this.setView(targetCenter, targetZoom, options); } this._stop(); var from = this.project(this.getCenter()), to = this.project(targetCenter), size = this.getSize(), startZoom = this._zoom; targetCenter = toLatLng(targetCenter); targetZoom = targetZoom === undefined ? startZoom : targetZoom; var w0 = Math.max(size.x, size.y), w1 = w0 * this.getZoomScale(startZoom, targetZoom), u1 = (to.distanceTo(from)) || 1, rho = 1.42, rho2 = rho * rho; function r(i) { var s1 = i ? -1 : 1, s2 = i ? w1 : w0, t1 = w1 * w1 - w0 * w0 + s1 * rho2 * rho2 * u1 * u1, b1 = 2 * s2 * rho2 * u1, b = t1 / b1, sq = Math.sqrt(b * b + 1) - b; // workaround for floating point precision bug when sq = 0, log = -Infinite, // thus triggering an infinite loop in flyTo var log = sq < 0.000000001 ? -18 : Math.log(sq); return log; } function sinh(n) { return (Math.exp(n) - Math.exp(-n)) / 2; } function cosh(n) { return (Math.exp(n) + Math.exp(-n)) / 2; } function tanh(n) { return sinh(n) / cosh(n); } var r0 = r(0); function w(s) { return w0 * (cosh(r0) / cosh(r0 + rho * s)); } function u(s) { return w0 * (cosh(r0) * tanh(r0 + rho * s) - sinh(r0)) / rho2; } function easeOut(t) { return 1 - Math.pow(1 - t, 1.5); } var start = Date.now(), S = (r(1) - r0) / rho, duration = options.duration ? 1000 * options.duration : 1000 * S * 0.8; function frame() { var t = (Date.now() - start) / duration, s = easeOut(t) * S; if (t <= 1) { this._flyToFrame = requestAnimFrame(frame, this); this._move( this.unproject(from.add(to.subtract(from).multiplyBy(u(s) / u1)), startZoom), this.getScaleZoom(w0 / w(s), startZoom), {flyTo: true}); } else { this ._move(targetCenter, targetZoom) ._moveEnd(true); } } this._moveStart(true); frame.call(this); return this; }, // @method flyToBounds(bounds: LatLngBounds, options?: fitBounds options): this // Sets the view of the map with a smooth animation like [`flyTo`](#map-flyto), // but takes a bounds parameter like [`fitBounds`](#map-fitbounds). flyToBounds: function (bounds, options) { var target = this._getBoundsCenterZoom(bounds, options); return this.flyTo(target.center, target.zoom, options); }, // @method setMaxBounds(bounds: Bounds): this // Restricts the map view to the given bounds (see the [maxBounds](#map-maxbounds) option). setMaxBounds: function (bounds) { bounds = toLatLngBounds(bounds); if (!bounds.isValid()) { this.options.maxBounds = null; return this.off('moveend', this._panInsideMaxBounds); } else if (this.options.maxBounds) { this.off('moveend', this._panInsideMaxBounds); } this.options.maxBounds = bounds; if (this._loaded) { this._panInsideMaxBounds(); } return this.on('moveend', this._panInsideMaxBounds); }, // @method setMinZoom(zoom: Number): this // Sets the lower limit for the available zoom levels (see the [minZoom](#map-minzoom) option). setMinZoom: function (zoom) { this.options.minZoom = zoom; if (this._loaded && this.getZoom() < this.options.minZoom) { return this.setZoom(zoom); } return this; }, // @method setMaxZoom(zoom: Number): this // Sets the upper limit for the available zoom levels (see the [maxZoom](#map-maxzoom) option). setMaxZoom: function (zoom) { this.options.maxZoom = zoom; if (this._loaded && (this.getZoom() > this.options.maxZoom)) { return this.setZoom(zoom); } return this; }, // @method panInsideBounds(bounds: LatLngBounds, options?: Pan options): this // Pans the map to the closest view that would lie inside the given bounds (if it's not already), controlling the animation using the options specific, if any. panInsideBounds: function (bounds, options) { this._enforcingBounds = true; var center = this.getCenter(), newCenter = this._limitCenter(center, this._zoom, toLatLngBounds(bounds)); if (!center.equals(newCenter)) { this.panTo(newCenter, options); } this._enforcingBounds = false; return this; }, // @method invalidateSize(options: Zoom/Pan options): this // Checks if the map container size changed and updates the map if so — // call it after you've changed the map size dynamically, also animating // pan by default. If `options.pan` is `false`, panning will not occur. // If `options.debounceMoveend` is `true`, it will delay `moveend` event so // that it doesn't happen often even if the method is called many // times in a row. // @alternative // @method invalidateSize(animate: Boolean): this // Checks if the map container size changed and updates the map if so — // call it after you've changed the map size dynamically, also animating // pan by default. invalidateSize: function (options) { if (!this._loaded) { return this; } options = extend({ animate: false, pan: true }, options === true ? {animate: true} : options); var oldSize = this.getSize(); this._sizeChanged = true; this._lastCenter = 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(bind(this.fire, this, 'moveend'), 200); } else { this.fire('moveend'); } } // @section Map state change events // @event resize: ResizeEvent // Fired when the map is resized. return this.fire('resize', { oldSize: oldSize, newSize: newSize }); }, // @section Methods for modifying map state // @method stop(): this // Stops the currently running `panTo` or `flyTo` animation, if any. stop: function () { this.setZoom(this._limitZoom(this._zoom)); if (!this.options.zoomSnap) { this.fire('viewreset'); } return this._stop(); }, // @section Geolocation methods // @method locate(options?: Locate options): this // Tries to locate the user using the Geolocation API, firing a [`locationfound`](#map-locationfound) // event with location data on success or a [`locationerror`](#map-locationerror) event on failure, // and optionally sets the map view to the user's location with respect to // detection accuracy (or to the world view if geolocation failed). // Note that, if your page doesn't use HTTPS, this method will fail in // modern browsers ([Chrome 50 and newer](https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-powerful-features-on-insecure-origins)) // See `Locate options` for more details. locate: function (options) { options = this._locateOptions = extend({ timeout: 10000, watch: false // setView: false // maxZoom: // maximumAge: 0 // enableHighAccuracy: false }, options); if (!('geolocation' in navigator)) { this._handleGeolocationError({ code: 0, message: 'Geolocation not supported.' }); return this; } var onResponse = bind(this._handleGeolocationResponse, this), onError = bind(this._handleGeolocationError, this); if (options.watch) { this._locationWatchId = navigator.geolocation.watchPosition(onResponse, onError, options); } else { navigator.geolocation.getCurrentPosition(onResponse, onError, options); } return this; }, // @method stopLocate(): this // Stops watching location previously initiated by `map.locate({watch: true})` // and aborts resetting the map view if map.locate was called with // `{setView: true}`. stopLocate: function () { if (navigator.geolocation && navigator.geolocation.clearWatch) { 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(); } // @section Location events // @event locationerror: ErrorEvent // Fired when geolocation (using the [`locate`](#map-locate) method) failed. this.fire('locationerror', { code: c, message: 'Geolocation error: ' + message + '.' }); }, _handleGeolocationResponse: function (pos) { var lat = pos.coords.latitude, lng = pos.coords.longitude, latlng = new LatLng(lat, lng), bounds = latlng.toBounds(pos.coords.accuracy), options = this._locateOptions; if (options.setView) { var zoom = this.getBoundsZoom(bounds); this.setView(latlng, options.maxZoom ? Math.min(zoom, options.maxZoom) : 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]; } } // @event locationfound: LocationEvent // Fired when geolocation (using the [`locate`](#map-locate) method) // went successfully. this.fire('locationfound', data); }, // TODO handler.addTo // TODO Appropiate docs section? // @section Other Methods // @method addHandler(name: String, HandlerClass: Function): this // Adds a new `Handler` to the map, given its name and constructor function. 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; }, // @method remove(): this // Destroys the map and clears all related event listeners. remove: function () { this._initEvents(true); if (this._containerId !== this._container._leaflet_id) { throw new Error('Map container is being reused by another instance'); } try { // throws error in IE6-8 delete this._container._leaflet_id; delete this._containerId; } catch (e) { /*eslint-disable */ this._container._leaflet_id = undefined; /*eslint-enable */ this._containerId = undefined; } remove(this._mapPane); if (this._clearControlPos) { this._clearControlPos(); } this._clearHandlers(); if (this._loaded) { // @section Map state change events // @event unload: Event // Fired when the map is destroyed with [remove](#map-remove) method. this.fire('unload'); } var i; for (i in this._layers) { this._layers[i].remove(); } for (i in this._panes) { remove(this._panes[i]); } this._layers = []; this._panes = []; delete this._mapPane; delete this._renderer; return this; }, // @section Other Methods // @method createPane(name: String, container?: HTMLElement): HTMLElement // Creates a new [map pane](#map-pane) with the given name if it doesn't exist already, // then returns it. The pane is created as a child of `container`, or // as a child of the main map pane if not set. createPane: function (name, container) { var className = 'leaflet-pane' + (name ? ' leaflet-' + name.replace('Pane', '') + '-pane' : ''), pane = create$1('div', className, container || this._mapPane); if (name) { this._panes[name] = pane; } return pane; }, // @section Methods for Getting Map State // @method getCenter(): LatLng // Returns the geographical center of the map view getCenter: function () { this._checkIfLoaded(); if (this._lastCenter && !this._moved()) { return this._lastCenter; } return this.layerPointToLatLng(this._getCenterLayerPoint()); }, // @method getZoom(): Number // Returns the current zoom level of the map view getZoom: function () { return this._zoom; }, // @method getBounds(): LatLngBounds // Returns the geographical bounds visible in the current map view getBounds: function () { var bounds = this.getPixelBounds(), sw = this.unproject(bounds.getBottomLeft()), ne = this.unproject(bounds.getTopRight()); return new LatLngBounds(sw, ne); }, // @method getMinZoom(): Number // Returns the minimum zoom level of the map (if set in the `minZoom` option of the map or of any layers), or `0` by default. getMinZoom: function () { return this.options.minZoom === undefined ? this._layersMinZoom || 0 : this.options.minZoom; }, // @method getMaxZoom(): Number // Returns the maximum zoom level of the map (if set in the `maxZoom` option of the map or of any layers). getMaxZoom: function () { return this.options.maxZoom === undefined ? (this._layersMaxZoom === undefined ? Infinity : this._layersMaxZoom) : this.options.maxZoom; }, // @method getBoundsZoom(bounds: LatLngBounds, inside?: Boolean): Number // Returns the maximum zoom level on which the given bounds fit to the map // view in its entirety. If `inside` (optional) is set to `true`, the method // instead returns the minimum zoom level on which the map view fits into // the given bounds in its entirety. getBoundsZoom: function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number bounds = toLatLngBounds(bounds); padding = toPoint(padding || [0, 0]); var zoom = this.getZoom() || 0, min = this.getMinZoom(), max = this.getMaxZoom(), nw = bounds.getNorthWest(), se = bounds.getSouthEast(), size = this.getSize().subtract(padding), boundsSize = toBounds(this.project(se, zoom), this.project(nw, zoom)).getSize(), snap = any3d ? this.options.zoomSnap : 1, scalex = size.x / boundsSize.x, scaley = size.y / boundsSize.y, scale = inside ? Math.max(scalex, scaley) : Math.min(scalex, scaley); zoom = this.getScaleZoom(scale, zoom); if (snap) { zoom = Math.round(zoom / (snap / 100)) * (snap / 100); // don't jump if within 1% of a snap level zoom = inside ? Math.ceil(zoom / snap) * snap : Math.floor(zoom / snap) * snap; } return Math.max(min, Math.min(max, zoom)); }, // @method getSize(): Point // Returns the current size of the map container (in pixels). getSize: function () { if (!this._size || this._sizeChanged) { this._size = new Point( this._container.clientWidth || 0, this._container.clientHeight || 0); this._sizeChanged = false; } return this._size.clone(); }, // @method getPixelBounds(): Bounds // Returns the bounds of the current map view in projected pixel // coordinates (sometimes useful in layer and overlay implementations). getPixelBounds: function (center, zoom) { var topLeftPoint = this._getTopLeftPoint(center, zoom); return new Bounds(topLeftPoint, topLeftPoint.add(this.getSize())); }, // TODO: Check semantics - isn't the pixel origin the 0,0 coord relative to // the map pane? "left point of the map layer" can be confusing, specially // since there can be negative offsets. // @method getPixelOrigin(): Point // Returns the projected pixel coordinates of the top left point of // the map layer (useful in custom layer and overlay implementations). getPixelOrigin: function () { this._checkIfLoaded(); return this._pixelOrigin; }, // @method getPixelWorldBounds(zoom?: Number): Bounds // Returns the world's bounds in pixel coordinates for zoom level `zoom`. // If `zoom` is omitted, the map's current zoom level is used. getPixelWorldBounds: function (zoom) { return this.options.crs.getProjectedBounds(zoom === undefined ? this.getZoom() : zoom); }, // @section Other Methods // @method getPane(pane: String|HTMLElement): HTMLElement // Returns a [map pane](#map-pane), given its name or its HTML element (its identity). getPane: function (pane) { return typeof pane === 'string' ? this._panes[pane] : pane; }, // @method getPanes(): Object // Returns a plain object containing the names of all [panes](#map-pane) as keys and // the panes as values. getPanes: function () { return this._panes; }, // @method getContainer: HTMLElement // Returns the HTML element that contains the map. getContainer: function () { return this._container; }, // @section Conversion Methods // @method getZoomScale(toZoom: Number, fromZoom: Number): Number // Returns the scale factor to be applied to a map transition from zoom level // `fromZoom` to `toZoom`. Used internally to help with zoom animations. getZoomScale: function (toZoom, fromZoom) { // TODO replace with universal implementation after refactoring projections var crs = this.options.crs; fromZoom = fromZoom === undefined ? this._zoom : fromZoom; return crs.scale(toZoom) / crs.scale(fromZoom); }, // @method getScaleZoom(scale: Number, fromZoom: Number): Number // Returns the zoom level that the map would end up at, if it is at `fromZoom` // level and everything is scaled by a factor of `scale`. Inverse of // [`getZoomScale`](#map-getZoomScale). getScaleZoom: function (scale, fromZoom) { var crs = this.options.crs; fromZoom = fromZoom === undefined ? this._zoom : fromZoom; var zoom = crs.zoom(scale * crs.scale(fromZoom)); return isNaN(zoom) ? Infinity : zoom; }, // @method project(latlng: LatLng, zoom: Number): Point // Projects a geographical coordinate `LatLng` according to the projection // of the map's CRS, then scales it according to `zoom` and the CRS's // `Transformation`. The result is pixel coordinate relative to // the CRS origin. project: function (latlng, zoom) { zoom = zoom === undefined ? this._zoom : zoom; return this.options.crs.latLngToPoint(toLatLng(latlng), zoom); }, // @method unproject(point: Point, zoom: Number): LatLng // Inverse of [`project`](#map-project). unproject: function (point, zoom) { zoom = zoom === undefined ? this._zoom : zoom; return this.options.crs.pointToLatLng(toPoint(point), zoom); }, // @method layerPointToLatLng(point: Point): LatLng // Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin), // returns the corresponding geographical coordinate (for the current zoom level). layerPointToLatLng: function (point) { var projectedPoint = toPoint(point).add(this.getPixelOrigin()); return this.unproject(projectedPoint); }, // @method latLngToLayerPoint(latlng: LatLng): Point // Given a geographical coordinate, returns the corresponding pixel coordinate // relative to the [origin pixel](#map-getpixelorigin). latLngToLayerPoint: function (latlng) { var projectedPoint = this.project(toLatLng(latlng))._round(); return projectedPoint._subtract(this.getPixelOrigin()); }, // @method wrapLatLng(latlng: LatLng): LatLng // Returns a `LatLng` where `lat` and `lng` has been wrapped according to the // map's CRS's `wrapLat` and `wrapLng` properties, if they are outside the // CRS's bounds. // By default this means longitude is wrapped around the dateline so its // value is between -180 and +180 degrees. wrapLatLng: function (latlng) { return this.options.crs.wrapLatLng(toLatLng(latlng)); }, // @method wrapLatLngBounds(bounds: LatLngBounds): LatLngBounds // Returns a `LatLngBounds` with the same size as the given one, ensuring that // its center is within the CRS's bounds. // By default this means the center longitude is wrapped around the dateline so its // value is between -180 and +180 degrees, and the majority of the bounds // overlaps the CRS's bounds. wrapLatLngBounds: function (latlng) { return this.options.crs.wrapLatLngBounds(toLatLngBounds(latlng)); }, // @method distance(latlng1: LatLng, latlng2: LatLng): Number // Returns the distance between two geographical coordinates according to // the map's CRS. By default this measures distance in meters. distance: function (latlng1, latlng2) { return this.options.crs.distance(toLatLng(latlng1), toLatLng(latlng2)); }, // @method containerPointToLayerPoint(point: Point): Point // Given a pixel coordinate relative to the map container, returns the corresponding // pixel coordinate relative to the [origin pixel](#map-getpixelorigin). containerPointToLayerPoint: function (point) { // (Point) return toPoint(point).subtract(this._getMapPanePos()); }, // @method layerPointToContainerPoint(point: Point): Point // Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin), // returns the corresponding pixel coordinate relative to the map container. layerPointToContainerPoint: function (point) { // (Point) return toPoint(point).add(this._getMapPanePos()); }, // @method containerPointToLatLng(point: Point): LatLng // Given a pixel coordinate relative to the map container, returns // the corresponding geographical coordinate (for the current zoom level). containerPointToLatLng: function (point) { var layerPoint = this.containerPointToLayerPoint(toPoint(point)); return this.layerPointToLatLng(layerPoint); }, // @method latLngToContainerPoint(latlng: LatLng): Point // Given a geographical coordinate, returns the corresponding pixel coordinate // relative to the map container. latLngToContainerPoint: function (latlng) { return this.layerPointToContainerPoint(this.latLngToLayerPoint(toLatLng(latlng))); }, // @method mouseEventToContainerPoint(ev: MouseEvent): Point // Given a MouseEvent object, returns the pixel coordinate relative to the // map container where the event took place. mouseEventToContainerPoint: function (e) { return getMousePosition(e, this._container); }, // @method mouseEventToLayerPoint(ev: MouseEvent): Point // Given a MouseEvent object, returns the pixel coordinate relative to // the [origin pixel](#map-getpixelorigin) where the event took place. mouseEventToLayerPoint: function (e) { return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e)); }, // @method mouseEventToLatLng(ev: MouseEvent): LatLng // Given a MouseEvent object, returns geographical coordinate where the // event took place. mouseEventToLatLng: function (e) { // (MouseEvent) return this.layerPointToLatLng(this.mouseEventToLayerPoint(e)); }, // map initialization methods _initContainer: function (id) { var container = this._container = get(id); if (!container) { throw new Error('Map container not found.'); } else if (container._leaflet_id) { throw new Error('Map container is already initialized.'); } on(container, 'scroll', this._onScroll, this); this._containerId = stamp(container); }, _initLayout: function () { var container = this._container; this._fadeAnimated = this.options.fadeAnimation && any3d; addClass(container, 'leaflet-container' + (touch ? ' leaflet-touch' : '') + (retina ? ' leaflet-retina' : '') + (ielt9 ? ' leaflet-oldie' : '') + (safari ? ' leaflet-safari' : '') + (this._fadeAnimated ? ' leaflet-fade-anim' : '')); var position = 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._paneRenderers = {}; // @section // // Panes are DOM elements used to control the ordering of layers on the map. You // can access panes with [`map.getPane`](#map-getpane) or // [`map.getPanes`](#map-getpanes) methods. New panes can be created with the // [`map.createPane`](#map-createpane) method. // // Every map has the following default panes that differ only in zIndex. // // @pane mapPane: HTMLElement = 'auto' // Pane that contains all other map panes this._mapPane = this.createPane('mapPane', this._container); setPosition(this._mapPane, new Point(0, 0)); // @pane tilePane: HTMLElement = 200 // Pane for `GridLayer`s and `TileLayer`s this.createPane('tilePane'); // @pane overlayPane: HTMLElement = 400 // Pane for vector overlays (`Path`s), like `Polyline`s and `Polygon`s this.createPane('shadowPane'); // @pane shadowPane: HTMLElement = 500 // Pane for overlay shadows (e.g. `Marker` shadows) this.createPane('overlayPane'); // @pane markerPane: HTMLElement = 600 // Pane for `Icon`s of `Marker`s this.createPane('markerPane'); // @pane tooltipPane: HTMLElement = 650 // Pane for tooltip. this.createPane('tooltipPane'); // @pane popupPane: HTMLElement = 700 // Pane for `Popup`s. this.createPane('popupPane'); if (!this.options.markerZoomAnimation) { addClass(panes.markerPane, 'leaflet-zoom-hide'); addClass(panes.shadowPane, 'leaflet-zoom-hide'); } }, // private methods that modify map state // @section Map state change events _resetView: function (center, zoom) { setPosition(this._mapPane, new Point(0, 0)); var loading = !this._loaded; this._loaded = true; zoom = this._limitZoom(zoom); this.fire('viewprereset'); var zoomChanged = this._zoom !== zoom; this ._moveStart(zoomChanged) ._move(center, zoom) ._moveEnd(zoomChanged); // @event viewreset: Event // Fired when the map needs to redraw its content (this usually happens // on map zoom or load). Very useful for creating custom overlays. this.fire('viewreset'); // @event load: Event // Fired when the map is initialized (when its center and zoom are set // for the first time). if (loading) { this.fire('load'); } }, _moveStart: function (zoomChanged) { // @event zoomstart: Event // Fired when the map zoom is about to change (e.g. before zoom animation). // @event movestart: Event // Fired when the view of the map starts changing (e.g. user starts dragging the map). if (zoomChanged) { this.fire('zoomstart'); } return this.fire('movestart'); }, _move: function (center, zoom, data) { if (zoom === undefined) { zoom = this._zoom; } var zoomChanged = this._zoom !== zoom; this._zoom = zoom; this._lastCenter = center; this._pixelOrigin = this._getNewPixelOrigin(center); // @event zoom: Event // Fired repeatedly during any change in zoom level, including zoom // and fly animations. if (zoomChanged || (data && data.pinch)) { // Always fire 'zoom' if pinching because #3530 this.fire('zoom', data); } // @event move: Event // Fired repeatedly during any movement of the map, including pan and // fly animations. return this.fire('move', data); }, _moveEnd: function (zoomChanged) { // @event zoomend: Event // Fired when the map has changed, after any animations. if (zoomChanged) { this.fire('zoomend'); } // @event moveend: Event // Fired when the center of the map stops changing (e.g. user stopped // dragging the map). return this.fire('moveend'); }, _stop: function () { cancelAnimFrame(this._flyToFrame); if (this._panAnim) { this._panAnim.stop(); } return this; }, _rawPanBy: function (offset) { setPosition(this._mapPane, this._getMapPanePos().subtract(offset)); }, _getZoomSpan: function () { return this.getMaxZoom() - this.getMinZoom(); }, _panInsideMaxBounds: function () { if (!this._enforcingBounds) { this.panInsideBounds(this.options.maxBounds); } }, _checkIfLoaded: function () { if (!this._loaded) { throw new Error('Set map center and zoom first.'); } }, // DOM event handling // @section Interaction events _initEvents: function (remove$$1) { this._targets = {}; this._targets[stamp(this._container)] = this; var onOff = remove$$1 ? off : on; // @event click: MouseEvent // Fired when the user clicks (or taps) the map. // @event dblclick: MouseEvent // Fired when the user double-clicks (or double-taps) the map. // @event mousedown: MouseEvent // Fired when the user pushes the mouse button on the map. // @event mouseup: MouseEvent // Fired when the user releases the mouse button on the map. // @event mouseover: MouseEvent // Fired when the mouse enters the map. // @event mouseout: MouseEvent // Fired when the mouse leaves the map. // @event mousemove: MouseEvent // Fired while the mouse moves over the map. // @event contextmenu: MouseEvent // Fired when the user pushes the right mouse button on the map, prevents // default browser context menu from showing if there are listeners on // this event. Also fired on mobile when the user holds a single touch // for a second (also called long press). // @event keypress: KeyboardEvent // Fired when the user presses a key from the keyboard while the map is focused. onOff(this._container, 'click dblclick mousedown mouseup ' + 'mouseover mouseout mousemove contextmenu keypress', this._handleDOMEvent, this); if (this.options.trackResize) { onOff(window, 'resize', this._onResize, this); } if (any3d && this.options.transform3DLimit) { (remove$$1 ? this.off : this.on).call(this, 'moveend', this._onMoveEnd); } }, _onResize: function () { cancelAnimFrame(this._resizeRequest); this._resizeRequest = requestAnimFrame( function () { this.invalidateSize({debounceMoveend: true}); }, this); }, _onScroll: function () { this._container.scrollTop = 0; this._container.scrollLeft = 0; }, _onMoveEnd: function () { var pos = this._getMapPanePos(); if (Math.max(Math.abs(pos.x), Math.abs(pos.y)) >= this.options.transform3DLimit) { // https://bugzilla.mozilla.org/show_bug.cgi?id=1203873 but Webkit also have // a pixel offset on very high values, see: http://jsfiddle.net/dg6r5hhb/ this._resetView(this.getCenter(), this.getZoom()); } }, _findEventTargets: function (e, type) { var targets = [], target, isHover = type === 'mouseout' || type === 'mouseover', src = e.target || e.srcElement, dragging = false; while (src) { target = this._targets[stamp(src)]; if (target && (type === 'click' || type === 'preclick') && !e._simulated && this._draggableMoved(target)) { // Prevent firing click after you just dragged an object. dragging = true; break; } if (target && target.listens(type, true)) { if (isHover && !isExternalTarget(src, e)) { break; } targets.push(target); if (isHover) { break; } } if (src === this._container) { break; } src = src.parentNode; } if (!targets.length && !dragging && !isHover && isExternalTarget(src, e)) { targets = [this]; } return targets; }, _handleDOMEvent: function (e) { if (!this._loaded || skipped(e)) { return; } var type = e.type; if (type === 'mousedown' || type === 'keypress') { // prevents outline when clicking on keyboard-focusable element preventOutline(e.target || e.srcElement); } this._fireDOMEvent(e, type); }, _mouseEvents: ['click', 'dblclick', 'mouseover', 'mouseout', 'contextmenu'], _fireDOMEvent: function (e, type, targets) { if (e.type === 'click') { // Fire a synthetic 'preclick' event which propagates up (mainly for closing popups). // @event preclick: MouseEvent // Fired before mouse click on the map (sometimes useful when you // want something to happen on click before any existing click // handlers start running). var synth = extend({}, e); synth.type = 'preclick'; this._fireDOMEvent(synth, synth.type, targets); } if (e._stopped) { return; } // Find the layer the event is propagating from and its parents. targets = (targets || []).concat(this._findEventTargets(e, type)); if (!targets.length) { return; } var target = targets[0]; if (type === 'contextmenu' && target.listens(type, true)) { preventDefault(e); } var data = { originalEvent: e }; if (e.type !== 'keypress') { var isMarker = (target.options && 'icon' in target.options); data.containerPoint = isMarker ? this.latLngToContainerPoint(target.getLatLng()) : this.mouseEventToContainerPoint(e); data.layerPoint = this.containerPointToLayerPoint(data.containerPoint); data.latlng = isMarker ? target.getLatLng() : this.layerPointToLatLng(data.layerPoint); } for (var i = 0; i < targets.length; i++) { targets[i].fire(type, data, true); if (data.originalEvent._stopped || (targets[i].options.bubblingMouseEvents === false && indexOf(this._mouseEvents, type) !== -1)) { return; } } }, _draggableMoved: function (obj) { obj = obj.dragging && obj.dragging.enabled() ? obj : this; return (obj.dragging && obj.dragging.moved()) || (this.boxZoom && this.boxZoom.moved()); }, _clearHandlers: function () { for (var i = 0, len = this._handlers.length; i < len; i++) { this._handlers[i].disable(); } }, // @section Other Methods // @method whenReady(fn: Function, context?: Object): this // Runs the given function `fn` when the map gets initialized with // a view (center and zoom) and at least one layer, or immediately // if it's already initialized, optionally passing a function context. whenReady: function (callback, context) { if (this._loaded) { callback.call(context || this, {target: this}); } else { this.on('load', callback, context); } return this; }, // private methods for getting map state _getMapPanePos: function () { return getPosition(this._mapPane) || new Point(0, 0); }, _moved: function () { var pos = this._getMapPanePos(); return pos && !pos.equals([0, 0]); }, _getTopLeftPoint: function (center, zoom) { var pixelOrigin = center && zoom !== undefined ? this._getNewPixelOrigin(center, zoom) : this.getPixelOrigin(); return pixelOrigin.subtract(this._getMapPanePos()); }, _getNewPixelOrigin: function (center, zoom) { var viewHalf = this.getSize()._divideBy(2); return this.project(center, zoom)._subtract(viewHalf)._add(this._getMapPanePos())._round(); }, _latLngToNewLayerPoint: function (latlng, zoom, center) { var topLeft = this._getNewPixelOrigin(center, zoom); return this.project(latlng, zoom)._subtract(topLeft); }, _latLngBoundsToNewLayerBounds: function (latLngBounds, zoom, center) { var topLeft = this._getNewPixelOrigin(center, zoom); return toBounds([ this.project(latLngBounds.getSouthWest(), zoom)._subtract(topLeft), this.project(latLngBounds.getNorthWest(), zoom)._subtract(topLeft), this.project(latLngBounds.getSouthEast(), zoom)._subtract(topLeft), this.project(latLngBounds.getNorthEast(), zoom)._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 Bounds(centerPoint.subtract(viewHalf), centerPoint.add(viewHalf)), offset = this._getBoundsOffset(viewBounds, bounds, zoom); // If offset is less than a pixel, ignore. // This prevents unstable projections from getting into // an infinite loop of tiny offsets. if (offset.round().equals([0, 0])) { return center; } 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 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 projectedMaxBounds = toBounds( this.project(maxBounds.getNorthEast(), zoom), this.project(maxBounds.getSouthWest(), zoom) ), minOffset = projectedMaxBounds.min.subtract(pxBounds.min), maxOffset = projectedMaxBounds.max.subtract(pxBounds.max), dx = this._rebound(minOffset.x, -maxOffset.x), dy = this._rebound(minOffset.y, -maxOffset.y); return new 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(), snap = any3d ? this.options.zoomSnap : 1; if (snap) { zoom = Math.round(zoom / snap) * snap; } return Math.max(min, Math.min(max, zoom)); }, _onPanTransitionStep: function () { this.fire('move'); }, _onPanTransitionEnd: function () { 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; }, _createAnimProxy: function () { var proxy = this._proxy = create$1('div', 'leaflet-proxy leaflet-zoom-animated'); this._panes.mapPane.appendChild(proxy); this.on('zoomanim', function (e) { var prop = TRANSFORM, transform = this._proxy.style[prop]; setTransform(this._proxy, this.project(e.center, e.zoom), this.getZoomScale(e.zoom, 1)); // workaround for case when transform is the same and so transitionend event is not fired if (transform === this._proxy.style[prop] && this._animatingZoom) { this._onZoomTransitionEnd(); } }, this); this.on('load moveend', function () { var c = this.getCenter(), z = this.getZoom(); setTransform(this._proxy, this.project(c, z), this.getZoomScale(z, 1)); }, this); this._on('unload', this._destroyAnimProxy, this); }, _destroyAnimProxy: function () { remove(this._proxy); delete this._proxy; }, _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); // 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; } requestAnimFrame(function () { this ._moveStart(true) ._animateZoom(center, zoom, true); }, this); return true; }, _animateZoom: function (center, zoom, startAnim, noUpdate) { if (startAnim) { this._animatingZoom = true; // remember what center/zoom to set after animation this._animateToCenter = center; this._animateToZoom = zoom; addClass(this._mapPane, 'leaflet-zoom-anim'); } // @event zoomanim: ZoomAnimEvent // Fired on every frame of a zoom animation this.fire('zoomanim', { center: center, zoom: zoom, noUpdate: noUpdate }); // Work around webkit not firing 'transitionend', see https://github.com/Leaflet/Leaflet/issues/3689, 2693 setTimeout(bind(this._onZoomTransitionEnd, this), 250); }, _onZoomTransitionEnd: function () { if (!this._animatingZoom) { return; } removeClass(this._mapPane, 'leaflet-zoom-anim'); this._animatingZoom = false; this._move(this._animateToCenter, this._animateToZoom); // This anim frame should prevent an obscure iOS webkit tile loading race condition. requestAnimFrame(function () { this._moveEnd(true); }, this); } }); // @section // @factory L.map(id: String, options?: Map options) // Instantiates a map object given the DOM ID of a `
    ` element // and optionally an object literal with `Map options`. // // @alternative // @factory L.map(el: HTMLElement, options?: Map options) // Instantiates a map object given an instance of a `
    ` HTML element // and optionally an object literal with `Map options`. function createMap(id, options) { return new Map(id, options); } /* * @class Control * @aka L.Control * @inherits Class * * L.Control is a base class for implementing map controls. Handles positioning. * All other controls extend from this class. */ var Control = Class.extend({ // @section // @aka Control options options: { // @option position: String = 'topright' // The position of the control (one of the map corners). Possible values are `'topleft'`, // `'topright'`, `'bottomleft'` or `'bottomright'` position: 'topright' }, initialize: function (options) { setOptions(this, options); }, /* @section * Classes extending L.Control will inherit the following methods: * * @method getPosition: string * Returns the position of the control. */ getPosition: function () { return this.options.position; }, // @method setPosition(position: string): this // Sets the position of the control. setPosition: function (position) { var map = this._map; if (map) { map.removeControl(this); } this.options.position = position; if (map) { map.addControl(this); } return this; }, // @method getContainer: HTMLElement // Returns the HTMLElement that contains the control. getContainer: function () { return this._container; }, // @method addTo(map: Map): this // Adds the control to the given map. addTo: function (map) { this.remove(); this._map = map; var container = this._container = this.onAdd(map), pos = this.getPosition(), corner = map._controlCorners[pos]; addClass(container, 'leaflet-control'); if (pos.indexOf('bottom') !== -1) { corner.insertBefore(container, corner.firstChild); } else { corner.appendChild(container); } return this; }, // @method remove: this // Removes the control from the map it is currently active on. remove: function () { if (!this._map) { return this; } remove(this._container); if (this.onRemove) { this.onRemove(this._map); } this._map = null; return this; }, _refocusOnMap: function (e) { // if map exists and event is not a keyboard event if (this._map && e && e.screenX > 0 && e.screenY > 0) { this._map.getContainer().focus(); } } }); var control = function (options) { return new Control(options); }; /* @section Extension methods * @uninheritable * * Every control should extend from `L.Control` and (re-)implement the following methods. * * @method onAdd(map: Map): HTMLElement * Should return the container DOM element for the control and add listeners on relevant map events. Called on [`control.addTo(map)`](#control-addTo). * * @method onRemove(map: Map) * Optional method. Should contain all clean up code that removes the listeners previously added in [`onAdd`](#control-onadd). Called on [`control.remove()`](#control-remove). */ /* @namespace Map * @section Methods for Layers and Controls */ Map.include({ // @method addControl(control: Control): this // Adds the given control to the map addControl: function (control) { control.addTo(this); return this; }, // @method removeControl(control: Control): this // Removes the given control from the map removeControl: function (control) { control.remove(); return this; }, _initControlPos: function () { var corners = this._controlCorners = {}, l = 'leaflet-', container = this._controlContainer = create$1('div', l + 'control-container', this._container); function createCorner(vSide, hSide) { var className = l + vSide + ' ' + l + hSide; corners[vSide + hSide] = create$1('div', className, container); } createCorner('top', 'left'); createCorner('top', 'right'); createCorner('bottom', 'left'); createCorner('bottom', 'right'); }, _clearControlPos: function () { for (var i in this._controlCorners) { remove(this._controlCorners[i]); } remove(this._controlContainer); delete this._controlCorners; delete this._controlContainer; } }); /* * @class Control.Layers * @aka L.Control.Layers * @inherits Control * * The layers control gives users the ability to switch between different base layers and switch overlays on/off (check out the [detailed example](http://leafletjs.com/examples/layers-control/)). Extends `Control`. * * @example * * ```js * var baseLayers = { * "Mapbox": mapbox, * "OpenStreetMap": osm * }; * * var overlays = { * "Marker": marker, * "Roads": roadsLayer * }; * * L.control.layers(baseLayers, overlays).addTo(map); * ``` * * The `baseLayers` and `overlays` parameters are object literals with layer names as keys and `Layer` objects as values: * * ```js * { * "": layer1, * "": layer2 * } * ``` * * The layer names can contain HTML, which allows you to add additional styling to the items: * * ```js * {" My Layer": myLayer} * ``` */ var Layers = Control.extend({ // @section // @aka Control.Layers options options: { // @option collapsed: Boolean = true // If `true`, the control will be collapsed into an icon and expanded on mouse hover or touch. collapsed: true, position: 'topright', // @option autoZIndex: Boolean = true // If `true`, the control will assign zIndexes in increasing order to all of its layers so that the order is preserved when switching them on/off. autoZIndex: true, // @option hideSingleBase: Boolean = false // If `true`, the base layers in the control will be hidden when there is only one. hideSingleBase: false, // @option sortLayers: Boolean = false // Whether to sort the layers. When `false`, layers will keep the order // in which they were added to the control. sortLayers: false, // @option sortFunction: Function = * // A [compare function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) // that will be used for sorting the layers, when `sortLayers` is `true`. // The function receives both the `L.Layer` instances and their names, as in // `sortFunction(layerA, layerB, nameA, nameB)`. // By default, it sorts layers alphabetically by their name. sortFunction: function (layerA, layerB, nameA, nameB) { return nameA < nameB ? -1 : (nameB < nameA ? 1 : 0); } }, initialize: function (baseLayers, overlays, options) { setOptions(this, options); this._layerControlInputs = []; 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(); this._map = map; map.on('zoomend', this._checkDisabledLayers, this); for (var i = 0; i < this._layers.length; i++) { this._layers[i].layer.on('add remove', this._onLayerChange, this); } return this._container; }, addTo: function (map) { Control.prototype.addTo.call(this, map); // Trigger expand after Layers Control has been inserted into DOM so that is now has an actual height. return this._expandIfNotCollapsed(); }, onRemove: function () { this._map.off('zoomend', this._checkDisabledLayers, this); for (var i = 0; i < this._layers.length; i++) { this._layers[i].layer.off('add remove', this._onLayerChange, this); } }, // @method addBaseLayer(layer: Layer, name: String): this // Adds a base layer (radio button entry) with the given name to the control. addBaseLayer: function (layer, name) { this._addLayer(layer, name); return (this._map) ? this._update() : this; }, // @method addOverlay(layer: Layer, name: String): this // Adds an overlay (checkbox entry) with the given name to the control. addOverlay: function (layer, name) { this._addLayer(layer, name, true); return (this._map) ? this._update() : this; }, // @method removeLayer(layer: Layer): this // Remove the given layer from the control. removeLayer: function (layer) { layer.off('add remove', this._onLayerChange, this); var obj = this._getLayer(stamp(layer)); if (obj) { this._layers.splice(this._layers.indexOf(obj), 1); } return (this._map) ? this._update() : this; }, // @method expand(): this // Expand the control container if collapsed. expand: function () { addClass(this._container, 'leaflet-control-layers-expanded'); this._form.style.height = null; var acceptableHeight = this._map.getSize().y - (this._container.offsetTop + 50); if (acceptableHeight < this._form.clientHeight) { addClass(this._form, 'leaflet-control-layers-scrollbar'); this._form.style.height = acceptableHeight + 'px'; } else { removeClass(this._form, 'leaflet-control-layers-scrollbar'); } this._checkDisabledLayers(); return this; }, // @method collapse(): this // Collapse the control container if expanded. collapse: function () { removeClass(this._container, 'leaflet-control-layers-expanded'); return this; }, _initLayout: function () { var className = 'leaflet-control-layers', container = this._container = create$1('div', className), collapsed = this.options.collapsed; // makes this work on IE touch devices by stopping it from firing a mouseout event when the touch is released container.setAttribute('aria-haspopup', true); disableClickPropagation(container); disableScrollPropagation(container); var form = this._form = create$1('form', className + '-list'); if (collapsed) { this._map.on('click', this.collapse, this); if (!android) { on(container, { mouseenter: this.expand, mouseleave: this.collapse }, this); } } var link = this._layersLink = create$1('a', className + '-toggle', container); link.href = '#'; link.title = 'Layers'; if (touch) { on(link, 'click', stop); on(link, 'click', this.expand, this); } else { on(link, 'focus', this.expand, this); } if (!collapsed) { this.expand(); } this._baseLayersList = create$1('div', className + '-base', form); this._separator = create$1('div', className + '-separator', form); this._overlaysList = create$1('div', className + '-overlays', form); container.appendChild(form); }, _getLayer: function (id) { for (var i = 0; i < this._layers.length; i++) { if (this._layers[i] && stamp(this._layers[i].layer) === id) { return this._layers[i]; } } }, _addLayer: function (layer, name, overlay) { if (this._map) { layer.on('add remove', this._onLayerChange, this); } this._layers.push({ layer: layer, name: name, overlay: overlay }); if (this.options.sortLayers) { this._layers.sort(bind(function (a, b) { return this.options.sortFunction(a.layer, b.layer, a.name, b.name); }, this)); } if (this.options.autoZIndex && layer.setZIndex) { this._lastZIndex++; layer.setZIndex(this._lastZIndex); } this._expandIfNotCollapsed(); }, _update: function () { if (!this._container) { return this; } empty(this._baseLayersList); empty(this._overlaysList); this._layerControlInputs = []; var baseLayersPresent, overlaysPresent, i, obj, baseLayersCount = 0; for (i = 0; i < this._layers.length; i++) { obj = this._layers[i]; this._addItem(obj); overlaysPresent = overlaysPresent || obj.overlay; baseLayersPresent = baseLayersPresent || !obj.overlay; baseLayersCount += !obj.overlay ? 1 : 0; } // Hide base layers section if there's only one layer. if (this.options.hideSingleBase) { baseLayersPresent = baseLayersPresent && baseLayersCount > 1; this._baseLayersList.style.display = baseLayersPresent ? '' : 'none'; } this._separator.style.display = overlaysPresent && baseLayersPresent ? '' : 'none'; return this; }, _onLayerChange: function (e) { if (!this._handlingClick) { this._update(); } var obj = this._getLayer(stamp(e.target)); // @namespace Map // @section Layer events // @event baselayerchange: LayersControlEvent // Fired when the base layer is changed through the [layer control](#control-layers). // @event overlayadd: LayersControlEvent // Fired when an overlay is selected through the [layer control](#control-layers). // @event overlayremove: LayersControlEvent // Fired when an overlay is deselected through the [layer control](#control-layers). // @namespace Control.Layers var type = obj.overlay ? (e.type === 'add' ? 'overlayadd' : 'overlayremove') : (e.type === 'add' ? '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 = ''; var radioFragment = document.createElement('div'); radioFragment.innerHTML = radioHtml; return radioFragment.firstChild; }, _addItem: function (obj) { var label = document.createElement('label'), checked = this._map.hasLayer(obj.layer), input; 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); } this._layerControlInputs.push(input); input.layerId = stamp(obj.layer); on(input, 'click', this._onInputClick, this); var name = document.createElement('span'); name.innerHTML = ' ' + obj.name; // Helps from preventing layer control flicker when checkboxes are disabled // https://github.com/Leaflet/Leaflet/issues/2771 var holder = document.createElement('div'); label.appendChild(holder); holder.appendChild(input); holder.appendChild(name); var container = obj.overlay ? this._overlaysList : this._baseLayersList; container.appendChild(label); this._checkDisabledLayers(); return label; }, _onInputClick: function () { var inputs = this._layerControlInputs, input, layer; var addedLayers = [], removedLayers = []; this._handlingClick = true; for (var i = inputs.length - 1; i >= 0; i--) { input = inputs[i]; layer = this._getLayer(input.layerId).layer; if (input.checked) { addedLayers.push(layer); } else if (!input.checked) { removedLayers.push(layer); } } // Bugfix issue 2318: Should remove all old layers before readding new ones for (i = 0; i < removedLayers.length; i++) { if (this._map.hasLayer(removedLayers[i])) { this._map.removeLayer(removedLayers[i]); } } for (i = 0; i < addedLayers.length; i++) { if (!this._map.hasLayer(addedLayers[i])) { this._map.addLayer(addedLayers[i]); } } this._handlingClick = false; this._refocusOnMap(); }, _checkDisabledLayers: function () { var inputs = this._layerControlInputs, input, layer, zoom = this._map.getZoom(); for (var i = inputs.length - 1; i >= 0; i--) { input = inputs[i]; layer = this._getLayer(input.layerId).layer; input.disabled = (layer.options.minZoom !== undefined && zoom < layer.options.minZoom) || (layer.options.maxZoom !== undefined && zoom > layer.options.maxZoom); } }, _expandIfNotCollapsed: function () { if (this._map && !this.options.collapsed) { this.expand(); } return this; }, _expand: function () { // Backward compatibility, remove me in 1.1. return this.expand(); }, _collapse: function () { // Backward compatibility, remove me in 1.1. return this.collapse(); } }); // @factory L.control.layers(baselayers?: Object, overlays?: Object, options?: Control.Layers options) // Creates an attribution control with the given layers. Base layers will be switched with radio buttons, while overlays will be switched with checkboxes. Note that all base layers should be passed in the base layers object, but only one should be added to the map during map instantiation. var layers = function (baseLayers, overlays, options) { return new Layers(baseLayers, overlays, options); }; /* * @class Control.Zoom * @aka L.Control.Zoom * @inherits Control * * A basic zoom control with two buttons (zoom in and zoom out). It is put on the map by default unless you set its [`zoomControl` option](#map-zoomcontrol) to `false`. Extends `Control`. */ var Zoom = Control.extend({ // @section // @aka Control.Zoom options options: { position: 'topleft', // @option zoomInText: String = '+' // The text set on the 'zoom in' button. zoomInText: '+', // @option zoomInTitle: String = 'Zoom in' // The title set on the 'zoom in' button. zoomInTitle: 'Zoom in', // @option zoomOutText: String = '−' // The text set on the 'zoom out' button. zoomOutText: '−', // @option zoomOutTitle: String = 'Zoom out' // The title set on the 'zoom out' button. zoomOutTitle: 'Zoom out' }, onAdd: function (map) { var zoomName = 'leaflet-control-zoom', container = create$1('div', zoomName + ' leaflet-bar'), options = this.options; this._zoomInButton = this._createButton(options.zoomInText, options.zoomInTitle, zoomName + '-in', container, this._zoomIn); this._zoomOutButton = this._createButton(options.zoomOutText, options.zoomOutTitle, zoomName + '-out', container, this._zoomOut); this._updateDisabled(); map.on('zoomend zoomlevelschange', this._updateDisabled, this); return container; }, onRemove: function (map) { map.off('zoomend zoomlevelschange', this._updateDisabled, this); }, disable: function () { this._disabled = true; this._updateDisabled(); return this; }, enable: function () { this._disabled = false; this._updateDisabled(); return this; }, _zoomIn: function (e) { if (!this._disabled && this._map._zoom < this._map.getMaxZoom()) { this._map.zoomIn(this._map.options.zoomDelta * (e.shiftKey ? 3 : 1)); } }, _zoomOut: function (e) { if (!this._disabled && this._map._zoom > this._map.getMinZoom()) { this._map.zoomOut(this._map.options.zoomDelta * (e.shiftKey ? 3 : 1)); } }, _createButton: function (html, title, className, container, fn) { var link = create$1('a', className, container); link.innerHTML = html; link.href = '#'; link.title = title; /* * Will force screen readers like VoiceOver to read this as "Zoom in - button" */ link.setAttribute('role', 'button'); link.setAttribute('aria-label', title); disableClickPropagation(link); on(link, 'click', stop); on(link, 'click', fn, this); on(link, 'click', this._refocusOnMap, this); return link; }, _updateDisabled: function () { var map = this._map, className = 'leaflet-disabled'; removeClass(this._zoomInButton, className); removeClass(this._zoomOutButton, className); if (this._disabled || map._zoom === map.getMinZoom()) { addClass(this._zoomOutButton, className); } if (this._disabled || map._zoom === map.getMaxZoom()) { addClass(this._zoomInButton, className); } } }); // @namespace Map // @section Control options // @option zoomControl: Boolean = true // Whether a [zoom control](#control-zoom) is added to the map by default. Map.mergeOptions({ zoomControl: true }); Map.addInitHook(function () { if (this.options.zoomControl) { this.zoomControl = new Zoom(); this.addControl(this.zoomControl); } }); // @namespace Control.Zoom // @factory L.control.zoom(options: Control.Zoom options) // Creates a zoom control var zoom = function (options) { return new Zoom(options); }; /* * @class Control.Scale * @aka L.Control.Scale * @inherits Control * * A simple scale control that shows the scale of the current center of screen in metric (m/km) and imperial (mi/ft) systems. Extends `Control`. * * @example * * ```js * L.control.scale().addTo(map); * ``` */ var Scale = Control.extend({ // @section // @aka Control.Scale options options: { position: 'bottomleft', // @option maxWidth: Number = 100 // Maximum width of the control in pixels. The width is set dynamically to show round values (e.g. 100, 200, 500). maxWidth: 100, // @option metric: Boolean = True // Whether to show the metric scale line (m/km). metric: true, // @option imperial: Boolean = True // Whether to show the imperial scale line (mi/ft). imperial: true // @option updateWhenIdle: Boolean = false // If `true`, the control is updated on [`moveend`](#map-moveend), otherwise it's always up-to-date (updated on [`move`](#map-move)). }, onAdd: function (map) { var className = 'leaflet-control-scale', container = create$1('div', className), options = this.options; this._addScales(options, className + '-line', 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 = create$1('div', className, container); } if (options.imperial) { this._iScale = create$1('div', className, container); } }, _update: function () { var map = this._map, y = map.getSize().y / 2; var maxMeters = map.distance( map.containerPointToLatLng([0, y]), map.containerPointToLatLng([this.options.maxWidth, y])); this._updateScales(maxMeters); }, _updateScales: function (maxMeters) { if (this.options.metric && maxMeters) { this._updateMetric(maxMeters); } if (this.options.imperial && maxMeters) { this._updateImperial(maxMeters); } }, _updateMetric: function (maxMeters) { var meters = this._getRoundNum(maxMeters), label = meters < 1000 ? meters + ' m' : (meters / 1000) + ' km'; this._updateScale(this._mScale, label, meters / maxMeters); }, _updateImperial: function (maxMeters) { var maxFeet = maxMeters * 3.2808399, maxMiles, miles, feet; if (maxFeet > 5280) { maxMiles = maxFeet / 5280; miles = this._getRoundNum(maxMiles); this._updateScale(this._iScale, miles + ' mi', miles / maxMiles); } else { feet = this._getRoundNum(maxFeet); this._updateScale(this._iScale, feet + ' ft', feet / maxFeet); } }, _updateScale: function (scale, text, ratio) { scale.style.width = Math.round(this.options.maxWidth * ratio) + 'px'; scale.innerHTML = text; }, _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; } }); // @factory L.control.scale(options?: Control.Scale options) // Creates an scale control with the given options. var scale = function (options) { return new Scale(options); }; /* * @class Control.Attribution * @aka L.Control.Attribution * @inherits Control * * The attribution control allows you to display attribution data in a small text box on a map. It is put on the map by default unless you set its [`attributionControl` option](#map-attributioncontrol) to `false`, and it fetches attribution texts from layers with the [`getAttribution` method](#layer-getattribution) automatically. Extends Control. */ var Attribution = Control.extend({ // @section // @aka Control.Attribution options options: { position: 'bottomright', // @option prefix: String = 'Leaflet' // The HTML text shown before the attributions. Pass `false` to disable. prefix: 'Leaflet' }, initialize: function (options) { setOptions(this, options); this._attributions = {}; }, onAdd: function (map) { map.attributionControl = this; this._container = create$1('div', 'leaflet-control-attribution'); disableClickPropagation(this._container); // TODO ugly, refactor for (var i in map._layers) { if (map._layers[i].getAttribution) { this.addAttribution(map._layers[i].getAttribution()); } } this._update(); return this._container; }, // @method setPrefix(prefix: String): this // Sets the text before the attributions. setPrefix: function (prefix) { this.options.prefix = prefix; this._update(); return this; }, // @method addAttribution(text: String): this // Adds an attribution text (e.g. `'Vector data © Mapbox'`). addAttribution: function (text) { if (!text) { return this; } if (!this._attributions[text]) { this._attributions[text] = 0; } this._attributions[text]++; this._update(); return this; }, // @method removeAttribution(text: String): this // Removes an attribution text. removeAttribution: function (text) { if (!text) { return this; } 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(' | '); } }); // @namespace Map // @section Control options // @option attributionControl: Boolean = true // Whether a [attribution control](#control-attribution) is added to the map by default. Map.mergeOptions({ attributionControl: true }); Map.addInitHook(function () { if (this.options.attributionControl) { new Attribution().addTo(this); } }); // @namespace Control.Attribution // @factory L.control.attribution(options: Control.Attribution options) // Creates an attribution control. var attribution = function (options) { return new Attribution(options); }; Control.Layers = Layers; Control.Zoom = Zoom; Control.Scale = Scale; Control.Attribution = Attribution; control.layers = layers; control.zoom = zoom; control.scale = scale; control.attribution = attribution; /* 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. */ // @class Handler // @aka L.Handler // Abstract class for map interaction handlers var Handler = Class.extend({ initialize: function (map) { this._map = map; }, // @method enable(): this // Enables the handler enable: function () { if (this._enabled) { return this; } this._enabled = true; this.addHooks(); return this; }, // @method disable(): this // Disables the handler disable: function () { if (!this._enabled) { return this; } this._enabled = false; this.removeHooks(); return this; }, // @method enabled(): Boolean // Returns `true` if the handler is enabled enabled: function () { return !!this._enabled; } // @section Extension methods // Classes inheriting from `Handler` must implement the two following methods: // @method addHooks() // Called when the handler is enabled, should add event hooks. // @method removeHooks() // Called when the handler is disabled, should remove the event hooks added previously. }); var Mixin = {Events: Events}; /* * @class Draggable * @aka L.Draggable * @inherits Evented * * A class for making DOM elements draggable (including touch support). * Used internally for map and marker dragging. Only works for elements * that were positioned with [`L.DomUtil.setPosition`](#domutil-setposition). * * @example * ```js * var draggable = new L.Draggable(elementToDrag); * draggable.enable(); * ``` */ var START = touch ? 'touchstart mousedown' : 'mousedown'; var END = { mousedown: 'mouseup', touchstart: 'touchend', pointerdown: 'touchend', MSPointerDown: 'touchend' }; var MOVE = { mousedown: 'mousemove', touchstart: 'touchmove', pointerdown: 'touchmove', MSPointerDown: 'touchmove' }; var Draggable = Evented.extend({ options: { // @section // @aka Draggable options // @option clickTolerance: Number = 3 // The max number of pixels a user can shift the mouse pointer during a click // for it to be considered a valid click (as opposed to a mouse drag). clickTolerance: 3 }, // @constructor L.Draggable(el: HTMLElement, dragHandle?: HTMLElement, preventOutline?: Boolean, options?: Draggable options) // Creates a `Draggable` object for moving `el` when you start dragging the `dragHandle` element (equals `el` itself by default). initialize: function (element, dragStartTarget, preventOutline$$1, options) { setOptions(this, options); this._element = element; this._dragStartTarget = dragStartTarget || element; this._preventOutline = preventOutline$$1; }, // @method enable() // Enables the dragging ability enable: function () { if (this._enabled) { return; } on(this._dragStartTarget, START, this._onDown, this); this._enabled = true; }, // @method disable() // Disables the dragging ability disable: function () { if (!this._enabled) { return; } // If we're currently dragging this draggable, // disabling it counts as first ending the drag. if (Draggable._dragging === this) { this.finishDrag(); } off(this._dragStartTarget, START, this._onDown, this); this._enabled = false; this._moved = false; }, _onDown: function (e) { // Ignore simulated events, since we handle both touch and // mouse explicitly; otherwise we risk getting duplicates of // touch events, see #4315. // Also ignore the event if disabled; this happens in IE11 // under some circumstances, see #3666. if (e._simulated || !this._enabled) { return; } this._moved = false; if (hasClass(this._element, 'leaflet-zoom-anim')) { return; } if (Draggable._dragging || e.shiftKey || ((e.which !== 1) && (e.button !== 1) && !e.touches)) { return; } Draggable._dragging = this; // Prevent dragging multiple objects at once. if (this._preventOutline) { preventOutline(this._element); } disableImageDrag(); disableTextSelection(); if (this._moving) { return; } // @event down: Event // Fired when a drag is about to start. this.fire('down'); var first = e.touches ? e.touches[0] : e; this._startPoint = new Point(first.clientX, first.clientY); on(document, MOVE[e.type], this._onMove, this); on(document, END[e.type], this._onUp, this); }, _onMove: function (e) { // Ignore simulated events, since we handle both touch and // mouse explicitly; otherwise we risk getting duplicates of // touch events, see #4315. // Also ignore the event if disabled; this happens in IE11 // under some circumstances, see #3666. if (e._simulated || !this._enabled) { return; } 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 Point(first.clientX, first.clientY), offset = newPoint.subtract(this._startPoint); if (!offset.x && !offset.y) { return; } if (Math.abs(offset.x) + Math.abs(offset.y) < this.options.clickTolerance) { return; } preventDefault(e); if (!this._moved) { // @event dragstart: Event // Fired when a drag starts this.fire('dragstart'); this._moved = true; this._startPos = getPosition(this._element).subtract(offset); addClass(document.body, 'leaflet-dragging'); this._lastTarget = e.target || e.srcElement; // IE and Edge do not give the element, so fetch it // if necessary if ((window.SVGElementInstance) && (this._lastTarget instanceof SVGElementInstance)) { this._lastTarget = this._lastTarget.correspondingUseElement; } addClass(this._lastTarget, 'leaflet-drag-target'); } this._newPos = this._startPos.add(offset); this._moving = true; cancelAnimFrame(this._animRequest); this._lastEvent = e; this._animRequest = requestAnimFrame(this._updatePosition, this, true); }, _updatePosition: function () { var e = {originalEvent: this._lastEvent}; // @event predrag: Event // Fired continuously during dragging *before* each corresponding // update of the element's position. this.fire('predrag', e); setPosition(this._element, this._newPos); // @event drag: Event // Fired continuously during dragging. this.fire('drag', e); }, _onUp: function (e) { // Ignore simulated events, since we handle both touch and // mouse explicitly; otherwise we risk getting duplicates of // touch events, see #4315. // Also ignore the event if disabled; this happens in IE11 // under some circumstances, see #3666. if (e._simulated || !this._enabled) { return; } this.finishDrag(); }, finishDrag: function () { removeClass(document.body, 'leaflet-dragging'); if (this._lastTarget) { removeClass(this._lastTarget, 'leaflet-drag-target'); this._lastTarget = null; } for (var i in MOVE) { off(document, MOVE[i], this._onMove, this); off(document, END[i], this._onUp, this); } enableImageDrag(); enableTextSelection(); if (this._moved && this._moving) { // ensure drag is not fired after dragend cancelAnimFrame(this._animRequest); // @event dragend: DragEndEvent // Fired when the drag ends. this.fire('dragend', { distance: this._newPos.distanceTo(this._startPos) }); } this._moving = false; Draggable._dragging = false; } }); /* * @namespace LineUtil * * Various utility functions for polyine points processing, used by Leaflet internally to make polylines lightning-fast. */ // Simplify polyline with vertex reduction and Douglas-Peucker simplification. // Improves rendering performance dramatically by lessening the number of points to draw. // @function simplify(points: Point[], tolerance: Number): Point[] // Dramatically reduces the number of points in a polyline while retaining // its shape and returns a new array of simplified points, using the // [Douglas-Peucker algorithm](http://en.wikipedia.org/wiki/Douglas-Peucker_algorithm). // Used for a huge performance boost when processing/displaying Leaflet polylines for // each zoom level and also reducing visual noise. tolerance affects the amount of // simplification (lesser value means higher quality but slower and with more points). // Also released as a separated micro-library [Simplify.js](http://mourner.github.com/simplify-js/). function simplify(points, tolerance) { if (!tolerance || !points.length) { return points.slice(); } var sqTolerance = tolerance * tolerance; // stage 1: vertex reduction points = _reducePoints(points, sqTolerance); // stage 2: Douglas-Peucker simplification points = _simplifyDP(points, sqTolerance); return points; } // @function pointToSegmentDistance(p: Point, p1: Point, p2: Point): Number // Returns the distance between point `p` and segment `p1` to `p2`. function pointToSegmentDistance(p, p1, p2) { return Math.sqrt(_sqClosestPointOnSegment(p, p1, p2, true)); } // @function closestPointOnSegment(p: Point, p1: Point, p2: Point): Number // Returns the closest point from a point `p` on a segment `p1` to `p2`. function closestPointOnSegment(p, p1, p2) { return _sqClosestPointOnSegment(p, p1, p2); } // Douglas-Peucker simplification, see http://en.wikipedia.org/wiki/Douglas-Peucker_algorithm function _simplifyDP(points, sqTolerance) { var len = points.length, ArrayConstructor = typeof Uint8Array !== undefined + '' ? Uint8Array : Array, markers = new ArrayConstructor(len); markers[0] = markers[len - 1] = 1; _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; } function _simplifyDPStep(points, markers, sqTolerance, first, last) { var maxSqDist = 0, index, i, sqDist; for (i = first + 1; i <= last - 1; i++) { sqDist = _sqClosestPointOnSegment(points[i], points[first], points[last], true); if (sqDist > maxSqDist) { index = i; maxSqDist = sqDist; } } if (maxSqDist > sqTolerance) { markers[index] = 1; _simplifyDPStep(points, markers, sqTolerance, first, index); _simplifyDPStep(points, markers, sqTolerance, index, last); } } // reduce points that are too close to each other to a single point function _reducePoints(points, sqTolerance) { var reducedPoints = [points[0]]; for (var i = 1, prev = 0, len = points.length; i < len; i++) { if (_sqDist(points[i], points[prev]) > sqTolerance) { reducedPoints.push(points[i]); prev = i; } } if (prev < len - 1) { reducedPoints.push(points[len - 1]); } return reducedPoints; } var _lastCode; // @function clipSegment(a: Point, b: Point, bounds: Bounds, useLastCode?: Boolean, round?: Boolean): Point[]|Boolean // Clips the segment a to b by rectangular bounds with the // [Cohen-Sutherland algorithm](https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm) // (modifying the segment points directly!). Used by Leaflet to only show polyline // points that are on the screen or near, increasing performance. function clipSegment(a, b, bounds, useLastCode, round) { var codeA = useLastCode ? _lastCode : _getBitCode(a, bounds), codeB = _getBitCode(b, bounds), codeOut, p, newCode; // save 2nd code to avoid calculating it on the next segment _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) if (codeA & codeB) { return false; } // other cases codeOut = codeA || codeB; p = _getEdgeIntersection(a, b, codeOut, bounds, round); newCode = _getBitCode(p, bounds); if (codeOut === codeA) { a = p; codeA = newCode; } else { b = p; codeB = newCode; } } } function _getEdgeIntersection(a, b, code, bounds, round) { var dx = b.x - a.x, dy = b.y - a.y, min = bounds.min, max = bounds.max, x, y; if (code & 8) { // top x = a.x + dx * (max.y - a.y) / dy; y = max.y; } else if (code & 4) { // bottom x = a.x + dx * (min.y - a.y) / dy; y = min.y; } else if (code & 2) { // right x = max.x; y = a.y + dy * (max.x - a.x) / dx; } else if (code & 1) { // left x = min.x; y = a.y + dy * (min.x - a.x) / dx; } return new Point(x, y, round); } function _getBitCode(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) function _sqDist(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 function _sqClosestPointOnSegment(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 Point(x, y); } // @function isFlat(latlngs: LatLng[]): Boolean // Returns true if `latlngs` is a flat array, false is nested. function isFlat(latlngs) { return !isArray(latlngs[0]) || (typeof latlngs[0][0] !== 'object' && typeof latlngs[0][0] !== 'undefined'); } function _flat(latlngs) { console.warn('Deprecated use of _flat, please use L.LineUtil.isFlat instead.'); return isFlat(latlngs); } var LineUtil = (Object.freeze || Object)({ simplify: simplify, pointToSegmentDistance: pointToSegmentDistance, closestPointOnSegment: closestPointOnSegment, clipSegment: clipSegment, _getEdgeIntersection: _getEdgeIntersection, _getBitCode: _getBitCode, _sqClosestPointOnSegment: _sqClosestPointOnSegment, isFlat: isFlat, _flat: _flat }); /* * @namespace PolyUtil * Various utility functions for polygon geometries. */ /* @function clipPolygon(points: Point[], bounds: Bounds, round?: Boolean): Point[] * Clips the polygon geometry defined by the given `points` by the given bounds (using the [Sutherland-Hodgeman algorithm](https://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm)). * Used by Leaflet to only show polygon points that are on the screen or near, increasing * performance. Note that polygon points needs different algorithm for clipping * than polyline, so there's a seperate method for it. */ function clipPolygon(points, bounds, round) { var clippedPoints, edges = [1, 4, 2, 8], i, j, k, a, b, len, edge, p; for (i = 0, len = points.length; i < len; i++) { points[i]._code = _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 = _getEdgeIntersection(b, a, edge, bounds, round); p._code = _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 = _getEdgeIntersection(b, a, edge, bounds, round); p._code = _getBitCode(p, bounds); clippedPoints.push(p); } } points = clippedPoints; } return points; } var PolyUtil = (Object.freeze || Object)({ clipPolygon: clipPolygon }); /* * @namespace Projection * @section * Leaflet comes with a set of already defined Projections out of the box: * * @projection L.Projection.LonLat * * Equirectangular, or Plate Carree projection — the most simple projection, * mostly used by GIS enthusiasts. Directly maps `x` as longitude, and `y` as * latitude. Also suitable for flat worlds, e.g. game maps. Used by the * `EPSG:4326` and `Simple` CRS. */ var LonLat = { project: function (latlng) { return new Point(latlng.lng, latlng.lat); }, unproject: function (point) { return new LatLng(point.y, point.x); }, bounds: new Bounds([-180, -90], [180, 90]) }; /* * @namespace Projection * @projection L.Projection.Mercator * * Elliptical Mercator projection — more complex than Spherical Mercator. Takes into account that Earth is a geoid, not a perfect sphere. Used by the EPSG:3395 CRS. */ var Mercator = { R: 6378137, R_MINOR: 6356752.314245179, bounds: new Bounds([-20037508.34279, -15496570.73972], [20037508.34279, 18764656.23138]), project: function (latlng) { var d = Math.PI / 180, r = this.R, y = latlng.lat * d, tmp = this.R_MINOR / r, e = Math.sqrt(1 - tmp * tmp), con = e * Math.sin(y); var ts = Math.tan(Math.PI / 4 - y / 2) / Math.pow((1 - con) / (1 + con), e / 2); y = -r * Math.log(Math.max(ts, 1E-10)); return new Point(latlng.lng * d * r, y); }, unproject: function (point) { var d = 180 / Math.PI, r = this.R, tmp = this.R_MINOR / r, e = Math.sqrt(1 - tmp * tmp), ts = Math.exp(-point.y / r), phi = Math.PI / 2 - 2 * Math.atan(ts); for (var i = 0, dphi = 0.1, con; i < 15 && Math.abs(dphi) > 1e-7; i++) { con = e * Math.sin(phi); con = Math.pow((1 - con) / (1 + con), e / 2); dphi = Math.PI / 2 - 2 * Math.atan(ts * con) - phi; phi += dphi; } return new LatLng(phi * d, point.x * d / r); } }; /* * @class Projection * An object with methods for projecting geographical coordinates of the world onto * a flat surface (and back). See [Map projection](http://en.wikipedia.org/wiki/Map_projection). * @property bounds: Bounds * The bounds (specified in CRS units) where the projection is valid * @method project(latlng: LatLng): Point * Projects geographical coordinates into a 2D point. * Only accepts actual `L.LatLng` instances, not arrays. * @method unproject(point: Point): LatLng * The inverse of `project`. Projects a 2D point into a geographical location. * Only accepts actual `L.Point` instances, not arrays. */ var index = (Object.freeze || Object)({ LonLat: LonLat, Mercator: Mercator, SphericalMercator: SphericalMercator }); /* * @namespace CRS * @crs L.CRS.EPSG3395 * * Rarely used by some commercial tile providers. Uses Elliptical Mercator projection. */ var EPSG3395 = extend({}, Earth, { code: 'EPSG:3395', projection: Mercator, transformation: (function () { var scale = 0.5 / (Math.PI * Mercator.R); return toTransformation(scale, 0.5, -scale, 0.5); }()) }); /* * @namespace CRS * @crs L.CRS.EPSG4326 * * A common CRS among GIS enthusiasts. Uses simple Equirectangular projection. * * Leaflet 1.0.x complies with the [TMS coordinate scheme for EPSG:4326](https://wiki.osgeo.org/wiki/Tile_Map_Service_Specification#global-geodetic), * which is a breaking change from 0.7.x behaviour. If you are using a `TileLayer` * with this CRS, ensure that there are two 256x256 pixel tiles covering the * whole earth at zoom level zero, and that the tile coordinate origin is (-180,+90), * or (-180,-90) for `TileLayer`s with [the `tms` option](#tilelayer-tms) set. */ var EPSG4326 = extend({}, Earth, { code: 'EPSG:4326', projection: LonLat, transformation: toTransformation(1 / 180, 1, -1 / 180, 0.5) }); /* * @namespace CRS * @crs L.CRS.Simple * * A simple CRS that maps longitude and latitude into `x` and `y` directly. * May be used for maps of flat surfaces (e.g. game maps). Note that the `y` * axis should still be inverted (going from bottom to top). `distance()` returns * simple euclidean distance. */ var Simple = extend({}, CRS, { projection: LonLat, transformation: toTransformation(1, 0, -1, 0), scale: function (zoom) { return Math.pow(2, zoom); }, zoom: function (scale) { return Math.log(scale) / Math.LN2; }, distance: function (latlng1, latlng2) { var dx = latlng2.lng - latlng1.lng, dy = latlng2.lat - latlng1.lat; return Math.sqrt(dx * dx + dy * dy); }, infinite: true }); CRS.Earth = Earth; CRS.EPSG3395 = EPSG3395; CRS.EPSG3857 = EPSG3857; CRS.EPSG900913 = EPSG900913; CRS.EPSG4326 = EPSG4326; CRS.Simple = Simple; /* * @class Layer * @inherits Evented * @aka L.Layer * @aka ILayer * * A set of methods from the Layer base class that all Leaflet layers use. * Inherits all methods, options and events from `L.Evented`. * * @example * * ```js * var layer = L.Marker(latlng).addTo(map); * layer.addTo(map); * layer.remove(); * ``` * * @event add: Event * Fired after the layer is added to a map * * @event remove: Event * Fired after the layer is removed from a map */ var Layer = Evented.extend({ // Classes extending `L.Layer` will inherit the following options: options: { // @option pane: String = 'overlayPane' // By default the layer will be added to the map's [overlay pane](#map-overlaypane). Overriding this option will cause the layer to be placed on another pane by default. pane: 'overlayPane', // @option attribution: String = null // String to be shown in the attribution control, describes the layer data, e.g. "© Mapbox". attribution: null, bubblingMouseEvents: true }, /* @section * Classes extending `L.Layer` will inherit the following methods: * * @method addTo(map: Map|LayerGroup): this * Adds the layer to the given map or layer group. */ addTo: function (map) { map.addLayer(this); return this; }, // @method remove: this // Removes the layer from the map it is currently active on. remove: function () { return this.removeFrom(this._map || this._mapToAdd); }, // @method removeFrom(map: Map): this // Removes the layer from the given map removeFrom: function (obj) { if (obj) { obj.removeLayer(this); } return this; }, // @method getPane(name? : String): HTMLElement // Returns the `HTMLElement` representing the named pane on the map. If `name` is omitted, returns the pane for this layer. getPane: function (name) { return this._map.getPane(name ? (this.options[name] || name) : this.options.pane); }, addInteractiveTarget: function (targetEl) { this._map._targets[stamp(targetEl)] = this; return this; }, removeInteractiveTarget: function (targetEl) { delete this._map._targets[stamp(targetEl)]; return this; }, // @method getAttribution: String // Used by the `attribution control`, returns the [attribution option](#gridlayer-attribution). getAttribution: function () { return this.options.attribution; }, _layerAdd: function (e) { var map = e.target; // check in case layer gets added and then removed before the map is ready if (!map.hasLayer(this)) { return; } this._map = map; this._zoomAnimated = map._zoomAnimated; if (this.getEvents) { var events = this.getEvents(); map.on(events, this); this.once('remove', function () { map.off(events, this); }, this); } this.onAdd(map); if (this.getAttribution && map.attributionControl) { map.attributionControl.addAttribution(this.getAttribution()); } this.fire('add'); map.fire('layeradd', {layer: this}); } }); /* @section Extension methods * @uninheritable * * Every layer should extend from `L.Layer` and (re-)implement the following methods. * * @method onAdd(map: Map): this * Should contain code that creates DOM elements for the layer, adds them to `map panes` where they should belong and puts listeners on relevant map events. Called on [`map.addLayer(layer)`](#map-addlayer). * * @method onRemove(map: Map): this * Should contain all clean up code that removes the layer's elements from the DOM and removes listeners previously added in [`onAdd`](#layer-onadd). Called on [`map.removeLayer(layer)`](#map-removelayer). * * @method getEvents(): Object * This optional method should return an object like `{ viewreset: this._reset }` for [`addEventListener`](#evented-addeventlistener). The event handlers in this object will be automatically added and removed from the map with your layer. * * @method getAttribution(): String * This optional method should return a string containing HTML to be shown on the `Attribution control` whenever the layer is visible. * * @method beforeAdd(map: Map): this * Optional method. Called on [`map.addLayer(layer)`](#map-addlayer), before the layer is added to the map, before events are initialized, without waiting until the map is in a usable state. Use for early initialization only. */ /* @namespace Map * @section Layer events * * @event layeradd: LayerEvent * Fired when a new layer is added to the map. * * @event layerremove: LayerEvent * Fired when some layer is removed from the map * * @section Methods for Layers and Controls */ Map.include({ // @method addLayer(layer: Layer): this // Adds the given layer to the map addLayer: function (layer) { if (!layer._layerAdd) { throw new Error('The provided object is not a Layer.'); } var id = stamp(layer); if (this._layers[id]) { return this; } this._layers[id] = layer; layer._mapToAdd = this; if (layer.beforeAdd) { layer.beforeAdd(this); } this.whenReady(layer._layerAdd, layer); return this; }, // @method removeLayer(layer: Layer): this // Removes the given layer from the map. removeLayer: function (layer) { var id = stamp(layer); if (!this._layers[id]) { return this; } if (this._loaded) { layer.onRemove(this); } if (layer.getAttribution && this.attributionControl) { this.attributionControl.removeAttribution(layer.getAttribution()); } delete this._layers[id]; if (this._loaded) { this.fire('layerremove', {layer: layer}); layer.fire('remove'); } layer._map = layer._mapToAdd = null; return this; }, // @method hasLayer(layer: Layer): Boolean // Returns `true` if the given layer is currently added to the map hasLayer: function (layer) { return !!layer && (stamp(layer) in this._layers); }, /* @method eachLayer(fn: Function, context?: Object): this * Iterates over the layers of the map, optionally specifying context of the iterator function. * ``` * map.eachLayer(function(layer){ * layer.bindPopup('Hello'); * }); * ``` */ eachLayer: function (method, context) { for (var i in this._layers) { method.call(context, this._layers[i]); } return this; }, _addLayers: function (layers) { layers = layers ? (isArray(layers) ? layers : [layers]) : []; for (var i = 0, len = layers.length; i < len; i++) { this.addLayer(layers[i]); } }, _addZoomLimit: function (layer) { if (isNaN(layer.options.maxZoom) || !isNaN(layer.options.minZoom)) { this._zoomBoundLayers[stamp(layer)] = layer; this._updateZoomLevels(); } }, _removeZoomLimit: function (layer) { var id = stamp(layer); if (this._zoomBoundLayers[id]) { delete this._zoomBoundLayers[id]; this._updateZoomLevels(); } }, _updateZoomLevels: function () { var minZoom = Infinity, maxZoom = -Infinity, oldZoomSpan = this._getZoomSpan(); for (var i in this._zoomBoundLayers) { var options = this._zoomBoundLayers[i].options; minZoom = options.minZoom === undefined ? minZoom : Math.min(minZoom, options.minZoom); maxZoom = options.maxZoom === undefined ? maxZoom : Math.max(maxZoom, options.maxZoom); } this._layersMaxZoom = maxZoom === -Infinity ? undefined : maxZoom; this._layersMinZoom = minZoom === Infinity ? undefined : minZoom; // @section Map state change events // @event zoomlevelschange: Event // Fired when the number of zoomlevels on the map is changed due // to adding or removing a layer. if (oldZoomSpan !== this._getZoomSpan()) { this.fire('zoomlevelschange'); } if (this.options.maxZoom === undefined && this._layersMaxZoom && this.getZoom() > this._layersMaxZoom) { this.setZoom(this._layersMaxZoom); } if (this.options.minZoom === undefined && this._layersMinZoom && this.getZoom() < this._layersMinZoom) { this.setZoom(this._layersMinZoom); } } }); /* * @class LayerGroup * @aka L.LayerGroup * @inherits Layer * * Used to group several layers and handle them as one. If you add it to the map, * any layers added or removed from the group will be added/removed on the map as * well. Extends `Layer`. * * @example * * ```js * L.layerGroup([marker1, marker2]) * .addLayer(polyline) * .addTo(map); * ``` */ var LayerGroup = Layer.extend({ initialize: function (layers) { this._layers = {}; var i, len; if (layers) { for (i = 0, len = layers.length; i < len; i++) { this.addLayer(layers[i]); } } }, // @method addLayer(layer: Layer): this // Adds the given layer to the group. addLayer: function (layer) { var id = this.getLayerId(layer); this._layers[id] = layer; if (this._map) { this._map.addLayer(layer); } return this; }, // @method removeLayer(layer: Layer): this // Removes the given layer from the group. // @alternative // @method removeLayer(id: Number): this // Removes the layer with the given internal ID from the group. 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; }, // @method hasLayer(layer: Layer): Boolean // Returns `true` if the given layer is currently added to the group. // @alternative // @method hasLayer(id: Number): Boolean // Returns `true` if the given internal ID is currently added to the group. hasLayer: function (layer) { return !!layer && (layer in this._layers || this.getLayerId(layer) in this._layers); }, // @method clearLayers(): this // Removes all the layers from the group. clearLayers: function () { for (var i in this._layers) { this.removeLayer(this._layers[i]); } return this; }, // @method invoke(methodName: String, …): this // Calls `methodName` on every layer contained in this group, passing any // additional parameters. Has no effect if the layers contained do not // implement `methodName`. 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) { for (var i in this._layers) { map.addLayer(this._layers[i]); } }, onRemove: function (map) { for (var i in this._layers) { map.removeLayer(this._layers[i]); } }, // @method eachLayer(fn: Function, context?: Object): this // Iterates over the layers of the group, optionally specifying context of the iterator function. // ```js // group.eachLayer(function (layer) { // layer.bindPopup('Hello'); // }); // ``` eachLayer: function (method, context) { for (var i in this._layers) { method.call(context, this._layers[i]); } return this; }, // @method getLayer(id: Number): Layer // Returns the layer with the given internal ID. getLayer: function (id) { return this._layers[id]; }, // @method getLayers(): Layer[] // Returns an array of all the layers added to the group. getLayers: function () { var layers = []; for (var i in this._layers) { layers.push(this._layers[i]); } return layers; }, // @method setZIndex(zIndex: Number): this // Calls `setZIndex` on every layer contained in this group, passing the z-index. setZIndex: function (zIndex) { return this.invoke('setZIndex', zIndex); }, // @method getLayerId(layer: Layer): Number // Returns the internal ID for a layer getLayerId: function (layer) { return stamp(layer); } }); // @factory L.layerGroup(layers?: Layer[]) // Create a layer group, optionally given an initial set of layers. var layerGroup = function (layers) { return new LayerGroup(layers); }; /* * @class FeatureGroup * @aka L.FeatureGroup * @inherits LayerGroup * * Extended `LayerGroup` that makes it easier to do the same thing to all its member layers: * * [`bindPopup`](#layer-bindpopup) binds a popup to all of the layers at once (likewise with [`bindTooltip`](#layer-bindtooltip)) * * Events are propagated to the `FeatureGroup`, so if the group has an event * handler, it will handle events from any of the layers. This includes mouse events * and custom events. * * Has `layeradd` and `layerremove` events * * @example * * ```js * L.featureGroup([marker1, marker2, polyline]) * .bindPopup('Hello world!') * .on('click', function() { alert('Clicked on a member of the group!'); }) * .addTo(map); * ``` */ var FeatureGroup = LayerGroup.extend({ addLayer: function (layer) { if (this.hasLayer(layer)) { return this; } layer.addEventParent(this); LayerGroup.prototype.addLayer.call(this, layer); // @event layeradd: LayerEvent // Fired when a layer is added to this `FeatureGroup` return this.fire('layeradd', {layer: layer}); }, removeLayer: function (layer) { if (!this.hasLayer(layer)) { return this; } if (layer in this._layers) { layer = this._layers[layer]; } layer.removeEventParent(this); LayerGroup.prototype.removeLayer.call(this, layer); // @event layerremove: LayerEvent // Fired when a layer is removed from this `FeatureGroup` return this.fire('layerremove', {layer: layer}); }, // @method setStyle(style: Path options): this // Sets the given path options to each layer of the group that has a `setStyle` method. setStyle: function (style) { return this.invoke('setStyle', style); }, // @method bringToFront(): this // Brings the layer group to the top of all other layers bringToFront: function () { return this.invoke('bringToFront'); }, // @method bringToBack(): this // Brings the layer group to the top of all other layers bringToBack: function () { return this.invoke('bringToBack'); }, // @method getBounds(): LatLngBounds // Returns the LatLngBounds of the Feature Group (created from bounds and coordinates of its children). getBounds: function () { var bounds = new LatLngBounds(); for (var id in this._layers) { var layer = this._layers[id]; bounds.extend(layer.getBounds ? layer.getBounds() : layer.getLatLng()); } return bounds; } }); // @factory L.featureGroup(layers: Layer[]) // Create a feature group, optionally given an initial set of layers. var featureGroup = function (layers) { return new FeatureGroup(layers); }; /* * @class Icon * @aka L.Icon * * Represents an icon to provide when creating a marker. * * @example * * ```js * var myIcon = L.icon({ * iconUrl: 'my-icon.png', * iconRetinaUrl: 'my-icon@2x.png', * iconSize: [38, 95], * iconAnchor: [22, 94], * popupAnchor: [-3, -76], * shadowUrl: 'my-icon-shadow.png', * shadowRetinaUrl: 'my-icon-shadow@2x.png', * shadowSize: [68, 95], * shadowAnchor: [22, 94] * }); * * L.marker([50.505, 30.57], {icon: myIcon}).addTo(map); * ``` * * `L.Icon.Default` extends `L.Icon` and is the blue icon Leaflet uses for markers by default. * */ var Icon = Class.extend({ /* @section * @aka Icon options * * @option iconUrl: String = null * **(required)** The URL to the icon image (absolute or relative to your script path). * * @option iconRetinaUrl: String = null * The URL to a retina sized version of the icon image (absolute or relative to your * script path). Used for Retina screen devices. * * @option iconSize: Point = null * Size of the icon image in pixels. * * @option iconAnchor: Point = null * The coordinates of the "tip" of the icon (relative to its top left corner). The icon * will be aligned so that this point is at the marker's geographical location. Centered * by default if size is specified, also can be set in CSS with negative margins. * * @option popupAnchor: Point = null * The coordinates of the point from which popups will "open", relative to the icon anchor. * * @option shadowUrl: String = null * The URL to the icon shadow image. If not specified, no shadow image will be created. * * @option shadowRetinaUrl: String = null * * @option shadowSize: Point = null * Size of the shadow image in pixels. * * @option shadowAnchor: Point = null * The coordinates of the "tip" of the shadow (relative to its top left corner) (the same * as iconAnchor if not specified). * * @option className: String = '' * A custom class name to assign to both icon and shadow images. Empty by default. */ initialize: function (options) { setOptions(this, options); }, // @method createIcon(oldIcon?: HTMLElement): HTMLElement // Called internally when the icon has to be shown, returns a `` HTML element // styled according to the options. createIcon: function (oldIcon) { return this._createIcon('icon', oldIcon); }, // @method createShadow(oldIcon?: HTMLElement): HTMLElement // As `createIcon`, but for the shadow beneath it. 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 = this._createImg(src, oldIcon && oldIcon.tagName === 'IMG' ? oldIcon : null); this._setIconStyles(img, name); return img; }, _setIconStyles: function (img, name) { var options = this.options; var sizeOption = options[name + 'Size']; if (typeof sizeOption === 'number') { sizeOption = [sizeOption, sizeOption]; } var size = toPoint(sizeOption), anchor = toPoint(name === 'shadow' && options.shadowAnchor || options.iconAnchor || size && 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) { return retina && this.options[name + 'RetinaUrl'] || this.options[name + 'Url']; } }); // @factory L.icon(options: Icon options) // Creates an icon instance with the given options. function icon(options) { return new Icon(options); } /* * @miniclass Icon.Default (Icon) * @aka L.Icon.Default * @section * * A trivial subclass of `Icon`, represents the icon to use in `Marker`s when * no icon is specified. Points to the blue marker image distributed with Leaflet * releases. * * In order to customize the default icon, just change the properties of `L.Icon.Default.prototype.options` * (which is a set of `Icon options`). * * If you want to _completely_ replace the default icon, override the * `L.Marker.prototype.options.icon` with your own icon instead. */ var IconDefault = Icon.extend({ options: { iconUrl: 'marker-icon.png', iconRetinaUrl: 'marker-icon-2x.png', shadowUrl: 'marker-shadow.png', iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], tooltipAnchor: [16, -28], shadowSize: [41, 41] }, _getIconUrl: function (name) { if (!IconDefault.imagePath) { // Deprecated, backwards-compatibility only IconDefault.imagePath = this._detectIconPath(); } // @option imagePath: String // `Icon.Default` will try to auto-detect the absolute location of the // blue icon images. If you are placing these images in a non-standard // way, set this option to point to the right absolute path. return (this.options.imagePath || IconDefault.imagePath) + Icon.prototype._getIconUrl.call(this, name); }, _detectIconPath: function () { var el = create$1('div', 'leaflet-default-icon-path', document.body); var path = getStyle(el, 'background-image') || getStyle(el, 'backgroundImage'); // IE8 document.body.removeChild(el); if (path === null || path.indexOf('url') !== 0) { path = ''; } else { path = path.replace(/^url\([\"\']?/, '').replace(/marker-icon\.png[\"\']?\)$/, ''); } return path; } }); /* * L.Handler.MarkerDrag is used internally by L.Marker to make the markers draggable. */ /* @namespace Marker * @section Interaction handlers * * Interaction handlers are properties of a marker instance that allow you to control interaction behavior in runtime, enabling or disabling certain features such as dragging (see `Handler` methods). Example: * * ```js * marker.dragging.disable(); * ``` * * @property dragging: Handler * Marker dragging handler (by both mouse and touch). Only valid when the marker is on the map (Otherwise set [`marker.options.draggable`](#marker-draggable)). */ var MarkerDrag = Handler.extend({ initialize: function (marker) { this._marker = marker; }, addHooks: function () { var icon = this._marker._icon; if (!this._draggable) { this._draggable = new Draggable(icon, icon, true); } this._draggable.on({ dragstart: this._onDragStart, drag: this._onDrag, dragend: this._onDragEnd }, this).enable(); addClass(icon, 'leaflet-marker-draggable'); }, removeHooks: function () { this._draggable.off({ dragstart: this._onDragStart, drag: this._onDrag, dragend: this._onDragEnd }, this).disable(); if (this._marker._icon) { removeClass(this._marker._icon, 'leaflet-marker-draggable'); } }, moved: function () { return this._draggable && this._draggable._moved; }, _onDragStart: function () { // @section Dragging events // @event dragstart: Event // Fired when the user starts dragging the marker. // @event movestart: Event // Fired when the marker starts moving (because of dragging). this._oldLatLng = this._marker.getLatLng(); this._marker .closePopup() .fire('movestart') .fire('dragstart'); }, _onDrag: function (e) { var marker = this._marker, shadow = marker._shadow, iconPos = getPosition(marker._icon), latlng = marker._map.layerPointToLatLng(iconPos); // update shadow position if (shadow) { setPosition(shadow, iconPos); } marker._latlng = latlng; e.latlng = latlng; e.oldLatLng = this._oldLatLng; // @event drag: Event // Fired repeatedly while the user drags the marker. marker .fire('move', e) .fire('drag', e); }, _onDragEnd: function (e) { // @event dragend: DragEndEvent // Fired when the user stops dragging the marker. // @event moveend: Event // Fired when the marker stops moving (because of dragging). delete this._oldLatLng; this._marker .fire('moveend') .fire('dragend', e); } }); /* * @class Marker * @inherits Interactive layer * @aka L.Marker * L.Marker is used to display clickable/draggable icons on the map. Extends `Layer`. * * @example * * ```js * L.marker([50.5, 30.5]).addTo(map); * ``` */ var Marker = Layer.extend({ // @section // @aka Marker options options: { // @option icon: Icon = * // Icon instance to use for rendering the marker. // See [Icon documentation](#L.Icon) for details on how to customize the marker icon. // If not specified, a common instance of `L.Icon.Default` is used. icon: new IconDefault(), // Option inherited from "Interactive layer" abstract class interactive: true, // @option draggable: Boolean = false // Whether the marker is draggable with mouse/touch or not. draggable: false, // @option keyboard: Boolean = true // Whether the marker can be tabbed to with a keyboard and clicked by pressing enter. keyboard: true, // @option title: String = '' // Text for the browser tooltip that appear on marker hover (no tooltip by default). title: '', // @option alt: String = '' // Text for the `alt` attribute of the icon image (useful for accessibility). alt: '', // @option zIndexOffset: Number = 0 // By default, marker images zIndex is set automatically based on its latitude. Use this option if you want to put the marker on top of all others (or below), specifying a high value like `1000` (or high negative value, respectively). zIndexOffset: 0, // @option opacity: Number = 1.0 // The opacity of the marker. opacity: 1, // @option riseOnHover: Boolean = false // If `true`, the marker will get on top of others when you hover the mouse over it. riseOnHover: false, // @option riseOffset: Number = 250 // The z-index offset used for the `riseOnHover` feature. riseOffset: 250, // @option pane: String = 'markerPane' // `Map pane` where the markers icon will be added. pane: 'markerPane', // @option bubblingMouseEvents: Boolean = false // When `true`, a mouse event on this marker will trigger the same event on the map // (unless [`L.DomEvent.stopPropagation`](#domevent-stoppropagation) is used). bubblingMouseEvents: false }, /* @section * * In addition to [shared layer methods](#Layer) like `addTo()` and `remove()` and [popup methods](#Popup) like bindPopup() you can also use the following methods: */ initialize: function (latlng, options) { setOptions(this, options); this._latlng = toLatLng(latlng); }, onAdd: function (map) { this._zoomAnimated = this._zoomAnimated && map.options.markerZoomAnimation; if (this._zoomAnimated) { map.on('zoomanim', this._animateZoom, this); } this._initIcon(); this.update(); }, onRemove: function (map) { if (this.dragging && this.dragging.enabled()) { this.options.draggable = true; this.dragging.removeHooks(); } delete this.dragging; if (this._zoomAnimated) { map.off('zoomanim', this._animateZoom, this); } this._removeIcon(); this._removeShadow(); }, getEvents: function () { return { zoom: this.update, viewreset: this.update }; }, // @method getLatLng: LatLng // Returns the current geographical position of the marker. getLatLng: function () { return this._latlng; }, // @method setLatLng(latlng: LatLng): this // Changes the marker position to the given point. setLatLng: function (latlng) { var oldLatLng = this._latlng; this._latlng = toLatLng(latlng); this.update(); // @event move: Event // Fired when the marker is moved via [`setLatLng`](#marker-setlatlng) or by [dragging](#marker-dragging). Old and new coordinates are included in event arguments as `oldLatLng`, `latlng`. return this.fire('move', {oldLatLng: oldLatLng, latlng: this._latlng}); }, // @method setZIndexOffset(offset: Number): this // Changes the [zIndex offset](#marker-zindexoffset) of the marker. setZIndexOffset: function (offset) { this.options.zIndexOffset = offset; return this.update(); }, // @method setIcon(icon: Icon): this // Changes the marker icon. setIcon: function (icon) { this.options.icon = icon; if (this._map) { this._initIcon(); this.update(); } if (this._popup) { this.bindPopup(this._popup, this._popup.options); } return this; }, getElement: function () { return this._icon; }, update: function () { if (this._icon) { var pos = this._map.latLngToLayerPoint(this._latlng).round(); this._setPos(pos); } return this; }, _initIcon: function () { var options = this.options, classToAdd = 'leaflet-zoom-' + (this._zoomAnimated ? 'animated' : '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; } } addClass(icon, classToAdd); if (options.keyboard) { icon.tabIndex = '0'; } this._icon = icon; if (options.riseOnHover) { this.on({ mouseover: this._bringToFront, mouseout: this._resetZIndex }); } var newShadow = options.icon.createShadow(this._shadow), addShadow = false; if (newShadow !== this._shadow) { this._removeShadow(); addShadow = true; } if (newShadow) { addClass(newShadow, classToAdd); newShadow.alt = ''; } this._shadow = newShadow; if (options.opacity < 1) { this._updateOpacity(); } if (addIcon) { this.getPane().appendChild(this._icon); } this._initInteraction(); if (newShadow && addShadow) { this.getPane('shadowPane').appendChild(this._shadow); } }, _removeIcon: function () { if (this.options.riseOnHover) { this.off({ mouseover: this._bringToFront, mouseout: this._resetZIndex }); } remove(this._icon); this.removeInteractiveTarget(this._icon); this._icon = null; }, _removeShadow: function () { if (this._shadow) { remove(this._shadow); } this._shadow = null; }, _setPos: function (pos) { setPosition(this._icon, pos); if (this._shadow) { 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.interactive) { return; } addClass(this._icon, 'leaflet-interactive'); this.addInteractiveTarget(this._icon); if (MarkerDrag) { var draggable = this.options.draggable; if (this.dragging) { draggable = this.dragging.enabled(); this.dragging.disable(); } this.dragging = new MarkerDrag(this); if (draggable) { this.dragging.enable(); } } }, // @method setOpacity(opacity: Number): this // Changes the opacity of the marker. setOpacity: function (opacity) { this.options.opacity = opacity; if (this._map) { this._updateOpacity(); } return this; }, _updateOpacity: function () { var opacity = this.options.opacity; setOpacity(this._icon, opacity); if (this._shadow) { setOpacity(this._shadow, opacity); } }, _bringToFront: function () { this._updateZIndex(this.options.riseOffset); }, _resetZIndex: function () { this._updateZIndex(0); }, _getPopupAnchor: function () { return this.options.icon.options.popupAnchor || [0, 0]; }, _getTooltipAnchor: function () { return this.options.icon.options.tooltipAnchor || [0, 0]; } }); // factory L.marker(latlng: LatLng, options? : Marker options) // @factory L.marker(latlng: LatLng, options? : Marker options) // Instantiates a Marker object given a geographical point and optionally an options object. function marker(latlng, options) { return new Marker(latlng, options); } /* * @class Path * @aka L.Path * @inherits Interactive layer * * An abstract class that contains options and constants shared between vector * overlays (Polygon, Polyline, Circle). Do not use it directly. Extends `Layer`. */ var Path = Layer.extend({ // @section // @aka Path options options: { // @option stroke: Boolean = true // Whether to draw stroke along the path. Set it to `false` to disable borders on polygons or circles. stroke: true, // @option color: String = '#3388ff' // Stroke color color: '#3388ff', // @option weight: Number = 3 // Stroke width in pixels weight: 3, // @option opacity: Number = 1.0 // Stroke opacity opacity: 1, // @option lineCap: String= 'round' // A string that defines [shape to be used at the end](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-linecap) of the stroke. lineCap: 'round', // @option lineJoin: String = 'round' // A string that defines [shape to be used at the corners](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-linejoin) of the stroke. lineJoin: 'round', // @option dashArray: String = null // A string that defines the stroke [dash pattern](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-dasharray). Doesn't work on `Canvas`-powered layers in [some old browsers](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/setLineDash#Browser_compatibility). dashArray: null, // @option dashOffset: String = null // A string that defines the [distance into the dash pattern to start the dash](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-dashoffset). Doesn't work on `Canvas`-powered layers in [some old browsers](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/setLineDash#Browser_compatibility). dashOffset: null, // @option fill: Boolean = depends // Whether to fill the path with color. Set it to `false` to disable filling on polygons or circles. fill: false, // @option fillColor: String = * // Fill color. Defaults to the value of the [`color`](#path-color) option fillColor: null, // @option fillOpacity: Number = 0.2 // Fill opacity. fillOpacity: 0.2, // @option fillRule: String = 'evenodd' // A string that defines [how the inside of a shape](https://developer.mozilla.org/docs/Web/SVG/Attribute/fill-rule) is determined. fillRule: 'evenodd', // className: '', // Option inherited from "Interactive layer" abstract class interactive: true, // @option bubblingMouseEvents: Boolean = true // When `true`, a mouse event on this path will trigger the same event on the map // (unless [`L.DomEvent.stopPropagation`](#domevent-stoppropagation) is used). bubblingMouseEvents: true }, beforeAdd: function (map) { // Renderer is set here because we need to call renderer.getEvents // before this.getEvents. this._renderer = map.getRenderer(this); }, onAdd: function () { this._renderer._initPath(this); this._reset(); this._renderer._addPath(this); }, onRemove: function () { this._renderer._removePath(this); }, // @method redraw(): this // Redraws the layer. Sometimes useful after you changed the coordinates that the path uses. redraw: function () { if (this._map) { this._renderer._updatePath(this); } return this; }, // @method setStyle(style: Path options): this // Changes the appearance of a Path based on the options in the `Path options` object. setStyle: function (style) { setOptions(this, style); if (this._renderer) { this._renderer._updateStyle(this); } return this; }, // @method bringToFront(): this // Brings the layer to the top of all path layers. bringToFront: function () { if (this._renderer) { this._renderer._bringToFront(this); } return this; }, // @method bringToBack(): this // Brings the layer to the bottom of all path layers. bringToBack: function () { if (this._renderer) { this._renderer._bringToBack(this); } return this; }, getElement: function () { return this._path; }, _reset: function () { // defined in child classes this._project(); this._update(); }, _clickTolerance: function () { // used when doing hit detection for Canvas layers return (this.options.stroke ? this.options.weight / 2 : 0) + (touch ? 10 : 0); } }); /* * @class CircleMarker * @aka L.CircleMarker * @inherits Path * * A circle of a fixed size with radius specified in pixels. Extends `Path`. */ var CircleMarker = Path.extend({ // @section // @aka CircleMarker options options: { fill: true, // @option radius: Number = 10 // Radius of the circle marker, in pixels radius: 10 }, initialize: function (latlng, options) { setOptions(this, options); this._latlng = toLatLng(latlng); this._radius = this.options.radius; }, // @method setLatLng(latLng: LatLng): this // Sets the position of a circle marker to a new location. setLatLng: function (latlng) { this._latlng = toLatLng(latlng); this.redraw(); return this.fire('move', {latlng: this._latlng}); }, // @method getLatLng(): LatLng // Returns the current geographical position of the circle marker getLatLng: function () { return this._latlng; }, // @method setRadius(radius: Number): this // Sets the radius of a circle marker. Units are in pixels. setRadius: function (radius) { this.options.radius = this._radius = radius; return this.redraw(); }, // @method getRadius(): Number // Returns the current radius of the circle getRadius: function () { return this._radius; }, setStyle : function (options) { var radius = options && options.radius || this._radius; Path.prototype.setStyle.call(this, options); this.setRadius(radius); return this; }, _project: function () { this._point = this._map.latLngToLayerPoint(this._latlng); this._updateBounds(); }, _updateBounds: function () { var r = this._radius, r2 = this._radiusY || r, w = this._clickTolerance(), p = [r + w, r2 + w]; this._pxBounds = new Bounds(this._point.subtract(p), this._point.add(p)); }, _update: function () { if (this._map) { this._updatePath(); } }, _updatePath: function () { this._renderer._updateCircle(this); }, _empty: function () { return this._radius && !this._renderer._bounds.intersects(this._pxBounds); }, // Needed by the `Canvas` renderer for interactivity _containsPoint: function (p) { return p.distanceTo(this._point) <= this._radius + this._clickTolerance(); } }); // @factory L.circleMarker(latlng: LatLng, options?: CircleMarker options) // Instantiates a circle marker object given a geographical point, and an optional options object. function circleMarker(latlng, options) { return new CircleMarker(latlng, options); } /* * @class Circle * @aka L.Circle * @inherits CircleMarker * * A class for drawing circle overlays on a map. Extends `CircleMarker`. * * It's an approximation and starts to diverge from a real circle closer to poles (due to projection distortion). * * @example * * ```js * L.circle([50.5, 30.5], {radius: 200}).addTo(map); * ``` */ var Circle = CircleMarker.extend({ initialize: function (latlng, options, legacyOptions) { if (typeof options === 'number') { // Backwards compatibility with 0.7.x factory (latlng, radius, options?) options = extend({}, legacyOptions, {radius: options}); } setOptions(this, options); this._latlng = toLatLng(latlng); if (isNaN(this.options.radius)) { throw new Error('Circle radius cannot be NaN'); } // @section // @aka Circle options // @option radius: Number; Radius of the circle, in meters. this._mRadius = this.options.radius; }, // @method setRadius(radius: Number): this // Sets the radius of a circle. Units are in meters. setRadius: function (radius) { this._mRadius = radius; return this.redraw(); }, // @method getRadius(): Number // Returns the current radius of a circle. Units are in meters. getRadius: function () { return this._mRadius; }, // @method getBounds(): LatLngBounds // Returns the `LatLngBounds` of the path. getBounds: function () { var half = [this._radius, this._radiusY || this._radius]; return new LatLngBounds( this._map.layerPointToLatLng(this._point.subtract(half)), this._map.layerPointToLatLng(this._point.add(half))); }, setStyle: Path.prototype.setStyle, _project: function () { var lng = this._latlng.lng, lat = this._latlng.lat, map = this._map, crs = map.options.crs; if (crs.distance === Earth.distance) { var d = Math.PI / 180, latR = (this._mRadius / Earth.R) / d, top = map.project([lat + latR, lng]), bottom = map.project([lat - latR, lng]), p = top.add(bottom).divideBy(2), lat2 = map.unproject(p).lat, lngR = Math.acos((Math.cos(latR * d) - Math.sin(lat * d) * Math.sin(lat2 * d)) / (Math.cos(lat * d) * Math.cos(lat2 * d))) / d; if (isNaN(lngR) || lngR === 0) { lngR = latR / Math.cos(Math.PI / 180 * lat); // Fallback for edge case, #2425 } this._point = p.subtract(map.getPixelOrigin()); this._radius = isNaN(lngR) ? 0 : Math.max(Math.round(p.x - map.project([lat2, lng - lngR]).x), 1); this._radiusY = Math.max(Math.round(p.y - top.y), 1); } else { var latlng2 = crs.unproject(crs.project(this._latlng).subtract([this._mRadius, 0])); this._point = map.latLngToLayerPoint(this._latlng); this._radius = this._point.x - map.latLngToLayerPoint(latlng2).x; } this._updateBounds(); } }); // @factory L.circle(latlng: LatLng, options?: Circle options) // Instantiates a circle object given a geographical point, and an options object // which contains the circle radius. // @alternative // @factory L.circle(latlng: LatLng, radius: Number, options?: Circle options) // Obsolete way of instantiating a circle, for compatibility with 0.7.x code. // Do not use in new applications or plugins. function circle(latlng, options, legacyOptions) { return new Circle(latlng, options, legacyOptions); } /* * @class Polyline * @aka L.Polyline * @inherits Path * * A class for drawing polyline overlays on a map. Extends `Path`. * * @example * * ```js * // create a red polyline from an array of LatLng points * var latlngs = [ * [45.51, -122.68], * [37.77, -122.43], * [34.04, -118.2] * ]; * * var polyline = L.polyline(latlngs, {color: 'red'}).addTo(map); * * // zoom the map to the polyline * map.fitBounds(polyline.getBounds()); * ``` * * You can also pass a multi-dimensional array to represent a `MultiPolyline` shape: * * ```js * // create a red polyline from an array of arrays of LatLng points * var latlngs = [ * [[45.51, -122.68], * [37.77, -122.43], * [34.04, -118.2]], * [[40.78, -73.91], * [41.83, -87.62], * [32.76, -96.72]] * ]; * ``` */ var Polyline = Path.extend({ // @section // @aka Polyline options options: { // @option smoothFactor: Number = 1.0 // How much to simplify the polyline on each zoom level. More means // better performance and smoother look, and less means more accurate representation. smoothFactor: 1.0, // @option noClip: Boolean = false // Disable polyline clipping. noClip: false }, initialize: function (latlngs, options) { setOptions(this, options); this._setLatLngs(latlngs); }, // @method getLatLngs(): LatLng[] // Returns an array of the points in the path, or nested arrays of points in case of multi-polyline. getLatLngs: function () { return this._latlngs; }, // @method setLatLngs(latlngs: LatLng[]): this // Replaces all the points in the polyline with the given array of geographical points. setLatLngs: function (latlngs) { this._setLatLngs(latlngs); return this.redraw(); }, // @method isEmpty(): Boolean // Returns `true` if the Polyline has no LatLngs. isEmpty: function () { return !this._latlngs.length; }, closestLayerPoint: function (p) { var minDistance = Infinity, minPoint = null, closest = _sqClosestPointOnSegment, p1, p2; for (var j = 0, jLen = this._parts.length; j < jLen; j++) { var points = this._parts[j]; for (var i = 1, len = points.length; i < len; i++) { p1 = points[i - 1]; p2 = points[i]; var sqDist = closest(p, p1, p2, true); if (sqDist < minDistance) { minDistance = sqDist; minPoint = closest(p, p1, p2); } } } if (minPoint) { minPoint.distance = Math.sqrt(minDistance); } return minPoint; }, // @method getCenter(): LatLng // Returns the center ([centroid](http://en.wikipedia.org/wiki/Centroid)) of the polyline. getCenter: function () { // throws error when not yet added to map as this center calculation requires projected coordinates if (!this._map) { throw new Error('Must add layer to map before using getCenter()'); } var i, halfDist, segDist, dist, p1, p2, ratio, points = this._rings[0], len = points.length; if (!len) { return null; } // polyline centroid algorithm; only uses the first ring if there are multiple for (i = 0, halfDist = 0; i < len - 1; i++) { halfDist += points[i].distanceTo(points[i + 1]) / 2; } // The line is so small in the current view that all points are on the same pixel. if (halfDist === 0) { return this._map.layerPointToLatLng(points[0]); } for (i = 0, dist = 0; i < len - 1; i++) { p1 = points[i]; p2 = points[i + 1]; segDist = p1.distanceTo(p2); dist += segDist; if (dist > halfDist) { ratio = (dist - halfDist) / segDist; return this._map.layerPointToLatLng([ p2.x - ratio * (p2.x - p1.x), p2.y - ratio * (p2.y - p1.y) ]); } } }, // @method getBounds(): LatLngBounds // Returns the `LatLngBounds` of the path. getBounds: function () { return this._bounds; }, // @method addLatLng(latlng: LatLng, latlngs? LatLng[]): this // Adds a given point to the polyline. By default, adds to the first ring of // the polyline in case of a multi-polyline, but can be overridden by passing // a specific ring as a LatLng array (that you can earlier access with [`getLatLngs`](#polyline-getlatlngs)). addLatLng: function (latlng, latlngs) { latlngs = latlngs || this._defaultShape(); latlng = toLatLng(latlng); latlngs.push(latlng); this._bounds.extend(latlng); return this.redraw(); }, _setLatLngs: function (latlngs) { this._bounds = new LatLngBounds(); this._latlngs = this._convertLatLngs(latlngs); }, _defaultShape: function () { return isFlat(this._latlngs) ? this._latlngs : this._latlngs[0]; }, // recursively convert latlngs input into actual LatLng instances; calculate bounds along the way _convertLatLngs: function (latlngs) { var result = [], flat = isFlat(latlngs); for (var i = 0, len = latlngs.length; i < len; i++) { if (flat) { result[i] = toLatLng(latlngs[i]); this._bounds.extend(result[i]); } else { result[i] = this._convertLatLngs(latlngs[i]); } } return result; }, _project: function () { var pxBounds = new Bounds(); this._rings = []; this._projectLatlngs(this._latlngs, this._rings, pxBounds); var w = this._clickTolerance(), p = new Point(w, w); if (this._bounds.isValid() && pxBounds.isValid()) { pxBounds.min._subtract(p); pxBounds.max._add(p); this._pxBounds = pxBounds; } }, // recursively turns latlngs into a set of rings with projected coordinates _projectLatlngs: function (latlngs, result, projectedBounds) { var flat = latlngs[0] instanceof LatLng, len = latlngs.length, i, ring; if (flat) { ring = []; for (i = 0; i < len; i++) { ring[i] = this._map.latLngToLayerPoint(latlngs[i]); projectedBounds.extend(ring[i]); } result.push(ring); } else { for (i = 0; i < len; i++) { this._projectLatlngs(latlngs[i], result, projectedBounds); } } }, // clip polyline by renderer bounds so that we have less to render for performance _clipPoints: function () { var bounds = this._renderer._bounds; this._parts = []; if (!this._pxBounds || !this._pxBounds.intersects(bounds)) { return; } if (this.options.noClip) { this._parts = this._rings; return; } var parts = this._parts, i, j, k, len, len2, segment, points; for (i = 0, k = 0, len = this._rings.length; i < len; i++) { points = this._rings[i]; for (j = 0, len2 = points.length; j < len2 - 1; j++) { segment = clipSegment(points[j], points[j + 1], bounds, j, true); 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[j + 1]) || (j === len2 - 2)) { parts[k].push(segment[1]); k++; } } } }, // simplify each clipped part of the polyline for performance _simplifyPoints: function () { var parts = this._parts, tolerance = this.options.smoothFactor; for (var i = 0, len = parts.length; i < len; i++) { parts[i] = simplify(parts[i], tolerance); } }, _update: function () { if (!this._map) { return; } this._clipPoints(); this._simplifyPoints(); this._updatePath(); }, _updatePath: function () { this._renderer._updatePoly(this); }, // Needed by the `Canvas` renderer for interactivity _containsPoint: function (p, closed) { var i, j, k, len, len2, part, w = this._clickTolerance(); if (!this._pxBounds || !this._pxBounds.contains(p)) { return false; } // hit detection for polylines 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; } if (pointToSegmentDistance(p, part[k], part[j]) <= w) { return true; } } } return false; } }); // @factory L.polyline(latlngs: LatLng[], options?: Polyline options) // Instantiates a polyline object given an array of geographical points and // optionally an options object. You can create a `Polyline` object with // multiple separate lines (`MultiPolyline`) by passing an array of arrays // of geographic points. function polyline(latlngs, options) { return new Polyline(latlngs, options); } // Retrocompat. Allow plugins to support Leaflet versions before and after 1.1. Polyline._flat = _flat; /* * @class Polygon * @aka L.Polygon * @inherits Polyline * * A class for drawing polygon overlays on a map. Extends `Polyline`. * * Note that points you pass when creating a polygon shouldn't have an additional last point equal to the first one — it's better to filter out such points. * * * @example * * ```js * // create a red polygon from an array of LatLng points * var latlngs = [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]]; * * var polygon = L.polygon(latlngs, {color: 'red'}).addTo(map); * * // zoom the map to the polygon * map.fitBounds(polygon.getBounds()); * ``` * * You can also pass an array of arrays of latlngs, with the first array representing the outer shape and the other arrays representing holes in the outer shape: * * ```js * var latlngs = [ * [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]], // outer ring * [[37.29, -108.58],[40.71, -108.58],[40.71, -102.50],[37.29, -102.50]] // hole * ]; * ``` * * Additionally, you can pass a multi-dimensional array to represent a MultiPolygon shape. * * ```js * var latlngs = [ * [ // first polygon * [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]], // outer ring * [[37.29, -108.58],[40.71, -108.58],[40.71, -102.50],[37.29, -102.50]] // hole * ], * [ // second polygon * [[41, -111.03],[45, -111.04],[45, -104.05],[41, -104.05]] * ] * ]; * ``` */ var Polygon = Polyline.extend({ options: { fill: true }, isEmpty: function () { return !this._latlngs.length || !this._latlngs[0].length; }, getCenter: function () { // throws error when not yet added to map as this center calculation requires projected coordinates if (!this._map) { throw new Error('Must add layer to map before using getCenter()'); } var i, j, p1, p2, f, area, x, y, center, points = this._rings[0], len = points.length; if (!len) { return null; } // polygon centroid algorithm; only uses the first ring if there are multiple area = x = y = 0; for (i = 0, j = len - 1; i < len; j = i++) { p1 = points[i]; p2 = points[j]; f = p1.y * p2.x - p2.y * p1.x; x += (p1.x + p2.x) * f; y += (p1.y + p2.y) * f; area += f * 3; } if (area === 0) { // Polygon is so small that all points are on same pixel. center = points[0]; } else { center = [x / area, y / area]; } return this._map.layerPointToLatLng(center); }, _convertLatLngs: function (latlngs) { var result = Polyline.prototype._convertLatLngs.call(this, latlngs), len = result.length; // remove last point if it equals first one if (len >= 2 && result[0] instanceof LatLng && result[0].equals(result[len - 1])) { result.pop(); } return result; }, _setLatLngs: function (latlngs) { Polyline.prototype._setLatLngs.call(this, latlngs); if (isFlat(this._latlngs)) { this._latlngs = [this._latlngs]; } }, _defaultShape: function () { return isFlat(this._latlngs[0]) ? this._latlngs[0] : this._latlngs[0][0]; }, _clipPoints: function () { // polygons need a different clipping algorithm so we redefine that var bounds = this._renderer._bounds, w = this.options.weight, p = new Point(w, w); // increase clip padding by stroke width to avoid stroke on clip edges bounds = new Bounds(bounds.min.subtract(p), bounds.max.add(p)); this._parts = []; if (!this._pxBounds || !this._pxBounds.intersects(bounds)) { return; } if (this.options.noClip) { this._parts = this._rings; return; } for (var i = 0, len = this._rings.length, clipped; i < len; i++) { clipped = clipPolygon(this._rings[i], bounds, true); if (clipped.length) { this._parts.push(clipped); } } }, _updatePath: function () { this._renderer._updatePoly(this, true); }, // Needed by the `Canvas` renderer for interactivity _containsPoint: function (p) { var inside = false, part, p1, p2, i, j, k, len, len2; if (!this._pxBounds.contains(p)) { return false; } // 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; } } } // also check if it's on polygon stroke return inside || Polyline.prototype._containsPoint.call(this, p, true); } }); // @factory L.polygon(latlngs: LatLng[], options?: Polyline options) function polygon(latlngs, options) { return new Polygon(latlngs, options); } /* * @class GeoJSON * @aka L.GeoJSON * @inherits FeatureGroup * * Represents a GeoJSON object or an array of GeoJSON objects. Allows you to parse * GeoJSON data and display it on the map. Extends `FeatureGroup`. * * @example * * ```js * L.geoJSON(data, { * style: function (feature) { * return {color: feature.properties.color}; * } * }).bindPopup(function (layer) { * return layer.feature.properties.description; * }).addTo(map); * ``` */ var GeoJSON = FeatureGroup.extend({ /* @section * @aka GeoJSON options * * @option pointToLayer: Function = * * A `Function` defining how GeoJSON points spawn Leaflet layers. It is internally * called when data is added, passing the GeoJSON point feature and its `LatLng`. * The default is to spawn a default `Marker`: * ```js * function(geoJsonPoint, latlng) { * return L.marker(latlng); * } * ``` * * @option style: Function = * * A `Function` defining the `Path options` for styling GeoJSON lines and polygons, * called internally when data is added. * The default value is to not override any defaults: * ```js * function (geoJsonFeature) { * return {} * } * ``` * * @option onEachFeature: Function = * * A `Function` that will be called once for each created `Feature`, after it has * been created and styled. Useful for attaching events and popups to features. * The default is to do nothing with the newly created layers: * ```js * function (feature, layer) {} * ``` * * @option filter: Function = * * A `Function` that will be used to decide whether to include a feature or not. * The default is to include all features: * ```js * function (geoJsonFeature) { * return true; * } * ``` * Note: dynamically changing the `filter` option will have effect only on newly * added data. It will _not_ re-evaluate already included features. * * @option coordsToLatLng: Function = * * A `Function` that will be used for converting GeoJSON coordinates to `LatLng`s. * The default is the `coordsToLatLng` static method. */ initialize: function (geojson, options) { setOptions(this, options); this._layers = {}; if (geojson) { this.addData(geojson); } }, // @method addData( data ): this // Adds a GeoJSON object to the layer. addData: function (geojson) { var features = 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(feature); } } return this; } var options = this.options; if (options.filter && !options.filter(geojson)) { return this; } var layer = geometryToLayer(geojson, options); if (!layer) { return this; } layer.feature = asFeature(geojson); layer.defaultOptions = layer.options; this.resetStyle(layer); if (options.onEachFeature) { options.onEachFeature(geojson, layer); } return this.addLayer(layer); }, // @method resetStyle( layer ): this // Resets the given vector layer's style to the original GeoJSON style, useful for resetting style after hover events. resetStyle: function (layer) { // reset any custom styles layer.options = extend({}, layer.defaultOptions); this._setLayerStyle(layer, this.options.style); return this; }, // @method setStyle( style ): this // Changes styles of GeoJSON vector layers with the given style function. setStyle: function (style) { return 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); } } }); // @section // There are several static functions which can be called without instantiating L.GeoJSON: // @function geometryToLayer(featureData: Object, options?: GeoJSON options): Layer // Creates a `Layer` from a given GeoJSON feature. Can use a custom // [`pointToLayer`](#geojson-pointtolayer) and/or [`coordsToLatLng`](#geojson-coordstolatlng) // functions if provided as options. function geometryToLayer(geojson, options) { var geometry = geojson.type === 'Feature' ? geojson.geometry : geojson, coords = geometry ? geometry.coordinates : null, layers = [], pointToLayer = options && options.pointToLayer, _coordsToLatLng = options && options.coordsToLatLng || coordsToLatLng, latlng, latlngs, i, len; if (!coords && !geometry) { return null; } switch (geometry.type) { case 'Point': latlng = _coordsToLatLng(coords); return pointToLayer ? pointToLayer(geojson, latlng) : new Marker(latlng); case 'MultiPoint': for (i = 0, len = coords.length; i < len; i++) { latlng = _coordsToLatLng(coords[i]); layers.push(pointToLayer ? pointToLayer(geojson, latlng) : new Marker(latlng)); } return new FeatureGroup(layers); case 'LineString': case 'MultiLineString': latlngs = coordsToLatLngs(coords, geometry.type === 'LineString' ? 0 : 1, _coordsToLatLng); return new Polyline(latlngs, options); case 'Polygon': case 'MultiPolygon': latlngs = coordsToLatLngs(coords, geometry.type === 'Polygon' ? 1 : 2, _coordsToLatLng); return new Polygon(latlngs, options); case 'GeometryCollection': for (i = 0, len = geometry.geometries.length; i < len; i++) { var layer = geometryToLayer({ geometry: geometry.geometries[i], type: 'Feature', properties: geojson.properties }, options); if (layer) { layers.push(layer); } } return new FeatureGroup(layers); default: throw new Error('Invalid GeoJSON object.'); } } // @function coordsToLatLng(coords: Array): LatLng // Creates a `LatLng` object from an array of 2 numbers (longitude, latitude) // or 3 numbers (longitude, latitude, altitude) used in GeoJSON for points. function coordsToLatLng(coords) { return new LatLng(coords[1], coords[0], coords[2]); } // @function coordsToLatLngs(coords: Array, levelsDeep?: Number, coordsToLatLng?: Function): Array // Creates a multidimensional array of `LatLng`s from a GeoJSON coordinates array. // `levelsDeep` specifies the nesting level (0 is for an array of points, 1 for an array of arrays of points, etc., 0 by default). // Can use a custom [`coordsToLatLng`](#geojson-coordstolatlng) function. function coordsToLatLngs(coords, levelsDeep, _coordsToLatLng) { var latlngs = []; for (var i = 0, len = coords.length, latlng; i < len; i++) { latlng = levelsDeep ? coordsToLatLngs(coords[i], levelsDeep - 1, _coordsToLatLng) : (_coordsToLatLng || coordsToLatLng)(coords[i]); latlngs.push(latlng); } return latlngs; } // @function latLngToCoords(latlng: LatLng, precision?: Number): Array // Reverse of [`coordsToLatLng`](#geojson-coordstolatlng) function latLngToCoords(latlng, precision) { precision = typeof precision === 'number' ? precision : 6; return latlng.alt !== undefined ? [formatNum(latlng.lng, precision), formatNum(latlng.lat, precision), formatNum(latlng.alt, precision)] : [formatNum(latlng.lng, precision), formatNum(latlng.lat, precision)]; } // @function latLngsToCoords(latlngs: Array, levelsDeep?: Number, closed?: Boolean): Array // Reverse of [`coordsToLatLngs`](#geojson-coordstolatlngs) // `closed` determines whether the first point should be appended to the end of the array to close the feature, only used when `levelsDeep` is 0. False by default. function latLngsToCoords(latlngs, levelsDeep, closed, precision) { var coords = []; for (var i = 0, len = latlngs.length; i < len; i++) { coords.push(levelsDeep ? latLngsToCoords(latlngs[i], levelsDeep - 1, closed, precision) : latLngToCoords(latlngs[i], precision)); } if (!levelsDeep && closed) { coords.push(coords[0]); } return coords; } function getFeature(layer, newGeometry) { return layer.feature ? extend({}, layer.feature, {geometry: newGeometry}) : asFeature(newGeometry); } // @function asFeature(geojson: Object): Object // Normalize GeoJSON geometries/features into GeoJSON features. function asFeature(geojson) { if (geojson.type === 'Feature' || geojson.type === 'FeatureCollection') { return geojson; } return { type: 'Feature', properties: {}, geometry: geojson }; } var PointToGeoJSON = { toGeoJSON: function (precision) { return getFeature(this, { type: 'Point', coordinates: latLngToCoords(this.getLatLng(), precision) }); } }; // @namespace Marker // @method toGeoJSON(): Object // Returns a [`GeoJSON`](http://en.wikipedia.org/wiki/GeoJSON) representation of the marker (as a GeoJSON `Point` Feature). Marker.include(PointToGeoJSON); // @namespace CircleMarker // @method toGeoJSON(): Object // Returns a [`GeoJSON`](http://en.wikipedia.org/wiki/GeoJSON) representation of the circle marker (as a GeoJSON `Point` Feature). Circle.include(PointToGeoJSON); CircleMarker.include(PointToGeoJSON); // @namespace Polyline // @method toGeoJSON(): Object // Returns a [`GeoJSON`](http://en.wikipedia.org/wiki/GeoJSON) representation of the polyline (as a GeoJSON `LineString` or `MultiLineString` Feature). Polyline.include({ toGeoJSON: function (precision) { var multi = !isFlat(this._latlngs); var coords = latLngsToCoords(this._latlngs, multi ? 1 : 0, false, precision); return getFeature(this, { type: (multi ? 'Multi' : '') + 'LineString', coordinates: coords }); } }); // @namespace Polygon // @method toGeoJSON(): Object // Returns a [`GeoJSON`](http://en.wikipedia.org/wiki/GeoJSON) representation of the polygon (as a GeoJSON `Polygon` or `MultiPolygon` Feature). Polygon.include({ toGeoJSON: function (precision) { var holes = !isFlat(this._latlngs), multi = holes && !isFlat(this._latlngs[0]); var coords = latLngsToCoords(this._latlngs, multi ? 2 : holes ? 1 : 0, true, precision); if (!holes) { coords = [coords]; } return getFeature(this, { type: (multi ? 'Multi' : '') + 'Polygon', coordinates: coords }); } }); // @namespace LayerGroup LayerGroup.include({ toMultiPoint: function (precision) { var coords = []; this.eachLayer(function (layer) { coords.push(layer.toGeoJSON(precision).geometry.coordinates); }); return getFeature(this, { type: 'MultiPoint', coordinates: coords }); }, // @method toGeoJSON(): Object // Returns a [`GeoJSON`](http://en.wikipedia.org/wiki/GeoJSON) representation of the layer group (as a GeoJSON `FeatureCollection`, `GeometryCollection`, or `MultiPoint`). toGeoJSON: function (precision) { var type = this.feature && this.feature.geometry && this.feature.geometry.type; if (type === 'MultiPoint') { return this.toMultiPoint(precision); } var isGeometryCollection = type === 'GeometryCollection', jsons = []; this.eachLayer(function (layer) { if (layer.toGeoJSON) { var json = layer.toGeoJSON(precision); if (isGeometryCollection) { jsons.push(json.geometry); } else { var feature = asFeature(json); // Squash nested feature collections if (feature.type === 'FeatureCollection') { jsons.push.apply(jsons, feature.features); } else { jsons.push(feature); } } } }); if (isGeometryCollection) { return getFeature(this, { geometries: jsons, type: 'GeometryCollection' }); } return { type: 'FeatureCollection', features: jsons }; } }); // @namespace GeoJSON // @factory L.geoJSON(geojson?: Object, options?: GeoJSON options) // Creates a GeoJSON layer. Optionally accepts an object in // [GeoJSON format](http://geojson.org/geojson-spec.html) to display on the map // (you can alternatively add it later with `addData` method) and an `options` object. function geoJSON(geojson, options) { return new GeoJSON(geojson, options); } // Backward compatibility. var geoJson = geoJSON; /* * @class ImageOverlay * @aka L.ImageOverlay * @inherits Interactive layer * * Used to load and display a single image over specific bounds of the map. Extends `Layer`. * * @example * * ```js * var imageUrl = 'http://www.lib.utexas.edu/maps/historical/newark_nj_1922.jpg', * imageBounds = [[40.712216, -74.22655], [40.773941, -74.12544]]; * L.imageOverlay(imageUrl, imageBounds).addTo(map); * ``` */ var ImageOverlay = Layer.extend({ // @section // @aka ImageOverlay options options: { // @option opacity: Number = 1.0 // The opacity of the image overlay. opacity: 1, // @option alt: String = '' // Text for the `alt` attribute of the image (useful for accessibility). alt: '', // @option interactive: Boolean = false // If `true`, the image overlay will emit [mouse events](#interactive-layer) when clicked or hovered. interactive: false, // @option crossOrigin: Boolean = false // If true, the image will have its crossOrigin attribute set to ''. This is needed if you want to access image pixel data. crossOrigin: false, // @option errorOverlayUrl: String = '' // URL to the overlay image to show in place of the overlay that failed to load. errorOverlayUrl: '', // @option zIndex: Number = 1 // The explicit [zIndex](https://developer.mozilla.org/docs/Web/CSS/CSS_Positioning/Understanding_z_index) of the tile layer. zIndex: 1, // @option className: String = '' // A custom class name to assign to the image. Empty by default. className: '', }, initialize: function (url, bounds, options) { // (String, LatLngBounds, Object) this._url = url; this._bounds = toLatLngBounds(bounds); setOptions(this, options); }, onAdd: function () { if (!this._image) { this._initImage(); if (this.options.opacity < 1) { this._updateOpacity(); } } if (this.options.interactive) { addClass(this._image, 'leaflet-interactive'); this.addInteractiveTarget(this._image); } this.getPane().appendChild(this._image); this._reset(); }, onRemove: function () { remove(this._image); if (this.options.interactive) { this.removeInteractiveTarget(this._image); } }, // @method setOpacity(opacity: Number): this // Sets the opacity of the overlay. setOpacity: function (opacity) { this.options.opacity = opacity; if (this._image) { this._updateOpacity(); } return this; }, setStyle: function (styleOpts) { if (styleOpts.opacity) { this.setOpacity(styleOpts.opacity); } return this; }, // @method bringToFront(): this // Brings the layer to the top of all overlays. bringToFront: function () { if (this._map) { toFront(this._image); } return this; }, // @method bringToBack(): this // Brings the layer to the bottom of all overlays. bringToBack: function () { if (this._map) { toBack(this._image); } return this; }, // @method setUrl(url: String): this // Changes the URL of the image. setUrl: function (url) { this._url = url; if (this._image) { this._image.src = url; } return this; }, // @method setBounds(bounds: LatLngBounds): this // Update the bounds that this ImageOverlay covers setBounds: function (bounds) { this._bounds = toLatLngBounds(bounds); if (this._map) { this._reset(); } return this; }, getEvents: function () { var events = { zoom: this._reset, viewreset: this._reset }; if (this._zoomAnimated) { events.zoomanim = this._animateZoom; } return events; }, // @method: setZIndex(value: Number) : this // Changes the [zIndex](#imageoverlay-zindex) of the image overlay. setZIndex: function (value) { this.options.zIndex = value; this._updateZIndex(); return this; }, // @method getBounds(): LatLngBounds // Get the bounds that this ImageOverlay covers getBounds: function () { return this._bounds; }, // @method getElement(): HTMLElement // Returns the instance of [`HTMLImageElement`](https://developer.mozilla.org/docs/Web/API/HTMLImageElement) // used by this overlay. getElement: function () { return this._image; }, _initImage: function () { var img = this._image = create$1('img', 'leaflet-image-layer ' + (this._zoomAnimated ? 'leaflet-zoom-animated' : '') + (this.options.className || '')); img.onselectstart = falseFn; img.onmousemove = falseFn; // @event load: Event // Fired when the ImageOverlay layer has loaded its image img.onload = bind(this.fire, this, 'load'); img.onerror = bind(this._overlayOnError, this, 'error'); if (this.options.crossOrigin) { img.crossOrigin = ''; } if (this.options.zIndex) { this._updateZIndex(); } img.src = this._url; img.alt = this.options.alt; }, _animateZoom: function (e) { var scale = this._map.getZoomScale(e.zoom), offset = this._map._latLngBoundsToNewLayerBounds(this._bounds, e.zoom, e.center).min; setTransform(this._image, offset, scale); }, _reset: function () { var image = this._image, bounds = new Bounds( this._map.latLngToLayerPoint(this._bounds.getNorthWest()), this._map.latLngToLayerPoint(this._bounds.getSouthEast())), size = bounds.getSize(); setPosition(image, bounds.min); image.style.width = size.x + 'px'; image.style.height = size.y + 'px'; }, _updateOpacity: function () { setOpacity(this._image, this.options.opacity); }, _updateZIndex: function () { if (this._image && this.options.zIndex !== undefined && this.options.zIndex !== null) { this._image.style.zIndex = this.options.zIndex; } }, _overlayOnError: function () { // @event error: Event // Fired when the ImageOverlay layer has loaded its image this.fire('error'); var errorUrl = this.options.errorOverlayUrl; if (errorUrl && this._url !== errorUrl) { this._url = errorUrl; this._image.src = errorUrl; } } }); // @factory L.imageOverlay(imageUrl: String, bounds: LatLngBounds, options?: ImageOverlay options) // Instantiates an image overlay object given the URL of the image and the // geographical bounds it is tied to. var imageOverlay = function (url, bounds, options) { return new ImageOverlay(url, bounds, options); }; /* * @class VideoOverlay * @aka L.VideoOverlay * @inherits ImageOverlay * * Used to load and display a video player over specific bounds of the map. Extends `ImageOverlay`. * * A video overlay uses the [`
    text|text2

    text|text2

    ' + padd + '