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, {
513 'mousedown': this.onMouseDown,
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)
547 if (e.target.closest('a')) {
554 onPasteEvent : function(e,v)
556 // I think we better assume paste is going to be a dirty load of rubish from word..
558 // even pasting into a 'email version' of this widget will have to clean up that mess.
559 var cd = (e.browserEvent.clipboardData || window.clipboardData);
561 // check what type of paste - if it's an image, then handle it differently.
562 if (cd.files.length > 0) {
564 var urlAPI = (window.createObjectURL && window) ||
565 (window.URL && URL.revokeObjectURL && URL) ||
566 (window.webkitURL && webkitURL);
568 var url = urlAPI.createObjectURL( cd.files[0]);
569 this.insertAtCursor('<img src=" + url + ">');
573 var html = cd.getData('text/html'); // clipboard event
574 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
575 var images = parser.doc ? parser.doc.getElementsByType('pict') : [];
579 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable)/); }) // ignore headers
580 .map(function(g) { return g.toDataURL(); });
583 html = this.cleanWordChars(html);
585 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
588 var sn = this.getParentElement();
589 // check if d contains a table, and prevent nesting??
590 //Roo.log(d.getElementsByTagName('table'));
592 //Roo.log(sn.closest('table'));
593 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
595 this.insertAtCursor("You can not nest tables");
596 //Roo.log("prevent?"); // fixme -
600 if (images.length > 0) {
601 Roo.each(d.getElementsByTagName('img'), function(img, i) {
602 img.setAttribute('src', images[i]);
605 if (this.autoClean) {
606 new Roo.htmleditor.FilterStyleToTag({ node : d });
607 new Roo.htmleditor.FilterAttributes({
609 attrib_white : ['href', 'src', 'name', 'align'],
610 attrib_clean : ['href', 'src' ]
612 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
614 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT' ]} );
615 new Roo.htmleditor.FilterParagraph({ node : d });
616 new Roo.htmleditor.FilterSpan({ node : d });
617 new Roo.htmleditor.FilterLongBr({ node : d });
619 if (this.enableBlocks) {
621 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
622 if (img.closest('figure')) { // assume!! that it's aready
625 var fig = new Roo.htmleditor.BlockFigure({
628 fig.updateElement(img); // replace it..
634 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
635 if (this.enableBlocks) {
636 Roo.htmleditor.Block.initAll(this.doc.body);
642 // default behaveiour should be our local cleanup paste? (optional?)
643 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
644 //this.owner.fireEvent('paste', e, v);
647 onDestroy : function(){
653 //for (var i =0; i < this.toolbars.length;i++) {
654 // // fixme - ask toolbars for heights?
655 // this.toolbars[i].onDestroy();
658 //this.wrap.dom.innerHTML = '';
659 //this.wrap.remove();
664 onFirstFocus : function(){
667 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
669 this.activated = true;
672 if(Roo.isGecko){ // prevent silly gecko errors
674 var s = this.win.getSelection();
675 if(!s.focusNode || s.focusNode.nodeType != 3){
676 var r = s.getRangeAt(0);
677 r.selectNodeContents((this.doc.body || this.doc.documentElement));
682 this.execCmd('useCSS', true);
683 this.execCmd('styleWithCSS', false);
686 this.owner.fireEvent('activate', this);
690 adjustFont: function(btn){
691 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
692 //if(Roo.isSafari){ // safari
695 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
696 if(Roo.isSafari){ // safari
697 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
698 v = (v < 10) ? 10 : v;
699 v = (v > 48) ? 48 : v;
700 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
705 v = Math.max(1, v+adjust);
707 this.execCmd('FontSize', v );
710 onEditorEvent : function(e)
713 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
714 return; // we do not handle this.. (undo manager does..)
716 // in theory this detects if the last element is not a br, then we try and do that.
717 // its so clicking in space at bottom triggers adding a br and moving the cursor.
719 e.target.nodeName == 'BODY' &&
720 e.type == "mouseup" &&
721 this.doc.body.lastChild
723 var lc = this.doc.body.lastChild;
724 // gtx-trans is google translate plugin adding crap.
725 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
726 lc = lc.previousSibling;
728 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
729 // if last element is <BR> - then dont do anything.
731 var ns = this.doc.createElement('br');
732 this.doc.body.appendChild(ns);
733 range = this.doc.createRange();
734 range.setStartAfter(ns);
735 range.collapse(true);
736 var sel = this.win.getSelection();
737 sel.removeAllRanges();
744 this.fireEditorEvent(e);
745 // this.updateToolbar();
746 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
749 fireEditorEvent: function(e)
751 this.owner.fireEvent('editorevent', this, e);
754 insertTag : function(tg)
756 // could be a bit smarter... -> wrap the current selected tRoo..
757 if (tg.toLowerCase() == 'span' ||
758 tg.toLowerCase() == 'code' ||
759 tg.toLowerCase() == 'sup' ||
760 tg.toLowerCase() == 'sub'
763 range = this.createRange(this.getSelection());
764 var wrappingNode = this.doc.createElement(tg.toLowerCase());
765 wrappingNode.appendChild(range.extractContents());
766 range.insertNode(wrappingNode);
773 this.execCmd("formatblock", tg);
774 this.undoManager.addEvent();
777 insertText : function(txt)
781 var range = this.createRange();
782 range.deleteContents();
783 //alert(Sender.getAttribute('label'));
785 range.insertNode(this.doc.createTextNode(txt));
786 this.undoManager.addEvent();
792 * Executes a Midas editor command on the editor document and performs necessary focus and
793 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
794 * @param {String} cmd The Midas command
795 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
797 relayCmd : function(cmd, value)
803 case 'justifycenter':
804 // if we are in a cell, then we will adjust the
805 var n = this.getParentElement();
806 var td = n.closest('td');
808 var bl = Roo.htmleditor.Block.factory(td);
809 bl.textAlign = cmd.replace('justify','');
811 this.owner.fireEvent('editorevent', this);
814 this.execCmd('styleWithCSS', true); //
818 // if there is no selection, then we insert, and set the curson inside it..
819 this.execCmd('styleWithCSS', false);
829 this.execCmd(cmd, value);
830 this.owner.fireEvent('editorevent', this);
831 //this.updateToolbar();
832 this.owner.deferFocus();
836 * Executes a Midas editor command directly on the editor document.
837 * For visual commands, you should use {@link #relayCmd} instead.
838 * <b>This should only be called after the editor is initialized.</b>
839 * @param {String} cmd The Midas command
840 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
842 execCmd : function(cmd, value){
843 this.doc.execCommand(cmd, false, value === undefined ? null : value);
850 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
852 * @param {String} text | dom node..
854 insertAtCursor : function(text)
861 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
865 // from jquery ui (MIT licenced)
869 if (win.getSelection && win.getSelection().getRangeAt) {
871 // delete the existing?
873 this.createRange(this.getSelection()).deleteContents();
874 range = win.getSelection().getRangeAt(0);
875 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
876 range.insertNode(node);
877 range = range.cloneRange();
878 range.collapse(false);
880 win.getSelection().removeAllRanges();
881 win.getSelection().addRange(range);
885 } else if (win.document.selection && win.document.selection.createRange) {
886 // no firefox support
887 var txt = typeof(text) == 'string' ? text : text.outerHTML;
888 win.document.selection.createRange().pasteHTML(txt);
891 // no firefox support
892 var txt = typeof(text) == 'string' ? text : text.outerHTML;
893 this.execCmd('InsertHTML', txt);
901 mozKeyPress : function(e){
903 var c = e.getCharCode(), cmd;
906 c = String.fromCharCode(c).toLowerCase();
920 // this.cleanUpPaste.defer(100, this);
938 fixKeys : function(){ // load time branching for fastest keydown performance
943 var k = e.getKey(), r;
946 r = this.doc.selection.createRange();
949 r.pasteHTML('    ');
954 /// this is handled by Roo.htmleditor.KeyEnter
957 r = this.doc.selection.createRange();
959 var target = r.parentElement();
960 if(!target || target.tagName.toLowerCase() != 'li'){
962 r.pasteHTML('<br/>');
969 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
970 // this.cleanUpPaste.defer(100, this);
976 }else if(Roo.isOpera){
982 this.execCmd('InsertHTML','    ');
986 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
987 // this.cleanUpPaste.defer(100, this);
992 }else if(Roo.isSafari){
998 this.execCmd('InsertText','\t');
1002 this.mozKeyPress(e);
1004 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1005 // this.cleanUpPaste.defer(100, this);
1013 getAllAncestors: function()
1015 var p = this.getSelectedNode();
1018 a.push(p); // push blank onto stack..
1019 p = this.getParentElement();
1023 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1027 a.push(this.doc.body);
1031 lastSelNode : false,
1034 getSelection : function()
1036 this.assignDocWin();
1037 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1041 * @param {DomElement} node the node to select
1043 selectNode : function(node, collapse)
1045 var nodeRange = node.ownerDocument.createRange();
1047 nodeRange.selectNode(node);
1049 nodeRange.selectNodeContents(node);
1051 if (collapse === true) {
1052 nodeRange.collapse(true);
1055 var s = this.win.getSelection();
1056 s.removeAllRanges();
1057 s.addRange(nodeRange);
1060 getSelectedNode: function()
1062 // this may only work on Gecko!!!
1064 // should we cache this!!!!
1068 var range = this.createRange(this.getSelection()).cloneRange();
1071 var parent = range.parentElement();
1073 var testRange = range.duplicate();
1074 testRange.moveToElementText(parent);
1075 if (testRange.inRange(range)) {
1078 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1081 parent = parent.parentElement;
1086 // is ancestor a text element.
1087 var ac = range.commonAncestorContainer;
1088 if (ac.nodeType == 3) {
1092 var ar = ac.childNodes;
1095 var other_nodes = [];
1096 var has_other_nodes = false;
1097 for (var i=0;i<ar.length;i++) {
1098 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1101 // fullly contained node.
1103 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1108 // probably selected..
1109 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1110 other_nodes.push(ar[i]);
1114 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1119 has_other_nodes = true;
1121 if (!nodes.length && other_nodes.length) {
1124 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1132 createRange: function(sel)
1134 // this has strange effects when using with
1135 // top toolbar - not sure if it's a great idea.
1136 //this.editor.contentWindow.focus();
1137 if (typeof sel != "undefined") {
1139 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1141 return this.doc.createRange();
1144 return this.doc.createRange();
1147 getParentElement: function()
1150 this.assignDocWin();
1151 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1153 var range = this.createRange(sel);
1156 var p = range.commonAncestorContainer;
1157 while (p.nodeType == 3) { // text node
1168 * Range intersection.. the hard stuff...
1172 * [ -- selected range --- ]
1176 * if end is before start or hits it. fail.
1177 * if start is after end or hits it fail.
1179 * if either hits (but other is outside. - then it's not
1185 // @see http://www.thismuchiknow.co.uk/?p=64.
1186 rangeIntersectsNode : function(range, node)
1188 var nodeRange = node.ownerDocument.createRange();
1190 nodeRange.selectNode(node);
1192 nodeRange.selectNodeContents(node);
1195 var rangeStartRange = range.cloneRange();
1196 rangeStartRange.collapse(true);
1198 var rangeEndRange = range.cloneRange();
1199 rangeEndRange.collapse(false);
1201 var nodeStartRange = nodeRange.cloneRange();
1202 nodeStartRange.collapse(true);
1204 var nodeEndRange = nodeRange.cloneRange();
1205 nodeEndRange.collapse(false);
1207 return rangeStartRange.compareBoundaryPoints(
1208 Range.START_TO_START, nodeEndRange) == -1 &&
1209 rangeEndRange.compareBoundaryPoints(
1210 Range.START_TO_START, nodeStartRange) == 1;
1214 rangeCompareNode : function(range, node)
1216 var nodeRange = node.ownerDocument.createRange();
1218 nodeRange.selectNode(node);
1220 nodeRange.selectNodeContents(node);
1224 range.collapse(true);
1226 nodeRange.collapse(true);
1228 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1229 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1231 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1233 var nodeIsBefore = ss == 1;
1234 var nodeIsAfter = ee == -1;
1236 if (nodeIsBefore && nodeIsAfter) {
1239 if (!nodeIsBefore && nodeIsAfter) {
1240 return 1; //right trailed.
1243 if (nodeIsBefore && !nodeIsAfter) {
1244 return 2; // left trailed.
1250 cleanWordChars : function(input) {// change the chars to hex code
1253 [ 8211, "–" ],
1254 [ 8212, "—" ],
1263 Roo.each(swapCodes, function(sw) {
1264 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1266 output = output.replace(swapper, sw[1]);
1276 cleanUpChild : function (node)
1279 new Roo.htmleditor.FilterComment({node : node});
1280 new Roo.htmleditor.FilterAttributes({
1282 attrib_black : this.ablack,
1283 attrib_clean : this.aclean,
1284 style_white : this.cwhite,
1285 style_black : this.cblack
1287 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1288 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1294 * Clean up MS wordisms...
1295 * @deprecated - use filter directly
1297 cleanWord : function(node)
1299 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1306 * @deprecated - use filters
1308 cleanTableWidths : function(node)
1310 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1317 applyBlacklists : function()
1319 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1320 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1322 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1323 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1324 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1328 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1329 if (b.indexOf(tag) > -1) {
1332 this.white.push(tag);
1336 Roo.each(w, function(tag) {
1337 if (b.indexOf(tag) > -1) {
1340 if (this.white.indexOf(tag) > -1) {
1343 this.white.push(tag);
1348 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1349 if (w.indexOf(tag) > -1) {
1352 this.black.push(tag);
1356 Roo.each(b, function(tag) {
1357 if (w.indexOf(tag) > -1) {
1360 if (this.black.indexOf(tag) > -1) {
1363 this.black.push(tag);
1368 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1369 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1373 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1374 if (b.indexOf(tag) > -1) {
1377 this.cwhite.push(tag);
1381 Roo.each(w, function(tag) {
1382 if (b.indexOf(tag) > -1) {
1385 if (this.cwhite.indexOf(tag) > -1) {
1388 this.cwhite.push(tag);
1393 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1394 if (w.indexOf(tag) > -1) {
1397 this.cblack.push(tag);
1401 Roo.each(b, function(tag) {
1402 if (w.indexOf(tag) > -1) {
1405 if (this.cblack.indexOf(tag) > -1) {
1408 this.cblack.push(tag);
1413 setStylesheets : function(stylesheets)
1415 if(typeof(stylesheets) == 'string'){
1416 Roo.get(this.iframe.contentDocument.head).createChild({
1427 Roo.each(stylesheets, function(s) {
1432 Roo.get(_this.iframe.contentDocument.head).createChild({
1444 updateLanguage : function()
1446 if (!this.iframe || !this.iframe.contentDocument) {
1449 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1453 removeStylesheets : function()
1457 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1462 setStyle : function(style)
1464 Roo.get(this.iframe.contentDocument.head).createChild({
1473 // hide stuff that is not compatible
1491 * @cfg {String} fieldClass @hide
1494 * @cfg {String} focusClass @hide
1497 * @cfg {String} autoCreate @hide
1500 * @cfg {String} inputType @hide
1503 * @cfg {String} invalidClass @hide
1506 * @cfg {String} invalidText @hide
1509 * @cfg {String} msgFx @hide
1512 * @cfg {String} validateOnBlur @hide
1516 Roo.HtmlEditorCore.white = [
1517 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1519 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1520 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1521 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1522 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1523 'TABLE', 'UL', 'XMP',
1525 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1528 'DIR', 'MENU', 'OL', 'UL', 'DL',
1534 Roo.HtmlEditorCore.black = [
1535 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1537 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1538 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1539 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1540 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1541 //'FONT' // CLEAN LATER..
1542 'COLGROUP', 'COL' // messy tables.
1545 Roo.HtmlEditorCore.clean = [ // ?? needed???
1546 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1548 Roo.HtmlEditorCore.tag_remove = [
1553 Roo.HtmlEditorCore.ablack = [
1557 Roo.HtmlEditorCore.aclean = [
1558 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1562 Roo.HtmlEditorCore.pwhite= [
1563 'http', 'https', 'mailto'
1566 // white listed style attributes.
1567 Roo.HtmlEditorCore.cwhite= [
1568 // 'text-align', /// default is to allow most things..
1574 // black listed style attributes.
1575 Roo.HtmlEditorCore.cblack= [
1576 // 'font-size' -- this can be set by the project