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({
408 attrib_clean : ['href', 'src' ]
411 var tidy = new Roo.htmleditor.TidySerializer({
414 html = tidy.serialize(div);
420 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
421 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
423 html = '<div style="'+m[0]+'">' + html + '</div>';
426 html = this.cleanHtml(html);
427 // fix up the special chars.. normaly like back quotes in word...
428 // however we do not want to do this with chinese..
429 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
431 var cc = match.charCodeAt();
433 // Get the character value, handling surrogate pairs
434 if (match.length == 2) {
435 // It's a surrogate pair, calculate the Unicode code point
436 var high = match.charCodeAt(0) - 0xD800;
437 var low = match.charCodeAt(1) - 0xDC00;
438 cc = (high * 0x400) + low + 0x10000;
440 (cc >= 0x4E00 && cc < 0xA000 ) ||
441 (cc >= 0x3400 && cc < 0x4E00 ) ||
442 (cc >= 0xf900 && cc < 0xfb00 )
447 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
448 return "&#" + cc + ";";
455 if(this.owner.fireEvent('beforesync', this, html) !== false){
456 this.el.dom.value = html;
457 this.owner.fireEvent('sync', this, html);
463 * TEXTAREA -> EDITABLE
464 * Protected method that will not generally be called directly. Pushes the value of the textarea
465 * into the iframe editor.
467 pushValue : function()
469 //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
470 if(this.initialized){
471 var v = this.el.dom.value.trim();
474 if(this.owner.fireEvent('beforepush', this, v) !== false){
475 var d = (this.doc.body || this.doc.documentElement);
478 this.el.dom.value = d.innerHTML;
479 this.owner.fireEvent('push', this, v);
481 if (this.autoClean) {
482 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
483 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
485 if (this.enableBlocks) {
486 Roo.htmleditor.Block.initAll(this.doc.body);
489 this.updateLanguage();
491 var lc = this.doc.body.lastChild;
492 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
493 // add an extra line at the end.
494 this.doc.body.appendChild(this.doc.createElement('br'));
502 deferFocus : function(){
503 this.focus.defer(10, this);
508 if(this.win && !this.sourceEditMode){
515 assignDocWin: function()
517 var iframe = this.iframe;
520 this.doc = iframe.contentWindow.document;
521 this.win = iframe.contentWindow;
523 // if (!Roo.get(this.frameId)) {
526 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
527 // this.win = Roo.get(this.frameId).dom.contentWindow;
529 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
533 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
534 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
539 initEditor : function(){
540 //console.log("INIT EDITOR");
545 this.doc.designMode="on";
547 this.doc.write(this.getDocMarkup());
550 var dbody = (this.doc.body || this.doc.documentElement);
551 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
552 // this copies styles from the containing element into thsi one..
553 // not sure why we need all of this..
554 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
556 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
557 //ss['background-attachment'] = 'fixed'; // w3c
558 dbody.bgProperties = 'fixed'; // ie
559 dbody.setAttribute("translate", "no");
561 //Roo.DomHelper.applyStyles(dbody, ss);
562 Roo.EventManager.on(this.doc, {
564 'mouseup': this.onEditorEvent,
565 'dblclick': this.onEditorEvent,
566 'click': this.onEditorEvent,
567 'keyup': this.onEditorEvent,
572 Roo.EventManager.on(this.doc, {
573 'paste': this.onPasteEvent,
577 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
580 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
581 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
583 this.initialized = true;
586 // initialize special key events - enter
587 new Roo.htmleditor.KeyEnter({core : this});
591 this.owner.fireEvent('initialize', this);
594 // this is to prevent a href clicks resulting in a redirect?
596 onPasteEvent : function(e,v)
598 // I think we better assume paste is going to be a dirty load of rubish from word..
600 // even pasting into a 'email version' of this widget will have to clean up that mess.
601 var cd = (e.browserEvent.clipboardData || window.clipboardData);
603 // check what type of paste - if it's an image, then handle it differently.
604 if (cd.files && cd.files.length > 0) {
606 var urlAPI = (window.createObjectURL && window) ||
607 (window.URL && URL.revokeObjectURL && URL) ||
608 (window.webkitURL && webkitURL);
610 var url = urlAPI.createObjectURL( cd.files[0]);
611 var d = (new DOMParser().parseFromString('<img src="' + url + '">', 'text/html')).body;
613 if (this.enableBlocks) {
615 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
616 if (img.closest('figure')) { // assume!! that it's aready
619 var fig = new Roo.htmleditor.BlockFigure({
622 fig.updateElement(img); // replace it..
626 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
628 this.owner.fireEvent('paste', this);
631 if (cd.types.indexOf('text/html') < 0 ) {
635 var html = cd.getData('text/html'); // clipboard event
636 if (cd.types.indexOf('text/rtf') > -1) {
637 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
638 images = parser.doc ? parser.doc.getElementsByType('pict') : [];
643 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable|footerf)/); }) // ignore headers/footers etc.
644 .map(function(g) { return g.toDataURL(); })
645 .filter(function(g) { return g != 'about:blank'; });
648 html = this.cleanWordChars(html);
650 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
653 var sn = this.getParentElement();
654 // check if d contains a table, and prevent nesting??
655 //Roo.log(d.getElementsByTagName('table'));
657 //Roo.log(sn.closest('table'));
658 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
660 this.insertAtCursor("You can not nest tables");
661 //Roo.log("prevent?"); // fixme -
667 if (images.length > 0) {
668 // replace all v:imagedata - with img.
669 var ar = Array.from(d.getElementsByTagName('v:imagedata'));
670 Roo.each(ar, function(node) {
671 node.parentNode.insertBefore(d.ownerDocument.createElement('img'), node );
672 node.parentNode.removeChild(node);
676 Roo.each(d.getElementsByTagName('img'), function(img, i) {
677 img.setAttribute('src', images[i]);
680 if (this.autoClean) {
681 new Roo.htmleditor.FilterWord({ node : d });
683 new Roo.htmleditor.FilterStyleToTag({ node : d });
684 new Roo.htmleditor.FilterAttributes({
686 attrib_white : ['href', 'src', 'name', 'align', 'colspan', 'rowspan', 'data-display', 'data-width', 'start'],
687 attrib_clean : ['href', 'src' ]
689 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
691 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', ':' ]} );
692 new Roo.htmleditor.FilterParagraph({ node : d });
693 new Roo.htmleditor.FilterSpan({ node : d });
694 new Roo.htmleditor.FilterLongBr({ node : d });
695 new Roo.htmleditor.FilterComment({ node : d });
699 if (this.enableBlocks) {
701 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
702 if (img.closest('figure')) { // assume!! that it's aready
705 var fig = new Roo.htmleditor.BlockFigure({
708 fig.updateElement(img); // replace it..
714 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
715 if (this.enableBlocks) {
716 Roo.htmleditor.Block.initAll(this.doc.body);
721 this.owner.fireEvent('paste', this);
723 // default behaveiour should be our local cleanup paste? (optional?)
724 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
725 //this.owner.fireEvent('paste', e, v);
728 onDestroy : function(){
734 //for (var i =0; i < this.toolbars.length;i++) {
735 // // fixme - ask toolbars for heights?
736 // this.toolbars[i].onDestroy();
739 //this.wrap.dom.innerHTML = '';
740 //this.wrap.remove();
745 onFirstFocus : function(){
748 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
750 this.activated = true;
753 if(Roo.isGecko){ // prevent silly gecko errors
755 var s = this.win.getSelection();
756 if(!s.focusNode || s.focusNode.nodeType != 3){
757 var r = s.getRangeAt(0);
758 r.selectNodeContents((this.doc.body || this.doc.documentElement));
763 this.execCmd('useCSS', true);
764 this.execCmd('styleWithCSS', false);
767 this.owner.fireEvent('activate', this);
771 adjustFont: function(btn){
772 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
773 //if(Roo.isSafari){ // safari
776 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
777 if(Roo.isSafari){ // safari
778 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
779 v = (v < 10) ? 10 : v;
780 v = (v > 48) ? 48 : v;
781 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
786 v = Math.max(1, v+adjust);
788 this.execCmd('FontSize', v );
791 onEditorEvent : function(e)
795 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
796 return; // we do not handle this.. (undo manager does..)
798 // in theory this detects if the last element is not a br, then we try and do that.
799 // its so clicking in space at bottom triggers adding a br and moving the cursor.
801 e.target.nodeName == 'BODY' &&
802 e.type == "mouseup" &&
803 this.doc.body.lastChild
805 var lc = this.doc.body.lastChild;
806 // gtx-trans is google translate plugin adding crap.
807 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
808 lc = lc.previousSibling;
810 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
811 // if last element is <BR> - then dont do anything.
813 var ns = this.doc.createElement('br');
814 this.doc.body.appendChild(ns);
815 range = this.doc.createRange();
816 range.setStartAfter(ns);
817 range.collapse(true);
818 var sel = this.win.getSelection();
819 sel.removeAllRanges();
826 this.fireEditorEvent(e);
827 // this.updateToolbar();
828 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
831 fireEditorEvent: function(e)
833 this.owner.fireEvent('editorevent', this, e);
836 insertTag : function(tg)
838 // could be a bit smarter... -> wrap the current selected tRoo..
839 if (tg.toLowerCase() == 'span' ||
840 tg.toLowerCase() == 'code' ||
841 tg.toLowerCase() == 'sup' ||
842 tg.toLowerCase() == 'sub'
845 range = this.createRange(this.getSelection());
846 var wrappingNode = this.doc.createElement(tg.toLowerCase());
847 wrappingNode.appendChild(range.extractContents());
848 range.insertNode(wrappingNode);
855 this.execCmd("formatblock", tg);
856 this.undoManager.addEvent();
859 insertText : function(txt)
863 var range = this.createRange();
864 range.deleteContents();
865 //alert(Sender.getAttribute('label'));
867 range.insertNode(this.doc.createTextNode(txt));
868 this.undoManager.addEvent();
874 * Executes a Midas editor command on the editor document and performs necessary focus and
875 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
876 * @param {String} cmd The Midas command
877 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
879 relayCmd : function(cmd, value)
885 case 'justifycenter':
886 // if we are in a cell, then we will adjust the
887 var n = this.getParentElement();
888 var td = n.closest('td');
890 var bl = Roo.htmleditor.Block.factory(td);
891 bl.textAlign = cmd.replace('justify','');
893 this.owner.fireEvent('editorevent', this);
896 this.execCmd('styleWithCSS', true); //
900 // if there is no selection, then we insert, and set the curson inside it..
901 this.execCmd('styleWithCSS', false);
911 this.execCmd(cmd, value);
912 this.owner.fireEvent('editorevent', this);
913 //this.updateToolbar();
914 this.owner.deferFocus();
918 * Executes a Midas editor command directly on the editor document.
919 * For visual commands, you should use {@link #relayCmd} instead.
920 * <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 execCmd : function(cmd, value){
925 this.doc.execCommand(cmd, false, value === undefined ? null : value);
932 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
934 * @param {String} text | dom node..
936 insertAtCursor : function(text)
943 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
947 // from jquery ui (MIT licenced)
951 if (win.getSelection && win.getSelection().getRangeAt) {
953 // delete the existing?
955 this.createRange(this.getSelection()).deleteContents();
956 range = win.getSelection().getRangeAt(0);
957 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
958 range.insertNode(node);
959 range = range.cloneRange();
960 range.collapse(false);
962 win.getSelection().removeAllRanges();
963 win.getSelection().addRange(range);
967 } else if (win.document.selection && win.document.selection.createRange) {
968 // no firefox support
969 var txt = typeof(text) == 'string' ? text : text.outerHTML;
970 win.document.selection.createRange().pasteHTML(txt);
973 // no firefox support
974 var txt = typeof(text) == 'string' ? text : text.outerHTML;
975 this.execCmd('InsertHTML', txt);
983 mozKeyPress : function(e){
985 var c = e.getCharCode(), cmd;
988 c = String.fromCharCode(c).toLowerCase();
1002 // this.cleanUpPaste.defer(100, this);
1010 //this.execCmd(cmd);
1011 //this.deferFocus();
1020 fixKeys : function(){ // load time branching for fastest keydown performance
1025 var k = e.getKey(), r;
1028 r = this.doc.selection.createRange();
1031 r.pasteHTML('    ');
1036 /// this is handled by Roo.htmleditor.KeyEnter
1039 r = this.doc.selection.createRange();
1041 var target = r.parentElement();
1042 if(!target || target.tagName.toLowerCase() != 'li'){
1044 r.pasteHTML('<br/>');
1051 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1052 // this.cleanUpPaste.defer(100, this);
1058 }else if(Roo.isOpera){
1064 this.execCmd('InsertHTML','    ');
1068 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1069 // this.cleanUpPaste.defer(100, this);
1074 }else if(Roo.isSafari){
1080 this.execCmd('InsertText','\t');
1084 this.mozKeyPress(e);
1086 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1087 // this.cleanUpPaste.defer(100, this);
1095 getAllAncestors: function()
1097 var p = this.getSelectedNode();
1100 a.push(p); // push blank onto stack..
1101 p = this.getParentElement();
1105 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1109 a.push(this.doc.body);
1113 lastSelNode : false,
1116 getSelection : function()
1118 this.assignDocWin();
1119 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1123 * @param {DomElement} node the node to select
1125 selectNode : function(node, collapse)
1127 var nodeRange = node.ownerDocument.createRange();
1129 nodeRange.selectNode(node);
1131 nodeRange.selectNodeContents(node);
1133 if (collapse === true) {
1134 nodeRange.collapse(true);
1137 var s = this.win.getSelection();
1138 s.removeAllRanges();
1139 s.addRange(nodeRange);
1142 getSelectedNode: function()
1144 // this may only work on Gecko!!!
1146 // should we cache this!!!!
1150 var range = this.createRange(this.getSelection()).cloneRange();
1153 var parent = range.parentElement();
1155 var testRange = range.duplicate();
1156 testRange.moveToElementText(parent);
1157 if (testRange.inRange(range)) {
1160 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1163 parent = parent.parentElement;
1168 // is ancestor a text element.
1169 var ac = range.commonAncestorContainer;
1170 if (ac.nodeType == 3) {
1174 var ar = ac.childNodes;
1177 var other_nodes = [];
1178 var has_other_nodes = false;
1179 for (var i=0;i<ar.length;i++) {
1180 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1183 // fullly contained node.
1185 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1190 // probably selected..
1191 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1192 other_nodes.push(ar[i]);
1196 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1201 has_other_nodes = true;
1203 if (!nodes.length && other_nodes.length) {
1206 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1214 createRange: function(sel)
1216 // this has strange effects when using with
1217 // top toolbar - not sure if it's a great idea.
1218 //this.editor.contentWindow.focus();
1219 if (typeof sel != "undefined") {
1221 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1223 return this.doc.createRange();
1226 return this.doc.createRange();
1229 getParentElement: function()
1232 this.assignDocWin();
1233 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1235 var range = this.createRange(sel);
1238 var p = range.commonAncestorContainer;
1239 while (p.nodeType == 3) { // text node
1250 * Range intersection.. the hard stuff...
1254 * [ -- selected range --- ]
1258 * if end is before start or hits it. fail.
1259 * if start is after end or hits it fail.
1261 * if either hits (but other is outside. - then it's not
1267 // @see http://www.thismuchiknow.co.uk/?p=64.
1268 rangeIntersectsNode : function(range, node)
1270 var nodeRange = node.ownerDocument.createRange();
1272 nodeRange.selectNode(node);
1274 nodeRange.selectNodeContents(node);
1277 var rangeStartRange = range.cloneRange();
1278 rangeStartRange.collapse(true);
1280 var rangeEndRange = range.cloneRange();
1281 rangeEndRange.collapse(false);
1283 var nodeStartRange = nodeRange.cloneRange();
1284 nodeStartRange.collapse(true);
1286 var nodeEndRange = nodeRange.cloneRange();
1287 nodeEndRange.collapse(false);
1289 return rangeStartRange.compareBoundaryPoints(
1290 Range.START_TO_START, nodeEndRange) == -1 &&
1291 rangeEndRange.compareBoundaryPoints(
1292 Range.START_TO_START, nodeStartRange) == 1;
1296 rangeCompareNode : function(range, node)
1298 var nodeRange = node.ownerDocument.createRange();
1300 nodeRange.selectNode(node);
1302 nodeRange.selectNodeContents(node);
1306 range.collapse(true);
1308 nodeRange.collapse(true);
1310 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1311 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1313 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1315 var nodeIsBefore = ss == 1;
1316 var nodeIsAfter = ee == -1;
1318 if (nodeIsBefore && nodeIsAfter) {
1321 if (!nodeIsBefore && nodeIsAfter) {
1322 return 1; //right trailed.
1325 if (nodeIsBefore && !nodeIsAfter) {
1326 return 2; // left trailed.
1332 cleanWordChars : function(input) {// change the chars to hex code
1335 [ 8211, "–" ],
1336 [ 8212, "—" ],
1345 Roo.each(swapCodes, function(sw) {
1346 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1348 output = output.replace(swapper, sw[1]);
1358 cleanUpChild : function (node)
1361 new Roo.htmleditor.FilterComment({node : node});
1362 new Roo.htmleditor.FilterAttributes({
1364 attrib_black : this.ablack,
1365 attrib_clean : this.aclean,
1366 style_white : this.cwhite,
1367 style_black : this.cblack
1369 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1370 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1376 * Clean up MS wordisms...
1377 * @deprecated - use filter directly
1379 cleanWord : function(node)
1381 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1382 new Roo.htmleditor.FilterKeepChildren({node : node ? node : this.doc.body, tag : [ 'FONT', ':' ]} );
1389 * @deprecated - use filters
1391 cleanTableWidths : function(node)
1393 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1400 applyBlacklists : function()
1402 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1403 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1405 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1406 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1407 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1411 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1412 if (b.indexOf(tag) > -1) {
1415 this.white.push(tag);
1419 Roo.each(w, function(tag) {
1420 if (b.indexOf(tag) > -1) {
1423 if (this.white.indexOf(tag) > -1) {
1426 this.white.push(tag);
1431 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1432 if (w.indexOf(tag) > -1) {
1435 this.black.push(tag);
1439 Roo.each(b, function(tag) {
1440 if (w.indexOf(tag) > -1) {
1443 if (this.black.indexOf(tag) > -1) {
1446 this.black.push(tag);
1451 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1452 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1456 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1457 if (b.indexOf(tag) > -1) {
1460 this.cwhite.push(tag);
1464 Roo.each(w, function(tag) {
1465 if (b.indexOf(tag) > -1) {
1468 if (this.cwhite.indexOf(tag) > -1) {
1471 this.cwhite.push(tag);
1476 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1477 if (w.indexOf(tag) > -1) {
1480 this.cblack.push(tag);
1484 Roo.each(b, function(tag) {
1485 if (w.indexOf(tag) > -1) {
1488 if (this.cblack.indexOf(tag) > -1) {
1491 this.cblack.push(tag);
1496 setStylesheets : function(stylesheets)
1498 if(typeof(stylesheets) == 'string'){
1499 Roo.get(this.iframe.contentDocument.head).createChild({
1510 Roo.each(stylesheets, function(s) {
1515 Roo.get(_this.iframe.contentDocument.head).createChild({
1527 updateLanguage : function()
1529 if (!this.iframe || !this.iframe.contentDocument) {
1532 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1536 removeStylesheets : function()
1540 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1545 setStyle : function(style)
1547 Roo.get(this.iframe.contentDocument.head).createChild({
1556 // hide stuff that is not compatible
1574 * @cfg {String} fieldClass @hide
1577 * @cfg {String} focusClass @hide
1580 * @cfg {String} autoCreate @hide
1583 * @cfg {String} inputType @hide
1586 * @cfg {String} invalidClass @hide
1589 * @cfg {String} invalidText @hide
1592 * @cfg {String} msgFx @hide
1595 * @cfg {String} validateOnBlur @hide
1599 Roo.HtmlEditorCore.white = [
1600 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1602 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1603 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1604 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1605 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1606 'TABLE', 'UL', 'XMP',
1608 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1611 'DIR', 'MENU', 'OL', 'UL', 'DL',
1617 Roo.HtmlEditorCore.black = [
1618 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1620 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1621 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1622 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1623 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1624 //'FONT' // CLEAN LATER..
1625 'COLGROUP', 'COL' // messy tables.
1629 Roo.HtmlEditorCore.clean = [ // ?? needed???
1630 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1632 Roo.HtmlEditorCore.tag_remove = [
1637 Roo.HtmlEditorCore.ablack = [
1641 Roo.HtmlEditorCore.aclean = [
1642 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1646 Roo.HtmlEditorCore.pwhite= [
1647 'http', 'https', 'mailto'
1650 // white listed style attributes.
1651 Roo.HtmlEditorCore.cwhite= [
1652 // 'text-align', /// default is to allow most things..
1658 // black listed style attributes.
1659 Roo.HtmlEditorCore.cblack= [
1660 // 'font-size' -- this can be set by the project