1 //<script type="text/javascript">
4 * Based Ext JS Library 1.1.1
5 * Copyright(c) 2006-2007, Ext JS, LLC.
11 * @class Roo.HtmlEditorCore
12 * @extends Roo.Component
13 * Provides a the editing component for the HTML editors in Roo. (bootstrap and Roo.form)
15 * any element that has display set to 'none' can cause problems in Safari and Firefox.<br/><br/>
18 Roo.HtmlEditorCore = function(config){
21 Roo.HtmlEditorCore.superclass.constructor.call(this, config);
27 * Fires when the editor is fully initialized (including the iframe)
28 * @param {Roo.HtmlEditorCore} this
33 * Fires when the editor is first receives the focus. Any insertion must wait
34 * until after this event.
35 * @param {Roo.HtmlEditorCore} this
40 * Fires before the textarea is updated with content from the editor iframe. Return false
42 * @param {Roo.HtmlEditorCore} this
43 * @param {String} html
48 * Fires before the iframe editor is updated with content from the textarea. Return false
50 * @param {Roo.HtmlEditorCore} this
51 * @param {String} html
56 * Fires when the textarea is updated with content from the editor iframe.
57 * @param {Roo.HtmlEditorCore} this
58 * @param {String} html
63 * Fires when the iframe editor is updated with content from the textarea.
64 * @param {Roo.HtmlEditorCore} this
65 * @param {String} html
71 * Fires when on any editor (mouse up/down cursor movement etc.) - used for toolbar hooks.
72 * @param {Roo.HtmlEditorCore} this
79 // at this point this.owner is set, so we can start working out the whitelisted / blacklisted elements
81 // defaults : white / black...
82 this.applyBlacklists();
89 Roo.extend(Roo.HtmlEditorCore, Roo.Component, {
93 * @cfg {Roo.form.HtmlEditor|Roo.bootstrap.HtmlEditor} the owner field
99 * @cfg {String} resizable 's' or 'se' or 'e' - wrapps the element in a
104 * @cfg {Number} height (in pixels)
108 * @cfg {Number} width (in pixels)
112 * @cfg {boolean} autoClean - default true - loading and saving will remove quite a bit of formating,
113 * if you are doing an email editor, this probably needs disabling, it's designed
118 * @cfg {boolean} enableBlocks - default true - if the block editor (table and figure should be enabled)
122 * @cfg {Array} stylesheets url of stylesheets. set to [] to disable stylesheets.
127 * @cfg {String} language default en - language of text (usefull for rtl languages)
133 * @cfg {boolean} allowComments - default false - allow comments in HTML source
134 * - by default they are stripped - if you are editing email you may need this.
136 allowComments: false,
140 // private properties
141 validationEvent : false,
145 sourceEditMode : false,
146 onFocus : Roo.emptyFn,
152 // blacklist + whitelisted elements..
161 * Protected method that will not generally be called directly. It
162 * is called when the editor initializes the iframe with HTML contents. Override this method if you
163 * want to change the initialization markup of the iframe (e.g. to add stylesheets).
165 getDocMarkup : function(){
169 // inherit styels from page...??
170 if (this.stylesheets === false) {
172 Roo.get(document.head).select('style').each(function(node) {
173 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
176 Roo.get(document.head).select('link').each(function(node) {
177 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
180 } else if (!this.stylesheets.length) {
182 st = '<style type="text/css">' +
183 'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
186 for (var i in this.stylesheets) {
187 if (typeof(this.stylesheets[i]) != 'string') {
190 st += '<link rel="stylesheet" href="' + this.stylesheets[i] +'" type="text/css">';
195 st += '<style type="text/css">' +
196 'IMG { cursor: pointer } ' +
199 var cls = 'roo-htmleditor-body';
201 if(this.bodyCls.length){
202 cls += ' ' + this.bodyCls;
205 return '<html><head>' + st +
206 //<style type="text/css">' +
207 //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
209 ' </head><body contenteditable="true" data-enable-grammerly="true" class="' + cls + '"></body></html>';
213 onRender : function(ct, position)
216 //Roo.HtmlEditorCore.superclass.onRender.call(this, ct, position);
217 this.el = this.owner.inputEl ? this.owner.inputEl() : this.owner.el;
220 this.el.dom.style.border = '0 none';
221 this.el.dom.setAttribute('tabIndex', -1);
222 this.el.addClass('x-hidden hide');
226 if(Roo.isIE){ // fix IE 1px bogus margin
227 this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
231 this.frameId = Roo.id();
235 var iframe = this.owner.wrap.createChild({
237 cls: 'form-control', // bootstrap..
241 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
246 this.iframe = iframe.dom;
250 this.doc.designMode = 'on';
253 this.doc.write(this.getDocMarkup());
257 var task = { // must defer to wait for browser to be ready
259 //console.log("run task?" + this.doc.readyState);
261 if(this.doc.body || this.doc.readyState == 'complete'){
263 this.doc.designMode="on";
268 Roo.TaskMgr.stop(task);
269 this.initEditor.defer(10, this);
276 Roo.TaskMgr.start(task);
281 onResize : function(w, h)
283 Roo.log('resize: ' +w + ',' + h );
284 //Roo.HtmlEditorCore.superclass.onResize.apply(this, arguments);
288 if(typeof w == 'number'){
290 this.iframe.style.width = w + 'px';
292 if(typeof h == 'number'){
294 this.iframe.style.height = h + 'px';
296 (this.doc.body || this.doc.documentElement).style.height = (h - (this.iframePad*2)) + 'px';
303 * Toggles the editor between standard and source edit mode.
304 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
306 toggleSourceEdit : function(sourceEditMode){
308 this.sourceEditMode = sourceEditMode === true;
310 if(this.sourceEditMode){
312 Roo.get(this.iframe).addClass(['x-hidden','hide', 'd-none']); //FIXME - what's the BS styles for these
315 Roo.get(this.iframe).removeClass(['x-hidden','hide', 'd-none']);
316 //this.iframe.className = '';
319 //this.setSize(this.owner.wrap.getSize());
320 //this.fireEvent('editmodechange', this, this.sourceEditMode);
327 * Protected method that will not generally be called directly. If you need/want
328 * custom HTML cleanup, this is the method you should override.
329 * @param {String} html The HTML to be cleaned
330 * return {String} The cleaned HTML
332 cleanHtml : function(html){
335 if(Roo.isSafari){ // strip safari nonsense
336 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
339 if(html == ' '){
346 * HTML Editor -> Textarea
347 * Protected method that will not generally be called directly. Syncs the contents
348 * of the editor iframe with the textarea.
350 syncValue : function()
352 //Roo.log("HtmlEditorCore:syncValue (EDITOR->TEXT)");
353 if(this.initialized){
355 this.undoManager.addEvent();
358 var bd = (this.doc.body || this.doc.documentElement);
362 var div = document.createElement('div');
363 div.innerHTML = bd.innerHTML;
366 if (this.enableBlocks) {
367 new Roo.htmleditor.FilterBlock({ node : div });
372 var html = div.innerHTML;
374 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
375 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
377 html = '<div style="'+m[0]+'">' + html + '</div>';
380 html = this.cleanHtml(html);
381 // fix up the special chars.. normaly like back quotes in word...
382 // however we do not want to do this with chinese..
383 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
385 var cc = match.charCodeAt();
387 // Get the character value, handling surrogate pairs
388 if (match.length == 2) {
389 // It's a surrogate pair, calculate the Unicode code point
390 var high = match.charCodeAt(0) - 0xD800;
391 var low = match.charCodeAt(1) - 0xDC00;
392 cc = (high * 0x400) + low + 0x10000;
394 (cc >= 0x4E00 && cc < 0xA000 ) ||
395 (cc >= 0x3400 && cc < 0x4E00 ) ||
396 (cc >= 0xf900 && cc < 0xfb00 )
401 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
402 return "&#" + cc + ";";
409 if(this.owner.fireEvent('beforesync', this, html) !== false){
410 this.el.dom.value = html;
411 this.owner.fireEvent('sync', this, html);
417 * TEXTAREA -> EDITABLE
418 * Protected method that will not generally be called directly. Pushes the value of the textarea
419 * into the iframe editor.
421 pushValue : function()
423 //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
424 if(this.initialized){
425 var v = this.el.dom.value.trim();
428 if(this.owner.fireEvent('beforepush', this, v) !== false){
429 var d = (this.doc.body || this.doc.documentElement);
432 this.el.dom.value = d.innerHTML;
433 this.owner.fireEvent('push', this, v);
435 if (this.autoClean) {
436 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
437 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
440 Roo.htmleditor.Block.initAll(this.doc.body);
441 this.updateLanguage();
443 var lc = this.doc.body.lastChild;
444 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
445 // add an extra line at the end.
446 this.doc.body.appendChild(this.doc.createElement('br'));
454 deferFocus : function(){
455 this.focus.defer(10, this);
460 if(this.win && !this.sourceEditMode){
467 assignDocWin: function()
469 var iframe = this.iframe;
472 this.doc = iframe.contentWindow.document;
473 this.win = iframe.contentWindow;
475 // if (!Roo.get(this.frameId)) {
478 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
479 // this.win = Roo.get(this.frameId).dom.contentWindow;
481 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
485 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
486 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
491 initEditor : function(){
492 //console.log("INIT EDITOR");
497 this.doc.designMode="on";
499 this.doc.write(this.getDocMarkup());
502 var dbody = (this.doc.body || this.doc.documentElement);
503 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
504 // this copies styles from the containing element into thsi one..
505 // not sure why we need all of this..
506 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
508 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
509 //ss['background-attachment'] = 'fixed'; // w3c
510 dbody.bgProperties = 'fixed'; // ie
511 //Roo.DomHelper.applyStyles(dbody, ss);
512 Roo.EventManager.on(this.doc, {
514 'mouseup': this.onEditorEvent,
515 'dblclick': this.onEditorEvent,
516 'click': this.onEditorEvent,
517 'keyup': this.onEditorEvent,
522 Roo.EventManager.on(this.doc, {
523 'paste': this.onPasteEvent,
527 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
530 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
531 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
533 this.initialized = true;
536 // initialize special key events - enter
537 new Roo.htmleditor.KeyEnter({core : this});
541 this.owner.fireEvent('initialize', this);
544 // this is to prevent a href clicks resulting in a redirect?
546 onPasteEvent : function(e,v)
548 // I think we better assume paste is going to be a dirty load of rubish from word..
550 // even pasting into a 'email version' of this widget will have to clean up that mess.
551 var cd = (e.browserEvent.clipboardData || window.clipboardData);
553 // check what type of paste - if it's an image, then handle it differently.
554 if (cd.files.length > 0) {
556 var urlAPI = (window.createObjectURL && window) ||
557 (window.URL && URL.revokeObjectURL && URL) ||
558 (window.webkitURL && webkitURL);
560 var url = urlAPI.createObjectURL( cd.files[0]);
561 this.insertAtCursor('<img src=" + url + ">');
565 var html = cd.getData('text/html'); // clipboard event
566 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
567 var images = parser.doc ? parser.doc.getElementsByType('pict') : [];
571 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable)/); }) // ignore headers
572 .map(function(g) { return g.toDataURL(); });
575 html = this.cleanWordChars(html);
577 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
580 var sn = this.getParentElement();
581 // check if d contains a table, and prevent nesting??
582 //Roo.log(d.getElementsByTagName('table'));
584 //Roo.log(sn.closest('table'));
585 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
587 this.insertAtCursor("You can not nest tables");
588 //Roo.log("prevent?"); // fixme -
592 if (images.length > 0) {
593 Roo.each(d.getElementsByTagName('img'), function(img, i) {
594 img.setAttribute('src', images[i]);
597 if (this.autoClean) {
598 new Roo.htmleditor.FilterStyleToTag({ node : d });
599 new Roo.htmleditor.FilterAttributes({
601 attrib_white : ['href', 'src', 'name', 'align'],
602 attrib_clean : ['href', 'src' ]
604 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
606 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT' ]} );
607 new Roo.htmleditor.FilterParagraph({ node : d });
608 new Roo.htmleditor.FilterSpan({ node : d });
609 new Roo.htmleditor.FilterLongBr({ node : d });
611 if (this.enableBlocks) {
613 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
614 if (img.closest('figure')) { // assume!! that it's aready
617 var fig = new Roo.htmleditor.BlockFigure({
620 fig.updateElement(img); // replace it..
626 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
627 if (this.enableBlocks) {
628 Roo.htmleditor.Block.initAll(this.doc.body);
634 // default behaveiour should be our local cleanup paste? (optional?)
635 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
636 //this.owner.fireEvent('paste', e, v);
639 onDestroy : function(){
645 //for (var i =0; i < this.toolbars.length;i++) {
646 // // fixme - ask toolbars for heights?
647 // this.toolbars[i].onDestroy();
650 //this.wrap.dom.innerHTML = '';
651 //this.wrap.remove();
656 onFirstFocus : function(){
659 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
661 this.activated = true;
664 if(Roo.isGecko){ // prevent silly gecko errors
666 var s = this.win.getSelection();
667 if(!s.focusNode || s.focusNode.nodeType != 3){
668 var r = s.getRangeAt(0);
669 r.selectNodeContents((this.doc.body || this.doc.documentElement));
674 this.execCmd('useCSS', true);
675 this.execCmd('styleWithCSS', false);
678 this.owner.fireEvent('activate', this);
682 adjustFont: function(btn){
683 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
684 //if(Roo.isSafari){ // safari
687 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
688 if(Roo.isSafari){ // safari
689 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
690 v = (v < 10) ? 10 : v;
691 v = (v > 48) ? 48 : v;
692 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
697 v = Math.max(1, v+adjust);
699 this.execCmd('FontSize', v );
702 onEditorEvent : function(e)
705 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
706 return; // we do not handle this.. (undo manager does..)
708 // in theory this detects if the last element is not a br, then we try and do that.
709 // its so clicking in space at bottom triggers adding a br and moving the cursor.
711 e.target.nodeName == 'BODY' &&
712 e.type == "mouseup" &&
713 this.doc.body.lastChild
715 var lc = this.doc.body.lastChild;
716 // gtx-trans is google translate plugin adding crap.
717 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
718 lc = lc.previousSibling;
720 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
721 // if last element is <BR> - then dont do anything.
723 var ns = this.doc.createElement('br');
724 this.doc.body.appendChild(ns);
725 range = this.doc.createRange();
726 range.setStartAfter(ns);
727 range.collapse(true);
728 var sel = this.win.getSelection();
729 sel.removeAllRanges();
736 this.fireEditorEvent(e);
737 // this.updateToolbar();
738 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
741 fireEditorEvent: function(e)
743 this.owner.fireEvent('editorevent', this, e);
746 insertTag : function(tg)
748 // could be a bit smarter... -> wrap the current selected tRoo..
749 if (tg.toLowerCase() == 'span' ||
750 tg.toLowerCase() == 'code' ||
751 tg.toLowerCase() == 'sup' ||
752 tg.toLowerCase() == 'sub'
755 range = this.createRange(this.getSelection());
756 var wrappingNode = this.doc.createElement(tg.toLowerCase());
757 wrappingNode.appendChild(range.extractContents());
758 range.insertNode(wrappingNode);
765 this.execCmd("formatblock", tg);
766 this.undoManager.addEvent();
769 insertText : function(txt)
773 var range = this.createRange();
774 range.deleteContents();
775 //alert(Sender.getAttribute('label'));
777 range.insertNode(this.doc.createTextNode(txt));
778 this.undoManager.addEvent();
784 * Executes a Midas editor command on the editor document and performs necessary focus and
785 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
786 * @param {String} cmd The Midas command
787 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
789 relayCmd : function(cmd, value)
795 case 'justifycenter':
796 // if we are in a cell, then we will adjust the
797 var n = this.getParentElement();
798 var td = n.closest('td');
800 var bl = Roo.htmleditor.Block.factory(td);
801 bl.textAlign = cmd.replace('justify','');
803 this.owner.fireEvent('editorevent', this);
806 this.execCmd('styleWithCSS', true); //
810 // if there is no selection, then we insert, and set the curson inside it..
811 this.execCmd('styleWithCSS', false);
821 this.execCmd(cmd, value);
822 this.owner.fireEvent('editorevent', this);
823 //this.updateToolbar();
824 this.owner.deferFocus();
828 * Executes a Midas editor command directly on the editor document.
829 * For visual commands, you should use {@link #relayCmd} instead.
830 * <b>This should only be called after the editor is initialized.</b>
831 * @param {String} cmd The Midas command
832 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
834 execCmd : function(cmd, value){
835 this.doc.execCommand(cmd, false, value === undefined ? null : value);
842 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
844 * @param {String} text | dom node..
846 insertAtCursor : function(text)
853 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
857 // from jquery ui (MIT licenced)
861 if (win.getSelection && win.getSelection().getRangeAt) {
863 // delete the existing?
865 this.createRange(this.getSelection()).deleteContents();
866 range = win.getSelection().getRangeAt(0);
867 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
868 range.insertNode(node);
869 range = range.cloneRange();
870 range.collapse(false);
872 win.getSelection().removeAllRanges();
873 win.getSelection().addRange(range);
877 } else if (win.document.selection && win.document.selection.createRange) {
878 // no firefox support
879 var txt = typeof(text) == 'string' ? text : text.outerHTML;
880 win.document.selection.createRange().pasteHTML(txt);
883 // no firefox support
884 var txt = typeof(text) == 'string' ? text : text.outerHTML;
885 this.execCmd('InsertHTML', txt);
893 mozKeyPress : function(e){
895 var c = e.getCharCode(), cmd;
898 c = String.fromCharCode(c).toLowerCase();
912 // this.cleanUpPaste.defer(100, this);
930 fixKeys : function(){ // load time branching for fastest keydown performance
935 var k = e.getKey(), r;
938 r = this.doc.selection.createRange();
941 r.pasteHTML('    ');
946 /// this is handled by Roo.htmleditor.KeyEnter
949 r = this.doc.selection.createRange();
951 var target = r.parentElement();
952 if(!target || target.tagName.toLowerCase() != 'li'){
954 r.pasteHTML('<br/>');
961 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
962 // this.cleanUpPaste.defer(100, this);
968 }else if(Roo.isOpera){
974 this.execCmd('InsertHTML','    ');
978 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
979 // this.cleanUpPaste.defer(100, this);
984 }else if(Roo.isSafari){
990 this.execCmd('InsertText','\t');
996 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
997 // this.cleanUpPaste.defer(100, this);
1005 getAllAncestors: function()
1007 var p = this.getSelectedNode();
1010 a.push(p); // push blank onto stack..
1011 p = this.getParentElement();
1015 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1019 a.push(this.doc.body);
1023 lastSelNode : false,
1026 getSelection : function()
1028 this.assignDocWin();
1029 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1033 * @param {DomElement} node the node to select
1035 selectNode : function(node, collapse)
1037 var nodeRange = node.ownerDocument.createRange();
1039 nodeRange.selectNode(node);
1041 nodeRange.selectNodeContents(node);
1043 if (collapse === true) {
1044 nodeRange.collapse(true);
1047 var s = this.win.getSelection();
1048 s.removeAllRanges();
1049 s.addRange(nodeRange);
1052 getSelectedNode: function()
1054 // this may only work on Gecko!!!
1056 // should we cache this!!!!
1060 var range = this.createRange(this.getSelection()).cloneRange();
1063 var parent = range.parentElement();
1065 var testRange = range.duplicate();
1066 testRange.moveToElementText(parent);
1067 if (testRange.inRange(range)) {
1070 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1073 parent = parent.parentElement;
1078 // is ancestor a text element.
1079 var ac = range.commonAncestorContainer;
1080 if (ac.nodeType == 3) {
1084 var ar = ac.childNodes;
1087 var other_nodes = [];
1088 var has_other_nodes = false;
1089 for (var i=0;i<ar.length;i++) {
1090 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1093 // fullly contained node.
1095 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1100 // probably selected..
1101 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1102 other_nodes.push(ar[i]);
1106 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1111 has_other_nodes = true;
1113 if (!nodes.length && other_nodes.length) {
1116 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1124 createRange: function(sel)
1126 // this has strange effects when using with
1127 // top toolbar - not sure if it's a great idea.
1128 //this.editor.contentWindow.focus();
1129 if (typeof sel != "undefined") {
1131 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1133 return this.doc.createRange();
1136 return this.doc.createRange();
1139 getParentElement: function()
1142 this.assignDocWin();
1143 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1145 var range = this.createRange(sel);
1148 var p = range.commonAncestorContainer;
1149 while (p.nodeType == 3) { // text node
1160 * Range intersection.. the hard stuff...
1164 * [ -- selected range --- ]
1168 * if end is before start or hits it. fail.
1169 * if start is after end or hits it fail.
1171 * if either hits (but other is outside. - then it's not
1177 // @see http://www.thismuchiknow.co.uk/?p=64.
1178 rangeIntersectsNode : function(range, node)
1180 var nodeRange = node.ownerDocument.createRange();
1182 nodeRange.selectNode(node);
1184 nodeRange.selectNodeContents(node);
1187 var rangeStartRange = range.cloneRange();
1188 rangeStartRange.collapse(true);
1190 var rangeEndRange = range.cloneRange();
1191 rangeEndRange.collapse(false);
1193 var nodeStartRange = nodeRange.cloneRange();
1194 nodeStartRange.collapse(true);
1196 var nodeEndRange = nodeRange.cloneRange();
1197 nodeEndRange.collapse(false);
1199 return rangeStartRange.compareBoundaryPoints(
1200 Range.START_TO_START, nodeEndRange) == -1 &&
1201 rangeEndRange.compareBoundaryPoints(
1202 Range.START_TO_START, nodeStartRange) == 1;
1206 rangeCompareNode : function(range, node)
1208 var nodeRange = node.ownerDocument.createRange();
1210 nodeRange.selectNode(node);
1212 nodeRange.selectNodeContents(node);
1216 range.collapse(true);
1218 nodeRange.collapse(true);
1220 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1221 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1223 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1225 var nodeIsBefore = ss == 1;
1226 var nodeIsAfter = ee == -1;
1228 if (nodeIsBefore && nodeIsAfter) {
1231 if (!nodeIsBefore && nodeIsAfter) {
1232 return 1; //right trailed.
1235 if (nodeIsBefore && !nodeIsAfter) {
1236 return 2; // left trailed.
1242 cleanWordChars : function(input) {// change the chars to hex code
1245 [ 8211, "–" ],
1246 [ 8212, "—" ],
1255 Roo.each(swapCodes, function(sw) {
1256 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1258 output = output.replace(swapper, sw[1]);
1268 cleanUpChild : function (node)
1271 new Roo.htmleditor.FilterComment({node : node});
1272 new Roo.htmleditor.FilterAttributes({
1274 attrib_black : this.ablack,
1275 attrib_clean : this.aclean,
1276 style_white : this.cwhite,
1277 style_black : this.cblack
1279 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1280 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1286 * Clean up MS wordisms...
1287 * @deprecated - use filter directly
1289 cleanWord : function(node)
1291 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1298 * @deprecated - use filters
1300 cleanTableWidths : function(node)
1302 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1309 applyBlacklists : function()
1311 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1312 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1314 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1315 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1316 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1320 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1321 if (b.indexOf(tag) > -1) {
1324 this.white.push(tag);
1328 Roo.each(w, function(tag) {
1329 if (b.indexOf(tag) > -1) {
1332 if (this.white.indexOf(tag) > -1) {
1335 this.white.push(tag);
1340 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1341 if (w.indexOf(tag) > -1) {
1344 this.black.push(tag);
1348 Roo.each(b, function(tag) {
1349 if (w.indexOf(tag) > -1) {
1352 if (this.black.indexOf(tag) > -1) {
1355 this.black.push(tag);
1360 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1361 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1365 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1366 if (b.indexOf(tag) > -1) {
1369 this.cwhite.push(tag);
1373 Roo.each(w, function(tag) {
1374 if (b.indexOf(tag) > -1) {
1377 if (this.cwhite.indexOf(tag) > -1) {
1380 this.cwhite.push(tag);
1385 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1386 if (w.indexOf(tag) > -1) {
1389 this.cblack.push(tag);
1393 Roo.each(b, function(tag) {
1394 if (w.indexOf(tag) > -1) {
1397 if (this.cblack.indexOf(tag) > -1) {
1400 this.cblack.push(tag);
1405 setStylesheets : function(stylesheets)
1407 if(typeof(stylesheets) == 'string'){
1408 Roo.get(this.iframe.contentDocument.head).createChild({
1419 Roo.each(stylesheets, function(s) {
1424 Roo.get(_this.iframe.contentDocument.head).createChild({
1436 updateLanguage : function()
1438 if (!this.iframe || !this.iframe.contentDocument) {
1441 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1445 removeStylesheets : function()
1449 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1454 setStyle : function(style)
1456 Roo.get(this.iframe.contentDocument.head).createChild({
1465 // hide stuff that is not compatible
1483 * @cfg {String} fieldClass @hide
1486 * @cfg {String} focusClass @hide
1489 * @cfg {String} autoCreate @hide
1492 * @cfg {String} inputType @hide
1495 * @cfg {String} invalidClass @hide
1498 * @cfg {String} invalidText @hide
1501 * @cfg {String} msgFx @hide
1504 * @cfg {String} validateOnBlur @hide
1508 Roo.HtmlEditorCore.white = [
1509 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1511 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1512 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1513 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1514 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1515 'TABLE', 'UL', 'XMP',
1517 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1520 'DIR', 'MENU', 'OL', 'UL', 'DL',
1526 Roo.HtmlEditorCore.black = [
1527 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1529 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1530 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1531 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1532 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1533 //'FONT' // CLEAN LATER..
1534 'COLGROUP', 'COL' // messy tables.
1537 Roo.HtmlEditorCore.clean = [ // ?? needed???
1538 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1540 Roo.HtmlEditorCore.tag_remove = [
1545 Roo.HtmlEditorCore.ablack = [
1549 Roo.HtmlEditorCore.aclean = [
1550 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1554 Roo.HtmlEditorCore.pwhite= [
1555 'http', 'https', 'mailto'
1558 // white listed style attributes.
1559 Roo.HtmlEditorCore.cwhite= [
1560 // 'text-align', /// default is to allow most things..
1566 // black listed style attributes.
1567 Roo.HtmlEditorCore.cblack= [
1568 // 'font-size' -- this can be set by the project