1 //<script type="text/javascript">
4 * Based Ext JS Library 1.1.1
5 * Copyright(c) 2006-2007, Ext JS, LLC.
11 * @class Roo.HtmlEditorCore
12 * @extends Roo.Component
13 * Provides a the editing component for the HTML editors in Roo. (bootstrap and Roo.form)
15 * any element that has display set to 'none' can cause problems in Safari and Firefox.<br/><br/>
18 Roo.HtmlEditorCore = function(config){
21 Roo.HtmlEditorCore.superclass.constructor.call(this, config);
27 * Fires when the editor is fully initialized (including the iframe)
28 * @param {Roo.HtmlEditorCore} this
33 * Fires when the editor is first receives the focus. Any insertion must wait
34 * until after this event.
35 * @param {Roo.HtmlEditorCore} this
40 * Fires before the textarea is updated with content from the editor iframe. Return false
42 * @param {Roo.HtmlEditorCore} this
43 * @param {String} html
48 * Fires before the iframe editor is updated with content from the textarea. Return false
50 * @param {Roo.HtmlEditorCore} this
51 * @param {String} html
56 * Fires when the textarea is updated with content from the editor iframe.
57 * @param {Roo.HtmlEditorCore} this
58 * @param {String} html
63 * Fires when the iframe editor is updated with content from the textarea.
64 * @param {Roo.HtmlEditorCore} this
65 * @param {String} html
71 * Fires when on any editor (mouse up/down cursor movement etc.) - used for toolbar hooks.
72 * @param {Roo.HtmlEditorCore} this
79 // at this point this.owner is set, so we can start working out the whitelisted / blacklisted elements
81 // defaults : white / black...
82 this.applyBlacklists();
89 Roo.extend(Roo.HtmlEditorCore, Roo.Component, {
93 * @cfg {Roo.form.HtmlEditor|Roo.bootstrap.HtmlEditor} the owner field
99 * @cfg {String} resizable 's' or 'se' or 'e' - wrapps the element in a
104 * @cfg {Number} height (in pixels)
108 * @cfg {Number} width (in pixels)
112 * @cfg {boolean} autoClean - default true - loading and saving will remove quite a bit of formating,
113 * if you are doing an email editor, this probably needs disabling, it's designed
118 * @cfg {boolean} enableBlocks - default true - if the block editor (table and figure should be enabled)
122 * @cfg {Array} stylesheets url of stylesheets. set to [] to disable stylesheets.
127 * @cfg {String} language default en - language of text (usefull for rtl languages)
133 * @cfg {boolean} allowComments - default false - allow comments in HTML source
134 * - by default they are stripped - if you are editing email you may need this.
136 allowComments: false,
140 // private properties
141 validationEvent : false,
145 sourceEditMode : false,
146 onFocus : Roo.emptyFn,
152 // blacklist + whitelisted elements..
161 * Protected method that will not generally be called directly. It
162 * is called when the editor initializes the iframe with HTML contents. Override this method if you
163 * want to change the initialization markup of the iframe (e.g. to add stylesheets).
165 getDocMarkup : function(){
169 // inherit styels from page...??
170 if (this.stylesheets === false) {
172 Roo.get(document.head).select('style').each(function(node) {
173 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
176 Roo.get(document.head).select('link').each(function(node) {
177 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
180 } else if (!this.stylesheets.length) {
182 st = '<style type="text/css">' +
183 'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
186 for (var i in this.stylesheets) {
187 if (typeof(this.stylesheets[i]) != 'string') {
190 st += '<link rel="stylesheet" href="' + this.stylesheets[i] +'" type="text/css">';
195 st += '<style type="text/css">' +
196 'IMG { cursor: pointer } ' +
199 var cls = 'roo-htmleditor-body';
201 if(this.bodyCls.length){
202 cls += ' ' + this.bodyCls;
205 return '<html><head>' + st +
206 //<style type="text/css">' +
207 //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
209 ' </head><body contenteditable="true" data-enable-grammerly="true" class="' + cls + '"></body></html>';
213 onRender : function(ct, position)
216 //Roo.HtmlEditorCore.superclass.onRender.call(this, ct, position);
217 this.el = this.owner.inputEl ? this.owner.inputEl() : this.owner.el;
220 this.el.dom.style.border = '0 none';
221 this.el.dom.setAttribute('tabIndex', -1);
222 this.el.addClass('x-hidden hide');
226 if(Roo.isIE){ // fix IE 1px bogus margin
227 this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
231 this.frameId = Roo.id();
235 var iframe = this.owner.wrap.createChild({
237 cls: 'form-control', // bootstrap..
241 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
246 this.iframe = iframe.dom;
250 this.doc.designMode = 'on';
253 this.doc.write(this.getDocMarkup());
257 var task = { // must defer to wait for browser to be ready
259 //console.log("run task?" + this.doc.readyState);
261 if(this.doc.body || this.doc.readyState == 'complete'){
263 this.doc.designMode="on";
268 Roo.TaskMgr.stop(task);
269 this.initEditor.defer(10, this);
276 Roo.TaskMgr.start(task);
281 onResize : function(w, h)
283 Roo.log('resize: ' +w + ',' + h );
284 //Roo.HtmlEditorCore.superclass.onResize.apply(this, arguments);
288 if(typeof w == 'number'){
290 this.iframe.style.width = w + 'px';
292 if(typeof h == 'number'){
294 this.iframe.style.height = h + 'px';
296 (this.doc.body || this.doc.documentElement).style.height = (h - (this.iframePad*2)) + 'px';
303 * Toggles the editor between standard and source edit mode.
304 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
306 toggleSourceEdit : function(sourceEditMode){
308 this.sourceEditMode = sourceEditMode === true;
310 if(this.sourceEditMode){
312 Roo.get(this.iframe).addClass(['x-hidden','hide', 'd-none']); //FIXME - what's the BS styles for these
315 Roo.get(this.iframe).removeClass(['x-hidden','hide', 'd-none']);
316 //this.iframe.className = '';
319 //this.setSize(this.owner.wrap.getSize());
320 //this.fireEvent('editmodechange', this, this.sourceEditMode);
327 * Protected method that will not generally be called directly. If you need/want
328 * custom HTML cleanup, this is the method you should override.
329 * @param {String} html The HTML to be cleaned
330 * return {String} The cleaned HTML
332 cleanHtml : function(html){
335 if(Roo.isSafari){ // strip safari nonsense
336 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
339 if(html == ' '){
346 * HTML Editor -> Textarea
347 * Protected method that will not generally be called directly. Syncs the contents
348 * of the editor iframe with the textarea.
350 syncValue : function()
352 //Roo.log("HtmlEditorCore:syncValue (EDITOR->TEXT)");
353 if(this.initialized){
355 this.undoManager.addEvent();
358 var bd = (this.doc.body || this.doc.documentElement);
362 var div = document.createElement('div');
363 div.innerHTML = bd.innerHTML;
366 if (this.enableBlocks) {
367 new Roo.htmleditor.FilterBlock({ node : div });
372 var html = div.innerHTML;
374 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
375 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
377 html = '<div style="'+m[0]+'">' + html + '</div>';
380 html = this.cleanHtml(html);
381 // fix up the special chars.. normaly like back quotes in word...
382 // however we do not want to do this with chinese..
383 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
385 var cc = match.charCodeAt();
387 // Get the character value, handling surrogate pairs
388 if (match.length == 2) {
389 // It's a surrogate pair, calculate the Unicode code point
390 var high = match.charCodeAt(0) - 0xD800;
391 var low = match.charCodeAt(1) - 0xDC00;
392 cc = (high * 0x400) + low + 0x10000;
394 (cc >= 0x4E00 && cc < 0xA000 ) ||
395 (cc >= 0x3400 && cc < 0x4E00 ) ||
396 (cc >= 0xf900 && cc < 0xfb00 )
401 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
402 return "&#" + cc + ";";
409 if(this.owner.fireEvent('beforesync', this, html) !== false){
410 this.el.dom.value = html;
411 this.owner.fireEvent('sync', this, html);
417 * TEXTAREA -> EDITABLE
418 * Protected method that will not generally be called directly. Pushes the value of the textarea
419 * into the iframe editor.
421 pushValue : function()
423 //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
424 if(this.initialized){
425 var v = this.el.dom.value.trim();
428 if(this.owner.fireEvent('beforepush', this, v) !== false){
429 var d = (this.doc.body || this.doc.documentElement);
432 this.el.dom.value = d.innerHTML;
433 this.owner.fireEvent('push', this, v);
435 if (this.autoClean) {
436 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
437 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
440 Roo.htmleditor.Block.initAll(this.doc.body);
441 this.updateLanguage();
443 var lc = this.doc.body.lastChild;
444 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
445 // add an extra line at the end.
446 this.doc.body.appendChild(this.doc.createElement('br'));
454 deferFocus : function(){
455 this.focus.defer(10, this);
460 if(this.win && !this.sourceEditMode){
467 assignDocWin: function()
469 var iframe = this.iframe;
472 this.doc = iframe.contentWindow.document;
473 this.win = iframe.contentWindow;
475 // if (!Roo.get(this.frameId)) {
478 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
479 // this.win = Roo.get(this.frameId).dom.contentWindow;
481 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
485 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
486 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
491 initEditor : function(){
492 //console.log("INIT EDITOR");
497 this.doc.designMode="on";
499 this.doc.write(this.getDocMarkup());
502 var dbody = (this.doc.body || this.doc.documentElement);
503 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
504 // this copies styles from the containing element into thsi one..
505 // not sure why we need all of this..
506 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
508 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
509 //ss['background-attachment'] = 'fixed'; // w3c
510 dbody.bgProperties = 'fixed'; // ie
511 //Roo.DomHelper.applyStyles(dbody, ss);
512 Roo.EventManager.on(this.doc, {
514 'mouseup': this.onEditorEvent,
515 'dblclick': this.onEditorEvent,
516 'click': this.onEditorEvent,
517 'keyup': this.onEditorEvent,
522 Roo.EventManager.on(this.doc, {
523 'paste': this.onPasteEvent,
527 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
530 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
531 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
533 this.initialized = true;
536 // initialize special key events - enter
537 new Roo.htmleditor.KeyEnter({core : this});
541 this.owner.fireEvent('initialize', this);
544 // this is to prevent a href clicks resulting in a redirect?
545 onMouseDown : function(e)
551 onPasteEvent : function(e,v)
553 // I think we better assume paste is going to be a dirty load of rubish from word..
555 // even pasting into a 'email version' of this widget will have to clean up that mess.
556 var cd = (e.browserEvent.clipboardData || window.clipboardData);
558 // check what type of paste - if it's an image, then handle it differently.
559 if (cd.files.length > 0) {
561 var urlAPI = (window.createObjectURL && window) ||
562 (window.URL && URL.revokeObjectURL && URL) ||
563 (window.webkitURL && webkitURL);
565 var url = urlAPI.createObjectURL( cd.files[0]);
566 this.insertAtCursor('<img src=" + url + ">');
570 var html = cd.getData('text/html'); // clipboard event
571 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
572 var images = parser.doc ? parser.doc.getElementsByType('pict') : [];
576 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable)/); }) // ignore headers
577 .map(function(g) { return g.toDataURL(); });
580 html = this.cleanWordChars(html);
582 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
585 var sn = this.getParentElement();
586 // check if d contains a table, and prevent nesting??
587 //Roo.log(d.getElementsByTagName('table'));
589 //Roo.log(sn.closest('table'));
590 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
592 this.insertAtCursor("You can not nest tables");
593 //Roo.log("prevent?"); // fixme -
597 if (images.length > 0) {
598 Roo.each(d.getElementsByTagName('img'), function(img, i) {
599 img.setAttribute('src', images[i]);
602 if (this.autoClean) {
603 new Roo.htmleditor.FilterStyleToTag({ node : d });
604 new Roo.htmleditor.FilterAttributes({
606 attrib_white : ['href', 'src', 'name', 'align'],
607 attrib_clean : ['href', 'src' ]
609 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
611 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT' ]} );
612 new Roo.htmleditor.FilterParagraph({ node : d });
613 new Roo.htmleditor.FilterSpan({ node : d });
614 new Roo.htmleditor.FilterLongBr({ node : d });
616 if (this.enableBlocks) {
618 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
619 if (img.closest('figure')) { // assume!! that it's aready
622 var fig = new Roo.htmleditor.BlockFigure({
625 fig.updateElement(img); // replace it..
631 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
632 if (this.enableBlocks) {
633 Roo.htmleditor.Block.initAll(this.doc.body);
639 // default behaveiour should be our local cleanup paste? (optional?)
640 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
641 //this.owner.fireEvent('paste', e, v);
644 onDestroy : function(){
650 //for (var i =0; i < this.toolbars.length;i++) {
651 // // fixme - ask toolbars for heights?
652 // this.toolbars[i].onDestroy();
655 //this.wrap.dom.innerHTML = '';
656 //this.wrap.remove();
661 onFirstFocus : function(){
664 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
666 this.activated = true;
669 if(Roo.isGecko){ // prevent silly gecko errors
671 var s = this.win.getSelection();
672 if(!s.focusNode || s.focusNode.nodeType != 3){
673 var r = s.getRangeAt(0);
674 r.selectNodeContents((this.doc.body || this.doc.documentElement));
679 this.execCmd('useCSS', true);
680 this.execCmd('styleWithCSS', false);
683 this.owner.fireEvent('activate', this);
687 adjustFont: function(btn){
688 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
689 //if(Roo.isSafari){ // safari
692 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
693 if(Roo.isSafari){ // safari
694 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
695 v = (v < 10) ? 10 : v;
696 v = (v > 48) ? 48 : v;
697 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
702 v = Math.max(1, v+adjust);
704 this.execCmd('FontSize', v );
707 onEditorEvent : function(e)
710 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
711 return; // we do not handle this.. (undo manager does..)
713 // in theory this detects if the last element is not a br, then we try and do that.
714 // its so clicking in space at bottom triggers adding a br and moving the cursor.
716 e.target.nodeName == 'BODY' &&
717 e.type == "mouseup" &&
718 this.doc.body.lastChild
720 var lc = this.doc.body.lastChild;
721 // gtx-trans is google translate plugin adding crap.
722 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
723 lc = lc.previousSibling;
725 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
726 // if last element is <BR> - then dont do anything.
728 var ns = this.doc.createElement('br');
729 this.doc.body.appendChild(ns);
730 range = this.doc.createRange();
731 range.setStartAfter(ns);
732 range.collapse(true);
733 var sel = this.win.getSelection();
734 sel.removeAllRanges();
741 this.fireEditorEvent(e);
742 // this.updateToolbar();
743 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
746 fireEditorEvent: function(e)
748 this.owner.fireEvent('editorevent', this, e);
751 insertTag : function(tg)
753 // could be a bit smarter... -> wrap the current selected tRoo..
754 if (tg.toLowerCase() == 'span' ||
755 tg.toLowerCase() == 'code' ||
756 tg.toLowerCase() == 'sup' ||
757 tg.toLowerCase() == 'sub'
760 range = this.createRange(this.getSelection());
761 var wrappingNode = this.doc.createElement(tg.toLowerCase());
762 wrappingNode.appendChild(range.extractContents());
763 range.insertNode(wrappingNode);
770 this.execCmd("formatblock", tg);
771 this.undoManager.addEvent();
774 insertText : function(txt)
778 var range = this.createRange();
779 range.deleteContents();
780 //alert(Sender.getAttribute('label'));
782 range.insertNode(this.doc.createTextNode(txt));
783 this.undoManager.addEvent();
789 * Executes a Midas editor command on the editor document and performs necessary focus and
790 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
791 * @param {String} cmd The Midas command
792 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
794 relayCmd : function(cmd, value)
800 case 'justifycenter':
801 // if we are in a cell, then we will adjust the
802 var n = this.getParentElement();
803 var td = n.closest('td');
805 var bl = Roo.htmleditor.Block.factory(td);
806 bl.textAlign = cmd.replace('justify','');
808 this.owner.fireEvent('editorevent', this);
811 this.execCmd('styleWithCSS', true); //
815 // if there is no selection, then we insert, and set the curson inside it..
816 this.execCmd('styleWithCSS', false);
826 this.execCmd(cmd, value);
827 this.owner.fireEvent('editorevent', this);
828 //this.updateToolbar();
829 this.owner.deferFocus();
833 * Executes a Midas editor command directly on the editor document.
834 * For visual commands, you should use {@link #relayCmd} instead.
835 * <b>This should only be called after the editor is initialized.</b>
836 * @param {String} cmd The Midas command
837 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
839 execCmd : function(cmd, value){
840 this.doc.execCommand(cmd, false, value === undefined ? null : value);
847 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
849 * @param {String} text | dom node..
851 insertAtCursor : function(text)
858 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
862 // from jquery ui (MIT licenced)
866 if (win.getSelection && win.getSelection().getRangeAt) {
868 // delete the existing?
870 this.createRange(this.getSelection()).deleteContents();
871 range = win.getSelection().getRangeAt(0);
872 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
873 range.insertNode(node);
874 range = range.cloneRange();
875 range.collapse(false);
877 win.getSelection().removeAllRanges();
878 win.getSelection().addRange(range);
882 } else if (win.document.selection && win.document.selection.createRange) {
883 // no firefox support
884 var txt = typeof(text) == 'string' ? text : text.outerHTML;
885 win.document.selection.createRange().pasteHTML(txt);
888 // no firefox support
889 var txt = typeof(text) == 'string' ? text : text.outerHTML;
890 this.execCmd('InsertHTML', txt);
898 mozKeyPress : function(e){
900 var c = e.getCharCode(), cmd;
903 c = String.fromCharCode(c).toLowerCase();
917 // this.cleanUpPaste.defer(100, this);
935 fixKeys : function(){ // load time branching for fastest keydown performance
940 var k = e.getKey(), r;
943 r = this.doc.selection.createRange();
946 r.pasteHTML('    ');
951 /// this is handled by Roo.htmleditor.KeyEnter
954 r = this.doc.selection.createRange();
956 var target = r.parentElement();
957 if(!target || target.tagName.toLowerCase() != 'li'){
959 r.pasteHTML('<br/>');
966 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
967 // this.cleanUpPaste.defer(100, this);
973 }else if(Roo.isOpera){
979 this.execCmd('InsertHTML','    ');
983 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
984 // this.cleanUpPaste.defer(100, this);
989 }else if(Roo.isSafari){
995 this.execCmd('InsertText','\t');
1001 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1002 // this.cleanUpPaste.defer(100, this);
1010 getAllAncestors: function()
1012 var p = this.getSelectedNode();
1015 a.push(p); // push blank onto stack..
1016 p = this.getParentElement();
1020 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1024 a.push(this.doc.body);
1028 lastSelNode : false,
1031 getSelection : function()
1033 this.assignDocWin();
1034 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1038 * @param {DomElement} node the node to select
1040 selectNode : function(node, collapse)
1042 var nodeRange = node.ownerDocument.createRange();
1044 nodeRange.selectNode(node);
1046 nodeRange.selectNodeContents(node);
1048 if (collapse === true) {
1049 nodeRange.collapse(true);
1052 var s = this.win.getSelection();
1053 s.removeAllRanges();
1054 s.addRange(nodeRange);
1057 getSelectedNode: function()
1059 // this may only work on Gecko!!!
1061 // should we cache this!!!!
1065 var range = this.createRange(this.getSelection()).cloneRange();
1068 var parent = range.parentElement();
1070 var testRange = range.duplicate();
1071 testRange.moveToElementText(parent);
1072 if (testRange.inRange(range)) {
1075 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1078 parent = parent.parentElement;
1083 // is ancestor a text element.
1084 var ac = range.commonAncestorContainer;
1085 if (ac.nodeType == 3) {
1089 var ar = ac.childNodes;
1092 var other_nodes = [];
1093 var has_other_nodes = false;
1094 for (var i=0;i<ar.length;i++) {
1095 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1098 // fullly contained node.
1100 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1105 // probably selected..
1106 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1107 other_nodes.push(ar[i]);
1111 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1116 has_other_nodes = true;
1118 if (!nodes.length && other_nodes.length) {
1121 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1129 createRange: function(sel)
1131 // this has strange effects when using with
1132 // top toolbar - not sure if it's a great idea.
1133 //this.editor.contentWindow.focus();
1134 if (typeof sel != "undefined") {
1136 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1138 return this.doc.createRange();
1141 return this.doc.createRange();
1144 getParentElement: function()
1147 this.assignDocWin();
1148 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1150 var range = this.createRange(sel);
1153 var p = range.commonAncestorContainer;
1154 while (p.nodeType == 3) { // text node
1165 * Range intersection.. the hard stuff...
1169 * [ -- selected range --- ]
1173 * if end is before start or hits it. fail.
1174 * if start is after end or hits it fail.
1176 * if either hits (but other is outside. - then it's not
1182 // @see http://www.thismuchiknow.co.uk/?p=64.
1183 rangeIntersectsNode : function(range, node)
1185 var nodeRange = node.ownerDocument.createRange();
1187 nodeRange.selectNode(node);
1189 nodeRange.selectNodeContents(node);
1192 var rangeStartRange = range.cloneRange();
1193 rangeStartRange.collapse(true);
1195 var rangeEndRange = range.cloneRange();
1196 rangeEndRange.collapse(false);
1198 var nodeStartRange = nodeRange.cloneRange();
1199 nodeStartRange.collapse(true);
1201 var nodeEndRange = nodeRange.cloneRange();
1202 nodeEndRange.collapse(false);
1204 return rangeStartRange.compareBoundaryPoints(
1205 Range.START_TO_START, nodeEndRange) == -1 &&
1206 rangeEndRange.compareBoundaryPoints(
1207 Range.START_TO_START, nodeStartRange) == 1;
1211 rangeCompareNode : function(range, node)
1213 var nodeRange = node.ownerDocument.createRange();
1215 nodeRange.selectNode(node);
1217 nodeRange.selectNodeContents(node);
1221 range.collapse(true);
1223 nodeRange.collapse(true);
1225 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1226 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1228 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1230 var nodeIsBefore = ss == 1;
1231 var nodeIsAfter = ee == -1;
1233 if (nodeIsBefore && nodeIsAfter) {
1236 if (!nodeIsBefore && nodeIsAfter) {
1237 return 1; //right trailed.
1240 if (nodeIsBefore && !nodeIsAfter) {
1241 return 2; // left trailed.
1247 cleanWordChars : function(input) {// change the chars to hex code
1250 [ 8211, "–" ],
1251 [ 8212, "—" ],
1260 Roo.each(swapCodes, function(sw) {
1261 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1263 output = output.replace(swapper, sw[1]);
1273 cleanUpChild : function (node)
1276 new Roo.htmleditor.FilterComment({node : node});
1277 new Roo.htmleditor.FilterAttributes({
1279 attrib_black : this.ablack,
1280 attrib_clean : this.aclean,
1281 style_white : this.cwhite,
1282 style_black : this.cblack
1284 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1285 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1291 * Clean up MS wordisms...
1292 * @deprecated - use filter directly
1294 cleanWord : function(node)
1296 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1303 * @deprecated - use filters
1305 cleanTableWidths : function(node)
1307 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1314 applyBlacklists : function()
1316 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1317 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1319 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1320 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1321 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1325 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1326 if (b.indexOf(tag) > -1) {
1329 this.white.push(tag);
1333 Roo.each(w, function(tag) {
1334 if (b.indexOf(tag) > -1) {
1337 if (this.white.indexOf(tag) > -1) {
1340 this.white.push(tag);
1345 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1346 if (w.indexOf(tag) > -1) {
1349 this.black.push(tag);
1353 Roo.each(b, function(tag) {
1354 if (w.indexOf(tag) > -1) {
1357 if (this.black.indexOf(tag) > -1) {
1360 this.black.push(tag);
1365 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1366 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1370 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1371 if (b.indexOf(tag) > -1) {
1374 this.cwhite.push(tag);
1378 Roo.each(w, function(tag) {
1379 if (b.indexOf(tag) > -1) {
1382 if (this.cwhite.indexOf(tag) > -1) {
1385 this.cwhite.push(tag);
1390 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1391 if (w.indexOf(tag) > -1) {
1394 this.cblack.push(tag);
1398 Roo.each(b, function(tag) {
1399 if (w.indexOf(tag) > -1) {
1402 if (this.cblack.indexOf(tag) > -1) {
1405 this.cblack.push(tag);
1410 setStylesheets : function(stylesheets)
1412 if(typeof(stylesheets) == 'string'){
1413 Roo.get(this.iframe.contentDocument.head).createChild({
1424 Roo.each(stylesheets, function(s) {
1429 Roo.get(_this.iframe.contentDocument.head).createChild({
1441 updateLanguage : function()
1443 if (!this.iframe || !this.iframe.contentDocument) {
1446 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1450 removeStylesheets : function()
1454 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1459 setStyle : function(style)
1461 Roo.get(this.iframe.contentDocument.head).createChild({
1470 // hide stuff that is not compatible
1488 * @cfg {String} fieldClass @hide
1491 * @cfg {String} focusClass @hide
1494 * @cfg {String} autoCreate @hide
1497 * @cfg {String} inputType @hide
1500 * @cfg {String} invalidClass @hide
1503 * @cfg {String} invalidText @hide
1506 * @cfg {String} msgFx @hide
1509 * @cfg {String} validateOnBlur @hide
1513 Roo.HtmlEditorCore.white = [
1514 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1516 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1517 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1518 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1519 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1520 'TABLE', 'UL', 'XMP',
1522 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1525 'DIR', 'MENU', 'OL', 'UL', 'DL',
1531 Roo.HtmlEditorCore.black = [
1532 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1534 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1535 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1536 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1537 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1538 //'FONT' // CLEAN LATER..
1539 'COLGROUP', 'COL' // messy tables.
1542 Roo.HtmlEditorCore.clean = [ // ?? needed???
1543 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1545 Roo.HtmlEditorCore.tag_remove = [
1550 Roo.HtmlEditorCore.ablack = [
1554 Roo.HtmlEditorCore.aclean = [
1555 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1559 Roo.HtmlEditorCore.pwhite= [
1560 'http', 'https', 'mailto'
1563 // white listed style attributes.
1564 Roo.HtmlEditorCore.cwhite= [
1565 // 'text-align', /// default is to allow most things..
1571 // black listed style attributes.
1572 Roo.HtmlEditorCore.cblack= [
1573 // 'font-size' -- this can be set by the project