2 var whiteSpaceRe = /^\s*|\s*$/g,
\r
3 undefined, isRegExpBroken = 'B'.replace(/A(.)|B/, '$1') === '$1';
\r
8 minorVersion : '4.2',
\r
10 releaseDate : '2011-04-07',
\r
12 _init : function() {
\r
13 var t = this, d = document, na = navigator, ua = na.userAgent, i, nl, n, base, p, v;
\r
15 t.isOpera = win.opera && opera.buildNumber;
\r
17 t.isWebKit = /WebKit/.test(ua);
\r
19 t.isIE = !t.isWebKit && !t.isOpera && (/MSIE/gi).test(ua) && (/Explorer/gi).test(na.appName);
\r
21 t.isIE6 = t.isIE && /MSIE [56]/.test(ua);
\r
23 t.isGecko = !t.isWebKit && /Gecko/.test(ua);
\r
25 t.isMac = ua.indexOf('Mac') != -1;
\r
27 t.isAir = /adobeair/i.test(ua);
\r
29 t.isIDevice = /(iPad|iPhone)/.test(ua);
\r
31 // TinyMCE .NET webcontrol might be setting the values for TinyMCE
\r
32 if (win.tinyMCEPreInit) {
\r
33 t.suffix = tinyMCEPreInit.suffix;
\r
34 t.baseURL = tinyMCEPreInit.base;
\r
35 t.query = tinyMCEPreInit.query;
\r
39 // Get suffix and base
\r
42 // If base element found, add that infront of baseURL
\r
43 nl = d.getElementsByTagName('base');
\r
44 for (i=0; i<nl.length; i++) {
\r
45 if (v = nl[i].href) {
\r
46 // Host only value like http://site.com or http://site.com:8008
\r
47 if (/^https?:\/\/[^\/]+$/.test(v))
\r
50 base = v ? v.match(/.*\//)[0] : ''; // Get only directory
\r
54 function getBase(n) {
\r
55 if (n.src && /tiny_mce(|_gzip|_jquery|_prototype|_full)(_dev|_src)?.js/.test(n.src)) {
\r
56 if (/_(src|dev)\.js/g.test(n.src))
\r
59 if ((p = n.src.indexOf('?')) != -1)
\r
60 t.query = n.src.substring(p + 1);
\r
62 t.baseURL = n.src.substring(0, n.src.lastIndexOf('/'));
\r
64 // If path to script is relative and a base href was found add that one infront
\r
65 // the src property will always be an absolute one on non IE browsers and IE 8
\r
66 // so this logic will basically only be executed on older IE versions
\r
67 if (base && t.baseURL.indexOf('://') == -1 && t.baseURL.indexOf('/') !== 0)
\r
68 t.baseURL = base + t.baseURL;
\r
77 nl = d.getElementsByTagName('script');
\r
78 for (i=0; i<nl.length; i++) {
\r
84 n = d.getElementsByTagName('head')[0];
\r
86 nl = n.getElementsByTagName('script');
\r
87 for (i=0; i<nl.length; i++) {
\r
96 is : function(o, t) {
\r
98 return o !== undefined;
\r
100 if (t == 'array' && (o.hasOwnProperty && o instanceof Array))
\r
103 return typeof(o) == t;
\r
106 makeMap : function(items, delim, map) {
\r
109 items = items || [];
\r
110 delim = delim || ',';
\r
112 if (typeof(items) == "string")
\r
113 items = items.split(delim);
\r
119 map[items[i]] = {};
\r
124 each : function(o, cb, s) {
\r
132 if (o.length !== undefined) {
\r
133 // Indexed arrays, needed for Safari
\r
134 for (n=0, l = o.length; n < l; n++) {
\r
135 if (cb.call(s, o[n], n, o) === false)
\r
141 if (o.hasOwnProperty(n)) {
\r
142 if (cb.call(s, o[n], n, o) === false)
\r
152 map : function(a, f) {
\r
155 tinymce.each(a, function(v) {
\r
162 grep : function(a, f) {
\r
165 tinymce.each(a, function(v) {
\r
173 inArray : function(a, v) {
\r
177 for (i = 0, l = a.length; i < l; i++) {
\r
186 extend : function(o, e) {
\r
187 var i, l, a = arguments;
\r
189 for (i = 1, l = a.length; i < l; i++) {
\r
192 tinymce.each(e, function(v, n) {
\r
193 if (v !== undefined)
\r
202 trim : function(s) {
\r
203 return (s ? '' + s : '').replace(whiteSpaceRe, '');
\r
206 create : function(s, p, root) {
\r
207 var t = this, sp, ns, cn, scn, c, de = 0;
\r
209 // Parse : <prefix> <class>:<super class>
\r
210 s = /^((static) )?([\w.]+)(:([\w.]+))?/.exec(s);
\r
211 cn = s[3].match(/(^|\.)(\w+)$/i)[2]; // Class name
\r
213 // Create namespace for new class
\r
214 ns = t.createNS(s[3].replace(/\.\w+$/, ''), root);
\r
216 // Class already exists
\r
220 // Make pure static class
\r
221 if (s[2] == 'static') {
\r
225 this.onCreate(s[2], s[3], ns[cn]);
\r
230 // Create default constructor
\r
232 p[cn] = function() {};
\r
236 // Add constructor and methods
\r
238 t.extend(ns[cn].prototype, p);
\r
242 sp = t.resolve(s[5]).prototype;
\r
243 scn = s[5].match(/\.(\w+)$/i)[1]; // Class name
\r
245 // Extend constructor
\r
248 // Add passthrough constructor
\r
249 ns[cn] = function() {
\r
250 return sp[scn].apply(this, arguments);
\r
253 // Add inherit constructor
\r
254 ns[cn] = function() {
\r
255 this.parent = sp[scn];
\r
256 return c.apply(this, arguments);
\r
259 ns[cn].prototype[cn] = ns[cn];
\r
261 // Add super methods
\r
262 t.each(sp, function(f, n) {
\r
263 ns[cn].prototype[n] = sp[n];
\r
266 // Add overridden methods
\r
267 t.each(p, function(f, n) {
\r
268 // Extend methods if needed
\r
270 ns[cn].prototype[n] = function() {
\r
271 this.parent = sp[n];
\r
272 return f.apply(this, arguments);
\r
276 ns[cn].prototype[n] = f;
\r
281 // Add static methods
\r
282 t.each(p['static'], function(f, n) {
\r
287 this.onCreate(s[2], s[3], ns[cn].prototype);
\r
290 walk : function(o, f, n, s) {
\r
297 tinymce.each(o, function(o, i) {
\r
298 if (f.call(s, o, i, n) === false)
\r
301 tinymce.walk(o, f, n, s);
\r
306 createNS : function(n, o) {
\r
312 for (i=0; i<n.length; i++) {
\r
324 resolve : function(n, o) {
\r
330 for (i = 0, l = n.length; i < l; i++) {
\r
340 addUnload : function(f, s) {
\r
343 f = {func : f, scope : s || this};
\r
346 function unload() {
\r
347 var li = t.unloads, o, n;
\r
350 // Call unload handlers
\r
355 o.func.call(o.scope, 1); // Send in one arg to distinct unload and user destroy
\r
358 // Detach unload function
\r
359 if (win.detachEvent) {
\r
360 win.detachEvent('onbeforeunload', fakeUnload);
\r
361 win.detachEvent('onunload', unload);
\r
362 } else if (win.removeEventListener)
\r
363 win.removeEventListener('unload', unload, false);
\r
365 // Destroy references
\r
366 t.unloads = o = li = w = unload = 0;
\r
368 // Run garbarge collector on IE
\r
369 if (win.CollectGarbage)
\r
374 function fakeUnload() {
\r
377 // Is there things still loading, then do some magic
\r
378 if (d.readyState == 'interactive') {
\r
380 // Prevent memory leak
\r
381 d.detachEvent('onstop', stop);
\r
383 // Call unload handler
\r
390 // Fire unload when the currently loading page is stopped
\r
392 d.attachEvent('onstop', stop);
\r
394 // Remove onstop listener after a while to prevent the unload function
\r
395 // to execute if the user presses cancel in an onbeforeunload
\r
396 // confirm dialog and then presses the browser stop button
\r
397 win.setTimeout(function() {
\r
399 d.detachEvent('onstop', stop);
\r
404 // Attach unload handler
\r
405 if (win.attachEvent) {
\r
406 win.attachEvent('onunload', unload);
\r
407 win.attachEvent('onbeforeunload', fakeUnload);
\r
408 } else if (win.addEventListener)
\r
409 win.addEventListener('unload', unload, false);
\r
411 // Setup initial unload handler array
\r
419 removeUnload : function(f) {
\r
420 var u = this.unloads, r = null;
\r
422 tinymce.each(u, function(o, i) {
\r
423 if (o && o.func == f) {
\r
433 explode : function(s, d) {
\r
434 return s ? tinymce.map(s.split(d || ','), tinymce.trim) : s;
\r
437 _addVer : function(u) {
\r
443 v = (u.indexOf('?') == -1 ? '?' : '&') + this.query;
\r
445 if (u.indexOf('#') == -1)
\r
448 return u.replace('#', v + '#');
\r
451 // Fix function for IE 9 where regexps isn't working correctly
\r
452 // Todo: remove me once MS fixes the bug
\r
453 _replace : function(find, replace, str) {
\r
454 // On IE9 we have to fake $x replacement
\r
455 if (isRegExpBroken) {
\r
456 return str.replace(find, function() {
\r
457 var val = replace, args = arguments, i;
\r
459 for (i = 0; i < args.length - 2; i++) {
\r
460 if (args[i] === undefined) {
\r
461 val = val.replace(new RegExp('\\$' + i, 'g'), '');
\r
463 val = val.replace(new RegExp('\\$' + i, 'g'), args[i]);
\r
471 return str.replace(find, replace);
\r
476 // Initialize the API
\r
479 // Expose tinymce namespace to the global namespace (window)
\r
480 win.tinymce = win.tinyMCE = tinymce;
\r
482 // Describe the different namespaces
\r
487 tinymce.create('tinymce.util.Dispatcher', {
\r
491 Dispatcher : function(s) {
\r
492 this.scope = s || this;
\r
493 this.listeners = [];
\r
496 add : function(cb, s) {
\r
497 this.listeners.push({cb : cb, scope : s || this.scope});
\r
502 addToTop : function(cb, s) {
\r
503 this.listeners.unshift({cb : cb, scope : s || this.scope});
\r
508 remove : function(cb) {
\r
509 var l = this.listeners, o = null;
\r
511 tinymce.each(l, function(c, i) {
\r
522 dispatch : function() {
\r
523 var s, a = arguments, i, li = this.listeners, c;
\r
525 // Needs to be a real loop since the listener count might change while looping
\r
526 // And this is also more efficient
\r
527 for (i = 0; i<li.length; i++) {
\r
529 s = c.cb.apply(c.scope, a);
\r
541 var each = tinymce.each;
\r
543 tinymce.create('tinymce.util.URI', {
\r
544 URI : function(u, s) {
\r
545 var t = this, o, a, b;
\r
548 u = tinymce.trim(u);
\r
550 // Default settings
\r
551 s = t.settings = s || {};
\r
553 // Strange app protocol or local anchor
\r
554 if (/^(mailto|tel|news|javascript|about|data):/i.test(u) || /^\s*#/.test(u)) {
\r
559 // Absolute path with no host, fake host and protocol
\r
560 if (u.indexOf('/') === 0 && u.indexOf('//') !== 0)
\r
561 u = (s.base_uri ? s.base_uri.protocol || 'http' : 'http') + '://mce_host' + u;
\r
563 // Relative path http:// or protocol relative //path
\r
564 if (!/^\w*:?\/\//.test(u))
\r
565 u = (s.base_uri.protocol || 'http') + '://mce_host' + t.toAbsPath(s.base_uri.path, u);
\r
567 // Parse URL (Credits goes to Steave, http://blog.stevenlevithan.com/archives/parseuri)
\r
568 u = u.replace(/@@/g, '(mce_at)'); // Zope 3 workaround, they use @@something
\r
569 u = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(u);
\r
570 each(["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], function(v, i) {
\r
573 // Zope 3 workaround, they use @@something
\r
575 s = s.replace(/\(mce_at\)/g, '@@');
\r
580 if (b = s.base_uri) {
\r
582 t.protocol = b.protocol;
\r
585 t.userInfo = b.userInfo;
\r
587 if (!t.port && t.host == 'mce_host')
\r
590 if (!t.host || t.host == 'mce_host')
\r
596 //t.path = t.path || '/';
\r
599 setPath : function(p) {
\r
602 p = /^(.*?)\/?(\w+)?$/.exec(p);
\r
604 // Update path parts
\r
606 t.directory = p[1];
\r
614 toRelative : function(u) {
\r
620 u = new tinymce.util.URI(u, {base_uri : t});
\r
622 // Not on same domain/port or protocol
\r
623 if ((u.host != 'mce_host' && t.host != u.host && u.host) || t.port != u.port || t.protocol != u.protocol)
\r
626 o = t.toRelPath(t.path, u.path);
\r
630 o += '?' + u.query;
\r
634 o += '#' + u.anchor;
\r
639 toAbsolute : function(u, nh) {
\r
640 var u = new tinymce.util.URI(u, {base_uri : this});
\r
642 return u.getURI(this.host == u.host && this.protocol == u.protocol ? nh : 0);
\r
645 toRelPath : function(base, path) {
\r
646 var items, bp = 0, out = '', i, l;
\r
649 base = base.substring(0, base.lastIndexOf('/'));
\r
650 base = base.split('/');
\r
651 items = path.split('/');
\r
653 if (base.length >= items.length) {
\r
654 for (i = 0, l = base.length; i < l; i++) {
\r
655 if (i >= items.length || base[i] != items[i]) {
\r
662 if (base.length < items.length) {
\r
663 for (i = 0, l = items.length; i < l; i++) {
\r
664 if (i >= base.length || base[i] != items[i]) {
\r
674 for (i = 0, l = base.length - (bp - 1); i < l; i++)
\r
677 for (i = bp - 1, l = items.length; i < l; i++) {
\r
679 out += "/" + items[i];
\r
687 toAbsPath : function(base, path) {
\r
688 var i, nb = 0, o = [], tr, outPath;
\r
691 tr = /\/$/.test(path) ? '/' : '';
\r
692 base = base.split('/');
\r
693 path = path.split('/');
\r
695 // Remove empty chunks
\r
696 each(base, function(k) {
\r
703 // Merge relURLParts chunks
\r
704 for (i = path.length - 1, o = []; i >= 0; i--) {
\r
705 // Ignore empty or .
\r
706 if (path[i].length == 0 || path[i] == ".")
\r
710 if (path[i] == '..') {
\r
724 i = base.length - nb;
\r
728 outPath = o.reverse().join('/');
\r
730 outPath = base.slice(0, i).join('/') + '/' + o.reverse().join('/');
\r
732 // Add front / if it's needed
\r
733 if (outPath.indexOf('/') !== 0)
\r
734 outPath = '/' + outPath;
\r
736 // Add traling / if it's needed
\r
737 if (tr && outPath.lastIndexOf('/') !== outPath.length - 1)
\r
743 getURI : function(nh) {
\r
747 if (!t.source || nh) {
\r
752 s += t.protocol + '://';
\r
755 s += t.userInfo + '@';
\r
768 s += '?' + t.query;
\r
771 s += '#' + t.anchor;
\r
782 var each = tinymce.each;
\r
784 tinymce.create('static tinymce.util.Cookie', {
\r
785 getHash : function(n) {
\r
786 var v = this.get(n), h;
\r
789 each(v.split('&'), function(v) {
\r
792 h[unescape(v[0])] = unescape(v[1]);
\r
799 setHash : function(n, v, e, p, d, s) {
\r
802 each(v, function(v, k) {
\r
803 o += (!o ? '' : '&') + escape(k) + '=' + escape(v);
\r
806 this.set(n, o, e, p, d, s);
\r
809 get : function(n) {
\r
810 var c = document.cookie, e, p = n + "=", b;
\r
816 b = c.indexOf("; " + p);
\r
826 e = c.indexOf(";", b);
\r
831 return unescape(c.substring(b + p.length, e));
\r
834 set : function(n, v, e, p, d, s) {
\r
835 document.cookie = n + "=" + escape(v) +
\r
836 ((e) ? "; expires=" + e.toGMTString() : "") +
\r
837 ((p) ? "; path=" + escape(p) : "") +
\r
838 ((d) ? "; domain=" + d : "") +
\r
839 ((s) ? "; secure" : "");
\r
842 remove : function(n, p) {
\r
843 var d = new Date();
\r
845 d.setTime(d.getTime() - 1000);
\r
847 this.set(n, '', d, p, d);
\r
853 function serialize(o, quote) {
\r
856 quote = quote || '"';
\r
863 if (t == 'string') {
\r
864 v = '\bb\tt\nn\ff\rr\""\'\'\\\\';
\r
866 return quote + o.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g, function(a, b) {
\r
867 // Make sure single quotes never get encoded inside double quotes for JSON compatibility
\r
868 if (quote === '"' && a === "'")
\r
874 return '\\' + v.charAt(i + 1);
\r
876 a = b.charCodeAt().toString(16);
\r
878 return '\\u' + '0000'.substring(a.length) + a;
\r
882 if (t == 'object') {
\r
883 if (o.hasOwnProperty && o instanceof Array) {
\r
884 for (i=0, v = '['; i<o.length; i++)
\r
885 v += (i > 0 ? ',' : '') + serialize(o[i], quote);
\r
893 v += typeof o[i] != 'function' ? (v.length > 1 ? ',' + quote : quote) + i + quote +':' + serialize(o[i], quote) : '';
\r
901 tinymce.util.JSON = {
\r
902 serialize: serialize,
\r
904 parse: function(s) {
\r
906 return eval('(' + s + ')');
\r
914 tinymce.create('static tinymce.util.XHR', {
\r
915 send : function(o) {
\r
916 var x, t, w = window, c = 0;
\r
918 // Default settings
\r
919 o.scope = o.scope || this;
\r
920 o.success_scope = o.success_scope || o.scope;
\r
921 o.error_scope = o.error_scope || o.scope;
\r
922 o.async = o.async === false ? false : true;
\r
923 o.data = o.data || '';
\r
929 x = new ActiveXObject(s);
\r
936 x = w.XMLHttpRequest ? new XMLHttpRequest() : get('Microsoft.XMLHTTP') || get('Msxml2.XMLHTTP');
\r
939 if (x.overrideMimeType)
\r
940 x.overrideMimeType(o.content_type);
\r
942 x.open(o.type || (o.data ? 'POST' : 'GET'), o.url, o.async);
\r
944 if (o.content_type)
\r
945 x.setRequestHeader('Content-Type', o.content_type);
\r
947 x.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
\r
952 if (!o.async || x.readyState == 4 || c++ > 10000) {
\r
953 if (o.success && c < 10000 && x.status == 200)
\r
954 o.success.call(o.success_scope, '' + x.responseText, x, o);
\r
956 o.error.call(o.error_scope, c > 10000 ? 'TIMED_OUT' : 'GENERAL', x, o);
\r
960 w.setTimeout(ready, 10);
\r
963 // Syncronous request
\r
967 // Wait for response, onReadyStateChange can not be used since it leaks memory in IE
\r
968 t = w.setTimeout(ready, 10);
\r
974 var extend = tinymce.extend, JSON = tinymce.util.JSON, XHR = tinymce.util.XHR;
\r
976 tinymce.create('tinymce.util.JSONRequest', {
\r
977 JSONRequest : function(s) {
\r
978 this.settings = extend({
\r
983 send : function(o) {
\r
984 var ecb = o.error, scb = o.success;
\r
986 o = extend(this.settings, o);
\r
988 o.success = function(c, x) {
\r
991 if (typeof(c) == 'undefined') {
\r
993 error : 'JSON Parse error.'
\r
998 ecb.call(o.error_scope || o.scope, c.error, x);
\r
1000 scb.call(o.success_scope || o.scope, c.result);
\r
1003 o.error = function(ty, x) {
\r
1005 ecb.call(o.error_scope || o.scope, ty, x);
\r
1008 o.data = JSON.serialize({
\r
1009 id : o.id || 'c' + (this.count++),
\r
1010 method : o.method,
\r
1014 // JSON content type for Ruby on rails. Bug: #1883287
\r
1015 o.content_type = 'application/json';
\r
1021 sendRPC : function(o) {
\r
1022 return new tinymce.util.JSONRequest().send(o);
\r
1027 (function(tinymce) {
\r
1028 var namedEntities, baseEntities, reverseEntities,
\r
1029 attrsCharsRegExp = /[&\"\u007E-\uD7FF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
\r
1030 textCharsRegExp = /[<>&\u007E-\uD7FF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
\r
1031 rawCharsRegExp = /[<>&\"\']/g,
\r
1032 entityRegExp = /&(#)?([\w]+);/g,
\r
1034 128 : "\u20AC", 130 : "\u201A", 131 : "\u0192", 132 : "\u201E", 133 : "\u2026", 134 : "\u2020",
\r
1035 135 : "\u2021", 136 : "\u02C6", 137 : "\u2030", 138 : "\u0160", 139 : "\u2039", 140 : "\u0152",
\r
1036 142 : "\u017D", 145 : "\u2018", 146 : "\u2019", 147 : "\u201C", 148 : "\u201D", 149 : "\u2022",
\r
1037 150 : "\u2013", 151 : "\u2014", 152 : "\u02DC", 153 : "\u2122", 154 : "\u0161", 155 : "\u203A",
\r
1038 156 : "\u0153", 158 : "\u017E", 159 : "\u0178"
\r
1050 // Reverse lookup table for raw entities
\r
1051 reverseEntities = {
\r
1059 // Decodes text by using the browser
\r
1060 function nativeDecode(text) {
\r
1063 elm = document.createElement("div");
\r
1064 elm.innerHTML = text;
\r
1066 return elm.textContent || elm.innerText || text;
\r
1069 // Build a two way lookup table for the entities
\r
1070 function buildEntitiesLookup(items, radix) {
\r
1071 var i, chr, entity, lookup = {};
\r
1074 items = items.split(',');
\r
1075 radix = radix || 10;
\r
1077 // Build entities lookup table
\r
1078 for (i = 0; i < items.length; i += 2) {
\r
1079 chr = String.fromCharCode(parseInt(items[i], radix));
\r
1081 // Only add non base entities
\r
1082 if (!baseEntities[chr]) {
\r
1083 entity = '&' + items[i + 1] + ';';
\r
1084 lookup[chr] = entity;
\r
1085 lookup[entity] = chr;
\r
1093 // Unpack entities lookup where the numbers are in radix 32 to reduce the size
\r
1094 namedEntities = buildEntitiesLookup(
\r
1095 '50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' +
\r
1096 '5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' +
\r
1097 '5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' +
\r
1098 '5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' +
\r
1099 '68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' +
\r
1100 '6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' +
\r
1101 '6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' +
\r
1102 '75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' +
\r
1103 '7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' +
\r
1104 '7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' +
\r
1105 'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' +
\r
1106 'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' +
\r
1107 't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' +
\r
1108 'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' +
\r
1109 'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' +
\r
1110 '81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' +
\r
1111 '8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' +
\r
1112 '8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' +
\r
1113 '8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' +
\r
1114 '8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' +
\r
1115 'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' +
\r
1116 'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' +
\r
1117 'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' +
\r
1118 '80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' +
\r
1119 '811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro'
\r
1122 tinymce.html = tinymce.html || {};
\r
1124 tinymce.html.Entities = {
\r
1125 encodeRaw : function(text, attr) {
\r
1126 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
\r
1127 return baseEntities[chr] || chr;
\r
1131 encodeAllRaw : function(text) {
\r
1132 return ('' + text).replace(rawCharsRegExp, function(chr) {
\r
1133 return baseEntities[chr] || chr;
\r
1137 encodeNumeric : function(text, attr) {
\r
1138 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
\r
1139 // Multi byte sequence convert it to a single entity
\r
1140 if (chr.length > 1)
\r
1141 return '&#' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';';
\r
1143 return baseEntities[chr] || '&#' + chr.charCodeAt(0) + ';';
\r
1147 encodeNamed : function(text, attr, entities) {
\r
1148 entities = entities || namedEntities;
\r
1150 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
\r
1151 return baseEntities[chr] || entities[chr] || chr;
\r
1155 getEncodeFunc : function(name, entities) {
\r
1156 var Entities = tinymce.html.Entities;
\r
1158 entities = buildEntitiesLookup(entities) || namedEntities;
\r
1160 function encodeNamedAndNumeric(text, attr) {
\r
1161 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
\r
1162 return baseEntities[chr] || entities[chr] || '&#' + chr.charCodeAt(0) + ';' || chr;
\r
1166 function encodeCustomNamed(text, attr) {
\r
1167 return Entities.encodeNamed(text, attr, entities);
\r
1170 // Replace + with , to be compatible with previous TinyMCE versions
\r
1171 name = tinymce.makeMap(name.replace(/\+/g, ','));
\r
1173 // Named and numeric encoder
\r
1174 if (name.named && name.numeric)
\r
1175 return encodeNamedAndNumeric;
\r
1181 return encodeCustomNamed;
\r
1183 return Entities.encodeNamed;
\r
1188 return Entities.encodeNumeric;
\r
1191 return Entities.encodeRaw;
\r
1194 decode : function(text) {
\r
1195 return text.replace(entityRegExp, function(all, numeric, value) {
\r
1197 value = parseInt(value);
\r
1199 // Support upper UTF
\r
1200 if (value > 0xFFFF) {
\r
1203 return String.fromCharCode(0xD800 + (value >> 10), 0xDC00 + (value & 0x3FF));
\r
1205 return asciiMap[value] || String.fromCharCode(value);
\r
1208 return reverseEntities[all] || namedEntities[all] || nativeDecode(all);
\r
1214 tinymce.html.Styles = function(settings, schema) {
\r
1215 var rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi,
\r
1216 urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi,
\r
1217 styleRegExp = /\s*([^:]+):\s*([^;]+);?/g,
\r
1218 trimRightRegExp = /\s+$/,
\r
1219 urlColorRegExp = /rgb/,
\r
1220 undef, i, encodingLookup = {}, encodingItems;
\r
1222 settings = settings || {};
\r
1224 encodingItems = '\\" \\\' \\; \\: ; : _'.split(' ');
\r
1225 for (i = 0; i < encodingItems.length; i++) {
\r
1226 encodingLookup[encodingItems[i]] = '_' + i;
\r
1227 encodingLookup['_' + i] = encodingItems[i];
\r
1230 function toHex(match, r, g, b) {
\r
1231 function hex(val) {
\r
1232 val = parseInt(val).toString(16);
\r
1234 return val.length > 1 ? val : '0' + val; // 0 -> 00
\r
1237 return '#' + hex(r) + hex(g) + hex(b);
\r
1241 toHex : function(color) {
\r
1242 return color.replace(rgbRegExp, toHex);
\r
1245 parse : function(css) {
\r
1246 var styles = {}, matches, name, value, isEncoded, urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope || this;
\r
1248 function compress(prefix, suffix) {
\r
1249 var top, right, bottom, left;
\r
1251 // Get values and check it it needs compressing
\r
1252 top = styles[prefix + '-top' + suffix];
\r
1256 right = styles[prefix + '-right' + suffix];
\r
1260 bottom = styles[prefix + '-bottom' + suffix];
\r
1261 if (right != bottom)
\r
1264 left = styles[prefix + '-left' + suffix];
\r
1265 if (bottom != left)
\r
1269 styles[prefix + suffix] = left;
\r
1270 delete styles[prefix + '-top' + suffix];
\r
1271 delete styles[prefix + '-right' + suffix];
\r
1272 delete styles[prefix + '-bottom' + suffix];
\r
1273 delete styles[prefix + '-left' + suffix];
\r
1276 function canCompress(key) {
\r
1277 var value = styles[key], i;
\r
1279 if (!value || value.indexOf(' ') < 0)
\r
1282 value = value.split(' ');
\r
1285 if (value[i] !== value[0])
\r
1289 styles[key] = value[0];
\r
1294 function compress2(target, a, b, c) {
\r
1295 if (!canCompress(a))
\r
1298 if (!canCompress(b))
\r
1301 if (!canCompress(c))
\r
1305 styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c];
\r
1311 // Encodes the specified string by replacing all \" \' ; : with _<num>
\r
1312 function encode(str) {
\r
1315 return encodingLookup[str];
\r
1318 // Decodes the specified string by replacing all _<num> with it's original value \" \' etc
\r
1319 // It will also decode the \" \' if keep_slashes is set to fale or omitted
\r
1320 function decode(str, keep_slashes) {
\r
1322 str = str.replace(/_[0-9]/g, function(str) {
\r
1323 return encodingLookup[str];
\r
1327 if (!keep_slashes)
\r
1328 str = str.replace(/\\([\'\";:])/g, "$1");
\r
1334 // Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing
\r
1335 css = css.replace(/\\[\"\';:_]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, function(str) {
\r
1336 return str.replace(/[;:]/g, encode);
\r
1340 while (matches = styleRegExp.exec(css)) {
\r
1341 name = matches[1].replace(trimRightRegExp, '').toLowerCase();
\r
1342 value = matches[2].replace(trimRightRegExp, '');
\r
1344 if (name && value.length > 0) {
\r
1345 // Opera will produce 700 instead of bold in their style values
\r
1346 if (name === 'font-weight' && value === '700')
\r
1348 else if (name === 'color' || name === 'background-color') // Lowercase colors like RED
\r
1349 value = value.toLowerCase();
\r
1351 // Convert RGB colors to HEX
\r
1352 value = value.replace(rgbRegExp, toHex);
\r
1354 // Convert URLs and force them into url('value') format
\r
1355 value = value.replace(urlOrStrRegExp, function(match, url, url2, url3, str, str2) {
\r
1356 str = str || str2;
\r
1359 str = decode(str);
\r
1361 // Force strings into single quote format
\r
1362 return "'" + str.replace(/\'/g, "\\'") + "'";
\r
1365 url = decode(url || url2 || url3);
\r
1367 // Convert the URL to relative/absolute depending on config
\r
1369 url = urlConverter.call(urlConverterScope, url, 'style');
\r
1371 // Output new URL format
\r
1372 return "url('" + url.replace(/\'/g, "\\'") + "')";
\r
1375 styles[name] = isEncoded ? decode(value, true) : value;
\r
1378 styleRegExp.lastIndex = matches.index + matches[0].length;
\r
1381 // Compress the styles to reduce it's size for example IE will expand styles
\r
1382 compress("border", "");
\r
1383 compress("border", "-width");
\r
1384 compress("border", "-color");
\r
1385 compress("border", "-style");
\r
1386 compress("padding", "");
\r
1387 compress("margin", "");
\r
1388 compress2('border', 'border-width', 'border-style', 'border-color');
\r
1390 // Remove pointless border, IE produces these
\r
1391 if (styles.border === 'medium none')
\r
1392 delete styles.border;
\r
1398 serialize : function(styles, element_name) {
\r
1399 var css = '', name, value;
\r
1401 function serializeStyles(name) {
\r
1402 var styleList, i, l, name, value;
\r
1404 styleList = schema.styles[name];
\r
1406 for (i = 0, l = styleList.length; i < l; i++) {
\r
1407 name = styleList[i];
\r
1408 value = styles[name];
\r
1410 if (value !== undef && value.length > 0)
\r
1411 css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';';
\r
1416 // Serialize styles according to schema
\r
1417 if (element_name && schema && schema.styles) {
\r
1418 // Serialize global styles and element specific styles
\r
1419 serializeStyles('*');
\r
1420 serializeStyles(name);
\r
1422 // Output the styles in the order they are inside the object
\r
1423 for (name in styles) {
\r
1424 value = styles[name];
\r
1426 if (value !== undef && value.length > 0)
\r
1427 css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';';
\r
1436 (function(tinymce) {
\r
1437 var transitional = {}, boolAttrMap, blockElementsMap, shortEndedElementsMap, nonEmptyElementsMap,
\r
1438 whiteSpaceElementsMap, selfClosingElementsMap, makeMap = tinymce.makeMap, each = tinymce.each;
\r
1440 function split(str, delim) {
\r
1441 return str.split(delim || ',');
\r
1444 function unpack(lookup, data) {
\r
1445 var key, elements = {};
\r
1447 function replace(value) {
\r
1448 return value.replace(/[A-Z]+/g, function(key) {
\r
1449 return replace(lookup[key]);
\r
1454 for (key in lookup) {
\r
1455 if (lookup.hasOwnProperty(key))
\r
1456 lookup[key] = replace(lookup[key]);
\r
1459 // Unpack and parse data into object map
\r
1460 replace(data).replace(/#/g, '#text').replace(/(\w+)\[([^\]]+)\]\[([^\]]*)\]/g, function(str, name, attributes, children) {
\r
1461 attributes = split(attributes, '|');
\r
1463 elements[name] = {
\r
1464 attributes : makeMap(attributes),
\r
1465 attributesOrder : attributes,
\r
1466 children : makeMap(children, '|', {'#comment' : {}})
\r
1473 // Build a lookup table for block elements both lowercase and uppercase
\r
1474 blockElementsMap = 'h1,h2,h3,h4,h5,h6,hr,p,div,address,pre,form,table,tbody,thead,tfoot,' +
\r
1475 'th,tr,td,li,ol,ul,caption,blockquote,center,dl,dt,dd,dir,fieldset,' +
\r
1476 'noscript,menu,isindex,samp,header,footer,article,section,hgroup';
\r
1477 blockElementsMap = makeMap(blockElementsMap, ',', makeMap(blockElementsMap.toUpperCase()));
\r
1479 // This is the XHTML 1.0 transitional elements with it's attributes and children packed to reduce it's size
\r
1480 transitional = unpack({
\r
1483 ZG : 'E|span|width|align|char|charoff|valign',
\r
1484 X : 'p|T|div|U|W|isindex|fieldset|table',
\r
1485 ZF : 'E|align|char|charoff|valign',
\r
1486 W : 'pre|hr|blockquote|address|center|noframes',
\r
1487 ZE : 'abbr|axis|headers|scope|rowspan|colspan|align|char|charoff|valign|nowrap|bgcolor|width|height',
\r
1489 U : 'ul|ol|dl|menu|dir',
\r
1490 ZC : 'p|Y|div|U|W|table|br|span|bdo|object|applet|img|map|K|N|Q',
\r
1491 T : 'h1|h2|h3|h4|h5|h6',
\r
1494 ZA : 'a|G|J|M|O|P',
\r
1497 P : 'ins|del|script',
\r
1498 O : 'input|select|textarea|label|button',
\r
1500 M : 'em|strong|dfn|code|q|samp|kbd|var|cite|abbr|acronym',
\r
1503 J : 'tt|i|b|u|s|strike',
\r
1504 I : 'big|small|font|basefont',
\r
1506 G : 'br|span|bdo',
\r
1507 F : 'object|applet|img|map|iframe',
\r
1509 D : 'accesskey|tabindex|onfocus|onblur',
\r
1510 C : 'onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup',
\r
1511 B : 'lang|xml:lang|dir',
\r
1512 A : 'id|class|style|title'
\r
1513 }, 'script[id|charset|type|language|src|defer|xml:space][]' +
\r
1514 'style[B|id|type|media|title|xml:space][]' +
\r
1515 'object[E|declare|classid|codebase|data|type|codetype|archive|standby|width|height|usemap|name|tabindex|align|border|hspace|vspace][#|param|Y]' +
\r
1516 'param[id|name|value|valuetype|type][]' +
\r
1517 'p[E|align][#|S]' +
\r
1518 'a[E|D|charset|type|name|href|hreflang|rel|rev|shape|coords|target][#|Z]' +
\r
1519 'br[A|clear][]' +
\r
1521 'bdo[A|C|B][#|S]' +
\r
1522 'applet[A|codebase|archive|code|object|alt|name|width|height|align|hspace|vspace][#|param|Y]' +
\r
1523 'h1[E|align][#|S]' +
\r
1524 'img[E|src|alt|name|longdesc|width|height|usemap|ismap|align|border|hspace|vspace][]' +
\r
1525 'map[B|C|A|name][X|form|Q|area]' +
\r
1526 'h2[E|align][#|S]' +
\r
1527 'iframe[A|longdesc|name|src|frameborder|marginwidth|marginheight|scrolling|align|width|height][#|Y]' +
\r
1528 'h3[E|align][#|S]' +
\r
1534 'strike[E][#|S]' +
\r
1536 'small[E][#|S]' +
\r
1537 'font[A|B|size|color|face][#|S]' +
\r
1538 'basefont[id|size|color|face][]' +
\r
1540 'strong[E][#|S]' +
\r
1543 'q[E|cite][#|S]' +
\r
1549 'acronym[E][#|S]' +
\r
1552 'input[E|D|type|name|value|checked|disabled|readonly|size|maxlength|src|alt|usemap|onselect|onchange|accept|align][]' +
\r
1553 'select[E|name|size|multiple|disabled|tabindex|onfocus|onblur|onchange][optgroup|option]' +
\r
1554 'optgroup[E|disabled|label][option]' +
\r
1555 'option[E|selected|disabled|label|value][]' +
\r
1556 'textarea[E|D|name|rows|cols|disabled|readonly|onselect|onchange][]' +
\r
1557 'label[E|for|accesskey|onfocus|onblur][#|S]' +
\r
1558 'button[E|D|name|value|type|disabled][#|p|T|div|U|W|table|G|object|applet|img|map|K|N|Q]' +
\r
1559 'h4[E|align][#|S]' +
\r
1560 'ins[E|cite|datetime][#|Y]' +
\r
1561 'h5[E|align][#|S]' +
\r
1562 'del[E|cite|datetime][#|Y]' +
\r
1563 'h6[E|align][#|S]' +
\r
1564 'div[E|align][#|Y]' +
\r
1565 'ul[E|type|compact][li]' +
\r
1566 'li[E|type|value][#|Y]' +
\r
1567 'ol[E|type|compact|start][li]' +
\r
1568 'dl[E|compact][dt|dd]' +
\r
1571 'menu[E|compact][li]' +
\r
1572 'dir[E|compact][li]' +
\r
1573 'pre[E|width|xml:space][#|ZA]' +
\r
1574 'hr[E|align|noshade|size|width][]' +
\r
1575 'blockquote[E|cite][#|Y]' +
\r
1576 'address[E][#|S|p]' +
\r
1577 'center[E][#|Y]' +
\r
1578 'noframes[E][#|Y]' +
\r
1579 'isindex[A|B|prompt][]' +
\r
1580 'fieldset[E][#|legend|Y]' +
\r
1581 'legend[E|accesskey|align][#|S]' +
\r
1582 'table[E|summary|width|border|frame|rules|cellspacing|cellpadding|align|bgcolor][caption|col|colgroup|thead|tfoot|tbody|tr]' +
\r
1583 'caption[E|align][#|S]' +
\r
1585 'colgroup[ZG][col]' +
\r
1586 'thead[ZF][tr]' +
\r
1587 'tr[ZF|bgcolor][th|td]' +
\r
1588 'th[E|ZE][#|Y]' +
\r
1589 'form[E|action|method|name|enctype|onsubmit|onreset|accept|accept-charset|target][#|X|R|Q]' +
\r
1590 'noscript[E][#|Y]' +
\r
1591 'td[E|ZE][#|Y]' +
\r
1592 'tfoot[ZF][tr]' +
\r
1593 'tbody[ZF][tr]' +
\r
1594 'area[E|D|shape|coords|href|nohref|alt|target][]' +
\r
1595 'base[id|href|target][]' +
\r
1596 'body[E|onload|onunload|background|bgcolor|text|link|vlink|alink][#|Y]'
\r
1599 boolAttrMap = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected,preload,autoplay,loop,controls');
\r
1600 shortEndedElementsMap = makeMap('area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed,source');
\r
1601 nonEmptyElementsMap = tinymce.extend(makeMap('td,th,iframe,video,object'), shortEndedElementsMap);
\r
1602 whiteSpaceElementsMap = makeMap('pre,script,style');
\r
1603 selfClosingElementsMap = makeMap('colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr');
\r
1605 tinymce.html.Schema = function(settings) {
\r
1606 var self = this, elements = {}, children = {}, patternElements = [], validStyles;
\r
1608 settings = settings || {};
\r
1610 // Allow all elements and attributes if verify_html is set to false
\r
1611 if (settings.verify_html === false)
\r
1612 settings.valid_elements = '*[*]';
\r
1614 // Build styles list
\r
1615 if (settings.valid_styles) {
\r
1618 // Convert styles into a rule list
\r
1619 each(settings.valid_styles, function(value, key) {
\r
1620 validStyles[key] = tinymce.explode(value);
\r
1624 // Converts a wildcard expression string to a regexp for example *a will become /.*a/.
\r
1625 function patternToRegExp(str) {
\r
1626 return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$');
\r
1629 // Parses the specified valid_elements string and adds to the current rules
\r
1630 // This function is a bit hard to read since it's heavily optimized for speed
\r
1631 function addValidElements(valid_elements) {
\r
1632 var ei, el, ai, al, yl, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder,
\r
1633 prefix, outputName, globalAttributes, globalAttributesOrder, transElement, key, childKey, value,
\r
1634 elementRuleRegExp = /^([#+-])?([^\[\/]+)(?:\/([^\[]+))?(?:\[([^\]]+)\])?$/,
\r
1635 attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/,
\r
1636 hasPatternsRegExp = /[*?+]/;
\r
1638 if (valid_elements) {
\r
1639 // Split valid elements into an array with rules
\r
1640 valid_elements = split(valid_elements);
\r
1642 if (elements['@']) {
\r
1643 globalAttributes = elements['@'].attributes;
\r
1644 globalAttributesOrder = elements['@'].attributesOrder;
\r
1648 for (ei = 0, el = valid_elements.length; ei < el; ei++) {
\r
1649 // Parse element rule
\r
1650 matches = elementRuleRegExp.exec(valid_elements[ei]);
\r
1652 // Setup local names for matches
\r
1653 prefix = matches[1];
\r
1654 elementName = matches[2];
\r
1655 outputName = matches[3];
\r
1656 attrData = matches[4];
\r
1658 // Create new attributes and attributesOrder
\r
1660 attributesOrder = [];
\r
1662 // Create the new element
\r
1664 attributes : attributes,
\r
1665 attributesOrder : attributesOrder
\r
1668 // Padd empty elements prefix
\r
1669 if (prefix === '#')
\r
1670 element.paddEmpty = true;
\r
1672 // Remove empty elements prefix
\r
1673 if (prefix === '-')
\r
1674 element.removeEmpty = true;
\r
1676 // Copy attributes from global rule into current rule
\r
1677 if (globalAttributes) {
\r
1678 for (key in globalAttributes)
\r
1679 attributes[key] = globalAttributes[key];
\r
1681 attributesOrder.push.apply(attributesOrder, globalAttributesOrder);
\r
1684 // Attributes defined
\r
1686 attrData = split(attrData, '|');
\r
1687 for (ai = 0, al = attrData.length; ai < al; ai++) {
\r
1688 matches = attrRuleRegExp.exec(attrData[ai]);
\r
1691 attrType = matches[1];
\r
1692 attrName = matches[2].replace(/::/g, ':');
\r
1693 prefix = matches[3];
\r
1694 value = matches[4];
\r
1697 if (attrType === '!') {
\r
1698 element.attributesRequired = element.attributesRequired || [];
\r
1699 element.attributesRequired.push(attrName);
\r
1700 attr.required = true;
\r
1703 // Denied from global
\r
1704 if (attrType === '-') {
\r
1705 delete attributes[attrName];
\r
1706 attributesOrder.splice(tinymce.inArray(attributesOrder, attrName), 1);
\r
1713 if (prefix === '=') {
\r
1714 element.attributesDefault = element.attributesDefault || [];
\r
1715 element.attributesDefault.push({name: attrName, value: value});
\r
1716 attr.defaultValue = value;
\r
1720 if (prefix === ':') {
\r
1721 element.attributesForced = element.attributesForced || [];
\r
1722 element.attributesForced.push({name: attrName, value: value});
\r
1723 attr.forcedValue = value;
\r
1726 // Required values
\r
1727 if (prefix === '<')
\r
1728 attr.validValues = makeMap(value, '?');
\r
1731 // Check for attribute patterns
\r
1732 if (hasPatternsRegExp.test(attrName)) {
\r
1733 element.attributePatterns = element.attributePatterns || [];
\r
1734 attr.pattern = patternToRegExp(attrName);
\r
1735 element.attributePatterns.push(attr);
\r
1737 // Add attribute to order list if it doesn't already exist
\r
1738 if (!attributes[attrName])
\r
1739 attributesOrder.push(attrName);
\r
1741 attributes[attrName] = attr;
\r
1747 // Global rule, store away these for later usage
\r
1748 if (!globalAttributes && elementName == '@') {
\r
1749 globalAttributes = attributes;
\r
1750 globalAttributesOrder = attributesOrder;
\r
1753 // Handle substitute elements such as b/strong
\r
1755 element.outputName = elementName;
\r
1756 elements[outputName] = element;
\r
1759 // Add pattern or exact element
\r
1760 if (hasPatternsRegExp.test(elementName)) {
\r
1761 element.pattern = patternToRegExp(elementName);
\r
1762 patternElements.push(element);
\r
1764 elements[elementName] = element;
\r
1770 function setValidElements(valid_elements) {
\r
1772 patternElements = [];
\r
1774 addValidElements(valid_elements);
\r
1776 each(transitional, function(element, name) {
\r
1777 children[name] = element.children;
\r
1781 // Adds custom non HTML elements to the schema
\r
1782 function addCustomElements(custom_elements) {
\r
1783 var customElementRegExp = /^(~)?(.+)$/;
\r
1785 if (custom_elements) {
\r
1786 each(split(custom_elements), function(rule) {
\r
1787 var matches = customElementRegExp.exec(rule),
\r
1788 cloneName = matches[1] === '~' ? 'span' : 'div',
\r
1789 name = matches[2];
\r
1791 children[name] = children[cloneName];
\r
1793 // Add custom elements at span/div positions
\r
1794 each(children, function(element, child) {
\r
1795 if (element[cloneName])
\r
1796 element[name] = element[cloneName];
\r
1802 // Adds valid children to the schema object
\r
1803 function addValidChildren(valid_children) {
\r
1804 var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/;
\r
1806 if (valid_children) {
\r
1807 each(split(valid_children), function(rule) {
\r
1808 var matches = childRuleRegExp.exec(rule), parent, prefix;
\r
1811 prefix = matches[1];
\r
1813 // Add/remove items from default
\r
1815 parent = children[matches[2]];
\r
1817 parent = children[matches[2]] = {'#comment' : {}};
\r
1819 parent = children[matches[2]];
\r
1821 each(split(matches[3], '|'), function(child) {
\r
1822 if (prefix === '-')
\r
1823 delete parent[child];
\r
1825 parent[child] = {};
\r
1832 if (!settings.valid_elements) {
\r
1833 // No valid elements defined then clone the elements from the transitional spec
\r
1834 each(transitional, function(element, name) {
\r
1835 elements[name] = {
\r
1836 attributes : element.attributes,
\r
1837 attributesOrder : element.attributesOrder
\r
1840 children[name] = element.children;
\r
1844 each(split('strong/b,em/i'), function(item) {
\r
1845 item = split(item, '/');
\r
1846 elements[item[1]].outputName = item[0];
\r
1849 // Add default alt attribute for images
\r
1850 elements.img.attributesDefault = [{name: 'alt', value: ''}];
\r
1852 // Remove these if they are empty by default
\r
1853 each(split('ol,ul,li,sub,sup,blockquote,tr,div,span,font,a,table,tbody'), function(name) {
\r
1854 elements[name].removeEmpty = true;
\r
1857 // Padd these by default
\r
1858 each(split('p,h1,h2,h3,h4,h5,h6,th,td,pre,div,address,caption'), function(name) {
\r
1859 elements[name].paddEmpty = true;
\r
1862 setValidElements(settings.valid_elements);
\r
1864 addCustomElements(settings.custom_elements);
\r
1865 addValidChildren(settings.valid_children);
\r
1866 addValidElements(settings.extended_valid_elements);
\r
1868 // Todo: Remove this when we fix list handling to be valid
\r
1869 addValidChildren('+ol[ul|ol],+ul[ul|ol]');
\r
1871 // Delete invalid elements
\r
1872 if (settings.invalid_elements) {
\r
1873 tinymce.each(tinymce.explode(settings.invalid_elements), function(item) {
\r
1874 if (elements[item])
\r
1875 delete elements[item];
\r
1879 self.children = children;
\r
1881 self.styles = validStyles;
\r
1883 self.getBoolAttrs = function() {
\r
1884 return boolAttrMap;
\r
1887 self.getBlockElements = function() {
\r
1888 return blockElementsMap;
\r
1891 self.getShortEndedElements = function() {
\r
1892 return shortEndedElementsMap;
\r
1895 self.getSelfClosingElements = function() {
\r
1896 return selfClosingElementsMap;
\r
1899 self.getNonEmptyElements = function() {
\r
1900 return nonEmptyElementsMap;
\r
1903 self.getWhiteSpaceElements = function() {
\r
1904 return whiteSpaceElementsMap;
\r
1907 self.isValidChild = function(name, child) {
\r
1908 var parent = children[name];
\r
1910 return !!(parent && parent[child]);
\r
1913 self.getElementRule = function(name) {
\r
1914 var element = elements[name], i;
\r
1916 // Exact match found
\r
1920 // No exact match then try the patterns
\r
1921 i = patternElements.length;
\r
1923 element = patternElements[i];
\r
1925 if (element.pattern.test(name))
\r
1930 self.addValidElements = addValidElements;
\r
1932 self.setValidElements = setValidElements;
\r
1934 self.addCustomElements = addCustomElements;
\r
1936 self.addValidChildren = addValidChildren;
\r
1939 // Expose boolMap and blockElementMap as static properties for usage in DOMUtils
\r
1940 tinymce.html.Schema.boolAttrMap = boolAttrMap;
\r
1941 tinymce.html.Schema.blockElementsMap = blockElementsMap;
\r
1944 (function(tinymce) {
\r
1945 tinymce.html.SaxParser = function(settings, schema) {
\r
1946 var self = this, noop = function() {};
\r
1948 settings = settings || {};
\r
1949 self.schema = schema = schema || new tinymce.html.Schema();
\r
1951 if (settings.fix_self_closing !== false)
\r
1952 settings.fix_self_closing = true;
\r
1954 // Add handler functions from settings and setup default handlers
\r
1955 tinymce.each('comment cdata text start end pi doctype'.split(' '), function(name) {
\r
1957 self[name] = settings[name] || noop;
\r
1960 self.parse = function(html) {
\r
1961 var self = this, matches, index = 0, value, endRegExp, stack = [], attrList, i, text, name,
\r
1962 shortEndedElements, fillAttrsMap, isShortEnded, validate, elementRule, isValidElement, attr, attribsValue,
\r
1963 validAttributesMap, validAttributePatterns, attributesRequired, attributesDefault, attributesForced, selfClosing,
\r
1964 tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0, decode = tinymce.html.Entities.decode, fixSelfClosing;
\r
1966 function processEndTag(name) {
\r
1969 // Find position of parent of the same type
\r
1970 pos = stack.length;
\r
1972 if (stack[pos].name === name)
\r
1978 // Close all the open elements
\r
1979 for (i = stack.length - 1; i >= pos; i--) {
\r
1983 self.end(name.name);
\r
1986 // Remove the open elements from the stack
\r
1987 stack.length = pos;
\r
1991 // Precompile RegExps and map objects
\r
1992 tokenRegExp = new RegExp('<(?:' +
\r
1993 '(?:!--([\\w\\W]*?)-->)|' + // Comment
\r
1994 '(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|' + // CDATA
\r
1995 '(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE
\r
1996 '(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI
\r
1997 '(?:\\/([^>]+)>)|' + // End element
\r
1998 '(?:([^\\s\\/<>]+)\\s*((?:[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*)>)' + // Start element
\r
2001 attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:\\.|[^\"])*)\")|(?:\'((?:\\.|[^\'])*)\')|([^>\s]+)))?/g;
\r
2002 specialElements = {
\r
2003 'script' : /<\/script[^>]*>/gi,
\r
2004 'style' : /<\/style[^>]*>/gi,
\r
2005 'noscript' : /<\/noscript[^>]*>/gi
\r
2008 // Setup lookup tables for empty elements and boolean attributes
\r
2009 shortEndedElements = schema.getShortEndedElements();
\r
2010 selfClosing = schema.getSelfClosingElements();
\r
2011 fillAttrsMap = schema.getBoolAttrs();
\r
2012 validate = settings.validate;
\r
2013 fixSelfClosing = settings.fix_self_closing;
\r
2015 while (matches = tokenRegExp.exec(html)) {
\r
2017 if (index < matches.index)
\r
2018 self.text(decode(html.substr(index, matches.index - index)));
\r
2020 if (value = matches[6]) { // End element
\r
2021 processEndTag(value.toLowerCase());
\r
2022 } else if (value = matches[7]) { // Start element
\r
2023 value = value.toLowerCase();
\r
2024 isShortEnded = value in shortEndedElements;
\r
2026 // Is self closing tag for example an <li> after an open <li>
\r
2027 if (fixSelfClosing && selfClosing[value] && stack.length > 0 && stack[stack.length - 1].name === value)
\r
2028 processEndTag(value);
\r
2030 // Validate element
\r
2031 if (!validate || (elementRule = schema.getElementRule(value))) {
\r
2032 isValidElement = true;
\r
2034 // Grab attributes map and patters when validation is enabled
\r
2036 validAttributesMap = elementRule.attributes;
\r
2037 validAttributePatterns = elementRule.attributePatterns;
\r
2040 // Parse attributes
\r
2041 if (attribsValue = matches[8]) {
\r
2043 attrList.map = {};
\r
2045 attribsValue.replace(attrRegExp, function(match, name, value, val2, val3) {
\r
2048 name = name.toLowerCase();
\r
2049 value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute
\r
2051 // Validate name and value
\r
2052 if (validate && name.indexOf('data-') !== 0) {
\r
2053 attrRule = validAttributesMap[name];
\r
2055 // Find rule by pattern matching
\r
2056 if (!attrRule && validAttributePatterns) {
\r
2057 i = validAttributePatterns.length;
\r
2059 attrRule = validAttributePatterns[i];
\r
2060 if (attrRule.pattern.test(name))
\r
2064 // No rule matched
\r
2069 // No attribute rule found
\r
2074 if (attrRule.validValues && !(value in attrRule.validValues))
\r
2078 // Add attribute to list and map
\r
2079 attrList.map[name] = value;
\r
2087 attrList.map = {};
\r
2090 // Process attributes if validation is enabled
\r
2092 attributesRequired = elementRule.attributesRequired;
\r
2093 attributesDefault = elementRule.attributesDefault;
\r
2094 attributesForced = elementRule.attributesForced;
\r
2096 // Handle forced attributes
\r
2097 if (attributesForced) {
\r
2098 i = attributesForced.length;
\r
2100 attr = attributesForced[i];
\r
2102 attrValue = attr.value;
\r
2104 if (attrValue === '{$uid}')
\r
2105 attrValue = 'mce_' + idCount++;
\r
2107 attrList.map[name] = attrValue;
\r
2108 attrList.push({name: name, value: attrValue});
\r
2112 // Handle default attributes
\r
2113 if (attributesDefault) {
\r
2114 i = attributesDefault.length;
\r
2116 attr = attributesDefault[i];
\r
2119 if (!(name in attrList.map)) {
\r
2120 attrValue = attr.value;
\r
2122 if (attrValue === '{$uid}')
\r
2123 attrValue = 'mce_' + idCount++;
\r
2125 attrList.map[name] = attrValue;
\r
2126 attrList.push({name: name, value: attrValue});
\r
2131 // Handle required attributes
\r
2132 if (attributesRequired) {
\r
2133 i = attributesRequired.length;
\r
2135 if (attributesRequired[i] in attrList.map)
\r
2139 // None of the required attributes where found
\r
2141 isValidElement = false;
\r
2144 // Invalidate element if it's marked as bogus
\r
2145 if (attrList.map['data-mce-bogus'])
\r
2146 isValidElement = false;
\r
2149 if (isValidElement)
\r
2150 self.start(value, attrList, isShortEnded);
\r
2152 isValidElement = false;
\r
2154 // Treat script, noscript and style a bit different since they may include code that looks like elements
\r
2155 if (endRegExp = specialElements[value]) {
\r
2156 endRegExp.lastIndex = index = matches.index + matches[0].length;
\r
2158 if (matches = endRegExp.exec(html)) {
\r
2159 if (isValidElement)
\r
2160 text = html.substr(index, matches.index - index);
\r
2162 index = matches.index + matches[0].length;
\r
2164 text = html.substr(index);
\r
2165 index = html.length;
\r
2168 if (isValidElement && text.length > 0)
\r
2169 self.text(text, true);
\r
2171 if (isValidElement)
\r
2174 tokenRegExp.lastIndex = index;
\r
2178 // Push value on to stack
\r
2179 if (!isShortEnded) {
\r
2180 if (!attribsValue || attribsValue.indexOf('/') != attribsValue.length - 1)
\r
2181 stack.push({name: value, valid: isValidElement});
\r
2182 else if (isValidElement)
\r
2185 } else if (value = matches[1]) { // Comment
\r
2186 self.comment(value);
\r
2187 } else if (value = matches[2]) { // CDATA
\r
2188 self.cdata(value);
\r
2189 } else if (value = matches[3]) { // DOCTYPE
\r
2190 self.doctype(value);
\r
2191 } else if (value = matches[4]) { // PI
\r
2192 self.pi(value, matches[5]);
\r
2195 index = matches.index + matches[0].length;
\r
2199 if (index < html.length)
\r
2200 self.text(decode(html.substr(index)));
\r
2202 // Close any open elements
\r
2203 for (i = stack.length - 1; i >= 0; i--) {
\r
2207 self.end(value.name);
\r
2213 (function(tinymce) {
\r
2214 var whiteSpaceRegExp = /^[ \t\r\n]*$/, typeLookup = {
\r
2220 '#document-fragment' : 11
\r
2223 // Walks the tree left/right
\r
2224 function walk(node, root_node, prev) {
\r
2225 var sibling, parent, startName = prev ? 'lastChild' : 'firstChild', siblingName = prev ? 'prev' : 'next';
\r
2227 // Walk into nodes if it has a start
\r
2228 if (node[startName])
\r
2229 return node[startName];
\r
2231 // Return the sibling if it has one
\r
2232 if (node !== root_node) {
\r
2233 sibling = node[siblingName];
\r
2238 // Walk up the parents to look for siblings
\r
2239 for (parent = node.parent; parent && parent !== root_node; parent = parent.parent) {
\r
2240 sibling = parent[siblingName];
\r
2248 function Node(name, type) {
\r
2253 this.attributes = [];
\r
2254 this.attributes.map = {};
\r
2258 tinymce.extend(Node.prototype, {
\r
2259 replace : function(node) {
\r
2265 self.insert(node, self);
\r
2271 attr : function(name, value) {
\r
2272 var self = this, attrs, i, undef;
\r
2274 if (typeof name !== "string") {
\r
2276 self.attr(i, name[i]);
\r
2281 if (attrs = self.attributes) {
\r
2282 if (value !== undef) {
\r
2283 // Remove attribute
\r
2284 if (value === null) {
\r
2285 if (name in attrs.map) {
\r
2286 delete attrs.map[name];
\r
2290 if (attrs[i].name === name) {
\r
2291 attrs = attrs.splice(i, 1);
\r
2301 if (name in attrs.map) {
\r
2305 if (attrs[i].name === name) {
\r
2306 attrs[i].value = value;
\r
2311 attrs.push({name: name, value: value});
\r
2313 attrs.map[name] = value;
\r
2317 return attrs.map[name];
\r
2322 clone : function() {
\r
2323 var self = this, clone = new Node(self.name, self.type), i, l, selfAttrs, selfAttr, cloneAttrs;
\r
2325 // Clone element attributes
\r
2326 if (selfAttrs = self.attributes) {
\r
2328 cloneAttrs.map = {};
\r
2330 for (i = 0, l = selfAttrs.length; i < l; i++) {
\r
2331 selfAttr = selfAttrs[i];
\r
2333 // Clone everything except id
\r
2334 if (selfAttr.name !== 'id') {
\r
2335 cloneAttrs[cloneAttrs.length] = {name: selfAttr.name, value: selfAttr.value};
\r
2336 cloneAttrs.map[selfAttr.name] = selfAttr.value;
\r
2340 clone.attributes = cloneAttrs;
\r
2343 clone.value = self.value;
\r
2344 clone.shortEnded = self.shortEnded;
\r
2349 wrap : function(wrapper) {
\r
2352 self.parent.insert(wrapper, self);
\r
2353 wrapper.append(self);
\r
2358 unwrap : function() {
\r
2359 var self = this, node, next;
\r
2361 for (node = self.firstChild; node; ) {
\r
2363 self.insert(node, self, true);
\r
2370 remove : function() {
\r
2371 var self = this, parent = self.parent, next = self.next, prev = self.prev;
\r
2374 if (parent.firstChild === self) {
\r
2375 parent.firstChild = next;
\r
2383 if (parent.lastChild === self) {
\r
2384 parent.lastChild = prev;
\r
2392 self.parent = self.next = self.prev = null;
\r
2398 append : function(node) {
\r
2399 var self = this, last;
\r
2404 last = self.lastChild;
\r
2408 self.lastChild = node;
\r
2410 self.lastChild = self.firstChild = node;
\r
2412 node.parent = self;
\r
2417 insert : function(node, ref_node, before) {
\r
2423 parent = ref_node.parent || this;
\r
2426 if (ref_node === parent.firstChild)
\r
2427 parent.firstChild = node;
\r
2429 ref_node.prev.next = node;
\r
2431 node.prev = ref_node.prev;
\r
2432 node.next = ref_node;
\r
2433 ref_node.prev = node;
\r
2435 if (ref_node === parent.lastChild)
\r
2436 parent.lastChild = node;
\r
2438 ref_node.next.prev = node;
\r
2440 node.next = ref_node.next;
\r
2441 node.prev = ref_node;
\r
2442 ref_node.next = node;
\r
2445 node.parent = parent;
\r
2450 getAll : function(name) {
\r
2451 var self = this, node, collection = [];
\r
2453 for (node = self.firstChild; node; node = walk(node, self)) {
\r
2454 if (node.name === name)
\r
2455 collection.push(node);
\r
2458 return collection;
\r
2461 empty : function() {
\r
2462 var self = this, nodes, i, node;
\r
2464 // Remove all children
\r
2465 if (self.firstChild) {
\r
2468 // Collect the children
\r
2469 for (node = self.firstChild; node; node = walk(node, self))
\r
2472 // Remove the children
\r
2476 node.parent = node.firstChild = node.lastChild = node.next = node.prev = null;
\r
2480 self.firstChild = self.lastChild = null;
\r
2485 isEmpty : function(elements) {
\r
2486 var self = this, node = self.firstChild, i, name;
\r
2490 if (node.type === 1) {
\r
2491 // Ignore bogus elements
\r
2492 if (node.attributes.map['data-mce-bogus'])
\r
2495 // Keep empty elements like <img />
\r
2496 if (elements[node.name])
\r
2499 // Keep elements with data attributes or name attribute like <a name="1"></a>
\r
2500 i = node.attributes.length;
\r
2502 name = node.attributes[i].name;
\r
2503 if (name === "name" || name.indexOf('data-') === 0)
\r
2508 // Keep non whitespace text nodes
\r
2509 if ((node.type === 3 && !whiteSpaceRegExp.test(node.value)))
\r
2511 } while (node = walk(node, self));
\r
2518 tinymce.extend(Node, {
\r
2519 create : function(name, attrs) {
\r
2520 var node, attrName;
\r
2523 node = new Node(name, typeLookup[name] || 1);
\r
2525 // Add attributes if needed
\r
2527 for (attrName in attrs)
\r
2528 node.attr(attrName, attrs[attrName]);
\r
2535 tinymce.html.Node = Node;
\r
2538 (function(tinymce) {
\r
2539 var Node = tinymce.html.Node;
\r
2541 tinymce.html.DomParser = function(settings, schema) {
\r
2542 var self = this, nodeFilters = {}, attributeFilters = [], matchedNodes = {}, matchedAttributes = {};
\r
2544 settings = settings || {};
\r
2545 settings.validate = "validate" in settings ? settings.validate : true;
\r
2546 settings.root_name = settings.root_name || 'body';
\r
2547 self.schema = schema = schema || new tinymce.html.Schema();
\r
2549 function fixInvalidChildren(nodes) {
\r
2550 var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i,
\r
2551 childClone, nonEmptyElements, nonSplitableElements, sibling, nextNode;
\r
2553 nonSplitableElements = tinymce.makeMap('tr,td,th,tbody,thead,tfoot,table');
\r
2554 nonEmptyElements = schema.getNonEmptyElements();
\r
2556 for (ni = 0; ni < nodes.length; ni++) {
\r
2559 // Already removed
\r
2563 // Get list of all parent nodes until we find a valid parent to stick the child into
\r
2565 for (parent = node.parent; parent && !schema.isValidChild(parent.name, node.name) && !nonSplitableElements[parent.name]; parent = parent.parent)
\r
2566 parents.push(parent);
\r
2568 // Found a suitable parent
\r
2569 if (parent && parents.length > 1) {
\r
2570 // Reverse the array since it makes looping easier
\r
2571 parents.reverse();
\r
2573 // Clone the related parent and insert that after the moved node
\r
2574 newParent = currentNode = self.filterNode(parents[0].clone());
\r
2576 // Start cloning and moving children on the left side of the target node
\r
2577 for (i = 0; i < parents.length - 1; i++) {
\r
2578 if (schema.isValidChild(currentNode.name, parents[i].name)) {
\r
2579 tempNode = self.filterNode(parents[i].clone());
\r
2580 currentNode.append(tempNode);
\r
2582 tempNode = currentNode;
\r
2584 for (childNode = parents[i].firstChild; childNode && childNode != parents[i + 1]; ) {
\r
2585 nextNode = childNode.next;
\r
2586 tempNode.append(childNode);
\r
2587 childNode = nextNode;
\r
2590 currentNode = tempNode;
\r
2593 if (!newParent.isEmpty(nonEmptyElements)) {
\r
2594 parent.insert(newParent, parents[0], true);
\r
2595 parent.insert(node, newParent);
\r
2597 parent.insert(node, parents[0], true);
\r
2600 // Check if the element is empty by looking through it's contents and special treatment for <p><br /></p>
\r
2601 parent = parents[0];
\r
2602 if (parent.isEmpty(nonEmptyElements) || parent.firstChild === parent.lastChild && parent.firstChild.name === 'br') {
\r
2603 parent.empty().remove();
\r
2605 } else if (node.parent) {
\r
2606 // If it's an LI try to find a UL/OL for it or wrap it
\r
2607 if (node.name === 'li') {
\r
2608 sibling = node.prev;
\r
2609 if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) {
\r
2610 sibling.append(node);
\r
2614 sibling = node.next;
\r
2615 if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) {
\r
2616 sibling.insert(node, sibling.firstChild, true);
\r
2620 node.wrap(self.filterNode(new Node('ul', 1)));
\r
2624 // Try wrapping the element in a DIV
\r
2625 if (schema.isValidChild(node.parent.name, 'div') && schema.isValidChild('div', node.name)) {
\r
2626 node.wrap(self.filterNode(new Node('div', 1)));
\r
2628 // We failed wrapping it, then remove or unwrap it
\r
2629 if (node.name === 'style' || node.name === 'script')
\r
2630 node.empty().remove();
\r
2638 self.filterNode = function(node) {
\r
2639 var i, name, list;
\r
2641 // Run element filters
\r
2642 if (name in nodeFilters) {
\r
2643 list = matchedNodes[name];
\r
2648 matchedNodes[name] = [node];
\r
2651 // Run attribute filters
\r
2652 i = attributeFilters.length;
\r
2654 name = attributeFilters[i].name;
\r
2656 if (name in node.attributes.map) {
\r
2657 list = matchedAttributes[name];
\r
2662 matchedAttributes[name] = [node];
\r
2669 self.addNodeFilter = function(name, callback) {
\r
2670 tinymce.each(tinymce.explode(name), function(name) {
\r
2671 var list = nodeFilters[name];
\r
2674 nodeFilters[name] = list = [];
\r
2676 list.push(callback);
\r
2680 self.addAttributeFilter = function(name, callback) {
\r
2681 tinymce.each(tinymce.explode(name), function(name) {
\r
2684 for (i = 0; i < attributeFilters.length; i++) {
\r
2685 if (attributeFilters[i].name === name) {
\r
2686 attributeFilters[i].callbacks.push(callback);
\r
2691 attributeFilters.push({name: name, callbacks: [callback]});
\r
2695 self.parse = function(html, args) {
\r
2696 var parser, rootNode, node, nodes, i, l, fi, fl, list, name, validate,
\r
2697 blockElements, startWhiteSpaceRegExp, invalidChildren = [],
\r
2698 endWhiteSpaceRegExp, allWhiteSpaceRegExp, whiteSpaceElements, children, nonEmptyElements;
\r
2700 args = args || {};
\r
2701 matchedNodes = {};
\r
2702 matchedAttributes = {};
\r
2703 blockElements = tinymce.extend(tinymce.makeMap('script,style,head,html,body,title,meta,param'), schema.getBlockElements());
\r
2704 nonEmptyElements = schema.getNonEmptyElements();
\r
2705 children = schema.children;
\r
2706 validate = settings.validate;
\r
2708 whiteSpaceElements = schema.getWhiteSpaceElements();
\r
2709 startWhiteSpaceRegExp = /^[ \t\r\n]+/;
\r
2710 endWhiteSpaceRegExp = /[ \t\r\n]+$/;
\r
2711 allWhiteSpaceRegExp = /[ \t\r\n]+/g;
\r
2713 function createNode(name, type) {
\r
2714 var node = new Node(name, type), list;
\r
2716 if (name in nodeFilters) {
\r
2717 list = matchedNodes[name];
\r
2722 matchedNodes[name] = [node];
\r
2728 function removeWhitespaceBefore(node) {
\r
2729 var textNode, textVal, sibling;
\r
2731 for (textNode = node.prev; textNode && textNode.type === 3; ) {
\r
2732 textVal = textNode.value.replace(endWhiteSpaceRegExp, '');
\r
2734 if (textVal.length > 0) {
\r
2735 textNode.value = textVal;
\r
2736 textNode = textNode.prev;
\r
2738 sibling = textNode.prev;
\r
2739 textNode.remove();
\r
2740 textNode = sibling;
\r
2745 parser = new tinymce.html.SaxParser({
\r
2746 validate : validate,
\r
2747 fix_self_closing : !validate, // Let the DOM parser handle <li> in <li> or <p> in <p> for better results
\r
2749 cdata: function(text) {
\r
2750 node.append(createNode('#cdata', 4)).value = text;
\r
2753 text: function(text, raw) {
\r
2756 // Trim all redundant whitespace on non white space elements
\r
2757 if (!whiteSpaceElements[node.name]) {
\r
2758 text = text.replace(allWhiteSpaceRegExp, ' ');
\r
2760 if (node.lastChild && blockElements[node.lastChild.name])
\r
2761 text = text.replace(startWhiteSpaceRegExp, '');
\r
2764 // Do we need to create the node
\r
2765 if (text.length !== 0) {
\r
2766 textNode = createNode('#text', 3);
\r
2767 textNode.raw = !!raw;
\r
2768 node.append(textNode).value = text;
\r
2772 comment: function(text) {
\r
2773 node.append(createNode('#comment', 8)).value = text;
\r
2776 pi: function(name, text) {
\r
2777 node.append(createNode(name, 7)).value = text;
\r
2778 removeWhitespaceBefore(node);
\r
2781 doctype: function(text) {
\r
2784 newNode = node.append(createNode('#doctype', 10));
\r
2785 newNode.value = text;
\r
2786 removeWhitespaceBefore(node);
\r
2789 start: function(name, attrs, empty) {
\r
2790 var newNode, attrFiltersLen, elementRule, textNode, attrName, text, sibling, parent;
\r
2792 elementRule = validate ? schema.getElementRule(name) : {};
\r
2793 if (elementRule) {
\r
2794 newNode = createNode(elementRule.outputName || name, 1);
\r
2795 newNode.attributes = attrs;
\r
2796 newNode.shortEnded = empty;
\r
2798 node.append(newNode);
\r
2800 // Check if node is valid child of the parent node is the child is
\r
2801 // unknown we don't collect it since it's probably a custom element
\r
2802 parent = children[node.name];
\r
2803 if (parent && children[newNode.name] && !parent[newNode.name])
\r
2804 invalidChildren.push(newNode);
\r
2806 attrFiltersLen = attributeFilters.length;
\r
2807 while (attrFiltersLen--) {
\r
2808 attrName = attributeFilters[attrFiltersLen].name;
\r
2810 if (attrName in attrs.map) {
\r
2811 list = matchedAttributes[attrName];
\r
2814 list.push(newNode);
\r
2816 matchedAttributes[attrName] = [newNode];
\r
2820 // Trim whitespace before block
\r
2821 if (blockElements[name])
\r
2822 removeWhitespaceBefore(newNode);
\r
2824 // Change current node if the element wasn't empty i.e not <br /> or <img />
\r
2830 end: function(name) {
\r
2831 var textNode, elementRule, text, sibling, tempNode;
\r
2833 elementRule = validate ? schema.getElementRule(name) : {};
\r
2834 if (elementRule) {
\r
2835 if (blockElements[name]) {
\r
2836 if (!whiteSpaceElements[node.name]) {
\r
2837 // Trim whitespace at beginning of block
\r
2838 for (textNode = node.firstChild; textNode && textNode.type === 3; ) {
\r
2839 text = textNode.value.replace(startWhiteSpaceRegExp, '');
\r
2841 if (text.length > 0) {
\r
2842 textNode.value = text;
\r
2843 textNode = textNode.next;
\r
2845 sibling = textNode.next;
\r
2846 textNode.remove();
\r
2847 textNode = sibling;
\r
2851 // Trim whitespace at end of block
\r
2852 for (textNode = node.lastChild; textNode && textNode.type === 3; ) {
\r
2853 text = textNode.value.replace(endWhiteSpaceRegExp, '');
\r
2855 if (text.length > 0) {
\r
2856 textNode.value = text;
\r
2857 textNode = textNode.prev;
\r
2859 sibling = textNode.prev;
\r
2860 textNode.remove();
\r
2861 textNode = sibling;
\r
2866 // Trim start white space
\r
2867 textNode = node.prev;
\r
2868 if (textNode && textNode.type === 3) {
\r
2869 text = textNode.value.replace(startWhiteSpaceRegExp, '');
\r
2871 if (text.length > 0)
\r
2872 textNode.value = text;
\r
2874 textNode.remove();
\r
2878 // Handle empty nodes
\r
2879 if (elementRule.removeEmpty || elementRule.paddEmpty) {
\r
2880 if (node.isEmpty(nonEmptyElements)) {
\r
2881 if (elementRule.paddEmpty)
\r
2882 node.empty().append(new Node('#text', '3')).value = '\u00a0';
\r
2884 // Leave nodes that have a name like <a name="name">
\r
2885 if (!node.attributes.map.name) {
\r
2886 tempNode = node.parent;
\r
2887 node.empty().remove();
\r
2895 node = node.parent;
\r
2900 rootNode = node = new Node(settings.root_name, 11);
\r
2902 parser.parse(html);
\r
2905 fixInvalidChildren(invalidChildren);
\r
2907 // Run node filters
\r
2908 for (name in matchedNodes) {
\r
2909 list = nodeFilters[name];
\r
2910 nodes = matchedNodes[name];
\r
2912 // Remove already removed children
\r
2913 fi = nodes.length;
\r
2915 if (!nodes[fi].parent)
\r
2916 nodes.splice(fi, 1);
\r
2919 for (i = 0, l = list.length; i < l; i++)
\r
2920 list[i](nodes, name, args);
\r
2923 // Run attribute filters
\r
2924 for (i = 0, l = attributeFilters.length; i < l; i++) {
\r
2925 list = attributeFilters[i];
\r
2927 if (list.name in matchedAttributes) {
\r
2928 nodes = matchedAttributes[list.name];
\r
2930 // Remove already removed children
\r
2931 fi = nodes.length;
\r
2933 if (!nodes[fi].parent)
\r
2934 nodes.splice(fi, 1);
\r
2937 for (fi = 0, fl = list.callbacks.length; fi < fl; fi++)
\r
2938 list.callbacks[fi](nodes, list.name, args);
\r
2945 // Remove <br> at end of block elements Gecko and WebKit injects BR elements to
\r
2946 // make it possible to place the caret inside empty blocks. This logic tries to remove
\r
2947 // these elements and keep br elements that where intended to be there intact
\r
2948 if (settings.remove_trailing_brs) {
\r
2949 self.addNodeFilter('br', function(nodes, name) {
\r
2950 var i, l = nodes.length, node, blockElements = schema.getBlockElements(),
\r
2951 nonEmptyElements = schema.getNonEmptyElements(), parent, prev, prevName;
\r
2953 // Must loop forwards since it will otherwise remove all brs in <p>a<br><br><br></p>
\r
2954 for (i = 0; i < l; i++) {
\r
2956 parent = node.parent;
\r
2958 if (blockElements[node.parent.name] && node === parent.lastChild) {
\r
2959 // Loop all nodes to the right of the current node and check for other BR elements
\r
2960 // excluding bookmarks since they are invisible
\r
2963 prevName = prev.name;
\r
2965 // Ignore bookmarks
\r
2966 if (prevName !== "span" || prev.attr('data-mce-type') !== 'bookmark') {
\r
2967 // Found a non BR element
\r
2968 if (prevName !== "br")
\r
2971 // Found another br it's a <br><br> structure then don't remove anything
\r
2972 if (prevName === 'br') {
\r
2984 // Is the parent to be considered empty after we removed the BR
\r
2985 if (parent.isEmpty(nonEmptyElements)) {
\r
2986 elementRule = schema.getElementRule(parent.name);
\r
2988 // Remove or padd the element depending on schema rule
\r
2989 if (elementRule.removeEmpty)
\r
2991 else if (elementRule.paddEmpty)
\r
2992 parent.empty().append(new tinymce.html.Node('#text', 3)).value = '\u00a0';
\r
3002 tinymce.html.Writer = function(settings) {
\r
3003 var html = [], indent, indentBefore, indentAfter, encode, htmlOutput;
\r
3005 settings = settings || {};
\r
3006 indent = settings.indent;
\r
3007 indentBefore = tinymce.makeMap(settings.indent_before || '');
\r
3008 indentAfter = tinymce.makeMap(settings.indent_after || '');
\r
3009 encode = tinymce.html.Entities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities);
\r
3010 htmlOutput = settings.element_format == "html";
\r
3013 start: function(name, attrs, empty) {
\r
3014 var i, l, attr, value;
\r
3016 if (indent && indentBefore[name] && html.length > 0) {
\r
3017 value = html[html.length - 1];
\r
3019 if (value.length > 0 && value !== '\n')
\r
3023 html.push('<', name);
\r
3026 for (i = 0, l = attrs.length; i < l; i++) {
\r
3028 html.push(' ', attr.name, '="', encode(attr.value, true), '"');
\r
3032 if (!empty || htmlOutput)
\r
3033 html[html.length] = '>';
\r
3035 html[html.length] = ' />';
\r
3037 if (empty && indent && indentAfter[name] && html.length > 0) {
\r
3038 value = html[html.length - 1];
\r
3040 if (value.length > 0 && value !== '\n')
\r
3045 end: function(name) {
\r
3048 /*if (indent && indentBefore[name] && html.length > 0) {
\r
3049 value = html[html.length - 1];
\r
3051 if (value.length > 0 && value !== '\n')
\r
3055 html.push('</', name, '>');
\r
3057 if (indent && indentAfter[name] && html.length > 0) {
\r
3058 value = html[html.length - 1];
\r
3060 if (value.length > 0 && value !== '\n')
\r
3065 text: function(text, raw) {
\r
3066 if (text.length > 0)
\r
3067 html[html.length] = raw ? text : encode(text);
\r
3070 cdata: function(text) {
\r
3071 html.push('<![CDATA[', text, ']]>');
\r
3074 comment: function(text) {
\r
3075 html.push('<!--', text, '-->');
\r
3078 pi: function(name, text) {
\r
3080 html.push('<?', name, ' ', text, '?>');
\r
3082 html.push('<?', name, '?>');
\r
3088 doctype: function(text) {
\r
3089 html.push('<!DOCTYPE', text, '>', indent ? '\n' : '');
\r
3092 reset: function() {
\r
3096 getContent: function() {
\r
3097 return html.join('').replace(/\n$/, '');
\r
3102 (function(tinymce) {
\r
3103 tinymce.html.Serializer = function(settings, schema) {
\r
3104 var self = this, writer = new tinymce.html.Writer(settings);
\r
3106 settings = settings || {};
\r
3107 settings.validate = "validate" in settings ? settings.validate : true;
\r
3109 self.schema = schema = schema || new tinymce.html.Schema();
\r
3110 self.writer = writer;
\r
3112 self.serialize = function(node) {
\r
3113 var handlers, validate;
\r
3115 validate = settings.validate;
\r
3119 3: function(node, raw) {
\r
3120 writer.text(node.value, node.raw);
\r
3124 8: function(node) {
\r
3125 writer.comment(node.value);
\r
3128 // Processing instruction
\r
3129 7: function(node) {
\r
3130 writer.pi(node.name, node.value);
\r
3134 10: function(node) {
\r
3135 writer.doctype(node.value);
\r
3139 4: function(node) {
\r
3140 writer.cdata(node.value);
\r
3143 // Document fragment
\r
3144 11: function(node) {
\r
3145 if ((node = node.firstChild)) {
\r
3148 } while (node = node.next);
\r
3155 function walk(node) {
\r
3156 var handler = handlers[node.type], name, isEmpty, attrs, attrName, attrValue, sortedAttrs, i, l, elementRule;
\r
3160 isEmpty = node.shortEnded;
\r
3161 attrs = node.attributes;
\r
3163 // Sort attributes
\r
3164 if (validate && attrs && attrs.length > 1) {
\r
3166 sortedAttrs.map = {};
\r
3168 elementRule = schema.getElementRule(node.name);
\r
3169 for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) {
\r
3170 attrName = elementRule.attributesOrder[i];
\r
3172 if (attrName in attrs.map) {
\r
3173 attrValue = attrs.map[attrName];
\r
3174 sortedAttrs.map[attrName] = attrValue;
\r
3175 sortedAttrs.push({name: attrName, value: attrValue});
\r
3179 for (i = 0, l = attrs.length; i < l; i++) {
\r
3180 attrName = attrs[i].name;
\r
3182 if (!(attrName in sortedAttrs.map)) {
\r
3183 attrValue = attrs.map[attrName];
\r
3184 sortedAttrs.map[attrName] = attrValue;
\r
3185 sortedAttrs.push({name: attrName, value: attrValue});
\r
3189 attrs = sortedAttrs;
\r
3192 writer.start(node.name, attrs, isEmpty);
\r
3195 if ((node = node.firstChild)) {
\r
3198 } while (node = node.next);
\r
3207 // Serialize element and treat all non elements as fragments
\r
3208 if (node.type == 1 && !settings.inner)
\r
3211 handlers[11](node);
\r
3213 return writer.getContent();
\r
3218 (function(tinymce) {
\r
3220 var each = tinymce.each,
\r
3222 isWebKit = tinymce.isWebKit,
\r
3223 isIE = tinymce.isIE,
\r
3224 Entities = tinymce.html.Entities,
\r
3225 simpleSelectorRe = /^([a-z0-9],?)+$/i,
\r
3226 blockElementsMap = tinymce.html.Schema.blockElementsMap,
\r
3227 whiteSpaceRegExp = /^[ \t\r\n]*$/;
\r
3229 tinymce.create('tinymce.dom.DOMUtils', {
\r
3233 pixelStyles : /^(top|left|bottom|right|width|height|borderWidth)$/,
\r
3235 "for" : "htmlFor",
\r
3236 "class" : "className",
\r
3237 className : "className",
\r
3238 checked : "checked",
\r
3239 disabled : "disabled",
\r
3240 maxlength : "maxLength",
\r
3241 readonly : "readOnly",
\r
3242 selected : "selected",
\r
3249 DOMUtils : function(d, s) {
\r
3250 var t = this, globalStyle;
\r
3255 t.cssFlicker = false;
\r
3257 t.stdMode = !tinymce.isIE || d.documentMode >= 8;
\r
3258 t.boxModel = !tinymce.isIE || d.compatMode == "CSS1Compat" || t.stdMode;
\r
3259 t.hasOuterHTML = "outerHTML" in d.createElement("a");
\r
3261 t.settings = s = tinymce.extend({
\r
3262 keep_values : false,
\r
3266 t.schema = s.schema;
\r
3267 t.styles = new tinymce.html.Styles({
\r
3268 url_converter : s.url_converter,
\r
3269 url_converter_scope : s.url_converter_scope
\r
3272 // Fix IE6SP2 flicker and check it failed for pre SP2
\r
3273 if (tinymce.isIE6) {
\r
3275 d.execCommand('BackgroundImageCache', false, true);
\r
3277 t.cssFlicker = true;
\r
3282 // Add missing HTML 4/5 elements to IE
\r
3283 ('abbr article aside audio canvas ' +
\r
3284 'details figcaption figure footer ' +
\r
3285 'header hgroup mark menu meter nav ' +
\r
3286 'output progress section summary ' +
\r
3287 'time video').replace(/\w+/g, function(name) {
\r
3288 d.createElement(name);
\r
3292 tinymce.addUnload(t.destroy, t);
\r
3295 getRoot : function() {
\r
3296 var t = this, s = t.settings;
\r
3298 return (s && t.get(s.root_element)) || t.doc.body;
\r
3301 getViewPort : function(w) {
\r
3304 w = !w ? this.win : w;
\r
3306 b = this.boxModel ? d.documentElement : d.body;
\r
3308 // Returns viewport size excluding scrollbars
\r
3310 x : w.pageXOffset || b.scrollLeft,
\r
3311 y : w.pageYOffset || b.scrollTop,
\r
3312 w : w.innerWidth || b.clientWidth,
\r
3313 h : w.innerHeight || b.clientHeight
\r
3317 getRect : function(e) {
\r
3318 var p, t = this, sr;
\r
3322 sr = t.getSize(e);
\r
3332 getSize : function(e) {
\r
3333 var t = this, w, h;
\r
3336 w = t.getStyle(e, 'width');
\r
3337 h = t.getStyle(e, 'height');
\r
3339 // Non pixel value, then force offset/clientWidth
\r
3340 if (w.indexOf('px') === -1)
\r
3343 // Non pixel value, then force offset/clientWidth
\r
3344 if (h.indexOf('px') === -1)
\r
3348 w : parseInt(w) || e.offsetWidth || e.clientWidth,
\r
3349 h : parseInt(h) || e.offsetHeight || e.clientHeight
\r
3353 getParent : function(n, f, r) {
\r
3354 return this.getParents(n, f, r, false);
\r
3357 getParents : function(n, f, r, c) {
\r
3358 var t = this, na, se = t.settings, o = [];
\r
3361 c = c === undefined;
\r
3363 if (se.strict_root)
\r
3364 r = r || t.getRoot();
\r
3366 // Wrap node name as func
\r
3367 if (is(f, 'string')) {
\r
3371 f = function(n) {return n.nodeType == 1;};
\r
3374 return t.is(n, na);
\r
3380 if (n == r || !n.nodeType || n.nodeType === 9)
\r
3393 return c ? o : null;
\r
3396 get : function(e) {
\r
3399 if (e && this.doc && typeof(e) == 'string') {
\r
3401 e = this.doc.getElementById(e);
\r
3403 // IE and Opera returns meta elements when they match the specified input ID, but getElementsByName seems to do the trick
\r
3404 if (e && e.id !== n)
\r
3405 return this.doc.getElementsByName(n)[1];
\r
3411 getNext : function(node, selector) {
\r
3412 return this._findSib(node, selector, 'nextSibling');
\r
3415 getPrev : function(node, selector) {
\r
3416 return this._findSib(node, selector, 'previousSibling');
\r
3420 select : function(pa, s) {
\r
3423 return tinymce.dom.Sizzle(pa, t.get(s) || t.get(t.settings.root_element) || t.doc, []);
\r
3426 is : function(n, selector) {
\r
3429 // If it isn't an array then try to do some simple selectors instead of Sizzle for to boost performance
\r
3430 if (n.length === undefined) {
\r
3431 // Simple all selector
\r
3432 if (selector === '*')
\r
3433 return n.nodeType == 1;
\r
3435 // Simple selector just elements
\r
3436 if (simpleSelectorRe.test(selector)) {
\r
3437 selector = selector.toLowerCase().split(/,/);
\r
3438 n = n.nodeName.toLowerCase();
\r
3440 for (i = selector.length - 1; i >= 0; i--) {
\r
3441 if (selector[i] == n)
\r
3449 return tinymce.dom.Sizzle.matches(selector, n.nodeType ? [n] : n).length > 0;
\r
3453 add : function(p, n, a, h, c) {
\r
3456 return this.run(p, function(p) {
\r
3459 e = is(n, 'string') ? t.doc.createElement(n) : n;
\r
3460 t.setAttribs(e, a);
\r
3469 return !c ? p.appendChild(e) : e;
\r
3473 create : function(n, a, h) {
\r
3474 return this.add(this.doc.createElement(n), n, a, h, 1);
\r
3477 createHTML : function(n, a, h) {
\r
3478 var o = '', t = this, k;
\r
3483 if (a.hasOwnProperty(k))
\r
3484 o += ' ' + k + '="' + t.encode(a[k]) + '"';
\r
3487 // A call to tinymce.is doesn't work for some odd reason on IE9 possible bug inside their JS runtime
\r
3488 if (typeof(h) != "undefined")
\r
3489 return o + '>' + h + '</' + n + '>';
\r
3494 remove : function(node, keep_children) {
\r
3495 return this.run(node, function(node) {
\r
3496 var child, parent = node.parentNode;
\r
3501 if (keep_children) {
\r
3502 while (child = node.firstChild) {
\r
3503 // IE 8 will crash if you don't remove completely empty text nodes
\r
3504 if (!tinymce.isIE || child.nodeType !== 3 || child.nodeValue)
\r
3505 parent.insertBefore(child, node);
\r
3507 node.removeChild(child);
\r
3511 return parent.removeChild(node);
\r
3515 setStyle : function(n, na, v) {
\r
3518 return t.run(n, function(e) {
\r
3523 // Camelcase it, if needed
\r
3524 na = na.replace(/-(\D)/g, function(a, b){
\r
3525 return b.toUpperCase();
\r
3528 // Default px suffix on these
\r
3529 if (t.pixelStyles.test(na) && (tinymce.is(v, 'number') || /^[\-0-9\.]+$/.test(v)))
\r
3534 // IE specific opacity
\r
3536 s.filter = v === '' ? '' : "alpha(opacity=" + (v * 100) + ")";
\r
3538 if (!n.currentStyle || !n.currentStyle.hasLayout)
\r
3539 s.display = 'inline-block';
\r
3542 // Fix for older browsers
\r
3543 s[na] = s['-moz-opacity'] = s['-khtml-opacity'] = v || '';
\r
3547 isIE ? s.styleFloat = v : s.cssFloat = v;
\r
3554 // Force update of the style data
\r
3555 if (t.settings.update_styles)
\r
3556 t.setAttrib(e, 'data-mce-style');
\r
3560 getStyle : function(n, na, c) {
\r
3567 if (this.doc.defaultView && c) {
\r
3568 // Remove camelcase
\r
3569 na = na.replace(/[A-Z]/g, function(a){
\r
3574 return this.doc.defaultView.getComputedStyle(n, null).getPropertyValue(na);
\r
3576 // Old safari might fail
\r
3581 // Camelcase it, if needed
\r
3582 na = na.replace(/-(\D)/g, function(a, b){
\r
3583 return b.toUpperCase();
\r
3586 if (na == 'float')
\r
3587 na = isIE ? 'styleFloat' : 'cssFloat';
\r
3590 if (n.currentStyle && c)
\r
3591 return n.currentStyle[na];
\r
3593 return n.style ? n.style[na] : undefined;
\r
3596 setStyles : function(e, o) {
\r
3597 var t = this, s = t.settings, ol;
\r
3599 ol = s.update_styles;
\r
3600 s.update_styles = 0;
\r
3602 each(o, function(v, n) {
\r
3603 t.setStyle(e, n, v);
\r
3606 // Update style info
\r
3607 s.update_styles = ol;
\r
3608 if (s.update_styles)
\r
3609 t.setAttrib(e, s.cssText);
\r
3612 removeAllAttribs: function(e) {
\r
3613 return this.run(e, function(e) {
\r
3614 var i, attrs = e.attributes;
\r
3615 for (i = attrs.length - 1; i >= 0; i--) {
\r
3616 e.removeAttributeNode(attrs.item(i));
\r
3621 setAttrib : function(e, n, v) {
\r
3624 // Whats the point
\r
3628 // Strict XML mode
\r
3629 if (t.settings.strict)
\r
3630 n = n.toLowerCase();
\r
3632 return this.run(e, function(e) {
\r
3633 var s = t.settings;
\r
3637 if (!is(v, 'string')) {
\r
3638 each(v, function(v, n) {
\r
3639 t.setStyle(e, n, v);
\r
3645 // No mce_style for elements with these since they might get resized by the user
\r
3646 if (s.keep_values) {
\r
3647 if (v && !t._isRes(v))
\r
3648 e.setAttribute('data-mce-style', v, 2);
\r
3650 e.removeAttribute('data-mce-style', 2);
\r
3653 e.style.cssText = v;
\r
3657 e.className = v || ''; // Fix IE null bug
\r
3662 if (s.keep_values) {
\r
3663 if (s.url_converter)
\r
3664 v = s.url_converter.call(s.url_converter_scope || t, v, n, e);
\r
3666 t.setAttrib(e, 'data-mce-' + n, v, 2);
\r
3672 e.setAttribute('data-mce-style', v);
\r
3676 if (is(v) && v !== null && v.length !== 0)
\r
3677 e.setAttribute(n, '' + v, 2);
\r
3679 e.removeAttribute(n, 2);
\r
3683 setAttribs : function(e, o) {
\r
3686 return this.run(e, function(e) {
\r
3687 each(o, function(v, n) {
\r
3688 t.setAttrib(e, n, v);
\r
3693 getAttrib : function(e, n, dv) {
\r
3698 if (!e || e.nodeType !== 1)
\r
3704 // Try the mce variant for these
\r
3705 if (/^(src|href|style|coords|shape)$/.test(n)) {
\r
3706 v = e.getAttribute("data-mce-" + n);
\r
3712 if (isIE && t.props[n]) {
\r
3713 v = e[t.props[n]];
\r
3714 v = v && v.nodeValue ? v.nodeValue : v;
\r
3718 v = e.getAttribute(n, 2);
\r
3720 // Check boolean attribs
\r
3721 if (/^(checked|compact|declare|defer|disabled|ismap|multiple|nohref|noshade|nowrap|readonly|selected)$/.test(n)) {
\r
3722 if (e[t.props[n]] === true && v === '')
\r
3725 return v ? n : '';
\r
3728 // Inner input elements will override attributes on form elements
\r
3729 if (e.nodeName === "FORM" && e.getAttributeNode(n))
\r
3730 return e.getAttributeNode(n).nodeValue;
\r
3732 if (n === 'style') {
\r
3733 v = v || e.style.cssText;
\r
3736 v = t.serializeStyle(t.parseStyle(v), e.nodeName);
\r
3738 if (t.settings.keep_values && !t._isRes(v))
\r
3739 e.setAttribute('data-mce-style', v);
\r
3743 // Remove Apple and WebKit stuff
\r
3744 if (isWebKit && n === "class" && v)
\r
3745 v = v.replace(/(apple|webkit)\-[a-z\-]+/gi, '');
\r
3747 // Handle IE issues
\r
3752 // IE returns 1 as default value
\r
3759 // IE returns +0 as default value for size
\r
3760 if (v === '+0' || v === 20 || v === 0)
\r
3777 // IE returns -1 as default value
\r
3785 // IE returns default value
\r
3786 if (v === 32768 || v === 2147483647 || v === '32768')
\r
3801 v = v.toLowerCase();
\r
3805 // IE has odd anonymous function for event attributes
\r
3806 if (n.indexOf('on') === 0 && v)
\r
3807 v = tinymce._replace(/^function\s+\w+\(\)\s+\{\s+(.*)\s+\}$/, '$1', '' + v);
\r
3811 return (v !== undefined && v !== null && v !== '') ? '' + v : dv;
\r
3814 getPos : function(n, ro) {
\r
3815 var t = this, x = 0, y = 0, e, d = t.doc, r;
\r
3818 ro = ro || d.body;
\r
3821 // Use getBoundingClientRect on IE, Opera has it but it's not perfect
\r
3822 if (isIE && !t.stdMode) {
\r
3823 n = n.getBoundingClientRect();
\r
3824 e = t.boxModel ? d.documentElement : d.body;
\r
3825 x = t.getStyle(t.select('html')[0], 'borderWidth'); // Remove border
\r
3826 x = (x == 'medium' || t.boxModel && !t.isIE6) && 2 || x;
\r
3828 return {x : n.left + e.scrollLeft - x, y : n.top + e.scrollTop - x};
\r
3832 while (r && r != ro && r.nodeType) {
\r
3833 x += r.offsetLeft || 0;
\r
3834 y += r.offsetTop || 0;
\r
3835 r = r.offsetParent;
\r
3839 while (r && r != ro && r.nodeType) {
\r
3840 x -= r.scrollLeft || 0;
\r
3841 y -= r.scrollTop || 0;
\r
3846 return {x : x, y : y};
\r
3849 parseStyle : function(st) {
\r
3850 return this.styles.parse(st);
\r
3853 serializeStyle : function(o, name) {
\r
3854 return this.styles.serialize(o, name);
\r
3857 loadCSS : function(u) {
\r
3858 var t = this, d = t.doc, head;
\r
3863 head = t.select('head')[0];
\r
3865 each(u.split(','), function(u) {
\r
3871 t.files[u] = true;
\r
3872 link = t.create('link', {rel : 'stylesheet', href : tinymce._addVer(u)});
\r
3874 // IE 8 has a bug where dynamically loading stylesheets would produce a 1 item remaining bug
\r
3875 // This fix seems to resolve that issue by realcing the document ones a stylesheet finishes loading
\r
3876 // It's ugly but it seems to work fine.
\r
3877 if (isIE && d.documentMode && d.recalc) {
\r
3878 link.onload = function() {
\r
3882 link.onload = null;
\r
3886 head.appendChild(link);
\r
3890 addClass : function(e, c) {
\r
3891 return this.run(e, function(e) {
\r
3897 if (this.hasClass(e, c))
\r
3898 return e.className;
\r
3900 o = this.removeClass(e, c);
\r
3902 return e.className = (o != '' ? (o + ' ') : '') + c;
\r
3906 removeClass : function(e, c) {
\r
3909 return t.run(e, function(e) {
\r
3912 if (t.hasClass(e, c)) {
\r
3914 re = new RegExp("(^|\\s+)" + c + "(\\s+|$)", "g");
\r
3916 v = e.className.replace(re, ' ');
\r
3917 v = tinymce.trim(v != ' ' ? v : '');
\r
3921 // Empty class attr
\r
3923 e.removeAttribute('class');
\r
3924 e.removeAttribute('className');
\r
3930 return e.className;
\r
3934 hasClass : function(n, c) {
\r
3940 return (' ' + n.className + ' ').indexOf(' ' + c + ' ') !== -1;
\r
3943 show : function(e) {
\r
3944 return this.setStyle(e, 'display', 'block');
\r
3947 hide : function(e) {
\r
3948 return this.setStyle(e, 'display', 'none');
\r
3951 isHidden : function(e) {
\r
3954 return !e || e.style.display == 'none' || this.getStyle(e, 'display') == 'none';
\r
3957 uniqueId : function(p) {
\r
3958 return (!p ? 'mce_' : p) + (this.counter++);
\r
3961 setHTML : function(element, html) {
\r
3964 return self.run(element, function(element) {
\r
3966 // Remove all child nodes, IE keeps empty text nodes in DOM
\r
3967 while (element.firstChild)
\r
3968 element.removeChild(element.firstChild);
\r
3971 // IE will remove comments from the beginning
\r
3972 // unless you padd the contents with something
\r
3973 element.innerHTML = '<br />' + html;
\r
3974 element.removeChild(element.firstChild);
\r
3976 // IE sometimes produces an unknown runtime error on innerHTML if it's an block element within a block element for example a div inside a p
\r
3977 // This seems to fix this problem
\r
3979 // Create new div with HTML contents and a BR infront to keep comments
\r
3980 element = self.create('div');
\r
3981 element.innerHTML = '<br />' + html;
\r
3983 // Add all children from div to target
\r
3984 each (element.childNodes, function(node, i) {
\r
3985 // Skip br element
\r
3987 element.appendChild(node);
\r
3991 element.innerHTML = html;
\r
3997 getOuterHTML : function(elm) {
\r
3998 var doc, self = this;
\r
4000 elm = self.get(elm);
\r
4005 if (elm.nodeType === 1 && self.hasOuterHTML)
\r
4006 return elm.outerHTML;
\r
4008 doc = (elm.ownerDocument || self.doc).createElement("body");
\r
4009 doc.appendChild(elm.cloneNode(true));
\r
4011 return doc.innerHTML;
\r
4014 setOuterHTML : function(e, h, d) {
\r
4017 function setHTML(e, h, d) {
\r
4020 tp = d.createElement("body");
\r
4025 t.insertAfter(n.cloneNode(true), e);
\r
4026 n = n.previousSibling;
\r
4032 return this.run(e, function(e) {
\r
4035 // Only set HTML on elements
\r
4036 if (e.nodeType == 1) {
\r
4037 d = d || e.ownerDocument || t.doc;
\r
4041 // Try outerHTML for IE it sometimes produces an unknown runtime error
\r
4042 if (isIE && e.nodeType == 1)
\r
4047 // Fix for unknown runtime error
\r
4056 decode : Entities.decode,
\r
4058 encode : Entities.encodeAllRaw,
\r
4060 insertAfter : function(node, reference_node) {
\r
4061 reference_node = this.get(reference_node);
\r
4063 return this.run(node, function(node) {
\r
4064 var parent, nextSibling;
\r
4066 parent = reference_node.parentNode;
\r
4067 nextSibling = reference_node.nextSibling;
\r
4070 parent.insertBefore(node, nextSibling);
\r
4072 parent.appendChild(node);
\r
4078 isBlock : function(node) {
\r
4079 var type = node.nodeType;
\r
4081 // If it's a node then check the type and use the nodeName
\r
4083 return !!(type === 1 && blockElementsMap[node.nodeName]);
\r
4085 return !!blockElementsMap[node];
\r
4088 replace : function(n, o, k) {
\r
4091 if (is(o, 'array'))
\r
4092 n = n.cloneNode(true);
\r
4094 return t.run(o, function(o) {
\r
4096 each(tinymce.grep(o.childNodes), function(c) {
\r
4101 return o.parentNode.replaceChild(n, o);
\r
4105 rename : function(elm, name) {
\r
4106 var t = this, newElm;
\r
4108 if (elm.nodeName != name.toUpperCase()) {
\r
4109 // Rename block element
\r
4110 newElm = t.create(name);
\r
4112 // Copy attribs to new block
\r
4113 each(t.getAttribs(elm), function(attr_node) {
\r
4114 t.setAttrib(newElm, attr_node.nodeName, t.getAttrib(elm, attr_node.nodeName));
\r
4118 t.replace(newElm, elm, 1);
\r
4121 return newElm || elm;
\r
4124 findCommonAncestor : function(a, b) {
\r
4130 while (pe && ps != pe)
\r
4131 pe = pe.parentNode;
\r
4136 ps = ps.parentNode;
\r
4139 if (!ps && a.ownerDocument)
\r
4140 return a.ownerDocument.documentElement;
\r
4145 toHex : function(s) {
\r
4146 var c = /^\s*rgb\s*?\(\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?\)\s*$/i.exec(s);
\r
4149 s = parseInt(s).toString(16);
\r
4151 return s.length > 1 ? s : '0' + s; // 0 -> 00
\r
4155 s = '#' + hex(c[1]) + hex(c[2]) + hex(c[3]);
\r
4163 getClasses : function() {
\r
4164 var t = this, cl = [], i, lo = {}, f = t.settings.class_filter, ov;
\r
4169 function addClasses(s) {
\r
4170 // IE style imports
\r
4171 each(s.imports, function(r) {
\r
4175 each(s.cssRules || s.rules, function(r) {
\r
4176 // Real type or fake it on IE
\r
4177 switch (r.type || 1) {
\r
4180 if (r.selectorText) {
\r
4181 each(r.selectorText.split(','), function(v) {
\r
4182 v = v.replace(/^\s*|\s*$|^\s\./g, "");
\r
4184 // Is internal or it doesn't contain a class
\r
4185 if (/\.mce/.test(v) || !/\.[\w\-]+$/.test(v))
\r
4188 // Remove everything but class name
\r
4190 v = tinymce._replace(/.*\.([a-z0-9_\-]+).*/i, '$1', v);
\r
4193 if (f && !(v = f(v, ov)))
\r
4197 cl.push({'class' : v});
\r
4206 addClasses(r.styleSheet);
\r
4213 each(t.doc.styleSheets, addClasses);
\r
4218 if (cl.length > 0)
\r
4224 run : function(e, f, s) {
\r
4227 if (t.doc && typeof(e) === 'string')
\r
4234 if (!e.nodeType && (e.length || e.length === 0)) {
\r
4237 each(e, function(e, i) {
\r
4239 if (typeof(e) == 'string')
\r
4240 e = t.doc.getElementById(e);
\r
4242 o.push(f.call(s, e, i));
\r
4249 return f.call(s, e);
\r
4252 getAttribs : function(n) {
\r
4263 // Object will throw exception in IE
\r
4264 if (n.nodeName == 'OBJECT')
\r
4265 return n.attributes;
\r
4267 // IE doesn't keep the selected attribute if you clone option elements
\r
4268 if (n.nodeName === 'OPTION' && this.getAttrib(n, 'selected'))
\r
4269 o.push({specified : 1, nodeName : 'selected'});
\r
4271 // It's crazy that this is faster in IE but it's because it returns all attributes all the time
\r
4272 n.cloneNode(false).outerHTML.replace(/<\/?[\w:\-]+ ?|=[\"][^\"]+\"|=\'[^\']+\'|=[\w\-]+|>/gi, '').replace(/[\w:\-]+/gi, function(a) {
\r
4273 o.push({specified : 1, nodeName : a});
\r
4279 return n.attributes;
\r
4282 isEmpty : function(node, elements) {
\r
4283 var self = this, i, attributes, type, walker, name;
\r
4285 node = node.firstChild;
\r
4287 walker = new tinymce.dom.TreeWalker(node);
\r
4288 elements = elements || self.schema ? self.schema.getNonEmptyElements() : null;
\r
4291 type = node.nodeType;
\r
4294 // Ignore bogus elements
\r
4295 if (node.getAttribute('data-mce-bogus'))
\r
4298 // Keep empty elements like <img />
\r
4299 if (elements && elements[node.nodeName.toLowerCase()])
\r
4302 // Keep elements with data attributes or name attribute like <a name="1"></a>
\r
4303 attributes = self.getAttribs(node);
\r
4304 i = node.attributes.length;
\r
4306 name = node.attributes[i].nodeName;
\r
4307 if (name === "name" || name.indexOf('data-') === 0)
\r
4312 // Keep non whitespace text nodes
\r
4313 if ((type === 3 && !whiteSpaceRegExp.test(node.nodeValue)))
\r
4315 } while (node = walker.next());
\r
4321 destroy : function(s) {
\r
4325 t.events.destroy();
\r
4327 t.win = t.doc = t.root = t.events = null;
\r
4329 // Manual destroy then remove unload handler
\r
4331 tinymce.removeUnload(t.destroy);
\r
4334 createRng : function() {
\r
4337 return d.createRange ? d.createRange() : new tinymce.dom.Range(this);
\r
4340 nodeIndex : function(node, normalized) {
\r
4341 var idx = 0, lastNodeType, lastNode, nodeType, nodeValueExists;
\r
4344 for (lastNodeType = node.nodeType, node = node.previousSibling, lastNode = node; node; node = node.previousSibling) {
\r
4345 nodeType = node.nodeType;
\r
4347 // Normalize text nodes
\r
4348 if (normalized && nodeType == 3) {
\r
4349 // ensure that text nodes that have been removed are handled correctly in Internet Explorer.
\r
4350 // (the nodeValue attribute will not exist, and will error here).
\r
4351 nodeValueExists = false;
\r
4352 try {nodeValueExists = node.nodeValue.length} catch (c) {}
\r
4353 if (nodeType == lastNodeType || !nodeValueExists)
\r
4357 lastNodeType = nodeType;
\r
4364 split : function(pe, e, re) {
\r
4365 var t = this, r = t.createRng(), bef, aft, pa;
\r
4367 // W3C valid browsers tend to leave empty nodes to the left/right side of the contents, this makes sense
\r
4368 // but we don't want that in our code since it serves no purpose for the end user
\r
4369 // For example if this is chopped:
\r
4370 // <p>text 1<span><b>CHOP</b></span>text 2</p>
\r
4372 // <p>text 1<span></span></p><b>CHOP</b><p><span></span>text 2</p>
\r
4373 // this function will then trim of empty edges and produce:
\r
4374 // <p>text 1</p><b>CHOP</b><p>text 2</p>
\r
4375 function trim(node) {
\r
4376 var i, children = node.childNodes, type = node.nodeType;
\r
4378 if (type == 1 && node.getAttribute('data-mce-type') == 'bookmark')
\r
4381 for (i = children.length - 1; i >= 0; i--)
\r
4382 trim(children[i]);
\r
4385 // Keep non whitespace text nodes
\r
4386 if (type == 3 && node.nodeValue.length > 0) {
\r
4387 // If parent element isn't a block or there isn't any useful contents for example "<p> </p>"
\r
4388 if (!t.isBlock(node.parentNode) || tinymce.trim(node.nodeValue).length > 0)
\r
4390 } else if (type == 1) {
\r
4391 // If the only child is a bookmark then move it up
\r
4392 children = node.childNodes;
\r
4393 if (children.length == 1 && children[0] && children[0].nodeType == 1 && children[0].getAttribute('data-mce-type') == 'bookmark')
\r
4394 node.parentNode.insertBefore(children[0], node);
\r
4396 // Keep non empty elements or img, hr etc
\r
4397 if (children.length || /^(br|hr|input|img)$/i.test(node.nodeName))
\r
4408 // Get before chunk
\r
4409 r.setStart(pe.parentNode, t.nodeIndex(pe));
\r
4410 r.setEnd(e.parentNode, t.nodeIndex(e));
\r
4411 bef = r.extractContents();
\r
4413 // Get after chunk
\r
4414 r = t.createRng();
\r
4415 r.setStart(e.parentNode, t.nodeIndex(e) + 1);
\r
4416 r.setEnd(pe.parentNode, t.nodeIndex(pe) + 1);
\r
4417 aft = r.extractContents();
\r
4419 // Insert before chunk
\r
4420 pa = pe.parentNode;
\r
4421 pa.insertBefore(trim(bef), pe);
\r
4423 // Insert middle chunk
\r
4425 pa.replaceChild(re, e);
\r
4427 pa.insertBefore(e, pe);
\r
4429 // Insert after chunk
\r
4430 pa.insertBefore(trim(aft), pe);
\r
4437 bind : function(target, name, func, scope) {
\r
4441 t.events = new tinymce.dom.EventUtils();
\r
4443 return t.events.add(target, name, func, scope || this);
\r
4446 unbind : function(target, name, func) {
\r
4450 t.events = new tinymce.dom.EventUtils();
\r
4452 return t.events.remove(target, name, func);
\r
4456 _findSib : function(node, selector, name) {
\r
4457 var t = this, f = selector;
\r
4460 // If expression make a function of it using is
\r
4461 if (is(f, 'string')) {
\r
4462 f = function(node) {
\r
4463 return t.is(node, selector);
\r
4467 // Loop all siblings
\r
4468 for (node = node[name]; node; node = node[name]) {
\r
4477 _isRes : function(c) {
\r
4478 // Is live resizble element
\r
4479 return /^(top|left|bottom|right|width|height)/i.test(c) || /;\s*(top|left|bottom|right|width|height)/i.test(c);
\r
4483 walk : function(n, f, s) {
\r
4484 var d = this.doc, w;
\r
4486 if (d.createTreeWalker) {
\r
4487 w = d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, false);
\r
4489 while ((n = w.nextNode()) != null)
\r
4490 f.call(s || this, n);
\r
4492 tinymce.walk(n, f, 'childNodes', s);
\r
4497 toRGB : function(s) {
\r
4498 var c = /^\s*?#([0-9A-F]{2})([0-9A-F]{1,2})([0-9A-F]{2})?\s*?$/.exec(s);
\r
4501 // #FFF -> #FFFFFF
\r
4503 c[3] = c[2] = c[1];
\r
4505 return "rgb(" + parseInt(c[1], 16) + "," + parseInt(c[2], 16) + "," + parseInt(c[3], 16) + ")";
\r
4513 tinymce.DOM = new tinymce.dom.DOMUtils(document, {process_html : 0});
\r
4517 // Range constructor
\r
4518 function Range(dom) {
\r
4526 START_OFFSET = 'startOffset',
\r
4527 START_CONTAINER = 'startContainer',
\r
4528 END_CONTAINER = 'endContainer',
\r
4529 END_OFFSET = 'endOffset',
\r
4530 extend = tinymce.extend,
\r
4531 nodeIndex = dom.nodeIndex;
\r
4535 startContainer : doc,
\r
4537 endContainer : doc,
\r
4540 commonAncestorContainer : doc,
\r
4542 // Range constants
\r
4543 START_TO_START : 0,
\r
4549 setStart : setStart,
\r
4551 setStartBefore : setStartBefore,
\r
4552 setStartAfter : setStartAfter,
\r
4553 setEndBefore : setEndBefore,
\r
4554 setEndAfter : setEndAfter,
\r
4555 collapse : collapse,
\r
4556 selectNode : selectNode,
\r
4557 selectNodeContents : selectNodeContents,
\r
4558 compareBoundaryPoints : compareBoundaryPoints,
\r
4559 deleteContents : deleteContents,
\r
4560 extractContents : extractContents,
\r
4561 cloneContents : cloneContents,
\r
4562 insertNode : insertNode,
\r
4563 surroundContents : surroundContents,
\r
4564 cloneRange : cloneRange
\r
4567 function setStart(n, o) {
\r
4568 _setEndPoint(TRUE, n, o);
\r
4571 function setEnd(n, o) {
\r
4572 _setEndPoint(FALSE, n, o);
\r
4575 function setStartBefore(n) {
\r
4576 setStart(n.parentNode, nodeIndex(n));
\r
4579 function setStartAfter(n) {
\r
4580 setStart(n.parentNode, nodeIndex(n) + 1);
\r
4583 function setEndBefore(n) {
\r
4584 setEnd(n.parentNode, nodeIndex(n));
\r
4587 function setEndAfter(n) {
\r
4588 setEnd(n.parentNode, nodeIndex(n) + 1);
\r
4591 function collapse(ts) {
\r
4593 t[END_CONTAINER] = t[START_CONTAINER];
\r
4594 t[END_OFFSET] = t[START_OFFSET];
\r
4596 t[START_CONTAINER] = t[END_CONTAINER];
\r
4597 t[START_OFFSET] = t[END_OFFSET];
\r
4600 t.collapsed = TRUE;
\r
4603 function selectNode(n) {
\r
4604 setStartBefore(n);
\r
4608 function selectNodeContents(n) {
\r
4610 setEnd(n, n.nodeType === 1 ? n.childNodes.length : n.nodeValue.length);
\r
4613 function compareBoundaryPoints(h, r) {
\r
4614 var sc = t[START_CONTAINER], so = t[START_OFFSET], ec = t[END_CONTAINER], eo = t[END_OFFSET],
\r
4615 rsc = r.startContainer, rso = r.startOffset, rec = r.endContainer, reo = r.endOffset;
\r
4617 // Check START_TO_START
\r
4619 return _compareBoundaryPoints(sc, so, rsc, rso);
\r
4621 // Check START_TO_END
\r
4623 return _compareBoundaryPoints(ec, eo, rsc, rso);
\r
4625 // Check END_TO_END
\r
4627 return _compareBoundaryPoints(ec, eo, rec, reo);
\r
4629 // Check END_TO_START
\r
4631 return _compareBoundaryPoints(sc, so, rec, reo);
\r
4634 function deleteContents() {
\r
4635 _traverse(DELETE);
\r
4638 function extractContents() {
\r
4639 return _traverse(EXTRACT);
\r
4642 function cloneContents() {
\r
4643 return _traverse(CLONE);
\r
4646 function insertNode(n) {
\r
4647 var startContainer = this[START_CONTAINER],
\r
4648 startOffset = this[START_OFFSET], nn, o;
\r
4650 // Node is TEXT_NODE or CDATA
\r
4651 if ((startContainer.nodeType === 3 || startContainer.nodeType === 4) && startContainer.nodeValue) {
\r
4652 if (!startOffset) {
\r
4653 // At the start of text
\r
4654 startContainer.parentNode.insertBefore(n, startContainer);
\r
4655 } else if (startOffset >= startContainer.nodeValue.length) {
\r
4656 // At the end of text
\r
4657 dom.insertAfter(n, startContainer);
\r
4659 // Middle, need to split
\r
4660 nn = startContainer.splitText(startOffset);
\r
4661 startContainer.parentNode.insertBefore(n, nn);
\r
4664 // Insert element node
\r
4665 if (startContainer.childNodes.length > 0)
\r
4666 o = startContainer.childNodes[startOffset];
\r
4669 startContainer.insertBefore(n, o);
\r
4671 startContainer.appendChild(n);
\r
4675 function surroundContents(n) {
\r
4676 var f = t.extractContents();
\r
4683 function cloneRange() {
\r
4684 return extend(new Range(dom), {
\r
4685 startContainer : t[START_CONTAINER],
\r
4686 startOffset : t[START_OFFSET],
\r
4687 endContainer : t[END_CONTAINER],
\r
4688 endOffset : t[END_OFFSET],
\r
4689 collapsed : t.collapsed,
\r
4690 commonAncestorContainer : t.commonAncestorContainer
\r
4694 // Private methods
\r
4696 function _getSelectedNode(container, offset) {
\r
4699 if (container.nodeType == 3 /* TEXT_NODE */)
\r
4705 child = container.firstChild;
\r
4706 while (child && offset > 0) {
\r
4708 child = child.nextSibling;
\r
4717 function _isCollapsed() {
\r
4718 return (t[START_CONTAINER] == t[END_CONTAINER] && t[START_OFFSET] == t[END_OFFSET]);
\r
4721 function _compareBoundaryPoints(containerA, offsetA, containerB, offsetB) {
\r
4722 var c, offsetC, n, cmnRoot, childA, childB;
\r
4724 // In the first case the boundary-points have the same container. A is before B
\r
4725 // if its offset is less than the offset of B, A is equal to B if its offset is
\r
4726 // equal to the offset of B, and A is after B if its offset is greater than the
\r
4728 if (containerA == containerB) {
\r
4729 if (offsetA == offsetB)
\r
4730 return 0; // equal
\r
4732 if (offsetA < offsetB)
\r
4733 return -1; // before
\r
4735 return 1; // after
\r
4738 // In the second case a child node C of the container of A is an ancestor
\r
4739 // container of B. In this case, A is before B if the offset of A is less than or
\r
4740 // equal to the index of the child node C and A is after B otherwise.
\r
4742 while (c && c.parentNode != containerA)
\r
4747 n = containerA.firstChild;
\r
4749 while (n != c && offsetC < offsetA) {
\r
4751 n = n.nextSibling;
\r
4754 if (offsetA <= offsetC)
\r
4755 return -1; // before
\r
4757 return 1; // after
\r
4760 // In the third case a child node C of the container of B is an ancestor container
\r
4761 // of A. In this case, A is before B if the index of the child node C is less than
\r
4762 // the offset of B and A is after B otherwise.
\r
4764 while (c && c.parentNode != containerB) {
\r
4770 n = containerB.firstChild;
\r
4772 while (n != c && offsetC < offsetB) {
\r
4774 n = n.nextSibling;
\r
4777 if (offsetC < offsetB)
\r
4778 return -1; // before
\r
4780 return 1; // after
\r
4783 // In the fourth case, none of three other cases hold: the containers of A and B
\r
4784 // are siblings or descendants of sibling nodes. In this case, A is before B if
\r
4785 // the container of A is before the container of B in a pre-order traversal of the
\r
4786 // Ranges' context tree and A is after B otherwise.
\r
4787 cmnRoot = dom.findCommonAncestor(containerA, containerB);
\r
4788 childA = containerA;
\r
4790 while (childA && childA.parentNode != cmnRoot)
\r
4791 childA = childA.parentNode;
\r
4796 childB = containerB;
\r
4797 while (childB && childB.parentNode != cmnRoot)
\r
4798 childB = childB.parentNode;
\r
4803 if (childA == childB)
\r
4804 return 0; // equal
\r
4806 n = cmnRoot.firstChild;
\r
4809 return -1; // before
\r
4812 return 1; // after
\r
4814 n = n.nextSibling;
\r
4818 function _setEndPoint(st, n, o) {
\r
4822 t[START_CONTAINER] = n;
\r
4823 t[START_OFFSET] = o;
\r
4825 t[END_CONTAINER] = n;
\r
4826 t[END_OFFSET] = o;
\r
4829 // If one boundary-point of a Range is set to have a root container
\r
4830 // other than the current one for the Range, the Range is collapsed to
\r
4831 // the new position. This enforces the restriction that both boundary-
\r
4832 // points of a Range must have the same root container.
\r
4833 ec = t[END_CONTAINER];
\r
4834 while (ec.parentNode)
\r
4835 ec = ec.parentNode;
\r
4837 sc = t[START_CONTAINER];
\r
4838 while (sc.parentNode)
\r
4839 sc = sc.parentNode;
\r
4842 // The start position of a Range is guaranteed to never be after the
\r
4843 // end position. To enforce this restriction, if the start is set to
\r
4844 // be at a position after the end, the Range is collapsed to that
\r
4846 if (_compareBoundaryPoints(t[START_CONTAINER], t[START_OFFSET], t[END_CONTAINER], t[END_OFFSET]) > 0)
\r
4851 t.collapsed = _isCollapsed();
\r
4852 t.commonAncestorContainer = dom.findCommonAncestor(t[START_CONTAINER], t[END_CONTAINER]);
\r
4855 function _traverse(how) {
\r
4856 var c, endContainerDepth = 0, startContainerDepth = 0, p, depthDiff, startNode, endNode, sp, ep;
\r
4858 if (t[START_CONTAINER] == t[END_CONTAINER])
\r
4859 return _traverseSameContainer(how);
\r
4861 for (c = t[END_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) {
\r
4862 if (p == t[START_CONTAINER])
\r
4863 return _traverseCommonStartContainer(c, how);
\r
4865 ++endContainerDepth;
\r
4868 for (c = t[START_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) {
\r
4869 if (p == t[END_CONTAINER])
\r
4870 return _traverseCommonEndContainer(c, how);
\r
4872 ++startContainerDepth;
\r
4875 depthDiff = startContainerDepth - endContainerDepth;
\r
4877 startNode = t[START_CONTAINER];
\r
4878 while (depthDiff > 0) {
\r
4879 startNode = startNode.parentNode;
\r
4883 endNode = t[END_CONTAINER];
\r
4884 while (depthDiff < 0) {
\r
4885 endNode = endNode.parentNode;
\r
4889 // ascend the ancestor hierarchy until we have a common parent.
\r
4890 for (sp = startNode.parentNode, ep = endNode.parentNode; sp != ep; sp = sp.parentNode, ep = ep.parentNode) {
\r
4895 return _traverseCommonAncestors(startNode, endNode, how);
\r
4898 function _traverseSameContainer(how) {
\r
4899 var frag, s, sub, n, cnt, sibling, xferNode;
\r
4901 if (how != DELETE)
\r
4902 frag = doc.createDocumentFragment();
\r
4904 // If selection is empty, just return the fragment
\r
4905 if (t[START_OFFSET] == t[END_OFFSET])
\r
4908 // Text node needs special case handling
\r
4909 if (t[START_CONTAINER].nodeType == 3 /* TEXT_NODE */) {
\r
4910 // get the substring
\r
4911 s = t[START_CONTAINER].nodeValue;
\r
4912 sub = s.substring(t[START_OFFSET], t[END_OFFSET]);
\r
4914 // set the original text node to its new value
\r
4915 if (how != CLONE) {
\r
4916 t[START_CONTAINER].deleteData(t[START_OFFSET], t[END_OFFSET] - t[START_OFFSET]);
\r
4918 // Nothing is partially selected, so collapse to start point
\r
4922 if (how == DELETE)
\r
4925 frag.appendChild(doc.createTextNode(sub));
\r
4929 // Copy nodes between the start/end offsets.
\r
4930 n = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]);
\r
4931 cnt = t[END_OFFSET] - t[START_OFFSET];
\r
4934 sibling = n.nextSibling;
\r
4935 xferNode = _traverseFullySelected(n, how);
\r
4938 frag.appendChild( xferNode );
\r
4944 // Nothing is partially selected, so collapse to start point
\r
4951 function _traverseCommonStartContainer(endAncestor, how) {
\r
4952 var frag, n, endIdx, cnt, sibling, xferNode;
\r
4954 if (how != DELETE)
\r
4955 frag = doc.createDocumentFragment();
\r
4957 n = _traverseRightBoundary(endAncestor, how);
\r
4960 frag.appendChild(n);
\r
4962 endIdx = nodeIndex(endAncestor);
\r
4963 cnt = endIdx - t[START_OFFSET];
\r
4966 // Collapse to just before the endAncestor, which
\r
4967 // is partially selected.
\r
4968 if (how != CLONE) {
\r
4969 t.setEndBefore(endAncestor);
\r
4970 t.collapse(FALSE);
\r
4976 n = endAncestor.previousSibling;
\r
4978 sibling = n.previousSibling;
\r
4979 xferNode = _traverseFullySelected(n, how);
\r
4982 frag.insertBefore(xferNode, frag.firstChild);
\r
4988 // Collapse to just before the endAncestor, which
\r
4989 // is partially selected.
\r
4990 if (how != CLONE) {
\r
4991 t.setEndBefore(endAncestor);
\r
4992 t.collapse(FALSE);
\r
4998 function _traverseCommonEndContainer(startAncestor, how) {
\r
4999 var frag, startIdx, n, cnt, sibling, xferNode;
\r
5001 if (how != DELETE)
\r
5002 frag = doc.createDocumentFragment();
\r
5004 n = _traverseLeftBoundary(startAncestor, how);
\r
5006 frag.appendChild(n);
\r
5008 startIdx = nodeIndex(startAncestor);
\r
5009 ++startIdx; // Because we already traversed it
\r
5011 cnt = t[END_OFFSET] - startIdx;
\r
5012 n = startAncestor.nextSibling;
\r
5014 sibling = n.nextSibling;
\r
5015 xferNode = _traverseFullySelected(n, how);
\r
5018 frag.appendChild(xferNode);
\r
5024 if (how != CLONE) {
\r
5025 t.setStartAfter(startAncestor);
\r
5032 function _traverseCommonAncestors(startAncestor, endAncestor, how) {
\r
5033 var n, frag, commonParent, startOffset, endOffset, cnt, sibling, nextSibling;
\r
5035 if (how != DELETE)
\r
5036 frag = doc.createDocumentFragment();
\r
5038 n = _traverseLeftBoundary(startAncestor, how);
\r
5040 frag.appendChild(n);
\r
5042 commonParent = startAncestor.parentNode;
\r
5043 startOffset = nodeIndex(startAncestor);
\r
5044 endOffset = nodeIndex(endAncestor);
\r
5047 cnt = endOffset - startOffset;
\r
5048 sibling = startAncestor.nextSibling;
\r
5051 nextSibling = sibling.nextSibling;
\r
5052 n = _traverseFullySelected(sibling, how);
\r
5055 frag.appendChild(n);
\r
5057 sibling = nextSibling;
\r
5061 n = _traverseRightBoundary(endAncestor, how);
\r
5064 frag.appendChild(n);
\r
5066 if (how != CLONE) {
\r
5067 t.setStartAfter(startAncestor);
\r
5074 function _traverseRightBoundary(root, how) {
\r
5075 var next = _getSelectedNode(t[END_CONTAINER], t[END_OFFSET] - 1), parent, clonedParent, prevSibling, clonedChild, clonedGrandParent, isFullySelected = next != t[END_CONTAINER];
\r
5078 return _traverseNode(next, isFullySelected, FALSE, how);
\r
5080 parent = next.parentNode;
\r
5081 clonedParent = _traverseNode(parent, FALSE, FALSE, how);
\r
5085 prevSibling = next.previousSibling;
\r
5086 clonedChild = _traverseNode(next, isFullySelected, FALSE, how);
\r
5088 if (how != DELETE)
\r
5089 clonedParent.insertBefore(clonedChild, clonedParent.firstChild);
\r
5091 isFullySelected = TRUE;
\r
5092 next = prevSibling;
\r
5095 if (parent == root)
\r
5096 return clonedParent;
\r
5098 next = parent.previousSibling;
\r
5099 parent = parent.parentNode;
\r
5101 clonedGrandParent = _traverseNode(parent, FALSE, FALSE, how);
\r
5103 if (how != DELETE)
\r
5104 clonedGrandParent.appendChild(clonedParent);
\r
5106 clonedParent = clonedGrandParent;
\r
5110 function _traverseLeftBoundary(root, how) {
\r
5111 var next = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]), isFullySelected = next != t[START_CONTAINER], parent, clonedParent, nextSibling, clonedChild, clonedGrandParent;
\r
5114 return _traverseNode(next, isFullySelected, TRUE, how);
\r
5116 parent = next.parentNode;
\r
5117 clonedParent = _traverseNode(parent, FALSE, TRUE, how);
\r
5121 nextSibling = next.nextSibling;
\r
5122 clonedChild = _traverseNode(next, isFullySelected, TRUE, how);
\r
5124 if (how != DELETE)
\r
5125 clonedParent.appendChild(clonedChild);
\r
5127 isFullySelected = TRUE;
\r
5128 next = nextSibling;
\r
5131 if (parent == root)
\r
5132 return clonedParent;
\r
5134 next = parent.nextSibling;
\r
5135 parent = parent.parentNode;
\r
5137 clonedGrandParent = _traverseNode(parent, FALSE, TRUE, how);
\r
5139 if (how != DELETE)
\r
5140 clonedGrandParent.appendChild(clonedParent);
\r
5142 clonedParent = clonedGrandParent;
\r
5146 function _traverseNode(n, isFullySelected, isLeft, how) {
\r
5147 var txtValue, newNodeValue, oldNodeValue, offset, newNode;
\r
5149 if (isFullySelected)
\r
5150 return _traverseFullySelected(n, how);
\r
5152 if (n.nodeType == 3 /* TEXT_NODE */) {
\r
5153 txtValue = n.nodeValue;
\r
5156 offset = t[START_OFFSET];
\r
5157 newNodeValue = txtValue.substring(offset);
\r
5158 oldNodeValue = txtValue.substring(0, offset);
\r
5160 offset = t[END_OFFSET];
\r
5161 newNodeValue = txtValue.substring(0, offset);
\r
5162 oldNodeValue = txtValue.substring(offset);
\r
5166 n.nodeValue = oldNodeValue;
\r
5168 if (how == DELETE)
\r
5171 newNode = n.cloneNode(FALSE);
\r
5172 newNode.nodeValue = newNodeValue;
\r
5177 if (how == DELETE)
\r
5180 return n.cloneNode(FALSE);
\r
5183 function _traverseFullySelected(n, how) {
\r
5184 if (how != DELETE)
\r
5185 return how == CLONE ? n.cloneNode(TRUE) : n;
\r
5187 n.parentNode.removeChild(n);
\r
5195 function Selection(selection) {
\r
5196 var t = this, invisibleChar = '\uFEFF', range, lastIERng, dom = selection.dom, TRUE = true, FALSE = false;
\r
5198 // Returns a W3C DOM compatible range object by using the IE Range API
\r
5199 function getRange() {
\r
5200 var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed;
\r
5202 // If selection is outside the current document just return an empty range
\r
5203 element = ieRange.item ? ieRange.item(0) : ieRange.parentElement();
\r
5204 if (element.ownerDocument != dom.doc)
\r
5207 collapsed = selection.isCollapsed();
\r
5209 // Handle control selection or text selection of a image
\r
5210 if (ieRange.item || !element.hasChildNodes()) {
\r
5212 domRange.setStart(element, 0);
\r
5213 domRange.setEnd(element, 0);
\r
5215 domRange.setStart(element.parentNode, dom.nodeIndex(element));
\r
5216 domRange.setEnd(domRange.startContainer, domRange.startOffset + 1);
\r
5222 function findEndPoint(start) {
\r
5223 var marker, container, offset, nodes, startIndex = 0, endIndex, index, parent, checkRng, position;
\r
5225 // Setup temp range and collapse it
\r
5226 checkRng = ieRange.duplicate();
\r
5227 checkRng.collapse(start);
\r
5229 // Create marker and insert it at the end of the endpoints parent
\r
5230 marker = dom.create('a');
\r
5231 parent = checkRng.parentElement();
\r
5233 // If parent doesn't have any children then set the container to that parent and the index to 0
\r
5234 if (!parent.hasChildNodes()) {
\r
5235 domRange[start ? 'setStart' : 'setEnd'](parent, 0);
\r
5239 parent.appendChild(marker);
\r
5240 checkRng.moveToElementText(marker);
\r
5241 position = ieRange.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', checkRng);
\r
5242 if (position > 0) {
\r
5243 // The position is after the end of the parent element.
\r
5244 // This is the case where IE puts the caret to the left edge of a table.
\r
5245 domRange[start ? 'setStartAfter' : 'setEndAfter'](parent);
\r
5246 dom.remove(marker);
\r
5250 // Setup node list and endIndex
\r
5251 nodes = tinymce.grep(parent.childNodes);
\r
5252 endIndex = nodes.length - 1;
\r
5253 // Perform a binary search for the position
\r
5254 while (startIndex <= endIndex) {
\r
5255 index = Math.floor((startIndex + endIndex) / 2);
\r
5257 // Insert marker and check it's position relative to the selection
\r
5258 parent.insertBefore(marker, nodes[index]);
\r
5259 checkRng.moveToElementText(marker);
\r
5260 position = ieRange.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', checkRng);
\r
5261 if (position > 0) {
\r
5262 // Marker is to the right
\r
5263 startIndex = index + 1;
\r
5264 } else if (position < 0) {
\r
5265 // Marker is to the left
\r
5266 endIndex = index - 1;
\r
5268 // Maker is where we are
\r
5274 // Setup container
\r
5275 container = position > 0 || index == 0 ? marker.nextSibling : marker.previousSibling;
\r
5277 // Handle element selection
\r
5278 if (container.nodeType == 1) {
\r
5279 dom.remove(marker);
\r
5281 // Find offset and container
\r
5282 offset = dom.nodeIndex(container);
\r
5283 container = container.parentNode;
\r
5285 // Move the offset if we are setting the end or the position is after an element
\r
5286 if (!start || index > 0)
\r
5289 // Calculate offset within text node
\r
5290 if (position > 0 || index == 0) {
\r
5291 checkRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', ieRange);
\r
5292 offset = checkRng.text.length;
\r
5294 checkRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', ieRange);
\r
5295 offset = container.nodeValue.length - checkRng.text.length;
\r
5298 dom.remove(marker);
\r
5301 domRange[start ? 'setStart' : 'setEnd'](container, offset);
\r
5304 // Find start point
\r
5305 findEndPoint(true);
\r
5307 // Find end point if needed
\r
5314 this.addRange = function(rng) {
\r
5315 var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, doc = selection.dom.doc, body = doc.body;
\r
5317 function setEndPoint(start) {
\r
5318 var container, offset, marker, tmpRng, nodes;
\r
5320 marker = dom.create('a');
\r
5321 container = start ? startContainer : endContainer;
\r
5322 offset = start ? startOffset : endOffset;
\r
5323 tmpRng = ieRng.duplicate();
\r
5325 if (container == doc || container == doc.documentElement) {
\r
5330 if (container.nodeType == 3) {
\r
5331 container.parentNode.insertBefore(marker, container);
\r
5332 tmpRng.moveToElementText(marker);
\r
5333 tmpRng.moveStart('character', offset);
\r
5334 dom.remove(marker);
\r
5335 ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
\r
5337 nodes = container.childNodes;
\r
5339 if (nodes.length) {
\r
5340 if (offset >= nodes.length) {
\r
5341 dom.insertAfter(marker, nodes[nodes.length - 1]);
\r
5343 container.insertBefore(marker, nodes[offset]);
\r
5346 tmpRng.moveToElementText(marker);
\r
5348 // Empty node selection for example <div>|</div>
\r
5349 marker = doc.createTextNode(invisibleChar);
\r
5350 container.appendChild(marker);
\r
5351 tmpRng.moveToElementText(marker.parentNode);
\r
5352 tmpRng.collapse(TRUE);
\r
5355 ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
\r
5356 dom.remove(marker);
\r
5360 // Destroy cached range
\r
5363 // Setup some shorter versions
\r
5364 startContainer = rng.startContainer;
\r
5365 startOffset = rng.startOffset;
\r
5366 endContainer = rng.endContainer;
\r
5367 endOffset = rng.endOffset;
\r
5368 ieRng = body.createTextRange();
\r
5370 // If single element selection then try making a control selection out of it
\r
5371 if (startContainer == endContainer && startContainer.nodeType == 1 && startOffset == endOffset - 1) {
\r
5372 if (startOffset == endOffset - 1) {
\r
5374 ctrlRng = body.createControlRange();
\r
5375 ctrlRng.addElement(startContainer.childNodes[startOffset]);
\r
5384 // Set start/end point of selection
\r
5385 setEndPoint(true);
\r
5388 // Select the new range and scroll it into view
\r
5392 this.getRangeAt = function() {
\r
5393 // Setup new range if the cache is empty
\r
5394 if (!range || !tinymce.dom.RangeUtils.compareRanges(lastIERng, selection.getRng())) {
\r
5395 range = getRange();
\r
5397 // Store away text range for next call
\r
5398 lastIERng = selection.getRng();
\r
5401 // IE will say that the range is equal then produce an invalid argument exception
\r
5402 // if you perform specific operations in a keyup event. For example Ctrl+Del.
\r
5403 // This hack will invalidate the range cache if the exception occurs
\r
5405 range.startContainer.nextSibling;
\r
5407 range = getRange();
\r
5411 // Return cached range
\r
5415 this.destroy = function() {
\r
5416 // Destroy cached range and last IE range to avoid memory leaks
\r
5417 lastIERng = range = null;
\r
5421 // Expose the selection object
\r
5422 tinymce.dom.TridentSelection = Selection;
\r
5427 * Sizzle CSS Selector Engine - v1.0
\r
5428 * Copyright 2009, The Dojo Foundation
\r
5429 * Released under the MIT, BSD, and GPL Licenses.
\r
5430 * More information: http://sizzlejs.com/
\r
5434 var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
\r
5436 toString = Object.prototype.toString,
\r
5437 hasDuplicate = false,
\r
5438 baseHasDuplicate = true;
\r
5440 // Here we check if the JavaScript engine is using some sort of
\r
5441 // optimization where it does not always call our comparision
\r
5442 // function. If that is the case, discard the hasDuplicate value.
\r
5443 // Thus far that includes Google Chrome.
\r
5444 [0, 0].sort(function(){
\r
5445 baseHasDuplicate = false;
\r
5449 var Sizzle = function(selector, context, results, seed) {
\r
5450 results = results || [];
\r
5451 context = context || document;
\r
5453 var origContext = context;
\r
5455 if ( context.nodeType !== 1 && context.nodeType !== 9 ) {
\r
5459 if ( !selector || typeof selector !== "string" ) {
\r
5463 var parts = [], m, set, checkSet, extra, prune = true, contextXML = Sizzle.isXML(context),
\r
5464 soFar = selector, ret, cur, pop, i;
\r
5466 // Reset the position of the chunker regexp (start from head)
\r
5469 m = chunker.exec(soFar);
\r
5474 parts.push( m[1] );
\r
5483 if ( parts.length > 1 && origPOS.exec( selector ) ) {
\r
5484 if ( parts.length === 2 && Expr.relative[ parts[0] ] ) {
\r
5485 set = posProcess( parts[0] + parts[1], context );
\r
5487 set = Expr.relative[ parts[0] ] ?
\r
5489 Sizzle( parts.shift(), context );
\r
5491 while ( parts.length ) {
\r
5492 selector = parts.shift();
\r
5494 if ( Expr.relative[ selector ] ) {
\r
5495 selector += parts.shift();
\r
5498 set = posProcess( selector, set );
\r
5502 // Take a shortcut and set the context if the root selector is an ID
\r
5503 // (but not if it'll be faster if the inner selector is an ID)
\r
5504 if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML &&
\r
5505 Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) {
\r
5506 ret = Sizzle.find( parts.shift(), context, contextXML );
\r
5507 context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0];
\r
5512 { expr: parts.pop(), set: makeArray(seed) } :
\r
5513 Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML );
\r
5514 set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set;
\r
5516 if ( parts.length > 0 ) {
\r
5517 checkSet = makeArray(set);
\r
5522 while ( parts.length ) {
\r
5523 cur = parts.pop();
\r
5526 if ( !Expr.relative[ cur ] ) {
\r
5529 pop = parts.pop();
\r
5532 if ( pop == null ) {
\r
5536 Expr.relative[ cur ]( checkSet, pop, contextXML );
\r
5539 checkSet = parts = [];
\r
5543 if ( !checkSet ) {
\r
5547 if ( !checkSet ) {
\r
5548 Sizzle.error( cur || selector );
\r
5551 if ( toString.call(checkSet) === "[object Array]" ) {
\r
5553 results.push.apply( results, checkSet );
\r
5554 } else if ( context && context.nodeType === 1 ) {
\r
5555 for ( i = 0; checkSet[i] != null; i++ ) {
\r
5556 if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) {
\r
5557 results.push( set[i] );
\r
5561 for ( i = 0; checkSet[i] != null; i++ ) {
\r
5562 if ( checkSet[i] && checkSet[i].nodeType === 1 ) {
\r
5563 results.push( set[i] );
\r
5568 makeArray( checkSet, results );
\r
5572 Sizzle( extra, origContext, results, seed );
\r
5573 Sizzle.uniqueSort( results );
\r
5579 Sizzle.uniqueSort = function(results){
\r
5580 if ( sortOrder ) {
\r
5581 hasDuplicate = baseHasDuplicate;
\r
5582 results.sort(sortOrder);
\r
5584 if ( hasDuplicate ) {
\r
5585 for ( var i = 1; i < results.length; i++ ) {
\r
5586 if ( results[i] === results[i-1] ) {
\r
5587 results.splice(i--, 1);
\r
5596 Sizzle.matches = function(expr, set){
\r
5597 return Sizzle(expr, null, null, set);
\r
5600 Sizzle.find = function(expr, context, isXML){
\r
5607 for ( var i = 0, l = Expr.order.length; i < l; i++ ) {
\r
5608 var type = Expr.order[i], match;
\r
5610 if ( (match = Expr.leftMatch[ type ].exec( expr )) ) {
\r
5611 var left = match[1];
\r
5612 match.splice(1,1);
\r
5614 if ( left.substr( left.length - 1 ) !== "\\" ) {
\r
5615 match[1] = (match[1] || "").replace(/\\/g, "");
\r
5616 set = Expr.find[ type ]( match, context, isXML );
\r
5617 if ( set != null ) {
\r
5618 expr = expr.replace( Expr.match[ type ], "" );
\r
5626 set = context.getElementsByTagName("*");
\r
5629 return {set: set, expr: expr};
\r
5632 Sizzle.filter = function(expr, set, inplace, not){
\r
5633 var old = expr, result = [], curLoop = set, match, anyFound,
\r
5634 isXMLFilter = set && set[0] && Sizzle.isXML(set[0]);
\r
5636 while ( expr && set.length ) {
\r
5637 for ( var type in Expr.filter ) {
\r
5638 if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) {
\r
5639 var filter = Expr.filter[ type ], found, item, left = match[1];
\r
5642 match.splice(1,1);
\r
5644 if ( left.substr( left.length - 1 ) === "\\" ) {
\r
5648 if ( curLoop === result ) {
\r
5652 if ( Expr.preFilter[ type ] ) {
\r
5653 match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter );
\r
5656 anyFound = found = true;
\r
5657 } else if ( match === true ) {
\r
5663 for ( var i = 0; (item = curLoop[i]) != null; i++ ) {
\r
5665 found = filter( item, match, i, curLoop );
\r
5666 var pass = not ^ !!found;
\r
5668 if ( inplace && found != null ) {
\r
5672 curLoop[i] = false;
\r
5674 } else if ( pass ) {
\r
5675 result.push( item );
\r
5682 if ( found !== undefined ) {
\r
5687 expr = expr.replace( Expr.match[ type ], "" );
\r
5689 if ( !anyFound ) {
\r
5698 // Improper expression
\r
5699 if ( expr === old ) {
\r
5700 if ( anyFound == null ) {
\r
5701 Sizzle.error( expr );
\r
5713 Sizzle.error = function( msg ) {
\r
5714 throw "Syntax error, unrecognized expression: " + msg;
\r
5717 var Expr = Sizzle.selectors = {
\r
5718 order: [ "ID", "NAME", "TAG" ],
\r
5720 ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,
\r
5721 CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,
\r
5722 NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,
\r
5723 ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,
\r
5724 TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,
\r
5725 CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+\-]*)\))?/,
\r
5726 POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,
\r
5727 PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/
\r
5731 "class": "className",
\r
5735 href: function(elem){
\r
5736 return elem.getAttribute("href");
\r
5740 "+": function(checkSet, part){
\r
5741 var isPartStr = typeof part === "string",
\r
5742 isTag = isPartStr && !/\W/.test(part),
\r
5743 isPartStrNotTag = isPartStr && !isTag;
\r
5746 part = part.toLowerCase();
\r
5749 for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) {
\r
5750 if ( (elem = checkSet[i]) ) {
\r
5751 while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {}
\r
5753 checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ?
\r
5759 if ( isPartStrNotTag ) {
\r
5760 Sizzle.filter( part, checkSet, true );
\r
5763 ">": function(checkSet, part){
\r
5764 var isPartStr = typeof part === "string",
\r
5765 elem, i = 0, l = checkSet.length;
\r
5767 if ( isPartStr && !/\W/.test(part) ) {
\r
5768 part = part.toLowerCase();
\r
5770 for ( ; i < l; i++ ) {
\r
5771 elem = checkSet[i];
\r
5773 var parent = elem.parentNode;
\r
5774 checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false;
\r
5778 for ( ; i < l; i++ ) {
\r
5779 elem = checkSet[i];
\r
5781 checkSet[i] = isPartStr ?
\r
5783 elem.parentNode === part;
\r
5787 if ( isPartStr ) {
\r
5788 Sizzle.filter( part, checkSet, true );
\r
5792 "": function(checkSet, part, isXML){
\r
5793 var doneName = done++, checkFn = dirCheck, nodeCheck;
\r
5795 if ( typeof part === "string" && !/\W/.test(part) ) {
\r
5796 part = part.toLowerCase();
\r
5798 checkFn = dirNodeCheck;
\r
5801 checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML);
\r
5803 "~": function(checkSet, part, isXML){
\r
5804 var doneName = done++, checkFn = dirCheck, nodeCheck;
\r
5806 if ( typeof part === "string" && !/\W/.test(part) ) {
\r
5807 part = part.toLowerCase();
\r
5809 checkFn = dirNodeCheck;
\r
5812 checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML);
\r
5816 ID: function(match, context, isXML){
\r
5817 if ( typeof context.getElementById !== "undefined" && !isXML ) {
\r
5818 var m = context.getElementById(match[1]);
\r
5819 return m ? [m] : [];
\r
5822 NAME: function(match, context){
\r
5823 if ( typeof context.getElementsByName !== "undefined" ) {
\r
5824 var ret = [], results = context.getElementsByName(match[1]);
\r
5826 for ( var i = 0, l = results.length; i < l; i++ ) {
\r
5827 if ( results[i].getAttribute("name") === match[1] ) {
\r
5828 ret.push( results[i] );
\r
5832 return ret.length === 0 ? null : ret;
\r
5835 TAG: function(match, context){
\r
5836 return context.getElementsByTagName(match[1]);
\r
5840 CLASS: function(match, curLoop, inplace, result, not, isXML){
\r
5841 match = " " + match[1].replace(/\\/g, "") + " ";
\r
5847 for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) {
\r
5849 if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n]/g, " ").indexOf(match) >= 0) ) {
\r
5851 result.push( elem );
\r
5853 } else if ( inplace ) {
\r
5854 curLoop[i] = false;
\r
5861 ID: function(match){
\r
5862 return match[1].replace(/\\/g, "");
\r
5864 TAG: function(match, curLoop){
\r
5865 return match[1].toLowerCase();
\r
5867 CHILD: function(match){
\r
5868 if ( match[1] === "nth" ) {
\r
5869 // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6'
\r
5870 var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec(
\r
5871 match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" ||
\r
5872 !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]);
\r
5874 // calculate the numbers (first)n+(last) including if they are negative
\r
5875 match[2] = (test[1] + (test[2] || 1)) - 0;
\r
5876 match[3] = test[3] - 0;
\r
5879 // TODO: Move to normal caching system
\r
5880 match[0] = done++;
\r
5884 ATTR: function(match, curLoop, inplace, result, not, isXML){
\r
5885 var name = match[1].replace(/\\/g, "");
\r
5887 if ( !isXML && Expr.attrMap[name] ) {
\r
5888 match[1] = Expr.attrMap[name];
\r
5891 if ( match[2] === "~=" ) {
\r
5892 match[4] = " " + match[4] + " ";
\r
5897 PSEUDO: function(match, curLoop, inplace, result, not){
\r
5898 if ( match[1] === "not" ) {
\r
5899 // If we're dealing with a complex expression, or a simple one
\r
5900 if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) {
\r
5901 match[3] = Sizzle(match[3], null, null, curLoop);
\r
5903 var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not);
\r
5905 result.push.apply( result, ret );
\r
5909 } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) {
\r
5915 POS: function(match){
\r
5916 match.unshift( true );
\r
5921 enabled: function(elem){
\r
5922 return elem.disabled === false && elem.type !== "hidden";
\r
5924 disabled: function(elem){
\r
5925 return elem.disabled === true;
\r
5927 checked: function(elem){
\r
5928 return elem.checked === true;
\r
5930 selected: function(elem){
\r
5931 // Accessing this property makes selected-by-default
\r
5932 // options in Safari work properly
\r
5933 elem.parentNode.selectedIndex;
\r
5934 return elem.selected === true;
\r
5936 parent: function(elem){
\r
5937 return !!elem.firstChild;
\r
5939 empty: function(elem){
\r
5940 return !elem.firstChild;
\r
5942 has: function(elem, i, match){
\r
5943 return !!Sizzle( match[3], elem ).length;
\r
5945 header: function(elem){
\r
5946 return (/h\d/i).test( elem.nodeName );
\r
5948 text: function(elem){
\r
5949 return "text" === elem.type;
\r
5951 radio: function(elem){
\r
5952 return "radio" === elem.type;
\r
5954 checkbox: function(elem){
\r
5955 return "checkbox" === elem.type;
\r
5957 file: function(elem){
\r
5958 return "file" === elem.type;
\r
5960 password: function(elem){
\r
5961 return "password" === elem.type;
\r
5963 submit: function(elem){
\r
5964 return "submit" === elem.type;
\r
5966 image: function(elem){
\r
5967 return "image" === elem.type;
\r
5969 reset: function(elem){
\r
5970 return "reset" === elem.type;
\r
5972 button: function(elem){
\r
5973 return "button" === elem.type || elem.nodeName.toLowerCase() === "button";
\r
5975 input: function(elem){
\r
5976 return (/input|select|textarea|button/i).test(elem.nodeName);
\r
5980 first: function(elem, i){
\r
5983 last: function(elem, i, match, array){
\r
5984 return i === array.length - 1;
\r
5986 even: function(elem, i){
\r
5987 return i % 2 === 0;
\r
5989 odd: function(elem, i){
\r
5990 return i % 2 === 1;
\r
5992 lt: function(elem, i, match){
\r
5993 return i < match[3] - 0;
\r
5995 gt: function(elem, i, match){
\r
5996 return i > match[3] - 0;
\r
5998 nth: function(elem, i, match){
\r
5999 return match[3] - 0 === i;
\r
6001 eq: function(elem, i, match){
\r
6002 return match[3] - 0 === i;
\r
6006 PSEUDO: function(elem, match, i, array){
\r
6007 var name = match[1], filter = Expr.filters[ name ];
\r
6010 return filter( elem, i, match, array );
\r
6011 } else if ( name === "contains" ) {
\r
6012 return (elem.textContent || elem.innerText || Sizzle.getText([ elem ]) || "").indexOf(match[3]) >= 0;
\r
6013 } else if ( name === "not" ) {
\r
6014 var not = match[3];
\r
6016 for ( var j = 0, l = not.length; j < l; j++ ) {
\r
6017 if ( not[j] === elem ) {
\r
6024 Sizzle.error( "Syntax error, unrecognized expression: " + name );
\r
6027 CHILD: function(elem, match){
\r
6028 var type = match[1], node = elem;
\r
6032 while ( (node = node.previousSibling) ) {
\r
6033 if ( node.nodeType === 1 ) {
\r
6037 if ( type === "first" ) {
\r
6042 while ( (node = node.nextSibling) ) {
\r
6043 if ( node.nodeType === 1 ) {
\r
6049 var first = match[2], last = match[3];
\r
6051 if ( first === 1 && last === 0 ) {
\r
6055 var doneName = match[0],
\r
6056 parent = elem.parentNode;
\r
6058 if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) {
\r
6060 for ( node = parent.firstChild; node; node = node.nextSibling ) {
\r
6061 if ( node.nodeType === 1 ) {
\r
6062 node.nodeIndex = ++count;
\r
6065 parent.sizcache = doneName;
\r
6068 var diff = elem.nodeIndex - last;
\r
6069 if ( first === 0 ) {
\r
6070 return diff === 0;
\r
6072 return ( diff % first === 0 && diff / first >= 0 );
\r
6076 ID: function(elem, match){
\r
6077 return elem.nodeType === 1 && elem.getAttribute("id") === match;
\r
6079 TAG: function(elem, match){
\r
6080 return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match;
\r
6082 CLASS: function(elem, match){
\r
6083 return (" " + (elem.className || elem.getAttribute("class")) + " ")
\r
6084 .indexOf( match ) > -1;
\r
6086 ATTR: function(elem, match){
\r
6087 var name = match[1],
\r
6088 result = Expr.attrHandle[ name ] ?
\r
6089 Expr.attrHandle[ name ]( elem ) :
\r
6090 elem[ name ] != null ?
\r
6092 elem.getAttribute( name ),
\r
6093 value = result + "",
\r
6097 return result == null ?
\r
6102 value.indexOf(check) >= 0 :
\r
6104 (" " + value + " ").indexOf(check) >= 0 :
\r
6106 value && result !== false :
\r
6110 value.indexOf(check) === 0 :
\r
6112 value.substr(value.length - check.length) === check :
\r
6114 value === check || value.substr(0, check.length + 1) === check + "-" :
\r
6117 POS: function(elem, match, i, array){
\r
6118 var name = match[2], filter = Expr.setFilters[ name ];
\r
6121 return filter( elem, i, match, array );
\r
6127 var origPOS = Expr.match.POS,
\r
6128 fescape = function(all, num){
\r
6129 return "\\" + (num - 0 + 1);
\r
6132 for ( var type in Expr.match ) {
\r
6133 Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) );
\r
6134 Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) );
\r
6137 var makeArray = function(array, results) {
\r
6138 array = Array.prototype.slice.call( array, 0 );
\r
6141 results.push.apply( results, array );
\r
6148 // Perform a simple check to determine if the browser is capable of
\r
6149 // converting a NodeList to an array using builtin methods.
\r
6150 // Also verifies that the returned array holds DOM nodes
\r
6151 // (which is not the case in the Blackberry browser)
\r
6153 Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType;
\r
6155 // Provide a fallback method if it does not work
\r
6157 makeArray = function(array, results) {
\r
6158 var ret = results || [], i = 0;
\r
6160 if ( toString.call(array) === "[object Array]" ) {
\r
6161 Array.prototype.push.apply( ret, array );
\r
6163 if ( typeof array.length === "number" ) {
\r
6164 for ( var l = array.length; i < l; i++ ) {
\r
6165 ret.push( array[i] );
\r
6168 for ( ; array[i]; i++ ) {
\r
6169 ret.push( array[i] );
\r
6180 if ( document.documentElement.compareDocumentPosition ) {
\r
6181 sortOrder = function( a, b ) {
\r
6182 if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) {
\r
6184 hasDuplicate = true;
\r
6186 return a.compareDocumentPosition ? -1 : 1;
\r
6189 var ret = a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1;
\r
6190 if ( ret === 0 ) {
\r
6191 hasDuplicate = true;
\r
6195 } else if ( "sourceIndex" in document.documentElement ) {
\r
6196 sortOrder = function( a, b ) {
\r
6197 if ( !a.sourceIndex || !b.sourceIndex ) {
\r
6199 hasDuplicate = true;
\r
6201 return a.sourceIndex ? -1 : 1;
\r
6204 var ret = a.sourceIndex - b.sourceIndex;
\r
6205 if ( ret === 0 ) {
\r
6206 hasDuplicate = true;
\r
6210 } else if ( document.createRange ) {
\r
6211 sortOrder = function( a, b ) {
\r
6212 if ( !a.ownerDocument || !b.ownerDocument ) {
\r
6214 hasDuplicate = true;
\r
6216 return a.ownerDocument ? -1 : 1;
\r
6219 var aRange = a.ownerDocument.createRange(), bRange = b.ownerDocument.createRange();
\r
6220 aRange.setStart(a, 0);
\r
6221 aRange.setEnd(a, 0);
\r
6222 bRange.setStart(b, 0);
\r
6223 bRange.setEnd(b, 0);
\r
6224 var ret = aRange.compareBoundaryPoints(Range.START_TO_END, bRange);
\r
6225 if ( ret === 0 ) {
\r
6226 hasDuplicate = true;
\r
6232 // Utility function for retreiving the text value of an array of DOM nodes
\r
6233 Sizzle.getText = function( elems ) {
\r
6234 var ret = "", elem;
\r
6236 for ( var i = 0; elems[i]; i++ ) {
\r
6239 // Get the text from text nodes and CDATA nodes
\r
6240 if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
\r
6241 ret += elem.nodeValue;
\r
6243 // Traverse everything else, except comment nodes
\r
6244 } else if ( elem.nodeType !== 8 ) {
\r
6245 ret += Sizzle.getText( elem.childNodes );
\r
6252 // Check to see if the browser returns elements by name when
\r
6253 // querying by getElementById (and provide a workaround)
\r
6255 // We're going to inject a fake input element with a specified name
\r
6256 var form = document.createElement("div"),
\r
6257 id = "script" + (new Date()).getTime();
\r
6258 form.innerHTML = "<a name='" + id + "'/>";
\r
6260 // Inject it into the root element, check its status, and remove it quickly
\r
6261 var root = document.documentElement;
\r
6262 root.insertBefore( form, root.firstChild );
\r
6264 // The workaround has to do additional checks after a getElementById
\r
6265 // Which slows things down for other browsers (hence the branching)
\r
6266 if ( document.getElementById( id ) ) {
\r
6267 Expr.find.ID = function(match, context, isXML){
\r
6268 if ( typeof context.getElementById !== "undefined" && !isXML ) {
\r
6269 var m = context.getElementById(match[1]);
\r
6270 return m ? m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : [];
\r
6274 Expr.filter.ID = function(elem, match){
\r
6275 var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id");
\r
6276 return elem.nodeType === 1 && node && node.nodeValue === match;
\r
6280 root.removeChild( form );
\r
6281 root = form = null; // release memory in IE
\r
6285 // Check to see if the browser returns only elements
\r
6286 // when doing getElementsByTagName("*")
\r
6288 // Create a fake element
\r
6289 var div = document.createElement("div");
\r
6290 div.appendChild( document.createComment("") );
\r
6292 // Make sure no comments are found
\r
6293 if ( div.getElementsByTagName("*").length > 0 ) {
\r
6294 Expr.find.TAG = function(match, context){
\r
6295 var results = context.getElementsByTagName(match[1]);
\r
6297 // Filter out possible comments
\r
6298 if ( match[1] === "*" ) {
\r
6301 for ( var i = 0; results[i]; i++ ) {
\r
6302 if ( results[i].nodeType === 1 ) {
\r
6303 tmp.push( results[i] );
\r
6314 // Check to see if an attribute returns normalized href attributes
\r
6315 div.innerHTML = "<a href='#'></a>";
\r
6316 if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" &&
\r
6317 div.firstChild.getAttribute("href") !== "#" ) {
\r
6318 Expr.attrHandle.href = function(elem){
\r
6319 return elem.getAttribute("href", 2);
\r
6323 div = null; // release memory in IE
\r
6326 if ( document.querySelectorAll ) {
\r
6328 var oldSizzle = Sizzle, div = document.createElement("div");
\r
6329 div.innerHTML = "<p class='TEST'></p>";
\r
6331 // Safari can't handle uppercase or unicode characters when
\r
6332 // in quirks mode.
\r
6333 if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) {
\r
6337 Sizzle = function(query, context, extra, seed){
\r
6338 context = context || document;
\r
6340 // Only use querySelectorAll on non-XML documents
\r
6341 // (ID selectors don't work in non-HTML documents)
\r
6342 if ( !seed && context.nodeType === 9 && !Sizzle.isXML(context) ) {
\r
6344 return makeArray( context.querySelectorAll(query), extra );
\r
6348 return oldSizzle(query, context, extra, seed);
\r
6351 for ( var prop in oldSizzle ) {
\r
6352 Sizzle[ prop ] = oldSizzle[ prop ];
\r
6355 div = null; // release memory in IE
\r
6360 var div = document.createElement("div");
\r
6362 div.innerHTML = "<div class='test e'></div><div class='test'></div>";
\r
6364 // Opera can't find a second classname (in 9.6)
\r
6365 // Also, make sure that getElementsByClassName actually exists
\r
6366 if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) {
\r
6370 // Safari caches class attributes, doesn't catch changes (in 3.2)
\r
6371 div.lastChild.className = "e";
\r
6373 if ( div.getElementsByClassName("e").length === 1 ) {
\r
6377 Expr.order.splice(1, 0, "CLASS");
\r
6378 Expr.find.CLASS = function(match, context, isXML) {
\r
6379 if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) {
\r
6380 return context.getElementsByClassName(match[1]);
\r
6384 div = null; // release memory in IE
\r
6387 function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
\r
6388 for ( var i = 0, l = checkSet.length; i < l; i++ ) {
\r
6389 var elem = checkSet[i];
\r
6392 var match = false;
\r
6395 if ( elem.sizcache === doneName ) {
\r
6396 match = checkSet[elem.sizset];
\r
6400 if ( elem.nodeType === 1 && !isXML ){
\r
6401 elem.sizcache = doneName;
\r
6405 if ( elem.nodeName.toLowerCase() === cur ) {
\r
6413 checkSet[i] = match;
\r
6418 function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
\r
6419 for ( var i = 0, l = checkSet.length; i < l; i++ ) {
\r
6420 var elem = checkSet[i];
\r
6423 var match = false;
\r
6426 if ( elem.sizcache === doneName ) {
\r
6427 match = checkSet[elem.sizset];
\r
6431 if ( elem.nodeType === 1 ) {
\r
6433 elem.sizcache = doneName;
\r
6436 if ( typeof cur !== "string" ) {
\r
6437 if ( elem === cur ) {
\r
6442 } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) {
\r
6451 checkSet[i] = match;
\r
6456 Sizzle.contains = document.compareDocumentPosition ? function(a, b){
\r
6457 return !!(a.compareDocumentPosition(b) & 16);
\r
6458 } : function(a, b){
\r
6459 return a !== b && (a.contains ? a.contains(b) : true);
\r
6462 Sizzle.isXML = function(elem){
\r
6463 // documentElement is verified for cases where it doesn't yet exist
\r
6464 // (such as loading iframes in IE - #4833)
\r
6465 var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement;
\r
6466 return documentElement ? documentElement.nodeName !== "HTML" : false;
\r
6469 var posProcess = function(selector, context){
\r
6470 var tmpSet = [], later = "", match,
\r
6471 root = context.nodeType ? [context] : context;
\r
6473 // Position selectors must be done after the filter
\r
6474 // And so must :not(positional) so we move all PSEUDOs to the end
\r
6475 while ( (match = Expr.match.PSEUDO.exec( selector )) ) {
\r
6476 later += match[0];
\r
6477 selector = selector.replace( Expr.match.PSEUDO, "" );
\r
6480 selector = Expr.relative[selector] ? selector + "*" : selector;
\r
6482 for ( var i = 0, l = root.length; i < l; i++ ) {
\r
6483 Sizzle( selector, root[i], tmpSet );
\r
6486 return Sizzle.filter( later, tmpSet );
\r
6491 window.tinymce.dom.Sizzle = Sizzle;
\r
6496 (function(tinymce) {
\r
6498 var each = tinymce.each, DOM = tinymce.DOM, isIE = tinymce.isIE, isWebKit = tinymce.isWebKit, Event;
\r
6500 tinymce.create('tinymce.dom.EventUtils', {
\r
6501 EventUtils : function() {
\r
6506 add : function(o, n, f, s) {
\r
6507 var cb, t = this, el = t.events, r;
\r
6509 if (n instanceof Array) {
\r
6512 each(n, function(n) {
\r
6513 r.push(t.add(o, n, f, s));
\r
6520 if (o && o.hasOwnProperty && o instanceof Array) {
\r
6523 each(o, function(o) {
\r
6525 r.push(t.add(o, n, f, s));
\r
6536 // Setup event callback
\r
6537 cb = function(e) {
\r
6538 // Is all events disabled
\r
6542 e = e || window.event;
\r
6544 // Patch in target, preventDefault and stopPropagation in IE it's W3C valid
\r
6547 e.target = e.srcElement;
\r
6549 // Patch in preventDefault, stopPropagation methods for W3C compatibility
\r
6550 tinymce.extend(e, t._stoppers);
\r
6556 return f.call(s, e);
\r
6559 if (n == 'unload') {
\r
6560 tinymce.unloads.unshift({func : cb});
\r
6564 if (n == 'init') {
\r
6573 // Store away listener reference
\r
6587 remove : function(o, n, f) {
\r
6588 var t = this, a = t.events, s = false, r;
\r
6591 if (o && o.hasOwnProperty && o instanceof Array) {
\r
6594 each(o, function(o) {
\r
6596 r.push(t.remove(o, n, f));
\r
6604 each(a, function(e, i) {
\r
6605 if (e.obj == o && e.name == n && (!f || (e.func == f || e.cfunc == f))) {
\r
6607 t._remove(o, n, e.cfunc);
\r
6616 clear : function(o) {
\r
6617 var t = this, a = t.events, i, e;
\r
6622 for (i = a.length - 1; i >= 0; i--) {
\r
6625 if (e.obj === o) {
\r
6626 t._remove(e.obj, e.name, e.cfunc);
\r
6627 e.obj = e.cfunc = null;
\r
6634 cancel : function(e) {
\r
6640 return this.prevent(e);
\r
6643 stop : function(e) {
\r
6644 if (e.stopPropagation)
\r
6645 e.stopPropagation();
\r
6647 e.cancelBubble = true;
\r
6652 prevent : function(e) {
\r
6653 if (e.preventDefault)
\r
6654 e.preventDefault();
\r
6656 e.returnValue = false;
\r
6661 destroy : function() {
\r
6664 each(t.events, function(e, i) {
\r
6665 t._remove(e.obj, e.name, e.cfunc);
\r
6666 e.obj = e.cfunc = null;
\r
6673 _add : function(o, n, f) {
\r
6674 if (o.attachEvent)
\r
6675 o.attachEvent('on' + n, f);
\r
6676 else if (o.addEventListener)
\r
6677 o.addEventListener(n, f, false);
\r
6682 _remove : function(o, n, f) {
\r
6685 if (o.detachEvent)
\r
6686 o.detachEvent('on' + n, f);
\r
6687 else if (o.removeEventListener)
\r
6688 o.removeEventListener(n, f, false);
\r
6690 o['on' + n] = null;
\r
6692 // Might fail with permission denined on IE so we just ignore that
\r
6697 _pageInit : function(win) {
\r
6700 // Keep it from running more than once
\r
6704 t.domLoaded = true;
\r
6706 each(t.inits, function(c) {
\r
6713 _wait : function(win) {
\r
6714 var t = this, doc = win.document;
\r
6716 // No need since the document is already loaded
\r
6717 if (win.tinyMCE_GZ && tinyMCE_GZ.loaded) {
\r
6723 if (doc.attachEvent) {
\r
6724 doc.attachEvent("onreadystatechange", function() {
\r
6725 if (doc.readyState === "complete") {
\r
6726 doc.detachEvent("onreadystatechange", arguments.callee);
\r
6731 if (doc.documentElement.doScroll && win == win.top) {
\r
6737 // If IE is used, use the trick by Diego Perini
\r
6738 // http://javascript.nwbox.com/IEContentLoaded/
\r
6739 doc.documentElement.doScroll("left");
\r
6741 setTimeout(arguments.callee, 0);
\r
6748 } else if (doc.addEventListener) {
\r
6749 t._add(win, 'DOMContentLoaded', function() {
\r
6754 t._add(win, 'load', function() {
\r
6760 preventDefault : function() {
\r
6761 this.returnValue = false;
\r
6764 stopPropagation : function() {
\r
6765 this.cancelBubble = true;
\r
6770 Event = tinymce.dom.Event = new tinymce.dom.EventUtils();
\r
6772 // Dispatch DOM content loaded event for IE and Safari
\r
6773 Event._wait(window);
\r
6775 tinymce.addUnload(function() {
\r
6780 (function(tinymce) {
\r
6781 tinymce.dom.Element = function(id, settings) {
\r
6782 var t = this, dom, el;
\r
6784 t.settings = settings = settings || {};
\r
6786 t.dom = dom = settings.dom || tinymce.DOM;
\r
6788 // Only IE leaks DOM references, this is a lot faster
\r
6789 if (!tinymce.isIE)
\r
6790 el = dom.get(t.id);
\r
6793 ('getPos,getRect,getParent,add,setStyle,getStyle,setStyles,' +
\r
6794 'setAttrib,setAttribs,getAttrib,addClass,removeClass,' +
\r
6795 'hasClass,getOuterHTML,setOuterHTML,remove,show,hide,' +
\r
6796 'isHidden,setHTML,get').split(/,/)
\r
6798 t[k] = function() {
\r
6801 for (i = 0; i < arguments.length; i++)
\r
6802 a.push(arguments[i]);
\r
6804 a = dom[k].apply(dom, a);
\r
6811 tinymce.extend(t, {
\r
6812 on : function(n, f, s) {
\r
6813 return tinymce.dom.Event.add(t.id, n, f, s);
\r
6816 getXY : function() {
\r
6818 x : parseInt(t.getStyle('left')),
\r
6819 y : parseInt(t.getStyle('top'))
\r
6823 getSize : function() {
\r
6824 var n = dom.get(t.id);
\r
6827 w : parseInt(t.getStyle('width') || n.clientWidth),
\r
6828 h : parseInt(t.getStyle('height') || n.clientHeight)
\r
6832 moveTo : function(x, y) {
\r
6833 t.setStyles({left : x, top : y});
\r
6836 moveBy : function(x, y) {
\r
6837 var p = t.getXY();
\r
6839 t.moveTo(p.x + x, p.y + y);
\r
6842 resizeTo : function(w, h) {
\r
6843 t.setStyles({width : w, height : h});
\r
6846 resizeBy : function(w, h) {
\r
6847 var s = t.getSize();
\r
6849 t.resizeTo(s.w + w, s.h + h);
\r
6852 update : function(k) {
\r
6855 if (tinymce.isIE6 && settings.blocker) {
\r
6859 if (k.indexOf('get') === 0 || k.indexOf('has') === 0 || k.indexOf('is') === 0)
\r
6862 // Remove blocker on remove
\r
6863 if (k == 'remove') {
\r
6864 dom.remove(t.blocker);
\r
6869 t.blocker = dom.uniqueId();
\r
6870 b = dom.add(settings.container || dom.getRoot(), 'iframe', {id : t.blocker, style : 'position:absolute;', frameBorder : 0, src : 'javascript:""'});
\r
6871 dom.setStyle(b, 'opacity', 0);
\r
6873 b = dom.get(t.blocker);
\r
6875 dom.setStyles(b, {
\r
6876 left : t.getStyle('left', 1),
\r
6877 top : t.getStyle('top', 1),
\r
6878 width : t.getStyle('width', 1),
\r
6879 height : t.getStyle('height', 1),
\r
6880 display : t.getStyle('display', 1),
\r
6881 zIndex : parseInt(t.getStyle('zIndex', 1) || 0) - 1
\r
6889 (function(tinymce) {
\r
6890 function trimNl(s) {
\r
6891 return s.replace(/[\n\r]+/g, '');
\r
6895 var is = tinymce.is, isIE = tinymce.isIE, each = tinymce.each;
\r
6897 tinymce.create('tinymce.dom.Selection', {
\r
6898 Selection : function(dom, win, serializer) {
\r
6903 t.serializer = serializer;
\r
6907 'onBeforeSetContent',
\r
6909 'onBeforeGetContent',
\r
6915 t[e] = new tinymce.util.Dispatcher(t);
\r
6918 // No W3C Range support
\r
6919 if (!t.win.getSelection)
\r
6920 t.tridentSel = new tinymce.dom.TridentSelection(t);
\r
6922 if (tinymce.isIE && dom.boxModel)
\r
6923 this._fixIESelection();
\r
6926 tinymce.addUnload(t.destroy, t);
\r
6929 getContent : function(s) {
\r
6930 var t = this, r = t.getRng(), e = t.dom.create("body"), se = t.getSel(), wb, wa, n;
\r
6935 s.format = s.format || 'html';
\r
6936 t.onBeforeGetContent.dispatch(t, s);
\r
6938 if (s.format == 'text')
\r
6939 return t.isCollapsed() ? '' : (r.text || (se.toString ? se.toString() : ''));
\r
6941 if (r.cloneContents) {
\r
6942 n = r.cloneContents();
\r
6946 } else if (is(r.item) || is(r.htmlText))
\r
6947 e.innerHTML = r.item ? r.item(0).outerHTML : r.htmlText;
\r
6949 e.innerHTML = r.toString();
\r
6951 // Keep whitespace before and after
\r
6952 if (/^\s/.test(e.innerHTML))
\r
6955 if (/\s+$/.test(e.innerHTML))
\r
6958 s.getInner = true;
\r
6960 s.content = t.isCollapsed() ? '' : wb + t.serializer.serialize(e, s) + wa;
\r
6961 t.onGetContent.dispatch(t, s);
\r
6966 setContent : function(content, args) {
\r
6967 var self = this, rng = self.getRng(), caretNode, doc = self.win.document, frag, temp;
\r
6969 args = args || {format : 'html'};
\r
6971 content = args.content = content;
\r
6973 // Dispatch before set content event
\r
6974 if (!args.no_events)
\r
6975 self.onBeforeSetContent.dispatch(self, args);
\r
6977 content = args.content;
\r
6979 if (rng.insertNode) {
\r
6980 // Make caret marker since insertNode places the caret in the beginning of text after insert
\r
6981 content += '<span id="__caret">_</span>';
\r
6983 // Delete and insert new node
\r
6984 if (rng.startContainer == doc && rng.endContainer == doc) {
\r
6985 // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents
\r
6986 doc.body.innerHTML = content;
\r
6988 rng.deleteContents();
\r
6990 if (doc.body.childNodes.length == 0) {
\r
6991 doc.body.innerHTML = content;
\r
6993 // createContextualFragment doesn't exists in IE 9 DOMRanges
\r
6994 if (rng.createContextualFragment) {
\r
6995 rng.insertNode(rng.createContextualFragment(content));
\r
6997 // Fake createContextualFragment call in IE 9
\r
6998 frag = doc.createDocumentFragment();
\r
6999 temp = doc.createElement('div');
\r
7001 frag.appendChild(temp);
\r
7002 temp.outerHTML = content;
\r
7004 rng.insertNode(frag);
\r
7009 // Move to caret marker
\r
7010 caretNode = self.dom.get('__caret');
\r
7012 // Make sure we wrap it compleatly, Opera fails with a simple select call
\r
7013 rng = doc.createRange();
\r
7014 rng.setStartBefore(caretNode);
\r
7015 rng.setEndBefore(caretNode);
\r
7018 // Remove the caret position
\r
7019 self.dom.remove('__caret');
\r
7023 // Delete content and get caret text selection
\r
7024 doc.execCommand('Delete', false, null);
\r
7025 rng = self.getRng();
\r
7028 rng.pasteHTML(content);
\r
7031 // Dispatch set content event
\r
7032 if (!args.no_events)
\r
7033 self.onSetContent.dispatch(self, args);
\r
7036 getStart : function() {
\r
7037 var rng = this.getRng(), startElement, parentElement, checkRng, node;
\r
7039 if (rng.duplicate || rng.item) {
\r
7040 // Control selection, return first item
\r
7042 return rng.item(0);
\r
7044 // Get start element
\r
7045 checkRng = rng.duplicate();
\r
7046 checkRng.collapse(1);
\r
7047 startElement = checkRng.parentElement();
\r
7049 // Check if range parent is inside the start element, then return the inner parent element
\r
7050 // This will fix issues when a single element is selected, IE would otherwise return the wrong start element
\r
7051 parentElement = node = rng.parentElement();
\r
7052 while (node = node.parentNode) {
\r
7053 if (node == startElement) {
\r
7054 startElement = parentElement;
\r
7059 return startElement;
\r
7061 startElement = rng.startContainer;
\r
7063 if (startElement.nodeType == 1 && startElement.hasChildNodes())
\r
7064 startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)];
\r
7066 if (startElement && startElement.nodeType == 3)
\r
7067 return startElement.parentNode;
\r
7069 return startElement;
\r
7073 getEnd : function() {
\r
7074 var t = this, r = t.getRng(), e, eo;
\r
7076 if (r.duplicate || r.item) {
\r
7080 r = r.duplicate();
\r
7082 e = r.parentElement();
\r
7084 if (e && e.nodeName == 'BODY')
\r
7085 return e.lastChild || e;
\r
7089 e = r.endContainer;
\r
7092 if (e.nodeType == 1 && e.hasChildNodes())
\r
7093 e = e.childNodes[eo > 0 ? eo - 1 : eo];
\r
7095 if (e && e.nodeType == 3)
\r
7096 return e.parentNode;
\r
7102 getBookmark : function(type, normalized) {
\r
7103 var t = this, dom = t.dom, rng, rng2, id, collapsed, name, element, index, chr = '\uFEFF', styles;
\r
7105 function findIndex(name, element) {
\r
7108 each(dom.select(name), function(node, i) {
\r
7109 if (node == element)
\r
7117 function getLocation() {
\r
7118 var rng = t.getRng(true), root = dom.getRoot(), bookmark = {};
\r
7120 function getPoint(rng, start) {
\r
7121 var container = rng[start ? 'startContainer' : 'endContainer'],
\r
7122 offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0;
\r
7124 if (container.nodeType == 3) {
\r
7126 for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling)
\r
7127 offset += node.nodeValue.length;
\r
7130 point.push(offset);
\r
7132 childNodes = container.childNodes;
\r
7134 if (offset >= childNodes.length && childNodes.length) {
\r
7136 offset = Math.max(0, childNodes.length - 1);
\r
7139 point.push(t.dom.nodeIndex(childNodes[offset], normalized) + after);
\r
7142 for (; container && container != root; container = container.parentNode)
\r
7143 point.push(t.dom.nodeIndex(container, normalized));
\r
7148 bookmark.start = getPoint(rng, true);
\r
7150 if (!t.isCollapsed())
\r
7151 bookmark.end = getPoint(rng);
\r
7156 return getLocation();
\r
7159 // Handle simple range
\r
7161 return {rng : t.getRng()};
\r
7164 id = dom.uniqueId();
\r
7165 collapsed = tinyMCE.activeEditor.selection.isCollapsed();
\r
7166 styles = 'overflow:hidden;line-height:0px';
\r
7168 // Explorer method
\r
7169 if (rng.duplicate || rng.item) {
\r
7172 rng2 = rng.duplicate();
\r
7175 // Insert start marker
\r
7177 rng.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_start" style="' + styles + '">' + chr + '</span>');
\r
7179 // Insert end marker
\r
7181 rng2.collapse(false);
\r
7183 // Detect the empty space after block elements in IE and move the end back one character <p></p>] becomes <p>]</p>
\r
7184 rng.moveToElementText(rng2.parentElement());
\r
7185 if (rng.compareEndPoints('StartToEnd', rng2) == 0)
\r
7186 rng2.move('character', -1);
\r
7188 rng2.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_end" style="' + styles + '">' + chr + '</span>');
\r
7191 // IE might throw unspecified error so lets ignore it
\r
7195 // Control selection
\r
7196 element = rng.item(0);
\r
7197 name = element.nodeName;
\r
7199 return {name : name, index : findIndex(name, element)};
\r
7202 element = t.getNode();
\r
7203 name = element.nodeName;
\r
7204 if (name == 'IMG')
\r
7205 return {name : name, index : findIndex(name, element)};
\r
7208 rng2 = rng.cloneRange();
\r
7210 // Insert end marker
\r
7212 rng2.collapse(false);
\r
7213 rng2.insertNode(dom.create('span', {'data-mce-type' : "bookmark", id : id + '_end', style : styles}, chr));
\r
7216 rng.collapse(true);
\r
7217 rng.insertNode(dom.create('span', {'data-mce-type' : "bookmark", id : id + '_start', style : styles}, chr));
\r
7220 t.moveToBookmark({id : id, keep : 1});
\r
7225 moveToBookmark : function(bookmark) {
\r
7226 var t = this, dom = t.dom, marker1, marker2, rng, root, startContainer, endContainer, startOffset, endOffset;
\r
7228 // Clear selection cache
\r
7230 t.tridentSel.destroy();
\r
7233 if (bookmark.start) {
\r
7234 rng = dom.createRng();
\r
7235 root = dom.getRoot();
\r
7237 function setEndPoint(start) {
\r
7238 var point = bookmark[start ? 'start' : 'end'], i, node, offset, children;
\r
7241 offset = point[0];
\r
7243 // Find container node
\r
7244 for (node = root, i = point.length - 1; i >= 1; i--) {
\r
7245 children = node.childNodes;
\r
7247 if (point[i] > children.length - 1)
\r
7250 node = children[point[i]];
\r
7253 // Move text offset to best suitable location
\r
7254 if (node.nodeType === 3)
\r
7255 offset = Math.min(point[0], node.nodeValue.length);
\r
7257 // Move element offset to best suitable location
\r
7258 if (node.nodeType === 1)
\r
7259 offset = Math.min(point[0], node.childNodes.length);
\r
7261 // Set offset within container node
\r
7263 rng.setStart(node, offset);
\r
7265 rng.setEnd(node, offset);
\r
7271 if (setEndPoint(true) && setEndPoint()) {
\r
7274 } else if (bookmark.id) {
\r
7275 function restoreEndPoint(suffix) {
\r
7276 var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep;
\r
7279 node = marker.parentNode;
\r
7281 if (suffix == 'start') {
\r
7283 idx = dom.nodeIndex(marker);
\r
7285 node = marker.firstChild;
\r
7289 startContainer = endContainer = node;
\r
7290 startOffset = endOffset = idx;
\r
7293 idx = dom.nodeIndex(marker);
\r
7295 node = marker.firstChild;
\r
7299 endContainer = node;
\r
7304 prev = marker.previousSibling;
\r
7305 next = marker.nextSibling;
\r
7307 // Remove all marker text nodes
\r
7308 each(tinymce.grep(marker.childNodes), function(node) {
\r
7309 if (node.nodeType == 3)
\r
7310 node.nodeValue = node.nodeValue.replace(/\uFEFF/g, '');
\r
7313 // Remove marker but keep children if for example contents where inserted into the marker
\r
7314 // Also remove duplicated instances of the marker for example by a split operation or by WebKit auto split on paste feature
\r
7315 while (marker = dom.get(bookmark.id + '_' + suffix))
\r
7316 dom.remove(marker, 1);
\r
7318 // If siblings are text nodes then merge them unless it's Opera since it some how removes the node
\r
7319 // and we are sniffing since adding a lot of detection code for a browser with 3% of the market isn't worth the effort. Sorry, Opera but it's just a fact
\r
7320 if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !tinymce.isOpera) {
\r
7321 idx = prev.nodeValue.length;
\r
7322 prev.appendData(next.nodeValue);
\r
7325 if (suffix == 'start') {
\r
7326 startContainer = endContainer = prev;
\r
7327 startOffset = endOffset = idx;
\r
7329 endContainer = prev;
\r
7337 function addBogus(node) {
\r
7338 // Adds a bogus BR element for empty block elements or just a space on IE since it renders BR elements incorrectly
\r
7339 if (dom.isBlock(node) && !node.innerHTML)
\r
7340 node.innerHTML = !isIE ? '<br data-mce-bogus="1" />' : ' ';
\r
7345 // Restore start/end points
\r
7346 restoreEndPoint('start');
\r
7347 restoreEndPoint('end');
\r
7349 if (startContainer) {
\r
7350 rng = dom.createRng();
\r
7351 rng.setStart(addBogus(startContainer), startOffset);
\r
7352 rng.setEnd(addBogus(endContainer), endOffset);
\r
7355 } else if (bookmark.name) {
\r
7356 t.select(dom.select(bookmark.name)[bookmark.index]);
\r
7357 } else if (bookmark.rng)
\r
7358 t.setRng(bookmark.rng);
\r
7362 select : function(node, content) {
\r
7363 var t = this, dom = t.dom, rng = dom.createRng(), idx;
\r
7366 idx = dom.nodeIndex(node);
\r
7367 rng.setStart(node.parentNode, idx);
\r
7368 rng.setEnd(node.parentNode, idx + 1);
\r
7370 // Find first/last text node or BR element
\r
7372 function setPoint(node, start) {
\r
7373 var walker = new tinymce.dom.TreeWalker(node, node);
\r
7377 if (node.nodeType == 3 && tinymce.trim(node.nodeValue).length != 0) {
\r
7379 rng.setStart(node, 0);
\r
7381 rng.setEnd(node, node.nodeValue.length);
\r
7387 if (node.nodeName == 'BR') {
\r
7389 rng.setStartBefore(node);
\r
7391 rng.setEndBefore(node);
\r
7395 } while (node = (start ? walker.next() : walker.prev()));
\r
7398 setPoint(node, 1);
\r
7408 isCollapsed : function() {
\r
7409 var t = this, r = t.getRng(), s = t.getSel();
\r
7414 if (r.compareEndPoints)
\r
7415 return r.compareEndPoints('StartToEnd', r) === 0;
\r
7417 return !s || r.collapsed;
\r
7420 collapse : function(to_start) {
\r
7421 var self = this, rng = self.getRng(), node;
\r
7423 // Control range on IE
\r
7425 node = rng.item(0);
\r
7426 rng = self.win.document.body.createTextRange();
\r
7427 rng.moveToElementText(node);
\r
7430 rng.collapse(!!to_start);
\r
7434 getSel : function() {
\r
7435 var t = this, w = this.win;
\r
7437 return w.getSelection ? w.getSelection() : w.document.selection;
\r
7440 getRng : function(w3c) {
\r
7441 var t = this, s, r, elm, doc = t.win.document;
\r
7443 // Found tridentSel object then we need to use that one
\r
7444 if (w3c && t.tridentSel)
\r
7445 return t.tridentSel.getRangeAt(0);
\r
7448 if (s = t.getSel())
\r
7449 r = s.rangeCount > 0 ? s.getRangeAt(0) : (s.createRange ? s.createRange() : doc.createRange());
\r
7451 // IE throws unspecified error here if TinyMCE is placed in a frame/iframe
\r
7454 // We have W3C ranges and it's IE then fake control selection since IE9 doesn't handle that correctly yet
\r
7455 if (tinymce.isIE && r && r.setStart && doc.selection.createRange().item) {
\r
7456 elm = doc.selection.createRange().item(0);
\r
7457 r = doc.createRange();
\r
7458 r.setStartBefore(elm);
\r
7459 r.setEndAfter(elm);
\r
7462 // No range found then create an empty one
\r
7463 // This can occur when the editor is placed in a hidden container element on Gecko
\r
7464 // Or on IE when there was an exception
\r
7466 r = doc.createRange ? doc.createRange() : doc.body.createTextRange();
\r
7468 if (t.selectedRange && t.explicitRange) {
\r
7469 if (r.compareBoundaryPoints(r.START_TO_START, t.selectedRange) === 0 && r.compareBoundaryPoints(r.END_TO_END, t.selectedRange) === 0) {
\r
7470 // Safari, Opera and Chrome only ever select text which causes the range to change.
\r
7471 // This lets us use the originally set range if the selection hasn't been changed by the user.
\r
7472 r = t.explicitRange;
\r
7474 t.selectedRange = null;
\r
7475 t.explicitRange = null;
\r
7482 setRng : function(r) {
\r
7485 if (!t.tridentSel) {
\r
7489 t.explicitRange = r;
\r
7492 s.removeAllRanges();
\r
7494 // IE9 might throw errors here don't know why
\r
7498 t.selectedRange = s.getRangeAt(0);
\r
7502 if (r.cloneRange) {
\r
7503 t.tridentSel.addRange(r);
\r
7507 // Is IE specific range
\r
7511 // Needed for some odd IE bug #1843306
\r
7516 setNode : function(n) {
\r
7519 t.setContent(t.dom.getOuterHTML(n));
\r
7524 getNode : function() {
\r
7525 var t = this, rng = t.getRng(), sel = t.getSel(), elm, start = rng.startContainer, end = rng.endContainer;
\r
7527 // Range maybe lost after the editor is made visible again
\r
7529 return t.dom.getRoot();
\r
7531 if (rng.setStart) {
\r
7532 elm = rng.commonAncestorContainer;
\r
7534 // Handle selection a image or other control like element such as anchors
\r
7535 if (!rng.collapsed) {
\r
7536 if (rng.startContainer == rng.endContainer) {
\r
7537 if (rng.endOffset - rng.startOffset < 2) {
\r
7538 if (rng.startContainer.hasChildNodes())
\r
7539 elm = rng.startContainer.childNodes[rng.startOffset];
\r
7543 // If the anchor node is a element instead of a text node then return this element
\r
7544 //if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1)
\r
7545 // return sel.anchorNode.childNodes[sel.anchorOffset];
\r
7547 // Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent.
\r
7548 // This happens when you double click an underlined word in FireFox.
\r
7549 if (start.nodeType === 3 && end.nodeType === 3) {
\r
7550 function skipEmptyTextNodes(n, forwards) {
\r
7552 while (n && n.nodeType === 3 && n.length === 0) {
\r
7553 n = forwards ? n.nextSibling : n.previousSibling;
\r
7557 if (start.length === rng.startOffset) {
\r
7558 start = skipEmptyTextNodes(start.nextSibling, true);
\r
7560 start = start.parentNode;
\r
7562 if (rng.endOffset === 0) {
\r
7563 end = skipEmptyTextNodes(end.previousSibling, false);
\r
7565 end = end.parentNode;
\r
7568 if (start && start === end)
\r
7573 if (elm && elm.nodeType == 3)
\r
7574 return elm.parentNode;
\r
7579 return rng.item ? rng.item(0) : rng.parentElement();
\r
7582 getSelectedBlocks : function(st, en) {
\r
7583 var t = this, dom = t.dom, sb, eb, n, bl = [];
\r
7585 sb = dom.getParent(st || t.getStart(), dom.isBlock);
\r
7586 eb = dom.getParent(en || t.getEnd(), dom.isBlock);
\r
7591 if (sb && eb && sb != eb) {
\r
7594 while ((n = n.nextSibling) && n != eb) {
\r
7595 if (dom.isBlock(n))
\r
7600 if (eb && sb != eb)
\r
7606 destroy : function(s) {
\r
7612 t.tridentSel.destroy();
\r
7614 // Manual destroy then remove unload handler
\r
7616 tinymce.removeUnload(t.destroy);
\r
7619 // IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode
\r
7620 _fixIESelection : function() {
\r
7621 var dom = this.dom, doc = dom.doc, body = doc.body, started, startRng, htmlElm;
\r
7623 // Make HTML element unselectable since we are going to handle selection by hand
\r
7624 doc.documentElement.unselectable = true;
\r
7626 // Return range from point or null if it failed
\r
7627 function rngFromPoint(x, y) {
\r
7628 var rng = body.createTextRange();
\r
7631 rng.moveToPoint(x, y);
\r
7633 // IE sometimes throws and exception, so lets just ignore it
\r
7640 // Fires while the selection is changing
\r
7641 function selectionChange(e) {
\r
7644 // Check if the button is down or not
\r
7646 // Create range from mouse position
\r
7647 pointRng = rngFromPoint(e.x, e.y);
\r
7650 // Check if pointRange is before/after selection then change the endPoint
\r
7651 if (pointRng.compareEndPoints('StartToStart', startRng) > 0)
\r
7652 pointRng.setEndPoint('StartToStart', startRng);
\r
7654 pointRng.setEndPoint('EndToEnd', startRng);
\r
7656 pointRng.select();
\r
7662 // Removes listeners
\r
7663 function endSelection() {
\r
7664 var rng = doc.selection.createRange();
\r
7666 // If the range is collapsed then use the last start range
\r
7667 if (startRng && !rng.item && rng.compareEndPoints('StartToEnd', rng) === 0)
\r
7668 startRng.select();
\r
7670 dom.unbind(doc, 'mouseup', endSelection);
\r
7671 dom.unbind(doc, 'mousemove', selectionChange);
\r
7672 startRng = started = 0;
\r
7675 // Detect when user selects outside BODY
\r
7676 dom.bind(doc, ['mousedown', 'contextmenu'], function(e) {
\r
7677 if (e.target.nodeName === 'HTML') {
\r
7681 // Detect vertical scrollbar, since IE will fire a mousedown on the scrollbar and have target set as HTML
\r
7682 htmlElm = doc.documentElement;
\r
7683 if (htmlElm.scrollHeight > htmlElm.clientHeight)
\r
7687 // Setup start position
\r
7688 startRng = rngFromPoint(e.x, e.y);
\r
7690 // Listen for selection change events
\r
7691 dom.bind(doc, 'mouseup', endSelection);
\r
7692 dom.bind(doc, 'mousemove', selectionChange);
\r
7695 startRng.select();
\r
7703 (function(tinymce) {
\r
7704 tinymce.dom.Serializer = function(settings, dom, schema) {
\r
7705 var onPreProcess, onPostProcess, isIE = tinymce.isIE, each = tinymce.each, htmlParser;
\r
7707 // Support the old apply_source_formatting option
\r
7708 if (!settings.apply_source_formatting)
\r
7709 settings.indent = false;
\r
7711 settings.remove_trailing_brs = true;
\r
7713 // Default DOM and Schema if they are undefined
\r
7714 dom = dom || tinymce.DOM;
\r
7715 schema = schema || new tinymce.html.Schema(settings);
\r
7716 settings.entity_encoding = settings.entity_encoding || 'named';
\r
7718 onPreProcess = new tinymce.util.Dispatcher(self);
\r
7720 onPostProcess = new tinymce.util.Dispatcher(self);
\r
7722 htmlParser = new tinymce.html.DomParser(settings, schema);
\r
7724 // Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed
\r
7725 htmlParser.addAttributeFilter('src,href,style', function(nodes, name) {
\r
7726 var i = nodes.length, node, value, internalName = 'data-mce-' + name, urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope, undef;
\r
7731 value = node.attributes.map[internalName];
\r
7732 if (value !== undef) {
\r
7733 // Set external name to internal value and remove internal
\r
7734 node.attr(name, value.length > 0 ? value : null);
\r
7735 node.attr(internalName, null);
\r
7737 // No internal attribute found then convert the value we have in the DOM
\r
7738 value = node.attributes.map[name];
\r
7740 if (name === "style")
\r
7741 value = dom.serializeStyle(dom.parseStyle(value), node.name);
\r
7742 else if (urlConverter)
\r
7743 value = urlConverter.call(urlConverterScope, value, name, node.name);
\r
7745 node.attr(name, value.length > 0 ? value : null);
\r
7750 // Remove internal classes mceItem<..>
\r
7751 htmlParser.addAttributeFilter('class', function(nodes, name) {
\r
7752 var i = nodes.length, node, value;
\r
7756 value = node.attr('class').replace(/\s*mce(Item\w+|Selected)\s*/g, '');
\r
7757 node.attr('class', value.length > 0 ? value : null);
\r
7761 // Remove bookmark elements
\r
7762 htmlParser.addAttributeFilter('data-mce-type', function(nodes, name, args) {
\r
7763 var i = nodes.length, node;
\r
7768 if (node.attributes.map['data-mce-type'] === 'bookmark' && !args.cleanup)
\r
7773 // Force script into CDATA sections and remove the mce- prefix also add comments around styles
\r
7774 htmlParser.addNodeFilter('script,style', function(nodes, name) {
\r
7775 var i = nodes.length, node, value;
\r
7777 function trim(value) {
\r
7778 return value.replace(/(<!--\[CDATA\[|\]\]-->)/g, '\n')
\r
7779 .replace(/^[\r\n]*|[\r\n]*$/g, '')
\r
7780 .replace(/^\s*(\/\/\s*<!--|\/\/\s*<!\[CDATA\[|<!--|<!\[CDATA\[)[\r\n]*/g, '')
\r
7781 .replace(/\s*(\/\/\s*\]\]>|\/\/\s*-->|\]\]>|-->|\]\]-->)\s*$/g, '');
\r
7786 value = node.firstChild ? node.firstChild.value : '';
\r
7788 if (name === "script") {
\r
7789 // Remove mce- prefix from script elements
\r
7790 node.attr('type', (node.attr('type') || 'text/javascript').replace(/^mce\-/, ''));
\r
7792 if (value.length > 0)
\r
7793 node.firstChild.value = '// <![CDATA[\n' + trim(value) + '\n// ]]>';
\r
7795 if (value.length > 0)
\r
7796 node.firstChild.value = '<!--\n' + trim(value) + '\n-->';
\r
7801 // Convert comments to cdata and handle protected comments
\r
7802 htmlParser.addNodeFilter('#comment', function(nodes, name) {
\r
7803 var i = nodes.length, node;
\r
7808 if (node.value.indexOf('[CDATA[') === 0) {
\r
7809 node.name = '#cdata';
\r
7811 node.value = node.value.replace(/^\[CDATA\[|\]\]$/g, '');
\r
7812 } else if (node.value.indexOf('mce:protected ') === 0) {
\r
7813 node.name = "#text";
\r
7816 node.value = unescape(node.value).substr(14);
\r
7821 htmlParser.addNodeFilter('xml:namespace,input', function(nodes, name) {
\r
7822 var i = nodes.length, node;
\r
7826 if (node.type === 7)
\r
7828 else if (node.type === 1) {
\r
7829 if (name === "input" && !("type" in node.attributes.map))
\r
7830 node.attr('type', 'text');
\r
7835 // Fix list elements, TODO: Replace this later
\r
7836 if (settings.fix_list_elements) {
\r
7837 htmlParser.addNodeFilter('ul,ol', function(nodes, name) {
\r
7838 var i = nodes.length, node, parentNode;
\r
7842 parentNode = node.parent;
\r
7844 if (parentNode.name === 'ul' || parentNode.name === 'ol') {
\r
7845 if (node.prev && node.prev.name === 'li') {
\r
7846 node.prev.append(node);
\r
7853 // Remove internal data attributes
\r
7854 htmlParser.addAttributeFilter('data-mce-src,data-mce-href,data-mce-style', function(nodes, name) {
\r
7855 var i = nodes.length;
\r
7858 nodes[i].attr(name, null);
\r
7862 // Return public methods
\r
7866 addNodeFilter : htmlParser.addNodeFilter,
\r
7868 addAttributeFilter : htmlParser.addAttributeFilter,
\r
7870 onPreProcess : onPreProcess,
\r
7872 onPostProcess : onPostProcess,
\r
7874 serialize : function(node, args) {
\r
7875 var impl, doc, oldDoc, htmlSerializer, content;
\r
7877 // Explorer won't clone contents of script and style and the
\r
7878 // selected index of select elements are cleared on a clone operation.
\r
7879 if (isIE && dom.select('script,style,select').length > 0) {
\r
7880 content = node.innerHTML;
\r
7881 node = node.cloneNode(false);
\r
7882 dom.setHTML(node, content);
\r
7884 node = node.cloneNode(true);
\r
7886 // Nodes needs to be attached to something in WebKit/Opera
\r
7887 // Older builds of Opera crashes if you attach the node to an document created dynamically
\r
7888 // and since we can't feature detect a crash we need to sniff the acutal build number
\r
7889 // This fix will make DOM ranges and make Sizzle happy!
\r
7890 impl = node.ownerDocument.implementation;
\r
7891 if (impl.createHTMLDocument) {
\r
7892 // Create an empty HTML document
\r
7893 doc = impl.createHTMLDocument("");
\r
7895 // Add the element or it's children if it's a body element to the new document
\r
7896 each(node.nodeName == 'BODY' ? node.childNodes : [node], function(node) {
\r
7897 doc.body.appendChild(doc.importNode(node, true));
\r
7900 // Grab first child or body element for serialization
\r
7901 if (node.nodeName != 'BODY')
\r
7902 node = doc.body.firstChild;
\r
7906 // set the new document in DOMUtils so createElement etc works
\r
7911 args = args || {};
\r
7912 args.format = args.format || 'html';
\r
7915 if (!args.no_events) {
\r
7917 onPreProcess.dispatch(self, args);
\r
7920 // Setup serializer
\r
7921 htmlSerializer = new tinymce.html.Serializer(settings, schema);
\r
7923 // Parse and serialize HTML
\r
7924 args.content = htmlSerializer.serialize(
\r
7925 htmlParser.parse(args.getInner ? node.innerHTML : tinymce.trim(dom.getOuterHTML(node), args), args)
\r
7928 // Replace all BOM characters for now until we can find a better solution
\r
7929 if (!args.cleanup)
\r
7930 args.content = args.content.replace(/\uFEFF/g, '');
\r
7933 if (!args.no_events)
\r
7934 onPostProcess.dispatch(self, args);
\r
7936 // Restore the old document if it was changed
\r
7942 return args.content;
\r
7945 addRules : function(rules) {
\r
7946 schema.addValidElements(rules);
\r
7949 setRules : function(rules) {
\r
7950 schema.setValidElements(rules);
\r
7955 (function(tinymce) {
\r
7956 tinymce.dom.ScriptLoader = function(settings) {
\r
7962 scriptLoadedCallbacks = {},
\r
7963 queueLoadedCallbacks = [],
\r
7967 function loadScript(url, callback) {
\r
7968 var t = this, dom = tinymce.DOM, elm, uri, loc, id;
\r
7970 // Execute callback when script is loaded
\r
7975 elm.onreadystatechange = elm.onload = elm = null;
\r
7980 function error() {
\r
7981 // Report the error so it's easier for people to spot loading errors
\r
7982 if (typeof(console) !== "undefined" && console.log)
\r
7983 console.log("Failed to load: " + url);
\r
7985 // We can't mark it as done if there is a load error since
\r
7986 // A) We don't want to produce 404 errors on the server and
\r
7987 // B) the onerror event won't fire on all browsers.
\r
7991 id = dom.uniqueId();
\r
7993 if (tinymce.isIE6) {
\r
7994 uri = new tinymce.util.URI(url);
\r
7997 // If script is from same domain and we
\r
7998 // use IE 6 then use XHR since it's more reliable
\r
7999 if (uri.host == loc.hostname && uri.port == loc.port && (uri.protocol + ':') == loc.protocol && uri.protocol.toLowerCase() != 'file') {
\r
8000 tinymce.util.XHR.send({
\r
8001 url : tinymce._addVer(uri.getURI()),
\r
8002 success : function(content) {
\r
8003 // Create new temp script element
\r
8004 var script = dom.create('script', {
\r
8005 type : 'text/javascript'
\r
8008 // Evaluate script in global scope
\r
8009 script.text = content;
\r
8010 document.getElementsByTagName('head')[0].appendChild(script);
\r
8011 dom.remove(script);
\r
8023 // Create new script element
\r
8024 elm = dom.create('script', {
\r
8026 type : 'text/javascript',
\r
8027 src : tinymce._addVer(url)
\r
8030 // Add onload listener for non IE browsers since IE9
\r
8031 // fires onload event before the script is parsed and executed
\r
8032 if (!tinymce.isIE)
\r
8033 elm.onload = done;
\r
8035 // Add onerror event will get fired on some browsers but not all of them
\r
8036 elm.onerror = error;
\r
8038 // Opera 9.60 doesn't seem to fire the onreadystate event at correctly
\r
8039 if (!tinymce.isOpera) {
\r
8040 elm.onreadystatechange = function() {
\r
8041 var state = elm.readyState;
\r
8043 // Loaded state is passed on IE 6 however there
\r
8044 // are known issues with this method but we can't use
\r
8045 // XHR in a cross domain loading
\r
8046 if (state == 'complete' || state == 'loaded')
\r
8051 // Most browsers support this feature so we report errors
\r
8052 // for those at least to help users track their missing plugins etc
\r
8053 // todo: Removed since it produced error if the document is unloaded by navigating away, re-add it as an option
\r
8054 /*elm.onerror = function() {
\r
8055 alert('Failed to load: ' + url);
\r
8058 // Add script to document
\r
8059 (document.getElementsByTagName('head')[0] || document.body).appendChild(elm);
\r
8062 this.isDone = function(url) {
\r
8063 return states[url] == LOADED;
\r
8066 this.markDone = function(url) {
\r
8067 states[url] = LOADED;
\r
8070 this.add = this.load = function(url, callback, scope) {
\r
8071 var item, state = states[url];
\r
8073 // Add url to load queue
\r
8074 if (state == undefined) {
\r
8076 states[url] = QUEUED;
\r
8080 // Store away callback for later execution
\r
8081 if (!scriptLoadedCallbacks[url])
\r
8082 scriptLoadedCallbacks[url] = [];
\r
8084 scriptLoadedCallbacks[url].push({
\r
8086 scope : scope || this
\r
8091 this.loadQueue = function(callback, scope) {
\r
8092 this.loadScripts(queue, callback, scope);
\r
8095 this.loadScripts = function(scripts, callback, scope) {
\r
8098 function execScriptLoadedCallbacks(url) {
\r
8099 // Execute URL callback functions
\r
8100 tinymce.each(scriptLoadedCallbacks[url], function(callback) {
\r
8101 callback.func.call(callback.scope);
\r
8104 scriptLoadedCallbacks[url] = undefined;
\r
8107 queueLoadedCallbacks.push({
\r
8109 scope : scope || this
\r
8112 loadScripts = function() {
\r
8113 var loadingScripts = tinymce.grep(scripts);
\r
8115 // Current scripts has been handled
\r
8116 scripts.length = 0;
\r
8118 // Load scripts that needs to be loaded
\r
8119 tinymce.each(loadingScripts, function(url) {
\r
8120 // Script is already loaded then execute script callbacks directly
\r
8121 if (states[url] == LOADED) {
\r
8122 execScriptLoadedCallbacks(url);
\r
8126 // Is script not loading then start loading it
\r
8127 if (states[url] != LOADING) {
\r
8128 states[url] = LOADING;
\r
8131 loadScript(url, function() {
\r
8132 states[url] = LOADED;
\r
8135 execScriptLoadedCallbacks(url);
\r
8137 // Load more scripts if they where added by the recently loaded script
\r
8143 // No scripts are currently loading then execute all pending queue loaded callbacks
\r
8145 tinymce.each(queueLoadedCallbacks, function(callback) {
\r
8146 callback.func.call(callback.scope);
\r
8149 queueLoadedCallbacks.length = 0;
\r
8157 // Global script loader
\r
8158 tinymce.ScriptLoader = new tinymce.dom.ScriptLoader();
\r
8161 tinymce.dom.TreeWalker = function(start_node, root_node) {
\r
8162 var node = start_node;
\r
8164 function findSibling(node, start_name, sibling_name, shallow) {
\r
8165 var sibling, parent;
\r
8168 // Walk into nodes if it has a start
\r
8169 if (!shallow && node[start_name])
\r
8170 return node[start_name];
\r
8172 // Return the sibling if it has one
\r
8173 if (node != root_node) {
\r
8174 sibling = node[sibling_name];
\r
8178 // Walk up the parents to look for siblings
\r
8179 for (parent = node.parentNode; parent && parent != root_node; parent = parent.parentNode) {
\r
8180 sibling = parent[sibling_name];
\r
8188 this.current = function() {
\r
8192 this.next = function(shallow) {
\r
8193 return (node = findSibling(node, 'firstChild', 'nextSibling', shallow));
\r
8196 this.prev = function(shallow) {
\r
8197 return (node = findSibling(node, 'lastChild', 'previousSibling', shallow));
\r
8201 (function(tinymce) {
\r
8202 tinymce.dom.RangeUtils = function(dom) {
\r
8203 var INVISIBLE_CHAR = '\uFEFF';
\r
8205 this.walk = function(rng, callback) {
\r
8206 var startContainer = rng.startContainer,
\r
8207 startOffset = rng.startOffset,
\r
8208 endContainer = rng.endContainer,
\r
8209 endOffset = rng.endOffset,
\r
8210 ancestor, startPoint,
\r
8211 endPoint, node, parent, siblings, nodes;
\r
8213 // Handle table cell selection the table plugin enables
\r
8214 // you to fake select table cells and perform formatting actions on them
\r
8215 nodes = dom.select('td.mceSelected,th.mceSelected');
\r
8216 if (nodes.length > 0) {
\r
8217 tinymce.each(nodes, function(node) {
\r
8224 function collectSiblings(node, name, end_node) {
\r
8225 var siblings = [];
\r
8227 for (; node && node != end_node; node = node[name])
\r
8228 siblings.push(node);
\r
8233 function findEndPoint(node, root) {
\r
8235 if (node.parentNode == root)
\r
8238 node = node.parentNode;
\r
8242 function walkBoundary(start_node, end_node, next) {
\r
8243 var siblingName = next ? 'nextSibling' : 'previousSibling';
\r
8245 for (node = start_node, parent = node.parentNode; node && node != end_node; node = parent) {
\r
8246 parent = node.parentNode;
\r
8247 siblings = collectSiblings(node == start_node ? node : node[siblingName], siblingName);
\r
8249 if (siblings.length) {
\r
8251 siblings.reverse();
\r
8253 callback(siblings);
\r
8258 // If index based start position then resolve it
\r
8259 if (startContainer.nodeType == 1 && startContainer.hasChildNodes())
\r
8260 startContainer = startContainer.childNodes[startOffset];
\r
8262 // If index based end position then resolve it
\r
8263 if (endContainer.nodeType == 1 && endContainer.hasChildNodes())
\r
8264 endContainer = endContainer.childNodes[Math.min(endOffset - 1, endContainer.childNodes.length - 1)];
\r
8266 // Find common ancestor and end points
\r
8267 ancestor = dom.findCommonAncestor(startContainer, endContainer);
\r
8270 if (startContainer == endContainer)
\r
8271 return callback([startContainer]);
\r
8273 // Process left side
\r
8274 for (node = startContainer; node; node = node.parentNode) {
\r
8275 if (node == endContainer)
\r
8276 return walkBoundary(startContainer, ancestor, true);
\r
8278 if (node == ancestor)
\r
8282 // Process right side
\r
8283 for (node = endContainer; node; node = node.parentNode) {
\r
8284 if (node == startContainer)
\r
8285 return walkBoundary(endContainer, ancestor);
\r
8287 if (node == ancestor)
\r
8291 // Find start/end point
\r
8292 startPoint = findEndPoint(startContainer, ancestor) || startContainer;
\r
8293 endPoint = findEndPoint(endContainer, ancestor) || endContainer;
\r
8296 walkBoundary(startContainer, startPoint, true);
\r
8298 // Walk the middle from start to end point
\r
8299 siblings = collectSiblings(
\r
8300 startPoint == startContainer ? startPoint : startPoint.nextSibling,
\r
8302 endPoint == endContainer ? endPoint.nextSibling : endPoint
\r
8305 if (siblings.length)
\r
8306 callback(siblings);
\r
8308 // Walk right leaf
\r
8309 walkBoundary(endContainer, endPoint);
\r
8312 /* this.split = function(rng) {
\r
8313 var startContainer = rng.startContainer,
\r
8314 startOffset = rng.startOffset,
\r
8315 endContainer = rng.endContainer,
\r
8316 endOffset = rng.endOffset;
\r
8318 function splitText(node, offset) {
\r
8319 if (offset == node.nodeValue.length)
\r
8320 node.appendData(INVISIBLE_CHAR);
\r
8322 node = node.splitText(offset);
\r
8324 if (node.nodeValue === INVISIBLE_CHAR)
\r
8325 node.nodeValue = '';
\r
8330 // Handle single text node
\r
8331 if (startContainer == endContainer) {
\r
8332 if (startContainer.nodeType == 3) {
\r
8333 if (startOffset != 0)
\r
8334 startContainer = endContainer = splitText(startContainer, startOffset);
\r
8336 if (endOffset - startOffset != startContainer.nodeValue.length)
\r
8337 splitText(startContainer, endOffset - startOffset);
\r
8340 // Split startContainer text node if needed
\r
8341 if (startContainer.nodeType == 3 && startOffset != 0) {
\r
8342 startContainer = splitText(startContainer, startOffset);
\r
8346 // Split endContainer text node if needed
\r
8347 if (endContainer.nodeType == 3 && endOffset != endContainer.nodeValue.length) {
\r
8348 endContainer = splitText(endContainer, endOffset).previousSibling;
\r
8349 endOffset = endContainer.nodeValue.length;
\r
8354 startContainer : startContainer,
\r
8355 startOffset : startOffset,
\r
8356 endContainer : endContainer,
\r
8357 endOffset : endOffset
\r
8363 tinymce.dom.RangeUtils.compareRanges = function(rng1, rng2) {
\r
8364 if (rng1 && rng2) {
\r
8365 // Compare native IE ranges
\r
8366 if (rng1.item || rng1.duplicate) {
\r
8367 // Both are control ranges and the selected element matches
\r
8368 if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0))
\r
8371 // Both are text ranges and the range matches
\r
8372 if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1))
\r
8375 // Compare w3c ranges
\r
8376 return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset;
\r
8384 (function(tinymce) {
\r
8385 var Event = tinymce.dom.Event, each = tinymce.each;
\r
8387 tinymce.create('tinymce.ui.KeyboardNavigation', {
\r
8388 KeyboardNavigation: function(settings, dom) {
\r
8389 var t = this, root = settings.root, items = settings.items,
\r
8390 enableUpDown = settings.enableUpDown, enableLeftRight = settings.enableLeftRight || !settings.enableUpDown,
\r
8391 excludeFromTabOrder = settings.excludeFromTabOrder,
\r
8392 itemFocussed, itemBlurred, rootKeydown, rootFocussed, focussedId;
\r
8394 dom = dom || tinymce.DOM;
\r
8396 itemFocussed = function(evt) {
\r
8397 focussedId = evt.target.id;
\r
8400 itemBlurred = function(evt) {
\r
8401 dom.setAttrib(evt.target.id, 'tabindex', '-1');
\r
8404 rootFocussed = function(evt) {
\r
8405 var item = dom.get(focussedId);
\r
8406 dom.setAttrib(item, 'tabindex', '0');
\r
8410 t.focus = function() {
\r
8411 dom.get(focussedId).focus();
\r
8414 t.destroy = function() {
\r
8415 each(items, function(item) {
\r
8416 dom.unbind(dom.get(item.id), 'focus', itemFocussed);
\r
8417 dom.unbind(dom.get(item.id), 'blur', itemBlurred);
\r
8420 dom.unbind(dom.get(root), 'focus', rootFocussed);
\r
8421 dom.unbind(dom.get(root), 'keydown', rootKeydown);
\r
8423 items = dom = root = t.focus = itemFocussed = itemBlurred = rootKeydown = rootFocussed = null;
\r
8424 t.destroy = function() {};
\r
8427 t.moveFocus = function(dir, evt) {
\r
8428 var idx = -1, controls = t.controls, newFocus;
\r
8433 each(items, function(item, index) {
\r
8434 if (item.id === focussedId) {
\r
8442 idx = items.length - 1;
\r
8443 } else if (idx >= items.length) {
\r
8447 newFocus = items[idx];
\r
8448 dom.setAttrib(focussedId, 'tabindex', '-1');
\r
8449 dom.setAttrib(newFocus.id, 'tabindex', '0');
\r
8450 dom.get(newFocus.id).focus();
\r
8452 if (settings.actOnFocus) {
\r
8453 settings.onAction(newFocus.id);
\r
8457 Event.cancel(evt);
\r
8460 rootKeydown = function(evt) {
\r
8461 var DOM_VK_LEFT = 37, DOM_VK_RIGHT = 39, DOM_VK_UP = 38, DOM_VK_DOWN = 40, DOM_VK_ESCAPE = 27, DOM_VK_ENTER = 14, DOM_VK_RETURN = 13, DOM_VK_SPACE = 32;
\r
8463 switch (evt.keyCode) {
\r
8465 if (enableLeftRight) t.moveFocus(-1);
\r
8468 case DOM_VK_RIGHT:
\r
8469 if (enableLeftRight) t.moveFocus(1);
\r
8473 if (enableUpDown) t.moveFocus(-1);
\r
8477 if (enableUpDown) t.moveFocus(1);
\r
8480 case DOM_VK_ESCAPE:
\r
8481 if (settings.onCancel) {
\r
8482 settings.onCancel();
\r
8483 Event.cancel(evt);
\r
8487 case DOM_VK_ENTER:
\r
8488 case DOM_VK_RETURN:
\r
8489 case DOM_VK_SPACE:
\r
8490 if (settings.onAction) {
\r
8491 settings.onAction(focussedId);
\r
8492 Event.cancel(evt);
\r
8498 // Set up state and listeners for each item.
\r
8499 each(items, function(item, idx) {
\r
8503 item.id = dom.uniqueId('_mce_item_');
\r
8506 if (excludeFromTabOrder) {
\r
8507 dom.bind(item.id, 'blur', itemBlurred);
\r
8510 tabindex = (idx === 0 ? '0' : '-1');
\r
8513 dom.setAttrib(item.id, 'tabindex', tabindex);
\r
8514 dom.bind(dom.get(item.id), 'focus', itemFocussed);
\r
8517 // Setup initial state for root element.
\r
8519 focussedId = items[0].id;
\r
8522 dom.setAttrib(root, 'tabindex', '-1');
\r
8524 // Setup listeners for root element.
\r
8525 dom.bind(dom.get(root), 'focus', rootFocussed);
\r
8526 dom.bind(dom.get(root), 'keydown', rootKeydown);
\r
8530 (function(tinymce) {
\r
8531 // Shorten class names
\r
8532 var DOM = tinymce.DOM, is = tinymce.is;
\r
8534 tinymce.create('tinymce.ui.Control', {
\r
8535 Control : function(id, s, editor) {
\r
8537 this.settings = s = s || {};
\r
8538 this.rendered = false;
\r
8539 this.onRender = new tinymce.util.Dispatcher(this);
\r
8540 this.classPrefix = '';
\r
8541 this.scope = s.scope || this;
\r
8542 this.disabled = 0;
\r
8544 this.editor = editor;
\r
8547 setAriaProperty : function(property, value) {
\r
8548 var element = DOM.get(this.id + '_aria') || DOM.get(this.id);
\r
8550 DOM.setAttrib(element, 'aria-' + property, !!value);
\r
8554 focus : function() {
\r
8555 DOM.get(this.id).focus();
\r
8558 setDisabled : function(s) {
\r
8559 if (s != this.disabled) {
\r
8560 this.setAriaProperty('disabled', s);
\r
8562 this.setState('Disabled', s);
\r
8563 this.setState('Enabled', !s);
\r
8564 this.disabled = s;
\r
8568 isDisabled : function() {
\r
8569 return this.disabled;
\r
8572 setActive : function(s) {
\r
8573 if (s != this.active) {
\r
8574 this.setState('Active', s);
\r
8576 this.setAriaProperty('pressed', s);
\r
8580 isActive : function() {
\r
8581 return this.active;
\r
8584 setState : function(c, s) {
\r
8585 var n = DOM.get(this.id);
\r
8587 c = this.classPrefix + c;
\r
8590 DOM.addClass(n, c);
\r
8592 DOM.removeClass(n, c);
\r
8595 isRendered : function() {
\r
8596 return this.rendered;
\r
8599 renderHTML : function() {
\r
8602 renderTo : function(n) {
\r
8603 DOM.setHTML(n, this.renderHTML());
\r
8606 postRender : function() {
\r
8609 // Set pending states
\r
8610 if (is(t.disabled)) {
\r
8616 if (is(t.active)) {
\r
8623 remove : function() {
\r
8624 DOM.remove(this.id);
\r
8628 destroy : function() {
\r
8629 tinymce.dom.Event.clear(this.id);
\r
8633 tinymce.create('tinymce.ui.Container:tinymce.ui.Control', {
\r
8634 Container : function(id, s, editor) {
\r
8635 this.parent(id, s, editor);
\r
8637 this.controls = [];
\r
8642 add : function(c) {
\r
8643 this.lookup[c.id] = c;
\r
8644 this.controls.push(c);
\r
8649 get : function(n) {
\r
8650 return this.lookup[n];
\r
8655 tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', {
\r
8656 Separator : function(id, s) {
\r
8657 this.parent(id, s);
\r
8658 this.classPrefix = 'mceSeparator';
\r
8659 this.setDisabled(true);
\r
8662 renderHTML : function() {
\r
8663 return tinymce.DOM.createHTML('span', {'class' : this.classPrefix, role : 'separator', 'aria-orientation' : 'vertical', tabindex : '-1'});
\r
8667 (function(tinymce) {
\r
8668 var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, walk = tinymce.walk;
\r
8670 tinymce.create('tinymce.ui.MenuItem:tinymce.ui.Control', {
\r
8671 MenuItem : function(id, s) {
\r
8672 this.parent(id, s);
\r
8673 this.classPrefix = 'mceMenuItem';
\r
8676 setSelected : function(s) {
\r
8677 this.setState('Selected', s);
\r
8678 this.setAriaProperty('checked', !!s);
\r
8679 this.selected = s;
\r
8682 isSelected : function() {
\r
8683 return this.selected;
\r
8686 postRender : function() {
\r
8691 // Set pending state
\r
8692 if (is(t.selected))
\r
8693 t.setSelected(t.selected);
\r
8698 (function(tinymce) {
\r
8699 var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, walk = tinymce.walk;
\r
8701 tinymce.create('tinymce.ui.Menu:tinymce.ui.MenuItem', {
\r
8702 Menu : function(id, s) {
\r
8707 t.collapsed = false;
\r
8709 t.onAddItem = new tinymce.util.Dispatcher(this);
\r
8712 expand : function(d) {
\r
8716 walk(t, function(o) {
\r
8722 t.collapsed = false;
\r
8725 collapse : function(d) {
\r
8729 walk(t, function(o) {
\r
8735 t.collapsed = true;
\r
8738 isCollapsed : function() {
\r
8739 return this.collapsed;
\r
8742 add : function(o) {
\r
8744 o = new tinymce.ui.MenuItem(o.id || DOM.uniqueId(), o);
\r
8746 this.onAddItem.dispatch(this, o);
\r
8748 return this.items[o.id] = o;
\r
8751 addSeparator : function() {
\r
8752 return this.add({separator : true});
\r
8755 addMenu : function(o) {
\r
8757 o = this.createMenu(o);
\r
8761 return this.add(o);
\r
8764 hasMenus : function() {
\r
8765 return this.menuCount !== 0;
\r
8768 remove : function(o) {
\r
8769 delete this.items[o.id];
\r
8772 removeAll : function() {
\r
8775 walk(t, function(o) {
\r
8787 createMenu : function(o) {
\r
8788 var m = new tinymce.ui.Menu(o.id || DOM.uniqueId(), o);
\r
8790 m.onAddItem.add(this.onAddItem.dispatch, this.onAddItem);
\r
8796 (function(tinymce) {
\r
8797 var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, Event = tinymce.dom.Event, Element = tinymce.dom.Element;
\r
8799 tinymce.create('tinymce.ui.DropMenu:tinymce.ui.Menu', {
\r
8800 DropMenu : function(id, s) {
\r
8802 s.container = s.container || DOM.doc.body;
\r
8803 s.offset_x = s.offset_x || 0;
\r
8804 s.offset_y = s.offset_y || 0;
\r
8805 s.vp_offset_x = s.vp_offset_x || 0;
\r
8806 s.vp_offset_y = s.vp_offset_y || 0;
\r
8808 if (is(s.icons) && !s.icons)
\r
8809 s['class'] += ' mceNoIcons';
\r
8811 this.parent(id, s);
\r
8812 this.onShowMenu = new tinymce.util.Dispatcher(this);
\r
8813 this.onHideMenu = new tinymce.util.Dispatcher(this);
\r
8814 this.classPrefix = 'mceMenu';
\r
8817 createMenu : function(s) {
\r
8818 var t = this, cs = t.settings, m;
\r
8820 s.container = s.container || cs.container;
\r
8822 s.constrain = s.constrain || cs.constrain;
\r
8823 s['class'] = s['class'] || cs['class'];
\r
8824 s.vp_offset_x = s.vp_offset_x || cs.vp_offset_x;
\r
8825 s.vp_offset_y = s.vp_offset_y || cs.vp_offset_y;
\r
8826 s.keyboard_focus = cs.keyboard_focus;
\r
8827 m = new tinymce.ui.DropMenu(s.id || DOM.uniqueId(), s);
\r
8829 m.onAddItem.add(t.onAddItem.dispatch, t.onAddItem);
\r
8834 focus : function() {
\r
8836 if (t.keyboardNav) {
\r
8837 t.keyboardNav.focus();
\r
8841 update : function() {
\r
8842 var t = this, s = t.settings, tb = DOM.get('menu_' + t.id + '_tbl'), co = DOM.get('menu_' + t.id + '_co'), tw, th;
\r
8844 tw = s.max_width ? Math.min(tb.clientWidth, s.max_width) : tb.clientWidth;
\r
8845 th = s.max_height ? Math.min(tb.clientHeight, s.max_height) : tb.clientHeight;
\r
8847 if (!DOM.boxModel)
\r
8848 t.element.setStyles({width : tw + 2, height : th + 2});
\r
8850 t.element.setStyles({width : tw, height : th});
\r
8853 DOM.setStyle(co, 'width', tw);
\r
8855 if (s.max_height) {
\r
8856 DOM.setStyle(co, 'height', th);
\r
8858 if (tb.clientHeight < s.max_height)
\r
8859 DOM.setStyle(co, 'overflow', 'hidden');
\r
8863 showMenu : function(x, y, px) {
\r
8864 var t = this, s = t.settings, co, vp = DOM.getViewPort(), w, h, mx, my, ot = 2, dm, tb, cp = t.classPrefix;
\r
8868 if (t.isMenuVisible)
\r
8871 if (!t.rendered) {
\r
8872 co = DOM.add(t.settings.container, t.renderNode());
\r
8874 each(t.items, function(o) {
\r
8878 t.element = new Element('menu_' + t.id, {blocker : 1, container : s.container});
\r
8880 co = DOM.get('menu_' + t.id);
\r
8882 // Move layer out of sight unless it's Opera since it scrolls to top of page due to an bug
\r
8883 if (!tinymce.isOpera)
\r
8884 DOM.setStyles(co, {left : -0xFFFF , top : -0xFFFF});
\r
8889 x += s.offset_x || 0;
\r
8890 y += s.offset_y || 0;
\r
8894 // Move inside viewport if not submenu
\r
8895 if (s.constrain) {
\r
8896 w = co.clientWidth - ot;
\r
8897 h = co.clientHeight - ot;
\r
8901 if ((x + s.vp_offset_x + w) > mx)
\r
8902 x = px ? px - w : Math.max(0, (mx - s.vp_offset_x) - w);
\r
8904 if ((y + s.vp_offset_y + h) > my)
\r
8905 y = Math.max(0, (my - s.vp_offset_y) - h);
\r
8908 DOM.setStyles(co, {left : x , top : y});
\r
8909 t.element.update();
\r
8911 t.isMenuVisible = 1;
\r
8912 t.mouseClickFunc = Event.add(co, 'click', function(e) {
\r
8917 if (e && (e = DOM.getParent(e, 'tr')) && !DOM.hasClass(e, cp + 'ItemSub')) {
\r
8918 m = t.items[e.id];
\r
8920 if (m.isDisabled())
\r
8929 dm = dm.settings.parent;
\r
8932 if (m.settings.onclick)
\r
8933 m.settings.onclick(e);
\r
8935 return Event.cancel(e); // Cancel to fix onbeforeunload problem
\r
8939 if (t.hasMenus()) {
\r
8940 t.mouseOverFunc = Event.add(co, 'mouseover', function(e) {
\r
8944 if (e && (e = DOM.getParent(e, 'tr'))) {
\r
8945 m = t.items[e.id];
\r
8948 t.lastMenu.collapse(1);
\r
8950 if (m.isDisabled())
\r
8953 if (e && DOM.hasClass(e, cp + 'ItemSub')) {
\r
8954 //p = DOM.getPos(s.container);
\r
8955 r = DOM.getRect(e);
\r
8956 m.showMenu((r.x + r.w - ot), r.y - ot, r.x);
\r
8958 DOM.addClass(DOM.get(m.id).firstChild, cp + 'ItemActive');
\r
8964 Event.add(co, 'keydown', t._keyHandler, t);
\r
8966 t.onShowMenu.dispatch(t);
\r
8968 if (s.keyboard_focus) {
\r
8969 t._setupKeyboardNav();
\r
8973 hideMenu : function(c) {
\r
8974 var t = this, co = DOM.get('menu_' + t.id), e;
\r
8976 if (!t.isMenuVisible)
\r
8979 if (t.keyboardNav) t.keyboardNav.destroy();
\r
8980 Event.remove(co, 'mouseover', t.mouseOverFunc);
\r
8981 Event.remove(co, 'click', t.mouseClickFunc);
\r
8982 Event.remove(co, 'keydown', t._keyHandler);
\r
8984 t.isMenuVisible = 0;
\r
8992 if (e = DOM.get(t.id))
\r
8993 DOM.removeClass(e.firstChild, t.classPrefix + 'ItemActive');
\r
8995 t.onHideMenu.dispatch(t);
\r
8998 add : function(o) {
\r
9003 if (t.isRendered && (co = DOM.get('menu_' + t.id)))
\r
9004 t._add(DOM.select('tbody', co)[0], o);
\r
9009 collapse : function(d) {
\r
9014 remove : function(o) {
\r
9018 return this.parent(o);
\r
9021 destroy : function() {
\r
9022 var t = this, co = DOM.get('menu_' + t.id);
\r
9024 if (t.keyboardNav) t.keyboardNav.destroy();
\r
9025 Event.remove(co, 'mouseover', t.mouseOverFunc);
\r
9026 Event.remove(DOM.select('a', co), 'focus', t.mouseOverFunc);
\r
9027 Event.remove(co, 'click', t.mouseClickFunc);
\r
9028 Event.remove(co, 'keydown', t._keyHandler);
\r
9031 t.element.remove();
\r
9036 renderNode : function() {
\r
9037 var t = this, s = t.settings, n, tb, co, w;
\r
9039 w = DOM.create('div', {role: 'listbox', id : 'menu_' + t.id, 'class' : s['class'], 'style' : 'position:absolute;left:0;top:0;z-index:200000;outline:0'});
\r
9040 if (t.settings.parent) {
\r
9041 DOM.setAttrib(w, 'aria-parent', 'menu_' + t.settings.parent.id);
\r
9043 co = DOM.add(w, 'div', {role: 'presentation', id : 'menu_' + t.id + '_co', 'class' : t.classPrefix + (s['class'] ? ' ' + s['class'] : '')});
\r
9044 t.element = new Element('menu_' + t.id, {blocker : 1, container : s.container});
\r
9047 DOM.add(co, 'span', {'class' : t.classPrefix + 'Line'});
\r
9049 // n = DOM.add(co, 'div', {id : 'menu_' + t.id + '_co', 'class' : 'mceMenuContainer'});
\r
9050 n = DOM.add(co, 'table', {role: 'presentation', id : 'menu_' + t.id + '_tbl', border : 0, cellPadding : 0, cellSpacing : 0});
\r
9051 tb = DOM.add(n, 'tbody');
\r
9053 each(t.items, function(o) {
\r
9057 t.rendered = true;
\r
9062 // Internal functions
\r
9063 _setupKeyboardNav : function(){
\r
9064 var contextMenu, menuItems, t=this;
\r
9065 contextMenu = DOM.select('#menu_' + t.id)[0];
\r
9066 menuItems = DOM.select('a[role=option]', 'menu_' + t.id);
\r
9067 menuItems.splice(0,0,contextMenu);
\r
9068 t.keyboardNav = new tinymce.ui.KeyboardNavigation({
\r
9069 root: 'menu_' + t.id,
\r
9071 onCancel: function() {
\r
9074 enableUpDown: true
\r
9076 contextMenu.focus();
\r
9079 _keyHandler : function(evt) {
\r
9081 switch (evt.keyCode) {
\r
9083 if (t.settings.parent) {
\r
9085 t.settings.parent.focus();
\r
9086 Event.cancel(evt);
\r
9090 if (t.mouseOverFunc)
\r
9091 t.mouseOverFunc(evt);
\r
9096 _add : function(tb, o) {
\r
9097 var n, s = o.settings, a, ro, it, cp = this.classPrefix, ic;
\r
9099 if (s.separator) {
\r
9100 ro = DOM.add(tb, 'tr', {id : o.id, 'class' : cp + 'ItemSeparator'});
\r
9101 DOM.add(ro, 'td', {'class' : cp + 'ItemSeparator'});
\r
9103 if (n = ro.previousSibling)
\r
9104 DOM.addClass(n, 'mceLast');
\r
9109 n = ro = DOM.add(tb, 'tr', {id : o.id, 'class' : cp + 'Item ' + cp + 'ItemEnabled'});
\r
9110 n = it = DOM.add(n, s.titleItem ? 'th' : 'td');
\r
9111 n = a = DOM.add(n, 'a', {id: o.id + '_aria', role: s.titleItem ? 'presentation' : 'option', href : 'javascript:;', onclick : "return false;", onmousedown : 'return false;'});
\r
9114 DOM.setAttrib(a, 'aria-haspopup', 'true');
\r
9115 DOM.setAttrib(a, 'aria-owns', 'menu_' + o.id);
\r
9118 DOM.addClass(it, s['class']);
\r
9119 // n = DOM.add(n, 'span', {'class' : 'item'});
\r
9121 ic = DOM.add(n, 'span', {'class' : 'mceIcon' + (s.icon ? ' mce_' + s.icon : '')});
\r
9124 DOM.add(ic, 'img', {src : s.icon_src});
\r
9126 n = DOM.add(n, s.element || 'span', {'class' : 'mceText', title : o.settings.title}, o.settings.title);
\r
9128 if (o.settings.style)
\r
9129 DOM.setAttrib(n, 'style', o.settings.style);
\r
9131 if (tb.childNodes.length == 1)
\r
9132 DOM.addClass(ro, 'mceFirst');
\r
9134 if ((n = ro.previousSibling) && DOM.hasClass(n, cp + 'ItemSeparator'))
\r
9135 DOM.addClass(ro, 'mceFirst');
\r
9138 DOM.addClass(ro, cp + 'ItemSub');
\r
9140 if (n = ro.previousSibling)
\r
9141 DOM.removeClass(n, 'mceLast');
\r
9143 DOM.addClass(ro, 'mceLast');
\r
9147 (function(tinymce) {
\r
9148 var DOM = tinymce.DOM;
\r
9150 tinymce.create('tinymce.ui.Button:tinymce.ui.Control', {
\r
9151 Button : function(id, s, ed) {
\r
9152 this.parent(id, s, ed);
\r
9153 this.classPrefix = 'mceButton';
\r
9156 renderHTML : function() {
\r
9157 var cp = this.classPrefix, s = this.settings, h, l;
\r
9159 l = DOM.encode(s.label || '');
\r
9160 h = '<a role="button" id="' + this.id + '" href="javascript:;" class="' + cp + ' ' + cp + 'Enabled ' + s['class'] + (l ? ' ' + cp + 'Labeled' : '') +'" onmousedown="return false;" onclick="return false;" aria-labelledby="' + this.id + '_voice" title="' + DOM.encode(s.title) + '">';
\r
9163 h += '<img class="mceIcon" src="' + s.image + '" alt="' + DOM.encode(s.title) + '" />' + l;
\r
9165 h += '<span class="mceIcon ' + s['class'] + '"></span>' + (l ? '<span class="' + cp + 'Label">' + l + '</span>' : '');
\r
9167 h += '<span class="mceVoiceLabel mceIconOnly" style="display: none;" id="' + this.id + '_voice">' + s.title + '</span>';
\r
9172 postRender : function() {
\r
9173 var t = this, s = t.settings;
\r
9175 tinymce.dom.Event.add(t.id, 'click', function(e) {
\r
9176 if (!t.isDisabled())
\r
9177 return s.onclick.call(s.scope, e);
\r
9183 (function(tinymce) {
\r
9184 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher;
\r
9186 tinymce.create('tinymce.ui.ListBox:tinymce.ui.Control', {
\r
9187 ListBox : function(id, s, ed) {
\r
9190 t.parent(id, s, ed);
\r
9194 t.onChange = new Dispatcher(t);
\r
9196 t.onPostRender = new Dispatcher(t);
\r
9198 t.onAdd = new Dispatcher(t);
\r
9200 t.onRenderMenu = new tinymce.util.Dispatcher(this);
\r
9202 t.classPrefix = 'mceListBox';
\r
9205 select : function(va) {
\r
9206 var t = this, fv, f;
\r
9208 if (va == undefined)
\r
9209 return t.selectByIndex(-1);
\r
9211 // Is string or number make function selector
\r
9212 if (va && va.call)
\r
9220 // Do we need to do something?
\r
9221 if (va != t.selectedValue) {
\r
9223 each(t.items, function(o, i) {
\r
9226 t.selectByIndex(i);
\r
9232 t.selectByIndex(-1);
\r
9236 selectByIndex : function(idx) {
\r
9237 var t = this, e, o;
\r
9239 if (idx != t.selectedIndex) {
\r
9240 e = DOM.get(t.id + '_text');
\r
9244 t.selectedValue = o.value;
\r
9245 t.selectedIndex = idx;
\r
9246 DOM.setHTML(e, DOM.encode(o.title));
\r
9247 DOM.removeClass(e, 'mceTitle');
\r
9248 DOM.setAttrib(t.id, 'aria-valuenow', o.title);
\r
9250 DOM.setHTML(e, DOM.encode(t.settings.title));
\r
9251 DOM.addClass(e, 'mceTitle');
\r
9252 t.selectedValue = t.selectedIndex = null;
\r
9253 DOM.setAttrib(t.id, 'aria-valuenow', t.settings.title);
\r
9259 add : function(n, v, o) {
\r
9263 o = tinymce.extend(o, {
\r
9269 t.onAdd.dispatch(t, o);
\r
9272 getLength : function() {
\r
9273 return this.items.length;
\r
9276 renderHTML : function() {
\r
9277 var h = '', t = this, s = t.settings, cp = t.classPrefix;
\r
9279 h = '<span role="button" aria-haspopup="true" aria-labelledby="' + t.id +'_text" aria-describedby="' + t.id + '_voiceDesc"><table role="presentation" tabindex="0" id="' + t.id + '" cellpadding="0" cellspacing="0" class="' + cp + ' ' + cp + 'Enabled' + (s['class'] ? (' ' + s['class']) : '') + '"><tbody><tr>';
\r
9280 h += '<td>' + DOM.createHTML('span', {id: t.id + '_voiceDesc', 'class': 'voiceLabel', style:'display:none;'}, t.settings.title);
\r
9281 h += DOM.createHTML('a', {id : t.id + '_text', tabindex : -1, href : 'javascript:;', 'class' : 'mceText', onclick : "return false;", onmousedown : 'return false;'}, DOM.encode(t.settings.title)) + '</td>';
\r
9282 h += '<td>' + DOM.createHTML('a', {id : t.id + '_open', tabindex : -1, href : 'javascript:;', 'class' : 'mceOpen', onclick : "return false;", onmousedown : 'return false;'}, '<span><span style="display:none;" class="mceIconOnly" aria-hidden="true">\u25BC</span></span>') + '</td>';
\r
9283 h += '</tr></tbody></table></span>';
\r
9288 showMenu : function() {
\r
9289 var t = this, p1, p2, e = DOM.get(this.id), m;
\r
9291 if (t.isDisabled() || t.items.length == 0)
\r
9294 if (t.menu && t.menu.isMenuVisible)
\r
9295 return t.hideMenu();
\r
9297 if (!t.isMenuRendered) {
\r
9299 t.isMenuRendered = true;
\r
9302 p1 = DOM.getPos(this.settings.menu_container);
\r
9303 p2 = DOM.getPos(e);
\r
9306 m.settings.offset_x = p2.x;
\r
9307 m.settings.offset_y = p2.y;
\r
9308 m.settings.keyboard_focus = !tinymce.isOpera; // Opera is buggy when it comes to auto focus
\r
9312 m.items[t.oldID].setSelected(0);
\r
9314 each(t.items, function(o) {
\r
9315 if (o.value === t.selectedValue) {
\r
9316 m.items[o.id].setSelected(1);
\r
9321 m.showMenu(0, e.clientHeight);
\r
9323 Event.add(DOM.doc, 'mousedown', t.hideMenu, t);
\r
9324 DOM.addClass(t.id, t.classPrefix + 'Selected');
\r
9326 //DOM.get(t.id + '_text').focus();
\r
9329 hideMenu : function(e) {
\r
9332 if (t.menu && t.menu.isMenuVisible) {
\r
9333 DOM.removeClass(t.id, t.classPrefix + 'Selected');
\r
9335 // Prevent double toogles by canceling the mouse click event to the button
\r
9336 if (e && e.type == "mousedown" && (e.target.id == t.id + '_text' || e.target.id == t.id + '_open'))
\r
9339 if (!e || !DOM.getParent(e.target, '.mceMenu')) {
\r
9340 DOM.removeClass(t.id, t.classPrefix + 'Selected');
\r
9341 Event.remove(DOM.doc, 'mousedown', t.hideMenu, t);
\r
9342 t.menu.hideMenu();
\r
9347 renderMenu : function() {
\r
9350 m = t.settings.control_manager.createDropMenu(t.id + '_menu', {
\r
9352 'class' : t.classPrefix + 'Menu mceNoIcons',
\r
9357 m.onHideMenu.add(function() {
\r
9363 title : t.settings.title,
\r
9364 'class' : 'mceMenuItemTitle',
\r
9365 onclick : function() {
\r
9366 if (t.settings.onselect('') !== false)
\r
9367 t.select(''); // Must be runned after
\r
9371 each(t.items, function(o) {
\r
9372 // No value then treat it as a title
\r
9373 if (o.value === undefined) {
\r
9376 'class' : 'mceMenuItemTitle',
\r
9377 onclick : function() {
\r
9378 if (t.settings.onselect('') !== false)
\r
9379 t.select(''); // Must be runned after
\r
9383 o.id = DOM.uniqueId();
\r
9384 o.onclick = function() {
\r
9385 if (t.settings.onselect(o.value) !== false)
\r
9386 t.select(o.value); // Must be runned after
\r
9393 t.onRenderMenu.dispatch(t, m);
\r
9397 postRender : function() {
\r
9398 var t = this, cp = t.classPrefix;
\r
9400 Event.add(t.id, 'click', t.showMenu, t);
\r
9401 Event.add(t.id, 'keydown', function(evt) {
\r
9402 if (evt.keyCode == 32) { // Space
\r
9404 Event.cancel(evt);
\r
9407 Event.add(t.id, 'focus', function() {
\r
9408 if (!t._focused) {
\r
9409 t.keyDownHandler = Event.add(t.id, 'keydown', function(e) {
\r
9410 if (e.keyCode == 40) {
\r
9415 t.keyPressHandler = Event.add(t.id, 'keypress', function(e) {
\r
9417 if (e.keyCode == 13) {
\r
9418 // Fake select on enter
\r
9419 v = t.selectedValue;
\r
9420 t.selectedValue = null; // Needs to be null to fake change
\r
9422 t.settings.onselect(v);
\r
9429 Event.add(t.id, 'blur', function() {
\r
9430 Event.remove(t.id, 'keydown', t.keyDownHandler);
\r
9431 Event.remove(t.id, 'keypress', t.keyPressHandler);
\r
9435 // Old IE doesn't have hover on all elements
\r
9436 if (tinymce.isIE6 || !DOM.boxModel) {
\r
9437 Event.add(t.id, 'mouseover', function() {
\r
9438 if (!DOM.hasClass(t.id, cp + 'Disabled'))
\r
9439 DOM.addClass(t.id, cp + 'Hover');
\r
9442 Event.add(t.id, 'mouseout', function() {
\r
9443 if (!DOM.hasClass(t.id, cp + 'Disabled'))
\r
9444 DOM.removeClass(t.id, cp + 'Hover');
\r
9448 t.onPostRender.dispatch(t, DOM.get(t.id));
\r
9451 destroy : function() {
\r
9454 Event.clear(this.id + '_text');
\r
9455 Event.clear(this.id + '_open');
\r
9459 (function(tinymce) {
\r
9460 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher;
\r
9462 tinymce.create('tinymce.ui.NativeListBox:tinymce.ui.ListBox', {
\r
9463 NativeListBox : function(id, s) {
\r
9464 this.parent(id, s);
\r
9465 this.classPrefix = 'mceNativeListBox';
\r
9468 setDisabled : function(s) {
\r
9469 DOM.get(this.id).disabled = s;
\r
9470 this.setAriaProperty('disabled', s);
\r
9473 isDisabled : function() {
\r
9474 return DOM.get(this.id).disabled;
\r
9477 select : function(va) {
\r
9478 var t = this, fv, f;
\r
9480 if (va == undefined)
\r
9481 return t.selectByIndex(-1);
\r
9483 // Is string or number make function selector
\r
9484 if (va && va.call)
\r
9492 // Do we need to do something?
\r
9493 if (va != t.selectedValue) {
\r
9495 each(t.items, function(o, i) {
\r
9498 t.selectByIndex(i);
\r
9504 t.selectByIndex(-1);
\r
9508 selectByIndex : function(idx) {
\r
9509 DOM.get(this.id).selectedIndex = idx + 1;
\r
9510 this.selectedValue = this.items[idx] ? this.items[idx].value : null;
\r
9513 add : function(n, v, a) {
\r
9519 if (t.isRendered())
\r
9520 DOM.add(DOM.get(this.id), 'option', a, n);
\r
9529 t.onAdd.dispatch(t, o);
\r
9532 getLength : function() {
\r
9533 return this.items.length;
\r
9536 renderHTML : function() {
\r
9539 h = DOM.createHTML('option', {value : ''}, '-- ' + t.settings.title + ' --');
\r
9541 each(t.items, function(it) {
\r
9542 h += DOM.createHTML('option', {value : it.value}, it.title);
\r
9545 h = DOM.createHTML('select', {id : t.id, 'class' : 'mceNativeListBox', 'aria-labelledby': t.id + '_aria'}, h);
\r
9546 h += DOM.createHTML('span', {id : t.id + '_aria', 'style': 'display: none'}, t.settings.title);
\r
9550 postRender : function() {
\r
9551 var t = this, ch, changeListenerAdded = true;
\r
9553 t.rendered = true;
\r
9555 function onChange(e) {
\r
9556 var v = t.items[e.target.selectedIndex - 1];
\r
9558 if (v && (v = v.value)) {
\r
9559 t.onChange.dispatch(t, v);
\r
9561 if (t.settings.onselect)
\r
9562 t.settings.onselect(v);
\r
9566 Event.add(t.id, 'change', onChange);
\r
9568 // Accessibility keyhandler
\r
9569 Event.add(t.id, 'keydown', function(e) {
\r
9572 Event.remove(t.id, 'change', ch);
\r
9573 changeListenerAdded = false;
\r
9575 bf = Event.add(t.id, 'blur', function() {
\r
9576 if (changeListenerAdded) return;
\r
9577 changeListenerAdded = true;
\r
9578 Event.add(t.id, 'change', onChange);
\r
9579 Event.remove(t.id, 'blur', bf);
\r
9582 if (e.keyCode == 13 || e.keyCode == 32) {
\r
9584 return Event.cancel(e);
\r
9588 t.onPostRender.dispatch(t, DOM.get(t.id));
\r
9592 (function(tinymce) {
\r
9593 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each;
\r
9595 tinymce.create('tinymce.ui.MenuButton:tinymce.ui.Button', {
\r
9596 MenuButton : function(id, s, ed) {
\r
9597 this.parent(id, s, ed);
\r
9599 this.onRenderMenu = new tinymce.util.Dispatcher(this);
\r
9601 s.menu_container = s.menu_container || DOM.doc.body;
\r
9604 showMenu : function() {
\r
9605 var t = this, p1, p2, e = DOM.get(t.id), m;
\r
9607 if (t.isDisabled())
\r
9610 if (!t.isMenuRendered) {
\r
9612 t.isMenuRendered = true;
\r
9615 if (t.isMenuVisible)
\r
9616 return t.hideMenu();
\r
9618 p1 = DOM.getPos(t.settings.menu_container);
\r
9619 p2 = DOM.getPos(e);
\r
9622 m.settings.offset_x = p2.x;
\r
9623 m.settings.offset_y = p2.y;
\r
9624 m.settings.vp_offset_x = p2.x;
\r
9625 m.settings.vp_offset_y = p2.y;
\r
9626 m.settings.keyboard_focus = t._focused;
\r
9627 m.showMenu(0, e.clientHeight);
\r
9629 Event.add(DOM.doc, 'mousedown', t.hideMenu, t);
\r
9630 t.setState('Selected', 1);
\r
9632 t.isMenuVisible = 1;
\r
9635 renderMenu : function() {
\r
9638 m = t.settings.control_manager.createDropMenu(t.id + '_menu', {
\r
9640 'class' : this.classPrefix + 'Menu',
\r
9641 icons : t.settings.icons
\r
9644 m.onHideMenu.add(function() {
\r
9649 t.onRenderMenu.dispatch(t, m);
\r
9653 hideMenu : function(e) {
\r
9656 // Prevent double toogles by canceling the mouse click event to the button
\r
9657 if (e && e.type == "mousedown" && DOM.getParent(e.target, function(e) {return e.id === t.id || e.id === t.id + '_open';}))
\r
9660 if (!e || !DOM.getParent(e.target, '.mceMenu')) {
\r
9661 t.setState('Selected', 0);
\r
9662 Event.remove(DOM.doc, 'mousedown', t.hideMenu, t);
\r
9664 t.menu.hideMenu();
\r
9667 t.isMenuVisible = 0;
\r
9670 postRender : function() {
\r
9671 var t = this, s = t.settings;
\r
9673 Event.add(t.id, 'click', function() {
\r
9674 if (!t.isDisabled()) {
\r
9676 s.onclick(t.value);
\r
9685 (function(tinymce) {
\r
9686 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each;
\r
9688 tinymce.create('tinymce.ui.SplitButton:tinymce.ui.MenuButton', {
\r
9689 SplitButton : function(id, s, ed) {
\r
9690 this.parent(id, s, ed);
\r
9691 this.classPrefix = 'mceSplitButton';
\r
9694 renderHTML : function() {
\r
9695 var h, t = this, s = t.settings, h1;
\r
9697 h = '<tbody><tr>';
\r
9700 h1 = DOM.createHTML('img ', {src : s.image, role: 'presentation', 'class' : 'mceAction ' + s['class']});
\r
9702 h1 = DOM.createHTML('span', {'class' : 'mceAction ' + s['class']}, '');
\r
9704 h1 += DOM.createHTML('span', {'class': 'mceVoiceLabel mceIconOnly', id: t.id + '_voice', style: 'display:none;'}, s.title);
\r
9705 h += '<td >' + DOM.createHTML('a', {role: 'button', id : t.id + '_action', tabindex: '-1', href : 'javascript:;', 'class' : 'mceAction ' + s['class'], onclick : "return false;", onmousedown : 'return false;', title : s.title}, h1) + '</td>';
\r
9707 h1 = DOM.createHTML('span', {'class' : 'mceOpen ' + s['class']}, '<span style="display:none;" class="mceIconOnly" aria-hidden="true">\u25BC</span>');
\r
9708 h += '<td >' + DOM.createHTML('a', {role: 'button', id : t.id + '_open', tabindex: '-1', href : 'javascript:;', 'class' : 'mceOpen ' + s['class'], onclick : "return false;", onmousedown : 'return false;', title : s.title}, h1) + '</td>';
\r
9710 h += '</tr></tbody>';
\r
9711 h = DOM.createHTML('table', {id : t.id, role: 'presentation', tabindex: '0', 'class' : 'mceSplitButton mceSplitButtonEnabled ' + s['class'], cellpadding : '0', cellspacing : '0', title : s.title}, h);
\r
9712 return DOM.createHTML('span', {role: 'button', 'aria-labelledby': t.id + '_voice', 'aria-haspopup': 'true'}, h);
\r
9715 postRender : function() {
\r
9716 var t = this, s = t.settings, activate;
\r
9719 activate = function(evt) {
\r
9720 if (!t.isDisabled()) {
\r
9721 s.onclick(t.value);
\r
9722 Event.cancel(evt);
\r
9725 Event.add(t.id + '_action', 'click', activate);
\r
9726 Event.add(t.id, ['click', 'keydown'], function(evt) {
\r
9727 var DOM_VK_SPACE = 32, DOM_VK_ENTER = 14, DOM_VK_RETURN = 13, DOM_VK_UP = 38, DOM_VK_DOWN = 40;
\r
9728 if ((evt.keyCode === 32 || evt.keyCode === 13 || evt.keyCode === 14) && !evt.altKey && !evt.ctrlKey && !evt.metaKey) {
\r
9730 Event.cancel(evt);
\r
9731 } else if (evt.type === 'click' || evt.keyCode === DOM_VK_DOWN) {
\r
9733 Event.cancel(evt);
\r
9738 Event.add(t.id + '_open', 'click', function (evt) {
\r
9740 Event.cancel(evt);
\r
9742 Event.add([t.id, t.id + '_open'], 'focus', function() {t._focused = 1;});
\r
9743 Event.add([t.id, t.id + '_open'], 'blur', function() {t._focused = 0;});
\r
9745 // Old IE doesn't have hover on all elements
\r
9746 if (tinymce.isIE6 || !DOM.boxModel) {
\r
9747 Event.add(t.id, 'mouseover', function() {
\r
9748 if (!DOM.hasClass(t.id, 'mceSplitButtonDisabled'))
\r
9749 DOM.addClass(t.id, 'mceSplitButtonHover');
\r
9752 Event.add(t.id, 'mouseout', function() {
\r
9753 if (!DOM.hasClass(t.id, 'mceSplitButtonDisabled'))
\r
9754 DOM.removeClass(t.id, 'mceSplitButtonHover');
\r
9759 destroy : function() {
\r
9762 Event.clear(this.id + '_action');
\r
9763 Event.clear(this.id + '_open');
\r
9764 Event.clear(this.id);
\r
9769 (function(tinymce) {
\r
9770 var DOM = tinymce.DOM, Event = tinymce.dom.Event, is = tinymce.is, each = tinymce.each;
\r
9772 tinymce.create('tinymce.ui.ColorSplitButton:tinymce.ui.SplitButton', {
\r
9773 ColorSplitButton : function(id, s, ed) {
\r
9776 t.parent(id, s, ed);
\r
9778 t.settings = s = tinymce.extend({
\r
9779 colors : '000000,993300,333300,003300,003366,000080,333399,333333,800000,FF6600,808000,008000,008080,0000FF,666699,808080,FF0000,FF9900,99CC00,339966,33CCCC,3366FF,800080,999999,FF00FF,FFCC00,FFFF00,00FF00,00FFFF,00CCFF,993366,C0C0C0,FF99CC,FFCC99,FFFF99,CCFFCC,CCFFFF,99CCFF,CC99FF,FFFFFF',
\r
9781 default_color : '#888888'
\r
9784 t.onShowMenu = new tinymce.util.Dispatcher(t);
\r
9786 t.onHideMenu = new tinymce.util.Dispatcher(t);
\r
9788 t.value = s.default_color;
\r
9791 showMenu : function() {
\r
9792 var t = this, r, p, e, p2;
\r
9794 if (t.isDisabled())
\r
9797 if (!t.isMenuRendered) {
\r
9799 t.isMenuRendered = true;
\r
9802 if (t.isMenuVisible)
\r
9803 return t.hideMenu();
\r
9805 e = DOM.get(t.id);
\r
9806 DOM.show(t.id + '_menu');
\r
9807 DOM.addClass(e, 'mceSplitButtonSelected');
\r
9808 p2 = DOM.getPos(e);
\r
9809 DOM.setStyles(t.id + '_menu', {
\r
9811 top : p2.y + e.clientHeight,
\r
9816 Event.add(DOM.doc, 'mousedown', t.hideMenu, t);
\r
9817 t.onShowMenu.dispatch(t);
\r
9820 t._keyHandler = Event.add(t.id + '_menu', 'keydown', function(e) {
\r
9821 if (e.keyCode == 27)
\r
9825 DOM.select('a', t.id + '_menu')[0].focus(); // Select first link
\r
9828 t.isMenuVisible = 1;
\r
9831 hideMenu : function(e) {
\r
9834 if (t.isMenuVisible) {
\r
9835 // Prevent double toogles by canceling the mouse click event to the button
\r
9836 if (e && e.type == "mousedown" && DOM.getParent(e.target, function(e) {return e.id === t.id + '_open';}))
\r
9839 if (!e || !DOM.getParent(e.target, '.mceSplitButtonMenu')) {
\r
9840 DOM.removeClass(t.id, 'mceSplitButtonSelected');
\r
9841 Event.remove(DOM.doc, 'mousedown', t.hideMenu, t);
\r
9842 Event.remove(t.id + '_menu', 'keydown', t._keyHandler);
\r
9843 DOM.hide(t.id + '_menu');
\r
9846 t.isMenuVisible = 0;
\r
9850 renderMenu : function() {
\r
9851 var t = this, m, i = 0, s = t.settings, n, tb, tr, w, context;
\r
9853 w = DOM.add(s.menu_container, 'div', {role: 'listbox', id : t.id + '_menu', 'class' : s['menu_class'] + ' ' + s['class'], style : 'position:absolute;left:0;top:-1000px;'});
\r
9854 m = DOM.add(w, 'div', {'class' : s['class'] + ' mceSplitButtonMenu'});
\r
9855 DOM.add(m, 'span', {'class' : 'mceMenuLine'});
\r
9857 n = DOM.add(m, 'table', {role: 'presentation', 'class' : 'mceColorSplitMenu'});
\r
9858 tb = DOM.add(n, 'tbody');
\r
9860 // Generate color grid
\r
9862 each(is(s.colors, 'array') ? s.colors : s.colors.split(','), function(c) {
\r
9863 c = c.replace(/^#/, '');
\r
9866 tr = DOM.add(tb, 'tr');
\r
9867 i = s.grid_width - 1;
\r
9870 n = DOM.add(tr, 'td');
\r
9871 n = DOM.add(n, 'a', {
\r
9873 href : 'javascript:;',
\r
9875 backgroundColor : '#' + c
\r
9877 'title': t.editor.getLang('colors.' + c, c),
\r
9878 'data-mce-color' : '#' + c
\r
9881 if (t.editor.forcedHighContrastMode) {
\r
9882 n = DOM.add(n, 'canvas', { width: 16, height: 16, 'aria-hidden': 'true' });
\r
9883 if (n.getContext && (context = n.getContext("2d"))) {
\r
9884 context.fillStyle = '#' + c;
\r
9885 context.fillRect(0, 0, 16, 16);
\r
9887 // No point leaving a canvas element around if it's not supported for drawing on anyway.
\r
9893 if (s.more_colors_func) {
\r
9894 n = DOM.add(tb, 'tr');
\r
9895 n = DOM.add(n, 'td', {colspan : s.grid_width, 'class' : 'mceMoreColors'});
\r
9896 n = DOM.add(n, 'a', {role: 'option', id : t.id + '_more', href : 'javascript:;', onclick : 'return false;', 'class' : 'mceMoreColors'}, s.more_colors_title);
\r
9898 Event.add(n, 'click', function(e) {
\r
9899 s.more_colors_func.call(s.more_colors_scope || this);
\r
9900 return Event.cancel(e); // Cancel to fix onbeforeunload problem
\r
9904 DOM.addClass(m, 'mceColorSplitMenu');
\r
9906 new tinymce.ui.KeyboardNavigation({
\r
9907 root: t.id + '_menu',
\r
9908 items: DOM.select('a', t.id + '_menu'),
\r
9909 onCancel: function() {
\r
9915 // Prevent IE from scrolling and hindering click to occur #4019
\r
9916 Event.add(t.id + '_menu', 'mousedown', function(e) {return Event.cancel(e);});
\r
9918 Event.add(t.id + '_menu', 'click', function(e) {
\r
9921 e = DOM.getParent(e.target, 'a', tb);
\r
9923 if (e && e.nodeName.toLowerCase() == 'a' && (c = e.getAttribute('data-mce-color')))
\r
9926 return Event.cancel(e); // Prevent IE auto save warning
\r
9932 setColor : function(c) {
\r
9933 this.displayColor(c);
\r
9935 this.settings.onselect(c);
\r
9938 displayColor : function(c) {
\r
9941 DOM.setStyle(t.id + '_preview', 'backgroundColor', c);
\r
9946 postRender : function() {
\r
9947 var t = this, id = t.id;
\r
9950 DOM.add(id + '_action', 'div', {id : id + '_preview', 'class' : 'mceColorPreview'});
\r
9951 DOM.setStyle(t.id + '_preview', 'backgroundColor', t.value);
\r
9954 destroy : function() {
\r
9957 Event.clear(this.id + '_menu');
\r
9958 Event.clear(this.id + '_more');
\r
9959 DOM.remove(this.id + '_menu');
\r
9964 (function(tinymce) {
\r
9965 // Shorten class names
\r
9966 var dom = tinymce.DOM, each = tinymce.each, Event = tinymce.dom.Event;
\r
9967 tinymce.create('tinymce.ui.ToolbarGroup:tinymce.ui.Container', {
\r
9968 renderHTML : function() {
\r
9969 var t = this, h = [], controls = t.controls, each = tinymce.each, settings = t.settings;
\r
9971 h.push('<div id="' + t.id + '" role="group" aria-labelledby="' + t.id + '_voice">');
\r
9972 //TODO: ACC test this out - adding a role = application for getting the landmarks working well.
\r
9973 h.push("<span role='application'>");
\r
9974 h.push('<span id="' + t.id + '_voice" class="mceVoiceLabel" style="display:none;">' + dom.encode(settings.name) + '</span>');
\r
9975 each(controls, function(toolbar) {
\r
9976 h.push(toolbar.renderHTML());
\r
9978 h.push("</span>");
\r
9981 return h.join('');
\r
9984 focus : function() {
\r
9985 this.keyNav.focus();
\r
9988 postRender : function() {
\r
9989 var t = this, items = [];
\r
9991 each(t.controls, function(toolbar) {
\r
9992 each (toolbar.controls, function(control) {
\r
9994 items.push(control);
\r
9999 t.keyNav = new tinymce.ui.KeyboardNavigation({
\r
10002 onCancel: function() {
\r
10003 t.editor.focus();
\r
10005 excludeFromTabOrder: !t.settings.tab_focus_toolbar
\r
10009 destroy : function() {
\r
10013 self.keyNav.destroy();
\r
10014 Event.clear(self.id);
\r
10019 (function(tinymce) {
\r
10020 // Shorten class names
\r
10021 var dom = tinymce.DOM, each = tinymce.each;
\r
10022 tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', {
\r
10023 renderHTML : function() {
\r
10024 var t = this, h = '', c, co, s = t.settings, i, pr, nx, cl;
\r
10027 for (i=0; i<cl.length; i++) {
\r
10028 // Get current control, prev control, next control and if the control is a list box or not
\r
10033 // Add toolbar start
\r
10035 c = 'mceToolbarStart';
\r
10038 c += ' mceToolbarStartButton';
\r
10039 else if (co.SplitButton)
\r
10040 c += ' mceToolbarStartSplitButton';
\r
10041 else if (co.ListBox)
\r
10042 c += ' mceToolbarStartListBox';
\r
10044 h += dom.createHTML('td', {'class' : c}, dom.createHTML('span', null, '<!-- IE -->'));
\r
10047 // Add toolbar end before list box and after the previous button
\r
10048 // This is to fix the o2k7 editor skins
\r
10049 if (pr && co.ListBox) {
\r
10050 if (pr.Button || pr.SplitButton)
\r
10051 h += dom.createHTML('td', {'class' : 'mceToolbarEnd'}, dom.createHTML('span', null, '<!-- IE -->'));
\r
10054 // Render control HTML
\r
10056 // IE 8 quick fix, needed to propertly generate a hit area for anchors
\r
10058 h += '<td style="position: relative">' + co.renderHTML() + '</td>';
\r
10060 h += '<td>' + co.renderHTML() + '</td>';
\r
10062 // Add toolbar start after list box and before the next button
\r
10063 // This is to fix the o2k7 editor skins
\r
10064 if (nx && co.ListBox) {
\r
10065 if (nx.Button || nx.SplitButton)
\r
10066 h += dom.createHTML('td', {'class' : 'mceToolbarStart'}, dom.createHTML('span', null, '<!-- IE -->'));
\r
10070 c = 'mceToolbarEnd';
\r
10073 c += ' mceToolbarEndButton';
\r
10074 else if (co.SplitButton)
\r
10075 c += ' mceToolbarEndSplitButton';
\r
10076 else if (co.ListBox)
\r
10077 c += ' mceToolbarEndListBox';
\r
10079 h += dom.createHTML('td', {'class' : c}, dom.createHTML('span', null, '<!-- IE -->'));
\r
10081 return dom.createHTML('table', {id : t.id, 'class' : 'mceToolbar' + (s['class'] ? ' ' + s['class'] : ''), cellpadding : '0', cellspacing : '0', align : t.settings.align || '', role: 'presentation', tabindex: '-1'}, '<tbody><tr>' + h + '</tr></tbody>');
\r
10086 (function(tinymce) {
\r
10087 var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each;
\r
10089 tinymce.create('tinymce.AddOnManager', {
\r
10090 AddOnManager : function() {
\r
10095 self.lookup = {};
\r
10096 self.onAdd = new Dispatcher(self);
\r
10099 get : function(n) {
\r
10100 return this.lookup[n];
\r
10103 requireLangPack : function(n) {
\r
10104 var s = tinymce.settings;
\r
10106 if (s && s.language && s.language_load !== false)
\r
10107 tinymce.ScriptLoader.add(this.urls[n] + '/langs/' + s.language + '.js');
\r
10110 add : function(id, o) {
\r
10111 this.items.push(o);
\r
10112 this.lookup[id] = o;
\r
10113 this.onAdd.dispatch(this, id, o);
\r
10118 load : function(n, u, cb, s) {
\r
10124 if (u.indexOf('/') != 0 && u.indexOf('://') == -1)
\r
10125 u = tinymce.baseURL + '/' + u;
\r
10127 t.urls[n] = u.substring(0, u.lastIndexOf('/'));
\r
10129 if (!t.lookup[n])
\r
10130 tinymce.ScriptLoader.add(u, cb, s);
\r
10134 // Create plugin and theme managers
\r
10135 tinymce.PluginManager = new tinymce.AddOnManager();
\r
10136 tinymce.ThemeManager = new tinymce.AddOnManager();
\r
10139 (function(tinymce) {
\r
10141 var each = tinymce.each, extend = tinymce.extend,
\r
10142 DOM = tinymce.DOM, Event = tinymce.dom.Event,
\r
10143 ThemeManager = tinymce.ThemeManager, PluginManager = tinymce.PluginManager,
\r
10144 explode = tinymce.explode,
\r
10145 Dispatcher = tinymce.util.Dispatcher, undefined, instanceCounter = 0;
\r
10147 // Setup some URLs where the editor API is located and where the document is
\r
10148 tinymce.documentBaseURL = window.location.href.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, '');
\r
10149 if (!/[\/\\]$/.test(tinymce.documentBaseURL))
\r
10150 tinymce.documentBaseURL += '/';
\r
10152 tinymce.baseURL = new tinymce.util.URI(tinymce.documentBaseURL).toAbsolute(tinymce.baseURL);
\r
10154 tinymce.baseURI = new tinymce.util.URI(tinymce.baseURL);
\r
10156 // Add before unload listener
\r
10157 // This was required since IE was leaking memory if you added and removed beforeunload listeners
\r
10158 // with attachEvent/detatchEvent so this only adds one listener and instances can the attach to the onBeforeUnload event
\r
10159 tinymce.onBeforeUnload = new Dispatcher(tinymce);
\r
10161 // Must be on window or IE will leak if the editor is placed in frame or iframe
\r
10162 Event.add(window, 'beforeunload', function(e) {
\r
10163 tinymce.onBeforeUnload.dispatch(tinymce, e);
\r
10166 tinymce.onAddEditor = new Dispatcher(tinymce);
\r
10168 tinymce.onRemoveEditor = new Dispatcher(tinymce);
\r
10170 tinymce.EditorManager = extend(tinymce, {
\r
10175 activeEditor : null,
\r
10177 init : function(s) {
\r
10178 var t = this, pl, sl = tinymce.ScriptLoader, e, el = [], ed;
\r
10180 function execCallback(se, n, s) {
\r
10186 if (tinymce.is(f, 'string')) {
\r
10187 s = f.replace(/\.\w+$/, '');
\r
10188 s = s ? tinymce.resolve(s) : 0;
\r
10189 f = tinymce.resolve(f);
\r
10192 return f.apply(s || this, Array.prototype.slice.call(arguments, 2));
\r
10196 theme : "simple",
\r
10203 Event.add(document, 'init', function() {
\r
10206 execCallback(s, 'onpageload');
\r
10208 switch (s.mode) {
\r
10210 l = s.elements || '';
\r
10212 if(l.length > 0) {
\r
10213 each(explode(l), function(v) {
\r
10214 if (DOM.get(v)) {
\r
10215 ed = new tinymce.Editor(v, s);
\r
10219 each(document.forms, function(f) {
\r
10220 each(f.elements, function(e) {
\r
10221 if (e.name === v) {
\r
10222 v = 'mce_editor_' + instanceCounter++;
\r
10223 DOM.setAttrib(e, 'id', v);
\r
10225 ed = new tinymce.Editor(v, s);
\r
10236 case "textareas":
\r
10237 case "specific_textareas":
\r
10238 function hasClass(n, c) {
\r
10239 return c.constructor === RegExp ? c.test(n.className) : DOM.hasClass(n, c);
\r
10242 each(DOM.select('textarea'), function(v) {
\r
10243 if (s.editor_deselector && hasClass(v, s.editor_deselector))
\r
10246 if (!s.editor_selector || hasClass(v, s.editor_selector)) {
\r
10247 // Can we use the name
\r
10248 e = DOM.get(v.name);
\r
10252 // Generate unique name if missing or already exists
\r
10253 if (!v.id || t.get(v.id))
\r
10254 v.id = DOM.uniqueId();
\r
10256 ed = new tinymce.Editor(v.id, s);
\r
10264 // Call onInit when all editors are initialized
\r
10268 each(el, function(ed) {
\r
10271 if (!ed.initialized) {
\r
10273 ed.onInit.add(function() {
\r
10278 execCallback(s, 'oninit');
\r
10285 execCallback(s, 'oninit');
\r
10291 get : function(id) {
\r
10292 if (id === undefined)
\r
10293 return this.editors;
\r
10295 return this.editors[id];
\r
10298 getInstanceById : function(id) {
\r
10299 return this.get(id);
\r
10302 add : function(editor) {
\r
10303 var self = this, editors = self.editors;
\r
10305 // Add named and index editor instance
\r
10306 editors[editor.id] = editor;
\r
10307 editors.push(editor);
\r
10309 self._setActive(editor);
\r
10310 self.onAddEditor.dispatch(self, editor);
\r
10316 remove : function(editor) {
\r
10317 var t = this, i, editors = t.editors;
\r
10319 // Not in the collection
\r
10320 if (!editors[editor.id])
\r
10323 delete editors[editor.id];
\r
10325 for (i = 0; i < editors.length; i++) {
\r
10326 if (editors[i] == editor) {
\r
10327 editors.splice(i, 1);
\r
10332 // Select another editor since the active one was removed
\r
10333 if (t.activeEditor == editor)
\r
10334 t._setActive(editors[0]);
\r
10336 editor.destroy();
\r
10337 t.onRemoveEditor.dispatch(t, editor);
\r
10342 execCommand : function(c, u, v) {
\r
10343 var t = this, ed = t.get(v), w;
\r
10345 // Manager commands
\r
10351 case "mceAddEditor":
\r
10352 case "mceAddControl":
\r
10354 new tinymce.Editor(v, t.settings).render();
\r
10358 case "mceAddFrameControl":
\r
10361 // Add tinyMCE global instance and tinymce namespace to specified window
\r
10362 w.tinyMCE = tinyMCE;
\r
10363 w.tinymce = tinymce;
\r
10365 tinymce.DOM.doc = w.document;
\r
10366 tinymce.DOM.win = w;
\r
10368 ed = new tinymce.Editor(v.element_id, v);
\r
10371 // Fix IE memory leaks
\r
10372 if (tinymce.isIE) {
\r
10375 w.detachEvent('onunload', clr);
\r
10376 w = w.tinyMCE = w.tinymce = null; // IE leak
\r
10379 w.attachEvent('onunload', clr);
\r
10382 v.page_window = null;
\r
10386 case "mceRemoveEditor":
\r
10387 case "mceRemoveControl":
\r
10393 case 'mceToggleEditor':
\r
10395 t.execCommand('mceAddControl', 0, v);
\r
10399 if (ed.isHidden())
\r
10407 // Run command on active editor
\r
10408 if (t.activeEditor)
\r
10409 return t.activeEditor.execCommand(c, u, v);
\r
10414 execInstanceCommand : function(id, c, u, v) {
\r
10415 var ed = this.get(id);
\r
10418 return ed.execCommand(c, u, v);
\r
10423 triggerSave : function() {
\r
10424 each(this.editors, function(e) {
\r
10429 addI18n : function(p, o) {
\r
10430 var lo, i18n = this.i18n;
\r
10432 if (!tinymce.is(p, 'string')) {
\r
10433 each(p, function(o, lc) {
\r
10434 each(o, function(o, g) {
\r
10435 each(o, function(o, k) {
\r
10436 if (g === 'common')
\r
10437 i18n[lc + '.' + k] = o;
\r
10439 i18n[lc + '.' + g + '.' + k] = o;
\r
10444 each(o, function(o, k) {
\r
10445 i18n[p + '.' + k] = o;
\r
10450 // Private methods
\r
10452 _setActive : function(editor) {
\r
10453 this.selectedInstance = this.activeEditor = editor;
\r
10458 (function(tinymce) {
\r
10459 // Shorten these names
\r
10460 var DOM = tinymce.DOM, Event = tinymce.dom.Event, extend = tinymce.extend,
\r
10461 Dispatcher = tinymce.util.Dispatcher, each = tinymce.each, isGecko = tinymce.isGecko,
\r
10462 isIE = tinymce.isIE, isWebKit = tinymce.isWebKit, is = tinymce.is,
\r
10463 ThemeManager = tinymce.ThemeManager, PluginManager = tinymce.PluginManager,
\r
10464 inArray = tinymce.inArray, grep = tinymce.grep, explode = tinymce.explode;
\r
10466 tinymce.create('tinymce.Editor', {
\r
10467 Editor : function(id, s) {
\r
10470 t.id = t.editorId = id;
\r
10472 t.execCommands = {};
\r
10473 t.queryStateCommands = {};
\r
10474 t.queryValueCommands = {};
\r
10476 t.isNotDirty = false;
\r
10480 // Add events to the editor
\r
10484 'onBeforeRenderUI',
\r
10524 'onBeforeSetContent',
\r
10526 'onBeforeGetContent',
\r
10540 'onBeforeExecCommand',
\r
10550 'onSetProgressState'
\r
10552 t[e] = new Dispatcher(t);
\r
10555 t.settings = s = extend({
\r
10558 docs_language : 'en',
\r
10559 theme : 'simple',
\r
10560 skin : 'default',
\r
10562 delta_height : 0,
\r
10565 document_base_url : tinymce.documentBaseURL,
\r
10566 add_form_submit_trigger : 1,
\r
10567 submit_patch : 1,
\r
10568 add_unload_trigger : 1,
\r
10569 convert_urls : 1,
\r
10570 relative_urls : 1,
\r
10571 remove_script_host : 1,
\r
10572 table_inline_editing : 0,
\r
10573 object_resizing : 1,
\r
10575 accessibility_focus : 1,
\r
10576 custom_shortcuts : 1,
\r
10577 custom_undo_redo_keyboard_shortcuts : 1,
\r
10578 custom_undo_redo_restore_selection : 1,
\r
10579 custom_undo_redo : 1,
\r
10580 doctype : tinymce.isIE6 ? '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">' : '<!DOCTYPE>', // Use old doctype on IE 6 to avoid horizontal scroll
\r
10581 visual_table_class : 'mceItemTable',
\r
10583 font_size_style_values : 'xx-small,x-small,small,medium,large,x-large,xx-large',
\r
10584 apply_source_formatting : 1,
\r
10585 directionality : 'ltr',
\r
10586 forced_root_block : 'p',
\r
10587 hidden_input : 1,
\r
10588 padd_empty_editor : 1,
\r
10591 force_p_newlines : 1,
\r
10592 indentation : '30px',
\r
10594 fix_table_elements : 1,
\r
10595 inline_styles : 1,
\r
10596 convert_fonts_to_spans : true,
\r
10597 indent : 'simple',
\r
10598 indent_before : 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,tfoot,tbody,tr',
\r
10599 indent_after : 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,tfoot,tbody,tr',
\r
10601 entity_encoding : 'named',
\r
10602 url_converter : t.convertURL,
\r
10603 url_converter_scope : t,
\r
10604 ie7_compat : true
\r
10607 t.documentBaseURI = new tinymce.util.URI(s.document_base_url || tinymce.documentBaseURL, {
\r
10608 base_uri : tinyMCE.baseURI
\r
10611 t.baseURI = tinymce.baseURI;
\r
10613 t.contentCSS = [];
\r
10616 t.execCallback('setup', t);
\r
10619 render : function(nst) {
\r
10620 var t = this, s = t.settings, id = t.id, sl = tinymce.ScriptLoader;
\r
10622 // Page is not loaded yet, wait for it
\r
10623 if (!Event.domLoaded) {
\r
10624 Event.add(document, 'init', function() {
\r
10630 tinyMCE.settings = s;
\r
10632 // Element not found, then skip initialization
\r
10633 if (!t.getElement())
\r
10636 // Is a iPad/iPhone, then skip initialization. We need to sniff here since the
\r
10637 // browser says it has contentEditable support but there is no visible caret
\r
10638 // We will remove this check ones Apple implements full contentEditable support
\r
10639 if (tinymce.isIDevice)
\r
10642 // Add hidden input for non input elements inside form elements
\r
10643 if (!/TEXTAREA|INPUT/i.test(t.getElement().nodeName) && s.hidden_input && DOM.getParent(id, 'form'))
\r
10644 DOM.insertAfter(DOM.create('input', {type : 'hidden', name : id}), id);
\r
10646 if (tinymce.WindowManager)
\r
10647 t.windowManager = new tinymce.WindowManager(t);
\r
10649 if (s.encoding == 'xml') {
\r
10650 t.onGetContent.add(function(ed, o) {
\r
10652 o.content = DOM.encode(o.content);
\r
10656 if (s.add_form_submit_trigger) {
\r
10657 t.onSubmit.addToTop(function() {
\r
10658 if (t.initialized) {
\r
10660 t.isNotDirty = 1;
\r
10665 if (s.add_unload_trigger) {
\r
10666 t._beforeUnload = tinyMCE.onBeforeUnload.add(function() {
\r
10667 if (t.initialized && !t.destroyed && !t.isHidden())
\r
10668 t.save({format : 'raw', no_events : true});
\r
10672 tinymce.addUnload(t.destroy, t);
\r
10674 if (s.submit_patch) {
\r
10675 t.onBeforeRenderUI.add(function() {
\r
10676 var n = t.getElement().form;
\r
10681 // Already patched
\r
10682 if (n._mceOldSubmit)
\r
10685 // Check page uses id="submit" or name="submit" for it's submit button
\r
10686 if (!n.submit.nodeType && !n.submit.length) {
\r
10687 t.formElement = n;
\r
10688 n._mceOldSubmit = n.submit;
\r
10689 n.submit = function() {
\r
10690 // Save all instances
\r
10691 tinymce.triggerSave();
\r
10692 t.isNotDirty = 1;
\r
10694 return t.formElement._mceOldSubmit(t.formElement);
\r
10703 function loadScripts() {
\r
10704 if (s.language && s.language_load !== false)
\r
10705 sl.add(tinymce.baseURL + '/langs/' + s.language + '.js');
\r
10707 if (s.theme && s.theme.charAt(0) != '-' && !ThemeManager.urls[s.theme])
\r
10708 ThemeManager.load(s.theme, 'themes/' + s.theme + '/editor_template' + tinymce.suffix + '.js');
\r
10710 each(explode(s.plugins), function(p) {
\r
10711 if (p && p.charAt(0) != '-' && !PluginManager.urls[p]) {
\r
10712 // Skip safari plugin, since it is removed as of 3.3b1
\r
10713 if (p == 'safari')
\r
10716 PluginManager.load(p, 'plugins/' + p + '/editor_plugin' + tinymce.suffix + '.js');
\r
10720 // Init when que is loaded
\r
10721 sl.loadQueue(function() {
\r
10730 init : function() {
\r
10731 var n, t = this, s = t.settings, w, h, e = t.getElement(), o, ti, u, bi, bc, re, i;
\r
10735 s.aria_label = s.aria_label || DOM.getAttrib(e, 'aria-label', t.getLang('aria.rich_text_area'));
\r
10738 s.theme = s.theme.replace(/-/, '');
\r
10739 o = ThemeManager.get(s.theme);
\r
10740 t.theme = new o();
\r
10742 if (t.theme.init && s.init_theme)
\r
10743 t.theme.init(t, ThemeManager.urls[s.theme] || tinymce.documentBaseURL.replace(/\/$/, ''));
\r
10746 // Create all plugins
\r
10747 each(explode(s.plugins.replace(/\-/g, '')), function(p) {
\r
10748 var c = PluginManager.get(p), u = PluginManager.urls[p] || tinymce.documentBaseURL.replace(/\/$/, ''), po;
\r
10751 po = new c(t, u);
\r
10753 t.plugins[p] = po;
\r
10760 // Setup popup CSS path(s)
\r
10761 if (s.popup_css !== false) {
\r
10763 s.popup_css = t.documentBaseURI.toAbsolute(s.popup_css);
\r
10765 s.popup_css = t.baseURI.toAbsolute("themes/" + s.theme + "/skins/" + s.skin + "/dialog.css");
\r
10768 if (s.popup_css_add)
\r
10769 s.popup_css += ',' + t.documentBaseURI.toAbsolute(s.popup_css_add);
\r
10771 t.controlManager = new tinymce.ControlManager(t);
\r
10773 if (s.custom_undo_redo) {
\r
10774 t.onBeforeExecCommand.add(function(ed, cmd, ui, val, a) {
\r
10775 if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint' && (!a || !a.skip_undo))
\r
10776 t.undoManager.beforeChange();
\r
10779 t.onExecCommand.add(function(ed, cmd, ui, val, a) {
\r
10780 if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint' && (!a || !a.skip_undo))
\r
10781 t.undoManager.add();
\r
10785 t.onExecCommand.add(function(ed, c) {
\r
10786 // Don't refresh the select lists until caret move
\r
10787 if (!/^(FontName|FontSize)$/.test(c))
\r
10791 // Remove ghost selections on images and tables in Gecko
\r
10793 function repaint(a, o) {
\r
10794 if (!o || !o.initial)
\r
10795 t.execCommand('mceRepaint');
\r
10798 t.onUndo.add(repaint);
\r
10799 t.onRedo.add(repaint);
\r
10800 t.onSetContent.add(repaint);
\r
10803 // Enables users to override the control factory
\r
10804 t.onBeforeRenderUI.dispatch(t, t.controlManager);
\r
10807 if (s.render_ui) {
\r
10808 w = s.width || e.style.width || e.offsetWidth;
\r
10809 h = s.height || e.style.height || e.offsetHeight;
\r
10810 t.orgDisplay = e.style.display;
\r
10811 re = /^[0-9\.]+(|px)$/i;
\r
10813 if (re.test('' + w))
\r
10814 w = Math.max(parseInt(w) + (o.deltaWidth || 0), 100);
\r
10816 if (re.test('' + h))
\r
10817 h = Math.max(parseInt(h) + (o.deltaHeight || 0), 100);
\r
10820 o = t.theme.renderUI({
\r
10824 deltaWidth : s.delta_width,
\r
10825 deltaHeight : s.delta_height
\r
10828 t.editorContainer = o.editorContainer;
\r
10832 // User specified a document.domain value
\r
10833 if (document.domain && location.hostname != document.domain)
\r
10834 tinymce.relaxedDomain = document.domain;
\r
10837 DOM.setStyles(o.sizeContainer || o.editorContainer, {
\r
10842 // Load specified content CSS last
\r
10843 if (s.content_css) {
\r
10844 tinymce.each(explode(s.content_css), function(u) {
\r
10845 t.contentCSS.push(t.documentBaseURI.toAbsolute(u));
\r
10849 h = (o.iframeHeight || h) + (typeof(h) == 'number' ? (o.deltaHeight || 0) : '');
\r
10853 t.iframeHTML = s.doctype + '<html><head xmlns="http://www.w3.org/1999/xhtml">';
\r
10855 // We only need to override paths if we have to
\r
10856 // IE has a bug where it remove site absolute urls to relative ones if this is specified
\r
10857 if (s.document_base_url != tinymce.documentBaseURL)
\r
10858 t.iframeHTML += '<base href="' + t.documentBaseURI.getURI() + '" />';
\r
10860 // IE8 doesn't support carets behind images setting ie7_compat would force IE8+ to run in IE7 compat mode.
\r
10861 if (s.ie7_compat)
\r
10862 t.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=7" />';
\r
10864 t.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=edge" />';
\r
10866 t.iframeHTML += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />';
\r
10868 // Firefox 2 doesn't load stylesheets correctly this way
\r
10869 if (!isGecko || !/Firefox\/2/.test(navigator.userAgent)) {
\r
10870 for (i = 0; i < t.contentCSS.length; i++)
\r
10871 t.iframeHTML += '<link type="text/css" rel="stylesheet" href="' + t.contentCSS[i] + '" />';
\r
10873 t.contentCSS = [];
\r
10876 bi = s.body_id || 'tinymce';
\r
10877 if (bi.indexOf('=') != -1) {
\r
10878 bi = t.getParam('body_id', '', 'hash');
\r
10879 bi = bi[t.id] || bi;
\r
10882 bc = s.body_class || '';
\r
10883 if (bc.indexOf('=') != -1) {
\r
10884 bc = t.getParam('body_class', '', 'hash');
\r
10885 bc = bc[t.id] || '';
\r
10888 t.iframeHTML += '</head><body id="' + bi + '" class="mceContentBody ' + bc + '"></body></html>';
\r
10890 // Domain relaxing enabled, then set document domain
\r
10891 if (tinymce.relaxedDomain && (isIE || (tinymce.isOpera && parseFloat(opera.version()) < 11))) {
\r
10892 // We need to write the contents here in IE since multiple writes messes up refresh button and back button
\r
10893 u = 'javascript:(function(){document.open();document.domain="' + document.domain + '";var ed = window.parent.tinyMCE.get("' + t.id + '");document.write(ed.iframeHTML);document.close();ed.setupIframe();})()';
\r
10897 // TODO: ACC add the appropriate description on this.
\r
10898 n = DOM.add(o.iframeContainer, 'iframe', {
\r
10899 id : t.id + "_ifr",
\r
10900 src : u || 'javascript:""', // Workaround for HTTPS warning in IE6/7
\r
10901 frameBorder : '0',
\r
10902 title : s.aria_label,
\r
10909 t.contentAreaContainer = o.iframeContainer;
\r
10910 DOM.get(o.editorContainer).style.display = t.orgDisplay;
\r
10911 DOM.get(t.id).style.display = 'none';
\r
10912 DOM.setAttrib(t.id, 'aria-hidden', true);
\r
10914 if (!tinymce.relaxedDomain || !u)
\r
10917 e = n = o = null; // Cleanup
\r
10920 setupIframe : function() {
\r
10921 var t = this, s = t.settings, e = DOM.get(t.id), d = t.getDoc(), h, b;
\r
10923 // Setup iframe body
\r
10924 if (!isIE || !tinymce.relaxedDomain) {
\r
10926 d.write(t.iframeHTML);
\r
10929 if (tinymce.relaxedDomain)
\r
10930 d.domain = tinymce.relaxedDomain;
\r
10933 // Design mode needs to be added here Ctrl+A will fail otherwise
\r
10937 d.designMode = 'On';
\r
10939 // Will fail on Gecko if the editor is placed in an hidden container element
\r
10940 // The design mode will be set ones the editor is focused
\r
10944 // IE needs to use contentEditable or it will display non secure items for HTTPS
\r
10946 // It will not steal focus if we hide it while setting contentEditable
\r
10951 b.contentEditable = true;
\r
10956 t.schema = new tinymce.html.Schema(s);
\r
10958 t.dom = new tinymce.dom.DOMUtils(t.getDoc(), {
\r
10959 keep_values : true,
\r
10960 url_converter : t.convertURL,
\r
10961 url_converter_scope : t,
\r
10962 hex_colors : s.force_hex_style_colors,
\r
10963 class_filter : s.class_filter,
\r
10964 update_styles : 1,
\r
10965 fix_ie_paragraphs : 1,
\r
10966 schema : t.schema
\r
10969 t.parser = new tinymce.html.DomParser(s, t.schema);
\r
10971 // Force anchor names closed
\r
10972 t.parser.addAttributeFilter('name', function(nodes, name) {
\r
10973 var i = nodes.length, sibling, prevSibling, parent, node;
\r
10977 if (node.name === 'a' && node.firstChild) {
\r
10978 parent = node.parent;
\r
10980 // Move children after current node
\r
10981 sibling = node.lastChild;
\r
10983 prevSibling = sibling.prev;
\r
10984 parent.insert(sibling, node);
\r
10985 sibling = prevSibling;
\r
10986 } while (sibling);
\r
10991 // Convert src and href into data-mce-src, data-mce-href and data-mce-style
\r
10992 t.parser.addAttributeFilter('src,href,style', function(nodes, name) {
\r
10993 var i = nodes.length, node, dom = t.dom, value;
\r
10997 value = node.attr(name);
\r
10999 if (name === "style")
\r
11000 node.attr('data-mce-style', dom.serializeStyle(dom.parseStyle(value), node.name));
\r
11002 node.attr('data-mce-' + name, t.convertURL(value, name, node.name));
\r
11006 // Keep scripts from executing
\r
11007 t.parser.addNodeFilter('script', function(nodes, name) {
\r
11008 var i = nodes.length;
\r
11011 nodes[i].attr('type', 'mce-text/javascript');
\r
11014 t.parser.addNodeFilter('#cdata', function(nodes, name) {
\r
11015 var i = nodes.length, node;
\r
11020 node.name = '#comment';
\r
11021 node.value = '[CDATA[' + node.value + ']]';
\r
11025 t.parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', function(nodes, name) {
\r
11026 var i = nodes.length, node, nonEmptyElements = t.schema.getNonEmptyElements();
\r
11031 if (node.isEmpty(nonEmptyElements))
\r
11032 node.empty().append(new tinymce.html.Node('br', 1)).shortEnded = true;
\r
11036 t.serializer = new tinymce.dom.Serializer(s, t.dom, t.schema);
\r
11038 t.selection = new tinymce.dom.Selection(t.dom, t.getWin(), t.serializer);
\r
11040 t.formatter = new tinymce.Formatter(this);
\r
11042 // Register default formats
\r
11043 t.formatter.register({
\r
11045 {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'left'}},
\r
11046 {selector : 'img,table', collapsed : false, styles : {'float' : 'left'}}
\r
11050 {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'center'}},
\r
11051 {selector : 'img', collapsed : false, styles : {display : 'block', marginLeft : 'auto', marginRight : 'auto'}},
\r
11052 {selector : 'table', collapsed : false, styles : {marginLeft : 'auto', marginRight : 'auto'}}
\r
11056 {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'right'}},
\r
11057 {selector : 'img,table', collapsed : false, styles : {'float' : 'right'}}
\r
11061 {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'justify'}}
\r
11065 {inline : 'strong', remove : 'all'},
\r
11066 {inline : 'span', styles : {fontWeight : 'bold'}},
\r
11067 {inline : 'b', remove : 'all'}
\r
11071 {inline : 'em', remove : 'all'},
\r
11072 {inline : 'span', styles : {fontStyle : 'italic'}},
\r
11073 {inline : 'i', remove : 'all'}
\r
11077 {inline : 'span', styles : {textDecoration : 'underline'}, exact : true},
\r
11078 {inline : 'u', remove : 'all'}
\r
11081 strikethrough : [
\r
11082 {inline : 'span', styles : {textDecoration : 'line-through'}, exact : true},
\r
11083 {inline : 'strike', remove : 'all'}
\r
11086 forecolor : {inline : 'span', styles : {color : '%value'}, wrap_links : false},
\r
11087 hilitecolor : {inline : 'span', styles : {backgroundColor : '%value'}, wrap_links : false},
\r
11088 fontname : {inline : 'span', styles : {fontFamily : '%value'}},
\r
11089 fontsize : {inline : 'span', styles : {fontSize : '%value'}},
\r
11090 fontsize_class : {inline : 'span', attributes : {'class' : '%value'}},
\r
11091 blockquote : {block : 'blockquote', wrapper : 1, remove : 'all'},
\r
11092 subscript : {inline : 'sub'},
\r
11093 superscript : {inline : 'sup'},
\r
11096 {selector : 'b,strong,em,i,font,u,strike', remove : 'all', split : true, expand : false, block_expand : true, deep : true},
\r
11097 {selector : 'span', attributes : ['style', 'class'], remove : 'empty', split : true, expand : false, deep : true},
\r
11098 {selector : '*', attributes : ['style', 'class'], split : false, expand : false, deep : true}
\r
11102 // Register default block formats
\r
11103 each('p h1 h2 h3 h4 h5 h6 div address pre div code dt dd samp'.split(/\s/), function(name) {
\r
11104 t.formatter.register(name, {block : name, remove : 'all'});
\r
11107 // Register user defined formats
\r
11108 t.formatter.register(t.settings.formats);
\r
11110 t.undoManager = new tinymce.UndoManager(t);
\r
11113 t.undoManager.onAdd.add(function(um, l) {
\r
11114 if (um.hasUndo())
\r
11115 return t.onChange.dispatch(t, l, um);
\r
11118 t.undoManager.onUndo.add(function(um, l) {
\r
11119 return t.onUndo.dispatch(t, l, um);
\r
11122 t.undoManager.onRedo.add(function(um, l) {
\r
11123 return t.onRedo.dispatch(t, l, um);
\r
11126 t.forceBlocks = new tinymce.ForceBlocks(t, {
\r
11127 forced_root_block : s.forced_root_block
\r
11130 t.editorCommands = new tinymce.EditorCommands(t);
\r
11133 t.serializer.onPreProcess.add(function(se, o) {
\r
11134 return t.onPreProcess.dispatch(t, o, se);
\r
11137 t.serializer.onPostProcess.add(function(se, o) {
\r
11138 return t.onPostProcess.dispatch(t, o, se);
\r
11141 t.onPreInit.dispatch(t);
\r
11143 if (!s.gecko_spellcheck)
\r
11144 t.getBody().spellcheck = 0;
\r
11149 t.controlManager.onPostRender.dispatch(t, t.controlManager);
\r
11150 t.onPostRender.dispatch(t);
\r
11152 if (s.directionality)
\r
11153 t.getBody().dir = s.directionality;
\r
11156 t.getBody().style.whiteSpace = "nowrap";
\r
11158 if (s.handle_node_change_callback) {
\r
11159 t.onNodeChange.add(function(ed, cm, n) {
\r
11160 t.execCallback('handle_node_change_callback', t.id, n, -1, -1, true, t.selection.isCollapsed());
\r
11164 if (s.save_callback) {
\r
11165 t.onSaveContent.add(function(ed, o) {
\r
11166 var h = t.execCallback('save_callback', t.id, o.content, t.getBody());
\r
11173 if (s.onchange_callback) {
\r
11174 t.onChange.add(function(ed, l) {
\r
11175 t.execCallback('onchange_callback', t, l);
\r
11180 t.onBeforeSetContent.add(function(ed, o) {
\r
11182 each(s.protect, function(pattern) {
\r
11183 o.content = o.content.replace(pattern, function(str) {
\r
11184 return '<!--mce:protected ' + escape(str) + '-->';
\r
11191 if (s.convert_newlines_to_brs) {
\r
11192 t.onBeforeSetContent.add(function(ed, o) {
\r
11194 o.content = o.content.replace(/\r?\n/g, '<br />');
\r
11198 if (s.preformatted) {
\r
11199 t.onPostProcess.add(function(ed, o) {
\r
11200 o.content = o.content.replace(/^\s*<pre.*?>/, '');
\r
11201 o.content = o.content.replace(/<\/pre>\s*$/, '');
\r
11204 o.content = '<pre class="mceItemHidden">' + o.content + '</pre>';
\r
11208 if (s.verify_css_classes) {
\r
11209 t.serializer.attribValueFilter = function(n, v) {
\r
11212 if (n == 'class') {
\r
11213 // Build regexp for classes
\r
11214 if (!t.classesRE) {
\r
11215 cl = t.dom.getClasses();
\r
11217 if (cl.length > 0) {
\r
11220 each (cl, function(o) {
\r
11221 s += (s ? '|' : '') + o['class'];
\r
11224 t.classesRE = new RegExp('(' + s + ')', 'gi');
\r
11228 return !t.classesRE || /(\bmceItem\w+\b|\bmceTemp\w+\b)/g.test(v) || t.classesRE.test(v) ? v : '';
\r
11235 if (s.cleanup_callback) {
\r
11236 t.onBeforeSetContent.add(function(ed, o) {
\r
11237 o.content = t.execCallback('cleanup_callback', 'insert_to_editor', o.content, o);
\r
11240 t.onPreProcess.add(function(ed, o) {
\r
11242 t.execCallback('cleanup_callback', 'insert_to_editor_dom', o.node, o);
\r
11245 t.execCallback('cleanup_callback', 'get_from_editor_dom', o.node, o);
\r
11248 t.onPostProcess.add(function(ed, o) {
\r
11250 o.content = t.execCallback('cleanup_callback', 'insert_to_editor', o.content, o);
\r
11253 o.content = t.execCallback('cleanup_callback', 'get_from_editor', o.content, o);
\r
11257 if (s.save_callback) {
\r
11258 t.onGetContent.add(function(ed, o) {
\r
11260 o.content = t.execCallback('save_callback', t.id, o.content, t.getBody());
\r
11264 if (s.handle_event_callback) {
\r
11265 t.onEvent.add(function(ed, e, o) {
\r
11266 if (t.execCallback('handle_event_callback', e, ed, o) === false)
\r
11271 // Add visual aids when new contents is added
\r
11272 t.onSetContent.add(function() {
\r
11273 t.addVisual(t.getBody());
\r
11276 // Remove empty contents
\r
11277 if (s.padd_empty_editor) {
\r
11278 t.onPostProcess.add(function(ed, o) {
\r
11279 o.content = o.content.replace(/^(<p[^>]*>( | |\s|\u00a0|)<\/p>[\r\n]*|<br \/>[\r\n]*)$/, '');
\r
11284 // Fix gecko link bug, when a link is placed at the end of block elements there is
\r
11285 // no way to move the caret behind the link. This fix adds a bogus br element after the link
\r
11286 function fixLinks(ed, o) {
\r
11287 each(ed.dom.select('a'), function(n) {
\r
11288 var pn = n.parentNode;
\r
11290 if (ed.dom.isBlock(pn) && pn.lastChild === n)
\r
11291 ed.dom.add(pn, 'br', {'data-mce-bogus' : 1});
\r
11295 t.onExecCommand.add(function(ed, cmd) {
\r
11296 if (cmd === 'CreateLink')
\r
11300 t.onSetContent.add(t.selection.onSetContent.add(fixLinks));
\r
11302 if (!s.readonly) {
\r
11304 // Design mode must be set here once again to fix a bug where
\r
11305 // Ctrl+A/Delete/Backspace didn't work if the editor was added using mceAddControl then removed then added again
\r
11306 d.designMode = 'Off';
\r
11307 d.designMode = 'On';
\r
11309 // Will fail on Gecko if the editor is placed in an hidden container element
\r
11310 // The design mode will be set ones the editor is focused
\r
11315 // A small timeout was needed since firefox will remove. Bug: #1838304
\r
11316 setTimeout(function () {
\r
11320 t.load({initial : true, format : 'html'});
\r
11321 t.startContent = t.getContent({format : 'raw'});
\r
11322 t.undoManager.add();
\r
11323 t.initialized = true;
\r
11325 t.onInit.dispatch(t);
\r
11326 t.execCallback('setupcontent_callback', t.id, t.getBody(), t.getDoc());
\r
11327 t.execCallback('init_instance_callback', t);
\r
11329 t.nodeChanged({initial : 1});
\r
11331 // Load specified content CSS last
\r
11332 each(t.contentCSS, function(u) {
\r
11333 t.dom.loadCSS(u);
\r
11336 // Handle auto focus
\r
11337 if (s.auto_focus) {
\r
11338 setTimeout(function () {
\r
11339 var ed = tinymce.get(s.auto_focus);
\r
11341 ed.selection.select(ed.getBody(), 1);
\r
11342 ed.selection.collapse(1);
\r
11343 ed.getWin().focus();
\r
11352 focus : function(sf) {
\r
11353 var oed, t = this, ce = t.settings.content_editable, ieRng, controlElm, doc = t.getDoc();
\r
11356 // Get selected control element
\r
11357 ieRng = t.selection.getRng();
\r
11358 if (ieRng.item) {
\r
11359 controlElm = ieRng.item(0);
\r
11362 // Is not content editable
\r
11364 t.getWin().focus();
\r
11366 // Restore selected control element
\r
11367 // This is needed when for example an image is selected within a
\r
11368 // layer a call to focus will then remove the control selection
\r
11369 if (controlElm && controlElm.ownerDocument == doc) {
\r
11370 ieRng = doc.body.createControlRange();
\r
11371 ieRng.addElement(controlElm);
\r
11377 if (tinymce.activeEditor != t) {
\r
11378 if ((oed = tinymce.activeEditor) != null)
\r
11379 oed.onDeactivate.dispatch(oed, t);
\r
11381 t.onActivate.dispatch(t, oed);
\r
11384 tinymce._setActive(t);
\r
11387 execCallback : function(n) {
\r
11388 var t = this, f = t.settings[n], s;
\r
11393 // Look through lookup
\r
11394 if (t.callbackLookup && (s = t.callbackLookup[n])) {
\r
11399 if (is(f, 'string')) {
\r
11400 s = f.replace(/\.\w+$/, '');
\r
11401 s = s ? tinymce.resolve(s) : 0;
\r
11402 f = tinymce.resolve(f);
\r
11403 t.callbackLookup = t.callbackLookup || {};
\r
11404 t.callbackLookup[n] = {func : f, scope : s};
\r
11407 return f.apply(s || t, Array.prototype.slice.call(arguments, 1));
\r
11410 translate : function(s) {
\r
11411 var c = this.settings.language || 'en', i18n = tinymce.i18n;
\r
11416 return i18n[c + '.' + s] || s.replace(/{\#([^}]+)\}/g, function(a, b) {
\r
11417 return i18n[c + '.' + b] || '{#' + b + '}';
\r
11421 getLang : function(n, dv) {
\r
11422 return tinymce.i18n[(this.settings.language || 'en') + '.' + n] || (is(dv) ? dv : '{#' + n + '}');
\r
11425 getParam : function(n, dv, ty) {
\r
11426 var tr = tinymce.trim, v = is(this.settings[n]) ? this.settings[n] : dv, o;
\r
11428 if (ty === 'hash') {
\r
11431 if (is(v, 'string')) {
\r
11432 each(v.indexOf('=') > 0 ? v.split(/[;,](?![^=;,]*(?:[;,]|$))/) : v.split(','), function(v) {
\r
11433 v = v.split('=');
\r
11435 if (v.length > 1)
\r
11436 o[tr(v[0])] = tr(v[1]);
\r
11438 o[tr(v[0])] = tr(v);
\r
11449 nodeChanged : function(o) {
\r
11450 var t = this, s = t.selection, n = s.getStart() || t.getBody();
\r
11452 // Fix for bug #1896577 it seems that this can not be fired while the editor is loading
\r
11453 if (t.initialized) {
\r
11455 n = isIE && n.ownerDocument != t.getDoc() ? t.getBody() : n; // Fix for IE initial state
\r
11457 // Get parents and add them to object
\r
11459 t.dom.getParent(n, function(node) {
\r
11460 if (node.nodeName == 'BODY')
\r
11463 o.parents.push(node);
\r
11466 t.onNodeChange.dispatch(
\r
11468 o ? o.controlManager || t.controlManager : t.controlManager,
\r
11476 addButton : function(n, s) {
\r
11479 t.buttons = t.buttons || {};
\r
11480 t.buttons[n] = s;
\r
11483 addCommand : function(name, callback, scope) {
\r
11484 this.execCommands[name] = {func : callback, scope : scope || this};
\r
11487 addQueryStateHandler : function(name, callback, scope) {
\r
11488 this.queryStateCommands[name] = {func : callback, scope : scope || this};
\r
11491 addQueryValueHandler : function(name, callback, scope) {
\r
11492 this.queryValueCommands[name] = {func : callback, scope : scope || this};
\r
11495 addShortcut : function(pa, desc, cmd_func, sc) {
\r
11498 if (!t.settings.custom_shortcuts)
\r
11501 t.shortcuts = t.shortcuts || {};
\r
11503 if (is(cmd_func, 'string')) {
\r
11506 cmd_func = function() {
\r
11507 t.execCommand(c, false, null);
\r
11511 if (is(cmd_func, 'object')) {
\r
11514 cmd_func = function() {
\r
11515 t.execCommand(c[0], c[1], c[2]);
\r
11519 each(explode(pa), function(pa) {
\r
11522 scope : sc || this,
\r
11529 each(explode(pa, '+'), function(v) {
\r
11538 o.charCode = v.charCodeAt(0);
\r
11539 o.keyCode = v.toUpperCase().charCodeAt(0);
\r
11543 t.shortcuts[(o.ctrl ? 'ctrl' : '') + ',' + (o.alt ? 'alt' : '') + ',' + (o.shift ? 'shift' : '') + ',' + o.keyCode] = o;
\r
11549 execCommand : function(cmd, ui, val, a) {
\r
11550 var t = this, s = 0, o, st;
\r
11552 if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint|SelectAll)$/.test(cmd) && (!a || !a.skip_focus))
\r
11556 t.onBeforeExecCommand.dispatch(t, cmd, ui, val, o);
\r
11560 // Command callback
\r
11561 if (t.execCallback('execcommand_callback', t.id, t.selection.getNode(), cmd, ui, val)) {
\r
11562 t.onExecCommand.dispatch(t, cmd, ui, val, a);
\r
11566 // Registred commands
\r
11567 if (o = t.execCommands[cmd]) {
\r
11568 st = o.func.call(o.scope, ui, val);
\r
11570 // Fall through on true
\r
11571 if (st !== true) {
\r
11572 t.onExecCommand.dispatch(t, cmd, ui, val, a);
\r
11577 // Plugin commands
\r
11578 each(t.plugins, function(p) {
\r
11579 if (p.execCommand && p.execCommand(cmd, ui, val)) {
\r
11580 t.onExecCommand.dispatch(t, cmd, ui, val, a);
\r
11589 // Theme commands
\r
11590 if (t.theme && t.theme.execCommand && t.theme.execCommand(cmd, ui, val)) {
\r
11591 t.onExecCommand.dispatch(t, cmd, ui, val, a);
\r
11595 // Editor commands
\r
11596 if (t.editorCommands.execCommand(cmd, ui, val)) {
\r
11597 t.onExecCommand.dispatch(t, cmd, ui, val, a);
\r
11601 // Browser commands
\r
11602 t.getDoc().execCommand(cmd, ui, val);
\r
11603 t.onExecCommand.dispatch(t, cmd, ui, val, a);
\r
11606 queryCommandState : function(cmd) {
\r
11607 var t = this, o, s;
\r
11609 // Is hidden then return undefined
\r
11610 if (t._isHidden())
\r
11613 // Registred commands
\r
11614 if (o = t.queryStateCommands[cmd]) {
\r
11615 s = o.func.call(o.scope);
\r
11617 // Fall though on true
\r
11622 // Registred commands
\r
11623 o = t.editorCommands.queryCommandState(cmd);
\r
11627 // Browser commands
\r
11629 return this.getDoc().queryCommandState(cmd);
\r
11631 // Fails sometimes see bug: 1896577
\r
11635 queryCommandValue : function(c) {
\r
11636 var t = this, o, s;
\r
11638 // Is hidden then return undefined
\r
11639 if (t._isHidden())
\r
11642 // Registred commands
\r
11643 if (o = t.queryValueCommands[c]) {
\r
11644 s = o.func.call(o.scope);
\r
11646 // Fall though on true
\r
11651 // Registred commands
\r
11652 o = t.editorCommands.queryCommandValue(c);
\r
11656 // Browser commands
\r
11658 return this.getDoc().queryCommandValue(c);
\r
11660 // Fails sometimes see bug: 1896577
\r
11664 show : function() {
\r
11667 DOM.show(t.getContainer());
\r
11672 hide : function() {
\r
11673 var t = this, d = t.getDoc();
\r
11675 // Fixed bug where IE has a blinking cursor left from the editor
\r
11677 d.execCommand('SelectAll');
\r
11679 // We must save before we hide so Safari doesn't crash
\r
11681 DOM.hide(t.getContainer());
\r
11682 DOM.setStyle(t.id, 'display', t.orgDisplay);
\r
11685 isHidden : function() {
\r
11686 return !DOM.isHidden(this.id);
\r
11689 setProgressState : function(b, ti, o) {
\r
11690 this.onSetProgressState.dispatch(this, b, ti, o);
\r
11695 load : function(o) {
\r
11696 var t = this, e = t.getElement(), h;
\r
11702 // Double encode existing entities in the value
\r
11703 h = t.setContent(is(e.value) ? e.value : e.innerHTML, o);
\r
11706 if (!o.no_events)
\r
11707 t.onLoadContent.dispatch(t, o);
\r
11709 o.element = e = null;
\r
11715 save : function(o) {
\r
11716 var t = this, e = t.getElement(), h, f;
\r
11718 if (!e || !t.initialized)
\r
11724 // Add undo level will trigger onchange event
\r
11725 if (!o.no_events) {
\r
11726 t.undoManager.typing = false;
\r
11727 t.undoManager.add();
\r
11731 h = o.content = t.getContent(o);
\r
11733 if (!o.no_events)
\r
11734 t.onSaveContent.dispatch(t, o);
\r
11738 if (!/TEXTAREA|INPUT/i.test(e.nodeName)) {
\r
11741 // Update hidden form element
\r
11742 if (f = DOM.getParent(t.id, 'form')) {
\r
11743 each(f.elements, function(e) {
\r
11744 if (e.name == t.id) {
\r
11753 o.element = e = null;
\r
11758 setContent : function(content, args) {
\r
11759 var self = this, rootNode, body = self.getBody();
\r
11761 // Setup args object
\r
11762 args = args || {};
\r
11763 args.format = args.format || 'html';
\r
11765 args.content = content;
\r
11767 // Do preprocessing
\r
11768 if (!args.no_events)
\r
11769 self.onBeforeSetContent.dispatch(self, args);
\r
11771 content = args.content;
\r
11773 // Padd empty content in Gecko and Safari. Commands will otherwise fail on the content
\r
11774 // It will also be impossible to place the caret in the editor unless there is a BR element present
\r
11775 if (!tinymce.isIE && (content.length === 0 || /^\s+$/.test(content))) {
\r
11776 body.innerHTML = '<br data-mce-bogus="1" />';
\r
11780 // Parse and serialize the html
\r
11781 if (args.format !== 'raw') {
\r
11782 content = new tinymce.html.Serializer({}, self.schema).serialize(
\r
11783 self.parser.parse(content)
\r
11787 // Set the new cleaned contents to the editor
\r
11788 args.content = tinymce.trim(content);
\r
11789 self.dom.setHTML(body, args.content);
\r
11791 // Do post processing
\r
11792 if (!args.no_events)
\r
11793 self.onSetContent.dispatch(self, args);
\r
11795 return args.content;
\r
11798 getContent : function(args) {
\r
11799 var self = this, content;
\r
11801 // Setup args object
\r
11802 args = args || {};
\r
11803 args.format = args.format || 'html';
\r
11806 // Do preprocessing
\r
11807 if (!args.no_events)
\r
11808 self.onBeforeGetContent.dispatch(self, args);
\r
11810 // Get raw contents or by default the cleaned contents
\r
11811 if (args.format == 'raw')
\r
11812 content = self.getBody().innerHTML;
\r
11814 content = self.serializer.serialize(self.getBody(), args);
\r
11816 args.content = tinymce.trim(content);
\r
11818 // Do post processing
\r
11819 if (!args.no_events)
\r
11820 self.onGetContent.dispatch(self, args);
\r
11822 return args.content;
\r
11825 isDirty : function() {
\r
11828 return tinymce.trim(self.startContent) != tinymce.trim(self.getContent({format : 'raw', no_events : 1})) && !self.isNotDirty;
\r
11831 getContainer : function() {
\r
11834 if (!t.container)
\r
11835 t.container = DOM.get(t.editorContainer || t.id + '_parent');
\r
11837 return t.container;
\r
11840 getContentAreaContainer : function() {
\r
11841 return this.contentAreaContainer;
\r
11844 getElement : function() {
\r
11845 return DOM.get(this.settings.content_element || this.id);
\r
11848 getWin : function() {
\r
11851 if (!t.contentWindow) {
\r
11852 e = DOM.get(t.id + "_ifr");
\r
11855 t.contentWindow = e.contentWindow;
\r
11858 return t.contentWindow;
\r
11861 getDoc : function() {
\r
11864 if (!t.contentDocument) {
\r
11868 t.contentDocument = w.document;
\r
11871 return t.contentDocument;
\r
11874 getBody : function() {
\r
11875 return this.bodyElement || this.getDoc().body;
\r
11878 convertURL : function(u, n, e) {
\r
11879 var t = this, s = t.settings;
\r
11881 // Use callback instead
\r
11882 if (s.urlconverter_callback)
\r
11883 return t.execCallback('urlconverter_callback', u, e, true, n);
\r
11885 // Don't convert link href since thats the CSS files that gets loaded into the editor also skip local file URLs
\r
11886 if (!s.convert_urls || (e && e.nodeName == 'LINK') || u.indexOf('file:') === 0)
\r
11889 // Convert to relative
\r
11890 if (s.relative_urls)
\r
11891 return t.documentBaseURI.toRelative(u);
\r
11893 // Convert to absolute
\r
11894 u = t.documentBaseURI.toAbsolute(u, s.remove_script_host);
\r
11899 addVisual : function(e) {
\r
11900 var t = this, s = t.settings;
\r
11902 e = e || t.getBody();
\r
11904 if (!is(t.hasVisual))
\r
11905 t.hasVisual = s.visual;
\r
11907 each(t.dom.select('table,a', e), function(e) {
\r
11910 switch (e.nodeName) {
\r
11912 v = t.dom.getAttrib(e, 'border');
\r
11914 if (!v || v == '0') {
\r
11916 t.dom.addClass(e, s.visual_table_class);
\r
11918 t.dom.removeClass(e, s.visual_table_class);
\r
11924 v = t.dom.getAttrib(e, 'name');
\r
11928 t.dom.addClass(e, 'mceItemAnchor');
\r
11930 t.dom.removeClass(e, 'mceItemAnchor');
\r
11937 t.onVisualAid.dispatch(t, e, t.hasVisual);
\r
11940 remove : function() {
\r
11941 var t = this, e = t.getContainer();
\r
11943 t.removed = 1; // Cancels post remove event execution
\r
11946 t.execCallback('remove_instance_callback', t);
\r
11947 t.onRemove.dispatch(t);
\r
11949 // Clear all execCommand listeners this is required to avoid errors if the editor was removed inside another command
\r
11950 t.onExecCommand.listeners = [];
\r
11952 tinymce.remove(t);
\r
11956 destroy : function(s) {
\r
11959 // One time is enough
\r
11964 tinymce.removeUnload(t.destroy);
\r
11965 tinyMCE.onBeforeUnload.remove(t._beforeUnload);
\r
11967 // Manual destroy
\r
11968 if (t.theme && t.theme.destroy)
\r
11969 t.theme.destroy();
\r
11971 // Destroy controls, selection and dom
\r
11972 t.controlManager.destroy();
\r
11973 t.selection.destroy();
\r
11976 // Remove all events
\r
11978 // Don't clear the window or document if content editable
\r
11979 // is enabled since other instances might still be present
\r
11980 if (!t.settings.content_editable) {
\r
11981 Event.clear(t.getWin());
\r
11982 Event.clear(t.getDoc());
\r
11985 Event.clear(t.getBody());
\r
11986 Event.clear(t.formElement);
\r
11989 if (t.formElement) {
\r
11990 t.formElement.submit = t.formElement._mceOldSubmit;
\r
11991 t.formElement._mceOldSubmit = null;
\r
11994 t.contentAreaContainer = t.formElement = t.container = t.settings.content_element = t.bodyElement = t.contentDocument = t.contentWindow = null;
\r
11997 t.selection = t.selection.win = t.selection.dom = t.selection.dom.doc = null;
\r
12002 // Internal functions
\r
12004 _addEvents : function() {
\r
12005 // 'focus', 'blur', 'dblclick', 'beforedeactivate', submit, reset
\r
12006 var t = this, i, s = t.settings, dom = t.dom, lo = {
\r
12007 mouseup : 'onMouseUp',
\r
12008 mousedown : 'onMouseDown',
\r
12009 click : 'onClick',
\r
12010 keyup : 'onKeyUp',
\r
12011 keydown : 'onKeyDown',
\r
12012 keypress : 'onKeyPress',
\r
12013 submit : 'onSubmit',
\r
12014 reset : 'onReset',
\r
12015 contextmenu : 'onContextMenu',
\r
12016 dblclick : 'onDblClick',
\r
12017 paste : 'onPaste' // Doesn't work in all browsers yet
\r
12020 function eventHandler(e, o) {
\r
12023 // Don't fire events when it's removed
\r
12027 // Generic event handler
\r
12028 if (t.onEvent.dispatch(t, e, o) !== false) {
\r
12029 // Specific event handler
\r
12030 t[lo[e.fakeType || e.type]].dispatch(t, e, o);
\r
12034 // Add DOM events
\r
12035 each(lo, function(v, k) {
\r
12037 case 'contextmenu':
\r
12038 dom.bind(t.getDoc(), k, eventHandler);
\r
12042 dom.bind(t.getBody(), k, function(e) {
\r
12049 dom.bind(t.getElement().form || DOM.getParent(t.id, 'form'), k, eventHandler);
\r
12053 dom.bind(s.content_editable ? t.getBody() : t.getDoc(), k, eventHandler);
\r
12057 dom.bind(s.content_editable ? t.getBody() : (isGecko ? t.getDoc() : t.getWin()), 'focus', function(e) {
\r
12062 // Fixes bug where a specified document_base_uri could result in broken images
\r
12063 // This will also fix drag drop of images in Gecko
\r
12064 if (tinymce.isGecko) {
\r
12065 dom.bind(t.getDoc(), 'DOMNodeInserted', function(e) {
\r
12070 if (e.nodeType === 1 && e.nodeName === 'IMG' && (v = e.getAttribute('data-mce-src')))
\r
12071 e.src = t.documentBaseURI.toAbsolute(v);
\r
12075 // Set various midas options in Gecko
\r
12077 function setOpts() {
\r
12078 var t = this, d = t.getDoc(), s = t.settings;
\r
12080 if (isGecko && !s.readonly) {
\r
12081 if (t._isHidden()) {
\r
12083 if (!s.content_editable)
\r
12084 d.designMode = 'On';
\r
12086 // Fails if it's hidden
\r
12091 // Try new Gecko method
\r
12092 d.execCommand("styleWithCSS", 0, false);
\r
12094 // Use old method
\r
12095 if (!t._isHidden())
\r
12096 try {d.execCommand("useCSS", 0, true);} catch (ex) {}
\r
12099 if (!s.table_inline_editing)
\r
12100 try {d.execCommand('enableInlineTableEditing', false, false);} catch (ex) {}
\r
12102 if (!s.object_resizing)
\r
12103 try {d.execCommand('enableObjectResizing', false, false);} catch (ex) {}
\r
12107 t.onBeforeExecCommand.add(setOpts);
\r
12108 t.onMouseDown.add(setOpts);
\r
12111 // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250
\r
12112 // WebKit can't even do simple things like selecting an image
\r
12113 // This also fixes so it's possible to select mceItemAnchors
\r
12114 if (tinymce.isWebKit) {
\r
12115 t.onClick.add(function(ed, e) {
\r
12118 // Needs tobe the setBaseAndExtend or it will fail to select floated images
\r
12119 if (e.nodeName == 'IMG' || (e.nodeName == 'A' && dom.hasClass(e, 'mceItemAnchor'))) {
\r
12120 t.selection.getSel().setBaseAndExtent(e, 0, e, 1);
\r
12126 // Add node change handlers
\r
12127 t.onMouseUp.add(t.nodeChanged);
\r
12128 //t.onClick.add(t.nodeChanged);
\r
12129 t.onKeyUp.add(function(ed, e) {
\r
12130 var c = e.keyCode;
\r
12132 if ((c >= 33 && c <= 36) || (c >= 37 && c <= 40) || c == 13 || c == 45 || c == 46 || c == 8 || (tinymce.isMac && (c == 91 || c == 93)) || e.ctrlKey)
\r
12136 // Add reset handler
\r
12137 t.onReset.add(function() {
\r
12138 t.setContent(t.startContent, {format : 'raw'});
\r
12142 if (s.custom_shortcuts) {
\r
12143 if (s.custom_undo_redo_keyboard_shortcuts) {
\r
12144 t.addShortcut('ctrl+z', t.getLang('undo_desc'), 'Undo');
\r
12145 t.addShortcut('ctrl+y', t.getLang('redo_desc'), 'Redo');
\r
12148 // Add default shortcuts for gecko
\r
12149 t.addShortcut('ctrl+b', t.getLang('bold_desc'), 'Bold');
\r
12150 t.addShortcut('ctrl+i', t.getLang('italic_desc'), 'Italic');
\r
12151 t.addShortcut('ctrl+u', t.getLang('underline_desc'), 'Underline');
\r
12153 // BlockFormat shortcuts keys
\r
12154 for (i=1; i<=6; i++)
\r
12155 t.addShortcut('ctrl+' + i, '', ['FormatBlock', false, 'h' + i]);
\r
12157 t.addShortcut('ctrl+7', '', ['FormatBlock', false, '<p>']);
\r
12158 t.addShortcut('ctrl+8', '', ['FormatBlock', false, '<div>']);
\r
12159 t.addShortcut('ctrl+9', '', ['FormatBlock', false, '<address>']);
\r
12161 function find(e) {
\r
12164 if (!e.altKey && !e.ctrlKey && !e.metaKey)
\r
12167 each(t.shortcuts, function(o) {
\r
12168 if (tinymce.isMac && o.ctrl != e.metaKey)
\r
12170 else if (!tinymce.isMac && o.ctrl != e.ctrlKey)
\r
12173 if (o.alt != e.altKey)
\r
12176 if (o.shift != e.shiftKey)
\r
12179 if (e.keyCode == o.keyCode || (e.charCode && e.charCode == o.charCode)) {
\r
12188 t.onKeyUp.add(function(ed, e) {
\r
12192 return Event.cancel(e);
\r
12195 t.onKeyPress.add(function(ed, e) {
\r
12199 return Event.cancel(e);
\r
12202 t.onKeyDown.add(function(ed, e) {
\r
12206 o.func.call(o.scope);
\r
12207 return Event.cancel(e);
\r
12212 if (tinymce.isIE) {
\r
12213 // Fix so resize will only update the width and height attributes not the styles of an image
\r
12214 // It will also block mceItemNoResize items
\r
12215 dom.bind(t.getDoc(), 'controlselect', function(e) {
\r
12216 var re = t.resizeInfo, cb;
\r
12220 // Don't do this action for non image elements
\r
12221 if (e.nodeName !== 'IMG')
\r
12225 dom.unbind(re.node, re.ev, re.cb);
\r
12227 if (!dom.hasClass(e, 'mceItemNoResize')) {
\r
12228 ev = 'resizeend';
\r
12229 cb = dom.bind(e, ev, function(e) {
\r
12234 if (v = dom.getStyle(e, 'width')) {
\r
12235 dom.setAttrib(e, 'width', v.replace(/[^0-9%]+/g, ''));
\r
12236 dom.setStyle(e, 'width', '');
\r
12239 if (v = dom.getStyle(e, 'height')) {
\r
12240 dom.setAttrib(e, 'height', v.replace(/[^0-9%]+/g, ''));
\r
12241 dom.setStyle(e, 'height', '');
\r
12245 ev = 'resizestart';
\r
12246 cb = dom.bind(e, 'resizestart', Event.cancel, Event);
\r
12249 re = t.resizeInfo = {
\r
12256 t.onKeyDown.add(function(ed, e) {
\r
12259 switch (e.keyCode) {
\r
12261 sel = t.getDoc().selection;
\r
12263 // Fix IE control + backspace browser bug
\r
12264 if (sel.createRange && sel.createRange().item) {
\r
12265 ed.dom.remove(sel.createRange().item(0));
\r
12266 return Event.cancel(e);
\r
12272 if (tinymce.isOpera) {
\r
12273 t.onClick.add(function(ed, e) {
\r
12274 Event.prevent(e);
\r
12278 // Add custom undo/redo handlers
\r
12279 if (s.custom_undo_redo) {
\r
12280 function addUndo() {
\r
12281 t.undoManager.typing = false;
\r
12282 t.undoManager.add();
\r
12285 dom.bind(t.getDoc(), 'focusout', function(e) {
\r
12286 if (!t.removed && t.undoManager.typing)
\r
12290 // Add undo level when contents is drag/dropped within the editor
\r
12291 t.dom.bind(t.dom.getRoot(), 'dragend', function(e) {
\r
12295 t.onKeyUp.add(function(ed, e) {
\r
12296 var rng, parent, bookmark;
\r
12298 // Fix for bug #3168, to remove odd ".." nodes from the DOM we need to get/set the HTML of the parent node.
\r
12299 if (isIE && e.keyCode == 8) {
\r
12300 rng = t.selection.getRng();
\r
12301 if (rng.parentElement) {
\r
12302 parent = rng.parentElement();
\r
12303 bookmark = t.selection.getBookmark();
\r
12304 parent.innerHTML = parent.innerHTML;
\r
12305 t.selection.moveToBookmark(bookmark);
\r
12309 if ((e.keyCode >= 33 && e.keyCode <= 36) || (e.keyCode >= 37 && e.keyCode <= 40) || e.keyCode == 13 || e.keyCode == 45 || e.ctrlKey)
\r
12313 t.onKeyDown.add(function(ed, e) {
\r
12314 var rng, parent, bookmark, keyCode = e.keyCode;
\r
12316 // IE has a really odd bug where the DOM might include an node that doesn't have
\r
12317 // a proper structure. If you try to access nodeValue it would throw an illegal value exception.
\r
12318 // This seems to only happen when you delete contents and it seems to be avoidable if you refresh the element
\r
12319 // after you delete contents from it. See: #3008923
\r
12320 if (isIE && keyCode == 46) {
\r
12321 rng = t.selection.getRng();
\r
12323 if (rng.parentElement) {
\r
12324 parent = rng.parentElement();
\r
12326 if (!t.undoManager.typing) {
\r
12327 t.undoManager.beforeChange();
\r
12328 t.undoManager.typing = true;
\r
12329 t.undoManager.add();
\r
12332 // Select next word when ctrl key is used in combo with delete
\r
12334 rng.moveEnd('word', 1);
\r
12338 // Delete contents
\r
12339 t.selection.getSel().clear();
\r
12341 // Check if we are within the same parent
\r
12342 if (rng.parentElement() == parent) {
\r
12343 bookmark = t.selection.getBookmark();
\r
12346 // Update the HTML and hopefully it will remove the artifacts
\r
12347 parent.innerHTML = parent.innerHTML;
\r
12349 // And since it's IE it can sometimes produce an unknown runtime error
\r
12352 // Restore the caret position
\r
12353 t.selection.moveToBookmark(bookmark);
\r
12356 // Block the default delete behavior since it might be broken
\r
12357 e.preventDefault();
\r
12362 // Is caracter positon keys left,right,up,down,home,end,pgdown,pgup,enter
\r
12363 if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 13 || keyCode == 45) {
\r
12364 // Add position before enter key is pressed, used by IE since it still uses the default browser behavior
\r
12365 // Todo: Remove this once we normalize enter behavior on IE
\r
12366 if (tinymce.isIE && keyCode == 13)
\r
12367 t.undoManager.beforeChange();
\r
12369 if (t.undoManager.typing)
\r
12375 // If key isn't shift,ctrl,alt,capslock,metakey
\r
12376 if ((keyCode < 16 || keyCode > 20) && keyCode != 224 && keyCode != 91 && !t.undoManager.typing) {
\r
12377 t.undoManager.beforeChange();
\r
12378 t.undoManager.add();
\r
12379 t.undoManager.typing = true;
\r
12383 t.onMouseDown.add(function() {
\r
12384 if (t.undoManager.typing)
\r
12389 // Bug fix for FireFox keeping styles from end of selection instead of start.
\r
12390 if (tinymce.isGecko) {
\r
12391 function getAttributeApplyFunction() {
\r
12392 var template = t.dom.getAttribs(t.selection.getStart().cloneNode(false));
\r
12394 return function() {
\r
12395 var target = t.selection.getStart();
\r
12396 t.dom.removeAllAttribs(target);
\r
12397 each(template, function(attr) {
\r
12398 target.setAttributeNode(attr.cloneNode(true));
\r
12403 function isSelectionAcrossElements() {
\r
12404 var s = t.selection;
\r
12406 return !s.isCollapsed() && s.getStart() != s.getEnd();
\r
12409 t.onKeyPress.add(function(ed, e) {
\r
12410 var applyAttributes;
\r
12412 if ((e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) {
\r
12413 applyAttributes = getAttributeApplyFunction();
\r
12414 t.getDoc().execCommand('delete', false, null);
\r
12415 applyAttributes();
\r
12417 return Event.cancel(e);
\r
12421 t.dom.bind(t.getDoc(), 'cut', function(e) {
\r
12422 var applyAttributes;
\r
12424 if (isSelectionAcrossElements()) {
\r
12425 applyAttributes = getAttributeApplyFunction();
\r
12426 t.onKeyUp.addToTop(Event.cancel, Event);
\r
12428 setTimeout(function() {
\r
12429 applyAttributes();
\r
12430 t.onKeyUp.remove(Event.cancel, Event);
\r
12437 _isHidden : function() {
\r
12443 // Weird, wheres that cursor selection?
\r
12444 s = this.selection.getSel();
\r
12445 return (!s || !s.rangeCount || s.rangeCount == 0);
\r
12450 (function(tinymce) {
\r
12451 // Added for compression purposes
\r
12452 var each = tinymce.each, undefined, TRUE = true, FALSE = false;
\r
12454 tinymce.EditorCommands = function(editor) {
\r
12455 var dom = editor.dom,
\r
12456 selection = editor.selection,
\r
12457 commands = {state: {}, exec : {}, value : {}},
\r
12458 settings = editor.settings,
\r
12461 function execCommand(command, ui, value) {
\r
12464 command = command.toLowerCase();
\r
12465 if (func = commands.exec[command]) {
\r
12466 func(command, ui, value);
\r
12473 function queryCommandState(command) {
\r
12476 command = command.toLowerCase();
\r
12477 if (func = commands.state[command])
\r
12478 return func(command);
\r
12483 function queryCommandValue(command) {
\r
12486 command = command.toLowerCase();
\r
12487 if (func = commands.value[command])
\r
12488 return func(command);
\r
12493 function addCommands(command_list, type) {
\r
12494 type = type || 'exec';
\r
12496 each(command_list, function(callback, command) {
\r
12497 each(command.toLowerCase().split(','), function(command) {
\r
12498 commands[type][command] = callback;
\r
12503 // Expose public methods
\r
12504 tinymce.extend(this, {
\r
12505 execCommand : execCommand,
\r
12506 queryCommandState : queryCommandState,
\r
12507 queryCommandValue : queryCommandValue,
\r
12508 addCommands : addCommands
\r
12511 // Private methods
\r
12513 function execNativeCommand(command, ui, value) {
\r
12514 if (ui === undefined)
\r
12517 if (value === undefined)
\r
12520 return editor.getDoc().execCommand(command, ui, value);
\r
12523 function isFormatMatch(name) {
\r
12524 return editor.formatter.match(name);
\r
12527 function toggleFormat(name, value) {
\r
12528 editor.formatter.toggle(name, value ? {value : value} : undefined);
\r
12531 function storeSelection(type) {
\r
12532 bookmark = selection.getBookmark(type);
\r
12535 function restoreSelection() {
\r
12536 selection.moveToBookmark(bookmark);
\r
12539 // Add execCommand overrides
\r
12541 // Ignore these, added for compatibility
\r
12542 'mceResetDesignMode,mceBeginUndoLevel' : function() {},
\r
12544 // Add undo manager logic
\r
12545 'mceEndUndoLevel,mceAddUndoLevel' : function() {
\r
12546 editor.undoManager.add();
\r
12549 'Cut,Copy,Paste' : function(command) {
\r
12550 var doc = editor.getDoc(), failed;
\r
12552 // Try executing the native command
\r
12554 execNativeCommand(command);
\r
12556 // Command failed
\r
12560 // Present alert message about clipboard access not being available
\r
12561 if (failed || !doc.queryCommandSupported(command)) {
\r
12562 if (tinymce.isGecko) {
\r
12563 editor.windowManager.confirm(editor.getLang('clipboard_msg'), function(state) {
\r
12565 open('http://www.mozilla.org/editor/midasdemo/securityprefs.html', '_blank');
\r
12568 editor.windowManager.alert(editor.getLang('clipboard_no_support'));
\r
12572 // Override unlink command
\r
12573 unlink : function(command) {
\r
12574 if (selection.isCollapsed())
\r
12575 selection.select(selection.getNode());
\r
12577 execNativeCommand(command);
\r
12578 selection.collapse(FALSE);
\r
12581 // Override justify commands to use the text formatter engine
\r
12582 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) {
\r
12583 var align = command.substring(7);
\r
12585 // Remove all other alignments first
\r
12586 each('left,center,right,full'.split(','), function(name) {
\r
12587 if (align != name)
\r
12588 editor.formatter.remove('align' + name);
\r
12591 toggleFormat('align' + align);
\r
12592 execCommand('mceRepaint');
\r
12595 // Override list commands to fix WebKit bug
\r
12596 'InsertUnorderedList,InsertOrderedList' : function(command) {
\r
12597 var listElm, listParent;
\r
12599 execNativeCommand(command);
\r
12601 // WebKit produces lists within block elements so we need to split them
\r
12602 // we will replace the native list creation logic to custom logic later on
\r
12603 // TODO: Remove this when the list creation logic is removed
\r
12604 listElm = dom.getParent(selection.getNode(), 'ol,ul');
\r
12606 listParent = listElm.parentNode;
\r
12608 // If list is within a text block then split that block
\r
12609 if (/^(H[1-6]|P|ADDRESS|PRE)$/.test(listParent.nodeName)) {
\r
12610 storeSelection();
\r
12611 dom.split(listParent, listElm);
\r
12612 restoreSelection();
\r
12617 // Override commands to use the text formatter engine
\r
12618 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) {
\r
12619 toggleFormat(command);
\r
12622 // Override commands to use the text formatter engine
\r
12623 'ForeColor,HiliteColor,FontName' : function(command, ui, value) {
\r
12624 toggleFormat(command, value);
\r
12627 FontSize : function(command, ui, value) {
\r
12628 var fontClasses, fontSizes;
\r
12630 // Convert font size 1-7 to styles
\r
12631 if (value >= 1 && value <= 7) {
\r
12632 fontSizes = tinymce.explode(settings.font_size_style_values);
\r
12633 fontClasses = tinymce.explode(settings.font_size_classes);
\r
12636 value = fontClasses[value - 1] || value;
\r
12638 value = fontSizes[value - 1] || value;
\r
12641 toggleFormat(command, value);
\r
12644 RemoveFormat : function(command) {
\r
12645 editor.formatter.remove(command);
\r
12648 mceBlockQuote : function(command) {
\r
12649 toggleFormat('blockquote');
\r
12652 FormatBlock : function(command, ui, value) {
\r
12653 return toggleFormat(value || 'p');
\r
12656 mceCleanup : function() {
\r
12657 var bookmark = selection.getBookmark();
\r
12659 editor.setContent(editor.getContent({cleanup : TRUE}), {cleanup : TRUE});
\r
12661 selection.moveToBookmark(bookmark);
\r
12664 mceRemoveNode : function(command, ui, value) {
\r
12665 var node = value || selection.getNode();
\r
12667 // Make sure that the body node isn't removed
\r
12668 if (node != editor.getBody()) {
\r
12669 storeSelection();
\r
12670 editor.dom.remove(node, TRUE);
\r
12671 restoreSelection();
\r
12675 mceSelectNodeDepth : function(command, ui, value) {
\r
12678 dom.getParent(selection.getNode(), function(node) {
\r
12679 if (node.nodeType == 1 && counter++ == value) {
\r
12680 selection.select(node);
\r
12683 }, editor.getBody());
\r
12686 mceSelectNode : function(command, ui, value) {
\r
12687 selection.select(value);
\r
12690 mceInsertContent : function(command, ui, value) {
\r
12691 var caretNode, rng, rootNode, parent, node, rng, nodeRect, viewPortRect, args;
\r
12693 function findSuitableCaretNode(node, root_node, next) {
\r
12694 var walker = new tinymce.dom.TreeWalker(next ? node.nextSibling : node.previousSibling, root_node);
\r
12696 while ((node = walker.current())) {
\r
12697 if ((node.nodeType == 3 && tinymce.trim(node.nodeValue).length) || node.nodeName == 'BR' || node.nodeName == 'IMG')
\r
12707 args = {content: value, format: 'html'};
\r
12708 selection.onBeforeSetContent.dispatch(selection, args);
\r
12709 value = args.content;
\r
12711 // Add caret at end of contents if it's missing
\r
12712 if (value.indexOf('{$caret}') == -1)
\r
12713 value += '{$caret}';
\r
12715 // Set the content at selection to a span and replace it's contents with the value
\r
12716 selection.setContent('<span id="__mce">\uFEFF</span>', {no_events : false});
\r
12717 dom.setOuterHTML('__mce', value.replace(/\{\$caret\}/, '<span data-mce-type="bookmark" id="__mce">\uFEFF</span>'));
\r
12719 caretNode = dom.select('#__mce')[0];
\r
12720 rootNode = dom.getRoot();
\r
12722 // Move the caret into the last suitable location within the previous sibling if it's a block since the block might be split
\r
12723 if (caretNode.previousSibling && dom.isBlock(caretNode.previousSibling) || caretNode.parentNode == rootNode) {
\r
12724 node = findSuitableCaretNode(caretNode, rootNode);
\r
12726 if (node.nodeName == 'BR')
\r
12727 node.parentNode.insertBefore(caretNode, node);
\r
12729 dom.insertAfter(caretNode, node);
\r
12733 // Find caret root parent and clean it up using the serializer to avoid nesting
\r
12734 while (caretNode) {
\r
12735 if (caretNode === rootNode) {
\r
12736 // Clean up the parent element by parsing and serializing it
\r
12737 // This will remove invalid elements/attributes and fix nesting issues
\r
12738 dom.setOuterHTML(parent,
\r
12739 new tinymce.html.Serializer({}, editor.schema).serialize(
\r
12740 editor.parser.parse(dom.getOuterHTML(parent))
\r
12747 parent = caretNode;
\r
12748 caretNode = caretNode.parentNode;
\r
12751 // Find caret after cleanup and move selection to that location
\r
12752 caretNode = dom.select('#__mce')[0];
\r
12754 node = findSuitableCaretNode(caretNode, rootNode) || findSuitableCaretNode(caretNode, rootNode, true);
\r
12755 dom.remove(caretNode);
\r
12758 rng = dom.createRng();
\r
12760 if (node.nodeType == 3) {
\r
12761 rng.setStart(node, node.length);
\r
12762 rng.setEnd(node, node.length);
\r
12764 if (node.nodeName == 'BR') {
\r
12765 rng.setStartBefore(node);
\r
12766 rng.setEndBefore(node);
\r
12768 rng.setStartAfter(node);
\r
12769 rng.setEndAfter(node);
\r
12773 selection.setRng(rng);
\r
12775 // Scroll range into view scrollIntoView on element can't be used since it will scroll the main view port as well
\r
12776 if (!tinymce.isIE) {
\r
12777 node = dom.create('span', null, '\u00a0');
\r
12778 rng.insertNode(node);
\r
12779 nodeRect = dom.getRect(node);
\r
12780 viewPortRect = dom.getViewPort(editor.getWin());
\r
12782 // Check if node is out side the viewport if it is then scroll to it
\r
12783 if ((nodeRect.y > viewPortRect.y + viewPortRect.h || nodeRect.y < viewPortRect.y) ||
\r
12784 (nodeRect.x > viewPortRect.x + viewPortRect.w || nodeRect.x < viewPortRect.x)) {
\r
12785 editor.getBody().scrollLeft = nodeRect.x;
\r
12786 editor.getBody().scrollTop = nodeRect.y;
\r
12789 dom.remove(node);
\r
12792 // Make sure that the selection is collapsed after we removed the node fixes a WebKit bug
\r
12793 // where WebKit would place the endContainer/endOffset at a different location than the startContainer/startOffset
\r
12794 selection.collapse(true);
\r
12798 selection.onSetContent.dispatch(selection, args);
\r
12799 editor.addVisual();
\r
12802 mceInsertRawHTML : function(command, ui, value) {
\r
12803 selection.setContent('tiny_mce_marker');
\r
12804 editor.setContent(editor.getContent().replace(/tiny_mce_marker/g, function() { return value }));
\r
12807 mceSetContent : function(command, ui, value) {
\r
12808 editor.setContent(value);
\r
12811 'Indent,Outdent' : function(command) {
\r
12812 var intentValue, indentUnit, value;
\r
12814 // Setup indent level
\r
12815 intentValue = settings.indentation;
\r
12816 indentUnit = /[a-z%]+$/i.exec(intentValue);
\r
12817 intentValue = parseInt(intentValue);
\r
12819 if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) {
\r
12820 each(selection.getSelectedBlocks(), function(element) {
\r
12821 if (command == 'outdent') {
\r
12822 value = Math.max(0, parseInt(element.style.paddingLeft || 0) - intentValue);
\r
12823 dom.setStyle(element, 'paddingLeft', value ? value + indentUnit : '');
\r
12825 dom.setStyle(element, 'paddingLeft', (parseInt(element.style.paddingLeft || 0) + intentValue) + indentUnit);
\r
12828 execNativeCommand(command);
\r
12831 mceRepaint : function() {
\r
12834 if (tinymce.isGecko) {
\r
12836 storeSelection(TRUE);
\r
12838 if (selection.getSel())
\r
12839 selection.getSel().selectAllChildren(editor.getBody());
\r
12841 selection.collapse(TRUE);
\r
12842 restoreSelection();
\r
12849 mceToggleFormat : function(command, ui, value) {
\r
12850 editor.formatter.toggle(value);
\r
12853 InsertHorizontalRule : function() {
\r
12854 editor.execCommand('mceInsertContent', false, '<hr />');
\r
12857 mceToggleVisualAid : function() {
\r
12858 editor.hasVisual = !editor.hasVisual;
\r
12859 editor.addVisual();
\r
12862 mceReplaceContent : function(command, ui, value) {
\r
12863 editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({format : 'text'})));
\r
12866 mceInsertLink : function(command, ui, value) {
\r
12867 var link = dom.getParent(selection.getNode(), 'a'), img, floatVal;
\r
12869 if (tinymce.is(value, 'string'))
\r
12870 value = {href : value};
\r
12872 // Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here.
\r
12873 value.href = value.href.replace(' ', '%20');
\r
12876 // WebKit can't create links on float images for some odd reason so just remove it and restore it later
\r
12877 if (tinymce.isWebKit) {
\r
12878 img = dom.getParent(selection.getNode(), 'img');
\r
12881 floatVal = img.style.cssFloat;
\r
12882 img.style.cssFloat = null;
\r
12886 execNativeCommand('CreateLink', FALSE, 'javascript:mctmp(0);');
\r
12888 // Restore float value
\r
12890 img.style.cssFloat = floatVal;
\r
12892 each(dom.select("a[href='javascript:mctmp(0);']"), function(link) {
\r
12893 dom.setAttribs(link, value);
\r
12897 dom.setAttribs(link, value);
\r
12899 editor.dom.remove(link, TRUE);
\r
12903 selectAll : function() {
\r
12904 var root = dom.getRoot(), rng = dom.createRng();
\r
12906 rng.setStart(root, 0);
\r
12907 rng.setEnd(root, root.childNodes.length);
\r
12909 editor.selection.setRng(rng);
\r
12913 // Add queryCommandState overrides
\r
12915 // Override justify commands
\r
12916 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) {
\r
12917 return isFormatMatch('align' + command.substring(7));
\r
12920 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) {
\r
12921 return isFormatMatch(command);
\r
12924 mceBlockQuote : function() {
\r
12925 return isFormatMatch('blockquote');
\r
12928 Outdent : function() {
\r
12931 if (settings.inline_styles) {
\r
12932 if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0)
\r
12935 if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0)
\r
12939 return queryCommandState('InsertUnorderedList') || queryCommandState('InsertOrderedList') || (!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE'));
\r
12942 'InsertUnorderedList,InsertOrderedList' : function(command) {
\r
12943 return dom.getParent(selection.getNode(), command == 'insertunorderedlist' ? 'UL' : 'OL');
\r
12947 // Add queryCommandValue overrides
\r
12949 'FontSize,FontName' : function(command) {
\r
12950 var value = 0, parent;
\r
12952 if (parent = dom.getParent(selection.getNode(), 'span')) {
\r
12953 if (command == 'fontsize')
\r
12954 value = parent.style.fontSize;
\r
12956 value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase();
\r
12963 // Add undo manager logic
\r
12964 if (settings.custom_undo_redo) {
\r
12966 Undo : function() {
\r
12967 editor.undoManager.undo();
\r
12970 Redo : function() {
\r
12971 editor.undoManager.redo();
\r
12978 (function(tinymce) {
\r
12979 var Dispatcher = tinymce.util.Dispatcher;
\r
12981 tinymce.UndoManager = function(editor) {
\r
12982 var self, index = 0, data = [];
\r
12984 function getContent() {
\r
12985 return tinymce.trim(editor.getContent({format : 'raw', no_events : 1}));
\r
12991 onAdd : new Dispatcher(self),
\r
12993 onUndo : new Dispatcher(self),
\r
12995 onRedo : new Dispatcher(self),
\r
12997 beforeChange : function() {
\r
12998 // Set before bookmark on previous level
\r
13000 data[index].beforeBookmark = editor.selection.getBookmark(2, true);
\r
13003 add : function(level) {
\r
13004 var i, settings = editor.settings, lastLevel;
\r
13006 level = level || {};
\r
13007 level.content = getContent();
\r
13009 // Add undo level if needed
\r
13010 lastLevel = data[index];
\r
13011 if (lastLevel && lastLevel.content == level.content)
\r
13014 // Time to compress
\r
13015 if (settings.custom_undo_redo_levels) {
\r
13016 if (data.length > settings.custom_undo_redo_levels) {
\r
13017 for (i = 0; i < data.length - 1; i++)
\r
13018 data[i] = data[i + 1];
\r
13021 index = data.length;
\r
13025 // Get a non intrusive normalized bookmark
\r
13026 level.bookmark = editor.selection.getBookmark(2, true);
\r
13028 // Crop array if needed
\r
13029 if (index < data.length - 1)
\r
13030 data.length = index + 1;
\r
13032 data.push(level);
\r
13033 index = data.length - 1;
\r
13035 self.onAdd.dispatch(self, level);
\r
13036 editor.isNotDirty = 0;
\r
13041 undo : function() {
\r
13044 if (self.typing) {
\r
13046 self.typing = false;
\r
13050 level = data[--index];
\r
13052 editor.setContent(level.content, {format : 'raw'});
\r
13053 editor.selection.moveToBookmark(level.beforeBookmark);
\r
13055 self.onUndo.dispatch(self, level);
\r
13061 redo : function() {
\r
13064 if (index < data.length - 1) {
\r
13065 level = data[++index];
\r
13067 editor.setContent(level.content, {format : 'raw'});
\r
13068 editor.selection.moveToBookmark(level.bookmark);
\r
13070 self.onRedo.dispatch(self, level);
\r
13076 clear : function() {
\r
13079 self.typing = false;
\r
13082 hasUndo : function() {
\r
13083 return index > 0 || this.typing;
\r
13086 hasRedo : function() {
\r
13087 return index < data.length - 1 && !this.typing;
\r
13093 (function(tinymce) {
\r
13095 var Event = tinymce.dom.Event,
\r
13096 isIE = tinymce.isIE,
\r
13097 isGecko = tinymce.isGecko,
\r
13098 isOpera = tinymce.isOpera,
\r
13099 each = tinymce.each,
\r
13100 extend = tinymce.extend,
\r
13104 function cloneFormats(node) {
\r
13105 var clone, temp, inner;
\r
13108 if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U)$/.test(node.nodeName)) {
\r
13110 temp = node.cloneNode(false);
\r
13111 temp.appendChild(clone);
\r
13114 clone = inner = node.cloneNode(false);
\r
13117 clone.removeAttribute('id');
\r
13119 } while (node = node.parentNode);
\r
13122 return {wrapper : clone, inner : inner};
\r
13125 // Checks if the selection/caret is at the end of the specified block element
\r
13126 function isAtEnd(rng, par) {
\r
13127 var rng2 = par.ownerDocument.createRange();
\r
13129 rng2.setStart(rng.endContainer, rng.endOffset);
\r
13130 rng2.setEndAfter(par);
\r
13132 // Get number of characters to the right of the cursor if it's zero then we are at the end and need to merge the next block element
\r
13133 return rng2.cloneContents().textContent.length == 0;
\r
13136 function splitList(selection, dom, li) {
\r
13137 var listBlock, block;
\r
13139 if (dom.isEmpty(li)) {
\r
13140 listBlock = dom.getParent(li, 'ul,ol');
\r
13142 if (!dom.getParent(listBlock.parentNode, 'ul,ol')) {
\r
13143 dom.split(listBlock, li);
\r
13144 block = dom.create('p', 0, '<br data-mce-bogus="1" />');
\r
13145 dom.replace(block, li);
\r
13146 selection.select(block, 1);
\r
13155 tinymce.create('tinymce.ForceBlocks', {
\r
13156 ForceBlocks : function(ed) {
\r
13157 var t = this, s = ed.settings, elm;
\r
13161 elm = (s.forced_root_block || 'p').toLowerCase();
\r
13162 s.element = elm.toUpperCase();
\r
13164 ed.onPreInit.add(t.setup, t);
\r
13166 if (s.forced_root_block) {
\r
13167 ed.onInit.add(t.forceRoots, t);
\r
13168 ed.onSetContent.add(t.forceRoots, t);
\r
13169 ed.onBeforeGetContent.add(t.forceRoots, t);
\r
13170 ed.onExecCommand.add(function(ed, cmd) {
\r
13171 if (cmd == 'mceInsertContent') {
\r
13173 ed.nodeChanged();
\r
13179 setup : function() {
\r
13180 var t = this, ed = t.editor, s = ed.settings, dom = ed.dom, selection = ed.selection;
\r
13182 // Force root blocks when typing and when getting output
\r
13183 if (s.forced_root_block) {
\r
13184 ed.onBeforeExecCommand.add(t.forceRoots, t);
\r
13185 ed.onKeyUp.add(t.forceRoots, t);
\r
13186 ed.onPreProcess.add(t.forceRoots, t);
\r
13189 if (s.force_br_newlines) {
\r
13190 // Force IE to produce BRs on enter
\r
13192 ed.onKeyPress.add(function(ed, e) {
\r
13195 if (e.keyCode == 13 && selection.getNode().nodeName != 'LI') {
\r
13196 selection.setContent('<br id="__" /> ', {format : 'raw'});
\r
13197 n = dom.get('__');
\r
13198 n.removeAttribute('id');
\r
13199 selection.select(n);
\r
13200 selection.collapse();
\r
13201 return Event.cancel(e);
\r
13207 if (s.force_p_newlines) {
\r
13209 ed.onKeyPress.add(function(ed, e) {
\r
13210 if (e.keyCode == 13 && !e.shiftKey && !t.insertPara(e))
\r
13214 // Ungly hack to for IE to preserve the formatting when you press
\r
13215 // enter at the end of a block element with formatted contents
\r
13216 // This logic overrides the browsers default logic with
\r
13217 // custom logic that enables us to control the output
\r
13218 tinymce.addUnload(function() {
\r
13219 t._previousFormats = 0; // Fix IE leak
\r
13222 ed.onKeyPress.add(function(ed, e) {
\r
13223 t._previousFormats = 0;
\r
13225 // Clone the current formats, this will later be applied to the new block contents
\r
13226 if (e.keyCode == 13 && !e.shiftKey && ed.selection.isCollapsed() && s.keep_styles)
\r
13227 t._previousFormats = cloneFormats(ed.selection.getStart());
\r
13230 ed.onKeyUp.add(function(ed, e) {
\r
13231 // Let IE break the element and the wrap the new caret location in the previous formats
\r
13232 if (e.keyCode == 13 && !e.shiftKey) {
\r
13233 var parent = ed.selection.getStart(), fmt = t._previousFormats;
\r
13235 // Parent is an empty block
\r
13236 if (!parent.hasChildNodes() && fmt) {
\r
13237 parent = dom.getParent(parent, dom.isBlock);
\r
13239 if (parent && parent.nodeName != 'LI') {
\r
13240 parent.innerHTML = '';
\r
13242 if (t._previousFormats) {
\r
13243 parent.appendChild(fmt.wrapper);
\r
13244 fmt.inner.innerHTML = '\uFEFF';
\r
13246 parent.innerHTML = '\uFEFF';
\r
13248 selection.select(parent, 1);
\r
13249 selection.collapse(true);
\r
13250 ed.getDoc().execCommand('Delete', false, null);
\r
13251 t._previousFormats = 0;
\r
13259 ed.onKeyDown.add(function(ed, e) {
\r
13260 if ((e.keyCode == 8 || e.keyCode == 46) && !e.shiftKey)
\r
13261 t.backspaceDelete(e, e.keyCode == 8);
\r
13266 // Workaround for missing shift+enter support, http://bugs.webkit.org/show_bug.cgi?id=16973
\r
13267 if (tinymce.isWebKit) {
\r
13268 function insertBr(ed) {
\r
13269 var rng = selection.getRng(), br, div = dom.create('div', null, ' '), divYPos, vpHeight = dom.getViewPort(ed.getWin()).h;
\r
13271 // Insert BR element
\r
13272 rng.insertNode(br = dom.create('br'));
\r
13274 // Place caret after BR
\r
13275 rng.setStartAfter(br);
\r
13276 rng.setEndAfter(br);
\r
13277 selection.setRng(rng);
\r
13279 // Could not place caret after BR then insert an nbsp entity and move the caret
\r
13280 if (selection.getSel().focusNode == br.previousSibling) {
\r
13281 selection.select(dom.insertAfter(dom.doc.createTextNode('\u00a0'), br));
\r
13282 selection.collapse(TRUE);
\r
13285 // Create a temporary DIV after the BR and get the position as it
\r
13286 // seems like getPos() returns 0 for text nodes and BR elements.
\r
13287 dom.insertAfter(div, br);
\r
13288 divYPos = dom.getPos(div).y;
\r
13291 // Scroll to new position, scrollIntoView can't be used due to bug: http://bugs.webkit.org/show_bug.cgi?id=16117
\r
13292 if (divYPos > vpHeight) // It is not necessary to scroll if the DIV is inside the view port.
\r
13293 ed.getWin().scrollTo(0, divYPos);
\r
13296 ed.onKeyPress.add(function(ed, e) {
\r
13297 if (e.keyCode == 13 && (e.shiftKey || (s.force_br_newlines && !dom.getParent(selection.getNode(), 'h1,h2,h3,h4,h5,h6,ol,ul')))) {
\r
13304 // IE specific fixes
\r
13306 // Replaces IE:s auto generated paragraphs with the specified element name
\r
13307 if (s.element != 'P') {
\r
13308 ed.onKeyPress.add(function(ed, e) {
\r
13309 t.lastElm = selection.getNode().nodeName;
\r
13312 ed.onKeyUp.add(function(ed, e) {
\r
13313 var bl, n = selection.getNode(), b = ed.getBody();
\r
13315 if (b.childNodes.length === 1 && n.nodeName == 'P') {
\r
13316 n = dom.rename(n, s.element);
\r
13317 selection.select(n);
\r
13318 selection.collapse();
\r
13319 ed.nodeChanged();
\r
13320 } else if (e.keyCode == 13 && !e.shiftKey && t.lastElm != 'P') {
\r
13321 bl = dom.getParent(n, 'p');
\r
13324 dom.rename(bl, s.element);
\r
13325 ed.nodeChanged();
\r
13333 find : function(n, t, s) {
\r
13334 var ed = this.editor, w = ed.getDoc().createTreeWalker(n, 4, null, FALSE), c = -1;
\r
13336 while (n = w.nextNode()) {
\r
13340 if (t == 0 && n == s)
\r
13344 if (t == 1 && c == s)
\r
13351 forceRoots : function(ed, e) {
\r
13352 var t = this, ed = t.editor, b = ed.getBody(), d = ed.getDoc(), se = ed.selection, s = se.getSel(), r = se.getRng(), si = -2, ei, so, eo, tr, c = -0xFFFFFF;
\r
13353 var nx, bl, bp, sp, le, nl = b.childNodes, i, n, eid;
\r
13355 // Fix for bug #1863847
\r
13356 //if (e && e.keyCode == 13)
\r
13359 // Wrap non blocks into blocks
\r
13360 for (i = nl.length - 1; i >= 0; i--) {
\r
13363 // Ignore internal elements
\r
13364 if (nx.nodeType === 1 && nx.getAttribute('data-mce-type')) {
\r
13369 // Is text or non block element
\r
13370 if (nx.nodeType === 3 || (!t.dom.isBlock(nx) && nx.nodeType !== 8 && !/^(script|mce:script|style|mce:style)$/i.test(nx.nodeName))) {
\r
13372 // Create new block but ignore whitespace
\r
13373 if (nx.nodeType != 3 || /[^\s]/g.test(nx.nodeValue)) {
\r
13374 // Store selection
\r
13375 if (si == -2 && r) {
\r
13376 if (!isIE || r.setStart) {
\r
13377 // If selection is element then mark it
\r
13378 if (r.startContainer.nodeType == 1 && (n = r.startContainer.childNodes[r.startOffset]) && n.nodeType == 1) {
\r
13379 // Save the id of the selected element
\r
13380 eid = n.getAttribute("id");
\r
13381 n.setAttribute("id", "__mce");
\r
13383 // If element is inside body, might not be the case in contentEdiable mode
\r
13384 if (ed.dom.getParent(r.startContainer, function(e) {return e === b;})) {
\r
13385 so = r.startOffset;
\r
13386 eo = r.endOffset;
\r
13387 si = t.find(b, 0, r.startContainer);
\r
13388 ei = t.find(b, 0, r.endContainer);
\r
13392 // Force control range into text range
\r
13394 tr = d.body.createTextRange();
\r
13395 tr.moveToElementText(r.item(0));
\r
13399 tr = d.body.createTextRange();
\r
13400 tr.moveToElementText(b);
\r
13402 bp = tr.move('character', c) * -1;
\r
13404 tr = r.duplicate();
\r
13406 sp = tr.move('character', c) * -1;
\r
13408 tr = r.duplicate();
\r
13410 le = (tr.move('character', c) * -1) - sp;
\r
13417 // Uses replaceChild instead of cloneNode since it removes selected attribute from option elements on IE
\r
13418 // See: http://support.microsoft.com/kb/829907
\r
13419 bl = ed.dom.create(ed.settings.forced_root_block);
\r
13420 nx.parentNode.replaceChild(bl, nx);
\r
13421 bl.appendChild(nx);
\r
13424 if (bl.hasChildNodes())
\r
13425 bl.insertBefore(nx, bl.firstChild);
\r
13427 bl.appendChild(nx);
\r
13430 bl = null; // Time to create new block
\r
13433 // Restore selection
\r
13435 if (!isIE || r.setStart) {
\r
13436 bl = b.getElementsByTagName(ed.settings.element)[0];
\r
13437 r = d.createRange();
\r
13439 // Select last location or generated block
\r
13441 r.setStart(t.find(b, 1, si), so);
\r
13443 r.setStart(bl, 0);
\r
13445 // Select last location or generated block
\r
13447 r.setEnd(t.find(b, 1, ei), eo);
\r
13452 s.removeAllRanges();
\r
13457 r = s.createRange();
\r
13458 r.moveToElementText(b);
\r
13460 r.moveStart('character', si);
\r
13461 r.moveEnd('character', ei);
\r
13467 } else if ((!isIE || r.setStart) && (n = ed.dom.get('__mce'))) {
\r
13468 // Restore the id of the selected element
\r
13470 n.setAttribute('id', eid);
\r
13472 n.removeAttribute('id');
\r
13474 // Move caret before selected element
\r
13475 r = d.createRange();
\r
13476 r.setStartBefore(n);
\r
13477 r.setEndBefore(n);
\r
13482 getParentBlock : function(n) {
\r
13483 var d = this.dom;
\r
13485 return d.getParent(n, d.isBlock);
\r
13488 insertPara : function(e) {
\r
13489 var t = this, ed = t.editor, dom = ed.dom, d = ed.getDoc(), se = ed.settings, s = ed.selection.getSel(), r = s.getRangeAt(0), b = d.body;
\r
13490 var rb, ra, dir, sn, so, en, eo, sb, eb, bn, bef, aft, sc, ec, n, vp = dom.getViewPort(ed.getWin()), y, ch, car;
\r
13492 ed.undoManager.beforeChange();
\r
13494 // If root blocks are forced then use Operas default behavior since it's really good
\r
13495 // Removed due to bug: #1853816
\r
13496 // if (se.forced_root_block && isOpera)
\r
13499 // Setup before range
\r
13500 rb = d.createRange();
\r
13502 // If is before the first block element and in body, then move it into first block element
\r
13503 rb.setStart(s.anchorNode, s.anchorOffset);
\r
13504 rb.collapse(TRUE);
\r
13506 // Setup after range
\r
13507 ra = d.createRange();
\r
13509 // If is before the first block element and in body, then move it into first block element
\r
13510 ra.setStart(s.focusNode, s.focusOffset);
\r
13511 ra.collapse(TRUE);
\r
13513 // Setup start/end points
\r
13514 dir = rb.compareBoundaryPoints(rb.START_TO_END, ra) < 0;
\r
13515 sn = dir ? s.anchorNode : s.focusNode;
\r
13516 so = dir ? s.anchorOffset : s.focusOffset;
\r
13517 en = dir ? s.focusNode : s.anchorNode;
\r
13518 eo = dir ? s.focusOffset : s.anchorOffset;
\r
13520 // If selection is in empty table cell
\r
13521 if (sn === en && /^(TD|TH)$/.test(sn.nodeName)) {
\r
13522 if (sn.firstChild.nodeName == 'BR')
\r
13523 dom.remove(sn.firstChild); // Remove BR
\r
13525 // Create two new block elements
\r
13526 if (sn.childNodes.length == 0) {
\r
13527 ed.dom.add(sn, se.element, null, '<br />');
\r
13528 aft = ed.dom.add(sn, se.element, null, '<br />');
\r
13530 n = sn.innerHTML;
\r
13531 sn.innerHTML = '';
\r
13532 ed.dom.add(sn, se.element, null, n);
\r
13533 aft = ed.dom.add(sn, se.element, null, '<br />');
\r
13536 // Move caret into the last one
\r
13537 r = d.createRange();
\r
13538 r.selectNodeContents(aft);
\r
13540 ed.selection.setRng(r);
\r
13545 // If the caret is in an invalid location in FF we need to move it into the first block
\r
13546 if (sn == b && en == b && b.firstChild && ed.dom.isBlock(b.firstChild)) {
\r
13547 sn = en = sn.firstChild;
\r
13549 rb = d.createRange();
\r
13550 rb.setStart(sn, 0);
\r
13551 ra = d.createRange();
\r
13552 ra.setStart(en, 0);
\r
13555 // Never use body as start or end node
\r
13556 sn = sn.nodeName == "HTML" ? d.body : sn; // Fix for Opera bug: https://bugs.opera.com/show_bug.cgi?id=273224&comments=yes
\r
13557 sn = sn.nodeName == "BODY" ? sn.firstChild : sn;
\r
13558 en = en.nodeName == "HTML" ? d.body : en; // Fix for Opera bug: https://bugs.opera.com/show_bug.cgi?id=273224&comments=yes
\r
13559 en = en.nodeName == "BODY" ? en.firstChild : en;
\r
13561 // Get start and end blocks
\r
13562 sb = t.getParentBlock(sn);
\r
13563 eb = t.getParentBlock(en);
\r
13564 bn = sb ? sb.nodeName : se.element; // Get block name to create
\r
13566 // Return inside list use default browser behavior
\r
13567 if (n = t.dom.getParent(sb, 'li,pre')) {
\r
13568 if (n.nodeName == 'LI')
\r
13569 return splitList(ed.selection, t.dom, n);
\r
13574 // If caption or absolute layers then always generate new blocks within
\r
13575 if (sb && (sb.nodeName == 'CAPTION' || /absolute|relative|fixed/gi.test(dom.getStyle(sb, 'position', 1)))) {
\r
13580 // If caption or absolute layers then always generate new blocks within
\r
13581 if (eb && (eb.nodeName == 'CAPTION' || /absolute|relative|fixed/gi.test(dom.getStyle(sb, 'position', 1)))) {
\r
13587 if (/(TD|TABLE|TH|CAPTION)/.test(bn) || (sb && bn == "DIV" && /left|right/gi.test(dom.getStyle(sb, 'float', 1)))) {
\r
13592 // Setup new before and after blocks
\r
13593 bef = (sb && sb.nodeName == bn) ? sb.cloneNode(0) : ed.dom.create(bn);
\r
13594 aft = (eb && eb.nodeName == bn) ? eb.cloneNode(0) : ed.dom.create(bn);
\r
13596 // Remove id from after clone
\r
13597 aft.removeAttribute('id');
\r
13599 // Is header and cursor is at the end, then force paragraph under
\r
13600 if (/^(H[1-6])$/.test(bn) && isAtEnd(r, sb))
\r
13601 aft = ed.dom.create(se.element);
\r
13603 // Find start chop node
\r
13606 if (n == b || n.nodeType == 9 || t.dom.isBlock(n) || /(TD|TABLE|TH|CAPTION)/.test(n.nodeName))
\r
13610 } while ((n = n.previousSibling ? n.previousSibling : n.parentNode));
\r
13612 // Find end chop node
\r
13615 if (n == b || n.nodeType == 9 || t.dom.isBlock(n) || /(TD|TABLE|TH|CAPTION)/.test(n.nodeName))
\r
13619 } while ((n = n.nextSibling ? n.nextSibling : n.parentNode));
\r
13621 // Place first chop part into before block element
\r
13622 if (sc.nodeName == bn)
\r
13623 rb.setStart(sc, 0);
\r
13625 rb.setStartBefore(sc);
\r
13627 rb.setEnd(sn, so);
\r
13628 bef.appendChild(rb.cloneContents() || d.createTextNode('')); // Empty text node needed for Safari
\r
13630 // Place secnd chop part within new block element
\r
13632 ra.setEndAfter(ec);
\r
13634 //console.debug(s.focusNode, s.focusOffset);
\r
13637 ra.setStart(en, eo);
\r
13638 aft.appendChild(ra.cloneContents() || d.createTextNode('')); // Empty text node needed for Safari
\r
13640 // Create range around everything
\r
13641 r = d.createRange();
\r
13642 if (!sc.previousSibling && sc.parentNode.nodeName == bn) {
\r
13643 r.setStartBefore(sc.parentNode);
\r
13645 if (rb.startContainer.nodeName == bn && rb.startOffset == 0)
\r
13646 r.setStartBefore(rb.startContainer);
\r
13648 r.setStart(rb.startContainer, rb.startOffset);
\r
13651 if (!ec.nextSibling && ec.parentNode.nodeName == bn)
\r
13652 r.setEndAfter(ec.parentNode);
\r
13654 r.setEnd(ra.endContainer, ra.endOffset);
\r
13656 // Delete and replace it with new block elements
\r
13657 r.deleteContents();
\r
13660 ed.getWin().scrollTo(0, vp.y);
\r
13662 // Never wrap blocks in blocks
\r
13663 if (bef.firstChild && bef.firstChild.nodeName == bn)
\r
13664 bef.innerHTML = bef.firstChild.innerHTML;
\r
13666 if (aft.firstChild && aft.firstChild.nodeName == bn)
\r
13667 aft.innerHTML = aft.firstChild.innerHTML;
\r
13669 // Padd empty blocks
\r
13670 if (dom.isEmpty(bef))
\r
13671 bef.innerHTML = '<br />';
\r
13673 function appendStyles(e, en) {
\r
13674 var nl = [], nn, n, i;
\r
13676 e.innerHTML = '';
\r
13678 // Make clones of style elements
\r
13679 if (se.keep_styles) {
\r
13682 // We only want style specific elements
\r
13683 if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U)$/.test(n.nodeName)) {
\r
13684 nn = n.cloneNode(FALSE);
\r
13685 dom.setAttrib(nn, 'id', ''); // Remove ID since it needs to be unique
\r
13688 } while (n = n.parentNode);
\r
13691 // Append style elements to aft
\r
13692 if (nl.length > 0) {
\r
13693 for (i = nl.length - 1, nn = e; i >= 0; i--)
\r
13694 nn = nn.appendChild(nl[i]);
\r
13696 // Padd most inner style element
\r
13697 nl[0].innerHTML = isOpera ? '\u00a0' : '<br />'; // Extra space for Opera so that the caret can move there
\r
13698 return nl[0]; // Move caret to most inner element
\r
13700 e.innerHTML = isOpera ? '\u00a0' : '<br />'; // Extra space for Opera so that the caret can move there
\r
13703 // Fill empty afterblook with current style
\r
13704 if (dom.isEmpty(aft))
\r
13705 car = appendStyles(aft, en);
\r
13707 // Opera needs this one backwards for older versions
\r
13708 if (isOpera && parseFloat(opera.version()) < 9.5) {
\r
13709 r.insertNode(bef);
\r
13710 r.insertNode(aft);
\r
13712 r.insertNode(aft);
\r
13713 r.insertNode(bef);
\r
13720 function first(n) {
\r
13721 return d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, FALSE).nextNode() || n;
\r
13724 // Move cursor and scroll into view
\r
13725 r = d.createRange();
\r
13726 r.selectNodeContents(isGecko ? first(car || aft) : car || aft);
\r
13728 s.removeAllRanges();
\r
13731 // scrollIntoView seems to scroll the parent window in most browsers now including FF 3.0b4 so it's time to stop using it and do it our selfs
\r
13732 y = ed.dom.getPos(aft).y;
\r
13733 //ch = aft.clientHeight;
\r
13735 // Is element within viewport
\r
13736 if (y < vp.y || y + 25 > vp.y + vp.h) {
\r
13737 ed.getWin().scrollTo(0, y < vp.y ? y : y - vp.h + 25); // Needs to be hardcoded to roughly one line of text if a huge text block is broken into two blocks
\r
13740 'Element: y=' + y + ', h=' + ch + ', ' +
\r
13741 'Viewport: y=' + vp.y + ", h=" + vp.h + ', bottom=' + (vp.y + vp.h)
\r
13745 ed.undoManager.add();
\r
13750 backspaceDelete : function(e, bs) {
\r
13751 var t = this, ed = t.editor, b = ed.getBody(), dom = ed.dom, n, se = ed.selection, r = se.getRng(), sc = r.startContainer, n, w, tn, walker;
\r
13753 // Delete when caret is behind a element doesn't work correctly on Gecko see #3011651
\r
13754 if (!bs && r.collapsed && sc.nodeType == 1 && r.startOffset == sc.childNodes.length) {
\r
13755 walker = new tinymce.dom.TreeWalker(sc.lastChild, sc);
\r
13757 // Walk the dom backwards until we find a text node
\r
13758 for (n = sc.lastChild; n; n = walker.prev()) {
\r
13759 if (n.nodeType == 3) {
\r
13760 r.setStart(n, n.nodeValue.length);
\r
13761 r.collapse(true);
\r
13768 // The caret sometimes gets stuck in Gecko if you delete empty paragraphs
\r
13769 // This workaround removes the element by hand and moves the caret to the previous element
\r
13770 if (sc && ed.dom.isBlock(sc) && !/^(TD|TH)$/.test(sc.nodeName) && bs) {
\r
13771 if (sc.childNodes.length == 0 || (sc.childNodes.length == 1 && sc.firstChild.nodeName == 'BR')) {
\r
13772 // Find previous block element
\r
13774 while ((n = n.previousSibling) && !ed.dom.isBlock(n)) ;
\r
13777 if (sc != b.firstChild) {
\r
13778 // Find last text node
\r
13779 w = ed.dom.doc.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, FALSE);
\r
13780 while (tn = w.nextNode())
\r
13783 // Place caret at the end of last text node
\r
13784 r = ed.getDoc().createRange();
\r
13785 r.setStart(n, n.nodeValue ? n.nodeValue.length : 0);
\r
13786 r.setEnd(n, n.nodeValue ? n.nodeValue.length : 0);
\r
13789 // Remove the target container
\r
13790 ed.dom.remove(sc);
\r
13793 return Event.cancel(e);
\r
13801 (function(tinymce) {
\r
13803 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, extend = tinymce.extend;
\r
13805 tinymce.create('tinymce.ControlManager', {
\r
13806 ControlManager : function(ed, s) {
\r
13812 t.onAdd = new tinymce.util.Dispatcher(t);
\r
13813 t.onPostRender = new tinymce.util.Dispatcher(t);
\r
13814 t.prefix = s.prefix || ed.id + '_';
\r
13817 t.onPostRender.add(function() {
\r
13818 each(t.controls, function(c) {
\r
13824 get : function(id) {
\r
13825 return this.controls[this.prefix + id] || this.controls[id];
\r
13828 setActive : function(id, s) {
\r
13831 if (c = this.get(id))
\r
13837 setDisabled : function(id, s) {
\r
13840 if (c = this.get(id))
\r
13841 c.setDisabled(s);
\r
13846 add : function(c) {
\r
13850 t.controls[c.id] = c;
\r
13851 t.onAdd.dispatch(c, t);
\r
13857 createControl : function(n) {
\r
13858 var c, t = this, ed = t.editor;
\r
13860 each(ed.plugins, function(p) {
\r
13861 if (p.createControl) {
\r
13862 c = p.createControl(n, t);
\r
13871 case "separator":
\r
13872 return t.createSeparator();
\r
13875 if (!c && ed.buttons && (c = ed.buttons[n]))
\r
13876 return t.createButton(n, c);
\r
13881 createDropMenu : function(id, s, cc) {
\r
13882 var t = this, ed = t.editor, c, bm, v, cls;
\r
13885 'class' : 'mceDropDown',
\r
13886 constrain : ed.settings.constrain_menus
\r
13889 s['class'] = s['class'] + ' ' + ed.getParam('skin') + 'Skin';
\r
13890 if (v = ed.getParam('skin_variant'))
\r
13891 s['class'] += ' ' + ed.getParam('skin') + 'Skin' + v.substring(0, 1).toUpperCase() + v.substring(1);
\r
13893 id = t.prefix + id;
\r
13894 cls = cc || t._cls.dropmenu || tinymce.ui.DropMenu;
\r
13895 c = t.controls[id] = new cls(id, s);
\r
13896 c.onAddItem.add(function(c, o) {
\r
13897 var s = o.settings;
\r
13899 s.title = ed.getLang(s.title, s.title);
\r
13901 if (!s.onclick) {
\r
13902 s.onclick = function(v) {
\r
13904 ed.execCommand(s.cmd, s.ui || false, s.value);
\r
13909 ed.onRemove.add(function() {
\r
13913 // Fix for bug #1897785, #1898007
\r
13914 if (tinymce.isIE) {
\r
13915 c.onShowMenu.add(function() {
\r
13916 // IE 8 needs focus in order to store away a range with the current collapsed caret location
\r
13919 bm = ed.selection.getBookmark(1);
\r
13922 c.onHideMenu.add(function() {
\r
13924 ed.selection.moveToBookmark(bm);
\r
13933 createListBox : function(id, s, cc) {
\r
13934 var t = this, ed = t.editor, cmd, c, cls;
\r
13939 s.title = ed.translate(s.title);
\r
13940 s.scope = s.scope || ed;
\r
13942 if (!s.onselect) {
\r
13943 s.onselect = function(v) {
\r
13944 ed.execCommand(s.cmd, s.ui || false, v || s.value);
\r
13950 'class' : 'mce_' + id,
\r
13952 control_manager : t
\r
13955 id = t.prefix + id;
\r
13957 if (ed.settings.use_native_selects)
\r
13958 c = new tinymce.ui.NativeListBox(id, s);
\r
13960 cls = cc || t._cls.listbox || tinymce.ui.ListBox;
\r
13961 c = new cls(id, s, ed);
\r
13964 t.controls[id] = c;
\r
13966 // Fix focus problem in Safari
\r
13967 if (tinymce.isWebKit) {
\r
13968 c.onPostRender.add(function(c, n) {
\r
13969 // Store bookmark on mousedown
\r
13970 Event.add(n, 'mousedown', function() {
\r
13971 ed.bookmark = ed.selection.getBookmark(1);
\r
13974 // Restore on focus, since it might be lost
\r
13975 Event.add(n, 'focus', function() {
\r
13976 ed.selection.moveToBookmark(ed.bookmark);
\r
13977 ed.bookmark = null;
\r
13983 ed.onMouseDown.add(c.hideMenu, c);
\r
13988 createButton : function(id, s, cc) {
\r
13989 var t = this, ed = t.editor, o, c, cls;
\r
13994 s.title = ed.translate(s.title);
\r
13995 s.label = ed.translate(s.label);
\r
13996 s.scope = s.scope || ed;
\r
13998 if (!s.onclick && !s.menu_button) {
\r
13999 s.onclick = function() {
\r
14000 ed.execCommand(s.cmd, s.ui || false, s.value);
\r
14006 'class' : 'mce_' + id,
\r
14007 unavailable_prefix : ed.getLang('unavailable', ''),
\r
14009 control_manager : t
\r
14012 id = t.prefix + id;
\r
14014 if (s.menu_button) {
\r
14015 cls = cc || t._cls.menubutton || tinymce.ui.MenuButton;
\r
14016 c = new cls(id, s, ed);
\r
14017 ed.onMouseDown.add(c.hideMenu, c);
\r
14019 cls = t._cls.button || tinymce.ui.Button;
\r
14020 c = new cls(id, s);
\r
14026 createMenuButton : function(id, s, cc) {
\r
14028 s.menu_button = 1;
\r
14030 return this.createButton(id, s, cc);
\r
14033 createSplitButton : function(id, s, cc) {
\r
14034 var t = this, ed = t.editor, cmd, c, cls;
\r
14039 s.title = ed.translate(s.title);
\r
14040 s.scope = s.scope || ed;
\r
14042 if (!s.onclick) {
\r
14043 s.onclick = function(v) {
\r
14044 ed.execCommand(s.cmd, s.ui || false, v || s.value);
\r
14048 if (!s.onselect) {
\r
14049 s.onselect = function(v) {
\r
14050 ed.execCommand(s.cmd, s.ui || false, v || s.value);
\r
14056 'class' : 'mce_' + id,
\r
14058 control_manager : t
\r
14061 id = t.prefix + id;
\r
14062 cls = cc || t._cls.splitbutton || tinymce.ui.SplitButton;
\r
14063 c = t.add(new cls(id, s, ed));
\r
14064 ed.onMouseDown.add(c.hideMenu, c);
\r
14069 createColorSplitButton : function(id, s, cc) {
\r
14070 var t = this, ed = t.editor, cmd, c, cls, bm;
\r
14075 s.title = ed.translate(s.title);
\r
14076 s.scope = s.scope || ed;
\r
14078 if (!s.onclick) {
\r
14079 s.onclick = function(v) {
\r
14080 if (tinymce.isIE)
\r
14081 bm = ed.selection.getBookmark(1);
\r
14083 ed.execCommand(s.cmd, s.ui || false, v || s.value);
\r
14087 if (!s.onselect) {
\r
14088 s.onselect = function(v) {
\r
14089 ed.execCommand(s.cmd, s.ui || false, v || s.value);
\r
14095 'class' : 'mce_' + id,
\r
14096 'menu_class' : ed.getParam('skin') + 'Skin',
\r
14098 more_colors_title : ed.getLang('more_colors')
\r
14101 id = t.prefix + id;
\r
14102 cls = cc || t._cls.colorsplitbutton || tinymce.ui.ColorSplitButton;
\r
14103 c = new cls(id, s, ed);
\r
14104 ed.onMouseDown.add(c.hideMenu, c);
\r
14106 // Remove the menu element when the editor is removed
\r
14107 ed.onRemove.add(function() {
\r
14111 // Fix for bug #1897785, #1898007
\r
14112 if (tinymce.isIE) {
\r
14113 c.onShowMenu.add(function() {
\r
14114 // IE 8 needs focus in order to store away a range with the current collapsed caret location
\r
14116 bm = ed.selection.getBookmark(1);
\r
14119 c.onHideMenu.add(function() {
\r
14121 ed.selection.moveToBookmark(bm);
\r
14130 createToolbar : function(id, s, cc) {
\r
14131 var c, t = this, cls;
\r
14133 id = t.prefix + id;
\r
14134 cls = cc || t._cls.toolbar || tinymce.ui.Toolbar;
\r
14135 c = new cls(id, s, t.editor);
\r
14143 createToolbarGroup : function(id, s, cc) {
\r
14144 var c, t = this, cls;
\r
14145 id = t.prefix + id;
\r
14146 cls = cc || this._cls.toolbarGroup || tinymce.ui.ToolbarGroup;
\r
14147 c = new cls(id, s, t.editor);
\r
14155 createSeparator : function(cc) {
\r
14156 var cls = cc || this._cls.separator || tinymce.ui.Separator;
\r
14158 return new cls();
\r
14161 setControlType : function(n, c) {
\r
14162 return this._cls[n.toLowerCase()] = c;
\r
14165 destroy : function() {
\r
14166 each(this.controls, function(c) {
\r
14170 this.controls = null;
\r
14175 (function(tinymce) {
\r
14176 var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each, isIE = tinymce.isIE, isOpera = tinymce.isOpera;
\r
14178 tinymce.create('tinymce.WindowManager', {
\r
14179 WindowManager : function(ed) {
\r
14183 t.onOpen = new Dispatcher(t);
\r
14184 t.onClose = new Dispatcher(t);
\r
14189 open : function(s, p) {
\r
14190 var t = this, f = '', x, y, mo = t.editor.settings.dialog_type == 'modal', w, sw, sh, vp = tinymce.DOM.getViewPort(), u;
\r
14192 // Default some options
\r
14195 sw = isOpera ? vp.w : screen.width; // Opera uses windows inside the Opera window
\r
14196 sh = isOpera ? vp.h : screen.height;
\r
14197 s.name = s.name || 'mc_' + new Date().getTime();
\r
14198 s.width = parseInt(s.width || 320);
\r
14199 s.height = parseInt(s.height || 240);
\r
14200 s.resizable = true;
\r
14201 s.left = s.left || parseInt(sw / 2.0) - (s.width / 2.0);
\r
14202 s.top = s.top || parseInt(sh / 2.0) - (s.height / 2.0);
\r
14203 p.inline = false;
\r
14204 p.mce_width = s.width;
\r
14205 p.mce_height = s.height;
\r
14206 p.mce_auto_focus = s.auto_focus;
\r
14212 s.dialogWidth = s.width + 'px';
\r
14213 s.dialogHeight = s.height + 'px';
\r
14214 s.scroll = s.scrollbars || false;
\r
14218 // Build features string
\r
14219 each(s, function(v, k) {
\r
14220 if (tinymce.is(v, 'boolean'))
\r
14221 v = v ? 'yes' : 'no';
\r
14223 if (!/^(name|url)$/.test(k)) {
\r
14225 f += (f ? ';' : '') + k + ':' + v;
\r
14227 f += (f ? ',' : '') + k + '=' + v;
\r
14233 t.onOpen.dispatch(t, s, p);
\r
14235 u = s.url || s.file;
\r
14236 u = tinymce._addVer(u);
\r
14239 if (isIE && mo) {
\r
14241 window.showModalDialog(u, window, f);
\r
14243 w = window.open(u, s.name, f);
\r
14249 alert(t.editor.getLang('popup_blocked'));
\r
14252 close : function(w) {
\r
14254 this.onClose.dispatch(this);
\r
14257 createInstance : function(cl, a, b, c, d, e) {
\r
14258 var f = tinymce.resolve(cl);
\r
14260 return new f(a, b, c, d, e);
\r
14263 confirm : function(t, cb, s, w) {
\r
14266 cb.call(s || this, w.confirm(this._decode(this.editor.getLang(t, t))));
\r
14269 alert : function(tx, cb, s, w) {
\r
14273 w.alert(t._decode(t.editor.getLang(tx, tx)));
\r
14279 resizeBy : function(dw, dh, win) {
\r
14280 win.resizeBy(dw, dh);
\r
14283 // Internal functions
\r
14285 _decode : function(s) {
\r
14286 return tinymce.DOM.decode(s).replace(/\\n/g, '\n');
\r
14290 (function(tinymce) {
\r
14291 tinymce.Formatter = function(ed) {
\r
14292 var formats = {},
\r
14293 each = tinymce.each,
\r
14295 selection = ed.selection,
\r
14296 TreeWalker = tinymce.dom.TreeWalker,
\r
14297 rangeUtils = new tinymce.dom.RangeUtils(dom),
\r
14298 isValid = ed.schema.isValidChild,
\r
14299 isBlock = dom.isBlock,
\r
14300 forcedRootBlock = ed.settings.forced_root_block,
\r
14301 nodeIndex = dom.nodeIndex,
\r
14302 INVISIBLE_CHAR = '\uFEFF',
\r
14303 MCE_ATTR_RE = /^(src|href|style)$/,
\r
14307 pendingFormats = {apply : [], remove : []};
\r
14309 function isArray(obj) {
\r
14310 return obj instanceof Array;
\r
14313 function getParents(node, selector) {
\r
14314 return dom.getParents(node, selector, dom.getRoot());
\r
14317 function isCaretNode(node) {
\r
14318 return node.nodeType === 1 && (node.face === 'mceinline' || node.style.fontFamily === 'mceinline');
\r
14321 // Public functions
\r
14323 function get(name) {
\r
14324 return name ? formats[name] : formats;
\r
14327 function register(name, format) {
\r
14329 if (typeof(name) !== 'string') {
\r
14330 each(name, function(format, name) {
\r
14331 register(name, format);
\r
14334 // Force format into array and add it to internal collection
\r
14335 format = format.length ? format : [format];
\r
14337 each(format, function(format) {
\r
14338 // Set deep to false by default on selector formats this to avoid removing
\r
14339 // alignment on images inside paragraphs when alignment is changed on paragraphs
\r
14340 if (format.deep === undefined)
\r
14341 format.deep = !format.selector;
\r
14343 // Default to true
\r
14344 if (format.split === undefined)
\r
14345 format.split = !format.selector || format.inline;
\r
14347 // Default to true
\r
14348 if (format.remove === undefined && format.selector && !format.inline)
\r
14349 format.remove = 'none';
\r
14351 // Mark format as a mixed format inline + block level
\r
14352 if (format.selector && format.inline) {
\r
14353 format.mixed = true;
\r
14354 format.block_expand = true;
\r
14357 // Split classes if needed
\r
14358 if (typeof(format.classes) === 'string')
\r
14359 format.classes = format.classes.split(/\s+/);
\r
14362 formats[name] = format;
\r
14367 var getTextDecoration = function(node) {
\r
14370 ed.dom.getParent(node, function(n) {
\r
14371 decoration = ed.dom.getStyle(n, 'text-decoration');
\r
14372 return decoration && decoration !== 'none';
\r
14375 return decoration;
\r
14378 var processUnderlineAndColor = function(node) {
\r
14379 var textDecoration;
\r
14380 if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) {
\r
14381 textDecoration = getTextDecoration(node.parentNode);
\r
14382 if (ed.dom.getStyle(node, 'color') && textDecoration) {
\r
14383 ed.dom.setStyle(node, 'text-decoration', textDecoration);
\r
14384 } else if (ed.dom.getStyle(node, 'textdecoration') === textDecoration) {
\r
14385 ed.dom.setStyle(node, 'text-decoration', null);
\r
14390 function apply(name, vars, node) {
\r
14391 var formatList = get(name), format = formatList[0], bookmark, rng, i, isCollapsed = selection.isCollapsed();
\r
14393 function moveStart(rng) {
\r
14394 var container = rng.startContainer,
\r
14395 offset = rng.startOffset,
\r
14398 // Move startContainer/startOffset in to a suitable node
\r
14399 if (container.nodeType == 1 || container.nodeValue === "") {
\r
14400 container = container.nodeType == 1 ? container.childNodes[offset] : container;
\r
14402 // Might fail if the offset is behind the last element in it's container
\r
14404 walker = new TreeWalker(container, container.parentNode);
\r
14405 for (node = walker.current(); node; node = walker.next()) {
\r
14406 if (node.nodeType == 3 && !isWhiteSpaceNode(node)) {
\r
14407 rng.setStart(node, 0);
\r
14417 function setElementFormat(elm, fmt) {
\r
14418 fmt = fmt || format;
\r
14421 each(fmt.styles, function(value, name) {
\r
14422 dom.setStyle(elm, name, replaceVars(value, vars));
\r
14425 each(fmt.attributes, function(value, name) {
\r
14426 dom.setAttrib(elm, name, replaceVars(value, vars));
\r
14429 each(fmt.classes, function(value) {
\r
14430 value = replaceVars(value, vars);
\r
14432 if (!dom.hasClass(elm, value))
\r
14433 dom.addClass(elm, value);
\r
14438 function applyRngStyle(rng) {
\r
14439 var newWrappers = [], wrapName, wrapElm;
\r
14441 // Setup wrapper element
\r
14442 wrapName = format.inline || format.block;
\r
14443 wrapElm = dom.create(wrapName);
\r
14444 setElementFormat(wrapElm);
\r
14446 rangeUtils.walk(rng, function(nodes) {
\r
14447 var currentWrapElm;
\r
14449 function process(node) {
\r
14450 var nodeName = node.nodeName.toLowerCase(), parentName = node.parentNode.nodeName.toLowerCase(), found;
\r
14452 // Stop wrapping on br elements
\r
14453 if (isEq(nodeName, 'br')) {
\r
14454 currentWrapElm = 0;
\r
14456 // Remove any br elements when we wrap things
\r
14457 if (format.block)
\r
14458 dom.remove(node);
\r
14463 // If node is wrapper type
\r
14464 if (format.wrapper && matchNode(node, name, vars)) {
\r
14465 currentWrapElm = 0;
\r
14469 // Can we rename the block
\r
14470 if (format.block && !format.wrapper && isTextBlock(nodeName)) {
\r
14471 node = dom.rename(node, wrapName);
\r
14472 setElementFormat(node);
\r
14473 newWrappers.push(node);
\r
14474 currentWrapElm = 0;
\r
14478 // Handle selector patterns
\r
14479 if (format.selector) {
\r
14480 // Look for matching formats
\r
14481 each(formatList, function(format) {
\r
14482 // Check collapsed state if it exists
\r
14483 if ('collapsed' in format && format.collapsed !== isCollapsed) {
\r
14487 if (dom.is(node, format.selector) && !isCaretNode(node)) {
\r
14488 setElementFormat(node, format);
\r
14493 // Continue processing if a selector match wasn't found and a inline element is defined
\r
14494 if (!format.inline || found) {
\r
14495 currentWrapElm = 0;
\r
14500 // Is it valid to wrap this item
\r
14501 if (isValid(wrapName, nodeName) && isValid(parentName, wrapName) &&
\r
14502 !(node.nodeType === 3 && node.nodeValue.length === 1 && node.nodeValue.charCodeAt(0) === 65279)) {
\r
14503 // Start wrapping
\r
14504 if (!currentWrapElm) {
\r
14506 currentWrapElm = wrapElm.cloneNode(FALSE);
\r
14507 node.parentNode.insertBefore(currentWrapElm, node);
\r
14508 newWrappers.push(currentWrapElm);
\r
14511 currentWrapElm.appendChild(node);
\r
14513 // Start a new wrapper for possible children
\r
14514 currentWrapElm = 0;
\r
14516 each(tinymce.grep(node.childNodes), process);
\r
14518 // End the last wrapper
\r
14519 currentWrapElm = 0;
\r
14523 // Process siblings from range
\r
14524 each(nodes, process);
\r
14527 // Wrap links inside as well, for example color inside a link when the wrapper is around the link
\r
14528 if (format.wrap_links === false) {
\r
14529 each(newWrappers, function(node) {
\r
14530 function process(node) {
\r
14531 var i, currentWrapElm, children;
\r
14533 if (node.nodeName === 'A') {
\r
14534 currentWrapElm = wrapElm.cloneNode(FALSE);
\r
14535 newWrappers.push(currentWrapElm);
\r
14537 children = tinymce.grep(node.childNodes);
\r
14538 for (i = 0; i < children.length; i++)
\r
14539 currentWrapElm.appendChild(children[i]);
\r
14541 node.appendChild(currentWrapElm);
\r
14544 each(tinymce.grep(node.childNodes), process);
\r
14552 each(newWrappers, function(node) {
\r
14555 function getChildCount(node) {
\r
14558 each(node.childNodes, function(node) {
\r
14559 if (!isWhiteSpaceNode(node) && !isBookmarkNode(node))
\r
14566 function mergeStyles(node) {
\r
14567 var child, clone;
\r
14569 each(node.childNodes, function(node) {
\r
14570 if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) {
\r
14572 return FALSE; // break loop
\r
14576 // If child was found and of the same type as the current node
\r
14577 if (child && matchName(child, format)) {
\r
14578 clone = child.cloneNode(FALSE);
\r
14579 setElementFormat(clone);
\r
14581 dom.replace(clone, node, TRUE);
\r
14582 dom.remove(child, 1);
\r
14585 return clone || node;
\r
14588 childCount = getChildCount(node);
\r
14590 // Remove empty nodes but only if there is multiple wrappers and they are not block
\r
14591 // elements so never remove single <h1></h1> since that would remove the currrent empty block element where the caret is at
\r
14592 if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) {
\r
14593 dom.remove(node, 1);
\r
14597 if (format.inline || format.wrapper) {
\r
14598 // Merges the current node with it's children of similar type to reduce the number of elements
\r
14599 if (!format.exact && childCount === 1)
\r
14600 node = mergeStyles(node);
\r
14602 // Remove/merge children
\r
14603 each(formatList, function(format) {
\r
14604 // Merge all children of similar type will move styles from child to parent
\r
14605 // this: <span style="color:red"><b><span style="color:red; font-size:10px">text</span></b></span>
\r
14606 // will become: <span style="color:red"><b><span style="font-size:10px">text</span></b></span>
\r
14607 each(dom.select(format.inline, node), function(child) {
\r
14610 // When wrap_links is set to false we don't want
\r
14611 // to remove the format on children within links
\r
14612 if (format.wrap_links === false) {
\r
14613 parent = child.parentNode;
\r
14616 if (parent.nodeName === 'A')
\r
14618 } while (parent = parent.parentNode);
\r
14621 removeFormat(format, vars, child, format.exact ? child : null);
\r
14625 // Remove child if direct parent is of same type
\r
14626 if (matchNode(node.parentNode, name, vars)) {
\r
14627 dom.remove(node, 1);
\r
14632 // Look for parent with similar style format
\r
14633 if (format.merge_with_parents) {
\r
14634 dom.getParent(node.parentNode, function(parent) {
\r
14635 if (matchNode(parent, name, vars)) {
\r
14636 dom.remove(node, 1);
\r
14643 // Merge next and previous siblings if they are similar <b>text</b><b>text</b> becomes <b>texttext</b>
\r
14645 node = mergeSiblings(getNonWhiteSpaceSibling(node), node);
\r
14646 node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE));
\r
14654 rng = dom.createRng();
\r
14656 rng.setStartBefore(node);
\r
14657 rng.setEndAfter(node);
\r
14659 applyRngStyle(expandRng(rng, formatList));
\r
14661 if (!isCollapsed || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) {
\r
14662 // Obtain selection node before selection is unselected by applyRngStyle()
\r
14663 var curSelNode = ed.selection.getNode();
\r
14665 // Apply formatting to selection
\r
14666 bookmark = selection.getBookmark();
\r
14667 applyRngStyle(expandRng(selection.getRng(TRUE), formatList));
\r
14669 // Colored nodes should be underlined so that the color of the underline matches the text color.
\r
14670 if (format.styles && (format.styles.color || format.styles.textDecoration)) {
\r
14671 tinymce.walk(curSelNode, processUnderlineAndColor, 'childNodes');
\r
14672 processUnderlineAndColor(curSelNode);
\r
14675 selection.moveToBookmark(bookmark);
\r
14676 selection.setRng(moveStart(selection.getRng(TRUE)));
\r
14677 ed.nodeChanged();
\r
14679 performCaretAction('apply', name, vars);
\r
14684 function remove(name, vars, node) {
\r
14685 var formatList = get(name), format = formatList[0], bookmark, i, rng;
\r
14687 function moveStart(rng) {
\r
14688 var container = rng.startContainer,
\r
14689 offset = rng.startOffset,
\r
14690 walker, node, nodes, tmpNode;
\r
14692 // Convert text node into index if possible
\r
14693 if (container.nodeType == 3 && offset >= container.nodeValue.length - 1) {
\r
14694 container = container.parentNode;
\r
14695 offset = nodeIndex(container) + 1;
\r
14698 // Move startContainer/startOffset in to a suitable node
\r
14699 if (container.nodeType == 1) {
\r
14700 nodes = container.childNodes;
\r
14701 container = nodes[Math.min(offset, nodes.length - 1)];
\r
14702 walker = new TreeWalker(container);
\r
14704 // If offset is at end of the parent node walk to the next one
\r
14705 if (offset > nodes.length - 1)
\r
14708 for (node = walker.current(); node; node = walker.next()) {
\r
14709 if (node.nodeType == 3 && !isWhiteSpaceNode(node)) {
\r
14710 // IE has a "neat" feature where it moves the start node into the closest element
\r
14711 // we can avoid this by inserting an element before it and then remove it after we set the selection
\r
14712 tmpNode = dom.create('a', null, INVISIBLE_CHAR);
\r
14713 node.parentNode.insertBefore(tmpNode, node);
\r
14715 // Set selection and remove tmpNode
\r
14716 rng.setStart(node, 0);
\r
14717 selection.setRng(rng);
\r
14718 dom.remove(tmpNode);
\r
14726 // Merges the styles for each node
\r
14727 function process(node) {
\r
14728 var children, i, l;
\r
14730 // Grab the children first since the nodelist might be changed
\r
14731 children = tinymce.grep(node.childNodes);
\r
14733 // Process current node
\r
14734 for (i = 0, l = formatList.length; i < l; i++) {
\r
14735 if (removeFormat(formatList[i], vars, node, node))
\r
14739 // Process the children
\r
14740 if (format.deep) {
\r
14741 for (i = 0, l = children.length; i < l; i++)
\r
14742 process(children[i]);
\r
14746 function findFormatRoot(container) {
\r
14749 // Find format root
\r
14750 each(getParents(container.parentNode).reverse(), function(parent) {
\r
14753 // Find format root element
\r
14754 if (!formatRoot && parent.id != '_start' && parent.id != '_end') {
\r
14755 // Is the node matching the format we are looking for
\r
14756 format = matchNode(parent, name, vars);
\r
14757 if (format && format.split !== false)
\r
14758 formatRoot = parent;
\r
14762 return formatRoot;
\r
14765 function wrapAndSplit(format_root, container, target, split) {
\r
14766 var parent, clone, lastClone, firstClone, i, formatRootParent;
\r
14768 // Format root found then clone formats and split it
\r
14769 if (format_root) {
\r
14770 formatRootParent = format_root.parentNode;
\r
14772 for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) {
\r
14773 clone = parent.cloneNode(FALSE);
\r
14775 for (i = 0; i < formatList.length; i++) {
\r
14776 if (removeFormat(formatList[i], vars, clone, clone)) {
\r
14782 // Build wrapper node
\r
14785 clone.appendChild(lastClone);
\r
14788 firstClone = clone;
\r
14790 lastClone = clone;
\r
14794 // Never split block elements if the format is mixed
\r
14795 if (split && (!format.mixed || !isBlock(format_root)))
\r
14796 container = dom.split(format_root, container);
\r
14798 // Wrap container in cloned formats
\r
14800 target.parentNode.insertBefore(lastClone, target);
\r
14801 firstClone.appendChild(target);
\r
14805 return container;
\r
14808 function splitToFormatRoot(container) {
\r
14809 return wrapAndSplit(findFormatRoot(container), container, container, true);
\r
14812 function unwrap(start) {
\r
14813 var node = dom.get(start ? '_start' : '_end'),
\r
14814 out = node[start ? 'firstChild' : 'lastChild'];
\r
14816 // If the end is placed within the start the result will be removed
\r
14817 // So this checks if the out node is a bookmark node if it is it
\r
14818 // checks for another more suitable node
\r
14819 if (isBookmarkNode(out))
\r
14820 out = out[start ? 'firstChild' : 'lastChild'];
\r
14822 dom.remove(node, true);
\r
14827 function removeRngStyle(rng) {
\r
14828 var startContainer, endContainer;
\r
14830 rng = expandRng(rng, formatList, TRUE);
\r
14832 if (format.split) {
\r
14833 startContainer = getContainer(rng, TRUE);
\r
14834 endContainer = getContainer(rng);
\r
14836 if (startContainer != endContainer) {
\r
14837 // Wrap start/end nodes in span element since these might be cloned/moved
\r
14838 startContainer = wrap(startContainer, 'span', {id : '_start', 'data-mce-type' : 'bookmark'});
\r
14839 endContainer = wrap(endContainer, 'span', {id : '_end', 'data-mce-type' : 'bookmark'});
\r
14841 // Split start/end
\r
14842 splitToFormatRoot(startContainer);
\r
14843 splitToFormatRoot(endContainer);
\r
14845 // Unwrap start/end to get real elements again
\r
14846 startContainer = unwrap(TRUE);
\r
14847 endContainer = unwrap();
\r
14849 startContainer = endContainer = splitToFormatRoot(startContainer);
\r
14851 // Update range positions since they might have changed after the split operations
\r
14852 rng.startContainer = startContainer.parentNode;
\r
14853 rng.startOffset = nodeIndex(startContainer);
\r
14854 rng.endContainer = endContainer.parentNode;
\r
14855 rng.endOffset = nodeIndex(endContainer) + 1;
\r
14858 // Remove items between start/end
\r
14859 rangeUtils.walk(rng, function(nodes) {
\r
14860 each(nodes, function(node) {
\r
14863 // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined.
\r
14864 if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' && node.parentNode && getTextDecoration(node.parentNode) === 'underline') {
\r
14865 removeFormat({'deep': false, 'exact': true, 'inline': 'span', 'styles': {'textDecoration' : 'underline'}}, null, node);
\r
14873 rng = dom.createRng();
\r
14874 rng.setStartBefore(node);
\r
14875 rng.setEndAfter(node);
\r
14876 removeRngStyle(rng);
\r
14880 if (!selection.isCollapsed() || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) {
\r
14881 bookmark = selection.getBookmark();
\r
14882 removeRngStyle(selection.getRng(TRUE));
\r
14883 selection.moveToBookmark(bookmark);
\r
14885 // Check if start element still has formatting then we are at: "<b>text|</b>text" and need to move the start into the next text node
\r
14886 if (match(name, vars, selection.getStart())) {
\r
14887 moveStart(selection.getRng(true));
\r
14890 ed.nodeChanged();
\r
14892 performCaretAction('remove', name, vars);
\r
14895 function toggle(name, vars, node) {
\r
14896 var fmt = get(name);
\r
14898 if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0]['toggle']))
\r
14899 remove(name, vars, node);
\r
14901 apply(name, vars, node);
\r
14904 function matchNode(node, name, vars, similar) {
\r
14905 var formatList = get(name), format, i, classes;
\r
14907 function matchItems(node, format, item_name) {
\r
14908 var key, value, items = format[item_name], i;
\r
14910 // Check all items
\r
14912 // Non indexed object
\r
14913 if (items.length === undefined) {
\r
14914 for (key in items) {
\r
14915 if (items.hasOwnProperty(key)) {
\r
14916 if (item_name === 'attributes')
\r
14917 value = dom.getAttrib(node, key);
\r
14919 value = getStyle(node, key);
\r
14921 if (similar && !value && !format.exact)
\r
14924 if ((!similar || format.exact) && !isEq(value, replaceVars(items[key], vars)))
\r
14929 // Only one match needed for indexed arrays
\r
14930 for (i = 0; i < items.length; i++) {
\r
14931 if (item_name === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i]))
\r
14940 if (formatList && node) {
\r
14941 // Check each format in list
\r
14942 for (i = 0; i < formatList.length; i++) {
\r
14943 format = formatList[i];
\r
14945 // Name name, attributes, styles and classes
\r
14946 if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) {
\r
14948 if (classes = format.classes) {
\r
14949 for (i = 0; i < classes.length; i++) {
\r
14950 if (!dom.hasClass(node, classes[i]))
\r
14961 function match(name, vars, node) {
\r
14962 var startNode, i;
\r
14964 function matchParents(node) {
\r
14965 // Find first node with similar format settings
\r
14966 node = dom.getParent(node, function(node) {
\r
14967 return !!matchNode(node, name, vars, true);
\r
14970 // Do an exact check on the similar format element
\r
14971 return matchNode(node, name, vars);
\r
14974 // Check specified node
\r
14976 return matchParents(node);
\r
14978 // Check pending formats
\r
14979 if (selection.isCollapsed()) {
\r
14980 for (i = pendingFormats.apply.length - 1; i >= 0; i--) {
\r
14981 if (pendingFormats.apply[i].name == name)
\r
14985 for (i = pendingFormats.remove.length - 1; i >= 0; i--) {
\r
14986 if (pendingFormats.remove[i].name == name)
\r
14990 return matchParents(selection.getNode());
\r
14993 // Check selected node
\r
14994 node = selection.getNode();
\r
14995 if (matchParents(node))
\r
14998 // Check start node if it's different
\r
14999 startNode = selection.getStart();
\r
15000 if (startNode != node) {
\r
15001 if (matchParents(startNode))
\r
15008 function matchAll(names, vars) {
\r
15009 var startElement, matchedFormatNames = [], checkedMap = {}, i, ni, name;
\r
15011 // If the selection is collapsed then check pending formats
\r
15012 if (selection.isCollapsed()) {
\r
15013 for (ni = 0; ni < names.length; ni++) {
\r
15014 // If the name is to be removed, then stop it from being added
\r
15015 for (i = pendingFormats.remove.length - 1; i >= 0; i--) {
\r
15016 name = names[ni];
\r
15018 if (pendingFormats.remove[i].name == name) {
\r
15019 checkedMap[name] = true;
\r
15025 // If the format is to be applied
\r
15026 for (i = pendingFormats.apply.length - 1; i >= 0; i--) {
\r
15027 for (ni = 0; ni < names.length; ni++) {
\r
15028 name = names[ni];
\r
15030 if (!checkedMap[name] && pendingFormats.apply[i].name == name) {
\r
15031 checkedMap[name] = true;
\r
15032 matchedFormatNames.push(name);
\r
15038 // Check start of selection for formats
\r
15039 startElement = selection.getStart();
\r
15040 dom.getParent(startElement, function(node) {
\r
15043 for (i = 0; i < names.length; i++) {
\r
15046 if (!checkedMap[name] && matchNode(node, name, vars)) {
\r
15047 checkedMap[name] = true;
\r
15048 matchedFormatNames.push(name);
\r
15053 return matchedFormatNames;
\r
15056 function canApply(name) {
\r
15057 var formatList = get(name), startNode, parents, i, x, selector;
\r
15059 if (formatList) {
\r
15060 startNode = selection.getStart();
\r
15061 parents = getParents(startNode);
\r
15063 for (x = formatList.length - 1; x >= 0; x--) {
\r
15064 selector = formatList[x].selector;
\r
15066 // Format is not selector based, then always return TRUE
\r
15070 for (i = parents.length - 1; i >= 0; i--) {
\r
15071 if (dom.is(parents[i], selector))
\r
15080 // Expose to public
\r
15081 tinymce.extend(this, {
\r
15083 register : register,
\r
15088 matchAll : matchAll,
\r
15089 matchNode : matchNode,
\r
15090 canApply : canApply
\r
15093 // Private functions
\r
15095 function matchName(node, format) {
\r
15096 // Check for inline match
\r
15097 if (isEq(node, format.inline))
\r
15100 // Check for block match
\r
15101 if (isEq(node, format.block))
\r
15104 // Check for selector match
\r
15105 if (format.selector)
\r
15106 return dom.is(node, format.selector);
\r
15109 function isEq(str1, str2) {
\r
15110 str1 = str1 || '';
\r
15111 str2 = str2 || '';
\r
15113 str1 = '' + (str1.nodeName || str1);
\r
15114 str2 = '' + (str2.nodeName || str2);
\r
15116 return str1.toLowerCase() == str2.toLowerCase();
\r
15119 function getStyle(node, name) {
\r
15120 var styleVal = dom.getStyle(node, name);
\r
15122 // Force the format to hex
\r
15123 if (name == 'color' || name == 'backgroundColor')
\r
15124 styleVal = dom.toHex(styleVal);
\r
15126 // Opera will return bold as 700
\r
15127 if (name == 'fontWeight' && styleVal == 700)
\r
15128 styleVal = 'bold';
\r
15130 return '' + styleVal;
\r
15133 function replaceVars(value, vars) {
\r
15134 if (typeof(value) != "string")
\r
15135 value = value(vars);
\r
15137 value = value.replace(/%(\w+)/g, function(str, name) {
\r
15138 return vars[name] || str;
\r
15145 function isWhiteSpaceNode(node) {
\r
15146 return node && node.nodeType === 3 && /^([\s\r\n]+|)$/.test(node.nodeValue);
\r
15149 function wrap(node, name, attrs) {
\r
15150 var wrapper = dom.create(name, attrs);
\r
15152 node.parentNode.insertBefore(wrapper, node);
\r
15153 wrapper.appendChild(node);
\r
15158 function expandRng(rng, format, remove) {
\r
15159 var startContainer = rng.startContainer,
\r
15160 startOffset = rng.startOffset,
\r
15161 endContainer = rng.endContainer,
\r
15162 endOffset = rng.endOffset, sibling, lastIdx, leaf;
\r
15164 // This function walks up the tree if there is no siblings before/after the node
\r
15165 function findParentContainer(container, child_name, sibling_name, root) {
\r
15166 var parent, child;
\r
15168 root = root || dom.getRoot();
\r
15171 // Check if we can move up are we at root level or body level
\r
15172 parent = container.parentNode;
\r
15174 // Stop expanding on block elements or root depending on format
\r
15175 if (parent == root || (!format[0].block_expand && isBlock(parent)))
\r
15176 return container;
\r
15178 for (sibling = parent[child_name]; sibling && sibling != container; sibling = sibling[sibling_name]) {
\r
15179 if (sibling.nodeType == 1 && !isBookmarkNode(sibling))
\r
15180 return container;
\r
15182 if (sibling.nodeType == 3 && !isWhiteSpaceNode(sibling))
\r
15183 return container;
\r
15186 container = container.parentNode;
\r
15189 return container;
\r
15192 // This function walks down the tree to find the leaf at the selection.
\r
15193 // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node.
\r
15194 function findLeaf(node, offset) {
\r
15195 if (offset === undefined)
\r
15196 offset = node.nodeType === 3 ? node.length : node.childNodes.length;
\r
15197 while (node && node.hasChildNodes()) {
\r
15198 node = node.childNodes[offset];
\r
15200 offset = node.nodeType === 3 ? node.length : node.childNodes.length;
\r
15202 return { node: node, offset: offset };
\r
15205 // If index based start position then resolve it
\r
15206 if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) {
\r
15207 lastIdx = startContainer.childNodes.length - 1;
\r
15208 startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset];
\r
15210 if (startContainer.nodeType == 3)
\r
15214 // If index based end position then resolve it
\r
15215 if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) {
\r
15216 lastIdx = endContainer.childNodes.length - 1;
\r
15217 endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1];
\r
15219 if (endContainer.nodeType == 3)
\r
15220 endOffset = endContainer.nodeValue.length;
\r
15223 // Exclude bookmark nodes if possible
\r
15224 if (isBookmarkNode(startContainer.parentNode))
\r
15225 startContainer = startContainer.parentNode;
\r
15227 if (isBookmarkNode(startContainer))
\r
15228 startContainer = startContainer.nextSibling || startContainer;
\r
15230 if (isBookmarkNode(endContainer.parentNode)) {
\r
15231 endOffset = dom.nodeIndex(endContainer);
\r
15232 endContainer = endContainer.parentNode;
\r
15235 if (isBookmarkNode(endContainer) && endContainer.previousSibling) {
\r
15236 endContainer = endContainer.previousSibling;
\r
15237 endOffset = endContainer.length;
\r
15240 if (format[0].inline) {
\r
15241 // Avoid applying formatting to a trailing space.
\r
15242 leaf = findLeaf(endContainer, endOffset);
\r
15244 while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling)
\r
15245 leaf = findLeaf(leaf.node.previousSibling);
\r
15247 if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 &&
\r
15248 leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') {
\r
15250 if (leaf.offset > 1) {
\r
15251 endContainer = leaf.node;
\r
15252 endContainer.splitText(leaf.offset - 1);
\r
15253 } else if (leaf.node.previousSibling) {
\r
15254 endContainer = leaf.node.previousSibling;
\r
15260 // Move start/end point up the tree if the leaves are sharp and if we are in different containers
\r
15261 // Example * becomes !: !<p><b><i>*text</i><i>text*</i></b></p>!
\r
15262 // This will reduce the number of wrapper elements that needs to be created
\r
15263 // Move start point up the tree
\r
15264 if (format[0].inline || format[0].block_expand) {
\r
15265 startContainer = findParentContainer(startContainer, 'firstChild', 'nextSibling');
\r
15266 endContainer = findParentContainer(endContainer, 'lastChild', 'previousSibling');
\r
15269 // Expand start/end container to matching selector
\r
15270 if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) {
\r
15271 function findSelectorEndPoint(container, sibling_name) {
\r
15272 var parents, i, y, curFormat;
\r
15274 if (container.nodeType == 3 && container.nodeValue.length == 0 && container[sibling_name])
\r
15275 container = container[sibling_name];
\r
15277 parents = getParents(container);
\r
15278 for (i = 0; i < parents.length; i++) {
\r
15279 for (y = 0; y < format.length; y++) {
\r
15280 curFormat = format[y];
\r
15282 // If collapsed state is set then skip formats that doesn't match that
\r
15283 if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed)
\r
15286 if (dom.is(parents[i], curFormat.selector))
\r
15287 return parents[i];
\r
15291 return container;
\r
15294 // Find new startContainer/endContainer if there is better one
\r
15295 startContainer = findSelectorEndPoint(startContainer, 'previousSibling');
\r
15296 endContainer = findSelectorEndPoint(endContainer, 'nextSibling');
\r
15299 // Expand start/end container to matching block element or text node
\r
15300 if (format[0].block || format[0].selector) {
\r
15301 function findBlockEndPoint(container, sibling_name, sibling_name2) {
\r
15304 // Expand to block of similar type
\r
15305 if (!format[0].wrapper)
\r
15306 node = dom.getParent(container, format[0].block);
\r
15308 // Expand to first wrappable block element or any block element
\r
15310 node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, isBlock);
\r
15312 // Exclude inner lists from wrapping
\r
15313 if (node && format[0].wrapper)
\r
15314 node = getParents(node, 'ul,ol').reverse()[0] || node;
\r
15316 // Didn't find a block element look for first/last wrappable element
\r
15318 node = container;
\r
15320 while (node[sibling_name] && !isBlock(node[sibling_name])) {
\r
15321 node = node[sibling_name];
\r
15323 // Break on BR but include it will be removed later on
\r
15324 // we can't remove it now since we need to check if it can be wrapped
\r
15325 if (isEq(node, 'br'))
\r
15330 return node || container;
\r
15333 // Find new startContainer/endContainer if there is better one
\r
15334 startContainer = findBlockEndPoint(startContainer, 'previousSibling');
\r
15335 endContainer = findBlockEndPoint(endContainer, 'nextSibling');
\r
15337 // Non block element then try to expand up the leaf
\r
15338 if (format[0].block) {
\r
15339 if (!isBlock(startContainer))
\r
15340 startContainer = findParentContainer(startContainer, 'firstChild', 'nextSibling');
\r
15342 if (!isBlock(endContainer))
\r
15343 endContainer = findParentContainer(endContainer, 'lastChild', 'previousSibling');
\r
15347 // Setup index for startContainer
\r
15348 if (startContainer.nodeType == 1) {
\r
15349 startOffset = nodeIndex(startContainer);
\r
15350 startContainer = startContainer.parentNode;
\r
15353 // Setup index for endContainer
\r
15354 if (endContainer.nodeType == 1) {
\r
15355 endOffset = nodeIndex(endContainer) + 1;
\r
15356 endContainer = endContainer.parentNode;
\r
15359 // Return new range like object
\r
15361 startContainer : startContainer,
\r
15362 startOffset : startOffset,
\r
15363 endContainer : endContainer,
\r
15364 endOffset : endOffset
\r
15368 function removeFormat(format, vars, node, compare_node) {
\r
15369 var i, attrs, stylesModified;
\r
15371 // Check if node matches format
\r
15372 if (!matchName(node, format))
\r
15375 // Should we compare with format attribs and styles
\r
15376 if (format.remove != 'all') {
\r
15378 each(format.styles, function(value, name) {
\r
15379 value = replaceVars(value, vars);
\r
15382 if (typeof(name) === 'number') {
\r
15384 compare_node = 0;
\r
15387 if (!compare_node || isEq(getStyle(compare_node, name), value))
\r
15388 dom.setStyle(node, name, '');
\r
15390 stylesModified = 1;
\r
15393 // Remove style attribute if it's empty
\r
15394 if (stylesModified && dom.getAttrib(node, 'style') == '') {
\r
15395 node.removeAttribute('style');
\r
15396 node.removeAttribute('data-mce-style');
\r
15399 // Remove attributes
\r
15400 each(format.attributes, function(value, name) {
\r
15403 value = replaceVars(value, vars);
\r
15406 if (typeof(name) === 'number') {
\r
15408 compare_node = 0;
\r
15411 if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) {
\r
15412 // Keep internal classes
\r
15413 if (name == 'class') {
\r
15414 value = dom.getAttrib(node, name);
\r
15416 // Build new class value where everything is removed except the internal prefixed classes
\r
15418 each(value.split(/\s+/), function(cls) {
\r
15419 if (/mce\w+/.test(cls))
\r
15420 valueOut += (valueOut ? ' ' : '') + cls;
\r
15423 // We got some internal classes left
\r
15425 dom.setAttrib(node, name, valueOut);
\r
15431 // IE6 has a bug where the attribute doesn't get removed correctly
\r
15432 if (name == "class")
\r
15433 node.removeAttribute('className');
\r
15435 // Remove mce prefixed attributes
\r
15436 if (MCE_ATTR_RE.test(name))
\r
15437 node.removeAttribute('data-mce-' + name);
\r
15439 node.removeAttribute(name);
\r
15443 // Remove classes
\r
15444 each(format.classes, function(value) {
\r
15445 value = replaceVars(value, vars);
\r
15447 if (!compare_node || dom.hasClass(compare_node, value))
\r
15448 dom.removeClass(node, value);
\r
15451 // Check for non internal attributes
\r
15452 attrs = dom.getAttribs(node);
\r
15453 for (i = 0; i < attrs.length; i++) {
\r
15454 if (attrs[i].nodeName.indexOf('_') !== 0)
\r
15459 // Remove the inline child if it's empty for example <b> or <span>
\r
15460 if (format.remove != 'none') {
\r
15461 removeNode(node, format);
\r
15466 function removeNode(node, format) {
\r
15467 var parentNode = node.parentNode, rootBlockElm;
\r
15469 if (format.block) {
\r
15470 if (!forcedRootBlock) {
\r
15471 function find(node, next, inc) {
\r
15472 node = getNonWhiteSpaceSibling(node, next, inc);
\r
15474 return !node || (node.nodeName == 'BR' || isBlock(node));
\r
15477 // Append BR elements if needed before we remove the block
\r
15478 if (isBlock(node) && !isBlock(parentNode)) {
\r
15479 if (!find(node, FALSE) && !find(node.firstChild, TRUE, 1))
\r
15480 node.insertBefore(dom.create('br'), node.firstChild);
\r
15482 if (!find(node, TRUE) && !find(node.lastChild, FALSE, 1))
\r
15483 node.appendChild(dom.create('br'));
\r
15486 // Wrap the block in a forcedRootBlock if we are at the root of document
\r
15487 if (parentNode == dom.getRoot()) {
\r
15488 if (!format.list_block || !isEq(node, format.list_block)) {
\r
15489 each(tinymce.grep(node.childNodes), function(node) {
\r
15490 if (isValid(forcedRootBlock, node.nodeName.toLowerCase())) {
\r
15491 if (!rootBlockElm)
\r
15492 rootBlockElm = wrap(node, forcedRootBlock);
\r
15494 rootBlockElm.appendChild(node);
\r
15496 rootBlockElm = 0;
\r
15503 // Never remove nodes that isn't the specified inline element if a selector is specified too
\r
15504 if (format.selector && format.inline && !isEq(format.inline, node))
\r
15507 dom.remove(node, 1);
\r
15510 function getNonWhiteSpaceSibling(node, next, inc) {
\r
15512 next = next ? 'nextSibling' : 'previousSibling';
\r
15514 for (node = inc ? node : node[next]; node; node = node[next]) {
\r
15515 if (node.nodeType == 1 || !isWhiteSpaceNode(node))
\r
15521 function isBookmarkNode(node) {
\r
15522 return node && node.nodeType == 1 && node.getAttribute('data-mce-type') == 'bookmark';
\r
15525 function mergeSiblings(prev, next) {
\r
15526 var marker, sibling, tmpSibling;
\r
15528 function compareElements(node1, node2) {
\r
15529 // Not the same name
\r
15530 if (node1.nodeName != node2.nodeName)
\r
15533 function getAttribs(node) {
\r
15534 var attribs = {};
\r
15536 each(dom.getAttribs(node), function(attr) {
\r
15537 var name = attr.nodeName.toLowerCase();
\r
15539 // Don't compare internal attributes or style
\r
15540 if (name.indexOf('_') !== 0 && name !== 'style')
\r
15541 attribs[name] = dom.getAttrib(node, name);
\r
15547 function compareObjects(obj1, obj2) {
\r
15550 for (name in obj1) {
\r
15551 // Obj1 has item obj2 doesn't have
\r
15552 if (obj1.hasOwnProperty(name)) {
\r
15553 value = obj2[name];
\r
15555 // Obj2 doesn't have obj1 item
\r
15556 if (value === undefined)
\r
15559 // Obj2 item has a different value
\r
15560 if (obj1[name] != value)
\r
15563 // Delete similar value
\r
15564 delete obj2[name];
\r
15568 // Check if obj 2 has something obj 1 doesn't have
\r
15569 for (name in obj2) {
\r
15570 // Obj2 has item obj1 doesn't have
\r
15571 if (obj2.hasOwnProperty(name))
\r
15578 // Attribs are not the same
\r
15579 if (!compareObjects(getAttribs(node1), getAttribs(node2)))
\r
15582 // Styles are not the same
\r
15583 if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style'))))
\r
15589 // Check if next/prev exists and that they are elements
\r
15590 if (prev && next) {
\r
15591 function findElementSibling(node, sibling_name) {
\r
15592 for (sibling = node; sibling; sibling = sibling[sibling_name]) {
\r
15593 if (sibling.nodeType == 3 && sibling.nodeValue.length !== 0)
\r
15596 if (sibling.nodeType == 1 && !isBookmarkNode(sibling))
\r
15603 // If previous sibling is empty then jump over it
\r
15604 prev = findElementSibling(prev, 'previousSibling');
\r
15605 next = findElementSibling(next, 'nextSibling');
\r
15607 // Compare next and previous nodes
\r
15608 if (compareElements(prev, next)) {
\r
15609 // Append nodes between
\r
15610 for (sibling = prev.nextSibling; sibling && sibling != next;) {
\r
15611 tmpSibling = sibling;
\r
15612 sibling = sibling.nextSibling;
\r
15613 prev.appendChild(tmpSibling);
\r
15616 // Remove next node
\r
15617 dom.remove(next);
\r
15619 // Move children into prev node
\r
15620 each(tinymce.grep(next.childNodes), function(node) {
\r
15621 prev.appendChild(node);
\r
15631 function isTextBlock(name) {
\r
15632 return /^(h[1-6]|p|div|pre|address|dl|dt|dd)$/.test(name);
\r
15635 function getContainer(rng, start) {
\r
15636 var container, offset, lastIdx;
\r
15638 container = rng[start ? 'startContainer' : 'endContainer'];
\r
15639 offset = rng[start ? 'startOffset' : 'endOffset'];
\r
15641 if (container.nodeType == 1) {
\r
15642 lastIdx = container.childNodes.length - 1;
\r
15644 if (!start && offset)
\r
15647 container = container.childNodes[offset > lastIdx ? lastIdx : offset];
\r
15650 return container;
\r
15653 function performCaretAction(type, name, vars) {
\r
15654 var i, currentPendingFormats = pendingFormats[type],
\r
15655 otherPendingFormats = pendingFormats[type == 'apply' ? 'remove' : 'apply'];
\r
15657 function hasPending() {
\r
15658 return pendingFormats.apply.length || pendingFormats.remove.length;
\r
15661 function resetPending() {
\r
15662 pendingFormats.apply = [];
\r
15663 pendingFormats.remove = [];
\r
15666 function perform(caret_node) {
\r
15667 // Apply pending formats
\r
15668 each(pendingFormats.apply.reverse(), function(item) {
\r
15669 apply(item.name, item.vars, caret_node);
\r
15671 // Colored nodes should be underlined so that the color of the underline matches the text color.
\r
15672 if (item.name === 'forecolor' && item.vars.value)
\r
15673 processUnderlineAndColor(caret_node.parentNode);
\r
15676 // Remove pending formats
\r
15677 each(pendingFormats.remove.reverse(), function(item) {
\r
15678 remove(item.name, item.vars, caret_node);
\r
15681 dom.remove(caret_node, 1);
\r
15685 // Check if it already exists then ignore it
\r
15686 for (i = currentPendingFormats.length - 1; i >= 0; i--) {
\r
15687 if (currentPendingFormats[i].name == name)
\r
15691 currentPendingFormats.push({name : name, vars : vars});
\r
15693 // Check if it's in the other type, then remove it
\r
15694 for (i = otherPendingFormats.length - 1; i >= 0; i--) {
\r
15695 if (otherPendingFormats[i].name == name)
\r
15696 otherPendingFormats.splice(i, 1);
\r
15699 // Pending apply or remove formats
\r
15700 if (hasPending()) {
\r
15701 ed.getDoc().execCommand('FontName', false, 'mceinline');
\r
15702 pendingFormats.lastRng = selection.getRng();
\r
15704 // IE will convert the current word
\r
15705 each(dom.select('font,span'), function(node) {
\r
15708 if (isCaretNode(node)) {
\r
15709 bookmark = selection.getBookmark();
\r
15711 selection.moveToBookmark(bookmark);
\r
15712 ed.nodeChanged();
\r
15716 // Only register listeners once if we need to
\r
15717 if (!pendingFormats.isListening && hasPending()) {
\r
15718 pendingFormats.isListening = true;
\r
15720 each('onKeyDown,onKeyUp,onKeyPress,onMouseUp'.split(','), function(event) {
\r
15721 ed[event].addToTop(function(ed, e) {
\r
15722 // Do we have pending formats and is the selection moved has moved
\r
15723 if (hasPending() && !tinymce.dom.RangeUtils.compareRanges(pendingFormats.lastRng, selection.getRng())) {
\r
15724 each(dom.select('font,span'), function(node) {
\r
15725 var textNode, rng;
\r
15727 // Look for marker
\r
15728 if (isCaretNode(node)) {
\r
15729 textNode = node.firstChild;
\r
15734 rng = dom.createRng();
\r
15735 rng.setStart(textNode, textNode.nodeValue.length);
\r
15736 rng.setEnd(textNode, textNode.nodeValue.length);
\r
15737 selection.setRng(rng);
\r
15738 ed.nodeChanged();
\r
15740 dom.remove(node);
\r
15744 // Always unbind and clear pending styles on keyup
\r
15745 if (e.type == 'keyup' || e.type == 'mouseup')
\r
15756 tinymce.onAddEditor.add(function(tinymce, ed) {
\r
15757 var filters, fontSizes, dom, settings = ed.settings;
\r
15759 if (settings.inline_styles) {
\r
15760 fontSizes = tinymce.explode(settings.font_size_style_values);
\r
15762 function replaceWithSpan(node, styles) {
\r
15763 tinymce.each(styles, function(value, name) {
\r
15765 dom.setStyle(node, name, value);
\r
15768 dom.rename(node, 'span');
\r
15772 font : function(dom, node) {
\r
15773 replaceWithSpan(node, {
\r
15774 backgroundColor : node.style.backgroundColor,
\r
15775 color : node.color,
\r
15776 fontFamily : node.face,
\r
15777 fontSize : fontSizes[parseInt(node.size) - 1]
\r
15781 u : function(dom, node) {
\r
15782 replaceWithSpan(node, {
\r
15783 textDecoration : 'underline'
\r
15787 strike : function(dom, node) {
\r
15788 replaceWithSpan(node, {
\r
15789 textDecoration : 'line-through'
\r
15794 function convert(editor, params) {
\r
15795 dom = editor.dom;
\r
15797 if (settings.convert_fonts_to_spans) {
\r
15798 tinymce.each(dom.select('font,u,strike', params.node), function(node) {
\r
15799 filters[node.nodeName.toLowerCase()](ed.dom, node);
\r
15804 ed.onPreProcess.add(convert);
\r
15805 ed.onSetContent.add(convert);
\r
15807 ed.onInit.add(function() {
\r
15808 ed.selection.onSetContent.add(convert);
\r