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({
409 attrib_clean : ['href', 'src' ]
412 var tidy = new Roo.htmleditor.TidySerializer({
415 html = tidy.serialize(div);
421 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
422 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
424 html = '<div style="'+m[0]+'">' + html + '</div>';
427 html = this.cleanHtml(html);
428 // fix up the special chars.. normaly like back quotes in word...
429 // however we do not want to do this with chinese..
430 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
432 var cc = match.charCodeAt();
434 // Get the character value, handling surrogate pairs
435 if (match.length == 2) {
436 // It's a surrogate pair, calculate the Unicode code point
437 var high = match.charCodeAt(0) - 0xD800;
438 var low = match.charCodeAt(1) - 0xDC00;
439 cc = (high * 0x400) + low + 0x10000;
441 (cc >= 0x4E00 && cc < 0xA000 ) ||
442 (cc >= 0x3400 && cc < 0x4E00 ) ||
443 (cc >= 0xf900 && cc < 0xfb00 )
448 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
449 return "&#" + cc + ";";
456 if(this.owner.fireEvent('beforesync', this, html) !== false){
457 this.el.dom.value = html;
458 this.owner.fireEvent('sync', this, html);
464 * TEXTAREA -> EDITABLE
465 * Protected method that will not generally be called directly. Pushes the value of the textarea
466 * into the iframe editor.
468 pushValue : function()
470 //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
471 if(this.initialized){
472 var v = this.el.dom.value.trim();
475 if(this.owner.fireEvent('beforepush', this, v) !== false){
476 var d = (this.doc.body || this.doc.documentElement);
479 this.el.dom.value = d.innerHTML;
480 this.owner.fireEvent('push', this, v);
482 if (this.autoClean) {
483 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
484 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
486 if (this.enableBlocks) {
487 Roo.htmleditor.Block.initAll(this.doc.body);
490 this.updateLanguage();
492 var lc = this.doc.body.lastChild;
493 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
494 // add an extra line at the end.
495 this.doc.body.appendChild(this.doc.createElement('br'));
503 deferFocus : function(){
504 this.focus.defer(10, this);
509 if(this.win && !this.sourceEditMode){
516 assignDocWin: function()
518 var iframe = this.iframe;
521 this.doc = iframe.contentWindow.document;
522 this.win = iframe.contentWindow;
524 // if (!Roo.get(this.frameId)) {
527 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
528 // this.win = Roo.get(this.frameId).dom.contentWindow;
530 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
534 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
535 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
540 initEditor : function(){
541 //console.log("INIT EDITOR");
546 this.doc.designMode="on";
548 this.doc.write(this.getDocMarkup());
551 var dbody = (this.doc.body || this.doc.documentElement);
552 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
553 // this copies styles from the containing element into thsi one..
554 // not sure why we need all of this..
555 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
557 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
558 //ss['background-attachment'] = 'fixed'; // w3c
559 dbody.bgProperties = 'fixed'; // ie
560 dbody.setAttribute("translate", "no");
562 //Roo.DomHelper.applyStyles(dbody, ss);
563 Roo.EventManager.on(this.doc, {
565 'mouseup': this.onEditorEvent,
566 'dblclick': this.onEditorEvent,
567 'click': this.onEditorEvent,
568 'keyup': this.onEditorEvent,
573 Roo.EventManager.on(this.doc, {
574 'paste': this.onPasteEvent,
578 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
581 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
582 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
584 this.initialized = true;
587 // initialize special key events - enter
588 new Roo.htmleditor.KeyEnter({core : this});
592 this.owner.fireEvent('initialize', this);
595 // this is to prevent a href clicks resulting in a redirect?
597 onPasteEvent : function(e,v)
599 // I think we better assume paste is going to be a dirty load of rubish from word..
601 // even pasting into a 'email version' of this widget will have to clean up that mess.
602 var cd = (e.browserEvent.clipboardData || window.clipboardData);
604 // check what type of paste - if it's an image, then handle it differently.
605 if (cd.files && cd.files.length > 0 && cd.types.indexOf('text/html') < 0) {
607 var urlAPI = (window.createObjectURL && window) ||
608 (window.URL && URL.revokeObjectURL && URL) ||
609 (window.webkitURL && webkitURL);
611 var r = new FileReader();
613 r.addEventListener('load',function()
616 var d = (new DOMParser().parseFromString('<img src="' + r.result+ '">', 'text/html')).body;
618 if (t.enableBlocks) {
620 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
621 if (img.closest('figure')) { // assume!! that it's aready
624 var fig = new Roo.htmleditor.BlockFigure({
627 fig.updateElement(img); // replace it..
631 t.insertAtCursor(d.innerHTML.replace(/ /g,' '));
632 t.owner.fireEvent('paste', this);
634 r.readAsDataURL(cd.files[0]);
640 if (cd.types.indexOf('text/html') < 0 ) {
644 var html = cd.getData('text/html'); // clipboard event
645 if (cd.types.indexOf('text/rtf') > -1) {
646 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
647 images = parser.doc ? parser.doc.getElementsByType('pict') : [];
652 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable|footerf)/); }) // ignore headers/footers etc.
653 .map(function(g) { return g.toDataURL(); })
654 .filter(function(g) { return g != 'about:blank'; });
657 html = this.cleanWordChars(html);
659 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
662 var sn = this.getParentElement();
663 // check if d contains a table, and prevent nesting??
664 //Roo.log(d.getElementsByTagName('table'));
666 //Roo.log(sn.closest('table'));
667 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
669 this.insertAtCursor("You can not nest tables");
670 //Roo.log("prevent?"); // fixme -
676 if (images.length > 0) {
677 // replace all v:imagedata - with img.
678 var ar = Array.from(d.getElementsByTagName('v:imagedata'));
679 Roo.each(ar, function(node) {
680 node.parentNode.insertBefore(d.ownerDocument.createElement('img'), node );
681 node.parentNode.removeChild(node);
685 Roo.each(d.getElementsByTagName('img'), function(img, i) {
686 img.setAttribute('src', images[i]);
689 if (this.autoClean) {
690 new Roo.htmleditor.FilterWord({ node : d });
692 new Roo.htmleditor.FilterStyleToTag({ node : d });
693 new Roo.htmleditor.FilterAttributes({
695 attrib_white : ['href', 'src', 'name', 'align', 'colspan', 'rowspan', 'data-display', 'data-width', 'start'],
696 attrib_clean : ['href', 'src' ]
698 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
700 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', ':' ]} );
701 new Roo.htmleditor.FilterParagraph({ node : d });
702 new Roo.htmleditor.FilterSpan({ node : d });
703 new Roo.htmleditor.FilterLongBr({ node : d });
704 new Roo.htmleditor.FilterComment({ node : d });
708 if (this.enableBlocks) {
710 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
711 if (img.closest('figure')) { // assume!! that it's aready
714 var fig = new Roo.htmleditor.BlockFigure({
717 fig.updateElement(img); // replace it..
723 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
724 if (this.enableBlocks) {
725 Roo.htmleditor.Block.initAll(this.doc.body);
730 this.owner.fireEvent('paste', this);
732 // default behaveiour should be our local cleanup paste? (optional?)
733 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
734 //this.owner.fireEvent('paste', e, v);
737 onDestroy : function(){
743 //for (var i =0; i < this.toolbars.length;i++) {
744 // // fixme - ask toolbars for heights?
745 // this.toolbars[i].onDestroy();
748 //this.wrap.dom.innerHTML = '';
749 //this.wrap.remove();
754 onFirstFocus : function(){
757 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
759 this.activated = true;
762 if(Roo.isGecko){ // prevent silly gecko errors
764 var s = this.win.getSelection();
765 if(!s.focusNode || s.focusNode.nodeType != 3){
766 var r = s.getRangeAt(0);
767 r.selectNodeContents((this.doc.body || this.doc.documentElement));
772 this.execCmd('useCSS', true);
773 this.execCmd('styleWithCSS', false);
776 this.owner.fireEvent('activate', this);
780 adjustFont: function(btn){
781 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
782 //if(Roo.isSafari){ // safari
785 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
786 if(Roo.isSafari){ // safari
787 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
788 v = (v < 10) ? 10 : v;
789 v = (v > 48) ? 48 : v;
790 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
795 v = Math.max(1, v+adjust);
797 this.execCmd('FontSize', v );
800 onEditorEvent : function(e)
804 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
805 return; // we do not handle this.. (undo manager does..)
807 // clicking a 'block'?
809 // in theory this detects if the last element is not a br, then we try and do that.
810 // its so clicking in space at bottom triggers adding a br and moving the cursor.
812 e.target.nodeName == 'BODY' &&
813 e.type == "mouseup" &&
814 this.doc.body.lastChild
816 var lc = this.doc.body.lastChild;
817 // gtx-trans is google translate plugin adding crap.
818 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
819 lc = lc.previousSibling;
821 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
822 // if last element is <BR> - then dont do anything.
824 var ns = this.doc.createElement('br');
825 this.doc.body.appendChild(ns);
826 range = this.doc.createRange();
827 range.setStartAfter(ns);
828 range.collapse(true);
829 var sel = this.win.getSelection();
830 sel.removeAllRanges();
837 this.fireEditorEvent(e);
838 // this.updateToolbar();
839 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
842 fireEditorEvent: function(e)
844 this.owner.fireEvent('editorevent', this, e);
847 insertTag : function(tg)
849 // could be a bit smarter... -> wrap the current selected tRoo..
850 if (tg.toLowerCase() == 'span' ||
851 tg.toLowerCase() == 'code' ||
852 tg.toLowerCase() == 'sup' ||
853 tg.toLowerCase() == 'sub'
856 range = this.createRange(this.getSelection());
857 var wrappingNode = this.doc.createElement(tg.toLowerCase());
858 wrappingNode.appendChild(range.extractContents());
859 range.insertNode(wrappingNode);
866 this.execCmd("formatblock", tg);
867 this.undoManager.addEvent();
870 insertText : function(txt)
874 var range = this.createRange();
875 range.deleteContents();
876 //alert(Sender.getAttribute('label'));
878 range.insertNode(this.doc.createTextNode(txt));
879 this.undoManager.addEvent();
885 * Executes a Midas editor command on the editor document and performs necessary focus and
886 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
887 * @param {String} cmd The Midas command
888 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
890 relayCmd : function(cmd, value)
896 case 'justifycenter':
897 // if we are in a cell, then we will adjust the
898 var n = this.getParentElement();
899 var td = n.closest('td');
901 var bl = Roo.htmleditor.Block.factory(td);
902 bl.textAlign = cmd.replace('justify','');
904 this.owner.fireEvent('editorevent', this);
907 this.execCmd('styleWithCSS', true); //
912 // if there is no selection, then we insert, and set the curson inside it..
913 this.execCmd('styleWithCSS', false);
923 this.execCmd(cmd, value);
924 this.owner.fireEvent('editorevent', this);
925 //this.updateToolbar();
926 this.owner.deferFocus();
930 * Executes a Midas editor command directly on the editor document.
931 * For visual commands, you should use {@link #relayCmd} instead.
932 * <b>This should only be called after the editor is initialized.</b>
933 * @param {String} cmd The Midas command
934 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
936 execCmd : function(cmd, value){
937 this.doc.execCommand(cmd, false, value === undefined ? null : value);
944 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
946 * @param {String} text | dom node..
948 insertAtCursor : function(text)
955 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
959 // from jquery ui (MIT licenced)
963 if (win.getSelection && win.getSelection().getRangeAt) {
965 // delete the existing?
967 this.createRange(this.getSelection()).deleteContents();
968 range = win.getSelection().getRangeAt(0);
969 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
970 range.insertNode(node);
971 range = range.cloneRange();
972 range.collapse(false);
974 win.getSelection().removeAllRanges();
975 win.getSelection().addRange(range);
979 } else if (win.document.selection && win.document.selection.createRange) {
980 // no firefox support
981 var txt = typeof(text) == 'string' ? text : text.outerHTML;
982 win.document.selection.createRange().pasteHTML(txt);
985 // no firefox support
986 var txt = typeof(text) == 'string' ? text : text.outerHTML;
987 this.execCmd('InsertHTML', txt);
995 mozKeyPress : function(e){
997 var c = e.getCharCode(), cmd;
1000 c = String.fromCharCode(c).toLowerCase();
1014 // this.cleanUpPaste.defer(100, this);
1022 //this.execCmd(cmd);
1023 //this.deferFocus();
1032 fixKeys : function(){ // load time branching for fastest keydown performance
1037 var k = e.getKey(), r;
1040 r = this.doc.selection.createRange();
1043 r.pasteHTML('    ');
1048 /// this is handled by Roo.htmleditor.KeyEnter
1051 r = this.doc.selection.createRange();
1053 var target = r.parentElement();
1054 if(!target || target.tagName.toLowerCase() != 'li'){
1056 r.pasteHTML('<br/>');
1063 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1064 // this.cleanUpPaste.defer(100, this);
1070 }else if(Roo.isOpera){
1076 this.execCmd('InsertHTML','    ');
1080 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1081 // this.cleanUpPaste.defer(100, this);
1086 }else if(Roo.isSafari){
1092 this.execCmd('InsertText','\t');
1096 this.mozKeyPress(e);
1098 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1099 // this.cleanUpPaste.defer(100, this);
1107 getAllAncestors: function()
1109 var p = this.getSelectedNode();
1112 a.push(p); // push blank onto stack..
1113 p = this.getParentElement();
1117 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1121 a.push(this.doc.body);
1125 lastSelNode : false,
1128 getSelection : function()
1130 this.assignDocWin();
1131 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1135 * @param {DomElement} node the node to select
1137 selectNode : function(node, collapse)
1139 var nodeRange = node.ownerDocument.createRange();
1141 nodeRange.selectNode(node);
1143 nodeRange.selectNodeContents(node);
1145 if (collapse === true) {
1146 nodeRange.collapse(true);
1149 var s = this.win.getSelection();
1150 s.removeAllRanges();
1151 s.addRange(nodeRange);
1154 getSelectedNode: function()
1156 // this may only work on Gecko!!!
1158 // should we cache this!!!!
1162 var range = this.createRange(this.getSelection()).cloneRange();
1165 var parent = range.parentElement();
1167 var testRange = range.duplicate();
1168 testRange.moveToElementText(parent);
1169 if (testRange.inRange(range)) {
1172 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1175 parent = parent.parentElement;
1180 // is ancestor a text element.
1181 var ac = range.commonAncestorContainer;
1182 if (ac.nodeType == 3) {
1186 var ar = ac.childNodes;
1189 var other_nodes = [];
1190 var has_other_nodes = false;
1191 for (var i=0;i<ar.length;i++) {
1192 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1195 // fullly contained node.
1197 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1202 // probably selected..
1203 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1204 other_nodes.push(ar[i]);
1208 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1213 has_other_nodes = true;
1215 if (!nodes.length && other_nodes.length) {
1218 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1226 createRange: function(sel)
1228 // this has strange effects when using with
1229 // top toolbar - not sure if it's a great idea.
1230 //this.editor.contentWindow.focus();
1231 if (typeof sel != "undefined") {
1233 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1235 return this.doc.createRange();
1238 return this.doc.createRange();
1241 getParentElement: function()
1244 this.assignDocWin();
1245 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1247 var range = this.createRange(sel);
1250 var p = range.commonAncestorContainer;
1251 while (p.nodeType == 3) { // text node
1262 * Range intersection.. the hard stuff...
1266 * [ -- selected range --- ]
1270 * if end is before start or hits it. fail.
1271 * if start is after end or hits it fail.
1273 * if either hits (but other is outside. - then it's not
1279 // @see http://www.thismuchiknow.co.uk/?p=64.
1280 rangeIntersectsNode : function(range, node)
1282 var nodeRange = node.ownerDocument.createRange();
1284 nodeRange.selectNode(node);
1286 nodeRange.selectNodeContents(node);
1289 var rangeStartRange = range.cloneRange();
1290 rangeStartRange.collapse(true);
1292 var rangeEndRange = range.cloneRange();
1293 rangeEndRange.collapse(false);
1295 var nodeStartRange = nodeRange.cloneRange();
1296 nodeStartRange.collapse(true);
1298 var nodeEndRange = nodeRange.cloneRange();
1299 nodeEndRange.collapse(false);
1301 return rangeStartRange.compareBoundaryPoints(
1302 Range.START_TO_START, nodeEndRange) == -1 &&
1303 rangeEndRange.compareBoundaryPoints(
1304 Range.START_TO_START, nodeStartRange) == 1;
1308 rangeCompareNode : function(range, node)
1310 var nodeRange = node.ownerDocument.createRange();
1312 nodeRange.selectNode(node);
1314 nodeRange.selectNodeContents(node);
1318 range.collapse(true);
1320 nodeRange.collapse(true);
1322 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1323 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1325 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1327 var nodeIsBefore = ss == 1;
1328 var nodeIsAfter = ee == -1;
1330 if (nodeIsBefore && nodeIsAfter) {
1333 if (!nodeIsBefore && nodeIsAfter) {
1334 return 1; //right trailed.
1337 if (nodeIsBefore && !nodeIsAfter) {
1338 return 2; // left trailed.
1344 cleanWordChars : function(input) {// change the chars to hex code
1347 [ 8211, "–" ],
1348 [ 8212, "—" ],
1357 Roo.each(swapCodes, function(sw) {
1358 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1360 output = output.replace(swapper, sw[1]);
1370 cleanUpChild : function (node)
1373 new Roo.htmleditor.FilterComment({node : node});
1374 new Roo.htmleditor.FilterAttributes({
1376 attrib_black : this.ablack,
1377 attrib_clean : this.aclean,
1378 style_white : this.cwhite,
1379 style_black : this.cblack
1381 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1382 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1388 * Clean up MS wordisms...
1389 * @deprecated - use filter directly
1391 cleanWord : function(node)
1393 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1394 new Roo.htmleditor.FilterKeepChildren({node : node ? node : this.doc.body, tag : [ 'FONT', ':' ]} );
1401 * @deprecated - use filters
1403 cleanTableWidths : function(node)
1405 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1412 applyBlacklists : function()
1414 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1415 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1417 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1418 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1419 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1423 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1424 if (b.indexOf(tag) > -1) {
1427 this.white.push(tag);
1431 Roo.each(w, function(tag) {
1432 if (b.indexOf(tag) > -1) {
1435 if (this.white.indexOf(tag) > -1) {
1438 this.white.push(tag);
1443 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1444 if (w.indexOf(tag) > -1) {
1447 this.black.push(tag);
1451 Roo.each(b, function(tag) {
1452 if (w.indexOf(tag) > -1) {
1455 if (this.black.indexOf(tag) > -1) {
1458 this.black.push(tag);
1463 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1464 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1468 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1469 if (b.indexOf(tag) > -1) {
1472 this.cwhite.push(tag);
1476 Roo.each(w, function(tag) {
1477 if (b.indexOf(tag) > -1) {
1480 if (this.cwhite.indexOf(tag) > -1) {
1483 this.cwhite.push(tag);
1488 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1489 if (w.indexOf(tag) > -1) {
1492 this.cblack.push(tag);
1496 Roo.each(b, function(tag) {
1497 if (w.indexOf(tag) > -1) {
1500 if (this.cblack.indexOf(tag) > -1) {
1503 this.cblack.push(tag);
1508 setStylesheets : function(stylesheets)
1510 if(typeof(stylesheets) == 'string'){
1511 Roo.get(this.iframe.contentDocument.head).createChild({
1522 Roo.each(stylesheets, function(s) {
1527 Roo.get(_this.iframe.contentDocument.head).createChild({
1539 updateLanguage : function()
1541 if (!this.iframe || !this.iframe.contentDocument) {
1544 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1548 removeStylesheets : function()
1552 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1557 setStyle : function(style)
1559 Roo.get(this.iframe.contentDocument.head).createChild({
1568 // hide stuff that is not compatible
1586 * @cfg {String} fieldClass @hide
1589 * @cfg {String} focusClass @hide
1592 * @cfg {String} autoCreate @hide
1595 * @cfg {String} inputType @hide
1598 * @cfg {String} invalidClass @hide
1601 * @cfg {String} invalidText @hide
1604 * @cfg {String} msgFx @hide
1607 * @cfg {String} validateOnBlur @hide
1611 Roo.HtmlEditorCore.white = [
1612 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1614 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1615 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1616 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1617 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1618 'TABLE', 'UL', 'XMP',
1620 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1623 'DIR', 'MENU', 'OL', 'UL', 'DL',
1629 Roo.HtmlEditorCore.black = [
1630 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1632 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1633 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1634 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1635 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1636 //'FONT' // CLEAN LATER..
1637 'COLGROUP', 'COL' // messy tables.
1641 Roo.HtmlEditorCore.clean = [ // ?? needed???
1642 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1644 Roo.HtmlEditorCore.tag_remove = [
1649 Roo.HtmlEditorCore.ablack = [
1653 Roo.HtmlEditorCore.aclean = [
1654 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1658 Roo.HtmlEditorCore.pwhite= [
1659 'http', 'https', 'mailto'
1662 // white listed style attributes.
1663 Roo.HtmlEditorCore.cwhite= [
1664 // 'text-align', /// default is to allow most things..
1670 // black listed style attributes.
1671 Roo.HtmlEditorCore.cblack= [
1672 // 'font-size' -- this can be set by the project