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 st += '<meta name="google" content="notranslate">'
201 var cls = 'notranslate roo-htmleditor-body';
203 if(this.bodyCls.length){
204 cls += ' ' + this.bodyCls;
207 return '<html class="notranslate" translate="no"><head>' + st +
208 //<style type="text/css">' +
209 //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
211 ' </head><body contenteditable="true" data-enable-grammerly="true" class="' + cls + '"></body></html>';
215 onRender : function(ct, position)
218 //Roo.HtmlEditorCore.superclass.onRender.call(this, ct, position);
219 this.el = this.owner.inputEl ? this.owner.inputEl() : this.owner.el;
222 this.el.dom.style.border = '0 none';
223 this.el.dom.setAttribute('tabIndex', -1);
224 this.el.addClass('x-hidden hide');
228 if(Roo.isIE){ // fix IE 1px bogus margin
229 this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
233 this.frameId = Roo.id();
237 var iframe = this.owner.wrap.createChild({
239 cls: 'form-control', // bootstrap..
243 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
248 this.iframe = iframe.dom;
252 this.doc.designMode = 'on';
255 this.doc.write(this.getDocMarkup());
259 var task = { // must defer to wait for browser to be ready
261 //console.log("run task?" + this.doc.readyState);
263 if(this.doc.body || this.doc.readyState == 'complete'){
265 this.doc.designMode="on";
270 Roo.TaskMgr.stop(task);
271 this.initEditor.defer(10, this);
278 Roo.TaskMgr.start(task);
283 onResize : function(w, h)
285 Roo.log('resize: ' +w + ',' + h );
286 //Roo.HtmlEditorCore.superclass.onResize.apply(this, arguments);
290 if(typeof w == 'number'){
292 this.iframe.style.width = w + 'px';
294 if(typeof h == 'number'){
296 this.iframe.style.height = h + 'px';
298 (this.doc.body || this.doc.documentElement).style.height = (h - (this.iframePad*2)) + 'px';
305 * Toggles the editor between standard and source edit mode.
306 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
308 toggleSourceEdit : function(sourceEditMode){
310 this.sourceEditMode = sourceEditMode === true;
312 if(this.sourceEditMode){
314 Roo.get(this.iframe).addClass(['x-hidden','hide', 'd-none']); //FIXME - what's the BS styles for these
317 Roo.get(this.iframe).removeClass(['x-hidden','hide', 'd-none']);
318 //this.iframe.className = '';
321 //this.setSize(this.owner.wrap.getSize());
322 //this.fireEvent('editmodechange', this, this.sourceEditMode);
329 * Protected method that will not generally be called directly. If you need/want
330 * custom HTML cleanup, this is the method you should override.
331 * @param {String} html The HTML to be cleaned
332 * return {String} The cleaned HTML
334 cleanHtml : function(html)
338 if(Roo.isSafari){ // strip safari nonsense
339 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
342 if(html == ' '){
349 * HTML Editor -> Textarea
350 * Protected method that will not generally be called directly. Syncs the contents
351 * of the editor iframe with the textarea.
353 syncValue : function()
355 //Roo.log("HtmlEditorCore:syncValue (EDITOR->TEXT)");
356 if(this.initialized){
358 this.undoManager.addEvent();
361 var bd = (this.doc.body || this.doc.documentElement);
365 var div = document.createElement('div');
366 div.innerHTML = bd.innerHTML;
370 if (this.enableBlocks) {
371 new Roo.htmleditor.FilterBlock({ node : div });
374 var tidy = new Roo.htmleditor.TidySerializer({
377 var html = tidy.serialize(div)
381 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
382 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
384 html = '<div style="'+m[0]+'">' + html + '</div>';
387 html = this.cleanHtml(html);
388 // fix up the special chars.. normaly like back quotes in word...
389 // however we do not want to do this with chinese..
390 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
392 var cc = match.charCodeAt();
394 // Get the character value, handling surrogate pairs
395 if (match.length == 2) {
396 // It's a surrogate pair, calculate the Unicode code point
397 var high = match.charCodeAt(0) - 0xD800;
398 var low = match.charCodeAt(1) - 0xDC00;
399 cc = (high * 0x400) + low + 0x10000;
401 (cc >= 0x4E00 && cc < 0xA000 ) ||
402 (cc >= 0x3400 && cc < 0x4E00 ) ||
403 (cc >= 0xf900 && cc < 0xfb00 )
408 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
409 return "&#" + cc + ";";
416 if(this.owner.fireEvent('beforesync', this, html) !== false){
417 this.el.dom.value = html;
418 this.owner.fireEvent('sync', this, html);
424 * TEXTAREA -> EDITABLE
425 * Protected method that will not generally be called directly. Pushes the value of the textarea
426 * into the iframe editor.
428 pushValue : function()
430 //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
431 if(this.initialized){
432 var v = this.el.dom.value.trim();
435 if(this.owner.fireEvent('beforepush', this, v) !== false){
436 var d = (this.doc.body || this.doc.documentElement);
439 this.el.dom.value = d.innerHTML;
440 this.owner.fireEvent('push', this, v);
442 if (this.autoClean) {
443 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
444 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
447 Roo.htmleditor.Block.initAll(this.doc.body);
448 this.updateLanguage();
450 var lc = this.doc.body.lastChild;
451 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
452 // add an extra line at the end.
453 this.doc.body.appendChild(this.doc.createElement('br'));
461 deferFocus : function(){
462 this.focus.defer(10, this);
467 if(this.win && !this.sourceEditMode){
474 assignDocWin: function()
476 var iframe = this.iframe;
479 this.doc = iframe.contentWindow.document;
480 this.win = iframe.contentWindow;
482 // if (!Roo.get(this.frameId)) {
485 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
486 // this.win = Roo.get(this.frameId).dom.contentWindow;
488 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
492 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
493 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
498 initEditor : function(){
499 //console.log("INIT EDITOR");
504 this.doc.designMode="on";
506 this.doc.write(this.getDocMarkup());
509 var dbody = (this.doc.body || this.doc.documentElement);
510 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
511 // this copies styles from the containing element into thsi one..
512 // not sure why we need all of this..
513 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
515 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
516 //ss['background-attachment'] = 'fixed'; // w3c
517 dbody.bgProperties = 'fixed'; // ie
518 dbody.setAttribute("translate", "no");
520 //Roo.DomHelper.applyStyles(dbody, ss);
521 Roo.EventManager.on(this.doc, {
523 'mouseup': this.onEditorEvent,
524 'dblclick': this.onEditorEvent,
525 'click': this.onEditorEvent,
526 'keyup': this.onEditorEvent,
531 Roo.EventManager.on(this.doc, {
532 'paste': this.onPasteEvent,
536 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
539 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
540 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
542 this.initialized = true;
545 // initialize special key events - enter
546 new Roo.htmleditor.KeyEnter({core : this});
550 this.owner.fireEvent('initialize', this);
553 // this is to prevent a href clicks resulting in a redirect?
555 onPasteEvent : function(e,v)
557 // I think we better assume paste is going to be a dirty load of rubish from word..
559 // even pasting into a 'email version' of this widget will have to clean up that mess.
560 var cd = (e.browserEvent.clipboardData || window.clipboardData);
562 // check what type of paste - if it's an image, then handle it differently.
563 if (cd.files.length > 0) {
565 var urlAPI = (window.createObjectURL && window) ||
566 (window.URL && URL.revokeObjectURL && URL) ||
567 (window.webkitURL && webkitURL);
569 var url = urlAPI.createObjectURL( cd.files[0]);
570 this.insertAtCursor('<img src=" + url + ">');
574 var html = cd.getData('text/html'); // clipboard event
575 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
576 var images = parser.doc ? parser.doc.getElementsByType('pict') : [];
580 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable)/); }) // ignore headers
581 .map(function(g) { return g.toDataURL(); });
584 html = this.cleanWordChars(html);
586 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
589 var sn = this.getParentElement();
590 // check if d contains a table, and prevent nesting??
591 //Roo.log(d.getElementsByTagName('table'));
593 //Roo.log(sn.closest('table'));
594 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
596 this.insertAtCursor("You can not nest tables");
597 //Roo.log("prevent?"); // fixme -
601 if (images.length > 0) {
602 Roo.each(d.getElementsByTagName('img'), function(img, i) {
603 img.setAttribute('src', images[i]);
606 if (this.autoClean) {
607 new Roo.htmleditor.FilterStyleToTag({ node : d });
608 new Roo.htmleditor.FilterAttributes({
610 attrib_white : ['href', 'src', 'name', 'align'],
611 attrib_clean : ['href', 'src' ]
613 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
615 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT' ]} );
616 new Roo.htmleditor.FilterParagraph({ node : d });
617 new Roo.htmleditor.FilterSpan({ node : d });
618 new Roo.htmleditor.FilterLongBr({ node : d });
620 if (this.enableBlocks) {
622 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
623 if (img.closest('figure')) { // assume!! that it's aready
626 var fig = new Roo.htmleditor.BlockFigure({
629 fig.updateElement(img); // replace it..
635 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
636 if (this.enableBlocks) {
637 Roo.htmleditor.Block.initAll(this.doc.body);
643 // default behaveiour should be our local cleanup paste? (optional?)
644 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
645 //this.owner.fireEvent('paste', e, v);
648 onDestroy : function(){
654 //for (var i =0; i < this.toolbars.length;i++) {
655 // // fixme - ask toolbars for heights?
656 // this.toolbars[i].onDestroy();
659 //this.wrap.dom.innerHTML = '';
660 //this.wrap.remove();
665 onFirstFocus : function(){
668 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
670 this.activated = true;
673 if(Roo.isGecko){ // prevent silly gecko errors
675 var s = this.win.getSelection();
676 if(!s.focusNode || s.focusNode.nodeType != 3){
677 var r = s.getRangeAt(0);
678 r.selectNodeContents((this.doc.body || this.doc.documentElement));
683 this.execCmd('useCSS', true);
684 this.execCmd('styleWithCSS', false);
687 this.owner.fireEvent('activate', this);
691 adjustFont: function(btn){
692 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
693 //if(Roo.isSafari){ // safari
696 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
697 if(Roo.isSafari){ // safari
698 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
699 v = (v < 10) ? 10 : v;
700 v = (v > 48) ? 48 : v;
701 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
706 v = Math.max(1, v+adjust);
708 this.execCmd('FontSize', v );
711 onEditorEvent : function(e)
715 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
716 return; // we do not handle this.. (undo manager does..)
718 // in theory this detects if the last element is not a br, then we try and do that.
719 // its so clicking in space at bottom triggers adding a br and moving the cursor.
721 e.target.nodeName == 'BODY' &&
722 e.type == "mouseup" &&
723 this.doc.body.lastChild
725 var lc = this.doc.body.lastChild;
726 // gtx-trans is google translate plugin adding crap.
727 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
728 lc = lc.previousSibling;
730 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
731 // if last element is <BR> - then dont do anything.
733 var ns = this.doc.createElement('br');
734 this.doc.body.appendChild(ns);
735 range = this.doc.createRange();
736 range.setStartAfter(ns);
737 range.collapse(true);
738 var sel = this.win.getSelection();
739 sel.removeAllRanges();
746 this.fireEditorEvent(e);
747 // this.updateToolbar();
748 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
751 fireEditorEvent: function(e)
753 this.owner.fireEvent('editorevent', this, e);
756 insertTag : function(tg)
758 // could be a bit smarter... -> wrap the current selected tRoo..
759 if (tg.toLowerCase() == 'span' ||
760 tg.toLowerCase() == 'code' ||
761 tg.toLowerCase() == 'sup' ||
762 tg.toLowerCase() == 'sub'
765 range = this.createRange(this.getSelection());
766 var wrappingNode = this.doc.createElement(tg.toLowerCase());
767 wrappingNode.appendChild(range.extractContents());
768 range.insertNode(wrappingNode);
775 this.execCmd("formatblock", tg);
776 this.undoManager.addEvent();
779 insertText : function(txt)
783 var range = this.createRange();
784 range.deleteContents();
785 //alert(Sender.getAttribute('label'));
787 range.insertNode(this.doc.createTextNode(txt));
788 this.undoManager.addEvent();
794 * Executes a Midas editor command on the editor document and performs necessary focus and
795 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
796 * @param {String} cmd The Midas command
797 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
799 relayCmd : function(cmd, value)
805 case 'justifycenter':
806 // if we are in a cell, then we will adjust the
807 var n = this.getParentElement();
808 var td = n.closest('td');
810 var bl = Roo.htmleditor.Block.factory(td);
811 bl.textAlign = cmd.replace('justify','');
813 this.owner.fireEvent('editorevent', this);
816 this.execCmd('styleWithCSS', true); //
820 // if there is no selection, then we insert, and set the curson inside it..
821 this.execCmd('styleWithCSS', false);
831 this.execCmd(cmd, value);
832 this.owner.fireEvent('editorevent', this);
833 //this.updateToolbar();
834 this.owner.deferFocus();
838 * Executes a Midas editor command directly on the editor document.
839 * For visual commands, you should use {@link #relayCmd} instead.
840 * <b>This should only be called after the editor is initialized.</b>
841 * @param {String} cmd The Midas command
842 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
844 execCmd : function(cmd, value){
845 this.doc.execCommand(cmd, false, value === undefined ? null : value);
852 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
854 * @param {String} text | dom node..
856 insertAtCursor : function(text)
863 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
867 // from jquery ui (MIT licenced)
871 if (win.getSelection && win.getSelection().getRangeAt) {
873 // delete the existing?
875 this.createRange(this.getSelection()).deleteContents();
876 range = win.getSelection().getRangeAt(0);
877 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
878 range.insertNode(node);
879 range = range.cloneRange();
880 range.collapse(false);
882 win.getSelection().removeAllRanges();
883 win.getSelection().addRange(range);
887 } else if (win.document.selection && win.document.selection.createRange) {
888 // no firefox support
889 var txt = typeof(text) == 'string' ? text : text.outerHTML;
890 win.document.selection.createRange().pasteHTML(txt);
893 // no firefox support
894 var txt = typeof(text) == 'string' ? text : text.outerHTML;
895 this.execCmd('InsertHTML', txt);
903 mozKeyPress : function(e){
905 var c = e.getCharCode(), cmd;
908 c = String.fromCharCode(c).toLowerCase();
922 // this.cleanUpPaste.defer(100, this);
940 fixKeys : function(){ // load time branching for fastest keydown performance
945 var k = e.getKey(), r;
948 r = this.doc.selection.createRange();
951 r.pasteHTML('    ');
956 /// this is handled by Roo.htmleditor.KeyEnter
959 r = this.doc.selection.createRange();
961 var target = r.parentElement();
962 if(!target || target.tagName.toLowerCase() != 'li'){
964 r.pasteHTML('<br/>');
971 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
972 // this.cleanUpPaste.defer(100, this);
978 }else if(Roo.isOpera){
984 this.execCmd('InsertHTML','    ');
988 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
989 // this.cleanUpPaste.defer(100, this);
994 }else if(Roo.isSafari){
1000 this.execCmd('InsertText','\t');
1004 this.mozKeyPress(e);
1006 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1007 // this.cleanUpPaste.defer(100, this);
1015 getAllAncestors: function()
1017 var p = this.getSelectedNode();
1020 a.push(p); // push blank onto stack..
1021 p = this.getParentElement();
1025 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1029 a.push(this.doc.body);
1033 lastSelNode : false,
1036 getSelection : function()
1038 this.assignDocWin();
1039 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1043 * @param {DomElement} node the node to select
1045 selectNode : function(node, collapse)
1047 var nodeRange = node.ownerDocument.createRange();
1049 nodeRange.selectNode(node);
1051 nodeRange.selectNodeContents(node);
1053 if (collapse === true) {
1054 nodeRange.collapse(true);
1057 var s = this.win.getSelection();
1058 s.removeAllRanges();
1059 s.addRange(nodeRange);
1062 getSelectedNode: function()
1064 // this may only work on Gecko!!!
1066 // should we cache this!!!!
1070 var range = this.createRange(this.getSelection()).cloneRange();
1073 var parent = range.parentElement();
1075 var testRange = range.duplicate();
1076 testRange.moveToElementText(parent);
1077 if (testRange.inRange(range)) {
1080 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1083 parent = parent.parentElement;
1088 // is ancestor a text element.
1089 var ac = range.commonAncestorContainer;
1090 if (ac.nodeType == 3) {
1094 var ar = ac.childNodes;
1097 var other_nodes = [];
1098 var has_other_nodes = false;
1099 for (var i=0;i<ar.length;i++) {
1100 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1103 // fullly contained node.
1105 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1110 // probably selected..
1111 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1112 other_nodes.push(ar[i]);
1116 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1121 has_other_nodes = true;
1123 if (!nodes.length && other_nodes.length) {
1126 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1134 createRange: function(sel)
1136 // this has strange effects when using with
1137 // top toolbar - not sure if it's a great idea.
1138 //this.editor.contentWindow.focus();
1139 if (typeof sel != "undefined") {
1141 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1143 return this.doc.createRange();
1146 return this.doc.createRange();
1149 getParentElement: function()
1152 this.assignDocWin();
1153 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1155 var range = this.createRange(sel);
1158 var p = range.commonAncestorContainer;
1159 while (p.nodeType == 3) { // text node
1170 * Range intersection.. the hard stuff...
1174 * [ -- selected range --- ]
1178 * if end is before start or hits it. fail.
1179 * if start is after end or hits it fail.
1181 * if either hits (but other is outside. - then it's not
1187 // @see http://www.thismuchiknow.co.uk/?p=64.
1188 rangeIntersectsNode : function(range, node)
1190 var nodeRange = node.ownerDocument.createRange();
1192 nodeRange.selectNode(node);
1194 nodeRange.selectNodeContents(node);
1197 var rangeStartRange = range.cloneRange();
1198 rangeStartRange.collapse(true);
1200 var rangeEndRange = range.cloneRange();
1201 rangeEndRange.collapse(false);
1203 var nodeStartRange = nodeRange.cloneRange();
1204 nodeStartRange.collapse(true);
1206 var nodeEndRange = nodeRange.cloneRange();
1207 nodeEndRange.collapse(false);
1209 return rangeStartRange.compareBoundaryPoints(
1210 Range.START_TO_START, nodeEndRange) == -1 &&
1211 rangeEndRange.compareBoundaryPoints(
1212 Range.START_TO_START, nodeStartRange) == 1;
1216 rangeCompareNode : function(range, node)
1218 var nodeRange = node.ownerDocument.createRange();
1220 nodeRange.selectNode(node);
1222 nodeRange.selectNodeContents(node);
1226 range.collapse(true);
1228 nodeRange.collapse(true);
1230 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1231 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1233 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1235 var nodeIsBefore = ss == 1;
1236 var nodeIsAfter = ee == -1;
1238 if (nodeIsBefore && nodeIsAfter) {
1241 if (!nodeIsBefore && nodeIsAfter) {
1242 return 1; //right trailed.
1245 if (nodeIsBefore && !nodeIsAfter) {
1246 return 2; // left trailed.
1252 cleanWordChars : function(input) {// change the chars to hex code
1255 [ 8211, "–" ],
1256 [ 8212, "—" ],
1265 Roo.each(swapCodes, function(sw) {
1266 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1268 output = output.replace(swapper, sw[1]);
1278 cleanUpChild : function (node)
1281 new Roo.htmleditor.FilterComment({node : node});
1282 new Roo.htmleditor.FilterAttributes({
1284 attrib_black : this.ablack,
1285 attrib_clean : this.aclean,
1286 style_white : this.cwhite,
1287 style_black : this.cblack
1289 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1290 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1296 * Clean up MS wordisms...
1297 * @deprecated - use filter directly
1299 cleanWord : function(node)
1301 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1308 * @deprecated - use filters
1310 cleanTableWidths : function(node)
1312 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1319 applyBlacklists : function()
1321 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1322 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1324 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1325 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1326 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1330 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1331 if (b.indexOf(tag) > -1) {
1334 this.white.push(tag);
1338 Roo.each(w, function(tag) {
1339 if (b.indexOf(tag) > -1) {
1342 if (this.white.indexOf(tag) > -1) {
1345 this.white.push(tag);
1350 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1351 if (w.indexOf(tag) > -1) {
1354 this.black.push(tag);
1358 Roo.each(b, function(tag) {
1359 if (w.indexOf(tag) > -1) {
1362 if (this.black.indexOf(tag) > -1) {
1365 this.black.push(tag);
1370 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1371 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1375 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1376 if (b.indexOf(tag) > -1) {
1379 this.cwhite.push(tag);
1383 Roo.each(w, function(tag) {
1384 if (b.indexOf(tag) > -1) {
1387 if (this.cwhite.indexOf(tag) > -1) {
1390 this.cwhite.push(tag);
1395 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1396 if (w.indexOf(tag) > -1) {
1399 this.cblack.push(tag);
1403 Roo.each(b, function(tag) {
1404 if (w.indexOf(tag) > -1) {
1407 if (this.cblack.indexOf(tag) > -1) {
1410 this.cblack.push(tag);
1415 setStylesheets : function(stylesheets)
1417 if(typeof(stylesheets) == 'string'){
1418 Roo.get(this.iframe.contentDocument.head).createChild({
1429 Roo.each(stylesheets, function(s) {
1434 Roo.get(_this.iframe.contentDocument.head).createChild({
1446 updateLanguage : function()
1448 if (!this.iframe || !this.iframe.contentDocument) {
1451 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1455 removeStylesheets : function()
1459 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1464 setStyle : function(style)
1466 Roo.get(this.iframe.contentDocument.head).createChild({
1475 // hide stuff that is not compatible
1493 * @cfg {String} fieldClass @hide
1496 * @cfg {String} focusClass @hide
1499 * @cfg {String} autoCreate @hide
1502 * @cfg {String} inputType @hide
1505 * @cfg {String} invalidClass @hide
1508 * @cfg {String} invalidText @hide
1511 * @cfg {String} msgFx @hide
1514 * @cfg {String} validateOnBlur @hide
1518 Roo.HtmlEditorCore.white = [
1519 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1521 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1522 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1523 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1524 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1525 'TABLE', 'UL', 'XMP',
1527 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1530 'DIR', 'MENU', 'OL', 'UL', 'DL',
1536 Roo.HtmlEditorCore.black = [
1537 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1539 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1540 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1541 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1542 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1543 //'FONT' // CLEAN LATER..
1544 'COLGROUP', 'COL' // messy tables.
1547 Roo.HtmlEditorCore.clean = [ // ?? needed???
1548 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1550 Roo.HtmlEditorCore.tag_remove = [
1555 Roo.HtmlEditorCore.ablack = [
1559 Roo.HtmlEditorCore.aclean = [
1560 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1564 Roo.HtmlEditorCore.pwhite= [
1565 'http', 'https', 'mailto'
1568 // white listed style attributes.
1569 Roo.HtmlEditorCore.cwhite= [
1570 // 'text-align', /// default is to allow most things..
1576 // black listed style attributes.
1577 Roo.HtmlEditorCore.cblack= [
1578 // 'font-size' -- this can be set by the project