2 var whiteSpaceRe = /^\s*|\s*$/g,
\r
3 undefined, isRegExpBroken = 'B'.replace(/A(.)|B/, '$1') === '$1';
\r
8 minorVersion : '4.5',
\r
10 releaseDate : '2011-09-06',
\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);
\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 v += typeof o[i] != 'function' ? (v.length > 1 ? ',' + quote : quote) + i + quote +':' + serialize(o[i], quote) : '';
\r
913 tinymce.util.JSON = {
\r
914 serialize: serialize,
\r
916 parse: function(s) {
\r
918 return eval('(' + s + ')');
\r
926 tinymce.create('static tinymce.util.XHR', {
\r
927 send : function(o) {
\r
928 var x, t, w = window, c = 0;
\r
930 // Default settings
\r
931 o.scope = o.scope || this;
\r
932 o.success_scope = o.success_scope || o.scope;
\r
933 o.error_scope = o.error_scope || o.scope;
\r
934 o.async = o.async === false ? false : true;
\r
935 o.data = o.data || '';
\r
941 x = new ActiveXObject(s);
\r
948 x = w.XMLHttpRequest ? new XMLHttpRequest() : get('Microsoft.XMLHTTP') || get('Msxml2.XMLHTTP');
\r
951 if (x.overrideMimeType)
\r
952 x.overrideMimeType(o.content_type);
\r
954 x.open(o.type || (o.data ? 'POST' : 'GET'), o.url, o.async);
\r
956 if (o.content_type)
\r
957 x.setRequestHeader('Content-Type', o.content_type);
\r
959 x.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
\r
964 if (!o.async || x.readyState == 4 || c++ > 10000) {
\r
965 if (o.success && c < 10000 && x.status == 200)
\r
966 o.success.call(o.success_scope, '' + x.responseText, x, o);
\r
968 o.error.call(o.error_scope, c > 10000 ? 'TIMED_OUT' : 'GENERAL', x, o);
\r
972 w.setTimeout(ready, 10);
\r
975 // Syncronous request
\r
979 // Wait for response, onReadyStateChange can not be used since it leaks memory in IE
\r
980 t = w.setTimeout(ready, 10);
\r
986 var extend = tinymce.extend, JSON = tinymce.util.JSON, XHR = tinymce.util.XHR;
\r
988 tinymce.create('tinymce.util.JSONRequest', {
\r
989 JSONRequest : function(s) {
\r
990 this.settings = extend({
\r
995 send : function(o) {
\r
996 var ecb = o.error, scb = o.success;
\r
998 o = extend(this.settings, o);
\r
1000 o.success = function(c, x) {
\r
1001 c = JSON.parse(c);
\r
1003 if (typeof(c) == 'undefined') {
\r
1005 error : 'JSON Parse error.'
\r
1010 ecb.call(o.error_scope || o.scope, c.error, x);
\r
1012 scb.call(o.success_scope || o.scope, c.result);
\r
1015 o.error = function(ty, x) {
\r
1017 ecb.call(o.error_scope || o.scope, ty, x);
\r
1020 o.data = JSON.serialize({
\r
1021 id : o.id || 'c' + (this.count++),
\r
1022 method : o.method,
\r
1026 // JSON content type for Ruby on rails. Bug: #1883287
\r
1027 o.content_type = 'application/json';
\r
1033 sendRPC : function(o) {
\r
1034 return new tinymce.util.JSONRequest().send(o);
\r
1039 (function(tinymce){
\r
1048 (function(tinymce) {
\r
1049 var VK = tinymce.VK, BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE;
\r
1051 function cleanupStylesWhenDeleting(ed) {
\r
1052 var dom = ed.dom, selection = ed.selection;
\r
1054 ed.onKeyDown.add(function(ed, e) {
\r
1055 var rng, blockElm, node, clonedSpan, isDelete;
\r
1057 isDelete = e.keyCode == DELETE;
\r
1058 if (isDelete || e.keyCode == BACKSPACE) {
\r
1059 e.preventDefault();
\r
1060 rng = selection.getRng();
\r
1062 // Find root block
\r
1063 blockElm = dom.getParent(rng.startContainer, dom.isBlock);
\r
1065 // On delete clone the root span of the next block element
\r
1067 blockElm = dom.getNext(blockElm, dom.isBlock);
\r
1069 // Locate root span element and clone it since it would otherwise get merged by the "apple-style-span" on delete/backspace
\r
1071 node = blockElm.firstChild;
\r
1073 if (node && node.nodeName === 'SPAN') {
\r
1074 clonedSpan = node.cloneNode(false);
\r
1078 // Do the backspace/delete actiopn
\r
1079 ed.getDoc().execCommand(isDelete ? 'ForwardDelete' : 'Delete', false, null);
\r
1081 // Find all odd apple-style-spans
\r
1082 blockElm = dom.getParent(rng.startContainer, dom.isBlock);
\r
1083 tinymce.each(dom.select('span.Apple-style-span,font.Apple-style-span', blockElm), function(span) {
\r
1084 var rng = dom.createRng();
\r
1086 // Set range selection before the span we are about to remove
\r
1087 rng.setStartBefore(span);
\r
1088 rng.setEndBefore(span);
\r
1091 dom.replace(clonedSpan.cloneNode(false), span, true);
\r
1093 dom.remove(span, true);
\r
1096 // Restore the selection
\r
1097 selection.setRng(rng);
\r
1103 function emptyEditorWhenDeleting(ed) {
\r
1104 ed.onKeyUp.add(function(ed, e) {
\r
1105 var keyCode = e.keyCode;
\r
1107 if (keyCode == DELETE || keyCode == BACKSPACE) {
\r
1108 if (ed.dom.isEmpty(ed.getBody())) {
\r
1109 ed.setContent('', {format : 'raw'});
\r
1117 tinymce.create('tinymce.util.Quirks', {
\r
1118 Quirks: function(ed) {
\r
1119 // Load WebKit specific fixed
\r
1120 if (tinymce.isWebKit) {
\r
1121 cleanupStylesWhenDeleting(ed);
\r
1122 emptyEditorWhenDeleting(ed);
\r
1125 // Load IE specific fixes
\r
1126 if (tinymce.isIE) {
\r
1127 emptyEditorWhenDeleting(ed);
\r
1132 (function(tinymce) {
\r
1133 var namedEntities, baseEntities, reverseEntities,
\r
1134 attrsCharsRegExp = /[&<>\"\u007E-\uD7FF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
\r
1135 textCharsRegExp = /[<>&\u007E-\uD7FF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
\r
1136 rawCharsRegExp = /[<>&\"\']/g,
\r
1137 entityRegExp = /&(#x|#)?([\w]+);/g,
\r
1139 128 : "\u20AC", 130 : "\u201A", 131 : "\u0192", 132 : "\u201E", 133 : "\u2026", 134 : "\u2020",
\r
1140 135 : "\u2021", 136 : "\u02C6", 137 : "\u2030", 138 : "\u0160", 139 : "\u2039", 140 : "\u0152",
\r
1141 142 : "\u017D", 145 : "\u2018", 146 : "\u2019", 147 : "\u201C", 148 : "\u201D", 149 : "\u2022",
\r
1142 150 : "\u2013", 151 : "\u2014", 152 : "\u02DC", 153 : "\u2122", 154 : "\u0161", 155 : "\u203A",
\r
1143 156 : "\u0153", 158 : "\u017E", 159 : "\u0178"
\r
1148 '\"' : '"', // Needs to be escaped since the YUI compressor would otherwise break the code
\r
1155 // Reverse lookup table for raw entities
\r
1156 reverseEntities = {
\r
1164 // Decodes text by using the browser
\r
1165 function nativeDecode(text) {
\r
1168 elm = document.createElement("div");
\r
1169 elm.innerHTML = text;
\r
1171 return elm.textContent || elm.innerText || text;
\r
1174 // Build a two way lookup table for the entities
\r
1175 function buildEntitiesLookup(items, radix) {
\r
1176 var i, chr, entity, lookup = {};
\r
1179 items = items.split(',');
\r
1180 radix = radix || 10;
\r
1182 // Build entities lookup table
\r
1183 for (i = 0; i < items.length; i += 2) {
\r
1184 chr = String.fromCharCode(parseInt(items[i], radix));
\r
1186 // Only add non base entities
\r
1187 if (!baseEntities[chr]) {
\r
1188 entity = '&' + items[i + 1] + ';';
\r
1189 lookup[chr] = entity;
\r
1190 lookup[entity] = chr;
\r
1198 // Unpack entities lookup where the numbers are in radix 32 to reduce the size
\r
1199 namedEntities = buildEntitiesLookup(
\r
1200 '50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' +
\r
1201 '5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' +
\r
1202 '5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' +
\r
1203 '5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' +
\r
1204 '68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' +
\r
1205 '6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' +
\r
1206 '6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' +
\r
1207 '75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' +
\r
1208 '7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' +
\r
1209 '7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' +
\r
1210 'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' +
\r
1211 'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' +
\r
1212 't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' +
\r
1213 'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' +
\r
1214 'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' +
\r
1215 '81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' +
\r
1216 '8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' +
\r
1217 '8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' +
\r
1218 '8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' +
\r
1219 '8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' +
\r
1220 'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' +
\r
1221 'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' +
\r
1222 'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' +
\r
1223 '80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' +
\r
1224 '811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro'
\r
1227 tinymce.html = tinymce.html || {};
\r
1229 tinymce.html.Entities = {
\r
1230 encodeRaw : function(text, attr) {
\r
1231 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
\r
1232 return baseEntities[chr] || chr;
\r
1236 encodeAllRaw : function(text) {
\r
1237 return ('' + text).replace(rawCharsRegExp, function(chr) {
\r
1238 return baseEntities[chr] || chr;
\r
1242 encodeNumeric : function(text, attr) {
\r
1243 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
\r
1244 // Multi byte sequence convert it to a single entity
\r
1245 if (chr.length > 1)
\r
1246 return '&#' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';';
\r
1248 return baseEntities[chr] || '&#' + chr.charCodeAt(0) + ';';
\r
1252 encodeNamed : function(text, attr, entities) {
\r
1253 entities = entities || namedEntities;
\r
1255 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
\r
1256 return baseEntities[chr] || entities[chr] || chr;
\r
1260 getEncodeFunc : function(name, entities) {
\r
1261 var Entities = tinymce.html.Entities;
\r
1263 entities = buildEntitiesLookup(entities) || namedEntities;
\r
1265 function encodeNamedAndNumeric(text, attr) {
\r
1266 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
\r
1267 return baseEntities[chr] || entities[chr] || '&#' + chr.charCodeAt(0) + ';' || chr;
\r
1271 function encodeCustomNamed(text, attr) {
\r
1272 return Entities.encodeNamed(text, attr, entities);
\r
1275 // Replace + with , to be compatible with previous TinyMCE versions
\r
1276 name = tinymce.makeMap(name.replace(/\+/g, ','));
\r
1278 // Named and numeric encoder
\r
1279 if (name.named && name.numeric)
\r
1280 return encodeNamedAndNumeric;
\r
1286 return encodeCustomNamed;
\r
1288 return Entities.encodeNamed;
\r
1293 return Entities.encodeNumeric;
\r
1296 return Entities.encodeRaw;
\r
1299 decode : function(text) {
\r
1300 return text.replace(entityRegExp, function(all, numeric, value) {
\r
1302 value = parseInt(value, numeric.length === 2 ? 16 : 10);
\r
1304 // Support upper UTF
\r
1305 if (value > 0xFFFF) {
\r
1308 return String.fromCharCode(0xD800 + (value >> 10), 0xDC00 + (value & 0x3FF));
\r
1310 return asciiMap[value] || String.fromCharCode(value);
\r
1313 return reverseEntities[all] || namedEntities[all] || nativeDecode(all);
\r
1319 tinymce.html.Styles = function(settings, schema) {
\r
1320 var rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi,
\r
1321 urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi,
\r
1322 styleRegExp = /\s*([^:]+):\s*([^;]+);?/g,
\r
1323 trimRightRegExp = /\s+$/,
\r
1324 urlColorRegExp = /rgb/,
\r
1325 undef, i, encodingLookup = {}, encodingItems;
\r
1327 settings = settings || {};
\r
1329 encodingItems = '\\" \\\' \\; \\: ; : \uFEFF'.split(' ');
\r
1330 for (i = 0; i < encodingItems.length; i++) {
\r
1331 encodingLookup[encodingItems[i]] = '\uFEFF' + i;
\r
1332 encodingLookup['\uFEFF' + i] = encodingItems[i];
\r
1335 function toHex(match, r, g, b) {
\r
1336 function hex(val) {
\r
1337 val = parseInt(val).toString(16);
\r
1339 return val.length > 1 ? val : '0' + val; // 0 -> 00
\r
1342 return '#' + hex(r) + hex(g) + hex(b);
\r
1346 toHex : function(color) {
\r
1347 return color.replace(rgbRegExp, toHex);
\r
1350 parse : function(css) {
\r
1351 var styles = {}, matches, name, value, isEncoded, urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope || this;
\r
1353 function compress(prefix, suffix) {
\r
1354 var top, right, bottom, left;
\r
1356 // Get values and check it it needs compressing
\r
1357 top = styles[prefix + '-top' + suffix];
\r
1361 right = styles[prefix + '-right' + suffix];
\r
1365 bottom = styles[prefix + '-bottom' + suffix];
\r
1366 if (right != bottom)
\r
1369 left = styles[prefix + '-left' + suffix];
\r
1370 if (bottom != left)
\r
1374 styles[prefix + suffix] = left;
\r
1375 delete styles[prefix + '-top' + suffix];
\r
1376 delete styles[prefix + '-right' + suffix];
\r
1377 delete styles[prefix + '-bottom' + suffix];
\r
1378 delete styles[prefix + '-left' + suffix];
\r
1381 function canCompress(key) {
\r
1382 var value = styles[key], i;
\r
1384 if (!value || value.indexOf(' ') < 0)
\r
1387 value = value.split(' ');
\r
1390 if (value[i] !== value[0])
\r
1394 styles[key] = value[0];
\r
1399 function compress2(target, a, b, c) {
\r
1400 if (!canCompress(a))
\r
1403 if (!canCompress(b))
\r
1406 if (!canCompress(c))
\r
1410 styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c];
\r
1416 // Encodes the specified string by replacing all \" \' ; : with _<num>
\r
1417 function encode(str) {
\r
1420 return encodingLookup[str];
\r
1423 // Decodes the specified string by replacing all _<num> with it's original value \" \' etc
\r
1424 // It will also decode the \" \' if keep_slashes is set to fale or omitted
\r
1425 function decode(str, keep_slashes) {
\r
1427 str = str.replace(/\uFEFF[0-9]/g, function(str) {
\r
1428 return encodingLookup[str];
\r
1432 if (!keep_slashes)
\r
1433 str = str.replace(/\\([\'\";:])/g, "$1");
\r
1439 // Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing
\r
1440 css = css.replace(/\\[\"\';:\uFEFF]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, function(str) {
\r
1441 return str.replace(/[;:]/g, encode);
\r
1445 while (matches = styleRegExp.exec(css)) {
\r
1446 name = matches[1].replace(trimRightRegExp, '').toLowerCase();
\r
1447 value = matches[2].replace(trimRightRegExp, '');
\r
1449 if (name && value.length > 0) {
\r
1450 // Opera will produce 700 instead of bold in their style values
\r
1451 if (name === 'font-weight' && value === '700')
\r
1453 else if (name === 'color' || name === 'background-color') // Lowercase colors like RED
\r
1454 value = value.toLowerCase();
\r
1456 // Convert RGB colors to HEX
\r
1457 value = value.replace(rgbRegExp, toHex);
\r
1459 // Convert URLs and force them into url('value') format
\r
1460 value = value.replace(urlOrStrRegExp, function(match, url, url2, url3, str, str2) {
\r
1461 str = str || str2;
\r
1464 str = decode(str);
\r
1466 // Force strings into single quote format
\r
1467 return "'" + str.replace(/\'/g, "\\'") + "'";
\r
1470 url = decode(url || url2 || url3);
\r
1472 // Convert the URL to relative/absolute depending on config
\r
1474 url = urlConverter.call(urlConverterScope, url, 'style');
\r
1476 // Output new URL format
\r
1477 return "url('" + url.replace(/\'/g, "\\'") + "')";
\r
1480 styles[name] = isEncoded ? decode(value, true) : value;
\r
1483 styleRegExp.lastIndex = matches.index + matches[0].length;
\r
1486 // Compress the styles to reduce it's size for example IE will expand styles
\r
1487 compress("border", "");
\r
1488 compress("border", "-width");
\r
1489 compress("border", "-color");
\r
1490 compress("border", "-style");
\r
1491 compress("padding", "");
\r
1492 compress("margin", "");
\r
1493 compress2('border', 'border-width', 'border-style', 'border-color');
\r
1495 // Remove pointless border, IE produces these
\r
1496 if (styles.border === 'medium none')
\r
1497 delete styles.border;
\r
1503 serialize : function(styles, element_name) {
\r
1504 var css = '', name, value;
\r
1506 function serializeStyles(name) {
\r
1507 var styleList, i, l, value;
\r
1509 styleList = schema.styles[name];
\r
1511 for (i = 0, l = styleList.length; i < l; i++) {
\r
1512 name = styleList[i];
\r
1513 value = styles[name];
\r
1515 if (value !== undef && value.length > 0)
\r
1516 css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';';
\r
1521 // Serialize styles according to schema
\r
1522 if (element_name && schema && schema.styles) {
\r
1523 // Serialize global styles and element specific styles
\r
1524 serializeStyles('*');
\r
1525 serializeStyles(element_name);
\r
1527 // Output the styles in the order they are inside the object
\r
1528 for (name in styles) {
\r
1529 value = styles[name];
\r
1531 if (value !== undef && value.length > 0)
\r
1532 css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';';
\r
1541 (function(tinymce) {
\r
1542 var transitional = {}, boolAttrMap, blockElementsMap, shortEndedElementsMap, nonEmptyElementsMap, customElementsMap = {},
\r
1543 defaultWhiteSpaceElementsMap, selfClosingElementsMap, makeMap = tinymce.makeMap, each = tinymce.each;
\r
1545 function split(str, delim) {
\r
1546 return str.split(delim || ',');
\r
1549 function unpack(lookup, data) {
\r
1550 var key, elements = {};
\r
1552 function replace(value) {
\r
1553 return value.replace(/[A-Z]+/g, function(key) {
\r
1554 return replace(lookup[key]);
\r
1559 for (key in lookup) {
\r
1560 if (lookup.hasOwnProperty(key))
\r
1561 lookup[key] = replace(lookup[key]);
\r
1564 // Unpack and parse data into object map
\r
1565 replace(data).replace(/#/g, '#text').replace(/(\w+)\[([^\]]+)\]\[([^\]]*)\]/g, function(str, name, attributes, children) {
\r
1566 attributes = split(attributes, '|');
\r
1568 elements[name] = {
\r
1569 attributes : makeMap(attributes),
\r
1570 attributesOrder : attributes,
\r
1571 children : makeMap(children, '|', {'#comment' : {}})
\r
1578 // Build a lookup table for block elements both lowercase and uppercase
\r
1579 blockElementsMap = 'h1,h2,h3,h4,h5,h6,hr,p,div,address,pre,form,table,tbody,thead,tfoot,' +
\r
1580 'th,tr,td,li,ol,ul,caption,blockquote,center,dl,dt,dd,dir,fieldset,' +
\r
1581 'noscript,menu,isindex,samp,header,footer,article,section,hgroup';
\r
1582 blockElementsMap = makeMap(blockElementsMap, ',', makeMap(blockElementsMap.toUpperCase()));
\r
1584 // This is the XHTML 1.0 transitional elements with it's attributes and children packed to reduce it's size
\r
1585 transitional = unpack({
\r
1588 ZG : 'E|span|width|align|char|charoff|valign',
\r
1589 X : 'p|T|div|U|W|isindex|fieldset|table',
\r
1590 ZF : 'E|align|char|charoff|valign',
\r
1591 W : 'pre|hr|blockquote|address|center|noframes',
\r
1592 ZE : 'abbr|axis|headers|scope|rowspan|colspan|align|char|charoff|valign|nowrap|bgcolor|width|height',
\r
1594 U : 'ul|ol|dl|menu|dir',
\r
1595 ZC : 'p|Y|div|U|W|table|br|span|bdo|object|applet|img|map|K|N|Q',
\r
1596 T : 'h1|h2|h3|h4|h5|h6',
\r
1599 ZA : 'a|G|J|M|O|P',
\r
1602 P : 'ins|del|script',
\r
1603 O : 'input|select|textarea|label|button',
\r
1605 M : 'em|strong|dfn|code|q|samp|kbd|var|cite|abbr|acronym',
\r
1608 J : 'tt|i|b|u|s|strike',
\r
1609 I : 'big|small|font|basefont',
\r
1611 G : 'br|span|bdo',
\r
1612 F : 'object|applet|img|map|iframe',
\r
1614 D : 'accesskey|tabindex|onfocus|onblur',
\r
1615 C : 'onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup',
\r
1616 B : 'lang|xml:lang|dir',
\r
1617 A : 'id|class|style|title'
\r
1618 }, 'script[id|charset|type|language|src|defer|xml:space][]' +
\r
1619 'style[B|id|type|media|title|xml:space][]' +
\r
1620 'object[E|declare|classid|codebase|data|type|codetype|archive|standby|width|height|usemap|name|tabindex|align|border|hspace|vspace][#|param|Y]' +
\r
1621 'param[id|name|value|valuetype|type][]' +
\r
1622 'p[E|align][#|S]' +
\r
1623 'a[E|D|charset|type|name|href|hreflang|rel|rev|shape|coords|target][#|Z]' +
\r
1624 'br[A|clear][]' +
\r
1626 'bdo[A|C|B][#|S]' +
\r
1627 'applet[A|codebase|archive|code|object|alt|name|width|height|align|hspace|vspace][#|param|Y]' +
\r
1628 'h1[E|align][#|S]' +
\r
1629 'img[E|src|alt|name|longdesc|width|height|usemap|ismap|align|border|hspace|vspace][]' +
\r
1630 'map[B|C|A|name][X|form|Q|area]' +
\r
1631 'h2[E|align][#|S]' +
\r
1632 'iframe[A|longdesc|name|src|frameborder|marginwidth|marginheight|scrolling|align|width|height][#|Y]' +
\r
1633 'h3[E|align][#|S]' +
\r
1639 'strike[E][#|S]' +
\r
1641 'small[E][#|S]' +
\r
1642 'font[A|B|size|color|face][#|S]' +
\r
1643 'basefont[id|size|color|face][]' +
\r
1645 'strong[E][#|S]' +
\r
1648 'q[E|cite][#|S]' +
\r
1654 'acronym[E][#|S]' +
\r
1657 'input[E|D|type|name|value|checked|disabled|readonly|size|maxlength|src|alt|usemap|onselect|onchange|accept|align][]' +
\r
1658 'select[E|name|size|multiple|disabled|tabindex|onfocus|onblur|onchange][optgroup|option]' +
\r
1659 'optgroup[E|disabled|label][option]' +
\r
1660 'option[E|selected|disabled|label|value][]' +
\r
1661 'textarea[E|D|name|rows|cols|disabled|readonly|onselect|onchange][]' +
\r
1662 'label[E|for|accesskey|onfocus|onblur][#|S]' +
\r
1663 'button[E|D|name|value|type|disabled][#|p|T|div|U|W|table|G|object|applet|img|map|K|N|Q]' +
\r
1664 'h4[E|align][#|S]' +
\r
1665 'ins[E|cite|datetime][#|Y]' +
\r
1666 'h5[E|align][#|S]' +
\r
1667 'del[E|cite|datetime][#|Y]' +
\r
1668 'h6[E|align][#|S]' +
\r
1669 'div[E|align][#|Y]' +
\r
1670 'ul[E|type|compact][li]' +
\r
1671 'li[E|type|value][#|Y]' +
\r
1672 'ol[E|type|compact|start][li]' +
\r
1673 'dl[E|compact][dt|dd]' +
\r
1676 'menu[E|compact][li]' +
\r
1677 'dir[E|compact][li]' +
\r
1678 'pre[E|width|xml:space][#|ZA]' +
\r
1679 'hr[E|align|noshade|size|width][]' +
\r
1680 'blockquote[E|cite][#|Y]' +
\r
1681 'address[E][#|S|p]' +
\r
1682 'center[E][#|Y]' +
\r
1683 'noframes[E][#|Y]' +
\r
1684 'isindex[A|B|prompt][]' +
\r
1685 'fieldset[E][#|legend|Y]' +
\r
1686 'legend[E|accesskey|align][#|S]' +
\r
1687 'table[E|summary|width|border|frame|rules|cellspacing|cellpadding|align|bgcolor][caption|col|colgroup|thead|tfoot|tbody|tr]' +
\r
1688 'caption[E|align][#|S]' +
\r
1690 'colgroup[ZG][col]' +
\r
1691 'thead[ZF][tr]' +
\r
1692 'tr[ZF|bgcolor][th|td]' +
\r
1693 'th[E|ZE][#|Y]' +
\r
1694 'form[E|action|method|name|enctype|onsubmit|onreset|accept|accept-charset|target][#|X|R|Q]' +
\r
1695 'noscript[E][#|Y]' +
\r
1696 'td[E|ZE][#|Y]' +
\r
1697 'tfoot[ZF][tr]' +
\r
1698 'tbody[ZF][tr]' +
\r
1699 'area[E|D|shape|coords|href|nohref|alt|target][]' +
\r
1700 'base[id|href|target][]' +
\r
1701 'body[E|onload|onunload|background|bgcolor|text|link|vlink|alink][#|Y]'
\r
1704 boolAttrMap = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected,autoplay,loop,controls');
\r
1705 shortEndedElementsMap = makeMap('area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed,source');
\r
1706 nonEmptyElementsMap = tinymce.extend(makeMap('td,th,iframe,video,audio,object'), shortEndedElementsMap);
\r
1707 defaultWhiteSpaceElementsMap = makeMap('pre,script,style,textarea');
\r
1708 selfClosingElementsMap = makeMap('colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr');
\r
1710 tinymce.html.Schema = function(settings) {
\r
1711 var self = this, elements = {}, children = {}, patternElements = [], validStyles, whiteSpaceElementsMap;
\r
1713 settings = settings || {};
\r
1715 // Allow all elements and attributes if verify_html is set to false
\r
1716 if (settings.verify_html === false)
\r
1717 settings.valid_elements = '*[*]';
\r
1719 // Build styles list
\r
1720 if (settings.valid_styles) {
\r
1723 // Convert styles into a rule list
\r
1724 each(settings.valid_styles, function(value, key) {
\r
1725 validStyles[key] = tinymce.explode(value);
\r
1729 whiteSpaceElementsMap = settings.whitespace_elements ? makeMap(settings.whitespace_elements) : defaultWhiteSpaceElementsMap;
\r
1731 // Converts a wildcard expression string to a regexp for example *a will become /.*a/.
\r
1732 function patternToRegExp(str) {
\r
1733 return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$');
\r
1736 // Parses the specified valid_elements string and adds to the current rules
\r
1737 // This function is a bit hard to read since it's heavily optimized for speed
\r
1738 function addValidElements(valid_elements) {
\r
1739 var ei, el, ai, al, yl, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder,
\r
1740 prefix, outputName, globalAttributes, globalAttributesOrder, transElement, key, childKey, value,
\r
1741 elementRuleRegExp = /^([#+-])?([^\[\/]+)(?:\/([^\[]+))?(?:\[([^\]]+)\])?$/,
\r
1742 attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/,
\r
1743 hasPatternsRegExp = /[*?+]/;
\r
1745 if (valid_elements) {
\r
1746 // Split valid elements into an array with rules
\r
1747 valid_elements = split(valid_elements);
\r
1749 if (elements['@']) {
\r
1750 globalAttributes = elements['@'].attributes;
\r
1751 globalAttributesOrder = elements['@'].attributesOrder;
\r
1755 for (ei = 0, el = valid_elements.length; ei < el; ei++) {
\r
1756 // Parse element rule
\r
1757 matches = elementRuleRegExp.exec(valid_elements[ei]);
\r
1759 // Setup local names for matches
\r
1760 prefix = matches[1];
\r
1761 elementName = matches[2];
\r
1762 outputName = matches[3];
\r
1763 attrData = matches[4];
\r
1765 // Create new attributes and attributesOrder
\r
1767 attributesOrder = [];
\r
1769 // Create the new element
\r
1771 attributes : attributes,
\r
1772 attributesOrder : attributesOrder
\r
1775 // Padd empty elements prefix
\r
1776 if (prefix === '#')
\r
1777 element.paddEmpty = true;
\r
1779 // Remove empty elements prefix
\r
1780 if (prefix === '-')
\r
1781 element.removeEmpty = true;
\r
1783 // Copy attributes from global rule into current rule
\r
1784 if (globalAttributes) {
\r
1785 for (key in globalAttributes)
\r
1786 attributes[key] = globalAttributes[key];
\r
1788 attributesOrder.push.apply(attributesOrder, globalAttributesOrder);
\r
1791 // Attributes defined
\r
1793 attrData = split(attrData, '|');
\r
1794 for (ai = 0, al = attrData.length; ai < al; ai++) {
\r
1795 matches = attrRuleRegExp.exec(attrData[ai]);
\r
1798 attrType = matches[1];
\r
1799 attrName = matches[2].replace(/::/g, ':');
\r
1800 prefix = matches[3];
\r
1801 value = matches[4];
\r
1804 if (attrType === '!') {
\r
1805 element.attributesRequired = element.attributesRequired || [];
\r
1806 element.attributesRequired.push(attrName);
\r
1807 attr.required = true;
\r
1810 // Denied from global
\r
1811 if (attrType === '-') {
\r
1812 delete attributes[attrName];
\r
1813 attributesOrder.splice(tinymce.inArray(attributesOrder, attrName), 1);
\r
1820 if (prefix === '=') {
\r
1821 element.attributesDefault = element.attributesDefault || [];
\r
1822 element.attributesDefault.push({name: attrName, value: value});
\r
1823 attr.defaultValue = value;
\r
1827 if (prefix === ':') {
\r
1828 element.attributesForced = element.attributesForced || [];
\r
1829 element.attributesForced.push({name: attrName, value: value});
\r
1830 attr.forcedValue = value;
\r
1833 // Required values
\r
1834 if (prefix === '<')
\r
1835 attr.validValues = makeMap(value, '?');
\r
1838 // Check for attribute patterns
\r
1839 if (hasPatternsRegExp.test(attrName)) {
\r
1840 element.attributePatterns = element.attributePatterns || [];
\r
1841 attr.pattern = patternToRegExp(attrName);
\r
1842 element.attributePatterns.push(attr);
\r
1844 // Add attribute to order list if it doesn't already exist
\r
1845 if (!attributes[attrName])
\r
1846 attributesOrder.push(attrName);
\r
1848 attributes[attrName] = attr;
\r
1854 // Global rule, store away these for later usage
\r
1855 if (!globalAttributes && elementName == '@') {
\r
1856 globalAttributes = attributes;
\r
1857 globalAttributesOrder = attributesOrder;
\r
1860 // Handle substitute elements such as b/strong
\r
1862 element.outputName = elementName;
\r
1863 elements[outputName] = element;
\r
1866 // Add pattern or exact element
\r
1867 if (hasPatternsRegExp.test(elementName)) {
\r
1868 element.pattern = patternToRegExp(elementName);
\r
1869 patternElements.push(element);
\r
1871 elements[elementName] = element;
\r
1877 function setValidElements(valid_elements) {
\r
1879 patternElements = [];
\r
1881 addValidElements(valid_elements);
\r
1883 each(transitional, function(element, name) {
\r
1884 children[name] = element.children;
\r
1888 // Adds custom non HTML elements to the schema
\r
1889 function addCustomElements(custom_elements) {
\r
1890 var customElementRegExp = /^(~)?(.+)$/;
\r
1892 if (custom_elements) {
\r
1893 each(split(custom_elements), function(rule) {
\r
1894 var matches = customElementRegExp.exec(rule),
\r
1895 inline = matches[1] === '~',
\r
1896 cloneName = inline ? 'span' : 'div',
\r
1897 name = matches[2];
\r
1899 children[name] = children[cloneName];
\r
1900 customElementsMap[name] = cloneName;
\r
1902 // If it's not marked as inline then add it to valid block elements
\r
1904 blockElementsMap[name] = {};
\r
1906 // Add custom elements at span/div positions
\r
1907 each(children, function(element, child) {
\r
1908 if (element[cloneName])
\r
1909 element[name] = element[cloneName];
\r
1915 // Adds valid children to the schema object
\r
1916 function addValidChildren(valid_children) {
\r
1917 var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/;
\r
1919 if (valid_children) {
\r
1920 each(split(valid_children), function(rule) {
\r
1921 var matches = childRuleRegExp.exec(rule), parent, prefix;
\r
1924 prefix = matches[1];
\r
1926 // Add/remove items from default
\r
1928 parent = children[matches[2]];
\r
1930 parent = children[matches[2]] = {'#comment' : {}};
\r
1932 parent = children[matches[2]];
\r
1934 each(split(matches[3], '|'), function(child) {
\r
1935 if (prefix === '-')
\r
1936 delete parent[child];
\r
1938 parent[child] = {};
\r
1945 function getElementRule(name) {
\r
1946 var element = elements[name], i;
\r
1948 // Exact match found
\r
1952 // No exact match then try the patterns
\r
1953 i = patternElements.length;
\r
1955 element = patternElements[i];
\r
1957 if (element.pattern.test(name))
\r
1962 if (!settings.valid_elements) {
\r
1963 // No valid elements defined then clone the elements from the transitional spec
\r
1964 each(transitional, function(element, name) {
\r
1965 elements[name] = {
\r
1966 attributes : element.attributes,
\r
1967 attributesOrder : element.attributesOrder
\r
1970 children[name] = element.children;
\r
1974 each(split('strong/b,em/i'), function(item) {
\r
1975 item = split(item, '/');
\r
1976 elements[item[1]].outputName = item[0];
\r
1979 // Add default alt attribute for images
\r
1980 elements.img.attributesDefault = [{name: 'alt', value: ''}];
\r
1982 // Remove these if they are empty by default
\r
1983 each(split('ol,ul,sub,sup,blockquote,span,font,a,table,tbody,tr'), function(name) {
\r
1984 elements[name].removeEmpty = true;
\r
1987 // Padd these by default
\r
1988 each(split('p,h1,h2,h3,h4,h5,h6,th,td,pre,div,address,caption'), function(name) {
\r
1989 elements[name].paddEmpty = true;
\r
1992 setValidElements(settings.valid_elements);
\r
1994 addCustomElements(settings.custom_elements);
\r
1995 addValidChildren(settings.valid_children);
\r
1996 addValidElements(settings.extended_valid_elements);
\r
1998 // Todo: Remove this when we fix list handling to be valid
\r
1999 addValidChildren('+ol[ul|ol],+ul[ul|ol]');
\r
2001 // If the user didn't allow span only allow internal spans
\r
2002 if (!getElementRule('span'))
\r
2003 addValidElements('span[!data-mce-type|*]');
\r
2005 // Delete invalid elements
\r
2006 if (settings.invalid_elements) {
\r
2007 tinymce.each(tinymce.explode(settings.invalid_elements), function(item) {
\r
2008 if (elements[item])
\r
2009 delete elements[item];
\r
2013 self.children = children;
\r
2015 self.styles = validStyles;
\r
2017 self.getBoolAttrs = function() {
\r
2018 return boolAttrMap;
\r
2021 self.getBlockElements = function() {
\r
2022 return blockElementsMap;
\r
2025 self.getShortEndedElements = function() {
\r
2026 return shortEndedElementsMap;
\r
2029 self.getSelfClosingElements = function() {
\r
2030 return selfClosingElementsMap;
\r
2033 self.getNonEmptyElements = function() {
\r
2034 return nonEmptyElementsMap;
\r
2037 self.getWhiteSpaceElements = function() {
\r
2038 return whiteSpaceElementsMap;
\r
2041 self.isValidChild = function(name, child) {
\r
2042 var parent = children[name];
\r
2044 return !!(parent && parent[child]);
\r
2047 self.getElementRule = getElementRule;
\r
2049 self.getCustomElements = function() {
\r
2050 return customElementsMap;
\r
2053 self.addValidElements = addValidElements;
\r
2055 self.setValidElements = setValidElements;
\r
2057 self.addCustomElements = addCustomElements;
\r
2059 self.addValidChildren = addValidChildren;
\r
2062 // Expose boolMap and blockElementMap as static properties for usage in DOMUtils
\r
2063 tinymce.html.Schema.boolAttrMap = boolAttrMap;
\r
2064 tinymce.html.Schema.blockElementsMap = blockElementsMap;
\r
2067 (function(tinymce) {
\r
2068 tinymce.html.SaxParser = function(settings, schema) {
\r
2069 var self = this, noop = function() {};
\r
2071 settings = settings || {};
\r
2072 self.schema = schema = schema || new tinymce.html.Schema();
\r
2074 if (settings.fix_self_closing !== false)
\r
2075 settings.fix_self_closing = true;
\r
2077 // Add handler functions from settings and setup default handlers
\r
2078 tinymce.each('comment cdata text start end pi doctype'.split(' '), function(name) {
\r
2080 self[name] = settings[name] || noop;
\r
2083 self.parse = function(html) {
\r
2084 var self = this, matches, index = 0, value, endRegExp, stack = [], attrList, i, text, name, isInternalElement, removeInternalElements,
\r
2085 shortEndedElements, fillAttrsMap, isShortEnded, validate, elementRule, isValidElement, attr, attribsValue, invalidPrefixRegExp,
\r
2086 validAttributesMap, validAttributePatterns, attributesRequired, attributesDefault, attributesForced, selfClosing,
\r
2087 tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0, decode = tinymce.html.Entities.decode, fixSelfClosing, isIE;
\r
2089 function processEndTag(name) {
\r
2092 // Find position of parent of the same type
\r
2093 pos = stack.length;
\r
2095 if (stack[pos].name === name)
\r
2101 // Close all the open elements
\r
2102 for (i = stack.length - 1; i >= pos; i--) {
\r
2106 self.end(name.name);
\r
2109 // Remove the open elements from the stack
\r
2110 stack.length = pos;
\r
2114 // Precompile RegExps and map objects
\r
2115 tokenRegExp = new RegExp('<(?:' +
\r
2116 '(?:!--([\\w\\W]*?)-->)|' + // Comment
\r
2117 '(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|' + // CDATA
\r
2118 '(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE
\r
2119 '(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI
\r
2120 '(?:\\/([^>]+)>)|' + // End element
\r
2121 '(?:([^\\s\\/<>]+)\\s*((?:[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*)>)' + // Start element
\r
2124 attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:\\.|[^\"])*)\")|(?:\'((?:\\.|[^\'])*)\')|([^>\s]+)))?/g;
\r
2125 specialElements = {
\r
2126 'script' : /<\/script[^>]*>/gi,
\r
2127 'style' : /<\/style[^>]*>/gi,
\r
2128 'noscript' : /<\/noscript[^>]*>/gi
\r
2131 // Setup lookup tables for empty elements and boolean attributes
\r
2132 shortEndedElements = schema.getShortEndedElements();
\r
2133 selfClosing = schema.getSelfClosingElements();
\r
2134 fillAttrsMap = schema.getBoolAttrs();
\r
2135 validate = settings.validate;
\r
2136 removeInternalElements = settings.remove_internals;
\r
2137 fixSelfClosing = settings.fix_self_closing;
\r
2138 isIE = tinymce.isIE;
\r
2139 invalidPrefixRegExp = /^:/;
\r
2141 while (matches = tokenRegExp.exec(html)) {
\r
2143 if (index < matches.index)
\r
2144 self.text(decode(html.substr(index, matches.index - index)));
\r
2146 if (value = matches[6]) { // End element
\r
2147 value = value.toLowerCase();
\r
2149 // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements
\r
2150 if (isIE && invalidPrefixRegExp.test(value))
\r
2151 value = value.substr(1);
\r
2153 processEndTag(value);
\r
2154 } else if (value = matches[7]) { // Start element
\r
2155 value = value.toLowerCase();
\r
2157 // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements
\r
2158 if (isIE && invalidPrefixRegExp.test(value))
\r
2159 value = value.substr(1);
\r
2161 isShortEnded = value in shortEndedElements;
\r
2163 // Is self closing tag for example an <li> after an open <li>
\r
2164 if (fixSelfClosing && selfClosing[value] && stack.length > 0 && stack[stack.length - 1].name === value)
\r
2165 processEndTag(value);
\r
2167 // Validate element
\r
2168 if (!validate || (elementRule = schema.getElementRule(value))) {
\r
2169 isValidElement = true;
\r
2171 // Grab attributes map and patters when validation is enabled
\r
2173 validAttributesMap = elementRule.attributes;
\r
2174 validAttributePatterns = elementRule.attributePatterns;
\r
2177 // Parse attributes
\r
2178 if (attribsValue = matches[8]) {
\r
2179 isInternalElement = attribsValue.indexOf('data-mce-type') !== -1; // Check if the element is an internal element
\r
2181 // If the element has internal attributes then remove it if we are told to do so
\r
2182 if (isInternalElement && removeInternalElements)
\r
2183 isValidElement = false;
\r
2186 attrList.map = {};
\r
2188 attribsValue.replace(attrRegExp, function(match, name, value, val2, val3) {
\r
2191 name = name.toLowerCase();
\r
2192 value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute
\r
2194 // Validate name and value
\r
2195 if (validate && !isInternalElement && name.indexOf('data-') !== 0) {
\r
2196 attrRule = validAttributesMap[name];
\r
2198 // Find rule by pattern matching
\r
2199 if (!attrRule && validAttributePatterns) {
\r
2200 i = validAttributePatterns.length;
\r
2202 attrRule = validAttributePatterns[i];
\r
2203 if (attrRule.pattern.test(name))
\r
2207 // No rule matched
\r
2212 // No attribute rule found
\r
2217 if (attrRule.validValues && !(value in attrRule.validValues))
\r
2221 // Add attribute to list and map
\r
2222 attrList.map[name] = value;
\r
2230 attrList.map = {};
\r
2233 // Process attributes if validation is enabled
\r
2234 if (validate && !isInternalElement) {
\r
2235 attributesRequired = elementRule.attributesRequired;
\r
2236 attributesDefault = elementRule.attributesDefault;
\r
2237 attributesForced = elementRule.attributesForced;
\r
2239 // Handle forced attributes
\r
2240 if (attributesForced) {
\r
2241 i = attributesForced.length;
\r
2243 attr = attributesForced[i];
\r
2245 attrValue = attr.value;
\r
2247 if (attrValue === '{$uid}')
\r
2248 attrValue = 'mce_' + idCount++;
\r
2250 attrList.map[name] = attrValue;
\r
2251 attrList.push({name: name, value: attrValue});
\r
2255 // Handle default attributes
\r
2256 if (attributesDefault) {
\r
2257 i = attributesDefault.length;
\r
2259 attr = attributesDefault[i];
\r
2262 if (!(name in attrList.map)) {
\r
2263 attrValue = attr.value;
\r
2265 if (attrValue === '{$uid}')
\r
2266 attrValue = 'mce_' + idCount++;
\r
2268 attrList.map[name] = attrValue;
\r
2269 attrList.push({name: name, value: attrValue});
\r
2274 // Handle required attributes
\r
2275 if (attributesRequired) {
\r
2276 i = attributesRequired.length;
\r
2278 if (attributesRequired[i] in attrList.map)
\r
2282 // None of the required attributes where found
\r
2284 isValidElement = false;
\r
2287 // Invalidate element if it's marked as bogus
\r
2288 if (attrList.map['data-mce-bogus'])
\r
2289 isValidElement = false;
\r
2292 if (isValidElement)
\r
2293 self.start(value, attrList, isShortEnded);
\r
2295 isValidElement = false;
\r
2297 // Treat script, noscript and style a bit different since they may include code that looks like elements
\r
2298 if (endRegExp = specialElements[value]) {
\r
2299 endRegExp.lastIndex = index = matches.index + matches[0].length;
\r
2301 if (matches = endRegExp.exec(html)) {
\r
2302 if (isValidElement)
\r
2303 text = html.substr(index, matches.index - index);
\r
2305 index = matches.index + matches[0].length;
\r
2307 text = html.substr(index);
\r
2308 index = html.length;
\r
2311 if (isValidElement && text.length > 0)
\r
2312 self.text(text, true);
\r
2314 if (isValidElement)
\r
2317 tokenRegExp.lastIndex = index;
\r
2321 // Push value on to stack
\r
2322 if (!isShortEnded) {
\r
2323 if (!attribsValue || attribsValue.indexOf('/') != attribsValue.length - 1)
\r
2324 stack.push({name: value, valid: isValidElement});
\r
2325 else if (isValidElement)
\r
2328 } else if (value = matches[1]) { // Comment
\r
2329 self.comment(value);
\r
2330 } else if (value = matches[2]) { // CDATA
\r
2331 self.cdata(value);
\r
2332 } else if (value = matches[3]) { // DOCTYPE
\r
2333 self.doctype(value);
\r
2334 } else if (value = matches[4]) { // PI
\r
2335 self.pi(value, matches[5]);
\r
2338 index = matches.index + matches[0].length;
\r
2342 if (index < html.length)
\r
2343 self.text(decode(html.substr(index)));
\r
2345 // Close any open elements
\r
2346 for (i = stack.length - 1; i >= 0; i--) {
\r
2350 self.end(value.name);
\r
2356 (function(tinymce) {
\r
2357 var whiteSpaceRegExp = /^[ \t\r\n]*$/, typeLookup = {
\r
2363 '#document-fragment' : 11
\r
2366 // Walks the tree left/right
\r
2367 function walk(node, root_node, prev) {
\r
2368 var sibling, parent, startName = prev ? 'lastChild' : 'firstChild', siblingName = prev ? 'prev' : 'next';
\r
2370 // Walk into nodes if it has a start
\r
2371 if (node[startName])
\r
2372 return node[startName];
\r
2374 // Return the sibling if it has one
\r
2375 if (node !== root_node) {
\r
2376 sibling = node[siblingName];
\r
2381 // Walk up the parents to look for siblings
\r
2382 for (parent = node.parent; parent && parent !== root_node; parent = parent.parent) {
\r
2383 sibling = parent[siblingName];
\r
2391 function Node(name, type) {
\r
2396 this.attributes = [];
\r
2397 this.attributes.map = {};
\r
2401 tinymce.extend(Node.prototype, {
\r
2402 replace : function(node) {
\r
2408 self.insert(node, self);
\r
2414 attr : function(name, value) {
\r
2415 var self = this, attrs, i, undef;
\r
2417 if (typeof name !== "string") {
\r
2419 self.attr(i, name[i]);
\r
2424 if (attrs = self.attributes) {
\r
2425 if (value !== undef) {
\r
2426 // Remove attribute
\r
2427 if (value === null) {
\r
2428 if (name in attrs.map) {
\r
2429 delete attrs.map[name];
\r
2433 if (attrs[i].name === name) {
\r
2434 attrs = attrs.splice(i, 1);
\r
2444 if (name in attrs.map) {
\r
2448 if (attrs[i].name === name) {
\r
2449 attrs[i].value = value;
\r
2454 attrs.push({name: name, value: value});
\r
2456 attrs.map[name] = value;
\r
2460 return attrs.map[name];
\r
2465 clone : function() {
\r
2466 var self = this, clone = new Node(self.name, self.type), i, l, selfAttrs, selfAttr, cloneAttrs;
\r
2468 // Clone element attributes
\r
2469 if (selfAttrs = self.attributes) {
\r
2471 cloneAttrs.map = {};
\r
2473 for (i = 0, l = selfAttrs.length; i < l; i++) {
\r
2474 selfAttr = selfAttrs[i];
\r
2476 // Clone everything except id
\r
2477 if (selfAttr.name !== 'id') {
\r
2478 cloneAttrs[cloneAttrs.length] = {name: selfAttr.name, value: selfAttr.value};
\r
2479 cloneAttrs.map[selfAttr.name] = selfAttr.value;
\r
2483 clone.attributes = cloneAttrs;
\r
2486 clone.value = self.value;
\r
2487 clone.shortEnded = self.shortEnded;
\r
2492 wrap : function(wrapper) {
\r
2495 self.parent.insert(wrapper, self);
\r
2496 wrapper.append(self);
\r
2501 unwrap : function() {
\r
2502 var self = this, node, next;
\r
2504 for (node = self.firstChild; node; ) {
\r
2506 self.insert(node, self, true);
\r
2513 remove : function() {
\r
2514 var self = this, parent = self.parent, next = self.next, prev = self.prev;
\r
2517 if (parent.firstChild === self) {
\r
2518 parent.firstChild = next;
\r
2526 if (parent.lastChild === self) {
\r
2527 parent.lastChild = prev;
\r
2535 self.parent = self.next = self.prev = null;
\r
2541 append : function(node) {
\r
2542 var self = this, last;
\r
2547 last = self.lastChild;
\r
2551 self.lastChild = node;
\r
2553 self.lastChild = self.firstChild = node;
\r
2555 node.parent = self;
\r
2560 insert : function(node, ref_node, before) {
\r
2566 parent = ref_node.parent || this;
\r
2569 if (ref_node === parent.firstChild)
\r
2570 parent.firstChild = node;
\r
2572 ref_node.prev.next = node;
\r
2574 node.prev = ref_node.prev;
\r
2575 node.next = ref_node;
\r
2576 ref_node.prev = node;
\r
2578 if (ref_node === parent.lastChild)
\r
2579 parent.lastChild = node;
\r
2581 ref_node.next.prev = node;
\r
2583 node.next = ref_node.next;
\r
2584 node.prev = ref_node;
\r
2585 ref_node.next = node;
\r
2588 node.parent = parent;
\r
2593 getAll : function(name) {
\r
2594 var self = this, node, collection = [];
\r
2596 for (node = self.firstChild; node; node = walk(node, self)) {
\r
2597 if (node.name === name)
\r
2598 collection.push(node);
\r
2601 return collection;
\r
2604 empty : function() {
\r
2605 var self = this, nodes, i, node;
\r
2607 // Remove all children
\r
2608 if (self.firstChild) {
\r
2611 // Collect the children
\r
2612 for (node = self.firstChild; node; node = walk(node, self))
\r
2615 // Remove the children
\r
2619 node.parent = node.firstChild = node.lastChild = node.next = node.prev = null;
\r
2623 self.firstChild = self.lastChild = null;
\r
2628 isEmpty : function(elements) {
\r
2629 var self = this, node = self.firstChild, i, name;
\r
2633 if (node.type === 1) {
\r
2634 // Ignore bogus elements
\r
2635 if (node.attributes.map['data-mce-bogus'])
\r
2638 // Keep empty elements like <img />
\r
2639 if (elements[node.name])
\r
2642 // Keep elements with data attributes or name attribute like <a name="1"></a>
\r
2643 i = node.attributes.length;
\r
2645 name = node.attributes[i].name;
\r
2646 if (name === "name" || name.indexOf('data-') === 0)
\r
2651 // Keep non whitespace text nodes
\r
2652 if ((node.type === 3 && !whiteSpaceRegExp.test(node.value)))
\r
2654 } while (node = walk(node, self));
\r
2660 walk : function(prev) {
\r
2661 return walk(this, null, prev);
\r
2665 tinymce.extend(Node, {
\r
2666 create : function(name, attrs) {
\r
2667 var node, attrName;
\r
2670 node = new Node(name, typeLookup[name] || 1);
\r
2672 // Add attributes if needed
\r
2674 for (attrName in attrs)
\r
2675 node.attr(attrName, attrs[attrName]);
\r
2682 tinymce.html.Node = Node;
\r
2685 (function(tinymce) {
\r
2686 var Node = tinymce.html.Node;
\r
2688 tinymce.html.DomParser = function(settings, schema) {
\r
2689 var self = this, nodeFilters = {}, attributeFilters = [], matchedNodes = {}, matchedAttributes = {};
\r
2691 settings = settings || {};
\r
2692 settings.validate = "validate" in settings ? settings.validate : true;
\r
2693 settings.root_name = settings.root_name || 'body';
\r
2694 self.schema = schema = schema || new tinymce.html.Schema();
\r
2696 function fixInvalidChildren(nodes) {
\r
2697 var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i,
\r
2698 childClone, nonEmptyElements, nonSplitableElements, sibling, nextNode;
\r
2700 nonSplitableElements = tinymce.makeMap('tr,td,th,tbody,thead,tfoot,table');
\r
2701 nonEmptyElements = schema.getNonEmptyElements();
\r
2703 for (ni = 0; ni < nodes.length; ni++) {
\r
2706 // Already removed
\r
2710 // Get list of all parent nodes until we find a valid parent to stick the child into
\r
2712 for (parent = node.parent; parent && !schema.isValidChild(parent.name, node.name) && !nonSplitableElements[parent.name]; parent = parent.parent)
\r
2713 parents.push(parent);
\r
2715 // Found a suitable parent
\r
2716 if (parent && parents.length > 1) {
\r
2717 // Reverse the array since it makes looping easier
\r
2718 parents.reverse();
\r
2720 // Clone the related parent and insert that after the moved node
\r
2721 newParent = currentNode = self.filterNode(parents[0].clone());
\r
2723 // Start cloning and moving children on the left side of the target node
\r
2724 for (i = 0; i < parents.length - 1; i++) {
\r
2725 if (schema.isValidChild(currentNode.name, parents[i].name)) {
\r
2726 tempNode = self.filterNode(parents[i].clone());
\r
2727 currentNode.append(tempNode);
\r
2729 tempNode = currentNode;
\r
2731 for (childNode = parents[i].firstChild; childNode && childNode != parents[i + 1]; ) {
\r
2732 nextNode = childNode.next;
\r
2733 tempNode.append(childNode);
\r
2734 childNode = nextNode;
\r
2737 currentNode = tempNode;
\r
2740 if (!newParent.isEmpty(nonEmptyElements)) {
\r
2741 parent.insert(newParent, parents[0], true);
\r
2742 parent.insert(node, newParent);
\r
2744 parent.insert(node, parents[0], true);
\r
2747 // Check if the element is empty by looking through it's contents and special treatment for <p><br /></p>
\r
2748 parent = parents[0];
\r
2749 if (parent.isEmpty(nonEmptyElements) || parent.firstChild === parent.lastChild && parent.firstChild.name === 'br') {
\r
2750 parent.empty().remove();
\r
2752 } else if (node.parent) {
\r
2753 // If it's an LI try to find a UL/OL for it or wrap it
\r
2754 if (node.name === 'li') {
\r
2755 sibling = node.prev;
\r
2756 if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) {
\r
2757 sibling.append(node);
\r
2761 sibling = node.next;
\r
2762 if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) {
\r
2763 sibling.insert(node, sibling.firstChild, true);
\r
2767 node.wrap(self.filterNode(new Node('ul', 1)));
\r
2771 // Try wrapping the element in a DIV
\r
2772 if (schema.isValidChild(node.parent.name, 'div') && schema.isValidChild('div', node.name)) {
\r
2773 node.wrap(self.filterNode(new Node('div', 1)));
\r
2775 // We failed wrapping it, then remove or unwrap it
\r
2776 if (node.name === 'style' || node.name === 'script')
\r
2777 node.empty().remove();
\r
2785 self.filterNode = function(node) {
\r
2786 var i, name, list;
\r
2788 // Run element filters
\r
2789 if (name in nodeFilters) {
\r
2790 list = matchedNodes[name];
\r
2795 matchedNodes[name] = [node];
\r
2798 // Run attribute filters
\r
2799 i = attributeFilters.length;
\r
2801 name = attributeFilters[i].name;
\r
2803 if (name in node.attributes.map) {
\r
2804 list = matchedAttributes[name];
\r
2809 matchedAttributes[name] = [node];
\r
2816 self.addNodeFilter = function(name, callback) {
\r
2817 tinymce.each(tinymce.explode(name), function(name) {
\r
2818 var list = nodeFilters[name];
\r
2821 nodeFilters[name] = list = [];
\r
2823 list.push(callback);
\r
2827 self.addAttributeFilter = function(name, callback) {
\r
2828 tinymce.each(tinymce.explode(name), function(name) {
\r
2831 for (i = 0; i < attributeFilters.length; i++) {
\r
2832 if (attributeFilters[i].name === name) {
\r
2833 attributeFilters[i].callbacks.push(callback);
\r
2838 attributeFilters.push({name: name, callbacks: [callback]});
\r
2842 self.parse = function(html, args) {
\r
2843 var parser, rootNode, node, nodes, i, l, fi, fl, list, name, validate,
\r
2844 blockElements, startWhiteSpaceRegExp, invalidChildren = [],
\r
2845 endWhiteSpaceRegExp, allWhiteSpaceRegExp, whiteSpaceElements, children, nonEmptyElements, rootBlockName;
\r
2847 args = args || {};
\r
2848 matchedNodes = {};
\r
2849 matchedAttributes = {};
\r
2850 blockElements = tinymce.extend(tinymce.makeMap('script,style,head,html,body,title,meta,param'), schema.getBlockElements());
\r
2851 nonEmptyElements = schema.getNonEmptyElements();
\r
2852 children = schema.children;
\r
2853 validate = settings.validate;
\r
2854 rootBlockName = "forced_root_block" in args ? args.forced_root_block : settings.forced_root_block;
\r
2856 whiteSpaceElements = schema.getWhiteSpaceElements();
\r
2857 startWhiteSpaceRegExp = /^[ \t\r\n]+/;
\r
2858 endWhiteSpaceRegExp = /[ \t\r\n]+$/;
\r
2859 allWhiteSpaceRegExp = /[ \t\r\n]+/g;
\r
2861 function addRootBlocks() {
\r
2862 var node = rootNode.firstChild, next, rootBlockNode;
\r
2867 if (node.type == 3 || (node.type == 1 && node.name !== 'p' && !blockElements[node.name] && !node.attr('data-mce-type'))) {
\r
2868 if (!rootBlockNode) {
\r
2869 // Create a new root block element
\r
2870 rootBlockNode = createNode(rootBlockName, 1);
\r
2871 rootNode.insert(rootBlockNode, node);
\r
2872 rootBlockNode.append(node);
\r
2874 rootBlockNode.append(node);
\r
2876 rootBlockNode = null;
\r
2883 function createNode(name, type) {
\r
2884 var node = new Node(name, type), list;
\r
2886 if (name in nodeFilters) {
\r
2887 list = matchedNodes[name];
\r
2892 matchedNodes[name] = [node];
\r
2898 function removeWhitespaceBefore(node) {
\r
2899 var textNode, textVal, sibling;
\r
2901 for (textNode = node.prev; textNode && textNode.type === 3; ) {
\r
2902 textVal = textNode.value.replace(endWhiteSpaceRegExp, '');
\r
2904 if (textVal.length > 0) {
\r
2905 textNode.value = textVal;
\r
2906 textNode = textNode.prev;
\r
2908 sibling = textNode.prev;
\r
2909 textNode.remove();
\r
2910 textNode = sibling;
\r
2915 parser = new tinymce.html.SaxParser({
\r
2916 validate : validate,
\r
2917 fix_self_closing : !validate, // Let the DOM parser handle <li> in <li> or <p> in <p> for better results
\r
2919 cdata: function(text) {
\r
2920 node.append(createNode('#cdata', 4)).value = text;
\r
2923 text: function(text, raw) {
\r
2926 // Trim all redundant whitespace on non white space elements
\r
2927 if (!whiteSpaceElements[node.name]) {
\r
2928 text = text.replace(allWhiteSpaceRegExp, ' ');
\r
2930 if (node.lastChild && blockElements[node.lastChild.name])
\r
2931 text = text.replace(startWhiteSpaceRegExp, '');
\r
2934 // Do we need to create the node
\r
2935 if (text.length !== 0) {
\r
2936 textNode = createNode('#text', 3);
\r
2937 textNode.raw = !!raw;
\r
2938 node.append(textNode).value = text;
\r
2942 comment: function(text) {
\r
2943 node.append(createNode('#comment', 8)).value = text;
\r
2946 pi: function(name, text) {
\r
2947 node.append(createNode(name, 7)).value = text;
\r
2948 removeWhitespaceBefore(node);
\r
2951 doctype: function(text) {
\r
2954 newNode = node.append(createNode('#doctype', 10));
\r
2955 newNode.value = text;
\r
2956 removeWhitespaceBefore(node);
\r
2959 start: function(name, attrs, empty) {
\r
2960 var newNode, attrFiltersLen, elementRule, textNode, attrName, text, sibling, parent;
\r
2962 elementRule = validate ? schema.getElementRule(name) : {};
\r
2963 if (elementRule) {
\r
2964 newNode = createNode(elementRule.outputName || name, 1);
\r
2965 newNode.attributes = attrs;
\r
2966 newNode.shortEnded = empty;
\r
2968 node.append(newNode);
\r
2970 // Check if node is valid child of the parent node is the child is
\r
2971 // unknown we don't collect it since it's probably a custom element
\r
2972 parent = children[node.name];
\r
2973 if (parent && children[newNode.name] && !parent[newNode.name])
\r
2974 invalidChildren.push(newNode);
\r
2976 attrFiltersLen = attributeFilters.length;
\r
2977 while (attrFiltersLen--) {
\r
2978 attrName = attributeFilters[attrFiltersLen].name;
\r
2980 if (attrName in attrs.map) {
\r
2981 list = matchedAttributes[attrName];
\r
2984 list.push(newNode);
\r
2986 matchedAttributes[attrName] = [newNode];
\r
2990 // Trim whitespace before block
\r
2991 if (blockElements[name])
\r
2992 removeWhitespaceBefore(newNode);
\r
2994 // Change current node if the element wasn't empty i.e not <br /> or <img />
\r
3000 end: function(name) {
\r
3001 var textNode, elementRule, text, sibling, tempNode;
\r
3003 elementRule = validate ? schema.getElementRule(name) : {};
\r
3004 if (elementRule) {
\r
3005 if (blockElements[name]) {
\r
3006 if (!whiteSpaceElements[node.name]) {
\r
3007 // Trim whitespace at beginning of block
\r
3008 for (textNode = node.firstChild; textNode && textNode.type === 3; ) {
\r
3009 text = textNode.value.replace(startWhiteSpaceRegExp, '');
\r
3011 if (text.length > 0) {
\r
3012 textNode.value = text;
\r
3013 textNode = textNode.next;
\r
3015 sibling = textNode.next;
\r
3016 textNode.remove();
\r
3017 textNode = sibling;
\r
3021 // Trim whitespace at end of block
\r
3022 for (textNode = node.lastChild; textNode && textNode.type === 3; ) {
\r
3023 text = textNode.value.replace(endWhiteSpaceRegExp, '');
\r
3025 if (text.length > 0) {
\r
3026 textNode.value = text;
\r
3027 textNode = textNode.prev;
\r
3029 sibling = textNode.prev;
\r
3030 textNode.remove();
\r
3031 textNode = sibling;
\r
3036 // Trim start white space
\r
3037 textNode = node.prev;
\r
3038 if (textNode && textNode.type === 3) {
\r
3039 text = textNode.value.replace(startWhiteSpaceRegExp, '');
\r
3041 if (text.length > 0)
\r
3042 textNode.value = text;
\r
3044 textNode.remove();
\r
3048 // Handle empty nodes
\r
3049 if (elementRule.removeEmpty || elementRule.paddEmpty) {
\r
3050 if (node.isEmpty(nonEmptyElements)) {
\r
3051 if (elementRule.paddEmpty)
\r
3052 node.empty().append(new Node('#text', '3')).value = '\u00a0';
\r
3054 // Leave nodes that have a name like <a name="name">
\r
3055 if (!node.attributes.map.name) {
\r
3056 tempNode = node.parent;
\r
3057 node.empty().remove();
\r
3065 node = node.parent;
\r
3070 rootNode = node = new Node(args.context || settings.root_name, 11);
\r
3072 parser.parse(html);
\r
3074 // Fix invalid children or report invalid children in a contextual parsing
\r
3075 if (validate && invalidChildren.length) {
\r
3076 if (!args.context)
\r
3077 fixInvalidChildren(invalidChildren);
\r
3079 args.invalid = true;
\r
3082 // Wrap nodes in the root into block elements if the root is body
\r
3083 if (rootBlockName && rootNode.name == 'body')
\r
3086 // Run filters only when the contents is valid
\r
3087 if (!args.invalid) {
\r
3088 // Run node filters
\r
3089 for (name in matchedNodes) {
\r
3090 list = nodeFilters[name];
\r
3091 nodes = matchedNodes[name];
\r
3093 // Remove already removed children
\r
3094 fi = nodes.length;
\r
3096 if (!nodes[fi].parent)
\r
3097 nodes.splice(fi, 1);
\r
3100 for (i = 0, l = list.length; i < l; i++)
\r
3101 list[i](nodes, name, args);
\r
3104 // Run attribute filters
\r
3105 for (i = 0, l = attributeFilters.length; i < l; i++) {
\r
3106 list = attributeFilters[i];
\r
3108 if (list.name in matchedAttributes) {
\r
3109 nodes = matchedAttributes[list.name];
\r
3111 // Remove already removed children
\r
3112 fi = nodes.length;
\r
3114 if (!nodes[fi].parent)
\r
3115 nodes.splice(fi, 1);
\r
3118 for (fi = 0, fl = list.callbacks.length; fi < fl; fi++)
\r
3119 list.callbacks[fi](nodes, list.name, args);
\r
3127 // Remove <br> at end of block elements Gecko and WebKit injects BR elements to
\r
3128 // make it possible to place the caret inside empty blocks. This logic tries to remove
\r
3129 // these elements and keep br elements that where intended to be there intact
\r
3130 if (settings.remove_trailing_brs) {
\r
3131 self.addNodeFilter('br', function(nodes, name) {
\r
3132 var i, l = nodes.length, node, blockElements = schema.getBlockElements(),
\r
3133 nonEmptyElements = schema.getNonEmptyElements(), parent, prev, prevName;
\r
3135 // Remove brs from body element as well
\r
3136 blockElements.body = 1;
\r
3138 // Must loop forwards since it will otherwise remove all brs in <p>a<br><br><br></p>
\r
3139 for (i = 0; i < l; i++) {
\r
3141 parent = node.parent;
\r
3143 if (blockElements[node.parent.name] && node === parent.lastChild) {
\r
3144 // Loop all nodes to the right of the current node and check for other BR elements
\r
3145 // excluding bookmarks since they are invisible
\r
3148 prevName = prev.name;
\r
3150 // Ignore bookmarks
\r
3151 if (prevName !== "span" || prev.attr('data-mce-type') !== 'bookmark') {
\r
3152 // Found a non BR element
\r
3153 if (prevName !== "br")
\r
3156 // Found another br it's a <br><br> structure then don't remove anything
\r
3157 if (prevName === 'br') {
\r
3169 // Is the parent to be considered empty after we removed the BR
\r
3170 if (parent.isEmpty(nonEmptyElements)) {
\r
3171 elementRule = schema.getElementRule(parent.name);
\r
3173 // Remove or padd the element depending on schema rule
\r
3174 if (elementRule) {
\r
3175 if (elementRule.removeEmpty)
\r
3177 else if (elementRule.paddEmpty)
\r
3178 parent.empty().append(new tinymce.html.Node('#text', 3)).value = '\u00a0';
\r
3189 tinymce.html.Writer = function(settings) {
\r
3190 var html = [], indent, indentBefore, indentAfter, encode, htmlOutput;
\r
3192 settings = settings || {};
\r
3193 indent = settings.indent;
\r
3194 indentBefore = tinymce.makeMap(settings.indent_before || '');
\r
3195 indentAfter = tinymce.makeMap(settings.indent_after || '');
\r
3196 encode = tinymce.html.Entities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities);
\r
3197 htmlOutput = settings.element_format == "html";
\r
3200 start: function(name, attrs, empty) {
\r
3201 var i, l, attr, value;
\r
3203 if (indent && indentBefore[name] && html.length > 0) {
\r
3204 value = html[html.length - 1];
\r
3206 if (value.length > 0 && value !== '\n')
\r
3210 html.push('<', name);
\r
3213 for (i = 0, l = attrs.length; i < l; i++) {
\r
3215 html.push(' ', attr.name, '="', encode(attr.value, true), '"');
\r
3219 if (!empty || htmlOutput)
\r
3220 html[html.length] = '>';
\r
3222 html[html.length] = ' />';
\r
3224 if (empty && indent && indentAfter[name] && html.length > 0) {
\r
3225 value = html[html.length - 1];
\r
3227 if (value.length > 0 && value !== '\n')
\r
3232 end: function(name) {
\r
3235 /*if (indent && indentBefore[name] && html.length > 0) {
\r
3236 value = html[html.length - 1];
\r
3238 if (value.length > 0 && value !== '\n')
\r
3242 html.push('</', name, '>');
\r
3244 if (indent && indentAfter[name] && html.length > 0) {
\r
3245 value = html[html.length - 1];
\r
3247 if (value.length > 0 && value !== '\n')
\r
3252 text: function(text, raw) {
\r
3253 if (text.length > 0)
\r
3254 html[html.length] = raw ? text : encode(text);
\r
3257 cdata: function(text) {
\r
3258 html.push('<![CDATA[', text, ']]>');
\r
3261 comment: function(text) {
\r
3262 html.push('<!--', text, '-->');
\r
3265 pi: function(name, text) {
\r
3267 html.push('<?', name, ' ', text, '?>');
\r
3269 html.push('<?', name, '?>');
\r
3275 doctype: function(text) {
\r
3276 html.push('<!DOCTYPE', text, '>', indent ? '\n' : '');
\r
3279 reset: function() {
\r
3283 getContent: function() {
\r
3284 return html.join('').replace(/\n$/, '');
\r
3289 (function(tinymce) {
\r
3290 tinymce.html.Serializer = function(settings, schema) {
\r
3291 var self = this, writer = new tinymce.html.Writer(settings);
\r
3293 settings = settings || {};
\r
3294 settings.validate = "validate" in settings ? settings.validate : true;
\r
3296 self.schema = schema = schema || new tinymce.html.Schema();
\r
3297 self.writer = writer;
\r
3299 self.serialize = function(node) {
\r
3300 var handlers, validate;
\r
3302 validate = settings.validate;
\r
3306 3: function(node, raw) {
\r
3307 writer.text(node.value, node.raw);
\r
3311 8: function(node) {
\r
3312 writer.comment(node.value);
\r
3315 // Processing instruction
\r
3316 7: function(node) {
\r
3317 writer.pi(node.name, node.value);
\r
3321 10: function(node) {
\r
3322 writer.doctype(node.value);
\r
3326 4: function(node) {
\r
3327 writer.cdata(node.value);
\r
3330 // Document fragment
\r
3331 11: function(node) {
\r
3332 if ((node = node.firstChild)) {
\r
3335 } while (node = node.next);
\r
3342 function walk(node) {
\r
3343 var handler = handlers[node.type], name, isEmpty, attrs, attrName, attrValue, sortedAttrs, i, l, elementRule;
\r
3347 isEmpty = node.shortEnded;
\r
3348 attrs = node.attributes;
\r
3350 // Sort attributes
\r
3351 if (validate && attrs && attrs.length > 1) {
\r
3353 sortedAttrs.map = {};
\r
3355 elementRule = schema.getElementRule(node.name);
\r
3356 for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) {
\r
3357 attrName = elementRule.attributesOrder[i];
\r
3359 if (attrName in attrs.map) {
\r
3360 attrValue = attrs.map[attrName];
\r
3361 sortedAttrs.map[attrName] = attrValue;
\r
3362 sortedAttrs.push({name: attrName, value: attrValue});
\r
3366 for (i = 0, l = attrs.length; i < l; i++) {
\r
3367 attrName = attrs[i].name;
\r
3369 if (!(attrName in sortedAttrs.map)) {
\r
3370 attrValue = attrs.map[attrName];
\r
3371 sortedAttrs.map[attrName] = attrValue;
\r
3372 sortedAttrs.push({name: attrName, value: attrValue});
\r
3376 attrs = sortedAttrs;
\r
3379 writer.start(node.name, attrs, isEmpty);
\r
3382 if ((node = node.firstChild)) {
\r
3385 } while (node = node.next);
\r
3394 // Serialize element and treat all non elements as fragments
\r
3395 if (node.type == 1 && !settings.inner)
\r
3398 handlers[11](node);
\r
3400 return writer.getContent();
\r
3405 (function(tinymce) {
\r
3407 var each = tinymce.each,
\r
3409 isWebKit = tinymce.isWebKit,
\r
3410 isIE = tinymce.isIE,
\r
3411 Entities = tinymce.html.Entities,
\r
3412 simpleSelectorRe = /^([a-z0-9],?)+$/i,
\r
3413 blockElementsMap = tinymce.html.Schema.blockElementsMap,
\r
3414 whiteSpaceRegExp = /^[ \t\r\n]*$/;
\r
3416 tinymce.create('tinymce.dom.DOMUtils', {
\r
3420 pixelStyles : /^(top|left|bottom|right|width|height|borderWidth)$/,
\r
3422 "for" : "htmlFor",
\r
3423 "class" : "className",
\r
3424 className : "className",
\r
3425 checked : "checked",
\r
3426 disabled : "disabled",
\r
3427 maxlength : "maxLength",
\r
3428 readonly : "readOnly",
\r
3429 selected : "selected",
\r
3436 DOMUtils : function(d, s) {
\r
3437 var t = this, globalStyle, name;
\r
3442 t.cssFlicker = false;
\r
3444 t.stdMode = !tinymce.isIE || d.documentMode >= 8;
\r
3445 t.boxModel = !tinymce.isIE || d.compatMode == "CSS1Compat" || t.stdMode;
\r
3446 t.hasOuterHTML = "outerHTML" in d.createElement("a");
\r
3448 t.settings = s = tinymce.extend({
\r
3449 keep_values : false,
\r
3453 t.schema = s.schema;
\r
3454 t.styles = new tinymce.html.Styles({
\r
3455 url_converter : s.url_converter,
\r
3456 url_converter_scope : s.url_converter_scope
\r
3459 // Fix IE6SP2 flicker and check it failed for pre SP2
\r
3460 if (tinymce.isIE6) {
\r
3462 d.execCommand('BackgroundImageCache', false, true);
\r
3464 t.cssFlicker = true;
\r
3468 if (isIE && s.schema) {
\r
3469 // Add missing HTML 4/5 elements to IE
\r
3470 ('abbr article aside audio canvas ' +
\r
3471 'details figcaption figure footer ' +
\r
3472 'header hgroup mark menu meter nav ' +
\r
3473 'output progress section summary ' +
\r
3474 'time video').replace(/\w+/g, function(name) {
\r
3475 d.createElement(name);
\r
3478 // Create all custom elements
\r
3479 for (name in s.schema.getCustomElements()) {
\r
3480 d.createElement(name);
\r
3484 tinymce.addUnload(t.destroy, t);
\r
3487 getRoot : function() {
\r
3488 var t = this, s = t.settings;
\r
3490 return (s && t.get(s.root_element)) || t.doc.body;
\r
3493 getViewPort : function(w) {
\r
3496 w = !w ? this.win : w;
\r
3498 b = this.boxModel ? d.documentElement : d.body;
\r
3500 // Returns viewport size excluding scrollbars
\r
3502 x : w.pageXOffset || b.scrollLeft,
\r
3503 y : w.pageYOffset || b.scrollTop,
\r
3504 w : w.innerWidth || b.clientWidth,
\r
3505 h : w.innerHeight || b.clientHeight
\r
3509 getRect : function(e) {
\r
3510 var p, t = this, sr;
\r
3514 sr = t.getSize(e);
\r
3524 getSize : function(e) {
\r
3525 var t = this, w, h;
\r
3528 w = t.getStyle(e, 'width');
\r
3529 h = t.getStyle(e, 'height');
\r
3531 // Non pixel value, then force offset/clientWidth
\r
3532 if (w.indexOf('px') === -1)
\r
3535 // Non pixel value, then force offset/clientWidth
\r
3536 if (h.indexOf('px') === -1)
\r
3540 w : parseInt(w) || e.offsetWidth || e.clientWidth,
\r
3541 h : parseInt(h) || e.offsetHeight || e.clientHeight
\r
3545 getParent : function(n, f, r) {
\r
3546 return this.getParents(n, f, r, false);
\r
3549 getParents : function(n, f, r, c) {
\r
3550 var t = this, na, se = t.settings, o = [];
\r
3553 c = c === undefined;
\r
3555 if (se.strict_root)
\r
3556 r = r || t.getRoot();
\r
3558 // Wrap node name as func
\r
3559 if (is(f, 'string')) {
\r
3563 f = function(n) {return n.nodeType == 1;};
\r
3566 return t.is(n, na);
\r
3572 if (n == r || !n.nodeType || n.nodeType === 9)
\r
3585 return c ? o : null;
\r
3588 get : function(e) {
\r
3591 if (e && this.doc && typeof(e) == 'string') {
\r
3593 e = this.doc.getElementById(e);
\r
3595 // IE and Opera returns meta elements when they match the specified input ID, but getElementsByName seems to do the trick
\r
3596 if (e && e.id !== n)
\r
3597 return this.doc.getElementsByName(n)[1];
\r
3603 getNext : function(node, selector) {
\r
3604 return this._findSib(node, selector, 'nextSibling');
\r
3607 getPrev : function(node, selector) {
\r
3608 return this._findSib(node, selector, 'previousSibling');
\r
3612 select : function(pa, s) {
\r
3615 return tinymce.dom.Sizzle(pa, t.get(s) || t.get(t.settings.root_element) || t.doc, []);
\r
3618 is : function(n, selector) {
\r
3621 // If it isn't an array then try to do some simple selectors instead of Sizzle for to boost performance
\r
3622 if (n.length === undefined) {
\r
3623 // Simple all selector
\r
3624 if (selector === '*')
\r
3625 return n.nodeType == 1;
\r
3627 // Simple selector just elements
\r
3628 if (simpleSelectorRe.test(selector)) {
\r
3629 selector = selector.toLowerCase().split(/,/);
\r
3630 n = n.nodeName.toLowerCase();
\r
3632 for (i = selector.length - 1; i >= 0; i--) {
\r
3633 if (selector[i] == n)
\r
3641 return tinymce.dom.Sizzle.matches(selector, n.nodeType ? [n] : n).length > 0;
\r
3645 add : function(p, n, a, h, c) {
\r
3648 return this.run(p, function(p) {
\r
3651 e = is(n, 'string') ? t.doc.createElement(n) : n;
\r
3652 t.setAttribs(e, a);
\r
3661 return !c ? p.appendChild(e) : e;
\r
3665 create : function(n, a, h) {
\r
3666 return this.add(this.doc.createElement(n), n, a, h, 1);
\r
3669 createHTML : function(n, a, h) {
\r
3670 var o = '', t = this, k;
\r
3675 if (a.hasOwnProperty(k))
\r
3676 o += ' ' + k + '="' + t.encode(a[k]) + '"';
\r
3679 // A call to tinymce.is doesn't work for some odd reason on IE9 possible bug inside their JS runtime
\r
3680 if (typeof(h) != "undefined")
\r
3681 return o + '>' + h + '</' + n + '>';
\r
3686 remove : function(node, keep_children) {
\r
3687 return this.run(node, function(node) {
\r
3688 var child, parent = node.parentNode;
\r
3693 if (keep_children) {
\r
3694 while (child = node.firstChild) {
\r
3695 // IE 8 will crash if you don't remove completely empty text nodes
\r
3696 if (!tinymce.isIE || child.nodeType !== 3 || child.nodeValue)
\r
3697 parent.insertBefore(child, node);
\r
3699 node.removeChild(child);
\r
3703 return parent.removeChild(node);
\r
3707 setStyle : function(n, na, v) {
\r
3710 return t.run(n, function(e) {
\r
3715 // Camelcase it, if needed
\r
3716 na = na.replace(/-(\D)/g, function(a, b){
\r
3717 return b.toUpperCase();
\r
3720 // Default px suffix on these
\r
3721 if (t.pixelStyles.test(na) && (tinymce.is(v, 'number') || /^[\-0-9\.]+$/.test(v)))
\r
3726 // IE specific opacity
\r
3728 s.filter = v === '' ? '' : "alpha(opacity=" + (v * 100) + ")";
\r
3730 if (!n.currentStyle || !n.currentStyle.hasLayout)
\r
3731 s.display = 'inline-block';
\r
3734 // Fix for older browsers
\r
3735 s[na] = s['-moz-opacity'] = s['-khtml-opacity'] = v || '';
\r
3739 isIE ? s.styleFloat = v : s.cssFloat = v;
\r
3746 // Force update of the style data
\r
3747 if (t.settings.update_styles)
\r
3748 t.setAttrib(e, 'data-mce-style');
\r
3752 getStyle : function(n, na, c) {
\r
3759 if (this.doc.defaultView && c) {
\r
3760 // Remove camelcase
\r
3761 na = na.replace(/[A-Z]/g, function(a){
\r
3766 return this.doc.defaultView.getComputedStyle(n, null).getPropertyValue(na);
\r
3768 // Old safari might fail
\r
3773 // Camelcase it, if needed
\r
3774 na = na.replace(/-(\D)/g, function(a, b){
\r
3775 return b.toUpperCase();
\r
3778 if (na == 'float')
\r
3779 na = isIE ? 'styleFloat' : 'cssFloat';
\r
3782 if (n.currentStyle && c)
\r
3783 return n.currentStyle[na];
\r
3785 return n.style ? n.style[na] : undefined;
\r
3788 setStyles : function(e, o) {
\r
3789 var t = this, s = t.settings, ol;
\r
3791 ol = s.update_styles;
\r
3792 s.update_styles = 0;
\r
3794 each(o, function(v, n) {
\r
3795 t.setStyle(e, n, v);
\r
3798 // Update style info
\r
3799 s.update_styles = ol;
\r
3800 if (s.update_styles)
\r
3801 t.setAttrib(e, s.cssText);
\r
3804 removeAllAttribs: function(e) {
\r
3805 return this.run(e, function(e) {
\r
3806 var i, attrs = e.attributes;
\r
3807 for (i = attrs.length - 1; i >= 0; i--) {
\r
3808 e.removeAttributeNode(attrs.item(i));
\r
3813 setAttrib : function(e, n, v) {
\r
3816 // Whats the point
\r
3820 // Strict XML mode
\r
3821 if (t.settings.strict)
\r
3822 n = n.toLowerCase();
\r
3824 return this.run(e, function(e) {
\r
3825 var s = t.settings;
\r
3829 if (!is(v, 'string')) {
\r
3830 each(v, function(v, n) {
\r
3831 t.setStyle(e, n, v);
\r
3837 // No mce_style for elements with these since they might get resized by the user
\r
3838 if (s.keep_values) {
\r
3839 if (v && !t._isRes(v))
\r
3840 e.setAttribute('data-mce-style', v, 2);
\r
3842 e.removeAttribute('data-mce-style', 2);
\r
3845 e.style.cssText = v;
\r
3849 e.className = v || ''; // Fix IE null bug
\r
3854 if (s.keep_values) {
\r
3855 if (s.url_converter)
\r
3856 v = s.url_converter.call(s.url_converter_scope || t, v, n, e);
\r
3858 t.setAttrib(e, 'data-mce-' + n, v, 2);
\r
3864 e.setAttribute('data-mce-style', v);
\r
3868 if (is(v) && v !== null && v.length !== 0)
\r
3869 e.setAttribute(n, '' + v, 2);
\r
3871 e.removeAttribute(n, 2);
\r
3875 setAttribs : function(e, o) {
\r
3878 return this.run(e, function(e) {
\r
3879 each(o, function(v, n) {
\r
3880 t.setAttrib(e, n, v);
\r
3885 getAttrib : function(e, n, dv) {
\r
3886 var v, t = this, undef;
\r
3890 if (!e || e.nodeType !== 1)
\r
3891 return dv === undef ? false : dv;
\r
3896 // Try the mce variant for these
\r
3897 if (/^(src|href|style|coords|shape)$/.test(n)) {
\r
3898 v = e.getAttribute("data-mce-" + n);
\r
3904 if (isIE && t.props[n]) {
\r
3905 v = e[t.props[n]];
\r
3906 v = v && v.nodeValue ? v.nodeValue : v;
\r
3910 v = e.getAttribute(n, 2);
\r
3912 // Check boolean attribs
\r
3913 if (/^(checked|compact|declare|defer|disabled|ismap|multiple|nohref|noshade|nowrap|readonly|selected)$/.test(n)) {
\r
3914 if (e[t.props[n]] === true && v === '')
\r
3917 return v ? n : '';
\r
3920 // Inner input elements will override attributes on form elements
\r
3921 if (e.nodeName === "FORM" && e.getAttributeNode(n))
\r
3922 return e.getAttributeNode(n).nodeValue;
\r
3924 if (n === 'style') {
\r
3925 v = v || e.style.cssText;
\r
3928 v = t.serializeStyle(t.parseStyle(v), e.nodeName);
\r
3930 if (t.settings.keep_values && !t._isRes(v))
\r
3931 e.setAttribute('data-mce-style', v);
\r
3935 // Remove Apple and WebKit stuff
\r
3936 if (isWebKit && n === "class" && v)
\r
3937 v = v.replace(/(apple|webkit)\-[a-z\-]+/gi, '');
\r
3939 // Handle IE issues
\r
3944 // IE returns 1 as default value
\r
3951 // IE returns +0 as default value for size
\r
3952 if (v === '+0' || v === 20 || v === 0)
\r
3969 // IE returns -1 as default value
\r
3977 // IE returns default value
\r
3978 if (v === 32768 || v === 2147483647 || v === '32768')
\r
3993 v = v.toLowerCase();
\r
3997 // IE has odd anonymous function for event attributes
\r
3998 if (n.indexOf('on') === 0 && v)
\r
3999 v = tinymce._replace(/^function\s+\w+\(\)\s+\{\s+(.*)\s+\}$/, '$1', '' + v);
\r
4003 return (v !== undef && v !== null && v !== '') ? '' + v : dv;
\r
4006 getPos : function(n, ro) {
\r
4007 var t = this, x = 0, y = 0, e, d = t.doc, r;
\r
4010 ro = ro || d.body;
\r
4013 // Use getBoundingClientRect if it exists since it's faster than looping offset nodes
\r
4014 if (n.getBoundingClientRect) {
\r
4015 n = n.getBoundingClientRect();
\r
4016 e = t.boxModel ? d.documentElement : d.body;
\r
4018 // Add scroll offsets from documentElement or body since IE with the wrong box model will use d.body and so do WebKit
\r
4019 // Also remove the body/documentelement clientTop/clientLeft on IE 6, 7 since they offset the position
\r
4020 x = n.left + (d.documentElement.scrollLeft || d.body.scrollLeft) - e.clientTop;
\r
4021 y = n.top + (d.documentElement.scrollTop || d.body.scrollTop) - e.clientLeft;
\r
4023 return {x : x, y : y};
\r
4027 while (r && r != ro && r.nodeType) {
\r
4028 x += r.offsetLeft || 0;
\r
4029 y += r.offsetTop || 0;
\r
4030 r = r.offsetParent;
\r
4034 while (r && r != ro && r.nodeType) {
\r
4035 x -= r.scrollLeft || 0;
\r
4036 y -= r.scrollTop || 0;
\r
4041 return {x : x, y : y};
\r
4044 parseStyle : function(st) {
\r
4045 return this.styles.parse(st);
\r
4048 serializeStyle : function(o, name) {
\r
4049 return this.styles.serialize(o, name);
\r
4052 loadCSS : function(u) {
\r
4053 var t = this, d = t.doc, head;
\r
4058 head = t.select('head')[0];
\r
4060 each(u.split(','), function(u) {
\r
4066 t.files[u] = true;
\r
4067 link = t.create('link', {rel : 'stylesheet', href : tinymce._addVer(u)});
\r
4069 // IE 8 has a bug where dynamically loading stylesheets would produce a 1 item remaining bug
\r
4070 // This fix seems to resolve that issue by realcing the document ones a stylesheet finishes loading
\r
4071 // It's ugly but it seems to work fine.
\r
4072 if (isIE && d.documentMode && d.recalc) {
\r
4073 link.onload = function() {
\r
4077 link.onload = null;
\r
4081 head.appendChild(link);
\r
4085 addClass : function(e, c) {
\r
4086 return this.run(e, function(e) {
\r
4092 if (this.hasClass(e, c))
\r
4093 return e.className;
\r
4095 o = this.removeClass(e, c);
\r
4097 return e.className = (o != '' ? (o + ' ') : '') + c;
\r
4101 removeClass : function(e, c) {
\r
4104 return t.run(e, function(e) {
\r
4107 if (t.hasClass(e, c)) {
\r
4109 re = new RegExp("(^|\\s+)" + c + "(\\s+|$)", "g");
\r
4111 v = e.className.replace(re, ' ');
\r
4112 v = tinymce.trim(v != ' ' ? v : '');
\r
4116 // Empty class attr
\r
4118 e.removeAttribute('class');
\r
4119 e.removeAttribute('className');
\r
4125 return e.className;
\r
4129 hasClass : function(n, c) {
\r
4135 return (' ' + n.className + ' ').indexOf(' ' + c + ' ') !== -1;
\r
4138 show : function(e) {
\r
4139 return this.setStyle(e, 'display', 'block');
\r
4142 hide : function(e) {
\r
4143 return this.setStyle(e, 'display', 'none');
\r
4146 isHidden : function(e) {
\r
4149 return !e || e.style.display == 'none' || this.getStyle(e, 'display') == 'none';
\r
4152 uniqueId : function(p) {
\r
4153 return (!p ? 'mce_' : p) + (this.counter++);
\r
4156 setHTML : function(element, html) {
\r
4159 return self.run(element, function(element) {
\r
4161 // Remove all child nodes, IE keeps empty text nodes in DOM
\r
4162 while (element.firstChild)
\r
4163 element.removeChild(element.firstChild);
\r
4166 // IE will remove comments from the beginning
\r
4167 // unless you padd the contents with something
\r
4168 element.innerHTML = '<br />' + html;
\r
4169 element.removeChild(element.firstChild);
\r
4171 // 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
4172 // This seems to fix this problem
\r
4174 // Create new div with HTML contents and a BR infront to keep comments
\r
4175 element = self.create('div');
\r
4176 element.innerHTML = '<br />' + html;
\r
4178 // Add all children from div to target
\r
4179 each (element.childNodes, function(node, i) {
\r
4180 // Skip br element
\r
4182 element.appendChild(node);
\r
4186 element.innerHTML = html;
\r
4192 getOuterHTML : function(elm) {
\r
4193 var doc, self = this;
\r
4195 elm = self.get(elm);
\r
4200 if (elm.nodeType === 1 && self.hasOuterHTML)
\r
4201 return elm.outerHTML;
\r
4203 doc = (elm.ownerDocument || self.doc).createElement("body");
\r
4204 doc.appendChild(elm.cloneNode(true));
\r
4206 return doc.innerHTML;
\r
4209 setOuterHTML : function(e, h, d) {
\r
4212 function setHTML(e, h, d) {
\r
4215 tp = d.createElement("body");
\r
4220 t.insertAfter(n.cloneNode(true), e);
\r
4221 n = n.previousSibling;
\r
4227 return this.run(e, function(e) {
\r
4230 // Only set HTML on elements
\r
4231 if (e.nodeType == 1) {
\r
4232 d = d || e.ownerDocument || t.doc;
\r
4236 // Try outerHTML for IE it sometimes produces an unknown runtime error
\r
4237 if (isIE && e.nodeType == 1)
\r
4242 // Fix for unknown runtime error
\r
4251 decode : Entities.decode,
\r
4253 encode : Entities.encodeAllRaw,
\r
4255 insertAfter : function(node, reference_node) {
\r
4256 reference_node = this.get(reference_node);
\r
4258 return this.run(node, function(node) {
\r
4259 var parent, nextSibling;
\r
4261 parent = reference_node.parentNode;
\r
4262 nextSibling = reference_node.nextSibling;
\r
4265 parent.insertBefore(node, nextSibling);
\r
4267 parent.appendChild(node);
\r
4273 isBlock : function(node) {
\r
4274 var type = node.nodeType;
\r
4276 // If it's a node then check the type and use the nodeName
\r
4278 return !!(type === 1 && blockElementsMap[node.nodeName]);
\r
4280 return !!blockElementsMap[node];
\r
4283 replace : function(n, o, k) {
\r
4286 if (is(o, 'array'))
\r
4287 n = n.cloneNode(true);
\r
4289 return t.run(o, function(o) {
\r
4291 each(tinymce.grep(o.childNodes), function(c) {
\r
4296 return o.parentNode.replaceChild(n, o);
\r
4300 rename : function(elm, name) {
\r
4301 var t = this, newElm;
\r
4303 if (elm.nodeName != name.toUpperCase()) {
\r
4304 // Rename block element
\r
4305 newElm = t.create(name);
\r
4307 // Copy attribs to new block
\r
4308 each(t.getAttribs(elm), function(attr_node) {
\r
4309 t.setAttrib(newElm, attr_node.nodeName, t.getAttrib(elm, attr_node.nodeName));
\r
4313 t.replace(newElm, elm, 1);
\r
4316 return newElm || elm;
\r
4319 findCommonAncestor : function(a, b) {
\r
4325 while (pe && ps != pe)
\r
4326 pe = pe.parentNode;
\r
4331 ps = ps.parentNode;
\r
4334 if (!ps && a.ownerDocument)
\r
4335 return a.ownerDocument.documentElement;
\r
4340 toHex : function(s) {
\r
4341 var c = /^\s*rgb\s*?\(\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?\)\s*$/i.exec(s);
\r
4344 s = parseInt(s).toString(16);
\r
4346 return s.length > 1 ? s : '0' + s; // 0 -> 00
\r
4350 s = '#' + hex(c[1]) + hex(c[2]) + hex(c[3]);
\r
4358 getClasses : function() {
\r
4359 var t = this, cl = [], i, lo = {}, f = t.settings.class_filter, ov;
\r
4364 function addClasses(s) {
\r
4365 // IE style imports
\r
4366 each(s.imports, function(r) {
\r
4370 each(s.cssRules || s.rules, function(r) {
\r
4371 // Real type or fake it on IE
\r
4372 switch (r.type || 1) {
\r
4375 if (r.selectorText) {
\r
4376 each(r.selectorText.split(','), function(v) {
\r
4377 v = v.replace(/^\s*|\s*$|^\s\./g, "");
\r
4379 // Is internal or it doesn't contain a class
\r
4380 if (/\.mce/.test(v) || !/\.[\w\-]+$/.test(v))
\r
4383 // Remove everything but class name
\r
4385 v = tinymce._replace(/.*\.([a-z0-9_\-]+).*/i, '$1', v);
\r
4388 if (f && !(v = f(v, ov)))
\r
4392 cl.push({'class' : v});
\r
4401 addClasses(r.styleSheet);
\r
4408 each(t.doc.styleSheets, addClasses);
\r
4413 if (cl.length > 0)
\r
4419 run : function(e, f, s) {
\r
4422 if (t.doc && typeof(e) === 'string')
\r
4429 if (!e.nodeType && (e.length || e.length === 0)) {
\r
4432 each(e, function(e, i) {
\r
4434 if (typeof(e) == 'string')
\r
4435 e = t.doc.getElementById(e);
\r
4437 o.push(f.call(s, e, i));
\r
4444 return f.call(s, e);
\r
4447 getAttribs : function(n) {
\r
4458 // Object will throw exception in IE
\r
4459 if (n.nodeName == 'OBJECT')
\r
4460 return n.attributes;
\r
4462 // IE doesn't keep the selected attribute if you clone option elements
\r
4463 if (n.nodeName === 'OPTION' && this.getAttrib(n, 'selected'))
\r
4464 o.push({specified : 1, nodeName : 'selected'});
\r
4466 // It's crazy that this is faster in IE but it's because it returns all attributes all the time
\r
4467 n.cloneNode(false).outerHTML.replace(/<\/?[\w:\-]+ ?|=[\"][^\"]+\"|=\'[^\']+\'|=[\w\-]+|>/gi, '').replace(/[\w:\-]+/gi, function(a) {
\r
4468 o.push({specified : 1, nodeName : a});
\r
4474 return n.attributes;
\r
4477 isEmpty : function(node, elements) {
\r
4478 var self = this, i, attributes, type, walker, name, parentNode;
\r
4480 node = node.firstChild;
\r
4482 walker = new tinymce.dom.TreeWalker(node);
\r
4483 elements = elements || self.schema ? self.schema.getNonEmptyElements() : null;
\r
4486 type = node.nodeType;
\r
4489 // Ignore bogus elements
\r
4490 if (node.getAttribute('data-mce-bogus'))
\r
4493 // Keep empty elements like <img />
\r
4494 name = node.nodeName.toLowerCase();
\r
4495 if (elements && elements[name]) {
\r
4496 // Ignore single BR elements in blocks like <p><br /></p>
\r
4497 parentNode = node.parentNode;
\r
4498 if (name === 'br' && self.isBlock(parentNode) && parentNode.firstChild === node && parentNode.lastChild === node) {
\r
4505 // Keep elements with data-bookmark attributes or name attribute like <a name="1"></a>
\r
4506 attributes = self.getAttribs(node);
\r
4507 i = node.attributes.length;
\r
4509 name = node.attributes[i].nodeName;
\r
4510 if (name === "name" || name === 'data-mce-bookmark')
\r
4515 // Keep non whitespace text nodes
\r
4516 if ((type === 3 && !whiteSpaceRegExp.test(node.nodeValue)))
\r
4518 } while (node = walker.next());
\r
4524 destroy : function(s) {
\r
4528 t.events.destroy();
\r
4530 t.win = t.doc = t.root = t.events = null;
\r
4532 // Manual destroy then remove unload handler
\r
4534 tinymce.removeUnload(t.destroy);
\r
4537 createRng : function() {
\r
4540 return d.createRange ? d.createRange() : new tinymce.dom.Range(this);
\r
4543 nodeIndex : function(node, normalized) {
\r
4544 var idx = 0, lastNodeType, lastNode, nodeType;
\r
4547 for (lastNodeType = node.nodeType, node = node.previousSibling, lastNode = node; node; node = node.previousSibling) {
\r
4548 nodeType = node.nodeType;
\r
4550 // Normalize text nodes
\r
4551 if (normalized && nodeType == 3) {
\r
4552 if (nodeType == lastNodeType || !node.nodeValue.length)
\r
4556 lastNodeType = nodeType;
\r
4563 split : function(pe, e, re) {
\r
4564 var t = this, r = t.createRng(), bef, aft, pa;
\r
4566 // W3C valid browsers tend to leave empty nodes to the left/right side of the contents, this makes sense
\r
4567 // but we don't want that in our code since it serves no purpose for the end user
\r
4568 // For example if this is chopped:
\r
4569 // <p>text 1<span><b>CHOP</b></span>text 2</p>
\r
4571 // <p>text 1<span></span></p><b>CHOP</b><p><span></span>text 2</p>
\r
4572 // this function will then trim of empty edges and produce:
\r
4573 // <p>text 1</p><b>CHOP</b><p>text 2</p>
\r
4574 function trim(node) {
\r
4575 var i, children = node.childNodes, type = node.nodeType;
\r
4577 if (type == 1 && node.getAttribute('data-mce-type') == 'bookmark')
\r
4580 for (i = children.length - 1; i >= 0; i--)
\r
4581 trim(children[i]);
\r
4584 // Keep non whitespace text nodes
\r
4585 if (type == 3 && node.nodeValue.length > 0) {
\r
4586 // If parent element isn't a block or there isn't any useful contents for example "<p> </p>"
\r
4587 if (!t.isBlock(node.parentNode) || tinymce.trim(node.nodeValue).length > 0)
\r
4589 } else if (type == 1) {
\r
4590 // If the only child is a bookmark then move it up
\r
4591 children = node.childNodes;
\r
4592 if (children.length == 1 && children[0] && children[0].nodeType == 1 && children[0].getAttribute('data-mce-type') == 'bookmark')
\r
4593 node.parentNode.insertBefore(children[0], node);
\r
4595 // Keep non empty elements or img, hr etc
\r
4596 if (children.length || /^(br|hr|input|img)$/i.test(node.nodeName))
\r
4607 // Get before chunk
\r
4608 r.setStart(pe.parentNode, t.nodeIndex(pe));
\r
4609 r.setEnd(e.parentNode, t.nodeIndex(e));
\r
4610 bef = r.extractContents();
\r
4612 // Get after chunk
\r
4613 r = t.createRng();
\r
4614 r.setStart(e.parentNode, t.nodeIndex(e) + 1);
\r
4615 r.setEnd(pe.parentNode, t.nodeIndex(pe) + 1);
\r
4616 aft = r.extractContents();
\r
4618 // Insert before chunk
\r
4619 pa = pe.parentNode;
\r
4620 pa.insertBefore(trim(bef), pe);
\r
4622 // Insert middle chunk
\r
4624 pa.replaceChild(re, e);
\r
4626 pa.insertBefore(e, pe);
\r
4628 // Insert after chunk
\r
4629 pa.insertBefore(trim(aft), pe);
\r
4636 bind : function(target, name, func, scope) {
\r
4640 t.events = new tinymce.dom.EventUtils();
\r
4642 return t.events.add(target, name, func, scope || this);
\r
4645 unbind : function(target, name, func) {
\r
4649 t.events = new tinymce.dom.EventUtils();
\r
4651 return t.events.remove(target, name, func);
\r
4655 _findSib : function(node, selector, name) {
\r
4656 var t = this, f = selector;
\r
4659 // If expression make a function of it using is
\r
4660 if (is(f, 'string')) {
\r
4661 f = function(node) {
\r
4662 return t.is(node, selector);
\r
4666 // Loop all siblings
\r
4667 for (node = node[name]; node; node = node[name]) {
\r
4676 _isRes : function(c) {
\r
4677 // Is live resizble element
\r
4678 return /^(top|left|bottom|right|width|height)/i.test(c) || /;\s*(top|left|bottom|right|width|height)/i.test(c);
\r
4682 walk : function(n, f, s) {
\r
4683 var d = this.doc, w;
\r
4685 if (d.createTreeWalker) {
\r
4686 w = d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, false);
\r
4688 while ((n = w.nextNode()) != null)
\r
4689 f.call(s || this, n);
\r
4691 tinymce.walk(n, f, 'childNodes', s);
\r
4696 toRGB : function(s) {
\r
4697 var c = /^\s*?#([0-9A-F]{2})([0-9A-F]{1,2})([0-9A-F]{2})?\s*?$/.exec(s);
\r
4700 // #FFF -> #FFFFFF
\r
4702 c[3] = c[2] = c[1];
\r
4704 return "rgb(" + parseInt(c[1], 16) + "," + parseInt(c[2], 16) + "," + parseInt(c[3], 16) + ")";
\r
4712 tinymce.DOM = new tinymce.dom.DOMUtils(document, {process_html : 0});
\r
4716 // Range constructor
\r
4717 function Range(dom) {
\r
4725 START_OFFSET = 'startOffset',
\r
4726 START_CONTAINER = 'startContainer',
\r
4727 END_CONTAINER = 'endContainer',
\r
4728 END_OFFSET = 'endOffset',
\r
4729 extend = tinymce.extend,
\r
4730 nodeIndex = dom.nodeIndex;
\r
4734 startContainer : doc,
\r
4736 endContainer : doc,
\r
4739 commonAncestorContainer : doc,
\r
4741 // Range constants
\r
4742 START_TO_START : 0,
\r
4748 setStart : setStart,
\r
4750 setStartBefore : setStartBefore,
\r
4751 setStartAfter : setStartAfter,
\r
4752 setEndBefore : setEndBefore,
\r
4753 setEndAfter : setEndAfter,
\r
4754 collapse : collapse,
\r
4755 selectNode : selectNode,
\r
4756 selectNodeContents : selectNodeContents,
\r
4757 compareBoundaryPoints : compareBoundaryPoints,
\r
4758 deleteContents : deleteContents,
\r
4759 extractContents : extractContents,
\r
4760 cloneContents : cloneContents,
\r
4761 insertNode : insertNode,
\r
4762 surroundContents : surroundContents,
\r
4763 cloneRange : cloneRange
\r
4766 function setStart(n, o) {
\r
4767 _setEndPoint(TRUE, n, o);
\r
4770 function setEnd(n, o) {
\r
4771 _setEndPoint(FALSE, n, o);
\r
4774 function setStartBefore(n) {
\r
4775 setStart(n.parentNode, nodeIndex(n));
\r
4778 function setStartAfter(n) {
\r
4779 setStart(n.parentNode, nodeIndex(n) + 1);
\r
4782 function setEndBefore(n) {
\r
4783 setEnd(n.parentNode, nodeIndex(n));
\r
4786 function setEndAfter(n) {
\r
4787 setEnd(n.parentNode, nodeIndex(n) + 1);
\r
4790 function collapse(ts) {
\r
4792 t[END_CONTAINER] = t[START_CONTAINER];
\r
4793 t[END_OFFSET] = t[START_OFFSET];
\r
4795 t[START_CONTAINER] = t[END_CONTAINER];
\r
4796 t[START_OFFSET] = t[END_OFFSET];
\r
4799 t.collapsed = TRUE;
\r
4802 function selectNode(n) {
\r
4803 setStartBefore(n);
\r
4807 function selectNodeContents(n) {
\r
4809 setEnd(n, n.nodeType === 1 ? n.childNodes.length : n.nodeValue.length);
\r
4812 function compareBoundaryPoints(h, r) {
\r
4813 var sc = t[START_CONTAINER], so = t[START_OFFSET], ec = t[END_CONTAINER], eo = t[END_OFFSET],
\r
4814 rsc = r.startContainer, rso = r.startOffset, rec = r.endContainer, reo = r.endOffset;
\r
4816 // Check START_TO_START
\r
4818 return _compareBoundaryPoints(sc, so, rsc, rso);
\r
4820 // Check START_TO_END
\r
4822 return _compareBoundaryPoints(ec, eo, rsc, rso);
\r
4824 // Check END_TO_END
\r
4826 return _compareBoundaryPoints(ec, eo, rec, reo);
\r
4828 // Check END_TO_START
\r
4830 return _compareBoundaryPoints(sc, so, rec, reo);
\r
4833 function deleteContents() {
\r
4834 _traverse(DELETE);
\r
4837 function extractContents() {
\r
4838 return _traverse(EXTRACT);
\r
4841 function cloneContents() {
\r
4842 return _traverse(CLONE);
\r
4845 function insertNode(n) {
\r
4846 var startContainer = this[START_CONTAINER],
\r
4847 startOffset = this[START_OFFSET], nn, o;
\r
4849 // Node is TEXT_NODE or CDATA
\r
4850 if ((startContainer.nodeType === 3 || startContainer.nodeType === 4) && startContainer.nodeValue) {
\r
4851 if (!startOffset) {
\r
4852 // At the start of text
\r
4853 startContainer.parentNode.insertBefore(n, startContainer);
\r
4854 } else if (startOffset >= startContainer.nodeValue.length) {
\r
4855 // At the end of text
\r
4856 dom.insertAfter(n, startContainer);
\r
4858 // Middle, need to split
\r
4859 nn = startContainer.splitText(startOffset);
\r
4860 startContainer.parentNode.insertBefore(n, nn);
\r
4863 // Insert element node
\r
4864 if (startContainer.childNodes.length > 0)
\r
4865 o = startContainer.childNodes[startOffset];
\r
4868 startContainer.insertBefore(n, o);
\r
4870 startContainer.appendChild(n);
\r
4874 function surroundContents(n) {
\r
4875 var f = t.extractContents();
\r
4882 function cloneRange() {
\r
4883 return extend(new Range(dom), {
\r
4884 startContainer : t[START_CONTAINER],
\r
4885 startOffset : t[START_OFFSET],
\r
4886 endContainer : t[END_CONTAINER],
\r
4887 endOffset : t[END_OFFSET],
\r
4888 collapsed : t.collapsed,
\r
4889 commonAncestorContainer : t.commonAncestorContainer
\r
4893 // Private methods
\r
4895 function _getSelectedNode(container, offset) {
\r
4898 if (container.nodeType == 3 /* TEXT_NODE */)
\r
4904 child = container.firstChild;
\r
4905 while (child && offset > 0) {
\r
4907 child = child.nextSibling;
\r
4916 function _isCollapsed() {
\r
4917 return (t[START_CONTAINER] == t[END_CONTAINER] && t[START_OFFSET] == t[END_OFFSET]);
\r
4920 function _compareBoundaryPoints(containerA, offsetA, containerB, offsetB) {
\r
4921 var c, offsetC, n, cmnRoot, childA, childB;
\r
4923 // In the first case the boundary-points have the same container. A is before B
\r
4924 // if its offset is less than the offset of B, A is equal to B if its offset is
\r
4925 // equal to the offset of B, and A is after B if its offset is greater than the
\r
4927 if (containerA == containerB) {
\r
4928 if (offsetA == offsetB)
\r
4929 return 0; // equal
\r
4931 if (offsetA < offsetB)
\r
4932 return -1; // before
\r
4934 return 1; // after
\r
4937 // In the second case a child node C of the container of A is an ancestor
\r
4938 // container of B. In this case, A is before B if the offset of A is less than or
\r
4939 // equal to the index of the child node C and A is after B otherwise.
\r
4941 while (c && c.parentNode != containerA)
\r
4946 n = containerA.firstChild;
\r
4948 while (n != c && offsetC < offsetA) {
\r
4950 n = n.nextSibling;
\r
4953 if (offsetA <= offsetC)
\r
4954 return -1; // before
\r
4956 return 1; // after
\r
4959 // In the third case a child node C of the container of B is an ancestor container
\r
4960 // of A. In this case, A is before B if the index of the child node C is less than
\r
4961 // the offset of B and A is after B otherwise.
\r
4963 while (c && c.parentNode != containerB) {
\r
4969 n = containerB.firstChild;
\r
4971 while (n != c && offsetC < offsetB) {
\r
4973 n = n.nextSibling;
\r
4976 if (offsetC < offsetB)
\r
4977 return -1; // before
\r
4979 return 1; // after
\r
4982 // In the fourth case, none of three other cases hold: the containers of A and B
\r
4983 // are siblings or descendants of sibling nodes. In this case, A is before B if
\r
4984 // the container of A is before the container of B in a pre-order traversal of the
\r
4985 // Ranges' context tree and A is after B otherwise.
\r
4986 cmnRoot = dom.findCommonAncestor(containerA, containerB);
\r
4987 childA = containerA;
\r
4989 while (childA && childA.parentNode != cmnRoot)
\r
4990 childA = childA.parentNode;
\r
4995 childB = containerB;
\r
4996 while (childB && childB.parentNode != cmnRoot)
\r
4997 childB = childB.parentNode;
\r
5002 if (childA == childB)
\r
5003 return 0; // equal
\r
5005 n = cmnRoot.firstChild;
\r
5008 return -1; // before
\r
5011 return 1; // after
\r
5013 n = n.nextSibling;
\r
5017 function _setEndPoint(st, n, o) {
\r
5021 t[START_CONTAINER] = n;
\r
5022 t[START_OFFSET] = o;
\r
5024 t[END_CONTAINER] = n;
\r
5025 t[END_OFFSET] = o;
\r
5028 // If one boundary-point of a Range is set to have a root container
\r
5029 // other than the current one for the Range, the Range is collapsed to
\r
5030 // the new position. This enforces the restriction that both boundary-
\r
5031 // points of a Range must have the same root container.
\r
5032 ec = t[END_CONTAINER];
\r
5033 while (ec.parentNode)
\r
5034 ec = ec.parentNode;
\r
5036 sc = t[START_CONTAINER];
\r
5037 while (sc.parentNode)
\r
5038 sc = sc.parentNode;
\r
5041 // The start position of a Range is guaranteed to never be after the
\r
5042 // end position. To enforce this restriction, if the start is set to
\r
5043 // be at a position after the end, the Range is collapsed to that
\r
5045 if (_compareBoundaryPoints(t[START_CONTAINER], t[START_OFFSET], t[END_CONTAINER], t[END_OFFSET]) > 0)
\r
5050 t.collapsed = _isCollapsed();
\r
5051 t.commonAncestorContainer = dom.findCommonAncestor(t[START_CONTAINER], t[END_CONTAINER]);
\r
5054 function _traverse(how) {
\r
5055 var c, endContainerDepth = 0, startContainerDepth = 0, p, depthDiff, startNode, endNode, sp, ep;
\r
5057 if (t[START_CONTAINER] == t[END_CONTAINER])
\r
5058 return _traverseSameContainer(how);
\r
5060 for (c = t[END_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) {
\r
5061 if (p == t[START_CONTAINER])
\r
5062 return _traverseCommonStartContainer(c, how);
\r
5064 ++endContainerDepth;
\r
5067 for (c = t[START_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) {
\r
5068 if (p == t[END_CONTAINER])
\r
5069 return _traverseCommonEndContainer(c, how);
\r
5071 ++startContainerDepth;
\r
5074 depthDiff = startContainerDepth - endContainerDepth;
\r
5076 startNode = t[START_CONTAINER];
\r
5077 while (depthDiff > 0) {
\r
5078 startNode = startNode.parentNode;
\r
5082 endNode = t[END_CONTAINER];
\r
5083 while (depthDiff < 0) {
\r
5084 endNode = endNode.parentNode;
\r
5088 // ascend the ancestor hierarchy until we have a common parent.
\r
5089 for (sp = startNode.parentNode, ep = endNode.parentNode; sp != ep; sp = sp.parentNode, ep = ep.parentNode) {
\r
5094 return _traverseCommonAncestors(startNode, endNode, how);
\r
5097 function _traverseSameContainer(how) {
\r
5098 var frag, s, sub, n, cnt, sibling, xferNode;
\r
5100 if (how != DELETE)
\r
5101 frag = doc.createDocumentFragment();
\r
5103 // If selection is empty, just return the fragment
\r
5104 if (t[START_OFFSET] == t[END_OFFSET])
\r
5107 // Text node needs special case handling
\r
5108 if (t[START_CONTAINER].nodeType == 3 /* TEXT_NODE */) {
\r
5109 // get the substring
\r
5110 s = t[START_CONTAINER].nodeValue;
\r
5111 sub = s.substring(t[START_OFFSET], t[END_OFFSET]);
\r
5113 // set the original text node to its new value
\r
5114 if (how != CLONE) {
\r
5115 t[START_CONTAINER].deleteData(t[START_OFFSET], t[END_OFFSET] - t[START_OFFSET]);
\r
5117 // Nothing is partially selected, so collapse to start point
\r
5121 if (how == DELETE)
\r
5124 frag.appendChild(doc.createTextNode(sub));
\r
5128 // Copy nodes between the start/end offsets.
\r
5129 n = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]);
\r
5130 cnt = t[END_OFFSET] - t[START_OFFSET];
\r
5133 sibling = n.nextSibling;
\r
5134 xferNode = _traverseFullySelected(n, how);
\r
5137 frag.appendChild( xferNode );
\r
5143 // Nothing is partially selected, so collapse to start point
\r
5150 function _traverseCommonStartContainer(endAncestor, how) {
\r
5151 var frag, n, endIdx, cnt, sibling, xferNode;
\r
5153 if (how != DELETE)
\r
5154 frag = doc.createDocumentFragment();
\r
5156 n = _traverseRightBoundary(endAncestor, how);
\r
5159 frag.appendChild(n);
\r
5161 endIdx = nodeIndex(endAncestor);
\r
5162 cnt = endIdx - t[START_OFFSET];
\r
5165 // Collapse to just before the endAncestor, which
\r
5166 // is partially selected.
\r
5167 if (how != CLONE) {
\r
5168 t.setEndBefore(endAncestor);
\r
5169 t.collapse(FALSE);
\r
5175 n = endAncestor.previousSibling;
\r
5177 sibling = n.previousSibling;
\r
5178 xferNode = _traverseFullySelected(n, how);
\r
5181 frag.insertBefore(xferNode, frag.firstChild);
\r
5187 // Collapse to just before the endAncestor, which
\r
5188 // is partially selected.
\r
5189 if (how != CLONE) {
\r
5190 t.setEndBefore(endAncestor);
\r
5191 t.collapse(FALSE);
\r
5197 function _traverseCommonEndContainer(startAncestor, how) {
\r
5198 var frag, startIdx, n, cnt, sibling, xferNode;
\r
5200 if (how != DELETE)
\r
5201 frag = doc.createDocumentFragment();
\r
5203 n = _traverseLeftBoundary(startAncestor, how);
\r
5205 frag.appendChild(n);
\r
5207 startIdx = nodeIndex(startAncestor);
\r
5208 ++startIdx; // Because we already traversed it
\r
5210 cnt = t[END_OFFSET] - startIdx;
\r
5211 n = startAncestor.nextSibling;
\r
5213 sibling = n.nextSibling;
\r
5214 xferNode = _traverseFullySelected(n, how);
\r
5217 frag.appendChild(xferNode);
\r
5223 if (how != CLONE) {
\r
5224 t.setStartAfter(startAncestor);
\r
5231 function _traverseCommonAncestors(startAncestor, endAncestor, how) {
\r
5232 var n, frag, commonParent, startOffset, endOffset, cnt, sibling, nextSibling;
\r
5234 if (how != DELETE)
\r
5235 frag = doc.createDocumentFragment();
\r
5237 n = _traverseLeftBoundary(startAncestor, how);
\r
5239 frag.appendChild(n);
\r
5241 commonParent = startAncestor.parentNode;
\r
5242 startOffset = nodeIndex(startAncestor);
\r
5243 endOffset = nodeIndex(endAncestor);
\r
5246 cnt = endOffset - startOffset;
\r
5247 sibling = startAncestor.nextSibling;
\r
5250 nextSibling = sibling.nextSibling;
\r
5251 n = _traverseFullySelected(sibling, how);
\r
5254 frag.appendChild(n);
\r
5256 sibling = nextSibling;
\r
5260 n = _traverseRightBoundary(endAncestor, how);
\r
5263 frag.appendChild(n);
\r
5265 if (how != CLONE) {
\r
5266 t.setStartAfter(startAncestor);
\r
5273 function _traverseRightBoundary(root, how) {
\r
5274 var next = _getSelectedNode(t[END_CONTAINER], t[END_OFFSET] - 1), parent, clonedParent, prevSibling, clonedChild, clonedGrandParent, isFullySelected = next != t[END_CONTAINER];
\r
5277 return _traverseNode(next, isFullySelected, FALSE, how);
\r
5279 parent = next.parentNode;
\r
5280 clonedParent = _traverseNode(parent, FALSE, FALSE, how);
\r
5284 prevSibling = next.previousSibling;
\r
5285 clonedChild = _traverseNode(next, isFullySelected, FALSE, how);
\r
5287 if (how != DELETE)
\r
5288 clonedParent.insertBefore(clonedChild, clonedParent.firstChild);
\r
5290 isFullySelected = TRUE;
\r
5291 next = prevSibling;
\r
5294 if (parent == root)
\r
5295 return clonedParent;
\r
5297 next = parent.previousSibling;
\r
5298 parent = parent.parentNode;
\r
5300 clonedGrandParent = _traverseNode(parent, FALSE, FALSE, how);
\r
5302 if (how != DELETE)
\r
5303 clonedGrandParent.appendChild(clonedParent);
\r
5305 clonedParent = clonedGrandParent;
\r
5309 function _traverseLeftBoundary(root, how) {
\r
5310 var next = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]), isFullySelected = next != t[START_CONTAINER], parent, clonedParent, nextSibling, clonedChild, clonedGrandParent;
\r
5313 return _traverseNode(next, isFullySelected, TRUE, how);
\r
5315 parent = next.parentNode;
\r
5316 clonedParent = _traverseNode(parent, FALSE, TRUE, how);
\r
5320 nextSibling = next.nextSibling;
\r
5321 clonedChild = _traverseNode(next, isFullySelected, TRUE, how);
\r
5323 if (how != DELETE)
\r
5324 clonedParent.appendChild(clonedChild);
\r
5326 isFullySelected = TRUE;
\r
5327 next = nextSibling;
\r
5330 if (parent == root)
\r
5331 return clonedParent;
\r
5333 next = parent.nextSibling;
\r
5334 parent = parent.parentNode;
\r
5336 clonedGrandParent = _traverseNode(parent, FALSE, TRUE, how);
\r
5338 if (how != DELETE)
\r
5339 clonedGrandParent.appendChild(clonedParent);
\r
5341 clonedParent = clonedGrandParent;
\r
5345 function _traverseNode(n, isFullySelected, isLeft, how) {
\r
5346 var txtValue, newNodeValue, oldNodeValue, offset, newNode;
\r
5348 if (isFullySelected)
\r
5349 return _traverseFullySelected(n, how);
\r
5351 if (n.nodeType == 3 /* TEXT_NODE */) {
\r
5352 txtValue = n.nodeValue;
\r
5355 offset = t[START_OFFSET];
\r
5356 newNodeValue = txtValue.substring(offset);
\r
5357 oldNodeValue = txtValue.substring(0, offset);
\r
5359 offset = t[END_OFFSET];
\r
5360 newNodeValue = txtValue.substring(0, offset);
\r
5361 oldNodeValue = txtValue.substring(offset);
\r
5365 n.nodeValue = oldNodeValue;
\r
5367 if (how == DELETE)
\r
5370 newNode = n.cloneNode(FALSE);
\r
5371 newNode.nodeValue = newNodeValue;
\r
5376 if (how == DELETE)
\r
5379 return n.cloneNode(FALSE);
\r
5382 function _traverseFullySelected(n, how) {
\r
5383 if (how != DELETE)
\r
5384 return how == CLONE ? n.cloneNode(TRUE) : n;
\r
5386 n.parentNode.removeChild(n);
\r
5394 function Selection(selection) {
\r
5395 var self = this, dom = selection.dom, TRUE = true, FALSE = false;
\r
5397 function getPosition(rng, start) {
\r
5398 var checkRng, startIndex = 0, endIndex, inside,
\r
5399 children, child, offset, index, position = -1, parent;
\r
5401 // Setup test range, collapse it and get the parent
\r
5402 checkRng = rng.duplicate();
\r
5403 checkRng.collapse(start);
\r
5404 parent = checkRng.parentElement();
\r
5406 // Check if the selection is within the right document
\r
5407 if (parent.ownerDocument !== selection.dom.doc)
\r
5410 // IE will report non editable elements as it's parent so look for an editable one
\r
5411 while (parent.contentEditable === "false") {
\r
5412 parent = parent.parentNode;
\r
5415 // If parent doesn't have any children then return that we are inside the element
\r
5416 if (!parent.hasChildNodes()) {
\r
5417 return {node : parent, inside : 1};
\r
5420 // Setup node list and endIndex
\r
5421 children = parent.children;
\r
5422 endIndex = children.length - 1;
\r
5424 // Perform a binary search for the position
\r
5425 while (startIndex <= endIndex) {
\r
5426 index = Math.floor((startIndex + endIndex) / 2);
\r
5428 // Move selection to node and compare the ranges
\r
5429 child = children[index];
\r
5430 checkRng.moveToElementText(child);
\r
5431 position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng);
\r
5433 // Before/after or an exact match
\r
5434 if (position > 0) {
\r
5435 endIndex = index - 1;
\r
5436 } else if (position < 0) {
\r
5437 startIndex = index + 1;
\r
5439 return {node : child};
\r
5443 // Check if child position is before or we didn't find a position
\r
5444 if (position < 0) {
\r
5445 // No element child was found use the parent element and the offset inside that
\r
5447 checkRng.moveToElementText(parent);
\r
5448 checkRng.collapse(true);
\r
5452 checkRng.collapse(false);
\r
5454 checkRng.setEndPoint(start ? 'EndToStart' : 'EndToEnd', rng);
\r
5456 // Fix for edge case: <div style="width: 100px; height:100px;"><table>..</table>ab|c</div>
\r
5457 if (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) > 0) {
\r
5458 checkRng = rng.duplicate();
\r
5459 checkRng.collapse(start);
\r
5462 while (parent == checkRng.parentElement()) {
\r
5463 if (checkRng.move('character', -1) == 0)
\r
5470 offset = offset || checkRng.text.replace('\r\n', ' ').length;
\r
5472 // Child position is after the selection endpoint
\r
5473 checkRng.collapse(true);
\r
5474 checkRng.setEndPoint(start ? 'StartToStart' : 'StartToEnd', rng);
\r
5476 // Get the length of the text to find where the endpoint is relative to it's container
\r
5477 offset = checkRng.text.replace('\r\n', ' ').length;
\r
5480 return {node : child, position : position, offset : offset, inside : inside};
\r
5483 // Returns a W3C DOM compatible range object by using the IE Range API
\r
5484 function getRange() {
\r
5485 var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark, fail;
\r
5487 // If selection is outside the current document just return an empty range
\r
5488 element = ieRange.item ? ieRange.item(0) : ieRange.parentElement();
\r
5489 if (element.ownerDocument != dom.doc)
\r
5492 collapsed = selection.isCollapsed();
\r
5494 // Handle control selection
\r
5495 if (ieRange.item) {
\r
5496 domRange.setStart(element.parentNode, dom.nodeIndex(element));
\r
5497 domRange.setEnd(domRange.startContainer, domRange.startOffset + 1);
\r
5502 function findEndPoint(start) {
\r
5503 var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue;
\r
5505 container = endPoint.node;
\r
5506 offset = endPoint.offset;
\r
5508 if (endPoint.inside && !container.hasChildNodes()) {
\r
5509 domRange[start ? 'setStart' : 'setEnd'](container, 0);
\r
5513 if (offset === undef) {
\r
5514 domRange[start ? 'setStartBefore' : 'setEndAfter'](container);
\r
5518 if (endPoint.position < 0) {
\r
5519 sibling = endPoint.inside ? container.firstChild : container.nextSibling;
\r
5522 domRange[start ? 'setStartAfter' : 'setEndAfter'](container);
\r
5527 if (sibling.nodeType == 3)
\r
5528 domRange[start ? 'setStart' : 'setEnd'](sibling, 0);
\r
5530 domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling);
\r
5535 // Find the text node and offset
\r
5537 nodeValue = sibling.nodeValue;
\r
5538 textNodeOffset += nodeValue.length;
\r
5540 // We are at or passed the position we where looking for
\r
5541 if (textNodeOffset >= offset) {
\r
5542 container = sibling;
\r
5543 textNodeOffset -= offset;
\r
5544 textNodeOffset = nodeValue.length - textNodeOffset;
\r
5548 sibling = sibling.nextSibling;
\r
5551 // Find the text node and offset
\r
5552 sibling = container.previousSibling;
\r
5555 return domRange[start ? 'setStartBefore' : 'setEndBefore'](container);
\r
5557 // If there isn't any text to loop then use the first position
\r
5559 if (container.nodeType == 3)
\r
5560 domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length);
\r
5562 domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling);
\r
5568 textNodeOffset += sibling.nodeValue.length;
\r
5570 // We are at or passed the position we where looking for
\r
5571 if (textNodeOffset >= offset) {
\r
5572 container = sibling;
\r
5573 textNodeOffset -= offset;
\r
5577 sibling = sibling.previousSibling;
\r
5581 domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset);
\r
5585 // Find start point
\r
5586 findEndPoint(true);
\r
5588 // Find end point if needed
\r
5592 // IE has a nasty bug where text nodes might throw "invalid argument" when you
\r
5593 // access the nodeValue or other properties of text nodes. This seems to happend when
\r
5594 // text nodes are split into two nodes by a delete/backspace call. So lets detect it and try to fix it.
\r
5595 if (ex.number == -2147024809) {
\r
5596 // Get the current selection
\r
5597 bookmark = self.getBookmark(2);
\r
5599 // Get start element
\r
5600 tmpRange = ieRange.duplicate();
\r
5601 tmpRange.collapse(true);
\r
5602 element = tmpRange.parentElement();
\r
5604 // Get end element
\r
5606 tmpRange = ieRange.duplicate();
\r
5607 tmpRange.collapse(false);
\r
5608 element2 = tmpRange.parentElement();
\r
5609 element2.innerHTML = element2.innerHTML;
\r
5612 // Remove the broken elements
\r
5613 element.innerHTML = element.innerHTML;
\r
5615 // Restore the selection
\r
5616 self.moveToBookmark(bookmark);
\r
5618 // Since the range has moved we need to re-get it
\r
5619 ieRange = selection.getRng();
\r
5621 // Find start point
\r
5622 findEndPoint(true);
\r
5624 // Find end point if needed
\r
5628 throw ex; // Throw other errors
\r
5634 this.getBookmark = function(type) {
\r
5635 var rng = selection.getRng(), start, end, bookmark = {};
\r
5637 function getIndexes(node) {
\r
5638 var node, parent, root, children, i, indexes = [];
\r
5640 parent = node.parentNode;
\r
5641 root = dom.getRoot().parentNode;
\r
5643 while (parent != root) {
\r
5644 children = parent.children;
\r
5646 i = children.length;
\r
5648 if (node === children[i]) {
\r
5655 parent = parent.parentNode;
\r
5661 function getBookmarkEndPoint(start) {
\r
5664 position = getPosition(rng, start);
\r
5667 position : position.position,
\r
5668 offset : position.offset,
\r
5669 indexes : getIndexes(position.node),
\r
5670 inside : position.inside
\r
5675 // Non ubstructive bookmark
\r
5677 // Handle text selection
\r
5679 bookmark.start = getBookmarkEndPoint(true);
\r
5681 if (!selection.isCollapsed())
\r
5682 bookmark.end = getBookmarkEndPoint();
\r
5684 bookmark.start = {ctrl : true, indexes : getIndexes(rng.item(0))};
\r
5690 this.moveToBookmark = function(bookmark) {
\r
5691 var rng, body = dom.doc.body;
\r
5693 function resolveIndexes(indexes) {
\r
5694 var node, i, idx, children;
\r
5696 node = dom.getRoot();
\r
5697 for (i = indexes.length - 1; i >= 0; i--) {
\r
5698 children = node.children;
\r
5701 if (idx <= children.length - 1) {
\r
5702 node = children[idx];
\r
5709 function setBookmarkEndPoint(start) {
\r
5710 var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef;
\r
5713 moveLeft = endPoint.position > 0;
\r
5715 moveRng = body.createTextRange();
\r
5716 moveRng.moveToElementText(resolveIndexes(endPoint.indexes));
\r
5718 offset = endPoint.offset;
\r
5719 if (offset !== undef) {
\r
5720 moveRng.collapse(endPoint.inside || moveLeft);
\r
5721 moveRng.moveStart('character', moveLeft ? -offset : offset);
\r
5723 moveRng.collapse(start);
\r
5725 rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng);
\r
5728 rng.collapse(true);
\r
5732 if (bookmark.start) {
\r
5733 if (bookmark.start.ctrl) {
\r
5734 rng = body.createControlRange();
\r
5735 rng.addElement(resolveIndexes(bookmark.start.indexes));
\r
5738 rng = body.createTextRange();
\r
5739 setBookmarkEndPoint(true);
\r
5740 setBookmarkEndPoint();
\r
5746 this.addRange = function(rng) {
\r
5747 var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, doc = selection.dom.doc, body = doc.body;
\r
5749 function setEndPoint(start) {
\r
5750 var container, offset, marker, tmpRng, nodes;
\r
5752 marker = dom.create('a');
\r
5753 container = start ? startContainer : endContainer;
\r
5754 offset = start ? startOffset : endOffset;
\r
5755 tmpRng = ieRng.duplicate();
\r
5757 if (container == doc || container == doc.documentElement) {
\r
5762 if (container.nodeType == 3) {
\r
5763 container.parentNode.insertBefore(marker, container);
\r
5764 tmpRng.moveToElementText(marker);
\r
5765 tmpRng.moveStart('character', offset);
\r
5766 dom.remove(marker);
\r
5767 ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
\r
5769 nodes = container.childNodes;
\r
5771 if (nodes.length) {
\r
5772 if (offset >= nodes.length) {
\r
5773 dom.insertAfter(marker, nodes[nodes.length - 1]);
\r
5775 container.insertBefore(marker, nodes[offset]);
\r
5778 tmpRng.moveToElementText(marker);
\r
5780 // Empty node selection for example <div>|</div>
\r
5781 marker = doc.createTextNode('\uFEFF');
\r
5782 container.appendChild(marker);
\r
5783 tmpRng.moveToElementText(marker.parentNode);
\r
5784 tmpRng.collapse(TRUE);
\r
5787 ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
\r
5788 dom.remove(marker);
\r
5792 // Setup some shorter versions
\r
5793 startContainer = rng.startContainer;
\r
5794 startOffset = rng.startOffset;
\r
5795 endContainer = rng.endContainer;
\r
5796 endOffset = rng.endOffset;
\r
5797 ieRng = body.createTextRange();
\r
5799 // If single element selection then try making a control selection out of it
\r
5800 if (startContainer == endContainer && startContainer.nodeType == 1 && startOffset == endOffset - 1) {
\r
5801 if (startOffset == endOffset - 1) {
\r
5803 ctrlRng = body.createControlRange();
\r
5804 ctrlRng.addElement(startContainer.childNodes[startOffset]);
\r
5813 // Set start/end point of selection
\r
5814 setEndPoint(true);
\r
5817 // Select the new range and scroll it into view
\r
5821 // Expose range method
\r
5822 this.getRangeAt = getRange;
\r
5825 // Expose the selection object
\r
5826 tinymce.dom.TridentSelection = Selection;
\r
5831 * Sizzle CSS Selector Engine - v1.0
\r
5832 * Copyright 2009, The Dojo Foundation
\r
5833 * Released under the MIT, BSD, and GPL Licenses.
\r
5834 * More information: http://sizzlejs.com/
\r
5838 var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
\r
5840 toString = Object.prototype.toString,
\r
5841 hasDuplicate = false,
\r
5842 baseHasDuplicate = true;
\r
5844 // Here we check if the JavaScript engine is using some sort of
\r
5845 // optimization where it does not always call our comparision
\r
5846 // function. If that is the case, discard the hasDuplicate value.
\r
5847 // Thus far that includes Google Chrome.
\r
5848 [0, 0].sort(function(){
\r
5849 baseHasDuplicate = false;
\r
5853 var Sizzle = function(selector, context, results, seed) {
\r
5854 results = results || [];
\r
5855 context = context || document;
\r
5857 var origContext = context;
\r
5859 if ( context.nodeType !== 1 && context.nodeType !== 9 ) {
\r
5863 if ( !selector || typeof selector !== "string" ) {
\r
5867 var parts = [], m, set, checkSet, extra, prune = true, contextXML = Sizzle.isXML(context),
\r
5868 soFar = selector, ret, cur, pop, i;
\r
5870 // Reset the position of the chunker regexp (start from head)
\r
5873 m = chunker.exec(soFar);
\r
5878 parts.push( m[1] );
\r
5887 if ( parts.length > 1 && origPOS.exec( selector ) ) {
\r
5888 if ( parts.length === 2 && Expr.relative[ parts[0] ] ) {
\r
5889 set = posProcess( parts[0] + parts[1], context );
\r
5891 set = Expr.relative[ parts[0] ] ?
\r
5893 Sizzle( parts.shift(), context );
\r
5895 while ( parts.length ) {
\r
5896 selector = parts.shift();
\r
5898 if ( Expr.relative[ selector ] ) {
\r
5899 selector += parts.shift();
\r
5902 set = posProcess( selector, set );
\r
5906 // Take a shortcut and set the context if the root selector is an ID
\r
5907 // (but not if it'll be faster if the inner selector is an ID)
\r
5908 if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML &&
\r
5909 Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) {
\r
5910 ret = Sizzle.find( parts.shift(), context, contextXML );
\r
5911 context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0];
\r
5916 { expr: parts.pop(), set: makeArray(seed) } :
\r
5917 Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML );
\r
5918 set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set;
\r
5920 if ( parts.length > 0 ) {
\r
5921 checkSet = makeArray(set);
\r
5926 while ( parts.length ) {
\r
5927 cur = parts.pop();
\r
5930 if ( !Expr.relative[ cur ] ) {
\r
5933 pop = parts.pop();
\r
5936 if ( pop == null ) {
\r
5940 Expr.relative[ cur ]( checkSet, pop, contextXML );
\r
5943 checkSet = parts = [];
\r
5947 if ( !checkSet ) {
\r
5951 if ( !checkSet ) {
\r
5952 Sizzle.error( cur || selector );
\r
5955 if ( toString.call(checkSet) === "[object Array]" ) {
\r
5957 results.push.apply( results, checkSet );
\r
5958 } else if ( context && context.nodeType === 1 ) {
\r
5959 for ( i = 0; checkSet[i] != null; i++ ) {
\r
5960 if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) {
\r
5961 results.push( set[i] );
\r
5965 for ( i = 0; checkSet[i] != null; i++ ) {
\r
5966 if ( checkSet[i] && checkSet[i].nodeType === 1 ) {
\r
5967 results.push( set[i] );
\r
5972 makeArray( checkSet, results );
\r
5976 Sizzle( extra, origContext, results, seed );
\r
5977 Sizzle.uniqueSort( results );
\r
5983 Sizzle.uniqueSort = function(results){
\r
5984 if ( sortOrder ) {
\r
5985 hasDuplicate = baseHasDuplicate;
\r
5986 results.sort(sortOrder);
\r
5988 if ( hasDuplicate ) {
\r
5989 for ( var i = 1; i < results.length; i++ ) {
\r
5990 if ( results[i] === results[i-1] ) {
\r
5991 results.splice(i--, 1);
\r
6000 Sizzle.matches = function(expr, set){
\r
6001 return Sizzle(expr, null, null, set);
\r
6004 Sizzle.find = function(expr, context, isXML){
\r
6011 for ( var i = 0, l = Expr.order.length; i < l; i++ ) {
\r
6012 var type = Expr.order[i], match;
\r
6014 if ( (match = Expr.leftMatch[ type ].exec( expr )) ) {
\r
6015 var left = match[1];
\r
6016 match.splice(1,1);
\r
6018 if ( left.substr( left.length - 1 ) !== "\\" ) {
\r
6019 match[1] = (match[1] || "").replace(/\\/g, "");
\r
6020 set = Expr.find[ type ]( match, context, isXML );
\r
6021 if ( set != null ) {
\r
6022 expr = expr.replace( Expr.match[ type ], "" );
\r
6030 set = context.getElementsByTagName("*");
\r
6033 return {set: set, expr: expr};
\r
6036 Sizzle.filter = function(expr, set, inplace, not){
\r
6037 var old = expr, result = [], curLoop = set, match, anyFound,
\r
6038 isXMLFilter = set && set[0] && Sizzle.isXML(set[0]);
\r
6040 while ( expr && set.length ) {
\r
6041 for ( var type in Expr.filter ) {
\r
6042 if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) {
\r
6043 var filter = Expr.filter[ type ], found, item, left = match[1];
\r
6046 match.splice(1,1);
\r
6048 if ( left.substr( left.length - 1 ) === "\\" ) {
\r
6052 if ( curLoop === result ) {
\r
6056 if ( Expr.preFilter[ type ] ) {
\r
6057 match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter );
\r
6060 anyFound = found = true;
\r
6061 } else if ( match === true ) {
\r
6067 for ( var i = 0; (item = curLoop[i]) != null; i++ ) {
\r
6069 found = filter( item, match, i, curLoop );
\r
6070 var pass = not ^ !!found;
\r
6072 if ( inplace && found != null ) {
\r
6076 curLoop[i] = false;
\r
6078 } else if ( pass ) {
\r
6079 result.push( item );
\r
6086 if ( found !== undefined ) {
\r
6091 expr = expr.replace( Expr.match[ type ], "" );
\r
6093 if ( !anyFound ) {
\r
6102 // Improper expression
\r
6103 if ( expr === old ) {
\r
6104 if ( anyFound == null ) {
\r
6105 Sizzle.error( expr );
\r
6117 Sizzle.error = function( msg ) {
\r
6118 throw "Syntax error, unrecognized expression: " + msg;
\r
6121 var Expr = Sizzle.selectors = {
\r
6122 order: [ "ID", "NAME", "TAG" ],
\r
6124 ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,
\r
6125 CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,
\r
6126 NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,
\r
6127 ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,
\r
6128 TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,
\r
6129 CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+\-]*)\))?/,
\r
6130 POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,
\r
6131 PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/
\r
6135 "class": "className",
\r
6139 href: function(elem){
\r
6140 return elem.getAttribute("href");
\r
6144 "+": function(checkSet, part){
\r
6145 var isPartStr = typeof part === "string",
\r
6146 isTag = isPartStr && !/\W/.test(part),
\r
6147 isPartStrNotTag = isPartStr && !isTag;
\r
6150 part = part.toLowerCase();
\r
6153 for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) {
\r
6154 if ( (elem = checkSet[i]) ) {
\r
6155 while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {}
\r
6157 checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ?
\r
6163 if ( isPartStrNotTag ) {
\r
6164 Sizzle.filter( part, checkSet, true );
\r
6167 ">": function(checkSet, part){
\r
6168 var isPartStr = typeof part === "string",
\r
6169 elem, i = 0, l = checkSet.length;
\r
6171 if ( isPartStr && !/\W/.test(part) ) {
\r
6172 part = part.toLowerCase();
\r
6174 for ( ; i < l; i++ ) {
\r
6175 elem = checkSet[i];
\r
6177 var parent = elem.parentNode;
\r
6178 checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false;
\r
6182 for ( ; i < l; i++ ) {
\r
6183 elem = checkSet[i];
\r
6185 checkSet[i] = isPartStr ?
\r
6187 elem.parentNode === part;
\r
6191 if ( isPartStr ) {
\r
6192 Sizzle.filter( part, checkSet, true );
\r
6196 "": function(checkSet, part, isXML){
\r
6197 var doneName = done++, checkFn = dirCheck, nodeCheck;
\r
6199 if ( typeof part === "string" && !/\W/.test(part) ) {
\r
6200 part = part.toLowerCase();
\r
6202 checkFn = dirNodeCheck;
\r
6205 checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML);
\r
6207 "~": function(checkSet, part, isXML){
\r
6208 var doneName = done++, checkFn = dirCheck, nodeCheck;
\r
6210 if ( typeof part === "string" && !/\W/.test(part) ) {
\r
6211 part = part.toLowerCase();
\r
6213 checkFn = dirNodeCheck;
\r
6216 checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML);
\r
6220 ID: function(match, context, isXML){
\r
6221 if ( typeof context.getElementById !== "undefined" && !isXML ) {
\r
6222 var m = context.getElementById(match[1]);
\r
6223 return m ? [m] : [];
\r
6226 NAME: function(match, context){
\r
6227 if ( typeof context.getElementsByName !== "undefined" ) {
\r
6228 var ret = [], results = context.getElementsByName(match[1]);
\r
6230 for ( var i = 0, l = results.length; i < l; i++ ) {
\r
6231 if ( results[i].getAttribute("name") === match[1] ) {
\r
6232 ret.push( results[i] );
\r
6236 return ret.length === 0 ? null : ret;
\r
6239 TAG: function(match, context){
\r
6240 return context.getElementsByTagName(match[1]);
\r
6244 CLASS: function(match, curLoop, inplace, result, not, isXML){
\r
6245 match = " " + match[1].replace(/\\/g, "") + " ";
\r
6251 for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) {
\r
6253 if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n]/g, " ").indexOf(match) >= 0) ) {
\r
6255 result.push( elem );
\r
6257 } else if ( inplace ) {
\r
6258 curLoop[i] = false;
\r
6265 ID: function(match){
\r
6266 return match[1].replace(/\\/g, "");
\r
6268 TAG: function(match, curLoop){
\r
6269 return match[1].toLowerCase();
\r
6271 CHILD: function(match){
\r
6272 if ( match[1] === "nth" ) {
\r
6273 // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6'
\r
6274 var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec(
\r
6275 match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" ||
\r
6276 !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]);
\r
6278 // calculate the numbers (first)n+(last) including if they are negative
\r
6279 match[2] = (test[1] + (test[2] || 1)) - 0;
\r
6280 match[3] = test[3] - 0;
\r
6283 // TODO: Move to normal caching system
\r
6284 match[0] = done++;
\r
6288 ATTR: function(match, curLoop, inplace, result, not, isXML){
\r
6289 var name = match[1].replace(/\\/g, "");
\r
6291 if ( !isXML && Expr.attrMap[name] ) {
\r
6292 match[1] = Expr.attrMap[name];
\r
6295 if ( match[2] === "~=" ) {
\r
6296 match[4] = " " + match[4] + " ";
\r
6301 PSEUDO: function(match, curLoop, inplace, result, not){
\r
6302 if ( match[1] === "not" ) {
\r
6303 // If we're dealing with a complex expression, or a simple one
\r
6304 if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) {
\r
6305 match[3] = Sizzle(match[3], null, null, curLoop);
\r
6307 var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not);
\r
6309 result.push.apply( result, ret );
\r
6313 } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) {
\r
6319 POS: function(match){
\r
6320 match.unshift( true );
\r
6325 enabled: function(elem){
\r
6326 return elem.disabled === false && elem.type !== "hidden";
\r
6328 disabled: function(elem){
\r
6329 return elem.disabled === true;
\r
6331 checked: function(elem){
\r
6332 return elem.checked === true;
\r
6334 selected: function(elem){
\r
6335 // Accessing this property makes selected-by-default
\r
6336 // options in Safari work properly
\r
6337 elem.parentNode.selectedIndex;
\r
6338 return elem.selected === true;
\r
6340 parent: function(elem){
\r
6341 return !!elem.firstChild;
\r
6343 empty: function(elem){
\r
6344 return !elem.firstChild;
\r
6346 has: function(elem, i, match){
\r
6347 return !!Sizzle( match[3], elem ).length;
\r
6349 header: function(elem){
\r
6350 return (/h\d/i).test( elem.nodeName );
\r
6352 text: function(elem){
\r
6353 return "text" === elem.type;
\r
6355 radio: function(elem){
\r
6356 return "radio" === elem.type;
\r
6358 checkbox: function(elem){
\r
6359 return "checkbox" === elem.type;
\r
6361 file: function(elem){
\r
6362 return "file" === elem.type;
\r
6364 password: function(elem){
\r
6365 return "password" === elem.type;
\r
6367 submit: function(elem){
\r
6368 return "submit" === elem.type;
\r
6370 image: function(elem){
\r
6371 return "image" === elem.type;
\r
6373 reset: function(elem){
\r
6374 return "reset" === elem.type;
\r
6376 button: function(elem){
\r
6377 return "button" === elem.type || elem.nodeName.toLowerCase() === "button";
\r
6379 input: function(elem){
\r
6380 return (/input|select|textarea|button/i).test(elem.nodeName);
\r
6384 first: function(elem, i){
\r
6387 last: function(elem, i, match, array){
\r
6388 return i === array.length - 1;
\r
6390 even: function(elem, i){
\r
6391 return i % 2 === 0;
\r
6393 odd: function(elem, i){
\r
6394 return i % 2 === 1;
\r
6396 lt: function(elem, i, match){
\r
6397 return i < match[3] - 0;
\r
6399 gt: function(elem, i, match){
\r
6400 return i > match[3] - 0;
\r
6402 nth: function(elem, i, match){
\r
6403 return match[3] - 0 === i;
\r
6405 eq: function(elem, i, match){
\r
6406 return match[3] - 0 === i;
\r
6410 PSEUDO: function(elem, match, i, array){
\r
6411 var name = match[1], filter = Expr.filters[ name ];
\r
6414 return filter( elem, i, match, array );
\r
6415 } else if ( name === "contains" ) {
\r
6416 return (elem.textContent || elem.innerText || Sizzle.getText([ elem ]) || "").indexOf(match[3]) >= 0;
\r
6417 } else if ( name === "not" ) {
\r
6418 var not = match[3];
\r
6420 for ( var j = 0, l = not.length; j < l; j++ ) {
\r
6421 if ( not[j] === elem ) {
\r
6428 Sizzle.error( "Syntax error, unrecognized expression: " + name );
\r
6431 CHILD: function(elem, match){
\r
6432 var type = match[1], node = elem;
\r
6436 while ( (node = node.previousSibling) ) {
\r
6437 if ( node.nodeType === 1 ) {
\r
6441 if ( type === "first" ) {
\r
6446 while ( (node = node.nextSibling) ) {
\r
6447 if ( node.nodeType === 1 ) {
\r
6453 var first = match[2], last = match[3];
\r
6455 if ( first === 1 && last === 0 ) {
\r
6459 var doneName = match[0],
\r
6460 parent = elem.parentNode;
\r
6462 if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) {
\r
6464 for ( node = parent.firstChild; node; node = node.nextSibling ) {
\r
6465 if ( node.nodeType === 1 ) {
\r
6466 node.nodeIndex = ++count;
\r
6469 parent.sizcache = doneName;
\r
6472 var diff = elem.nodeIndex - last;
\r
6473 if ( first === 0 ) {
\r
6474 return diff === 0;
\r
6476 return ( diff % first === 0 && diff / first >= 0 );
\r
6480 ID: function(elem, match){
\r
6481 return elem.nodeType === 1 && elem.getAttribute("id") === match;
\r
6483 TAG: function(elem, match){
\r
6484 return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match;
\r
6486 CLASS: function(elem, match){
\r
6487 return (" " + (elem.className || elem.getAttribute("class")) + " ")
\r
6488 .indexOf( match ) > -1;
\r
6490 ATTR: function(elem, match){
\r
6491 var name = match[1],
\r
6492 result = Expr.attrHandle[ name ] ?
\r
6493 Expr.attrHandle[ name ]( elem ) :
\r
6494 elem[ name ] != null ?
\r
6496 elem.getAttribute( name ),
\r
6497 value = result + "",
\r
6501 return result == null ?
\r
6506 value.indexOf(check) >= 0 :
\r
6508 (" " + value + " ").indexOf(check) >= 0 :
\r
6510 value && result !== false :
\r
6514 value.indexOf(check) === 0 :
\r
6516 value.substr(value.length - check.length) === check :
\r
6518 value === check || value.substr(0, check.length + 1) === check + "-" :
\r
6521 POS: function(elem, match, i, array){
\r
6522 var name = match[2], filter = Expr.setFilters[ name ];
\r
6525 return filter( elem, i, match, array );
\r
6531 var origPOS = Expr.match.POS,
\r
6532 fescape = function(all, num){
\r
6533 return "\\" + (num - 0 + 1);
\r
6536 for ( var type in Expr.match ) {
\r
6537 Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) );
\r
6538 Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) );
\r
6541 var makeArray = function(array, results) {
\r
6542 array = Array.prototype.slice.call( array, 0 );
\r
6545 results.push.apply( results, array );
\r
6552 // Perform a simple check to determine if the browser is capable of
\r
6553 // converting a NodeList to an array using builtin methods.
\r
6554 // Also verifies that the returned array holds DOM nodes
\r
6555 // (which is not the case in the Blackberry browser)
\r
6557 Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType;
\r
6559 // Provide a fallback method if it does not work
\r
6561 makeArray = function(array, results) {
\r
6562 var ret = results || [], i = 0;
\r
6564 if ( toString.call(array) === "[object Array]" ) {
\r
6565 Array.prototype.push.apply( ret, array );
\r
6567 if ( typeof array.length === "number" ) {
\r
6568 for ( var l = array.length; i < l; i++ ) {
\r
6569 ret.push( array[i] );
\r
6572 for ( ; array[i]; i++ ) {
\r
6573 ret.push( array[i] );
\r
6584 if ( document.documentElement.compareDocumentPosition ) {
\r
6585 sortOrder = function( a, b ) {
\r
6586 if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) {
\r
6588 hasDuplicate = true;
\r
6590 return a.compareDocumentPosition ? -1 : 1;
\r
6593 var ret = a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1;
\r
6594 if ( ret === 0 ) {
\r
6595 hasDuplicate = true;
\r
6599 } else if ( "sourceIndex" in document.documentElement ) {
\r
6600 sortOrder = function( a, b ) {
\r
6601 if ( !a.sourceIndex || !b.sourceIndex ) {
\r
6603 hasDuplicate = true;
\r
6605 return a.sourceIndex ? -1 : 1;
\r
6608 var ret = a.sourceIndex - b.sourceIndex;
\r
6609 if ( ret === 0 ) {
\r
6610 hasDuplicate = true;
\r
6614 } else if ( document.createRange ) {
\r
6615 sortOrder = function( a, b ) {
\r
6616 if ( !a.ownerDocument || !b.ownerDocument ) {
\r
6618 hasDuplicate = true;
\r
6620 return a.ownerDocument ? -1 : 1;
\r
6623 var aRange = a.ownerDocument.createRange(), bRange = b.ownerDocument.createRange();
\r
6624 aRange.setStart(a, 0);
\r
6625 aRange.setEnd(a, 0);
\r
6626 bRange.setStart(b, 0);
\r
6627 bRange.setEnd(b, 0);
\r
6628 var ret = aRange.compareBoundaryPoints(Range.START_TO_END, bRange);
\r
6629 if ( ret === 0 ) {
\r
6630 hasDuplicate = true;
\r
6636 // Utility function for retreiving the text value of an array of DOM nodes
\r
6637 Sizzle.getText = function( elems ) {
\r
6638 var ret = "", elem;
\r
6640 for ( var i = 0; elems[i]; i++ ) {
\r
6643 // Get the text from text nodes and CDATA nodes
\r
6644 if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
\r
6645 ret += elem.nodeValue;
\r
6647 // Traverse everything else, except comment nodes
\r
6648 } else if ( elem.nodeType !== 8 ) {
\r
6649 ret += Sizzle.getText( elem.childNodes );
\r
6656 // Check to see if the browser returns elements by name when
\r
6657 // querying by getElementById (and provide a workaround)
\r
6659 // We're going to inject a fake input element with a specified name
\r
6660 var form = document.createElement("div"),
\r
6661 id = "script" + (new Date()).getTime();
\r
6662 form.innerHTML = "<a name='" + id + "'/>";
\r
6664 // Inject it into the root element, check its status, and remove it quickly
\r
6665 var root = document.documentElement;
\r
6666 root.insertBefore( form, root.firstChild );
\r
6668 // The workaround has to do additional checks after a getElementById
\r
6669 // Which slows things down for other browsers (hence the branching)
\r
6670 if ( document.getElementById( id ) ) {
\r
6671 Expr.find.ID = function(match, context, isXML){
\r
6672 if ( typeof context.getElementById !== "undefined" && !isXML ) {
\r
6673 var m = context.getElementById(match[1]);
\r
6674 return m ? m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : [];
\r
6678 Expr.filter.ID = function(elem, match){
\r
6679 var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id");
\r
6680 return elem.nodeType === 1 && node && node.nodeValue === match;
\r
6684 root.removeChild( form );
\r
6685 root = form = null; // release memory in IE
\r
6689 // Check to see if the browser returns only elements
\r
6690 // when doing getElementsByTagName("*")
\r
6692 // Create a fake element
\r
6693 var div = document.createElement("div");
\r
6694 div.appendChild( document.createComment("") );
\r
6696 // Make sure no comments are found
\r
6697 if ( div.getElementsByTagName("*").length > 0 ) {
\r
6698 Expr.find.TAG = function(match, context){
\r
6699 var results = context.getElementsByTagName(match[1]);
\r
6701 // Filter out possible comments
\r
6702 if ( match[1] === "*" ) {
\r
6705 for ( var i = 0; results[i]; i++ ) {
\r
6706 if ( results[i].nodeType === 1 ) {
\r
6707 tmp.push( results[i] );
\r
6718 // Check to see if an attribute returns normalized href attributes
\r
6719 div.innerHTML = "<a href='#'></a>";
\r
6720 if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" &&
\r
6721 div.firstChild.getAttribute("href") !== "#" ) {
\r
6722 Expr.attrHandle.href = function(elem){
\r
6723 return elem.getAttribute("href", 2);
\r
6727 div = null; // release memory in IE
\r
6730 if ( document.querySelectorAll ) {
\r
6732 var oldSizzle = Sizzle, div = document.createElement("div");
\r
6733 div.innerHTML = "<p class='TEST'></p>";
\r
6735 // Safari can't handle uppercase or unicode characters when
\r
6736 // in quirks mode.
\r
6737 if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) {
\r
6741 Sizzle = function(query, context, extra, seed){
\r
6742 context = context || document;
\r
6744 // Only use querySelectorAll on non-XML documents
\r
6745 // (ID selectors don't work in non-HTML documents)
\r
6746 if ( !seed && context.nodeType === 9 && !Sizzle.isXML(context) ) {
\r
6748 return makeArray( context.querySelectorAll(query), extra );
\r
6752 return oldSizzle(query, context, extra, seed);
\r
6755 for ( var prop in oldSizzle ) {
\r
6756 Sizzle[ prop ] = oldSizzle[ prop ];
\r
6759 div = null; // release memory in IE
\r
6764 var div = document.createElement("div");
\r
6766 div.innerHTML = "<div class='test e'></div><div class='test'></div>";
\r
6768 // Opera can't find a second classname (in 9.6)
\r
6769 // Also, make sure that getElementsByClassName actually exists
\r
6770 if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) {
\r
6774 // Safari caches class attributes, doesn't catch changes (in 3.2)
\r
6775 div.lastChild.className = "e";
\r
6777 if ( div.getElementsByClassName("e").length === 1 ) {
\r
6781 Expr.order.splice(1, 0, "CLASS");
\r
6782 Expr.find.CLASS = function(match, context, isXML) {
\r
6783 if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) {
\r
6784 return context.getElementsByClassName(match[1]);
\r
6788 div = null; // release memory in IE
\r
6791 function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
\r
6792 for ( var i = 0, l = checkSet.length; i < l; i++ ) {
\r
6793 var elem = checkSet[i];
\r
6796 var match = false;
\r
6799 if ( elem.sizcache === doneName ) {
\r
6800 match = checkSet[elem.sizset];
\r
6804 if ( elem.nodeType === 1 && !isXML ){
\r
6805 elem.sizcache = doneName;
\r
6809 if ( elem.nodeName.toLowerCase() === cur ) {
\r
6817 checkSet[i] = match;
\r
6822 function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
\r
6823 for ( var i = 0, l = checkSet.length; i < l; i++ ) {
\r
6824 var elem = checkSet[i];
\r
6827 var match = false;
\r
6830 if ( elem.sizcache === doneName ) {
\r
6831 match = checkSet[elem.sizset];
\r
6835 if ( elem.nodeType === 1 ) {
\r
6837 elem.sizcache = doneName;
\r
6840 if ( typeof cur !== "string" ) {
\r
6841 if ( elem === cur ) {
\r
6846 } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) {
\r
6855 checkSet[i] = match;
\r
6860 Sizzle.contains = document.compareDocumentPosition ? function(a, b){
\r
6861 return !!(a.compareDocumentPosition(b) & 16);
\r
6862 } : function(a, b){
\r
6863 return a !== b && (a.contains ? a.contains(b) : true);
\r
6866 Sizzle.isXML = function(elem){
\r
6867 // documentElement is verified for cases where it doesn't yet exist
\r
6868 // (such as loading iframes in IE - #4833)
\r
6869 var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement;
\r
6870 return documentElement ? documentElement.nodeName !== "HTML" : false;
\r
6873 var posProcess = function(selector, context){
\r
6874 var tmpSet = [], later = "", match,
\r
6875 root = context.nodeType ? [context] : context;
\r
6877 // Position selectors must be done after the filter
\r
6878 // And so must :not(positional) so we move all PSEUDOs to the end
\r
6879 while ( (match = Expr.match.PSEUDO.exec( selector )) ) {
\r
6880 later += match[0];
\r
6881 selector = selector.replace( Expr.match.PSEUDO, "" );
\r
6884 selector = Expr.relative[selector] ? selector + "*" : selector;
\r
6886 for ( var i = 0, l = root.length; i < l; i++ ) {
\r
6887 Sizzle( selector, root[i], tmpSet );
\r
6890 return Sizzle.filter( later, tmpSet );
\r
6895 window.tinymce.dom.Sizzle = Sizzle;
\r
6900 (function(tinymce) {
\r
6902 var each = tinymce.each, DOM = tinymce.DOM, isIE = tinymce.isIE, isWebKit = tinymce.isWebKit, Event;
\r
6904 tinymce.create('tinymce.dom.EventUtils', {
\r
6905 EventUtils : function() {
\r
6910 add : function(o, n, f, s) {
\r
6911 var cb, t = this, el = t.events, r;
\r
6913 if (n instanceof Array) {
\r
6916 each(n, function(n) {
\r
6917 r.push(t.add(o, n, f, s));
\r
6924 if (o && o.hasOwnProperty && o instanceof Array) {
\r
6927 each(o, function(o) {
\r
6929 r.push(t.add(o, n, f, s));
\r
6940 // Setup event callback
\r
6941 cb = function(e) {
\r
6942 // Is all events disabled
\r
6946 e = e || window.event;
\r
6948 // Patch in target, preventDefault and stopPropagation in IE it's W3C valid
\r
6951 e.target = e.srcElement;
\r
6953 // Patch in preventDefault, stopPropagation methods for W3C compatibility
\r
6954 tinymce.extend(e, t._stoppers);
\r
6960 return f.call(s, e);
\r
6963 if (n == 'unload') {
\r
6964 tinymce.unloads.unshift({func : cb});
\r
6968 if (n == 'init') {
\r
6977 // Store away listener reference
\r
6991 remove : function(o, n, f) {
\r
6992 var t = this, a = t.events, s = false, r;
\r
6995 if (o && o.hasOwnProperty && o instanceof Array) {
\r
6998 each(o, function(o) {
\r
7000 r.push(t.remove(o, n, f));
\r
7008 each(a, function(e, i) {
\r
7009 if (e.obj == o && e.name == n && (!f || (e.func == f || e.cfunc == f))) {
\r
7011 t._remove(o, n, e.cfunc);
\r
7020 clear : function(o) {
\r
7021 var t = this, a = t.events, i, e;
\r
7026 for (i = a.length - 1; i >= 0; i--) {
\r
7029 if (e.obj === o) {
\r
7030 t._remove(e.obj, e.name, e.cfunc);
\r
7031 e.obj = e.cfunc = null;
\r
7038 cancel : function(e) {
\r
7044 return this.prevent(e);
\r
7047 stop : function(e) {
\r
7048 if (e.stopPropagation)
\r
7049 e.stopPropagation();
\r
7051 e.cancelBubble = true;
\r
7056 prevent : function(e) {
\r
7057 if (e.preventDefault)
\r
7058 e.preventDefault();
\r
7060 e.returnValue = false;
\r
7065 destroy : function() {
\r
7068 each(t.events, function(e, i) {
\r
7069 t._remove(e.obj, e.name, e.cfunc);
\r
7070 e.obj = e.cfunc = null;
\r
7077 _add : function(o, n, f) {
\r
7078 if (o.attachEvent)
\r
7079 o.attachEvent('on' + n, f);
\r
7080 else if (o.addEventListener)
\r
7081 o.addEventListener(n, f, false);
\r
7086 _remove : function(o, n, f) {
\r
7089 if (o.detachEvent)
\r
7090 o.detachEvent('on' + n, f);
\r
7091 else if (o.removeEventListener)
\r
7092 o.removeEventListener(n, f, false);
\r
7094 o['on' + n] = null;
\r
7096 // Might fail with permission denined on IE so we just ignore that
\r
7101 _pageInit : function(win) {
\r
7104 // Keep it from running more than once
\r
7108 t.domLoaded = true;
\r
7110 each(t.inits, function(c) {
\r
7117 _wait : function(win) {
\r
7118 var t = this, doc = win.document;
\r
7120 // No need since the document is already loaded
\r
7121 if (win.tinyMCE_GZ && tinyMCE_GZ.loaded) {
\r
7127 if (doc.attachEvent) {
\r
7128 doc.attachEvent("onreadystatechange", function() {
\r
7129 if (doc.readyState === "complete") {
\r
7130 doc.detachEvent("onreadystatechange", arguments.callee);
\r
7135 if (doc.documentElement.doScroll && win == win.top) {
\r
7141 // If IE is used, use the trick by Diego Perini licensed under MIT by request to the author.
\r
7142 // http://javascript.nwbox.com/IEContentLoaded/
\r
7143 doc.documentElement.doScroll("left");
\r
7145 setTimeout(arguments.callee, 0);
\r
7152 } else if (doc.addEventListener) {
\r
7153 t._add(win, 'DOMContentLoaded', function() {
\r
7158 t._add(win, 'load', function() {
\r
7164 preventDefault : function() {
\r
7165 this.returnValue = false;
\r
7168 stopPropagation : function() {
\r
7169 this.cancelBubble = true;
\r
7174 Event = tinymce.dom.Event = new tinymce.dom.EventUtils();
\r
7176 // Dispatch DOM content loaded event for IE and Safari
\r
7177 Event._wait(window);
\r
7179 tinymce.addUnload(function() {
\r
7184 (function(tinymce) {
\r
7185 tinymce.dom.Element = function(id, settings) {
\r
7186 var t = this, dom, el;
\r
7188 t.settings = settings = settings || {};
\r
7190 t.dom = dom = settings.dom || tinymce.DOM;
\r
7192 // Only IE leaks DOM references, this is a lot faster
\r
7193 if (!tinymce.isIE)
\r
7194 el = dom.get(t.id);
\r
7197 ('getPos,getRect,getParent,add,setStyle,getStyle,setStyles,' +
\r
7198 'setAttrib,setAttribs,getAttrib,addClass,removeClass,' +
\r
7199 'hasClass,getOuterHTML,setOuterHTML,remove,show,hide,' +
\r
7200 'isHidden,setHTML,get').split(/,/)
\r
7202 t[k] = function() {
\r
7205 for (i = 0; i < arguments.length; i++)
\r
7206 a.push(arguments[i]);
\r
7208 a = dom[k].apply(dom, a);
\r
7215 tinymce.extend(t, {
\r
7216 on : function(n, f, s) {
\r
7217 return tinymce.dom.Event.add(t.id, n, f, s);
\r
7220 getXY : function() {
\r
7222 x : parseInt(t.getStyle('left')),
\r
7223 y : parseInt(t.getStyle('top'))
\r
7227 getSize : function() {
\r
7228 var n = dom.get(t.id);
\r
7231 w : parseInt(t.getStyle('width') || n.clientWidth),
\r
7232 h : parseInt(t.getStyle('height') || n.clientHeight)
\r
7236 moveTo : function(x, y) {
\r
7237 t.setStyles({left : x, top : y});
\r
7240 moveBy : function(x, y) {
\r
7241 var p = t.getXY();
\r
7243 t.moveTo(p.x + x, p.y + y);
\r
7246 resizeTo : function(w, h) {
\r
7247 t.setStyles({width : w, height : h});
\r
7250 resizeBy : function(w, h) {
\r
7251 var s = t.getSize();
\r
7253 t.resizeTo(s.w + w, s.h + h);
\r
7256 update : function(k) {
\r
7259 if (tinymce.isIE6 && settings.blocker) {
\r
7263 if (k.indexOf('get') === 0 || k.indexOf('has') === 0 || k.indexOf('is') === 0)
\r
7266 // Remove blocker on remove
\r
7267 if (k == 'remove') {
\r
7268 dom.remove(t.blocker);
\r
7273 t.blocker = dom.uniqueId();
\r
7274 b = dom.add(settings.container || dom.getRoot(), 'iframe', {id : t.blocker, style : 'position:absolute;', frameBorder : 0, src : 'javascript:""'});
\r
7275 dom.setStyle(b, 'opacity', 0);
\r
7277 b = dom.get(t.blocker);
\r
7279 dom.setStyles(b, {
\r
7280 left : t.getStyle('left', 1),
\r
7281 top : t.getStyle('top', 1),
\r
7282 width : t.getStyle('width', 1),
\r
7283 height : t.getStyle('height', 1),
\r
7284 display : t.getStyle('display', 1),
\r
7285 zIndex : parseInt(t.getStyle('zIndex', 1) || 0) - 1
\r
7293 (function(tinymce) {
\r
7294 function trimNl(s) {
\r
7295 return s.replace(/[\n\r]+/g, '');
\r
7299 var is = tinymce.is, isIE = tinymce.isIE, each = tinymce.each;
\r
7301 tinymce.create('tinymce.dom.Selection', {
\r
7302 Selection : function(dom, win, serializer) {
\r
7307 t.serializer = serializer;
\r
7311 'onBeforeSetContent',
\r
7313 'onBeforeGetContent',
\r
7319 t[e] = new tinymce.util.Dispatcher(t);
\r
7322 // No W3C Range support
\r
7323 if (!t.win.getSelection)
\r
7324 t.tridentSel = new tinymce.dom.TridentSelection(t);
\r
7326 if (tinymce.isIE && dom.boxModel)
\r
7327 this._fixIESelection();
\r
7330 tinymce.addUnload(t.destroy, t);
\r
7333 setCursorLocation: function(node, offset) {
\r
7334 var t = this; var r = t.dom.createRng();
\r
7335 r.setStart(node, offset);
\r
7336 r.setEnd(node, offset);
\r
7338 t.collapse(false);
\r
7340 getContent : function(s) {
\r
7341 var t = this, r = t.getRng(), e = t.dom.create("body"), se = t.getSel(), wb, wa, n;
\r
7346 s.format = s.format || 'html';
\r
7347 s.forced_root_block = '';
\r
7348 t.onBeforeGetContent.dispatch(t, s);
\r
7350 if (s.format == 'text')
\r
7351 return t.isCollapsed() ? '' : (r.text || (se.toString ? se.toString() : ''));
\r
7353 if (r.cloneContents) {
\r
7354 n = r.cloneContents();
\r
7358 } else if (is(r.item) || is(r.htmlText)) {
\r
7359 // IE will produce invalid markup if elements are present that
\r
7360 // it doesn't understand like custom elements or HTML5 elements.
\r
7361 // Adding a BR in front of the contents and then remoiving it seems to fix it though.
\r
7362 e.innerHTML = '<br>' + (r.item ? r.item(0).outerHTML : r.htmlText);
\r
7363 e.removeChild(e.firstChild);
\r
7365 e.innerHTML = r.toString();
\r
7367 // Keep whitespace before and after
\r
7368 if (/^\s/.test(e.innerHTML))
\r
7371 if (/\s+$/.test(e.innerHTML))
\r
7374 s.getInner = true;
\r
7376 s.content = t.isCollapsed() ? '' : wb + t.serializer.serialize(e, s) + wa;
\r
7377 t.onGetContent.dispatch(t, s);
\r
7382 setContent : function(content, args) {
\r
7383 var self = this, rng = self.getRng(), caretNode, doc = self.win.document, frag, temp;
\r
7385 args = args || {format : 'html'};
\r
7387 content = args.content = content;
\r
7389 // Dispatch before set content event
\r
7390 if (!args.no_events)
\r
7391 self.onBeforeSetContent.dispatch(self, args);
\r
7393 content = args.content;
\r
7395 if (rng.insertNode) {
\r
7396 // Make caret marker since insertNode places the caret in the beginning of text after insert
\r
7397 content += '<span id="__caret">_</span>';
\r
7399 // Delete and insert new node
\r
7400 if (rng.startContainer == doc && rng.endContainer == doc) {
\r
7401 // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents
\r
7402 doc.body.innerHTML = content;
\r
7404 rng.deleteContents();
\r
7406 if (doc.body.childNodes.length == 0) {
\r
7407 doc.body.innerHTML = content;
\r
7409 // createContextualFragment doesn't exists in IE 9 DOMRanges
\r
7410 if (rng.createContextualFragment) {
\r
7411 rng.insertNode(rng.createContextualFragment(content));
\r
7413 // Fake createContextualFragment call in IE 9
\r
7414 frag = doc.createDocumentFragment();
\r
7415 temp = doc.createElement('div');
\r
7417 frag.appendChild(temp);
\r
7418 temp.outerHTML = content;
\r
7420 rng.insertNode(frag);
\r
7425 // Move to caret marker
\r
7426 caretNode = self.dom.get('__caret');
\r
7428 // Make sure we wrap it compleatly, Opera fails with a simple select call
\r
7429 rng = doc.createRange();
\r
7430 rng.setStartBefore(caretNode);
\r
7431 rng.setEndBefore(caretNode);
\r
7434 // Remove the caret position
\r
7435 self.dom.remove('__caret');
\r
7440 // Might fail on Opera for some odd reason
\r
7444 // Delete content and get caret text selection
\r
7445 doc.execCommand('Delete', false, null);
\r
7446 rng = self.getRng();
\r
7449 // Explorer removes spaces from the beginning of pasted contents
\r
7450 if (/^\s+/.test(content)) {
\r
7451 rng.pasteHTML('<span id="__mce_tmp">_</span>' + content);
\r
7452 self.dom.remove('__mce_tmp');
\r
7454 rng.pasteHTML(content);
\r
7457 // Dispatch set content event
\r
7458 if (!args.no_events)
\r
7459 self.onSetContent.dispatch(self, args);
\r
7462 getStart : function() {
\r
7463 var rng = this.getRng(), startElement, parentElement, checkRng, node;
\r
7465 if (rng.duplicate || rng.item) {
\r
7466 // Control selection, return first item
\r
7468 return rng.item(0);
\r
7470 // Get start element
\r
7471 checkRng = rng.duplicate();
\r
7472 checkRng.collapse(1);
\r
7473 startElement = checkRng.parentElement();
\r
7475 // Check if range parent is inside the start element, then return the inner parent element
\r
7476 // This will fix issues when a single element is selected, IE would otherwise return the wrong start element
\r
7477 parentElement = node = rng.parentElement();
\r
7478 while (node = node.parentNode) {
\r
7479 if (node == startElement) {
\r
7480 startElement = parentElement;
\r
7485 return startElement;
\r
7487 startElement = rng.startContainer;
\r
7489 if (startElement.nodeType == 1 && startElement.hasChildNodes())
\r
7490 startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)];
\r
7492 if (startElement && startElement.nodeType == 3)
\r
7493 return startElement.parentNode;
\r
7495 return startElement;
\r
7499 getEnd : function() {
\r
7500 var t = this, r = t.getRng(), e, eo;
\r
7502 if (r.duplicate || r.item) {
\r
7506 r = r.duplicate();
\r
7508 e = r.parentElement();
\r
7510 if (e && e.nodeName == 'BODY')
\r
7511 return e.lastChild || e;
\r
7515 e = r.endContainer;
\r
7518 if (e.nodeType == 1 && e.hasChildNodes())
\r
7519 e = e.childNodes[eo > 0 ? eo - 1 : eo];
\r
7521 if (e && e.nodeType == 3)
\r
7522 return e.parentNode;
\r
7528 getBookmark : function(type, normalized) {
\r
7529 var t = this, dom = t.dom, rng, rng2, id, collapsed, name, element, index, chr = '\uFEFF', styles;
\r
7531 function findIndex(name, element) {
\r
7534 each(dom.select(name), function(node, i) {
\r
7535 if (node == element)
\r
7543 function getLocation() {
\r
7544 var rng = t.getRng(true), root = dom.getRoot(), bookmark = {};
\r
7546 function getPoint(rng, start) {
\r
7547 var container = rng[start ? 'startContainer' : 'endContainer'],
\r
7548 offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0;
\r
7550 if (container.nodeType == 3) {
\r
7552 for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling)
\r
7553 offset += node.nodeValue.length;
\r
7556 point.push(offset);
\r
7558 childNodes = container.childNodes;
\r
7560 if (offset >= childNodes.length && childNodes.length) {
\r
7562 offset = Math.max(0, childNodes.length - 1);
\r
7565 point.push(t.dom.nodeIndex(childNodes[offset], normalized) + after);
\r
7568 for (; container && container != root; container = container.parentNode)
\r
7569 point.push(t.dom.nodeIndex(container, normalized));
\r
7574 bookmark.start = getPoint(rng, true);
\r
7576 if (!t.isCollapsed())
\r
7577 bookmark.end = getPoint(rng);
\r
7583 return t.tridentSel.getBookmark(type);
\r
7585 return getLocation();
\r
7588 // Handle simple range
\r
7590 return {rng : t.getRng()};
\r
7593 id = dom.uniqueId();
\r
7594 collapsed = tinyMCE.activeEditor.selection.isCollapsed();
\r
7595 styles = 'overflow:hidden;line-height:0px';
\r
7597 // Explorer method
\r
7598 if (rng.duplicate || rng.item) {
\r
7601 rng2 = rng.duplicate();
\r
7604 // Insert start marker
\r
7606 rng.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_start" style="' + styles + '">' + chr + '</span>');
\r
7608 // Insert end marker
\r
7610 rng2.collapse(false);
\r
7612 // Detect the empty space after block elements in IE and move the end back one character <p></p>] becomes <p>]</p>
\r
7613 rng.moveToElementText(rng2.parentElement());
\r
7614 if (rng.compareEndPoints('StartToEnd', rng2) == 0)
\r
7615 rng2.move('character', -1);
\r
7617 rng2.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_end" style="' + styles + '">' + chr + '</span>');
\r
7620 // IE might throw unspecified error so lets ignore it
\r
7624 // Control selection
\r
7625 element = rng.item(0);
\r
7626 name = element.nodeName;
\r
7628 return {name : name, index : findIndex(name, element)};
\r
7631 element = t.getNode();
\r
7632 name = element.nodeName;
\r
7633 if (name == 'IMG')
\r
7634 return {name : name, index : findIndex(name, element)};
\r
7637 rng2 = rng.cloneRange();
\r
7639 // Insert end marker
\r
7641 rng2.collapse(false);
\r
7642 rng2.insertNode(dom.create('span', {'data-mce-type' : "bookmark", id : id + '_end', style : styles}, chr));
\r
7645 rng.collapse(true);
\r
7646 rng.insertNode(dom.create('span', {'data-mce-type' : "bookmark", id : id + '_start', style : styles}, chr));
\r
7649 t.moveToBookmark({id : id, keep : 1});
\r
7654 moveToBookmark : function(bookmark) {
\r
7655 var t = this, dom = t.dom, marker1, marker2, rng, root, startContainer, endContainer, startOffset, endOffset;
\r
7658 if (bookmark.start) {
\r
7659 rng = dom.createRng();
\r
7660 root = dom.getRoot();
\r
7662 function setEndPoint(start) {
\r
7663 var point = bookmark[start ? 'start' : 'end'], i, node, offset, children;
\r
7666 offset = point[0];
\r
7668 // Find container node
\r
7669 for (node = root, i = point.length - 1; i >= 1; i--) {
\r
7670 children = node.childNodes;
\r
7672 if (point[i] > children.length - 1)
\r
7675 node = children[point[i]];
\r
7678 // Move text offset to best suitable location
\r
7679 if (node.nodeType === 3)
\r
7680 offset = Math.min(point[0], node.nodeValue.length);
\r
7682 // Move element offset to best suitable location
\r
7683 if (node.nodeType === 1)
\r
7684 offset = Math.min(point[0], node.childNodes.length);
\r
7686 // Set offset within container node
\r
7688 rng.setStart(node, offset);
\r
7690 rng.setEnd(node, offset);
\r
7697 return t.tridentSel.moveToBookmark(bookmark);
\r
7699 if (setEndPoint(true) && setEndPoint()) {
\r
7702 } else if (bookmark.id) {
\r
7703 function restoreEndPoint(suffix) {
\r
7704 var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep;
\r
7707 node = marker.parentNode;
\r
7709 if (suffix == 'start') {
\r
7711 idx = dom.nodeIndex(marker);
\r
7713 node = marker.firstChild;
\r
7717 startContainer = endContainer = node;
\r
7718 startOffset = endOffset = idx;
\r
7721 idx = dom.nodeIndex(marker);
\r
7723 node = marker.firstChild;
\r
7727 endContainer = node;
\r
7732 prev = marker.previousSibling;
\r
7733 next = marker.nextSibling;
\r
7735 // Remove all marker text nodes
\r
7736 each(tinymce.grep(marker.childNodes), function(node) {
\r
7737 if (node.nodeType == 3)
\r
7738 node.nodeValue = node.nodeValue.replace(/\uFEFF/g, '');
\r
7741 // Remove marker but keep children if for example contents where inserted into the marker
\r
7742 // Also remove duplicated instances of the marker for example by a split operation or by WebKit auto split on paste feature
\r
7743 while (marker = dom.get(bookmark.id + '_' + suffix))
\r
7744 dom.remove(marker, 1);
\r
7746 // If siblings are text nodes then merge them unless it's Opera since it some how removes the node
\r
7747 // 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
7748 if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !tinymce.isOpera) {
\r
7749 idx = prev.nodeValue.length;
\r
7750 prev.appendData(next.nodeValue);
\r
7753 if (suffix == 'start') {
\r
7754 startContainer = endContainer = prev;
\r
7755 startOffset = endOffset = idx;
\r
7757 endContainer = prev;
\r
7765 function addBogus(node) {
\r
7766 // Adds a bogus BR element for empty block elements or just a space on IE since it renders BR elements incorrectly
\r
7767 if (dom.isBlock(node) && !node.innerHTML)
\r
7768 node.innerHTML = !isIE ? '<br data-mce-bogus="1" />' : ' ';
\r
7773 // Restore start/end points
\r
7774 restoreEndPoint('start');
\r
7775 restoreEndPoint('end');
\r
7777 if (startContainer) {
\r
7778 rng = dom.createRng();
\r
7779 rng.setStart(addBogus(startContainer), startOffset);
\r
7780 rng.setEnd(addBogus(endContainer), endOffset);
\r
7783 } else if (bookmark.name) {
\r
7784 t.select(dom.select(bookmark.name)[bookmark.index]);
\r
7785 } else if (bookmark.rng)
\r
7786 t.setRng(bookmark.rng);
\r
7790 select : function(node, content) {
\r
7791 var t = this, dom = t.dom, rng = dom.createRng(), idx;
\r
7794 idx = dom.nodeIndex(node);
\r
7795 rng.setStart(node.parentNode, idx);
\r
7796 rng.setEnd(node.parentNode, idx + 1);
\r
7798 // Find first/last text node or BR element
\r
7800 function setPoint(node, start) {
\r
7801 var walker = new tinymce.dom.TreeWalker(node, node);
\r
7805 if (node.nodeType == 3 && tinymce.trim(node.nodeValue).length != 0) {
\r
7807 rng.setStart(node, 0);
\r
7809 rng.setEnd(node, node.nodeValue.length);
\r
7815 if (node.nodeName == 'BR') {
\r
7817 rng.setStartBefore(node);
\r
7819 rng.setEndBefore(node);
\r
7823 } while (node = (start ? walker.next() : walker.prev()));
\r
7826 setPoint(node, 1);
\r
7836 isCollapsed : function() {
\r
7837 var t = this, r = t.getRng(), s = t.getSel();
\r
7842 if (r.compareEndPoints)
\r
7843 return r.compareEndPoints('StartToEnd', r) === 0;
\r
7845 return !s || r.collapsed;
\r
7848 collapse : function(to_start) {
\r
7849 var self = this, rng = self.getRng(), node;
\r
7851 // Control range on IE
\r
7853 node = rng.item(0);
\r
7854 rng = self.win.document.body.createTextRange();
\r
7855 rng.moveToElementText(node);
\r
7858 rng.collapse(!!to_start);
\r
7862 getSel : function() {
\r
7863 var t = this, w = this.win;
\r
7865 return w.getSelection ? w.getSelection() : w.document.selection;
\r
7868 getRng : function(w3c) {
\r
7869 var t = this, s, r, elm, doc = t.win.document;
\r
7871 // Found tridentSel object then we need to use that one
\r
7872 if (w3c && t.tridentSel)
\r
7873 return t.tridentSel.getRangeAt(0);
\r
7876 if (s = t.getSel())
\r
7877 r = s.rangeCount > 0 ? s.getRangeAt(0) : (s.createRange ? s.createRange() : doc.createRange());
\r
7879 // IE throws unspecified error here if TinyMCE is placed in a frame/iframe
\r
7882 // We have W3C ranges and it's IE then fake control selection since IE9 doesn't handle that correctly yet
\r
7883 if (tinymce.isIE && r && r.setStart && doc.selection.createRange().item) {
\r
7884 elm = doc.selection.createRange().item(0);
\r
7885 r = doc.createRange();
\r
7886 r.setStartBefore(elm);
\r
7887 r.setEndAfter(elm);
\r
7890 // No range found then create an empty one
\r
7891 // This can occur when the editor is placed in a hidden container element on Gecko
\r
7892 // Or on IE when there was an exception
\r
7894 r = doc.createRange ? doc.createRange() : doc.body.createTextRange();
\r
7896 if (t.selectedRange && t.explicitRange) {
\r
7897 if (r.compareBoundaryPoints(r.START_TO_START, t.selectedRange) === 0 && r.compareBoundaryPoints(r.END_TO_END, t.selectedRange) === 0) {
\r
7898 // Safari, Opera and Chrome only ever select text which causes the range to change.
\r
7899 // This lets us use the originally set range if the selection hasn't been changed by the user.
\r
7900 r = t.explicitRange;
\r
7902 t.selectedRange = null;
\r
7903 t.explicitRange = null;
\r
7910 setRng : function(r) {
\r
7913 if (!t.tridentSel) {
\r
7917 t.explicitRange = r;
\r
7920 s.removeAllRanges();
\r
7922 // IE9 might throw errors here don't know why
\r
7926 t.selectedRange = s.getRangeAt(0);
\r
7930 if (r.cloneRange) {
\r
7931 t.tridentSel.addRange(r);
\r
7935 // Is IE specific range
\r
7939 // Needed for some odd IE bug #1843306
\r
7944 setNode : function(n) {
\r
7947 t.setContent(t.dom.getOuterHTML(n));
\r
7952 getNode : function() {
\r
7953 var t = this, rng = t.getRng(), sel = t.getSel(), elm, start = rng.startContainer, end = rng.endContainer;
\r
7955 // Range maybe lost after the editor is made visible again
\r
7957 return t.dom.getRoot();
\r
7959 if (rng.setStart) {
\r
7960 elm = rng.commonAncestorContainer;
\r
7962 // Handle selection a image or other control like element such as anchors
\r
7963 if (!rng.collapsed) {
\r
7964 if (rng.startContainer == rng.endContainer) {
\r
7965 if (rng.endOffset - rng.startOffset < 2) {
\r
7966 if (rng.startContainer.hasChildNodes())
\r
7967 elm = rng.startContainer.childNodes[rng.startOffset];
\r
7971 // If the anchor node is a element instead of a text node then return this element
\r
7972 //if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1)
\r
7973 // return sel.anchorNode.childNodes[sel.anchorOffset];
\r
7975 // Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent.
\r
7976 // This happens when you double click an underlined word in FireFox.
\r
7977 if (start.nodeType === 3 && end.nodeType === 3) {
\r
7978 function skipEmptyTextNodes(n, forwards) {
\r
7980 while (n && n.nodeType === 3 && n.length === 0) {
\r
7981 n = forwards ? n.nextSibling : n.previousSibling;
\r
7985 if (start.length === rng.startOffset) {
\r
7986 start = skipEmptyTextNodes(start.nextSibling, true);
\r
7988 start = start.parentNode;
\r
7990 if (rng.endOffset === 0) {
\r
7991 end = skipEmptyTextNodes(end.previousSibling, false);
\r
7993 end = end.parentNode;
\r
7996 if (start && start === end)
\r
8001 if (elm && elm.nodeType == 3)
\r
8002 return elm.parentNode;
\r
8007 return rng.item ? rng.item(0) : rng.parentElement();
\r
8010 getSelectedBlocks : function(st, en) {
\r
8011 var t = this, dom = t.dom, sb, eb, n, bl = [];
\r
8013 sb = dom.getParent(st || t.getStart(), dom.isBlock);
\r
8014 eb = dom.getParent(en || t.getEnd(), dom.isBlock);
\r
8019 if (sb && eb && sb != eb) {
\r
8022 while ((n = n.nextSibling) && n != eb) {
\r
8023 if (dom.isBlock(n))
\r
8028 if (eb && sb != eb)
\r
8034 normalize : function() {
\r
8035 var self = this, rng, normalized;
\r
8037 // Normalize only on non IE browsers for now
\r
8041 function normalizeEndPoint(start) {
\r
8042 var container, offset, walker, dom = self.dom, body = dom.getRoot(), node;
\r
8044 container = rng[(start ? 'start' : 'end') + 'Container'];
\r
8045 offset = rng[(start ? 'start' : 'end') + 'Offset'];
\r
8047 // If the container is a document move it to the body element
\r
8048 if (container.nodeType === 9) {
\r
8049 container = container.body;
\r
8053 // If the container is body try move it into the closest text node or position
\r
8054 // TODO: Add more logic here to handle element selection cases
\r
8055 if (container === body) {
\r
8056 // Resolve the index
\r
8057 if (container.hasChildNodes()) {
\r
8058 container = container.childNodes[Math.min(!start && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1)];
\r
8061 // Don't walk into elements that doesn't have any child nodes like a IMG
\r
8062 if (container.hasChildNodes()) {
\r
8063 // Walk the DOM to find a text node to place the caret at or a BR
\r
8065 walker = new tinymce.dom.TreeWalker(container, body);
\r
8067 // Found a text node use that position
\r
8068 if (node.nodeType === 3) {
\r
8069 offset = start ? 0 : node.nodeValue.length - 1;
\r
8074 // Found a BR element that we can place the caret before
\r
8075 if (node.nodeName === 'BR') {
\r
8076 offset = dom.nodeIndex(node);
\r
8077 container = node.parentNode;
\r
8080 } while (node = (start ? walker.next() : walker.prev()));
\r
8082 normalized = true;
\r
8087 // Set endpoint if it was normalized
\r
8089 rng['set' + (start ? 'Start' : 'End')](container, offset);
\r
8092 rng = self.getRng();
\r
8094 // Normalize the end points
\r
8095 normalizeEndPoint(true);
\r
8097 if (rng.collapsed)
\r
8098 normalizeEndPoint();
\r
8100 // Set the selection if it was normalized
\r
8102 //console.log(self.dom.dumpRng(rng));
\r
8107 destroy : function(s) {
\r
8112 // Manual destroy then remove unload handler
\r
8114 tinymce.removeUnload(t.destroy);
\r
8117 // 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
8118 _fixIESelection : function() {
\r
8119 var dom = this.dom, doc = dom.doc, body = doc.body, started, startRng, htmlElm;
\r
8121 // Make HTML element unselectable since we are going to handle selection by hand
\r
8122 doc.documentElement.unselectable = true;
\r
8124 // Return range from point or null if it failed
\r
8125 function rngFromPoint(x, y) {
\r
8126 var rng = body.createTextRange();
\r
8129 rng.moveToPoint(x, y);
\r
8131 // IE sometimes throws and exception, so lets just ignore it
\r
8138 // Fires while the selection is changing
\r
8139 function selectionChange(e) {
\r
8142 // Check if the button is down or not
\r
8144 // Create range from mouse position
\r
8145 pointRng = rngFromPoint(e.x, e.y);
\r
8148 // Check if pointRange is before/after selection then change the endPoint
\r
8149 if (pointRng.compareEndPoints('StartToStart', startRng) > 0)
\r
8150 pointRng.setEndPoint('StartToStart', startRng);
\r
8152 pointRng.setEndPoint('EndToEnd', startRng);
\r
8154 pointRng.select();
\r
8160 // Removes listeners
\r
8161 function endSelection() {
\r
8162 var rng = doc.selection.createRange();
\r
8164 // If the range is collapsed then use the last start range
\r
8165 if (startRng && !rng.item && rng.compareEndPoints('StartToEnd', rng) === 0)
\r
8166 startRng.select();
\r
8168 dom.unbind(doc, 'mouseup', endSelection);
\r
8169 dom.unbind(doc, 'mousemove', selectionChange);
\r
8170 startRng = started = 0;
\r
8173 // Detect when user selects outside BODY
\r
8174 dom.bind(doc, ['mousedown', 'contextmenu'], function(e) {
\r
8175 if (e.target.nodeName === 'HTML') {
\r
8179 // Detect vertical scrollbar, since IE will fire a mousedown on the scrollbar and have target set as HTML
\r
8180 htmlElm = doc.documentElement;
\r
8181 if (htmlElm.scrollHeight > htmlElm.clientHeight)
\r
8185 // Setup start position
\r
8186 startRng = rngFromPoint(e.x, e.y);
\r
8188 // Listen for selection change events
\r
8189 dom.bind(doc, 'mouseup', endSelection);
\r
8190 dom.bind(doc, 'mousemove', selectionChange);
\r
8193 startRng.select();
\r
8201 (function(tinymce) {
\r
8202 tinymce.dom.Serializer = function(settings, dom, schema) {
\r
8203 var onPreProcess, onPostProcess, isIE = tinymce.isIE, each = tinymce.each, htmlParser;
\r
8205 // Support the old apply_source_formatting option
\r
8206 if (!settings.apply_source_formatting)
\r
8207 settings.indent = false;
\r
8209 settings.remove_trailing_brs = true;
\r
8211 // Default DOM and Schema if they are undefined
\r
8212 dom = dom || tinymce.DOM;
\r
8213 schema = schema || new tinymce.html.Schema(settings);
\r
8214 settings.entity_encoding = settings.entity_encoding || 'named';
\r
8216 onPreProcess = new tinymce.util.Dispatcher(self);
\r
8218 onPostProcess = new tinymce.util.Dispatcher(self);
\r
8220 htmlParser = new tinymce.html.DomParser(settings, schema);
\r
8222 // Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed
\r
8223 htmlParser.addAttributeFilter('src,href,style', function(nodes, name) {
\r
8224 var i = nodes.length, node, value, internalName = 'data-mce-' + name, urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope, undef;
\r
8229 value = node.attributes.map[internalName];
\r
8230 if (value !== undef) {
\r
8231 // Set external name to internal value and remove internal
\r
8232 node.attr(name, value.length > 0 ? value : null);
\r
8233 node.attr(internalName, null);
\r
8235 // No internal attribute found then convert the value we have in the DOM
\r
8236 value = node.attributes.map[name];
\r
8238 if (name === "style")
\r
8239 value = dom.serializeStyle(dom.parseStyle(value), node.name);
\r
8240 else if (urlConverter)
\r
8241 value = urlConverter.call(urlConverterScope, value, name, node.name);
\r
8243 node.attr(name, value.length > 0 ? value : null);
\r
8248 // Remove internal classes mceItem<..>
\r
8249 htmlParser.addAttributeFilter('class', function(nodes, name) {
\r
8250 var i = nodes.length, node, value;
\r
8254 value = node.attr('class').replace(/\s*mce(Item\w+|Selected)\s*/g, '');
\r
8255 node.attr('class', value.length > 0 ? value : null);
\r
8259 // Remove bookmark elements
\r
8260 htmlParser.addAttributeFilter('data-mce-type', function(nodes, name, args) {
\r
8261 var i = nodes.length, node;
\r
8266 if (node.attributes.map['data-mce-type'] === 'bookmark' && !args.cleanup)
\r
8271 // Force script into CDATA sections and remove the mce- prefix also add comments around styles
\r
8272 htmlParser.addNodeFilter('script,style', function(nodes, name) {
\r
8273 var i = nodes.length, node, value;
\r
8275 function trim(value) {
\r
8276 return value.replace(/(<!--\[CDATA\[|\]\]-->)/g, '\n')
\r
8277 .replace(/^[\r\n]*|[\r\n]*$/g, '')
\r
8278 .replace(/^\s*(\/\/\s*<!--|\/\/\s*<!\[CDATA\[|<!--|<!\[CDATA\[)[\r\n]*/g, '')
\r
8279 .replace(/\s*(\/\/\s*\]\]>|\/\/\s*-->|\]\]>|-->|\]\]-->)\s*$/g, '');
\r
8284 value = node.firstChild ? node.firstChild.value : '';
\r
8286 if (name === "script") {
\r
8287 // Remove mce- prefix from script elements
\r
8288 node.attr('type', (node.attr('type') || 'text/javascript').replace(/^mce\-/, ''));
\r
8290 if (value.length > 0)
\r
8291 node.firstChild.value = '// <![CDATA[\n' + trim(value) + '\n// ]]>';
\r
8293 if (value.length > 0)
\r
8294 node.firstChild.value = '<!--\n' + trim(value) + '\n-->';
\r
8299 // Convert comments to cdata and handle protected comments
\r
8300 htmlParser.addNodeFilter('#comment', function(nodes, name) {
\r
8301 var i = nodes.length, node;
\r
8306 if (node.value.indexOf('[CDATA[') === 0) {
\r
8307 node.name = '#cdata';
\r
8309 node.value = node.value.replace(/^\[CDATA\[|\]\]$/g, '');
\r
8310 } else if (node.value.indexOf('mce:protected ') === 0) {
\r
8311 node.name = "#text";
\r
8314 node.value = unescape(node.value).substr(14);
\r
8319 htmlParser.addNodeFilter('xml:namespace,input', function(nodes, name) {
\r
8320 var i = nodes.length, node;
\r
8324 if (node.type === 7)
\r
8326 else if (node.type === 1) {
\r
8327 if (name === "input" && !("type" in node.attributes.map))
\r
8328 node.attr('type', 'text');
\r
8333 // Fix list elements, TODO: Replace this later
\r
8334 if (settings.fix_list_elements) {
\r
8335 htmlParser.addNodeFilter('ul,ol', function(nodes, name) {
\r
8336 var i = nodes.length, node, parentNode;
\r
8340 parentNode = node.parent;
\r
8342 if (parentNode.name === 'ul' || parentNode.name === 'ol') {
\r
8343 if (node.prev && node.prev.name === 'li') {
\r
8344 node.prev.append(node);
\r
8351 // Remove internal data attributes
\r
8352 htmlParser.addAttributeFilter('data-mce-src,data-mce-href,data-mce-style', function(nodes, name) {
\r
8353 var i = nodes.length;
\r
8356 nodes[i].attr(name, null);
\r
8360 // Return public methods
\r
8364 addNodeFilter : htmlParser.addNodeFilter,
\r
8366 addAttributeFilter : htmlParser.addAttributeFilter,
\r
8368 onPreProcess : onPreProcess,
\r
8370 onPostProcess : onPostProcess,
\r
8372 serialize : function(node, args) {
\r
8373 var impl, doc, oldDoc, htmlSerializer, content;
\r
8375 // Explorer won't clone contents of script and style and the
\r
8376 // selected index of select elements are cleared on a clone operation.
\r
8377 if (isIE && dom.select('script,style,select,map').length > 0) {
\r
8378 content = node.innerHTML;
\r
8379 node = node.cloneNode(false);
\r
8380 dom.setHTML(node, content);
\r
8382 node = node.cloneNode(true);
\r
8384 // Nodes needs to be attached to something in WebKit/Opera
\r
8385 // Older builds of Opera crashes if you attach the node to an document created dynamically
\r
8386 // and since we can't feature detect a crash we need to sniff the acutal build number
\r
8387 // This fix will make DOM ranges and make Sizzle happy!
\r
8388 impl = node.ownerDocument.implementation;
\r
8389 if (impl.createHTMLDocument) {
\r
8390 // Create an empty HTML document
\r
8391 doc = impl.createHTMLDocument("");
\r
8393 // Add the element or it's children if it's a body element to the new document
\r
8394 each(node.nodeName == 'BODY' ? node.childNodes : [node], function(node) {
\r
8395 doc.body.appendChild(doc.importNode(node, true));
\r
8398 // Grab first child or body element for serialization
\r
8399 if (node.nodeName != 'BODY')
\r
8400 node = doc.body.firstChild;
\r
8404 // set the new document in DOMUtils so createElement etc works
\r
8409 args = args || {};
\r
8410 args.format = args.format || 'html';
\r
8413 if (!args.no_events) {
\r
8415 onPreProcess.dispatch(self, args);
\r
8418 // Setup serializer
\r
8419 htmlSerializer = new tinymce.html.Serializer(settings, schema);
\r
8421 // Parse and serialize HTML
\r
8422 args.content = htmlSerializer.serialize(
\r
8423 htmlParser.parse(args.getInner ? node.innerHTML : tinymce.trim(dom.getOuterHTML(node), args), args)
\r
8426 // Replace all BOM characters for now until we can find a better solution
\r
8427 if (!args.cleanup)
\r
8428 args.content = args.content.replace(/\uFEFF/g, '');
\r
8431 if (!args.no_events)
\r
8432 onPostProcess.dispatch(self, args);
\r
8434 // Restore the old document if it was changed
\r
8440 return args.content;
\r
8443 addRules : function(rules) {
\r
8444 schema.addValidElements(rules);
\r
8447 setRules : function(rules) {
\r
8448 schema.setValidElements(rules);
\r
8453 (function(tinymce) {
\r
8454 tinymce.dom.ScriptLoader = function(settings) {
\r
8460 scriptLoadedCallbacks = {},
\r
8461 queueLoadedCallbacks = [],
\r
8465 function loadScript(url, callback) {
\r
8466 var t = this, dom = tinymce.DOM, elm, uri, loc, id;
\r
8468 // Execute callback when script is loaded
\r
8473 elm.onreadystatechange = elm.onload = elm = null;
\r
8478 function error() {
\r
8479 // Report the error so it's easier for people to spot loading errors
\r
8480 if (typeof(console) !== "undefined" && console.log)
\r
8481 console.log("Failed to load: " + url);
\r
8483 // We can't mark it as done if there is a load error since
\r
8484 // A) We don't want to produce 404 errors on the server and
\r
8485 // B) the onerror event won't fire on all browsers.
\r
8489 id = dom.uniqueId();
\r
8491 if (tinymce.isIE6) {
\r
8492 uri = new tinymce.util.URI(url);
\r
8495 // If script is from same domain and we
\r
8496 // use IE 6 then use XHR since it's more reliable
\r
8497 if (uri.host == loc.hostname && uri.port == loc.port && (uri.protocol + ':') == loc.protocol && uri.protocol.toLowerCase() != 'file') {
\r
8498 tinymce.util.XHR.send({
\r
8499 url : tinymce._addVer(uri.getURI()),
\r
8500 success : function(content) {
\r
8501 // Create new temp script element
\r
8502 var script = dom.create('script', {
\r
8503 type : 'text/javascript'
\r
8506 // Evaluate script in global scope
\r
8507 script.text = content;
\r
8508 document.getElementsByTagName('head')[0].appendChild(script);
\r
8509 dom.remove(script);
\r
8521 // Create new script element
\r
8522 elm = dom.create('script', {
\r
8524 type : 'text/javascript',
\r
8525 src : tinymce._addVer(url)
\r
8528 // Add onload listener for non IE browsers since IE9
\r
8529 // fires onload event before the script is parsed and executed
\r
8530 if (!tinymce.isIE)
\r
8531 elm.onload = done;
\r
8533 // Add onerror event will get fired on some browsers but not all of them
\r
8534 elm.onerror = error;
\r
8536 // Opera 9.60 doesn't seem to fire the onreadystate event at correctly
\r
8537 if (!tinymce.isOpera) {
\r
8538 elm.onreadystatechange = function() {
\r
8539 var state = elm.readyState;
\r
8541 // Loaded state is passed on IE 6 however there
\r
8542 // are known issues with this method but we can't use
\r
8543 // XHR in a cross domain loading
\r
8544 if (state == 'complete' || state == 'loaded')
\r
8549 // Most browsers support this feature so we report errors
\r
8550 // for those at least to help users track their missing plugins etc
\r
8551 // todo: Removed since it produced error if the document is unloaded by navigating away, re-add it as an option
\r
8552 /*elm.onerror = function() {
\r
8553 alert('Failed to load: ' + url);
\r
8556 // Add script to document
\r
8557 (document.getElementsByTagName('head')[0] || document.body).appendChild(elm);
\r
8560 this.isDone = function(url) {
\r
8561 return states[url] == LOADED;
\r
8564 this.markDone = function(url) {
\r
8565 states[url] = LOADED;
\r
8568 this.add = this.load = function(url, callback, scope) {
\r
8569 var item, state = states[url];
\r
8571 // Add url to load queue
\r
8572 if (state == undefined) {
\r
8574 states[url] = QUEUED;
\r
8578 // Store away callback for later execution
\r
8579 if (!scriptLoadedCallbacks[url])
\r
8580 scriptLoadedCallbacks[url] = [];
\r
8582 scriptLoadedCallbacks[url].push({
\r
8584 scope : scope || this
\r
8589 this.loadQueue = function(callback, scope) {
\r
8590 this.loadScripts(queue, callback, scope);
\r
8593 this.loadScripts = function(scripts, callback, scope) {
\r
8596 function execScriptLoadedCallbacks(url) {
\r
8597 // Execute URL callback functions
\r
8598 tinymce.each(scriptLoadedCallbacks[url], function(callback) {
\r
8599 callback.func.call(callback.scope);
\r
8602 scriptLoadedCallbacks[url] = undefined;
\r
8605 queueLoadedCallbacks.push({
\r
8607 scope : scope || this
\r
8610 loadScripts = function() {
\r
8611 var loadingScripts = tinymce.grep(scripts);
\r
8613 // Current scripts has been handled
\r
8614 scripts.length = 0;
\r
8616 // Load scripts that needs to be loaded
\r
8617 tinymce.each(loadingScripts, function(url) {
\r
8618 // Script is already loaded then execute script callbacks directly
\r
8619 if (states[url] == LOADED) {
\r
8620 execScriptLoadedCallbacks(url);
\r
8624 // Is script not loading then start loading it
\r
8625 if (states[url] != LOADING) {
\r
8626 states[url] = LOADING;
\r
8629 loadScript(url, function() {
\r
8630 states[url] = LOADED;
\r
8633 execScriptLoadedCallbacks(url);
\r
8635 // Load more scripts if they where added by the recently loaded script
\r
8641 // No scripts are currently loading then execute all pending queue loaded callbacks
\r
8643 tinymce.each(queueLoadedCallbacks, function(callback) {
\r
8644 callback.func.call(callback.scope);
\r
8647 queueLoadedCallbacks.length = 0;
\r
8655 // Global script loader
\r
8656 tinymce.ScriptLoader = new tinymce.dom.ScriptLoader();
\r
8659 tinymce.dom.TreeWalker = function(start_node, root_node) {
\r
8660 var node = start_node;
\r
8662 function findSibling(node, start_name, sibling_name, shallow) {
\r
8663 var sibling, parent;
\r
8666 // Walk into nodes if it has a start
\r
8667 if (!shallow && node[start_name])
\r
8668 return node[start_name];
\r
8670 // Return the sibling if it has one
\r
8671 if (node != root_node) {
\r
8672 sibling = node[sibling_name];
\r
8676 // Walk up the parents to look for siblings
\r
8677 for (parent = node.parentNode; parent && parent != root_node; parent = parent.parentNode) {
\r
8678 sibling = parent[sibling_name];
\r
8686 this.current = function() {
\r
8690 this.next = function(shallow) {
\r
8691 return (node = findSibling(node, 'firstChild', 'nextSibling', shallow));
\r
8694 this.prev = function(shallow) {
\r
8695 return (node = findSibling(node, 'lastChild', 'previousSibling', shallow));
\r
8699 (function(tinymce) {
\r
8700 tinymce.dom.RangeUtils = function(dom) {
\r
8701 var INVISIBLE_CHAR = '\uFEFF';
\r
8703 this.walk = function(rng, callback) {
\r
8704 var startContainer = rng.startContainer,
\r
8705 startOffset = rng.startOffset,
\r
8706 endContainer = rng.endContainer,
\r
8707 endOffset = rng.endOffset,
\r
8708 ancestor, startPoint,
\r
8709 endPoint, node, parent, siblings, nodes;
\r
8711 // Handle table cell selection the table plugin enables
\r
8712 // you to fake select table cells and perform formatting actions on them
\r
8713 nodes = dom.select('td.mceSelected,th.mceSelected');
\r
8714 if (nodes.length > 0) {
\r
8715 tinymce.each(nodes, function(node) {
\r
8722 function collectSiblings(node, name, end_node) {
\r
8723 var siblings = [];
\r
8725 for (; node && node != end_node; node = node[name])
\r
8726 siblings.push(node);
\r
8731 function findEndPoint(node, root) {
\r
8733 if (node.parentNode == root)
\r
8736 node = node.parentNode;
\r
8740 function walkBoundary(start_node, end_node, next) {
\r
8741 var siblingName = next ? 'nextSibling' : 'previousSibling';
\r
8743 for (node = start_node, parent = node.parentNode; node && node != end_node; node = parent) {
\r
8744 parent = node.parentNode;
\r
8745 siblings = collectSiblings(node == start_node ? node : node[siblingName], siblingName);
\r
8747 if (siblings.length) {
\r
8749 siblings.reverse();
\r
8751 callback(siblings);
\r
8756 // If index based start position then resolve it
\r
8757 if (startContainer.nodeType == 1 && startContainer.hasChildNodes())
\r
8758 startContainer = startContainer.childNodes[startOffset];
\r
8760 // If index based end position then resolve it
\r
8761 if (endContainer.nodeType == 1 && endContainer.hasChildNodes())
\r
8762 endContainer = endContainer.childNodes[Math.min(endOffset - 1, endContainer.childNodes.length - 1)];
\r
8764 // Find common ancestor and end points
\r
8765 ancestor = dom.findCommonAncestor(startContainer, endContainer);
\r
8768 if (startContainer == endContainer)
\r
8769 return callback([startContainer]);
\r
8771 // Process left side
\r
8772 for (node = startContainer; node; node = node.parentNode) {
\r
8773 if (node == endContainer)
\r
8774 return walkBoundary(startContainer, ancestor, true);
\r
8776 if (node == ancestor)
\r
8780 // Process right side
\r
8781 for (node = endContainer; node; node = node.parentNode) {
\r
8782 if (node == startContainer)
\r
8783 return walkBoundary(endContainer, ancestor);
\r
8785 if (node == ancestor)
\r
8789 // Find start/end point
\r
8790 startPoint = findEndPoint(startContainer, ancestor) || startContainer;
\r
8791 endPoint = findEndPoint(endContainer, ancestor) || endContainer;
\r
8794 walkBoundary(startContainer, startPoint, true);
\r
8796 // Walk the middle from start to end point
\r
8797 siblings = collectSiblings(
\r
8798 startPoint == startContainer ? startPoint : startPoint.nextSibling,
\r
8800 endPoint == endContainer ? endPoint.nextSibling : endPoint
\r
8803 if (siblings.length)
\r
8804 callback(siblings);
\r
8806 // Walk right leaf
\r
8807 walkBoundary(endContainer, endPoint);
\r
8810 /* this.split = function(rng) {
\r
8811 var startContainer = rng.startContainer,
\r
8812 startOffset = rng.startOffset,
\r
8813 endContainer = rng.endContainer,
\r
8814 endOffset = rng.endOffset;
\r
8816 function splitText(node, offset) {
\r
8817 if (offset == node.nodeValue.length)
\r
8818 node.appendData(INVISIBLE_CHAR);
\r
8820 node = node.splitText(offset);
\r
8822 if (node.nodeValue === INVISIBLE_CHAR)
\r
8823 node.nodeValue = '';
\r
8828 // Handle single text node
\r
8829 if (startContainer == endContainer) {
\r
8830 if (startContainer.nodeType == 3) {
\r
8831 if (startOffset != 0)
\r
8832 startContainer = endContainer = splitText(startContainer, startOffset);
\r
8834 if (endOffset - startOffset != startContainer.nodeValue.length)
\r
8835 splitText(startContainer, endOffset - startOffset);
\r
8838 // Split startContainer text node if needed
\r
8839 if (startContainer.nodeType == 3 && startOffset != 0) {
\r
8840 startContainer = splitText(startContainer, startOffset);
\r
8844 // Split endContainer text node if needed
\r
8845 if (endContainer.nodeType == 3 && endOffset != endContainer.nodeValue.length) {
\r
8846 endContainer = splitText(endContainer, endOffset).previousSibling;
\r
8847 endOffset = endContainer.nodeValue.length;
\r
8852 startContainer : startContainer,
\r
8853 startOffset : startOffset,
\r
8854 endContainer : endContainer,
\r
8855 endOffset : endOffset
\r
8861 tinymce.dom.RangeUtils.compareRanges = function(rng1, rng2) {
\r
8862 if (rng1 && rng2) {
\r
8863 // Compare native IE ranges
\r
8864 if (rng1.item || rng1.duplicate) {
\r
8865 // Both are control ranges and the selected element matches
\r
8866 if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0))
\r
8869 // Both are text ranges and the range matches
\r
8870 if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1))
\r
8873 // Compare w3c ranges
\r
8874 return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset;
\r
8882 (function(tinymce) {
\r
8883 var Event = tinymce.dom.Event, each = tinymce.each;
\r
8885 tinymce.create('tinymce.ui.KeyboardNavigation', {
\r
8886 KeyboardNavigation: function(settings, dom) {
\r
8887 var t = this, root = settings.root, items = settings.items,
\r
8888 enableUpDown = settings.enableUpDown, enableLeftRight = settings.enableLeftRight || !settings.enableUpDown,
\r
8889 excludeFromTabOrder = settings.excludeFromTabOrder,
\r
8890 itemFocussed, itemBlurred, rootKeydown, rootFocussed, focussedId;
\r
8892 dom = dom || tinymce.DOM;
\r
8894 itemFocussed = function(evt) {
\r
8895 focussedId = evt.target.id;
\r
8898 itemBlurred = function(evt) {
\r
8899 dom.setAttrib(evt.target.id, 'tabindex', '-1');
\r
8902 rootFocussed = function(evt) {
\r
8903 var item = dom.get(focussedId);
\r
8904 dom.setAttrib(item, 'tabindex', '0');
\r
8908 t.focus = function() {
\r
8909 dom.get(focussedId).focus();
\r
8912 t.destroy = function() {
\r
8913 each(items, function(item) {
\r
8914 dom.unbind(dom.get(item.id), 'focus', itemFocussed);
\r
8915 dom.unbind(dom.get(item.id), 'blur', itemBlurred);
\r
8918 dom.unbind(dom.get(root), 'focus', rootFocussed);
\r
8919 dom.unbind(dom.get(root), 'keydown', rootKeydown);
\r
8921 items = dom = root = t.focus = itemFocussed = itemBlurred = rootKeydown = rootFocussed = null;
\r
8922 t.destroy = function() {};
\r
8925 t.moveFocus = function(dir, evt) {
\r
8926 var idx = -1, controls = t.controls, newFocus;
\r
8931 each(items, function(item, index) {
\r
8932 if (item.id === focussedId) {
\r
8940 idx = items.length - 1;
\r
8941 } else if (idx >= items.length) {
\r
8945 newFocus = items[idx];
\r
8946 dom.setAttrib(focussedId, 'tabindex', '-1');
\r
8947 dom.setAttrib(newFocus.id, 'tabindex', '0');
\r
8948 dom.get(newFocus.id).focus();
\r
8950 if (settings.actOnFocus) {
\r
8951 settings.onAction(newFocus.id);
\r
8955 Event.cancel(evt);
\r
8958 rootKeydown = function(evt) {
\r
8959 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
8961 switch (evt.keyCode) {
\r
8963 if (enableLeftRight) t.moveFocus(-1);
\r
8966 case DOM_VK_RIGHT:
\r
8967 if (enableLeftRight) t.moveFocus(1);
\r
8971 if (enableUpDown) t.moveFocus(-1);
\r
8975 if (enableUpDown) t.moveFocus(1);
\r
8978 case DOM_VK_ESCAPE:
\r
8979 if (settings.onCancel) {
\r
8980 settings.onCancel();
\r
8981 Event.cancel(evt);
\r
8985 case DOM_VK_ENTER:
\r
8986 case DOM_VK_RETURN:
\r
8987 case DOM_VK_SPACE:
\r
8988 if (settings.onAction) {
\r
8989 settings.onAction(focussedId);
\r
8990 Event.cancel(evt);
\r
8996 // Set up state and listeners for each item.
\r
8997 each(items, function(item, idx) {
\r
9001 item.id = dom.uniqueId('_mce_item_');
\r
9004 if (excludeFromTabOrder) {
\r
9005 dom.bind(item.id, 'blur', itemBlurred);
\r
9008 tabindex = (idx === 0 ? '0' : '-1');
\r
9011 dom.setAttrib(item.id, 'tabindex', tabindex);
\r
9012 dom.bind(dom.get(item.id), 'focus', itemFocussed);
\r
9015 // Setup initial state for root element.
\r
9017 focussedId = items[0].id;
\r
9020 dom.setAttrib(root, 'tabindex', '-1');
\r
9022 // Setup listeners for root element.
\r
9023 dom.bind(dom.get(root), 'focus', rootFocussed);
\r
9024 dom.bind(dom.get(root), 'keydown', rootKeydown);
\r
9028 (function(tinymce) {
\r
9029 // Shorten class names
\r
9030 var DOM = tinymce.DOM, is = tinymce.is;
\r
9032 tinymce.create('tinymce.ui.Control', {
\r
9033 Control : function(id, s, editor) {
\r
9035 this.settings = s = s || {};
\r
9036 this.rendered = false;
\r
9037 this.onRender = new tinymce.util.Dispatcher(this);
\r
9038 this.classPrefix = '';
\r
9039 this.scope = s.scope || this;
\r
9040 this.disabled = 0;
\r
9042 this.editor = editor;
\r
9045 setAriaProperty : function(property, value) {
\r
9046 var element = DOM.get(this.id + '_aria') || DOM.get(this.id);
\r
9048 DOM.setAttrib(element, 'aria-' + property, !!value);
\r
9052 focus : function() {
\r
9053 DOM.get(this.id).focus();
\r
9056 setDisabled : function(s) {
\r
9057 if (s != this.disabled) {
\r
9058 this.setAriaProperty('disabled', s);
\r
9060 this.setState('Disabled', s);
\r
9061 this.setState('Enabled', !s);
\r
9062 this.disabled = s;
\r
9066 isDisabled : function() {
\r
9067 return this.disabled;
\r
9070 setActive : function(s) {
\r
9071 if (s != this.active) {
\r
9072 this.setState('Active', s);
\r
9074 this.setAriaProperty('pressed', s);
\r
9078 isActive : function() {
\r
9079 return this.active;
\r
9082 setState : function(c, s) {
\r
9083 var n = DOM.get(this.id);
\r
9085 c = this.classPrefix + c;
\r
9088 DOM.addClass(n, c);
\r
9090 DOM.removeClass(n, c);
\r
9093 isRendered : function() {
\r
9094 return this.rendered;
\r
9097 renderHTML : function() {
\r
9100 renderTo : function(n) {
\r
9101 DOM.setHTML(n, this.renderHTML());
\r
9104 postRender : function() {
\r
9107 // Set pending states
\r
9108 if (is(t.disabled)) {
\r
9114 if (is(t.active)) {
\r
9121 remove : function() {
\r
9122 DOM.remove(this.id);
\r
9126 destroy : function() {
\r
9127 tinymce.dom.Event.clear(this.id);
\r
9131 tinymce.create('tinymce.ui.Container:tinymce.ui.Control', {
\r
9132 Container : function(id, s, editor) {
\r
9133 this.parent(id, s, editor);
\r
9135 this.controls = [];
\r
9140 add : function(c) {
\r
9141 this.lookup[c.id] = c;
\r
9142 this.controls.push(c);
\r
9147 get : function(n) {
\r
9148 return this.lookup[n];
\r
9153 tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', {
\r
9154 Separator : function(id, s) {
\r
9155 this.parent(id, s);
\r
9156 this.classPrefix = 'mceSeparator';
\r
9157 this.setDisabled(true);
\r
9160 renderHTML : function() {
\r
9161 return tinymce.DOM.createHTML('span', {'class' : this.classPrefix, role : 'separator', 'aria-orientation' : 'vertical', tabindex : '-1'});
\r
9165 (function(tinymce) {
\r
9166 var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, walk = tinymce.walk;
\r
9168 tinymce.create('tinymce.ui.MenuItem:tinymce.ui.Control', {
\r
9169 MenuItem : function(id, s) {
\r
9170 this.parent(id, s);
\r
9171 this.classPrefix = 'mceMenuItem';
\r
9174 setSelected : function(s) {
\r
9175 this.setState('Selected', s);
\r
9176 this.setAriaProperty('checked', !!s);
\r
9177 this.selected = s;
\r
9180 isSelected : function() {
\r
9181 return this.selected;
\r
9184 postRender : function() {
\r
9189 // Set pending state
\r
9190 if (is(t.selected))
\r
9191 t.setSelected(t.selected);
\r
9196 (function(tinymce) {
\r
9197 var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, walk = tinymce.walk;
\r
9199 tinymce.create('tinymce.ui.Menu:tinymce.ui.MenuItem', {
\r
9200 Menu : function(id, s) {
\r
9205 t.collapsed = false;
\r
9207 t.onAddItem = new tinymce.util.Dispatcher(this);
\r
9210 expand : function(d) {
\r
9214 walk(t, function(o) {
\r
9220 t.collapsed = false;
\r
9223 collapse : function(d) {
\r
9227 walk(t, function(o) {
\r
9233 t.collapsed = true;
\r
9236 isCollapsed : function() {
\r
9237 return this.collapsed;
\r
9240 add : function(o) {
\r
9242 o = new tinymce.ui.MenuItem(o.id || DOM.uniqueId(), o);
\r
9244 this.onAddItem.dispatch(this, o);
\r
9246 return this.items[o.id] = o;
\r
9249 addSeparator : function() {
\r
9250 return this.add({separator : true});
\r
9253 addMenu : function(o) {
\r
9255 o = this.createMenu(o);
\r
9259 return this.add(o);
\r
9262 hasMenus : function() {
\r
9263 return this.menuCount !== 0;
\r
9266 remove : function(o) {
\r
9267 delete this.items[o.id];
\r
9270 removeAll : function() {
\r
9273 walk(t, function(o) {
\r
9285 createMenu : function(o) {
\r
9286 var m = new tinymce.ui.Menu(o.id || DOM.uniqueId(), o);
\r
9288 m.onAddItem.add(this.onAddItem.dispatch, this.onAddItem);
\r
9294 (function(tinymce) {
\r
9295 var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, Event = tinymce.dom.Event, Element = tinymce.dom.Element;
\r
9297 tinymce.create('tinymce.ui.DropMenu:tinymce.ui.Menu', {
\r
9298 DropMenu : function(id, s) {
\r
9300 s.container = s.container || DOM.doc.body;
\r
9301 s.offset_x = s.offset_x || 0;
\r
9302 s.offset_y = s.offset_y || 0;
\r
9303 s.vp_offset_x = s.vp_offset_x || 0;
\r
9304 s.vp_offset_y = s.vp_offset_y || 0;
\r
9306 if (is(s.icons) && !s.icons)
\r
9307 s['class'] += ' mceNoIcons';
\r
9309 this.parent(id, s);
\r
9310 this.onShowMenu = new tinymce.util.Dispatcher(this);
\r
9311 this.onHideMenu = new tinymce.util.Dispatcher(this);
\r
9312 this.classPrefix = 'mceMenu';
\r
9315 createMenu : function(s) {
\r
9316 var t = this, cs = t.settings, m;
\r
9318 s.container = s.container || cs.container;
\r
9320 s.constrain = s.constrain || cs.constrain;
\r
9321 s['class'] = s['class'] || cs['class'];
\r
9322 s.vp_offset_x = s.vp_offset_x || cs.vp_offset_x;
\r
9323 s.vp_offset_y = s.vp_offset_y || cs.vp_offset_y;
\r
9324 s.keyboard_focus = cs.keyboard_focus;
\r
9325 m = new tinymce.ui.DropMenu(s.id || DOM.uniqueId(), s);
\r
9327 m.onAddItem.add(t.onAddItem.dispatch, t.onAddItem);
\r
9332 focus : function() {
\r
9334 if (t.keyboardNav) {
\r
9335 t.keyboardNav.focus();
\r
9339 update : function() {
\r
9340 var t = this, s = t.settings, tb = DOM.get('menu_' + t.id + '_tbl'), co = DOM.get('menu_' + t.id + '_co'), tw, th;
\r
9342 tw = s.max_width ? Math.min(tb.clientWidth, s.max_width) : tb.clientWidth;
\r
9343 th = s.max_height ? Math.min(tb.clientHeight, s.max_height) : tb.clientHeight;
\r
9345 if (!DOM.boxModel)
\r
9346 t.element.setStyles({width : tw + 2, height : th + 2});
\r
9348 t.element.setStyles({width : tw, height : th});
\r
9351 DOM.setStyle(co, 'width', tw);
\r
9353 if (s.max_height) {
\r
9354 DOM.setStyle(co, 'height', th);
\r
9356 if (tb.clientHeight < s.max_height)
\r
9357 DOM.setStyle(co, 'overflow', 'hidden');
\r
9361 showMenu : function(x, y, px) {
\r
9362 var t = this, s = t.settings, co, vp = DOM.getViewPort(), w, h, mx, my, ot = 2, dm, tb, cp = t.classPrefix;
\r
9366 if (t.isMenuVisible)
\r
9369 if (!t.rendered) {
\r
9370 co = DOM.add(t.settings.container, t.renderNode());
\r
9372 each(t.items, function(o) {
\r
9376 t.element = new Element('menu_' + t.id, {blocker : 1, container : s.container});
\r
9378 co = DOM.get('menu_' + t.id);
\r
9380 // Move layer out of sight unless it's Opera since it scrolls to top of page due to an bug
\r
9381 if (!tinymce.isOpera)
\r
9382 DOM.setStyles(co, {left : -0xFFFF , top : -0xFFFF});
\r
9387 x += s.offset_x || 0;
\r
9388 y += s.offset_y || 0;
\r
9392 // Move inside viewport if not submenu
\r
9393 if (s.constrain) {
\r
9394 w = co.clientWidth - ot;
\r
9395 h = co.clientHeight - ot;
\r
9399 if ((x + s.vp_offset_x + w) > mx)
\r
9400 x = px ? px - w : Math.max(0, (mx - s.vp_offset_x) - w);
\r
9402 if ((y + s.vp_offset_y + h) > my)
\r
9403 y = Math.max(0, (my - s.vp_offset_y) - h);
\r
9406 DOM.setStyles(co, {left : x , top : y});
\r
9407 t.element.update();
\r
9409 t.isMenuVisible = 1;
\r
9410 t.mouseClickFunc = Event.add(co, 'click', function(e) {
\r
9415 if (e && (e = DOM.getParent(e, 'tr')) && !DOM.hasClass(e, cp + 'ItemSub')) {
\r
9416 m = t.items[e.id];
\r
9418 if (m.isDisabled())
\r
9427 dm = dm.settings.parent;
\r
9430 if (m.settings.onclick)
\r
9431 m.settings.onclick(e);
\r
9433 return Event.cancel(e); // Cancel to fix onbeforeunload problem
\r
9437 if (t.hasMenus()) {
\r
9438 t.mouseOverFunc = Event.add(co, 'mouseover', function(e) {
\r
9442 if (e && (e = DOM.getParent(e, 'tr'))) {
\r
9443 m = t.items[e.id];
\r
9446 t.lastMenu.collapse(1);
\r
9448 if (m.isDisabled())
\r
9451 if (e && DOM.hasClass(e, cp + 'ItemSub')) {
\r
9452 //p = DOM.getPos(s.container);
\r
9453 r = DOM.getRect(e);
\r
9454 m.showMenu((r.x + r.w - ot), r.y - ot, r.x);
\r
9456 DOM.addClass(DOM.get(m.id).firstChild, cp + 'ItemActive');
\r
9462 Event.add(co, 'keydown', t._keyHandler, t);
\r
9464 t.onShowMenu.dispatch(t);
\r
9466 if (s.keyboard_focus) {
\r
9467 t._setupKeyboardNav();
\r
9471 hideMenu : function(c) {
\r
9472 var t = this, co = DOM.get('menu_' + t.id), e;
\r
9474 if (!t.isMenuVisible)
\r
9477 if (t.keyboardNav) t.keyboardNav.destroy();
\r
9478 Event.remove(co, 'mouseover', t.mouseOverFunc);
\r
9479 Event.remove(co, 'click', t.mouseClickFunc);
\r
9480 Event.remove(co, 'keydown', t._keyHandler);
\r
9482 t.isMenuVisible = 0;
\r
9490 if (e = DOM.get(t.id))
\r
9491 DOM.removeClass(e.firstChild, t.classPrefix + 'ItemActive');
\r
9493 t.onHideMenu.dispatch(t);
\r
9496 add : function(o) {
\r
9501 if (t.isRendered && (co = DOM.get('menu_' + t.id)))
\r
9502 t._add(DOM.select('tbody', co)[0], o);
\r
9507 collapse : function(d) {
\r
9512 remove : function(o) {
\r
9516 return this.parent(o);
\r
9519 destroy : function() {
\r
9520 var t = this, co = DOM.get('menu_' + t.id);
\r
9522 if (t.keyboardNav) t.keyboardNav.destroy();
\r
9523 Event.remove(co, 'mouseover', t.mouseOverFunc);
\r
9524 Event.remove(DOM.select('a', co), 'focus', t.mouseOverFunc);
\r
9525 Event.remove(co, 'click', t.mouseClickFunc);
\r
9526 Event.remove(co, 'keydown', t._keyHandler);
\r
9529 t.element.remove();
\r
9534 renderNode : function() {
\r
9535 var t = this, s = t.settings, n, tb, co, w;
\r
9537 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
9538 if (t.settings.parent) {
\r
9539 DOM.setAttrib(w, 'aria-parent', 'menu_' + t.settings.parent.id);
\r
9541 co = DOM.add(w, 'div', {role: 'presentation', id : 'menu_' + t.id + '_co', 'class' : t.classPrefix + (s['class'] ? ' ' + s['class'] : '')});
\r
9542 t.element = new Element('menu_' + t.id, {blocker : 1, container : s.container});
\r
9545 DOM.add(co, 'span', {'class' : t.classPrefix + 'Line'});
\r
9547 // n = DOM.add(co, 'div', {id : 'menu_' + t.id + '_co', 'class' : 'mceMenuContainer'});
\r
9548 n = DOM.add(co, 'table', {role: 'presentation', id : 'menu_' + t.id + '_tbl', border : 0, cellPadding : 0, cellSpacing : 0});
\r
9549 tb = DOM.add(n, 'tbody');
\r
9551 each(t.items, function(o) {
\r
9555 t.rendered = true;
\r
9560 // Internal functions
\r
9561 _setupKeyboardNav : function(){
\r
9562 var contextMenu, menuItems, t=this;
\r
9563 contextMenu = DOM.select('#menu_' + t.id)[0];
\r
9564 menuItems = DOM.select('a[role=option]', 'menu_' + t.id);
\r
9565 menuItems.splice(0,0,contextMenu);
\r
9566 t.keyboardNav = new tinymce.ui.KeyboardNavigation({
\r
9567 root: 'menu_' + t.id,
\r
9569 onCancel: function() {
\r
9572 enableUpDown: true
\r
9574 contextMenu.focus();
\r
9577 _keyHandler : function(evt) {
\r
9579 switch (evt.keyCode) {
\r
9581 if (t.settings.parent) {
\r
9583 t.settings.parent.focus();
\r
9584 Event.cancel(evt);
\r
9588 if (t.mouseOverFunc)
\r
9589 t.mouseOverFunc(evt);
\r
9594 _add : function(tb, o) {
\r
9595 var n, s = o.settings, a, ro, it, cp = this.classPrefix, ic;
\r
9597 if (s.separator) {
\r
9598 ro = DOM.add(tb, 'tr', {id : o.id, 'class' : cp + 'ItemSeparator'});
\r
9599 DOM.add(ro, 'td', {'class' : cp + 'ItemSeparator'});
\r
9601 if (n = ro.previousSibling)
\r
9602 DOM.addClass(n, 'mceLast');
\r
9607 n = ro = DOM.add(tb, 'tr', {id : o.id, 'class' : cp + 'Item ' + cp + 'ItemEnabled'});
\r
9608 n = it = DOM.add(n, s.titleItem ? 'th' : 'td');
\r
9609 n = a = DOM.add(n, 'a', {id: o.id + '_aria', role: s.titleItem ? 'presentation' : 'option', href : 'javascript:;', onclick : "return false;", onmousedown : 'return false;'});
\r
9612 DOM.setAttrib(a, 'aria-haspopup', 'true');
\r
9613 DOM.setAttrib(a, 'aria-owns', 'menu_' + o.id);
\r
9616 DOM.addClass(it, s['class']);
\r
9617 // n = DOM.add(n, 'span', {'class' : 'item'});
\r
9619 ic = DOM.add(n, 'span', {'class' : 'mceIcon' + (s.icon ? ' mce_' + s.icon : '')});
\r
9622 DOM.add(ic, 'img', {src : s.icon_src});
\r
9624 n = DOM.add(n, s.element || 'span', {'class' : 'mceText', title : o.settings.title}, o.settings.title);
\r
9626 if (o.settings.style)
\r
9627 DOM.setAttrib(n, 'style', o.settings.style);
\r
9629 if (tb.childNodes.length == 1)
\r
9630 DOM.addClass(ro, 'mceFirst');
\r
9632 if ((n = ro.previousSibling) && DOM.hasClass(n, cp + 'ItemSeparator'))
\r
9633 DOM.addClass(ro, 'mceFirst');
\r
9636 DOM.addClass(ro, cp + 'ItemSub');
\r
9638 if (n = ro.previousSibling)
\r
9639 DOM.removeClass(n, 'mceLast');
\r
9641 DOM.addClass(ro, 'mceLast');
\r
9645 (function(tinymce) {
\r
9646 var DOM = tinymce.DOM;
\r
9648 tinymce.create('tinymce.ui.Button:tinymce.ui.Control', {
\r
9649 Button : function(id, s, ed) {
\r
9650 this.parent(id, s, ed);
\r
9651 this.classPrefix = 'mceButton';
\r
9654 renderHTML : function() {
\r
9655 var cp = this.classPrefix, s = this.settings, h, l;
\r
9657 l = DOM.encode(s.label || '');
\r
9658 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
9659 if (s.image && !(this.editor &&this.editor.forcedHighContrastMode) )
\r
9660 h += '<img class="mceIcon" src="' + s.image + '" alt="' + DOM.encode(s.title) + '" />' + l;
\r
9662 h += '<span class="mceIcon ' + s['class'] + '"></span>' + (l ? '<span class="' + cp + 'Label">' + l + '</span>' : '');
\r
9664 h += '<span class="mceVoiceLabel mceIconOnly" style="display: none;" id="' + this.id + '_voice">' + s.title + '</span>';
\r
9669 postRender : function() {
\r
9670 var t = this, s = t.settings;
\r
9672 tinymce.dom.Event.add(t.id, 'click', function(e) {
\r
9673 if (!t.isDisabled())
\r
9674 return s.onclick.call(s.scope, e);
\r
9680 (function(tinymce) {
\r
9681 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher;
\r
9683 tinymce.create('tinymce.ui.ListBox:tinymce.ui.Control', {
\r
9684 ListBox : function(id, s, ed) {
\r
9687 t.parent(id, s, ed);
\r
9691 t.onChange = new Dispatcher(t);
\r
9693 t.onPostRender = new Dispatcher(t);
\r
9695 t.onAdd = new Dispatcher(t);
\r
9697 t.onRenderMenu = new tinymce.util.Dispatcher(this);
\r
9699 t.classPrefix = 'mceListBox';
\r
9702 select : function(va) {
\r
9703 var t = this, fv, f;
\r
9705 if (va == undefined)
\r
9706 return t.selectByIndex(-1);
\r
9708 // Is string or number make function selector
\r
9709 if (va && va.call)
\r
9717 // Do we need to do something?
\r
9718 if (va != t.selectedValue) {
\r
9720 each(t.items, function(o, i) {
\r
9723 t.selectByIndex(i);
\r
9729 t.selectByIndex(-1);
\r
9733 selectByIndex : function(idx) {
\r
9734 var t = this, e, o;
\r
9736 if (idx != t.selectedIndex) {
\r
9737 e = DOM.get(t.id + '_text');
\r
9741 t.selectedValue = o.value;
\r
9742 t.selectedIndex = idx;
\r
9743 DOM.setHTML(e, DOM.encode(o.title));
\r
9744 DOM.removeClass(e, 'mceTitle');
\r
9745 DOM.setAttrib(t.id, 'aria-valuenow', o.title);
\r
9747 DOM.setHTML(e, DOM.encode(t.settings.title));
\r
9748 DOM.addClass(e, 'mceTitle');
\r
9749 t.selectedValue = t.selectedIndex = null;
\r
9750 DOM.setAttrib(t.id, 'aria-valuenow', t.settings.title);
\r
9756 add : function(n, v, o) {
\r
9760 o = tinymce.extend(o, {
\r
9766 t.onAdd.dispatch(t, o);
\r
9769 getLength : function() {
\r
9770 return this.items.length;
\r
9773 renderHTML : function() {
\r
9774 var h = '', t = this, s = t.settings, cp = t.classPrefix;
\r
9776 h = '<span role="button" aria-haspopup="true" aria-labelledby="' + t.id +'_text" aria-describedby="' + t.id + '_voiceDesc"><table role="presentation" tabindex="0" id="' + t.id + '" cellpadding="0" cellspacing="0" class="' + cp + ' ' + cp + 'Enabled' + (s['class'] ? (' ' + s['class']) : '') + '"><tbody><tr>';
\r
9777 h += '<td>' + DOM.createHTML('span', {id: t.id + '_voiceDesc', 'class': 'voiceLabel', style:'display:none;'}, t.settings.title);
\r
9778 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
9779 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
9780 h += '</tr></tbody></table></span>';
\r
9785 showMenu : function() {
\r
9786 var t = this, p2, e = DOM.get(this.id), m;
\r
9788 if (t.isDisabled() || t.items.length == 0)
\r
9791 if (t.menu && t.menu.isMenuVisible)
\r
9792 return t.hideMenu();
\r
9794 if (!t.isMenuRendered) {
\r
9796 t.isMenuRendered = true;
\r
9799 p2 = DOM.getPos(e);
\r
9802 m.settings.offset_x = p2.x;
\r
9803 m.settings.offset_y = p2.y;
\r
9804 m.settings.keyboard_focus = !tinymce.isOpera; // Opera is buggy when it comes to auto focus
\r
9808 m.items[t.oldID].setSelected(0);
\r
9810 each(t.items, function(o) {
\r
9811 if (o.value === t.selectedValue) {
\r
9812 m.items[o.id].setSelected(1);
\r
9817 m.showMenu(0, e.clientHeight);
\r
9819 Event.add(DOM.doc, 'mousedown', t.hideMenu, t);
\r
9820 DOM.addClass(t.id, t.classPrefix + 'Selected');
\r
9822 //DOM.get(t.id + '_text').focus();
\r
9825 hideMenu : function(e) {
\r
9828 if (t.menu && t.menu.isMenuVisible) {
\r
9829 DOM.removeClass(t.id, t.classPrefix + 'Selected');
\r
9831 // Prevent double toogles by canceling the mouse click event to the button
\r
9832 if (e && e.type == "mousedown" && (e.target.id == t.id + '_text' || e.target.id == t.id + '_open'))
\r
9835 if (!e || !DOM.getParent(e.target, '.mceMenu')) {
\r
9836 DOM.removeClass(t.id, t.classPrefix + 'Selected');
\r
9837 Event.remove(DOM.doc, 'mousedown', t.hideMenu, t);
\r
9838 t.menu.hideMenu();
\r
9843 renderMenu : function() {
\r
9846 m = t.settings.control_manager.createDropMenu(t.id + '_menu', {
\r
9848 'class' : t.classPrefix + 'Menu mceNoIcons',
\r
9853 m.onHideMenu.add(function() {
\r
9859 title : t.settings.title,
\r
9860 'class' : 'mceMenuItemTitle',
\r
9861 onclick : function() {
\r
9862 if (t.settings.onselect('') !== false)
\r
9863 t.select(''); // Must be runned after
\r
9867 each(t.items, function(o) {
\r
9868 // No value then treat it as a title
\r
9869 if (o.value === undefined) {
\r
9872 'class' : 'mceMenuItemTitle',
\r
9873 onclick : function() {
\r
9874 if (t.settings.onselect('') !== false)
\r
9875 t.select(''); // Must be runned after
\r
9879 o.id = DOM.uniqueId();
\r
9880 o.onclick = function() {
\r
9881 if (t.settings.onselect(o.value) !== false)
\r
9882 t.select(o.value); // Must be runned after
\r
9889 t.onRenderMenu.dispatch(t, m);
\r
9893 postRender : function() {
\r
9894 var t = this, cp = t.classPrefix;
\r
9896 Event.add(t.id, 'click', t.showMenu, t);
\r
9897 Event.add(t.id, 'keydown', function(evt) {
\r
9898 if (evt.keyCode == 32) { // Space
\r
9900 Event.cancel(evt);
\r
9903 Event.add(t.id, 'focus', function() {
\r
9904 if (!t._focused) {
\r
9905 t.keyDownHandler = Event.add(t.id, 'keydown', function(e) {
\r
9906 if (e.keyCode == 40) {
\r
9911 t.keyPressHandler = Event.add(t.id, 'keypress', function(e) {
\r
9913 if (e.keyCode == 13) {
\r
9914 // Fake select on enter
\r
9915 v = t.selectedValue;
\r
9916 t.selectedValue = null; // Needs to be null to fake change
\r
9918 t.settings.onselect(v);
\r
9925 Event.add(t.id, 'blur', function() {
\r
9926 Event.remove(t.id, 'keydown', t.keyDownHandler);
\r
9927 Event.remove(t.id, 'keypress', t.keyPressHandler);
\r
9931 // Old IE doesn't have hover on all elements
\r
9932 if (tinymce.isIE6 || !DOM.boxModel) {
\r
9933 Event.add(t.id, 'mouseover', function() {
\r
9934 if (!DOM.hasClass(t.id, cp + 'Disabled'))
\r
9935 DOM.addClass(t.id, cp + 'Hover');
\r
9938 Event.add(t.id, 'mouseout', function() {
\r
9939 if (!DOM.hasClass(t.id, cp + 'Disabled'))
\r
9940 DOM.removeClass(t.id, cp + 'Hover');
\r
9944 t.onPostRender.dispatch(t, DOM.get(t.id));
\r
9947 destroy : function() {
\r
9950 Event.clear(this.id + '_text');
\r
9951 Event.clear(this.id + '_open');
\r
9955 (function(tinymce) {
\r
9956 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher;
\r
9958 tinymce.create('tinymce.ui.NativeListBox:tinymce.ui.ListBox', {
\r
9959 NativeListBox : function(id, s) {
\r
9960 this.parent(id, s);
\r
9961 this.classPrefix = 'mceNativeListBox';
\r
9964 setDisabled : function(s) {
\r
9965 DOM.get(this.id).disabled = s;
\r
9966 this.setAriaProperty('disabled', s);
\r
9969 isDisabled : function() {
\r
9970 return DOM.get(this.id).disabled;
\r
9973 select : function(va) {
\r
9974 var t = this, fv, f;
\r
9976 if (va == undefined)
\r
9977 return t.selectByIndex(-1);
\r
9979 // Is string or number make function selector
\r
9980 if (va && va.call)
\r
9988 // Do we need to do something?
\r
9989 if (va != t.selectedValue) {
\r
9991 each(t.items, function(o, i) {
\r
9994 t.selectByIndex(i);
\r
10000 t.selectByIndex(-1);
\r
10004 selectByIndex : function(idx) {
\r
10005 DOM.get(this.id).selectedIndex = idx + 1;
\r
10006 this.selectedValue = this.items[idx] ? this.items[idx].value : null;
\r
10009 add : function(n, v, a) {
\r
10015 if (t.isRendered())
\r
10016 DOM.add(DOM.get(this.id), 'option', a, n);
\r
10025 t.onAdd.dispatch(t, o);
\r
10028 getLength : function() {
\r
10029 return this.items.length;
\r
10032 renderHTML : function() {
\r
10035 h = DOM.createHTML('option', {value : ''}, '-- ' + t.settings.title + ' --');
\r
10037 each(t.items, function(it) {
\r
10038 h += DOM.createHTML('option', {value : it.value}, it.title);
\r
10041 h = DOM.createHTML('select', {id : t.id, 'class' : 'mceNativeListBox', 'aria-labelledby': t.id + '_aria'}, h);
\r
10042 h += DOM.createHTML('span', {id : t.id + '_aria', 'style': 'display: none'}, t.settings.title);
\r
10046 postRender : function() {
\r
10047 var t = this, ch, changeListenerAdded = true;
\r
10049 t.rendered = true;
\r
10051 function onChange(e) {
\r
10052 var v = t.items[e.target.selectedIndex - 1];
\r
10054 if (v && (v = v.value)) {
\r
10055 t.onChange.dispatch(t, v);
\r
10057 if (t.settings.onselect)
\r
10058 t.settings.onselect(v);
\r
10062 Event.add(t.id, 'change', onChange);
\r
10064 // Accessibility keyhandler
\r
10065 Event.add(t.id, 'keydown', function(e) {
\r
10068 Event.remove(t.id, 'change', ch);
\r
10069 changeListenerAdded = false;
\r
10071 bf = Event.add(t.id, 'blur', function() {
\r
10072 if (changeListenerAdded) return;
\r
10073 changeListenerAdded = true;
\r
10074 Event.add(t.id, 'change', onChange);
\r
10075 Event.remove(t.id, 'blur', bf);
\r
10078 //prevent default left and right keys on chrome - so that the keyboard navigation is used.
\r
10079 if (tinymce.isWebKit && (e.keyCode==37 ||e.keyCode==39)) {
\r
10080 return Event.prevent(e);
\r
10083 if (e.keyCode == 13 || e.keyCode == 32) {
\r
10085 return Event.cancel(e);
\r
10089 t.onPostRender.dispatch(t, DOM.get(t.id));
\r
10094 (function(tinymce) {
\r
10095 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each;
\r
10097 tinymce.create('tinymce.ui.MenuButton:tinymce.ui.Button', {
\r
10098 MenuButton : function(id, s, ed) {
\r
10099 this.parent(id, s, ed);
\r
10101 this.onRenderMenu = new tinymce.util.Dispatcher(this);
\r
10103 s.menu_container = s.menu_container || DOM.doc.body;
\r
10106 showMenu : function() {
\r
10107 var t = this, p1, p2, e = DOM.get(t.id), m;
\r
10109 if (t.isDisabled())
\r
10112 if (!t.isMenuRendered) {
\r
10114 t.isMenuRendered = true;
\r
10117 if (t.isMenuVisible)
\r
10118 return t.hideMenu();
\r
10120 p1 = DOM.getPos(t.settings.menu_container);
\r
10121 p2 = DOM.getPos(e);
\r
10124 m.settings.offset_x = p2.x;
\r
10125 m.settings.offset_y = p2.y;
\r
10126 m.settings.vp_offset_x = p2.x;
\r
10127 m.settings.vp_offset_y = p2.y;
\r
10128 m.settings.keyboard_focus = t._focused;
\r
10129 m.showMenu(0, e.clientHeight);
\r
10131 Event.add(DOM.doc, 'mousedown', t.hideMenu, t);
\r
10132 t.setState('Selected', 1);
\r
10134 t.isMenuVisible = 1;
\r
10137 renderMenu : function() {
\r
10140 m = t.settings.control_manager.createDropMenu(t.id + '_menu', {
\r
10142 'class' : this.classPrefix + 'Menu',
\r
10143 icons : t.settings.icons
\r
10146 m.onHideMenu.add(function() {
\r
10151 t.onRenderMenu.dispatch(t, m);
\r
10155 hideMenu : function(e) {
\r
10158 // Prevent double toogles by canceling the mouse click event to the button
\r
10159 if (e && e.type == "mousedown" && DOM.getParent(e.target, function(e) {return e.id === t.id || e.id === t.id + '_open';}))
\r
10162 if (!e || !DOM.getParent(e.target, '.mceMenu')) {
\r
10163 t.setState('Selected', 0);
\r
10164 Event.remove(DOM.doc, 'mousedown', t.hideMenu, t);
\r
10166 t.menu.hideMenu();
\r
10169 t.isMenuVisible = 0;
\r
10172 postRender : function() {
\r
10173 var t = this, s = t.settings;
\r
10175 Event.add(t.id, 'click', function() {
\r
10176 if (!t.isDisabled()) {
\r
10178 s.onclick(t.value);
\r
10187 (function(tinymce) {
\r
10188 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each;
\r
10190 tinymce.create('tinymce.ui.SplitButton:tinymce.ui.MenuButton', {
\r
10191 SplitButton : function(id, s, ed) {
\r
10192 this.parent(id, s, ed);
\r
10193 this.classPrefix = 'mceSplitButton';
\r
10196 renderHTML : function() {
\r
10197 var h, t = this, s = t.settings, h1;
\r
10199 h = '<tbody><tr>';
\r
10202 h1 = DOM.createHTML('img ', {src : s.image, role: 'presentation', 'class' : 'mceAction ' + s['class']});
\r
10204 h1 = DOM.createHTML('span', {'class' : 'mceAction ' + s['class']}, '');
\r
10206 h1 += DOM.createHTML('span', {'class': 'mceVoiceLabel mceIconOnly', id: t.id + '_voice', style: 'display:none;'}, s.title);
\r
10207 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
10209 h1 = DOM.createHTML('span', {'class' : 'mceOpen ' + s['class']}, '<span style="display:none;" class="mceIconOnly" aria-hidden="true">\u25BC</span>');
\r
10210 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
10212 h += '</tr></tbody>';
\r
10213 h = DOM.createHTML('table', {id : t.id, role: 'presentation', tabindex: '0', 'class' : 'mceSplitButton mceSplitButtonEnabled ' + s['class'], cellpadding : '0', cellspacing : '0', title : s.title}, h);
\r
10214 return DOM.createHTML('span', {role: 'button', 'aria-labelledby': t.id + '_voice', 'aria-haspopup': 'true'}, h);
\r
10217 postRender : function() {
\r
10218 var t = this, s = t.settings, activate;
\r
10221 activate = function(evt) {
\r
10222 if (!t.isDisabled()) {
\r
10223 s.onclick(t.value);
\r
10224 Event.cancel(evt);
\r
10227 Event.add(t.id + '_action', 'click', activate);
\r
10228 Event.add(t.id, ['click', 'keydown'], function(evt) {
\r
10229 var DOM_VK_SPACE = 32, DOM_VK_ENTER = 14, DOM_VK_RETURN = 13, DOM_VK_UP = 38, DOM_VK_DOWN = 40;
\r
10230 if ((evt.keyCode === 32 || evt.keyCode === 13 || evt.keyCode === 14) && !evt.altKey && !evt.ctrlKey && !evt.metaKey) {
\r
10232 Event.cancel(evt);
\r
10233 } else if (evt.type === 'click' || evt.keyCode === DOM_VK_DOWN) {
\r
10235 Event.cancel(evt);
\r
10240 Event.add(t.id + '_open', 'click', function (evt) {
\r
10242 Event.cancel(evt);
\r
10244 Event.add([t.id, t.id + '_open'], 'focus', function() {t._focused = 1;});
\r
10245 Event.add([t.id, t.id + '_open'], 'blur', function() {t._focused = 0;});
\r
10247 // Old IE doesn't have hover on all elements
\r
10248 if (tinymce.isIE6 || !DOM.boxModel) {
\r
10249 Event.add(t.id, 'mouseover', function() {
\r
10250 if (!DOM.hasClass(t.id, 'mceSplitButtonDisabled'))
\r
10251 DOM.addClass(t.id, 'mceSplitButtonHover');
\r
10254 Event.add(t.id, 'mouseout', function() {
\r
10255 if (!DOM.hasClass(t.id, 'mceSplitButtonDisabled'))
\r
10256 DOM.removeClass(t.id, 'mceSplitButtonHover');
\r
10261 destroy : function() {
\r
10264 Event.clear(this.id + '_action');
\r
10265 Event.clear(this.id + '_open');
\r
10266 Event.clear(this.id);
\r
10271 (function(tinymce) {
\r
10272 var DOM = tinymce.DOM, Event = tinymce.dom.Event, is = tinymce.is, each = tinymce.each;
\r
10274 tinymce.create('tinymce.ui.ColorSplitButton:tinymce.ui.SplitButton', {
\r
10275 ColorSplitButton : function(id, s, ed) {
\r
10278 t.parent(id, s, ed);
\r
10280 t.settings = s = tinymce.extend({
\r
10281 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
10283 default_color : '#888888'
\r
10286 t.onShowMenu = new tinymce.util.Dispatcher(t);
\r
10288 t.onHideMenu = new tinymce.util.Dispatcher(t);
\r
10290 t.value = s.default_color;
\r
10293 showMenu : function() {
\r
10294 var t = this, r, p, e, p2;
\r
10296 if (t.isDisabled())
\r
10299 if (!t.isMenuRendered) {
\r
10301 t.isMenuRendered = true;
\r
10304 if (t.isMenuVisible)
\r
10305 return t.hideMenu();
\r
10307 e = DOM.get(t.id);
\r
10308 DOM.show(t.id + '_menu');
\r
10309 DOM.addClass(e, 'mceSplitButtonSelected');
\r
10310 p2 = DOM.getPos(e);
\r
10311 DOM.setStyles(t.id + '_menu', {
\r
10313 top : p2.y + e.clientHeight,
\r
10318 Event.add(DOM.doc, 'mousedown', t.hideMenu, t);
\r
10319 t.onShowMenu.dispatch(t);
\r
10321 if (t._focused) {
\r
10322 t._keyHandler = Event.add(t.id + '_menu', 'keydown', function(e) {
\r
10323 if (e.keyCode == 27)
\r
10327 DOM.select('a', t.id + '_menu')[0].focus(); // Select first link
\r
10330 t.isMenuVisible = 1;
\r
10333 hideMenu : function(e) {
\r
10336 if (t.isMenuVisible) {
\r
10337 // Prevent double toogles by canceling the mouse click event to the button
\r
10338 if (e && e.type == "mousedown" && DOM.getParent(e.target, function(e) {return e.id === t.id + '_open';}))
\r
10341 if (!e || !DOM.getParent(e.target, '.mceSplitButtonMenu')) {
\r
10342 DOM.removeClass(t.id, 'mceSplitButtonSelected');
\r
10343 Event.remove(DOM.doc, 'mousedown', t.hideMenu, t);
\r
10344 Event.remove(t.id + '_menu', 'keydown', t._keyHandler);
\r
10345 DOM.hide(t.id + '_menu');
\r
10348 t.isMenuVisible = 0;
\r
10349 t.onHideMenu.dispatch();
\r
10353 renderMenu : function() {
\r
10354 var t = this, m, i = 0, s = t.settings, n, tb, tr, w, context;
\r
10356 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
10357 m = DOM.add(w, 'div', {'class' : s['class'] + ' mceSplitButtonMenu'});
\r
10358 DOM.add(m, 'span', {'class' : 'mceMenuLine'});
\r
10360 n = DOM.add(m, 'table', {role: 'presentation', 'class' : 'mceColorSplitMenu'});
\r
10361 tb = DOM.add(n, 'tbody');
\r
10363 // Generate color grid
\r
10365 each(is(s.colors, 'array') ? s.colors : s.colors.split(','), function(c) {
\r
10366 c = c.replace(/^#/, '');
\r
10369 tr = DOM.add(tb, 'tr');
\r
10370 i = s.grid_width - 1;
\r
10373 n = DOM.add(tr, 'td');
\r
10374 n = DOM.add(n, 'a', {
\r
10376 href : 'javascript:;',
\r
10378 backgroundColor : '#' + c
\r
10380 'title': t.editor.getLang('colors.' + c, c),
\r
10381 'data-mce-color' : '#' + c
\r
10384 if (t.editor.forcedHighContrastMode) {
\r
10385 n = DOM.add(n, 'canvas', { width: 16, height: 16, 'aria-hidden': 'true' });
\r
10386 if (n.getContext && (context = n.getContext("2d"))) {
\r
10387 context.fillStyle = '#' + c;
\r
10388 context.fillRect(0, 0, 16, 16);
\r
10390 // No point leaving a canvas element around if it's not supported for drawing on anyway.
\r
10396 if (s.more_colors_func) {
\r
10397 n = DOM.add(tb, 'tr');
\r
10398 n = DOM.add(n, 'td', {colspan : s.grid_width, 'class' : 'mceMoreColors'});
\r
10399 n = DOM.add(n, 'a', {role: 'option', id : t.id + '_more', href : 'javascript:;', onclick : 'return false;', 'class' : 'mceMoreColors'}, s.more_colors_title);
\r
10401 Event.add(n, 'click', function(e) {
\r
10402 s.more_colors_func.call(s.more_colors_scope || this);
\r
10403 return Event.cancel(e); // Cancel to fix onbeforeunload problem
\r
10407 DOM.addClass(m, 'mceColorSplitMenu');
\r
10409 new tinymce.ui.KeyboardNavigation({
\r
10410 root: t.id + '_menu',
\r
10411 items: DOM.select('a', t.id + '_menu'),
\r
10412 onCancel: function() {
\r
10418 // Prevent IE from scrolling and hindering click to occur #4019
\r
10419 Event.add(t.id + '_menu', 'mousedown', function(e) {return Event.cancel(e);});
\r
10421 Event.add(t.id + '_menu', 'click', function(e) {
\r
10424 e = DOM.getParent(e.target, 'a', tb);
\r
10426 if (e && e.nodeName.toLowerCase() == 'a' && (c = e.getAttribute('data-mce-color')))
\r
10429 return Event.cancel(e); // Prevent IE auto save warning
\r
10435 setColor : function(c) {
\r
10436 this.displayColor(c);
\r
10438 this.settings.onselect(c);
\r
10441 displayColor : function(c) {
\r
10444 DOM.setStyle(t.id + '_preview', 'backgroundColor', c);
\r
10449 postRender : function() {
\r
10450 var t = this, id = t.id;
\r
10453 DOM.add(id + '_action', 'div', {id : id + '_preview', 'class' : 'mceColorPreview'});
\r
10454 DOM.setStyle(t.id + '_preview', 'backgroundColor', t.value);
\r
10457 destroy : function() {
\r
10460 Event.clear(this.id + '_menu');
\r
10461 Event.clear(this.id + '_more');
\r
10462 DOM.remove(this.id + '_menu');
\r
10467 (function(tinymce) {
\r
10468 // Shorten class names
\r
10469 var dom = tinymce.DOM, each = tinymce.each, Event = tinymce.dom.Event;
\r
10470 tinymce.create('tinymce.ui.ToolbarGroup:tinymce.ui.Container', {
\r
10471 renderHTML : function() {
\r
10472 var t = this, h = [], controls = t.controls, each = tinymce.each, settings = t.settings;
\r
10474 h.push('<div id="' + t.id + '" role="group" aria-labelledby="' + t.id + '_voice">');
\r
10475 //TODO: ACC test this out - adding a role = application for getting the landmarks working well.
\r
10476 h.push("<span role='application'>");
\r
10477 h.push('<span id="' + t.id + '_voice" class="mceVoiceLabel" style="display:none;">' + dom.encode(settings.name) + '</span>');
\r
10478 each(controls, function(toolbar) {
\r
10479 h.push(toolbar.renderHTML());
\r
10481 h.push("</span>");
\r
10482 h.push('</div>');
\r
10484 return h.join('');
\r
10487 focus : function() {
\r
10488 this.keyNav.focus();
\r
10491 postRender : function() {
\r
10492 var t = this, items = [];
\r
10494 each(t.controls, function(toolbar) {
\r
10495 each (toolbar.controls, function(control) {
\r
10496 if (control.id) {
\r
10497 items.push(control);
\r
10502 t.keyNav = new tinymce.ui.KeyboardNavigation({
\r
10505 onCancel: function() {
\r
10506 t.editor.focus();
\r
10508 excludeFromTabOrder: !t.settings.tab_focus_toolbar
\r
10512 destroy : function() {
\r
10516 self.keyNav.destroy();
\r
10517 Event.clear(self.id);
\r
10522 (function(tinymce) {
\r
10523 // Shorten class names
\r
10524 var dom = tinymce.DOM, each = tinymce.each;
\r
10525 tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', {
\r
10526 renderHTML : function() {
\r
10527 var t = this, h = '', c, co, s = t.settings, i, pr, nx, cl;
\r
10530 for (i=0; i<cl.length; i++) {
\r
10531 // Get current control, prev control, next control and if the control is a list box or not
\r
10536 // Add toolbar start
\r
10538 c = 'mceToolbarStart';
\r
10541 c += ' mceToolbarStartButton';
\r
10542 else if (co.SplitButton)
\r
10543 c += ' mceToolbarStartSplitButton';
\r
10544 else if (co.ListBox)
\r
10545 c += ' mceToolbarStartListBox';
\r
10547 h += dom.createHTML('td', {'class' : c}, dom.createHTML('span', null, '<!-- IE -->'));
\r
10550 // Add toolbar end before list box and after the previous button
\r
10551 // This is to fix the o2k7 editor skins
\r
10552 if (pr && co.ListBox) {
\r
10553 if (pr.Button || pr.SplitButton)
\r
10554 h += dom.createHTML('td', {'class' : 'mceToolbarEnd'}, dom.createHTML('span', null, '<!-- IE -->'));
\r
10557 // Render control HTML
\r
10559 // IE 8 quick fix, needed to propertly generate a hit area for anchors
\r
10561 h += '<td style="position: relative">' + co.renderHTML() + '</td>';
\r
10563 h += '<td>' + co.renderHTML() + '</td>';
\r
10565 // Add toolbar start after list box and before the next button
\r
10566 // This is to fix the o2k7 editor skins
\r
10567 if (nx && co.ListBox) {
\r
10568 if (nx.Button || nx.SplitButton)
\r
10569 h += dom.createHTML('td', {'class' : 'mceToolbarStart'}, dom.createHTML('span', null, '<!-- IE -->'));
\r
10573 c = 'mceToolbarEnd';
\r
10576 c += ' mceToolbarEndButton';
\r
10577 else if (co.SplitButton)
\r
10578 c += ' mceToolbarEndSplitButton';
\r
10579 else if (co.ListBox)
\r
10580 c += ' mceToolbarEndListBox';
\r
10582 h += dom.createHTML('td', {'class' : c}, dom.createHTML('span', null, '<!-- IE -->'));
\r
10584 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
10589 (function(tinymce) {
\r
10590 var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each;
\r
10592 tinymce.create('tinymce.AddOnManager', {
\r
10593 AddOnManager : function() {
\r
10598 self.lookup = {};
\r
10599 self.onAdd = new Dispatcher(self);
\r
10602 get : function(n) {
\r
10603 if (this.lookup[n]) {
\r
10604 return this.lookup[n].instance;
\r
10606 return undefined;
\r
10610 dependencies : function(n) {
\r
10612 if (this.lookup[n]) {
\r
10613 result = this.lookup[n].dependencies;
\r
10615 return result || [];
\r
10618 requireLangPack : function(n) {
\r
10619 var s = tinymce.settings;
\r
10621 if (s && s.language && s.language_load !== false)
\r
10622 tinymce.ScriptLoader.add(this.urls[n] + '/langs/' + s.language + '.js');
\r
10625 add : function(id, o, dependencies) {
\r
10626 this.items.push(o);
\r
10627 this.lookup[id] = {instance:o, dependencies:dependencies};
\r
10628 this.onAdd.dispatch(this, id, o);
\r
10632 createUrl: function(baseUrl, dep) {
\r
10633 if (typeof dep === "object") {
\r
10636 return {prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix};
\r
10640 addComponents: function(pluginName, scripts) {
\r
10641 var pluginUrl = this.urls[pluginName];
\r
10642 tinymce.each(scripts, function(script){
\r
10643 tinymce.ScriptLoader.add(pluginUrl+"/"+script);
\r
10647 load : function(n, u, cb, s) {
\r
10648 var t = this, url = u;
\r
10650 function loadDependencies() {
\r
10651 var dependencies = t.dependencies(n);
\r
10652 tinymce.each(dependencies, function(dep) {
\r
10653 var newUrl = t.createUrl(u, dep);
\r
10654 t.load(newUrl.resource, newUrl, undefined, undefined);
\r
10660 cb.call(tinymce.ScriptLoader);
\r
10667 if (typeof u === "object")
\r
10668 url = u.prefix + u.resource + u.suffix;
\r
10670 if (url.indexOf('/') != 0 && url.indexOf('://') == -1)
\r
10671 url = tinymce.baseURL + '/' + url;
\r
10673 t.urls[n] = url.substring(0, url.lastIndexOf('/'));
\r
10675 if (t.lookup[n]) {
\r
10676 loadDependencies();
\r
10678 tinymce.ScriptLoader.add(url, loadDependencies, s);
\r
10683 // Create plugin and theme managers
\r
10684 tinymce.PluginManager = new tinymce.AddOnManager();
\r
10685 tinymce.ThemeManager = new tinymce.AddOnManager();
\r
10688 (function(tinymce) {
\r
10690 var each = tinymce.each, extend = tinymce.extend,
\r
10691 DOM = tinymce.DOM, Event = tinymce.dom.Event,
\r
10692 ThemeManager = tinymce.ThemeManager, PluginManager = tinymce.PluginManager,
\r
10693 explode = tinymce.explode,
\r
10694 Dispatcher = tinymce.util.Dispatcher, undefined, instanceCounter = 0;
\r
10696 // Setup some URLs where the editor API is located and where the document is
\r
10697 tinymce.documentBaseURL = window.location.href.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, '');
\r
10698 if (!/[\/\\]$/.test(tinymce.documentBaseURL))
\r
10699 tinymce.documentBaseURL += '/';
\r
10701 tinymce.baseURL = new tinymce.util.URI(tinymce.documentBaseURL).toAbsolute(tinymce.baseURL);
\r
10703 tinymce.baseURI = new tinymce.util.URI(tinymce.baseURL);
\r
10705 // Add before unload listener
\r
10706 // This was required since IE was leaking memory if you added and removed beforeunload listeners
\r
10707 // with attachEvent/detatchEvent so this only adds one listener and instances can the attach to the onBeforeUnload event
\r
10708 tinymce.onBeforeUnload = new Dispatcher(tinymce);
\r
10710 // Must be on window or IE will leak if the editor is placed in frame or iframe
\r
10711 Event.add(window, 'beforeunload', function(e) {
\r
10712 tinymce.onBeforeUnload.dispatch(tinymce, e);
\r
10715 tinymce.onAddEditor = new Dispatcher(tinymce);
\r
10717 tinymce.onRemoveEditor = new Dispatcher(tinymce);
\r
10719 tinymce.EditorManager = extend(tinymce, {
\r
10724 activeEditor : null,
\r
10726 init : function(s) {
\r
10727 var t = this, pl, sl = tinymce.ScriptLoader, e, el = [], ed;
\r
10729 function execCallback(se, n, s) {
\r
10735 if (tinymce.is(f, 'string')) {
\r
10736 s = f.replace(/\.\w+$/, '');
\r
10737 s = s ? tinymce.resolve(s) : 0;
\r
10738 f = tinymce.resolve(f);
\r
10741 return f.apply(s || this, Array.prototype.slice.call(arguments, 2));
\r
10745 theme : "simple",
\r
10752 Event.add(document, 'init', function() {
\r
10755 execCallback(s, 'onpageload');
\r
10757 switch (s.mode) {
\r
10759 l = s.elements || '';
\r
10761 if(l.length > 0) {
\r
10762 each(explode(l), function(v) {
\r
10763 if (DOM.get(v)) {
\r
10764 ed = new tinymce.Editor(v, s);
\r
10768 each(document.forms, function(f) {
\r
10769 each(f.elements, function(e) {
\r
10770 if (e.name === v) {
\r
10771 v = 'mce_editor_' + instanceCounter++;
\r
10772 DOM.setAttrib(e, 'id', v);
\r
10774 ed = new tinymce.Editor(v, s);
\r
10785 case "textareas":
\r
10786 case "specific_textareas":
\r
10787 function hasClass(n, c) {
\r
10788 return c.constructor === RegExp ? c.test(n.className) : DOM.hasClass(n, c);
\r
10791 each(DOM.select('textarea'), function(v) {
\r
10792 if (s.editor_deselector && hasClass(v, s.editor_deselector))
\r
10795 if (!s.editor_selector || hasClass(v, s.editor_selector)) {
\r
10796 // Can we use the name
\r
10797 e = DOM.get(v.name);
\r
10801 // Generate unique name if missing or already exists
\r
10802 if (!v.id || t.get(v.id))
\r
10803 v.id = DOM.uniqueId();
\r
10805 ed = new tinymce.Editor(v.id, s);
\r
10813 // Call onInit when all editors are initialized
\r
10817 each(el, function(ed) {
\r
10820 if (!ed.initialized) {
\r
10822 ed.onInit.add(function() {
\r
10827 execCallback(s, 'oninit');
\r
10834 execCallback(s, 'oninit');
\r
10840 get : function(id) {
\r
10841 if (id === undefined)
\r
10842 return this.editors;
\r
10844 return this.editors[id];
\r
10847 getInstanceById : function(id) {
\r
10848 return this.get(id);
\r
10851 add : function(editor) {
\r
10852 var self = this, editors = self.editors;
\r
10854 // Add named and index editor instance
\r
10855 editors[editor.id] = editor;
\r
10856 editors.push(editor);
\r
10858 self._setActive(editor);
\r
10859 self.onAddEditor.dispatch(self, editor);
\r
10865 remove : function(editor) {
\r
10866 var t = this, i, editors = t.editors;
\r
10868 // Not in the collection
\r
10869 if (!editors[editor.id])
\r
10872 delete editors[editor.id];
\r
10874 for (i = 0; i < editors.length; i++) {
\r
10875 if (editors[i] == editor) {
\r
10876 editors.splice(i, 1);
\r
10881 // Select another editor since the active one was removed
\r
10882 if (t.activeEditor == editor)
\r
10883 t._setActive(editors[0]);
\r
10885 editor.destroy();
\r
10886 t.onRemoveEditor.dispatch(t, editor);
\r
10891 execCommand : function(c, u, v) {
\r
10892 var t = this, ed = t.get(v), w;
\r
10894 // Manager commands
\r
10900 case "mceAddEditor":
\r
10901 case "mceAddControl":
\r
10903 new tinymce.Editor(v, t.settings).render();
\r
10907 case "mceAddFrameControl":
\r
10910 // Add tinyMCE global instance and tinymce namespace to specified window
\r
10911 w.tinyMCE = tinyMCE;
\r
10912 w.tinymce = tinymce;
\r
10914 tinymce.DOM.doc = w.document;
\r
10915 tinymce.DOM.win = w;
\r
10917 ed = new tinymce.Editor(v.element_id, v);
\r
10920 // Fix IE memory leaks
\r
10921 if (tinymce.isIE) {
\r
10924 w.detachEvent('onunload', clr);
\r
10925 w = w.tinyMCE = w.tinymce = null; // IE leak
\r
10928 w.attachEvent('onunload', clr);
\r
10931 v.page_window = null;
\r
10935 case "mceRemoveEditor":
\r
10936 case "mceRemoveControl":
\r
10942 case 'mceToggleEditor':
\r
10944 t.execCommand('mceAddControl', 0, v);
\r
10948 if (ed.isHidden())
\r
10956 // Run command on active editor
\r
10957 if (t.activeEditor)
\r
10958 return t.activeEditor.execCommand(c, u, v);
\r
10963 execInstanceCommand : function(id, c, u, v) {
\r
10964 var ed = this.get(id);
\r
10967 return ed.execCommand(c, u, v);
\r
10972 triggerSave : function() {
\r
10973 each(this.editors, function(e) {
\r
10978 addI18n : function(p, o) {
\r
10979 var lo, i18n = this.i18n;
\r
10981 if (!tinymce.is(p, 'string')) {
\r
10982 each(p, function(o, lc) {
\r
10983 each(o, function(o, g) {
\r
10984 each(o, function(o, k) {
\r
10985 if (g === 'common')
\r
10986 i18n[lc + '.' + k] = o;
\r
10988 i18n[lc + '.' + g + '.' + k] = o;
\r
10993 each(o, function(o, k) {
\r
10994 i18n[p + '.' + k] = o;
\r
10999 // Private methods
\r
11001 _setActive : function(editor) {
\r
11002 this.selectedInstance = this.activeEditor = editor;
\r
11007 (function(tinymce) {
\r
11008 // Shorten these names
\r
11009 var DOM = tinymce.DOM, Event = tinymce.dom.Event, extend = tinymce.extend,
\r
11010 Dispatcher = tinymce.util.Dispatcher, each = tinymce.each, isGecko = tinymce.isGecko,
\r
11011 isIE = tinymce.isIE, isWebKit = tinymce.isWebKit, is = tinymce.is,
\r
11012 ThemeManager = tinymce.ThemeManager, PluginManager = tinymce.PluginManager,
\r
11013 inArray = tinymce.inArray, grep = tinymce.grep, explode = tinymce.explode;
\r
11015 tinymce.create('tinymce.Editor', {
\r
11016 Editor : function(id, s) {
\r
11019 t.id = t.editorId = id;
\r
11021 t.execCommands = {};
\r
11022 t.queryStateCommands = {};
\r
11023 t.queryValueCommands = {};
\r
11025 t.isNotDirty = false;
\r
11029 // Add events to the editor
\r
11033 'onBeforeRenderUI',
\r
11073 'onBeforeSetContent',
\r
11075 'onBeforeGetContent',
\r
11089 'onBeforeExecCommand',
\r
11099 'onSetProgressState'
\r
11101 t[e] = new Dispatcher(t);
\r
11104 t.settings = s = extend({
\r
11107 docs_language : 'en',
\r
11108 theme : 'simple',
\r
11109 skin : 'default',
\r
11111 delta_height : 0,
\r
11114 document_base_url : tinymce.documentBaseURL,
\r
11115 add_form_submit_trigger : 1,
\r
11116 submit_patch : 1,
\r
11117 add_unload_trigger : 1,
\r
11118 convert_urls : 1,
\r
11119 relative_urls : 1,
\r
11120 remove_script_host : 1,
\r
11121 table_inline_editing : 0,
\r
11122 object_resizing : 1,
\r
11124 accessibility_focus : 1,
\r
11125 custom_shortcuts : 1,
\r
11126 custom_undo_redo_keyboard_shortcuts : 1,
\r
11127 custom_undo_redo_restore_selection : 1,
\r
11128 custom_undo_redo : 1,
\r
11129 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
11130 visual_table_class : 'mceItemTable',
\r
11132 font_size_style_values : 'xx-small,x-small,small,medium,large,x-large,xx-large',
\r
11133 apply_source_formatting : 1,
\r
11134 directionality : 'ltr',
\r
11135 forced_root_block : 'p',
\r
11136 hidden_input : 1,
\r
11137 padd_empty_editor : 1,
\r
11140 force_p_newlines : 1,
\r
11141 indentation : '30px',
\r
11143 fix_table_elements : 1,
\r
11144 inline_styles : 1,
\r
11145 convert_fonts_to_spans : true,
\r
11146 indent : 'simple',
\r
11147 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
11148 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
11150 entity_encoding : 'named',
\r
11151 url_converter : t.convertURL,
\r
11152 url_converter_scope : t,
\r
11153 ie7_compat : true
\r
11156 t.documentBaseURI = new tinymce.util.URI(s.document_base_url || tinymce.documentBaseURL, {
\r
11157 base_uri : tinyMCE.baseURI
\r
11160 t.baseURI = tinymce.baseURI;
\r
11162 t.contentCSS = [];
\r
11165 t.execCallback('setup', t);
\r
11168 render : function(nst) {
\r
11169 var t = this, s = t.settings, id = t.id, sl = tinymce.ScriptLoader;
\r
11171 // Page is not loaded yet, wait for it
\r
11172 if (!Event.domLoaded) {
\r
11173 Event.add(document, 'init', function() {
\r
11179 tinyMCE.settings = s;
\r
11181 // Element not found, then skip initialization
\r
11182 if (!t.getElement())
\r
11185 // Is a iPad/iPhone and not on iOS5, then skip initialization. We need to sniff
\r
11186 // here since the browser says it has contentEditable support but there is no visible
\r
11187 // caret We will remove this check ones Apple implements full contentEditable support
\r
11188 if (tinymce.isIDevice && !tinymce.isIOS5)
\r
11191 // Add hidden input for non input elements inside form elements
\r
11192 if (!/TEXTAREA|INPUT/i.test(t.getElement().nodeName) && s.hidden_input && DOM.getParent(id, 'form'))
\r
11193 DOM.insertAfter(DOM.create('input', {type : 'hidden', name : id}), id);
\r
11195 if (tinymce.WindowManager)
\r
11196 t.windowManager = new tinymce.WindowManager(t);
\r
11198 if (s.encoding == 'xml') {
\r
11199 t.onGetContent.add(function(ed, o) {
\r
11201 o.content = DOM.encode(o.content);
\r
11205 if (s.add_form_submit_trigger) {
\r
11206 t.onSubmit.addToTop(function() {
\r
11207 if (t.initialized) {
\r
11209 t.isNotDirty = 1;
\r
11214 if (s.add_unload_trigger) {
\r
11215 t._beforeUnload = tinyMCE.onBeforeUnload.add(function() {
\r
11216 if (t.initialized && !t.destroyed && !t.isHidden())
\r
11217 t.save({format : 'raw', no_events : true});
\r
11221 tinymce.addUnload(t.destroy, t);
\r
11223 if (s.submit_patch) {
\r
11224 t.onBeforeRenderUI.add(function() {
\r
11225 var n = t.getElement().form;
\r
11230 // Already patched
\r
11231 if (n._mceOldSubmit)
\r
11234 // Check page uses id="submit" or name="submit" for it's submit button
\r
11235 if (!n.submit.nodeType && !n.submit.length) {
\r
11236 t.formElement = n;
\r
11237 n._mceOldSubmit = n.submit;
\r
11238 n.submit = function() {
\r
11239 // Save all instances
\r
11240 tinymce.triggerSave();
\r
11241 t.isNotDirty = 1;
\r
11243 return t.formElement._mceOldSubmit(t.formElement);
\r
11252 function loadScripts() {
\r
11253 if (s.language && s.language_load !== false)
\r
11254 sl.add(tinymce.baseURL + '/langs/' + s.language + '.js');
\r
11256 if (s.theme && s.theme.charAt(0) != '-' && !ThemeManager.urls[s.theme])
\r
11257 ThemeManager.load(s.theme, 'themes/' + s.theme + '/editor_template' + tinymce.suffix + '.js');
\r
11259 each(explode(s.plugins), function(p) {
\r
11260 if (p &&!PluginManager.urls[p]) {
\r
11261 if (p.charAt(0) == '-') {
\r
11262 p = p.substr(1, p.length);
\r
11263 var dependencies = PluginManager.dependencies(p);
\r
11264 each(dependencies, function(dep) {
\r
11265 var defaultSettings = {prefix:'plugins/', resource: dep, suffix:'/editor_plugin' + tinymce.suffix + '.js'};
\r
11266 var dep = PluginManager.createUrl(defaultSettings, dep);
\r
11267 PluginManager.load(dep.resource, dep);
\r
11271 // Skip safari plugin, since it is removed as of 3.3b1
\r
11272 if (p == 'safari') {
\r
11275 PluginManager.load(p, {prefix:'plugins/', resource: p, suffix:'/editor_plugin' + tinymce.suffix + '.js'});
\r
11280 // Init when que is loaded
\r
11281 sl.loadQueue(function() {
\r
11290 init : function() {
\r
11291 var n, t = this, s = t.settings, w, h, e = t.getElement(), o, ti, u, bi, bc, re, i, initializedPlugins = [];
\r
11295 s.aria_label = s.aria_label || DOM.getAttrib(e, 'aria-label', t.getLang('aria.rich_text_area'));
\r
11298 s.theme = s.theme.replace(/-/, '');
\r
11299 o = ThemeManager.get(s.theme);
\r
11300 t.theme = new o();
\r
11302 if (t.theme.init && s.init_theme)
\r
11303 t.theme.init(t, ThemeManager.urls[s.theme] || tinymce.documentBaseURL.replace(/\/$/, ''));
\r
11305 function initPlugin(p) {
\r
11306 var c = PluginManager.get(p), u = PluginManager.urls[p] || tinymce.documentBaseURL.replace(/\/$/, ''), po;
\r
11307 if (c && tinymce.inArray(initializedPlugins,p) === -1) {
\r
11308 each(PluginManager.dependencies(p), function(dep){
\r
11311 po = new c(t, u);
\r
11313 t.plugins[p] = po;
\r
11317 initializedPlugins.push(p);
\r
11322 // Create all plugins
\r
11323 each(explode(s.plugins.replace(/\-/g, '')), initPlugin);
\r
11325 // Setup popup CSS path(s)
\r
11326 if (s.popup_css !== false) {
\r
11328 s.popup_css = t.documentBaseURI.toAbsolute(s.popup_css);
\r
11330 s.popup_css = t.baseURI.toAbsolute("themes/" + s.theme + "/skins/" + s.skin + "/dialog.css");
\r
11333 if (s.popup_css_add)
\r
11334 s.popup_css += ',' + t.documentBaseURI.toAbsolute(s.popup_css_add);
\r
11336 t.controlManager = new tinymce.ControlManager(t);
\r
11338 if (s.custom_undo_redo) {
\r
11339 t.onBeforeExecCommand.add(function(ed, cmd, ui, val, a) {
\r
11340 if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint' && (!a || !a.skip_undo))
\r
11341 t.undoManager.beforeChange();
\r
11344 t.onExecCommand.add(function(ed, cmd, ui, val, a) {
\r
11345 if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint' && (!a || !a.skip_undo))
\r
11346 t.undoManager.add();
\r
11350 t.onExecCommand.add(function(ed, c) {
\r
11351 // Don't refresh the select lists until caret move
\r
11352 if (!/^(FontName|FontSize)$/.test(c))
\r
11356 // Remove ghost selections on images and tables in Gecko
\r
11358 function repaint(a, o) {
\r
11359 if (!o || !o.initial)
\r
11360 t.execCommand('mceRepaint');
\r
11363 t.onUndo.add(repaint);
\r
11364 t.onRedo.add(repaint);
\r
11365 t.onSetContent.add(repaint);
\r
11368 // Enables users to override the control factory
\r
11369 t.onBeforeRenderUI.dispatch(t, t.controlManager);
\r
11372 if (s.render_ui) {
\r
11373 w = s.width || e.style.width || e.offsetWidth;
\r
11374 h = s.height || e.style.height || e.offsetHeight;
\r
11375 t.orgDisplay = e.style.display;
\r
11376 re = /^[0-9\.]+(|px)$/i;
\r
11378 if (re.test('' + w))
\r
11379 w = Math.max(parseInt(w) + (o.deltaWidth || 0), 100);
\r
11381 if (re.test('' + h))
\r
11382 h = Math.max(parseInt(h) + (o.deltaHeight || 0), 100);
\r
11385 o = t.theme.renderUI({
\r
11389 deltaWidth : s.delta_width,
\r
11390 deltaHeight : s.delta_height
\r
11393 t.editorContainer = o.editorContainer;
\r
11397 // User specified a document.domain value
\r
11398 if (document.domain && location.hostname != document.domain)
\r
11399 tinymce.relaxedDomain = document.domain;
\r
11402 DOM.setStyles(o.sizeContainer || o.editorContainer, {
\r
11407 // Load specified content CSS last
\r
11408 if (s.content_css) {
\r
11409 tinymce.each(explode(s.content_css), function(u) {
\r
11410 t.contentCSS.push(t.documentBaseURI.toAbsolute(u));
\r
11414 h = (o.iframeHeight || h) + (typeof(h) == 'number' ? (o.deltaHeight || 0) : '');
\r
11418 t.iframeHTML = s.doctype + '<html><head xmlns="http://www.w3.org/1999/xhtml">';
\r
11420 // We only need to override paths if we have to
\r
11421 // IE has a bug where it remove site absolute urls to relative ones if this is specified
\r
11422 if (s.document_base_url != tinymce.documentBaseURL)
\r
11423 t.iframeHTML += '<base href="' + t.documentBaseURI.getURI() + '" />';
\r
11425 // IE8 doesn't support carets behind images setting ie7_compat would force IE8+ to run in IE7 compat mode.
\r
11426 if (s.ie7_compat)
\r
11427 t.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=7" />';
\r
11429 t.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=edge" />';
\r
11431 t.iframeHTML += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />';
\r
11433 bi = s.body_id || 'tinymce';
\r
11434 if (bi.indexOf('=') != -1) {
\r
11435 bi = t.getParam('body_id', '', 'hash');
\r
11436 bi = bi[t.id] || bi;
\r
11439 bc = s.body_class || '';
\r
11440 if (bc.indexOf('=') != -1) {
\r
11441 bc = t.getParam('body_class', '', 'hash');
\r
11442 bc = bc[t.id] || '';
\r
11445 t.iframeHTML += '</head><body id="' + bi + '" class="mceContentBody ' + bc + '"><br></body></html>';
\r
11447 // Domain relaxing enabled, then set document domain
\r
11448 if (tinymce.relaxedDomain && (isIE || (tinymce.isOpera && parseFloat(opera.version()) < 11))) {
\r
11449 // We need to write the contents here in IE since multiple writes messes up refresh button and back button
\r
11450 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
11454 // TODO: ACC add the appropriate description on this.
\r
11455 n = DOM.add(o.iframeContainer, 'iframe', {
\r
11456 id : t.id + "_ifr",
\r
11457 src : u || 'javascript:""', // Workaround for HTTPS warning in IE6/7
\r
11458 frameBorder : '0',
\r
11459 allowTransparency : "true",
\r
11460 title : s.aria_label,
\r
11464 display : 'block' // Important for Gecko to render the iframe correctly
\r
11468 t.contentAreaContainer = o.iframeContainer;
\r
11469 DOM.get(o.editorContainer).style.display = t.orgDisplay;
\r
11470 DOM.get(t.id).style.display = 'none';
\r
11471 DOM.setAttrib(t.id, 'aria-hidden', true);
\r
11473 if (!tinymce.relaxedDomain || !u)
\r
11476 e = n = o = null; // Cleanup
\r
11479 setupIframe : function() {
\r
11480 var t = this, s = t.settings, e = DOM.get(t.id), d = t.getDoc(), h, b;
\r
11482 // Setup iframe body
\r
11483 if (!isIE || !tinymce.relaxedDomain) {
\r
11484 // Fix for a focus bug in FF 3.x where the body element
\r
11485 // wouldn't get proper focus if the user clicked on the HTML element
\r
11486 if (isGecko && !Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4
\r
11487 t.onMouseDown.add(function(ed, e) {
\r
11488 if (e.target.nodeName === "HTML") {
\r
11489 var body = t.getBody();
\r
11491 // Blur the body it's focused but not correctly focused
\r
11494 // Refocus the body after a little while
\r
11495 setTimeout(function() {
\r
11503 d.write(t.iframeHTML);
\r
11506 if (tinymce.relaxedDomain)
\r
11507 d.domain = tinymce.relaxedDomain;
\r
11510 // It will not steal focus while setting contentEditable
\r
11512 b.disabled = true;
\r
11515 b.contentEditable = true;
\r
11517 b.disabled = false;
\r
11519 t.schema = new tinymce.html.Schema(s);
\r
11521 t.dom = new tinymce.dom.DOMUtils(t.getDoc(), {
\r
11522 keep_values : true,
\r
11523 url_converter : t.convertURL,
\r
11524 url_converter_scope : t,
\r
11525 hex_colors : s.force_hex_style_colors,
\r
11526 class_filter : s.class_filter,
\r
11527 update_styles : 1,
\r
11528 fix_ie_paragraphs : 1,
\r
11529 schema : t.schema
\r
11532 t.parser = new tinymce.html.DomParser(s, t.schema);
\r
11534 // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included.
\r
11535 if (!t.settings.allow_html_in_named_anchor) {
\r
11536 t.parser.addAttributeFilter('name', function(nodes, name) {
\r
11537 var i = nodes.length, sibling, prevSibling, parent, node;
\r
11541 if (node.name === 'a' && node.firstChild) {
\r
11542 parent = node.parent;
\r
11544 // Move children after current node
\r
11545 sibling = node.lastChild;
\r
11547 prevSibling = sibling.prev;
\r
11548 parent.insert(sibling, node);
\r
11549 sibling = prevSibling;
\r
11550 } while (sibling);
\r
11556 // Convert src and href into data-mce-src, data-mce-href and data-mce-style
\r
11557 t.parser.addAttributeFilter('src,href,style', function(nodes, name) {
\r
11558 var i = nodes.length, node, dom = t.dom, value, internalName;
\r
11562 value = node.attr(name);
\r
11563 internalName = 'data-mce-' + name;
\r
11565 // Add internal attribute if we need to we don't on a refresh of the document
\r
11566 if (!node.attributes.map[internalName]) {
\r
11567 if (name === "style")
\r
11568 node.attr(internalName, dom.serializeStyle(dom.parseStyle(value), node.name));
\r
11570 node.attr(internalName, t.convertURL(value, name, node.name));
\r
11575 // Keep scripts from executing
\r
11576 t.parser.addNodeFilter('script', function(nodes, name) {
\r
11577 var i = nodes.length;
\r
11580 nodes[i].attr('type', 'mce-text/javascript');
\r
11583 t.parser.addNodeFilter('#cdata', function(nodes, name) {
\r
11584 var i = nodes.length, node;
\r
11589 node.name = '#comment';
\r
11590 node.value = '[CDATA[' + node.value + ']]';
\r
11594 t.parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', function(nodes, name) {
\r
11595 var i = nodes.length, node, nonEmptyElements = t.schema.getNonEmptyElements();
\r
11600 if (node.isEmpty(nonEmptyElements))
\r
11601 node.empty().append(new tinymce.html.Node('br', 1)).shortEnded = true;
\r
11605 t.serializer = new tinymce.dom.Serializer(s, t.dom, t.schema);
\r
11607 t.selection = new tinymce.dom.Selection(t.dom, t.getWin(), t.serializer);
\r
11609 t.formatter = new tinymce.Formatter(this);
\r
11611 // Register default formats
\r
11612 t.formatter.register({
\r
11614 {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'left'}},
\r
11615 {selector : 'img,table', collapsed : false, styles : {'float' : 'left'}}
\r
11619 {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'center'}},
\r
11620 {selector : 'img', collapsed : false, styles : {display : 'block', marginLeft : 'auto', marginRight : 'auto'}},
\r
11621 {selector : 'table', collapsed : false, styles : {marginLeft : 'auto', marginRight : 'auto'}}
\r
11625 {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'right'}},
\r
11626 {selector : 'img,table', collapsed : false, styles : {'float' : 'right'}}
\r
11630 {selector : 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'justify'}}
\r
11634 {inline : 'strong', remove : 'all'},
\r
11635 {inline : 'span', styles : {fontWeight : 'bold'}},
\r
11636 {inline : 'b', remove : 'all'}
\r
11640 {inline : 'em', remove : 'all'},
\r
11641 {inline : 'span', styles : {fontStyle : 'italic'}},
\r
11642 {inline : 'i', remove : 'all'}
\r
11646 {inline : 'span', styles : {textDecoration : 'underline'}, exact : true},
\r
11647 {inline : 'u', remove : 'all'}
\r
11650 strikethrough : [
\r
11651 {inline : 'span', styles : {textDecoration : 'line-through'}, exact : true},
\r
11652 {inline : 'strike', remove : 'all'}
\r
11655 forecolor : {inline : 'span', styles : {color : '%value'}, wrap_links : false},
\r
11656 hilitecolor : {inline : 'span', styles : {backgroundColor : '%value'}, wrap_links : false},
\r
11657 fontname : {inline : 'span', styles : {fontFamily : '%value'}},
\r
11658 fontsize : {inline : 'span', styles : {fontSize : '%value'}},
\r
11659 fontsize_class : {inline : 'span', attributes : {'class' : '%value'}},
\r
11660 blockquote : {block : 'blockquote', wrapper : 1, remove : 'all'},
\r
11661 subscript : {inline : 'sub'},
\r
11662 superscript : {inline : 'sup'},
\r
11664 link : {inline : 'a', selector : 'a', remove : 'all', split : true, deep : true,
\r
11665 onmatch : function(node) {
\r
11669 onformat : function(elm, fmt, vars) {
\r
11670 each(vars, function(value, key) {
\r
11671 t.dom.setAttrib(elm, key, value);
\r
11677 {selector : 'b,strong,em,i,font,u,strike', remove : 'all', split : true, expand : false, block_expand : true, deep : true},
\r
11678 {selector : 'span', attributes : ['style', 'class'], remove : 'empty', split : true, expand : false, deep : true},
\r
11679 {selector : '*', attributes : ['style', 'class'], split : false, expand : false, deep : true}
\r
11683 // Register default block formats
\r
11684 each('p h1 h2 h3 h4 h5 h6 div address pre div code dt dd samp'.split(/\s/), function(name) {
\r
11685 t.formatter.register(name, {block : name, remove : 'all'});
\r
11688 // Register user defined formats
\r
11689 t.formatter.register(t.settings.formats);
\r
11691 t.undoManager = new tinymce.UndoManager(t);
\r
11694 t.undoManager.onAdd.add(function(um, l) {
\r
11695 if (um.hasUndo())
\r
11696 return t.onChange.dispatch(t, l, um);
\r
11699 t.undoManager.onUndo.add(function(um, l) {
\r
11700 return t.onUndo.dispatch(t, l, um);
\r
11703 t.undoManager.onRedo.add(function(um, l) {
\r
11704 return t.onRedo.dispatch(t, l, um);
\r
11707 t.forceBlocks = new tinymce.ForceBlocks(t, {
\r
11708 forced_root_block : s.forced_root_block
\r
11711 t.editorCommands = new tinymce.EditorCommands(t);
\r
11714 t.serializer.onPreProcess.add(function(se, o) {
\r
11715 return t.onPreProcess.dispatch(t, o, se);
\r
11718 t.serializer.onPostProcess.add(function(se, o) {
\r
11719 return t.onPostProcess.dispatch(t, o, se);
\r
11722 t.onPreInit.dispatch(t);
\r
11724 if (!s.gecko_spellcheck)
\r
11725 t.getBody().spellcheck = 0;
\r
11730 t.controlManager.onPostRender.dispatch(t, t.controlManager);
\r
11731 t.onPostRender.dispatch(t);
\r
11733 t.quirks = new tinymce.util.Quirks(this);
\r
11735 if (s.directionality)
\r
11736 t.getBody().dir = s.directionality;
\r
11739 t.getBody().style.whiteSpace = "nowrap";
\r
11741 if (s.handle_node_change_callback) {
\r
11742 t.onNodeChange.add(function(ed, cm, n) {
\r
11743 t.execCallback('handle_node_change_callback', t.id, n, -1, -1, true, t.selection.isCollapsed());
\r
11747 if (s.save_callback) {
\r
11748 t.onSaveContent.add(function(ed, o) {
\r
11749 var h = t.execCallback('save_callback', t.id, o.content, t.getBody());
\r
11756 if (s.onchange_callback) {
\r
11757 t.onChange.add(function(ed, l) {
\r
11758 t.execCallback('onchange_callback', t, l);
\r
11763 t.onBeforeSetContent.add(function(ed, o) {
\r
11765 each(s.protect, function(pattern) {
\r
11766 o.content = o.content.replace(pattern, function(str) {
\r
11767 return '<!--mce:protected ' + escape(str) + '-->';
\r
11774 if (s.convert_newlines_to_brs) {
\r
11775 t.onBeforeSetContent.add(function(ed, o) {
\r
11777 o.content = o.content.replace(/\r?\n/g, '<br />');
\r
11781 if (s.preformatted) {
\r
11782 t.onPostProcess.add(function(ed, o) {
\r
11783 o.content = o.content.replace(/^\s*<pre.*?>/, '');
\r
11784 o.content = o.content.replace(/<\/pre>\s*$/, '');
\r
11787 o.content = '<pre class="mceItemHidden">' + o.content + '</pre>';
\r
11791 if (s.verify_css_classes) {
\r
11792 t.serializer.attribValueFilter = function(n, v) {
\r
11795 if (n == 'class') {
\r
11796 // Build regexp for classes
\r
11797 if (!t.classesRE) {
\r
11798 cl = t.dom.getClasses();
\r
11800 if (cl.length > 0) {
\r
11803 each (cl, function(o) {
\r
11804 s += (s ? '|' : '') + o['class'];
\r
11807 t.classesRE = new RegExp('(' + s + ')', 'gi');
\r
11811 return !t.classesRE || /(\bmceItem\w+\b|\bmceTemp\w+\b)/g.test(v) || t.classesRE.test(v) ? v : '';
\r
11818 if (s.cleanup_callback) {
\r
11819 t.onBeforeSetContent.add(function(ed, o) {
\r
11820 o.content = t.execCallback('cleanup_callback', 'insert_to_editor', o.content, o);
\r
11823 t.onPreProcess.add(function(ed, o) {
\r
11825 t.execCallback('cleanup_callback', 'insert_to_editor_dom', o.node, o);
\r
11828 t.execCallback('cleanup_callback', 'get_from_editor_dom', o.node, o);
\r
11831 t.onPostProcess.add(function(ed, o) {
\r
11833 o.content = t.execCallback('cleanup_callback', 'insert_to_editor', o.content, o);
\r
11836 o.content = t.execCallback('cleanup_callback', 'get_from_editor', o.content, o);
\r
11840 if (s.save_callback) {
\r
11841 t.onGetContent.add(function(ed, o) {
\r
11843 o.content = t.execCallback('save_callback', t.id, o.content, t.getBody());
\r
11847 if (s.handle_event_callback) {
\r
11848 t.onEvent.add(function(ed, e, o) {
\r
11849 if (t.execCallback('handle_event_callback', e, ed, o) === false)
\r
11854 // Add visual aids when new contents is added
\r
11855 t.onSetContent.add(function() {
\r
11856 t.addVisual(t.getBody());
\r
11859 // Remove empty contents
\r
11860 if (s.padd_empty_editor) {
\r
11861 t.onPostProcess.add(function(ed, o) {
\r
11862 o.content = o.content.replace(/^(<p[^>]*>( | |\s|\u00a0|)<\/p>[\r\n]*|<br \/>[\r\n]*)$/, '');
\r
11867 // Fix gecko link bug, when a link is placed at the end of block elements there is
\r
11868 // no way to move the caret behind the link. This fix adds a bogus br element after the link
\r
11869 function fixLinks(ed, o) {
\r
11870 each(ed.dom.select('a'), function(n) {
\r
11871 var pn = n.parentNode;
\r
11873 if (ed.dom.isBlock(pn) && pn.lastChild === n)
\r
11874 ed.dom.add(pn, 'br', {'data-mce-bogus' : 1});
\r
11878 t.onExecCommand.add(function(ed, cmd) {
\r
11879 if (cmd === 'CreateLink')
\r
11883 t.onSetContent.add(t.selection.onSetContent.add(fixLinks));
\r
11886 t.load({initial : true, format : 'html'});
\r
11887 t.startContent = t.getContent({format : 'raw'});
\r
11888 t.undoManager.add();
\r
11889 t.initialized = true;
\r
11891 t.onInit.dispatch(t);
\r
11892 t.execCallback('setupcontent_callback', t.id, t.getBody(), t.getDoc());
\r
11893 t.execCallback('init_instance_callback', t);
\r
11895 t.nodeChanged({initial : 1});
\r
11897 // Load specified content CSS last
\r
11898 each(t.contentCSS, function(u) {
\r
11899 t.dom.loadCSS(u);
\r
11902 // Handle auto focus
\r
11903 if (s.auto_focus) {
\r
11904 setTimeout(function () {
\r
11905 var ed = tinymce.get(s.auto_focus);
\r
11907 ed.selection.select(ed.getBody(), 1);
\r
11908 ed.selection.collapse(1);
\r
11909 ed.getBody().focus();
\r
11910 ed.getWin().focus();
\r
11918 focus : function(sf) {
\r
11919 var oed, t = this, selection = t.selection, ce = t.settings.content_editable, ieRng, controlElm, doc = t.getDoc();
\r
11922 // Get selected control element
\r
11923 ieRng = selection.getRng();
\r
11924 if (ieRng.item) {
\r
11925 controlElm = ieRng.item(0);
\r
11928 t._refreshContentEditable();
\r
11929 selection.normalize();
\r
11931 // Is not content editable
\r
11933 t.getWin().focus();
\r
11935 // Focus the body as well since it's contentEditable
\r
11936 if (tinymce.isGecko) {
\r
11937 t.getBody().focus();
\r
11940 // Restore selected control element
\r
11941 // This is needed when for example an image is selected within a
\r
11942 // layer a call to focus will then remove the control selection
\r
11943 if (controlElm && controlElm.ownerDocument == doc) {
\r
11944 ieRng = doc.body.createControlRange();
\r
11945 ieRng.addElement(controlElm);
\r
11951 if (tinymce.activeEditor != t) {
\r
11952 if ((oed = tinymce.activeEditor) != null)
\r
11953 oed.onDeactivate.dispatch(oed, t);
\r
11955 t.onActivate.dispatch(t, oed);
\r
11958 tinymce._setActive(t);
\r
11961 execCallback : function(n) {
\r
11962 var t = this, f = t.settings[n], s;
\r
11967 // Look through lookup
\r
11968 if (t.callbackLookup && (s = t.callbackLookup[n])) {
\r
11973 if (is(f, 'string')) {
\r
11974 s = f.replace(/\.\w+$/, '');
\r
11975 s = s ? tinymce.resolve(s) : 0;
\r
11976 f = tinymce.resolve(f);
\r
11977 t.callbackLookup = t.callbackLookup || {};
\r
11978 t.callbackLookup[n] = {func : f, scope : s};
\r
11981 return f.apply(s || t, Array.prototype.slice.call(arguments, 1));
\r
11984 translate : function(s) {
\r
11985 var c = this.settings.language || 'en', i18n = tinymce.i18n;
\r
11990 return i18n[c + '.' + s] || s.replace(/{\#([^}]+)\}/g, function(a, b) {
\r
11991 return i18n[c + '.' + b] || '{#' + b + '}';
\r
11995 getLang : function(n, dv) {
\r
11996 return tinymce.i18n[(this.settings.language || 'en') + '.' + n] || (is(dv) ? dv : '{#' + n + '}');
\r
11999 getParam : function(n, dv, ty) {
\r
12000 var tr = tinymce.trim, v = is(this.settings[n]) ? this.settings[n] : dv, o;
\r
12002 if (ty === 'hash') {
\r
12005 if (is(v, 'string')) {
\r
12006 each(v.indexOf('=') > 0 ? v.split(/[;,](?![^=;,]*(?:[;,]|$))/) : v.split(','), function(v) {
\r
12007 v = v.split('=');
\r
12009 if (v.length > 1)
\r
12010 o[tr(v[0])] = tr(v[1]);
\r
12012 o[tr(v[0])] = tr(v);
\r
12023 nodeChanged : function(o) {
\r
12024 var t = this, s = t.selection, n = s.getStart() || t.getBody();
\r
12026 // Fix for bug #1896577 it seems that this can not be fired while the editor is loading
\r
12027 if (t.initialized) {
\r
12029 n = isIE && n.ownerDocument != t.getDoc() ? t.getBody() : n; // Fix for IE initial state
\r
12031 // Get parents and add them to object
\r
12033 t.dom.getParent(n, function(node) {
\r
12034 if (node.nodeName == 'BODY')
\r
12037 o.parents.push(node);
\r
12040 t.onNodeChange.dispatch(
\r
12042 o ? o.controlManager || t.controlManager : t.controlManager,
\r
12050 addButton : function(n, s) {
\r
12053 t.buttons = t.buttons || {};
\r
12054 t.buttons[n] = s;
\r
12057 addCommand : function(name, callback, scope) {
\r
12058 this.execCommands[name] = {func : callback, scope : scope || this};
\r
12061 addQueryStateHandler : function(name, callback, scope) {
\r
12062 this.queryStateCommands[name] = {func : callback, scope : scope || this};
\r
12065 addQueryValueHandler : function(name, callback, scope) {
\r
12066 this.queryValueCommands[name] = {func : callback, scope : scope || this};
\r
12069 addShortcut : function(pa, desc, cmd_func, sc) {
\r
12072 if (!t.settings.custom_shortcuts)
\r
12075 t.shortcuts = t.shortcuts || {};
\r
12077 if (is(cmd_func, 'string')) {
\r
12080 cmd_func = function() {
\r
12081 t.execCommand(c, false, null);
\r
12085 if (is(cmd_func, 'object')) {
\r
12088 cmd_func = function() {
\r
12089 t.execCommand(c[0], c[1], c[2]);
\r
12093 each(explode(pa), function(pa) {
\r
12096 scope : sc || this,
\r
12103 each(explode(pa, '+'), function(v) {
\r
12112 o.charCode = v.charCodeAt(0);
\r
12113 o.keyCode = v.toUpperCase().charCodeAt(0);
\r
12117 t.shortcuts[(o.ctrl ? 'ctrl' : '') + ',' + (o.alt ? 'alt' : '') + ',' + (o.shift ? 'shift' : '') + ',' + o.keyCode] = o;
\r
12123 execCommand : function(cmd, ui, val, a) {
\r
12124 var t = this, s = 0, o, st;
\r
12126 if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint|SelectAll)$/.test(cmd) && (!a || !a.skip_focus))
\r
12130 t.onBeforeExecCommand.dispatch(t, cmd, ui, val, o);
\r
12134 // Command callback
\r
12135 if (t.execCallback('execcommand_callback', t.id, t.selection.getNode(), cmd, ui, val)) {
\r
12136 t.onExecCommand.dispatch(t, cmd, ui, val, a);
\r
12140 // Registred commands
\r
12141 if (o = t.execCommands[cmd]) {
\r
12142 st = o.func.call(o.scope, ui, val);
\r
12144 // Fall through on true
\r
12145 if (st !== true) {
\r
12146 t.onExecCommand.dispatch(t, cmd, ui, val, a);
\r
12151 // Plugin commands
\r
12152 each(t.plugins, function(p) {
\r
12153 if (p.execCommand && p.execCommand(cmd, ui, val)) {
\r
12154 t.onExecCommand.dispatch(t, cmd, ui, val, a);
\r
12163 // Theme commands
\r
12164 if (t.theme && t.theme.execCommand && t.theme.execCommand(cmd, ui, val)) {
\r
12165 t.onExecCommand.dispatch(t, cmd, ui, val, a);
\r
12169 // Editor commands
\r
12170 if (t.editorCommands.execCommand(cmd, ui, val)) {
\r
12171 t.onExecCommand.dispatch(t, cmd, ui, val, a);
\r
12175 // Browser commands
\r
12176 t.getDoc().execCommand(cmd, ui, val);
\r
12177 t.onExecCommand.dispatch(t, cmd, ui, val, a);
\r
12180 queryCommandState : function(cmd) {
\r
12181 var t = this, o, s;
\r
12183 // Is hidden then return undefined
\r
12184 if (t._isHidden())
\r
12187 // Registred commands
\r
12188 if (o = t.queryStateCommands[cmd]) {
\r
12189 s = o.func.call(o.scope);
\r
12191 // Fall though on true
\r
12196 // Registred commands
\r
12197 o = t.editorCommands.queryCommandState(cmd);
\r
12201 // Browser commands
\r
12203 return this.getDoc().queryCommandState(cmd);
\r
12205 // Fails sometimes see bug: 1896577
\r
12209 queryCommandValue : function(c) {
\r
12210 var t = this, o, s;
\r
12212 // Is hidden then return undefined
\r
12213 if (t._isHidden())
\r
12216 // Registred commands
\r
12217 if (o = t.queryValueCommands[c]) {
\r
12218 s = o.func.call(o.scope);
\r
12220 // Fall though on true
\r
12225 // Registred commands
\r
12226 o = t.editorCommands.queryCommandValue(c);
\r
12230 // Browser commands
\r
12232 return this.getDoc().queryCommandValue(c);
\r
12234 // Fails sometimes see bug: 1896577
\r
12238 show : function() {
\r
12241 DOM.show(t.getContainer());
\r
12246 hide : function() {
\r
12247 var t = this, d = t.getDoc();
\r
12249 // Fixed bug where IE has a blinking cursor left from the editor
\r
12251 d.execCommand('SelectAll');
\r
12253 // We must save before we hide so Safari doesn't crash
\r
12255 DOM.hide(t.getContainer());
\r
12256 DOM.setStyle(t.id, 'display', t.orgDisplay);
\r
12259 isHidden : function() {
\r
12260 return !DOM.isHidden(this.id);
\r
12263 setProgressState : function(b, ti, o) {
\r
12264 this.onSetProgressState.dispatch(this, b, ti, o);
\r
12269 load : function(o) {
\r
12270 var t = this, e = t.getElement(), h;
\r
12276 // Double encode existing entities in the value
\r
12277 h = t.setContent(is(e.value) ? e.value : e.innerHTML, o);
\r
12280 if (!o.no_events)
\r
12281 t.onLoadContent.dispatch(t, o);
\r
12283 o.element = e = null;
\r
12289 save : function(o) {
\r
12290 var t = this, e = t.getElement(), h, f;
\r
12292 if (!e || !t.initialized)
\r
12298 // Add undo level will trigger onchange event
\r
12299 if (!o.no_events) {
\r
12300 t.undoManager.typing = false;
\r
12301 t.undoManager.add();
\r
12305 h = o.content = t.getContent(o);
\r
12307 if (!o.no_events)
\r
12308 t.onSaveContent.dispatch(t, o);
\r
12312 if (!/TEXTAREA|INPUT/i.test(e.nodeName)) {
\r
12315 // Update hidden form element
\r
12316 if (f = DOM.getParent(t.id, 'form')) {
\r
12317 each(f.elements, function(e) {
\r
12318 if (e.name == t.id) {
\r
12327 o.element = e = null;
\r
12332 setContent : function(content, args) {
\r
12333 var self = this, rootNode, body = self.getBody(), forcedRootBlockName;
\r
12335 // Setup args object
\r
12336 args = args || {};
\r
12337 args.format = args.format || 'html';
\r
12339 args.content = content;
\r
12341 // Do preprocessing
\r
12342 if (!args.no_events)
\r
12343 self.onBeforeSetContent.dispatch(self, args);
\r
12345 content = args.content;
\r
12347 // Padd empty content in Gecko and Safari. Commands will otherwise fail on the content
\r
12348 // It will also be impossible to place the caret in the editor unless there is a BR element present
\r
12349 if (!tinymce.isIE && (content.length === 0 || /^\s+$/.test(content))) {
\r
12350 forcedRootBlockName = self.settings.forced_root_block;
\r
12351 if (forcedRootBlockName)
\r
12352 content = '<' + forcedRootBlockName + '><br data-mce-bogus="1"></' + forcedRootBlockName + '>';
\r
12354 content = '<br data-mce-bogus="1">';
\r
12356 body.innerHTML = content;
\r
12357 self.selection.select(body, true);
\r
12358 self.selection.collapse(true);
\r
12362 // Parse and serialize the html
\r
12363 if (args.format !== 'raw') {
\r
12364 content = new tinymce.html.Serializer({}, self.schema).serialize(
\r
12365 self.parser.parse(content)
\r
12369 // Set the new cleaned contents to the editor
\r
12370 args.content = tinymce.trim(content);
\r
12371 self.dom.setHTML(body, args.content);
\r
12373 // Do post processing
\r
12374 if (!args.no_events)
\r
12375 self.onSetContent.dispatch(self, args);
\r
12377 self.selection.normalize();
\r
12379 return args.content;
\r
12382 getContent : function(args) {
\r
12383 var self = this, content;
\r
12385 // Setup args object
\r
12386 args = args || {};
\r
12387 args.format = args.format || 'html';
\r
12390 // Do preprocessing
\r
12391 if (!args.no_events)
\r
12392 self.onBeforeGetContent.dispatch(self, args);
\r
12394 // Get raw contents or by default the cleaned contents
\r
12395 if (args.format == 'raw')
\r
12396 content = self.getBody().innerHTML;
\r
12398 content = self.serializer.serialize(self.getBody(), args);
\r
12400 args.content = tinymce.trim(content);
\r
12402 // Do post processing
\r
12403 if (!args.no_events)
\r
12404 self.onGetContent.dispatch(self, args);
\r
12406 return args.content;
\r
12409 isDirty : function() {
\r
12412 return tinymce.trim(self.startContent) != tinymce.trim(self.getContent({format : 'raw', no_events : 1})) && !self.isNotDirty;
\r
12415 getContainer : function() {
\r
12418 if (!t.container)
\r
12419 t.container = DOM.get(t.editorContainer || t.id + '_parent');
\r
12421 return t.container;
\r
12424 getContentAreaContainer : function() {
\r
12425 return this.contentAreaContainer;
\r
12428 getElement : function() {
\r
12429 return DOM.get(this.settings.content_element || this.id);
\r
12432 getWin : function() {
\r
12435 if (!t.contentWindow) {
\r
12436 e = DOM.get(t.id + "_ifr");
\r
12439 t.contentWindow = e.contentWindow;
\r
12442 return t.contentWindow;
\r
12445 getDoc : function() {
\r
12448 if (!t.contentDocument) {
\r
12452 t.contentDocument = w.document;
\r
12455 return t.contentDocument;
\r
12458 getBody : function() {
\r
12459 return this.bodyElement || this.getDoc().body;
\r
12462 convertURL : function(u, n, e) {
\r
12463 var t = this, s = t.settings;
\r
12465 // Use callback instead
\r
12466 if (s.urlconverter_callback)
\r
12467 return t.execCallback('urlconverter_callback', u, e, true, n);
\r
12469 // Don't convert link href since thats the CSS files that gets loaded into the editor also skip local file URLs
\r
12470 if (!s.convert_urls || (e && e.nodeName == 'LINK') || u.indexOf('file:') === 0)
\r
12473 // Convert to relative
\r
12474 if (s.relative_urls)
\r
12475 return t.documentBaseURI.toRelative(u);
\r
12477 // Convert to absolute
\r
12478 u = t.documentBaseURI.toAbsolute(u, s.remove_script_host);
\r
12483 addVisual : function(e) {
\r
12484 var t = this, s = t.settings;
\r
12486 e = e || t.getBody();
\r
12488 if (!is(t.hasVisual))
\r
12489 t.hasVisual = s.visual;
\r
12491 each(t.dom.select('table,a', e), function(e) {
\r
12494 switch (e.nodeName) {
\r
12496 v = t.dom.getAttrib(e, 'border');
\r
12498 if (!v || v == '0') {
\r
12500 t.dom.addClass(e, s.visual_table_class);
\r
12502 t.dom.removeClass(e, s.visual_table_class);
\r
12508 v = t.dom.getAttrib(e, 'name');
\r
12512 t.dom.addClass(e, 'mceItemAnchor');
\r
12514 t.dom.removeClass(e, 'mceItemAnchor');
\r
12521 t.onVisualAid.dispatch(t, e, t.hasVisual);
\r
12524 remove : function() {
\r
12525 var t = this, e = t.getContainer();
\r
12527 t.removed = 1; // Cancels post remove event execution
\r
12530 t.execCallback('remove_instance_callback', t);
\r
12531 t.onRemove.dispatch(t);
\r
12533 // Clear all execCommand listeners this is required to avoid errors if the editor was removed inside another command
\r
12534 t.onExecCommand.listeners = [];
\r
12536 tinymce.remove(t);
\r
12540 destroy : function(s) {
\r
12543 // One time is enough
\r
12548 tinymce.removeUnload(t.destroy);
\r
12549 tinyMCE.onBeforeUnload.remove(t._beforeUnload);
\r
12551 // Manual destroy
\r
12552 if (t.theme && t.theme.destroy)
\r
12553 t.theme.destroy();
\r
12555 // Destroy controls, selection and dom
\r
12556 t.controlManager.destroy();
\r
12557 t.selection.destroy();
\r
12560 // Remove all events
\r
12562 // Don't clear the window or document if content editable
\r
12563 // is enabled since other instances might still be present
\r
12564 if (!t.settings.content_editable) {
\r
12565 Event.clear(t.getWin());
\r
12566 Event.clear(t.getDoc());
\r
12569 Event.clear(t.getBody());
\r
12570 Event.clear(t.formElement);
\r
12573 if (t.formElement) {
\r
12574 t.formElement.submit = t.formElement._mceOldSubmit;
\r
12575 t.formElement._mceOldSubmit = null;
\r
12578 t.contentAreaContainer = t.formElement = t.container = t.settings.content_element = t.bodyElement = t.contentDocument = t.contentWindow = null;
\r
12581 t.selection = t.selection.win = t.selection.dom = t.selection.dom.doc = null;
\r
12586 // Internal functions
\r
12588 _addEvents : function() {
\r
12589 // 'focus', 'blur', 'dblclick', 'beforedeactivate', submit, reset
\r
12590 var t = this, i, s = t.settings, dom = t.dom, lo = {
\r
12591 mouseup : 'onMouseUp',
\r
12592 mousedown : 'onMouseDown',
\r
12593 click : 'onClick',
\r
12594 keyup : 'onKeyUp',
\r
12595 keydown : 'onKeyDown',
\r
12596 keypress : 'onKeyPress',
\r
12597 submit : 'onSubmit',
\r
12598 reset : 'onReset',
\r
12599 contextmenu : 'onContextMenu',
\r
12600 dblclick : 'onDblClick',
\r
12601 paste : 'onPaste' // Doesn't work in all browsers yet
\r
12604 function eventHandler(e, o) {
\r
12607 // Don't fire events when it's removed
\r
12611 // Generic event handler
\r
12612 if (t.onEvent.dispatch(t, e, o) !== false) {
\r
12613 // Specific event handler
\r
12614 t[lo[e.fakeType || e.type]].dispatch(t, e, o);
\r
12618 // Add DOM events
\r
12619 each(lo, function(v, k) {
\r
12621 case 'contextmenu':
\r
12622 dom.bind(t.getDoc(), k, eventHandler);
\r
12626 dom.bind(t.getBody(), k, function(e) {
\r
12633 dom.bind(t.getElement().form || DOM.getParent(t.id, 'form'), k, eventHandler);
\r
12637 dom.bind(s.content_editable ? t.getBody() : t.getDoc(), k, eventHandler);
\r
12641 dom.bind(s.content_editable ? t.getBody() : (isGecko ? t.getDoc() : t.getWin()), 'focus', function(e) {
\r
12646 // Fixes bug where a specified document_base_uri could result in broken images
\r
12647 // This will also fix drag drop of images in Gecko
\r
12648 if (tinymce.isGecko) {
\r
12649 dom.bind(t.getDoc(), 'DOMNodeInserted', function(e) {
\r
12654 if (e.nodeType === 1 && e.nodeName === 'IMG' && (v = e.getAttribute('data-mce-src')))
\r
12655 e.src = t.documentBaseURI.toAbsolute(v);
\r
12659 // Set various midas options in Gecko
\r
12661 function setOpts() {
\r
12662 var t = this, d = t.getDoc(), s = t.settings;
\r
12664 if (isGecko && !s.readonly) {
\r
12665 t._refreshContentEditable();
\r
12668 // Try new Gecko method
\r
12669 d.execCommand("styleWithCSS", 0, false);
\r
12671 // Use old method
\r
12672 if (!t._isHidden())
\r
12673 try {d.execCommand("useCSS", 0, true);} catch (ex) {}
\r
12676 if (!s.table_inline_editing)
\r
12677 try {d.execCommand('enableInlineTableEditing', false, false);} catch (ex) {}
\r
12679 if (!s.object_resizing)
\r
12680 try {d.execCommand('enableObjectResizing', false, false);} catch (ex) {}
\r
12684 t.onBeforeExecCommand.add(setOpts);
\r
12685 t.onMouseDown.add(setOpts);
\r
12688 t.onClick.add(function(ed, e) {
\r
12691 // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250
\r
12692 // WebKit can't even do simple things like selecting an image
\r
12693 // Needs tobe the setBaseAndExtend or it will fail to select floated images
\r
12694 if (tinymce.isWebKit && e.nodeName == 'IMG')
\r
12695 t.selection.getSel().setBaseAndExtent(e, 0, e, 1);
\r
12697 if (e.nodeName == 'A' && dom.hasClass(e, 'mceItemAnchor'))
\r
12698 t.selection.select(e);
\r
12703 // Add node change handlers
\r
12704 t.onMouseUp.add(t.nodeChanged);
\r
12705 //t.onClick.add(t.nodeChanged);
\r
12706 t.onKeyUp.add(function(ed, e) {
\r
12707 var c = e.keyCode;
\r
12709 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
12714 // Add block quote deletion handler
\r
12715 t.onKeyDown.add(function(ed, e) {
\r
12716 // Was the BACKSPACE key pressed?
\r
12717 if (e.keyCode != 8)
\r
12720 var n = ed.selection.getRng().startContainer;
\r
12721 var offset = ed.selection.getRng().startOffset;
\r
12723 while (n && n.nodeType && n.nodeType != 1 && n.parentNode)
\r
12724 n = n.parentNode;
\r
12726 // Is the cursor at the beginning of a blockquote?
\r
12727 if (n && n.parentNode && n.parentNode.tagName === 'BLOCKQUOTE' && n.parentNode.firstChild == n && offset == 0) {
\r
12728 // Remove the blockquote
\r
12729 ed.formatter.toggle('blockquote', null, n.parentNode);
\r
12731 // Move the caret to the beginning of n
\r
12732 var rng = ed.selection.getRng();
\r
12733 rng.setStart(n, 0);
\r
12734 rng.setEnd(n, 0);
\r
12735 ed.selection.setRng(rng);
\r
12736 ed.selection.collapse(false);
\r
12742 // Add reset handler
\r
12743 t.onReset.add(function() {
\r
12744 t.setContent(t.startContent, {format : 'raw'});
\r
12748 if (s.custom_shortcuts) {
\r
12749 if (s.custom_undo_redo_keyboard_shortcuts) {
\r
12750 t.addShortcut('ctrl+z', t.getLang('undo_desc'), 'Undo');
\r
12751 t.addShortcut('ctrl+y', t.getLang('redo_desc'), 'Redo');
\r
12754 // Add default shortcuts for gecko
\r
12755 t.addShortcut('ctrl+b', t.getLang('bold_desc'), 'Bold');
\r
12756 t.addShortcut('ctrl+i', t.getLang('italic_desc'), 'Italic');
\r
12757 t.addShortcut('ctrl+u', t.getLang('underline_desc'), 'Underline');
\r
12759 // BlockFormat shortcuts keys
\r
12760 for (i=1; i<=6; i++)
\r
12761 t.addShortcut('ctrl+' + i, '', ['FormatBlock', false, 'h' + i]);
\r
12763 t.addShortcut('ctrl+7', '', ['FormatBlock', false, 'p']);
\r
12764 t.addShortcut('ctrl+8', '', ['FormatBlock', false, 'div']);
\r
12765 t.addShortcut('ctrl+9', '', ['FormatBlock', false, 'address']);
\r
12767 function find(e) {
\r
12770 if (!e.altKey && !e.ctrlKey && !e.metaKey)
\r
12773 each(t.shortcuts, function(o) {
\r
12774 if (tinymce.isMac && o.ctrl != e.metaKey)
\r
12776 else if (!tinymce.isMac && o.ctrl != e.ctrlKey)
\r
12779 if (o.alt != e.altKey)
\r
12782 if (o.shift != e.shiftKey)
\r
12785 if (e.keyCode == o.keyCode || (e.charCode && e.charCode == o.charCode)) {
\r
12794 t.onKeyUp.add(function(ed, e) {
\r
12798 return Event.cancel(e);
\r
12801 t.onKeyPress.add(function(ed, e) {
\r
12805 return Event.cancel(e);
\r
12808 t.onKeyDown.add(function(ed, e) {
\r
12812 o.func.call(o.scope);
\r
12813 return Event.cancel(e);
\r
12818 if (tinymce.isIE) {
\r
12819 // Fix so resize will only update the width and height attributes not the styles of an image
\r
12820 // It will also block mceItemNoResize items
\r
12821 dom.bind(t.getDoc(), 'controlselect', function(e) {
\r
12822 var re = t.resizeInfo, cb;
\r
12826 // Don't do this action for non image elements
\r
12827 if (e.nodeName !== 'IMG')
\r
12831 dom.unbind(re.node, re.ev, re.cb);
\r
12833 if (!dom.hasClass(e, 'mceItemNoResize')) {
\r
12834 ev = 'resizeend';
\r
12835 cb = dom.bind(e, ev, function(e) {
\r
12840 if (v = dom.getStyle(e, 'width')) {
\r
12841 dom.setAttrib(e, 'width', v.replace(/[^0-9%]+/g, ''));
\r
12842 dom.setStyle(e, 'width', '');
\r
12845 if (v = dom.getStyle(e, 'height')) {
\r
12846 dom.setAttrib(e, 'height', v.replace(/[^0-9%]+/g, ''));
\r
12847 dom.setStyle(e, 'height', '');
\r
12851 ev = 'resizestart';
\r
12852 cb = dom.bind(e, 'resizestart', Event.cancel, Event);
\r
12855 re = t.resizeInfo = {
\r
12863 if (tinymce.isOpera) {
\r
12864 t.onClick.add(function(ed, e) {
\r
12865 Event.prevent(e);
\r
12869 // Add custom undo/redo handlers
\r
12870 if (s.custom_undo_redo) {
\r
12871 function addUndo() {
\r
12872 t.undoManager.typing = false;
\r
12873 t.undoManager.add();
\r
12876 dom.bind(t.getDoc(), 'focusout', function(e) {
\r
12877 if (!t.removed && t.undoManager.typing)
\r
12881 // Add undo level when contents is drag/dropped within the editor
\r
12882 t.dom.bind(t.dom.getRoot(), 'dragend', function(e) {
\r
12886 t.onKeyUp.add(function(ed, e) {
\r
12887 var keyCode = e.keyCode;
\r
12889 if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 13 || keyCode == 45 || e.ctrlKey)
\r
12893 t.onKeyDown.add(function(ed, e) {
\r
12894 var keyCode = e.keyCode, sel;
\r
12896 if (keyCode == 8) {
\r
12897 sel = t.getDoc().selection;
\r
12899 // Fix IE control + backspace browser bug
\r
12900 if (sel && sel.createRange && sel.createRange().item) {
\r
12901 t.undoManager.beforeChange();
\r
12902 ed.dom.remove(sel.createRange().item(0));
\r
12905 return Event.cancel(e);
\r
12909 // Is caracter positon keys left,right,up,down,home,end,pgdown,pgup,enter
\r
12910 if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 13 || keyCode == 45) {
\r
12911 // Add position before enter key is pressed, used by IE since it still uses the default browser behavior
\r
12912 // Todo: Remove this once we normalize enter behavior on IE
\r
12913 if (tinymce.isIE && keyCode == 13)
\r
12914 t.undoManager.beforeChange();
\r
12916 if (t.undoManager.typing)
\r
12922 // If key isn't shift,ctrl,alt,capslock,metakey
\r
12923 if ((keyCode < 16 || keyCode > 20) && keyCode != 224 && keyCode != 91 && !t.undoManager.typing) {
\r
12924 t.undoManager.beforeChange();
\r
12925 t.undoManager.typing = true;
\r
12926 t.undoManager.add();
\r
12930 t.onMouseDown.add(function() {
\r
12931 if (t.undoManager.typing)
\r
12936 // Fire a nodeChanged when the selection is changed on WebKit this fixes selection issues on iOS5
\r
12937 // It only fires the nodeChange event every 50ms since it would other wise update the UI when you type and it hogs the CPU
\r
12938 if (tinymce.isWebKit) {
\r
12939 dom.bind(t.getDoc(), 'selectionchange', function() {
\r
12940 if (t.selectionTimer) {
\r
12941 clearTimeout(t.selectionTimer);
\r
12942 t.selectionTimer = 0;
\r
12945 t.selectionTimer = window.setTimeout(function() {
\r
12951 // Bug fix for FireFox keeping styles from end of selection instead of start.
\r
12952 if (tinymce.isGecko) {
\r
12953 function getAttributeApplyFunction() {
\r
12954 var template = t.dom.getAttribs(t.selection.getStart().cloneNode(false));
\r
12956 return function() {
\r
12957 var target = t.selection.getStart();
\r
12959 if (target !== t.getBody()) {
\r
12960 t.dom.removeAllAttribs(target);
\r
12962 each(template, function(attr) {
\r
12963 target.setAttributeNode(attr.cloneNode(true));
\r
12969 function isSelectionAcrossElements() {
\r
12970 var s = t.selection;
\r
12972 return !s.isCollapsed() && s.getStart() != s.getEnd();
\r
12975 t.onKeyPress.add(function(ed, e) {
\r
12976 var applyAttributes;
\r
12978 if ((e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) {
\r
12979 applyAttributes = getAttributeApplyFunction();
\r
12980 t.getDoc().execCommand('delete', false, null);
\r
12981 applyAttributes();
\r
12983 return Event.cancel(e);
\r
12987 t.dom.bind(t.getDoc(), 'cut', function(e) {
\r
12988 var applyAttributes;
\r
12990 if (isSelectionAcrossElements()) {
\r
12991 applyAttributes = getAttributeApplyFunction();
\r
12992 t.onKeyUp.addToTop(Event.cancel, Event);
\r
12994 setTimeout(function() {
\r
12995 applyAttributes();
\r
12996 t.onKeyUp.remove(Event.cancel, Event);
\r
13003 _refreshContentEditable : function() {
\r
13004 var self = this, body, parent;
\r
13006 // Check if the editor was hidden and the re-initalize contentEditable mode by removing and adding the body again
\r
13007 if (self._isHidden()) {
\r
13008 body = self.getBody();
\r
13009 parent = body.parentNode;
\r
13011 parent.removeChild(body);
\r
13012 parent.appendChild(body);
\r
13018 _isHidden : function() {
\r
13024 // Weird, wheres that cursor selection?
\r
13025 s = this.selection.getSel();
\r
13026 return (!s || !s.rangeCount || s.rangeCount == 0);
\r
13031 (function(tinymce) {
\r
13032 // Added for compression purposes
\r
13033 var each = tinymce.each, undefined, TRUE = true, FALSE = false;
\r
13035 tinymce.EditorCommands = function(editor) {
\r
13036 var dom = editor.dom,
\r
13037 selection = editor.selection,
\r
13038 commands = {state: {}, exec : {}, value : {}},
\r
13039 settings = editor.settings,
\r
13040 formatter = editor.formatter,
\r
13043 function execCommand(command, ui, value) {
\r
13046 command = command.toLowerCase();
\r
13047 if (func = commands.exec[command]) {
\r
13048 func(command, ui, value);
\r
13055 function queryCommandState(command) {
\r
13058 command = command.toLowerCase();
\r
13059 if (func = commands.state[command])
\r
13060 return func(command);
\r
13065 function queryCommandValue(command) {
\r
13068 command = command.toLowerCase();
\r
13069 if (func = commands.value[command])
\r
13070 return func(command);
\r
13075 function addCommands(command_list, type) {
\r
13076 type = type || 'exec';
\r
13078 each(command_list, function(callback, command) {
\r
13079 each(command.toLowerCase().split(','), function(command) {
\r
13080 commands[type][command] = callback;
\r
13085 // Expose public methods
\r
13086 tinymce.extend(this, {
\r
13087 execCommand : execCommand,
\r
13088 queryCommandState : queryCommandState,
\r
13089 queryCommandValue : queryCommandValue,
\r
13090 addCommands : addCommands
\r
13093 // Private methods
\r
13095 function execNativeCommand(command, ui, value) {
\r
13096 if (ui === undefined)
\r
13099 if (value === undefined)
\r
13102 return editor.getDoc().execCommand(command, ui, value);
\r
13105 function isFormatMatch(name) {
\r
13106 return formatter.match(name);
\r
13109 function toggleFormat(name, value) {
\r
13110 formatter.toggle(name, value ? {value : value} : undefined);
\r
13113 function storeSelection(type) {
\r
13114 bookmark = selection.getBookmark(type);
\r
13117 function restoreSelection() {
\r
13118 selection.moveToBookmark(bookmark);
\r
13121 // Add execCommand overrides
\r
13123 // Ignore these, added for compatibility
\r
13124 'mceResetDesignMode,mceBeginUndoLevel' : function() {},
\r
13126 // Add undo manager logic
\r
13127 'mceEndUndoLevel,mceAddUndoLevel' : function() {
\r
13128 editor.undoManager.add();
\r
13131 'Cut,Copy,Paste' : function(command) {
\r
13132 var doc = editor.getDoc(), failed;
\r
13134 // Try executing the native command
\r
13136 execNativeCommand(command);
\r
13138 // Command failed
\r
13142 // Present alert message about clipboard access not being available
\r
13143 if (failed || !doc.queryCommandSupported(command)) {
\r
13144 if (tinymce.isGecko) {
\r
13145 editor.windowManager.confirm(editor.getLang('clipboard_msg'), function(state) {
\r
13147 open('http://www.mozilla.org/editor/midasdemo/securityprefs.html', '_blank');
\r
13150 editor.windowManager.alert(editor.getLang('clipboard_no_support'));
\r
13154 // Override unlink command
\r
13155 unlink : function(command) {
\r
13156 if (selection.isCollapsed())
\r
13157 selection.select(selection.getNode());
\r
13159 execNativeCommand(command);
\r
13160 selection.collapse(FALSE);
\r
13163 // Override justify commands to use the text formatter engine
\r
13164 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) {
\r
13165 var align = command.substring(7);
\r
13167 // Remove all other alignments first
\r
13168 each('left,center,right,full'.split(','), function(name) {
\r
13169 if (align != name)
\r
13170 formatter.remove('align' + name);
\r
13173 toggleFormat('align' + align);
\r
13174 execCommand('mceRepaint');
\r
13177 // Override list commands to fix WebKit bug
\r
13178 'InsertUnorderedList,InsertOrderedList' : function(command) {
\r
13179 var listElm, listParent;
\r
13181 execNativeCommand(command);
\r
13183 // WebKit produces lists within block elements so we need to split them
\r
13184 // we will replace the native list creation logic to custom logic later on
\r
13185 // TODO: Remove this when the list creation logic is removed
\r
13186 listElm = dom.getParent(selection.getNode(), 'ol,ul');
\r
13188 listParent = listElm.parentNode;
\r
13190 // If list is within a text block then split that block
\r
13191 if (/^(H[1-6]|P|ADDRESS|PRE)$/.test(listParent.nodeName)) {
\r
13192 storeSelection();
\r
13193 dom.split(listParent, listElm);
\r
13194 restoreSelection();
\r
13199 // Override commands to use the text formatter engine
\r
13200 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) {
\r
13201 toggleFormat(command);
\r
13204 // Override commands to use the text formatter engine
\r
13205 'ForeColor,HiliteColor,FontName' : function(command, ui, value) {
\r
13206 toggleFormat(command, value);
\r
13209 FontSize : function(command, ui, value) {
\r
13210 var fontClasses, fontSizes;
\r
13212 // Convert font size 1-7 to styles
\r
13213 if (value >= 1 && value <= 7) {
\r
13214 fontSizes = tinymce.explode(settings.font_size_style_values);
\r
13215 fontClasses = tinymce.explode(settings.font_size_classes);
\r
13218 value = fontClasses[value - 1] || value;
\r
13220 value = fontSizes[value - 1] || value;
\r
13223 toggleFormat(command, value);
\r
13226 RemoveFormat : function(command) {
\r
13227 formatter.remove(command);
\r
13230 mceBlockQuote : function(command) {
\r
13231 toggleFormat('blockquote');
\r
13234 FormatBlock : function(command, ui, value) {
\r
13235 return toggleFormat(value || 'p');
\r
13238 mceCleanup : function() {
\r
13239 var bookmark = selection.getBookmark();
\r
13241 editor.setContent(editor.getContent({cleanup : TRUE}), {cleanup : TRUE});
\r
13243 selection.moveToBookmark(bookmark);
\r
13246 mceRemoveNode : function(command, ui, value) {
\r
13247 var node = value || selection.getNode();
\r
13249 // Make sure that the body node isn't removed
\r
13250 if (node != editor.getBody()) {
\r
13251 storeSelection();
\r
13252 editor.dom.remove(node, TRUE);
\r
13253 restoreSelection();
\r
13257 mceSelectNodeDepth : function(command, ui, value) {
\r
13260 dom.getParent(selection.getNode(), function(node) {
\r
13261 if (node.nodeType == 1 && counter++ == value) {
\r
13262 selection.select(node);
\r
13265 }, editor.getBody());
\r
13268 mceSelectNode : function(command, ui, value) {
\r
13269 selection.select(value);
\r
13272 mceInsertContent : function(command, ui, value) {
\r
13273 var parser, serializer, parentNode, rootNode, fragment, args,
\r
13274 marker, nodeRect, viewPortRect, rng, node, node2, bookmarkHtml, viewportBodyElement;
\r
13276 // Setup parser and serializer
\r
13277 parser = editor.parser;
\r
13278 serializer = new tinymce.html.Serializer({}, editor.schema);
\r
13279 bookmarkHtml = '<span id="mce_marker" data-mce-type="bookmark">\uFEFF</span>';
\r
13281 // Run beforeSetContent handlers on the HTML to be inserted
\r
13282 args = {content: value, format: 'html'};
\r
13283 selection.onBeforeSetContent.dispatch(selection, args);
\r
13284 value = args.content;
\r
13286 // Add caret at end of contents if it's missing
\r
13287 if (value.indexOf('{$caret}') == -1)
\r
13288 value += '{$caret}';
\r
13290 // Replace the caret marker with a span bookmark element
\r
13291 value = value.replace(/\{\$caret\}/, bookmarkHtml);
\r
13293 // Insert node maker where we will insert the new HTML and get it's parent
\r
13294 if (!selection.isCollapsed())
\r
13295 editor.getDoc().execCommand('Delete', false, null);
\r
13297 parentNode = selection.getNode();
\r
13299 // Parse the fragment within the context of the parent node
\r
13300 args = {context : parentNode.nodeName.toLowerCase()};
\r
13301 fragment = parser.parse(value, args);
\r
13303 // Move the caret to a more suitable location
\r
13304 node = fragment.lastChild;
\r
13305 if (node.attr('id') == 'mce_marker') {
\r
13308 for (node = node.prev; node; node = node.walk(true)) {
\r
13309 if (node.type == 3 || !dom.isBlock(node.name)) {
\r
13310 node.parent.insert(marker, node, node.name === 'br');
\r
13316 // If parser says valid we can insert the contents into that parent
\r
13317 if (!args.invalid) {
\r
13318 value = serializer.serialize(fragment);
\r
13320 // Check if parent is empty or only has one BR element then set the innerHTML of that parent
\r
13321 node = parentNode.firstChild;
\r
13322 node2 = parentNode.lastChild;
\r
13323 if (!node || (node === node2 && node.nodeName === 'BR'))
\r
13324 dom.setHTML(parentNode, value);
\r
13326 selection.setContent(value);
\r
13328 // If the fragment was invalid within that context then we need
\r
13329 // to parse and process the parent it's inserted into
\r
13331 // Insert bookmark node and get the parent
\r
13332 selection.setContent(bookmarkHtml);
\r
13333 parentNode = editor.selection.getNode();
\r
13334 rootNode = editor.getBody();
\r
13336 // Opera will return the document node when selection is in root
\r
13337 if (parentNode.nodeType == 9)
\r
13338 parentNode = node = rootNode;
\r
13340 node = parentNode;
\r
13342 // Find the ancestor just before the root element
\r
13343 while (node !== rootNode) {
\r
13344 parentNode = node;
\r
13345 node = node.parentNode;
\r
13348 // Get the outer/inner HTML depending on if we are in the root and parser and serialize that
\r
13349 value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode);
\r
13350 value = serializer.serialize(
\r
13352 // Need to replace by using a function since $ in the contents would otherwise be a problem
\r
13353 value.replace(/<span (id="mce_marker"|id=mce_marker).+?<\/span>/i, function() {
\r
13354 return serializer.serialize(fragment);
\r
13359 // Set the inner/outer HTML depending on if we are in the root or not
\r
13360 if (parentNode == rootNode)
\r
13361 dom.setHTML(rootNode, value);
\r
13363 dom.setOuterHTML(parentNode, value);
\r
13366 marker = dom.get('mce_marker');
\r
13368 // Scroll range into view scrollIntoView on element can't be used since it will scroll the main view port as well
\r
13369 nodeRect = dom.getRect(marker);
\r
13370 viewPortRect = dom.getViewPort(editor.getWin());
\r
13372 // Check if node is out side the viewport if it is then scroll to it
\r
13373 if ((nodeRect.y + nodeRect.h > viewPortRect.y + viewPortRect.h || nodeRect.y < viewPortRect.y) ||
\r
13374 (nodeRect.x > viewPortRect.x + viewPortRect.w || nodeRect.x < viewPortRect.x)) {
\r
13375 viewportBodyElement = tinymce.isIE ? editor.getDoc().documentElement : editor.getBody();
\r
13376 viewportBodyElement.scrollLeft = nodeRect.x;
\r
13377 viewportBodyElement.scrollTop = nodeRect.y - viewPortRect.h + 25;
\r
13380 // Move selection before marker and remove it
\r
13381 rng = dom.createRng();
\r
13383 // If previous sibling is a text node set the selection to the end of that node
\r
13384 node = marker.previousSibling;
\r
13385 if (node && node.nodeType == 3) {
\r
13386 rng.setStart(node, node.nodeValue.length);
\r
13388 // If the previous sibling isn't a text node or doesn't exist set the selection before the marker node
\r
13389 rng.setStartBefore(marker);
\r
13390 rng.setEndBefore(marker);
\r
13393 // Remove the marker node and set the new range
\r
13394 dom.remove(marker);
\r
13395 selection.setRng(rng);
\r
13397 // Dispatch after event and add any visual elements needed
\r
13398 selection.onSetContent.dispatch(selection, args);
\r
13399 editor.addVisual();
\r
13402 mceInsertRawHTML : function(command, ui, value) {
\r
13403 selection.setContent('tiny_mce_marker');
\r
13404 editor.setContent(editor.getContent().replace(/tiny_mce_marker/g, function() { return value }));
\r
13407 mceSetContent : function(command, ui, value) {
\r
13408 editor.setContent(value);
\r
13411 'Indent,Outdent' : function(command) {
\r
13412 var intentValue, indentUnit, value;
\r
13414 // Setup indent level
\r
13415 intentValue = settings.indentation;
\r
13416 indentUnit = /[a-z%]+$/i.exec(intentValue);
\r
13417 intentValue = parseInt(intentValue);
\r
13419 if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) {
\r
13420 each(selection.getSelectedBlocks(), function(element) {
\r
13421 if (command == 'outdent') {
\r
13422 value = Math.max(0, parseInt(element.style.paddingLeft || 0) - intentValue);
\r
13423 dom.setStyle(element, 'paddingLeft', value ? value + indentUnit : '');
\r
13425 dom.setStyle(element, 'paddingLeft', (parseInt(element.style.paddingLeft || 0) + intentValue) + indentUnit);
\r
13428 execNativeCommand(command);
\r
13431 mceRepaint : function() {
\r
13434 if (tinymce.isGecko) {
\r
13436 storeSelection(TRUE);
\r
13438 if (selection.getSel())
\r
13439 selection.getSel().selectAllChildren(editor.getBody());
\r
13441 selection.collapse(TRUE);
\r
13442 restoreSelection();
\r
13449 mceToggleFormat : function(command, ui, value) {
\r
13450 formatter.toggle(value);
\r
13453 InsertHorizontalRule : function() {
\r
13454 editor.execCommand('mceInsertContent', false, '<hr />');
\r
13457 mceToggleVisualAid : function() {
\r
13458 editor.hasVisual = !editor.hasVisual;
\r
13459 editor.addVisual();
\r
13462 mceReplaceContent : function(command, ui, value) {
\r
13463 editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({format : 'text'})));
\r
13466 mceInsertLink : function(command, ui, value) {
\r
13469 if (typeof(value) == 'string')
\r
13470 value = {href : value};
\r
13472 anchor = dom.getParent(selection.getNode(), 'a');
\r
13474 // Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here.
\r
13475 value.href = value.href.replace(' ', '%20');
\r
13477 // Remove existing links if there could be child links or that the href isn't specified
\r
13478 if (!anchor || !value.href) {
\r
13479 formatter.remove('link');
\r
13482 // Apply new link to selection
\r
13483 if (value.href) {
\r
13484 formatter.apply('link', value, anchor);
\r
13488 selectAll : function() {
\r
13489 var root = dom.getRoot(), rng = dom.createRng();
\r
13491 rng.setStart(root, 0);
\r
13492 rng.setEnd(root, root.childNodes.length);
\r
13494 editor.selection.setRng(rng);
\r
13498 // Add queryCommandState overrides
\r
13500 // Override justify commands
\r
13501 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) {
\r
13502 return isFormatMatch('align' + command.substring(7));
\r
13505 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) {
\r
13506 return isFormatMatch(command);
\r
13509 mceBlockQuote : function() {
\r
13510 return isFormatMatch('blockquote');
\r
13513 Outdent : function() {
\r
13516 if (settings.inline_styles) {
\r
13517 if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0)
\r
13520 if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0)
\r
13524 return queryCommandState('InsertUnorderedList') || queryCommandState('InsertOrderedList') || (!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE'));
\r
13527 'InsertUnorderedList,InsertOrderedList' : function(command) {
\r
13528 return dom.getParent(selection.getNode(), command == 'insertunorderedlist' ? 'UL' : 'OL');
\r
13532 // Add queryCommandValue overrides
\r
13534 'FontSize,FontName' : function(command) {
\r
13535 var value = 0, parent;
\r
13537 if (parent = dom.getParent(selection.getNode(), 'span')) {
\r
13538 if (command == 'fontsize')
\r
13539 value = parent.style.fontSize;
\r
13541 value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase();
\r
13548 // Add undo manager logic
\r
13549 if (settings.custom_undo_redo) {
\r
13551 Undo : function() {
\r
13552 editor.undoManager.undo();
\r
13555 Redo : function() {
\r
13556 editor.undoManager.redo();
\r
13563 (function(tinymce) {
\r
13564 var Dispatcher = tinymce.util.Dispatcher;
\r
13566 tinymce.UndoManager = function(editor) {
\r
13567 var self, index = 0, data = [], beforeBookmark;
\r
13569 function getContent() {
\r
13570 return tinymce.trim(editor.getContent({format : 'raw', no_events : 1}));
\r
13576 onAdd : new Dispatcher(self),
\r
13578 onUndo : new Dispatcher(self),
\r
13580 onRedo : new Dispatcher(self),
\r
13582 beforeChange : function() {
\r
13583 beforeBookmark = editor.selection.getBookmark(2, true);
\r
13586 add : function(level) {
\r
13587 var i, settings = editor.settings, lastLevel;
\r
13589 level = level || {};
\r
13590 level.content = getContent();
\r
13592 // Add undo level if needed
\r
13593 lastLevel = data[index];
\r
13594 if (lastLevel && lastLevel.content == level.content)
\r
13597 // Set before bookmark on previous level
\r
13599 data[index].beforeBookmark = beforeBookmark;
\r
13601 // Time to compress
\r
13602 if (settings.custom_undo_redo_levels) {
\r
13603 if (data.length > settings.custom_undo_redo_levels) {
\r
13604 for (i = 0; i < data.length - 1; i++)
\r
13605 data[i] = data[i + 1];
\r
13608 index = data.length;
\r
13612 // Get a non intrusive normalized bookmark
\r
13613 level.bookmark = editor.selection.getBookmark(2, true);
\r
13615 // Crop array if needed
\r
13616 if (index < data.length - 1)
\r
13617 data.length = index + 1;
\r
13619 data.push(level);
\r
13620 index = data.length - 1;
\r
13622 self.onAdd.dispatch(self, level);
\r
13623 editor.isNotDirty = 0;
\r
13628 undo : function() {
\r
13631 if (self.typing) {
\r
13633 self.typing = false;
\r
13637 level = data[--index];
\r
13639 editor.setContent(level.content, {format : 'raw'});
\r
13640 editor.selection.moveToBookmark(level.beforeBookmark);
\r
13642 self.onUndo.dispatch(self, level);
\r
13648 redo : function() {
\r
13651 if (index < data.length - 1) {
\r
13652 level = data[++index];
\r
13654 editor.setContent(level.content, {format : 'raw'});
\r
13655 editor.selection.moveToBookmark(level.bookmark);
\r
13657 self.onRedo.dispatch(self, level);
\r
13663 clear : function() {
\r
13666 self.typing = false;
\r
13669 hasUndo : function() {
\r
13670 return index > 0 || this.typing;
\r
13673 hasRedo : function() {
\r
13674 return index < data.length - 1 && !this.typing;
\r
13680 (function(tinymce) {
\r
13682 var Event = tinymce.dom.Event,
\r
13683 isIE = tinymce.isIE,
\r
13684 isGecko = tinymce.isGecko,
\r
13685 isOpera = tinymce.isOpera,
\r
13686 each = tinymce.each,
\r
13687 extend = tinymce.extend,
\r
13691 function cloneFormats(node) {
\r
13692 var clone, temp, inner;
\r
13695 if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U)$/.test(node.nodeName)) {
\r
13697 temp = node.cloneNode(false);
\r
13698 temp.appendChild(clone);
\r
13701 clone = inner = node.cloneNode(false);
\r
13704 clone.removeAttribute('id');
\r
13706 } while (node = node.parentNode);
\r
13709 return {wrapper : clone, inner : inner};
\r
13712 // Checks if the selection/caret is at the end of the specified block element
\r
13713 function isAtEnd(rng, par) {
\r
13714 var rng2 = par.ownerDocument.createRange();
\r
13716 rng2.setStart(rng.endContainer, rng.endOffset);
\r
13717 rng2.setEndAfter(par);
\r
13719 // 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
13720 return rng2.cloneContents().textContent.length == 0;
\r
13723 function splitList(selection, dom, li) {
\r
13724 var listBlock, block;
\r
13726 if (dom.isEmpty(li)) {
\r
13727 listBlock = dom.getParent(li, 'ul,ol');
\r
13729 if (!dom.getParent(listBlock.parentNode, 'ul,ol')) {
\r
13730 dom.split(listBlock, li);
\r
13731 block = dom.create('p', 0, '<br data-mce-bogus="1" />');
\r
13732 dom.replace(block, li);
\r
13733 selection.select(block, 1);
\r
13742 tinymce.create('tinymce.ForceBlocks', {
\r
13743 ForceBlocks : function(ed) {
\r
13744 var t = this, s = ed.settings, elm;
\r
13748 elm = (s.forced_root_block || 'p').toLowerCase();
\r
13749 s.element = elm.toUpperCase();
\r
13751 ed.onPreInit.add(t.setup, t);
\r
13754 setup : function() {
\r
13755 var t = this, ed = t.editor, s = ed.settings, dom = ed.dom, selection = ed.selection, blockElements = ed.schema.getBlockElements();
\r
13757 // Force root blocks
\r
13758 if (s.forced_root_block) {
\r
13759 function addRootBlocks() {
\r
13760 var node = selection.getStart(), rootNode = ed.getBody(), rng, startContainer, startOffset, endContainer, endOffset, rootBlockNode, tempNode, offset = -0xFFFFFF;
\r
13762 if (!node || node.nodeType !== 1)
\r
13765 // Check if node is wrapped in block
\r
13766 while (node != rootNode) {
\r
13767 if (blockElements[node.nodeName])
\r
13770 node = node.parentNode;
\r
13773 // Get current selection
\r
13774 rng = selection.getRng();
\r
13775 if (rng.setStart) {
\r
13776 startContainer = rng.startContainer;
\r
13777 startOffset = rng.startOffset;
\r
13778 endContainer = rng.endContainer;
\r
13779 endOffset = rng.endOffset;
\r
13781 // Force control range into text range
\r
13783 rng = ed.getDoc().body.createTextRange();
\r
13784 rng.moveToElementText(rng.item(0));
\r
13787 tmpRng = rng.duplicate();
\r
13788 tmpRng.collapse(true);
\r
13789 startOffset = tmpRng.move('character', offset) * -1;
\r
13791 if (!tmpRng.collapsed) {
\r
13792 tmpRng = rng.duplicate();
\r
13793 tmpRng.collapse(false);
\r
13794 endOffset = (tmpRng.move('character', offset) * -1) - startOffset;
\r
13798 // Wrap non block elements and text nodes
\r
13799 for (node = rootNode.firstChild; node; node) {
\r
13800 if (node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName])) {
\r
13801 if (!rootBlockNode) {
\r
13802 rootBlockNode = dom.create(s.forced_root_block);
\r
13803 node.parentNode.insertBefore(rootBlockNode, node);
\r
13807 node = node.nextSibling;
\r
13808 rootBlockNode.appendChild(tempNode);
\r
13810 rootBlockNode = null;
\r
13811 node = node.nextSibling;
\r
13815 if (rng.setStart) {
\r
13816 rng.setStart(startContainer, startOffset);
\r
13817 rng.setEnd(endContainer, endOffset);
\r
13818 selection.setRng(rng);
\r
13821 rng = ed.getDoc().body.createTextRange();
\r
13822 rng.moveToElementText(rootNode);
\r
13823 rng.collapse(true);
\r
13824 rng.moveStart('character', startOffset);
\r
13826 if (endOffset > 0)
\r
13827 rng.moveEnd('character', endOffset);
\r
13835 ed.nodeChanged();
\r
13838 ed.onKeyUp.add(addRootBlocks);
\r
13839 ed.onClick.add(addRootBlocks);
\r
13842 if (s.force_br_newlines) {
\r
13843 // Force IE to produce BRs on enter
\r
13845 ed.onKeyPress.add(function(ed, e) {
\r
13848 if (e.keyCode == 13 && selection.getNode().nodeName != 'LI') {
\r
13849 selection.setContent('<br id="__" /> ', {format : 'raw'});
\r
13850 n = dom.get('__');
\r
13851 n.removeAttribute('id');
\r
13852 selection.select(n);
\r
13853 selection.collapse();
\r
13854 return Event.cancel(e);
\r
13860 if (s.force_p_newlines) {
\r
13862 ed.onKeyPress.add(function(ed, e) {
\r
13863 if (e.keyCode == 13 && !e.shiftKey && !t.insertPara(e))
\r
13867 // Ungly hack to for IE to preserve the formatting when you press
\r
13868 // enter at the end of a block element with formatted contents
\r
13869 // This logic overrides the browsers default logic with
\r
13870 // custom logic that enables us to control the output
\r
13871 tinymce.addUnload(function() {
\r
13872 t._previousFormats = 0; // Fix IE leak
\r
13875 ed.onKeyPress.add(function(ed, e) {
\r
13876 t._previousFormats = 0;
\r
13878 // Clone the current formats, this will later be applied to the new block contents
\r
13879 if (e.keyCode == 13 && !e.shiftKey && ed.selection.isCollapsed() && s.keep_styles)
\r
13880 t._previousFormats = cloneFormats(ed.selection.getStart());
\r
13883 ed.onKeyUp.add(function(ed, e) {
\r
13884 // Let IE break the element and the wrap the new caret location in the previous formats
\r
13885 if (e.keyCode == 13 && !e.shiftKey) {
\r
13886 var parent = ed.selection.getStart(), fmt = t._previousFormats;
\r
13888 // Parent is an empty block
\r
13889 if (!parent.hasChildNodes() && fmt) {
\r
13890 parent = dom.getParent(parent, dom.isBlock);
\r
13892 if (parent && parent.nodeName != 'LI') {
\r
13893 parent.innerHTML = '';
\r
13895 if (t._previousFormats) {
\r
13896 parent.appendChild(fmt.wrapper);
\r
13897 fmt.inner.innerHTML = '\uFEFF';
\r
13899 parent.innerHTML = '\uFEFF';
\r
13901 selection.select(parent, 1);
\r
13902 selection.collapse(true);
\r
13903 ed.getDoc().execCommand('Delete', false, null);
\r
13904 t._previousFormats = 0;
\r
13912 ed.onKeyDown.add(function(ed, e) {
\r
13913 if ((e.keyCode == 8 || e.keyCode == 46) && !e.shiftKey)
\r
13914 t.backspaceDelete(e, e.keyCode == 8);
\r
13919 // Workaround for missing shift+enter support, http://bugs.webkit.org/show_bug.cgi?id=16973
\r
13920 if (tinymce.isWebKit) {
\r
13921 function insertBr(ed) {
\r
13922 var rng = selection.getRng(), br, div = dom.create('div', null, ' '), divYPos, vpHeight = dom.getViewPort(ed.getWin()).h;
\r
13924 // Insert BR element
\r
13925 rng.insertNode(br = dom.create('br'));
\r
13927 // Place caret after BR
\r
13928 rng.setStartAfter(br);
\r
13929 rng.setEndAfter(br);
\r
13930 selection.setRng(rng);
\r
13932 // Could not place caret after BR then insert an nbsp entity and move the caret
\r
13933 if (selection.getSel().focusNode == br.previousSibling) {
\r
13934 selection.select(dom.insertAfter(dom.doc.createTextNode('\u00a0'), br));
\r
13935 selection.collapse(TRUE);
\r
13938 // Create a temporary DIV after the BR and get the position as it
\r
13939 // seems like getPos() returns 0 for text nodes and BR elements.
\r
13940 dom.insertAfter(div, br);
\r
13941 divYPos = dom.getPos(div).y;
\r
13944 // Scroll to new position, scrollIntoView can't be used due to bug: http://bugs.webkit.org/show_bug.cgi?id=16117
\r
13945 if (divYPos > vpHeight) // It is not necessary to scroll if the DIV is inside the view port.
\r
13946 ed.getWin().scrollTo(0, divYPos);
\r
13949 ed.onKeyPress.add(function(ed, e) {
\r
13950 if (e.keyCode == 13 && (e.shiftKey || (s.force_br_newlines && !dom.getParent(selection.getNode(), 'h1,h2,h3,h4,h5,h6,ol,ul')))) {
\r
13957 // IE specific fixes
\r
13959 // Replaces IE:s auto generated paragraphs with the specified element name
\r
13960 if (s.element != 'P') {
\r
13961 ed.onKeyPress.add(function(ed, e) {
\r
13962 t.lastElm = selection.getNode().nodeName;
\r
13965 ed.onKeyUp.add(function(ed, e) {
\r
13966 var bl, n = selection.getNode(), b = ed.getBody();
\r
13968 if (b.childNodes.length === 1 && n.nodeName == 'P') {
\r
13969 n = dom.rename(n, s.element);
\r
13970 selection.select(n);
\r
13971 selection.collapse();
\r
13972 ed.nodeChanged();
\r
13973 } else if (e.keyCode == 13 && !e.shiftKey && t.lastElm != 'P') {
\r
13974 bl = dom.getParent(n, 'p');
\r
13977 dom.rename(bl, s.element);
\r
13978 ed.nodeChanged();
\r
13986 getParentBlock : function(n) {
\r
13987 var d = this.dom;
\r
13989 return d.getParent(n, d.isBlock);
\r
13992 insertPara : function(e) {
\r
13993 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
13994 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
13996 ed.undoManager.beforeChange();
\r
13998 // If root blocks are forced then use Operas default behavior since it's really good
\r
13999 // Removed due to bug: #1853816
\r
14000 // if (se.forced_root_block && isOpera)
\r
14003 // Setup before range
\r
14004 rb = d.createRange();
\r
14006 // If is before the first block element and in body, then move it into first block element
\r
14007 rb.setStart(s.anchorNode, s.anchorOffset);
\r
14008 rb.collapse(TRUE);
\r
14010 // Setup after range
\r
14011 ra = d.createRange();
\r
14013 // If is before the first block element and in body, then move it into first block element
\r
14014 ra.setStart(s.focusNode, s.focusOffset);
\r
14015 ra.collapse(TRUE);
\r
14017 // Setup start/end points
\r
14018 dir = rb.compareBoundaryPoints(rb.START_TO_END, ra) < 0;
\r
14019 sn = dir ? s.anchorNode : s.focusNode;
\r
14020 so = dir ? s.anchorOffset : s.focusOffset;
\r
14021 en = dir ? s.focusNode : s.anchorNode;
\r
14022 eo = dir ? s.focusOffset : s.anchorOffset;
\r
14024 // If selection is in empty table cell
\r
14025 if (sn === en && /^(TD|TH)$/.test(sn.nodeName)) {
\r
14026 if (sn.firstChild.nodeName == 'BR')
\r
14027 dom.remove(sn.firstChild); // Remove BR
\r
14029 // Create two new block elements
\r
14030 if (sn.childNodes.length == 0) {
\r
14031 ed.dom.add(sn, se.element, null, '<br />');
\r
14032 aft = ed.dom.add(sn, se.element, null, '<br />');
\r
14034 n = sn.innerHTML;
\r
14035 sn.innerHTML = '';
\r
14036 ed.dom.add(sn, se.element, null, n);
\r
14037 aft = ed.dom.add(sn, se.element, null, '<br />');
\r
14040 // Move caret into the last one
\r
14041 r = d.createRange();
\r
14042 r.selectNodeContents(aft);
\r
14044 ed.selection.setRng(r);
\r
14049 // If the caret is in an invalid location in FF we need to move it into the first block
\r
14050 if (sn == b && en == b && b.firstChild && ed.dom.isBlock(b.firstChild)) {
\r
14051 sn = en = sn.firstChild;
\r
14053 rb = d.createRange();
\r
14054 rb.setStart(sn, 0);
\r
14055 ra = d.createRange();
\r
14056 ra.setStart(en, 0);
\r
14059 // If the body is totally empty add a BR element this might happen on webkit
\r
14060 if (!d.body.hasChildNodes()) {
\r
14061 d.body.appendChild(dom.create('br'));
\r
14064 // Never use body as start or end node
\r
14065 sn = sn.nodeName == "HTML" ? d.body : sn; // Fix for Opera bug: https://bugs.opera.com/show_bug.cgi?id=273224&comments=yes
\r
14066 sn = sn.nodeName == "BODY" ? sn.firstChild : sn;
\r
14067 en = en.nodeName == "HTML" ? d.body : en; // Fix for Opera bug: https://bugs.opera.com/show_bug.cgi?id=273224&comments=yes
\r
14068 en = en.nodeName == "BODY" ? en.firstChild : en;
\r
14070 // Get start and end blocks
\r
14071 sb = t.getParentBlock(sn);
\r
14072 eb = t.getParentBlock(en);
\r
14073 bn = sb ? sb.nodeName : se.element; // Get block name to create
\r
14075 // Return inside list use default browser behavior
\r
14076 if (n = t.dom.getParent(sb, 'li,pre')) {
\r
14077 if (n.nodeName == 'LI')
\r
14078 return splitList(ed.selection, t.dom, n);
\r
14083 // If caption or absolute layers then always generate new blocks within
\r
14084 if (sb && (sb.nodeName == 'CAPTION' || /absolute|relative|fixed/gi.test(dom.getStyle(sb, 'position', 1)))) {
\r
14089 // If caption or absolute layers then always generate new blocks within
\r
14090 if (eb && (eb.nodeName == 'CAPTION' || /absolute|relative|fixed/gi.test(dom.getStyle(sb, 'position', 1)))) {
\r
14096 if (/(TD|TABLE|TH|CAPTION)/.test(bn) || (sb && bn == "DIV" && /left|right/gi.test(dom.getStyle(sb, 'float', 1)))) {
\r
14101 // Setup new before and after blocks
\r
14102 bef = (sb && sb.nodeName == bn) ? sb.cloneNode(0) : ed.dom.create(bn);
\r
14103 aft = (eb && eb.nodeName == bn) ? eb.cloneNode(0) : ed.dom.create(bn);
\r
14105 // Remove id from after clone
\r
14106 aft.removeAttribute('id');
\r
14108 // Is header and cursor is at the end, then force paragraph under
\r
14109 if (/^(H[1-6])$/.test(bn) && isAtEnd(r, sb))
\r
14110 aft = ed.dom.create(se.element);
\r
14112 // Find start chop node
\r
14115 if (n == b || n.nodeType == 9 || t.dom.isBlock(n) || /(TD|TABLE|TH|CAPTION)/.test(n.nodeName))
\r
14119 } while ((n = n.previousSibling ? n.previousSibling : n.parentNode));
\r
14121 // Find end chop node
\r
14124 if (n == b || n.nodeType == 9 || t.dom.isBlock(n) || /(TD|TABLE|TH|CAPTION)/.test(n.nodeName))
\r
14128 } while ((n = n.nextSibling ? n.nextSibling : n.parentNode));
\r
14130 // Place first chop part into before block element
\r
14131 if (sc.nodeName == bn)
\r
14132 rb.setStart(sc, 0);
\r
14134 rb.setStartBefore(sc);
\r
14136 rb.setEnd(sn, so);
\r
14137 bef.appendChild(rb.cloneContents() || d.createTextNode('')); // Empty text node needed for Safari
\r
14139 // Place secnd chop part within new block element
\r
14141 ra.setEndAfter(ec);
\r
14143 //console.debug(s.focusNode, s.focusOffset);
\r
14146 ra.setStart(en, eo);
\r
14147 aft.appendChild(ra.cloneContents() || d.createTextNode('')); // Empty text node needed for Safari
\r
14149 // Create range around everything
\r
14150 r = d.createRange();
\r
14151 if (!sc.previousSibling && sc.parentNode.nodeName == bn) {
\r
14152 r.setStartBefore(sc.parentNode);
\r
14154 if (rb.startContainer.nodeName == bn && rb.startOffset == 0)
\r
14155 r.setStartBefore(rb.startContainer);
\r
14157 r.setStart(rb.startContainer, rb.startOffset);
\r
14160 if (!ec.nextSibling && ec.parentNode.nodeName == bn)
\r
14161 r.setEndAfter(ec.parentNode);
\r
14163 r.setEnd(ra.endContainer, ra.endOffset);
\r
14165 // Delete and replace it with new block elements
\r
14166 r.deleteContents();
\r
14169 ed.getWin().scrollTo(0, vp.y);
\r
14171 // Never wrap blocks in blocks
\r
14172 if (bef.firstChild && bef.firstChild.nodeName == bn)
\r
14173 bef.innerHTML = bef.firstChild.innerHTML;
\r
14175 if (aft.firstChild && aft.firstChild.nodeName == bn)
\r
14176 aft.innerHTML = aft.firstChild.innerHTML;
\r
14178 function appendStyles(e, en) {
\r
14179 var nl = [], nn, n, i;
\r
14181 e.innerHTML = '';
\r
14183 // Make clones of style elements
\r
14184 if (se.keep_styles) {
\r
14187 // We only want style specific elements
\r
14188 if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U)$/.test(n.nodeName)) {
\r
14189 nn = n.cloneNode(FALSE);
\r
14190 dom.setAttrib(nn, 'id', ''); // Remove ID since it needs to be unique
\r
14193 } while (n = n.parentNode);
\r
14196 // Append style elements to aft
\r
14197 if (nl.length > 0) {
\r
14198 for (i = nl.length - 1, nn = e; i >= 0; i--)
\r
14199 nn = nn.appendChild(nl[i]);
\r
14201 // Padd most inner style element
\r
14202 nl[0].innerHTML = isOpera ? '\u00a0' : '<br />'; // Extra space for Opera so that the caret can move there
\r
14203 return nl[0]; // Move caret to most inner element
\r
14205 e.innerHTML = isOpera ? '\u00a0' : '<br />'; // Extra space for Opera so that the caret can move there
\r
14208 // Padd empty blocks
\r
14209 if (dom.isEmpty(bef))
\r
14210 appendStyles(bef, sn);
\r
14212 // Fill empty afterblook with current style
\r
14213 if (dom.isEmpty(aft))
\r
14214 car = appendStyles(aft, en);
\r
14216 // Opera needs this one backwards for older versions
\r
14217 if (isOpera && parseFloat(opera.version()) < 9.5) {
\r
14218 r.insertNode(bef);
\r
14219 r.insertNode(aft);
\r
14221 r.insertNode(aft);
\r
14222 r.insertNode(bef);
\r
14229 // Move cursor and scroll into view
\r
14230 ed.selection.select(aft, true);
\r
14231 ed.selection.collapse(true);
\r
14233 // 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
14234 y = ed.dom.getPos(aft).y;
\r
14235 //ch = aft.clientHeight;
\r
14237 // Is element within viewport
\r
14238 if (y < vp.y || y + 25 > vp.y + vp.h) {
\r
14239 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
14242 'Element: y=' + y + ', h=' + ch + ', ' +
\r
14243 'Viewport: y=' + vp.y + ", h=" + vp.h + ', bottom=' + (vp.y + vp.h)
\r
14247 ed.undoManager.add();
\r
14252 backspaceDelete : function(e, bs) {
\r
14253 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
14255 // Delete when caret is behind a element doesn't work correctly on Gecko see #3011651
\r
14256 if (!bs && r.collapsed && sc.nodeType == 1 && r.startOffset == sc.childNodes.length) {
\r
14257 walker = new tinymce.dom.TreeWalker(sc.lastChild, sc);
\r
14259 // Walk the dom backwards until we find a text node
\r
14260 for (n = sc.lastChild; n; n = walker.prev()) {
\r
14261 if (n.nodeType == 3) {
\r
14262 r.setStart(n, n.nodeValue.length);
\r
14263 r.collapse(true);
\r
14270 // The caret sometimes gets stuck in Gecko if you delete empty paragraphs
\r
14271 // This workaround removes the element by hand and moves the caret to the previous element
\r
14272 if (sc && ed.dom.isBlock(sc) && !/^(TD|TH)$/.test(sc.nodeName) && bs) {
\r
14273 if (sc.childNodes.length == 0 || (sc.childNodes.length == 1 && sc.firstChild.nodeName == 'BR')) {
\r
14274 // Find previous block element
\r
14276 while ((n = n.previousSibling) && !ed.dom.isBlock(n)) ;
\r
14279 if (sc != b.firstChild) {
\r
14280 // Find last text node
\r
14281 w = ed.dom.doc.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, FALSE);
\r
14282 while (tn = w.nextNode())
\r
14285 // Place caret at the end of last text node
\r
14286 r = ed.getDoc().createRange();
\r
14287 r.setStart(n, n.nodeValue ? n.nodeValue.length : 0);
\r
14288 r.setEnd(n, n.nodeValue ? n.nodeValue.length : 0);
\r
14291 // Remove the target container
\r
14292 ed.dom.remove(sc);
\r
14295 return Event.cancel(e);
\r
14303 (function(tinymce) {
\r
14305 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, extend = tinymce.extend;
\r
14307 tinymce.create('tinymce.ControlManager', {
\r
14308 ControlManager : function(ed, s) {
\r
14314 t.onAdd = new tinymce.util.Dispatcher(t);
\r
14315 t.onPostRender = new tinymce.util.Dispatcher(t);
\r
14316 t.prefix = s.prefix || ed.id + '_';
\r
14319 t.onPostRender.add(function() {
\r
14320 each(t.controls, function(c) {
\r
14326 get : function(id) {
\r
14327 return this.controls[this.prefix + id] || this.controls[id];
\r
14330 setActive : function(id, s) {
\r
14333 if (c = this.get(id))
\r
14339 setDisabled : function(id, s) {
\r
14342 if (c = this.get(id))
\r
14343 c.setDisabled(s);
\r
14348 add : function(c) {
\r
14352 t.controls[c.id] = c;
\r
14353 t.onAdd.dispatch(c, t);
\r
14359 createControl : function(n) {
\r
14360 var c, t = this, ed = t.editor;
\r
14362 each(ed.plugins, function(p) {
\r
14363 if (p.createControl) {
\r
14364 c = p.createControl(n, t);
\r
14373 case "separator":
\r
14374 return t.createSeparator();
\r
14377 if (!c && ed.buttons && (c = ed.buttons[n]))
\r
14378 return t.createButton(n, c);
\r
14383 createDropMenu : function(id, s, cc) {
\r
14384 var t = this, ed = t.editor, c, bm, v, cls;
\r
14387 'class' : 'mceDropDown',
\r
14388 constrain : ed.settings.constrain_menus
\r
14391 s['class'] = s['class'] + ' ' + ed.getParam('skin') + 'Skin';
\r
14392 if (v = ed.getParam('skin_variant'))
\r
14393 s['class'] += ' ' + ed.getParam('skin') + 'Skin' + v.substring(0, 1).toUpperCase() + v.substring(1);
\r
14395 id = t.prefix + id;
\r
14396 cls = cc || t._cls.dropmenu || tinymce.ui.DropMenu;
\r
14397 c = t.controls[id] = new cls(id, s);
\r
14398 c.onAddItem.add(function(c, o) {
\r
14399 var s = o.settings;
\r
14401 s.title = ed.getLang(s.title, s.title);
\r
14403 if (!s.onclick) {
\r
14404 s.onclick = function(v) {
\r
14406 ed.execCommand(s.cmd, s.ui || false, s.value);
\r
14411 ed.onRemove.add(function() {
\r
14415 // Fix for bug #1897785, #1898007
\r
14416 if (tinymce.isIE) {
\r
14417 c.onShowMenu.add(function() {
\r
14418 // IE 8 needs focus in order to store away a range with the current collapsed caret location
\r
14421 bm = ed.selection.getBookmark(1);
\r
14424 c.onHideMenu.add(function() {
\r
14426 ed.selection.moveToBookmark(bm);
\r
14435 createListBox : function(id, s, cc) {
\r
14436 var t = this, ed = t.editor, cmd, c, cls;
\r
14441 s.title = ed.translate(s.title);
\r
14442 s.scope = s.scope || ed;
\r
14444 if (!s.onselect) {
\r
14445 s.onselect = function(v) {
\r
14446 ed.execCommand(s.cmd, s.ui || false, v || s.value);
\r
14452 'class' : 'mce_' + id,
\r
14454 control_manager : t
\r
14457 id = t.prefix + id;
\r
14459 if (ed.settings.use_native_selects)
\r
14460 c = new tinymce.ui.NativeListBox(id, s);
\r
14462 cls = cc || t._cls.listbox || tinymce.ui.ListBox;
\r
14463 c = new cls(id, s, ed);
\r
14466 t.controls[id] = c;
\r
14468 // Fix focus problem in Safari
\r
14469 if (tinymce.isWebKit) {
\r
14470 c.onPostRender.add(function(c, n) {
\r
14471 // Store bookmark on mousedown
\r
14472 Event.add(n, 'mousedown', function() {
\r
14473 ed.bookmark = ed.selection.getBookmark(1);
\r
14476 // Restore on focus, since it might be lost
\r
14477 Event.add(n, 'focus', function() {
\r
14478 ed.selection.moveToBookmark(ed.bookmark);
\r
14479 ed.bookmark = null;
\r
14485 ed.onMouseDown.add(c.hideMenu, c);
\r
14490 createButton : function(id, s, cc) {
\r
14491 var t = this, ed = t.editor, o, c, cls;
\r
14496 s.title = ed.translate(s.title);
\r
14497 s.label = ed.translate(s.label);
\r
14498 s.scope = s.scope || ed;
\r
14500 if (!s.onclick && !s.menu_button) {
\r
14501 s.onclick = function() {
\r
14502 ed.execCommand(s.cmd, s.ui || false, s.value);
\r
14508 'class' : 'mce_' + id,
\r
14509 unavailable_prefix : ed.getLang('unavailable', ''),
\r
14511 control_manager : t
\r
14514 id = t.prefix + id;
\r
14516 if (s.menu_button) {
\r
14517 cls = cc || t._cls.menubutton || tinymce.ui.MenuButton;
\r
14518 c = new cls(id, s, ed);
\r
14519 ed.onMouseDown.add(c.hideMenu, c);
\r
14521 cls = t._cls.button || tinymce.ui.Button;
\r
14522 c = new cls(id, s, ed);
\r
14528 createMenuButton : function(id, s, cc) {
\r
14530 s.menu_button = 1;
\r
14532 return this.createButton(id, s, cc);
\r
14535 createSplitButton : function(id, s, cc) {
\r
14536 var t = this, ed = t.editor, cmd, c, cls;
\r
14541 s.title = ed.translate(s.title);
\r
14542 s.scope = s.scope || ed;
\r
14544 if (!s.onclick) {
\r
14545 s.onclick = function(v) {
\r
14546 ed.execCommand(s.cmd, s.ui || false, v || s.value);
\r
14550 if (!s.onselect) {
\r
14551 s.onselect = function(v) {
\r
14552 ed.execCommand(s.cmd, s.ui || false, v || s.value);
\r
14558 'class' : 'mce_' + id,
\r
14560 control_manager : t
\r
14563 id = t.prefix + id;
\r
14564 cls = cc || t._cls.splitbutton || tinymce.ui.SplitButton;
\r
14565 c = t.add(new cls(id, s, ed));
\r
14566 ed.onMouseDown.add(c.hideMenu, c);
\r
14571 createColorSplitButton : function(id, s, cc) {
\r
14572 var t = this, ed = t.editor, cmd, c, cls, bm;
\r
14577 s.title = ed.translate(s.title);
\r
14578 s.scope = s.scope || ed;
\r
14580 if (!s.onclick) {
\r
14581 s.onclick = function(v) {
\r
14582 if (tinymce.isIE)
\r
14583 bm = ed.selection.getBookmark(1);
\r
14585 ed.execCommand(s.cmd, s.ui || false, v || s.value);
\r
14589 if (!s.onselect) {
\r
14590 s.onselect = function(v) {
\r
14591 ed.execCommand(s.cmd, s.ui || false, v || s.value);
\r
14597 'class' : 'mce_' + id,
\r
14598 'menu_class' : ed.getParam('skin') + 'Skin',
\r
14600 more_colors_title : ed.getLang('more_colors')
\r
14603 id = t.prefix + id;
\r
14604 cls = cc || t._cls.colorsplitbutton || tinymce.ui.ColorSplitButton;
\r
14605 c = new cls(id, s, ed);
\r
14606 ed.onMouseDown.add(c.hideMenu, c);
\r
14608 // Remove the menu element when the editor is removed
\r
14609 ed.onRemove.add(function() {
\r
14613 // Fix for bug #1897785, #1898007
\r
14614 if (tinymce.isIE) {
\r
14615 c.onShowMenu.add(function() {
\r
14616 // IE 8 needs focus in order to store away a range with the current collapsed caret location
\r
14618 bm = ed.selection.getBookmark(1);
\r
14621 c.onHideMenu.add(function() {
\r
14623 ed.selection.moveToBookmark(bm);
\r
14632 createToolbar : function(id, s, cc) {
\r
14633 var c, t = this, cls;
\r
14635 id = t.prefix + id;
\r
14636 cls = cc || t._cls.toolbar || tinymce.ui.Toolbar;
\r
14637 c = new cls(id, s, t.editor);
\r
14645 createToolbarGroup : function(id, s, cc) {
\r
14646 var c, t = this, cls;
\r
14647 id = t.prefix + id;
\r
14648 cls = cc || this._cls.toolbarGroup || tinymce.ui.ToolbarGroup;
\r
14649 c = new cls(id, s, t.editor);
\r
14657 createSeparator : function(cc) {
\r
14658 var cls = cc || this._cls.separator || tinymce.ui.Separator;
\r
14660 return new cls();
\r
14663 setControlType : function(n, c) {
\r
14664 return this._cls[n.toLowerCase()] = c;
\r
14667 destroy : function() {
\r
14668 each(this.controls, function(c) {
\r
14672 this.controls = null;
\r
14677 (function(tinymce) {
\r
14678 var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each, isIE = tinymce.isIE, isOpera = tinymce.isOpera;
\r
14680 tinymce.create('tinymce.WindowManager', {
\r
14681 WindowManager : function(ed) {
\r
14685 t.onOpen = new Dispatcher(t);
\r
14686 t.onClose = new Dispatcher(t);
\r
14691 open : function(s, p) {
\r
14692 var t = this, f = '', x, y, mo = t.editor.settings.dialog_type == 'modal', w, sw, sh, vp = tinymce.DOM.getViewPort(), u;
\r
14694 // Default some options
\r
14697 sw = isOpera ? vp.w : screen.width; // Opera uses windows inside the Opera window
\r
14698 sh = isOpera ? vp.h : screen.height;
\r
14699 s.name = s.name || 'mc_' + new Date().getTime();
\r
14700 s.width = parseInt(s.width || 320);
\r
14701 s.height = parseInt(s.height || 240);
\r
14702 s.resizable = true;
\r
14703 s.left = s.left || parseInt(sw / 2.0) - (s.width / 2.0);
\r
14704 s.top = s.top || parseInt(sh / 2.0) - (s.height / 2.0);
\r
14705 p.inline = false;
\r
14706 p.mce_width = s.width;
\r
14707 p.mce_height = s.height;
\r
14708 p.mce_auto_focus = s.auto_focus;
\r
14714 s.dialogWidth = s.width + 'px';
\r
14715 s.dialogHeight = s.height + 'px';
\r
14716 s.scroll = s.scrollbars || false;
\r
14720 // Build features string
\r
14721 each(s, function(v, k) {
\r
14722 if (tinymce.is(v, 'boolean'))
\r
14723 v = v ? 'yes' : 'no';
\r
14725 if (!/^(name|url)$/.test(k)) {
\r
14727 f += (f ? ';' : '') + k + ':' + v;
\r
14729 f += (f ? ',' : '') + k + '=' + v;
\r
14735 t.onOpen.dispatch(t, s, p);
\r
14737 u = s.url || s.file;
\r
14738 u = tinymce._addVer(u);
\r
14741 if (isIE && mo) {
\r
14743 window.showModalDialog(u, window, f);
\r
14745 w = window.open(u, s.name, f);
\r
14751 alert(t.editor.getLang('popup_blocked'));
\r
14754 close : function(w) {
\r
14756 this.onClose.dispatch(this);
\r
14759 createInstance : function(cl, a, b, c, d, e) {
\r
14760 var f = tinymce.resolve(cl);
\r
14762 return new f(a, b, c, d, e);
\r
14765 confirm : function(t, cb, s, w) {
\r
14768 cb.call(s || this, w.confirm(this._decode(this.editor.getLang(t, t))));
\r
14771 alert : function(tx, cb, s, w) {
\r
14775 w.alert(t._decode(t.editor.getLang(tx, tx)));
\r
14781 resizeBy : function(dw, dh, win) {
\r
14782 win.resizeBy(dw, dh);
\r
14785 // Internal functions
\r
14787 _decode : function(s) {
\r
14788 return tinymce.DOM.decode(s).replace(/\\n/g, '\n');
\r
14792 (function(tinymce) {
\r
14793 tinymce.Formatter = function(ed) {
\r
14794 var formats = {},
\r
14795 each = tinymce.each,
\r
14797 selection = ed.selection,
\r
14798 TreeWalker = tinymce.dom.TreeWalker,
\r
14799 rangeUtils = new tinymce.dom.RangeUtils(dom),
\r
14800 isValid = ed.schema.isValidChild,
\r
14801 isBlock = dom.isBlock,
\r
14802 forcedRootBlock = ed.settings.forced_root_block,
\r
14803 nodeIndex = dom.nodeIndex,
\r
14804 INVISIBLE_CHAR = '\uFEFF',
\r
14805 MCE_ATTR_RE = /^(src|href|style)$/,
\r
14809 pendingFormats = {apply : [], remove : []};
\r
14811 function isArray(obj) {
\r
14812 return obj instanceof Array;
\r
14815 function getParents(node, selector) {
\r
14816 return dom.getParents(node, selector, dom.getRoot());
\r
14819 function isCaretNode(node) {
\r
14820 return node.nodeType === 1 && (node.face === 'mceinline' || node.style.fontFamily === 'mceinline');
\r
14823 // Public functions
\r
14825 function get(name) {
\r
14826 return name ? formats[name] : formats;
\r
14829 function register(name, format) {
\r
14831 if (typeof(name) !== 'string') {
\r
14832 each(name, function(format, name) {
\r
14833 register(name, format);
\r
14836 // Force format into array and add it to internal collection
\r
14837 format = format.length ? format : [format];
\r
14839 each(format, function(format) {
\r
14840 // Set deep to false by default on selector formats this to avoid removing
\r
14841 // alignment on images inside paragraphs when alignment is changed on paragraphs
\r
14842 if (format.deep === undefined)
\r
14843 format.deep = !format.selector;
\r
14845 // Default to true
\r
14846 if (format.split === undefined)
\r
14847 format.split = !format.selector || format.inline;
\r
14849 // Default to true
\r
14850 if (format.remove === undefined && format.selector && !format.inline)
\r
14851 format.remove = 'none';
\r
14853 // Mark format as a mixed format inline + block level
\r
14854 if (format.selector && format.inline) {
\r
14855 format.mixed = true;
\r
14856 format.block_expand = true;
\r
14859 // Split classes if needed
\r
14860 if (typeof(format.classes) === 'string')
\r
14861 format.classes = format.classes.split(/\s+/);
\r
14864 formats[name] = format;
\r
14869 var getTextDecoration = function(node) {
\r
14872 ed.dom.getParent(node, function(n) {
\r
14873 decoration = ed.dom.getStyle(n, 'text-decoration');
\r
14874 return decoration && decoration !== 'none';
\r
14877 return decoration;
\r
14880 var processUnderlineAndColor = function(node) {
\r
14881 var textDecoration;
\r
14882 if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) {
\r
14883 textDecoration = getTextDecoration(node.parentNode);
\r
14884 if (ed.dom.getStyle(node, 'color') && textDecoration) {
\r
14885 ed.dom.setStyle(node, 'text-decoration', textDecoration);
\r
14886 } else if (ed.dom.getStyle(node, 'textdecoration') === textDecoration) {
\r
14887 ed.dom.setStyle(node, 'text-decoration', null);
\r
14892 function apply(name, vars, node) {
\r
14893 var formatList = get(name), format = formatList[0], bookmark, rng, i, isCollapsed = selection.isCollapsed();
\r
14895 function moveStart(rng) {
\r
14896 var container = rng.startContainer,
\r
14897 offset = rng.startOffset,
\r
14900 // Move startContainer/startOffset in to a suitable node
\r
14901 if (container.nodeType == 1 || container.nodeValue === "") {
\r
14902 container = container.nodeType == 1 ? container.childNodes[offset] : container;
\r
14904 // Might fail if the offset is behind the last element in it's container
\r
14906 walker = new TreeWalker(container, container.parentNode);
\r
14907 for (node = walker.current(); node; node = walker.next()) {
\r
14908 if (node.nodeType == 3 && !isWhiteSpaceNode(node)) {
\r
14909 rng.setStart(node, 0);
\r
14919 function setElementFormat(elm, fmt) {
\r
14920 fmt = fmt || format;
\r
14923 if (fmt.onformat) {
\r
14924 fmt.onformat(elm, fmt, vars, node);
\r
14927 each(fmt.styles, function(value, name) {
\r
14928 dom.setStyle(elm, name, replaceVars(value, vars));
\r
14931 each(fmt.attributes, function(value, name) {
\r
14932 dom.setAttrib(elm, name, replaceVars(value, vars));
\r
14935 each(fmt.classes, function(value) {
\r
14936 value = replaceVars(value, vars);
\r
14938 if (!dom.hasClass(elm, value))
\r
14939 dom.addClass(elm, value);
\r
14943 function adjustSelectionToVisibleSelection() {
\r
14944 function findSelectionEnd(start, end) {
\r
14945 var walker = new TreeWalker(end);
\r
14946 for (node = walker.current(); node; node = walker.prev()) {
\r
14947 if (node.childNodes.length > 1 || node == start) {
\r
14953 // Adjust selection so that a end container with a end offset of zero is not included in the selection
\r
14954 // as this isn't visible to the user.
\r
14955 var rng = ed.selection.getRng();
\r
14956 var start = rng.startContainer;
\r
14957 var end = rng.endContainer;
\r
14959 if (start != end && rng.endOffset == 0) {
\r
14960 var newEnd = findSelectionEnd(start, end);
\r
14961 var endOffset = newEnd.nodeType == 3 ? newEnd.length : newEnd.childNodes.length;
\r
14963 rng.setEnd(newEnd, endOffset);
\r
14969 function applyStyleToList(node, bookmark, wrapElm, newWrappers, process){
\r
14970 var nodes = [], listIndex = -1, list, startIndex = -1, endIndex = -1, currentWrapElm;
\r
14972 // find the index of the first child list.
\r
14973 each(node.childNodes, function(n, index) {
\r
14974 if (n.nodeName === "UL" || n.nodeName === "OL") {
\r
14975 listIndex = index;
\r
14981 // get the index of the bookmarks
\r
14982 each(node.childNodes, function(n, index) {
\r
14983 if (n.nodeName === "SPAN" && dom.getAttrib(n, "data-mce-type") == "bookmark") {
\r
14984 if (n.id == bookmark.id + "_start") {
\r
14985 startIndex = index;
\r
14986 } else if (n.id == bookmark.id + "_end") {
\r
14987 endIndex = index;
\r
14992 // if the selection spans across an embedded list, or there isn't an embedded list - handle processing normally
\r
14993 if (listIndex <= 0 || (startIndex < listIndex && endIndex > listIndex)) {
\r
14994 each(tinymce.grep(node.childNodes), process);
\r
14997 currentWrapElm = wrapElm.cloneNode(FALSE);
\r
14999 // create a list of the nodes on the same side of the list as the selection
\r
15000 each(tinymce.grep(node.childNodes), function(n, index) {
\r
15001 if ((startIndex < listIndex && index < listIndex) || (startIndex > listIndex && index > listIndex)) {
\r
15003 n.parentNode.removeChild(n);
\r
15007 // insert the wrapping element either before or after the list.
\r
15008 if (startIndex < listIndex) {
\r
15009 node.insertBefore(currentWrapElm, list);
\r
15010 } else if (startIndex > listIndex) {
\r
15011 node.insertBefore(currentWrapElm, list.nextSibling);
\r
15014 // add the new nodes to the list.
\r
15015 newWrappers.push(currentWrapElm);
\r
15017 each(nodes, function(node) {
\r
15018 currentWrapElm.appendChild(node);
\r
15021 return currentWrapElm;
\r
15025 function applyRngStyle(rng, bookmark) {
\r
15026 var newWrappers = [], wrapName, wrapElm;
\r
15028 // Setup wrapper element
\r
15029 wrapName = format.inline || format.block;
\r
15030 wrapElm = dom.create(wrapName);
\r
15031 setElementFormat(wrapElm);
\r
15033 rangeUtils.walk(rng, function(nodes) {
\r
15034 var currentWrapElm;
\r
15036 function process(node) {
\r
15037 var nodeName = node.nodeName.toLowerCase(), parentName = node.parentNode.nodeName.toLowerCase(), found;
\r
15039 // Stop wrapping on br elements
\r
15040 if (isEq(nodeName, 'br')) {
\r
15041 currentWrapElm = 0;
\r
15043 // Remove any br elements when we wrap things
\r
15044 if (format.block)
\r
15045 dom.remove(node);
\r
15050 // If node is wrapper type
\r
15051 if (format.wrapper && matchNode(node, name, vars)) {
\r
15052 currentWrapElm = 0;
\r
15056 // Can we rename the block
\r
15057 if (format.block && !format.wrapper && isTextBlock(nodeName)) {
\r
15058 node = dom.rename(node, wrapName);
\r
15059 setElementFormat(node);
\r
15060 newWrappers.push(node);
\r
15061 currentWrapElm = 0;
\r
15065 // Handle selector patterns
\r
15066 if (format.selector) {
\r
15067 // Look for matching formats
\r
15068 each(formatList, function(format) {
\r
15069 // Check collapsed state if it exists
\r
15070 if ('collapsed' in format && format.collapsed !== isCollapsed) {
\r
15074 if (dom.is(node, format.selector) && !isCaretNode(node)) {
\r
15075 setElementFormat(node, format);
\r
15080 // Continue processing if a selector match wasn't found and a inline element is defined
\r
15081 if (!format.inline || found) {
\r
15082 currentWrapElm = 0;
\r
15087 // Is it valid to wrap this item
\r
15088 if (isValid(wrapName, nodeName) && isValid(parentName, wrapName) &&
\r
15089 !(node.nodeType === 3 && node.nodeValue.length === 1 && node.nodeValue.charCodeAt(0) === 65279)) {
\r
15090 // Start wrapping
\r
15091 if (!currentWrapElm) {
\r
15093 currentWrapElm = wrapElm.cloneNode(FALSE);
\r
15094 node.parentNode.insertBefore(currentWrapElm, node);
\r
15095 newWrappers.push(currentWrapElm);
\r
15098 currentWrapElm.appendChild(node);
\r
15099 } else if (nodeName == 'li' && bookmark) {
\r
15100 // 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
15101 currentWrapElm = applyStyleToList(node, bookmark, wrapElm, newWrappers, process);
\r
15103 // Start a new wrapper for possible children
\r
15104 currentWrapElm = 0;
\r
15106 each(tinymce.grep(node.childNodes), process);
\r
15108 // End the last wrapper
\r
15109 currentWrapElm = 0;
\r
15113 // Process siblings from range
\r
15114 each(nodes, process);
\r
15117 // Wrap links inside as well, for example color inside a link when the wrapper is around the link
\r
15118 if (format.wrap_links === false) {
\r
15119 each(newWrappers, function(node) {
\r
15120 function process(node) {
\r
15121 var i, currentWrapElm, children;
\r
15123 if (node.nodeName === 'A') {
\r
15124 currentWrapElm = wrapElm.cloneNode(FALSE);
\r
15125 newWrappers.push(currentWrapElm);
\r
15127 children = tinymce.grep(node.childNodes);
\r
15128 for (i = 0; i < children.length; i++)
\r
15129 currentWrapElm.appendChild(children[i]);
\r
15131 node.appendChild(currentWrapElm);
\r
15134 each(tinymce.grep(node.childNodes), process);
\r
15142 each(newWrappers, function(node) {
\r
15145 function getChildCount(node) {
\r
15148 each(node.childNodes, function(node) {
\r
15149 if (!isWhiteSpaceNode(node) && !isBookmarkNode(node))
\r
15156 function mergeStyles(node) {
\r
15157 var child, clone;
\r
15159 each(node.childNodes, function(node) {
\r
15160 if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) {
\r
15162 return FALSE; // break loop
\r
15166 // If child was found and of the same type as the current node
\r
15167 if (child && matchName(child, format)) {
\r
15168 clone = child.cloneNode(FALSE);
\r
15169 setElementFormat(clone);
\r
15171 dom.replace(clone, node, TRUE);
\r
15172 dom.remove(child, 1);
\r
15175 return clone || node;
\r
15178 childCount = getChildCount(node);
\r
15180 // Remove empty nodes but only if there is multiple wrappers and they are not block
\r
15181 // elements so never remove single <h1></h1> since that would remove the currrent empty block element where the caret is at
\r
15182 if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) {
\r
15183 dom.remove(node, 1);
\r
15187 if (format.inline || format.wrapper) {
\r
15188 // Merges the current node with it's children of similar type to reduce the number of elements
\r
15189 if (!format.exact && childCount === 1)
\r
15190 node = mergeStyles(node);
\r
15192 // Remove/merge children
\r
15193 each(formatList, function(format) {
\r
15194 // Merge all children of similar type will move styles from child to parent
\r
15195 // this: <span style="color:red"><b><span style="color:red; font-size:10px">text</span></b></span>
\r
15196 // will become: <span style="color:red"><b><span style="font-size:10px">text</span></b></span>
\r
15197 each(dom.select(format.inline, node), function(child) {
\r
15200 // When wrap_links is set to false we don't want
\r
15201 // to remove the format on children within links
\r
15202 if (format.wrap_links === false) {
\r
15203 parent = child.parentNode;
\r
15206 if (parent.nodeName === 'A')
\r
15208 } while (parent = parent.parentNode);
\r
15211 removeFormat(format, vars, child, format.exact ? child : null);
\r
15215 // Remove child if direct parent is of same type
\r
15216 if (matchNode(node.parentNode, name, vars)) {
\r
15217 dom.remove(node, 1);
\r
15222 // Look for parent with similar style format
\r
15223 if (format.merge_with_parents) {
\r
15224 dom.getParent(node.parentNode, function(parent) {
\r
15225 if (matchNode(parent, name, vars)) {
\r
15226 dom.remove(node, 1);
\r
15233 // Merge next and previous siblings if they are similar <b>text</b><b>text</b> becomes <b>texttext</b>
\r
15234 if (node && format.merge_siblings !== false) {
\r
15235 node = mergeSiblings(getNonWhiteSpaceSibling(node), node);
\r
15236 node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE));
\r
15244 rng = dom.createRng();
\r
15246 rng.setStartBefore(node);
\r
15247 rng.setEndAfter(node);
\r
15249 applyRngStyle(expandRng(rng, formatList));
\r
15251 if (!isCollapsed || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) {
\r
15252 // Obtain selection node before selection is unselected by applyRngStyle()
\r
15253 var curSelNode = ed.selection.getNode();
\r
15255 // Apply formatting to selection
\r
15256 ed.selection.setRng(adjustSelectionToVisibleSelection());
\r
15257 bookmark = selection.getBookmark();
\r
15258 applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark);
\r
15260 // Colored nodes should be underlined so that the color of the underline matches the text color.
\r
15261 if (format.styles && (format.styles.color || format.styles.textDecoration)) {
\r
15262 tinymce.walk(curSelNode, processUnderlineAndColor, 'childNodes');
\r
15263 processUnderlineAndColor(curSelNode);
\r
15266 selection.moveToBookmark(bookmark);
\r
15267 selection.setRng(moveStart(selection.getRng(TRUE)));
\r
15268 ed.nodeChanged();
\r
15270 performCaretAction('apply', name, vars);
\r
15275 function remove(name, vars, node) {
\r
15276 var formatList = get(name), format = formatList[0], bookmark, i, rng;
\r
15277 function moveStart(rng) {
\r
15278 var container = rng.startContainer,
\r
15279 offset = rng.startOffset,
\r
15280 walker, node, nodes, tmpNode;
\r
15282 // Convert text node into index if possible
\r
15283 if (container.nodeType == 3 && offset >= container.nodeValue.length - 1) {
\r
15284 container = container.parentNode;
\r
15285 offset = nodeIndex(container) + 1;
\r
15288 // Move startContainer/startOffset in to a suitable node
\r
15289 if (container.nodeType == 1) {
\r
15290 nodes = container.childNodes;
\r
15291 container = nodes[Math.min(offset, nodes.length - 1)];
\r
15292 walker = new TreeWalker(container);
\r
15294 // If offset is at end of the parent node walk to the next one
\r
15295 if (offset > nodes.length - 1)
\r
15298 for (node = walker.current(); node; node = walker.next()) {
\r
15299 if (node.nodeType == 3 && !isWhiteSpaceNode(node)) {
\r
15300 // IE has a "neat" feature where it moves the start node into the closest element
\r
15301 // we can avoid this by inserting an element before it and then remove it after we set the selection
\r
15302 tmpNode = dom.create('a', null, INVISIBLE_CHAR);
\r
15303 node.parentNode.insertBefore(tmpNode, node);
\r
15305 // Set selection and remove tmpNode
\r
15306 rng.setStart(node, 0);
\r
15307 selection.setRng(rng);
\r
15308 dom.remove(tmpNode);
\r
15316 // Merges the styles for each node
\r
15317 function process(node) {
\r
15318 var children, i, l;
\r
15320 // Grab the children first since the nodelist might be changed
\r
15321 children = tinymce.grep(node.childNodes);
\r
15323 // Process current node
\r
15324 for (i = 0, l = formatList.length; i < l; i++) {
\r
15325 if (removeFormat(formatList[i], vars, node, node))
\r
15329 // Process the children
\r
15330 if (format.deep) {
\r
15331 for (i = 0, l = children.length; i < l; i++)
\r
15332 process(children[i]);
\r
15336 function findFormatRoot(container) {
\r
15339 // Find format root
\r
15340 each(getParents(container.parentNode).reverse(), function(parent) {
\r
15343 // Find format root element
\r
15344 if (!formatRoot && parent.id != '_start' && parent.id != '_end') {
\r
15345 // Is the node matching the format we are looking for
\r
15346 format = matchNode(parent, name, vars);
\r
15347 if (format && format.split !== false)
\r
15348 formatRoot = parent;
\r
15352 return formatRoot;
\r
15355 function wrapAndSplit(format_root, container, target, split) {
\r
15356 var parent, clone, lastClone, firstClone, i, formatRootParent;
\r
15358 // Format root found then clone formats and split it
\r
15359 if (format_root) {
\r
15360 formatRootParent = format_root.parentNode;
\r
15362 for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) {
\r
15363 clone = parent.cloneNode(FALSE);
\r
15365 for (i = 0; i < formatList.length; i++) {
\r
15366 if (removeFormat(formatList[i], vars, clone, clone)) {
\r
15372 // Build wrapper node
\r
15375 clone.appendChild(lastClone);
\r
15378 firstClone = clone;
\r
15380 lastClone = clone;
\r
15384 // Never split block elements if the format is mixed
\r
15385 if (split && (!format.mixed || !isBlock(format_root)))
\r
15386 container = dom.split(format_root, container);
\r
15388 // Wrap container in cloned formats
\r
15390 target.parentNode.insertBefore(lastClone, target);
\r
15391 firstClone.appendChild(target);
\r
15395 return container;
\r
15398 function splitToFormatRoot(container) {
\r
15399 return wrapAndSplit(findFormatRoot(container), container, container, true);
\r
15402 function unwrap(start) {
\r
15403 var node = dom.get(start ? '_start' : '_end'),
\r
15404 out = node[start ? 'firstChild' : 'lastChild'];
\r
15406 // If the end is placed within the start the result will be removed
\r
15407 // So this checks if the out node is a bookmark node if it is it
\r
15408 // checks for another more suitable node
\r
15409 if (isBookmarkNode(out))
\r
15410 out = out[start ? 'firstChild' : 'lastChild'];
\r
15412 dom.remove(node, true);
\r
15417 function removeRngStyle(rng) {
\r
15418 var startContainer, endContainer;
\r
15420 rng = expandRng(rng, formatList, TRUE);
\r
15422 if (format.split) {
\r
15423 startContainer = getContainer(rng, TRUE);
\r
15424 endContainer = getContainer(rng);
\r
15426 if (startContainer != endContainer) {
\r
15427 // Wrap start/end nodes in span element since these might be cloned/moved
\r
15428 startContainer = wrap(startContainer, 'span', {id : '_start', 'data-mce-type' : 'bookmark'});
\r
15429 endContainer = wrap(endContainer, 'span', {id : '_end', 'data-mce-type' : 'bookmark'});
\r
15431 // Split start/end
\r
15432 splitToFormatRoot(startContainer);
\r
15433 splitToFormatRoot(endContainer);
\r
15435 // Unwrap start/end to get real elements again
\r
15436 startContainer = unwrap(TRUE);
\r
15437 endContainer = unwrap();
\r
15439 startContainer = endContainer = splitToFormatRoot(startContainer);
\r
15441 // Update range positions since they might have changed after the split operations
\r
15442 rng.startContainer = startContainer.parentNode;
\r
15443 rng.startOffset = nodeIndex(startContainer);
\r
15444 rng.endContainer = endContainer.parentNode;
\r
15445 rng.endOffset = nodeIndex(endContainer) + 1;
\r
15448 // Remove items between start/end
\r
15449 rangeUtils.walk(rng, function(nodes) {
\r
15450 each(nodes, function(node) {
\r
15453 // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined.
\r
15454 if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' && node.parentNode && getTextDecoration(node.parentNode) === 'underline') {
\r
15455 removeFormat({'deep': false, 'exact': true, 'inline': 'span', 'styles': {'textDecoration' : 'underline'}}, null, node);
\r
15463 rng = dom.createRng();
\r
15464 rng.setStartBefore(node);
\r
15465 rng.setEndAfter(node);
\r
15466 removeRngStyle(rng);
\r
15470 if (!selection.isCollapsed() || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) {
\r
15471 bookmark = selection.getBookmark();
\r
15472 removeRngStyle(selection.getRng(TRUE));
\r
15473 selection.moveToBookmark(bookmark);
\r
15475 // 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
15476 if (match(name, vars, selection.getStart())) {
\r
15477 moveStart(selection.getRng(true));
\r
15480 ed.nodeChanged();
\r
15482 performCaretAction('remove', name, vars);
\r
15485 function toggle(name, vars, node) {
\r
15486 var fmt = get(name);
\r
15488 if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0]['toggle']))
\r
15489 remove(name, vars, node);
\r
15491 apply(name, vars, node);
\r
15494 function matchNode(node, name, vars, similar) {
\r
15495 var formatList = get(name), format, i, classes;
\r
15497 function matchItems(node, format, item_name) {
\r
15498 var key, value, items = format[item_name], i;
\r
15501 if (format.onmatch) {
\r
15502 return format.onmatch(node, format, item_name);
\r
15505 // Check all items
\r
15507 // Non indexed object
\r
15508 if (items.length === undefined) {
\r
15509 for (key in items) {
\r
15510 if (items.hasOwnProperty(key)) {
\r
15511 if (item_name === 'attributes')
\r
15512 value = dom.getAttrib(node, key);
\r
15514 value = getStyle(node, key);
\r
15516 if (similar && !value && !format.exact)
\r
15519 if ((!similar || format.exact) && !isEq(value, replaceVars(items[key], vars)))
\r
15524 // Only one match needed for indexed arrays
\r
15525 for (i = 0; i < items.length; i++) {
\r
15526 if (item_name === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i]))
\r
15535 if (formatList && node) {
\r
15536 // Check each format in list
\r
15537 for (i = 0; i < formatList.length; i++) {
\r
15538 format = formatList[i];
\r
15540 // Name name, attributes, styles and classes
\r
15541 if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) {
\r
15543 if (classes = format.classes) {
\r
15544 for (i = 0; i < classes.length; i++) {
\r
15545 if (!dom.hasClass(node, classes[i]))
\r
15556 function match(name, vars, node) {
\r
15557 var startNode, i;
\r
15559 function matchParents(node) {
\r
15560 // Find first node with similar format settings
\r
15561 node = dom.getParent(node, function(node) {
\r
15562 return !!matchNode(node, name, vars, true);
\r
15565 // Do an exact check on the similar format element
\r
15566 return matchNode(node, name, vars);
\r
15569 // Check specified node
\r
15571 return matchParents(node);
\r
15573 // Check pending formats
\r
15574 if (selection.isCollapsed()) {
\r
15575 for (i = pendingFormats.apply.length - 1; i >= 0; i--) {
\r
15576 if (pendingFormats.apply[i].name == name)
\r
15580 for (i = pendingFormats.remove.length - 1; i >= 0; i--) {
\r
15581 if (pendingFormats.remove[i].name == name)
\r
15585 return matchParents(selection.getNode());
\r
15588 // Check selected node
\r
15589 node = selection.getNode();
\r
15590 if (matchParents(node))
\r
15593 // Check start node if it's different
\r
15594 startNode = selection.getStart();
\r
15595 if (startNode != node) {
\r
15596 if (matchParents(startNode))
\r
15603 function matchAll(names, vars) {
\r
15604 var startElement, matchedFormatNames = [], checkedMap = {}, i, ni, name;
\r
15606 // If the selection is collapsed then check pending formats
\r
15607 if (selection.isCollapsed()) {
\r
15608 for (ni = 0; ni < names.length; ni++) {
\r
15609 // If the name is to be removed, then stop it from being added
\r
15610 for (i = pendingFormats.remove.length - 1; i >= 0; i--) {
\r
15611 name = names[ni];
\r
15613 if (pendingFormats.remove[i].name == name) {
\r
15614 checkedMap[name] = true;
\r
15620 // If the format is to be applied
\r
15621 for (i = pendingFormats.apply.length - 1; i >= 0; i--) {
\r
15622 for (ni = 0; ni < names.length; ni++) {
\r
15623 name = names[ni];
\r
15625 if (!checkedMap[name] && pendingFormats.apply[i].name == name) {
\r
15626 checkedMap[name] = true;
\r
15627 matchedFormatNames.push(name);
\r
15633 // Check start of selection for formats
\r
15634 startElement = selection.getStart();
\r
15635 dom.getParent(startElement, function(node) {
\r
15638 for (i = 0; i < names.length; i++) {
\r
15641 if (!checkedMap[name] && matchNode(node, name, vars)) {
\r
15642 checkedMap[name] = true;
\r
15643 matchedFormatNames.push(name);
\r
15648 return matchedFormatNames;
\r
15651 function canApply(name) {
\r
15652 var formatList = get(name), startNode, parents, i, x, selector;
\r
15654 if (formatList) {
\r
15655 startNode = selection.getStart();
\r
15656 parents = getParents(startNode);
\r
15658 for (x = formatList.length - 1; x >= 0; x--) {
\r
15659 selector = formatList[x].selector;
\r
15661 // Format is not selector based, then always return TRUE
\r
15665 for (i = parents.length - 1; i >= 0; i--) {
\r
15666 if (dom.is(parents[i], selector))
\r
15675 // Expose to public
\r
15676 tinymce.extend(this, {
\r
15678 register : register,
\r
15683 matchAll : matchAll,
\r
15684 matchNode : matchNode,
\r
15685 canApply : canApply
\r
15688 // Private functions
\r
15690 function matchName(node, format) {
\r
15691 // Check for inline match
\r
15692 if (isEq(node, format.inline))
\r
15695 // Check for block match
\r
15696 if (isEq(node, format.block))
\r
15699 // Check for selector match
\r
15700 if (format.selector)
\r
15701 return dom.is(node, format.selector);
\r
15704 function isEq(str1, str2) {
\r
15705 str1 = str1 || '';
\r
15706 str2 = str2 || '';
\r
15708 str1 = '' + (str1.nodeName || str1);
\r
15709 str2 = '' + (str2.nodeName || str2);
\r
15711 return str1.toLowerCase() == str2.toLowerCase();
\r
15714 function getStyle(node, name) {
\r
15715 var styleVal = dom.getStyle(node, name);
\r
15717 // Force the format to hex
\r
15718 if (name == 'color' || name == 'backgroundColor')
\r
15719 styleVal = dom.toHex(styleVal);
\r
15721 // Opera will return bold as 700
\r
15722 if (name == 'fontWeight' && styleVal == 700)
\r
15723 styleVal = 'bold';
\r
15725 return '' + styleVal;
\r
15728 function replaceVars(value, vars) {
\r
15729 if (typeof(value) != "string")
\r
15730 value = value(vars);
\r
15732 value = value.replace(/%(\w+)/g, function(str, name) {
\r
15733 return vars[name] || str;
\r
15740 function isWhiteSpaceNode(node) {
\r
15741 return node && node.nodeType === 3 && /^([\s\r\n]+|)$/.test(node.nodeValue);
\r
15744 function wrap(node, name, attrs) {
\r
15745 var wrapper = dom.create(name, attrs);
\r
15747 node.parentNode.insertBefore(wrapper, node);
\r
15748 wrapper.appendChild(node);
\r
15753 function expandRng(rng, format, remove) {
\r
15754 var startContainer = rng.startContainer,
\r
15755 startOffset = rng.startOffset,
\r
15756 endContainer = rng.endContainer,
\r
15757 endOffset = rng.endOffset, sibling, lastIdx, leaf;
\r
15759 // This function walks up the tree if there is no siblings before/after the node
\r
15760 function findParentContainer(container, child_name, sibling_name, root) {
\r
15761 var parent, child;
\r
15763 root = root || dom.getRoot();
\r
15766 // Check if we can move up are we at root level or body level
\r
15767 parent = container.parentNode;
\r
15769 // Stop expanding on block elements or root depending on format
\r
15770 if (parent == root || (!format[0].block_expand && isBlock(parent)))
\r
15771 return container;
\r
15773 for (sibling = parent[child_name]; sibling && sibling != container; sibling = sibling[sibling_name]) {
\r
15774 if (sibling.nodeType == 1 && !isBookmarkNode(sibling))
\r
15775 return container;
\r
15777 if (sibling.nodeType == 3 && !isWhiteSpaceNode(sibling))
\r
15778 return container;
\r
15781 container = container.parentNode;
\r
15784 return container;
\r
15787 // This function walks down the tree to find the leaf at the selection.
\r
15788 // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node.
\r
15789 function findLeaf(node, offset) {
\r
15790 if (offset === undefined)
\r
15791 offset = node.nodeType === 3 ? node.length : node.childNodes.length;
\r
15792 while (node && node.hasChildNodes()) {
\r
15793 node = node.childNodes[offset];
\r
15795 offset = node.nodeType === 3 ? node.length : node.childNodes.length;
\r
15797 return { node: node, offset: offset };
\r
15800 // If index based start position then resolve it
\r
15801 if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) {
\r
15802 lastIdx = startContainer.childNodes.length - 1;
\r
15803 startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset];
\r
15805 if (startContainer.nodeType == 3)
\r
15809 // If index based end position then resolve it
\r
15810 if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) {
\r
15811 lastIdx = endContainer.childNodes.length - 1;
\r
15812 endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1];
\r
15814 if (endContainer.nodeType == 3)
\r
15815 endOffset = endContainer.nodeValue.length;
\r
15818 // Exclude bookmark nodes if possible
\r
15819 if (isBookmarkNode(startContainer.parentNode))
\r
15820 startContainer = startContainer.parentNode;
\r
15822 if (isBookmarkNode(startContainer))
\r
15823 startContainer = startContainer.nextSibling || startContainer;
\r
15825 if (isBookmarkNode(endContainer.parentNode)) {
\r
15826 endOffset = dom.nodeIndex(endContainer);
\r
15827 endContainer = endContainer.parentNode;
\r
15830 if (isBookmarkNode(endContainer) && endContainer.previousSibling) {
\r
15831 endContainer = endContainer.previousSibling;
\r
15832 endOffset = endContainer.length;
\r
15835 if (format[0].inline) {
\r
15836 // Avoid applying formatting to a trailing space.
\r
15837 leaf = findLeaf(endContainer, endOffset);
\r
15839 while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling)
\r
15840 leaf = findLeaf(leaf.node.previousSibling);
\r
15842 if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 &&
\r
15843 leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') {
\r
15845 if (leaf.offset > 1) {
\r
15846 endContainer = leaf.node;
\r
15847 endContainer.splitText(leaf.offset - 1);
\r
15848 } else if (leaf.node.previousSibling) {
\r
15849 endContainer = leaf.node.previousSibling;
\r
15855 // Move start/end point up the tree if the leaves are sharp and if we are in different containers
\r
15856 // Example * becomes !: !<p><b><i>*text</i><i>text*</i></b></p>!
\r
15857 // This will reduce the number of wrapper elements that needs to be created
\r
15858 // Move start point up the tree
\r
15859 if (format[0].inline || format[0].block_expand) {
\r
15860 startContainer = findParentContainer(startContainer, 'firstChild', 'nextSibling');
\r
15861 endContainer = findParentContainer(endContainer, 'lastChild', 'previousSibling');
\r
15864 // Expand start/end container to matching selector
\r
15865 if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) {
\r
15866 function findSelectorEndPoint(container, sibling_name) {
\r
15867 var parents, i, y, curFormat;
\r
15869 if (container.nodeType == 3 && container.nodeValue.length == 0 && container[sibling_name])
\r
15870 container = container[sibling_name];
\r
15872 parents = getParents(container);
\r
15873 for (i = 0; i < parents.length; i++) {
\r
15874 for (y = 0; y < format.length; y++) {
\r
15875 curFormat = format[y];
\r
15877 // If collapsed state is set then skip formats that doesn't match that
\r
15878 if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed)
\r
15881 if (dom.is(parents[i], curFormat.selector))
\r
15882 return parents[i];
\r
15886 return container;
\r
15889 // Find new startContainer/endContainer if there is better one
\r
15890 startContainer = findSelectorEndPoint(startContainer, 'previousSibling');
\r
15891 endContainer = findSelectorEndPoint(endContainer, 'nextSibling');
\r
15894 // Expand start/end container to matching block element or text node
\r
15895 if (format[0].block || format[0].selector) {
\r
15896 function findBlockEndPoint(container, sibling_name, sibling_name2) {
\r
15899 // Expand to block of similar type
\r
15900 if (!format[0].wrapper)
\r
15901 node = dom.getParent(container, format[0].block);
\r
15903 // Expand to first wrappable block element or any block element
\r
15905 node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, isBlock);
\r
15907 // Exclude inner lists from wrapping
\r
15908 if (node && format[0].wrapper)
\r
15909 node = getParents(node, 'ul,ol').reverse()[0] || node;
\r
15911 // Didn't find a block element look for first/last wrappable element
\r
15913 node = container;
\r
15915 while (node[sibling_name] && !isBlock(node[sibling_name])) {
\r
15916 node = node[sibling_name];
\r
15918 // Break on BR but include it will be removed later on
\r
15919 // we can't remove it now since we need to check if it can be wrapped
\r
15920 if (isEq(node, 'br'))
\r
15925 return node || container;
\r
15928 // Find new startContainer/endContainer if there is better one
\r
15929 startContainer = findBlockEndPoint(startContainer, 'previousSibling');
\r
15930 endContainer = findBlockEndPoint(endContainer, 'nextSibling');
\r
15932 // Non block element then try to expand up the leaf
\r
15933 if (format[0].block) {
\r
15934 if (!isBlock(startContainer))
\r
15935 startContainer = findParentContainer(startContainer, 'firstChild', 'nextSibling');
\r
15937 if (!isBlock(endContainer))
\r
15938 endContainer = findParentContainer(endContainer, 'lastChild', 'previousSibling');
\r
15942 // Setup index for startContainer
\r
15943 if (startContainer.nodeType == 1) {
\r
15944 startOffset = nodeIndex(startContainer);
\r
15945 startContainer = startContainer.parentNode;
\r
15948 // Setup index for endContainer
\r
15949 if (endContainer.nodeType == 1) {
\r
15950 endOffset = nodeIndex(endContainer) + 1;
\r
15951 endContainer = endContainer.parentNode;
\r
15954 // Return new range like object
\r
15956 startContainer : startContainer,
\r
15957 startOffset : startOffset,
\r
15958 endContainer : endContainer,
\r
15959 endOffset : endOffset
\r
15963 function removeFormat(format, vars, node, compare_node) {
\r
15964 var i, attrs, stylesModified;
\r
15966 // Check if node matches format
\r
15967 if (!matchName(node, format))
\r
15970 // Should we compare with format attribs and styles
\r
15971 if (format.remove != 'all') {
\r
15973 each(format.styles, function(value, name) {
\r
15974 value = replaceVars(value, vars);
\r
15977 if (typeof(name) === 'number') {
\r
15979 compare_node = 0;
\r
15982 if (!compare_node || isEq(getStyle(compare_node, name), value))
\r
15983 dom.setStyle(node, name, '');
\r
15985 stylesModified = 1;
\r
15988 // Remove style attribute if it's empty
\r
15989 if (stylesModified && dom.getAttrib(node, 'style') == '') {
\r
15990 node.removeAttribute('style');
\r
15991 node.removeAttribute('data-mce-style');
\r
15994 // Remove attributes
\r
15995 each(format.attributes, function(value, name) {
\r
15998 value = replaceVars(value, vars);
\r
16001 if (typeof(name) === 'number') {
\r
16003 compare_node = 0;
\r
16006 if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) {
\r
16007 // Keep internal classes
\r
16008 if (name == 'class') {
\r
16009 value = dom.getAttrib(node, name);
\r
16011 // Build new class value where everything is removed except the internal prefixed classes
\r
16013 each(value.split(/\s+/), function(cls) {
\r
16014 if (/mce\w+/.test(cls))
\r
16015 valueOut += (valueOut ? ' ' : '') + cls;
\r
16018 // We got some internal classes left
\r
16020 dom.setAttrib(node, name, valueOut);
\r
16026 // IE6 has a bug where the attribute doesn't get removed correctly
\r
16027 if (name == "class")
\r
16028 node.removeAttribute('className');
\r
16030 // Remove mce prefixed attributes
\r
16031 if (MCE_ATTR_RE.test(name))
\r
16032 node.removeAttribute('data-mce-' + name);
\r
16034 node.removeAttribute(name);
\r
16038 // Remove classes
\r
16039 each(format.classes, function(value) {
\r
16040 value = replaceVars(value, vars);
\r
16042 if (!compare_node || dom.hasClass(compare_node, value))
\r
16043 dom.removeClass(node, value);
\r
16046 // Check for non internal attributes
\r
16047 attrs = dom.getAttribs(node);
\r
16048 for (i = 0; i < attrs.length; i++) {
\r
16049 if (attrs[i].nodeName.indexOf('_') !== 0)
\r
16054 // Remove the inline child if it's empty for example <b> or <span>
\r
16055 if (format.remove != 'none') {
\r
16056 removeNode(node, format);
\r
16061 function removeNode(node, format) {
\r
16062 var parentNode = node.parentNode, rootBlockElm;
\r
16064 if (format.block) {
\r
16065 if (!forcedRootBlock) {
\r
16066 function find(node, next, inc) {
\r
16067 node = getNonWhiteSpaceSibling(node, next, inc);
\r
16069 return !node || (node.nodeName == 'BR' || isBlock(node));
\r
16072 // Append BR elements if needed before we remove the block
\r
16073 if (isBlock(node) && !isBlock(parentNode)) {
\r
16074 if (!find(node, FALSE) && !find(node.firstChild, TRUE, 1))
\r
16075 node.insertBefore(dom.create('br'), node.firstChild);
\r
16077 if (!find(node, TRUE) && !find(node.lastChild, FALSE, 1))
\r
16078 node.appendChild(dom.create('br'));
\r
16081 // Wrap the block in a forcedRootBlock if we are at the root of document
\r
16082 if (parentNode == dom.getRoot()) {
\r
16083 if (!format.list_block || !isEq(node, format.list_block)) {
\r
16084 each(tinymce.grep(node.childNodes), function(node) {
\r
16085 if (isValid(forcedRootBlock, node.nodeName.toLowerCase())) {
\r
16086 if (!rootBlockElm)
\r
16087 rootBlockElm = wrap(node, forcedRootBlock);
\r
16089 rootBlockElm.appendChild(node);
\r
16091 rootBlockElm = 0;
\r
16098 // Never remove nodes that isn't the specified inline element if a selector is specified too
\r
16099 if (format.selector && format.inline && !isEq(format.inline, node))
\r
16102 dom.remove(node, 1);
\r
16105 function getNonWhiteSpaceSibling(node, next, inc) {
\r
16107 next = next ? 'nextSibling' : 'previousSibling';
\r
16109 for (node = inc ? node : node[next]; node; node = node[next]) {
\r
16110 if (node.nodeType == 1 || !isWhiteSpaceNode(node))
\r
16116 function isBookmarkNode(node) {
\r
16117 return node && node.nodeType == 1 && node.getAttribute('data-mce-type') == 'bookmark';
\r
16120 function mergeSiblings(prev, next) {
\r
16121 var marker, sibling, tmpSibling;
\r
16123 function compareElements(node1, node2) {
\r
16124 // Not the same name
\r
16125 if (node1.nodeName != node2.nodeName)
\r
16128 function getAttribs(node) {
\r
16129 var attribs = {};
\r
16131 each(dom.getAttribs(node), function(attr) {
\r
16132 var name = attr.nodeName.toLowerCase();
\r
16134 // Don't compare internal attributes or style
\r
16135 if (name.indexOf('_') !== 0 && name !== 'style')
\r
16136 attribs[name] = dom.getAttrib(node, name);
\r
16142 function compareObjects(obj1, obj2) {
\r
16145 for (name in obj1) {
\r
16146 // Obj1 has item obj2 doesn't have
\r
16147 if (obj1.hasOwnProperty(name)) {
\r
16148 value = obj2[name];
\r
16150 // Obj2 doesn't have obj1 item
\r
16151 if (value === undefined)
\r
16154 // Obj2 item has a different value
\r
16155 if (obj1[name] != value)
\r
16158 // Delete similar value
\r
16159 delete obj2[name];
\r
16163 // Check if obj 2 has something obj 1 doesn't have
\r
16164 for (name in obj2) {
\r
16165 // Obj2 has item obj1 doesn't have
\r
16166 if (obj2.hasOwnProperty(name))
\r
16173 // Attribs are not the same
\r
16174 if (!compareObjects(getAttribs(node1), getAttribs(node2)))
\r
16177 // Styles are not the same
\r
16178 if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style'))))
\r
16184 // Check if next/prev exists and that they are elements
\r
16185 if (prev && next) {
\r
16186 function findElementSibling(node, sibling_name) {
\r
16187 for (sibling = node; sibling; sibling = sibling[sibling_name]) {
\r
16188 if (sibling.nodeType == 3 && sibling.nodeValue.length !== 0)
\r
16191 if (sibling.nodeType == 1 && !isBookmarkNode(sibling))
\r
16198 // If previous sibling is empty then jump over it
\r
16199 prev = findElementSibling(prev, 'previousSibling');
\r
16200 next = findElementSibling(next, 'nextSibling');
\r
16202 // Compare next and previous nodes
\r
16203 if (compareElements(prev, next)) {
\r
16204 // Append nodes between
\r
16205 for (sibling = prev.nextSibling; sibling && sibling != next;) {
\r
16206 tmpSibling = sibling;
\r
16207 sibling = sibling.nextSibling;
\r
16208 prev.appendChild(tmpSibling);
\r
16211 // Remove next node
\r
16212 dom.remove(next);
\r
16214 // Move children into prev node
\r
16215 each(tinymce.grep(next.childNodes), function(node) {
\r
16216 prev.appendChild(node);
\r
16226 function isTextBlock(name) {
\r
16227 return /^(h[1-6]|p|div|pre|address|dl|dt|dd)$/.test(name);
\r
16230 function getContainer(rng, start) {
\r
16231 var container, offset, lastIdx;
\r
16233 container = rng[start ? 'startContainer' : 'endContainer'];
\r
16234 offset = rng[start ? 'startOffset' : 'endOffset'];
\r
16236 if (container.nodeType == 1) {
\r
16237 lastIdx = container.childNodes.length - 1;
\r
16239 if (!start && offset)
\r
16242 container = container.childNodes[offset > lastIdx ? lastIdx : offset];
\r
16245 return container;
\r
16248 function performCaretAction(type, name, vars) {
\r
16249 var i, currentPendingFormats = pendingFormats[type],
\r
16250 otherPendingFormats = pendingFormats[type == 'apply' ? 'remove' : 'apply'];
\r
16252 function hasPending() {
\r
16253 return pendingFormats.apply.length || pendingFormats.remove.length;
\r
16256 function resetPending() {
\r
16257 pendingFormats.apply = [];
\r
16258 pendingFormats.remove = [];
\r
16261 function perform(caret_node) {
\r
16262 // Apply pending formats
\r
16263 each(pendingFormats.apply.reverse(), function(item) {
\r
16264 apply(item.name, item.vars, caret_node);
\r
16266 // Colored nodes should be underlined so that the color of the underline matches the text color.
\r
16267 if (item.name === 'forecolor' && item.vars.value)
\r
16268 processUnderlineAndColor(caret_node.parentNode);
\r
16271 // Remove pending formats
\r
16272 each(pendingFormats.remove.reverse(), function(item) {
\r
16273 remove(item.name, item.vars, caret_node);
\r
16276 dom.remove(caret_node, 1);
\r
16280 // Check if it already exists then ignore it
\r
16281 for (i = currentPendingFormats.length - 1; i >= 0; i--) {
\r
16282 if (currentPendingFormats[i].name == name)
\r
16286 currentPendingFormats.push({name : name, vars : vars});
\r
16288 // Check if it's in the other type, then remove it
\r
16289 for (i = otherPendingFormats.length - 1; i >= 0; i--) {
\r
16290 if (otherPendingFormats[i].name == name)
\r
16291 otherPendingFormats.splice(i, 1);
\r
16294 // Pending apply or remove formats
\r
16295 if (hasPending()) {
\r
16296 ed.getDoc().execCommand('FontName', false, 'mceinline');
\r
16297 pendingFormats.lastRng = selection.getRng();
\r
16299 // IE will convert the current word
\r
16300 each(dom.select('font,span'), function(node) {
\r
16303 if (isCaretNode(node)) {
\r
16304 bookmark = selection.getBookmark();
\r
16306 selection.moveToBookmark(bookmark);
\r
16307 ed.nodeChanged();
\r
16311 // Only register listeners once if we need to
\r
16312 if (!pendingFormats.isListening && hasPending()) {
\r
16313 pendingFormats.isListening = true;
\r
16314 function performPendingFormat(node, textNode) {
\r
16315 var rng = dom.createRng();
\r
16318 rng.setStart(textNode, textNode.nodeValue.length);
\r
16319 rng.setEnd(textNode, textNode.nodeValue.length);
\r
16320 selection.setRng(rng);
\r
16321 ed.nodeChanged();
\r
16323 var enterKeyPressed = false;
\r
16325 each('onKeyDown,onKeyUp,onKeyPress,onMouseUp'.split(','), function(event) {
\r
16326 ed[event].addToTop(function(ed, e) {
\r
16327 if (e.keyCode==13 && !e.shiftKey) {
\r
16328 enterKeyPressed = true;
\r
16331 // Do we have pending formats and is the selection moved has moved
\r
16332 if (hasPending() && !tinymce.dom.RangeUtils.compareRanges(pendingFormats.lastRng, selection.getRng())) {
\r
16333 var foundCaret = false;
\r
16334 each(dom.select('font,span'), function(node) {
\r
16335 var textNode, rng;
\r
16337 // Look for marker
\r
16338 if (isCaretNode(node)) {
\r
16339 foundCaret = true;
\r
16340 textNode = node.firstChild;
\r
16342 // Find the first text node within node
\r
16343 while (textNode && textNode.nodeType != 3)
\r
16344 textNode = textNode.firstChild;
\r
16347 performPendingFormat(node, textNode);
\r
16349 dom.remove(node);
\r
16353 // no caret - so we are
\r
16354 if (enterKeyPressed && !foundCaret) {
\r
16355 var node = selection.getNode();
\r
16356 var textNode = node;
\r
16358 // Find the first text node within node
\r
16359 while (textNode && textNode.nodeType != 3)
\r
16360 textNode = textNode.firstChild;
\r
16362 node=textNode.parentNode;
\r
16363 while (!isBlock(node)){
\r
16364 node=node.parentNode;
\r
16366 performPendingFormat(node, textNode);
\r
16370 // Always unbind and clear pending styles on keyup
\r
16371 if (e.type == 'keyup' || e.type == 'mouseup') {
\r
16373 enterKeyPressed=false;
\r
16384 tinymce.onAddEditor.add(function(tinymce, ed) {
\r
16385 var filters, fontSizes, dom, settings = ed.settings;
\r
16387 if (settings.inline_styles) {
\r
16388 fontSizes = tinymce.explode(settings.font_size_style_values);
\r
16390 function replaceWithSpan(node, styles) {
\r
16391 tinymce.each(styles, function(value, name) {
\r
16393 dom.setStyle(node, name, value);
\r
16396 dom.rename(node, 'span');
\r
16400 font : function(dom, node) {
\r
16401 replaceWithSpan(node, {
\r
16402 backgroundColor : node.style.backgroundColor,
\r
16403 color : node.color,
\r
16404 fontFamily : node.face,
\r
16405 fontSize : fontSizes[parseInt(node.size) - 1]
\r
16409 u : function(dom, node) {
\r
16410 replaceWithSpan(node, {
\r
16411 textDecoration : 'underline'
\r
16415 strike : function(dom, node) {
\r
16416 replaceWithSpan(node, {
\r
16417 textDecoration : 'line-through'
\r
16422 function convert(editor, params) {
\r
16423 dom = editor.dom;
\r
16425 if (settings.convert_fonts_to_spans) {
\r
16426 tinymce.each(dom.select('font,u,strike', params.node), function(node) {
\r
16427 filters[node.nodeName.toLowerCase()](ed.dom, node);
\r
16432 ed.onPreProcess.add(convert);
\r
16433 ed.onSetContent.add(convert);
\r
16435 ed.onInit.add(function() {
\r
16436 ed.selection.onSetContent.add(convert);
\r