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 new Roo.htmleditor.FilterBlock({ node : div });
382 var html = div.innerHTML;
385 if (this.autoClean) {
387 new Roo.htmleditor.FilterAttributes({
397 'data-caption-display',
410 attrib_clean : ['href', 'src' ]
413 var tidy = new Roo.htmleditor.TidySerializer({
416 html = tidy.serialize(div);
422 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
423 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
425 html = '<div style="'+m[0]+'">' + html + '</div>';
428 html = this.cleanHtml(html);
429 // fix up the special chars.. normaly like back quotes in word...
430 // however we do not want to do this with chinese..
431 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
433 var cc = match.charCodeAt();
435 // Get the character value, handling surrogate pairs
436 if (match.length == 2) {
437 // It's a surrogate pair, calculate the Unicode code point
438 var high = match.charCodeAt(0) - 0xD800;
439 var low = match.charCodeAt(1) - 0xDC00;
440 cc = (high * 0x400) + low + 0x10000;
442 (cc >= 0x4E00 && cc < 0xA000 ) ||
443 (cc >= 0x3400 && cc < 0x4E00 ) ||
444 (cc >= 0xf900 && cc < 0xfb00 )
449 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
450 return "&#" + cc + ";";
457 if(this.owner.fireEvent('beforesync', this, html) !== false){
458 this.el.dom.value = html;
459 this.owner.fireEvent('sync', this, html);
465 * TEXTAREA -> EDITABLE
466 * Protected method that will not generally be called directly. Pushes the value of the textarea
467 * into the iframe editor.
469 pushValue : function()
471 //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
472 if(this.initialized){
473 var v = this.el.dom.value.trim();
476 if(this.owner.fireEvent('beforepush', this, v) !== false){
477 var d = (this.doc.body || this.doc.documentElement);
480 this.el.dom.value = d.innerHTML;
481 this.owner.fireEvent('push', this, v);
483 if (this.autoClean) {
484 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
485 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
487 if (this.enableBlocks) {
488 Roo.htmleditor.Block.initAll(this.doc.body);
491 this.updateLanguage();
493 var lc = this.doc.body.lastChild;
494 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
495 // add an extra line at the end.
496 this.doc.body.appendChild(this.doc.createElement('br'));
504 deferFocus : function(){
505 this.focus.defer(10, this);
510 if(this.win && !this.sourceEditMode){
517 assignDocWin: function()
519 var iframe = this.iframe;
522 this.doc = iframe.contentWindow.document;
523 this.win = iframe.contentWindow;
525 // if (!Roo.get(this.frameId)) {
528 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
529 // this.win = Roo.get(this.frameId).dom.contentWindow;
531 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
535 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
536 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
541 initEditor : function(){
542 //console.log("INIT EDITOR");
547 this.doc.designMode="on";
549 this.doc.write(this.getDocMarkup());
552 var dbody = (this.doc.body || this.doc.documentElement);
553 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
554 // this copies styles from the containing element into thsi one..
555 // not sure why we need all of this..
556 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
558 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
559 //ss['background-attachment'] = 'fixed'; // w3c
560 dbody.bgProperties = 'fixed'; // ie
561 dbody.setAttribute("translate", "no");
563 //Roo.DomHelper.applyStyles(dbody, ss);
564 Roo.EventManager.on(this.doc, {
566 'mouseup': this.onEditorEvent,
567 'dblclick': this.onEditorEvent,
568 'click': this.onEditorEvent,
569 'keyup': this.onEditorEvent,
574 Roo.EventManager.on(this.doc, {
575 'paste': this.onPasteEvent,
579 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
582 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
583 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
585 this.initialized = true;
588 // initialize special key events - enter
589 new Roo.htmleditor.KeyEnter({core : this});
593 this.owner.fireEvent('initialize', this);
596 // this is to prevent a href clicks resulting in a redirect?
598 onPasteEvent : function(e,v)
600 // I think we better assume paste is going to be a dirty load of rubish from word..
602 // even pasting into a 'email version' of this widget will have to clean up that mess.
603 var cd = (e.browserEvent.clipboardData || window.clipboardData);
605 // check what type of paste - if it's an image, then handle it differently.
606 if (cd.files && cd.files.length > 0 && cd.types.indexOf('text/html') < 0) {
608 var urlAPI = (window.createObjectURL && window) ||
609 (window.URL && URL.revokeObjectURL && URL) ||
610 (window.webkitURL && webkitURL);
612 var r = new FileReader();
614 r.addEventListener('load',function()
617 var d = (new DOMParser().parseFromString('<img src="' + r.result+ '">', 'text/html')).body;
619 if (t.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..
632 t.insertAtCursor(d.innerHTML.replace(/ /g,' '));
633 t.owner.fireEvent('paste', this);
635 r.readAsDataURL(cd.files[0]);
641 if (cd.types.indexOf('text/html') < 0 ) {
645 var html = cd.getData('text/html'); // clipboard event
646 if (cd.types.indexOf('text/rtf') > -1) {
647 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
648 images = parser.doc ? parser.doc.getElementsByType('pict') : [];
653 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable|footerf)/); }) // ignore headers/footers etc.
654 .map(function(g) { return g.toDataURL(); })
655 .filter(function(g) { return g != 'about:blank'; });
658 html = this.cleanWordChars(html);
663 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
666 var sn = this.getParentElement();
667 // check if d contains a table, and prevent nesting??
668 //Roo.log(d.getElementsByTagName('table'));
670 //Roo.log(sn.closest('table'));
671 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
673 this.insertAtCursor("You can not nest tables");
674 //Roo.log("prevent?"); // fixme -
680 if (images.length > 0) {
681 // replace all v:imagedata - with img.
682 var ar = Array.from(d.getElementsByTagName('v:imagedata'));
683 Roo.each(ar, function(node) {
684 node.parentNode.insertBefore(d.ownerDocument.createElement('img'), node );
685 node.parentNode.removeChild(node);
689 Roo.each(d.getElementsByTagName('img'), function(img, i) {
690 img.setAttribute('src', images[i]);
693 if (this.autoClean) {
694 new Roo.htmleditor.FilterWord({ node : d });
696 new Roo.htmleditor.FilterStyleToTag({ node : d });
697 new Roo.htmleditor.FilterAttributes({
706 /* THESE ARE NOT ALLWOED FOR PASTE
708 'data-caption-display',
722 attrib_clean : ['href', 'src' ]
724 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
726 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', ':' ]} );
727 new Roo.htmleditor.FilterParagraph({ node : d });
728 new Roo.htmleditor.FilterHashLink({node : d});
729 new Roo.htmleditor.FilterSpan({ node : d });
730 new Roo.htmleditor.FilterLongBr({ node : d });
731 new Roo.htmleditor.FilterComment({ node : d });
735 if (this.enableBlocks) {
737 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
738 if (img.closest('figure')) { // assume!! that it's aready
741 var fig = new Roo.htmleditor.BlockFigure({
744 fig.updateElement(img); // replace it..
750 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
751 if (this.enableBlocks) {
752 Roo.htmleditor.Block.initAll(this.doc.body);
757 this.owner.fireEvent('paste', this);
759 // default behaveiour should be our local cleanup paste? (optional?)
760 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
761 //this.owner.fireEvent('paste', e, v);
764 onDestroy : function(){
770 //for (var i =0; i < this.toolbars.length;i++) {
771 // // fixme - ask toolbars for heights?
772 // this.toolbars[i].onDestroy();
775 //this.wrap.dom.innerHTML = '';
776 //this.wrap.remove();
781 onFirstFocus : function(){
784 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
786 this.activated = true;
789 if(Roo.isGecko){ // prevent silly gecko errors
791 var s = this.win.getSelection();
792 if(!s.focusNode || s.focusNode.nodeType != 3){
793 var r = s.getRangeAt(0);
794 r.selectNodeContents((this.doc.body || this.doc.documentElement));
799 this.execCmd('useCSS', true);
800 this.execCmd('styleWithCSS', false);
803 this.owner.fireEvent('activate', this);
807 adjustFont: function(btn){
808 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
809 //if(Roo.isSafari){ // safari
812 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
813 if(Roo.isSafari){ // safari
814 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
815 v = (v < 10) ? 10 : v;
816 v = (v > 48) ? 48 : v;
817 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
822 v = Math.max(1, v+adjust);
824 this.execCmd('FontSize', v );
827 onEditorEvent : function(e)
831 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
832 return; // we do not handle this.. (undo manager does..)
834 // clicking a 'block'?
836 // in theory this detects if the last element is not a br, then we try and do that.
837 // its so clicking in space at bottom triggers adding a br and moving the cursor.
839 e.target.nodeName == 'BODY' &&
840 e.type == "mouseup" &&
841 this.doc.body.lastChild
843 var lc = this.doc.body.lastChild;
844 // gtx-trans is google translate plugin adding crap.
845 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
846 lc = lc.previousSibling;
848 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
849 // if last element is <BR> - then dont do anything.
851 var ns = this.doc.createElement('br');
852 this.doc.body.appendChild(ns);
853 range = this.doc.createRange();
854 range.setStartAfter(ns);
855 range.collapse(true);
856 var sel = this.win.getSelection();
857 sel.removeAllRanges();
864 this.fireEditorEvent(e);
865 // this.updateToolbar();
866 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
869 fireEditorEvent: function(e)
871 this.owner.fireEvent('editorevent', this, e);
874 insertTag : function(tg)
876 // could be a bit smarter... -> wrap the current selected tRoo..
877 if (tg.toLowerCase() == 'span' ||
878 tg.toLowerCase() == 'code' ||
879 tg.toLowerCase() == 'sup' ||
880 tg.toLowerCase() == 'sub'
883 range = this.createRange(this.getSelection());
884 var wrappingNode = this.doc.createElement(tg.toLowerCase());
885 wrappingNode.appendChild(range.extractContents());
886 range.insertNode(wrappingNode);
893 this.execCmd("formatblock", tg);
894 this.undoManager.addEvent();
897 insertText : function(txt)
901 var range = this.createRange();
902 range.deleteContents();
903 //alert(Sender.getAttribute('label'));
905 range.insertNode(this.doc.createTextNode(txt));
906 this.undoManager.addEvent();
912 * Executes a Midas editor command on the editor document and performs necessary focus and
913 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
914 * @param {String} cmd The Midas command
915 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
917 relayCmd : function(cmd, value)
923 case 'justifycenter':
924 // if we are in a cell, then we will adjust the
925 var n = this.getParentElement();
926 var td = n.closest('td');
928 var bl = Roo.htmleditor.Block.factory(td);
929 bl.textAlign = cmd.replace('justify','');
931 this.owner.fireEvent('editorevent', this);
934 this.execCmd('styleWithCSS', true); //
939 // if there is no selection, then we insert, and set the curson inside it..
940 this.execCmd('styleWithCSS', false);
950 this.execCmd(cmd, value);
951 this.owner.fireEvent('editorevent', this);
952 //this.updateToolbar();
953 this.owner.deferFocus();
957 * Executes a Midas editor command directly on the editor document.
958 * For visual commands, you should use {@link #relayCmd} instead.
959 * <b>This should only be called after the editor is initialized.</b>
960 * @param {String} cmd The Midas command
961 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
963 execCmd : function(cmd, value){
964 this.doc.execCommand(cmd, false, value === undefined ? null : value);
971 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
973 * @param {String} text | dom node..
975 insertAtCursor : function(text)
982 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
986 // from jquery ui (MIT licenced)
990 if (win.getSelection && win.getSelection().getRangeAt) {
992 // delete the existing?
994 this.createRange(this.getSelection()).deleteContents();
995 range = win.getSelection().getRangeAt(0);
996 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
997 range.insertNode(node);
998 range = range.cloneRange();
999 range.collapse(false);
1001 win.getSelection().removeAllRanges();
1002 win.getSelection().addRange(range);
1006 } else if (win.document.selection && win.document.selection.createRange) {
1007 // no firefox support
1008 var txt = typeof(text) == 'string' ? text : text.outerHTML;
1009 win.document.selection.createRange().pasteHTML(txt);
1012 // no firefox support
1013 var txt = typeof(text) == 'string' ? text : text.outerHTML;
1014 this.execCmd('InsertHTML', txt);
1022 mozKeyPress : function(e){
1024 var c = e.getCharCode(), cmd;
1027 c = String.fromCharCode(c).toLowerCase();
1041 // this.cleanUpPaste.defer(100, this);
1049 //this.execCmd(cmd);
1050 //this.deferFocus();
1059 fixKeys : function(){ // load time branching for fastest keydown performance
1064 var k = e.getKey(), r;
1067 r = this.doc.selection.createRange();
1070 r.pasteHTML('    ');
1075 /// this is handled by Roo.htmleditor.KeyEnter
1078 r = this.doc.selection.createRange();
1080 var target = r.parentElement();
1081 if(!target || target.tagName.toLowerCase() != 'li'){
1083 r.pasteHTML('<br/>');
1090 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1091 // this.cleanUpPaste.defer(100, this);
1097 }else if(Roo.isOpera){
1103 this.execCmd('InsertHTML','    ');
1107 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1108 // this.cleanUpPaste.defer(100, this);
1113 }else if(Roo.isSafari){
1119 this.execCmd('InsertText','\t');
1123 this.mozKeyPress(e);
1125 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1126 // this.cleanUpPaste.defer(100, this);
1134 getAllAncestors: function()
1136 var p = this.getSelectedNode();
1139 a.push(p); // push blank onto stack..
1140 p = this.getParentElement();
1144 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1148 a.push(this.doc.body);
1152 lastSelNode : false,
1155 getSelection : function()
1157 this.assignDocWin();
1158 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1162 * @param {DomElement} node the node to select
1164 selectNode : function(node, collapse)
1166 var nodeRange = node.ownerDocument.createRange();
1168 nodeRange.selectNode(node);
1170 nodeRange.selectNodeContents(node);
1172 if (collapse === true) {
1173 nodeRange.collapse(true);
1176 var s = this.win.getSelection();
1177 s.removeAllRanges();
1178 s.addRange(nodeRange);
1181 getSelectedNode: function()
1183 // this may only work on Gecko!!!
1185 // should we cache this!!!!
1189 var range = this.createRange(this.getSelection()).cloneRange();
1192 var parent = range.parentElement();
1194 var testRange = range.duplicate();
1195 testRange.moveToElementText(parent);
1196 if (testRange.inRange(range)) {
1199 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1202 parent = parent.parentElement;
1207 // is ancestor a text element.
1208 var ac = range.commonAncestorContainer;
1209 if (ac.nodeType == 3) {
1213 var ar = ac.childNodes;
1216 var other_nodes = [];
1217 var has_other_nodes = false;
1218 for (var i=0;i<ar.length;i++) {
1219 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1222 // fullly contained node.
1224 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1229 // probably selected..
1230 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1231 other_nodes.push(ar[i]);
1235 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1240 has_other_nodes = true;
1242 if (!nodes.length && other_nodes.length) {
1245 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1253 createRange: function(sel)
1255 // this has strange effects when using with
1256 // top toolbar - not sure if it's a great idea.
1257 //this.editor.contentWindow.focus();
1258 if (typeof sel != "undefined") {
1260 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1262 return this.doc.createRange();
1265 return this.doc.createRange();
1268 getParentElement: function()
1271 this.assignDocWin();
1272 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1274 var range = this.createRange(sel);
1277 var p = range.commonAncestorContainer;
1278 while (p.nodeType == 3) { // text node
1289 * Range intersection.. the hard stuff...
1293 * [ -- selected range --- ]
1297 * if end is before start or hits it. fail.
1298 * if start is after end or hits it fail.
1300 * if either hits (but other is outside. - then it's not
1306 // @see http://www.thismuchiknow.co.uk/?p=64.
1307 rangeIntersectsNode : function(range, node)
1309 var nodeRange = node.ownerDocument.createRange();
1311 nodeRange.selectNode(node);
1313 nodeRange.selectNodeContents(node);
1316 var rangeStartRange = range.cloneRange();
1317 rangeStartRange.collapse(true);
1319 var rangeEndRange = range.cloneRange();
1320 rangeEndRange.collapse(false);
1322 var nodeStartRange = nodeRange.cloneRange();
1323 nodeStartRange.collapse(true);
1325 var nodeEndRange = nodeRange.cloneRange();
1326 nodeEndRange.collapse(false);
1328 return rangeStartRange.compareBoundaryPoints(
1329 Range.START_TO_START, nodeEndRange) == -1 &&
1330 rangeEndRange.compareBoundaryPoints(
1331 Range.START_TO_START, nodeStartRange) == 1;
1335 rangeCompareNode : function(range, node)
1337 var nodeRange = node.ownerDocument.createRange();
1339 nodeRange.selectNode(node);
1341 nodeRange.selectNodeContents(node);
1345 range.collapse(true);
1347 nodeRange.collapse(true);
1349 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1350 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1352 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1354 var nodeIsBefore = ss == 1;
1355 var nodeIsAfter = ee == -1;
1357 if (nodeIsBefore && nodeIsAfter) {
1360 if (!nodeIsBefore && nodeIsAfter) {
1361 return 1; //right trailed.
1364 if (nodeIsBefore && !nodeIsAfter) {
1365 return 2; // left trailed.
1371 cleanWordChars : function(input) {// change the chars to hex code
1374 [ 8211, "–" ],
1375 [ 8212, "—" ],
1384 Roo.each(swapCodes, function(sw) {
1385 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1387 output = output.replace(swapper, sw[1]);
1397 cleanUpChild : function (node)
1400 new Roo.htmleditor.FilterComment({node : node});
1401 new Roo.htmleditor.FilterAttributes({
1403 attrib_black : this.ablack,
1404 attrib_clean : this.aclean,
1405 style_white : this.cwhite,
1406 style_black : this.cblack
1408 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1409 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1415 * Clean up MS wordisms...
1416 * @deprecated - use filter directly
1418 cleanWord : function(node)
1420 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1421 new Roo.htmleditor.FilterKeepChildren({node : node ? node : this.doc.body, tag : [ 'FONT', ':' ]} );
1428 * @deprecated - use filters
1430 cleanTableWidths : function(node)
1432 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1439 applyBlacklists : function()
1441 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1442 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1444 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1445 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1446 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1450 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1451 if (b.indexOf(tag) > -1) {
1454 this.white.push(tag);
1458 Roo.each(w, function(tag) {
1459 if (b.indexOf(tag) > -1) {
1462 if (this.white.indexOf(tag) > -1) {
1465 this.white.push(tag);
1470 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1471 if (w.indexOf(tag) > -1) {
1474 this.black.push(tag);
1478 Roo.each(b, function(tag) {
1479 if (w.indexOf(tag) > -1) {
1482 if (this.black.indexOf(tag) > -1) {
1485 this.black.push(tag);
1490 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1491 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1495 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1496 if (b.indexOf(tag) > -1) {
1499 this.cwhite.push(tag);
1503 Roo.each(w, function(tag) {
1504 if (b.indexOf(tag) > -1) {
1507 if (this.cwhite.indexOf(tag) > -1) {
1510 this.cwhite.push(tag);
1515 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1516 if (w.indexOf(tag) > -1) {
1519 this.cblack.push(tag);
1523 Roo.each(b, function(tag) {
1524 if (w.indexOf(tag) > -1) {
1527 if (this.cblack.indexOf(tag) > -1) {
1530 this.cblack.push(tag);
1535 setStylesheets : function(stylesheets)
1537 if(typeof(stylesheets) == 'string'){
1538 Roo.get(this.iframe.contentDocument.head).createChild({
1549 Roo.each(stylesheets, function(s) {
1554 Roo.get(_this.iframe.contentDocument.head).createChild({
1566 updateLanguage : function()
1568 if (!this.iframe || !this.iframe.contentDocument) {
1571 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1575 removeStylesheets : function()
1579 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1584 setStyle : function(style)
1586 Roo.get(this.iframe.contentDocument.head).createChild({
1595 // hide stuff that is not compatible
1613 * @cfg {String} fieldClass @hide
1616 * @cfg {String} focusClass @hide
1619 * @cfg {String} autoCreate @hide
1622 * @cfg {String} inputType @hide
1625 * @cfg {String} invalidClass @hide
1628 * @cfg {String} invalidText @hide
1631 * @cfg {String} msgFx @hide
1634 * @cfg {String} validateOnBlur @hide
1638 Roo.HtmlEditorCore.white = [
1639 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1641 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1642 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1643 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1644 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1645 'TABLE', 'UL', 'XMP',
1647 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1650 'DIR', 'MENU', 'OL', 'UL', 'DL',
1656 Roo.HtmlEditorCore.black = [
1657 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1659 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1660 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1661 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1662 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1663 //'FONT' // CLEAN LATER..
1664 'COLGROUP', 'COL' // messy tables.
1668 Roo.HtmlEditorCore.clean = [ // ?? needed???
1669 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1671 Roo.HtmlEditorCore.tag_remove = [
1676 Roo.HtmlEditorCore.ablack = [
1680 Roo.HtmlEditorCore.aclean = [
1681 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1685 Roo.HtmlEditorCore.pwhite= [
1686 'http', 'https', 'mailto'
1689 // white listed style attributes.
1690 Roo.HtmlEditorCore.cwhite= [
1691 // 'text-align', /// default is to allow most things..
1697 // black listed style attributes.
1698 Roo.HtmlEditorCore.cblack= [
1699 // 'font-size' -- this can be set by the project