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 if (this.undoManager) {
359 this.undoManager.addEvent();
363 var bd = (this.doc.body || this.doc.documentElement);
366 var sel = this.win.getSelection();
368 var div = document.createElement('div');
369 div.innerHTML = bd.innerHTML;
370 var gtx = div.getElementsByClassName('gtx-trans-icon'); // google translate - really annoying and difficult to get rid of.
371 if (gtx.length > 0) {
372 var rm = gtx.item(0).parentNode;
373 rm.parentNode.removeChild(rm);
377 if (this.enableBlocks) {
378 new Roo.htmleditor.FilterBlock({ node : div });
381 var tidy = new Roo.htmleditor.TidySerializer({
384 var html = tidy.serialize(div);
388 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
389 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
391 html = '<div style="'+m[0]+'">' + html + '</div>';
394 html = this.cleanHtml(html);
395 // fix up the special chars.. normaly like back quotes in word...
396 // however we do not want to do this with chinese..
397 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
399 var cc = match.charCodeAt();
401 // Get the character value, handling surrogate pairs
402 if (match.length == 2) {
403 // It's a surrogate pair, calculate the Unicode code point
404 var high = match.charCodeAt(0) - 0xD800;
405 var low = match.charCodeAt(1) - 0xDC00;
406 cc = (high * 0x400) + low + 0x10000;
408 (cc >= 0x4E00 && cc < 0xA000 ) ||
409 (cc >= 0x3400 && cc < 0x4E00 ) ||
410 (cc >= 0xf900 && cc < 0xfb00 )
415 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
416 return "&#" + cc + ";";
423 if(this.owner.fireEvent('beforesync', this, html) !== false){
424 this.el.dom.value = html;
425 this.owner.fireEvent('sync', this, html);
431 * TEXTAREA -> EDITABLE
432 * Protected method that will not generally be called directly. Pushes the value of the textarea
433 * into the iframe editor.
435 pushValue : function()
437 //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
438 if(this.initialized){
439 var v = this.el.dom.value.trim();
442 if(this.owner.fireEvent('beforepush', this, v) !== false){
443 var d = (this.doc.body || this.doc.documentElement);
446 this.el.dom.value = d.innerHTML;
447 this.owner.fireEvent('push', this, v);
449 if (this.autoClean) {
450 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
451 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
453 if (this.enableBlocks) {
454 Roo.htmleditor.Block.initAll(this.doc.body);
457 this.updateLanguage();
459 var lc = this.doc.body.lastChild;
460 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
461 // add an extra line at the end.
462 this.doc.body.appendChild(this.doc.createElement('br'));
470 deferFocus : function(){
471 this.focus.defer(10, this);
476 if(this.win && !this.sourceEditMode){
483 assignDocWin: function()
485 var iframe = this.iframe;
488 this.doc = iframe.contentWindow.document;
489 this.win = iframe.contentWindow;
491 // if (!Roo.get(this.frameId)) {
494 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
495 // this.win = Roo.get(this.frameId).dom.contentWindow;
497 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
501 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
502 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
507 initEditor : function(){
508 //console.log("INIT EDITOR");
513 this.doc.designMode="on";
515 this.doc.write(this.getDocMarkup());
518 var dbody = (this.doc.body || this.doc.documentElement);
519 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
520 // this copies styles from the containing element into thsi one..
521 // not sure why we need all of this..
522 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
524 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
525 //ss['background-attachment'] = 'fixed'; // w3c
526 dbody.bgProperties = 'fixed'; // ie
527 dbody.setAttribute("translate", "no");
529 //Roo.DomHelper.applyStyles(dbody, ss);
530 Roo.EventManager.on(this.doc, {
532 'mouseup': this.onEditorEvent,
533 'dblclick': this.onEditorEvent,
534 'click': this.onEditorEvent,
535 'keyup': this.onEditorEvent,
540 Roo.EventManager.on(this.doc, {
541 'paste': this.onPasteEvent,
545 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
548 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
549 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
551 this.initialized = true;
554 // initialize special key events - enter
555 new Roo.htmleditor.KeyEnter({core : this});
559 this.owner.fireEvent('initialize', this);
562 // this is to prevent a href clicks resulting in a redirect?
564 onPasteEvent : function(e,v)
566 // I think we better assume paste is going to be a dirty load of rubish from word..
568 // even pasting into a 'email version' of this widget will have to clean up that mess.
569 var cd = (e.browserEvent.clipboardData || window.clipboardData);
571 // check what type of paste - if it's an image, then handle it differently.
572 if (cd.files && cd.files.length > 0) {
574 var urlAPI = (window.createObjectURL && window) ||
575 (window.URL && URL.revokeObjectURL && URL) ||
576 (window.webkitURL && webkitURL);
578 var url = urlAPI.createObjectURL( cd.files[0]);
579 this.insertAtCursor('<img src=" + url + ">');
582 if (cd.types.indexOf('text/html') < 0 ) {
586 var html = cd.getData('text/html'); // clipboard event
587 if (cd.types.indexOf('text/rtf') > -1) {
588 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
589 images = parser.doc ? parser.doc.getElementsByType('pict') : [];
594 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable|footerf)/); }) // ignore headers/footers etc.
595 .map(function(g) { return g.toDataURL(); })
596 .filter(function(g) { return g != 'about:blank'; });
599 html = this.cleanWordChars(html);
601 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
604 var sn = this.getParentElement();
605 // check if d contains a table, and prevent nesting??
606 //Roo.log(d.getElementsByTagName('table'));
608 //Roo.log(sn.closest('table'));
609 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
611 this.insertAtCursor("You can not nest tables");
612 //Roo.log("prevent?"); // fixme -
618 if (images.length > 0) {
619 // replace all v:imagedata - with img.
620 var ar = Array.from(d.getElementsByTagName('v:imagedata'));
621 Roo.each(ar, function(node) {
622 node.parentNode.insertBefore(d.ownerDocument.createElement('img'), node );
623 node.parentNode.removeChild(node);
627 Roo.each(d.getElementsByTagName('img'), function(img, i) {
628 img.setAttribute('src', images[i]);
631 if (this.autoClean) {
632 new Roo.htmleditor.FilterWord({ node : d });
634 new Roo.htmleditor.FilterStyleToTag({ node : d });
635 new Roo.htmleditor.FilterAttributes({
637 attrib_white : ['href', 'src', 'name', 'align', 'colspan', 'rowspan', 'data-display', 'data-width', 'start'],
638 attrib_clean : ['href', 'src' ]
640 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
642 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', ':' ]} );
643 new Roo.htmleditor.FilterParagraph({ node : d });
644 new Roo.htmleditor.FilterSpan({ node : d });
645 new Roo.htmleditor.FilterLongBr({ node : d });
646 new Roo.htmleditor.FilterComment({ node : d });
650 if (this.enableBlocks) {
652 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
653 if (img.closest('figure')) { // assume!! that it's aready
656 var fig = new Roo.htmleditor.BlockFigure({
659 fig.updateElement(img); // replace it..
665 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
666 if (this.enableBlocks) {
667 Roo.htmleditor.Block.initAll(this.doc.body);
673 // default behaveiour should be our local cleanup paste? (optional?)
674 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
675 //this.owner.fireEvent('paste', e, v);
678 onDestroy : function(){
684 //for (var i =0; i < this.toolbars.length;i++) {
685 // // fixme - ask toolbars for heights?
686 // this.toolbars[i].onDestroy();
689 //this.wrap.dom.innerHTML = '';
690 //this.wrap.remove();
695 onFirstFocus : function(){
698 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
700 this.activated = true;
703 if(Roo.isGecko){ // prevent silly gecko errors
705 var s = this.win.getSelection();
706 if(!s.focusNode || s.focusNode.nodeType != 3){
707 var r = s.getRangeAt(0);
708 r.selectNodeContents((this.doc.body || this.doc.documentElement));
713 this.execCmd('useCSS', true);
714 this.execCmd('styleWithCSS', false);
717 this.owner.fireEvent('activate', this);
721 adjustFont: function(btn){
722 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
723 //if(Roo.isSafari){ // safari
726 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
727 if(Roo.isSafari){ // safari
728 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
729 v = (v < 10) ? 10 : v;
730 v = (v > 48) ? 48 : v;
731 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
736 v = Math.max(1, v+adjust);
738 this.execCmd('FontSize', v );
741 onEditorEvent : function(e)
745 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
746 return; // we do not handle this.. (undo manager does..)
748 // in theory this detects if the last element is not a br, then we try and do that.
749 // its so clicking in space at bottom triggers adding a br and moving the cursor.
751 e.target.nodeName == 'BODY' &&
752 e.type == "mouseup" &&
753 this.doc.body.lastChild
755 var lc = this.doc.body.lastChild;
756 // gtx-trans is google translate plugin adding crap.
757 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
758 lc = lc.previousSibling;
760 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
761 // if last element is <BR> - then dont do anything.
763 var ns = this.doc.createElement('br');
764 this.doc.body.appendChild(ns);
765 range = this.doc.createRange();
766 range.setStartAfter(ns);
767 range.collapse(true);
768 var sel = this.win.getSelection();
769 sel.removeAllRanges();
776 this.fireEditorEvent(e);
777 // this.updateToolbar();
778 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
781 fireEditorEvent: function(e)
783 this.owner.fireEvent('editorevent', this, e);
786 insertTag : function(tg)
788 // could be a bit smarter... -> wrap the current selected tRoo..
789 if (tg.toLowerCase() == 'span' ||
790 tg.toLowerCase() == 'code' ||
791 tg.toLowerCase() == 'sup' ||
792 tg.toLowerCase() == 'sub'
795 range = this.createRange(this.getSelection());
796 var wrappingNode = this.doc.createElement(tg.toLowerCase());
797 wrappingNode.appendChild(range.extractContents());
798 range.insertNode(wrappingNode);
805 this.execCmd("formatblock", tg);
806 this.undoManager.addEvent();
809 insertText : function(txt)
813 var range = this.createRange();
814 range.deleteContents();
815 //alert(Sender.getAttribute('label'));
817 range.insertNode(this.doc.createTextNode(txt));
818 this.undoManager.addEvent();
824 * Executes a Midas editor command on the editor document and performs necessary focus and
825 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
826 * @param {String} cmd The Midas command
827 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
829 relayCmd : function(cmd, value)
835 case 'justifycenter':
836 // if we are in a cell, then we will adjust the
837 var n = this.getParentElement();
838 var td = n.closest('td');
840 var bl = Roo.htmleditor.Block.factory(td);
841 bl.textAlign = cmd.replace('justify','');
843 this.owner.fireEvent('editorevent', this);
846 this.execCmd('styleWithCSS', true); //
850 // if there is no selection, then we insert, and set the curson inside it..
851 this.execCmd('styleWithCSS', false);
861 this.execCmd(cmd, value);
862 this.owner.fireEvent('editorevent', this);
863 //this.updateToolbar();
864 this.owner.deferFocus();
868 * Executes a Midas editor command directly on the editor document.
869 * For visual commands, you should use {@link #relayCmd} instead.
870 * <b>This should only be called after the editor is initialized.</b>
871 * @param {String} cmd The Midas command
872 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
874 execCmd : function(cmd, value){
875 this.doc.execCommand(cmd, false, value === undefined ? null : value);
882 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
884 * @param {String} text | dom node..
886 insertAtCursor : function(text)
893 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
897 // from jquery ui (MIT licenced)
901 if (win.getSelection && win.getSelection().getRangeAt) {
903 // delete the existing?
905 this.createRange(this.getSelection()).deleteContents();
906 range = win.getSelection().getRangeAt(0);
907 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
908 range.insertNode(node);
909 range = range.cloneRange();
910 range.collapse(false);
912 win.getSelection().removeAllRanges();
913 win.getSelection().addRange(range);
917 } else if (win.document.selection && win.document.selection.createRange) {
918 // no firefox support
919 var txt = typeof(text) == 'string' ? text : text.outerHTML;
920 win.document.selection.createRange().pasteHTML(txt);
923 // no firefox support
924 var txt = typeof(text) == 'string' ? text : text.outerHTML;
925 this.execCmd('InsertHTML', txt);
933 mozKeyPress : function(e){
935 var c = e.getCharCode(), cmd;
938 c = String.fromCharCode(c).toLowerCase();
952 // this.cleanUpPaste.defer(100, this);
970 fixKeys : function(){ // load time branching for fastest keydown performance
975 var k = e.getKey(), r;
978 r = this.doc.selection.createRange();
981 r.pasteHTML('    ');
986 /// this is handled by Roo.htmleditor.KeyEnter
989 r = this.doc.selection.createRange();
991 var target = r.parentElement();
992 if(!target || target.tagName.toLowerCase() != 'li'){
994 r.pasteHTML('<br/>');
1001 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1002 // this.cleanUpPaste.defer(100, this);
1008 }else if(Roo.isOpera){
1014 this.execCmd('InsertHTML','    ');
1018 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1019 // this.cleanUpPaste.defer(100, this);
1024 }else if(Roo.isSafari){
1030 this.execCmd('InsertText','\t');
1034 this.mozKeyPress(e);
1036 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1037 // this.cleanUpPaste.defer(100, this);
1045 getAllAncestors: function()
1047 var p = this.getSelectedNode();
1050 a.push(p); // push blank onto stack..
1051 p = this.getParentElement();
1055 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1059 a.push(this.doc.body);
1063 lastSelNode : false,
1066 getSelection : function()
1068 this.assignDocWin();
1069 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1073 * @param {DomElement} node the node to select
1075 selectNode : function(node, collapse)
1077 var nodeRange = node.ownerDocument.createRange();
1079 nodeRange.selectNode(node);
1081 nodeRange.selectNodeContents(node);
1083 if (collapse === true) {
1084 nodeRange.collapse(true);
1087 var s = this.win.getSelection();
1088 s.removeAllRanges();
1089 s.addRange(nodeRange);
1092 getSelectedNode: function()
1094 // this may only work on Gecko!!!
1096 // should we cache this!!!!
1100 var range = this.createRange(this.getSelection()).cloneRange();
1103 var parent = range.parentElement();
1105 var testRange = range.duplicate();
1106 testRange.moveToElementText(parent);
1107 if (testRange.inRange(range)) {
1110 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1113 parent = parent.parentElement;
1118 // is ancestor a text element.
1119 var ac = range.commonAncestorContainer;
1120 if (ac.nodeType == 3) {
1124 var ar = ac.childNodes;
1127 var other_nodes = [];
1128 var has_other_nodes = false;
1129 for (var i=0;i<ar.length;i++) {
1130 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1133 // fullly contained node.
1135 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1140 // probably selected..
1141 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1142 other_nodes.push(ar[i]);
1146 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1151 has_other_nodes = true;
1153 if (!nodes.length && other_nodes.length) {
1156 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1164 createRange: function(sel)
1166 // this has strange effects when using with
1167 // top toolbar - not sure if it's a great idea.
1168 //this.editor.contentWindow.focus();
1169 if (typeof sel != "undefined") {
1171 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1173 return this.doc.createRange();
1176 return this.doc.createRange();
1179 getParentElement: function()
1182 this.assignDocWin();
1183 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1185 var range = this.createRange(sel);
1188 var p = range.commonAncestorContainer;
1189 while (p.nodeType == 3) { // text node
1200 * Range intersection.. the hard stuff...
1204 * [ -- selected range --- ]
1208 * if end is before start or hits it. fail.
1209 * if start is after end or hits it fail.
1211 * if either hits (but other is outside. - then it's not
1217 // @see http://www.thismuchiknow.co.uk/?p=64.
1218 rangeIntersectsNode : function(range, node)
1220 var nodeRange = node.ownerDocument.createRange();
1222 nodeRange.selectNode(node);
1224 nodeRange.selectNodeContents(node);
1227 var rangeStartRange = range.cloneRange();
1228 rangeStartRange.collapse(true);
1230 var rangeEndRange = range.cloneRange();
1231 rangeEndRange.collapse(false);
1233 var nodeStartRange = nodeRange.cloneRange();
1234 nodeStartRange.collapse(true);
1236 var nodeEndRange = nodeRange.cloneRange();
1237 nodeEndRange.collapse(false);
1239 return rangeStartRange.compareBoundaryPoints(
1240 Range.START_TO_START, nodeEndRange) == -1 &&
1241 rangeEndRange.compareBoundaryPoints(
1242 Range.START_TO_START, nodeStartRange) == 1;
1246 rangeCompareNode : function(range, node)
1248 var nodeRange = node.ownerDocument.createRange();
1250 nodeRange.selectNode(node);
1252 nodeRange.selectNodeContents(node);
1256 range.collapse(true);
1258 nodeRange.collapse(true);
1260 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1261 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1263 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1265 var nodeIsBefore = ss == 1;
1266 var nodeIsAfter = ee == -1;
1268 if (nodeIsBefore && nodeIsAfter) {
1271 if (!nodeIsBefore && nodeIsAfter) {
1272 return 1; //right trailed.
1275 if (nodeIsBefore && !nodeIsAfter) {
1276 return 2; // left trailed.
1282 cleanWordChars : function(input) {// change the chars to hex code
1285 [ 8211, "–" ],
1286 [ 8212, "—" ],
1295 Roo.each(swapCodes, function(sw) {
1296 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1298 output = output.replace(swapper, sw[1]);
1308 cleanUpChild : function (node)
1311 new Roo.htmleditor.FilterComment({node : node});
1312 new Roo.htmleditor.FilterAttributes({
1314 attrib_black : this.ablack,
1315 attrib_clean : this.aclean,
1316 style_white : this.cwhite,
1317 style_black : this.cblack
1319 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1320 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1326 * Clean up MS wordisms...
1327 * @deprecated - use filter directly
1329 cleanWord : function(node)
1331 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1332 new Roo.htmleditor.FilterKeepChildren({node : node ? node : this.doc.body, tag : [ 'FONT', ':' ]} );
1339 * @deprecated - use filters
1341 cleanTableWidths : function(node)
1343 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1350 applyBlacklists : function()
1352 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1353 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1355 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1356 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1357 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1361 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1362 if (b.indexOf(tag) > -1) {
1365 this.white.push(tag);
1369 Roo.each(w, function(tag) {
1370 if (b.indexOf(tag) > -1) {
1373 if (this.white.indexOf(tag) > -1) {
1376 this.white.push(tag);
1381 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1382 if (w.indexOf(tag) > -1) {
1385 this.black.push(tag);
1389 Roo.each(b, function(tag) {
1390 if (w.indexOf(tag) > -1) {
1393 if (this.black.indexOf(tag) > -1) {
1396 this.black.push(tag);
1401 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1402 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1406 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1407 if (b.indexOf(tag) > -1) {
1410 this.cwhite.push(tag);
1414 Roo.each(w, function(tag) {
1415 if (b.indexOf(tag) > -1) {
1418 if (this.cwhite.indexOf(tag) > -1) {
1421 this.cwhite.push(tag);
1426 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1427 if (w.indexOf(tag) > -1) {
1430 this.cblack.push(tag);
1434 Roo.each(b, function(tag) {
1435 if (w.indexOf(tag) > -1) {
1438 if (this.cblack.indexOf(tag) > -1) {
1441 this.cblack.push(tag);
1446 setStylesheets : function(stylesheets)
1448 if(typeof(stylesheets) == 'string'){
1449 Roo.get(this.iframe.contentDocument.head).createChild({
1460 Roo.each(stylesheets, function(s) {
1465 Roo.get(_this.iframe.contentDocument.head).createChild({
1477 updateLanguage : function()
1479 if (!this.iframe || !this.iframe.contentDocument) {
1482 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1486 removeStylesheets : function()
1490 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1495 setStyle : function(style)
1497 Roo.get(this.iframe.contentDocument.head).createChild({
1506 // hide stuff that is not compatible
1524 * @cfg {String} fieldClass @hide
1527 * @cfg {String} focusClass @hide
1530 * @cfg {String} autoCreate @hide
1533 * @cfg {String} inputType @hide
1536 * @cfg {String} invalidClass @hide
1539 * @cfg {String} invalidText @hide
1542 * @cfg {String} msgFx @hide
1545 * @cfg {String} validateOnBlur @hide
1549 Roo.HtmlEditorCore.white = [
1550 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1552 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1553 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1554 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1555 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1556 'TABLE', 'UL', 'XMP',
1558 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1561 'DIR', 'MENU', 'OL', 'UL', 'DL',
1567 Roo.HtmlEditorCore.black = [
1568 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1570 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1571 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1572 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1573 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1574 //'FONT' // CLEAN LATER..
1575 'COLGROUP', 'COL' // messy tables.
1579 Roo.HtmlEditorCore.clean = [ // ?? needed???
1580 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1582 Roo.HtmlEditorCore.tag_remove = [
1587 Roo.HtmlEditorCore.ablack = [
1591 Roo.HtmlEditorCore.aclean = [
1592 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1596 Roo.HtmlEditorCore.pwhite= [
1597 'http', 'https', 'mailto'
1600 // white listed style attributes.
1601 Roo.HtmlEditorCore.cwhite= [
1602 // 'text-align', /// default is to allow most things..
1608 // black listed style attributes.
1609 Roo.HtmlEditorCore.cblack= [
1610 // 'font-size' -- this can be set by the project