2 var whiteSpaceRe = /^\s*|\s*$/g,
\r
3 undefined, isRegExpBroken = 'B'.replace(/A(.)|B/, '$1') === '$1';
\r
8 minorVersion : '4.9',
\r
10 releaseDate : '2012-02-23',
\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.isIE7 = t.isIE && /MSIE [7]/.test(ua);
\r
25 t.isIE8 = t.isIE && /MSIE [8]/.test(ua);
\r
27 t.isIE9 = t.isIE && /MSIE [9]/.test(ua);
\r
29 t.isGecko = !t.isWebKit && /Gecko/.test(ua);
\r
31 t.isMac = ua.indexOf('Mac') != -1;
\r
33 t.isAir = /adobeair/i.test(ua);
\r
35 t.isIDevice = /(iPad|iPhone)/.test(ua);
\r
37 t.isIOS5 = t.isIDevice && ua.match(/AppleWebKit\/(\d*)/)[1]>=534;
\r
39 // TinyMCE .NET webcontrol might be setting the values for TinyMCE
\r
40 if (win.tinyMCEPreInit) {
\r
41 t.suffix = tinyMCEPreInit.suffix;
\r
42 t.baseURL = tinyMCEPreInit.base;
\r
43 t.query = tinyMCEPreInit.query;
\r
47 // Get suffix and base
\r
50 // If base element found, add that infront of baseURL
\r
51 nl = d.getElementsByTagName('base');
\r
52 for (i=0; i<nl.length; i++) {
\r
53 if (v = nl[i].href) {
\r
54 // Host only value like http://site.com or http://site.com:8008
\r
55 if (/^https?:\/\/[^\/]+$/.test(v))
\r
58 base = v ? v.match(/.*\//)[0] : ''; // Get only directory
\r
62 function getBase(n) {
\r
63 if (n.src && /tiny_mce(|_gzip|_jquery|_prototype|_full)(_dev|_src)?.js/.test(n.src)) {
\r
64 if (/_(src|dev)\.js/g.test(n.src))
\r
67 if ((p = n.src.indexOf('?')) != -1)
\r
68 t.query = n.src.substring(p + 1);
\r
70 t.baseURL = n.src.substring(0, n.src.lastIndexOf('/'));
\r
72 // If path to script is relative and a base href was found add that one infront
\r
73 // the src property will always be an absolute one on non IE browsers and IE 8
\r
74 // so this logic will basically only be executed on older IE versions
\r
75 if (base && t.baseURL.indexOf('://') == -1 && t.baseURL.indexOf('/') !== 0)
\r
76 t.baseURL = base + t.baseURL;
\r
85 nl = d.getElementsByTagName('script');
\r
86 for (i=0; i<nl.length; i++) {
\r
92 n = d.getElementsByTagName('head')[0];
\r
94 nl = n.getElementsByTagName('script');
\r
95 for (i=0; i<nl.length; i++) {
\r
104 is : function(o, t) {
\r
106 return o !== undefined;
\r
108 if (t == 'array' && (o.hasOwnProperty && o instanceof Array))
\r
111 return typeof(o) == t;
\r
114 makeMap : function(items, delim, map) {
\r
117 items = items || [];
\r
118 delim = delim || ',';
\r
120 if (typeof(items) == "string")
\r
121 items = items.split(delim);
\r
127 map[items[i]] = {};
\r
132 each : function(o, cb, s) {
\r
140 if (o.length !== undefined) {
\r
141 // Indexed arrays, needed for Safari
\r
142 for (n=0, l = o.length; n < l; n++) {
\r
143 if (cb.call(s, o[n], n, o) === false)
\r
149 if (o.hasOwnProperty(n)) {
\r
150 if (cb.call(s, o[n], n, o) === false)
\r
160 map : function(a, f) {
\r
163 tinymce.each(a, function(v) {
\r
170 grep : function(a, f) {
\r
173 tinymce.each(a, function(v) {
\r
181 inArray : function(a, v) {
\r
185 for (i = 0, l = a.length; i < l; i++) {
\r
194 extend : function(o, e) {
\r
195 var i, l, a = arguments;
\r
197 for (i = 1, l = a.length; i < l; i++) {
\r
200 tinymce.each(e, function(v, n) {
\r
201 if (v !== undefined)
\r
210 trim : function(s) {
\r
211 return (s ? '' + s : '').replace(whiteSpaceRe, '');
\r
214 create : function(s, p, root) {
\r
215 var t = this, sp, ns, cn, scn, c, de = 0;
\r
217 // Parse : <prefix> <class>:<super class>
\r
218 s = /^((static) )?([\w.]+)(:([\w.]+))?/.exec(s);
\r
219 cn = s[3].match(/(^|\.)(\w+)$/i)[2]; // Class name
\r
221 // Create namespace for new class
\r
222 ns = t.createNS(s[3].replace(/\.\w+$/, ''), root);
\r
224 // Class already exists
\r
228 // Make pure static class
\r
229 if (s[2] == 'static') {
\r
233 this.onCreate(s[2], s[3], ns[cn]);
\r
238 // Create default constructor
\r
240 p[cn] = function() {};
\r
244 // Add constructor and methods
\r
246 t.extend(ns[cn].prototype, p);
\r
250 sp = t.resolve(s[5]).prototype;
\r
251 scn = s[5].match(/\.(\w+)$/i)[1]; // Class name
\r
253 // Extend constructor
\r
256 // Add passthrough constructor
\r
257 ns[cn] = function() {
\r
258 return sp[scn].apply(this, arguments);
\r
261 // Add inherit constructor
\r
262 ns[cn] = function() {
\r
263 this.parent = sp[scn];
\r
264 return c.apply(this, arguments);
\r
267 ns[cn].prototype[cn] = ns[cn];
\r
269 // Add super methods
\r
270 t.each(sp, function(f, n) {
\r
271 ns[cn].prototype[n] = sp[n];
\r
274 // Add overridden methods
\r
275 t.each(p, function(f, n) {
\r
276 // Extend methods if needed
\r
278 ns[cn].prototype[n] = function() {
\r
279 this.parent = sp[n];
\r
280 return f.apply(this, arguments);
\r
284 ns[cn].prototype[n] = f;
\r
289 // Add static methods
\r
290 t.each(p['static'], function(f, n) {
\r
295 this.onCreate(s[2], s[3], ns[cn].prototype);
\r
298 walk : function(o, f, n, s) {
\r
305 tinymce.each(o, function(o, i) {
\r
306 if (f.call(s, o, i, n) === false)
\r
309 tinymce.walk(o, f, n, s);
\r
314 createNS : function(n, o) {
\r
320 for (i=0; i<n.length; i++) {
\r
332 resolve : function(n, o) {
\r
338 for (i = 0, l = n.length; i < l; i++) {
\r
348 addUnload : function(f, s) {
\r
351 f = {func : f, scope : s || this};
\r
354 function unload() {
\r
355 var li = t.unloads, o, n;
\r
358 // Call unload handlers
\r
363 o.func.call(o.scope, 1); // Send in one arg to distinct unload and user destroy
\r
366 // Detach unload function
\r
367 if (win.detachEvent) {
\r
368 win.detachEvent('onbeforeunload', fakeUnload);
\r
369 win.detachEvent('onunload', unload);
\r
370 } else if (win.removeEventListener)
\r
371 win.removeEventListener('unload', unload, false);
\r
373 // Destroy references
\r
374 t.unloads = o = li = w = unload = 0;
\r
376 // Run garbarge collector on IE
\r
377 if (win.CollectGarbage)
\r
382 function fakeUnload() {
\r
385 // Is there things still loading, then do some magic
\r
386 if (d.readyState == 'interactive') {
\r
388 // Prevent memory leak
\r
389 d.detachEvent('onstop', stop);
\r
391 // Call unload handler
\r
398 // Fire unload when the currently loading page is stopped
\r
400 d.attachEvent('onstop', stop);
\r
402 // Remove onstop listener after a while to prevent the unload function
\r
403 // to execute if the user presses cancel in an onbeforeunload
\r
404 // confirm dialog and then presses the browser stop button
\r
405 win.setTimeout(function() {
\r
407 d.detachEvent('onstop', stop);
\r
412 // Attach unload handler
\r
413 if (win.attachEvent) {
\r
414 win.attachEvent('onunload', unload);
\r
415 win.attachEvent('onbeforeunload', fakeUnload);
\r
416 } else if (win.addEventListener)
\r
417 win.addEventListener('unload', unload, false);
\r
419 // Setup initial unload handler array
\r
427 removeUnload : function(f) {
\r
428 var u = this.unloads, r = null;
\r
430 tinymce.each(u, function(o, i) {
\r
431 if (o && o.func == f) {
\r
441 explode : function(s, d) {
\r
442 return s ? tinymce.map(s.split(d || ','), tinymce.trim) : s;
\r
445 _addVer : function(u) {
\r
451 v = (u.indexOf('?') == -1 ? '?' : '&') + this.query;
\r
453 if (u.indexOf('#') == -1)
\r
456 return u.replace('#', v + '#');
\r
459 // Fix function for IE 9 where regexps isn't working correctly
\r
460 // Todo: remove me once MS fixes the bug
\r
461 _replace : function(find, replace, str) {
\r
462 // On IE9 we have to fake $x replacement
\r
463 if (isRegExpBroken) {
\r
464 return str.replace(find, function() {
\r
465 var val = replace, args = arguments, i;
\r
467 for (i = 0; i < args.length - 2; i++) {
\r
468 if (args[i] === undefined) {
\r
469 val = val.replace(new RegExp('\\$' + i, 'g'), '');
\r
471 val = val.replace(new RegExp('\\$' + i, 'g'), args[i]);
\r
479 return str.replace(find, replace);
\r
484 // Initialize the API
\r
487 // Expose tinymce namespace to the global namespace (window)
\r
488 win.tinymce = win.tinyMCE = tinymce;
\r
490 // Describe the different namespaces
\r
496 tinymce.create('tinymce.util.Dispatcher', {
\r
500 Dispatcher : function(s) {
\r
501 this.scope = s || this;
\r
502 this.listeners = [];
\r
505 add : function(cb, s) {
\r
506 this.listeners.push({cb : cb, scope : s || this.scope});
\r
511 addToTop : function(cb, s) {
\r
512 this.listeners.unshift({cb : cb, scope : s || this.scope});
\r
517 remove : function(cb) {
\r
518 var l = this.listeners, o = null;
\r
520 tinymce.each(l, function(c, i) {
\r
531 dispatch : function() {
\r
532 var s, a = arguments, i, li = this.listeners, c;
\r
534 // Needs to be a real loop since the listener count might change while looping
\r
535 // And this is also more efficient
\r
536 for (i = 0; i<li.length; i++) {
\r
538 s = c.cb.apply(c.scope, a.length > 0 ? a : [c.scope]);
\r
550 var each = tinymce.each;
\r
552 tinymce.create('tinymce.util.URI', {
\r
553 URI : function(u, s) {
\r
554 var t = this, o, a, b, base_url;
\r
557 u = tinymce.trim(u);
\r
559 // Default settings
\r
560 s = t.settings = s || {};
\r
562 // Strange app protocol that isn't http/https or local anchor
\r
563 // For example: mailto,skype,tel etc.
\r
564 if (/^([\w\-]+):([^\/]{2})/i.test(u) || /^\s*#/.test(u)) {
\r
569 // Absolute path with no host, fake host and protocol
\r
570 if (u.indexOf('/') === 0 && u.indexOf('//') !== 0)
\r
571 u = (s.base_uri ? s.base_uri.protocol || 'http' : 'http') + '://mce_host' + u;
\r
573 // Relative path http:// or protocol relative //path
\r
574 if (!/^[\w-]*:?\/\//.test(u)) {
\r
575 base_url = s.base_uri ? s.base_uri.path : new tinymce.util.URI(location.href).directory;
\r
576 u = ((s.base_uri && s.base_uri.protocol) || 'http') + '://mce_host' + t.toAbsPath(base_url, u);
\r
579 // Parse URL (Credits goes to Steave, http://blog.stevenlevithan.com/archives/parseuri)
\r
580 u = u.replace(/@@/g, '(mce_at)'); // Zope 3 workaround, they use @@something
\r
581 u = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(u);
\r
582 each(["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], function(v, i) {
\r
585 // Zope 3 workaround, they use @@something
\r
587 s = s.replace(/\(mce_at\)/g, '@@');
\r
592 if (b = s.base_uri) {
\r
594 t.protocol = b.protocol;
\r
597 t.userInfo = b.userInfo;
\r
599 if (!t.port && t.host == 'mce_host')
\r
602 if (!t.host || t.host == 'mce_host')
\r
608 //t.path = t.path || '/';
\r
611 setPath : function(p) {
\r
614 p = /^(.*?)\/?(\w+)?$/.exec(p);
\r
616 // Update path parts
\r
618 t.directory = p[1];
\r
626 toRelative : function(u) {
\r
632 u = new tinymce.util.URI(u, {base_uri : t});
\r
634 // Not on same domain/port or protocol
\r
635 if ((u.host != 'mce_host' && t.host != u.host && u.host) || t.port != u.port || t.protocol != u.protocol)
\r
638 o = t.toRelPath(t.path, u.path);
\r
642 o += '?' + u.query;
\r
646 o += '#' + u.anchor;
\r
651 toAbsolute : function(u, nh) {
\r
652 var u = new tinymce.util.URI(u, {base_uri : this});
\r
654 return u.getURI(this.host == u.host && this.protocol == u.protocol ? nh : 0);
\r
657 toRelPath : function(base, path) {
\r
658 var items, bp = 0, out = '', i, l;
\r
661 base = base.substring(0, base.lastIndexOf('/'));
\r
662 base = base.split('/');
\r
663 items = path.split('/');
\r
665 if (base.length >= items.length) {
\r
666 for (i = 0, l = base.length; i < l; i++) {
\r
667 if (i >= items.length || base[i] != items[i]) {
\r
674 if (base.length < items.length) {
\r
675 for (i = 0, l = items.length; i < l; i++) {
\r
676 if (i >= base.length || base[i] != items[i]) {
\r
686 for (i = 0, l = base.length - (bp - 1); i < l; i++)
\r
689 for (i = bp - 1, l = items.length; i < l; i++) {
\r
691 out += "/" + items[i];
\r
699 toAbsPath : function(base, path) {
\r
700 var i, nb = 0, o = [], tr, outPath;
\r
703 tr = /\/$/.test(path) ? '/' : '';
\r
704 base = base.split('/');
\r
705 path = path.split('/');
\r
707 // Remove empty chunks
\r
708 each(base, function(k) {
\r
715 // Merge relURLParts chunks
\r
716 for (i = path.length - 1, o = []; i >= 0; i--) {
\r
717 // Ignore empty or .
\r
718 if (path[i].length == 0 || path[i] == ".")
\r
722 if (path[i] == '..') {
\r
736 i = base.length - nb;
\r
740 outPath = o.reverse().join('/');
\r
742 outPath = base.slice(0, i).join('/') + '/' + o.reverse().join('/');
\r
744 // Add front / if it's needed
\r
745 if (outPath.indexOf('/') !== 0)
\r
746 outPath = '/' + outPath;
\r
748 // Add traling / if it's needed
\r
749 if (tr && outPath.lastIndexOf('/') !== outPath.length - 1)
\r
755 getURI : function(nh) {
\r
759 if (!t.source || nh) {
\r
764 s += t.protocol + '://';
\r
767 s += t.userInfo + '@';
\r
780 s += '?' + t.query;
\r
783 s += '#' + t.anchor;
\r
794 var each = tinymce.each;
\r
796 tinymce.create('static tinymce.util.Cookie', {
\r
797 getHash : function(n) {
\r
798 var v = this.get(n), h;
\r
801 each(v.split('&'), function(v) {
\r
804 h[unescape(v[0])] = unescape(v[1]);
\r
811 setHash : function(n, v, e, p, d, s) {
\r
814 each(v, function(v, k) {
\r
815 o += (!o ? '' : '&') + escape(k) + '=' + escape(v);
\r
818 this.set(n, o, e, p, d, s);
\r
821 get : function(n) {
\r
822 var c = document.cookie, e, p = n + "=", b;
\r
828 b = c.indexOf("; " + p);
\r
838 e = c.indexOf(";", b);
\r
843 return unescape(c.substring(b + p.length, e));
\r
846 set : function(n, v, e, p, d, s) {
\r
847 document.cookie = n + "=" + escape(v) +
\r
848 ((e) ? "; expires=" + e.toGMTString() : "") +
\r
849 ((p) ? "; path=" + escape(p) : "") +
\r
850 ((d) ? "; domain=" + d : "") +
\r
851 ((s) ? "; secure" : "");
\r
854 remove : function(n, p) {
\r
855 var d = new Date();
\r
857 d.setTime(d.getTime() - 1000);
\r
859 this.set(n, '', d, p, d);
\r
865 function serialize(o, quote) {
\r
868 quote = quote || '"';
\r
875 if (t == 'string') {
\r
876 v = '\bb\tt\nn\ff\rr\""\'\'\\\\';
\r
878 return quote + o.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g, function(a, b) {
\r
879 // Make sure single quotes never get encoded inside double quotes for JSON compatibility
\r
880 if (quote === '"' && a === "'")
\r
886 return '\\' + v.charAt(i + 1);
\r
888 a = b.charCodeAt().toString(16);
\r
890 return '\\u' + '0000'.substring(a.length) + a;
\r
894 if (t == 'object') {
\r
895 if (o.hasOwnProperty && o instanceof Array) {
\r
896 for (i=0, v = '['; i<o.length; i++)
\r
897 v += (i > 0 ? ',' : '') + serialize(o[i], quote);
\r
905 if (o.hasOwnProperty(i)) {
\r
906 v += typeof o[i] != 'function' ? (v.length > 1 ? ',' + quote : quote) + i + quote +':' + serialize(o[i], quote) : '';
\r
916 tinymce.util.JSON = {
\r
917 serialize: serialize,
\r
919 parse: function(s) {
\r
921 return eval('(' + s + ')');
\r
930 tinymce.create('static tinymce.util.XHR', {
\r
931 send : function(o) {
\r
932 var x, t, w = window, c = 0;
\r
934 // Default settings
\r
935 o.scope = o.scope || this;
\r
936 o.success_scope = o.success_scope || o.scope;
\r
937 o.error_scope = o.error_scope || o.scope;
\r
938 o.async = o.async === false ? false : true;
\r
939 o.data = o.data || '';
\r
945 x = new ActiveXObject(s);
\r
952 x = w.XMLHttpRequest ? new XMLHttpRequest() : get('Microsoft.XMLHTTP') || get('Msxml2.XMLHTTP');
\r
955 if (x.overrideMimeType)
\r
956 x.overrideMimeType(o.content_type);
\r
958 x.open(o.type || (o.data ? 'POST' : 'GET'), o.url, o.async);
\r
960 if (o.content_type)
\r
961 x.setRequestHeader('Content-Type', o.content_type);
\r
963 x.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
\r
968 if (!o.async || x.readyState == 4 || c++ > 10000) {
\r
969 if (o.success && c < 10000 && x.status == 200)
\r
970 o.success.call(o.success_scope, '' + x.responseText, x, o);
\r
972 o.error.call(o.error_scope, c > 10000 ? 'TIMED_OUT' : 'GENERAL', x, o);
\r
976 w.setTimeout(ready, 10);
\r
979 // Syncronous request
\r
983 // Wait for response, onReadyStateChange can not be used since it leaks memory in IE
\r
984 t = w.setTimeout(ready, 10);
\r
990 var extend = tinymce.extend, JSON = tinymce.util.JSON, XHR = tinymce.util.XHR;
\r
992 tinymce.create('tinymce.util.JSONRequest', {
\r
993 JSONRequest : function(s) {
\r
994 this.settings = extend({
\r
999 send : function(o) {
\r
1000 var ecb = o.error, scb = o.success;
\r
1002 o = extend(this.settings, o);
\r
1004 o.success = function(c, x) {
\r
1005 c = JSON.parse(c);
\r
1007 if (typeof(c) == 'undefined') {
\r
1009 error : 'JSON Parse error.'
\r
1014 ecb.call(o.error_scope || o.scope, c.error, x);
\r
1016 scb.call(o.success_scope || o.scope, c.result);
\r
1019 o.error = function(ty, x) {
\r
1021 ecb.call(o.error_scope || o.scope, ty, x);
\r
1024 o.data = JSON.serialize({
\r
1025 id : o.id || 'c' + (this.count++),
\r
1026 method : o.method,
\r
1030 // JSON content type for Ruby on rails. Bug: #1883287
\r
1031 o.content_type = 'application/json';
\r
1037 sendRPC : function(o) {
\r
1038 return new tinymce.util.JSONRequest().send(o);
\r
1043 (function(tinymce){
\r
1052 modifierPressed: function (e) {
\r
1053 return e.shiftKey || e.ctrlKey || e.altKey;
\r
1058 (function(tinymce) {
\r
1059 var VK = tinymce.VK, BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE;
\r
1061 function cleanupStylesWhenDeleting(ed) {
\r
1062 var dom = ed.dom, selection = ed.selection;
\r
1064 ed.onKeyDown.add(function(ed, e) {
\r
1065 var rng, blockElm, node, clonedSpan, isDelete;
\r
1067 isDelete = e.keyCode == DELETE;
\r
1068 if ((isDelete || e.keyCode == BACKSPACE) && !VK.modifierPressed(e)) {
\r
1069 e.preventDefault();
\r
1070 rng = selection.getRng();
\r
1072 // Find root block
\r
1073 blockElm = dom.getParent(rng.startContainer, dom.isBlock);
\r
1075 // On delete clone the root span of the next block element
\r
1077 blockElm = dom.getNext(blockElm, dom.isBlock);
\r
1079 // Locate root span element and clone it since it would otherwise get merged by the "apple-style-span" on delete/backspace
\r
1081 node = blockElm.firstChild;
\r
1083 // Ignore empty text nodes
\r
1084 while (node && node.nodeType == 3 && node.nodeValue.length == 0)
\r
1085 node = node.nextSibling;
\r
1087 if (node && node.nodeName === 'SPAN') {
\r
1088 clonedSpan = node.cloneNode(false);
\r
1092 // Do the backspace/delete action
\r
1093 ed.getDoc().execCommand(isDelete ? 'ForwardDelete' : 'Delete', false, null);
\r
1095 // Find all odd apple-style-spans
\r
1096 blockElm = dom.getParent(rng.startContainer, dom.isBlock);
\r
1097 tinymce.each(dom.select('span.Apple-style-span,font.Apple-style-span', blockElm), function(span) {
\r
1098 var bm = selection.getBookmark();
\r
1101 dom.replace(clonedSpan.cloneNode(false), span, true);
\r
1103 dom.remove(span, true);
\r
1106 // Restore the selection
\r
1107 selection.moveToBookmark(bm);
\r
1113 function emptyEditorWhenDeleting(ed) {
\r
1115 function serializeRng(rng) {
\r
1116 var body = ed.dom.create("body");
\r
1117 var contents = rng.cloneContents();
\r
1118 body.appendChild(contents);
\r
1119 return ed.selection.serializer.serialize(body, {format: 'html'});
\r
1122 function allContentsSelected(rng) {
\r
1123 var selection = serializeRng(rng);
\r
1125 var allRng = ed.dom.createRng();
\r
1126 allRng.selectNode(ed.getBody());
\r
1128 var allSelection = serializeRng(allRng);
\r
1129 return selection === allSelection;
\r
1132 ed.onKeyDown.addToTop(function(ed, e) {
\r
1133 var keyCode = e.keyCode;
\r
1134 if (keyCode == DELETE || keyCode == BACKSPACE) {
\r
1135 var rng = ed.selection.getRng(true);
\r
1136 if (!rng.collapsed && allContentsSelected(rng)) {
\r
1137 ed.setContent('', {format : 'raw'});
\r
1139 e.preventDefault();
\r
1146 function inputMethodFocus(ed) {
\r
1147 ed.dom.bind(ed.getDoc(), 'focusin', function() {
\r
1148 ed.selection.setRng(ed.selection.getRng());
\r
1152 function removeHrOnBackspace(ed) {
\r
1153 ed.onKeyDown.add(function(ed, e) {
\r
1154 if (e.keyCode === BACKSPACE) {
\r
1155 if (ed.selection.isCollapsed() && ed.selection.getRng(true).startOffset === 0) {
\r
1156 var node = ed.selection.getNode();
\r
1157 var previousSibling = node.previousSibling;
\r
1158 if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "hr") {
\r
1159 ed.dom.remove(previousSibling);
\r
1160 tinymce.dom.Event.cancel(e);
\r
1167 function focusBody(ed) {
\r
1168 // Fix for a focus bug in FF 3.x where the body element
\r
1169 // wouldn't get proper focus if the user clicked on the HTML element
\r
1170 if (!Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4
\r
1171 ed.onMouseDown.add(function(ed, e) {
\r
1172 if (e.target.nodeName === "HTML") {
\r
1173 var body = ed.getBody();
\r
1175 // Blur the body it's focused but not correctly focused
\r
1178 // Refocus the body after a little while
\r
1179 setTimeout(function() {
\r
1187 function selectControlElements(ed) {
\r
1188 ed.onClick.add(function(ed, e) {
\r
1191 // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250
\r
1192 // WebKit can't even do simple things like selecting an image
\r
1193 // Needs tobe the setBaseAndExtend or it will fail to select floated images
\r
1194 if (/^(IMG|HR)$/.test(e.nodeName))
\r
1195 ed.selection.getSel().setBaseAndExtent(e, 0, e, 1);
\r
1197 if (e.nodeName == 'A' && ed.dom.hasClass(e, 'mceItemAnchor'))
\r
1198 ed.selection.select(e);
\r
1204 function removeStylesOnPTagsInheritedFromHeadingTag(ed) {
\r
1205 ed.onKeyDown.add(function(ed, event) {
\r
1206 function checkInHeadingTag(ed) {
\r
1207 var currentNode = ed.selection.getNode();
\r
1208 var headingTags = 'h1,h2,h3,h4,h5,h6';
\r
1209 return ed.dom.is(currentNode, headingTags) || ed.dom.getParent(currentNode, headingTags) !== null;
\r
1212 if (event.keyCode === VK.ENTER && !VK.modifierPressed(event) && checkInHeadingTag(ed)) {
\r
1213 setTimeout(function() {
\r
1214 var currentNode = ed.selection.getNode();
\r
1215 if (ed.dom.is(currentNode, 'p')) {
\r
1216 ed.dom.setAttrib(currentNode, 'style', null);
\r
1217 // While tiny's content is correct after this method call, the content shown is not representative of it and needs to be 'repainted'
\r
1218 ed.execCommand('mceCleanup');
\r
1224 function selectionChangeNodeChanged(ed) {
\r
1225 var lastRng, selectionTimer;
\r
1227 ed.dom.bind(ed.getDoc(), 'selectionchange', function() {
\r
1228 if (selectionTimer) {
\r
1229 clearTimeout(selectionTimer);
\r
1230 selectionTimer = 0;
\r
1233 selectionTimer = window.setTimeout(function() {
\r
1234 var rng = ed.selection.getRng();
\r
1236 // Compare the ranges to see if it was a real change or not
\r
1237 if (!lastRng || !tinymce.dom.RangeUtils.compareRanges(rng, lastRng)) {
\r
1245 function ensureBodyHasRoleApplication(ed) {
\r
1246 document.body.setAttribute("role", "application");
\r
1249 tinymce.create('tinymce.util.Quirks', {
\r
1250 Quirks: function(ed) {
\r
1252 if (tinymce.isWebKit) {
\r
1253 cleanupStylesWhenDeleting(ed);
\r
1254 emptyEditorWhenDeleting(ed);
\r
1255 inputMethodFocus(ed);
\r
1256 selectControlElements(ed);
\r
1259 if (tinymce.isIDevice) {
\r
1260 selectionChangeNodeChanged(ed);
\r
1265 if (tinymce.isIE) {
\r
1266 removeHrOnBackspace(ed);
\r
1267 emptyEditorWhenDeleting(ed);
\r
1268 ensureBodyHasRoleApplication(ed);
\r
1269 removeStylesOnPTagsInheritedFromHeadingTag(ed)
\r
1273 if (tinymce.isGecko) {
\r
1274 removeHrOnBackspace(ed);
\r
1281 (function(tinymce) {
\r
1282 var namedEntities, baseEntities, reverseEntities,
\r
1283 attrsCharsRegExp = /[&<>\"\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
\r
1284 textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
\r
1285 rawCharsRegExp = /[<>&\"\']/g,
\r
1286 entityRegExp = /&(#x|#)?([\w]+);/g,
\r
1288 128 : "\u20AC", 130 : "\u201A", 131 : "\u0192", 132 : "\u201E", 133 : "\u2026", 134 : "\u2020",
\r
1289 135 : "\u2021", 136 : "\u02C6", 137 : "\u2030", 138 : "\u0160", 139 : "\u2039", 140 : "\u0152",
\r
1290 142 : "\u017D", 145 : "\u2018", 146 : "\u2019", 147 : "\u201C", 148 : "\u201D", 149 : "\u2022",
\r
1291 150 : "\u2013", 151 : "\u2014", 152 : "\u02DC", 153 : "\u2122", 154 : "\u0161", 155 : "\u203A",
\r
1292 156 : "\u0153", 158 : "\u017E", 159 : "\u0178"
\r
1297 '\"' : '"', // Needs to be escaped since the YUI compressor would otherwise break the code
\r
1304 // Reverse lookup table for raw entities
\r
1305 reverseEntities = {
\r
1313 // Decodes text by using the browser
\r
1314 function nativeDecode(text) {
\r
1317 elm = document.createElement("div");
\r
1318 elm.innerHTML = text;
\r
1320 return elm.textContent || elm.innerText || text;
\r
1323 // Build a two way lookup table for the entities
\r
1324 function buildEntitiesLookup(items, radix) {
\r
1325 var i, chr, entity, lookup = {};
\r
1328 items = items.split(',');
\r
1329 radix = radix || 10;
\r
1331 // Build entities lookup table
\r
1332 for (i = 0; i < items.length; i += 2) {
\r
1333 chr = String.fromCharCode(parseInt(items[i], radix));
\r
1335 // Only add non base entities
\r
1336 if (!baseEntities[chr]) {
\r
1337 entity = '&' + items[i + 1] + ';';
\r
1338 lookup[chr] = entity;
\r
1339 lookup[entity] = chr;
\r
1347 // Unpack entities lookup where the numbers are in radix 32 to reduce the size
\r
1348 namedEntities = buildEntitiesLookup(
\r
1349 '50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' +
\r
1350 '5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' +
\r
1351 '5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' +
\r
1352 '5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' +
\r
1353 '68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' +
\r
1354 '6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' +
\r
1355 '6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' +
\r
1356 '75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' +
\r
1357 '7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' +
\r
1358 '7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' +
\r
1359 'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' +
\r
1360 'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' +
\r
1361 't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' +
\r
1362 'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' +
\r
1363 'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' +
\r
1364 '81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' +
\r
1365 '8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' +
\r
1366 '8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' +
\r
1367 '8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' +
\r
1368 '8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' +
\r
1369 'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' +
\r
1370 'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' +
\r
1371 'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' +
\r
1372 '80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' +
\r
1373 '811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro'
\r
1376 tinymce.html = tinymce.html || {};
\r
1378 tinymce.html.Entities = {
\r
1379 encodeRaw : function(text, attr) {
\r
1380 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
\r
1381 return baseEntities[chr] || chr;
\r
1385 encodeAllRaw : function(text) {
\r
1386 return ('' + text).replace(rawCharsRegExp, function(chr) {
\r
1387 return baseEntities[chr] || chr;
\r
1391 encodeNumeric : function(text, attr) {
\r
1392 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
\r
1393 // Multi byte sequence convert it to a single entity
\r
1394 if (chr.length > 1)
\r
1395 return '&#' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';';
\r
1397 return baseEntities[chr] || '&#' + chr.charCodeAt(0) + ';';
\r
1401 encodeNamed : function(text, attr, entities) {
\r
1402 entities = entities || namedEntities;
\r
1404 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
\r
1405 return baseEntities[chr] || entities[chr] || chr;
\r
1409 getEncodeFunc : function(name, entities) {
\r
1410 var Entities = tinymce.html.Entities;
\r
1412 entities = buildEntitiesLookup(entities) || namedEntities;
\r
1414 function encodeNamedAndNumeric(text, attr) {
\r
1415 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
\r
1416 return baseEntities[chr] || entities[chr] || '&#' + chr.charCodeAt(0) + ';' || chr;
\r
1420 function encodeCustomNamed(text, attr) {
\r
1421 return Entities.encodeNamed(text, attr, entities);
\r
1424 // Replace + with , to be compatible with previous TinyMCE versions
\r
1425 name = tinymce.makeMap(name.replace(/\+/g, ','));
\r
1427 // Named and numeric encoder
\r
1428 if (name.named && name.numeric)
\r
1429 return encodeNamedAndNumeric;
\r
1435 return encodeCustomNamed;
\r
1437 return Entities.encodeNamed;
\r
1442 return Entities.encodeNumeric;
\r
1445 return Entities.encodeRaw;
\r
1448 decode : function(text) {
\r
1449 return text.replace(entityRegExp, function(all, numeric, value) {
\r
1451 value = parseInt(value, numeric.length === 2 ? 16 : 10);
\r
1453 // Support upper UTF
\r
1454 if (value > 0xFFFF) {
\r
1457 return String.fromCharCode(0xD800 + (value >> 10), 0xDC00 + (value & 0x3FF));
\r
1459 return asciiMap[value] || String.fromCharCode(value);
\r
1462 return reverseEntities[all] || namedEntities[all] || nativeDecode(all);
\r
1468 tinymce.html.Styles = function(settings, schema) {
\r
1469 var rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi,
\r
1470 urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi,
\r
1471 styleRegExp = /\s*([^:]+):\s*([^;]+);?/g,
\r
1472 trimRightRegExp = /\s+$/,
\r
1473 urlColorRegExp = /rgb/,
\r
1474 undef, i, encodingLookup = {}, encodingItems;
\r
1476 settings = settings || {};
\r
1478 encodingItems = '\\" \\\' \\; \\: ; : \uFEFF'.split(' ');
\r
1479 for (i = 0; i < encodingItems.length; i++) {
\r
1480 encodingLookup[encodingItems[i]] = '\uFEFF' + i;
\r
1481 encodingLookup['\uFEFF' + i] = encodingItems[i];
\r
1484 function toHex(match, r, g, b) {
\r
1485 function hex(val) {
\r
1486 val = parseInt(val).toString(16);
\r
1488 return val.length > 1 ? val : '0' + val; // 0 -> 00
\r
1491 return '#' + hex(r) + hex(g) + hex(b);
\r
1495 toHex : function(color) {
\r
1496 return color.replace(rgbRegExp, toHex);
\r
1499 parse : function(css) {
\r
1500 var styles = {}, matches, name, value, isEncoded, urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope || this;
\r
1502 function compress(prefix, suffix) {
\r
1503 var top, right, bottom, left;
\r
1505 // Get values and check it it needs compressing
\r
1506 top = styles[prefix + '-top' + suffix];
\r
1510 right = styles[prefix + '-right' + suffix];
\r
1514 bottom = styles[prefix + '-bottom' + suffix];
\r
1515 if (right != bottom)
\r
1518 left = styles[prefix + '-left' + suffix];
\r
1519 if (bottom != left)
\r
1523 styles[prefix + suffix] = left;
\r
1524 delete styles[prefix + '-top' + suffix];
\r
1525 delete styles[prefix + '-right' + suffix];
\r
1526 delete styles[prefix + '-bottom' + suffix];
\r
1527 delete styles[prefix + '-left' + suffix];
\r
1530 function canCompress(key) {
\r
1531 var value = styles[key], i;
\r
1533 if (!value || value.indexOf(' ') < 0)
\r
1536 value = value.split(' ');
\r
1539 if (value[i] !== value[0])
\r
1543 styles[key] = value[0];
\r
1548 function compress2(target, a, b, c) {
\r
1549 if (!canCompress(a))
\r
1552 if (!canCompress(b))
\r
1555 if (!canCompress(c))
\r
1559 styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c];
\r
1565 // Encodes the specified string by replacing all \" \' ; : with _<num>
\r
1566 function encode(str) {
\r
1569 return encodingLookup[str];
\r
1572 // Decodes the specified string by replacing all _<num> with it's original value \" \' etc
\r
1573 // It will also decode the \" \' if keep_slashes is set to fale or omitted
\r
1574 function decode(str, keep_slashes) {
\r
1576 str = str.replace(/\uFEFF[0-9]/g, function(str) {
\r
1577 return encodingLookup[str];
\r
1581 if (!keep_slashes)
\r
1582 str = str.replace(/\\([\'\";:])/g, "$1");
\r
1588 // Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing
\r
1589 css = css.replace(/\\[\"\';:\uFEFF]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, function(str) {
\r
1590 return str.replace(/[;:]/g, encode);
\r
1594 while (matches = styleRegExp.exec(css)) {
\r
1595 name = matches[1].replace(trimRightRegExp, '').toLowerCase();
\r
1596 value = matches[2].replace(trimRightRegExp, '');
\r
1598 if (name && value.length > 0) {
\r
1599 // Opera will produce 700 instead of bold in their style values
\r
1600 if (name === 'font-weight' && value === '700')
\r
1602 else if (name === 'color' || name === 'background-color') // Lowercase colors like RED
\r
1603 value = value.toLowerCase();
\r
1605 // Convert RGB colors to HEX
\r
1606 value = value.replace(rgbRegExp, toHex);
\r
1608 // Convert URLs and force them into url('value') format
\r
1609 value = value.replace(urlOrStrRegExp, function(match, url, url2, url3, str, str2) {
\r
1610 str = str || str2;
\r
1613 str = decode(str);
\r
1615 // Force strings into single quote format
\r
1616 return "'" + str.replace(/\'/g, "\\'") + "'";
\r
1619 url = decode(url || url2 || url3);
\r
1621 // Convert the URL to relative/absolute depending on config
\r
1623 url = urlConverter.call(urlConverterScope, url, 'style');
\r
1625 // Output new URL format
\r
1626 return "url('" + url.replace(/\'/g, "\\'") + "')";
\r
1629 styles[name] = isEncoded ? decode(value, true) : value;
\r
1632 styleRegExp.lastIndex = matches.index + matches[0].length;
\r
1635 // Compress the styles to reduce it's size for example IE will expand styles
\r
1636 compress("border", "");
\r
1637 compress("border", "-width");
\r
1638 compress("border", "-color");
\r
1639 compress("border", "-style");
\r
1640 compress("padding", "");
\r
1641 compress("margin", "");
\r
1642 compress2('border', 'border-width', 'border-style', 'border-color');
\r
1644 // Remove pointless border, IE produces these
\r
1645 if (styles.border === 'medium none')
\r
1646 delete styles.border;
\r
1652 serialize : function(styles, element_name) {
\r
1653 var css = '', name, value;
\r
1655 function serializeStyles(name) {
\r
1656 var styleList, i, l, value;
\r
1658 styleList = schema.styles[name];
\r
1660 for (i = 0, l = styleList.length; i < l; i++) {
\r
1661 name = styleList[i];
\r
1662 value = styles[name];
\r
1664 if (value !== undef && value.length > 0)
\r
1665 css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';';
\r
1670 // Serialize styles according to schema
\r
1671 if (element_name && schema && schema.styles) {
\r
1672 // Serialize global styles and element specific styles
\r
1673 serializeStyles('*');
\r
1674 serializeStyles(element_name);
\r
1676 // Output the styles in the order they are inside the object
\r
1677 for (name in styles) {
\r
1678 value = styles[name];
\r
1680 if (value !== undef && value.length > 0)
\r
1681 css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';';
\r
1690 (function(tinymce) {
\r
1691 var transitional = {}, boolAttrMap, blockElementsMap, shortEndedElementsMap, nonEmptyElementsMap, customElementsMap = {},
\r
1692 defaultWhiteSpaceElementsMap, selfClosingElementsMap, makeMap = tinymce.makeMap, each = tinymce.each;
\r
1694 function split(str, delim) {
\r
1695 return str.split(delim || ',');
\r
1698 function unpack(lookup, data) {
\r
1699 var key, elements = {};
\r
1701 function replace(value) {
\r
1702 return value.replace(/[A-Z]+/g, function(key) {
\r
1703 return replace(lookup[key]);
\r
1708 for (key in lookup) {
\r
1709 if (lookup.hasOwnProperty(key))
\r
1710 lookup[key] = replace(lookup[key]);
\r
1713 // Unpack and parse data into object map
\r
1714 replace(data).replace(/#/g, '#text').replace(/(\w+)\[([^\]]+)\]\[([^\]]*)\]/g, function(str, name, attributes, children) {
\r
1715 attributes = split(attributes, '|');
\r
1717 elements[name] = {
\r
1718 attributes : makeMap(attributes),
\r
1719 attributesOrder : attributes,
\r
1720 children : makeMap(children, '|', {'#comment' : {}})
\r
1727 // Build a lookup table for block elements both lowercase and uppercase
\r
1728 blockElementsMap = 'h1,h2,h3,h4,h5,h6,hr,p,div,address,pre,form,table,tbody,thead,tfoot,' +
\r
1729 'th,tr,td,li,ol,ul,caption,blockquote,center,dl,dt,dd,dir,fieldset,' +
\r
1730 'noscript,menu,isindex,samp,header,footer,article,section,hgroup';
\r
1731 blockElementsMap = makeMap(blockElementsMap, ',', makeMap(blockElementsMap.toUpperCase()));
\r
1733 // This is the XHTML 1.0 transitional elements with it's attributes and children packed to reduce it's size
\r
1734 transitional = unpack({
\r
1737 ZG : 'E|span|width|align|char|charoff|valign',
\r
1738 X : 'p|T|div|U|W|isindex|fieldset|table',
\r
1739 ZF : 'E|align|char|charoff|valign',
\r
1740 W : 'pre|hr|blockquote|address|center|noframes',
\r
1741 ZE : 'abbr|axis|headers|scope|rowspan|colspan|align|char|charoff|valign|nowrap|bgcolor|width|height',
\r
1743 U : 'ul|ol|dl|menu|dir',
\r
1744 ZC : 'p|Y|div|U|W|table|br|span|bdo|object|applet|img|map|K|N|Q',
\r
1745 T : 'h1|h2|h3|h4|h5|h6',
\r
1748 ZA : 'a|G|J|M|O|P',
\r
1751 P : 'ins|del|script',
\r
1752 O : 'input|select|textarea|label|button',
\r
1754 M : 'em|strong|dfn|code|q|samp|kbd|var|cite|abbr|acronym',
\r
1757 J : 'tt|i|b|u|s|strike',
\r
1758 I : 'big|small|font|basefont',
\r
1760 G : 'br|span|bdo',
\r
1761 F : 'object|applet|img|map|iframe',
\r
1763 D : 'accesskey|tabindex|onfocus|onblur',
\r
1764 C : 'onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup',
\r
1765 B : 'lang|xml:lang|dir',
\r
1766 A : 'id|class|style|title'
\r
1767 }, 'script[id|charset|type|language|src|defer|xml:space][]' +
\r
1768 'style[B|id|type|media|title|xml:space][]' +
\r
1769 'object[E|declare|classid|codebase|data|type|codetype|archive|standby|width|height|usemap|name|tabindex|align|border|hspace|vspace][#|param|Y]' +
\r
1770 'param[id|name|value|valuetype|type][]' +
\r
1771 'p[E|align][#|S]' +
\r
1772 'a[E|D|charset|type|name|href|hreflang|rel|rev|shape|coords|target][#|Z]' +
\r
1773 'br[A|clear][]' +
\r
1775 'bdo[A|C|B][#|S]' +
\r
1776 'applet[A|codebase|archive|code|object|alt|name|width|height|align|hspace|vspace][#|param|Y]' +
\r
1777 'h1[E|align][#|S]' +
\r
1778 'img[E|src|alt|name|longdesc|width|height|usemap|ismap|align|border|hspace|vspace][]' +
\r
1779 'map[B|C|A|name][X|form|Q|area]' +
\r
1780 'h2[E|align][#|S]' +
\r
1781 'iframe[A|longdesc|name|src|frameborder|marginwidth|marginheight|scrolling|align|width|height][#|Y]' +
\r
1782 'h3[E|align][#|S]' +
\r
1788 'strike[E][#|S]' +
\r
1790 'small[E][#|S]' +
\r
1791 'font[A|B|size|color|face][#|S]' +
\r
1792 'basefont[id|size|color|face][]' +
\r
1794 'strong[E][#|S]' +
\r
1797 'q[E|cite][#|S]' +
\r
1803 'acronym[E][#|S]' +
\r
1806 'input[E|D|type|name|value|checked|disabled|readonly|size|maxlength|src|alt|usemap|onselect|onchange|accept|align][]' +
\r
1807 'select[E|name|size|multiple|disabled|tabindex|onfocus|onblur|onchange][optgroup|option]' +
\r
1808 'optgroup[E|disabled|label][option]' +
\r
1809 'option[E|selected|disabled|label|value][]' +
\r
1810 'textarea[E|D|name|rows|cols|disabled|readonly|onselect|onchange][]' +
\r
1811 'label[E|for|accesskey|onfocus|onblur][#|S]' +
\r
1812 'button[E|D|name|value|type|disabled][#|p|T|div|U|W|table|G|object|applet|img|map|K|N|Q]' +
\r
1813 'h4[E|align][#|S]' +
\r
1814 'ins[E|cite|datetime][#|Y]' +
\r
1815 'h5[E|align][#|S]' +
\r
1816 'del[E|cite|datetime][#|Y]' +
\r
1817 'h6[E|align][#|S]' +
\r
1818 'div[E|align][#|Y]' +
\r
1819 'ul[E|type|compact][li]' +
\r
1820 'li[E|type|value][#|Y]' +
\r
1821 'ol[E|type|compact|start][li]' +
\r
1822 'dl[E|compact][dt|dd]' +
\r
1825 'menu[E|compact][li]' +
\r
1826 'dir[E|compact][li]' +
\r
1827 'pre[E|width|xml:space][#|ZA]' +
\r
1828 'hr[E|align|noshade|size|width][]' +
\r
1829 'blockquote[E|cite][#|Y]' +
\r
1830 'address[E][#|S|p]' +
\r
1831 'center[E][#|Y]' +
\r
1832 'noframes[E][#|Y]' +
\r
1833 'isindex[A|B|prompt][]' +
\r
1834 'fieldset[E][#|legend|Y]' +
\r
1835 'legend[E|accesskey|align][#|S]' +
\r
1836 'table[E|summary|width|border|frame|rules|cellspacing|cellpadding|align|bgcolor][caption|col|colgroup|thead|tfoot|tbody|tr]' +
\r
1837 'caption[E|align][#|S]' +
\r
1839 'colgroup[ZG][col]' +
\r
1840 'thead[ZF][tr]' +
\r
1841 'tr[ZF|bgcolor][th|td]' +
\r
1842 'th[E|ZE][#|Y]' +
\r
1843 'form[E|action|method|name|enctype|onsubmit|onreset|accept|accept-charset|target][#|X|R|Q]' +
\r
1844 'noscript[E][#|Y]' +
\r
1845 'td[E|ZE][#|Y]' +
\r
1846 'tfoot[ZF][tr]' +
\r
1847 'tbody[ZF][tr]' +
\r
1848 'area[E|D|shape|coords|href|nohref|alt|target][]' +
\r
1849 'base[id|href|target][]' +
\r
1850 'body[E|onload|onunload|background|bgcolor|text|link|vlink|alink][#|Y]'
\r
1853 boolAttrMap = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected,autoplay,loop,controls');
\r
1854 shortEndedElementsMap = makeMap('area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed,source');
\r
1855 nonEmptyElementsMap = tinymce.extend(makeMap('td,th,iframe,video,audio,object'), shortEndedElementsMap);
\r
1856 defaultWhiteSpaceElementsMap = makeMap('pre,script,style,textarea');
\r
1857 selfClosingElementsMap = makeMap('colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr');
\r
1859 tinymce.html.Schema = function(settings) {
\r
1860 var self = this, elements = {}, children = {}, patternElements = [], validStyles, whiteSpaceElementsMap;
\r
1862 settings = settings || {};
\r
1864 // Allow all elements and attributes if verify_html is set to false
\r
1865 if (settings.verify_html === false)
\r
1866 settings.valid_elements = '*[*]';
\r
1868 // Build styles list
\r
1869 if (settings.valid_styles) {
\r
1872 // Convert styles into a rule list
\r
1873 each(settings.valid_styles, function(value, key) {
\r
1874 validStyles[key] = tinymce.explode(value);
\r
1878 whiteSpaceElementsMap = settings.whitespace_elements ? makeMap(settings.whitespace_elements) : defaultWhiteSpaceElementsMap;
\r
1880 // Converts a wildcard expression string to a regexp for example *a will become /.*a/.
\r
1881 function patternToRegExp(str) {
\r
1882 return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$');
\r
1885 // Parses the specified valid_elements string and adds to the current rules
\r
1886 // This function is a bit hard to read since it's heavily optimized for speed
\r
1887 function addValidElements(valid_elements) {
\r
1888 var ei, el, ai, al, yl, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder,
\r
1889 prefix, outputName, globalAttributes, globalAttributesOrder, transElement, key, childKey, value,
\r
1890 elementRuleRegExp = /^([#+-])?([^\[\/]+)(?:\/([^\[]+))?(?:\[([^\]]+)\])?$/,
\r
1891 attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/,
\r
1892 hasPatternsRegExp = /[*?+]/;
\r
1894 if (valid_elements) {
\r
1895 // Split valid elements into an array with rules
\r
1896 valid_elements = split(valid_elements);
\r
1898 if (elements['@']) {
\r
1899 globalAttributes = elements['@'].attributes;
\r
1900 globalAttributesOrder = elements['@'].attributesOrder;
\r
1904 for (ei = 0, el = valid_elements.length; ei < el; ei++) {
\r
1905 // Parse element rule
\r
1906 matches = elementRuleRegExp.exec(valid_elements[ei]);
\r
1908 // Setup local names for matches
\r
1909 prefix = matches[1];
\r
1910 elementName = matches[2];
\r
1911 outputName = matches[3];
\r
1912 attrData = matches[4];
\r
1914 // Create new attributes and attributesOrder
\r
1916 attributesOrder = [];
\r
1918 // Create the new element
\r
1920 attributes : attributes,
\r
1921 attributesOrder : attributesOrder
\r
1924 // Padd empty elements prefix
\r
1925 if (prefix === '#')
\r
1926 element.paddEmpty = true;
\r
1928 // Remove empty elements prefix
\r
1929 if (prefix === '-')
\r
1930 element.removeEmpty = true;
\r
1932 // Copy attributes from global rule into current rule
\r
1933 if (globalAttributes) {
\r
1934 for (key in globalAttributes)
\r
1935 attributes[key] = globalAttributes[key];
\r
1937 attributesOrder.push.apply(attributesOrder, globalAttributesOrder);
\r
1940 // Attributes defined
\r
1942 attrData = split(attrData, '|');
\r
1943 for (ai = 0, al = attrData.length; ai < al; ai++) {
\r
1944 matches = attrRuleRegExp.exec(attrData[ai]);
\r
1947 attrType = matches[1];
\r
1948 attrName = matches[2].replace(/::/g, ':');
\r
1949 prefix = matches[3];
\r
1950 value = matches[4];
\r
1953 if (attrType === '!') {
\r
1954 element.attributesRequired = element.attributesRequired || [];
\r
1955 element.attributesRequired.push(attrName);
\r
1956 attr.required = true;
\r
1959 // Denied from global
\r
1960 if (attrType === '-') {
\r
1961 delete attributes[attrName];
\r
1962 attributesOrder.splice(tinymce.inArray(attributesOrder, attrName), 1);
\r
1969 if (prefix === '=') {
\r
1970 element.attributesDefault = element.attributesDefault || [];
\r
1971 element.attributesDefault.push({name: attrName, value: value});
\r
1972 attr.defaultValue = value;
\r
1976 if (prefix === ':') {
\r
1977 element.attributesForced = element.attributesForced || [];
\r
1978 element.attributesForced.push({name: attrName, value: value});
\r
1979 attr.forcedValue = value;
\r
1982 // Required values
\r
1983 if (prefix === '<')
\r
1984 attr.validValues = makeMap(value, '?');
\r
1987 // Check for attribute patterns
\r
1988 if (hasPatternsRegExp.test(attrName)) {
\r
1989 element.attributePatterns = element.attributePatterns || [];
\r
1990 attr.pattern = patternToRegExp(attrName);
\r
1991 element.attributePatterns.push(attr);
\r
1993 // Add attribute to order list if it doesn't already exist
\r
1994 if (!attributes[attrName])
\r
1995 attributesOrder.push(attrName);
\r
1997 attributes[attrName] = attr;
\r
2003 // Global rule, store away these for later usage
\r
2004 if (!globalAttributes && elementName == '@') {
\r
2005 globalAttributes = attributes;
\r
2006 globalAttributesOrder = attributesOrder;
\r
2009 // Handle substitute elements such as b/strong
\r
2011 element.outputName = elementName;
\r
2012 elements[outputName] = element;
\r
2015 // Add pattern or exact element
\r
2016 if (hasPatternsRegExp.test(elementName)) {
\r
2017 element.pattern = patternToRegExp(elementName);
\r
2018 patternElements.push(element);
\r
2020 elements[elementName] = element;
\r
2026 function setValidElements(valid_elements) {
\r
2028 patternElements = [];
\r
2030 addValidElements(valid_elements);
\r
2032 each(transitional, function(element, name) {
\r
2033 children[name] = element.children;
\r
2037 // Adds custom non HTML elements to the schema
\r
2038 function addCustomElements(custom_elements) {
\r
2039 var customElementRegExp = /^(~)?(.+)$/;
\r
2041 if (custom_elements) {
\r
2042 each(split(custom_elements), function(rule) {
\r
2043 var matches = customElementRegExp.exec(rule),
\r
2044 inline = matches[1] === '~',
\r
2045 cloneName = inline ? 'span' : 'div',
\r
2046 name = matches[2];
\r
2048 children[name] = children[cloneName];
\r
2049 customElementsMap[name] = cloneName;
\r
2051 // If it's not marked as inline then add it to valid block elements
\r
2053 blockElementsMap[name] = {};
\r
2055 // Add custom elements at span/div positions
\r
2056 each(children, function(element, child) {
\r
2057 if (element[cloneName])
\r
2058 element[name] = element[cloneName];
\r
2064 // Adds valid children to the schema object
\r
2065 function addValidChildren(valid_children) {
\r
2066 var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/;
\r
2068 if (valid_children) {
\r
2069 each(split(valid_children), function(rule) {
\r
2070 var matches = childRuleRegExp.exec(rule), parent, prefix;
\r
2073 prefix = matches[1];
\r
2075 // Add/remove items from default
\r
2077 parent = children[matches[2]];
\r
2079 parent = children[matches[2]] = {'#comment' : {}};
\r
2081 parent = children[matches[2]];
\r
2083 each(split(matches[3], '|'), function(child) {
\r
2084 if (prefix === '-')
\r
2085 delete parent[child];
\r
2087 parent[child] = {};
\r
2094 function getElementRule(name) {
\r
2095 var element = elements[name], i;
\r
2097 // Exact match found
\r
2101 // No exact match then try the patterns
\r
2102 i = patternElements.length;
\r
2104 element = patternElements[i];
\r
2106 if (element.pattern.test(name))
\r
2111 if (!settings.valid_elements) {
\r
2112 // No valid elements defined then clone the elements from the transitional spec
\r
2113 each(transitional, function(element, name) {
\r
2114 elements[name] = {
\r
2115 attributes : element.attributes,
\r
2116 attributesOrder : element.attributesOrder
\r
2119 children[name] = element.children;
\r
2123 each(split('strong/b,em/i'), function(item) {
\r
2124 item = split(item, '/');
\r
2125 elements[item[1]].outputName = item[0];
\r
2128 // Add default alt attribute for images
\r
2129 elements.img.attributesDefault = [{name: 'alt', value: ''}];
\r
2131 // Remove these if they are empty by default
\r
2132 each(split('ol,ul,sub,sup,blockquote,span,font,a,table,tbody,tr'), function(name) {
\r
2133 elements[name].removeEmpty = true;
\r
2136 // Padd these by default
\r
2137 each(split('p,h1,h2,h3,h4,h5,h6,th,td,pre,div,address,caption'), function(name) {
\r
2138 elements[name].paddEmpty = true;
\r
2141 setValidElements(settings.valid_elements);
\r
2143 addCustomElements(settings.custom_elements);
\r
2144 addValidChildren(settings.valid_children);
\r
2145 addValidElements(settings.extended_valid_elements);
\r
2147 // Todo: Remove this when we fix list handling to be valid
\r
2148 addValidChildren('+ol[ul|ol],+ul[ul|ol]');
\r
2150 // If the user didn't allow span only allow internal spans
\r
2151 if (!getElementRule('span'))
\r
2152 addValidElements('span[!data-mce-type|*]');
\r
2154 // Delete invalid elements
\r
2155 if (settings.invalid_elements) {
\r
2156 tinymce.each(tinymce.explode(settings.invalid_elements), function(item) {
\r
2157 if (elements[item])
\r
2158 delete elements[item];
\r
2162 self.children = children;
\r
2164 self.styles = validStyles;
\r
2166 self.getBoolAttrs = function() {
\r
2167 return boolAttrMap;
\r
2170 self.getBlockElements = function() {
\r
2171 return blockElementsMap;
\r
2174 self.getShortEndedElements = function() {
\r
2175 return shortEndedElementsMap;
\r
2178 self.getSelfClosingElements = function() {
\r
2179 return selfClosingElementsMap;
\r
2182 self.getNonEmptyElements = function() {
\r
2183 return nonEmptyElementsMap;
\r
2186 self.getWhiteSpaceElements = function() {
\r
2187 return whiteSpaceElementsMap;
\r
2190 self.isValidChild = function(name, child) {
\r
2191 var parent = children[name];
\r
2193 return !!(parent && parent[child]);
\r
2196 self.getElementRule = getElementRule;
\r
2198 self.getCustomElements = function() {
\r
2199 return customElementsMap;
\r
2202 self.addValidElements = addValidElements;
\r
2204 self.setValidElements = setValidElements;
\r
2206 self.addCustomElements = addCustomElements;
\r
2208 self.addValidChildren = addValidChildren;
\r
2211 // Expose boolMap and blockElementMap as static properties for usage in DOMUtils
\r
2212 tinymce.html.Schema.boolAttrMap = boolAttrMap;
\r
2213 tinymce.html.Schema.blockElementsMap = blockElementsMap;
\r
2216 (function(tinymce) {
\r
2217 tinymce.html.SaxParser = function(settings, schema) {
\r
2218 var self = this, noop = function() {};
\r
2220 settings = settings || {};
\r
2221 self.schema = schema = schema || new tinymce.html.Schema();
\r
2223 if (settings.fix_self_closing !== false)
\r
2224 settings.fix_self_closing = true;
\r
2226 // Add handler functions from settings and setup default handlers
\r
2227 tinymce.each('comment cdata text start end pi doctype'.split(' '), function(name) {
\r
2229 self[name] = settings[name] || noop;
\r
2232 self.parse = function(html) {
\r
2233 var self = this, matches, index = 0, value, endRegExp, stack = [], attrList, i, text, name, isInternalElement, removeInternalElements,
\r
2234 shortEndedElements, fillAttrsMap, isShortEnded, validate, elementRule, isValidElement, attr, attribsValue, invalidPrefixRegExp,
\r
2235 validAttributesMap, validAttributePatterns, attributesRequired, attributesDefault, attributesForced, selfClosing,
\r
2236 tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0, decode = tinymce.html.Entities.decode, fixSelfClosing, isIE;
\r
2238 function processEndTag(name) {
\r
2241 // Find position of parent of the same type
\r
2242 pos = stack.length;
\r
2244 if (stack[pos].name === name)
\r
2250 // Close all the open elements
\r
2251 for (i = stack.length - 1; i >= pos; i--) {
\r
2255 self.end(name.name);
\r
2258 // Remove the open elements from the stack
\r
2259 stack.length = pos;
\r
2263 // Precompile RegExps and map objects
\r
2264 tokenRegExp = new RegExp('<(?:' +
\r
2265 '(?:!--([\\w\\W]*?)-->)|' + // Comment
\r
2266 '(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|' + // CDATA
\r
2267 '(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE
\r
2268 '(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI
\r
2269 '(?:\\/([^>]+)>)|' + // End element
\r
2270 '(?:([^\\s\\/<>]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/)>)' + // Start element
\r
2273 attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:\\.|[^\"])*)\")|(?:\'((?:\\.|[^\'])*)\')|([^>\s]+)))?/g;
\r
2274 specialElements = {
\r
2275 'script' : /<\/script[^>]*>/gi,
\r
2276 'style' : /<\/style[^>]*>/gi,
\r
2277 'noscript' : /<\/noscript[^>]*>/gi
\r
2280 // Setup lookup tables for empty elements and boolean attributes
\r
2281 shortEndedElements = schema.getShortEndedElements();
\r
2282 selfClosing = schema.getSelfClosingElements();
\r
2283 fillAttrsMap = schema.getBoolAttrs();
\r
2284 validate = settings.validate;
\r
2285 removeInternalElements = settings.remove_internals;
\r
2286 fixSelfClosing = settings.fix_self_closing;
\r
2287 isIE = tinymce.isIE;
\r
2288 invalidPrefixRegExp = /^:/;
\r
2290 while (matches = tokenRegExp.exec(html)) {
\r
2292 if (index < matches.index)
\r
2293 self.text(decode(html.substr(index, matches.index - index)));
\r
2295 if (value = matches[6]) { // End element
\r
2296 value = value.toLowerCase();
\r
2298 // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements
\r
2299 if (isIE && invalidPrefixRegExp.test(value))
\r
2300 value = value.substr(1);
\r
2302 processEndTag(value);
\r
2303 } else if (value = matches[7]) { // Start element
\r
2304 value = value.toLowerCase();
\r
2306 // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements
\r
2307 if (isIE && invalidPrefixRegExp.test(value))
\r
2308 value = value.substr(1);
\r
2310 isShortEnded = value in shortEndedElements;
\r
2312 // Is self closing tag for example an <li> after an open <li>
\r
2313 if (fixSelfClosing && selfClosing[value] && stack.length > 0 && stack[stack.length - 1].name === value)
\r
2314 processEndTag(value);
\r
2316 // Validate element
\r
2317 if (!validate || (elementRule = schema.getElementRule(value))) {
\r
2318 isValidElement = true;
\r
2320 // Grab attributes map and patters when validation is enabled
\r
2322 validAttributesMap = elementRule.attributes;
\r
2323 validAttributePatterns = elementRule.attributePatterns;
\r
2326 // Parse attributes
\r
2327 if (attribsValue = matches[8]) {
\r
2328 isInternalElement = attribsValue.indexOf('data-mce-type') !== -1; // Check if the element is an internal element
\r
2330 // If the element has internal attributes then remove it if we are told to do so
\r
2331 if (isInternalElement && removeInternalElements)
\r
2332 isValidElement = false;
\r
2335 attrList.map = {};
\r
2337 attribsValue.replace(attrRegExp, function(match, name, value, val2, val3) {
\r
2340 name = name.toLowerCase();
\r
2341 value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute
\r
2343 // Validate name and value
\r
2344 if (validate && !isInternalElement && name.indexOf('data-') !== 0) {
\r
2345 attrRule = validAttributesMap[name];
\r
2347 // Find rule by pattern matching
\r
2348 if (!attrRule && validAttributePatterns) {
\r
2349 i = validAttributePatterns.length;
\r
2351 attrRule = validAttributePatterns[i];
\r
2352 if (attrRule.pattern.test(name))
\r
2356 // No rule matched
\r
2361 // No attribute rule found
\r
2366 if (attrRule.validValues && !(value in attrRule.validValues))
\r
2370 // Add attribute to list and map
\r
2371 attrList.map[name] = value;
\r
2379 attrList.map = {};
\r
2382 // Process attributes if validation is enabled
\r
2383 if (validate && !isInternalElement) {
\r
2384 attributesRequired = elementRule.attributesRequired;
\r
2385 attributesDefault = elementRule.attributesDefault;
\r
2386 attributesForced = elementRule.attributesForced;
\r
2388 // Handle forced attributes
\r
2389 if (attributesForced) {
\r
2390 i = attributesForced.length;
\r
2392 attr = attributesForced[i];
\r
2394 attrValue = attr.value;
\r
2396 if (attrValue === '{$uid}')
\r
2397 attrValue = 'mce_' + idCount++;
\r
2399 attrList.map[name] = attrValue;
\r
2400 attrList.push({name: name, value: attrValue});
\r
2404 // Handle default attributes
\r
2405 if (attributesDefault) {
\r
2406 i = attributesDefault.length;
\r
2408 attr = attributesDefault[i];
\r
2411 if (!(name in attrList.map)) {
\r
2412 attrValue = attr.value;
\r
2414 if (attrValue === '{$uid}')
\r
2415 attrValue = 'mce_' + idCount++;
\r
2417 attrList.map[name] = attrValue;
\r
2418 attrList.push({name: name, value: attrValue});
\r
2423 // Handle required attributes
\r
2424 if (attributesRequired) {
\r
2425 i = attributesRequired.length;
\r
2427 if (attributesRequired[i] in attrList.map)
\r
2431 // None of the required attributes where found
\r
2433 isValidElement = false;
\r
2436 // Invalidate element if it's marked as bogus
\r
2437 if (attrList.map['data-mce-bogus'])
\r
2438 isValidElement = false;
\r
2441 if (isValidElement)
\r
2442 self.start(value, attrList, isShortEnded);
\r
2444 isValidElement = false;
\r
2446 // Treat script, noscript and style a bit different since they may include code that looks like elements
\r
2447 if (endRegExp = specialElements[value]) {
\r
2448 endRegExp.lastIndex = index = matches.index + matches[0].length;
\r
2450 if (matches = endRegExp.exec(html)) {
\r
2451 if (isValidElement)
\r
2452 text = html.substr(index, matches.index - index);
\r
2454 index = matches.index + matches[0].length;
\r
2456 text = html.substr(index);
\r
2457 index = html.length;
\r
2460 if (isValidElement && text.length > 0)
\r
2461 self.text(text, true);
\r
2463 if (isValidElement)
\r
2466 tokenRegExp.lastIndex = index;
\r
2470 // Push value on to stack
\r
2471 if (!isShortEnded) {
\r
2472 if (!attribsValue || attribsValue.indexOf('/') != attribsValue.length - 1)
\r
2473 stack.push({name: value, valid: isValidElement});
\r
2474 else if (isValidElement)
\r
2477 } else if (value = matches[1]) { // Comment
\r
2478 self.comment(value);
\r
2479 } else if (value = matches[2]) { // CDATA
\r
2480 self.cdata(value);
\r
2481 } else if (value = matches[3]) { // DOCTYPE
\r
2482 self.doctype(value);
\r
2483 } else if (value = matches[4]) { // PI
\r
2484 self.pi(value, matches[5]);
\r
2487 index = matches.index + matches[0].length;
\r
2491 if (index < html.length)
\r
2492 self.text(decode(html.substr(index)));
\r
2494 // Close any open elements
\r
2495 for (i = stack.length - 1; i >= 0; i--) {
\r
2499 self.end(value.name);
\r
2505 (function(tinymce) {
\r
2506 var whiteSpaceRegExp = /^[ \t\r\n]*$/, typeLookup = {
\r
2512 '#document-fragment' : 11
\r
2515 // Walks the tree left/right
\r
2516 function walk(node, root_node, prev) {
\r
2517 var sibling, parent, startName = prev ? 'lastChild' : 'firstChild', siblingName = prev ? 'prev' : 'next';
\r
2519 // Walk into nodes if it has a start
\r
2520 if (node[startName])
\r
2521 return node[startName];
\r
2523 // Return the sibling if it has one
\r
2524 if (node !== root_node) {
\r
2525 sibling = node[siblingName];
\r
2530 // Walk up the parents to look for siblings
\r
2531 for (parent = node.parent; parent && parent !== root_node; parent = parent.parent) {
\r
2532 sibling = parent[siblingName];
\r
2540 function Node(name, type) {
\r
2545 this.attributes = [];
\r
2546 this.attributes.map = {};
\r
2550 tinymce.extend(Node.prototype, {
\r
2551 replace : function(node) {
\r
2557 self.insert(node, self);
\r
2563 attr : function(name, value) {
\r
2564 var self = this, attrs, i, undef;
\r
2566 if (typeof name !== "string") {
\r
2568 self.attr(i, name[i]);
\r
2573 if (attrs = self.attributes) {
\r
2574 if (value !== undef) {
\r
2575 // Remove attribute
\r
2576 if (value === null) {
\r
2577 if (name in attrs.map) {
\r
2578 delete attrs.map[name];
\r
2582 if (attrs[i].name === name) {
\r
2583 attrs = attrs.splice(i, 1);
\r
2593 if (name in attrs.map) {
\r
2597 if (attrs[i].name === name) {
\r
2598 attrs[i].value = value;
\r
2603 attrs.push({name: name, value: value});
\r
2605 attrs.map[name] = value;
\r
2609 return attrs.map[name];
\r
2614 clone : function() {
\r
2615 var self = this, clone = new Node(self.name, self.type), i, l, selfAttrs, selfAttr, cloneAttrs;
\r
2617 // Clone element attributes
\r
2618 if (selfAttrs = self.attributes) {
\r
2620 cloneAttrs.map = {};
\r
2622 for (i = 0, l = selfAttrs.length; i < l; i++) {
\r
2623 selfAttr = selfAttrs[i];
\r
2625 // Clone everything except id
\r
2626 if (selfAttr.name !== 'id') {
\r
2627 cloneAttrs[cloneAttrs.length] = {name: selfAttr.name, value: selfAttr.value};
\r
2628 cloneAttrs.map[selfAttr.name] = selfAttr.value;
\r
2632 clone.attributes = cloneAttrs;
\r
2635 clone.value = self.value;
\r
2636 clone.shortEnded = self.shortEnded;
\r
2641 wrap : function(wrapper) {
\r
2644 self.parent.insert(wrapper, self);
\r
2645 wrapper.append(self);
\r
2650 unwrap : function() {
\r
2651 var self = this, node, next;
\r
2653 for (node = self.firstChild; node; ) {
\r
2655 self.insert(node, self, true);
\r
2662 remove : function() {
\r
2663 var self = this, parent = self.parent, next = self.next, prev = self.prev;
\r
2666 if (parent.firstChild === self) {
\r
2667 parent.firstChild = next;
\r
2675 if (parent.lastChild === self) {
\r
2676 parent.lastChild = prev;
\r
2684 self.parent = self.next = self.prev = null;
\r
2690 append : function(node) {
\r
2691 var self = this, last;
\r
2696 last = self.lastChild;
\r
2700 self.lastChild = node;
\r
2702 self.lastChild = self.firstChild = node;
\r
2704 node.parent = self;
\r
2709 insert : function(node, ref_node, before) {
\r
2715 parent = ref_node.parent || this;
\r
2718 if (ref_node === parent.firstChild)
\r
2719 parent.firstChild = node;
\r
2721 ref_node.prev.next = node;
\r
2723 node.prev = ref_node.prev;
\r
2724 node.next = ref_node;
\r
2725 ref_node.prev = node;
\r
2727 if (ref_node === parent.lastChild)
\r
2728 parent.lastChild = node;
\r
2730 ref_node.next.prev = node;
\r
2732 node.next = ref_node.next;
\r
2733 node.prev = ref_node;
\r
2734 ref_node.next = node;
\r
2737 node.parent = parent;
\r
2742 getAll : function(name) {
\r
2743 var self = this, node, collection = [];
\r
2745 for (node = self.firstChild; node; node = walk(node, self)) {
\r
2746 if (node.name === name)
\r
2747 collection.push(node);
\r
2750 return collection;
\r
2753 empty : function() {
\r
2754 var self = this, nodes, i, node;
\r
2756 // Remove all children
\r
2757 if (self.firstChild) {
\r
2760 // Collect the children
\r
2761 for (node = self.firstChild; node; node = walk(node, self))
\r
2764 // Remove the children
\r
2768 node.parent = node.firstChild = node.lastChild = node.next = node.prev = null;
\r
2772 self.firstChild = self.lastChild = null;
\r
2777 isEmpty : function(elements) {
\r
2778 var self = this, node = self.firstChild, i, name;
\r
2782 if (node.type === 1) {
\r
2783 // Ignore bogus elements
\r
2784 if (node.attributes.map['data-mce-bogus'])
\r
2787 // Keep empty elements like <img />
\r
2788 if (elements[node.name])
\r
2791 // Keep elements with data attributes or name attribute like <a name="1"></a>
\r
2792 i = node.attributes.length;
\r
2794 name = node.attributes[i].name;
\r
2795 if (name === "name" || name.indexOf('data-') === 0)
\r
2801 if (node.type === 8)
\r
2804 // Keep non whitespace text nodes
\r
2805 if ((node.type === 3 && !whiteSpaceRegExp.test(node.value)))
\r
2807 } while (node = walk(node, self));
\r
2813 walk : function(prev) {
\r
2814 return walk(this, null, prev);
\r
2818 tinymce.extend(Node, {
\r
2819 create : function(name, attrs) {
\r
2820 var node, attrName;
\r
2823 node = new Node(name, typeLookup[name] || 1);
\r
2825 // Add attributes if needed
\r
2827 for (attrName in attrs)
\r
2828 node.attr(attrName, attrs[attrName]);
\r
2835 tinymce.html.Node = Node;
\r
2838 (function(tinymce) {
\r
2839 var Node = tinymce.html.Node;
\r
2841 tinymce.html.DomParser = function(settings, schema) {
\r
2842 var self = this, nodeFilters = {}, attributeFilters = [], matchedNodes = {}, matchedAttributes = {};
\r
2844 settings = settings || {};
\r
2845 settings.validate = "validate" in settings ? settings.validate : true;
\r
2846 settings.root_name = settings.root_name || 'body';
\r
2847 self.schema = schema = schema || new tinymce.html.Schema();
\r
2849 function fixInvalidChildren(nodes) {
\r
2850 var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i,
\r
2851 childClone, nonEmptyElements, nonSplitableElements, sibling, nextNode;
\r
2853 nonSplitableElements = tinymce.makeMap('tr,td,th,tbody,thead,tfoot,table');
\r
2854 nonEmptyElements = schema.getNonEmptyElements();
\r
2856 for (ni = 0; ni < nodes.length; ni++) {
\r
2859 // Already removed
\r
2863 // Get list of all parent nodes until we find a valid parent to stick the child into
\r
2865 for (parent = node.parent; parent && !schema.isValidChild(parent.name, node.name) && !nonSplitableElements[parent.name]; parent = parent.parent)
\r
2866 parents.push(parent);
\r
2868 // Found a suitable parent
\r
2869 if (parent && parents.length > 1) {
\r
2870 // Reverse the array since it makes looping easier
\r
2871 parents.reverse();
\r
2873 // Clone the related parent and insert that after the moved node
\r
2874 newParent = currentNode = self.filterNode(parents[0].clone());
\r
2876 // Start cloning and moving children on the left side of the target node
\r
2877 for (i = 0; i < parents.length - 1; i++) {
\r
2878 if (schema.isValidChild(currentNode.name, parents[i].name)) {
\r
2879 tempNode = self.filterNode(parents[i].clone());
\r
2880 currentNode.append(tempNode);
\r
2882 tempNode = currentNode;
\r
2884 for (childNode = parents[i].firstChild; childNode && childNode != parents[i + 1]; ) {
\r
2885 nextNode = childNode.next;
\r
2886 tempNode.append(childNode);
\r
2887 childNode = nextNode;
\r
2890 currentNode = tempNode;
\r
2893 if (!newParent.isEmpty(nonEmptyElements)) {
\r
2894 parent.insert(newParent, parents[0], true);
\r
2895 parent.insert(node, newParent);
\r
2897 parent.insert(node, parents[0], true);
\r
2900 // Check if the element is empty by looking through it's contents and special treatment for <p><br /></p>
\r
2901 parent = parents[0];
\r
2902 if (parent.isEmpty(nonEmptyElements) || parent.firstChild === parent.lastChild && parent.firstChild.name === 'br') {
\r
2903 parent.empty().remove();
\r
2905 } else if (node.parent) {
\r
2906 // If it's an LI try to find a UL/OL for it or wrap it
\r
2907 if (node.name === 'li') {
\r
2908 sibling = node.prev;
\r
2909 if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) {
\r
2910 sibling.append(node);
\r
2914 sibling = node.next;
\r
2915 if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) {
\r
2916 sibling.insert(node, sibling.firstChild, true);
\r
2920 node.wrap(self.filterNode(new Node('ul', 1)));
\r
2924 // Try wrapping the element in a DIV
\r
2925 if (schema.isValidChild(node.parent.name, 'div') && schema.isValidChild('div', node.name)) {
\r
2926 node.wrap(self.filterNode(new Node('div', 1)));
\r
2928 // We failed wrapping it, then remove or unwrap it
\r
2929 if (node.name === 'style' || node.name === 'script')
\r
2930 node.empty().remove();
\r
2938 self.filterNode = function(node) {
\r
2939 var i, name, list;
\r
2941 // Run element filters
\r
2942 if (name in nodeFilters) {
\r
2943 list = matchedNodes[name];
\r
2948 matchedNodes[name] = [node];
\r
2951 // Run attribute filters
\r
2952 i = attributeFilters.length;
\r
2954 name = attributeFilters[i].name;
\r
2956 if (name in node.attributes.map) {
\r
2957 list = matchedAttributes[name];
\r
2962 matchedAttributes[name] = [node];
\r
2969 self.addNodeFilter = function(name, callback) {
\r
2970 tinymce.each(tinymce.explode(name), function(name) {
\r
2971 var list = nodeFilters[name];
\r
2974 nodeFilters[name] = list = [];
\r
2976 list.push(callback);
\r
2980 self.addAttributeFilter = function(name, callback) {
\r
2981 tinymce.each(tinymce.explode(name), function(name) {
\r
2984 for (i = 0; i < attributeFilters.length; i++) {
\r
2985 if (attributeFilters[i].name === name) {
\r
2986 attributeFilters[i].callbacks.push(callback);
\r
2991 attributeFilters.push({name: name, callbacks: [callback]});
\r
2995 self.parse = function(html, args) {
\r
2996 var parser, rootNode, node, nodes, i, l, fi, fl, list, name, validate,
\r
2997 blockElements, startWhiteSpaceRegExp, invalidChildren = [],
\r
2998 endWhiteSpaceRegExp, allWhiteSpaceRegExp, whiteSpaceElements, children, nonEmptyElements, rootBlockName;
\r
3000 args = args || {};
\r
3001 matchedNodes = {};
\r
3002 matchedAttributes = {};
\r
3003 blockElements = tinymce.extend(tinymce.makeMap('script,style,head,html,body,title,meta,param'), schema.getBlockElements());
\r
3004 nonEmptyElements = schema.getNonEmptyElements();
\r
3005 children = schema.children;
\r
3006 validate = settings.validate;
\r
3007 rootBlockName = "forced_root_block" in args ? args.forced_root_block : settings.forced_root_block;
\r
3009 whiteSpaceElements = schema.getWhiteSpaceElements();
\r
3010 startWhiteSpaceRegExp = /^[ \t\r\n]+/;
\r
3011 endWhiteSpaceRegExp = /[ \t\r\n]+$/;
\r
3012 allWhiteSpaceRegExp = /[ \t\r\n]+/g;
\r
3014 function addRootBlocks() {
\r
3015 var node = rootNode.firstChild, next, rootBlockNode;
\r
3020 if (node.type == 3 || (node.type == 1 && node.name !== 'p' && !blockElements[node.name] && !node.attr('data-mce-type'))) {
\r
3021 if (!rootBlockNode) {
\r
3022 // Create a new root block element
\r
3023 rootBlockNode = createNode(rootBlockName, 1);
\r
3024 rootNode.insert(rootBlockNode, node);
\r
3025 rootBlockNode.append(node);
\r
3027 rootBlockNode.append(node);
\r
3029 rootBlockNode = null;
\r
3036 function createNode(name, type) {
\r
3037 var node = new Node(name, type), list;
\r
3039 if (name in nodeFilters) {
\r
3040 list = matchedNodes[name];
\r
3045 matchedNodes[name] = [node];
\r
3051 function removeWhitespaceBefore(node) {
\r
3052 var textNode, textVal, sibling;
\r
3054 for (textNode = node.prev; textNode && textNode.type === 3; ) {
\r
3055 textVal = textNode.value.replace(endWhiteSpaceRegExp, '');
\r
3057 if (textVal.length > 0) {
\r
3058 textNode.value = textVal;
\r
3059 textNode = textNode.prev;
\r
3061 sibling = textNode.prev;
\r
3062 textNode.remove();
\r
3063 textNode = sibling;
\r
3068 parser = new tinymce.html.SaxParser({
\r
3069 validate : validate,
\r
3070 fix_self_closing : !validate, // Let the DOM parser handle <li> in <li> or <p> in <p> for better results
\r
3072 cdata: function(text) {
\r
3073 node.append(createNode('#cdata', 4)).value = text;
\r
3076 text: function(text, raw) {
\r
3079 // Trim all redundant whitespace on non white space elements
\r
3080 if (!whiteSpaceElements[node.name]) {
\r
3081 text = text.replace(allWhiteSpaceRegExp, ' ');
\r
3083 if (node.lastChild && blockElements[node.lastChild.name])
\r
3084 text = text.replace(startWhiteSpaceRegExp, '');
\r
3087 // Do we need to create the node
\r
3088 if (text.length !== 0) {
\r
3089 textNode = createNode('#text', 3);
\r
3090 textNode.raw = !!raw;
\r
3091 node.append(textNode).value = text;
\r
3095 comment: function(text) {
\r
3096 node.append(createNode('#comment', 8)).value = text;
\r
3099 pi: function(name, text) {
\r
3100 node.append(createNode(name, 7)).value = text;
\r
3101 removeWhitespaceBefore(node);
\r
3104 doctype: function(text) {
\r
3107 newNode = node.append(createNode('#doctype', 10));
\r
3108 newNode.value = text;
\r
3109 removeWhitespaceBefore(node);
\r
3112 start: function(name, attrs, empty) {
\r
3113 var newNode, attrFiltersLen, elementRule, textNode, attrName, text, sibling, parent;
\r
3115 elementRule = validate ? schema.getElementRule(name) : {};
\r
3116 if (elementRule) {
\r
3117 newNode = createNode(elementRule.outputName || name, 1);
\r
3118 newNode.attributes = attrs;
\r
3119 newNode.shortEnded = empty;
\r
3121 node.append(newNode);
\r
3123 // Check if node is valid child of the parent node is the child is
\r
3124 // unknown we don't collect it since it's probably a custom element
\r
3125 parent = children[node.name];
\r
3126 if (parent && children[newNode.name] && !parent[newNode.name])
\r
3127 invalidChildren.push(newNode);
\r
3129 attrFiltersLen = attributeFilters.length;
\r
3130 while (attrFiltersLen--) {
\r
3131 attrName = attributeFilters[attrFiltersLen].name;
\r
3133 if (attrName in attrs.map) {
\r
3134 list = matchedAttributes[attrName];
\r
3137 list.push(newNode);
\r
3139 matchedAttributes[attrName] = [newNode];
\r
3143 // Trim whitespace before block
\r
3144 if (blockElements[name])
\r
3145 removeWhitespaceBefore(newNode);
\r
3147 // Change current node if the element wasn't empty i.e not <br /> or <img />
\r
3153 end: function(name) {
\r
3154 var textNode, elementRule, text, sibling, tempNode;
\r
3156 elementRule = validate ? schema.getElementRule(name) : {};
\r
3157 if (elementRule) {
\r
3158 if (blockElements[name]) {
\r
3159 if (!whiteSpaceElements[node.name]) {
\r
3160 // Trim whitespace at beginning of block
\r
3161 for (textNode = node.firstChild; textNode && textNode.type === 3; ) {
\r
3162 text = textNode.value.replace(startWhiteSpaceRegExp, '');
\r
3164 if (text.length > 0) {
\r
3165 textNode.value = text;
\r
3166 textNode = textNode.next;
\r
3168 sibling = textNode.next;
\r
3169 textNode.remove();
\r
3170 textNode = sibling;
\r
3174 // Trim whitespace at end of block
\r
3175 for (textNode = node.lastChild; textNode && textNode.type === 3; ) {
\r
3176 text = textNode.value.replace(endWhiteSpaceRegExp, '');
\r
3178 if (text.length > 0) {
\r
3179 textNode.value = text;
\r
3180 textNode = textNode.prev;
\r
3182 sibling = textNode.prev;
\r
3183 textNode.remove();
\r
3184 textNode = sibling;
\r
3189 // Trim start white space
\r
3190 textNode = node.prev;
\r
3191 if (textNode && textNode.type === 3) {
\r
3192 text = textNode.value.replace(startWhiteSpaceRegExp, '');
\r
3194 if (text.length > 0)
\r
3195 textNode.value = text;
\r
3197 textNode.remove();
\r
3201 // Handle empty nodes
\r
3202 if (elementRule.removeEmpty || elementRule.paddEmpty) {
\r
3203 if (node.isEmpty(nonEmptyElements)) {
\r
3204 if (elementRule.paddEmpty)
\r
3205 node.empty().append(new Node('#text', '3')).value = '\u00a0';
\r
3207 // Leave nodes that have a name like <a name="name">
\r
3208 if (!node.attributes.map.name) {
\r
3209 tempNode = node.parent;
\r
3210 node.empty().remove();
\r
3218 node = node.parent;
\r
3223 rootNode = node = new Node(args.context || settings.root_name, 11);
\r
3225 parser.parse(html);
\r
3227 // Fix invalid children or report invalid children in a contextual parsing
\r
3228 if (validate && invalidChildren.length) {
\r
3229 if (!args.context)
\r
3230 fixInvalidChildren(invalidChildren);
\r
3232 args.invalid = true;
\r
3235 // Wrap nodes in the root into block elements if the root is body
\r
3236 if (rootBlockName && rootNode.name == 'body')
\r
3239 // Run filters only when the contents is valid
\r
3240 if (!args.invalid) {
\r
3241 // Run node filters
\r
3242 for (name in matchedNodes) {
\r
3243 list = nodeFilters[name];
\r
3244 nodes = matchedNodes[name];
\r
3246 // Remove already removed children
\r
3247 fi = nodes.length;
\r
3249 if (!nodes[fi].parent)
\r
3250 nodes.splice(fi, 1);
\r
3253 for (i = 0, l = list.length; i < l; i++)
\r
3254 list[i](nodes, name, args);
\r
3257 // Run attribute filters
\r
3258 for (i = 0, l = attributeFilters.length; i < l; i++) {
\r
3259 list = attributeFilters[i];
\r
3261 if (list.name in matchedAttributes) {
\r
3262 nodes = matchedAttributes[list.name];
\r
3264 // Remove already removed children
\r
3265 fi = nodes.length;
\r
3267 if (!nodes[fi].parent)
\r
3268 nodes.splice(fi, 1);
\r
3271 for (fi = 0, fl = list.callbacks.length; fi < fl; fi++)
\r
3272 list.callbacks[fi](nodes, list.name, args);
\r
3280 // Remove <br> at end of block elements Gecko and WebKit injects BR elements to
\r
3281 // make it possible to place the caret inside empty blocks. This logic tries to remove
\r
3282 // these elements and keep br elements that where intended to be there intact
\r
3283 if (settings.remove_trailing_brs) {
\r
3284 self.addNodeFilter('br', function(nodes, name) {
\r
3285 var i, l = nodes.length, node, blockElements = schema.getBlockElements(),
\r
3286 nonEmptyElements = schema.getNonEmptyElements(), parent, prev, prevName;
\r
3288 // Remove brs from body element as well
\r
3289 blockElements.body = 1;
\r
3291 // Must loop forwards since it will otherwise remove all brs in <p>a<br><br><br></p>
\r
3292 for (i = 0; i < l; i++) {
\r
3294 parent = node.parent;
\r
3296 if (blockElements[node.parent.name] && node === parent.lastChild) {
\r
3297 // Loop all nodes to the right of the current node and check for other BR elements
\r
3298 // excluding bookmarks since they are invisible
\r
3301 prevName = prev.name;
\r
3303 // Ignore bookmarks
\r
3304 if (prevName !== "span" || prev.attr('data-mce-type') !== 'bookmark') {
\r
3305 // Found a non BR element
\r
3306 if (prevName !== "br")
\r
3309 // Found another br it's a <br><br> structure then don't remove anything
\r
3310 if (prevName === 'br') {
\r
3322 // Is the parent to be considered empty after we removed the BR
\r
3323 if (parent.isEmpty(nonEmptyElements)) {
\r
3324 elementRule = schema.getElementRule(parent.name);
\r
3326 // Remove or padd the element depending on schema rule
\r
3327 if (elementRule) {
\r
3328 if (elementRule.removeEmpty)
\r
3330 else if (elementRule.paddEmpty)
\r
3331 parent.empty().append(new tinymce.html.Node('#text', 3)).value = '\u00a0';
\r
3342 tinymce.html.Writer = function(settings) {
\r
3343 var html = [], indent, indentBefore, indentAfter, encode, htmlOutput;
\r
3345 settings = settings || {};
\r
3346 indent = settings.indent;
\r
3347 indentBefore = tinymce.makeMap(settings.indent_before || '');
\r
3348 indentAfter = tinymce.makeMap(settings.indent_after || '');
\r
3349 encode = tinymce.html.Entities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities);
\r
3350 htmlOutput = settings.element_format == "html";
\r
3353 start: function(name, attrs, empty) {
\r
3354 var i, l, attr, value;
\r
3356 if (indent && indentBefore[name] && html.length > 0) {
\r
3357 value = html[html.length - 1];
\r
3359 if (value.length > 0 && value !== '\n')
\r
3363 html.push('<', name);
\r
3366 for (i = 0, l = attrs.length; i < l; i++) {
\r
3368 html.push(' ', attr.name, '="', encode(attr.value, true), '"');
\r
3372 if (!empty || htmlOutput)
\r
3373 html[html.length] = '>';
\r
3375 html[html.length] = ' />';
\r
3377 if (empty && indent && indentAfter[name] && html.length > 0) {
\r
3378 value = html[html.length - 1];
\r
3380 if (value.length > 0 && value !== '\n')
\r
3385 end: function(name) {
\r
3388 /*if (indent && indentBefore[name] && html.length > 0) {
\r
3389 value = html[html.length - 1];
\r
3391 if (value.length > 0 && value !== '\n')
\r
3395 html.push('</', name, '>');
\r
3397 if (indent && indentAfter[name] && html.length > 0) {
\r
3398 value = html[html.length - 1];
\r
3400 if (value.length > 0 && value !== '\n')
\r
3405 text: function(text, raw) {
\r
3406 if (text.length > 0)
\r
3407 html[html.length] = raw ? text : encode(text);
\r
3410 cdata: function(text) {
\r
3411 html.push('<![CDATA[', text, ']]>');
\r
3414 comment: function(text) {
\r
3415 html.push('<!--', text, '-->');
\r
3418 pi: function(name, text) {
\r
3420 html.push('<?', name, ' ', text, '?>');
\r
3422 html.push('<?', name, '?>');
\r
3428 doctype: function(text) {
\r
3429 html.push('<!DOCTYPE', text, '>', indent ? '\n' : '');
\r
3432 reset: function() {
\r
3436 getContent: function() {
\r
3437 return html.join('').replace(/\n$/, '');
\r
3442 (function(tinymce) {
\r
3443 tinymce.html.Serializer = function(settings, schema) {
\r
3444 var self = this, writer = new tinymce.html.Writer(settings);
\r
3446 settings = settings || {};
\r
3447 settings.validate = "validate" in settings ? settings.validate : true;
\r
3449 self.schema = schema = schema || new tinymce.html.Schema();
\r
3450 self.writer = writer;
\r
3452 self.serialize = function(node) {
\r
3453 var handlers, validate;
\r
3455 validate = settings.validate;
\r
3459 3: function(node, raw) {
\r
3460 writer.text(node.value, node.raw);
\r
3464 8: function(node) {
\r
3465 writer.comment(node.value);
\r
3468 // Processing instruction
\r
3469 7: function(node) {
\r
3470 writer.pi(node.name, node.value);
\r
3474 10: function(node) {
\r
3475 writer.doctype(node.value);
\r
3479 4: function(node) {
\r
3480 writer.cdata(node.value);
\r
3483 // Document fragment
\r
3484 11: function(node) {
\r
3485 if ((node = node.firstChild)) {
\r
3488 } while (node = node.next);
\r
3495 function walk(node) {
\r
3496 var handler = handlers[node.type], name, isEmpty, attrs, attrName, attrValue, sortedAttrs, i, l, elementRule;
\r
3500 isEmpty = node.shortEnded;
\r
3501 attrs = node.attributes;
\r
3503 // Sort attributes
\r
3504 if (validate && attrs && attrs.length > 1) {
\r
3506 sortedAttrs.map = {};
\r
3508 elementRule = schema.getElementRule(node.name);
\r
3509 for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) {
\r
3510 attrName = elementRule.attributesOrder[i];
\r
3512 if (attrName in attrs.map) {
\r
3513 attrValue = attrs.map[attrName];
\r
3514 sortedAttrs.map[attrName] = attrValue;
\r
3515 sortedAttrs.push({name: attrName, value: attrValue});
\r
3519 for (i = 0, l = attrs.length; i < l; i++) {
\r
3520 attrName = attrs[i].name;
\r
3522 if (!(attrName in sortedAttrs.map)) {
\r
3523 attrValue = attrs.map[attrName];
\r
3524 sortedAttrs.map[attrName] = attrValue;
\r
3525 sortedAttrs.push({name: attrName, value: attrValue});
\r
3529 attrs = sortedAttrs;
\r
3532 writer.start(node.name, attrs, isEmpty);
\r
3535 if ((node = node.firstChild)) {
\r
3538 } while (node = node.next);
\r
3547 // Serialize element and treat all non elements as fragments
\r
3548 if (node.type == 1 && !settings.inner)
\r
3551 handlers[11](node);
\r
3553 return writer.getContent();
\r
3558 (function(tinymce) {
\r
3560 var each = tinymce.each,
\r
3562 isWebKit = tinymce.isWebKit,
\r
3563 isIE = tinymce.isIE,
\r
3564 Entities = tinymce.html.Entities,
\r
3565 simpleSelectorRe = /^([a-z0-9],?)+$/i,
\r
3566 blockElementsMap = tinymce.html.Schema.blockElementsMap,
\r
3567 whiteSpaceRegExp = /^[ \t\r\n]*$/;
\r
3569 tinymce.create('tinymce.dom.DOMUtils', {
\r
3573 pixelStyles : /^(top|left|bottom|right|width|height|borderWidth)$/,
\r
3575 "for" : "htmlFor",
\r
3576 "class" : "className",
\r
3577 className : "className",
\r
3578 checked : "checked",
\r
3579 disabled : "disabled",
\r
3580 maxlength : "maxLength",
\r
3581 readonly : "readOnly",
\r
3582 selected : "selected",
\r
3589 DOMUtils : function(d, s) {
\r
3590 var t = this, globalStyle, name;
\r
3595 t.cssFlicker = false;
\r
3597 t.stdMode = !tinymce.isIE || d.documentMode >= 8;
\r
3598 t.boxModel = !tinymce.isIE || d.compatMode == "CSS1Compat" || t.stdMode;
\r
3599 t.hasOuterHTML = "outerHTML" in d.createElement("a");
\r
3601 t.settings = s = tinymce.extend({
\r
3602 keep_values : false,
\r
3606 t.schema = s.schema;
\r
3607 t.styles = new tinymce.html.Styles({
\r
3608 url_converter : s.url_converter,
\r
3609 url_converter_scope : s.url_converter_scope
\r
3612 // Fix IE6SP2 flicker and check it failed for pre SP2
\r
3613 if (tinymce.isIE6) {
\r
3615 d.execCommand('BackgroundImageCache', false, true);
\r
3617 t.cssFlicker = true;
\r
3621 if (isIE && s.schema) {
\r
3622 // Add missing HTML 4/5 elements to IE
\r
3623 ('abbr article aside audio canvas ' +
\r
3624 'details figcaption figure footer ' +
\r
3625 'header hgroup mark menu meter nav ' +
\r
3626 'output progress section summary ' +
\r
3627 'time video').replace(/\w+/g, function(name) {
\r
3628 d.createElement(name);
\r
3631 // Create all custom elements
\r
3632 for (name in s.schema.getCustomElements()) {
\r
3633 d.createElement(name);
\r
3637 tinymce.addUnload(t.destroy, t);
\r
3640 getRoot : function() {
\r
3641 var t = this, s = t.settings;
\r
3643 return (s && t.get(s.root_element)) || t.doc.body;
\r
3646 getViewPort : function(w) {
\r
3649 w = !w ? this.win : w;
\r
3651 b = this.boxModel ? d.documentElement : d.body;
\r
3653 // Returns viewport size excluding scrollbars
\r
3655 x : w.pageXOffset || b.scrollLeft,
\r
3656 y : w.pageYOffset || b.scrollTop,
\r
3657 w : w.innerWidth || b.clientWidth,
\r
3658 h : w.innerHeight || b.clientHeight
\r
3662 getRect : function(e) {
\r
3663 var p, t = this, sr;
\r
3667 sr = t.getSize(e);
\r
3677 getSize : function(e) {
\r
3678 var t = this, w, h;
\r
3681 w = t.getStyle(e, 'width');
\r
3682 h = t.getStyle(e, 'height');
\r
3684 // Non pixel value, then force offset/clientWidth
\r
3685 if (w.indexOf('px') === -1)
\r
3688 // Non pixel value, then force offset/clientWidth
\r
3689 if (h.indexOf('px') === -1)
\r
3693 w : parseInt(w) || e.offsetWidth || e.clientWidth,
\r
3694 h : parseInt(h) || e.offsetHeight || e.clientHeight
\r
3698 getParent : function(n, f, r) {
\r
3699 return this.getParents(n, f, r, false);
\r
3702 getParents : function(n, f, r, c) {
\r
3703 var t = this, na, se = t.settings, o = [];
\r
3706 c = c === undefined;
\r
3708 if (se.strict_root)
\r
3709 r = r || t.getRoot();
\r
3711 // Wrap node name as func
\r
3712 if (is(f, 'string')) {
\r
3716 f = function(n) {return n.nodeType == 1;};
\r
3719 return t.is(n, na);
\r
3725 if (n == r || !n.nodeType || n.nodeType === 9)
\r
3738 return c ? o : null;
\r
3741 get : function(e) {
\r
3744 if (e && this.doc && typeof(e) == 'string') {
\r
3746 e = this.doc.getElementById(e);
\r
3748 // IE and Opera returns meta elements when they match the specified input ID, but getElementsByName seems to do the trick
\r
3749 if (e && e.id !== n)
\r
3750 return this.doc.getElementsByName(n)[1];
\r
3756 getNext : function(node, selector) {
\r
3757 return this._findSib(node, selector, 'nextSibling');
\r
3760 getPrev : function(node, selector) {
\r
3761 return this._findSib(node, selector, 'previousSibling');
\r
3765 select : function(pa, s) {
\r
3768 return tinymce.dom.Sizzle(pa, t.get(s) || t.get(t.settings.root_element) || t.doc, []);
\r
3771 is : function(n, selector) {
\r
3774 // If it isn't an array then try to do some simple selectors instead of Sizzle for to boost performance
\r
3775 if (n.length === undefined) {
\r
3776 // Simple all selector
\r
3777 if (selector === '*')
\r
3778 return n.nodeType == 1;
\r
3780 // Simple selector just elements
\r
3781 if (simpleSelectorRe.test(selector)) {
\r
3782 selector = selector.toLowerCase().split(/,/);
\r
3783 n = n.nodeName.toLowerCase();
\r
3785 for (i = selector.length - 1; i >= 0; i--) {
\r
3786 if (selector[i] == n)
\r
3794 return tinymce.dom.Sizzle.matches(selector, n.nodeType ? [n] : n).length > 0;
\r
3798 add : function(p, n, a, h, c) {
\r
3801 return this.run(p, function(p) {
\r
3804 e = is(n, 'string') ? t.doc.createElement(n) : n;
\r
3805 t.setAttribs(e, a);
\r
3814 return !c ? p.appendChild(e) : e;
\r
3818 create : function(n, a, h) {
\r
3819 return this.add(this.doc.createElement(n), n, a, h, 1);
\r
3822 createHTML : function(n, a, h) {
\r
3823 var o = '', t = this, k;
\r
3828 if (a.hasOwnProperty(k))
\r
3829 o += ' ' + k + '="' + t.encode(a[k]) + '"';
\r
3832 // A call to tinymce.is doesn't work for some odd reason on IE9 possible bug inside their JS runtime
\r
3833 if (typeof(h) != "undefined")
\r
3834 return o + '>' + h + '</' + n + '>';
\r
3839 remove : function(node, keep_children) {
\r
3840 return this.run(node, function(node) {
\r
3841 var child, parent = node.parentNode;
\r
3846 if (keep_children) {
\r
3847 while (child = node.firstChild) {
\r
3848 // IE 8 will crash if you don't remove completely empty text nodes
\r
3849 if (!tinymce.isIE || child.nodeType !== 3 || child.nodeValue)
\r
3850 parent.insertBefore(child, node);
\r
3852 node.removeChild(child);
\r
3856 return parent.removeChild(node);
\r
3860 setStyle : function(n, na, v) {
\r
3863 return t.run(n, function(e) {
\r
3868 // Camelcase it, if needed
\r
3869 na = na.replace(/-(\D)/g, function(a, b){
\r
3870 return b.toUpperCase();
\r
3873 // Default px suffix on these
\r
3874 if (t.pixelStyles.test(na) && (tinymce.is(v, 'number') || /^[\-0-9\.]+$/.test(v)))
\r
3879 // IE specific opacity
\r
3881 s.filter = v === '' ? '' : "alpha(opacity=" + (v * 100) + ")";
\r
3883 if (!n.currentStyle || !n.currentStyle.hasLayout)
\r
3884 s.display = 'inline-block';
\r
3887 // Fix for older browsers
\r
3888 s[na] = s['-moz-opacity'] = s['-khtml-opacity'] = v || '';
\r
3892 isIE ? s.styleFloat = v : s.cssFloat = v;
\r
3899 // Force update of the style data
\r
3900 if (t.settings.update_styles)
\r
3901 t.setAttrib(e, 'data-mce-style');
\r
3905 getStyle : function(n, na, c) {
\r
3912 if (this.doc.defaultView && c) {
\r
3913 // Remove camelcase
\r
3914 na = na.replace(/[A-Z]/g, function(a){
\r
3919 return this.doc.defaultView.getComputedStyle(n, null).getPropertyValue(na);
\r
3921 // Old safari might fail
\r
3926 // Camelcase it, if needed
\r
3927 na = na.replace(/-(\D)/g, function(a, b){
\r
3928 return b.toUpperCase();
\r
3931 if (na == 'float')
\r
3932 na = isIE ? 'styleFloat' : 'cssFloat';
\r
3935 if (n.currentStyle && c)
\r
3936 return n.currentStyle[na];
\r
3938 return n.style ? n.style[na] : undefined;
\r
3941 setStyles : function(e, o) {
\r
3942 var t = this, s = t.settings, ol;
\r
3944 ol = s.update_styles;
\r
3945 s.update_styles = 0;
\r
3947 each(o, function(v, n) {
\r
3948 t.setStyle(e, n, v);
\r
3951 // Update style info
\r
3952 s.update_styles = ol;
\r
3953 if (s.update_styles)
\r
3954 t.setAttrib(e, s.cssText);
\r
3957 removeAllAttribs: function(e) {
\r
3958 return this.run(e, function(e) {
\r
3959 var i, attrs = e.attributes;
\r
3960 for (i = attrs.length - 1; i >= 0; i--) {
\r
3961 e.removeAttributeNode(attrs.item(i));
\r
3966 setAttrib : function(e, n, v) {
\r
3969 // Whats the point
\r
3973 // Strict XML mode
\r
3974 if (t.settings.strict)
\r
3975 n = n.toLowerCase();
\r
3977 return this.run(e, function(e) {
\r
3978 var s = t.settings;
\r
3979 var originalValue = e.getAttribute(n);
\r
3983 if (!is(v, 'string')) {
\r
3984 each(v, function(v, n) {
\r
3985 t.setStyle(e, n, v);
\r
3991 // No mce_style for elements with these since they might get resized by the user
\r
3992 if (s.keep_values) {
\r
3993 if (v && !t._isRes(v))
\r
3994 e.setAttribute('data-mce-style', v, 2);
\r
3996 e.removeAttribute('data-mce-style', 2);
\r
3999 e.style.cssText = v;
\r
4003 e.className = v || ''; // Fix IE null bug
\r
4008 if (s.keep_values) {
\r
4009 if (s.url_converter)
\r
4010 v = s.url_converter.call(s.url_converter_scope || t, v, n, e);
\r
4012 t.setAttrib(e, 'data-mce-' + n, v, 2);
\r
4018 e.setAttribute('data-mce-style', v);
\r
4022 if (is(v) && v !== null && v.length !== 0)
\r
4023 e.setAttribute(n, '' + v, 2);
\r
4025 e.removeAttribute(n, 2);
\r
4027 // fire onChangeAttrib event for attributes that have changed
\r
4028 if (tinyMCE.activeEditor && originalValue != v) {
\r
4029 var ed = tinyMCE.activeEditor;
\r
4030 ed.onSetAttrib.dispatch(ed, e, n, v);
\r
4035 setAttribs : function(e, o) {
\r
4038 return this.run(e, function(e) {
\r
4039 each(o, function(v, n) {
\r
4040 t.setAttrib(e, n, v);
\r
4045 getAttrib : function(e, n, dv) {
\r
4046 var v, t = this, undef;
\r
4050 if (!e || e.nodeType !== 1)
\r
4051 return dv === undef ? false : dv;
\r
4056 // Try the mce variant for these
\r
4057 if (/^(src|href|style|coords|shape)$/.test(n)) {
\r
4058 v = e.getAttribute("data-mce-" + n);
\r
4064 if (isIE && t.props[n]) {
\r
4065 v = e[t.props[n]];
\r
4066 v = v && v.nodeValue ? v.nodeValue : v;
\r
4070 v = e.getAttribute(n, 2);
\r
4072 // Check boolean attribs
\r
4073 if (/^(checked|compact|declare|defer|disabled|ismap|multiple|nohref|noshade|nowrap|readonly|selected)$/.test(n)) {
\r
4074 if (e[t.props[n]] === true && v === '')
\r
4077 return v ? n : '';
\r
4080 // Inner input elements will override attributes on form elements
\r
4081 if (e.nodeName === "FORM" && e.getAttributeNode(n))
\r
4082 return e.getAttributeNode(n).nodeValue;
\r
4084 if (n === 'style') {
\r
4085 v = v || e.style.cssText;
\r
4088 v = t.serializeStyle(t.parseStyle(v), e.nodeName);
\r
4090 if (t.settings.keep_values && !t._isRes(v))
\r
4091 e.setAttribute('data-mce-style', v);
\r
4095 // Remove Apple and WebKit stuff
\r
4096 if (isWebKit && n === "class" && v)
\r
4097 v = v.replace(/(apple|webkit)\-[a-z\-]+/gi, '');
\r
4099 // Handle IE issues
\r
4104 // IE returns 1 as default value
\r
4111 // IE returns +0 as default value for size
\r
4112 if (v === '+0' || v === 20 || v === 0)
\r
4129 // IE returns -1 as default value
\r
4137 // IE returns default value
\r
4138 if (v === 32768 || v === 2147483647 || v === '32768')
\r
4153 v = v.toLowerCase();
\r
4157 // IE has odd anonymous function for event attributes
\r
4158 if (n.indexOf('on') === 0 && v)
\r
4159 v = tinymce._replace(/^function\s+\w+\(\)\s+\{\s+(.*)\s+\}$/, '$1', '' + v);
\r
4163 return (v !== undef && v !== null && v !== '') ? '' + v : dv;
\r
4166 getPos : function(n, ro) {
\r
4167 var t = this, x = 0, y = 0, e, d = t.doc, r;
\r
4170 ro = ro || d.body;
\r
4173 // Use getBoundingClientRect if it exists since it's faster than looping offset nodes
\r
4174 if (n.getBoundingClientRect) {
\r
4175 n = n.getBoundingClientRect();
\r
4176 e = t.boxModel ? d.documentElement : d.body;
\r
4178 // Add scroll offsets from documentElement or body since IE with the wrong box model will use d.body and so do WebKit
\r
4179 // Also remove the body/documentelement clientTop/clientLeft on IE 6, 7 since they offset the position
\r
4180 x = n.left + (d.documentElement.scrollLeft || d.body.scrollLeft) - e.clientTop;
\r
4181 y = n.top + (d.documentElement.scrollTop || d.body.scrollTop) - e.clientLeft;
\r
4183 return {x : x, y : y};
\r
4187 while (r && r != ro && r.nodeType) {
\r
4188 x += r.offsetLeft || 0;
\r
4189 y += r.offsetTop || 0;
\r
4190 r = r.offsetParent;
\r
4194 while (r && r != ro && r.nodeType) {
\r
4195 x -= r.scrollLeft || 0;
\r
4196 y -= r.scrollTop || 0;
\r
4201 return {x : x, y : y};
\r
4204 parseStyle : function(st) {
\r
4205 return this.styles.parse(st);
\r
4208 serializeStyle : function(o, name) {
\r
4209 return this.styles.serialize(o, name);
\r
4212 loadCSS : function(u) {
\r
4213 var t = this, d = t.doc, head;
\r
4218 head = t.select('head')[0];
\r
4220 each(u.split(','), function(u) {
\r
4226 t.files[u] = true;
\r
4227 link = t.create('link', {rel : 'stylesheet', href : tinymce._addVer(u)});
\r
4229 // IE 8 has a bug where dynamically loading stylesheets would produce a 1 item remaining bug
\r
4230 // This fix seems to resolve that issue by realcing the document ones a stylesheet finishes loading
\r
4231 // It's ugly but it seems to work fine.
\r
4232 if (isIE && d.documentMode && d.recalc) {
\r
4233 link.onload = function() {
\r
4237 link.onload = null;
\r
4241 head.appendChild(link);
\r
4245 addClass : function(e, c) {
\r
4246 return this.run(e, function(e) {
\r
4252 if (this.hasClass(e, c))
\r
4253 return e.className;
\r
4255 o = this.removeClass(e, c);
\r
4257 return e.className = (o != '' ? (o + ' ') : '') + c;
\r
4261 removeClass : function(e, c) {
\r
4264 return t.run(e, function(e) {
\r
4267 if (t.hasClass(e, c)) {
\r
4269 re = new RegExp("(^|\\s+)" + c + "(\\s+|$)", "g");
\r
4271 v = e.className.replace(re, ' ');
\r
4272 v = tinymce.trim(v != ' ' ? v : '');
\r
4276 // Empty class attr
\r
4278 e.removeAttribute('class');
\r
4279 e.removeAttribute('className');
\r
4285 return e.className;
\r
4289 hasClass : function(n, c) {
\r
4295 return (' ' + n.className + ' ').indexOf(' ' + c + ' ') !== -1;
\r
4298 show : function(e) {
\r
4299 return this.setStyle(e, 'display', 'block');
\r
4302 hide : function(e) {
\r
4303 return this.setStyle(e, 'display', 'none');
\r
4306 isHidden : function(e) {
\r
4309 return !e || e.style.display == 'none' || this.getStyle(e, 'display') == 'none';
\r
4312 uniqueId : function(p) {
\r
4313 return (!p ? 'mce_' : p) + (this.counter++);
\r
4316 setHTML : function(element, html) {
\r
4319 return self.run(element, function(element) {
\r
4321 // Remove all child nodes, IE keeps empty text nodes in DOM
\r
4322 while (element.firstChild)
\r
4323 element.removeChild(element.firstChild);
\r
4326 // IE will remove comments from the beginning
\r
4327 // unless you padd the contents with something
\r
4328 element.innerHTML = '<br />' + html;
\r
4329 element.removeChild(element.firstChild);
\r
4331 // 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
4332 // This seems to fix this problem
\r
4334 // Create new div with HTML contents and a BR infront to keep comments
\r
4335 element = self.create('div');
\r
4336 element.innerHTML = '<br />' + html;
\r
4338 // Add all children from div to target
\r
4339 each (element.childNodes, function(node, i) {
\r
4340 // Skip br element
\r
4342 element.appendChild(node);
\r
4346 element.innerHTML = html;
\r
4352 getOuterHTML : function(elm) {
\r
4353 var doc, self = this;
\r
4355 elm = self.get(elm);
\r
4360 if (elm.nodeType === 1 && self.hasOuterHTML)
\r
4361 return elm.outerHTML;
\r
4363 doc = (elm.ownerDocument || self.doc).createElement("body");
\r
4364 doc.appendChild(elm.cloneNode(true));
\r
4366 return doc.innerHTML;
\r
4369 setOuterHTML : function(e, h, d) {
\r
4372 function setHTML(e, h, d) {
\r
4375 tp = d.createElement("body");
\r
4380 t.insertAfter(n.cloneNode(true), e);
\r
4381 n = n.previousSibling;
\r
4387 return this.run(e, function(e) {
\r
4390 // Only set HTML on elements
\r
4391 if (e.nodeType == 1) {
\r
4392 d = d || e.ownerDocument || t.doc;
\r
4396 // Try outerHTML for IE it sometimes produces an unknown runtime error
\r
4397 if (isIE && e.nodeType == 1)
\r
4402 // Fix for unknown runtime error
\r
4411 decode : Entities.decode,
\r
4413 encode : Entities.encodeAllRaw,
\r
4415 insertAfter : function(node, reference_node) {
\r
4416 reference_node = this.get(reference_node);
\r
4418 return this.run(node, function(node) {
\r
4419 var parent, nextSibling;
\r
4421 parent = reference_node.parentNode;
\r
4422 nextSibling = reference_node.nextSibling;
\r
4425 parent.insertBefore(node, nextSibling);
\r
4427 parent.appendChild(node);
\r
4433 isBlock : function(node) {
\r
4434 var type = node.nodeType;
\r
4436 // If it's a node then check the type and use the nodeName
\r
4438 return !!(type === 1 && blockElementsMap[node.nodeName]);
\r
4440 return !!blockElementsMap[node];
\r
4443 replace : function(n, o, k) {
\r
4446 if (is(o, 'array'))
\r
4447 n = n.cloneNode(true);
\r
4449 return t.run(o, function(o) {
\r
4451 each(tinymce.grep(o.childNodes), function(c) {
\r
4456 return o.parentNode.replaceChild(n, o);
\r
4460 rename : function(elm, name) {
\r
4461 var t = this, newElm;
\r
4463 if (elm.nodeName != name.toUpperCase()) {
\r
4464 // Rename block element
\r
4465 newElm = t.create(name);
\r
4467 // Copy attribs to new block
\r
4468 each(t.getAttribs(elm), function(attr_node) {
\r
4469 t.setAttrib(newElm, attr_node.nodeName, t.getAttrib(elm, attr_node.nodeName));
\r
4473 t.replace(newElm, elm, 1);
\r
4476 return newElm || elm;
\r
4479 findCommonAncestor : function(a, b) {
\r
4485 while (pe && ps != pe)
\r
4486 pe = pe.parentNode;
\r
4491 ps = ps.parentNode;
\r
4494 if (!ps && a.ownerDocument)
\r
4495 return a.ownerDocument.documentElement;
\r
4500 toHex : function(s) {
\r
4501 var c = /^\s*rgb\s*?\(\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?\)\s*$/i.exec(s);
\r
4504 s = parseInt(s).toString(16);
\r
4506 return s.length > 1 ? s : '0' + s; // 0 -> 00
\r
4510 s = '#' + hex(c[1]) + hex(c[2]) + hex(c[3]);
\r
4518 getClasses : function() {
\r
4519 var t = this, cl = [], i, lo = {}, f = t.settings.class_filter, ov;
\r
4524 function addClasses(s) {
\r
4525 // IE style imports
\r
4526 each(s.imports, function(r) {
\r
4530 each(s.cssRules || s.rules, function(r) {
\r
4531 // Real type or fake it on IE
\r
4532 switch (r.type || 1) {
\r
4535 if (r.selectorText) {
\r
4536 each(r.selectorText.split(','), function(v) {
\r
4537 v = v.replace(/^\s*|\s*$|^\s\./g, "");
\r
4539 // Is internal or it doesn't contain a class
\r
4540 if (/\.mce/.test(v) || !/\.[\w\-]+$/.test(v))
\r
4543 // Remove everything but class name
\r
4545 v = tinymce._replace(/.*\.([a-z0-9_\-]+).*/i, '$1', v);
\r
4548 if (f && !(v = f(v, ov)))
\r
4552 cl.push({'class' : v});
\r
4561 addClasses(r.styleSheet);
\r
4568 each(t.doc.styleSheets, addClasses);
\r
4573 if (cl.length > 0)
\r
4579 run : function(e, f, s) {
\r
4582 if (t.doc && typeof(e) === 'string')
\r
4589 if (!e.nodeType && (e.length || e.length === 0)) {
\r
4592 each(e, function(e, i) {
\r
4594 if (typeof(e) == 'string')
\r
4595 e = t.doc.getElementById(e);
\r
4597 o.push(f.call(s, e, i));
\r
4604 return f.call(s, e);
\r
4607 getAttribs : function(n) {
\r
4618 // Object will throw exception in IE
\r
4619 if (n.nodeName == 'OBJECT')
\r
4620 return n.attributes;
\r
4622 // IE doesn't keep the selected attribute if you clone option elements
\r
4623 if (n.nodeName === 'OPTION' && this.getAttrib(n, 'selected'))
\r
4624 o.push({specified : 1, nodeName : 'selected'});
\r
4626 // It's crazy that this is faster in IE but it's because it returns all attributes all the time
\r
4627 n.cloneNode(false).outerHTML.replace(/<\/?[\w:\-]+ ?|=[\"][^\"]+\"|=\'[^\']+\'|=[\w\-]+|>/gi, '').replace(/[\w:\-]+/gi, function(a) {
\r
4628 o.push({specified : 1, nodeName : a});
\r
4634 return n.attributes;
\r
4637 isEmpty : function(node, elements) {
\r
4638 var self = this, i, attributes, type, walker, name, parentNode;
\r
4640 node = node.firstChild;
\r
4642 walker = new tinymce.dom.TreeWalker(node);
\r
4643 elements = elements || self.schema ? self.schema.getNonEmptyElements() : null;
\r
4646 type = node.nodeType;
\r
4649 // Ignore bogus elements
\r
4650 if (node.getAttribute('data-mce-bogus'))
\r
4653 // Keep empty elements like <img />
\r
4654 name = node.nodeName.toLowerCase();
\r
4655 if (elements && elements[name]) {
\r
4656 // Ignore single BR elements in blocks like <p><br /></p>
\r
4657 parentNode = node.parentNode;
\r
4658 if (name === 'br' && self.isBlock(parentNode) && parentNode.firstChild === node && parentNode.lastChild === node) {
\r
4665 // Keep elements with data-bookmark attributes or name attribute like <a name="1"></a>
\r
4666 attributes = self.getAttribs(node);
\r
4667 i = node.attributes.length;
\r
4669 name = node.attributes[i].nodeName;
\r
4670 if (name === "name" || name === 'data-mce-bookmark')
\r
4675 // Keep comment nodes
\r
4679 // Keep non whitespace text nodes
\r
4680 if ((type === 3 && !whiteSpaceRegExp.test(node.nodeValue)))
\r
4682 } while (node = walker.next());
\r
4688 destroy : function(s) {
\r
4692 t.events.destroy();
\r
4694 t.win = t.doc = t.root = t.events = null;
\r
4696 // Manual destroy then remove unload handler
\r
4698 tinymce.removeUnload(t.destroy);
\r
4701 createRng : function() {
\r
4704 return d.createRange ? d.createRange() : new tinymce.dom.Range(this);
\r
4707 nodeIndex : function(node, normalized) {
\r
4708 var idx = 0, lastNodeType, lastNode, nodeType;
\r
4711 for (lastNodeType = node.nodeType, node = node.previousSibling, lastNode = node; node; node = node.previousSibling) {
\r
4712 nodeType = node.nodeType;
\r
4714 // Normalize text nodes
\r
4715 if (normalized && nodeType == 3) {
\r
4716 if (nodeType == lastNodeType || !node.nodeValue.length)
\r
4720 lastNodeType = nodeType;
\r
4727 split : function(pe, e, re) {
\r
4728 var t = this, r = t.createRng(), bef, aft, pa;
\r
4730 // W3C valid browsers tend to leave empty nodes to the left/right side of the contents, this makes sense
\r
4731 // but we don't want that in our code since it serves no purpose for the end user
\r
4732 // For example if this is chopped:
\r
4733 // <p>text 1<span><b>CHOP</b></span>text 2</p>
\r
4735 // <p>text 1<span></span></p><b>CHOP</b><p><span></span>text 2</p>
\r
4736 // this function will then trim of empty edges and produce:
\r
4737 // <p>text 1</p><b>CHOP</b><p>text 2</p>
\r
4738 function trim(node) {
\r
4739 var i, children = node.childNodes, type = node.nodeType;
\r
4741 function surroundedBySpans(node) {
\r
4742 var previousIsSpan = node.previousSibling && node.previousSibling.nodeName == 'SPAN';
\r
4743 var nextIsSpan = node.nextSibling && node.nextSibling.nodeName == 'SPAN';
\r
4744 return previousIsSpan && nextIsSpan;
\r
4747 if (type == 1 && node.getAttribute('data-mce-type') == 'bookmark')
\r
4750 for (i = children.length - 1; i >= 0; i--)
\r
4751 trim(children[i]);
\r
4754 // Keep non whitespace text nodes
\r
4755 if (type == 3 && node.nodeValue.length > 0) {
\r
4756 // If parent element isn't a block or there isn't any useful contents for example "<p> </p>"
\r
4757 // Also keep text nodes with only spaces if surrounded by spans.
\r
4758 // eg. "<p><span>a</span> <span>b</span></p>" should keep space between a and b
\r
4759 var trimmedLength = tinymce.trim(node.nodeValue).length;
\r
4760 if (!t.isBlock(node.parentNode) || trimmedLength > 0 || trimmedLength == 0 && surroundedBySpans(node))
\r
4762 } else if (type == 1) {
\r
4763 // If the only child is a bookmark then move it up
\r
4764 children = node.childNodes;
\r
4765 if (children.length == 1 && children[0] && children[0].nodeType == 1 && children[0].getAttribute('data-mce-type') == 'bookmark')
\r
4766 node.parentNode.insertBefore(children[0], node);
\r
4768 // Keep non empty elements or img, hr etc
\r
4769 if (children.length || /^(br|hr|input|img)$/i.test(node.nodeName))
\r
4780 // Get before chunk
\r
4781 r.setStart(pe.parentNode, t.nodeIndex(pe));
\r
4782 r.setEnd(e.parentNode, t.nodeIndex(e));
\r
4783 bef = r.extractContents();
\r
4785 // Get after chunk
\r
4786 r = t.createRng();
\r
4787 r.setStart(e.parentNode, t.nodeIndex(e) + 1);
\r
4788 r.setEnd(pe.parentNode, t.nodeIndex(pe) + 1);
\r
4789 aft = r.extractContents();
\r
4791 // Insert before chunk
\r
4792 pa = pe.parentNode;
\r
4793 pa.insertBefore(trim(bef), pe);
\r
4795 // Insert middle chunk
\r
4797 pa.replaceChild(re, e);
\r
4799 pa.insertBefore(e, pe);
\r
4801 // Insert after chunk
\r
4802 pa.insertBefore(trim(aft), pe);
\r
4809 bind : function(target, name, func, scope) {
\r
4813 t.events = new tinymce.dom.EventUtils();
\r
4815 return t.events.add(target, name, func, scope || this);
\r
4818 unbind : function(target, name, func) {
\r
4822 t.events = new tinymce.dom.EventUtils();
\r
4824 return t.events.remove(target, name, func);
\r
4828 _findSib : function(node, selector, name) {
\r
4829 var t = this, f = selector;
\r
4832 // If expression make a function of it using is
\r
4833 if (is(f, 'string')) {
\r
4834 f = function(node) {
\r
4835 return t.is(node, selector);
\r
4839 // Loop all siblings
\r
4840 for (node = node[name]; node; node = node[name]) {
\r
4849 _isRes : function(c) {
\r
4850 // Is live resizble element
\r
4851 return /^(top|left|bottom|right|width|height)/i.test(c) || /;\s*(top|left|bottom|right|width|height)/i.test(c);
\r
4855 walk : function(n, f, s) {
\r
4856 var d = this.doc, w;
\r
4858 if (d.createTreeWalker) {
\r
4859 w = d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, false);
\r
4861 while ((n = w.nextNode()) != null)
\r
4862 f.call(s || this, n);
\r
4864 tinymce.walk(n, f, 'childNodes', s);
\r
4869 toRGB : function(s) {
\r
4870 var c = /^\s*?#([0-9A-F]{2})([0-9A-F]{1,2})([0-9A-F]{2})?\s*?$/.exec(s);
\r
4873 // #FFF -> #FFFFFF
\r
4875 c[3] = c[2] = c[1];
\r
4877 return "rgb(" + parseInt(c[1], 16) + "," + parseInt(c[2], 16) + "," + parseInt(c[3], 16) + ")";
\r
4885 tinymce.DOM = new tinymce.dom.DOMUtils(document, {process_html : 0});
\r
4889 // Range constructor
\r
4890 function Range(dom) {
\r
4898 START_OFFSET = 'startOffset',
\r
4899 START_CONTAINER = 'startContainer',
\r
4900 END_CONTAINER = 'endContainer',
\r
4901 END_OFFSET = 'endOffset',
\r
4902 extend = tinymce.extend,
\r
4903 nodeIndex = dom.nodeIndex;
\r
4907 startContainer : doc,
\r
4909 endContainer : doc,
\r
4912 commonAncestorContainer : doc,
\r
4914 // Range constants
\r
4915 START_TO_START : 0,
\r
4921 setStart : setStart,
\r
4923 setStartBefore : setStartBefore,
\r
4924 setStartAfter : setStartAfter,
\r
4925 setEndBefore : setEndBefore,
\r
4926 setEndAfter : setEndAfter,
\r
4927 collapse : collapse,
\r
4928 selectNode : selectNode,
\r
4929 selectNodeContents : selectNodeContents,
\r
4930 compareBoundaryPoints : compareBoundaryPoints,
\r
4931 deleteContents : deleteContents,
\r
4932 extractContents : extractContents,
\r
4933 cloneContents : cloneContents,
\r
4934 insertNode : insertNode,
\r
4935 surroundContents : surroundContents,
\r
4936 cloneRange : cloneRange
\r
4939 function setStart(n, o) {
\r
4940 _setEndPoint(TRUE, n, o);
\r
4943 function setEnd(n, o) {
\r
4944 _setEndPoint(FALSE, n, o);
\r
4947 function setStartBefore(n) {
\r
4948 setStart(n.parentNode, nodeIndex(n));
\r
4951 function setStartAfter(n) {
\r
4952 setStart(n.parentNode, nodeIndex(n) + 1);
\r
4955 function setEndBefore(n) {
\r
4956 setEnd(n.parentNode, nodeIndex(n));
\r
4959 function setEndAfter(n) {
\r
4960 setEnd(n.parentNode, nodeIndex(n) + 1);
\r
4963 function collapse(ts) {
\r
4965 t[END_CONTAINER] = t[START_CONTAINER];
\r
4966 t[END_OFFSET] = t[START_OFFSET];
\r
4968 t[START_CONTAINER] = t[END_CONTAINER];
\r
4969 t[START_OFFSET] = t[END_OFFSET];
\r
4972 t.collapsed = TRUE;
\r
4975 function selectNode(n) {
\r
4976 setStartBefore(n);
\r
4980 function selectNodeContents(n) {
\r
4982 setEnd(n, n.nodeType === 1 ? n.childNodes.length : n.nodeValue.length);
\r
4985 function compareBoundaryPoints(h, r) {
\r
4986 var sc = t[START_CONTAINER], so = t[START_OFFSET], ec = t[END_CONTAINER], eo = t[END_OFFSET],
\r
4987 rsc = r.startContainer, rso = r.startOffset, rec = r.endContainer, reo = r.endOffset;
\r
4989 // Check START_TO_START
\r
4991 return _compareBoundaryPoints(sc, so, rsc, rso);
\r
4993 // Check START_TO_END
\r
4995 return _compareBoundaryPoints(ec, eo, rsc, rso);
\r
4997 // Check END_TO_END
\r
4999 return _compareBoundaryPoints(ec, eo, rec, reo);
\r
5001 // Check END_TO_START
\r
5003 return _compareBoundaryPoints(sc, so, rec, reo);
\r
5006 function deleteContents() {
\r
5007 _traverse(DELETE);
\r
5010 function extractContents() {
\r
5011 return _traverse(EXTRACT);
\r
5014 function cloneContents() {
\r
5015 return _traverse(CLONE);
\r
5018 function insertNode(n) {
\r
5019 var startContainer = this[START_CONTAINER],
\r
5020 startOffset = this[START_OFFSET], nn, o;
\r
5022 // Node is TEXT_NODE or CDATA
\r
5023 if ((startContainer.nodeType === 3 || startContainer.nodeType === 4) && startContainer.nodeValue) {
\r
5024 if (!startOffset) {
\r
5025 // At the start of text
\r
5026 startContainer.parentNode.insertBefore(n, startContainer);
\r
5027 } else if (startOffset >= startContainer.nodeValue.length) {
\r
5028 // At the end of text
\r
5029 dom.insertAfter(n, startContainer);
\r
5031 // Middle, need to split
\r
5032 nn = startContainer.splitText(startOffset);
\r
5033 startContainer.parentNode.insertBefore(n, nn);
\r
5036 // Insert element node
\r
5037 if (startContainer.childNodes.length > 0)
\r
5038 o = startContainer.childNodes[startOffset];
\r
5041 startContainer.insertBefore(n, o);
\r
5043 startContainer.appendChild(n);
\r
5047 function surroundContents(n) {
\r
5048 var f = t.extractContents();
\r
5055 function cloneRange() {
\r
5056 return extend(new Range(dom), {
\r
5057 startContainer : t[START_CONTAINER],
\r
5058 startOffset : t[START_OFFSET],
\r
5059 endContainer : t[END_CONTAINER],
\r
5060 endOffset : t[END_OFFSET],
\r
5061 collapsed : t.collapsed,
\r
5062 commonAncestorContainer : t.commonAncestorContainer
\r
5066 // Private methods
\r
5068 function _getSelectedNode(container, offset) {
\r
5071 if (container.nodeType == 3 /* TEXT_NODE */)
\r
5077 child = container.firstChild;
\r
5078 while (child && offset > 0) {
\r
5080 child = child.nextSibling;
\r
5089 function _isCollapsed() {
\r
5090 return (t[START_CONTAINER] == t[END_CONTAINER] && t[START_OFFSET] == t[END_OFFSET]);
\r
5093 function _compareBoundaryPoints(containerA, offsetA, containerB, offsetB) {
\r
5094 var c, offsetC, n, cmnRoot, childA, childB;
\r
5096 // In the first case the boundary-points have the same container. A is before B
\r
5097 // if its offset is less than the offset of B, A is equal to B if its offset is
\r
5098 // equal to the offset of B, and A is after B if its offset is greater than the
\r
5100 if (containerA == containerB) {
\r
5101 if (offsetA == offsetB)
\r
5102 return 0; // equal
\r
5104 if (offsetA < offsetB)
\r
5105 return -1; // before
\r
5107 return 1; // after
\r
5110 // In the second case a child node C of the container of A is an ancestor
\r
5111 // container of B. In this case, A is before B if the offset of A is less than or
\r
5112 // equal to the index of the child node C and A is after B otherwise.
\r
5114 while (c && c.parentNode != containerA)
\r
5119 n = containerA.firstChild;
\r
5121 while (n != c && offsetC < offsetA) {
\r
5123 n = n.nextSibling;
\r
5126 if (offsetA <= offsetC)
\r
5127 return -1; // before
\r
5129 return 1; // after
\r
5132 // In the third case a child node C of the container of B is an ancestor container
\r
5133 // of A. In this case, A is before B if the index of the child node C is less than
\r
5134 // the offset of B and A is after B otherwise.
\r
5136 while (c && c.parentNode != containerB) {
\r
5142 n = containerB.firstChild;
\r
5144 while (n != c && offsetC < offsetB) {
\r
5146 n = n.nextSibling;
\r
5149 if (offsetC < offsetB)
\r
5150 return -1; // before
\r
5152 return 1; // after
\r
5155 // In the fourth case, none of three other cases hold: the containers of A and B
\r
5156 // are siblings or descendants of sibling nodes. In this case, A is before B if
\r
5157 // the container of A is before the container of B in a pre-order traversal of the
\r
5158 // Ranges' context tree and A is after B otherwise.
\r
5159 cmnRoot = dom.findCommonAncestor(containerA, containerB);
\r
5160 childA = containerA;
\r
5162 while (childA && childA.parentNode != cmnRoot)
\r
5163 childA = childA.parentNode;
\r
5168 childB = containerB;
\r
5169 while (childB && childB.parentNode != cmnRoot)
\r
5170 childB = childB.parentNode;
\r
5175 if (childA == childB)
\r
5176 return 0; // equal
\r
5178 n = cmnRoot.firstChild;
\r
5181 return -1; // before
\r
5184 return 1; // after
\r
5186 n = n.nextSibling;
\r
5190 function _setEndPoint(st, n, o) {
\r
5194 t[START_CONTAINER] = n;
\r
5195 t[START_OFFSET] = o;
\r
5197 t[END_CONTAINER] = n;
\r
5198 t[END_OFFSET] = o;
\r
5201 // If one boundary-point of a Range is set to have a root container
\r
5202 // other than the current one for the Range, the Range is collapsed to
\r
5203 // the new position. This enforces the restriction that both boundary-
\r
5204 // points of a Range must have the same root container.
\r
5205 ec = t[END_CONTAINER];
\r
5206 while (ec.parentNode)
\r
5207 ec = ec.parentNode;
\r
5209 sc = t[START_CONTAINER];
\r
5210 while (sc.parentNode)
\r
5211 sc = sc.parentNode;
\r
5214 // The start position of a Range is guaranteed to never be after the
\r
5215 // end position. To enforce this restriction, if the start is set to
\r
5216 // be at a position after the end, the Range is collapsed to that
\r
5218 if (_compareBoundaryPoints(t[START_CONTAINER], t[START_OFFSET], t[END_CONTAINER], t[END_OFFSET]) > 0)
\r
5223 t.collapsed = _isCollapsed();
\r
5224 t.commonAncestorContainer = dom.findCommonAncestor(t[START_CONTAINER], t[END_CONTAINER]);
\r
5227 function _traverse(how) {
\r
5228 var c, endContainerDepth = 0, startContainerDepth = 0, p, depthDiff, startNode, endNode, sp, ep;
\r
5230 if (t[START_CONTAINER] == t[END_CONTAINER])
\r
5231 return _traverseSameContainer(how);
\r
5233 for (c = t[END_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) {
\r
5234 if (p == t[START_CONTAINER])
\r
5235 return _traverseCommonStartContainer(c, how);
\r
5237 ++endContainerDepth;
\r
5240 for (c = t[START_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) {
\r
5241 if (p == t[END_CONTAINER])
\r
5242 return _traverseCommonEndContainer(c, how);
\r
5244 ++startContainerDepth;
\r
5247 depthDiff = startContainerDepth - endContainerDepth;
\r
5249 startNode = t[START_CONTAINER];
\r
5250 while (depthDiff > 0) {
\r
5251 startNode = startNode.parentNode;
\r
5255 endNode = t[END_CONTAINER];
\r
5256 while (depthDiff < 0) {
\r
5257 endNode = endNode.parentNode;
\r
5261 // ascend the ancestor hierarchy until we have a common parent.
\r
5262 for (sp = startNode.parentNode, ep = endNode.parentNode; sp != ep; sp = sp.parentNode, ep = ep.parentNode) {
\r
5267 return _traverseCommonAncestors(startNode, endNode, how);
\r
5270 function _traverseSameContainer(how) {
\r
5271 var frag, s, sub, n, cnt, sibling, xferNode;
\r
5273 if (how != DELETE)
\r
5274 frag = doc.createDocumentFragment();
\r
5276 // If selection is empty, just return the fragment
\r
5277 if (t[START_OFFSET] == t[END_OFFSET])
\r
5280 // Text node needs special case handling
\r
5281 if (t[START_CONTAINER].nodeType == 3 /* TEXT_NODE */) {
\r
5282 // get the substring
\r
5283 s = t[START_CONTAINER].nodeValue;
\r
5284 sub = s.substring(t[START_OFFSET], t[END_OFFSET]);
\r
5286 // set the original text node to its new value
\r
5287 if (how != CLONE) {
\r
5288 t[START_CONTAINER].deleteData(t[START_OFFSET], t[END_OFFSET] - t[START_OFFSET]);
\r
5290 // Nothing is partially selected, so collapse to start point
\r
5294 if (how == DELETE)
\r
5297 frag.appendChild(doc.createTextNode(sub));
\r
5301 // Copy nodes between the start/end offsets.
\r
5302 n = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]);
\r
5303 cnt = t[END_OFFSET] - t[START_OFFSET];
\r
5306 sibling = n.nextSibling;
\r
5307 xferNode = _traverseFullySelected(n, how);
\r
5310 frag.appendChild( xferNode );
\r
5316 // Nothing is partially selected, so collapse to start point
\r
5323 function _traverseCommonStartContainer(endAncestor, how) {
\r
5324 var frag, n, endIdx, cnt, sibling, xferNode;
\r
5326 if (how != DELETE)
\r
5327 frag = doc.createDocumentFragment();
\r
5329 n = _traverseRightBoundary(endAncestor, how);
\r
5332 frag.appendChild(n);
\r
5334 endIdx = nodeIndex(endAncestor);
\r
5335 cnt = endIdx - t[START_OFFSET];
\r
5338 // Collapse to just before the endAncestor, which
\r
5339 // is partially selected.
\r
5340 if (how != CLONE) {
\r
5341 t.setEndBefore(endAncestor);
\r
5342 t.collapse(FALSE);
\r
5348 n = endAncestor.previousSibling;
\r
5350 sibling = n.previousSibling;
\r
5351 xferNode = _traverseFullySelected(n, how);
\r
5354 frag.insertBefore(xferNode, frag.firstChild);
\r
5360 // Collapse to just before the endAncestor, which
\r
5361 // is partially selected.
\r
5362 if (how != CLONE) {
\r
5363 t.setEndBefore(endAncestor);
\r
5364 t.collapse(FALSE);
\r
5370 function _traverseCommonEndContainer(startAncestor, how) {
\r
5371 var frag, startIdx, n, cnt, sibling, xferNode;
\r
5373 if (how != DELETE)
\r
5374 frag = doc.createDocumentFragment();
\r
5376 n = _traverseLeftBoundary(startAncestor, how);
\r
5378 frag.appendChild(n);
\r
5380 startIdx = nodeIndex(startAncestor);
\r
5381 ++startIdx; // Because we already traversed it
\r
5383 cnt = t[END_OFFSET] - startIdx;
\r
5384 n = startAncestor.nextSibling;
\r
5386 sibling = n.nextSibling;
\r
5387 xferNode = _traverseFullySelected(n, how);
\r
5390 frag.appendChild(xferNode);
\r
5396 if (how != CLONE) {
\r
5397 t.setStartAfter(startAncestor);
\r
5404 function _traverseCommonAncestors(startAncestor, endAncestor, how) {
\r
5405 var n, frag, commonParent, startOffset, endOffset, cnt, sibling, nextSibling;
\r
5407 if (how != DELETE)
\r
5408 frag = doc.createDocumentFragment();
\r
5410 n = _traverseLeftBoundary(startAncestor, how);
\r
5412 frag.appendChild(n);
\r
5414 commonParent = startAncestor.parentNode;
\r
5415 startOffset = nodeIndex(startAncestor);
\r
5416 endOffset = nodeIndex(endAncestor);
\r
5419 cnt = endOffset - startOffset;
\r
5420 sibling = startAncestor.nextSibling;
\r
5423 nextSibling = sibling.nextSibling;
\r
5424 n = _traverseFullySelected(sibling, how);
\r
5427 frag.appendChild(n);
\r
5429 sibling = nextSibling;
\r
5433 n = _traverseRightBoundary(endAncestor, how);
\r
5436 frag.appendChild(n);
\r
5438 if (how != CLONE) {
\r
5439 t.setStartAfter(startAncestor);
\r
5446 function _traverseRightBoundary(root, how) {
\r
5447 var next = _getSelectedNode(t[END_CONTAINER], t[END_OFFSET] - 1), parent, clonedParent, prevSibling, clonedChild, clonedGrandParent, isFullySelected = next != t[END_CONTAINER];
\r
5450 return _traverseNode(next, isFullySelected, FALSE, how);
\r
5452 parent = next.parentNode;
\r
5453 clonedParent = _traverseNode(parent, FALSE, FALSE, how);
\r
5457 prevSibling = next.previousSibling;
\r
5458 clonedChild = _traverseNode(next, isFullySelected, FALSE, how);
\r
5460 if (how != DELETE)
\r
5461 clonedParent.insertBefore(clonedChild, clonedParent.firstChild);
\r
5463 isFullySelected = TRUE;
\r
5464 next = prevSibling;
\r
5467 if (parent == root)
\r
5468 return clonedParent;
\r
5470 next = parent.previousSibling;
\r
5471 parent = parent.parentNode;
\r
5473 clonedGrandParent = _traverseNode(parent, FALSE, FALSE, how);
\r
5475 if (how != DELETE)
\r
5476 clonedGrandParent.appendChild(clonedParent);
\r
5478 clonedParent = clonedGrandParent;
\r
5482 function _traverseLeftBoundary(root, how) {
\r
5483 var next = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]), isFullySelected = next != t[START_CONTAINER], parent, clonedParent, nextSibling, clonedChild, clonedGrandParent;
\r
5486 return _traverseNode(next, isFullySelected, TRUE, how);
\r
5488 parent = next.parentNode;
\r
5489 clonedParent = _traverseNode(parent, FALSE, TRUE, how);
\r
5493 nextSibling = next.nextSibling;
\r
5494 clonedChild = _traverseNode(next, isFullySelected, TRUE, how);
\r
5496 if (how != DELETE)
\r
5497 clonedParent.appendChild(clonedChild);
\r
5499 isFullySelected = TRUE;
\r
5500 next = nextSibling;
\r
5503 if (parent == root)
\r
5504 return clonedParent;
\r
5506 next = parent.nextSibling;
\r
5507 parent = parent.parentNode;
\r
5509 clonedGrandParent = _traverseNode(parent, FALSE, TRUE, how);
\r
5511 if (how != DELETE)
\r
5512 clonedGrandParent.appendChild(clonedParent);
\r
5514 clonedParent = clonedGrandParent;
\r
5518 function _traverseNode(n, isFullySelected, isLeft, how) {
\r
5519 var txtValue, newNodeValue, oldNodeValue, offset, newNode;
\r
5521 if (isFullySelected)
\r
5522 return _traverseFullySelected(n, how);
\r
5524 if (n.nodeType == 3 /* TEXT_NODE */) {
\r
5525 txtValue = n.nodeValue;
\r
5528 offset = t[START_OFFSET];
\r
5529 newNodeValue = txtValue.substring(offset);
\r
5530 oldNodeValue = txtValue.substring(0, offset);
\r
5532 offset = t[END_OFFSET];
\r
5533 newNodeValue = txtValue.substring(0, offset);
\r
5534 oldNodeValue = txtValue.substring(offset);
\r
5538 n.nodeValue = oldNodeValue;
\r
5540 if (how == DELETE)
\r
5543 newNode = n.cloneNode(FALSE);
\r
5544 newNode.nodeValue = newNodeValue;
\r
5549 if (how == DELETE)
\r
5552 return n.cloneNode(FALSE);
\r
5555 function _traverseFullySelected(n, how) {
\r
5556 if (how != DELETE)
\r
5557 return how == CLONE ? n.cloneNode(TRUE) : n;
\r
5559 n.parentNode.removeChild(n);
\r
5567 function Selection(selection) {
\r
5568 var self = this, dom = selection.dom, TRUE = true, FALSE = false;
\r
5570 function getPosition(rng, start) {
\r
5571 var checkRng, startIndex = 0, endIndex, inside,
\r
5572 children, child, offset, index, position = -1, parent;
\r
5574 // Setup test range, collapse it and get the parent
\r
5575 checkRng = rng.duplicate();
\r
5576 checkRng.collapse(start);
\r
5577 parent = checkRng.parentElement();
\r
5579 // Check if the selection is within the right document
\r
5580 if (parent.ownerDocument !== selection.dom.doc)
\r
5583 // IE will report non editable elements as it's parent so look for an editable one
\r
5584 while (parent.contentEditable === "false") {
\r
5585 parent = parent.parentNode;
\r
5588 // If parent doesn't have any children then return that we are inside the element
\r
5589 if (!parent.hasChildNodes()) {
\r
5590 return {node : parent, inside : 1};
\r
5593 // Setup node list and endIndex
\r
5594 children = parent.children;
\r
5595 endIndex = children.length - 1;
\r
5597 // Perform a binary search for the position
\r
5598 while (startIndex <= endIndex) {
\r
5599 index = Math.floor((startIndex + endIndex) / 2);
\r
5601 // Move selection to node and compare the ranges
\r
5602 child = children[index];
\r
5603 checkRng.moveToElementText(child);
\r
5604 position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng);
\r
5606 // Before/after or an exact match
\r
5607 if (position > 0) {
\r
5608 endIndex = index - 1;
\r
5609 } else if (position < 0) {
\r
5610 startIndex = index + 1;
\r
5612 return {node : child};
\r
5616 // Check if child position is before or we didn't find a position
\r
5617 if (position < 0) {
\r
5618 // No element child was found use the parent element and the offset inside that
\r
5620 checkRng.moveToElementText(parent);
\r
5621 checkRng.collapse(true);
\r
5625 checkRng.collapse(false);
\r
5627 checkRng.setEndPoint(start ? 'EndToStart' : 'EndToEnd', rng);
\r
5629 // Fix for edge case: <div style="width: 100px; height:100px;"><table>..</table>ab|c</div>
\r
5630 if (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) > 0) {
\r
5631 checkRng = rng.duplicate();
\r
5632 checkRng.collapse(start);
\r
5635 while (parent == checkRng.parentElement()) {
\r
5636 if (checkRng.move('character', -1) == 0)
\r
5643 offset = offset || checkRng.text.replace('\r\n', ' ').length;
\r
5645 // Child position is after the selection endpoint
\r
5646 checkRng.collapse(true);
\r
5647 checkRng.setEndPoint(start ? 'StartToStart' : 'StartToEnd', rng);
\r
5649 // Get the length of the text to find where the endpoint is relative to it's container
\r
5650 offset = checkRng.text.replace('\r\n', ' ').length;
\r
5653 return {node : child, position : position, offset : offset, inside : inside};
\r
5656 // Returns a W3C DOM compatible range object by using the IE Range API
\r
5657 function getRange() {
\r
5658 var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark, fail;
\r
5660 // If selection is outside the current document just return an empty range
\r
5661 element = ieRange.item ? ieRange.item(0) : ieRange.parentElement();
\r
5662 if (element.ownerDocument != dom.doc)
\r
5665 collapsed = selection.isCollapsed();
\r
5667 // Handle control selection
\r
5668 if (ieRange.item) {
\r
5669 domRange.setStart(element.parentNode, dom.nodeIndex(element));
\r
5670 domRange.setEnd(domRange.startContainer, domRange.startOffset + 1);
\r
5675 function findEndPoint(start) {
\r
5676 var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue;
\r
5678 container = endPoint.node;
\r
5679 offset = endPoint.offset;
\r
5681 if (endPoint.inside && !container.hasChildNodes()) {
\r
5682 domRange[start ? 'setStart' : 'setEnd'](container, 0);
\r
5686 if (offset === undef) {
\r
5687 domRange[start ? 'setStartBefore' : 'setEndAfter'](container);
\r
5691 if (endPoint.position < 0) {
\r
5692 sibling = endPoint.inside ? container.firstChild : container.nextSibling;
\r
5695 domRange[start ? 'setStartAfter' : 'setEndAfter'](container);
\r
5700 if (sibling.nodeType == 3)
\r
5701 domRange[start ? 'setStart' : 'setEnd'](sibling, 0);
\r
5703 domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling);
\r
5708 // Find the text node and offset
\r
5710 nodeValue = sibling.nodeValue;
\r
5711 textNodeOffset += nodeValue.length;
\r
5713 // We are at or passed the position we where looking for
\r
5714 if (textNodeOffset >= offset) {
\r
5715 container = sibling;
\r
5716 textNodeOffset -= offset;
\r
5717 textNodeOffset = nodeValue.length - textNodeOffset;
\r
5721 sibling = sibling.nextSibling;
\r
5724 // Find the text node and offset
\r
5725 sibling = container.previousSibling;
\r
5728 return domRange[start ? 'setStartBefore' : 'setEndBefore'](container);
\r
5730 // If there isn't any text to loop then use the first position
\r
5732 if (container.nodeType == 3)
\r
5733 domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length);
\r
5735 domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling);
\r
5741 textNodeOffset += sibling.nodeValue.length;
\r
5743 // We are at or passed the position we where looking for
\r
5744 if (textNodeOffset >= offset) {
\r
5745 container = sibling;
\r
5746 textNodeOffset -= offset;
\r
5750 sibling = sibling.previousSibling;
\r
5754 domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset);
\r
5758 // Find start point
\r
5759 findEndPoint(true);
\r
5761 // Find end point if needed
\r
5765 // IE has a nasty bug where text nodes might throw "invalid argument" when you
\r
5766 // access the nodeValue or other properties of text nodes. This seems to happend when
\r
5767 // text nodes are split into two nodes by a delete/backspace call. So lets detect it and try to fix it.
\r
5768 if (ex.number == -2147024809) {
\r
5769 // Get the current selection
\r
5770 bookmark = self.getBookmark(2);
\r
5772 // Get start element
\r
5773 tmpRange = ieRange.duplicate();
\r
5774 tmpRange.collapse(true);
\r
5775 element = tmpRange.parentElement();
\r
5777 // Get end element
\r
5779 tmpRange = ieRange.duplicate();
\r
5780 tmpRange.collapse(false);
\r
5781 element2 = tmpRange.parentElement();
\r
5782 element2.innerHTML = element2.innerHTML;
\r
5785 // Remove the broken elements
\r
5786 element.innerHTML = element.innerHTML;
\r
5788 // Restore the selection
\r
5789 self.moveToBookmark(bookmark);
\r
5791 // Since the range has moved we need to re-get it
\r
5792 ieRange = selection.getRng();
\r
5794 // Find start point
\r
5795 findEndPoint(true);
\r
5797 // Find end point if needed
\r
5801 throw ex; // Throw other errors
\r
5807 this.getBookmark = function(type) {
\r
5808 var rng = selection.getRng(), start, end, bookmark = {};
\r
5810 function getIndexes(node) {
\r
5811 var node, parent, root, children, i, indexes = [];
\r
5813 parent = node.parentNode;
\r
5814 root = dom.getRoot().parentNode;
\r
5816 while (parent != root && parent.nodeType !== 9) {
\r
5817 children = parent.children;
\r
5819 i = children.length;
\r
5821 if (node === children[i]) {
\r
5828 parent = parent.parentNode;
\r
5834 function getBookmarkEndPoint(start) {
\r
5837 position = getPosition(rng, start);
\r
5840 position : position.position,
\r
5841 offset : position.offset,
\r
5842 indexes : getIndexes(position.node),
\r
5843 inside : position.inside
\r
5848 // Non ubstructive bookmark
\r
5850 // Handle text selection
\r
5852 bookmark.start = getBookmarkEndPoint(true);
\r
5854 if (!selection.isCollapsed())
\r
5855 bookmark.end = getBookmarkEndPoint();
\r
5857 bookmark.start = {ctrl : true, indexes : getIndexes(rng.item(0))};
\r
5863 this.moveToBookmark = function(bookmark) {
\r
5864 var rng, body = dom.doc.body;
\r
5866 function resolveIndexes(indexes) {
\r
5867 var node, i, idx, children;
\r
5869 node = dom.getRoot();
\r
5870 for (i = indexes.length - 1; i >= 0; i--) {
\r
5871 children = node.children;
\r
5874 if (idx <= children.length - 1) {
\r
5875 node = children[idx];
\r
5882 function setBookmarkEndPoint(start) {
\r
5883 var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef;
\r
5886 moveLeft = endPoint.position > 0;
\r
5888 moveRng = body.createTextRange();
\r
5889 moveRng.moveToElementText(resolveIndexes(endPoint.indexes));
\r
5891 offset = endPoint.offset;
\r
5892 if (offset !== undef) {
\r
5893 moveRng.collapse(endPoint.inside || moveLeft);
\r
5894 moveRng.moveStart('character', moveLeft ? -offset : offset);
\r
5896 moveRng.collapse(start);
\r
5898 rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng);
\r
5901 rng.collapse(true);
\r
5905 if (bookmark.start) {
\r
5906 if (bookmark.start.ctrl) {
\r
5907 rng = body.createControlRange();
\r
5908 rng.addElement(resolveIndexes(bookmark.start.indexes));
\r
5911 rng = body.createTextRange();
\r
5912 setBookmarkEndPoint(true);
\r
5913 setBookmarkEndPoint();
\r
5919 this.addRange = function(rng) {
\r
5920 var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, doc = selection.dom.doc, body = doc.body;
\r
5922 function setEndPoint(start) {
\r
5923 var container, offset, marker, tmpRng, nodes;
\r
5925 marker = dom.create('a');
\r
5926 container = start ? startContainer : endContainer;
\r
5927 offset = start ? startOffset : endOffset;
\r
5928 tmpRng = ieRng.duplicate();
\r
5930 if (container == doc || container == doc.documentElement) {
\r
5935 if (container.nodeType == 3) {
\r
5936 container.parentNode.insertBefore(marker, container);
\r
5937 tmpRng.moveToElementText(marker);
\r
5938 tmpRng.moveStart('character', offset);
\r
5939 dom.remove(marker);
\r
5940 ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
\r
5942 nodes = container.childNodes;
\r
5944 if (nodes.length) {
\r
5945 if (offset >= nodes.length) {
\r
5946 dom.insertAfter(marker, nodes[nodes.length - 1]);
\r
5948 container.insertBefore(marker, nodes[offset]);
\r
5951 tmpRng.moveToElementText(marker);
\r
5953 // Empty node selection for example <div>|</div>
\r
5954 marker = doc.createTextNode('\uFEFF');
\r
5955 container.appendChild(marker);
\r
5956 tmpRng.moveToElementText(marker.parentNode);
\r
5957 tmpRng.collapse(TRUE);
\r
5960 ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
\r
5961 dom.remove(marker);
\r
5965 // Setup some shorter versions
\r
5966 startContainer = rng.startContainer;
\r
5967 startOffset = rng.startOffset;
\r
5968 endContainer = rng.endContainer;
\r
5969 endOffset = rng.endOffset;
\r
5970 ieRng = body.createTextRange();
\r
5972 // If single element selection then try making a control selection out of it
\r
5973 if (startContainer == endContainer && startContainer.nodeType == 1 && startOffset == endOffset - 1) {
\r
5974 if (startOffset == endOffset - 1) {
\r
5976 ctrlRng = body.createControlRange();
\r
5977 ctrlRng.addElement(startContainer.childNodes[startOffset]);
\r
5986 // Set start/end point of selection
\r
5987 setEndPoint(true);
\r
5990 // Select the new range and scroll it into view
\r
5994 // Expose range method
\r
5995 this.getRangeAt = getRange;
\r
5998 // Expose the selection object
\r
5999 tinymce.dom.TridentSelection = Selection;
\r
6004 * Sizzle CSS Selector Engine - v1.0
\r
6005 * Copyright 2009, The Dojo Foundation
\r
6006 * Released under the MIT, BSD, and GPL Licenses.
\r
6007 * More information: http://sizzlejs.com/
\r
6011 var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
\r
6013 toString = Object.prototype.toString,
\r
6014 hasDuplicate = false,
\r
6015 baseHasDuplicate = true;
\r
6017 // Here we check if the JavaScript engine is using some sort of
\r
6018 // optimization where it does not always call our comparision
\r
6019 // function. If that is the case, discard the hasDuplicate value.
\r
6020 // Thus far that includes Google Chrome.
\r
6021 [0, 0].sort(function(){
\r
6022 baseHasDuplicate = false;
\r
6026 var Sizzle = function(selector, context, results, seed) {
\r
6027 results = results || [];
\r
6028 context = context || document;
\r
6030 var origContext = context;
\r
6032 if ( context.nodeType !== 1 && context.nodeType !== 9 ) {
\r
6036 if ( !selector || typeof selector !== "string" ) {
\r
6040 var parts = [], m, set, checkSet, extra, prune = true, contextXML = Sizzle.isXML(context),
\r
6041 soFar = selector, ret, cur, pop, i;
\r
6043 // Reset the position of the chunker regexp (start from head)
\r
6046 m = chunker.exec(soFar);
\r
6051 parts.push( m[1] );
\r
6060 if ( parts.length > 1 && origPOS.exec( selector ) ) {
\r
6061 if ( parts.length === 2 && Expr.relative[ parts[0] ] ) {
\r
6062 set = posProcess( parts[0] + parts[1], context );
\r
6064 set = Expr.relative[ parts[0] ] ?
\r
6066 Sizzle( parts.shift(), context );
\r
6068 while ( parts.length ) {
\r
6069 selector = parts.shift();
\r
6071 if ( Expr.relative[ selector ] ) {
\r
6072 selector += parts.shift();
\r
6075 set = posProcess( selector, set );
\r
6079 // Take a shortcut and set the context if the root selector is an ID
\r
6080 // (but not if it'll be faster if the inner selector is an ID)
\r
6081 if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML &&
\r
6082 Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) {
\r
6083 ret = Sizzle.find( parts.shift(), context, contextXML );
\r
6084 context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0];
\r
6089 { expr: parts.pop(), set: makeArray(seed) } :
\r
6090 Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML );
\r
6091 set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set;
\r
6093 if ( parts.length > 0 ) {
\r
6094 checkSet = makeArray(set);
\r
6099 while ( parts.length ) {
\r
6100 cur = parts.pop();
\r
6103 if ( !Expr.relative[ cur ] ) {
\r
6106 pop = parts.pop();
\r
6109 if ( pop == null ) {
\r
6113 Expr.relative[ cur ]( checkSet, pop, contextXML );
\r
6116 checkSet = parts = [];
\r
6120 if ( !checkSet ) {
\r
6124 if ( !checkSet ) {
\r
6125 Sizzle.error( cur || selector );
\r
6128 if ( toString.call(checkSet) === "[object Array]" ) {
\r
6130 results.push.apply( results, checkSet );
\r
6131 } else if ( context && context.nodeType === 1 ) {
\r
6132 for ( i = 0; checkSet[i] != null; i++ ) {
\r
6133 if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) {
\r
6134 results.push( set[i] );
\r
6138 for ( i = 0; checkSet[i] != null; i++ ) {
\r
6139 if ( checkSet[i] && checkSet[i].nodeType === 1 ) {
\r
6140 results.push( set[i] );
\r
6145 makeArray( checkSet, results );
\r
6149 Sizzle( extra, origContext, results, seed );
\r
6150 Sizzle.uniqueSort( results );
\r
6156 Sizzle.uniqueSort = function(results){
\r
6157 if ( sortOrder ) {
\r
6158 hasDuplicate = baseHasDuplicate;
\r
6159 results.sort(sortOrder);
\r
6161 if ( hasDuplicate ) {
\r
6162 for ( var i = 1; i < results.length; i++ ) {
\r
6163 if ( results[i] === results[i-1] ) {
\r
6164 results.splice(i--, 1);
\r
6173 Sizzle.matches = function(expr, set){
\r
6174 return Sizzle(expr, null, null, set);
\r
6177 Sizzle.find = function(expr, context, isXML){
\r
6184 for ( var i = 0, l = Expr.order.length; i < l; i++ ) {
\r
6185 var type = Expr.order[i], match;
\r
6187 if ( (match = Expr.leftMatch[ type ].exec( expr )) ) {
\r
6188 var left = match[1];
\r
6189 match.splice(1,1);
\r
6191 if ( left.substr( left.length - 1 ) !== "\\" ) {
\r
6192 match[1] = (match[1] || "").replace(/\\/g, "");
\r
6193 set = Expr.find[ type ]( match, context, isXML );
\r
6194 if ( set != null ) {
\r
6195 expr = expr.replace( Expr.match[ type ], "" );
\r
6203 set = context.getElementsByTagName("*");
\r
6206 return {set: set, expr: expr};
\r
6209 Sizzle.filter = function(expr, set, inplace, not){
\r
6210 var old = expr, result = [], curLoop = set, match, anyFound,
\r
6211 isXMLFilter = set && set[0] && Sizzle.isXML(set[0]);
\r
6213 while ( expr && set.length ) {
\r
6214 for ( var type in Expr.filter ) {
\r
6215 if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) {
\r
6216 var filter = Expr.filter[ type ], found, item, left = match[1];
\r
6219 match.splice(1,1);
\r
6221 if ( left.substr( left.length - 1 ) === "\\" ) {
\r
6225 if ( curLoop === result ) {
\r
6229 if ( Expr.preFilter[ type ] ) {
\r
6230 match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter );
\r
6233 anyFound = found = true;
\r
6234 } else if ( match === true ) {
\r
6240 for ( var i = 0; (item = curLoop[i]) != null; i++ ) {
\r
6242 found = filter( item, match, i, curLoop );
\r
6243 var pass = not ^ !!found;
\r
6245 if ( inplace && found != null ) {
\r
6249 curLoop[i] = false;
\r
6251 } else if ( pass ) {
\r
6252 result.push( item );
\r
6259 if ( found !== undefined ) {
\r
6264 expr = expr.replace( Expr.match[ type ], "" );
\r
6266 if ( !anyFound ) {
\r
6275 // Improper expression
\r
6276 if ( expr === old ) {
\r
6277 if ( anyFound == null ) {
\r
6278 Sizzle.error( expr );
\r
6290 Sizzle.error = function( msg ) {
\r
6291 throw "Syntax error, unrecognized expression: " + msg;
\r
6294 var Expr = Sizzle.selectors = {
\r
6295 order: [ "ID", "NAME", "TAG" ],
\r
6297 ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,
\r
6298 CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,
\r
6299 NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,
\r
6300 ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,
\r
6301 TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,
\r
6302 CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+\-]*)\))?/,
\r
6303 POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,
\r
6304 PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/
\r
6308 "class": "className",
\r
6312 href: function(elem){
\r
6313 return elem.getAttribute("href");
\r
6317 "+": function(checkSet, part){
\r
6318 var isPartStr = typeof part === "string",
\r
6319 isTag = isPartStr && !/\W/.test(part),
\r
6320 isPartStrNotTag = isPartStr && !isTag;
\r
6323 part = part.toLowerCase();
\r
6326 for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) {
\r
6327 if ( (elem = checkSet[i]) ) {
\r
6328 while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {}
\r
6330 checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ?
\r
6336 if ( isPartStrNotTag ) {
\r
6337 Sizzle.filter( part, checkSet, true );
\r
6340 ">": function(checkSet, part){
\r
6341 var isPartStr = typeof part === "string",
\r
6342 elem, i = 0, l = checkSet.length;
\r
6344 if ( isPartStr && !/\W/.test(part) ) {
\r
6345 part = part.toLowerCase();
\r
6347 for ( ; i < l; i++ ) {
\r
6348 elem = checkSet[i];
\r
6350 var parent = elem.parentNode;
\r
6351 checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false;
\r
6355 for ( ; i < l; i++ ) {
\r
6356 elem = checkSet[i];
\r
6358 checkSet[i] = isPartStr ?
\r
6360 elem.parentNode === part;
\r
6364 if ( isPartStr ) {
\r
6365 Sizzle.filter( part, checkSet, true );
\r
6369 "": function(checkSet, part, isXML){
\r
6370 var doneName = done++, checkFn = dirCheck, nodeCheck;
\r
6372 if ( typeof part === "string" && !/\W/.test(part) ) {
\r
6373 part = part.toLowerCase();
\r
6375 checkFn = dirNodeCheck;
\r
6378 checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML);
\r
6380 "~": function(checkSet, part, isXML){
\r
6381 var doneName = done++, checkFn = dirCheck, nodeCheck;
\r
6383 if ( typeof part === "string" && !/\W/.test(part) ) {
\r
6384 part = part.toLowerCase();
\r
6386 checkFn = dirNodeCheck;
\r
6389 checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML);
\r
6393 ID: function(match, context, isXML){
\r
6394 if ( typeof context.getElementById !== "undefined" && !isXML ) {
\r
6395 var m = context.getElementById(match[1]);
\r
6396 return m ? [m] : [];
\r
6399 NAME: function(match, context){
\r
6400 if ( typeof context.getElementsByName !== "undefined" ) {
\r
6401 var ret = [], results = context.getElementsByName(match[1]);
\r
6403 for ( var i = 0, l = results.length; i < l; i++ ) {
\r
6404 if ( results[i].getAttribute("name") === match[1] ) {
\r
6405 ret.push( results[i] );
\r
6409 return ret.length === 0 ? null : ret;
\r
6412 TAG: function(match, context){
\r
6413 return context.getElementsByTagName(match[1]);
\r
6417 CLASS: function(match, curLoop, inplace, result, not, isXML){
\r
6418 match = " " + match[1].replace(/\\/g, "") + " ";
\r
6424 for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) {
\r
6426 if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n]/g, " ").indexOf(match) >= 0) ) {
\r
6428 result.push( elem );
\r
6430 } else if ( inplace ) {
\r
6431 curLoop[i] = false;
\r
6438 ID: function(match){
\r
6439 return match[1].replace(/\\/g, "");
\r
6441 TAG: function(match, curLoop){
\r
6442 return match[1].toLowerCase();
\r
6444 CHILD: function(match){
\r
6445 if ( match[1] === "nth" ) {
\r
6446 // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6'
\r
6447 var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec(
\r
6448 match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" ||
\r
6449 !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]);
\r
6451 // calculate the numbers (first)n+(last) including if they are negative
\r
6452 match[2] = (test[1] + (test[2] || 1)) - 0;
\r
6453 match[3] = test[3] - 0;
\r
6456 // TODO: Move to normal caching system
\r
6457 match[0] = done++;
\r
6461 ATTR: function(match, curLoop, inplace, result, not, isXML){
\r
6462 var name = match[1].replace(/\\/g, "");
\r
6464 if ( !isXML && Expr.attrMap[name] ) {
\r
6465 match[1] = Expr.attrMap[name];
\r
6468 if ( match[2] === "~=" ) {
\r
6469 match[4] = " " + match[4] + " ";
\r
6474 PSEUDO: function(match, curLoop, inplace, result, not){
\r
6475 if ( match[1] === "not" ) {
\r
6476 // If we're dealing with a complex expression, or a simple one
\r
6477 if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) {
\r
6478 match[3] = Sizzle(match[3], null, null, curLoop);
\r
6480 var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not);
\r
6482 result.push.apply( result, ret );
\r
6486 } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) {
\r
6492 POS: function(match){
\r
6493 match.unshift( true );
\r
6498 enabled: function(elem){
\r
6499 return elem.disabled === false && elem.type !== "hidden";
\r
6501 disabled: function(elem){
\r
6502 return elem.disabled === true;
\r
6504 checked: function(elem){
\r
6505 return elem.checked === true;
\r
6507 selected: function(elem){
\r
6508 // Accessing this property makes selected-by-default
\r
6509 // options in Safari work properly
\r
6510 elem.parentNode.selectedIndex;
\r
6511 return elem.selected === true;
\r
6513 parent: function(elem){
\r
6514 return !!elem.firstChild;
\r
6516 empty: function(elem){
\r
6517 return !elem.firstChild;
\r
6519 has: function(elem, i, match){
\r
6520 return !!Sizzle( match[3], elem ).length;
\r
6522 header: function(elem){
\r
6523 return (/h\d/i).test( elem.nodeName );
\r
6525 text: function(elem){
\r
6526 return "text" === elem.type;
\r
6528 radio: function(elem){
\r
6529 return "radio" === elem.type;
\r
6531 checkbox: function(elem){
\r
6532 return "checkbox" === elem.type;
\r
6534 file: function(elem){
\r
6535 return "file" === elem.type;
\r
6537 password: function(elem){
\r
6538 return "password" === elem.type;
\r
6540 submit: function(elem){
\r
6541 return "submit" === elem.type;
\r
6543 image: function(elem){
\r
6544 return "image" === elem.type;
\r
6546 reset: function(elem){
\r
6547 return "reset" === elem.type;
\r
6549 button: function(elem){
\r
6550 return "button" === elem.type || elem.nodeName.toLowerCase() === "button";
\r
6552 input: function(elem){
\r
6553 return (/input|select|textarea|button/i).test(elem.nodeName);
\r
6557 first: function(elem, i){
\r
6560 last: function(elem, i, match, array){
\r
6561 return i === array.length - 1;
\r
6563 even: function(elem, i){
\r
6564 return i % 2 === 0;
\r
6566 odd: function(elem, i){
\r
6567 return i % 2 === 1;
\r
6569 lt: function(elem, i, match){
\r
6570 return i < match[3] - 0;
\r
6572 gt: function(elem, i, match){
\r
6573 return i > match[3] - 0;
\r
6575 nth: function(elem, i, match){
\r
6576 return match[3] - 0 === i;
\r
6578 eq: function(elem, i, match){
\r
6579 return match[3] - 0 === i;
\r
6583 PSEUDO: function(elem, match, i, array){
\r
6584 var name = match[1], filter = Expr.filters[ name ];
\r
6587 return filter( elem, i, match, array );
\r
6588 } else if ( name === "contains" ) {
\r
6589 return (elem.textContent || elem.innerText || Sizzle.getText([ elem ]) || "").indexOf(match[3]) >= 0;
\r
6590 } else if ( name === "not" ) {
\r
6591 var not = match[3];
\r
6593 for ( var j = 0, l = not.length; j < l; j++ ) {
\r
6594 if ( not[j] === elem ) {
\r
6601 Sizzle.error( "Syntax error, unrecognized expression: " + name );
\r
6604 CHILD: function(elem, match){
\r
6605 var type = match[1], node = elem;
\r
6609 while ( (node = node.previousSibling) ) {
\r
6610 if ( node.nodeType === 1 ) {
\r
6614 if ( type === "first" ) {
\r
6619 while ( (node = node.nextSibling) ) {
\r
6620 if ( node.nodeType === 1 ) {
\r
6626 var first = match[2], last = match[3];
\r
6628 if ( first === 1 && last === 0 ) {
\r
6632 var doneName = match[0],
\r
6633 parent = elem.parentNode;
\r
6635 if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) {
\r
6637 for ( node = parent.firstChild; node; node = node.nextSibling ) {
\r
6638 if ( node.nodeType === 1 ) {
\r
6639 node.nodeIndex = ++count;
\r
6642 parent.sizcache = doneName;
\r
6645 var diff = elem.nodeIndex - last;
\r
6646 if ( first === 0 ) {
\r
6647 return diff === 0;
\r
6649 return ( diff % first === 0 && diff / first >= 0 );
\r
6653 ID: function(elem, match){
\r
6654 return elem.nodeType === 1 && elem.getAttribute("id") === match;
\r
6656 TAG: function(elem, match){
\r
6657 return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match;
\r
6659 CLASS: function(elem, match){
\r
6660 return (" " + (elem.className || elem.getAttribute("class")) + " ")
\r
6661 .indexOf( match ) > -1;
\r
6663 ATTR: function(elem, match){
\r
6664 var name = match[1],
\r
6665 result = Expr.attrHandle[ name ] ?
\r
6666 Expr.attrHandle[ name ]( elem ) :
\r
6667 elem[ name ] != null ?
\r
6669 elem.getAttribute( name ),
\r
6670 value = result + "",
\r
6674 return result == null ?
\r
6679 value.indexOf(check) >= 0 :
\r
6681 (" " + value + " ").indexOf(check) >= 0 :
\r
6683 value && result !== false :
\r
6687 value.indexOf(check) === 0 :
\r
6689 value.substr(value.length - check.length) === check :
\r
6691 value === check || value.substr(0, check.length + 1) === check + "-" :
\r
6694 POS: function(elem, match, i, array){
\r
6695 var name = match[2], filter = Expr.setFilters[ name ];
\r
6698 return filter( elem, i, match, array );
\r
6704 var origPOS = Expr.match.POS,
\r
6705 fescape = function(all, num){
\r
6706 return "\\" + (num - 0 + 1);
\r
6709 for ( var type in Expr.match ) {
\r
6710 Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) );
\r
6711 Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) );
\r
6714 var makeArray = function(array, results) {
\r
6715 array = Array.prototype.slice.call( array, 0 );
\r
6718 results.push.apply( results, array );
\r
6725 // Perform a simple check to determine if the browser is capable of
\r
6726 // converting a NodeList to an array using builtin methods.
\r
6727 // Also verifies that the returned array holds DOM nodes
\r
6728 // (which is not the case in the Blackberry browser)
\r
6730 Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType;
\r
6732 // Provide a fallback method if it does not work
\r
6734 makeArray = function(array, results) {
\r
6735 var ret = results || [], i = 0;
\r
6737 if ( toString.call(array) === "[object Array]" ) {
\r
6738 Array.prototype.push.apply( ret, array );
\r
6740 if ( typeof array.length === "number" ) {
\r
6741 for ( var l = array.length; i < l; i++ ) {
\r
6742 ret.push( array[i] );
\r
6745 for ( ; array[i]; i++ ) {
\r
6746 ret.push( array[i] );
\r
6757 if ( document.documentElement.compareDocumentPosition ) {
\r
6758 sortOrder = function( a, b ) {
\r
6759 if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) {
\r
6761 hasDuplicate = true;
\r
6763 return a.compareDocumentPosition ? -1 : 1;
\r
6766 var ret = a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1;
\r
6767 if ( ret === 0 ) {
\r
6768 hasDuplicate = true;
\r
6772 } else if ( "sourceIndex" in document.documentElement ) {
\r
6773 sortOrder = function( a, b ) {
\r
6774 if ( !a.sourceIndex || !b.sourceIndex ) {
\r
6776 hasDuplicate = true;
\r
6778 return a.sourceIndex ? -1 : 1;
\r
6781 var ret = a.sourceIndex - b.sourceIndex;
\r
6782 if ( ret === 0 ) {
\r
6783 hasDuplicate = true;
\r
6787 } else if ( document.createRange ) {
\r
6788 sortOrder = function( a, b ) {
\r
6789 if ( !a.ownerDocument || !b.ownerDocument ) {
\r
6791 hasDuplicate = true;
\r
6793 return a.ownerDocument ? -1 : 1;
\r
6796 var aRange = a.ownerDocument.createRange(), bRange = b.ownerDocument.createRange();
\r
6797 aRange.setStart(a, 0);
\r
6798 aRange.setEnd(a, 0);
\r
6799 bRange.setStart(b, 0);
\r
6800 bRange.setEnd(b, 0);
\r
6801 var ret = aRange.compareBoundaryPoints(Range.START_TO_END, bRange);
\r
6802 if ( ret === 0 ) {
\r
6803 hasDuplicate = true;
\r
6809 // Utility function for retreiving the text value of an array of DOM nodes
\r
6810 Sizzle.getText = function( elems ) {
\r
6811 var ret = "", elem;
\r
6813 for ( var i = 0; elems[i]; i++ ) {
\r
6816 // Get the text from text nodes and CDATA nodes
\r
6817 if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
\r
6818 ret += elem.nodeValue;
\r
6820 // Traverse everything else, except comment nodes
\r
6821 } else if ( elem.nodeType !== 8 ) {
\r
6822 ret += Sizzle.getText( elem.childNodes );
\r
6829 // Check to see if the browser returns elements by name when
\r
6830 // querying by getElementById (and provide a workaround)
\r
6832 // We're going to inject a fake input element with a specified name
\r
6833 var form = document.createElement("div"),
\r
6834 id = "script" + (new Date()).getTime();
\r
6835 form.innerHTML = "<a name='" + id + "'/>";
\r
6837 // Inject it into the root element, check its status, and remove it quickly
\r
6838 var root = document.documentElement;
\r
6839 root.insertBefore( form, root.firstChild );
\r
6841 // The workaround has to do additional checks after a getElementById
\r
6842 // Which slows things down for other browsers (hence the branching)
\r
6843 if ( document.getElementById( id ) ) {
\r
6844 Expr.find.ID = function(match, context, isXML){
\r
6845 if ( typeof context.getElementById !== "undefined" && !isXML ) {
\r
6846 var m = context.getElementById(match[1]);
\r
6847 return m ? m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : [];
\r
6851 Expr.filter.ID = function(elem, match){
\r
6852 var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id");
\r
6853 return elem.nodeType === 1 && node && node.nodeValue === match;
\r
6857 root.removeChild( form );
\r
6858 root = form = null; // release memory in IE
\r
6862 // Check to see if the browser returns only elements
\r
6863 // when doing getElementsByTagName("*")
\r
6865 // Create a fake element
\r
6866 var div = document.createElement("div");
\r
6867 div.appendChild( document.createComment("") );
\r
6869 // Make sure no comments are found
\r
6870 if ( div.getElementsByTagName("*").length > 0 ) {
\r
6871 Expr.find.TAG = function(match, context){
\r
6872 var results = context.getElementsByTagName(match[1]);
\r
6874 // Filter out possible comments
\r
6875 if ( match[1] === "*" ) {
\r
6878 for ( var i = 0; results[i]; i++ ) {
\r
6879 if ( results[i].nodeType === 1 ) {
\r
6880 tmp.push( results[i] );
\r
6891 // Check to see if an attribute returns normalized href attributes
\r
6892 div.innerHTML = "<a href='#'></a>";
\r
6893 if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" &&
\r
6894 div.firstChild.getAttribute("href") !== "#" ) {
\r
6895 Expr.attrHandle.href = function(elem){
\r
6896 return elem.getAttribute("href", 2);
\r
6900 div = null; // release memory in IE
\r
6903 if ( document.querySelectorAll ) {
\r
6905 var oldSizzle = Sizzle, div = document.createElement("div");
\r
6906 div.innerHTML = "<p class='TEST'></p>";
\r
6908 // Safari can't handle uppercase or unicode characters when
\r
6909 // in quirks mode.
\r
6910 if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) {
\r
6914 Sizzle = function(query, context, extra, seed){
\r
6915 context = context || document;
\r
6917 // Only use querySelectorAll on non-XML documents
\r
6918 // (ID selectors don't work in non-HTML documents)
\r
6919 if ( !seed && context.nodeType === 9 && !Sizzle.isXML(context) ) {
\r
6921 return makeArray( context.querySelectorAll(query), extra );
\r
6925 return oldSizzle(query, context, extra, seed);
\r
6928 for ( var prop in oldSizzle ) {
\r
6929 Sizzle[ prop ] = oldSizzle[ prop ];
\r
6932 div = null; // release memory in IE
\r
6937 var div = document.createElement("div");
\r
6939 div.innerHTML = "<div class='test e'></div><div class='test'></div>";
\r
6941 // Opera can't find a second classname (in 9.6)
\r
6942 // Also, make sure that getElementsByClassName actually exists
\r
6943 if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) {
\r
6947 // Safari caches class attributes, doesn't catch changes (in 3.2)
\r
6948 div.lastChild.className = "e";
\r
6950 if ( div.getElementsByClassName("e").length === 1 ) {
\r
6954 Expr.order.splice(1, 0, "CLASS");
\r
6955 Expr.find.CLASS = function(match, context, isXML) {
\r
6956 if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) {
\r
6957 return context.getElementsByClassName(match[1]);
\r
6961 div = null; // release memory in IE
\r
6964 function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
\r
6965 for ( var i = 0, l = checkSet.length; i < l; i++ ) {
\r
6966 var elem = checkSet[i];
\r
6969 var match = false;
\r
6972 if ( elem.sizcache === doneName ) {
\r
6973 match = checkSet[elem.sizset];
\r
6977 if ( elem.nodeType === 1 && !isXML ){
\r
6978 elem.sizcache = doneName;
\r
6982 if ( elem.nodeName.toLowerCase() === cur ) {
\r
6990 checkSet[i] = match;
\r
6995 function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
\r
6996 for ( var i = 0, l = checkSet.length; i < l; i++ ) {
\r
6997 var elem = checkSet[i];
\r
7000 var match = false;
\r
7003 if ( elem.sizcache === doneName ) {
\r
7004 match = checkSet[elem.sizset];
\r
7008 if ( elem.nodeType === 1 ) {
\r
7010 elem.sizcache = doneName;
\r
7013 if ( typeof cur !== "string" ) {
\r
7014 if ( elem === cur ) {
\r
7019 } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) {
\r
7028 checkSet[i] = match;
\r
7033 Sizzle.contains = document.compareDocumentPosition ? function(a, b){
\r
7034 return !!(a.compareDocumentPosition(b) & 16);
\r
7035 } : function(a, b){
\r
7036 return a !== b && (a.contains ? a.contains(b) : true);
\r
7039 Sizzle.isXML = function(elem){
\r
7040 // documentElement is verified for cases where it doesn't yet exist
\r
7041 // (such as loading iframes in IE - #4833)
\r
7042 var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement;
\r
7043 return documentElement ? documentElement.nodeName !== "HTML" : false;
\r
7046 var posProcess = function(selector, context){
\r
7047 var tmpSet = [], later = "", match,
\r
7048 root = context.nodeType ? [context] : context;
\r
7050 // Position selectors must be done after the filter
\r
7051 // And so must :not(positional) so we move all PSEUDOs to the end
\r
7052 while ( (match = Expr.match.PSEUDO.exec( selector )) ) {
\r
7053 later += match[0];
\r
7054 selector = selector.replace( Expr.match.PSEUDO, "" );
\r
7057 selector = Expr.relative[selector] ? selector + "*" : selector;
\r
7059 for ( var i = 0, l = root.length; i < l; i++ ) {
\r
7060 Sizzle( selector, root[i], tmpSet );
\r
7063 return Sizzle.filter( later, tmpSet );
\r
7068 window.tinymce.dom.Sizzle = Sizzle;
\r
7073 (function(tinymce) {
\r
7075 var each = tinymce.each, DOM = tinymce.DOM, isIE = tinymce.isIE, isWebKit = tinymce.isWebKit, Event;
\r
7077 tinymce.create('tinymce.dom.EventUtils', {
\r
7078 EventUtils : function() {
\r
7083 add : function(o, n, f, s) {
\r
7084 var cb, t = this, el = t.events, r;
\r
7086 if (n instanceof Array) {
\r
7089 each(n, function(n) {
\r
7090 r.push(t.add(o, n, f, s));
\r
7097 if (o && o.hasOwnProperty && o instanceof Array) {
\r
7100 each(o, function(o) {
\r
7102 r.push(t.add(o, n, f, s));
\r
7113 // Setup event callback
\r
7114 cb = function(e) {
\r
7115 // Is all events disabled
\r
7119 e = e || window.event;
\r
7121 // Patch in target, preventDefault and stopPropagation in IE it's W3C valid
\r
7124 e.target = e.srcElement;
\r
7126 // Patch in preventDefault, stopPropagation methods for W3C compatibility
\r
7127 tinymce.extend(e, t._stoppers);
\r
7133 return f.call(s, e);
\r
7136 if (n == 'unload') {
\r
7137 tinymce.unloads.unshift({func : cb});
\r
7141 if (n == 'init') {
\r
7150 // Store away listener reference
\r
7164 remove : function(o, n, f) {
\r
7165 var t = this, a = t.events, s = false, r;
\r
7168 if (o && o.hasOwnProperty && o instanceof Array) {
\r
7171 each(o, function(o) {
\r
7173 r.push(t.remove(o, n, f));
\r
7181 each(a, function(e, i) {
\r
7182 if (e.obj == o && e.name == n && (!f || (e.func == f || e.cfunc == f))) {
\r
7184 t._remove(o, n, e.cfunc);
\r
7193 clear : function(o) {
\r
7194 var t = this, a = t.events, i, e;
\r
7199 for (i = a.length - 1; i >= 0; i--) {
\r
7202 if (e.obj === o) {
\r
7203 t._remove(e.obj, e.name, e.cfunc);
\r
7204 e.obj = e.cfunc = null;
\r
7211 cancel : function(e) {
\r
7217 return this.prevent(e);
\r
7220 stop : function(e) {
\r
7221 if (e.stopPropagation)
\r
7222 e.stopPropagation();
\r
7224 e.cancelBubble = true;
\r
7229 prevent : function(e) {
\r
7230 if (e.preventDefault)
\r
7231 e.preventDefault();
\r
7233 e.returnValue = false;
\r
7238 destroy : function() {
\r
7241 each(t.events, function(e, i) {
\r
7242 t._remove(e.obj, e.name, e.cfunc);
\r
7243 e.obj = e.cfunc = null;
\r
7250 _add : function(o, n, f) {
\r
7251 if (o.attachEvent)
\r
7252 o.attachEvent('on' + n, f);
\r
7253 else if (o.addEventListener)
\r
7254 o.addEventListener(n, f, false);
\r
7259 _remove : function(o, n, f) {
\r
7262 if (o.detachEvent)
\r
7263 o.detachEvent('on' + n, f);
\r
7264 else if (o.removeEventListener)
\r
7265 o.removeEventListener(n, f, false);
\r
7267 o['on' + n] = null;
\r
7269 // Might fail with permission denined on IE so we just ignore that
\r
7274 _pageInit : function(win) {
\r
7277 // Keep it from running more than once
\r
7281 t.domLoaded = true;
\r
7283 each(t.inits, function(c) {
\r
7290 _wait : function(win) {
\r
7291 var t = this, doc = win.document;
\r
7293 // No need since the document is already loaded
\r
7294 if (win.tinyMCE_GZ && tinyMCE_GZ.loaded) {
\r
7299 // When loaded asynchronously, the DOM Content may already be loaded
\r
7300 if (doc.readyState === 'complete') {
\r
7306 if (doc.attachEvent) {
\r
7307 doc.attachEvent("onreadystatechange", function() {
\r
7308 if (doc.readyState === "complete") {
\r
7309 doc.detachEvent("onreadystatechange", arguments.callee);
\r
7314 if (doc.documentElement.doScroll && win == win.top) {
\r
7320 // If IE is used, use the trick by Diego Perini licensed under MIT by request to the author.
\r
7321 // http://javascript.nwbox.com/IEContentLoaded/
\r
7322 doc.documentElement.doScroll("left");
\r
7324 setTimeout(arguments.callee, 0);
\r
7331 } else if (doc.addEventListener) {
\r
7332 t._add(win, 'DOMContentLoaded', function() {
\r
7337 t._add(win, 'load', function() {
\r
7343 preventDefault : function() {
\r
7344 this.returnValue = false;
\r
7347 stopPropagation : function() {
\r
7348 this.cancelBubble = true;
\r
7353 Event = tinymce.dom.Event = new tinymce.dom.EventUtils();
\r
7355 // Dispatch DOM content loaded event for IE and Safari
\r
7356 Event._wait(window);
\r
7358 tinymce.addUnload(function() {
\r
7363 (function(tinymce) {
\r
7364 tinymce.dom.Element = function(id, settings) {
\r
7365 var t = this, dom, el;
\r
7367 t.settings = settings = settings || {};
\r
7369 t.dom = dom = settings.dom || tinymce.DOM;
\r
7371 // Only IE leaks DOM references, this is a lot faster
\r
7372 if (!tinymce.isIE)
\r
7373 el = dom.get(t.id);
\r
7376 ('getPos,getRect,getParent,add,setStyle,getStyle,setStyles,' +
\r
7377 'setAttrib,setAttribs,getAttrib,addClass,removeClass,' +
\r
7378 'hasClass,getOuterHTML,setOuterHTML,remove,show,hide,' +
\r
7379 'isHidden,setHTML,get').split(/,/)
\r
7381 t[k] = function() {
\r
7384 for (i = 0; i < arguments.length; i++)
\r
7385 a.push(arguments[i]);
\r
7387 a = dom[k].apply(dom, a);
\r
7394 tinymce.extend(t, {
\r
7395 on : function(n, f, s) {
\r
7396 return tinymce.dom.Event.add(t.id, n, f, s);
\r
7399 getXY : function() {
\r
7401 x : parseInt(t.getStyle('left')),
\r
7402 y : parseInt(t.getStyle('top'))
\r
7406 getSize : function() {
\r
7407 var n = dom.get(t.id);
\r
7410 w : parseInt(t.getStyle('width') || n.clientWidth),
\r
7411 h : parseInt(t.getStyle('height') || n.clientHeight)
\r
7415 moveTo : function(x, y) {
\r
7416 t.setStyles({left : x, top : y});
\r
7419 moveBy : function(x, y) {
\r
7420 var p = t.getXY();
\r
7422 t.moveTo(p.x + x, p.y + y);
\r
7425 resizeTo : function(w, h) {
\r
7426 t.setStyles({width : w, height : h});
\r
7429 resizeBy : function(w, h) {
\r
7430 var s = t.getSize();
\r
7432 t.resizeTo(s.w + w, s.h + h);
\r
7435 update : function(k) {
\r
7438 if (tinymce.isIE6 && settings.blocker) {
\r
7442 if (k.indexOf('get') === 0 || k.indexOf('has') === 0 || k.indexOf('is') === 0)
\r
7445 // Remove blocker on remove
\r
7446 if (k == 'remove') {
\r
7447 dom.remove(t.blocker);
\r
7452 t.blocker = dom.uniqueId();
\r
7453 b = dom.add(settings.container || dom.getRoot(), 'iframe', {id : t.blocker, style : 'position:absolute;', frameBorder : 0, src : 'javascript:""'});
\r
7454 dom.setStyle(b, 'opacity', 0);
\r
7456 b = dom.get(t.blocker);
\r
7458 dom.setStyles(b, {
\r
7459 left : t.getStyle('left', 1),
\r
7460 top : t.getStyle('top', 1),
\r
7461 width : t.getStyle('width', 1),
\r
7462 height : t.getStyle('height', 1),
\r
7463 display : t.getStyle('display', 1),
\r
7464 zIndex : parseInt(t.getStyle('zIndex', 1) || 0) - 1
\r
7472 (function(tinymce) {
\r
7473 function trimNl(s) {
\r
7474 return s.replace(/[\n\r]+/g, '');
\r
7478 var is = tinymce.is, isIE = tinymce.isIE, each = tinymce.each;
\r
7480 tinymce.create('tinymce.dom.Selection', {
\r
7481 Selection : function(dom, win, serializer) {
\r
7486 t.serializer = serializer;
\r
7490 'onBeforeSetContent',
\r
7492 'onBeforeGetContent',
\r
7498 t[e] = new tinymce.util.Dispatcher(t);
\r
7501 // No W3C Range support
\r
7502 if (!t.win.getSelection)
\r
7503 t.tridentSel = new tinymce.dom.TridentSelection(t);
\r
7505 if (tinymce.isIE && dom.boxModel)
\r
7506 this._fixIESelection();
\r
7509 tinymce.addUnload(t.destroy, t);
\r
7512 setCursorLocation: function(node, offset) {
\r
7513 var t = this; var r = t.dom.createRng();
\r
7514 r.setStart(node, offset);
\r
7515 r.setEnd(node, offset);
\r
7517 t.collapse(false);
\r
7519 getContent : function(s) {
\r
7520 var t = this, r = t.getRng(), e = t.dom.create("body"), se = t.getSel(), wb, wa, n;
\r
7525 s.format = s.format || 'html';
\r
7526 s.forced_root_block = '';
\r
7527 t.onBeforeGetContent.dispatch(t, s);
\r
7529 if (s.format == 'text')
\r
7530 return t.isCollapsed() ? '' : (r.text || (se.toString ? se.toString() : ''));
\r
7532 if (r.cloneContents) {
\r
7533 n = r.cloneContents();
\r
7537 } else if (is(r.item) || is(r.htmlText)) {
\r
7538 // IE will produce invalid markup if elements are present that
\r
7539 // it doesn't understand like custom elements or HTML5 elements.
\r
7540 // Adding a BR in front of the contents and then remoiving it seems to fix it though.
\r
7541 e.innerHTML = '<br>' + (r.item ? r.item(0).outerHTML : r.htmlText);
\r
7542 e.removeChild(e.firstChild);
\r
7544 e.innerHTML = r.toString();
\r
7546 // Keep whitespace before and after
\r
7547 if (/^\s/.test(e.innerHTML))
\r
7550 if (/\s+$/.test(e.innerHTML))
\r
7553 s.getInner = true;
\r
7555 s.content = t.isCollapsed() ? '' : wb + t.serializer.serialize(e, s) + wa;
\r
7556 t.onGetContent.dispatch(t, s);
\r
7561 setContent : function(content, args) {
\r
7562 var self = this, rng = self.getRng(), caretNode, doc = self.win.document, frag, temp;
\r
7564 args = args || {format : 'html'};
\r
7566 content = args.content = content;
\r
7568 // Dispatch before set content event
\r
7569 if (!args.no_events)
\r
7570 self.onBeforeSetContent.dispatch(self, args);
\r
7572 content = args.content;
\r
7574 if (rng.insertNode) {
\r
7575 // Make caret marker since insertNode places the caret in the beginning of text after insert
\r
7576 content += '<span id="__caret">_</span>';
\r
7578 // Delete and insert new node
\r
7579 if (rng.startContainer == doc && rng.endContainer == doc) {
\r
7580 // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents
\r
7581 doc.body.innerHTML = content;
\r
7583 rng.deleteContents();
\r
7585 if (doc.body.childNodes.length == 0) {
\r
7586 doc.body.innerHTML = content;
\r
7588 // createContextualFragment doesn't exists in IE 9 DOMRanges
\r
7589 if (rng.createContextualFragment) {
\r
7590 rng.insertNode(rng.createContextualFragment(content));
\r
7592 // Fake createContextualFragment call in IE 9
\r
7593 frag = doc.createDocumentFragment();
\r
7594 temp = doc.createElement('div');
\r
7596 frag.appendChild(temp);
\r
7597 temp.outerHTML = content;
\r
7599 rng.insertNode(frag);
\r
7604 // Move to caret marker
\r
7605 caretNode = self.dom.get('__caret');
\r
7607 // Make sure we wrap it compleatly, Opera fails with a simple select call
\r
7608 rng = doc.createRange();
\r
7609 rng.setStartBefore(caretNode);
\r
7610 rng.setEndBefore(caretNode);
\r
7613 // Remove the caret position
\r
7614 self.dom.remove('__caret');
\r
7619 // Might fail on Opera for some odd reason
\r
7623 // Delete content and get caret text selection
\r
7624 doc.execCommand('Delete', false, null);
\r
7625 rng = self.getRng();
\r
7628 // Explorer removes spaces from the beginning of pasted contents
\r
7629 if (/^\s+/.test(content)) {
\r
7630 rng.pasteHTML('<span id="__mce_tmp">_</span>' + content);
\r
7631 self.dom.remove('__mce_tmp');
\r
7633 rng.pasteHTML(content);
\r
7636 // Dispatch set content event
\r
7637 if (!args.no_events)
\r
7638 self.onSetContent.dispatch(self, args);
\r
7641 getStart : function() {
\r
7642 var rng = this.getRng(), startElement, parentElement, checkRng, node;
\r
7644 if (rng.duplicate || rng.item) {
\r
7645 // Control selection, return first item
\r
7647 return rng.item(0);
\r
7649 // Get start element
\r
7650 checkRng = rng.duplicate();
\r
7651 checkRng.collapse(1);
\r
7652 startElement = checkRng.parentElement();
\r
7654 // Check if range parent is inside the start element, then return the inner parent element
\r
7655 // This will fix issues when a single element is selected, IE would otherwise return the wrong start element
\r
7656 parentElement = node = rng.parentElement();
\r
7657 while (node = node.parentNode) {
\r
7658 if (node == startElement) {
\r
7659 startElement = parentElement;
\r
7664 return startElement;
\r
7666 startElement = rng.startContainer;
\r
7668 if (startElement.nodeType == 1 && startElement.hasChildNodes())
\r
7669 startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)];
\r
7671 if (startElement && startElement.nodeType == 3)
\r
7672 return startElement.parentNode;
\r
7674 return startElement;
\r
7678 getEnd : function() {
\r
7679 var t = this, r = t.getRng(), e, eo;
\r
7681 if (r.duplicate || r.item) {
\r
7685 r = r.duplicate();
\r
7687 e = r.parentElement();
\r
7689 if (e && e.nodeName == 'BODY')
\r
7690 return e.lastChild || e;
\r
7694 e = r.endContainer;
\r
7697 if (e.nodeType == 1 && e.hasChildNodes())
\r
7698 e = e.childNodes[eo > 0 ? eo - 1 : eo];
\r
7700 if (e && e.nodeType == 3)
\r
7701 return e.parentNode;
\r
7707 getBookmark : function(type, normalized) {
\r
7708 var t = this, dom = t.dom, rng, rng2, id, collapsed, name, element, index, chr = '\uFEFF', styles;
\r
7710 function findIndex(name, element) {
\r
7713 each(dom.select(name), function(node, i) {
\r
7714 if (node == element)
\r
7722 function getLocation() {
\r
7723 var rng = t.getRng(true), root = dom.getRoot(), bookmark = {};
\r
7725 function getPoint(rng, start) {
\r
7726 var container = rng[start ? 'startContainer' : 'endContainer'],
\r
7727 offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0;
\r
7729 if (container.nodeType == 3) {
\r
7731 for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling)
\r
7732 offset += node.nodeValue.length;
\r
7735 point.push(offset);
\r
7737 childNodes = container.childNodes;
\r
7739 if (offset >= childNodes.length && childNodes.length) {
\r
7741 offset = Math.max(0, childNodes.length - 1);
\r
7744 point.push(t.dom.nodeIndex(childNodes[offset], normalized) + after);
\r
7747 for (; container && container != root; container = container.parentNode)
\r
7748 point.push(t.dom.nodeIndex(container, normalized));
\r
7753 bookmark.start = getPoint(rng, true);
\r
7755 if (!t.isCollapsed())
\r
7756 bookmark.end = getPoint(rng);
\r
7762 return t.tridentSel.getBookmark(type);
\r
7764 return getLocation();
\r
7767 // Handle simple range
\r
7769 return {rng : t.getRng()};
\r
7772 id = dom.uniqueId();
\r
7773 collapsed = tinyMCE.activeEditor.selection.isCollapsed();
\r
7774 styles = 'overflow:hidden;line-height:0px';
\r
7776 // Explorer method
\r
7777 if (rng.duplicate || rng.item) {
\r
7780 rng2 = rng.duplicate();
\r
7783 // Insert start marker
\r
7785 rng.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_start" style="' + styles + '">' + chr + '</span>');
\r
7787 // Insert end marker
\r
7789 rng2.collapse(false);
\r
7791 // Detect the empty space after block elements in IE and move the end back one character <p></p>] becomes <p>]</p>
\r
7792 rng.moveToElementText(rng2.parentElement());
\r
7793 if (rng.compareEndPoints('StartToEnd', rng2) == 0)
\r
7794 rng2.move('character', -1);
\r
7796 rng2.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_end" style="' + styles + '">' + chr + '</span>');
\r
7799 // IE might throw unspecified error so lets ignore it
\r
7803 // Control selection
\r
7804 element = rng.item(0);
\r
7805 name = element.nodeName;
\r
7807 return {name : name, index : findIndex(name, element)};
\r
7810 element = t.getNode();
\r
7811 name = element.nodeName;
\r
7812 if (name == 'IMG')
\r
7813 return {name : name, index : findIndex(name, element)};
\r
7816 rng2 = rng.cloneRange();
\r
7818 // Insert end marker
\r
7820 rng2.collapse(false);
\r
7821 rng2.insertNode(dom.create('span', {'data-mce-type' : "bookmark", id : id + '_end', style : styles}, chr));
\r
7824 rng.collapse(true);
\r
7825 rng.insertNode(dom.create('span', {'data-mce-type' : "bookmark", id : id + '_start', style : styles}, chr));
\r
7828 t.moveToBookmark({id : id, keep : 1});
\r
7833 moveToBookmark : function(bookmark) {
\r
7834 var t = this, dom = t.dom, marker1, marker2, rng, root, startContainer, endContainer, startOffset, endOffset;
\r
7837 if (bookmark.start) {
\r
7838 rng = dom.createRng();
\r
7839 root = dom.getRoot();
\r
7841 function setEndPoint(start) {
\r
7842 var point = bookmark[start ? 'start' : 'end'], i, node, offset, children;
\r
7845 offset = point[0];
\r
7847 // Find container node
\r
7848 for (node = root, i = point.length - 1; i >= 1; i--) {
\r
7849 children = node.childNodes;
\r
7851 if (point[i] > children.length - 1)
\r
7854 node = children[point[i]];
\r
7857 // Move text offset to best suitable location
\r
7858 if (node.nodeType === 3)
\r
7859 offset = Math.min(point[0], node.nodeValue.length);
\r
7861 // Move element offset to best suitable location
\r
7862 if (node.nodeType === 1)
\r
7863 offset = Math.min(point[0], node.childNodes.length);
\r
7865 // Set offset within container node
\r
7867 rng.setStart(node, offset);
\r
7869 rng.setEnd(node, offset);
\r
7876 return t.tridentSel.moveToBookmark(bookmark);
\r
7878 if (setEndPoint(true) && setEndPoint()) {
\r
7881 } else if (bookmark.id) {
\r
7882 function restoreEndPoint(suffix) {
\r
7883 var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep;
\r
7886 node = marker.parentNode;
\r
7888 if (suffix == 'start') {
\r
7890 idx = dom.nodeIndex(marker);
\r
7892 node = marker.firstChild;
\r
7896 startContainer = endContainer = node;
\r
7897 startOffset = endOffset = idx;
\r
7900 idx = dom.nodeIndex(marker);
\r
7902 node = marker.firstChild;
\r
7906 endContainer = node;
\r
7911 prev = marker.previousSibling;
\r
7912 next = marker.nextSibling;
\r
7914 // Remove all marker text nodes
\r
7915 each(tinymce.grep(marker.childNodes), function(node) {
\r
7916 if (node.nodeType == 3)
\r
7917 node.nodeValue = node.nodeValue.replace(/\uFEFF/g, '');
\r
7920 // Remove marker but keep children if for example contents where inserted into the marker
\r
7921 // Also remove duplicated instances of the marker for example by a split operation or by WebKit auto split on paste feature
\r
7922 while (marker = dom.get(bookmark.id + '_' + suffix))
\r
7923 dom.remove(marker, 1);
\r
7925 // If siblings are text nodes then merge them unless it's Opera since it some how removes the node
\r
7926 // 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
7927 if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !tinymce.isOpera) {
\r
7928 idx = prev.nodeValue.length;
\r
7929 prev.appendData(next.nodeValue);
\r
7932 if (suffix == 'start') {
\r
7933 startContainer = endContainer = prev;
\r
7934 startOffset = endOffset = idx;
\r
7936 endContainer = prev;
\r
7944 function addBogus(node) {
\r
7945 // Adds a bogus BR element for empty block elements or just a space on IE since it renders BR elements incorrectly
\r
7946 if (dom.isBlock(node) && !node.innerHTML)
\r
7947 node.innerHTML = !isIE ? '<br data-mce-bogus="1" />' : ' ';
\r
7952 // Restore start/end points
\r
7953 restoreEndPoint('start');
\r
7954 restoreEndPoint('end');
\r
7956 if (startContainer) {
\r
7957 rng = dom.createRng();
\r
7958 rng.setStart(addBogus(startContainer), startOffset);
\r
7959 rng.setEnd(addBogus(endContainer), endOffset);
\r
7962 } else if (bookmark.name) {
\r
7963 t.select(dom.select(bookmark.name)[bookmark.index]);
\r
7964 } else if (bookmark.rng)
\r
7965 t.setRng(bookmark.rng);
\r
7969 select : function(node, content) {
\r
7970 var t = this, dom = t.dom, rng = dom.createRng(), idx;
\r
7973 idx = dom.nodeIndex(node);
\r
7974 rng.setStart(node.parentNode, idx);
\r
7975 rng.setEnd(node.parentNode, idx + 1);
\r
7977 // Find first/last text node or BR element
\r
7979 function setPoint(node, start) {
\r
7980 var walker = new tinymce.dom.TreeWalker(node, node);
\r
7984 if (node.nodeType == 3 && tinymce.trim(node.nodeValue).length != 0) {
\r
7986 rng.setStart(node, 0);
\r
7988 rng.setEnd(node, node.nodeValue.length);
\r
7994 if (node.nodeName == 'BR') {
\r
7996 rng.setStartBefore(node);
\r
7998 rng.setEndBefore(node);
\r
8002 } while (node = (start ? walker.next() : walker.prev()));
\r
8005 setPoint(node, 1);
\r
8015 isCollapsed : function() {
\r
8016 var t = this, r = t.getRng(), s = t.getSel();
\r
8021 if (r.compareEndPoints)
\r
8022 return r.compareEndPoints('StartToEnd', r) === 0;
\r
8024 return !s || r.collapsed;
\r
8027 collapse : function(to_start) {
\r
8028 var self = this, rng = self.getRng(), node;
\r
8030 // Control range on IE
\r
8032 node = rng.item(0);
\r
8033 rng = self.win.document.body.createTextRange();
\r
8034 rng.moveToElementText(node);
\r
8037 rng.collapse(!!to_start);
\r
8041 getSel : function() {
\r
8042 var t = this, w = this.win;
\r
8044 return w.getSelection ? w.getSelection() : w.document.selection;
\r
8047 getRng : function(w3c) {
\r
8048 var t = this, s, r, elm, doc = t.win.document;
\r
8050 // Found tridentSel object then we need to use that one
\r
8051 if (w3c && t.tridentSel)
\r
8052 return t.tridentSel.getRangeAt(0);
\r
8055 if (s = t.getSel())
\r
8056 r = s.rangeCount > 0 ? s.getRangeAt(0) : (s.createRange ? s.createRange() : doc.createRange());
\r
8058 // IE throws unspecified error here if TinyMCE is placed in a frame/iframe
\r
8061 // We have W3C ranges and it's IE then fake control selection since IE9 doesn't handle that correctly yet
\r
8062 if (tinymce.isIE && r && r.setStart && doc.selection.createRange().item) {
\r
8063 elm = doc.selection.createRange().item(0);
\r
8064 r = doc.createRange();
\r
8065 r.setStartBefore(elm);
\r
8066 r.setEndAfter(elm);
\r
8069 // No range found then create an empty one
\r
8070 // This can occur when the editor is placed in a hidden container element on Gecko
\r
8071 // Or on IE when there was an exception
\r
8073 r = doc.createRange ? doc.createRange() : doc.body.createTextRange();
\r
8075 if (t.selectedRange && t.explicitRange) {
\r
8076 if (r.compareBoundaryPoints(r.START_TO_START, t.selectedRange) === 0 && r.compareBoundaryPoints(r.END_TO_END, t.selectedRange) === 0) {
\r
8077 // Safari, Opera and Chrome only ever select text which causes the range to change.
\r
8078 // This lets us use the originally set range if the selection hasn't been changed by the user.
\r
8079 r = t.explicitRange;
\r
8081 t.selectedRange = null;
\r
8082 t.explicitRange = null;
\r
8089 setRng : function(r) {
\r
8092 if (!t.tridentSel) {
\r
8096 t.explicitRange = r;
\r
8099 s.removeAllRanges();
\r
8101 // IE9 might throw errors here don't know why
\r
8105 // adding range isn't always successful so we need to check range count otherwise an exception can occur
\r
8106 t.selectedRange = s.rangeCount > 0 ? s.getRangeAt(0) : null;
\r
8110 if (r.cloneRange) {
\r
8111 t.tridentSel.addRange(r);
\r
8115 // Is IE specific range
\r
8119 // Needed for some odd IE bug #1843306
\r
8124 setNode : function(n) {
\r
8127 t.setContent(t.dom.getOuterHTML(n));
\r
8132 getNode : function() {
\r
8133 var t = this, rng = t.getRng(), sel = t.getSel(), elm, start = rng.startContainer, end = rng.endContainer;
\r
8135 // Range maybe lost after the editor is made visible again
\r
8137 return t.dom.getRoot();
\r
8139 if (rng.setStart) {
\r
8140 elm = rng.commonAncestorContainer;
\r
8142 // Handle selection a image or other control like element such as anchors
\r
8143 if (!rng.collapsed) {
\r
8144 if (rng.startContainer == rng.endContainer) {
\r
8145 if (rng.endOffset - rng.startOffset < 2) {
\r
8146 if (rng.startContainer.hasChildNodes())
\r
8147 elm = rng.startContainer.childNodes[rng.startOffset];
\r
8151 // If the anchor node is a element instead of a text node then return this element
\r
8152 //if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1)
\r
8153 // return sel.anchorNode.childNodes[sel.anchorOffset];
\r
8155 // Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent.
\r
8156 // This happens when you double click an underlined word in FireFox.
\r
8157 if (start.nodeType === 3 && end.nodeType === 3) {
\r
8158 function skipEmptyTextNodes(n, forwards) {
\r
8160 while (n && n.nodeType === 3 && n.length === 0) {
\r
8161 n = forwards ? n.nextSibling : n.previousSibling;
\r
8165 if (start.length === rng.startOffset) {
\r
8166 start = skipEmptyTextNodes(start.nextSibling, true);
\r
8168 start = start.parentNode;
\r
8170 if (rng.endOffset === 0) {
\r
8171 end = skipEmptyTextNodes(end.previousSibling, false);
\r
8173 end = end.parentNode;
\r
8176 if (start && start === end)
\r
8181 if (elm && elm.nodeType == 3)
\r
8182 return elm.parentNode;
\r
8187 return rng.item ? rng.item(0) : rng.parentElement();
\r
8190 getSelectedBlocks : function(st, en) {
\r
8191 var t = this, dom = t.dom, sb, eb, n, bl = [];
\r
8193 sb = dom.getParent(st || t.getStart(), dom.isBlock);
\r
8194 eb = dom.getParent(en || t.getEnd(), dom.isBlock);
\r
8199 if (sb && eb && sb != eb) {
\r
8202 var walker = new tinymce.dom.TreeWalker(sb, dom.getRoot());
\r
8203 while ((n = walker.next()) && n != eb) {
\r
8204 if (dom.isBlock(n))
\r
8209 if (eb && sb != eb)
\r
8215 normalize : function() {
\r
8216 var self = this, rng, normalized;
\r
8219 // Retain selection direction.
\r
8220 // Lean left/right on Gecko for inline elements.
\r
8221 // Run this on mouse up/key up when the user manually moves the selection
\r
8223 // Normalize only on non IE browsers for now
\r
8227 function normalizeEndPoint(start) {
\r
8228 var container, offset, walker, dom = self.dom, body = dom.getRoot(), node;
\r
8230 container = rng[(start ? 'start' : 'end') + 'Container'];
\r
8231 offset = rng[(start ? 'start' : 'end') + 'Offset'];
\r
8233 // If the container is a document move it to the body element
\r
8234 if (container.nodeType === 9) {
\r
8235 container = container.body;
\r
8239 // If the container is body try move it into the closest text node or position
\r
8240 // TODO: Add more logic here to handle element selection cases
\r
8241 if (container === body) {
\r
8242 // Resolve the index
\r
8243 if (container.hasChildNodes()) {
\r
8244 container = container.childNodes[Math.min(!start && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1)];
\r
8247 // Don't walk into elements that doesn't have any child nodes like a IMG
\r
8248 if (container.hasChildNodes()) {
\r
8249 // Walk the DOM to find a text node to place the caret at or a BR
\r
8251 walker = new tinymce.dom.TreeWalker(container, body);
\r
8253 // Found a text node use that position
\r
8254 if (node.nodeType === 3) {
\r
8255 offset = start ? 0 : node.nodeValue.length - 1;
\r
8257 normalized = true;
\r
8261 // Found a BR/IMG element that we can place the caret before
\r
8262 if (/^(BR|IMG)$/.test(node.nodeName)) {
\r
8263 offset = dom.nodeIndex(node);
\r
8264 container = node.parentNode;
\r
8266 // Put caret after image when moving the end point
\r
8267 if (node.nodeName == "IMG" && !start) {
\r
8271 normalized = true;
\r
8274 } while (node = (start ? walker.next() : walker.prev()));
\r
8279 // Set endpoint if it was normalized
\r
8281 rng['set' + (start ? 'Start' : 'End')](container, offset);
\r
8284 rng = self.getRng();
\r
8286 // Normalize the end points
\r
8287 normalizeEndPoint(true);
\r
8289 if (!rng.collapsed)
\r
8290 normalizeEndPoint();
\r
8292 // Set the selection if it was normalized
\r
8294 //console.log(self.dom.dumpRng(rng));
\r
8299 destroy : function(s) {
\r
8304 // Manual destroy then remove unload handler
\r
8306 tinymce.removeUnload(t.destroy);
\r
8309 // 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
8310 _fixIESelection : function() {
\r
8311 var dom = this.dom, doc = dom.doc, body = doc.body, started, startRng, htmlElm;
\r
8313 // Make HTML element unselectable since we are going to handle selection by hand
\r
8314 doc.documentElement.unselectable = true;
\r
8316 // Return range from point or null if it failed
\r
8317 function rngFromPoint(x, y) {
\r
8318 var rng = body.createTextRange();
\r
8321 rng.moveToPoint(x, y);
\r
8323 // IE sometimes throws and exception, so lets just ignore it
\r
8330 // Fires while the selection is changing
\r
8331 function selectionChange(e) {
\r
8334 // Check if the button is down or not
\r
8336 // Create range from mouse position
\r
8337 pointRng = rngFromPoint(e.x, e.y);
\r
8340 // Check if pointRange is before/after selection then change the endPoint
\r
8341 if (pointRng.compareEndPoints('StartToStart', startRng) > 0)
\r
8342 pointRng.setEndPoint('StartToStart', startRng);
\r
8344 pointRng.setEndPoint('EndToEnd', startRng);
\r
8346 pointRng.select();
\r
8352 // Removes listeners
\r
8353 function endSelection() {
\r
8354 var rng = doc.selection.createRange();
\r
8356 // If the range is collapsed then use the last start range
\r
8357 if (startRng && !rng.item && rng.compareEndPoints('StartToEnd', rng) === 0)
\r
8358 startRng.select();
\r
8360 dom.unbind(doc, 'mouseup', endSelection);
\r
8361 dom.unbind(doc, 'mousemove', selectionChange);
\r
8362 startRng = started = 0;
\r
8365 // Detect when user selects outside BODY
\r
8366 dom.bind(doc, ['mousedown', 'contextmenu'], function(e) {
\r
8367 if (e.target.nodeName === 'HTML') {
\r
8371 // Detect vertical scrollbar, since IE will fire a mousedown on the scrollbar and have target set as HTML
\r
8372 htmlElm = doc.documentElement;
\r
8373 if (htmlElm.scrollHeight > htmlElm.clientHeight)
\r
8377 // Setup start position
\r
8378 startRng = rngFromPoint(e.x, e.y);
\r
8380 // Listen for selection change events
\r
8381 dom.bind(doc, 'mouseup', endSelection);
\r
8382 dom.bind(doc, 'mousemove', selectionChange);
\r
8385 startRng.select();
\r
8393 (function(tinymce) {
\r
8394 tinymce.dom.Serializer = function(settings, dom, schema) {
\r
8395 var onPreProcess, onPostProcess, isIE = tinymce.isIE, each = tinymce.each, htmlParser;
\r
8397 // Support the old apply_source_formatting option
\r
8398 if (!settings.apply_source_formatting)
\r
8399 settings.indent = false;
\r
8401 // Default DOM and Schema if they are undefined
\r
8402 dom = dom || tinymce.DOM;
\r
8403 schema = schema || new tinymce.html.Schema(settings);
\r
8404 settings.entity_encoding = settings.entity_encoding || 'named';
\r
8405 settings.remove_trailing_brs = "remove_trailing_brs" in settings ? settings.remove_trailing_brs : true;
\r
8407 onPreProcess = new tinymce.util.Dispatcher(self);
\r
8409 onPostProcess = new tinymce.util.Dispatcher(self);
\r
8411 htmlParser = new tinymce.html.DomParser(settings, schema);
\r
8413 // Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed
\r
8414 htmlParser.addAttributeFilter('src,href,style', function(nodes, name) {
\r
8415 var i = nodes.length, node, value, internalName = 'data-mce-' + name, urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope, undef;
\r
8420 value = node.attributes.map[internalName];
\r
8421 if (value !== undef) {
\r
8422 // Set external name to internal value and remove internal
\r
8423 node.attr(name, value.length > 0 ? value : null);
\r
8424 node.attr(internalName, null);
\r
8426 // No internal attribute found then convert the value we have in the DOM
\r
8427 value = node.attributes.map[name];
\r
8429 if (name === "style")
\r
8430 value = dom.serializeStyle(dom.parseStyle(value), node.name);
\r
8431 else if (urlConverter)
\r
8432 value = urlConverter.call(urlConverterScope, value, name, node.name);
\r
8434 node.attr(name, value.length > 0 ? value : null);
\r
8439 // Remove internal classes mceItem<..>
\r
8440 htmlParser.addAttributeFilter('class', function(nodes, name) {
\r
8441 var i = nodes.length, node, value;
\r
8445 value = node.attr('class').replace(/\s*mce(Item\w+|Selected)\s*/g, '');
\r
8446 node.attr('class', value.length > 0 ? value : null);
\r
8450 // Remove bookmark elements
\r
8451 htmlParser.addAttributeFilter('data-mce-type', function(nodes, name, args) {
\r
8452 var i = nodes.length, node;
\r
8457 if (node.attributes.map['data-mce-type'] === 'bookmark' && !args.cleanup)
\r
8462 // Force script into CDATA sections and remove the mce- prefix also add comments around styles
\r
8463 htmlParser.addNodeFilter('script,style', function(nodes, name) {
\r
8464 var i = nodes.length, node, value;
\r
8466 function trim(value) {
\r
8467 return value.replace(/(<!--\[CDATA\[|\]\]-->)/g, '\n')
\r
8468 .replace(/^[\r\n]*|[\r\n]*$/g, '')
\r
8469 .replace(/^\s*((<!--)?(\s*\/\/)?\s*<!\[CDATA\[|(<!--\s*)?\/\*\s*<!\[CDATA\[\s*\*\/|(\/\/)?\s*<!--|\/\*\s*<!--\s*\*\/)\s*[\r\n]*/gi, '')
\r
8470 .replace(/\s*(\/\*\s*\]\]>\s*\*\/(-->)?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g, '');
\r
8475 value = node.firstChild ? node.firstChild.value : '';
\r
8477 if (name === "script") {
\r
8478 // Remove mce- prefix from script elements
\r
8479 node.attr('type', (node.attr('type') || 'text/javascript').replace(/^mce\-/, ''));
\r
8481 if (value.length > 0)
\r
8482 node.firstChild.value = '// <![CDATA[\n' + trim(value) + '\n// ]]>';
\r
8484 if (value.length > 0)
\r
8485 node.firstChild.value = '<!--\n' + trim(value) + '\n-->';
\r
8490 // Convert comments to cdata and handle protected comments
\r
8491 htmlParser.addNodeFilter('#comment', function(nodes, name) {
\r
8492 var i = nodes.length, node;
\r
8497 if (node.value.indexOf('[CDATA[') === 0) {
\r
8498 node.name = '#cdata';
\r
8500 node.value = node.value.replace(/^\[CDATA\[|\]\]$/g, '');
\r
8501 } else if (node.value.indexOf('mce:protected ') === 0) {
\r
8502 node.name = "#text";
\r
8505 node.value = unescape(node.value).substr(14);
\r
8510 htmlParser.addNodeFilter('xml:namespace,input', function(nodes, name) {
\r
8511 var i = nodes.length, node;
\r
8515 if (node.type === 7)
\r
8517 else if (node.type === 1) {
\r
8518 if (name === "input" && !("type" in node.attributes.map))
\r
8519 node.attr('type', 'text');
\r
8524 // Fix list elements, TODO: Replace this later
\r
8525 if (settings.fix_list_elements) {
\r
8526 htmlParser.addNodeFilter('ul,ol', function(nodes, name) {
\r
8527 var i = nodes.length, node, parentNode;
\r
8531 parentNode = node.parent;
\r
8533 if (parentNode.name === 'ul' || parentNode.name === 'ol') {
\r
8534 if (node.prev && node.prev.name === 'li') {
\r
8535 node.prev.append(node);
\r
8542 // Remove internal data attributes
\r
8543 htmlParser.addAttributeFilter('data-mce-src,data-mce-href,data-mce-style', function(nodes, name) {
\r
8544 var i = nodes.length;
\r
8547 nodes[i].attr(name, null);
\r
8551 // Return public methods
\r
8555 addNodeFilter : htmlParser.addNodeFilter,
\r
8557 addAttributeFilter : htmlParser.addAttributeFilter,
\r
8559 onPreProcess : onPreProcess,
\r
8561 onPostProcess : onPostProcess,
\r
8563 serialize : function(node, args) {
\r
8564 var impl, doc, oldDoc, htmlSerializer, content;
\r
8566 // Explorer won't clone contents of script and style and the
\r
8567 // selected index of select elements are cleared on a clone operation.
\r
8568 if (isIE && dom.select('script,style,select,map').length > 0) {
\r
8569 content = node.innerHTML;
\r
8570 node = node.cloneNode(false);
\r
8571 dom.setHTML(node, content);
\r
8573 node = node.cloneNode(true);
\r
8575 // Nodes needs to be attached to something in WebKit/Opera
\r
8576 // Older builds of Opera crashes if you attach the node to an document created dynamically
\r
8577 // and since we can't feature detect a crash we need to sniff the acutal build number
\r
8578 // This fix will make DOM ranges and make Sizzle happy!
\r
8579 impl = node.ownerDocument.implementation;
\r
8580 if (impl.createHTMLDocument) {
\r
8581 // Create an empty HTML document
\r
8582 doc = impl.createHTMLDocument("");
\r
8584 // Add the element or it's children if it's a body element to the new document
\r
8585 each(node.nodeName == 'BODY' ? node.childNodes : [node], function(node) {
\r
8586 doc.body.appendChild(doc.importNode(node, true));
\r
8589 // Grab first child or body element for serialization
\r
8590 if (node.nodeName != 'BODY')
\r
8591 node = doc.body.firstChild;
\r
8595 // set the new document in DOMUtils so createElement etc works
\r
8600 args = args || {};
\r
8601 args.format = args.format || 'html';
\r
8604 if (!args.no_events) {
\r
8606 onPreProcess.dispatch(self, args);
\r
8609 // Setup serializer
\r
8610 htmlSerializer = new tinymce.html.Serializer(settings, schema);
\r
8612 // Parse and serialize HTML
\r
8613 args.content = htmlSerializer.serialize(
\r
8614 htmlParser.parse(args.getInner ? node.innerHTML : tinymce.trim(dom.getOuterHTML(node), args), args)
\r
8617 // Replace all BOM characters for now until we can find a better solution
\r
8618 if (!args.cleanup)
\r
8619 args.content = args.content.replace(/\uFEFF|\u200B/g, '');
\r
8622 if (!args.no_events)
\r
8623 onPostProcess.dispatch(self, args);
\r
8625 // Restore the old document if it was changed
\r
8631 return args.content;
\r
8634 addRules : function(rules) {
\r
8635 schema.addValidElements(rules);
\r
8638 setRules : function(rules) {
\r
8639 schema.setValidElements(rules);
\r
8644 (function(tinymce) {
\r
8645 tinymce.dom.ScriptLoader = function(settings) {
\r
8651 scriptLoadedCallbacks = {},
\r
8652 queueLoadedCallbacks = [],
\r
8656 function loadScript(url, callback) {
\r
8657 var t = this, dom = tinymce.DOM, elm, uri, loc, id;
\r
8659 // Execute callback when script is loaded
\r
8664 elm.onreadystatechange = elm.onload = elm = null;
\r
8669 function error() {
\r
8670 // Report the error so it's easier for people to spot loading errors
\r
8671 if (typeof(console) !== "undefined" && console.log)
\r
8672 console.log("Failed to load: " + url);
\r
8674 // We can't mark it as done if there is a load error since
\r
8675 // A) We don't want to produce 404 errors on the server and
\r
8676 // B) the onerror event won't fire on all browsers.
\r
8680 id = dom.uniqueId();
\r
8682 if (tinymce.isIE6) {
\r
8683 uri = new tinymce.util.URI(url);
\r
8686 // If script is from same domain and we
\r
8687 // use IE 6 then use XHR since it's more reliable
\r
8688 if (uri.host == loc.hostname && uri.port == loc.port && (uri.protocol + ':') == loc.protocol && uri.protocol.toLowerCase() != 'file') {
\r
8689 tinymce.util.XHR.send({
\r
8690 url : tinymce._addVer(uri.getURI()),
\r
8691 success : function(content) {
\r
8692 // Create new temp script element
\r
8693 var script = dom.create('script', {
\r
8694 type : 'text/javascript'
\r
8697 // Evaluate script in global scope
\r
8698 script.text = content;
\r
8699 document.getElementsByTagName('head')[0].appendChild(script);
\r
8700 dom.remove(script);
\r
8712 // Create new script element
\r
8713 elm = dom.create('script', {
\r
8715 type : 'text/javascript',
\r
8716 src : tinymce._addVer(url)
\r
8719 // Add onload listener for non IE browsers since IE9
\r
8720 // fires onload event before the script is parsed and executed
\r
8721 if (!tinymce.isIE)
\r
8722 elm.onload = done;
\r
8724 // Add onerror event will get fired on some browsers but not all of them
\r
8725 elm.onerror = error;
\r
8727 // Opera 9.60 doesn't seem to fire the onreadystate event at correctly
\r
8728 if (!tinymce.isOpera) {
\r
8729 elm.onreadystatechange = function() {
\r
8730 var state = elm.readyState;
\r
8732 // Loaded state is passed on IE 6 however there
\r
8733 // are known issues with this method but we can't use
\r
8734 // XHR in a cross domain loading
\r
8735 if (state == 'complete' || state == 'loaded')
\r
8740 // Most browsers support this feature so we report errors
\r
8741 // for those at least to help users track their missing plugins etc
\r
8742 // todo: Removed since it produced error if the document is unloaded by navigating away, re-add it as an option
\r
8743 /*elm.onerror = function() {
\r
8744 alert('Failed to load: ' + url);
\r
8747 // Add script to document
\r
8748 (document.getElementsByTagName('head')[0] || document.body).appendChild(elm);
\r
8751 this.isDone = function(url) {
\r
8752 return states[url] == LOADED;
\r
8755 this.markDone = function(url) {
\r
8756 states[url] = LOADED;
\r
8759 this.add = this.load = function(url, callback, scope) {
\r
8760 var item, state = states[url];
\r
8762 // Add url to load queue
\r
8763 if (state == undefined) {
\r
8765 states[url] = QUEUED;
\r
8769 // Store away callback for later execution
\r
8770 if (!scriptLoadedCallbacks[url])
\r
8771 scriptLoadedCallbacks[url] = [];
\r
8773 scriptLoadedCallbacks[url].push({
\r
8775 scope : scope || this
\r
8780 this.loadQueue = function(callback, scope) {
\r
8781 this.loadScripts(queue, callback, scope);
\r
8784 this.loadScripts = function(scripts, callback, scope) {
\r
8787 function execScriptLoadedCallbacks(url) {
\r
8788 // Execute URL callback functions
\r
8789 tinymce.each(scriptLoadedCallbacks[url], function(callback) {
\r
8790 callback.func.call(callback.scope);
\r
8793 scriptLoadedCallbacks[url] = undefined;
\r
8796 queueLoadedCallbacks.push({
\r
8798 scope : scope || this
\r
8801 loadScripts = function() {
\r
8802 var loadingScripts = tinymce.grep(scripts);
\r
8804 // Current scripts has been handled
\r
8805 scripts.length = 0;
\r
8807 // Load scripts that needs to be loaded
\r
8808 tinymce.each(loadingScripts, function(url) {
\r
8809 // Script is already loaded then execute script callbacks directly
\r
8810 if (states[url] == LOADED) {
\r
8811 execScriptLoadedCallbacks(url);
\r
8815 // Is script not loading then start loading it
\r
8816 if (states[url] != LOADING) {
\r
8817 states[url] = LOADING;
\r
8820 loadScript(url, function() {
\r
8821 states[url] = LOADED;
\r
8824 execScriptLoadedCallbacks(url);
\r
8826 // Load more scripts if they where added by the recently loaded script
\r
8832 // No scripts are currently loading then execute all pending queue loaded callbacks
\r
8834 tinymce.each(queueLoadedCallbacks, function(callback) {
\r
8835 callback.func.call(callback.scope);
\r
8838 queueLoadedCallbacks.length = 0;
\r
8846 // Global script loader
\r
8847 tinymce.ScriptLoader = new tinymce.dom.ScriptLoader();
\r
8850 tinymce.dom.TreeWalker = function(start_node, root_node) {
\r
8851 var node = start_node;
\r
8853 function findSibling(node, start_name, sibling_name, shallow) {
\r
8854 var sibling, parent;
\r
8857 // Walk into nodes if it has a start
\r
8858 if (!shallow && node[start_name])
\r
8859 return node[start_name];
\r
8861 // Return the sibling if it has one
\r
8862 if (node != root_node) {
\r
8863 sibling = node[sibling_name];
\r
8867 // Walk up the parents to look for siblings
\r
8868 for (parent = node.parentNode; parent && parent != root_node; parent = parent.parentNode) {
\r
8869 sibling = parent[sibling_name];
\r
8877 this.current = function() {
\r
8881 this.next = function(shallow) {
\r
8882 return (node = findSibling(node, 'firstChild', 'nextSibling', shallow));
\r
8885 this.prev = function(shallow) {
\r
8886 return (node = findSibling(node, 'lastChild', 'previousSibling', shallow));
\r
8890 (function(tinymce) {
\r
8891 tinymce.dom.RangeUtils = function(dom) {
\r
8892 var INVISIBLE_CHAR = '\uFEFF';
\r
8894 this.walk = function(rng, callback) {
\r
8895 var startContainer = rng.startContainer,
\r
8896 startOffset = rng.startOffset,
\r
8897 endContainer = rng.endContainer,
\r
8898 endOffset = rng.endOffset,
\r
8899 ancestor, startPoint,
\r
8900 endPoint, node, parent, siblings, nodes;
\r
8902 // Handle table cell selection the table plugin enables
\r
8903 // you to fake select table cells and perform formatting actions on them
\r
8904 nodes = dom.select('td.mceSelected,th.mceSelected');
\r
8905 if (nodes.length > 0) {
\r
8906 tinymce.each(nodes, function(node) {
\r
8913 function exclude(nodes) {
\r
8916 // First node is excluded
\r
8918 if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) {
\r
8919 nodes.splice(0, 1);
\r
8922 // Last node is excluded
\r
8923 node = nodes[nodes.length - 1];
\r
8924 if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) {
\r
8925 nodes.splice(nodes.length - 1, 1);
\r
8931 function collectSiblings(node, name, end_node) {
\r
8932 var siblings = [];
\r
8934 for (; node && node != end_node; node = node[name])
\r
8935 siblings.push(node);
\r
8940 function findEndPoint(node, root) {
\r
8942 if (node.parentNode == root)
\r
8945 node = node.parentNode;
\r
8949 function walkBoundary(start_node, end_node, next) {
\r
8950 var siblingName = next ? 'nextSibling' : 'previousSibling';
\r
8952 for (node = start_node, parent = node.parentNode; node && node != end_node; node = parent) {
\r
8953 parent = node.parentNode;
\r
8954 siblings = collectSiblings(node == start_node ? node : node[siblingName], siblingName);
\r
8956 if (siblings.length) {
\r
8958 siblings.reverse();
\r
8960 callback(exclude(siblings));
\r
8965 // If index based start position then resolve it
\r
8966 if (startContainer.nodeType == 1 && startContainer.hasChildNodes())
\r
8967 startContainer = startContainer.childNodes[startOffset];
\r
8969 // If index based end position then resolve it
\r
8970 if (endContainer.nodeType == 1 && endContainer.hasChildNodes())
\r
8971 endContainer = endContainer.childNodes[Math.min(endOffset - 1, endContainer.childNodes.length - 1)];
\r
8974 if (startContainer == endContainer)
\r
8975 return callback(exclude([startContainer]));
\r
8977 // Find common ancestor and end points
\r
8978 ancestor = dom.findCommonAncestor(startContainer, endContainer);
\r
8980 // Process left side
\r
8981 for (node = startContainer; node; node = node.parentNode) {
\r
8982 if (node === endContainer)
\r
8983 return walkBoundary(startContainer, ancestor, true);
\r
8985 if (node === ancestor)
\r
8989 // Process right side
\r
8990 for (node = endContainer; node; node = node.parentNode) {
\r
8991 if (node === startContainer)
\r
8992 return walkBoundary(endContainer, ancestor);
\r
8994 if (node === ancestor)
\r
8998 // Find start/end point
\r
8999 startPoint = findEndPoint(startContainer, ancestor) || startContainer;
\r
9000 endPoint = findEndPoint(endContainer, ancestor) || endContainer;
\r
9003 walkBoundary(startContainer, startPoint, true);
\r
9005 // Walk the middle from start to end point
\r
9006 siblings = collectSiblings(
\r
9007 startPoint == startContainer ? startPoint : startPoint.nextSibling,
\r
9009 endPoint == endContainer ? endPoint.nextSibling : endPoint
\r
9012 if (siblings.length)
\r
9013 callback(exclude(siblings));
\r
9015 // Walk right leaf
\r
9016 walkBoundary(endContainer, endPoint);
\r
9019 this.split = function(rng) {
\r
9020 var startContainer = rng.startContainer,
\r
9021 startOffset = rng.startOffset,
\r
9022 endContainer = rng.endContainer,
\r
9023 endOffset = rng.endOffset;
\r
9025 function splitText(node, offset) {
\r
9026 return node.splitText(offset);
\r
9029 // Handle single text node
\r
9030 if (startContainer == endContainer && startContainer.nodeType == 3) {
\r
9031 if (startOffset > 0 && startOffset < startContainer.nodeValue.length) {
\r
9032 endContainer = splitText(startContainer, startOffset);
\r
9033 startContainer = endContainer.previousSibling;
\r
9035 if (endOffset > startOffset) {
\r
9036 endOffset = endOffset - startOffset;
\r
9037 startContainer = endContainer = splitText(endContainer, endOffset).previousSibling;
\r
9038 endOffset = endContainer.nodeValue.length;
\r
9045 // Split startContainer text node if needed
\r
9046 if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) {
\r
9047 startContainer = splitText(startContainer, startOffset);
\r
9051 // Split endContainer text node if needed
\r
9052 if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) {
\r
9053 endContainer = splitText(endContainer, endOffset).previousSibling;
\r
9054 endOffset = endContainer.nodeValue.length;
\r
9059 startContainer : startContainer,
\r
9060 startOffset : startOffset,
\r
9061 endContainer : endContainer,
\r
9062 endOffset : endOffset
\r
9068 tinymce.dom.RangeUtils.compareRanges = function(rng1, rng2) {
\r
9069 if (rng1 && rng2) {
\r
9070 // Compare native IE ranges
\r
9071 if (rng1.item || rng1.duplicate) {
\r
9072 // Both are control ranges and the selected element matches
\r
9073 if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0))
\r
9076 // Both are text ranges and the range matches
\r
9077 if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1))
\r
9080 // Compare w3c ranges
\r
9081 return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset;
\r
9089 (function(tinymce) {
\r
9090 var Event = tinymce.dom.Event, each = tinymce.each;
\r
9092 tinymce.create('tinymce.ui.KeyboardNavigation', {
\r
9093 KeyboardNavigation: function(settings, dom) {
\r
9094 var t = this, root = settings.root, items = settings.items,
\r
9095 enableUpDown = settings.enableUpDown, enableLeftRight = settings.enableLeftRight || !settings.enableUpDown,
\r
9096 excludeFromTabOrder = settings.excludeFromTabOrder,
\r
9097 itemFocussed, itemBlurred, rootKeydown, rootFocussed, focussedId;
\r
9099 dom = dom || tinymce.DOM;
\r
9101 itemFocussed = function(evt) {
\r
9102 focussedId = evt.target.id;
\r
9105 itemBlurred = function(evt) {
\r
9106 dom.setAttrib(evt.target.id, 'tabindex', '-1');
\r
9109 rootFocussed = function(evt) {
\r
9110 var item = dom.get(focussedId);
\r
9111 dom.setAttrib(item, 'tabindex', '0');
\r
9115 t.focus = function() {
\r
9116 dom.get(focussedId).focus();
\r
9119 t.destroy = function() {
\r
9120 each(items, function(item) {
\r
9121 dom.unbind(dom.get(item.id), 'focus', itemFocussed);
\r
9122 dom.unbind(dom.get(item.id), 'blur', itemBlurred);
\r
9125 dom.unbind(dom.get(root), 'focus', rootFocussed);
\r
9126 dom.unbind(dom.get(root), 'keydown', rootKeydown);
\r
9128 items = dom = root = t.focus = itemFocussed = itemBlurred = rootKeydown = rootFocussed = null;
\r
9129 t.destroy = function() {};
\r
9132 t.moveFocus = function(dir, evt) {
\r
9133 var idx = -1, controls = t.controls, newFocus;
\r
9138 each(items, function(item, index) {
\r
9139 if (item.id === focussedId) {
\r
9147 idx = items.length - 1;
\r
9148 } else if (idx >= items.length) {
\r
9152 newFocus = items[idx];
\r
9153 dom.setAttrib(focussedId, 'tabindex', '-1');
\r
9154 dom.setAttrib(newFocus.id, 'tabindex', '0');
\r
9155 dom.get(newFocus.id).focus();
\r
9157 if (settings.actOnFocus) {
\r
9158 settings.onAction(newFocus.id);
\r
9162 Event.cancel(evt);
\r
9165 rootKeydown = function(evt) {
\r
9166 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
9168 switch (evt.keyCode) {
\r
9170 if (enableLeftRight) t.moveFocus(-1);
\r
9173 case DOM_VK_RIGHT:
\r
9174 if (enableLeftRight) t.moveFocus(1);
\r
9178 if (enableUpDown) t.moveFocus(-1);
\r
9182 if (enableUpDown) t.moveFocus(1);
\r
9185 case DOM_VK_ESCAPE:
\r
9186 if (settings.onCancel) {
\r
9187 settings.onCancel();
\r
9188 Event.cancel(evt);
\r
9192 case DOM_VK_ENTER:
\r
9193 case DOM_VK_RETURN:
\r
9194 case DOM_VK_SPACE:
\r
9195 if (settings.onAction) {
\r
9196 settings.onAction(focussedId);
\r
9197 Event.cancel(evt);
\r
9203 // Set up state and listeners for each item.
\r
9204 each(items, function(item, idx) {
\r
9208 item.id = dom.uniqueId('_mce_item_');
\r
9211 if (excludeFromTabOrder) {
\r
9212 dom.bind(item.id, 'blur', itemBlurred);
\r
9215 tabindex = (idx === 0 ? '0' : '-1');
\r
9218 dom.setAttrib(item.id, 'tabindex', tabindex);
\r
9219 dom.bind(dom.get(item.id), 'focus', itemFocussed);
\r
9222 // Setup initial state for root element.
\r
9224 focussedId = items[0].id;
\r
9227 dom.setAttrib(root, 'tabindex', '-1');
\r
9229 // Setup listeners for root element.
\r
9230 dom.bind(dom.get(root), 'focus', rootFocussed);
\r
9231 dom.bind(dom.get(root), 'keydown', rootKeydown);
\r
9236 (function(tinymce) {
\r
9237 // Shorten class names
\r
9238 var DOM = tinymce.DOM, is = tinymce.is;
\r
9240 tinymce.create('tinymce.ui.Control', {
\r
9241 Control : function(id, s, editor) {
\r
9243 this.settings = s = s || {};
\r
9244 this.rendered = false;
\r
9245 this.onRender = new tinymce.util.Dispatcher(this);
\r
9246 this.classPrefix = '';
\r
9247 this.scope = s.scope || this;
\r
9248 this.disabled = 0;
\r
9250 this.editor = editor;
\r
9253 setAriaProperty : function(property, value) {
\r
9254 var element = DOM.get(this.id + '_aria') || DOM.get(this.id);
\r
9256 DOM.setAttrib(element, 'aria-' + property, !!value);
\r
9260 focus : function() {
\r
9261 DOM.get(this.id).focus();
\r
9264 setDisabled : function(s) {
\r
9265 if (s != this.disabled) {
\r
9266 this.setAriaProperty('disabled', s);
\r
9268 this.setState('Disabled', s);
\r
9269 this.setState('Enabled', !s);
\r
9270 this.disabled = s;
\r
9274 isDisabled : function() {
\r
9275 return this.disabled;
\r
9278 setActive : function(s) {
\r
9279 if (s != this.active) {
\r
9280 this.setState('Active', s);
\r
9282 this.setAriaProperty('pressed', s);
\r
9286 isActive : function() {
\r
9287 return this.active;
\r
9290 setState : function(c, s) {
\r
9291 var n = DOM.get(this.id);
\r
9293 c = this.classPrefix + c;
\r
9296 DOM.addClass(n, c);
\r
9298 DOM.removeClass(n, c);
\r
9301 isRendered : function() {
\r
9302 return this.rendered;
\r
9305 renderHTML : function() {
\r
9308 renderTo : function(n) {
\r
9309 DOM.setHTML(n, this.renderHTML());
\r
9312 postRender : function() {
\r
9315 // Set pending states
\r
9316 if (is(t.disabled)) {
\r
9322 if (is(t.active)) {
\r
9329 remove : function() {
\r
9330 DOM.remove(this.id);
\r
9334 destroy : function() {
\r
9335 tinymce.dom.Event.clear(this.id);
\r
9339 tinymce.create('tinymce.ui.Container:tinymce.ui.Control', {
\r
9340 Container : function(id, s, editor) {
\r
9341 this.parent(id, s, editor);
\r
9343 this.controls = [];
\r
9348 add : function(c) {
\r
9349 this.lookup[c.id] = c;
\r
9350 this.controls.push(c);
\r
9355 get : function(n) {
\r
9356 return this.lookup[n];
\r
9361 tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', {
\r
9362 Separator : function(id, s) {
\r
9363 this.parent(id, s);
\r
9364 this.classPrefix = 'mceSeparator';
\r
9365 this.setDisabled(true);
\r
9368 renderHTML : function() {
\r
9369 return tinymce.DOM.createHTML('span', {'class' : this.classPrefix, role : 'separator', 'aria-orientation' : 'vertical', tabindex : '-1'});
\r
9373 (function(tinymce) {
\r
9374 var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, walk = tinymce.walk;
\r
9376 tinymce.create('tinymce.ui.MenuItem:tinymce.ui.Control', {
\r
9377 MenuItem : function(id, s) {
\r
9378 this.parent(id, s);
\r
9379 this.classPrefix = 'mceMenuItem';
\r
9382 setSelected : function(s) {
\r
9383 this.setState('Selected', s);
\r
9384 this.setAriaProperty('checked', !!s);
\r
9385 this.selected = s;
\r
9388 isSelected : function() {
\r
9389 return this.selected;
\r
9392 postRender : function() {
\r
9397 // Set pending state
\r
9398 if (is(t.selected))
\r
9399 t.setSelected(t.selected);
\r
9404 (function(tinymce) {
\r
9405 var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, walk = tinymce.walk;
\r
9407 tinymce.create('tinymce.ui.Menu:tinymce.ui.MenuItem', {
\r
9408 Menu : function(id, s) {
\r
9413 t.collapsed = false;
\r
9415 t.onAddItem = new tinymce.util.Dispatcher(this);
\r
9418 expand : function(d) {
\r
9422 walk(t, function(o) {
\r
9428 t.collapsed = false;
\r
9431 collapse : function(d) {
\r
9435 walk(t, function(o) {
\r
9441 t.collapsed = true;
\r
9444 isCollapsed : function() {
\r
9445 return this.collapsed;
\r
9448 add : function(o) {
\r
9450 o = new tinymce.ui.MenuItem(o.id || DOM.uniqueId(), o);
\r
9452 this.onAddItem.dispatch(this, o);
\r
9454 return this.items[o.id] = o;
\r
9457 addSeparator : function() {
\r
9458 return this.add({separator : true});
\r
9461 addMenu : function(o) {
\r
9463 o = this.createMenu(o);
\r
9467 return this.add(o);
\r
9470 hasMenus : function() {
\r
9471 return this.menuCount !== 0;
\r
9474 remove : function(o) {
\r
9475 delete this.items[o.id];
\r
9478 removeAll : function() {
\r
9481 walk(t, function(o) {
\r
9493 createMenu : function(o) {
\r
9494 var m = new tinymce.ui.Menu(o.id || DOM.uniqueId(), o);
\r
9496 m.onAddItem.add(this.onAddItem.dispatch, this.onAddItem);
\r
9502 (function(tinymce) {
\r
9503 var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, Event = tinymce.dom.Event, Element = tinymce.dom.Element;
\r
9505 tinymce.create('tinymce.ui.DropMenu:tinymce.ui.Menu', {
\r
9506 DropMenu : function(id, s) {
\r
9508 s.container = s.container || DOM.doc.body;
\r
9509 s.offset_x = s.offset_x || 0;
\r
9510 s.offset_y = s.offset_y || 0;
\r
9511 s.vp_offset_x = s.vp_offset_x || 0;
\r
9512 s.vp_offset_y = s.vp_offset_y || 0;
\r
9514 if (is(s.icons) && !s.icons)
\r
9515 s['class'] += ' mceNoIcons';
\r
9517 this.parent(id, s);
\r
9518 this.onShowMenu = new tinymce.util.Dispatcher(this);
\r
9519 this.onHideMenu = new tinymce.util.Dispatcher(this);
\r
9520 this.classPrefix = 'mceMenu';
\r
9523 createMenu : function(s) {
\r
9524 var t = this, cs = t.settings, m;
\r
9526 s.container = s.container || cs.container;
\r
9528 s.constrain = s.constrain || cs.constrain;
\r
9529 s['class'] = s['class'] || cs['class'];
\r
9530 s.vp_offset_x = s.vp_offset_x || cs.vp_offset_x;
\r
9531 s.vp_offset_y = s.vp_offset_y || cs.vp_offset_y;
\r
9532 s.keyboard_focus = cs.keyboard_focus;
\r
9533 m = new tinymce.ui.DropMenu(s.id || DOM.uniqueId(), s);
\r
9535 m.onAddItem.add(t.onAddItem.dispatch, t.onAddItem);
\r
9540 focus : function() {
\r
9542 if (t.keyboardNav) {
\r
9543 t.keyboardNav.focus();
\r
9547 update : function() {
\r
9548 var t = this, s = t.settings, tb = DOM.get('menu_' + t.id + '_tbl'), co = DOM.get('menu_' + t.id + '_co'), tw, th;
\r
9550 tw = s.max_width ? Math.min(tb.clientWidth, s.max_width) : tb.clientWidth;
\r
9551 th = s.max_height ? Math.min(tb.clientHeight, s.max_height) : tb.clientHeight;
\r
9553 if (!DOM.boxModel)
\r
9554 t.element.setStyles({width : tw + 2, height : th + 2});
\r
9556 t.element.setStyles({width : tw, height : th});
\r
9559 DOM.setStyle(co, 'width', tw);
\r
9561 if (s.max_height) {
\r
9562 DOM.setStyle(co, 'height', th);
\r
9564 if (tb.clientHeight < s.max_height)
\r
9565 DOM.setStyle(co, 'overflow', 'hidden');
\r
9569 showMenu : function(x, y, px) {
\r
9570 var t = this, s = t.settings, co, vp = DOM.getViewPort(), w, h, mx, my, ot = 2, dm, tb, cp = t.classPrefix;
\r
9574 if (t.isMenuVisible)
\r
9577 if (!t.rendered) {
\r
9578 co = DOM.add(t.settings.container, t.renderNode());
\r
9580 each(t.items, function(o) {
\r
9584 t.element = new Element('menu_' + t.id, {blocker : 1, container : s.container});
\r
9586 co = DOM.get('menu_' + t.id);
\r
9588 // Move layer out of sight unless it's Opera since it scrolls to top of page due to an bug
\r
9589 if (!tinymce.isOpera)
\r
9590 DOM.setStyles(co, {left : -0xFFFF , top : -0xFFFF});
\r
9595 x += s.offset_x || 0;
\r
9596 y += s.offset_y || 0;
\r
9600 // Move inside viewport if not submenu
\r
9601 if (s.constrain) {
\r
9602 w = co.clientWidth - ot;
\r
9603 h = co.clientHeight - ot;
\r
9607 if ((x + s.vp_offset_x + w) > mx)
\r
9608 x = px ? px - w : Math.max(0, (mx - s.vp_offset_x) - w);
\r
9610 if ((y + s.vp_offset_y + h) > my)
\r
9611 y = Math.max(0, (my - s.vp_offset_y) - h);
\r
9614 DOM.setStyles(co, {left : x , top : y});
\r
9615 t.element.update();
\r
9617 t.isMenuVisible = 1;
\r
9618 t.mouseClickFunc = Event.add(co, 'click', function(e) {
\r
9623 if (e && (e = DOM.getParent(e, 'tr')) && !DOM.hasClass(e, cp + 'ItemSub')) {
\r
9624 m = t.items[e.id];
\r
9626 if (m.isDisabled())
\r
9635 dm = dm.settings.parent;
\r
9638 if (m.settings.onclick)
\r
9639 m.settings.onclick(e);
\r
9641 return Event.cancel(e); // Cancel to fix onbeforeunload problem
\r
9645 if (t.hasMenus()) {
\r
9646 t.mouseOverFunc = Event.add(co, 'mouseover', function(e) {
\r
9650 if (e && (e = DOM.getParent(e, 'tr'))) {
\r
9651 m = t.items[e.id];
\r
9654 t.lastMenu.collapse(1);
\r
9656 if (m.isDisabled())
\r
9659 if (e && DOM.hasClass(e, cp + 'ItemSub')) {
\r
9660 //p = DOM.getPos(s.container);
\r
9661 r = DOM.getRect(e);
\r
9662 m.showMenu((r.x + r.w - ot), r.y - ot, r.x);
\r
9664 DOM.addClass(DOM.get(m.id).firstChild, cp + 'ItemActive');
\r
9670 Event.add(co, 'keydown', t._keyHandler, t);
\r
9672 t.onShowMenu.dispatch(t);
\r
9674 if (s.keyboard_focus) {
\r
9675 t._setupKeyboardNav();
\r
9679 hideMenu : function(c) {
\r
9680 var t = this, co = DOM.get('menu_' + t.id), e;
\r
9682 if (!t.isMenuVisible)
\r
9685 if (t.keyboardNav) t.keyboardNav.destroy();
\r
9686 Event.remove(co, 'mouseover', t.mouseOverFunc);
\r
9687 Event.remove(co, 'click', t.mouseClickFunc);
\r
9688 Event.remove(co, 'keydown', t._keyHandler);
\r
9690 t.isMenuVisible = 0;
\r
9698 if (e = DOM.get(t.id))
\r
9699 DOM.removeClass(e.firstChild, t.classPrefix + 'ItemActive');
\r
9701 t.onHideMenu.dispatch(t);
\r
9704 add : function(o) {
\r
9709 if (t.isRendered && (co = DOM.get('menu_' + t.id)))
\r
9710 t._add(DOM.select('tbody', co)[0], o);
\r
9715 collapse : function(d) {
\r
9720 remove : function(o) {
\r
9724 return this.parent(o);
\r
9727 destroy : function() {
\r
9728 var t = this, co = DOM.get('menu_' + t.id);
\r
9730 if (t.keyboardNav) t.keyboardNav.destroy();
\r
9731 Event.remove(co, 'mouseover', t.mouseOverFunc);
\r
9732 Event.remove(DOM.select('a', co), 'focus', t.mouseOverFunc);
\r
9733 Event.remove(co, 'click', t.mouseClickFunc);
\r
9734 Event.remove(co, 'keydown', t._keyHandler);
\r
9737 t.element.remove();
\r
9742 renderNode : function() {
\r
9743 var t = this, s = t.settings, n, tb, co, w;
\r
9745 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
9746 if (t.settings.parent) {
\r
9747 DOM.setAttrib(w, 'aria-parent', 'menu_' + t.settings.parent.id);
\r
9749 co = DOM.add(w, 'div', {role: 'presentation', id : 'menu_' + t.id + '_co', 'class' : t.classPrefix + (s['class'] ? ' ' + s['class'] : '')});
\r
9750 t.element = new Element('menu_' + t.id, {blocker : 1, container : s.container});
\r
9753 DOM.add(co, 'span', {'class' : t.classPrefix + 'Line'});
\r
9755 // n = DOM.add(co, 'div', {id : 'menu_' + t.id + '_co', 'class' : 'mceMenuContainer'});
\r
9756 n = DOM.add(co, 'table', {role: 'presentation', id : 'menu_' + t.id + '_tbl', border : 0, cellPadding : 0, cellSpacing : 0});
\r
9757 tb = DOM.add(n, 'tbody');
\r
9759 each(t.items, function(o) {
\r
9763 t.rendered = true;
\r
9768 // Internal functions
\r
9769 _setupKeyboardNav : function(){
\r
9770 var contextMenu, menuItems, t=this;
\r
9771 contextMenu = DOM.get('menu_' + t.id);
\r
9772 menuItems = DOM.select('a[role=option]', 'menu_' + t.id);
\r
9773 menuItems.splice(0,0,contextMenu);
\r
9774 t.keyboardNav = new tinymce.ui.KeyboardNavigation({
\r
9775 root: 'menu_' + t.id,
\r
9777 onCancel: function() {
\r
9780 enableUpDown: true
\r
9782 contextMenu.focus();
\r
9785 _keyHandler : function(evt) {
\r
9787 switch (evt.keyCode) {
\r
9789 if (t.settings.parent) {
\r
9791 t.settings.parent.focus();
\r
9792 Event.cancel(evt);
\r
9796 if (t.mouseOverFunc)
\r
9797 t.mouseOverFunc(evt);
\r
9802 _add : function(tb, o) {
\r
9803 var n, s = o.settings, a, ro, it, cp = this.classPrefix, ic;
\r
9805 if (s.separator) {
\r
9806 ro = DOM.add(tb, 'tr', {id : o.id, 'class' : cp + 'ItemSeparator'});
\r
9807 DOM.add(ro, 'td', {'class' : cp + 'ItemSeparator'});
\r
9809 if (n = ro.previousSibling)
\r
9810 DOM.addClass(n, 'mceLast');
\r
9815 n = ro = DOM.add(tb, 'tr', {id : o.id, 'class' : cp + 'Item ' + cp + 'ItemEnabled'});
\r
9816 n = it = DOM.add(n, s.titleItem ? 'th' : 'td');
\r
9817 n = a = DOM.add(n, 'a', {id: o.id + '_aria', role: s.titleItem ? 'presentation' : 'option', href : 'javascript:;', onclick : "return false;", onmousedown : 'return false;'});
\r
9820 DOM.setAttrib(a, 'aria-haspopup', 'true');
\r
9821 DOM.setAttrib(a, 'aria-owns', 'menu_' + o.id);
\r
9824 DOM.addClass(it, s['class']);
\r
9825 // n = DOM.add(n, 'span', {'class' : 'item'});
\r
9827 ic = DOM.add(n, 'span', {'class' : 'mceIcon' + (s.icon ? ' mce_' + s.icon : '')});
\r
9830 DOM.add(ic, 'img', {src : s.icon_src});
\r
9832 n = DOM.add(n, s.element || 'span', {'class' : 'mceText', title : o.settings.title}, o.settings.title);
\r
9834 if (o.settings.style)
\r
9835 DOM.setAttrib(n, 'style', o.settings.style);
\r
9837 if (tb.childNodes.length == 1)
\r
9838 DOM.addClass(ro, 'mceFirst');
\r
9840 if ((n = ro.previousSibling) && DOM.hasClass(n, cp + 'ItemSeparator'))
\r
9841 DOM.addClass(ro, 'mceFirst');
\r
9844 DOM.addClass(ro, cp + 'ItemSub');
\r
9846 if (n = ro.previousSibling)
\r
9847 DOM.removeClass(n, 'mceLast');
\r
9849 DOM.addClass(ro, 'mceLast');
\r
9853 (function(tinymce) {
\r
9854 var DOM = tinymce.DOM;
\r
9856 tinymce.create('tinymce.ui.Button:tinymce.ui.Control', {
\r
9857 Button : function(id, s, ed) {
\r
9858 this.parent(id, s, ed);
\r
9859 this.classPrefix = 'mceButton';
\r
9862 renderHTML : function() {
\r
9863 var cp = this.classPrefix, s = this.settings, h, l;
\r
9865 l = DOM.encode(s.label || '');
\r
9866 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
9867 if (s.image && !(this.editor &&this.editor.forcedHighContrastMode) )
\r
9868 h += '<img class="mceIcon" src="' + s.image + '" alt="' + DOM.encode(s.title) + '" />' + l;
\r
9870 h += '<span class="mceIcon ' + s['class'] + '"></span>' + (l ? '<span class="' + cp + 'Label">' + l + '</span>' : '');
\r
9872 h += '<span class="mceVoiceLabel mceIconOnly" style="display: none;" id="' + this.id + '_voice">' + s.title + '</span>';
\r
9877 postRender : function() {
\r
9878 var t = this, s = t.settings, imgBookmark;
\r
9880 // In IE a large image that occupies the entire editor area will be deselected when a button is clicked, so
\r
9881 // need to keep the selection in case the selection is lost
\r
9882 if (tinymce.isIE && t.editor) {
\r
9883 tinymce.dom.Event.add(t.id, 'mousedown', function(e) {
\r
9884 var nodeName = t.editor.selection.getNode().nodeName;
\r
9885 imgBookmark = nodeName === 'IMG' ? t.editor.selection.getBookmark() : null;
\r
9888 tinymce.dom.Event.add(t.id, 'click', function(e) {
\r
9889 if (!t.isDisabled()) {
\r
9890 // restore the selection in case the selection is lost in IE
\r
9891 if (tinymce.isIE && t.editor && imgBookmark !== null) {
\r
9892 t.editor.selection.moveToBookmark(imgBookmark);
\r
9894 return s.onclick.call(s.scope, e);
\r
9897 tinymce.dom.Event.add(t.id, 'keyup', function(e) {
\r
9898 if (!t.isDisabled() && e.keyCode==tinymce.VK.SPACEBAR)
\r
9899 return s.onclick.call(s.scope, e);
\r
9905 (function(tinymce) {
\r
9906 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher;
\r
9908 tinymce.create('tinymce.ui.ListBox:tinymce.ui.Control', {
\r
9909 ListBox : function(id, s, ed) {
\r
9912 t.parent(id, s, ed);
\r
9916 t.onChange = new Dispatcher(t);
\r
9918 t.onPostRender = new Dispatcher(t);
\r
9920 t.onAdd = new Dispatcher(t);
\r
9922 t.onRenderMenu = new tinymce.util.Dispatcher(this);
\r
9924 t.classPrefix = 'mceListBox';
\r
9927 select : function(va) {
\r
9928 var t = this, fv, f;
\r
9930 if (va == undefined)
\r
9931 return t.selectByIndex(-1);
\r
9933 // Is string or number make function selector
\r
9934 if (va && typeof(va)=="function")
\r
9942 // Do we need to do something?
\r
9943 if (va != t.selectedValue) {
\r
9945 each(t.items, function(o, i) {
\r
9948 t.selectByIndex(i);
\r
9954 t.selectByIndex(-1);
\r
9958 selectByIndex : function(idx) {
\r
9959 var t = this, e, o, label;
\r
9961 if (idx != t.selectedIndex) {
\r
9962 e = DOM.get(t.id + '_text');
\r
9963 label = DOM.get(t.id + '_voiceDesc');
\r
9967 t.selectedValue = o.value;
\r
9968 t.selectedIndex = idx;
\r
9969 DOM.setHTML(e, DOM.encode(o.title));
\r
9970 DOM.setHTML(label, t.settings.title + " - " + o.title);
\r
9971 DOM.removeClass(e, 'mceTitle');
\r
9972 DOM.setAttrib(t.id, 'aria-valuenow', o.title);
\r
9974 DOM.setHTML(e, DOM.encode(t.settings.title));
\r
9975 DOM.setHTML(label, DOM.encode(t.settings.title));
\r
9976 DOM.addClass(e, 'mceTitle');
\r
9977 t.selectedValue = t.selectedIndex = null;
\r
9978 DOM.setAttrib(t.id, 'aria-valuenow', t.settings.title);
\r
9984 add : function(n, v, o) {
\r
9988 o = tinymce.extend(o, {
\r
9994 t.onAdd.dispatch(t, o);
\r
9997 getLength : function() {
\r
9998 return this.items.length;
\r
10001 renderHTML : function() {
\r
10002 var h = '', t = this, s = t.settings, cp = t.classPrefix;
\r
10004 h = '<span role="listbox" aria-haspopup="true" aria-labelledby="' + t.id +'_voiceDesc" 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
10005 h += '<td>' + DOM.createHTML('span', {id: t.id + '_voiceDesc', 'class': 'voiceLabel', style:'display:none;'}, t.settings.title);
\r
10006 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
10007 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
10008 h += '</tr></tbody></table></span>';
\r
10013 showMenu : function() {
\r
10014 var t = this, p2, e = DOM.get(this.id), m;
\r
10016 if (t.isDisabled() || t.items.length == 0)
\r
10019 if (t.menu && t.menu.isMenuVisible)
\r
10020 return t.hideMenu();
\r
10022 if (!t.isMenuRendered) {
\r
10024 t.isMenuRendered = true;
\r
10027 p2 = DOM.getPos(e);
\r
10030 m.settings.offset_x = p2.x;
\r
10031 m.settings.offset_y = p2.y;
\r
10032 m.settings.keyboard_focus = !tinymce.isOpera; // Opera is buggy when it comes to auto focus
\r
10034 // Select in menu
\r
10036 m.items[t.oldID].setSelected(0);
\r
10038 each(t.items, function(o) {
\r
10039 if (o.value === t.selectedValue) {
\r
10040 m.items[o.id].setSelected(1);
\r
10045 m.showMenu(0, e.clientHeight);
\r
10047 Event.add(DOM.doc, 'mousedown', t.hideMenu, t);
\r
10048 DOM.addClass(t.id, t.classPrefix + 'Selected');
\r
10050 //DOM.get(t.id + '_text').focus();
\r
10053 hideMenu : function(e) {
\r
10056 if (t.menu && t.menu.isMenuVisible) {
\r
10057 DOM.removeClass(t.id, t.classPrefix + 'Selected');
\r
10059 // Prevent double toogles by canceling the mouse click event to the button
\r
10060 if (e && e.type == "mousedown" && (e.target.id == t.id + '_text' || e.target.id == t.id + '_open'))
\r
10063 if (!e || !DOM.getParent(e.target, '.mceMenu')) {
\r
10064 DOM.removeClass(t.id, t.classPrefix + 'Selected');
\r
10065 Event.remove(DOM.doc, 'mousedown', t.hideMenu, t);
\r
10066 t.menu.hideMenu();
\r
10071 renderMenu : function() {
\r
10074 m = t.settings.control_manager.createDropMenu(t.id + '_menu', {
\r
10076 'class' : t.classPrefix + 'Menu mceNoIcons',
\r
10081 m.onHideMenu.add(function() {
\r
10087 title : t.settings.title,
\r
10088 'class' : 'mceMenuItemTitle',
\r
10089 onclick : function() {
\r
10090 if (t.settings.onselect('') !== false)
\r
10091 t.select(''); // Must be runned after
\r
10095 each(t.items, function(o) {
\r
10096 // No value then treat it as a title
\r
10097 if (o.value === undefined) {
\r
10101 'class' : 'mceMenuItemTitle',
\r
10102 onclick : function() {
\r
10103 if (t.settings.onselect('') !== false)
\r
10104 t.select(''); // Must be runned after
\r
10108 o.id = DOM.uniqueId();
\r
10109 o.role= "option";
\r
10110 o.onclick = function() {
\r
10111 if (t.settings.onselect(o.value) !== false)
\r
10112 t.select(o.value); // Must be runned after
\r
10119 t.onRenderMenu.dispatch(t, m);
\r
10123 postRender : function() {
\r
10124 var t = this, cp = t.classPrefix;
\r
10126 Event.add(t.id, 'click', t.showMenu, t);
\r
10127 Event.add(t.id, 'keydown', function(evt) {
\r
10128 if (evt.keyCode == 32) { // Space
\r
10130 Event.cancel(evt);
\r
10133 Event.add(t.id, 'focus', function() {
\r
10134 if (!t._focused) {
\r
10135 t.keyDownHandler = Event.add(t.id, 'keydown', function(e) {
\r
10136 if (e.keyCode == 40) {
\r
10141 t.keyPressHandler = Event.add(t.id, 'keypress', function(e) {
\r
10143 if (e.keyCode == 13) {
\r
10144 // Fake select on enter
\r
10145 v = t.selectedValue;
\r
10146 t.selectedValue = null; // Needs to be null to fake change
\r
10148 t.settings.onselect(v);
\r
10155 Event.add(t.id, 'blur', function() {
\r
10156 Event.remove(t.id, 'keydown', t.keyDownHandler);
\r
10157 Event.remove(t.id, 'keypress', t.keyPressHandler);
\r
10161 // Old IE doesn't have hover on all elements
\r
10162 if (tinymce.isIE6 || !DOM.boxModel) {
\r
10163 Event.add(t.id, 'mouseover', function() {
\r
10164 if (!DOM.hasClass(t.id, cp + 'Disabled'))
\r
10165 DOM.addClass(t.id, cp + 'Hover');
\r
10168 Event.add(t.id, 'mouseout', function() {
\r
10169 if (!DOM.hasClass(t.id, cp + 'Disabled'))
\r
10170 DOM.removeClass(t.id, cp + 'Hover');
\r
10174 t.onPostRender.dispatch(t, DOM.get(t.id));
\r
10177 destroy : function() {
\r
10180 Event.clear(this.id + '_text');
\r
10181 Event.clear(this.id + '_open');
\r
10186 (function(tinymce) {
\r
10187 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher;
\r
10189 tinymce.create('tinymce.ui.NativeListBox:tinymce.ui.ListBox', {
\r
10190 NativeListBox : function(id, s) {
\r
10191 this.parent(id, s);
\r
10192 this.classPrefix = 'mceNativeListBox';
\r
10195 setDisabled : function(s) {
\r
10196 DOM.get(this.id).disabled = s;
\r
10197 this.setAriaProperty('disabled', s);
\r
10200 isDisabled : function() {
\r
10201 return DOM.get(this.id).disabled;
\r
10204 select : function(va) {
\r
10205 var t = this, fv, f;
\r
10207 if (va == undefined)
\r
10208 return t.selectByIndex(-1);
\r
10210 // Is string or number make function selector
\r
10211 if (va && typeof(va)=="function")
\r
10214 f = function(v) {
\r
10219 // Do we need to do something?
\r
10220 if (va != t.selectedValue) {
\r
10222 each(t.items, function(o, i) {
\r
10223 if (f(o.value)) {
\r
10225 t.selectByIndex(i);
\r
10231 t.selectByIndex(-1);
\r
10235 selectByIndex : function(idx) {
\r
10236 DOM.get(this.id).selectedIndex = idx + 1;
\r
10237 this.selectedValue = this.items[idx] ? this.items[idx].value : null;
\r
10240 add : function(n, v, a) {
\r
10246 if (t.isRendered())
\r
10247 DOM.add(DOM.get(this.id), 'option', a, n);
\r
10256 t.onAdd.dispatch(t, o);
\r
10259 getLength : function() {
\r
10260 return this.items.length;
\r
10263 renderHTML : function() {
\r
10266 h = DOM.createHTML('option', {value : ''}, '-- ' + t.settings.title + ' --');
\r
10268 each(t.items, function(it) {
\r
10269 h += DOM.createHTML('option', {value : it.value}, it.title);
\r
10272 h = DOM.createHTML('select', {id : t.id, 'class' : 'mceNativeListBox', 'aria-labelledby': t.id + '_aria'}, h);
\r
10273 h += DOM.createHTML('span', {id : t.id + '_aria', 'style': 'display: none'}, t.settings.title);
\r
10277 postRender : function() {
\r
10278 var t = this, ch, changeListenerAdded = true;
\r
10280 t.rendered = true;
\r
10282 function onChange(e) {
\r
10283 var v = t.items[e.target.selectedIndex - 1];
\r
10285 if (v && (v = v.value)) {
\r
10286 t.onChange.dispatch(t, v);
\r
10288 if (t.settings.onselect)
\r
10289 t.settings.onselect(v);
\r
10293 Event.add(t.id, 'change', onChange);
\r
10295 // Accessibility keyhandler
\r
10296 Event.add(t.id, 'keydown', function(e) {
\r
10299 Event.remove(t.id, 'change', ch);
\r
10300 changeListenerAdded = false;
\r
10302 bf = Event.add(t.id, 'blur', function() {
\r
10303 if (changeListenerAdded) return;
\r
10304 changeListenerAdded = true;
\r
10305 Event.add(t.id, 'change', onChange);
\r
10306 Event.remove(t.id, 'blur', bf);
\r
10309 //prevent default left and right keys on chrome - so that the keyboard navigation is used.
\r
10310 if (tinymce.isWebKit && (e.keyCode==37 ||e.keyCode==39)) {
\r
10311 return Event.prevent(e);
\r
10314 if (e.keyCode == 13 || e.keyCode == 32) {
\r
10316 return Event.cancel(e);
\r
10320 t.onPostRender.dispatch(t, DOM.get(t.id));
\r
10325 (function(tinymce) {
\r
10326 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each;
\r
10328 tinymce.create('tinymce.ui.MenuButton:tinymce.ui.Button', {
\r
10329 MenuButton : function(id, s, ed) {
\r
10330 this.parent(id, s, ed);
\r
10332 this.onRenderMenu = new tinymce.util.Dispatcher(this);
\r
10334 s.menu_container = s.menu_container || DOM.doc.body;
\r
10337 showMenu : function() {
\r
10338 var t = this, p1, p2, e = DOM.get(t.id), m;
\r
10340 if (t.isDisabled())
\r
10343 if (!t.isMenuRendered) {
\r
10345 t.isMenuRendered = true;
\r
10348 if (t.isMenuVisible)
\r
10349 return t.hideMenu();
\r
10351 p1 = DOM.getPos(t.settings.menu_container);
\r
10352 p2 = DOM.getPos(e);
\r
10355 m.settings.offset_x = p2.x;
\r
10356 m.settings.offset_y = p2.y;
\r
10357 m.settings.vp_offset_x = p2.x;
\r
10358 m.settings.vp_offset_y = p2.y;
\r
10359 m.settings.keyboard_focus = t._focused;
\r
10360 m.showMenu(0, e.clientHeight);
\r
10362 Event.add(DOM.doc, 'mousedown', t.hideMenu, t);
\r
10363 t.setState('Selected', 1);
\r
10365 t.isMenuVisible = 1;
\r
10368 renderMenu : function() {
\r
10371 m = t.settings.control_manager.createDropMenu(t.id + '_menu', {
\r
10373 'class' : this.classPrefix + 'Menu',
\r
10374 icons : t.settings.icons
\r
10377 m.onHideMenu.add(function() {
\r
10382 t.onRenderMenu.dispatch(t, m);
\r
10386 hideMenu : function(e) {
\r
10389 // Prevent double toogles by canceling the mouse click event to the button
\r
10390 if (e && e.type == "mousedown" && DOM.getParent(e.target, function(e) {return e.id === t.id || e.id === t.id + '_open';}))
\r
10393 if (!e || !DOM.getParent(e.target, '.mceMenu')) {
\r
10394 t.setState('Selected', 0);
\r
10395 Event.remove(DOM.doc, 'mousedown', t.hideMenu, t);
\r
10397 t.menu.hideMenu();
\r
10400 t.isMenuVisible = 0;
\r
10403 postRender : function() {
\r
10404 var t = this, s = t.settings;
\r
10406 Event.add(t.id, 'click', function() {
\r
10407 if (!t.isDisabled()) {
\r
10409 s.onclick(t.value);
\r
10418 (function(tinymce) {
\r
10419 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each;
\r
10421 tinymce.create('tinymce.ui.SplitButton:tinymce.ui.MenuButton', {
\r
10422 SplitButton : function(id, s, ed) {
\r
10423 this.parent(id, s, ed);
\r
10424 this.classPrefix = 'mceSplitButton';
\r
10427 renderHTML : function() {
\r
10428 var h, t = this, s = t.settings, h1;
\r
10430 h = '<tbody><tr>';
\r
10433 h1 = DOM.createHTML('img ', {src : s.image, role: 'presentation', 'class' : 'mceAction ' + s['class']});
\r
10435 h1 = DOM.createHTML('span', {'class' : 'mceAction ' + s['class']}, '');
\r
10437 h1 += DOM.createHTML('span', {'class': 'mceVoiceLabel mceIconOnly', id: t.id + '_voice', style: 'display:none;'}, s.title);
\r
10438 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
10440 h1 = DOM.createHTML('span', {'class' : 'mceOpen ' + s['class']}, '<span style="display:none;" class="mceIconOnly" aria-hidden="true">\u25BC</span>');
\r
10441 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
10443 h += '</tr></tbody>';
\r
10444 h = DOM.createHTML('table', { role: 'presentation', 'class' : 'mceSplitButton mceSplitButtonEnabled ' + s['class'], cellpadding : '0', cellspacing : '0', title : s.title}, h);
\r
10445 return DOM.createHTML('div', {id : t.id, role: 'button', tabindex: '0', 'aria-labelledby': t.id + '_voice', 'aria-haspopup': 'true'}, h);
\r
10448 postRender : function() {
\r
10449 var t = this, s = t.settings, activate;
\r
10452 activate = function(evt) {
\r
10453 if (!t.isDisabled()) {
\r
10454 s.onclick(t.value);
\r
10455 Event.cancel(evt);
\r
10458 Event.add(t.id + '_action', 'click', activate);
\r
10459 Event.add(t.id, ['click', 'keydown'], function(evt) {
\r
10460 var DOM_VK_SPACE = 32, DOM_VK_ENTER = 14, DOM_VK_RETURN = 13, DOM_VK_UP = 38, DOM_VK_DOWN = 40;
\r
10461 if ((evt.keyCode === 32 || evt.keyCode === 13 || evt.keyCode === 14) && !evt.altKey && !evt.ctrlKey && !evt.metaKey) {
\r
10463 Event.cancel(evt);
\r
10464 } else if (evt.type === 'click' || evt.keyCode === DOM_VK_DOWN) {
\r
10466 Event.cancel(evt);
\r
10471 Event.add(t.id + '_open', 'click', function (evt) {
\r
10473 Event.cancel(evt);
\r
10475 Event.add([t.id, t.id + '_open'], 'focus', function() {t._focused = 1;});
\r
10476 Event.add([t.id, t.id + '_open'], 'blur', function() {t._focused = 0;});
\r
10478 // Old IE doesn't have hover on all elements
\r
10479 if (tinymce.isIE6 || !DOM.boxModel) {
\r
10480 Event.add(t.id, 'mouseover', function() {
\r
10481 if (!DOM.hasClass(t.id, 'mceSplitButtonDisabled'))
\r
10482 DOM.addClass(t.id, 'mceSplitButtonHover');
\r
10485 Event.add(t.id, 'mouseout', function() {
\r
10486 if (!DOM.hasClass(t.id, 'mceSplitButtonDisabled'))
\r
10487 DOM.removeClass(t.id, 'mceSplitButtonHover');
\r
10492 destroy : function() {
\r
10495 Event.clear(this.id + '_action');
\r
10496 Event.clear(this.id + '_open');
\r
10497 Event.clear(this.id);
\r
10502 (function(tinymce) {
\r
10503 var DOM = tinymce.DOM, Event = tinymce.dom.Event, is = tinymce.is, each = tinymce.each;
\r
10505 tinymce.create('tinymce.ui.ColorSplitButton:tinymce.ui.SplitButton', {
\r
10506 ColorSplitButton : function(id, s, ed) {
\r
10509 t.parent(id, s, ed);
\r
10511 t.settings = s = tinymce.extend({
\r
10512 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
10514 default_color : '#888888'
\r
10517 t.onShowMenu = new tinymce.util.Dispatcher(t);
\r
10519 t.onHideMenu = new tinymce.util.Dispatcher(t);
\r
10521 t.value = s.default_color;
\r
10524 showMenu : function() {
\r
10525 var t = this, r, p, e, p2;
\r
10527 if (t.isDisabled())
\r
10530 if (!t.isMenuRendered) {
\r
10532 t.isMenuRendered = true;
\r
10535 if (t.isMenuVisible)
\r
10536 return t.hideMenu();
\r
10538 e = DOM.get(t.id);
\r
10539 DOM.show(t.id + '_menu');
\r
10540 DOM.addClass(e, 'mceSplitButtonSelected');
\r
10541 p2 = DOM.getPos(e);
\r
10542 DOM.setStyles(t.id + '_menu', {
\r
10544 top : p2.y + e.clientHeight,
\r
10549 Event.add(DOM.doc, 'mousedown', t.hideMenu, t);
\r
10550 t.onShowMenu.dispatch(t);
\r
10552 if (t._focused) {
\r
10553 t._keyHandler = Event.add(t.id + '_menu', 'keydown', function(e) {
\r
10554 if (e.keyCode == 27)
\r
10558 DOM.select('a', t.id + '_menu')[0].focus(); // Select first link
\r
10561 t.isMenuVisible = 1;
\r
10564 hideMenu : function(e) {
\r
10567 if (t.isMenuVisible) {
\r
10568 // Prevent double toogles by canceling the mouse click event to the button
\r
10569 if (e && e.type == "mousedown" && DOM.getParent(e.target, function(e) {return e.id === t.id + '_open';}))
\r
10572 if (!e || !DOM.getParent(e.target, '.mceSplitButtonMenu')) {
\r
10573 DOM.removeClass(t.id, 'mceSplitButtonSelected');
\r
10574 Event.remove(DOM.doc, 'mousedown', t.hideMenu, t);
\r
10575 Event.remove(t.id + '_menu', 'keydown', t._keyHandler);
\r
10576 DOM.hide(t.id + '_menu');
\r
10579 t.isMenuVisible = 0;
\r
10580 t.onHideMenu.dispatch();
\r
10584 renderMenu : function() {
\r
10585 var t = this, m, i = 0, s = t.settings, n, tb, tr, w, context;
\r
10587 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
10588 m = DOM.add(w, 'div', {'class' : s['class'] + ' mceSplitButtonMenu'});
\r
10589 DOM.add(m, 'span', {'class' : 'mceMenuLine'});
\r
10591 n = DOM.add(m, 'table', {role: 'presentation', 'class' : 'mceColorSplitMenu'});
\r
10592 tb = DOM.add(n, 'tbody');
\r
10594 // Generate color grid
\r
10596 each(is(s.colors, 'array') ? s.colors : s.colors.split(','), function(c) {
\r
10597 c = c.replace(/^#/, '');
\r
10600 tr = DOM.add(tb, 'tr');
\r
10601 i = s.grid_width - 1;
\r
10604 n = DOM.add(tr, 'td');
\r
10606 href : 'javascript:;',
\r
10608 backgroundColor : '#' + c
\r
10610 'title': t.editor.getLang('colors.' + c, c),
\r
10611 'data-mce-color' : '#' + c
\r
10614 // adding a proper ARIA role = button causes JAWS to read things incorrectly on IE.
\r
10615 if (!tinymce.isIE ) {
\r
10616 settings['role']= 'option';
\r
10619 n = DOM.add(n, 'a', settings);
\r
10621 if (t.editor.forcedHighContrastMode) {
\r
10622 n = DOM.add(n, 'canvas', { width: 16, height: 16, 'aria-hidden': 'true' });
\r
10623 if (n.getContext && (context = n.getContext("2d"))) {
\r
10624 context.fillStyle = '#' + c;
\r
10625 context.fillRect(0, 0, 16, 16);
\r
10627 // No point leaving a canvas element around if it's not supported for drawing on anyway.
\r
10633 if (s.more_colors_func) {
\r
10634 n = DOM.add(tb, 'tr');
\r
10635 n = DOM.add(n, 'td', {colspan : s.grid_width, 'class' : 'mceMoreColors'});
\r
10636 n = DOM.add(n, 'a', {role: 'option', id : t.id + '_more', href : 'javascript:;', onclick : 'return false;', 'class' : 'mceMoreColors'}, s.more_colors_title);
\r
10638 Event.add(n, 'click', function(e) {
\r
10639 s.more_colors_func.call(s.more_colors_scope || this);
\r
10640 return Event.cancel(e); // Cancel to fix onbeforeunload problem
\r
10644 DOM.addClass(m, 'mceColorSplitMenu');
\r
10646 new tinymce.ui.KeyboardNavigation({
\r
10647 root: t.id + '_menu',
\r
10648 items: DOM.select('a', t.id + '_menu'),
\r
10649 onCancel: function() {
\r
10655 // Prevent IE from scrolling and hindering click to occur #4019
\r
10656 Event.add(t.id + '_menu', 'mousedown', function(e) {return Event.cancel(e);});
\r
10658 Event.add(t.id + '_menu', 'click', function(e) {
\r
10661 e = DOM.getParent(e.target, 'a', tb);
\r
10663 if (e && e.nodeName.toLowerCase() == 'a' && (c = e.getAttribute('data-mce-color')))
\r
10666 return Event.cancel(e); // Prevent IE auto save warning
\r
10672 setColor : function(c) {
\r
10673 this.displayColor(c);
\r
10675 this.settings.onselect(c);
\r
10678 displayColor : function(c) {
\r
10681 DOM.setStyle(t.id + '_preview', 'backgroundColor', c);
\r
10686 postRender : function() {
\r
10687 var t = this, id = t.id;
\r
10690 DOM.add(id + '_action', 'div', {id : id + '_preview', 'class' : 'mceColorPreview'});
\r
10691 DOM.setStyle(t.id + '_preview', 'backgroundColor', t.value);
\r
10694 destroy : function() {
\r
10697 Event.clear(this.id + '_menu');
\r
10698 Event.clear(this.id + '_more');
\r
10699 DOM.remove(this.id + '_menu');
\r
10704 (function(tinymce) {
\r
10705 // Shorten class names
\r
10706 var dom = tinymce.DOM, each = tinymce.each, Event = tinymce.dom.Event;
\r
10707 tinymce.create('tinymce.ui.ToolbarGroup:tinymce.ui.Container', {
\r
10708 renderHTML : function() {
\r
10709 var t = this, h = [], controls = t.controls, each = tinymce.each, settings = t.settings;
\r
10711 h.push('<div id="' + t.id + '" role="group" aria-labelledby="' + t.id + '_voice">');
\r
10712 //TODO: ACC test this out - adding a role = application for getting the landmarks working well.
\r
10713 h.push("<span role='application'>");
\r
10714 h.push('<span id="' + t.id + '_voice" class="mceVoiceLabel" style="display:none;">' + dom.encode(settings.name) + '</span>');
\r
10715 each(controls, function(toolbar) {
\r
10716 h.push(toolbar.renderHTML());
\r
10718 h.push("</span>");
\r
10719 h.push('</div>');
\r
10721 return h.join('');
\r
10724 focus : function() {
\r
10726 dom.get(t.id).focus();
\r
10729 postRender : function() {
\r
10730 var t = this, items = [];
\r
10732 each(t.controls, function(toolbar) {
\r
10733 each (toolbar.controls, function(control) {
\r
10734 if (control.id) {
\r
10735 items.push(control);
\r
10740 t.keyNav = new tinymce.ui.KeyboardNavigation({
\r
10743 onCancel: function() {
\r
10744 //Move focus if webkit so that navigation back will read the item.
\r
10745 if (tinymce.isWebKit) {
\r
10746 dom.get(t.editor.id+"_ifr").focus();
\r
10748 t.editor.focus();
\r
10750 excludeFromTabOrder: !t.settings.tab_focus_toolbar
\r
10754 destroy : function() {
\r
10758 self.keyNav.destroy();
\r
10759 Event.clear(self.id);
\r
10764 (function(tinymce) {
\r
10765 // Shorten class names
\r
10766 var dom = tinymce.DOM, each = tinymce.each;
\r
10767 tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', {
\r
10768 renderHTML : function() {
\r
10769 var t = this, h = '', c, co, s = t.settings, i, pr, nx, cl;
\r
10772 for (i=0; i<cl.length; i++) {
\r
10773 // Get current control, prev control, next control and if the control is a list box or not
\r
10778 // Add toolbar start
\r
10780 c = 'mceToolbarStart';
\r
10783 c += ' mceToolbarStartButton';
\r
10784 else if (co.SplitButton)
\r
10785 c += ' mceToolbarStartSplitButton';
\r
10786 else if (co.ListBox)
\r
10787 c += ' mceToolbarStartListBox';
\r
10789 h += dom.createHTML('td', {'class' : c}, dom.createHTML('span', null, '<!-- IE -->'));
\r
10792 // Add toolbar end before list box and after the previous button
\r
10793 // This is to fix the o2k7 editor skins
\r
10794 if (pr && co.ListBox) {
\r
10795 if (pr.Button || pr.SplitButton)
\r
10796 h += dom.createHTML('td', {'class' : 'mceToolbarEnd'}, dom.createHTML('span', null, '<!-- IE -->'));
\r
10799 // Render control HTML
\r
10801 // IE 8 quick fix, needed to propertly generate a hit area for anchors
\r
10803 h += '<td style="position: relative">' + co.renderHTML() + '</td>';
\r
10805 h += '<td>' + co.renderHTML() + '</td>';
\r
10807 // Add toolbar start after list box and before the next button
\r
10808 // This is to fix the o2k7 editor skins
\r
10809 if (nx && co.ListBox) {
\r
10810 if (nx.Button || nx.SplitButton)
\r
10811 h += dom.createHTML('td', {'class' : 'mceToolbarStart'}, dom.createHTML('span', null, '<!-- IE -->'));
\r
10815 c = 'mceToolbarEnd';
\r
10818 c += ' mceToolbarEndButton';
\r
10819 else if (co.SplitButton)
\r
10820 c += ' mceToolbarEndSplitButton';
\r
10821 else if (co.ListBox)
\r
10822 c += ' mceToolbarEndListBox';
\r
10824 h += dom.createHTML('td', {'class' : c}, dom.createHTML('span', null, '<!-- IE -->'));
\r
10826 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
10831 (function(tinymce) {
\r
10832 var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each;
\r
10834 tinymce.create('tinymce.AddOnManager', {
\r
10835 AddOnManager : function() {
\r
10840 self.lookup = {};
\r
10841 self.onAdd = new Dispatcher(self);
\r
10844 get : function(n) {
\r
10845 if (this.lookup[n]) {
\r
10846 return this.lookup[n].instance;
\r
10848 return undefined;
\r
10852 dependencies : function(n) {
\r
10854 if (this.lookup[n]) {
\r
10855 result = this.lookup[n].dependencies;
\r
10857 return result || [];
\r
10860 requireLangPack : function(n) {
\r
10861 var s = tinymce.settings;
\r
10863 if (s && s.language && s.language_load !== false)
\r
10864 tinymce.ScriptLoader.add(this.urls[n] + '/langs/' + s.language + '.js');
\r
10867 add : function(id, o, dependencies) {
\r
10868 this.items.push(o);
\r
10869 this.lookup[id] = {instance:o, dependencies:dependencies};
\r
10870 this.onAdd.dispatch(this, id, o);
\r
10874 createUrl: function(baseUrl, dep) {
\r
10875 if (typeof dep === "object") {
\r
10878 return {prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix};
\r
10882 addComponents: function(pluginName, scripts) {
\r
10883 var pluginUrl = this.urls[pluginName];
\r
10884 tinymce.each(scripts, function(script){
\r
10885 tinymce.ScriptLoader.add(pluginUrl+"/"+script);
\r
10889 load : function(n, u, cb, s) {
\r
10890 var t = this, url = u;
\r
10892 function loadDependencies() {
\r
10893 var dependencies = t.dependencies(n);
\r
10894 tinymce.each(dependencies, function(dep) {
\r
10895 var newUrl = t.createUrl(u, dep);
\r
10896 t.load(newUrl.resource, newUrl, undefined, undefined);
\r
10902 cb.call(tinymce.ScriptLoader);
\r
10909 if (typeof u === "object")
\r
10910 url = u.prefix + u.resource + u.suffix;
\r
10912 if (url.indexOf('/') != 0 && url.indexOf('://') == -1)
\r
10913 url = tinymce.baseURL + '/' + url;
\r
10915 t.urls[n] = url.substring(0, url.lastIndexOf('/'));
\r
10917 if (t.lookup[n]) {
\r
10918 loadDependencies();
\r
10920 tinymce.ScriptLoader.add(url, loadDependencies, s);
\r
10925 // Create plugin and theme managers
\r
10926 tinymce.PluginManager = new tinymce.AddOnManager();
\r
10927 tinymce.ThemeManager = new tinymce.AddOnManager();
\r
10930 (function(tinymce) {
\r
10932 var each = tinymce.each, extend = tinymce.extend,
\r
10933 DOM = tinymce.DOM, Event = tinymce.dom.Event,
\r
10934 ThemeManager = tinymce.ThemeManager, PluginManager = tinymce.PluginManager,
\r
10935 explode = tinymce.explode,
\r
10936 Dispatcher = tinymce.util.Dispatcher, undefined, instanceCounter = 0;
\r
10938 // Setup some URLs where the editor API is located and where the document is
\r
10939 tinymce.documentBaseURL = window.location.href.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, '');
\r
10940 if (!/[\/\\]$/.test(tinymce.documentBaseURL))
\r
10941 tinymce.documentBaseURL += '/';
\r
10943 tinymce.baseURL = new tinymce.util.URI(tinymce.documentBaseURL).toAbsolute(tinymce.baseURL);
\r
10945 tinymce.baseURI = new tinymce.util.URI(tinymce.baseURL);
\r
10947 // Add before unload listener
\r
10948 // This was required since IE was leaking memory if you added and removed beforeunload listeners
\r
10949 // with attachEvent/detatchEvent so this only adds one listener and instances can the attach to the onBeforeUnload event
\r
10950 tinymce.onBeforeUnload = new Dispatcher(tinymce);
\r
10952 // Must be on window or IE will leak if the editor is placed in frame or iframe
\r
10953 Event.add(window, 'beforeunload', function(e) {
\r
10954 tinymce.onBeforeUnload.dispatch(tinymce, e);
\r
10957 tinymce.onAddEditor = new Dispatcher(tinymce);
\r
10959 tinymce.onRemoveEditor = new Dispatcher(tinymce);
\r
10961 tinymce.EditorManager = extend(tinymce, {
\r
10966 activeEditor : null,
\r
10968 init : function(s) {
\r
10969 var t = this, pl, sl = tinymce.ScriptLoader, e, el = [], ed;
\r
10971 function execCallback(se, n, s) {
\r
10977 if (tinymce.is(f, 'string')) {
\r
10978 s = f.replace(/\.\w+$/, '');
\r
10979 s = s ? tinymce.resolve(s) : 0;
\r
10980 f = tinymce.resolve(f);
\r
10983 return f.apply(s || this, Array.prototype.slice.call(arguments, 2));
\r
10987 theme : "simple",
\r
10994 Event.add(document, 'init', function() {
\r
10997 execCallback(s, 'onpageload');
\r
10999 switch (s.mode) {
\r
11001 l = s.elements || '';
\r
11003 if(l.length > 0) {
\r
11004 each(explode(l), function(v) {
\r
11005 if (DOM.get(v)) {
\r
11006 ed = new tinymce.Editor(v, s);
\r
11010 each(document.forms, function(f) {
\r
11011 each(f.elements, function(e) {
\r
11012 if (e.name === v) {
\r
11013 v = 'mce_editor_' + instanceCounter++;
\r
11014 DOM.setAttrib(e, 'id', v);
\r
11016 ed = new tinymce.Editor(v, s);
\r
11027 case "textareas":
\r
11028 case "specific_textareas":
\r
11029 function hasClass(n, c) {
\r
11030 return c.constructor === RegExp ? c.test(n.className) : DOM.hasClass(n, c);
\r
11033 each(DOM.select('textarea'), function(v) {
\r
11034 if (s.editor_deselector && hasClass(v, s.editor_deselector))
\r
11037 if (!s.editor_selector || hasClass(v, s.editor_selector)) {
\r
11038 // Can we use the name
\r
11039 e = DOM.get(v.name);
\r
11043 // Generate unique name if missing or already exists
\r
11044 if (!v.id || t.get(v.id))
\r
11045 v.id = DOM.uniqueId();
\r
11047 ed = new tinymce.Editor(v.id, s);
\r
11055 // Call onInit when all editors are initialized
\r
11059 each(el, function(ed) {
\r
11062 if (!ed.initialized) {
\r
11064 ed.onInit.add(function() {
\r
11069 execCallback(s, 'oninit');
\r
11076 execCallback(s, 'oninit');
\r
11082 get : function(id) {
\r
11083 if (id === undefined)
\r
11084 return this.editors;
\r
11086 return this.editors[id];
\r
11089 getInstanceById : function(id) {
\r
11090 return this.get(id);
\r
11093 add : function(editor) {
\r
11094 var self = this, editors = self.editors;
\r
11096 // Add named and index editor instance
\r
11097 editors[editor.id] = editor;
\r
11098 editors.push(editor);
\r
11100 self._setActive(editor);
\r
11101 self.onAddEditor.dispatch(self, editor);
\r
11107 remove : function(editor) {
\r
11108 var t = this, i, editors = t.editors;
\r
11110 // Not in the collection
\r
11111 if (!editors[editor.id])
\r
11114 delete editors[editor.id];
\r
11116 for (i = 0; i < editors.length; i++) {
\r
11117 if (editors[i] == editor) {
\r
11118 editors.splice(i, 1);
\r
11123 // Select another editor since the active one was removed
\r
11124 if (t.activeEditor == editor)
\r
11125 t._setActive(editors[0]);
\r
11127 editor.destroy();
\r
11128 t.onRemoveEditor.dispatch(t, editor);
\r
11133 execCommand : function(c, u, v) {
\r
11134 var t = this, ed = t.get(v), w;
\r
11136 // Manager commands
\r
11142 case "mceAddEditor":
\r
11143 case "mceAddControl":
\r
11145 new tinymce.Editor(v, t.settings).render();
\r
11149 case "mceAddFrameControl":
\r
11152 // Add tinyMCE global instance and tinymce namespace to specified window
\r
11153 w.tinyMCE = tinyMCE;
\r
11154 w.tinymce = tinymce;
\r
11156 tinymce.DOM.doc = w.document;
\r
11157 tinymce.DOM.win = w;
\r
11159 ed = new tinymce.Editor(v.element_id, v);
\r
11162 // Fix IE memory leaks
\r
11163 if (tinymce.isIE) {
\r
11166 w.detachEvent('onunload', clr);
\r
11167 w = w.tinyMCE = w.tinymce = null; // IE leak
\r
11170 w.attachEvent('onunload', clr);
\r
11173 v.page_window = null;
\r
11177 case "mceRemoveEditor":
\r
11178 case "mceRemoveControl":
\r
11184 case 'mceToggleEditor':
\r
11186 t.execCommand('mceAddControl', 0, v);
\r
11190 if (ed.isHidden())
\r
11198 // Run command on active editor
\r
11199 if (t.activeEditor)
\r
11200 return t.activeEditor.execCommand(c, u, v);
\r
11205 execInstanceCommand : function(id, c, u, v) {
\r
11206 var ed = this.get(id);
\r
11209 return ed.execCommand(c, u, v);
\r
11214 triggerSave : function() {
\r
11215 each(this.editors, function(e) {
\r
11220 addI18n : function(p, o) {
\r
11221 var lo, i18n = this.i18n;
\r
11223 if (!tinymce.is(p, 'string')) {
\r
11224 each(p, function(o, lc) {
\r
11225 each(o, function(o, g) {
\r
11226 each(o, function(o, k) {
\r
11227 if (g === 'common')
\r
11228 i18n[lc + '.' + k] = o;
\r
11230 i18n[lc + '.' + g + '.' + k] = o;
\r
11235 each(o, function(o, k) {
\r
11236 i18n[p + '.' + k] = o;
\r
11241 // Private methods
\r
11243 _setActive : function(editor) {
\r
11244 this.selectedInstance = this.activeEditor = editor;
\r
11249 (function(tinymce) {
\r
11250 // Shorten these names
\r
11251 var DOM = tinymce.DOM, Event = tinymce.dom.Event, extend = tinymce.extend,
\r
11252 Dispatcher = tinymce.util.Dispatcher, each = tinymce.each, isGecko = tinymce.isGecko,
\r
11253 isIE = tinymce.isIE, isWebKit = tinymce.isWebKit, is = tinymce.is,
\r
11254 ThemeManager = tinymce.ThemeManager, PluginManager = tinymce.PluginManager,
\r
11255 inArray = tinymce.inArray, grep = tinymce.grep, explode = tinymce.explode, VK = tinymce.VK;
\r
11257 tinymce.create('tinymce.Editor', {
\r
11258 Editor : function(id, s) {
\r
11261 t.id = t.editorId = id;
\r
11263 t.execCommands = {};
\r
11264 t.queryStateCommands = {};
\r
11265 t.queryValueCommands = {};
\r
11267 t.isNotDirty = false;
\r
11271 // Add events to the editor
\r
11275 'onBeforeRenderUI',
\r
11317 'onBeforeSetContent',
\r
11319 'onBeforeGetContent',
\r
11333 'onBeforeExecCommand',
\r
11343 'onSetProgressState',
\r
11347 t[e] = new Dispatcher(t);
\r
11350 t.settings = s = extend({
\r
11353 docs_language : 'en',
\r
11354 theme : 'simple',
\r
11355 skin : 'default',
\r
11357 delta_height : 0,
\r
11360 document_base_url : tinymce.documentBaseURL,
\r
11361 add_form_submit_trigger : 1,
\r
11362 submit_patch : 1,
\r
11363 add_unload_trigger : 1,
\r
11364 convert_urls : 1,
\r
11365 relative_urls : 1,
\r
11366 remove_script_host : 1,
\r
11367 table_inline_editing : 0,
\r
11368 object_resizing : 1,
\r
11370 accessibility_focus : 1,
\r
11371 custom_shortcuts : 1,
\r
11372 custom_undo_redo_keyboard_shortcuts : 1,
\r
11373 custom_undo_redo_restore_selection : 1,
\r
11374 custom_undo_redo : 1,
\r
11375 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
11376 visual_table_class : 'mceItemTable',
\r
11378 font_size_style_values : 'xx-small,x-small,small,medium,large,x-large,xx-large',
\r
11379 font_size_legacy_values : 'xx-small,small,medium,large,x-large,xx-large,300%', // See: http://www.w3.org/TR/CSS2/fonts.html#propdef-font-size
\r
11380 apply_source_formatting : 1,
\r
11381 directionality : 'ltr',
\r
11382 forced_root_block : 'p',
\r
11383 hidden_input : 1,
\r
11384 padd_empty_editor : 1,
\r
11387 force_p_newlines : 1,
\r
11388 indentation : '30px',
\r
11390 fix_table_elements : 1,
\r
11391 inline_styles : 1,
\r
11392 convert_fonts_to_spans : true,
\r
11393 indent : 'simple',
\r
11394 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
11395 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
11397 entity_encoding : 'named',
\r
11398 url_converter : t.convertURL,
\r
11399 url_converter_scope : t,
\r
11400 ie7_compat : true
\r
11403 t.documentBaseURI = new tinymce.util.URI(s.document_base_url || tinymce.documentBaseURL, {
\r
11404 base_uri : tinyMCE.baseURI
\r
11407 t.baseURI = tinymce.baseURI;
\r
11409 t.contentCSS = [];
\r
11412 t.execCallback('setup', t);
\r
11415 render : function(nst) {
\r
11416 var t = this, s = t.settings, id = t.id, sl = tinymce.ScriptLoader;
\r
11418 // Page is not loaded yet, wait for it
\r
11419 if (!Event.domLoaded) {
\r
11420 Event.add(document, 'init', function() {
\r
11426 tinyMCE.settings = s;
\r
11428 // Element not found, then skip initialization
\r
11429 if (!t.getElement())
\r
11432 // Is a iPad/iPhone and not on iOS5, then skip initialization. We need to sniff
\r
11433 // here since the browser says it has contentEditable support but there is no visible
\r
11434 // caret We will remove this check ones Apple implements full contentEditable support
\r
11435 if (tinymce.isIDevice && !tinymce.isIOS5)
\r
11438 // Add hidden input for non input elements inside form elements
\r
11439 if (!/TEXTAREA|INPUT/i.test(t.getElement().nodeName) && s.hidden_input && DOM.getParent(id, 'form'))
\r
11440 DOM.insertAfter(DOM.create('input', {type : 'hidden', name : id}), id);
\r
11442 if (tinymce.WindowManager)
\r
11443 t.windowManager = new tinymce.WindowManager(t);
\r
11445 if (s.encoding == 'xml') {
\r
11446 t.onGetContent.add(function(ed, o) {
\r
11448 o.content = DOM.encode(o.content);
\r
11452 if (s.add_form_submit_trigger) {
\r
11453 t.onSubmit.addToTop(function() {
\r
11454 if (t.initialized) {
\r
11456 t.isNotDirty = 1;
\r
11461 if (s.add_unload_trigger) {
\r
11462 t._beforeUnload = tinyMCE.onBeforeUnload.add(function() {
\r
11463 if (t.initialized && !t.destroyed && !t.isHidden())
\r
11464 t.save({format : 'raw', no_events : true});
\r
11468 tinymce.addUnload(t.destroy, t);
\r
11470 if (s.submit_patch) {
\r
11471 t.onBeforeRenderUI.add(function() {
\r
11472 var n = t.getElement().form;
\r
11477 // Already patched
\r
11478 if (n._mceOldSubmit)
\r
11481 // Check page uses id="submit" or name="submit" for it's submit button
\r
11482 if (!n.submit.nodeType && !n.submit.length) {
\r
11483 t.formElement = n;
\r
11484 n._mceOldSubmit = n.submit;
\r
11485 n.submit = function() {
\r
11486 // Save all instances
\r
11487 tinymce.triggerSave();
\r
11488 t.isNotDirty = 1;
\r
11490 return t.formElement._mceOldSubmit(t.formElement);
\r
11499 function loadScripts() {
\r
11500 if (s.language && s.language_load !== false)
\r
11501 sl.add(tinymce.baseURL + '/langs/' + s.language + '.js');
\r
11503 if (s.theme && s.theme.charAt(0) != '-' && !ThemeManager.urls[s.theme])
\r
11504 ThemeManager.load(s.theme, 'themes/' + s.theme + '/editor_template' + tinymce.suffix + '.js');
\r
11506 each(explode(s.plugins), function(p) {
\r
11507 if (p &&!PluginManager.urls[p]) {
\r
11508 if (p.charAt(0) == '-') {
\r
11509 p = p.substr(1, p.length);
\r
11510 var dependencies = PluginManager.dependencies(p);
\r
11511 each(dependencies, function(dep) {
\r
11512 var defaultSettings = {prefix:'plugins/', resource: dep, suffix:'/editor_plugin' + tinymce.suffix + '.js'};
\r
11513 var dep = PluginManager.createUrl(defaultSettings, dep);
\r
11514 PluginManager.load(dep.resource, dep);
\r
11518 // Skip safari plugin, since it is removed as of 3.3b1
\r
11519 if (p == 'safari') {
\r
11522 PluginManager.load(p, {prefix:'plugins/', resource: p, suffix:'/editor_plugin' + tinymce.suffix + '.js'});
\r
11527 // Init when que is loaded
\r
11528 sl.loadQueue(function() {
\r
11537 init : function() {
\r
11538 var n, t = this, s = t.settings, w, h, e = t.getElement(), o, ti, u, bi, bc, re, i, initializedPlugins = [];
\r
11542 s.aria_label = s.aria_label || DOM.getAttrib(e, 'aria-label', t.getLang('aria.rich_text_area'));
\r
11545 s.theme = s.theme.replace(/-/, '');
\r
11546 o = ThemeManager.get(s.theme);
\r
11547 t.theme = new o();
\r
11549 if (t.theme.init && s.init_theme)
\r
11550 t.theme.init(t, ThemeManager.urls[s.theme] || tinymce.documentBaseURL.replace(/\/$/, ''));
\r
11552 function initPlugin(p) {
\r
11553 var c = PluginManager.get(p), u = PluginManager.urls[p] || tinymce.documentBaseURL.replace(/\/$/, ''), po;
\r
11554 if (c && tinymce.inArray(initializedPlugins,p) === -1) {
\r
11555 each(PluginManager.dependencies(p), function(dep){
\r
11558 po = new c(t, u);
\r
11560 t.plugins[p] = po;
\r
11564 initializedPlugins.push(p);
\r
11569 // Create all plugins
\r
11570 each(explode(s.plugins.replace(/\-/g, '')), initPlugin);
\r
11572 // Setup popup CSS path(s)
\r
11573 if (s.popup_css !== false) {
\r
11575 s.popup_css = t.documentBaseURI.toAbsolute(s.popup_css);
\r
11577 s.popup_css = t.baseURI.toAbsolute("themes/" + s.theme + "/skins/" + s.skin + "/dialog.css");
\r
11580 if (s.popup_css_add)
\r
11581 s.popup_css += ',' + t.documentBaseURI.toAbsolute(s.popup_css_add);
\r
11583 t.controlManager = new tinymce.ControlManager(t);
\r
11585 if (s.custom_undo_redo) {
\r
11586 t.onBeforeExecCommand.add(function(ed, cmd, ui, val, a) {
\r
11587 if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint' && (!a || !a.skip_undo))
\r
11588 t.undoManager.beforeChange();
\r
11591 t.onExecCommand.add(function(ed, cmd, ui, val, a) {
\r
11592 if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint' && (!a || !a.skip_undo))
\r
11593 t.undoManager.add();
\r
11597 t.onExecCommand.add(function(ed, c) {
\r
11598 // Don't refresh the select lists until caret move
\r
11599 if (!/^(FontName|FontSize)$/.test(c))
\r
11603 // Remove ghost selections on images and tables in Gecko
\r
11605 function repaint(a, o) {
\r
11606 if (!o || !o.initial)
\r
11607 t.execCommand('mceRepaint');
\r
11610 t.onUndo.add(repaint);
\r
11611 t.onRedo.add(repaint);
\r
11612 t.onSetContent.add(repaint);
\r
11615 // Enables users to override the control factory
\r
11616 t.onBeforeRenderUI.dispatch(t, t.controlManager);
\r
11619 if (s.render_ui) {
\r
11620 w = s.width || e.style.width || e.offsetWidth;
\r
11621 h = s.height || e.style.height || e.offsetHeight;
\r
11622 t.orgDisplay = e.style.display;
\r
11623 re = /^[0-9\.]+(|px)$/i;
\r
11625 if (re.test('' + w))
\r
11626 w = Math.max(parseInt(w) + (o.deltaWidth || 0), 100);
\r
11628 if (re.test('' + h))
\r
11629 h = Math.max(parseInt(h) + (o.deltaHeight || 0), 100);
\r
11632 o = t.theme.renderUI({
\r
11636 deltaWidth : s.delta_width,
\r
11637 deltaHeight : s.delta_height
\r
11640 t.editorContainer = o.editorContainer;
\r
11644 // User specified a document.domain value
\r
11645 if (document.domain && location.hostname != document.domain)
\r
11646 tinymce.relaxedDomain = document.domain;
\r
11649 DOM.setStyles(o.sizeContainer || o.editorContainer, {
\r
11654 // Load specified content CSS last
\r
11655 if (s.content_css) {
\r
11656 tinymce.each(explode(s.content_css), function(u) {
\r
11657 t.contentCSS.push(t.documentBaseURI.toAbsolute(u));
\r
11661 h = (o.iframeHeight || h) + (typeof(h) == 'number' ? (o.deltaHeight || 0) : '');
\r
11665 t.iframeHTML = s.doctype + '<html><head xmlns="http://www.w3.org/1999/xhtml">';
\r
11667 // We only need to override paths if we have to
\r
11668 // IE has a bug where it remove site absolute urls to relative ones if this is specified
\r
11669 if (s.document_base_url != tinymce.documentBaseURL)
\r
11670 t.iframeHTML += '<base href="' + t.documentBaseURI.getURI() + '" />';
\r
11672 // IE8 doesn't support carets behind images setting ie7_compat would force IE8+ to run in IE7 compat mode.
\r
11673 if (s.ie7_compat)
\r
11674 t.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=7" />';
\r
11676 t.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=edge" />';
\r
11678 t.iframeHTML += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />';
\r
11680 // Load the CSS by injecting them into the HTML this will reduce "flicker"
\r
11681 for (i = 0; i < t.contentCSS.length; i++) {
\r
11682 t.iframeHTML += '<link type="text/css" rel="stylesheet" href="' + t.contentCSS[i] + '" />';
\r
11685 t.contentCSS = [];
\r
11687 bi = s.body_id || 'tinymce';
\r
11688 if (bi.indexOf('=') != -1) {
\r
11689 bi = t.getParam('body_id', '', 'hash');
\r
11690 bi = bi[t.id] || bi;
\r
11693 bc = s.body_class || '';
\r
11694 if (bc.indexOf('=') != -1) {
\r
11695 bc = t.getParam('body_class', '', 'hash');
\r
11696 bc = bc[t.id] || '';
\r
11699 t.iframeHTML += '</head><body id="' + bi + '" class="mceContentBody ' + bc + '" onload="window.parent.tinyMCE.get(\'' + t.id + '\').onLoad.dispatch();"><br></body></html>';
\r
11701 // Domain relaxing enabled, then set document domain
\r
11702 if (tinymce.relaxedDomain && (isIE || (tinymce.isOpera && parseFloat(opera.version()) < 11))) {
\r
11703 // We need to write the contents here in IE since multiple writes messes up refresh button and back button
\r
11704 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
11708 // TODO: ACC add the appropriate description on this.
\r
11709 n = DOM.add(o.iframeContainer, 'iframe', {
\r
11710 id : t.id + "_ifr",
\r
11711 src : u || 'javascript:""', // Workaround for HTTPS warning in IE6/7
\r
11712 frameBorder : '0',
\r
11713 allowTransparency : "true",
\r
11714 title : s.aria_label,
\r
11718 display : 'block' // Important for Gecko to render the iframe correctly
\r
11722 t.contentAreaContainer = o.iframeContainer;
\r
11723 DOM.get(o.editorContainer).style.display = t.orgDisplay;
\r
11724 DOM.get(t.id).style.display = 'none';
\r
11725 DOM.setAttrib(t.id, 'aria-hidden', true);
\r
11727 if (!tinymce.relaxedDomain || !u)
\r
11730 e = n = o = null; // Cleanup
\r
11733 setupIframe : function() {
\r
11734 var t = this, s = t.settings, e = DOM.get(t.id), d = t.getDoc(), h, b;
\r
11736 // Setup iframe body
\r
11737 if (!isIE || !tinymce.relaxedDomain) {
\r
11739 d.write(t.iframeHTML);
\r
11742 if (tinymce.relaxedDomain)
\r
11743 d.domain = tinymce.relaxedDomain;
\r
11746 // It will not steal focus while setting contentEditable
\r
11748 b.disabled = true;
\r
11751 b.contentEditable = true;
\r
11753 b.disabled = false;
\r
11755 t.schema = new tinymce.html.Schema(s);
\r
11757 t.dom = new tinymce.dom.DOMUtils(t.getDoc(), {
\r
11758 keep_values : true,
\r
11759 url_converter : t.convertURL,
\r
11760 url_converter_scope : t,
\r
11761 hex_colors : s.force_hex_style_colors,
\r
11762 class_filter : s.class_filter,
\r
11763 update_styles : 1,
\r
11764 fix_ie_paragraphs : 1,
\r
11765 schema : t.schema
\r
11768 t.parser = new tinymce.html.DomParser(s, t.schema);
\r
11770 // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included.
\r
11771 if (!t.settings.allow_html_in_named_anchor) {
\r
11772 t.parser.addAttributeFilter('name', function(nodes, name) {
\r
11773 var i = nodes.length, sibling, prevSibling, parent, node;
\r
11777 if (node.name === 'a' && node.firstChild) {
\r
11778 parent = node.parent;
\r
11780 // Move children after current node
\r
11781 sibling = node.lastChild;
\r
11783 prevSibling = sibling.prev;
\r
11784 parent.insert(sibling, node);
\r
11785 sibling = prevSibling;
\r
11786 } while (sibling);
\r
11792 // Convert src and href into data-mce-src, data-mce-href and data-mce-style
\r
11793 t.parser.addAttributeFilter('src,href,style', function(nodes, name) {
\r
11794 var i = nodes.length, node, dom = t.dom, value, internalName;
\r
11798 value = node.attr(name);
\r
11799 internalName = 'data-mce-' + name;
\r
11801 // Add internal attribute if we need to we don't on a refresh of the document
\r
11802 if (!node.attributes.map[internalName]) {
\r
11803 if (name === "style")
\r
11804 node.attr(internalName, dom.serializeStyle(dom.parseStyle(value), node.name));
\r
11806 node.attr(internalName, t.convertURL(value, name, node.name));
\r
11811 // Keep scripts from executing
\r
11812 t.parser.addNodeFilter('script', function(nodes, name) {
\r
11813 var i = nodes.length, node;
\r
11817 node.attr('type', 'mce-' + (node.attr('type') || 'text/javascript'));
\r
11821 t.parser.addNodeFilter('#cdata', function(nodes, name) {
\r
11822 var i = nodes.length, node;
\r
11827 node.name = '#comment';
\r
11828 node.value = '[CDATA[' + node.value + ']]';
\r
11832 t.parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', function(nodes, name) {
\r
11833 var i = nodes.length, node, nonEmptyElements = t.schema.getNonEmptyElements();
\r
11838 if (node.isEmpty(nonEmptyElements))
\r
11839 node.empty().append(new tinymce.html.Node('br', 1)).shortEnded = true;
\r
11843 t.serializer = new tinymce.dom.Serializer(s, t.dom, t.schema);
\r
11845 t.selection = new tinymce.dom.Selection(t.dom, t.getWin(), t.serializer);
\r
11847 t.formatter = new tinymce.Formatter(this);
\r
11849 // Register default formats
\r
11850 t.formatter.register({
\r
11852 {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'left'}},
\r
11853 {selector : 'img,table', collapsed : false, styles : {'float' : 'left'}}
\r
11857 {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'center'}},
\r
11858 {selector : 'img', collapsed : false, styles : {display : 'block', marginLeft : 'auto', marginRight : 'auto'}},
\r
11859 {selector : 'table', collapsed : false, styles : {marginLeft : 'auto', marginRight : 'auto'}}
\r
11863 {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'right'}},
\r
11864 {selector : 'img,table', collapsed : false, styles : {'float' : 'right'}}
\r
11868 {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'justify'}}
\r
11872 {inline : 'strong', remove : 'all'},
\r
11873 {inline : 'span', styles : {fontWeight : 'bold'}},
\r
11874 {inline : 'b', remove : 'all'}
\r
11878 {inline : 'em', remove : 'all'},
\r
11879 {inline : 'span', styles : {fontStyle : 'italic'}},
\r
11880 {inline : 'i', remove : 'all'}
\r
11884 {inline : 'span', styles : {textDecoration : 'underline'}, exact : true},
\r
11885 {inline : 'u', remove : 'all'}
\r
11888 strikethrough : [
\r
11889 {inline : 'span', styles : {textDecoration : 'line-through'}, exact : true},
\r
11890 {inline : 'strike', remove : 'all'}
\r
11893 forecolor : {inline : 'span', styles : {color : '%value'}, wrap_links : false},
\r
11894 hilitecolor : {inline : 'span', styles : {backgroundColor : '%value'}, wrap_links : false},
\r
11895 fontname : {inline : 'span', styles : {fontFamily : '%value'}},
\r
11896 fontsize : {inline : 'span', styles : {fontSize : '%value'}},
\r
11897 fontsize_class : {inline : 'span', attributes : {'class' : '%value'}},
\r
11898 blockquote : {block : 'blockquote', wrapper : 1, remove : 'all'},
\r
11899 subscript : {inline : 'sub'},
\r
11900 superscript : {inline : 'sup'},
\r
11902 link : {inline : 'a', selector : 'a', remove : 'all', split : true, deep : true,
\r
11903 onmatch : function(node) {
\r
11907 onformat : function(elm, fmt, vars) {
\r
11908 each(vars, function(value, key) {
\r
11909 t.dom.setAttrib(elm, key, value);
\r
11915 {selector : 'b,strong,em,i,font,u,strike', remove : 'all', split : true, expand : false, block_expand : true, deep : true},
\r
11916 {selector : 'span', attributes : ['style', 'class'], remove : 'empty', split : true, expand : false, deep : true},
\r
11917 {selector : '*', attributes : ['style', 'class'], split : false, expand : false, deep : true}
\r
11921 // Register default block formats
\r
11922 each('p h1 h2 h3 h4 h5 h6 div address pre div code dt dd samp'.split(/\s/), function(name) {
\r
11923 t.formatter.register(name, {block : name, remove : 'all'});
\r
11926 // Register user defined formats
\r
11927 t.formatter.register(t.settings.formats);
\r
11929 t.undoManager = new tinymce.UndoManager(t);
\r
11932 t.undoManager.onAdd.add(function(um, l) {
\r
11933 if (um.hasUndo())
\r
11934 return t.onChange.dispatch(t, l, um);
\r
11937 t.undoManager.onUndo.add(function(um, l) {
\r
11938 return t.onUndo.dispatch(t, l, um);
\r
11941 t.undoManager.onRedo.add(function(um, l) {
\r
11942 return t.onRedo.dispatch(t, l, um);
\r
11945 t.forceBlocks = new tinymce.ForceBlocks(t, {
\r
11946 forced_root_block : s.forced_root_block
\r
11949 t.editorCommands = new tinymce.EditorCommands(t);
\r
11952 t.serializer.onPreProcess.add(function(se, o) {
\r
11953 return t.onPreProcess.dispatch(t, o, se);
\r
11956 t.serializer.onPostProcess.add(function(se, o) {
\r
11957 return t.onPostProcess.dispatch(t, o, se);
\r
11960 t.onPreInit.dispatch(t);
\r
11962 if (!s.gecko_spellcheck)
\r
11963 t.getBody().spellcheck = 0;
\r
11968 t.controlManager.onPostRender.dispatch(t, t.controlManager);
\r
11969 t.onPostRender.dispatch(t);
\r
11971 t.quirks = new tinymce.util.Quirks(this);
\r
11973 if (s.directionality)
\r
11974 t.getBody().dir = s.directionality;
\r
11977 t.getBody().style.whiteSpace = "nowrap";
\r
11979 if (s.handle_node_change_callback) {
\r
11980 t.onNodeChange.add(function(ed, cm, n) {
\r
11981 t.execCallback('handle_node_change_callback', t.id, n, -1, -1, true, t.selection.isCollapsed());
\r
11985 if (s.save_callback) {
\r
11986 t.onSaveContent.add(function(ed, o) {
\r
11987 var h = t.execCallback('save_callback', t.id, o.content, t.getBody());
\r
11994 if (s.onchange_callback) {
\r
11995 t.onChange.add(function(ed, l) {
\r
11996 t.execCallback('onchange_callback', t, l);
\r
12001 t.onBeforeSetContent.add(function(ed, o) {
\r
12003 each(s.protect, function(pattern) {
\r
12004 o.content = o.content.replace(pattern, function(str) {
\r
12005 return '<!--mce:protected ' + escape(str) + '-->';
\r
12012 if (s.convert_newlines_to_brs) {
\r
12013 t.onBeforeSetContent.add(function(ed, o) {
\r
12015 o.content = o.content.replace(/\r?\n/g, '<br />');
\r
12019 if (s.preformatted) {
\r
12020 t.onPostProcess.add(function(ed, o) {
\r
12021 o.content = o.content.replace(/^\s*<pre.*?>/, '');
\r
12022 o.content = o.content.replace(/<\/pre>\s*$/, '');
\r
12025 o.content = '<pre class="mceItemHidden">' + o.content + '</pre>';
\r
12029 if (s.verify_css_classes) {
\r
12030 t.serializer.attribValueFilter = function(n, v) {
\r
12033 if (n == 'class') {
\r
12034 // Build regexp for classes
\r
12035 if (!t.classesRE) {
\r
12036 cl = t.dom.getClasses();
\r
12038 if (cl.length > 0) {
\r
12041 each (cl, function(o) {
\r
12042 s += (s ? '|' : '') + o['class'];
\r
12045 t.classesRE = new RegExp('(' + s + ')', 'gi');
\r
12049 return !t.classesRE || /(\bmceItem\w+\b|\bmceTemp\w+\b)/g.test(v) || t.classesRE.test(v) ? v : '';
\r
12056 if (s.cleanup_callback) {
\r
12057 t.onBeforeSetContent.add(function(ed, o) {
\r
12058 o.content = t.execCallback('cleanup_callback', 'insert_to_editor', o.content, o);
\r
12061 t.onPreProcess.add(function(ed, o) {
\r
12063 t.execCallback('cleanup_callback', 'insert_to_editor_dom', o.node, o);
\r
12066 t.execCallback('cleanup_callback', 'get_from_editor_dom', o.node, o);
\r
12069 t.onPostProcess.add(function(ed, o) {
\r
12071 o.content = t.execCallback('cleanup_callback', 'insert_to_editor', o.content, o);
\r
12074 o.content = t.execCallback('cleanup_callback', 'get_from_editor', o.content, o);
\r
12078 if (s.save_callback) {
\r
12079 t.onGetContent.add(function(ed, o) {
\r
12081 o.content = t.execCallback('save_callback', t.id, o.content, t.getBody());
\r
12085 if (s.handle_event_callback) {
\r
12086 t.onEvent.add(function(ed, e, o) {
\r
12087 if (t.execCallback('handle_event_callback', e, ed, o) === false)
\r
12092 // Add visual aids when new contents is added
\r
12093 t.onSetContent.add(function() {
\r
12094 t.addVisual(t.getBody());
\r
12097 // Remove empty contents
\r
12098 if (s.padd_empty_editor) {
\r
12099 t.onPostProcess.add(function(ed, o) {
\r
12100 o.content = o.content.replace(/^(<p[^>]*>( | |\s|\u00a0|)<\/p>[\r\n]*|<br \/>[\r\n]*)$/, '');
\r
12105 // Fix gecko link bug, when a link is placed at the end of block elements there is
\r
12106 // no way to move the caret behind the link. This fix adds a bogus br element after the link
\r
12107 function fixLinks(ed, o) {
\r
12108 each(ed.dom.select('a'), function(n) {
\r
12109 var pn = n.parentNode;
\r
12111 if (ed.dom.isBlock(pn) && pn.lastChild === n)
\r
12112 ed.dom.add(pn, 'br', {'data-mce-bogus' : 1});
\r
12116 t.onExecCommand.add(function(ed, cmd) {
\r
12117 if (cmd === 'CreateLink')
\r
12121 t.onSetContent.add(t.selection.onSetContent.add(fixLinks));
\r
12124 t.load({initial : true, format : 'html'});
\r
12125 t.startContent = t.getContent({format : 'raw'});
\r
12126 t.undoManager.add();
\r
12127 t.initialized = true;
\r
12129 t.onInit.dispatch(t);
\r
12130 t.execCallback('setupcontent_callback', t.id, t.getBody(), t.getDoc());
\r
12131 t.execCallback('init_instance_callback', t);
\r
12133 t.nodeChanged({initial : 1});
\r
12135 // Load specified content CSS last
\r
12136 each(t.contentCSS, function(u) {
\r
12137 t.dom.loadCSS(u);
\r
12140 // Handle auto focus
\r
12141 if (s.auto_focus) {
\r
12142 setTimeout(function () {
\r
12143 var ed = tinymce.get(s.auto_focus);
\r
12145 ed.selection.select(ed.getBody(), 1);
\r
12146 ed.selection.collapse(1);
\r
12147 ed.getBody().focus();
\r
12148 ed.getWin().focus();
\r
12156 focus : function(sf) {
\r
12157 var oed, t = this, selection = t.selection, ce = t.settings.content_editable, ieRng, controlElm, doc = t.getDoc();
\r
12160 // Get selected control element
\r
12161 ieRng = selection.getRng();
\r
12162 if (ieRng.item) {
\r
12163 controlElm = ieRng.item(0);
\r
12166 t._refreshContentEditable();
\r
12168 // Is not content editable
\r
12170 t.getWin().focus();
\r
12172 // Focus the body as well since it's contentEditable
\r
12173 if (tinymce.isGecko) {
\r
12174 t.getBody().focus();
\r
12177 // Restore selected control element
\r
12178 // This is needed when for example an image is selected within a
\r
12179 // layer a call to focus will then remove the control selection
\r
12180 if (controlElm && controlElm.ownerDocument == doc) {
\r
12181 ieRng = doc.body.createControlRange();
\r
12182 ieRng.addElement(controlElm);
\r
12188 if (tinymce.activeEditor != t) {
\r
12189 if ((oed = tinymce.activeEditor) != null)
\r
12190 oed.onDeactivate.dispatch(oed, t);
\r
12192 t.onActivate.dispatch(t, oed);
\r
12195 tinymce._setActive(t);
\r
12198 execCallback : function(n) {
\r
12199 var t = this, f = t.settings[n], s;
\r
12204 // Look through lookup
\r
12205 if (t.callbackLookup && (s = t.callbackLookup[n])) {
\r
12210 if (is(f, 'string')) {
\r
12211 s = f.replace(/\.\w+$/, '');
\r
12212 s = s ? tinymce.resolve(s) : 0;
\r
12213 f = tinymce.resolve(f);
\r
12214 t.callbackLookup = t.callbackLookup || {};
\r
12215 t.callbackLookup[n] = {func : f, scope : s};
\r
12218 return f.apply(s || t, Array.prototype.slice.call(arguments, 1));
\r
12221 translate : function(s) {
\r
12222 var c = this.settings.language || 'en', i18n = tinymce.i18n;
\r
12227 return i18n[c + '.' + s] || s.replace(/{\#([^}]+)\}/g, function(a, b) {
\r
12228 return i18n[c + '.' + b] || '{#' + b + '}';
\r
12232 getLang : function(n, dv) {
\r
12233 return tinymce.i18n[(this.settings.language || 'en') + '.' + n] || (is(dv) ? dv : '{#' + n + '}');
\r
12236 getParam : function(n, dv, ty) {
\r
12237 var tr = tinymce.trim, v = is(this.settings[n]) ? this.settings[n] : dv, o;
\r
12239 if (ty === 'hash') {
\r
12242 if (is(v, 'string')) {
\r
12243 each(v.indexOf('=') > 0 ? v.split(/[;,](?![^=;,]*(?:[;,]|$))/) : v.split(','), function(v) {
\r
12244 v = v.split('=');
\r
12246 if (v.length > 1)
\r
12247 o[tr(v[0])] = tr(v[1]);
\r
12249 o[tr(v[0])] = tr(v);
\r
12260 nodeChanged : function(o) {
\r
12261 var t = this, s = t.selection, n = s.getStart() || t.getBody();
\r
12263 // Fix for bug #1896577 it seems that this can not be fired while the editor is loading
\r
12264 if (t.initialized) {
\r
12266 n = isIE && n.ownerDocument != t.getDoc() ? t.getBody() : n; // Fix for IE initial state
\r
12268 // Get parents and add them to object
\r
12270 t.dom.getParent(n, function(node) {
\r
12271 if (node.nodeName == 'BODY')
\r
12274 o.parents.push(node);
\r
12277 t.onNodeChange.dispatch(
\r
12279 o ? o.controlManager || t.controlManager : t.controlManager,
\r
12287 addButton : function(n, s) {
\r
12290 t.buttons = t.buttons || {};
\r
12291 t.buttons[n] = s;
\r
12294 addCommand : function(name, callback, scope) {
\r
12295 this.execCommands[name] = {func : callback, scope : scope || this};
\r
12298 addQueryStateHandler : function(name, callback, scope) {
\r
12299 this.queryStateCommands[name] = {func : callback, scope : scope || this};
\r
12302 addQueryValueHandler : function(name, callback, scope) {
\r
12303 this.queryValueCommands[name] = {func : callback, scope : scope || this};
\r
12306 addShortcut : function(pa, desc, cmd_func, sc) {
\r
12309 if (!t.settings.custom_shortcuts)
\r
12312 t.shortcuts = t.shortcuts || {};
\r
12314 if (is(cmd_func, 'string')) {
\r
12317 cmd_func = function() {
\r
12318 t.execCommand(c, false, null);
\r
12322 if (is(cmd_func, 'object')) {
\r
12325 cmd_func = function() {
\r
12326 t.execCommand(c[0], c[1], c[2]);
\r
12330 each(explode(pa), function(pa) {
\r
12333 scope : sc || this,
\r
12340 each(explode(pa, '+'), function(v) {
\r
12349 o.charCode = v.charCodeAt(0);
\r
12350 o.keyCode = v.toUpperCase().charCodeAt(0);
\r
12354 t.shortcuts[(o.ctrl ? 'ctrl' : '') + ',' + (o.alt ? 'alt' : '') + ',' + (o.shift ? 'shift' : '') + ',' + o.keyCode] = o;
\r
12360 execCommand : function(cmd, ui, val, a) {
\r
12361 var t = this, s = 0, o, st;
\r
12363 if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint|SelectAll)$/.test(cmd) && (!a || !a.skip_focus))
\r
12366 a = extend({}, a);
\r
12367 t.onBeforeExecCommand.dispatch(t, cmd, ui, val, a);
\r
12371 // Command callback
\r
12372 if (t.execCallback('execcommand_callback', t.id, t.selection.getNode(), cmd, ui, val)) {
\r
12373 t.onExecCommand.dispatch(t, cmd, ui, val, a);
\r
12377 // Registred commands
\r
12378 if (o = t.execCommands[cmd]) {
\r
12379 st = o.func.call(o.scope, ui, val);
\r
12381 // Fall through on true
\r
12382 if (st !== true) {
\r
12383 t.onExecCommand.dispatch(t, cmd, ui, val, a);
\r
12388 // Plugin commands
\r
12389 each(t.plugins, function(p) {
\r
12390 if (p.execCommand && p.execCommand(cmd, ui, val)) {
\r
12391 t.onExecCommand.dispatch(t, cmd, ui, val, a);
\r
12400 // Theme commands
\r
12401 if (t.theme && t.theme.execCommand && t.theme.execCommand(cmd, ui, val)) {
\r
12402 t.onExecCommand.dispatch(t, cmd, ui, val, a);
\r
12406 // Editor commands
\r
12407 if (t.editorCommands.execCommand(cmd, ui, val)) {
\r
12408 t.onExecCommand.dispatch(t, cmd, ui, val, a);
\r
12412 // Browser commands
\r
12413 t.getDoc().execCommand(cmd, ui, val);
\r
12414 t.onExecCommand.dispatch(t, cmd, ui, val, a);
\r
12417 queryCommandState : function(cmd) {
\r
12418 var t = this, o, s;
\r
12420 // Is hidden then return undefined
\r
12421 if (t._isHidden())
\r
12424 // Registred commands
\r
12425 if (o = t.queryStateCommands[cmd]) {
\r
12426 s = o.func.call(o.scope);
\r
12428 // Fall though on true
\r
12433 // Registred commands
\r
12434 o = t.editorCommands.queryCommandState(cmd);
\r
12438 // Browser commands
\r
12440 return this.getDoc().queryCommandState(cmd);
\r
12442 // Fails sometimes see bug: 1896577
\r
12446 queryCommandValue : function(c) {
\r
12447 var t = this, o, s;
\r
12449 // Is hidden then return undefined
\r
12450 if (t._isHidden())
\r
12453 // Registred commands
\r
12454 if (o = t.queryValueCommands[c]) {
\r
12455 s = o.func.call(o.scope);
\r
12457 // Fall though on true
\r
12462 // Registred commands
\r
12463 o = t.editorCommands.queryCommandValue(c);
\r
12467 // Browser commands
\r
12469 return this.getDoc().queryCommandValue(c);
\r
12471 // Fails sometimes see bug: 1896577
\r
12475 show : function() {
\r
12478 DOM.show(t.getContainer());
\r
12483 hide : function() {
\r
12484 var t = this, d = t.getDoc();
\r
12486 // Fixed bug where IE has a blinking cursor left from the editor
\r
12488 d.execCommand('SelectAll');
\r
12490 // We must save before we hide so Safari doesn't crash
\r
12492 DOM.hide(t.getContainer());
\r
12493 DOM.setStyle(t.id, 'display', t.orgDisplay);
\r
12496 isHidden : function() {
\r
12497 return !DOM.isHidden(this.id);
\r
12500 setProgressState : function(b, ti, o) {
\r
12501 this.onSetProgressState.dispatch(this, b, ti, o);
\r
12506 load : function(o) {
\r
12507 var t = this, e = t.getElement(), h;
\r
12513 // Double encode existing entities in the value
\r
12514 h = t.setContent(is(e.value) ? e.value : e.innerHTML, o);
\r
12517 if (!o.no_events)
\r
12518 t.onLoadContent.dispatch(t, o);
\r
12520 o.element = e = null;
\r
12526 save : function(o) {
\r
12527 var t = this, e = t.getElement(), h, f;
\r
12529 if (!e || !t.initialized)
\r
12535 // Add undo level will trigger onchange event
\r
12536 if (!o.no_events) {
\r
12537 t.undoManager.typing = false;
\r
12538 t.undoManager.add();
\r
12542 h = o.content = t.getContent(o);
\r
12544 if (!o.no_events)
\r
12545 t.onSaveContent.dispatch(t, o);
\r
12549 if (!/TEXTAREA|INPUT/i.test(e.nodeName)) {
\r
12552 // Update hidden form element
\r
12553 if (f = DOM.getParent(t.id, 'form')) {
\r
12554 each(f.elements, function(e) {
\r
12555 if (e.name == t.id) {
\r
12564 o.element = e = null;
\r
12569 setContent : function(content, args) {
\r
12570 var self = this, rootNode, body = self.getBody(), forcedRootBlockName;
\r
12572 // Setup args object
\r
12573 args = args || {};
\r
12574 args.format = args.format || 'html';
\r
12576 args.content = content;
\r
12578 // Do preprocessing
\r
12579 if (!args.no_events)
\r
12580 self.onBeforeSetContent.dispatch(self, args);
\r
12582 content = args.content;
\r
12584 // Padd empty content in Gecko and Safari. Commands will otherwise fail on the content
\r
12585 // It will also be impossible to place the caret in the editor unless there is a BR element present
\r
12586 if (!tinymce.isIE && (content.length === 0 || /^\s+$/.test(content))) {
\r
12587 forcedRootBlockName = self.settings.forced_root_block;
\r
12588 if (forcedRootBlockName)
\r
12589 content = '<' + forcedRootBlockName + '><br data-mce-bogus="1"></' + forcedRootBlockName + '>';
\r
12591 content = '<br data-mce-bogus="1">';
\r
12593 body.innerHTML = content;
\r
12594 self.selection.select(body, true);
\r
12595 self.selection.collapse(true);
\r
12599 // Parse and serialize the html
\r
12600 if (args.format !== 'raw') {
\r
12601 content = new tinymce.html.Serializer({}, self.schema).serialize(
\r
12602 self.parser.parse(content)
\r
12606 // Set the new cleaned contents to the editor
\r
12607 args.content = tinymce.trim(content);
\r
12608 self.dom.setHTML(body, args.content);
\r
12610 // Do post processing
\r
12611 if (!args.no_events)
\r
12612 self.onSetContent.dispatch(self, args);
\r
12614 self.selection.normalize();
\r
12616 return args.content;
\r
12619 getContent : function(args) {
\r
12620 var self = this, content;
\r
12622 // Setup args object
\r
12623 args = args || {};
\r
12624 args.format = args.format || 'html';
\r
12627 // Do preprocessing
\r
12628 if (!args.no_events)
\r
12629 self.onBeforeGetContent.dispatch(self, args);
\r
12631 // Get raw contents or by default the cleaned contents
\r
12632 if (args.format == 'raw')
\r
12633 content = self.getBody().innerHTML;
\r
12635 content = self.serializer.serialize(self.getBody(), args);
\r
12637 args.content = tinymce.trim(content);
\r
12639 // Do post processing
\r
12640 if (!args.no_events)
\r
12641 self.onGetContent.dispatch(self, args);
\r
12643 return args.content;
\r
12646 isDirty : function() {
\r
12649 return tinymce.trim(self.startContent) != tinymce.trim(self.getContent({format : 'raw', no_events : 1})) && !self.isNotDirty;
\r
12652 getContainer : function() {
\r
12655 if (!t.container)
\r
12656 t.container = DOM.get(t.editorContainer || t.id + '_parent');
\r
12658 return t.container;
\r
12661 getContentAreaContainer : function() {
\r
12662 return this.contentAreaContainer;
\r
12665 getElement : function() {
\r
12666 return DOM.get(this.settings.content_element || this.id);
\r
12669 getWin : function() {
\r
12672 if (!t.contentWindow) {
\r
12673 e = DOM.get(t.id + "_ifr");
\r
12676 t.contentWindow = e.contentWindow;
\r
12679 return t.contentWindow;
\r
12682 getDoc : function() {
\r
12685 if (!t.contentDocument) {
\r
12689 t.contentDocument = w.document;
\r
12692 return t.contentDocument;
\r
12695 getBody : function() {
\r
12696 return this.bodyElement || this.getDoc().body;
\r
12699 convertURL : function(u, n, e) {
\r
12700 var t = this, s = t.settings;
\r
12702 // Use callback instead
\r
12703 if (s.urlconverter_callback)
\r
12704 return t.execCallback('urlconverter_callback', u, e, true, n);
\r
12706 // Don't convert link href since thats the CSS files that gets loaded into the editor also skip local file URLs
\r
12707 if (!s.convert_urls || (e && e.nodeName == 'LINK') || u.indexOf('file:') === 0)
\r
12710 // Convert to relative
\r
12711 if (s.relative_urls)
\r
12712 return t.documentBaseURI.toRelative(u);
\r
12714 // Convert to absolute
\r
12715 u = t.documentBaseURI.toAbsolute(u, s.remove_script_host);
\r
12720 addVisual : function(e) {
\r
12721 var t = this, s = t.settings;
\r
12723 e = e || t.getBody();
\r
12725 if (!is(t.hasVisual))
\r
12726 t.hasVisual = s.visual;
\r
12728 each(t.dom.select('table,a', e), function(e) {
\r
12731 switch (e.nodeName) {
\r
12733 v = t.dom.getAttrib(e, 'border');
\r
12735 if (!v || v == '0') {
\r
12737 t.dom.addClass(e, s.visual_table_class);
\r
12739 t.dom.removeClass(e, s.visual_table_class);
\r
12745 v = t.dom.getAttrib(e, 'name');
\r
12749 t.dom.addClass(e, 'mceItemAnchor');
\r
12751 t.dom.removeClass(e, 'mceItemAnchor');
\r
12758 t.onVisualAid.dispatch(t, e, t.hasVisual);
\r
12761 remove : function() {
\r
12762 var t = this, e = t.getContainer();
\r
12764 t.removed = 1; // Cancels post remove event execution
\r
12767 t.execCallback('remove_instance_callback', t);
\r
12768 t.onRemove.dispatch(t);
\r
12770 // Clear all execCommand listeners this is required to avoid errors if the editor was removed inside another command
\r
12771 t.onExecCommand.listeners = [];
\r
12773 tinymce.remove(t);
\r
12777 destroy : function(s) {
\r
12780 // One time is enough
\r
12785 tinymce.removeUnload(t.destroy);
\r
12786 tinyMCE.onBeforeUnload.remove(t._beforeUnload);
\r
12788 // Manual destroy
\r
12789 if (t.theme && t.theme.destroy)
\r
12790 t.theme.destroy();
\r
12792 // Destroy controls, selection and dom
\r
12793 t.controlManager.destroy();
\r
12794 t.selection.destroy();
\r
12797 // Remove all events
\r
12799 // Don't clear the window or document if content editable
\r
12800 // is enabled since other instances might still be present
\r
12801 if (!t.settings.content_editable) {
\r
12802 Event.clear(t.getWin());
\r
12803 Event.clear(t.getDoc());
\r
12806 Event.clear(t.getBody());
\r
12807 Event.clear(t.formElement);
\r
12810 if (t.formElement) {
\r
12811 t.formElement.submit = t.formElement._mceOldSubmit;
\r
12812 t.formElement._mceOldSubmit = null;
\r
12815 t.contentAreaContainer = t.formElement = t.container = t.settings.content_element = t.bodyElement = t.contentDocument = t.contentWindow = null;
\r
12818 t.selection = t.selection.win = t.selection.dom = t.selection.dom.doc = null;
\r
12823 // Internal functions
\r
12825 _addEvents : function() {
\r
12826 // 'focus', 'blur', 'dblclick', 'beforedeactivate', submit, reset
\r
12827 var t = this, i, s = t.settings, dom = t.dom, lo = {
\r
12828 mouseup : 'onMouseUp',
\r
12829 mousedown : 'onMouseDown',
\r
12830 click : 'onClick',
\r
12831 keyup : 'onKeyUp',
\r
12832 keydown : 'onKeyDown',
\r
12833 keypress : 'onKeyPress',
\r
12834 submit : 'onSubmit',
\r
12835 reset : 'onReset',
\r
12836 contextmenu : 'onContextMenu',
\r
12837 dblclick : 'onDblClick',
\r
12838 paste : 'onPaste' // Doesn't work in all browsers yet
\r
12841 function eventHandler(e, o) {
\r
12844 // Don't fire events when it's removed
\r
12848 // Generic event handler
\r
12849 if (t.onEvent.dispatch(t, e, o) !== false) {
\r
12850 // Specific event handler
\r
12851 t[lo[e.fakeType || e.type]].dispatch(t, e, o);
\r
12855 // Add DOM events
\r
12856 each(lo, function(v, k) {
\r
12858 case 'contextmenu':
\r
12859 dom.bind(t.getDoc(), k, eventHandler);
\r
12863 dom.bind(t.getBody(), k, function(e) {
\r
12870 dom.bind(t.getElement().form || DOM.getParent(t.id, 'form'), k, eventHandler);
\r
12874 dom.bind(s.content_editable ? t.getBody() : t.getDoc(), k, eventHandler);
\r
12878 dom.bind(s.content_editable ? t.getBody() : (isGecko ? t.getDoc() : t.getWin()), 'focus', function(e) {
\r
12883 // Fixes bug where a specified document_base_uri could result in broken images
\r
12884 // This will also fix drag drop of images in Gecko
\r
12885 if (tinymce.isGecko) {
\r
12886 dom.bind(t.getDoc(), 'DOMNodeInserted', function(e) {
\r
12891 if (e.nodeType === 1 && e.nodeName === 'IMG' && (v = e.getAttribute('data-mce-src')))
\r
12892 e.src = t.documentBaseURI.toAbsolute(v);
\r
12896 // Set various midas options in Gecko
\r
12898 function setOpts() {
\r
12899 var t = this, d = t.getDoc(), s = t.settings;
\r
12901 if (isGecko && !s.readonly) {
\r
12902 t._refreshContentEditable();
\r
12905 // Try new Gecko method
\r
12906 d.execCommand("styleWithCSS", 0, false);
\r
12908 // Use old method
\r
12909 if (!t._isHidden())
\r
12910 try {d.execCommand("useCSS", 0, true);} catch (ex) {}
\r
12913 if (!s.table_inline_editing)
\r
12914 try {d.execCommand('enableInlineTableEditing', false, false);} catch (ex) {}
\r
12916 if (!s.object_resizing)
\r
12917 try {d.execCommand('enableObjectResizing', false, false);} catch (ex) {}
\r
12921 t.onBeforeExecCommand.add(setOpts);
\r
12922 t.onMouseDown.add(setOpts);
\r
12925 // Add node change handlers
\r
12926 t.onMouseUp.add(t.nodeChanged);
\r
12927 //t.onClick.add(t.nodeChanged);
\r
12928 t.onKeyUp.add(function(ed, e) {
\r
12929 var c = e.keyCode;
\r
12931 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
12936 // Add block quote deletion handler
\r
12937 t.onKeyDown.add(function(ed, e) {
\r
12938 if (e.keyCode != VK.BACKSPACE)
\r
12941 var rng = ed.selection.getRng();
\r
12942 if (!rng.collapsed)
\r
12945 var n = rng.startContainer;
\r
12946 var offset = rng.startOffset;
\r
12948 while (n && n.nodeType && n.nodeType != 1 && n.parentNode)
\r
12949 n = n.parentNode;
\r
12951 // Is the cursor at the beginning of a blockquote?
\r
12952 if (n && n.parentNode && n.parentNode.tagName === 'BLOCKQUOTE' && n.parentNode.firstChild == n && offset == 0) {
\r
12953 // Remove the blockquote
\r
12954 ed.formatter.toggle('blockquote', null, n.parentNode);
\r
12956 // Move the caret to the beginning of n
\r
12957 rng.setStart(n, 0);
\r
12958 rng.setEnd(n, 0);
\r
12959 ed.selection.setRng(rng);
\r
12960 ed.selection.collapse(false);
\r
12966 // Add reset handler
\r
12967 t.onReset.add(function() {
\r
12968 t.setContent(t.startContent, {format : 'raw'});
\r
12972 if (s.custom_shortcuts) {
\r
12973 if (s.custom_undo_redo_keyboard_shortcuts) {
\r
12974 t.addShortcut('ctrl+z', t.getLang('undo_desc'), 'Undo');
\r
12975 t.addShortcut('ctrl+y', t.getLang('redo_desc'), 'Redo');
\r
12978 // Add default shortcuts for gecko
\r
12979 t.addShortcut('ctrl+b', t.getLang('bold_desc'), 'Bold');
\r
12980 t.addShortcut('ctrl+i', t.getLang('italic_desc'), 'Italic');
\r
12981 t.addShortcut('ctrl+u', t.getLang('underline_desc'), 'Underline');
\r
12983 // BlockFormat shortcuts keys
\r
12984 for (i=1; i<=6; i++)
\r
12985 t.addShortcut('ctrl+' + i, '', ['FormatBlock', false, 'h' + i]);
\r
12987 t.addShortcut('ctrl+7', '', ['FormatBlock', false, 'p']);
\r
12988 t.addShortcut('ctrl+8', '', ['FormatBlock', false, 'div']);
\r
12989 t.addShortcut('ctrl+9', '', ['FormatBlock', false, 'address']);
\r
12991 function find(e) {
\r
12994 if (!e.altKey && !e.ctrlKey && !e.metaKey)
\r
12997 each(t.shortcuts, function(o) {
\r
12998 if (tinymce.isMac && o.ctrl != e.metaKey)
\r
13000 else if (!tinymce.isMac && o.ctrl != e.ctrlKey)
\r
13003 if (o.alt != e.altKey)
\r
13006 if (o.shift != e.shiftKey)
\r
13009 if (e.keyCode == o.keyCode || (e.charCode && e.charCode == o.charCode)) {
\r
13018 t.onKeyUp.add(function(ed, e) {
\r
13022 return Event.cancel(e);
\r
13025 t.onKeyPress.add(function(ed, e) {
\r
13029 return Event.cancel(e);
\r
13032 t.onKeyDown.add(function(ed, e) {
\r
13036 o.func.call(o.scope);
\r
13037 return Event.cancel(e);
\r
13042 if (tinymce.isIE) {
\r
13043 // Fix so resize will only update the width and height attributes not the styles of an image
\r
13044 // It will also block mceItemNoResize items
\r
13045 dom.bind(t.getDoc(), 'controlselect', function(e) {
\r
13046 var re = t.resizeInfo, cb;
\r
13050 // Don't do this action for non image elements
\r
13051 if (e.nodeName !== 'IMG')
\r
13055 dom.unbind(re.node, re.ev, re.cb);
\r
13057 if (!dom.hasClass(e, 'mceItemNoResize')) {
\r
13058 ev = 'resizeend';
\r
13059 cb = dom.bind(e, ev, function(e) {
\r
13064 if (v = dom.getStyle(e, 'width')) {
\r
13065 dom.setAttrib(e, 'width', v.replace(/[^0-9%]+/g, ''));
\r
13066 dom.setStyle(e, 'width', '');
\r
13069 if (v = dom.getStyle(e, 'height')) {
\r
13070 dom.setAttrib(e, 'height', v.replace(/[^0-9%]+/g, ''));
\r
13071 dom.setStyle(e, 'height', '');
\r
13075 ev = 'resizestart';
\r
13076 cb = dom.bind(e, 'resizestart', Event.cancel, Event);
\r
13079 re = t.resizeInfo = {
\r
13087 if (tinymce.isOpera) {
\r
13088 t.onClick.add(function(ed, e) {
\r
13089 Event.prevent(e);
\r
13093 // Add custom undo/redo handlers
\r
13094 if (s.custom_undo_redo) {
\r
13095 function addUndo() {
\r
13096 t.undoManager.typing = false;
\r
13097 t.undoManager.add();
\r
13100 var focusLostFunc = tinymce.isGecko ? 'blur' : 'focusout';
\r
13101 dom.bind(t.getDoc(), focusLostFunc, function(e){
\r
13102 if (!t.removed && t.undoManager.typing)
\r
13106 // Add undo level when contents is drag/dropped within the editor
\r
13107 t.dom.bind(t.dom.getRoot(), 'dragend', function(e) {
\r
13111 t.onKeyUp.add(function(ed, e) {
\r
13112 var keyCode = e.keyCode;
\r
13114 if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 13 || keyCode == 45 || e.ctrlKey)
\r
13118 t.onKeyDown.add(function(ed, e) {
\r
13119 var keyCode = e.keyCode, sel;
\r
13121 if (keyCode == 8) {
\r
13122 sel = t.getDoc().selection;
\r
13124 // Fix IE control + backspace browser bug
\r
13125 if (sel && sel.createRange && sel.createRange().item) {
\r
13126 t.undoManager.beforeChange();
\r
13127 ed.dom.remove(sel.createRange().item(0));
\r
13130 return Event.cancel(e);
\r
13134 // Is caracter positon keys left,right,up,down,home,end,pgdown,pgup,enter
\r
13135 if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 13 || keyCode == 45) {
\r
13136 // Add position before enter key is pressed, used by IE since it still uses the default browser behavior
\r
13137 // Todo: Remove this once we normalize enter behavior on IE
\r
13138 if (tinymce.isIE && keyCode == 13)
\r
13139 t.undoManager.beforeChange();
\r
13141 if (t.undoManager.typing)
\r
13147 // If key isn't shift,ctrl,alt,capslock,metakey
\r
13148 if ((keyCode < 16 || keyCode > 20) && keyCode != 224 && keyCode != 91 && !t.undoManager.typing) {
\r
13149 t.undoManager.beforeChange();
\r
13150 t.undoManager.typing = true;
\r
13151 t.undoManager.add();
\r
13155 t.onMouseDown.add(function() {
\r
13156 if (t.undoManager.typing)
\r
13161 // Bug fix for FireFox keeping styles from end of selection instead of start.
\r
13162 if (tinymce.isGecko) {
\r
13163 function getAttributeApplyFunction() {
\r
13164 var template = t.dom.getAttribs(t.selection.getStart().cloneNode(false));
\r
13166 return function() {
\r
13167 var target = t.selection.getStart();
\r
13169 if (target !== t.getBody()) {
\r
13170 t.dom.setAttrib(target, "style", null);
\r
13172 each(template, function(attr) {
\r
13173 target.setAttributeNode(attr.cloneNode(true));
\r
13179 function isSelectionAcrossElements() {
\r
13180 var s = t.selection;
\r
13182 return !s.isCollapsed() && s.getStart() != s.getEnd();
\r
13185 t.onKeyPress.add(function(ed, e) {
\r
13186 var applyAttributes;
\r
13188 if ((e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) {
\r
13189 applyAttributes = getAttributeApplyFunction();
\r
13190 t.getDoc().execCommand('delete', false, null);
\r
13191 applyAttributes();
\r
13193 return Event.cancel(e);
\r
13197 t.dom.bind(t.getDoc(), 'cut', function(e) {
\r
13198 var applyAttributes;
\r
13200 if (isSelectionAcrossElements()) {
\r
13201 applyAttributes = getAttributeApplyFunction();
\r
13202 t.onKeyUp.addToTop(Event.cancel, Event);
\r
13204 setTimeout(function() {
\r
13205 applyAttributes();
\r
13206 t.onKeyUp.remove(Event.cancel, Event);
\r
13213 _refreshContentEditable : function() {
\r
13214 var self = this, body, parent;
\r
13216 // Check if the editor was hidden and the re-initalize contentEditable mode by removing and adding the body again
\r
13217 if (self._isHidden()) {
\r
13218 body = self.getBody();
\r
13219 parent = body.parentNode;
\r
13221 parent.removeChild(body);
\r
13222 parent.appendChild(body);
\r
13228 _isHidden : function() {
\r
13234 // Weird, wheres that cursor selection?
\r
13235 s = this.selection.getSel();
\r
13236 return (!s || !s.rangeCount || s.rangeCount == 0);
\r
13241 (function(tinymce) {
\r
13242 // Added for compression purposes
\r
13243 var each = tinymce.each, undefined, TRUE = true, FALSE = false;
\r
13245 tinymce.EditorCommands = function(editor) {
\r
13246 var dom = editor.dom,
\r
13247 selection = editor.selection,
\r
13248 commands = {state: {}, exec : {}, value : {}},
\r
13249 settings = editor.settings,
\r
13250 formatter = editor.formatter,
\r
13253 function execCommand(command, ui, value) {
\r
13256 command = command.toLowerCase();
\r
13257 if (func = commands.exec[command]) {
\r
13258 func(command, ui, value);
\r
13265 function queryCommandState(command) {
\r
13268 command = command.toLowerCase();
\r
13269 if (func = commands.state[command])
\r
13270 return func(command);
\r
13275 function queryCommandValue(command) {
\r
13278 command = command.toLowerCase();
\r
13279 if (func = commands.value[command])
\r
13280 return func(command);
\r
13285 function addCommands(command_list, type) {
\r
13286 type = type || 'exec';
\r
13288 each(command_list, function(callback, command) {
\r
13289 each(command.toLowerCase().split(','), function(command) {
\r
13290 commands[type][command] = callback;
\r
13295 // Expose public methods
\r
13296 tinymce.extend(this, {
\r
13297 execCommand : execCommand,
\r
13298 queryCommandState : queryCommandState,
\r
13299 queryCommandValue : queryCommandValue,
\r
13300 addCommands : addCommands
\r
13303 // Private methods
\r
13305 function execNativeCommand(command, ui, value) {
\r
13306 if (ui === undefined)
\r
13309 if (value === undefined)
\r
13312 return editor.getDoc().execCommand(command, ui, value);
\r
13315 function isFormatMatch(name) {
\r
13316 return formatter.match(name);
\r
13319 function toggleFormat(name, value) {
\r
13320 formatter.toggle(name, value ? {value : value} : undefined);
\r
13323 function storeSelection(type) {
\r
13324 bookmark = selection.getBookmark(type);
\r
13327 function restoreSelection() {
\r
13328 selection.moveToBookmark(bookmark);
\r
13331 // Add execCommand overrides
\r
13333 // Ignore these, added for compatibility
\r
13334 'mceResetDesignMode,mceBeginUndoLevel' : function() {},
\r
13336 // Add undo manager logic
\r
13337 'mceEndUndoLevel,mceAddUndoLevel' : function() {
\r
13338 editor.undoManager.add();
\r
13341 'Cut,Copy,Paste' : function(command) {
\r
13342 var doc = editor.getDoc(), failed;
\r
13344 // Try executing the native command
\r
13346 execNativeCommand(command);
\r
13348 // Command failed
\r
13352 // Present alert message about clipboard access not being available
\r
13353 if (failed || !doc.queryCommandSupported(command)) {
\r
13354 if (tinymce.isGecko) {
\r
13355 editor.windowManager.confirm(editor.getLang('clipboard_msg'), function(state) {
\r
13357 open('http://www.mozilla.org/editor/midasdemo/securityprefs.html', '_blank');
\r
13360 editor.windowManager.alert(editor.getLang('clipboard_no_support'));
\r
13364 // Override unlink command
\r
13365 unlink : function(command) {
\r
13366 if (selection.isCollapsed())
\r
13367 selection.select(selection.getNode());
\r
13369 execNativeCommand(command);
\r
13370 selection.collapse(FALSE);
\r
13373 // Override justify commands to use the text formatter engine
\r
13374 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) {
\r
13375 var align = command.substring(7);
\r
13377 // Remove all other alignments first
\r
13378 each('left,center,right,full'.split(','), function(name) {
\r
13379 if (align != name)
\r
13380 formatter.remove('align' + name);
\r
13383 toggleFormat('align' + align);
\r
13384 execCommand('mceRepaint');
\r
13387 // Override list commands to fix WebKit bug
\r
13388 'InsertUnorderedList,InsertOrderedList' : function(command) {
\r
13389 var listElm, listParent;
\r
13391 execNativeCommand(command);
\r
13393 // WebKit produces lists within block elements so we need to split them
\r
13394 // we will replace the native list creation logic to custom logic later on
\r
13395 // TODO: Remove this when the list creation logic is removed
\r
13396 listElm = dom.getParent(selection.getNode(), 'ol,ul');
\r
13398 listParent = listElm.parentNode;
\r
13400 // If list is within a text block then split that block
\r
13401 if (/^(H[1-6]|P|ADDRESS|PRE)$/.test(listParent.nodeName)) {
\r
13402 storeSelection();
\r
13403 dom.split(listParent, listElm);
\r
13404 restoreSelection();
\r
13409 // Override commands to use the text formatter engine
\r
13410 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) {
\r
13411 toggleFormat(command);
\r
13414 // Override commands to use the text formatter engine
\r
13415 'ForeColor,HiliteColor,FontName' : function(command, ui, value) {
\r
13416 toggleFormat(command, value);
\r
13419 FontSize : function(command, ui, value) {
\r
13420 var fontClasses, fontSizes;
\r
13422 // Convert font size 1-7 to styles
\r
13423 if (value >= 1 && value <= 7) {
\r
13424 fontSizes = tinymce.explode(settings.font_size_style_values);
\r
13425 fontClasses = tinymce.explode(settings.font_size_classes);
\r
13428 value = fontClasses[value - 1] || value;
\r
13430 value = fontSizes[value - 1] || value;
\r
13433 toggleFormat(command, value);
\r
13436 RemoveFormat : function(command) {
\r
13437 formatter.remove(command);
\r
13440 mceBlockQuote : function(command) {
\r
13441 toggleFormat('blockquote');
\r
13444 FormatBlock : function(command, ui, value) {
\r
13445 return toggleFormat(value || 'p');
\r
13448 mceCleanup : function() {
\r
13449 var bookmark = selection.getBookmark();
\r
13451 editor.setContent(editor.getContent({cleanup : TRUE}), {cleanup : TRUE});
\r
13453 selection.moveToBookmark(bookmark);
\r
13456 mceRemoveNode : function(command, ui, value) {
\r
13457 var node = value || selection.getNode();
\r
13459 // Make sure that the body node isn't removed
\r
13460 if (node != editor.getBody()) {
\r
13461 storeSelection();
\r
13462 editor.dom.remove(node, TRUE);
\r
13463 restoreSelection();
\r
13467 mceSelectNodeDepth : function(command, ui, value) {
\r
13470 dom.getParent(selection.getNode(), function(node) {
\r
13471 if (node.nodeType == 1 && counter++ == value) {
\r
13472 selection.select(node);
\r
13475 }, editor.getBody());
\r
13478 mceSelectNode : function(command, ui, value) {
\r
13479 selection.select(value);
\r
13482 mceInsertContent : function(command, ui, value) {
\r
13483 var parser, serializer, parentNode, rootNode, fragment, args,
\r
13484 marker, nodeRect, viewPortRect, rng, node, node2, bookmarkHtml, viewportBodyElement;
\r
13486 //selection.normalize();
\r
13488 // Setup parser and serializer
\r
13489 parser = editor.parser;
\r
13490 serializer = new tinymce.html.Serializer({}, editor.schema);
\r
13491 bookmarkHtml = '<span id="mce_marker" data-mce-type="bookmark">\uFEFF</span>';
\r
13493 // Run beforeSetContent handlers on the HTML to be inserted
\r
13494 args = {content: value, format: 'html'};
\r
13495 selection.onBeforeSetContent.dispatch(selection, args);
\r
13496 value = args.content;
\r
13498 // Add caret at end of contents if it's missing
\r
13499 if (value.indexOf('{$caret}') == -1)
\r
13500 value += '{$caret}';
\r
13502 // Replace the caret marker with a span bookmark element
\r
13503 value = value.replace(/\{\$caret\}/, bookmarkHtml);
\r
13505 // Insert node maker where we will insert the new HTML and get it's parent
\r
13506 if (!selection.isCollapsed())
\r
13507 editor.getDoc().execCommand('Delete', false, null);
\r
13509 parentNode = selection.getNode();
\r
13511 // Parse the fragment within the context of the parent node
\r
13512 args = {context : parentNode.nodeName.toLowerCase()};
\r
13513 fragment = parser.parse(value, args);
\r
13515 // Move the caret to a more suitable location
\r
13516 node = fragment.lastChild;
\r
13517 if (node.attr('id') == 'mce_marker') {
\r
13520 for (node = node.prev; node; node = node.walk(true)) {
\r
13521 if (node.type == 3 || !dom.isBlock(node.name)) {
\r
13522 node.parent.insert(marker, node, node.name === 'br');
\r
13528 // If parser says valid we can insert the contents into that parent
\r
13529 if (!args.invalid) {
\r
13530 value = serializer.serialize(fragment);
\r
13532 // Check if parent is empty or only has one BR element then set the innerHTML of that parent
\r
13533 node = parentNode.firstChild;
\r
13534 node2 = parentNode.lastChild;
\r
13535 if (!node || (node === node2 && node.nodeName === 'BR'))
\r
13536 dom.setHTML(parentNode, value);
\r
13538 selection.setContent(value);
\r
13540 // If the fragment was invalid within that context then we need
\r
13541 // to parse and process the parent it's inserted into
\r
13543 // Insert bookmark node and get the parent
\r
13544 selection.setContent(bookmarkHtml);
\r
13545 parentNode = editor.selection.getNode();
\r
13546 rootNode = editor.getBody();
\r
13548 // Opera will return the document node when selection is in root
\r
13549 if (parentNode.nodeType == 9)
\r
13550 parentNode = node = rootNode;
\r
13552 node = parentNode;
\r
13554 // Find the ancestor just before the root element
\r
13555 while (node !== rootNode) {
\r
13556 parentNode = node;
\r
13557 node = node.parentNode;
\r
13560 // Get the outer/inner HTML depending on if we are in the root and parser and serialize that
\r
13561 value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode);
\r
13562 value = serializer.serialize(
\r
13564 // Need to replace by using a function since $ in the contents would otherwise be a problem
\r
13565 value.replace(/<span (id="mce_marker"|id=mce_marker).+?<\/span>/i, function() {
\r
13566 return serializer.serialize(fragment);
\r
13571 // Set the inner/outer HTML depending on if we are in the root or not
\r
13572 if (parentNode == rootNode)
\r
13573 dom.setHTML(rootNode, value);
\r
13575 dom.setOuterHTML(parentNode, value);
\r
13578 marker = dom.get('mce_marker');
\r
13580 // Scroll range into view scrollIntoView on element can't be used since it will scroll the main view port as well
\r
13581 nodeRect = dom.getRect(marker);
\r
13582 viewPortRect = dom.getViewPort(editor.getWin());
\r
13584 // Check if node is out side the viewport if it is then scroll to it
\r
13585 if ((nodeRect.y + nodeRect.h > viewPortRect.y + viewPortRect.h || nodeRect.y < viewPortRect.y) ||
\r
13586 (nodeRect.x > viewPortRect.x + viewPortRect.w || nodeRect.x < viewPortRect.x)) {
\r
13587 viewportBodyElement = tinymce.isIE ? editor.getDoc().documentElement : editor.getBody();
\r
13588 viewportBodyElement.scrollLeft = nodeRect.x;
\r
13589 viewportBodyElement.scrollTop = nodeRect.y - viewPortRect.h + 25;
\r
13592 // Move selection before marker and remove it
\r
13593 rng = dom.createRng();
\r
13595 // If previous sibling is a text node set the selection to the end of that node
\r
13596 node = marker.previousSibling;
\r
13597 if (node && node.nodeType == 3) {
\r
13598 rng.setStart(node, node.nodeValue.length);
\r
13600 // If the previous sibling isn't a text node or doesn't exist set the selection before the marker node
\r
13601 rng.setStartBefore(marker);
\r
13602 rng.setEndBefore(marker);
\r
13605 // Remove the marker node and set the new range
\r
13606 dom.remove(marker);
\r
13607 selection.setRng(rng);
\r
13609 // Dispatch after event and add any visual elements needed
\r
13610 selection.onSetContent.dispatch(selection, args);
\r
13611 editor.addVisual();
\r
13614 mceInsertRawHTML : function(command, ui, value) {
\r
13615 selection.setContent('tiny_mce_marker');
\r
13616 editor.setContent(editor.getContent().replace(/tiny_mce_marker/g, function() { return value }));
\r
13619 mceSetContent : function(command, ui, value) {
\r
13620 editor.setContent(value);
\r
13623 'Indent,Outdent' : function(command) {
\r
13624 var intentValue, indentUnit, value;
\r
13626 // Setup indent level
\r
13627 intentValue = settings.indentation;
\r
13628 indentUnit = /[a-z%]+$/i.exec(intentValue);
\r
13629 intentValue = parseInt(intentValue);
\r
13631 if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) {
\r
13632 each(selection.getSelectedBlocks(), function(element) {
\r
13633 if (command == 'outdent') {
\r
13634 value = Math.max(0, parseInt(element.style.paddingLeft || 0) - intentValue);
\r
13635 dom.setStyle(element, 'paddingLeft', value ? value + indentUnit : '');
\r
13637 dom.setStyle(element, 'paddingLeft', (parseInt(element.style.paddingLeft || 0) + intentValue) + indentUnit);
\r
13640 execNativeCommand(command);
\r
13643 mceRepaint : function() {
\r
13646 if (tinymce.isGecko) {
\r
13648 storeSelection(TRUE);
\r
13650 if (selection.getSel())
\r
13651 selection.getSel().selectAllChildren(editor.getBody());
\r
13653 selection.collapse(TRUE);
\r
13654 restoreSelection();
\r
13661 mceToggleFormat : function(command, ui, value) {
\r
13662 formatter.toggle(value);
\r
13665 InsertHorizontalRule : function() {
\r
13666 editor.execCommand('mceInsertContent', false, '<hr />');
\r
13669 mceToggleVisualAid : function() {
\r
13670 editor.hasVisual = !editor.hasVisual;
\r
13671 editor.addVisual();
\r
13674 mceReplaceContent : function(command, ui, value) {
\r
13675 editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({format : 'text'})));
\r
13678 mceInsertLink : function(command, ui, value) {
\r
13681 if (typeof(value) == 'string')
\r
13682 value = {href : value};
\r
13684 anchor = dom.getParent(selection.getNode(), 'a');
\r
13686 // Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here.
\r
13687 value.href = value.href.replace(' ', '%20');
\r
13689 // Remove existing links if there could be child links or that the href isn't specified
\r
13690 if (!anchor || !value.href) {
\r
13691 formatter.remove('link');
\r
13694 // Apply new link to selection
\r
13695 if (value.href) {
\r
13696 formatter.apply('link', value, anchor);
\r
13700 selectAll : function() {
\r
13701 var root = dom.getRoot(), rng = dom.createRng();
\r
13703 rng.setStart(root, 0);
\r
13704 rng.setEnd(root, root.childNodes.length);
\r
13706 editor.selection.setRng(rng);
\r
13710 // Add queryCommandState overrides
\r
13712 // Override justify commands
\r
13713 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) {
\r
13714 var name = 'align' + command.substring(7);
\r
13715 // Use Formatter.matchNode instead of Formatter.match so that we don't match on parent node. This fixes bug where for both left
\r
13716 // and right align buttons can be active. This could occur when selected nodes have align right and the parent has align left.
\r
13717 var nodes = selection.isCollapsed() ? [selection.getNode()] : selection.getSelectedBlocks();
\r
13718 var matches = tinymce.map(nodes, function(node) {
\r
13719 return !!formatter.matchNode(node, name);
\r
13721 return tinymce.inArray(matches, TRUE) !== -1;
\r
13724 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) {
\r
13725 return isFormatMatch(command);
\r
13728 mceBlockQuote : function() {
\r
13729 return isFormatMatch('blockquote');
\r
13732 Outdent : function() {
\r
13735 if (settings.inline_styles) {
\r
13736 if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0)
\r
13739 if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0)
\r
13743 return queryCommandState('InsertUnorderedList') || queryCommandState('InsertOrderedList') || (!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE'));
\r
13746 'InsertUnorderedList,InsertOrderedList' : function(command) {
\r
13747 return dom.getParent(selection.getNode(), command == 'insertunorderedlist' ? 'UL' : 'OL');
\r
13751 // Add queryCommandValue overrides
\r
13753 'FontSize,FontName' : function(command) {
\r
13754 var value = 0, parent;
\r
13756 if (parent = dom.getParent(selection.getNode(), 'span')) {
\r
13757 if (command == 'fontsize')
\r
13758 value = parent.style.fontSize;
\r
13760 value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase();
\r
13767 // Add undo manager logic
\r
13768 if (settings.custom_undo_redo) {
\r
13770 Undo : function() {
\r
13771 editor.undoManager.undo();
\r
13774 Redo : function() {
\r
13775 editor.undoManager.redo();
\r
13782 (function(tinymce) {
\r
13783 var Dispatcher = tinymce.util.Dispatcher;
\r
13785 tinymce.UndoManager = function(editor) {
\r
13786 var self, index = 0, data = [], beforeBookmark;
\r
13788 function getContent() {
\r
13789 return tinymce.trim(editor.getContent({format : 'raw', no_events : 1}));
\r
13795 onAdd : new Dispatcher(self),
\r
13797 onUndo : new Dispatcher(self),
\r
13799 onRedo : new Dispatcher(self),
\r
13801 beforeChange : function() {
\r
13802 beforeBookmark = editor.selection.getBookmark(2, true);
\r
13805 add : function(level) {
\r
13806 var i, settings = editor.settings, lastLevel;
\r
13808 level = level || {};
\r
13809 level.content = getContent();
\r
13811 // Add undo level if needed
\r
13812 lastLevel = data[index];
\r
13813 if (lastLevel && lastLevel.content == level.content)
\r
13816 // Set before bookmark on previous level
\r
13818 data[index].beforeBookmark = beforeBookmark;
\r
13820 // Time to compress
\r
13821 if (settings.custom_undo_redo_levels) {
\r
13822 if (data.length > settings.custom_undo_redo_levels) {
\r
13823 for (i = 0; i < data.length - 1; i++)
\r
13824 data[i] = data[i + 1];
\r
13827 index = data.length;
\r
13831 // Get a non intrusive normalized bookmark
\r
13832 level.bookmark = editor.selection.getBookmark(2, true);
\r
13834 // Crop array if needed
\r
13835 if (index < data.length - 1)
\r
13836 data.length = index + 1;
\r
13838 data.push(level);
\r
13839 index = data.length - 1;
\r
13841 self.onAdd.dispatch(self, level);
\r
13842 editor.isNotDirty = 0;
\r
13847 undo : function() {
\r
13850 if (self.typing) {
\r
13852 self.typing = false;
\r
13856 level = data[--index];
\r
13858 editor.setContent(level.content, {format : 'raw'});
\r
13859 editor.selection.moveToBookmark(level.beforeBookmark);
\r
13861 self.onUndo.dispatch(self, level);
\r
13867 redo : function() {
\r
13870 if (index < data.length - 1) {
\r
13871 level = data[++index];
\r
13873 editor.setContent(level.content, {format : 'raw'});
\r
13874 editor.selection.moveToBookmark(level.bookmark);
\r
13876 self.onRedo.dispatch(self, level);
\r
13882 clear : function() {
\r
13885 self.typing = false;
\r
13888 hasUndo : function() {
\r
13889 return index > 0 || this.typing;
\r
13892 hasRedo : function() {
\r
13893 return index < data.length - 1 && !this.typing;
\r
13899 (function(tinymce) {
\r
13901 var Event = tinymce.dom.Event,
\r
13902 isIE = tinymce.isIE,
\r
13903 isGecko = tinymce.isGecko,
\r
13904 isOpera = tinymce.isOpera,
\r
13905 each = tinymce.each,
\r
13906 extend = tinymce.extend,
\r
13910 function cloneFormats(node) {
\r
13911 var clone, temp, inner;
\r
13914 if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U)$/.test(node.nodeName)) {
\r
13916 temp = node.cloneNode(false);
\r
13917 temp.appendChild(clone);
\r
13920 clone = inner = node.cloneNode(false);
\r
13923 clone.removeAttribute('id');
\r
13925 } while (node = node.parentNode);
\r
13928 return {wrapper : clone, inner : inner};
\r
13931 // Checks if the selection/caret is at the end of the specified block element
\r
13932 function isAtEnd(rng, par) {
\r
13933 var rng2 = par.ownerDocument.createRange();
\r
13935 rng2.setStart(rng.endContainer, rng.endOffset);
\r
13936 rng2.setEndAfter(par);
\r
13938 // 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
13939 return rng2.cloneContents().textContent.length == 0;
\r
13942 function splitList(selection, dom, li) {
\r
13943 var listBlock, block;
\r
13945 if (dom.isEmpty(li)) {
\r
13946 listBlock = dom.getParent(li, 'ul,ol');
\r
13948 if (!dom.getParent(listBlock.parentNode, 'ul,ol')) {
\r
13949 dom.split(listBlock, li);
\r
13950 block = dom.create('p', 0, '<br data-mce-bogus="1" />');
\r
13951 dom.replace(block, li);
\r
13952 selection.select(block, 1);
\r
13961 tinymce.create('tinymce.ForceBlocks', {
\r
13962 ForceBlocks : function(ed) {
\r
13963 var t = this, s = ed.settings, elm;
\r
13967 elm = (s.forced_root_block || 'p').toLowerCase();
\r
13968 s.element = elm.toUpperCase();
\r
13970 ed.onPreInit.add(t.setup, t);
\r
13973 setup : function() {
\r
13974 var t = this, ed = t.editor, s = ed.settings, dom = ed.dom, selection = ed.selection, blockElements = ed.schema.getBlockElements();
\r
13976 // Force root blocks
\r
13977 if (s.forced_root_block) {
\r
13978 function addRootBlocks() {
\r
13979 var node = selection.getStart(), rootNode = ed.getBody(), rng, startContainer, startOffset, endContainer, endOffset, rootBlockNode, tempNode, offset = -0xFFFFFF;
\r
13981 if (!node || node.nodeType !== 1)
\r
13984 // Check if node is wrapped in block
\r
13985 while (node != rootNode) {
\r
13986 if (blockElements[node.nodeName])
\r
13989 node = node.parentNode;
\r
13992 // Get current selection
\r
13993 rng = selection.getRng();
\r
13994 if (rng.setStart) {
\r
13995 startContainer = rng.startContainer;
\r
13996 startOffset = rng.startOffset;
\r
13997 endContainer = rng.endContainer;
\r
13998 endOffset = rng.endOffset;
\r
14000 // Force control range into text range
\r
14002 rng = ed.getDoc().body.createTextRange();
\r
14003 rng.moveToElementText(rng.item(0));
\r
14006 tmpRng = rng.duplicate();
\r
14007 tmpRng.collapse(true);
\r
14008 startOffset = tmpRng.move('character', offset) * -1;
\r
14010 if (!tmpRng.collapsed) {
\r
14011 tmpRng = rng.duplicate();
\r
14012 tmpRng.collapse(false);
\r
14013 endOffset = (tmpRng.move('character', offset) * -1) - startOffset;
\r
14017 // Wrap non block elements and text nodes
\r
14018 for (node = rootNode.firstChild; node; node) {
\r
14019 if (node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName])) {
\r
14020 if (!rootBlockNode) {
\r
14021 rootBlockNode = dom.create(s.forced_root_block);
\r
14022 node.parentNode.insertBefore(rootBlockNode, node);
\r
14026 node = node.nextSibling;
\r
14027 rootBlockNode.appendChild(tempNode);
\r
14029 rootBlockNode = null;
\r
14030 node = node.nextSibling;
\r
14034 if (rng.setStart) {
\r
14035 rng.setStart(startContainer, startOffset);
\r
14036 rng.setEnd(endContainer, endOffset);
\r
14037 selection.setRng(rng);
\r
14040 rng = ed.getDoc().body.createTextRange();
\r
14041 rng.moveToElementText(rootNode);
\r
14042 rng.collapse(true);
\r
14043 rng.moveStart('character', startOffset);
\r
14045 if (endOffset > 0)
\r
14046 rng.moveEnd('character', endOffset);
\r
14054 ed.nodeChanged();
\r
14057 ed.onKeyUp.add(addRootBlocks);
\r
14058 ed.onClick.add(addRootBlocks);
\r
14061 if (s.force_br_newlines) {
\r
14062 // Force IE to produce BRs on enter
\r
14064 ed.onKeyPress.add(function(ed, e) {
\r
14067 if (e.keyCode == 13 && selection.getNode().nodeName != 'LI') {
\r
14068 selection.setContent('<br id="__" /> ', {format : 'raw'});
\r
14069 n = dom.get('__');
\r
14070 n.removeAttribute('id');
\r
14071 selection.select(n);
\r
14072 selection.collapse();
\r
14073 return Event.cancel(e);
\r
14079 if (s.force_p_newlines) {
\r
14081 ed.onKeyPress.add(function(ed, e) {
\r
14082 if (e.keyCode == 13 && !e.shiftKey && !t.insertPara(e))
\r
14086 // Ungly hack to for IE to preserve the formatting when you press
\r
14087 // enter at the end of a block element with formatted contents
\r
14088 // This logic overrides the browsers default logic with
\r
14089 // custom logic that enables us to control the output
\r
14090 tinymce.addUnload(function() {
\r
14091 t._previousFormats = 0; // Fix IE leak
\r
14094 ed.onKeyPress.add(function(ed, e) {
\r
14095 t._previousFormats = 0;
\r
14097 // Clone the current formats, this will later be applied to the new block contents
\r
14098 if (e.keyCode == 13 && !e.shiftKey && ed.selection.isCollapsed() && s.keep_styles)
\r
14099 t._previousFormats = cloneFormats(ed.selection.getStart());
\r
14102 ed.onKeyUp.add(function(ed, e) {
\r
14103 // Let IE break the element and the wrap the new caret location in the previous formats
\r
14104 if (e.keyCode == 13 && !e.shiftKey) {
\r
14105 var parent = ed.selection.getStart(), fmt = t._previousFormats;
\r
14107 // Parent is an empty block
\r
14108 if (!parent.hasChildNodes() && fmt) {
\r
14109 parent = dom.getParent(parent, dom.isBlock);
\r
14111 if (parent && parent.nodeName != 'LI') {
\r
14112 parent.innerHTML = '';
\r
14114 if (t._previousFormats) {
\r
14115 parent.appendChild(fmt.wrapper);
\r
14116 fmt.inner.innerHTML = '\uFEFF';
\r
14118 parent.innerHTML = '\uFEFF';
\r
14120 selection.select(parent, 1);
\r
14121 selection.collapse(true);
\r
14122 ed.getDoc().execCommand('Delete', false, null);
\r
14123 t._previousFormats = 0;
\r
14131 ed.onKeyDown.add(function(ed, e) {
\r
14132 if ((e.keyCode == 8 || e.keyCode == 46) && !e.shiftKey)
\r
14133 t.backspaceDelete(e, e.keyCode == 8);
\r
14138 // Workaround for missing shift+enter support, http://bugs.webkit.org/show_bug.cgi?id=16973
\r
14139 if (tinymce.isWebKit) {
\r
14140 function insertBr(ed) {
\r
14141 var rng = selection.getRng(), br, div = dom.create('div', null, ' '), divYPos, vpHeight = dom.getViewPort(ed.getWin()).h;
\r
14143 // Insert BR element
\r
14144 rng.insertNode(br = dom.create('br'));
\r
14146 // Place caret after BR
\r
14147 rng.setStartAfter(br);
\r
14148 rng.setEndAfter(br);
\r
14149 selection.setRng(rng);
\r
14151 // Could not place caret after BR then insert an nbsp entity and move the caret
\r
14152 if (selection.getSel().focusNode == br.previousSibling) {
\r
14153 selection.select(dom.insertAfter(dom.doc.createTextNode('\u00a0'), br));
\r
14154 selection.collapse(TRUE);
\r
14157 // Create a temporary DIV after the BR and get the position as it
\r
14158 // seems like getPos() returns 0 for text nodes and BR elements.
\r
14159 dom.insertAfter(div, br);
\r
14160 divYPos = dom.getPos(div).y;
\r
14163 // Scroll to new position, scrollIntoView can't be used due to bug: http://bugs.webkit.org/show_bug.cgi?id=16117
\r
14164 if (divYPos > vpHeight) // It is not necessary to scroll if the DIV is inside the view port.
\r
14165 ed.getWin().scrollTo(0, divYPos);
\r
14168 ed.onKeyPress.add(function(ed, e) {
\r
14169 if (e.keyCode == 13 && (e.shiftKey || (s.force_br_newlines && !dom.getParent(selection.getNode(), 'h1,h2,h3,h4,h5,h6,ol,ul')))) {
\r
14176 // IE specific fixes
\r
14178 // Replaces IE:s auto generated paragraphs with the specified element name
\r
14179 if (s.element != 'P') {
\r
14180 ed.onKeyPress.add(function(ed, e) {
\r
14181 t.lastElm = selection.getNode().nodeName;
\r
14184 ed.onKeyUp.add(function(ed, e) {
\r
14185 var bl, n = selection.getNode(), b = ed.getBody();
\r
14187 if (b.childNodes.length === 1 && n.nodeName == 'P') {
\r
14188 n = dom.rename(n, s.element);
\r
14189 selection.select(n);
\r
14190 selection.collapse();
\r
14191 ed.nodeChanged();
\r
14192 } else if (e.keyCode == 13 && !e.shiftKey && t.lastElm != 'P') {
\r
14193 bl = dom.getParent(n, 'p');
\r
14196 dom.rename(bl, s.element);
\r
14197 ed.nodeChanged();
\r
14205 getParentBlock : function(n) {
\r
14206 var d = this.dom;
\r
14208 return d.getParent(n, d.isBlock);
\r
14211 insertPara : function(e) {
\r
14212 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
14213 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
14215 ed.undoManager.beforeChange();
\r
14217 // If root blocks are forced then use Operas default behavior since it's really good
\r
14218 // Removed due to bug: #1853816
\r
14219 // if (se.forced_root_block && isOpera)
\r
14222 // Setup before range
\r
14223 rb = d.createRange();
\r
14225 // If is before the first block element and in body, then move it into first block element
\r
14226 rb.setStart(s.anchorNode, s.anchorOffset);
\r
14227 rb.collapse(TRUE);
\r
14229 // Setup after range
\r
14230 ra = d.createRange();
\r
14232 // If is before the first block element and in body, then move it into first block element
\r
14233 ra.setStart(s.focusNode, s.focusOffset);
\r
14234 ra.collapse(TRUE);
\r
14236 // Setup start/end points
\r
14237 dir = rb.compareBoundaryPoints(rb.START_TO_END, ra) < 0;
\r
14238 sn = dir ? s.anchorNode : s.focusNode;
\r
14239 so = dir ? s.anchorOffset : s.focusOffset;
\r
14240 en = dir ? s.focusNode : s.anchorNode;
\r
14241 eo = dir ? s.focusOffset : s.anchorOffset;
\r
14243 // If selection is in empty table cell
\r
14244 if (sn === en && /^(TD|TH)$/.test(sn.nodeName)) {
\r
14245 if (sn.firstChild.nodeName == 'BR')
\r
14246 dom.remove(sn.firstChild); // Remove BR
\r
14248 // Create two new block elements
\r
14249 if (sn.childNodes.length == 0) {
\r
14250 ed.dom.add(sn, se.element, null, '<br />');
\r
14251 aft = ed.dom.add(sn, se.element, null, '<br />');
\r
14253 n = sn.innerHTML;
\r
14254 sn.innerHTML = '';
\r
14255 ed.dom.add(sn, se.element, null, n);
\r
14256 aft = ed.dom.add(sn, se.element, null, '<br />');
\r
14259 // Move caret into the last one
\r
14260 r = d.createRange();
\r
14261 r.selectNodeContents(aft);
\r
14263 ed.selection.setRng(r);
\r
14268 // If the caret is in an invalid location in FF we need to move it into the first block
\r
14269 if (sn == b && en == b && b.firstChild && ed.dom.isBlock(b.firstChild)) {
\r
14270 sn = en = sn.firstChild;
\r
14272 rb = d.createRange();
\r
14273 rb.setStart(sn, 0);
\r
14274 ra = d.createRange();
\r
14275 ra.setStart(en, 0);
\r
14278 // If the body is totally empty add a BR element this might happen on webkit
\r
14279 if (!d.body.hasChildNodes()) {
\r
14280 d.body.appendChild(dom.create('br'));
\r
14283 // Never use body as start or end node
\r
14284 sn = sn.nodeName == "HTML" ? d.body : sn; // Fix for Opera bug: https://bugs.opera.com/show_bug.cgi?id=273224&comments=yes
\r
14285 sn = sn.nodeName == "BODY" ? sn.firstChild : sn;
\r
14286 en = en.nodeName == "HTML" ? d.body : en; // Fix for Opera bug: https://bugs.opera.com/show_bug.cgi?id=273224&comments=yes
\r
14287 en = en.nodeName == "BODY" ? en.firstChild : en;
\r
14289 // Get start and end blocks
\r
14290 sb = t.getParentBlock(sn);
\r
14291 eb = t.getParentBlock(en);
\r
14292 bn = sb ? sb.nodeName : se.element; // Get block name to create
\r
14294 // Return inside list use default browser behavior
\r
14295 if (n = t.dom.getParent(sb, 'li,pre')) {
\r
14296 if (n.nodeName == 'LI')
\r
14297 return splitList(ed.selection, t.dom, n);
\r
14302 // If caption or absolute layers then always generate new blocks within
\r
14303 if (sb && (sb.nodeName == 'CAPTION' || /absolute|relative|fixed/gi.test(dom.getStyle(sb, 'position', 1)))) {
\r
14308 // If caption or absolute layers then always generate new blocks within
\r
14309 if (eb && (eb.nodeName == 'CAPTION' || /absolute|relative|fixed/gi.test(dom.getStyle(sb, 'position', 1)))) {
\r
14315 if (/(TD|TABLE|TH|CAPTION)/.test(bn) || (sb && bn == "DIV" && /left|right/gi.test(dom.getStyle(sb, 'float', 1)))) {
\r
14320 // Setup new before and after blocks
\r
14321 bef = (sb && sb.nodeName == bn) ? sb.cloneNode(0) : ed.dom.create(bn);
\r
14322 aft = (eb && eb.nodeName == bn) ? eb.cloneNode(0) : ed.dom.create(bn);
\r
14324 // Remove id from after clone
\r
14325 aft.removeAttribute('id');
\r
14327 // Is header and cursor is at the end, then force paragraph under
\r
14328 if (/^(H[1-6])$/.test(bn) && isAtEnd(r, sb))
\r
14329 aft = ed.dom.create(se.element);
\r
14331 // Find start chop node
\r
14334 if (n == b || n.nodeType == 9 || t.dom.isBlock(n) || /(TD|TABLE|TH|CAPTION)/.test(n.nodeName))
\r
14338 } while ((n = n.previousSibling ? n.previousSibling : n.parentNode));
\r
14340 // Find end chop node
\r
14343 if (n == b || n.nodeType == 9 || t.dom.isBlock(n) || /(TD|TABLE|TH|CAPTION)/.test(n.nodeName))
\r
14347 } while ((n = n.nextSibling ? n.nextSibling : n.parentNode));
\r
14349 // Place first chop part into before block element
\r
14350 if (sc.nodeName == bn)
\r
14351 rb.setStart(sc, 0);
\r
14353 rb.setStartBefore(sc);
\r
14355 rb.setEnd(sn, so);
\r
14356 bef.appendChild(rb.cloneContents() || d.createTextNode('')); // Empty text node needed for Safari
\r
14358 // Place secnd chop part within new block element
\r
14360 ra.setEndAfter(ec);
\r
14362 //console.debug(s.focusNode, s.focusOffset);
\r
14365 ra.setStart(en, eo);
\r
14366 aft.appendChild(ra.cloneContents() || d.createTextNode('')); // Empty text node needed for Safari
\r
14368 // Create range around everything
\r
14369 r = d.createRange();
\r
14370 if (!sc.previousSibling && sc.parentNode.nodeName == bn) {
\r
14371 r.setStartBefore(sc.parentNode);
\r
14373 if (rb.startContainer.nodeName == bn && rb.startOffset == 0)
\r
14374 r.setStartBefore(rb.startContainer);
\r
14376 r.setStart(rb.startContainer, rb.startOffset);
\r
14379 if (!ec.nextSibling && ec.parentNode.nodeName == bn)
\r
14380 r.setEndAfter(ec.parentNode);
\r
14382 r.setEnd(ra.endContainer, ra.endOffset);
\r
14384 // Delete and replace it with new block elements
\r
14385 r.deleteContents();
\r
14388 ed.getWin().scrollTo(0, vp.y);
\r
14390 // Never wrap blocks in blocks
\r
14391 if (bef.firstChild && bef.firstChild.nodeName == bn)
\r
14392 bef.innerHTML = bef.firstChild.innerHTML;
\r
14394 if (aft.firstChild && aft.firstChild.nodeName == bn)
\r
14395 aft.innerHTML = aft.firstChild.innerHTML;
\r
14397 function appendStyles(e, en) {
\r
14398 var nl = [], nn, n, i;
\r
14400 e.innerHTML = '';
\r
14402 // Make clones of style elements
\r
14403 if (se.keep_styles) {
\r
14406 // We only want style specific elements
\r
14407 if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U)$/.test(n.nodeName)) {
\r
14408 nn = n.cloneNode(FALSE);
\r
14409 dom.setAttrib(nn, 'id', ''); // Remove ID since it needs to be unique
\r
14412 } while (n = n.parentNode);
\r
14415 // Append style elements to aft
\r
14416 if (nl.length > 0) {
\r
14417 for (i = nl.length - 1, nn = e; i >= 0; i--)
\r
14418 nn = nn.appendChild(nl[i]);
\r
14420 // Padd most inner style element
\r
14421 nl[0].innerHTML = isOpera ? '\u00a0' : '<br />'; // Extra space for Opera so that the caret can move there
\r
14422 return nl[0]; // Move caret to most inner element
\r
14424 e.innerHTML = isOpera ? '\u00a0' : '<br />'; // Extra space for Opera so that the caret can move there
\r
14427 // Padd empty blocks
\r
14428 if (dom.isEmpty(bef))
\r
14429 appendStyles(bef, sn);
\r
14431 // Fill empty afterblook with current style
\r
14432 if (dom.isEmpty(aft))
\r
14433 car = appendStyles(aft, en);
\r
14435 // Opera needs this one backwards for older versions
\r
14436 if (isOpera && parseFloat(opera.version()) < 9.5) {
\r
14437 r.insertNode(bef);
\r
14438 r.insertNode(aft);
\r
14440 r.insertNode(aft);
\r
14441 r.insertNode(bef);
\r
14448 // Move cursor and scroll into view
\r
14449 ed.selection.select(aft, true);
\r
14450 ed.selection.collapse(true);
\r
14452 // 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
14453 y = ed.dom.getPos(aft).y;
\r
14454 //ch = aft.clientHeight;
\r
14456 // Is element within viewport
\r
14457 if (y < vp.y || y + 25 > vp.y + vp.h) {
\r
14458 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
14461 'Element: y=' + y + ', h=' + ch + ', ' +
\r
14462 'Viewport: y=' + vp.y + ", h=" + vp.h + ', bottom=' + (vp.y + vp.h)
\r
14466 ed.undoManager.add();
\r
14471 backspaceDelete : function(e, bs) {
\r
14472 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
14474 // Delete when caret is behind a element doesn't work correctly on Gecko see #3011651
\r
14475 if (!bs && r.collapsed && sc.nodeType == 1 && r.startOffset == sc.childNodes.length) {
\r
14476 walker = new tinymce.dom.TreeWalker(sc.lastChild, sc);
\r
14478 // Walk the dom backwards until we find a text node
\r
14479 for (n = sc.lastChild; n; n = walker.prev()) {
\r
14480 if (n.nodeType == 3) {
\r
14481 r.setStart(n, n.nodeValue.length);
\r
14482 r.collapse(true);
\r
14489 // The caret sometimes gets stuck in Gecko if you delete empty paragraphs
\r
14490 // This workaround removes the element by hand and moves the caret to the previous element
\r
14491 if (sc && ed.dom.isBlock(sc) && !/^(TD|TH)$/.test(sc.nodeName) && bs) {
\r
14492 if (sc.childNodes.length == 0 || (sc.childNodes.length == 1 && sc.firstChild.nodeName == 'BR')) {
\r
14493 // Find previous block element
\r
14495 while ((n = n.previousSibling) && !ed.dom.isBlock(n)) ;
\r
14498 if (sc != b.firstChild) {
\r
14499 // Find last text node
\r
14500 w = ed.dom.doc.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, FALSE);
\r
14501 while (tn = w.nextNode())
\r
14504 // Place caret at the end of last text node
\r
14505 r = ed.getDoc().createRange();
\r
14506 r.setStart(n, n.nodeValue ? n.nodeValue.length : 0);
\r
14507 r.setEnd(n, n.nodeValue ? n.nodeValue.length : 0);
\r
14510 // Remove the target container
\r
14511 ed.dom.remove(sc);
\r
14514 return Event.cancel(e);
\r
14522 (function(tinymce) {
\r
14524 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, extend = tinymce.extend;
\r
14526 tinymce.create('tinymce.ControlManager', {
\r
14527 ControlManager : function(ed, s) {
\r
14533 t.onAdd = new tinymce.util.Dispatcher(t);
\r
14534 t.onPostRender = new tinymce.util.Dispatcher(t);
\r
14535 t.prefix = s.prefix || ed.id + '_';
\r
14538 t.onPostRender.add(function() {
\r
14539 each(t.controls, function(c) {
\r
14545 get : function(id) {
\r
14546 return this.controls[this.prefix + id] || this.controls[id];
\r
14549 setActive : function(id, s) {
\r
14552 if (c = this.get(id))
\r
14558 setDisabled : function(id, s) {
\r
14561 if (c = this.get(id))
\r
14562 c.setDisabled(s);
\r
14567 add : function(c) {
\r
14571 t.controls[c.id] = c;
\r
14572 t.onAdd.dispatch(c, t);
\r
14578 createControl : function(n) {
\r
14579 var c, t = this, ed = t.editor;
\r
14581 each(ed.plugins, function(p) {
\r
14582 if (p.createControl) {
\r
14583 c = p.createControl(n, t);
\r
14592 case "separator":
\r
14593 return t.createSeparator();
\r
14596 if (!c && ed.buttons && (c = ed.buttons[n]))
\r
14597 return t.createButton(n, c);
\r
14602 createDropMenu : function(id, s, cc) {
\r
14603 var t = this, ed = t.editor, c, bm, v, cls;
\r
14606 'class' : 'mceDropDown',
\r
14607 constrain : ed.settings.constrain_menus
\r
14610 s['class'] = s['class'] + ' ' + ed.getParam('skin') + 'Skin';
\r
14611 if (v = ed.getParam('skin_variant'))
\r
14612 s['class'] += ' ' + ed.getParam('skin') + 'Skin' + v.substring(0, 1).toUpperCase() + v.substring(1);
\r
14614 id = t.prefix + id;
\r
14615 cls = cc || t._cls.dropmenu || tinymce.ui.DropMenu;
\r
14616 c = t.controls[id] = new cls(id, s);
\r
14617 c.onAddItem.add(function(c, o) {
\r
14618 var s = o.settings;
\r
14620 s.title = ed.getLang(s.title, s.title);
\r
14622 if (!s.onclick) {
\r
14623 s.onclick = function(v) {
\r
14625 ed.execCommand(s.cmd, s.ui || false, s.value);
\r
14630 ed.onRemove.add(function() {
\r
14634 // Fix for bug #1897785, #1898007
\r
14635 if (tinymce.isIE) {
\r
14636 c.onShowMenu.add(function() {
\r
14637 // IE 8 needs focus in order to store away a range with the current collapsed caret location
\r
14640 bm = ed.selection.getBookmark(1);
\r
14643 c.onHideMenu.add(function() {
\r
14645 ed.selection.moveToBookmark(bm);
\r
14654 createListBox : function(id, s, cc) {
\r
14655 var t = this, ed = t.editor, cmd, c, cls;
\r
14660 s.title = ed.translate(s.title);
\r
14661 s.scope = s.scope || ed;
\r
14663 if (!s.onselect) {
\r
14664 s.onselect = function(v) {
\r
14665 ed.execCommand(s.cmd, s.ui || false, v || s.value);
\r
14671 'class' : 'mce_' + id,
\r
14673 control_manager : t
\r
14676 id = t.prefix + id;
\r
14679 function useNativeListForAccessibility(ed) {
\r
14680 return ed.settings.use_accessible_selects && !tinymce.isGecko
\r
14683 if (ed.settings.use_native_selects || useNativeListForAccessibility(ed))
\r
14684 c = new tinymce.ui.NativeListBox(id, s);
\r
14686 cls = cc || t._cls.listbox || tinymce.ui.ListBox;
\r
14687 c = new cls(id, s, ed);
\r
14690 t.controls[id] = c;
\r
14692 // Fix focus problem in Safari
\r
14693 if (tinymce.isWebKit) {
\r
14694 c.onPostRender.add(function(c, n) {
\r
14695 // Store bookmark on mousedown
\r
14696 Event.add(n, 'mousedown', function() {
\r
14697 ed.bookmark = ed.selection.getBookmark(1);
\r
14700 // Restore on focus, since it might be lost
\r
14701 Event.add(n, 'focus', function() {
\r
14702 ed.selection.moveToBookmark(ed.bookmark);
\r
14703 ed.bookmark = null;
\r
14709 ed.onMouseDown.add(c.hideMenu, c);
\r
14714 createButton : function(id, s, cc) {
\r
14715 var t = this, ed = t.editor, o, c, cls;
\r
14720 s.title = ed.translate(s.title);
\r
14721 s.label = ed.translate(s.label);
\r
14722 s.scope = s.scope || ed;
\r
14724 if (!s.onclick && !s.menu_button) {
\r
14725 s.onclick = function() {
\r
14726 ed.execCommand(s.cmd, s.ui || false, s.value);
\r
14732 'class' : 'mce_' + id,
\r
14733 unavailable_prefix : ed.getLang('unavailable', ''),
\r
14735 control_manager : t
\r
14738 id = t.prefix + id;
\r
14740 if (s.menu_button) {
\r
14741 cls = cc || t._cls.menubutton || tinymce.ui.MenuButton;
\r
14742 c = new cls(id, s, ed);
\r
14743 ed.onMouseDown.add(c.hideMenu, c);
\r
14745 cls = t._cls.button || tinymce.ui.Button;
\r
14746 c = new cls(id, s, ed);
\r
14752 createMenuButton : function(id, s, cc) {
\r
14754 s.menu_button = 1;
\r
14756 return this.createButton(id, s, cc);
\r
14759 createSplitButton : function(id, s, cc) {
\r
14760 var t = this, ed = t.editor, cmd, c, cls;
\r
14765 s.title = ed.translate(s.title);
\r
14766 s.scope = s.scope || ed;
\r
14768 if (!s.onclick) {
\r
14769 s.onclick = function(v) {
\r
14770 ed.execCommand(s.cmd, s.ui || false, v || s.value);
\r
14774 if (!s.onselect) {
\r
14775 s.onselect = function(v) {
\r
14776 ed.execCommand(s.cmd, s.ui || false, v || s.value);
\r
14782 'class' : 'mce_' + id,
\r
14784 control_manager : t
\r
14787 id = t.prefix + id;
\r
14788 cls = cc || t._cls.splitbutton || tinymce.ui.SplitButton;
\r
14789 c = t.add(new cls(id, s, ed));
\r
14790 ed.onMouseDown.add(c.hideMenu, c);
\r
14795 createColorSplitButton : function(id, s, cc) {
\r
14796 var t = this, ed = t.editor, cmd, c, cls, bm;
\r
14801 s.title = ed.translate(s.title);
\r
14802 s.scope = s.scope || ed;
\r
14804 if (!s.onclick) {
\r
14805 s.onclick = function(v) {
\r
14806 if (tinymce.isIE)
\r
14807 bm = ed.selection.getBookmark(1);
\r
14809 ed.execCommand(s.cmd, s.ui || false, v || s.value);
\r
14813 if (!s.onselect) {
\r
14814 s.onselect = function(v) {
\r
14815 ed.execCommand(s.cmd, s.ui || false, v || s.value);
\r
14821 'class' : 'mce_' + id,
\r
14822 'menu_class' : ed.getParam('skin') + 'Skin',
\r
14824 more_colors_title : ed.getLang('more_colors')
\r
14827 id = t.prefix + id;
\r
14828 cls = cc || t._cls.colorsplitbutton || tinymce.ui.ColorSplitButton;
\r
14829 c = new cls(id, s, ed);
\r
14830 ed.onMouseDown.add(c.hideMenu, c);
\r
14832 // Remove the menu element when the editor is removed
\r
14833 ed.onRemove.add(function() {
\r
14837 // Fix for bug #1897785, #1898007
\r
14838 if (tinymce.isIE) {
\r
14839 c.onShowMenu.add(function() {
\r
14840 // IE 8 needs focus in order to store away a range with the current collapsed caret location
\r
14842 bm = ed.selection.getBookmark(1);
\r
14845 c.onHideMenu.add(function() {
\r
14847 ed.selection.moveToBookmark(bm);
\r
14856 createToolbar : function(id, s, cc) {
\r
14857 var c, t = this, cls;
\r
14859 id = t.prefix + id;
\r
14860 cls = cc || t._cls.toolbar || tinymce.ui.Toolbar;
\r
14861 c = new cls(id, s, t.editor);
\r
14869 createToolbarGroup : function(id, s, cc) {
\r
14870 var c, t = this, cls;
\r
14871 id = t.prefix + id;
\r
14872 cls = cc || this._cls.toolbarGroup || tinymce.ui.ToolbarGroup;
\r
14873 c = new cls(id, s, t.editor);
\r
14881 createSeparator : function(cc) {
\r
14882 var cls = cc || this._cls.separator || tinymce.ui.Separator;
\r
14884 return new cls();
\r
14887 setControlType : function(n, c) {
\r
14888 return this._cls[n.toLowerCase()] = c;
\r
14891 destroy : function() {
\r
14892 each(this.controls, function(c) {
\r
14896 this.controls = null;
\r
14901 (function(tinymce) {
\r
14902 var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each, isIE = tinymce.isIE, isOpera = tinymce.isOpera;
\r
14904 tinymce.create('tinymce.WindowManager', {
\r
14905 WindowManager : function(ed) {
\r
14909 t.onOpen = new Dispatcher(t);
\r
14910 t.onClose = new Dispatcher(t);
\r
14915 open : function(s, p) {
\r
14916 var t = this, f = '', x, y, mo = t.editor.settings.dialog_type == 'modal', w, sw, sh, vp = tinymce.DOM.getViewPort(), u;
\r
14918 // Default some options
\r
14921 sw = isOpera ? vp.w : screen.width; // Opera uses windows inside the Opera window
\r
14922 sh = isOpera ? vp.h : screen.height;
\r
14923 s.name = s.name || 'mc_' + new Date().getTime();
\r
14924 s.width = parseInt(s.width || 320);
\r
14925 s.height = parseInt(s.height || 240);
\r
14926 s.resizable = true;
\r
14927 s.left = s.left || parseInt(sw / 2.0) - (s.width / 2.0);
\r
14928 s.top = s.top || parseInt(sh / 2.0) - (s.height / 2.0);
\r
14929 p.inline = false;
\r
14930 p.mce_width = s.width;
\r
14931 p.mce_height = s.height;
\r
14932 p.mce_auto_focus = s.auto_focus;
\r
14938 s.dialogWidth = s.width + 'px';
\r
14939 s.dialogHeight = s.height + 'px';
\r
14940 s.scroll = s.scrollbars || false;
\r
14944 // Build features string
\r
14945 each(s, function(v, k) {
\r
14946 if (tinymce.is(v, 'boolean'))
\r
14947 v = v ? 'yes' : 'no';
\r
14949 if (!/^(name|url)$/.test(k)) {
\r
14951 f += (f ? ';' : '') + k + ':' + v;
\r
14953 f += (f ? ',' : '') + k + '=' + v;
\r
14959 t.onOpen.dispatch(t, s, p);
\r
14961 u = s.url || s.file;
\r
14962 u = tinymce._addVer(u);
\r
14965 if (isIE && mo) {
\r
14967 window.showModalDialog(u, window, f);
\r
14969 w = window.open(u, s.name, f);
\r
14975 alert(t.editor.getLang('popup_blocked'));
\r
14978 close : function(w) {
\r
14980 this.onClose.dispatch(this);
\r
14983 createInstance : function(cl, a, b, c, d, e) {
\r
14984 var f = tinymce.resolve(cl);
\r
14986 return new f(a, b, c, d, e);
\r
14989 confirm : function(t, cb, s, w) {
\r
14992 cb.call(s || this, w.confirm(this._decode(this.editor.getLang(t, t))));
\r
14995 alert : function(tx, cb, s, w) {
\r
14999 w.alert(t._decode(t.editor.getLang(tx, tx)));
\r
15005 resizeBy : function(dw, dh, win) {
\r
15006 win.resizeBy(dw, dh);
\r
15009 // Internal functions
\r
15011 _decode : function(s) {
\r
15012 return tinymce.DOM.decode(s).replace(/\\n/g, '\n');
\r
15016 (function(tinymce) {
\r
15017 tinymce.Formatter = function(ed) {
\r
15018 var formats = {},
\r
15019 each = tinymce.each,
\r
15021 selection = ed.selection,
\r
15022 TreeWalker = tinymce.dom.TreeWalker,
\r
15023 rangeUtils = new tinymce.dom.RangeUtils(dom),
\r
15024 isValid = ed.schema.isValidChild,
\r
15025 isBlock = dom.isBlock,
\r
15026 forcedRootBlock = ed.settings.forced_root_block,
\r
15027 nodeIndex = dom.nodeIndex,
\r
15028 INVISIBLE_CHAR = '\uFEFF',
\r
15029 MCE_ATTR_RE = /^(src|href|style)$/,
\r
15034 function isArray(obj) {
\r
15035 return obj instanceof Array;
\r
15038 function getParents(node, selector) {
\r
15039 return dom.getParents(node, selector, dom.getRoot());
\r
15042 function isCaretNode(node) {
\r
15043 return node.nodeType === 1 && node.id === '_mce_caret';
\r
15046 // Public functions
\r
15048 function get(name) {
\r
15049 return name ? formats[name] : formats;
\r
15052 function register(name, format) {
\r
15054 if (typeof(name) !== 'string') {
\r
15055 each(name, function(format, name) {
\r
15056 register(name, format);
\r
15059 // Force format into array and add it to internal collection
\r
15060 format = format.length ? format : [format];
\r
15062 each(format, function(format) {
\r
15063 // Set deep to false by default on selector formats this to avoid removing
\r
15064 // alignment on images inside paragraphs when alignment is changed on paragraphs
\r
15065 if (format.deep === undefined)
\r
15066 format.deep = !format.selector;
\r
15068 // Default to true
\r
15069 if (format.split === undefined)
\r
15070 format.split = !format.selector || format.inline;
\r
15072 // Default to true
\r
15073 if (format.remove === undefined && format.selector && !format.inline)
\r
15074 format.remove = 'none';
\r
15076 // Mark format as a mixed format inline + block level
\r
15077 if (format.selector && format.inline) {
\r
15078 format.mixed = true;
\r
15079 format.block_expand = true;
\r
15082 // Split classes if needed
\r
15083 if (typeof(format.classes) === 'string')
\r
15084 format.classes = format.classes.split(/\s+/);
\r
15087 formats[name] = format;
\r
15092 var getTextDecoration = function(node) {
\r
15095 ed.dom.getParent(node, function(n) {
\r
15096 decoration = ed.dom.getStyle(n, 'text-decoration');
\r
15097 return decoration && decoration !== 'none';
\r
15100 return decoration;
\r
15103 var processUnderlineAndColor = function(node) {
\r
15104 var textDecoration;
\r
15105 if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) {
\r
15106 textDecoration = getTextDecoration(node.parentNode);
\r
15107 if (ed.dom.getStyle(node, 'color') && textDecoration) {
\r
15108 ed.dom.setStyle(node, 'text-decoration', textDecoration);
\r
15109 } else if (ed.dom.getStyle(node, 'textdecoration') === textDecoration) {
\r
15110 ed.dom.setStyle(node, 'text-decoration', null);
\r
15115 function apply(name, vars, node) {
\r
15116 var formatList = get(name), format = formatList[0], bookmark, rng, i, isCollapsed = selection.isCollapsed();
\r
15118 function setElementFormat(elm, fmt) {
\r
15119 fmt = fmt || format;
\r
15122 if (fmt.onformat) {
\r
15123 fmt.onformat(elm, fmt, vars, node);
\r
15126 each(fmt.styles, function(value, name) {
\r
15127 dom.setStyle(elm, name, replaceVars(value, vars));
\r
15130 each(fmt.attributes, function(value, name) {
\r
15131 dom.setAttrib(elm, name, replaceVars(value, vars));
\r
15134 each(fmt.classes, function(value) {
\r
15135 value = replaceVars(value, vars);
\r
15137 if (!dom.hasClass(elm, value))
\r
15138 dom.addClass(elm, value);
\r
15142 function adjustSelectionToVisibleSelection() {
\r
15143 function findSelectionEnd(start, end) {
\r
15144 var walker = new TreeWalker(end);
\r
15145 for (node = walker.current(); node; node = walker.prev()) {
\r
15146 if (node.childNodes.length > 1 || node == start) {
\r
15152 // Adjust selection so that a end container with a end offset of zero is not included in the selection
\r
15153 // as this isn't visible to the user.
\r
15154 var rng = ed.selection.getRng();
\r
15155 var start = rng.startContainer;
\r
15156 var end = rng.endContainer;
\r
15158 if (start != end && rng.endOffset == 0) {
\r
15159 var newEnd = findSelectionEnd(start, end);
\r
15160 var endOffset = newEnd.nodeType == 3 ? newEnd.length : newEnd.childNodes.length;
\r
15162 rng.setEnd(newEnd, endOffset);
\r
15168 function applyStyleToList(node, bookmark, wrapElm, newWrappers, process){
\r
15169 var nodes = [], listIndex = -1, list, startIndex = -1, endIndex = -1, currentWrapElm;
\r
15171 // find the index of the first child list.
\r
15172 each(node.childNodes, function(n, index) {
\r
15173 if (n.nodeName === "UL" || n.nodeName === "OL") {
\r
15174 listIndex = index;
\r
15180 // get the index of the bookmarks
\r
15181 each(node.childNodes, function(n, index) {
\r
15182 if (n.nodeName === "SPAN" && dom.getAttrib(n, "data-mce-type") == "bookmark") {
\r
15183 if (n.id == bookmark.id + "_start") {
\r
15184 startIndex = index;
\r
15185 } else if (n.id == bookmark.id + "_end") {
\r
15186 endIndex = index;
\r
15191 // if the selection spans across an embedded list, or there isn't an embedded list - handle processing normally
\r
15192 if (listIndex <= 0 || (startIndex < listIndex && endIndex > listIndex)) {
\r
15193 each(tinymce.grep(node.childNodes), process);
\r
15196 currentWrapElm = wrapElm.cloneNode(FALSE);
\r
15198 // create a list of the nodes on the same side of the list as the selection
\r
15199 each(tinymce.grep(node.childNodes), function(n, index) {
\r
15200 if ((startIndex < listIndex && index < listIndex) || (startIndex > listIndex && index > listIndex)) {
\r
15202 n.parentNode.removeChild(n);
\r
15206 // insert the wrapping element either before or after the list.
\r
15207 if (startIndex < listIndex) {
\r
15208 node.insertBefore(currentWrapElm, list);
\r
15209 } else if (startIndex > listIndex) {
\r
15210 node.insertBefore(currentWrapElm, list.nextSibling);
\r
15213 // add the new nodes to the list.
\r
15214 newWrappers.push(currentWrapElm);
\r
15216 each(nodes, function(node) {
\r
15217 currentWrapElm.appendChild(node);
\r
15220 return currentWrapElm;
\r
15224 function applyRngStyle(rng, bookmark, node_specific) {
\r
15225 var newWrappers = [], wrapName, wrapElm;
\r
15227 // Setup wrapper element
\r
15228 wrapName = format.inline || format.block;
\r
15229 wrapElm = dom.create(wrapName);
\r
15230 setElementFormat(wrapElm);
\r
15232 rangeUtils.walk(rng, function(nodes) {
\r
15233 var currentWrapElm;
\r
15235 function process(node) {
\r
15236 var nodeName = node.nodeName.toLowerCase(), parentName = node.parentNode.nodeName.toLowerCase(), found;
\r
15238 // Stop wrapping on br elements
\r
15239 if (isEq(nodeName, 'br')) {
\r
15240 currentWrapElm = 0;
\r
15242 // Remove any br elements when we wrap things
\r
15243 if (format.block)
\r
15244 dom.remove(node);
\r
15249 // If node is wrapper type
\r
15250 if (format.wrapper && matchNode(node, name, vars)) {
\r
15251 currentWrapElm = 0;
\r
15255 // Can we rename the block
\r
15256 if (format.block && !format.wrapper && isTextBlock(nodeName)) {
\r
15257 node = dom.rename(node, wrapName);
\r
15258 setElementFormat(node);
\r
15259 newWrappers.push(node);
\r
15260 currentWrapElm = 0;
\r
15264 // Handle selector patterns
\r
15265 if (format.selector) {
\r
15266 // Look for matching formats
\r
15267 each(formatList, function(format) {
\r
15268 // Check collapsed state if it exists
\r
15269 if ('collapsed' in format && format.collapsed !== isCollapsed) {
\r
15273 if (dom.is(node, format.selector) && !isCaretNode(node)) {
\r
15274 setElementFormat(node, format);
\r
15279 // Continue processing if a selector match wasn't found and a inline element is defined
\r
15280 if (!format.inline || found) {
\r
15281 currentWrapElm = 0;
\r
15286 // Is it valid to wrap this item
\r
15287 if (isValid(wrapName, nodeName) && isValid(parentName, wrapName) &&
\r
15288 !(!node_specific && node.nodeType === 3 && node.nodeValue.length === 1 && node.nodeValue.charCodeAt(0) === 65279) && !isCaretNode(node)) {
\r
15289 // Start wrapping
\r
15290 if (!currentWrapElm) {
\r
15292 currentWrapElm = wrapElm.cloneNode(FALSE);
\r
15293 node.parentNode.insertBefore(currentWrapElm, node);
\r
15294 newWrappers.push(currentWrapElm);
\r
15297 currentWrapElm.appendChild(node);
\r
15298 } else if (nodeName == 'li' && bookmark) {
\r
15299 // Start wrapping - if we are in a list node and have a bookmark, then we will always begin by wrapping in a new element.
\r
15300 currentWrapElm = applyStyleToList(node, bookmark, wrapElm, newWrappers, process);
\r
15302 // Start a new wrapper for possible children
\r
15303 currentWrapElm = 0;
\r
15305 each(tinymce.grep(node.childNodes), process);
\r
15307 // End the last wrapper
\r
15308 currentWrapElm = 0;
\r
15312 // Process siblings from range
\r
15313 each(nodes, process);
\r
15316 // Wrap links inside as well, for example color inside a link when the wrapper is around the link
\r
15317 if (format.wrap_links === false) {
\r
15318 each(newWrappers, function(node) {
\r
15319 function process(node) {
\r
15320 var i, currentWrapElm, children;
\r
15322 if (node.nodeName === 'A') {
\r
15323 currentWrapElm = wrapElm.cloneNode(FALSE);
\r
15324 newWrappers.push(currentWrapElm);
\r
15326 children = tinymce.grep(node.childNodes);
\r
15327 for (i = 0; i < children.length; i++)
\r
15328 currentWrapElm.appendChild(children[i]);
\r
15330 node.appendChild(currentWrapElm);
\r
15333 each(tinymce.grep(node.childNodes), process);
\r
15341 each(newWrappers, function(node) {
\r
15344 function getChildCount(node) {
\r
15347 each(node.childNodes, function(node) {
\r
15348 if (!isWhiteSpaceNode(node) && !isBookmarkNode(node))
\r
15355 function mergeStyles(node) {
\r
15356 var child, clone;
\r
15358 each(node.childNodes, function(node) {
\r
15359 if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) {
\r
15361 return FALSE; // break loop
\r
15365 // If child was found and of the same type as the current node
\r
15366 if (child && matchName(child, format)) {
\r
15367 clone = child.cloneNode(FALSE);
\r
15368 setElementFormat(clone);
\r
15370 dom.replace(clone, node, TRUE);
\r
15371 dom.remove(child, 1);
\r
15374 return clone || node;
\r
15377 childCount = getChildCount(node);
\r
15379 // Remove empty nodes but only if there is multiple wrappers and they are not block
\r
15380 // elements so never remove single <h1></h1> since that would remove the currrent empty block element where the caret is at
\r
15381 if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) {
\r
15382 dom.remove(node, 1);
\r
15386 if (format.inline || format.wrapper) {
\r
15387 // Merges the current node with it's children of similar type to reduce the number of elements
\r
15388 if (!format.exact && childCount === 1)
\r
15389 node = mergeStyles(node);
\r
15391 // Remove/merge children
\r
15392 each(formatList, function(format) {
\r
15393 // Merge all children of similar type will move styles from child to parent
\r
15394 // this: <span style="color:red"><b><span style="color:red; font-size:10px">text</span></b></span>
\r
15395 // will become: <span style="color:red"><b><span style="font-size:10px">text</span></b></span>
\r
15396 each(dom.select(format.inline, node), function(child) {
\r
15399 // When wrap_links is set to false we don't want
\r
15400 // to remove the format on children within links
\r
15401 if (format.wrap_links === false) {
\r
15402 parent = child.parentNode;
\r
15405 if (parent.nodeName === 'A')
\r
15407 } while (parent = parent.parentNode);
\r
15410 removeFormat(format, vars, child, format.exact ? child : null);
\r
15414 // Remove child if direct parent is of same type
\r
15415 if (matchNode(node.parentNode, name, vars)) {
\r
15416 dom.remove(node, 1);
\r
15421 // Look for parent with similar style format
\r
15422 if (format.merge_with_parents) {
\r
15423 dom.getParent(node.parentNode, function(parent) {
\r
15424 if (matchNode(parent, name, vars)) {
\r
15425 dom.remove(node, 1);
\r
15432 // Merge next and previous siblings if they are similar <b>text</b><b>text</b> becomes <b>texttext</b>
\r
15433 if (node && format.merge_siblings !== false) {
\r
15434 node = mergeSiblings(getNonWhiteSpaceSibling(node), node);
\r
15435 node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE));
\r
15443 if (node.nodeType) {
\r
15444 rng = dom.createRng();
\r
15445 rng.setStartBefore(node);
\r
15446 rng.setEndAfter(node);
\r
15447 applyRngStyle(expandRng(rng, formatList), null, true);
\r
15449 applyRngStyle(node, null, true);
\r
15452 if (!isCollapsed || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) {
\r
15453 // Obtain selection node before selection is unselected by applyRngStyle()
\r
15454 var curSelNode = ed.selection.getNode();
\r
15456 // Apply formatting to selection
\r
15457 ed.selection.setRng(adjustSelectionToVisibleSelection());
\r
15458 bookmark = selection.getBookmark();
\r
15459 applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark);
\r
15461 // Colored nodes should be underlined so that the color of the underline matches the text color.
\r
15462 if (format.styles && (format.styles.color || format.styles.textDecoration)) {
\r
15463 tinymce.walk(curSelNode, processUnderlineAndColor, 'childNodes');
\r
15464 processUnderlineAndColor(curSelNode);
\r
15467 selection.moveToBookmark(bookmark);
\r
15468 moveStart(selection.getRng(TRUE));
\r
15469 ed.nodeChanged();
\r
15471 performCaretAction('apply', name, vars);
\r
15476 function remove(name, vars, node) {
\r
15477 var formatList = get(name), format = formatList[0], bookmark, i, rng;
\r
15479 // Merges the styles for each node
\r
15480 function process(node) {
\r
15481 var children, i, l;
\r
15483 // Grab the children first since the nodelist might be changed
\r
15484 children = tinymce.grep(node.childNodes);
\r
15486 // Process current node
\r
15487 for (i = 0, l = formatList.length; i < l; i++) {
\r
15488 if (removeFormat(formatList[i], vars, node, node))
\r
15492 // Process the children
\r
15493 if (format.deep) {
\r
15494 for (i = 0, l = children.length; i < l; i++)
\r
15495 process(children[i]);
\r
15499 function findFormatRoot(container) {
\r
15502 // Find format root
\r
15503 each(getParents(container.parentNode).reverse(), function(parent) {
\r
15506 // Find format root element
\r
15507 if (!formatRoot && parent.id != '_start' && parent.id != '_end') {
\r
15508 // Is the node matching the format we are looking for
\r
15509 format = matchNode(parent, name, vars);
\r
15510 if (format && format.split !== false)
\r
15511 formatRoot = parent;
\r
15515 return formatRoot;
\r
15518 function wrapAndSplit(format_root, container, target, split) {
\r
15519 var parent, clone, lastClone, firstClone, i, formatRootParent;
\r
15521 // Format root found then clone formats and split it
\r
15522 if (format_root) {
\r
15523 formatRootParent = format_root.parentNode;
\r
15525 for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) {
\r
15526 clone = parent.cloneNode(FALSE);
\r
15528 for (i = 0; i < formatList.length; i++) {
\r
15529 if (removeFormat(formatList[i], vars, clone, clone)) {
\r
15535 // Build wrapper node
\r
15538 clone.appendChild(lastClone);
\r
15541 firstClone = clone;
\r
15543 lastClone = clone;
\r
15547 // Never split block elements if the format is mixed
\r
15548 if (split && (!format.mixed || !isBlock(format_root)))
\r
15549 container = dom.split(format_root, container);
\r
15551 // Wrap container in cloned formats
\r
15553 target.parentNode.insertBefore(lastClone, target);
\r
15554 firstClone.appendChild(target);
\r
15558 return container;
\r
15561 function splitToFormatRoot(container) {
\r
15562 return wrapAndSplit(findFormatRoot(container), container, container, true);
\r
15565 function unwrap(start) {
\r
15566 var node = dom.get(start ? '_start' : '_end'),
\r
15567 out = node[start ? 'firstChild' : 'lastChild'];
\r
15569 // If the end is placed within the start the result will be removed
\r
15570 // So this checks if the out node is a bookmark node if it is it
\r
15571 // checks for another more suitable node
\r
15572 if (isBookmarkNode(out))
\r
15573 out = out[start ? 'firstChild' : 'lastChild'];
\r
15575 dom.remove(node, true);
\r
15580 function removeRngStyle(rng) {
\r
15581 var startContainer, endContainer;
\r
15583 rng = expandRng(rng, formatList, TRUE);
\r
15585 if (format.split) {
\r
15586 startContainer = getContainer(rng, TRUE);
\r
15587 endContainer = getContainer(rng);
\r
15589 if (startContainer != endContainer) {
\r
15590 // Wrap start/end nodes in span element since these might be cloned/moved
\r
15591 startContainer = wrap(startContainer, 'span', {id : '_start', 'data-mce-type' : 'bookmark'});
\r
15592 endContainer = wrap(endContainer, 'span', {id : '_end', 'data-mce-type' : 'bookmark'});
\r
15594 // Split start/end
\r
15595 splitToFormatRoot(startContainer);
\r
15596 splitToFormatRoot(endContainer);
\r
15598 // Unwrap start/end to get real elements again
\r
15599 startContainer = unwrap(TRUE);
\r
15600 endContainer = unwrap();
\r
15602 startContainer = endContainer = splitToFormatRoot(startContainer);
\r
15604 // Update range positions since they might have changed after the split operations
\r
15605 rng.startContainer = startContainer.parentNode;
\r
15606 rng.startOffset = nodeIndex(startContainer);
\r
15607 rng.endContainer = endContainer.parentNode;
\r
15608 rng.endOffset = nodeIndex(endContainer) + 1;
\r
15611 // Remove items between start/end
\r
15612 rangeUtils.walk(rng, function(nodes) {
\r
15613 each(nodes, function(node) {
\r
15616 // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined.
\r
15617 if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' && node.parentNode && getTextDecoration(node.parentNode) === 'underline') {
\r
15618 removeFormat({'deep': false, 'exact': true, 'inline': 'span', 'styles': {'textDecoration' : 'underline'}}, null, node);
\r
15626 if (node.nodeType) {
\r
15627 rng = dom.createRng();
\r
15628 rng.setStartBefore(node);
\r
15629 rng.setEndAfter(node);
\r
15630 removeRngStyle(rng);
\r
15632 removeRngStyle(node);
\r
15638 if (!selection.isCollapsed() || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) {
\r
15639 bookmark = selection.getBookmark();
\r
15640 removeRngStyle(selection.getRng(TRUE));
\r
15641 selection.moveToBookmark(bookmark);
\r
15643 // 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
15644 if (format.inline && match(name, vars, selection.getStart())) {
\r
15645 moveStart(selection.getRng(true));
\r
15648 ed.nodeChanged();
\r
15650 performCaretAction('remove', name, vars);
\r
15652 // When you remove formatting from a table cell in WebKit (cell, not the contents of a cell) there is a rendering issue with column width
\r
15653 if (tinymce.isWebKit) {
\r
15654 ed.execCommand('mceCleanup');
\r
15658 function toggle(name, vars, node) {
\r
15659 var fmt = get(name);
\r
15661 if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0]['toggle']))
\r
15662 remove(name, vars, node);
\r
15664 apply(name, vars, node);
\r
15667 function matchNode(node, name, vars, similar) {
\r
15668 var formatList = get(name), format, i, classes;
\r
15670 function matchItems(node, format, item_name) {
\r
15671 var key, value, items = format[item_name], i;
\r
15674 if (format.onmatch) {
\r
15675 return format.onmatch(node, format, item_name);
\r
15678 // Check all items
\r
15680 // Non indexed object
\r
15681 if (items.length === undefined) {
\r
15682 for (key in items) {
\r
15683 if (items.hasOwnProperty(key)) {
\r
15684 if (item_name === 'attributes')
\r
15685 value = dom.getAttrib(node, key);
\r
15687 value = getStyle(node, key);
\r
15689 if (similar && !value && !format.exact)
\r
15692 if ((!similar || format.exact) && !isEq(value, replaceVars(items[key], vars)))
\r
15697 // Only one match needed for indexed arrays
\r
15698 for (i = 0; i < items.length; i++) {
\r
15699 if (item_name === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i]))
\r
15708 if (formatList && node) {
\r
15709 // Check each format in list
\r
15710 for (i = 0; i < formatList.length; i++) {
\r
15711 format = formatList[i];
\r
15713 // Name name, attributes, styles and classes
\r
15714 if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) {
\r
15716 if (classes = format.classes) {
\r
15717 for (i = 0; i < classes.length; i++) {
\r
15718 if (!dom.hasClass(node, classes[i]))
\r
15729 function match(name, vars, node) {
\r
15732 function matchParents(node) {
\r
15733 // Find first node with similar format settings
\r
15734 node = dom.getParent(node, function(node) {
\r
15735 return !!matchNode(node, name, vars, true);
\r
15738 // Do an exact check on the similar format element
\r
15739 return matchNode(node, name, vars);
\r
15742 // Check specified node
\r
15744 return matchParents(node);
\r
15746 // Check selected node
\r
15747 node = selection.getNode();
\r
15748 if (matchParents(node))
\r
15751 // Check start node if it's different
\r
15752 startNode = selection.getStart();
\r
15753 if (startNode != node) {
\r
15754 if (matchParents(startNode))
\r
15761 function matchAll(names, vars) {
\r
15762 var startElement, matchedFormatNames = [], checkedMap = {}, i, ni, name;
\r
15764 // Check start of selection for formats
\r
15765 startElement = selection.getStart();
\r
15766 dom.getParent(startElement, function(node) {
\r
15769 for (i = 0; i < names.length; i++) {
\r
15772 if (!checkedMap[name] && matchNode(node, name, vars)) {
\r
15773 checkedMap[name] = true;
\r
15774 matchedFormatNames.push(name);
\r
15779 return matchedFormatNames;
\r
15782 function canApply(name) {
\r
15783 var formatList = get(name), startNode, parents, i, x, selector;
\r
15785 if (formatList) {
\r
15786 startNode = selection.getStart();
\r
15787 parents = getParents(startNode);
\r
15789 for (x = formatList.length - 1; x >= 0; x--) {
\r
15790 selector = formatList[x].selector;
\r
15792 // Format is not selector based, then always return TRUE
\r
15796 for (i = parents.length - 1; i >= 0; i--) {
\r
15797 if (dom.is(parents[i], selector))
\r
15806 // Expose to public
\r
15807 tinymce.extend(this, {
\r
15809 register : register,
\r
15814 matchAll : matchAll,
\r
15815 matchNode : matchNode,
\r
15816 canApply : canApply
\r
15819 // Private functions
\r
15821 function matchName(node, format) {
\r
15822 // Check for inline match
\r
15823 if (isEq(node, format.inline))
\r
15826 // Check for block match
\r
15827 if (isEq(node, format.block))
\r
15830 // Check for selector match
\r
15831 if (format.selector)
\r
15832 return dom.is(node, format.selector);
\r
15835 function isEq(str1, str2) {
\r
15836 str1 = str1 || '';
\r
15837 str2 = str2 || '';
\r
15839 str1 = '' + (str1.nodeName || str1);
\r
15840 str2 = '' + (str2.nodeName || str2);
\r
15842 return str1.toLowerCase() == str2.toLowerCase();
\r
15845 function getStyle(node, name) {
\r
15846 var styleVal = dom.getStyle(node, name);
\r
15848 // Force the format to hex
\r
15849 if (name == 'color' || name == 'backgroundColor')
\r
15850 styleVal = dom.toHex(styleVal);
\r
15852 // Opera will return bold as 700
\r
15853 if (name == 'fontWeight' && styleVal == 700)
\r
15854 styleVal = 'bold';
\r
15856 return '' + styleVal;
\r
15859 function replaceVars(value, vars) {
\r
15860 if (typeof(value) != "string")
\r
15861 value = value(vars);
\r
15863 value = value.replace(/%(\w+)/g, function(str, name) {
\r
15864 return vars[name] || str;
\r
15871 function isWhiteSpaceNode(node) {
\r
15872 return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue);
\r
15875 function wrap(node, name, attrs) {
\r
15876 var wrapper = dom.create(name, attrs);
\r
15878 node.parentNode.insertBefore(wrapper, node);
\r
15879 wrapper.appendChild(node);
\r
15884 function expandRng(rng, format, remove) {
\r
15885 var startContainer = rng.startContainer,
\r
15886 startOffset = rng.startOffset,
\r
15887 endContainer = rng.endContainer,
\r
15888 endOffset = rng.endOffset, sibling, lastIdx, leaf, endPoint;
\r
15890 // This function walks up the tree if there is no siblings before/after the node
\r
15891 function findParentContainer(start) {
\r
15892 var container, parent, child, sibling, siblingName;
\r
15894 container = parent = start ? startContainer : endContainer;
\r
15895 siblingName = start ? 'previousSibling' : 'nextSibling';
\r
15896 root = dom.getRoot();
\r
15898 // If it's a text node and the offset is inside the text
\r
15899 if (container.nodeType == 3 && !isWhiteSpaceNode(container)) {
\r
15900 if (start ? startOffset > 0 : endOffset < container.nodeValue.length) {
\r
15901 return container;
\r
15906 // Stop expanding on block elements
\r
15907 if (!format[0].block_expand && isBlock(parent))
\r
15910 // Walk left/right
\r
15911 for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) {
\r
15912 if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling)) {
\r
15917 // Check if we can move up are we at root level or body level
\r
15918 if (parent.parentNode == root) {
\r
15919 container = parent;
\r
15923 parent = parent.parentNode;
\r
15926 return container;
\r
15929 // This function walks down the tree to find the leaf at the selection.
\r
15930 // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node.
\r
15931 function findLeaf(node, offset) {
\r
15932 if (offset === undefined)
\r
15933 offset = node.nodeType === 3 ? node.length : node.childNodes.length;
\r
15934 while (node && node.hasChildNodes()) {
\r
15935 node = node.childNodes[offset];
\r
15937 offset = node.nodeType === 3 ? node.length : node.childNodes.length;
\r
15939 return { node: node, offset: offset };
\r
15942 // If index based start position then resolve it
\r
15943 if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) {
\r
15944 lastIdx = startContainer.childNodes.length - 1;
\r
15945 startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset];
\r
15947 if (startContainer.nodeType == 3)
\r
15951 // If index based end position then resolve it
\r
15952 if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) {
\r
15953 lastIdx = endContainer.childNodes.length - 1;
\r
15954 endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1];
\r
15956 if (endContainer.nodeType == 3)
\r
15957 endOffset = endContainer.nodeValue.length;
\r
15960 // Exclude bookmark nodes if possible
\r
15961 if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) {
\r
15962 startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode;
\r
15963 startContainer = startContainer.nextSibling || startContainer;
\r
15965 if (startContainer.nodeType == 3)
\r
15969 if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) {
\r
15970 endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode;
\r
15971 endContainer = endContainer.previousSibling || endContainer;
\r
15973 if (endContainer.nodeType == 3)
\r
15974 endOffset = endContainer.length;
\r
15977 if (format[0].inline) {
\r
15978 if (rng.collapsed) {
\r
15979 function findWordEndPoint(container, offset, start) {
\r
15980 var walker, node, pos, lastTextNode;
\r
15982 function findSpace(node, offset) {
\r
15983 var pos, pos2, str = node.nodeValue;
\r
15985 if (typeof(offset) == "undefined") {
\r
15986 offset = start ? str.length : 0;
\r
15990 pos = str.lastIndexOf(' ', offset);
\r
15991 pos2 = str.lastIndexOf('\u00a0', offset);
\r
15992 pos = pos > pos2 ? pos : pos2;
\r
15994 // Include the space on remove to avoid tag soup
\r
15995 if (pos !== -1 && !remove) {
\r
15999 pos = str.indexOf(' ', offset);
\r
16000 pos2 = str.indexOf('\u00a0', offset);
\r
16001 pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2;
\r
16007 if (container.nodeType === 3) {
\r
16008 pos = findSpace(container, offset);
\r
16010 if (pos !== -1) {
\r
16011 return {container : container, offset : pos};
\r
16014 lastTextNode = container;
\r
16017 // Walk the nodes inside the block
\r
16018 walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody());
\r
16019 while (node = walker[start ? 'prev' : 'next']()) {
\r
16020 if (node.nodeType === 3) {
\r
16021 lastTextNode = node;
\r
16022 pos = findSpace(node);
\r
16024 if (pos !== -1) {
\r
16025 return {container : node, offset : pos};
\r
16027 } else if (isBlock(node)) {
\r
16032 if (lastTextNode) {
\r
16036 offset = lastTextNode.length;
\r
16039 return {container: lastTextNode, offset: offset};
\r
16043 // Expand left to closest word boundery
\r
16044 endPoint = findWordEndPoint(startContainer, startOffset, true);
\r
16046 startContainer = endPoint.container;
\r
16047 startOffset = endPoint.offset;
\r
16050 // Expand right to closest word boundery
\r
16051 endPoint = findWordEndPoint(endContainer, endOffset);
\r
16053 endContainer = endPoint.container;
\r
16054 endOffset = endPoint.offset;
\r
16058 // Avoid applying formatting to a trailing space.
\r
16059 leaf = findLeaf(endContainer, endOffset);
\r
16061 while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling)
\r
16062 leaf = findLeaf(leaf.node.previousSibling);
\r
16064 if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 &&
\r
16065 leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') {
\r
16067 if (leaf.offset > 1) {
\r
16068 endContainer = leaf.node;
\r
16069 endContainer.splitText(leaf.offset - 1);
\r
16070 } else if (leaf.node.previousSibling) {
\r
16071 // TODO: Figure out why this is in here
\r
16072 //endContainer = leaf.node.previousSibling;
\r
16078 // Move start/end point up the tree if the leaves are sharp and if we are in different containers
\r
16079 // Example * becomes !: !<p><b><i>*text</i><i>text*</i></b></p>!
\r
16080 // This will reduce the number of wrapper elements that needs to be created
\r
16081 // Move start point up the tree
\r
16082 if (format[0].inline || format[0].block_expand) {
\r
16083 if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) {
\r
16084 startContainer = findParentContainer(true);
\r
16087 if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) {
\r
16088 endContainer = findParentContainer();
\r
16092 // Expand start/end container to matching selector
\r
16093 if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) {
\r
16094 function findSelectorEndPoint(container, sibling_name) {
\r
16095 var parents, i, y, curFormat;
\r
16097 if (container.nodeType == 3 && container.nodeValue.length == 0 && container[sibling_name])
\r
16098 container = container[sibling_name];
\r
16100 parents = getParents(container);
\r
16101 for (i = 0; i < parents.length; i++) {
\r
16102 for (y = 0; y < format.length; y++) {
\r
16103 curFormat = format[y];
\r
16105 // If collapsed state is set then skip formats that doesn't match that
\r
16106 if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed)
\r
16109 if (dom.is(parents[i], curFormat.selector))
\r
16110 return parents[i];
\r
16114 return container;
\r
16117 // Find new startContainer/endContainer if there is better one
\r
16118 startContainer = findSelectorEndPoint(startContainer, 'previousSibling');
\r
16119 endContainer = findSelectorEndPoint(endContainer, 'nextSibling');
\r
16122 // Expand start/end container to matching block element or text node
\r
16123 if (format[0].block || format[0].selector) {
\r
16124 function findBlockEndPoint(container, sibling_name, sibling_name2) {
\r
16127 // Expand to block of similar type
\r
16128 if (!format[0].wrapper)
\r
16129 node = dom.getParent(container, format[0].block);
\r
16131 // Expand to first wrappable block element or any block element
\r
16133 node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, isBlock);
\r
16135 // Exclude inner lists from wrapping
\r
16136 if (node && format[0].wrapper)
\r
16137 node = getParents(node, 'ul,ol').reverse()[0] || node;
\r
16139 // Didn't find a block element look for first/last wrappable element
\r
16141 node = container;
\r
16143 while (node[sibling_name] && !isBlock(node[sibling_name])) {
\r
16144 node = node[sibling_name];
\r
16146 // Break on BR but include it will be removed later on
\r
16147 // we can't remove it now since we need to check if it can be wrapped
\r
16148 if (isEq(node, 'br'))
\r
16153 return node || container;
\r
16156 // Find new startContainer/endContainer if there is better one
\r
16157 startContainer = findBlockEndPoint(startContainer, 'previousSibling');
\r
16158 endContainer = findBlockEndPoint(endContainer, 'nextSibling');
\r
16160 // Non block element then try to expand up the leaf
\r
16161 if (format[0].block) {
\r
16162 if (!isBlock(startContainer))
\r
16163 startContainer = findParentContainer(true);
\r
16165 if (!isBlock(endContainer))
\r
16166 endContainer = findParentContainer();
\r
16170 // Setup index for startContainer
\r
16171 if (startContainer.nodeType == 1) {
\r
16172 startOffset = nodeIndex(startContainer);
\r
16173 startContainer = startContainer.parentNode;
\r
16176 // Setup index for endContainer
\r
16177 if (endContainer.nodeType == 1) {
\r
16178 endOffset = nodeIndex(endContainer) + 1;
\r
16179 endContainer = endContainer.parentNode;
\r
16182 // Return new range like object
\r
16184 startContainer : startContainer,
\r
16185 startOffset : startOffset,
\r
16186 endContainer : endContainer,
\r
16187 endOffset : endOffset
\r
16191 function removeFormat(format, vars, node, compare_node) {
\r
16192 var i, attrs, stylesModified;
\r
16194 // Check if node matches format
\r
16195 if (!matchName(node, format))
\r
16198 // Should we compare with format attribs and styles
\r
16199 if (format.remove != 'all') {
\r
16201 each(format.styles, function(value, name) {
\r
16202 value = replaceVars(value, vars);
\r
16205 if (typeof(name) === 'number') {
\r
16207 compare_node = 0;
\r
16210 if (!compare_node || isEq(getStyle(compare_node, name), value))
\r
16211 dom.setStyle(node, name, '');
\r
16213 stylesModified = 1;
\r
16216 // Remove style attribute if it's empty
\r
16217 if (stylesModified && dom.getAttrib(node, 'style') == '') {
\r
16218 node.removeAttribute('style');
\r
16219 node.removeAttribute('data-mce-style');
\r
16222 // Remove attributes
\r
16223 each(format.attributes, function(value, name) {
\r
16226 value = replaceVars(value, vars);
\r
16229 if (typeof(name) === 'number') {
\r
16231 compare_node = 0;
\r
16234 if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) {
\r
16235 // Keep internal classes
\r
16236 if (name == 'class') {
\r
16237 value = dom.getAttrib(node, name);
\r
16239 // Build new class value where everything is removed except the internal prefixed classes
\r
16241 each(value.split(/\s+/), function(cls) {
\r
16242 if (/mce\w+/.test(cls))
\r
16243 valueOut += (valueOut ? ' ' : '') + cls;
\r
16246 // We got some internal classes left
\r
16248 dom.setAttrib(node, name, valueOut);
\r
16254 // IE6 has a bug where the attribute doesn't get removed correctly
\r
16255 if (name == "class")
\r
16256 node.removeAttribute('className');
\r
16258 // Remove mce prefixed attributes
\r
16259 if (MCE_ATTR_RE.test(name))
\r
16260 node.removeAttribute('data-mce-' + name);
\r
16262 node.removeAttribute(name);
\r
16266 // Remove classes
\r
16267 each(format.classes, function(value) {
\r
16268 value = replaceVars(value, vars);
\r
16270 if (!compare_node || dom.hasClass(compare_node, value))
\r
16271 dom.removeClass(node, value);
\r
16274 // Check for non internal attributes
\r
16275 attrs = dom.getAttribs(node);
\r
16276 for (i = 0; i < attrs.length; i++) {
\r
16277 if (attrs[i].nodeName.indexOf('_') !== 0)
\r
16282 // Remove the inline child if it's empty for example <b> or <span>
\r
16283 if (format.remove != 'none') {
\r
16284 removeNode(node, format);
\r
16289 function removeNode(node, format) {
\r
16290 var parentNode = node.parentNode, rootBlockElm;
\r
16292 if (format.block) {
\r
16293 if (!forcedRootBlock) {
\r
16294 function find(node, next, inc) {
\r
16295 node = getNonWhiteSpaceSibling(node, next, inc);
\r
16297 return !node || (node.nodeName == 'BR' || isBlock(node));
\r
16300 // Append BR elements if needed before we remove the block
\r
16301 if (isBlock(node) && !isBlock(parentNode)) {
\r
16302 if (!find(node, FALSE) && !find(node.firstChild, TRUE, 1))
\r
16303 node.insertBefore(dom.create('br'), node.firstChild);
\r
16305 if (!find(node, TRUE) && !find(node.lastChild, FALSE, 1))
\r
16306 node.appendChild(dom.create('br'));
\r
16309 // Wrap the block in a forcedRootBlock if we are at the root of document
\r
16310 if (parentNode == dom.getRoot()) {
\r
16311 if (!format.list_block || !isEq(node, format.list_block)) {
\r
16312 each(tinymce.grep(node.childNodes), function(node) {
\r
16313 if (isValid(forcedRootBlock, node.nodeName.toLowerCase())) {
\r
16314 if (!rootBlockElm)
\r
16315 rootBlockElm = wrap(node, forcedRootBlock);
\r
16317 rootBlockElm.appendChild(node);
\r
16319 rootBlockElm = 0;
\r
16326 // Never remove nodes that isn't the specified inline element if a selector is specified too
\r
16327 if (format.selector && format.inline && !isEq(format.inline, node))
\r
16330 dom.remove(node, 1);
\r
16333 function getNonWhiteSpaceSibling(node, next, inc) {
\r
16335 next = next ? 'nextSibling' : 'previousSibling';
\r
16337 for (node = inc ? node : node[next]; node; node = node[next]) {
\r
16338 if (node.nodeType == 1 || !isWhiteSpaceNode(node))
\r
16344 function isBookmarkNode(node) {
\r
16345 return node && node.nodeType == 1 && node.getAttribute('data-mce-type') == 'bookmark';
\r
16348 function mergeSiblings(prev, next) {
\r
16349 var marker, sibling, tmpSibling;
\r
16351 function compareElements(node1, node2) {
\r
16352 // Not the same name
\r
16353 if (node1.nodeName != node2.nodeName)
\r
16356 function getAttribs(node) {
\r
16357 var attribs = {};
\r
16359 each(dom.getAttribs(node), function(attr) {
\r
16360 var name = attr.nodeName.toLowerCase();
\r
16362 // Don't compare internal attributes or style
\r
16363 if (name.indexOf('_') !== 0 && name !== 'style')
\r
16364 attribs[name] = dom.getAttrib(node, name);
\r
16370 function compareObjects(obj1, obj2) {
\r
16373 for (name in obj1) {
\r
16374 // Obj1 has item obj2 doesn't have
\r
16375 if (obj1.hasOwnProperty(name)) {
\r
16376 value = obj2[name];
\r
16378 // Obj2 doesn't have obj1 item
\r
16379 if (value === undefined)
\r
16382 // Obj2 item has a different value
\r
16383 if (obj1[name] != value)
\r
16386 // Delete similar value
\r
16387 delete obj2[name];
\r
16391 // Check if obj 2 has something obj 1 doesn't have
\r
16392 for (name in obj2) {
\r
16393 // Obj2 has item obj1 doesn't have
\r
16394 if (obj2.hasOwnProperty(name))
\r
16401 // Attribs are not the same
\r
16402 if (!compareObjects(getAttribs(node1), getAttribs(node2)))
\r
16405 // Styles are not the same
\r
16406 if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style'))))
\r
16412 // Check if next/prev exists and that they are elements
\r
16413 if (prev && next) {
\r
16414 function findElementSibling(node, sibling_name) {
\r
16415 for (sibling = node; sibling; sibling = sibling[sibling_name]) {
\r
16416 if (sibling.nodeType == 3 && sibling.nodeValue.length !== 0)
\r
16419 if (sibling.nodeType == 1 && !isBookmarkNode(sibling))
\r
16426 // If previous sibling is empty then jump over it
\r
16427 prev = findElementSibling(prev, 'previousSibling');
\r
16428 next = findElementSibling(next, 'nextSibling');
\r
16430 // Compare next and previous nodes
\r
16431 if (compareElements(prev, next)) {
\r
16432 // Append nodes between
\r
16433 for (sibling = prev.nextSibling; sibling && sibling != next;) {
\r
16434 tmpSibling = sibling;
\r
16435 sibling = sibling.nextSibling;
\r
16436 prev.appendChild(tmpSibling);
\r
16439 // Remove next node
\r
16440 dom.remove(next);
\r
16442 // Move children into prev node
\r
16443 each(tinymce.grep(next.childNodes), function(node) {
\r
16444 prev.appendChild(node);
\r
16454 function isTextBlock(name) {
\r
16455 return /^(h[1-6]|p|div|pre|address|dl|dt|dd)$/.test(name);
\r
16458 function getContainer(rng, start) {
\r
16459 var container, offset, lastIdx, walker;
\r
16461 container = rng[start ? 'startContainer' : 'endContainer'];
\r
16462 offset = rng[start ? 'startOffset' : 'endOffset'];
\r
16464 if (container.nodeType == 1) {
\r
16465 lastIdx = container.childNodes.length - 1;
\r
16467 if (!start && offset)
\r
16470 container = container.childNodes[offset > lastIdx ? lastIdx : offset];
\r
16473 // If start text node is excluded then walk to the next node
\r
16474 if (container.nodeType === 3 && start && offset >= container.nodeValue.length) {
\r
16475 container = new TreeWalker(container, ed.getBody()).next() || container;
\r
16478 // If end text node is excluded then walk to the previous node
\r
16479 if (container.nodeType === 3 && !start && offset == 0) {
\r
16480 container = new TreeWalker(container, ed.getBody()).prev() || container;
\r
16483 return container;
\r
16486 function performCaretAction(type, name, vars) {
\r
16487 var invisibleChar, caretContainerId = '_mce_caret', debug = ed.settings.caret_debug;
\r
16489 // Setup invisible character use zero width space on Gecko since it doesn't change the heigt of the container
\r
16490 invisibleChar = tinymce.isGecko ? '\u200B' : INVISIBLE_CHAR;
\r
16492 // Creates a caret container bogus element
\r
16493 function createCaretContainer(fill) {
\r
16494 var caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true, style: debug ? 'color:red' : ''});
\r
16497 caretContainer.appendChild(ed.getDoc().createTextNode(invisibleChar));
\r
16500 return caretContainer;
\r
16503 function isCaretContainerEmpty(node, nodes) {
\r
16505 if ((node.nodeType === 3 && node.nodeValue !== invisibleChar) || node.childNodes.length > 1) {
\r
16510 if (nodes && node.nodeType === 1) {
\r
16511 nodes.push(node);
\r
16514 node = node.firstChild;
\r
16520 // Returns any parent caret container element
\r
16521 function getParentCaretContainer(node) {
\r
16523 if (node.id === caretContainerId) {
\r
16527 node = node.parentNode;
\r
16531 // Finds the first text node in the specified node
\r
16532 function findFirstTextNode(node) {
\r
16536 walker = new TreeWalker(node, node);
\r
16538 for (node = walker.current(); node; node = walker.next()) {
\r
16539 if (node.nodeType === 3) {
\r
16546 // Removes the caret container for the specified node or all on the current document
\r
16547 function removeCaretContainer(node, move_caret) {
\r
16551 node = getParentCaretContainer(selection.getStart());
\r
16554 while (node = dom.get(caretContainerId)) {
\r
16555 removeCaretContainer(node, false);
\r
16559 rng = selection.getRng(true);
\r
16561 if (isCaretContainerEmpty(node)) {
\r
16562 if (move_caret !== false) {
\r
16563 rng.setStartBefore(node);
\r
16564 rng.setEndBefore(node);
\r
16567 dom.remove(node);
\r
16569 child = findFirstTextNode(node);
\r
16571 if (child.nodeValue.charAt(0) === INVISIBLE_CHAR) {
\r
16572 child = child.deleteData(0, 1);
\r
16575 dom.remove(node, 1);
\r
16578 selection.setRng(rng);
\r
16582 // Applies formatting to the caret postion
\r
16583 function applyCaretFormat() {
\r
16584 var rng, caretContainer, textNode, offset, bookmark, container, text;
\r
16586 rng = selection.getRng(true);
\r
16587 offset = rng.startOffset;
\r
16588 container = rng.startContainer;
\r
16589 text = container.nodeValue;
\r
16591 caretContainer = getParentCaretContainer(selection.getStart());
\r
16592 if (caretContainer) {
\r
16593 textNode = findFirstTextNode(caretContainer);
\r
16596 // Expand to word is caret is in the middle of a text node and the char before/after is a alpha numeric character
\r
16597 if (text && offset > 0 && offset < text.length && /\w/.test(text.charAt(offset)) && /\w/.test(text.charAt(offset - 1))) {
\r
16598 // Get bookmark of caret position
\r
16599 bookmark = selection.getBookmark();
\r
16601 // Collapse bookmark range (WebKit)
\r
16602 rng.collapse(true);
\r
16604 // Expand the range to the closest word and split it at those points
\r
16605 rng = expandRng(rng, get(name));
\r
16606 rng = rangeUtils.split(rng);
\r
16608 // Apply the format to the range
\r
16609 apply(name, vars, rng);
\r
16611 // Move selection back to caret position
\r
16612 selection.moveToBookmark(bookmark);
\r
16614 if (!caretContainer || textNode.nodeValue !== invisibleChar) {
\r
16615 caretContainer = createCaretContainer(true);
\r
16616 textNode = caretContainer.firstChild;
\r
16618 rng.insertNode(caretContainer);
\r
16621 apply(name, vars, caretContainer);
\r
16623 apply(name, vars, caretContainer);
\r
16626 // Move selection to text node
\r
16627 selection.setCursorLocation(textNode, offset);
\r
16631 function removeCaretFormat() {
\r
16632 var rng = selection.getRng(true), container, offset, bookmark,
\r
16633 hasContentAfter, node, formatNode, parents = [], i, caretContainer;
\r
16635 container = rng.startContainer;
\r
16636 offset = rng.startOffset;
\r
16637 node = container;
\r
16639 if (container.nodeType == 3) {
\r
16640 if (offset != container.nodeValue.length || container.nodeValue === invisibleChar) {
\r
16641 hasContentAfter = true;
\r
16644 node = node.parentNode;
\r
16648 if (matchNode(node, name, vars)) {
\r
16649 formatNode = node;
\r
16653 if (node.nextSibling) {
\r
16654 hasContentAfter = true;
\r
16657 parents.push(node);
\r
16658 node = node.parentNode;
\r
16661 // Node doesn't have the specified format
\r
16662 if (!formatNode) {
\r
16666 // Is there contents after the caret then remove the format on the element
\r
16667 if (hasContentAfter) {
\r
16668 // Get bookmark of caret position
\r
16669 bookmark = selection.getBookmark();
\r
16671 // Collapse bookmark range (WebKit)
\r
16672 rng.collapse(true);
\r
16674 // Expand the range to the closest word and split it at those points
\r
16675 rng = expandRng(rng, get(name), true);
\r
16676 rng = rangeUtils.split(rng);
\r
16678 // Remove the format from the range
\r
16679 remove(name, vars, rng);
\r
16681 // Move selection back to caret position
\r
16682 selection.moveToBookmark(bookmark);
\r
16684 caretContainer = createCaretContainer();
\r
16686 node = caretContainer;
\r
16687 for (i = parents.length - 1; i >= 0; i--) {
\r
16688 node.appendChild(parents[i].cloneNode(false));
\r
16689 node = node.firstChild;
\r
16692 // Insert invisible character into inner most format element
\r
16693 node.appendChild(dom.doc.createTextNode(invisibleChar));
\r
16694 node = node.firstChild;
\r
16696 // Insert caret container after the formated node
\r
16697 dom.insertAfter(caretContainer, formatNode);
\r
16699 // Move selection to text node
\r
16700 selection.setCursorLocation(node, 1);
\r
16704 // Only bind the caret events once
\r
16705 if (!self._hasCaretEvents) {
\r
16706 // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements
\r
16707 ed.onBeforeGetContent.addToTop(function() {
\r
16708 var nodes = [], i;
\r
16710 if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) {
\r
16712 i = nodes.length;
\r
16714 dom.setAttrib(nodes[i], 'data-mce-bogus', '1');
\r
16719 // Remove caret container on mouse up and on key up
\r
16720 tinymce.each('onMouseUp onKeyUp'.split(' '), function(name) {
\r
16721 ed[name].addToTop(function() {
\r
16722 removeCaretContainer();
\r
16726 // Remove caret container on keydown and it's a backspace, enter or left/right arrow keys
\r
16727 ed.onKeyDown.addToTop(function(ed, e) {
\r
16728 var keyCode = e.keyCode;
\r
16730 if (keyCode == 8 || keyCode == 37 || keyCode == 39) {
\r
16731 removeCaretContainer(getParentCaretContainer(selection.getStart()));
\r
16735 self._hasCaretEvents = true;
\r
16738 // Do apply or remove caret format
\r
16739 if (type == "apply") {
\r
16740 applyCaretFormat();
\r
16742 removeCaretFormat();
\r
16746 function moveStart(rng) {
\r
16747 var container = rng.startContainer,
\r
16748 offset = rng.startOffset,
\r
16749 walker, node, nodes, tmpNode;
\r
16751 // Convert text node into index if possible
\r
16752 if (container.nodeType == 3 && offset >= container.nodeValue.length - 1) {
\r
16753 container = container.parentNode;
\r
16754 offset = nodeIndex(container) + 1;
\r
16757 // Move startContainer/startOffset in to a suitable node
\r
16758 if (container.nodeType == 1) {
\r
16759 nodes = container.childNodes;
\r
16760 container = nodes[Math.min(offset, nodes.length - 1)];
\r
16761 walker = new TreeWalker(container);
\r
16763 // If offset is at end of the parent node walk to the next one
\r
16764 if (offset > nodes.length - 1)
\r
16767 for (node = walker.current(); node; node = walker.next()) {
\r
16768 if (node.nodeType == 3 && !isWhiteSpaceNode(node)) {
\r
16769 // IE has a "neat" feature where it moves the start node into the closest element
\r
16770 // we can avoid this by inserting an element before it and then remove it after we set the selection
\r
16771 tmpNode = dom.create('a', null, INVISIBLE_CHAR);
\r
16772 node.parentNode.insertBefore(tmpNode, node);
\r
16774 // Set selection and remove tmpNode
\r
16775 rng.setStart(node, 0);
\r
16776 selection.setRng(rng);
\r
16777 dom.remove(tmpNode);
\r
16788 tinymce.onAddEditor.add(function(tinymce, ed) {
\r
16789 var filters, fontSizes, dom, settings = ed.settings;
\r
16791 if (settings.inline_styles) {
\r
16792 fontSizes = tinymce.explode(settings.font_size_legacy_values);
\r
16794 function replaceWithSpan(node, styles) {
\r
16795 tinymce.each(styles, function(value, name) {
\r
16797 dom.setStyle(node, name, value);
\r
16800 dom.rename(node, 'span');
\r
16804 font : function(dom, node) {
\r
16805 replaceWithSpan(node, {
\r
16806 backgroundColor : node.style.backgroundColor,
\r
16807 color : node.color,
\r
16808 fontFamily : node.face,
\r
16809 fontSize : fontSizes[parseInt(node.size) - 1]
\r
16813 u : function(dom, node) {
\r
16814 replaceWithSpan(node, {
\r
16815 textDecoration : 'underline'
\r
16819 strike : function(dom, node) {
\r
16820 replaceWithSpan(node, {
\r
16821 textDecoration : 'line-through'
\r
16826 function convert(editor, params) {
\r
16827 dom = editor.dom;
\r
16829 if (settings.convert_fonts_to_spans) {
\r
16830 tinymce.each(dom.select('font,u,strike', params.node), function(node) {
\r
16831 filters[node.nodeName.toLowerCase()](ed.dom, node);
\r
16836 ed.onPreProcess.add(convert);
\r
16837 ed.onSetContent.add(convert);
\r
16839 ed.onInit.add(function() {
\r
16840 ed.selection.onSetContent.add(convert);
\r