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} css styling for resizing. (used on bootstrap only)
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.
126 * @cfg {String} language default en - language of text (usefull for rtl languages)
132 * @cfg {boolean} allowComments - default false - allow comments in HTML source
133 * - by default they are stripped - if you are editing email you may need this.
135 allowComments: false,
139 // private properties
140 validationEvent : false,
144 sourceEditMode : false,
145 onFocus : Roo.emptyFn,
151 // blacklist + whitelisted elements..
160 * Protected method that will not generally be called directly. It
161 * is called when the editor initializes the iframe with HTML contents. Override this method if you
162 * want to change the initialization markup of the iframe (e.g. to add stylesheets).
164 getDocMarkup : function(){
168 // inherit styels from page...??
169 if (this.stylesheets === false) {
171 Roo.get(document.head).select('style').each(function(node) {
172 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
175 Roo.get(document.head).select('link').each(function(node) {
176 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
179 } else if (!this.stylesheets.length) {
181 st = '<style type="text/css">' +
182 'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
185 for (var i in this.stylesheets) {
186 if (typeof(this.stylesheets[i]) != 'string') {
189 st += '<link rel="stylesheet" href="' + this.stylesheets[i] +'" type="text/css">';
194 st += '<style type="text/css">' +
195 'IMG { cursor: pointer } ' +
198 st += '<meta name="google" content="notranslate">';
200 var cls = 'notranslate roo-htmleditor-body';
202 if(this.bodyCls.length){
203 cls += ' ' + this.bodyCls;
206 return '<html class="notranslate" translate="no"><head>' + st +
207 //<style type="text/css">' +
208 //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
210 ' </head><body contenteditable="true" data-enable-grammerly="true" class="' + cls + '"></body></html>';
214 onRender : function(ct, position)
217 //Roo.HtmlEditorCore.superclass.onRender.call(this, ct, position);
218 this.el = this.owner.inputEl ? this.owner.inputEl() : this.owner.el;
221 this.el.dom.style.border = '0 none';
222 this.el.dom.setAttribute('tabIndex', -1);
223 this.el.addClass('x-hidden hide');
227 if(Roo.isIE){ // fix IE 1px bogus margin
228 this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
232 this.frameId = Roo.id();
236 cls: 'form-control', // bootstrap..
240 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
243 ifcfg.style = { resize : this.resize };
246 var iframe = this.owner.wrap.createChild(ifcfg, this.el);
249 this.iframe = iframe.dom;
253 this.doc.designMode = 'on';
256 this.doc.write(this.getDocMarkup());
260 var task = { // must defer to wait for browser to be ready
262 //console.log("run task?" + this.doc.readyState);
264 if(this.doc.body || this.doc.readyState == 'complete'){
266 this.doc.designMode="on";
271 Roo.TaskMgr.stop(task);
272 this.initEditor.defer(10, this);
279 Roo.TaskMgr.start(task);
284 onResize : function(w, h)
286 Roo.log('resize: ' +w + ',' + h );
287 //Roo.HtmlEditorCore.superclass.onResize.apply(this, arguments);
291 if(typeof w == 'number'){
293 this.iframe.style.width = w + 'px';
295 if(typeof h == 'number'){
297 this.iframe.style.height = h + 'px';
299 (this.doc.body || this.doc.documentElement).style.height = (h - (this.iframePad*2)) + 'px';
306 * Toggles the editor between standard and source edit mode.
307 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
309 toggleSourceEdit : function(sourceEditMode){
311 this.sourceEditMode = sourceEditMode === true;
313 if(this.sourceEditMode){
315 Roo.get(this.iframe).addClass(['x-hidden','hide', 'd-none']); //FIXME - what's the BS styles for these
318 Roo.get(this.iframe).removeClass(['x-hidden','hide', 'd-none']);
319 //this.iframe.className = '';
322 //this.setSize(this.owner.wrap.getSize());
323 //this.fireEvent('editmodechange', this, this.sourceEditMode);
330 * Protected method that will not generally be called directly. If you need/want
331 * custom HTML cleanup, this is the method you should override.
332 * @param {String} html The HTML to be cleaned
333 * return {String} The cleaned HTML
335 cleanHtml : function(html)
339 if(Roo.isSafari){ // strip safari nonsense
340 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
343 if(html == ' '){
350 * HTML Editor -> Textarea
351 * Protected method that will not generally be called directly. Syncs the contents
352 * of the editor iframe with the textarea.
354 syncValue : function()
356 //Roo.log("HtmlEditorCore:syncValue (EDITOR->TEXT)");
357 if(this.initialized){
359 if (this.undoManager) {
360 this.undoManager.addEvent();
364 var bd = (this.doc.body || this.doc.documentElement);
367 var sel = this.win.getSelection();
369 var div = document.createElement('div');
370 div.innerHTML = bd.innerHTML;
371 var gtx = div.getElementsByClassName('gtx-trans-icon'); // google translate - really annoying and difficult to get rid of.
372 if (gtx.length > 0) {
373 var rm = gtx.item(0).parentNode;
374 rm.parentNode.removeChild(rm);
378 if (this.enableBlocks) {
379 Array.from(bd.getElementsByTagName('img')).forEach(function(img) {
380 var fig = img.closest('figure');
382 var bf = new Roo.htmleditor.BlockFigure({
389 new Roo.htmleditor.FilterBlock({ node : div });
392 var html = div.innerHTML;
395 if (this.autoClean) {
397 new Roo.htmleditor.FilterAttributes({
407 'data-caption-display',
420 attrib_clean : ['href', 'src' ]
423 var tidy = new Roo.htmleditor.TidySerializer({
426 html = tidy.serialize(div);
432 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
433 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
435 html = '<div style="'+m[0]+'">' + html + '</div>';
438 html = this.cleanHtml(html);
439 // fix up the special chars.. normaly like back quotes in word...
440 // however we do not want to do this with chinese..
441 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
443 var cc = match.charCodeAt();
445 // Get the character value, handling surrogate pairs
446 if (match.length == 2) {
447 // It's a surrogate pair, calculate the Unicode code point
448 var high = match.charCodeAt(0) - 0xD800;
449 var low = match.charCodeAt(1) - 0xDC00;
450 cc = (high * 0x400) + low + 0x10000;
452 (cc >= 0x4E00 && cc < 0xA000 ) ||
453 (cc >= 0x3400 && cc < 0x4E00 ) ||
454 (cc >= 0xf900 && cc < 0xfb00 )
459 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
460 return "&#" + cc + ";";
467 if(this.owner.fireEvent('beforesync', this, html) !== false){
468 this.el.dom.value = html;
469 this.owner.fireEvent('sync', this, html);
475 * TEXTAREA -> EDITABLE
476 * Protected method that will not generally be called directly. Pushes the value of the textarea
477 * into the iframe editor.
479 pushValue : function()
481 //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
482 if(this.initialized){
483 var v = this.el.dom.value.trim();
486 if(this.owner.fireEvent('beforepush', this, v) !== false){
487 var d = (this.doc.body || this.doc.documentElement);
490 this.el.dom.value = d.innerHTML;
491 this.owner.fireEvent('push', this, v);
493 if (this.autoClean) {
494 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
495 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
497 if (this.enableBlocks) {
498 Roo.htmleditor.Block.initAll(this.doc.body);
501 this.updateLanguage();
503 var lc = this.doc.body.lastChild;
504 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
505 // add an extra line at the end.
506 this.doc.body.appendChild(this.doc.createElement('br'));
514 deferFocus : function(){
515 this.focus.defer(10, this);
520 if(this.win && !this.sourceEditMode){
527 assignDocWin: function()
529 var iframe = this.iframe;
532 this.doc = iframe.contentWindow.document;
533 this.win = iframe.contentWindow;
535 // if (!Roo.get(this.frameId)) {
538 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
539 // this.win = Roo.get(this.frameId).dom.contentWindow;
541 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
545 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
546 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
551 initEditor : function(){
552 //console.log("INIT EDITOR");
557 this.doc.designMode="on";
559 this.doc.write(this.getDocMarkup());
562 var dbody = (this.doc.body || this.doc.documentElement);
563 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
564 // this copies styles from the containing element into thsi one..
565 // not sure why we need all of this..
566 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
568 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
569 //ss['background-attachment'] = 'fixed'; // w3c
570 dbody.bgProperties = 'fixed'; // ie
571 dbody.setAttribute("translate", "no");
573 //Roo.DomHelper.applyStyles(dbody, ss);
574 Roo.EventManager.on(this.doc, {
576 'mouseup': this.onEditorEvent,
577 'dblclick': this.onEditorEvent,
578 'click': this.onEditorEvent,
579 'keyup': this.onEditorEvent,
584 Roo.EventManager.on(this.doc, {
585 'paste': this.onPasteEvent,
589 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
592 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
593 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
595 this.initialized = true;
598 // initialize special key events - enter
599 new Roo.htmleditor.KeyEnter({core : this});
603 this.owner.fireEvent('initialize', this);
606 // this is to prevent a href clicks resulting in a redirect?
608 onPasteEvent : function(e,v)
610 // I think we better assume paste is going to be a dirty load of rubish from word..
612 // even pasting into a 'email version' of this widget will have to clean up that mess.
613 var cd = (e.browserEvent.clipboardData || window.clipboardData);
615 // check what type of paste - if it's an image, then handle it differently.
616 if (cd.files && cd.files.length > 0 && cd.types.indexOf('text/html') < 0) {
618 var urlAPI = (window.createObjectURL && window) ||
619 (window.URL && URL.revokeObjectURL && URL) ||
620 (window.webkitURL && webkitURL);
622 var r = new FileReader();
624 r.addEventListener('load',function()
627 var d = (new DOMParser().parseFromString('<img src="' + r.result+ '">', 'text/html')).body;
629 if (t.enableBlocks) {
631 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
632 if (img.closest('figure')) { // assume!! that it's aready
635 var fig = new Roo.htmleditor.BlockFigure({
638 fig.updateElement(img); // replace it..
642 t.insertAtCursor(d.innerHTML.replace(/ /g,' '));
643 t.owner.fireEvent('paste', this);
645 r.readAsDataURL(cd.files[0]);
651 if (cd.types.indexOf('text/html') < 0 ) {
655 var html = cd.getData('text/html'); // clipboard event
656 if (cd.types.indexOf('text/rtf') > -1) {
657 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
658 images = parser.doc ? parser.doc.getElementsByType('pict') : [];
663 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable|footerf)/); }) // ignore headers/footers etc.
664 .map(function(g) { return g.toDataURL(); })
665 .filter(function(g) { return g != 'about:blank'; });
668 html = this.cleanWordChars(html);
670 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
673 var sn = this.getParentElement();
674 // check if d contains a table, and prevent nesting??
675 //Roo.log(d.getElementsByTagName('table'));
677 //Roo.log(sn.closest('table'));
678 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
680 this.insertAtCursor("You can not nest tables");
681 //Roo.log("prevent?"); // fixme -
687 if (images.length > 0) {
688 // replace all v:imagedata - with img.
689 var ar = Array.from(d.getElementsByTagName('v:imagedata'));
690 Roo.each(ar, function(node) {
691 node.parentNode.insertBefore(d.ownerDocument.createElement('img'), node );
692 node.parentNode.removeChild(node);
696 Roo.each(d.getElementsByTagName('img'), function(img, i) {
697 img.setAttribute('src', images[i]);
700 if (this.autoClean) {
701 new Roo.htmleditor.FilterWord({ node : d });
703 new Roo.htmleditor.FilterStyleToTag({ node : d });
704 new Roo.htmleditor.FilterAttributes({
713 /* THESE ARE NOT ALLWOED FOR PASTE
715 'data-caption-display',
729 attrib_clean : ['href', 'src' ]
731 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
733 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', ':' ]} );
734 new Roo.htmleditor.FilterParagraph({ node : d });
735 new Roo.htmleditor.FilterHashLink({node : d});
736 new Roo.htmleditor.FilterSpan({ node : d });
737 new Roo.htmleditor.FilterLongBr({ node : d });
738 new Roo.htmleditor.FilterComment({ node : d });
742 if (this.enableBlocks) {
744 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
745 if (img.closest('figure')) { // assume!! that it's aready
748 var fig = new Roo.htmleditor.BlockFigure({
751 fig.updateElement(img); // replace it..
757 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
758 if (this.enableBlocks) {
759 Roo.htmleditor.Block.initAll(this.doc.body);
764 this.owner.fireEvent('paste', this);
766 // default behaveiour should be our local cleanup paste? (optional?)
767 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
768 //this.owner.fireEvent('paste', e, v);
771 onDestroy : function(){
777 //for (var i =0; i < this.toolbars.length;i++) {
778 // // fixme - ask toolbars for heights?
779 // this.toolbars[i].onDestroy();
782 //this.wrap.dom.innerHTML = '';
783 //this.wrap.remove();
788 onFirstFocus : function(){
791 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
793 this.activated = true;
796 if(Roo.isGecko){ // prevent silly gecko errors
798 var s = this.win.getSelection();
799 if(!s.focusNode || s.focusNode.nodeType != 3){
800 var r = s.getRangeAt(0);
801 r.selectNodeContents((this.doc.body || this.doc.documentElement));
806 this.execCmd('useCSS', true);
807 this.execCmd('styleWithCSS', false);
810 this.owner.fireEvent('activate', this);
814 adjustFont: function(btn){
815 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
816 //if(Roo.isSafari){ // safari
819 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
820 if(Roo.isSafari){ // safari
821 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
822 v = (v < 10) ? 10 : v;
823 v = (v > 48) ? 48 : v;
824 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
829 v = Math.max(1, v+adjust);
831 this.execCmd('FontSize', v );
834 onEditorEvent : function(e)
838 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
839 return; // we do not handle this.. (undo manager does..)
841 // clicking a 'block'?
843 // in theory this detects if the last element is not a br, then we try and do that.
844 // its so clicking in space at bottom triggers adding a br and moving the cursor.
846 e.target.nodeName == 'BODY' &&
847 e.type == "mouseup" &&
848 this.doc.body.lastChild
850 var lc = this.doc.body.lastChild;
851 // gtx-trans is google translate plugin adding crap.
852 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
853 lc = lc.previousSibling;
855 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
856 // if last element is <BR> - then dont do anything.
858 var ns = this.doc.createElement('br');
859 this.doc.body.appendChild(ns);
860 range = this.doc.createRange();
861 range.setStartAfter(ns);
862 range.collapse(true);
863 var sel = this.win.getSelection();
864 sel.removeAllRanges();
871 this.fireEditorEvent(e);
872 // this.updateToolbar();
873 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
876 fireEditorEvent: function(e)
878 this.owner.fireEvent('editorevent', this, e);
881 insertTag : function(tg)
883 // could be a bit smarter... -> wrap the current selected tRoo..
884 if (tg.toLowerCase() == 'span' ||
885 tg.toLowerCase() == 'code' ||
886 tg.toLowerCase() == 'sup' ||
887 tg.toLowerCase() == 'sub'
890 range = this.createRange(this.getSelection());
891 var wrappingNode = this.doc.createElement(tg.toLowerCase());
892 wrappingNode.appendChild(range.extractContents());
893 range.insertNode(wrappingNode);
900 this.execCmd("formatblock", tg);
901 this.undoManager.addEvent();
904 insertText : function(txt)
908 var range = this.createRange();
909 range.deleteContents();
910 //alert(Sender.getAttribute('label'));
912 range.insertNode(this.doc.createTextNode(txt));
913 this.undoManager.addEvent();
919 * Executes a Midas editor command on the editor document and performs necessary focus and
920 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
921 * @param {String} cmd The Midas command
922 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
924 relayCmd : function(cmd, value)
930 case 'justifycenter':
931 // if we are in a cell, then we will adjust the
932 var n = this.getParentElement();
933 var td = n.closest('td');
935 var bl = Roo.htmleditor.Block.factory(td);
936 bl.textAlign = cmd.replace('justify','');
938 this.owner.fireEvent('editorevent', this);
941 this.execCmd('styleWithCSS', true); //
946 // if there is no selection, then we insert, and set the curson inside it..
947 this.execCmd('styleWithCSS', false);
957 this.execCmd(cmd, value);
958 this.owner.fireEvent('editorevent', this);
959 //this.updateToolbar();
960 this.owner.deferFocus();
964 * Executes a Midas editor command directly on the editor document.
965 * For visual commands, you should use {@link #relayCmd} instead.
966 * <b>This should only be called after the editor is initialized.</b>
967 * @param {String} cmd The Midas command
968 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
970 execCmd : function(cmd, value){
971 this.doc.execCommand(cmd, false, value === undefined ? null : value);
978 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
980 * @param {String} text | dom node..
982 insertAtCursor : function(text)
989 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
993 // from jquery ui (MIT licenced)
997 if (win.getSelection && win.getSelection().getRangeAt) {
999 // delete the existing?
1001 this.createRange(this.getSelection()).deleteContents();
1002 range = win.getSelection().getRangeAt(0);
1003 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
1004 range.insertNode(node);
1005 range = range.cloneRange();
1006 range.collapse(false);
1008 win.getSelection().removeAllRanges();
1009 win.getSelection().addRange(range);
1013 } else if (win.document.selection && win.document.selection.createRange) {
1014 // no firefox support
1015 var txt = typeof(text) == 'string' ? text : text.outerHTML;
1016 win.document.selection.createRange().pasteHTML(txt);
1019 // no firefox support
1020 var txt = typeof(text) == 'string' ? text : text.outerHTML;
1021 this.execCmd('InsertHTML', txt);
1029 mozKeyPress : function(e){
1031 var c = e.getCharCode(), cmd;
1034 c = String.fromCharCode(c).toLowerCase();
1048 // this.cleanUpPaste.defer(100, this);
1056 //this.execCmd(cmd);
1057 //this.deferFocus();
1066 fixKeys : function(){ // load time branching for fastest keydown performance
1071 var k = e.getKey(), r;
1074 r = this.doc.selection.createRange();
1077 r.pasteHTML('    ');
1082 /// this is handled by Roo.htmleditor.KeyEnter
1085 r = this.doc.selection.createRange();
1087 var target = r.parentElement();
1088 if(!target || target.tagName.toLowerCase() != 'li'){
1090 r.pasteHTML('<br/>');
1097 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1098 // this.cleanUpPaste.defer(100, this);
1104 }else if(Roo.isOpera){
1110 this.execCmd('InsertHTML','    ');
1114 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1115 // this.cleanUpPaste.defer(100, this);
1120 }else if(Roo.isSafari){
1126 this.execCmd('InsertText','\t');
1130 this.mozKeyPress(e);
1132 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1133 // this.cleanUpPaste.defer(100, this);
1141 getAllAncestors: function()
1143 var p = this.getSelectedNode();
1146 a.push(p); // push blank onto stack..
1147 p = this.getParentElement();
1151 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1155 a.push(this.doc.body);
1159 lastSelNode : false,
1162 getSelection : function()
1164 this.assignDocWin();
1165 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1169 * @param {DomElement} node the node to select
1171 selectNode : function(node, collapse)
1173 var nodeRange = node.ownerDocument.createRange();
1175 nodeRange.selectNode(node);
1177 nodeRange.selectNodeContents(node);
1179 if (collapse === true) {
1180 nodeRange.collapse(true);
1183 var s = this.win.getSelection();
1184 s.removeAllRanges();
1185 s.addRange(nodeRange);
1188 getSelectedNode: function()
1190 // this may only work on Gecko!!!
1192 // should we cache this!!!!
1196 var range = this.createRange(this.getSelection()).cloneRange();
1199 var parent = range.parentElement();
1201 var testRange = range.duplicate();
1202 testRange.moveToElementText(parent);
1203 if (testRange.inRange(range)) {
1206 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1209 parent = parent.parentElement;
1214 // is ancestor a text element.
1215 var ac = range.commonAncestorContainer;
1216 if (ac.nodeType == 3) {
1220 var ar = ac.childNodes;
1223 var other_nodes = [];
1224 var has_other_nodes = false;
1225 for (var i=0;i<ar.length;i++) {
1226 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1229 // fullly contained node.
1231 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1236 // probably selected..
1237 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1238 other_nodes.push(ar[i]);
1242 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1247 has_other_nodes = true;
1249 if (!nodes.length && other_nodes.length) {
1252 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1260 createRange: function(sel)
1262 // this has strange effects when using with
1263 // top toolbar - not sure if it's a great idea.
1264 //this.editor.contentWindow.focus();
1265 if (typeof sel != "undefined") {
1267 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1269 return this.doc.createRange();
1272 return this.doc.createRange();
1275 getParentElement: function()
1278 this.assignDocWin();
1279 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1281 var range = this.createRange(sel);
1284 var p = range.commonAncestorContainer;
1285 while (p.nodeType == 3) { // text node
1296 * Range intersection.. the hard stuff...
1300 * [ -- selected range --- ]
1304 * if end is before start or hits it. fail.
1305 * if start is after end or hits it fail.
1307 * if either hits (but other is outside. - then it's not
1313 // @see http://www.thismuchiknow.co.uk/?p=64.
1314 rangeIntersectsNode : function(range, node)
1316 var nodeRange = node.ownerDocument.createRange();
1318 nodeRange.selectNode(node);
1320 nodeRange.selectNodeContents(node);
1323 var rangeStartRange = range.cloneRange();
1324 rangeStartRange.collapse(true);
1326 var rangeEndRange = range.cloneRange();
1327 rangeEndRange.collapse(false);
1329 var nodeStartRange = nodeRange.cloneRange();
1330 nodeStartRange.collapse(true);
1332 var nodeEndRange = nodeRange.cloneRange();
1333 nodeEndRange.collapse(false);
1335 return rangeStartRange.compareBoundaryPoints(
1336 Range.START_TO_START, nodeEndRange) == -1 &&
1337 rangeEndRange.compareBoundaryPoints(
1338 Range.START_TO_START, nodeStartRange) == 1;
1342 rangeCompareNode : function(range, node)
1344 var nodeRange = node.ownerDocument.createRange();
1346 nodeRange.selectNode(node);
1348 nodeRange.selectNodeContents(node);
1352 range.collapse(true);
1354 nodeRange.collapse(true);
1356 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1357 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1359 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1361 var nodeIsBefore = ss == 1;
1362 var nodeIsAfter = ee == -1;
1364 if (nodeIsBefore && nodeIsAfter) {
1367 if (!nodeIsBefore && nodeIsAfter) {
1368 return 1; //right trailed.
1371 if (nodeIsBefore && !nodeIsAfter) {
1372 return 2; // left trailed.
1378 cleanWordChars : function(input) {// change the chars to hex code
1381 [ 8211, "–" ],
1382 [ 8212, "—" ],
1391 Roo.each(swapCodes, function(sw) {
1392 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1394 output = output.replace(swapper, sw[1]);
1404 cleanUpChild : function (node)
1407 new Roo.htmleditor.FilterComment({node : node});
1408 new Roo.htmleditor.FilterAttributes({
1410 attrib_black : this.ablack,
1411 attrib_clean : this.aclean,
1412 style_white : this.cwhite,
1413 style_black : this.cblack
1415 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1416 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1422 * Clean up MS wordisms...
1423 * @deprecated - use filter directly
1425 cleanWord : function(node)
1427 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1428 new Roo.htmleditor.FilterKeepChildren({node : node ? node : this.doc.body, tag : [ 'FONT', ':' ]} );
1435 * @deprecated - use filters
1437 cleanTableWidths : function(node)
1439 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1446 applyBlacklists : function()
1448 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1449 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1451 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1452 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1453 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1457 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1458 if (b.indexOf(tag) > -1) {
1461 this.white.push(tag);
1465 Roo.each(w, function(tag) {
1466 if (b.indexOf(tag) > -1) {
1469 if (this.white.indexOf(tag) > -1) {
1472 this.white.push(tag);
1477 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1478 if (w.indexOf(tag) > -1) {
1481 this.black.push(tag);
1485 Roo.each(b, function(tag) {
1486 if (w.indexOf(tag) > -1) {
1489 if (this.black.indexOf(tag) > -1) {
1492 this.black.push(tag);
1497 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1498 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1502 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1503 if (b.indexOf(tag) > -1) {
1506 this.cwhite.push(tag);
1510 Roo.each(w, function(tag) {
1511 if (b.indexOf(tag) > -1) {
1514 if (this.cwhite.indexOf(tag) > -1) {
1517 this.cwhite.push(tag);
1522 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1523 if (w.indexOf(tag) > -1) {
1526 this.cblack.push(tag);
1530 Roo.each(b, function(tag) {
1531 if (w.indexOf(tag) > -1) {
1534 if (this.cblack.indexOf(tag) > -1) {
1537 this.cblack.push(tag);
1542 setStylesheets : function(stylesheets)
1544 if(typeof(stylesheets) == 'string'){
1545 Roo.get(this.iframe.contentDocument.head).createChild({
1556 Roo.each(stylesheets, function(s) {
1561 Roo.get(_this.iframe.contentDocument.head).createChild({
1573 updateLanguage : function()
1575 if (!this.iframe || !this.iframe.contentDocument) {
1578 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1582 removeStylesheets : function()
1586 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1591 setStyle : function(style)
1593 Roo.get(this.iframe.contentDocument.head).createChild({
1602 // hide stuff that is not compatible
1620 * @cfg {String} fieldClass @hide
1623 * @cfg {String} focusClass @hide
1626 * @cfg {String} autoCreate @hide
1629 * @cfg {String} inputType @hide
1632 * @cfg {String} invalidClass @hide
1635 * @cfg {String} invalidText @hide
1638 * @cfg {String} msgFx @hide
1641 * @cfg {String} validateOnBlur @hide
1645 Roo.HtmlEditorCore.white = [
1646 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1648 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1649 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1650 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1651 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1652 'TABLE', 'UL', 'XMP',
1654 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1657 'DIR', 'MENU', 'OL', 'UL', 'DL',
1663 Roo.HtmlEditorCore.black = [
1664 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1666 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1667 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1668 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1669 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1670 //'FONT' // CLEAN LATER..
1671 'COLGROUP', 'COL' // messy tables.
1675 Roo.HtmlEditorCore.clean = [ // ?? needed???
1676 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1678 Roo.HtmlEditorCore.tag_remove = [
1683 Roo.HtmlEditorCore.ablack = [
1687 Roo.HtmlEditorCore.aclean = [
1688 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1692 Roo.HtmlEditorCore.pwhite= [
1693 'http', 'https', 'mailto'
1696 // white listed style attributes.
1697 Roo.HtmlEditorCore.cwhite= [
1698 // 'text-align', /// default is to allow most things..
1704 // black listed style attributes.
1705 Roo.HtmlEditorCore.cblack= [
1706 // 'font-size' -- this can be set by the project