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
78 // at this point this.owner is set, so we can start working out the whitelisted / blacklisted elements
80 // defaults : white / black...
81 this.applyBlacklists();
88 Roo.extend(Roo.HtmlEditorCore, Roo.Component, {
92 * @cfg {Roo.form.HtmlEditor|Roo.bootstrap.HtmlEditor} the owner field
98 * @cfg {String} resizable 's' or 'se' or 'e' - wrapps the element in a
103 * @cfg {Number} height (in pixels)
107 * @cfg {Number} width (in pixels)
111 * @cfg {boolean} autoClean - default true - loading and saving will remove quite a bit of formating,
112 * if you are doing an email editor, this probably needs disabling, it's designed
117 * @cfg {boolean} enableBlocks - default true - if the block editor (table and figure should be enabled)
121 * @cfg {Array} stylesheets url of stylesheets. set to [] to disable stylesheets.
127 * @cfg {boolean} allowComments - default false - allow comments in HTML source
128 * - by default they are stripped - if you are editing email you may need this.
130 allowComments: false,
134 // private properties
135 validationEvent : false,
139 sourceEditMode : false,
140 onFocus : Roo.emptyFn,
146 // blacklist + whitelisted elements..
155 * Protected method that will not generally be called directly. It
156 * is called when the editor initializes the iframe with HTML contents. Override this method if you
157 * want to change the initialization markup of the iframe (e.g. to add stylesheets).
159 getDocMarkup : function(){
163 // inherit styels from page...??
164 if (this.stylesheets === false) {
166 Roo.get(document.head).select('style').each(function(node) {
167 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
170 Roo.get(document.head).select('link').each(function(node) {
171 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
174 } else if (!this.stylesheets.length) {
176 st = '<style type="text/css">' +
177 'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
180 for (var i in this.stylesheets) {
181 if (typeof(this.stylesheets[i]) != 'string') {
184 st += '<link rel="stylesheet" href="' + this.stylesheets[i] +'" type="text/css">';
189 st += '<style type="text/css">' +
190 'IMG { cursor: pointer } ' +
193 var cls = 'roo-htmleditor-body';
195 if(this.bodyCls.length){
196 cls += ' ' + this.bodyCls;
199 return '<html><head>' + st +
200 //<style type="text/css">' +
201 //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
203 ' </head><body contenteditable="true" data-enable-grammerly="true" class="' + cls + '"></body></html>';
207 onRender : function(ct, position)
210 //Roo.HtmlEditorCore.superclass.onRender.call(this, ct, position);
211 this.el = this.owner.inputEl ? this.owner.inputEl() : this.owner.el;
214 this.el.dom.style.border = '0 none';
215 this.el.dom.setAttribute('tabIndex', -1);
216 this.el.addClass('x-hidden hide');
220 if(Roo.isIE){ // fix IE 1px bogus margin
221 this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
225 this.frameId = Roo.id();
229 var iframe = this.owner.wrap.createChild({
231 cls: 'form-control', // bootstrap..
235 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
240 this.iframe = iframe.dom;
244 this.doc.designMode = 'on';
247 this.doc.write(this.getDocMarkup());
251 var task = { // must defer to wait for browser to be ready
253 //console.log("run task?" + this.doc.readyState);
255 if(this.doc.body || this.doc.readyState == 'complete'){
257 this.doc.designMode="on";
262 Roo.TaskMgr.stop(task);
263 this.initEditor.defer(10, this);
270 Roo.TaskMgr.start(task);
275 onResize : function(w, h)
277 Roo.log('resize: ' +w + ',' + h );
278 //Roo.HtmlEditorCore.superclass.onResize.apply(this, arguments);
282 if(typeof w == 'number'){
284 this.iframe.style.width = w + 'px';
286 if(typeof h == 'number'){
288 this.iframe.style.height = h + 'px';
290 (this.doc.body || this.doc.documentElement).style.height = (h - (this.iframePad*2)) + 'px';
297 * Toggles the editor between standard and source edit mode.
298 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
300 toggleSourceEdit : function(sourceEditMode){
302 this.sourceEditMode = sourceEditMode === true;
304 if(this.sourceEditMode){
306 Roo.get(this.iframe).addClass(['x-hidden','hide', 'd-none']); //FIXME - what's the BS styles for these
309 Roo.get(this.iframe).removeClass(['x-hidden','hide', 'd-none']);
310 //this.iframe.className = '';
313 //this.setSize(this.owner.wrap.getSize());
314 //this.fireEvent('editmodechange', this, this.sourceEditMode);
321 * Protected method that will not generally be called directly. If you need/want
322 * custom HTML cleanup, this is the method you should override.
323 * @param {String} html The HTML to be cleaned
324 * return {String} The cleaned HTML
326 cleanHtml : function(html){
329 if(Roo.isSafari){ // strip safari nonsense
330 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
333 if(html == ' '){
340 * HTML Editor -> Textarea
341 * Protected method that will not generally be called directly. Syncs the contents
342 * of the editor iframe with the textarea.
344 syncValue : function()
346 //Roo.log("HtmlEditorCore:syncValue (EDITOR->TEXT)");
347 if(this.initialized){
349 this.undoManager.addEvent();
352 var bd = (this.doc.body || this.doc.documentElement);
356 var div = document.createElement('div');
357 div.innerHTML = bd.innerHTML;
360 if (this.enableBlocks) {
361 new Roo.htmleditor.FilterBlock({ node : div });
366 var html = div.innerHTML;
368 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
369 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
371 html = '<div style="'+m[0]+'">' + html + '</div>';
374 html = this.cleanHtml(html);
375 // fix up the special chars.. normaly like back quotes in word...
376 // however we do not want to do this with chinese..
377 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
379 var cc = match.charCodeAt();
381 // Get the character value, handling surrogate pairs
382 if (match.length == 2) {
383 // It's a surrogate pair, calculate the Unicode code point
384 var high = match.charCodeAt(0) - 0xD800;
385 var low = match.charCodeAt(1) - 0xDC00;
386 cc = (high * 0x400) + low + 0x10000;
388 (cc >= 0x4E00 && cc < 0xA000 ) ||
389 (cc >= 0x3400 && cc < 0x4E00 ) ||
390 (cc >= 0xf900 && cc < 0xfb00 )
395 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
396 return "&#" + cc + ";";
403 if(this.owner.fireEvent('beforesync', this, html) !== false){
404 this.el.dom.value = html;
405 this.owner.fireEvent('sync', this, html);
411 * TEXTAREA -> EDITABLE
412 * Protected method that will not generally be called directly. Pushes the value of the textarea
413 * into the iframe editor.
415 pushValue : function()
417 //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
418 if(this.initialized){
419 var v = this.el.dom.value.trim();
422 if(this.owner.fireEvent('beforepush', this, v) !== false){
423 var d = (this.doc.body || this.doc.documentElement);
426 this.el.dom.value = d.innerHTML;
427 this.owner.fireEvent('push', this, v);
429 if (this.autoClean) {
430 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
431 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
434 Roo.htmleditor.Block.initAll(this.doc.body);
436 var lc = this.doc.body.lastChild;
437 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
438 // add an extra line at the end.
439 this.doc.body.appendChild(this.doc.createElement('br'));
447 deferFocus : function(){
448 this.focus.defer(10, this);
453 if(this.win && !this.sourceEditMode){
460 assignDocWin: function()
462 var iframe = this.iframe;
465 this.doc = iframe.contentWindow.document;
466 this.win = iframe.contentWindow;
468 // if (!Roo.get(this.frameId)) {
471 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
472 // this.win = Roo.get(this.frameId).dom.contentWindow;
474 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
478 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
479 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
484 initEditor : function(){
485 //console.log("INIT EDITOR");
490 this.doc.designMode="on";
492 this.doc.write(this.getDocMarkup());
495 var dbody = (this.doc.body || this.doc.documentElement);
496 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
497 // this copies styles from the containing element into thsi one..
498 // not sure why we need all of this..
499 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
501 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
502 //ss['background-attachment'] = 'fixed'; // w3c
503 dbody.bgProperties = 'fixed'; // ie
504 //Roo.DomHelper.applyStyles(dbody, ss);
505 Roo.EventManager.on(this.doc, {
506 //'mousedown': this.onEditorEvent,
507 'mouseup': this.onEditorEvent,
508 'dblclick': this.onEditorEvent,
509 'click': this.onEditorEvent,
510 'keyup': this.onEditorEvent,
515 Roo.EventManager.on(this.doc, {
516 'paste': this.onPasteEvent,
520 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
523 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
524 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
526 this.initialized = true;
529 // initialize special key events - enter
530 new Roo.htmleditor.KeyEnter({core : this});
534 this.owner.fireEvent('initialize', this);
538 onPasteEvent : function(e,v)
540 // I think we better assume paste is going to be a dirty load of rubish from word..
542 // even pasting into a 'email version' of this widget will have to clean up that mess.
543 var cd = (e.browserEvent.clipboardData || window.clipboardData);
545 // check what type of paste - if it's an image, then handle it differently.
546 if (cd.files.length > 0) {
548 var urlAPI = (window.createObjectURL && window) ||
549 (window.URL && URL.revokeObjectURL && URL) ||
550 (window.webkitURL && webkitURL);
552 var url = urlAPI.createObjectURL( cd.files[0]);
553 this.insertAtCursor('<img src=" + url + ">');
557 var html = cd.getData('text/html'); // clipboard event
558 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
559 var images = parser.doc ? parser.doc.getElementsByType('pict') : [];
563 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable)/); }) // ignore headers
564 .map(function(g) { return g.toDataURL(); });
567 html = this.cleanWordChars(html);
569 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
572 var sn = this.getParentElement();
573 // check if d contains a table, and prevent nesting??
574 //Roo.log(d.getElementsByTagName('table'));
576 //Roo.log(sn.closest('table'));
577 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
579 this.insertAtCursor("You can not nest tables");
580 //Roo.log("prevent?"); // fixme -
584 if (images.length > 0) {
585 Roo.each(d.getElementsByTagName('img'), function(img, i) {
586 img.setAttribute('src', images[i]);
589 if (this.autoClean) {
590 new Roo.htmleditor.FilterStyleToTag({ node : d });
591 new Roo.htmleditor.FilterAttributes({
593 attrib_white : ['href', 'src', 'name', 'align'],
594 attrib_clean : ['href', 'src' ]
596 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
598 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT' ]} );
599 new Roo.htmleditor.FilterParagraph({ node : d });
600 new Roo.htmleditor.FilterSpan({ node : d });
601 new Roo.htmleditor.FilterLongBr({ node : d });
603 if (this.enableBlocks) {
605 Array.from(d.getElementByTagType('img')).forEach(function(img) {
606 if (img.closest('figure')) { // assume!! that it's aready
609 var fig = Roo.htmleditor.BlockFigure({
612 fig.updateElement(img); // replace it..
618 this.insertAtCursor(d.innerHTML);
619 Roo.htmleditor.Block.initAll(this.doc.body);
624 // default behaveiour should be our local cleanup paste? (optional?)
625 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
626 //this.owner.fireEvent('paste', e, v);
629 onDestroy : function(){
635 //for (var i =0; i < this.toolbars.length;i++) {
636 // // fixme - ask toolbars for heights?
637 // this.toolbars[i].onDestroy();
640 //this.wrap.dom.innerHTML = '';
641 //this.wrap.remove();
646 onFirstFocus : function(){
649 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
651 this.activated = true;
654 if(Roo.isGecko){ // prevent silly gecko errors
656 var s = this.win.getSelection();
657 if(!s.focusNode || s.focusNode.nodeType != 3){
658 var r = s.getRangeAt(0);
659 r.selectNodeContents((this.doc.body || this.doc.documentElement));
664 this.execCmd('useCSS', true);
665 this.execCmd('styleWithCSS', false);
668 this.owner.fireEvent('activate', this);
672 adjustFont: function(btn){
673 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
674 //if(Roo.isSafari){ // safari
677 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
678 if(Roo.isSafari){ // safari
679 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
680 v = (v < 10) ? 10 : v;
681 v = (v > 48) ? 48 : v;
682 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
687 v = Math.max(1, v+adjust);
689 this.execCmd('FontSize', v );
692 onEditorEvent : function(e)
695 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
696 return; // we do not handle this.. (undo manager does..)
698 // in theory this detects if the last element is not a br, then we try and do that.
699 // its so clicking in space at bottom triggers adding a br and moving the cursor.
701 e.target.nodeName == 'BODY' &&
702 e.type == "mouseup" &&
703 this.doc.body.lastChild
705 var lc = this.doc.body.lastChild;
706 // gtx-trans is google translate plugin adding crap.
707 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
708 lc = lc.previousSibling;
710 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
711 // if last element is <BR> - then dont do anything.
713 var ns = this.doc.createElement('br');
714 this.doc.body.appendChild(ns);
715 range = this.doc.createRange();
716 range.setStartAfter(ns);
717 range.collapse(true);
718 var sel = this.win.getSelection();
719 sel.removeAllRanges();
726 this.fireEditorEvent(e);
727 // this.updateToolbar();
728 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
731 fireEditorEvent: function(e)
733 this.owner.fireEvent('editorevent', this, e);
736 insertTag : function(tg)
738 // could be a bit smarter... -> wrap the current selected tRoo..
739 if (tg.toLowerCase() == 'span' ||
740 tg.toLowerCase() == 'code' ||
741 tg.toLowerCase() == 'sup' ||
742 tg.toLowerCase() == 'sub'
745 range = this.createRange(this.getSelection());
746 var wrappingNode = this.doc.createElement(tg.toLowerCase());
747 wrappingNode.appendChild(range.extractContents());
748 range.insertNode(wrappingNode);
755 this.execCmd("formatblock", tg);
756 this.undoManager.addEvent();
759 insertText : function(txt)
763 var range = this.createRange();
764 range.deleteContents();
765 //alert(Sender.getAttribute('label'));
767 range.insertNode(this.doc.createTextNode(txt));
768 this.undoManager.addEvent();
774 * Executes a Midas editor command on the editor document and performs necessary focus and
775 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
776 * @param {String} cmd The Midas command
777 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
779 relayCmd : function(cmd, value)
785 case 'justifycenter':
786 // if we are in a cell, then we will adjust the
787 var n = this.getParentElement();
788 var td = n.closest('td');
790 var bl = Roo.htmleditor.Block.factory(td);
791 bl.textAlign = cmd.replace('justify','');
793 this.owner.fireEvent('editorevent', this);
796 this.execCmd('styleWithCSS', true); //
800 // if there is no selection, then we insert, and set the curson inside it..
801 this.execCmd('styleWithCSS', false);
811 this.execCmd(cmd, value);
812 this.owner.fireEvent('editorevent', this);
813 //this.updateToolbar();
814 this.owner.deferFocus();
818 * Executes a Midas editor command directly on the editor document.
819 * For visual commands, you should use {@link #relayCmd} instead.
820 * <b>This should only be called after the editor is initialized.</b>
821 * @param {String} cmd The Midas command
822 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
824 execCmd : function(cmd, value){
825 this.doc.execCommand(cmd, false, value === undefined ? null : value);
832 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
834 * @param {String} text | dom node..
836 insertAtCursor : function(text)
843 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
847 // from jquery ui (MIT licenced)
851 if (win.getSelection && win.getSelection().getRangeAt) {
853 // delete the existing?
855 this.createRange(this.getSelection()).deleteContents();
856 range = win.getSelection().getRangeAt(0);
857 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
858 range.insertNode(node);
859 range = range.cloneRange();
860 range.collapse(false);
862 win.getSelection().removeAllRanges();
863 win.getSelection().addRange(range);
867 } else if (win.document.selection && win.document.selection.createRange) {
868 // no firefox support
869 var txt = typeof(text) == 'string' ? text : text.outerHTML;
870 win.document.selection.createRange().pasteHTML(txt);
873 // no firefox support
874 var txt = typeof(text) == 'string' ? text : text.outerHTML;
875 this.execCmd('InsertHTML', txt);
883 mozKeyPress : function(e){
885 var c = e.getCharCode(), cmd;
888 c = String.fromCharCode(c).toLowerCase();
902 // this.cleanUpPaste.defer(100, this);
920 fixKeys : function(){ // load time branching for fastest keydown performance
925 var k = e.getKey(), r;
928 r = this.doc.selection.createRange();
931 r.pasteHTML('    ');
936 /// this is handled by Roo.htmleditor.KeyEnter
939 r = this.doc.selection.createRange();
941 var target = r.parentElement();
942 if(!target || target.tagName.toLowerCase() != 'li'){
944 r.pasteHTML('<br/>');
951 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
952 // this.cleanUpPaste.defer(100, this);
958 }else if(Roo.isOpera){
964 this.execCmd('InsertHTML','    ');
968 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
969 // this.cleanUpPaste.defer(100, this);
974 }else if(Roo.isSafari){
980 this.execCmd('InsertText','\t');
986 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
987 // this.cleanUpPaste.defer(100, this);
995 getAllAncestors: function()
997 var p = this.getSelectedNode();
1000 a.push(p); // push blank onto stack..
1001 p = this.getParentElement();
1005 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1009 a.push(this.doc.body);
1013 lastSelNode : false,
1016 getSelection : function()
1018 this.assignDocWin();
1019 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1023 * @param {DomElement} node the node to select
1025 selectNode : function(node, collapse)
1027 var nodeRange = node.ownerDocument.createRange();
1029 nodeRange.selectNode(node);
1031 nodeRange.selectNodeContents(node);
1033 if (collapse === true) {
1034 nodeRange.collapse(true);
1037 var s = this.win.getSelection();
1038 s.removeAllRanges();
1039 s.addRange(nodeRange);
1042 getSelectedNode: function()
1044 // this may only work on Gecko!!!
1046 // should we cache this!!!!
1051 var range = this.createRange(this.getSelection()).cloneRange();
1054 var parent = range.parentElement();
1056 var testRange = range.duplicate();
1057 testRange.moveToElementText(parent);
1058 if (testRange.inRange(range)) {
1061 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1064 parent = parent.parentElement;
1069 // is ancestor a text element.
1070 var ac = range.commonAncestorContainer;
1071 if (ac.nodeType == 3) {
1075 var ar = ac.childNodes;
1078 var other_nodes = [];
1079 var has_other_nodes = false;
1080 for (var i=0;i<ar.length;i++) {
1081 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1084 // fullly contained node.
1086 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1091 // probably selected..
1092 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1093 other_nodes.push(ar[i]);
1097 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1102 has_other_nodes = true;
1104 if (!nodes.length && other_nodes.length) {
1107 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1113 createRange: function(sel)
1115 // this has strange effects when using with
1116 // top toolbar - not sure if it's a great idea.
1117 //this.editor.contentWindow.focus();
1118 if (typeof sel != "undefined") {
1120 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1122 return this.doc.createRange();
1125 return this.doc.createRange();
1128 getParentElement: function()
1131 this.assignDocWin();
1132 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1134 var range = this.createRange(sel);
1137 var p = range.commonAncestorContainer;
1138 while (p.nodeType == 3) { // text node
1149 * Range intersection.. the hard stuff...
1153 * [ -- selected range --- ]
1157 * if end is before start or hits it. fail.
1158 * if start is after end or hits it fail.
1160 * if either hits (but other is outside. - then it's not
1166 // @see http://www.thismuchiknow.co.uk/?p=64.
1167 rangeIntersectsNode : function(range, node)
1169 var nodeRange = node.ownerDocument.createRange();
1171 nodeRange.selectNode(node);
1173 nodeRange.selectNodeContents(node);
1176 var rangeStartRange = range.cloneRange();
1177 rangeStartRange.collapse(true);
1179 var rangeEndRange = range.cloneRange();
1180 rangeEndRange.collapse(false);
1182 var nodeStartRange = nodeRange.cloneRange();
1183 nodeStartRange.collapse(true);
1185 var nodeEndRange = nodeRange.cloneRange();
1186 nodeEndRange.collapse(false);
1188 return rangeStartRange.compareBoundaryPoints(
1189 Range.START_TO_START, nodeEndRange) == -1 &&
1190 rangeEndRange.compareBoundaryPoints(
1191 Range.START_TO_START, nodeStartRange) == 1;
1195 rangeCompareNode : function(range, node)
1197 var nodeRange = node.ownerDocument.createRange();
1199 nodeRange.selectNode(node);
1201 nodeRange.selectNodeContents(node);
1205 range.collapse(true);
1207 nodeRange.collapse(true);
1209 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1210 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1212 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1214 var nodeIsBefore = ss == 1;
1215 var nodeIsAfter = ee == -1;
1217 if (nodeIsBefore && nodeIsAfter) {
1220 if (!nodeIsBefore && nodeIsAfter) {
1221 return 1; //right trailed.
1224 if (nodeIsBefore && !nodeIsAfter) {
1225 return 2; // left trailed.
1231 cleanWordChars : function(input) {// change the chars to hex code
1234 [ 8211, "–" ],
1235 [ 8212, "—" ],
1244 Roo.each(swapCodes, function(sw) {
1245 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1247 output = output.replace(swapper, sw[1]);
1257 cleanUpChild : function (node)
1260 new Roo.htmleditor.FilterComment({node : node});
1261 new Roo.htmleditor.FilterAttributes({
1263 attrib_black : this.ablack,
1264 attrib_clean : this.aclean,
1265 style_white : this.cwhite,
1266 style_black : this.cblack
1268 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1269 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1275 * Clean up MS wordisms...
1276 * @deprecated - use filter directly
1278 cleanWord : function(node)
1280 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1287 * @deprecated - use filters
1289 cleanTableWidths : function(node)
1291 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1298 applyBlacklists : function()
1300 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1301 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1303 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1304 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1305 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1309 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1310 if (b.indexOf(tag) > -1) {
1313 this.white.push(tag);
1317 Roo.each(w, function(tag) {
1318 if (b.indexOf(tag) > -1) {
1321 if (this.white.indexOf(tag) > -1) {
1324 this.white.push(tag);
1329 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1330 if (w.indexOf(tag) > -1) {
1333 this.black.push(tag);
1337 Roo.each(b, function(tag) {
1338 if (w.indexOf(tag) > -1) {
1341 if (this.black.indexOf(tag) > -1) {
1344 this.black.push(tag);
1349 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1350 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1354 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1355 if (b.indexOf(tag) > -1) {
1358 this.cwhite.push(tag);
1362 Roo.each(w, function(tag) {
1363 if (b.indexOf(tag) > -1) {
1366 if (this.cwhite.indexOf(tag) > -1) {
1369 this.cwhite.push(tag);
1374 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1375 if (w.indexOf(tag) > -1) {
1378 this.cblack.push(tag);
1382 Roo.each(b, function(tag) {
1383 if (w.indexOf(tag) > -1) {
1386 if (this.cblack.indexOf(tag) > -1) {
1389 this.cblack.push(tag);
1394 setStylesheets : function(stylesheets)
1396 if(typeof(stylesheets) == 'string'){
1397 Roo.get(this.iframe.contentDocument.head).createChild({
1408 Roo.each(stylesheets, function(s) {
1413 Roo.get(_this.iframe.contentDocument.head).createChild({
1424 removeStylesheets : function()
1428 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1433 setStyle : function(style)
1435 Roo.get(this.iframe.contentDocument.head).createChild({
1444 // hide stuff that is not compatible
1462 * @cfg {String} fieldClass @hide
1465 * @cfg {String} focusClass @hide
1468 * @cfg {String} autoCreate @hide
1471 * @cfg {String} inputType @hide
1474 * @cfg {String} invalidClass @hide
1477 * @cfg {String} invalidText @hide
1480 * @cfg {String} msgFx @hide
1483 * @cfg {String} validateOnBlur @hide
1487 Roo.HtmlEditorCore.white = [
1488 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1490 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1491 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1492 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1493 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1494 'TABLE', 'UL', 'XMP',
1496 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1499 'DIR', 'MENU', 'OL', 'UL', 'DL',
1505 Roo.HtmlEditorCore.black = [
1506 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1508 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1509 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1510 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1511 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1512 //'FONT' // CLEAN LATER..
1513 'COLGROUP', 'COL' // messy tables.
1516 Roo.HtmlEditorCore.clean = [ // ?? needed???
1517 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1519 Roo.HtmlEditorCore.tag_remove = [
1524 Roo.HtmlEditorCore.ablack = [
1528 Roo.HtmlEditorCore.aclean = [
1529 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1533 Roo.HtmlEditorCore.pwhite= [
1534 'http', 'https', 'mailto'
1537 // white listed style attributes.
1538 Roo.HtmlEditorCore.cwhite= [
1539 // 'text-align', /// default is to allow most things..
1545 // black listed style attributes.
1546 Roo.HtmlEditorCore.cblack= [
1547 // 'font-size' -- this can be set by the project