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 this.insertAtCursor('<img src=" + url + ">');
614 if (cd.types.indexOf('text/html') < 0 ) {
618 var html = cd.getData('text/html'); // clipboard event
619 if (cd.types.indexOf('text/rtf') > -1) {
620 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
621 images = parser.doc ? parser.doc.getElementsByType('pict') : [];
626 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable|footerf)/); }) // ignore headers/footers etc.
627 .map(function(g) { return g.toDataURL(); })
628 .filter(function(g) { return g != 'about:blank'; });
631 html = this.cleanWordChars(html);
633 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
636 var sn = this.getParentElement();
637 // check if d contains a table, and prevent nesting??
638 //Roo.log(d.getElementsByTagName('table'));
640 //Roo.log(sn.closest('table'));
641 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
643 this.insertAtCursor("You can not nest tables");
644 //Roo.log("prevent?"); // fixme -
650 if (images.length > 0) {
651 // replace all v:imagedata - with img.
652 var ar = Array.from(d.getElementsByTagName('v:imagedata'));
653 Roo.each(ar, function(node) {
654 node.parentNode.insertBefore(d.ownerDocument.createElement('img'), node );
655 node.parentNode.removeChild(node);
659 Roo.each(d.getElementsByTagName('img'), function(img, i) {
660 img.setAttribute('src', images[i]);
663 if (this.autoClean) {
664 new Roo.htmleditor.FilterWord({ node : d });
666 new Roo.htmleditor.FilterStyleToTag({ node : d });
667 new Roo.htmleditor.FilterAttributes({
669 attrib_white : ['href', 'src', 'name', 'align', 'colspan', 'rowspan', 'data-display', 'data-width', 'start'],
670 attrib_clean : ['href', 'src' ]
672 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
674 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', ':' ]} );
675 new Roo.htmleditor.FilterParagraph({ node : d });
676 new Roo.htmleditor.FilterSpan({ node : d });
677 new Roo.htmleditor.FilterLongBr({ node : d });
678 new Roo.htmleditor.FilterComment({ node : d });
682 if (this.enableBlocks) {
684 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
685 if (img.closest('figure')) { // assume!! that it's aready
688 var fig = new Roo.htmleditor.BlockFigure({
691 fig.updateElement(img); // replace it..
697 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
698 if (this.enableBlocks) {
699 Roo.htmleditor.Block.initAll(this.doc.body);
704 this.owner.fireEvent('paste', this);
706 // default behaveiour should be our local cleanup paste? (optional?)
707 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
708 //this.owner.fireEvent('paste', e, v);
711 onDestroy : function(){
717 //for (var i =0; i < this.toolbars.length;i++) {
718 // // fixme - ask toolbars for heights?
719 // this.toolbars[i].onDestroy();
722 //this.wrap.dom.innerHTML = '';
723 //this.wrap.remove();
728 onFirstFocus : function(){
731 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
733 this.activated = true;
736 if(Roo.isGecko){ // prevent silly gecko errors
738 var s = this.win.getSelection();
739 if(!s.focusNode || s.focusNode.nodeType != 3){
740 var r = s.getRangeAt(0);
741 r.selectNodeContents((this.doc.body || this.doc.documentElement));
746 this.execCmd('useCSS', true);
747 this.execCmd('styleWithCSS', false);
750 this.owner.fireEvent('activate', this);
754 adjustFont: function(btn){
755 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
756 //if(Roo.isSafari){ // safari
759 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
760 if(Roo.isSafari){ // safari
761 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
762 v = (v < 10) ? 10 : v;
763 v = (v > 48) ? 48 : v;
764 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
769 v = Math.max(1, v+adjust);
771 this.execCmd('FontSize', v );
774 onEditorEvent : function(e)
778 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
779 return; // we do not handle this.. (undo manager does..)
781 // in theory this detects if the last element is not a br, then we try and do that.
782 // its so clicking in space at bottom triggers adding a br and moving the cursor.
784 e.target.nodeName == 'BODY' &&
785 e.type == "mouseup" &&
786 this.doc.body.lastChild
788 var lc = this.doc.body.lastChild;
789 // gtx-trans is google translate plugin adding crap.
790 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
791 lc = lc.previousSibling;
793 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
794 // if last element is <BR> - then dont do anything.
796 var ns = this.doc.createElement('br');
797 this.doc.body.appendChild(ns);
798 range = this.doc.createRange();
799 range.setStartAfter(ns);
800 range.collapse(true);
801 var sel = this.win.getSelection();
802 sel.removeAllRanges();
809 this.fireEditorEvent(e);
810 // this.updateToolbar();
811 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
814 fireEditorEvent: function(e)
816 this.owner.fireEvent('editorevent', this, e);
819 insertTag : function(tg)
821 // could be a bit smarter... -> wrap the current selected tRoo..
822 if (tg.toLowerCase() == 'span' ||
823 tg.toLowerCase() == 'code' ||
824 tg.toLowerCase() == 'sup' ||
825 tg.toLowerCase() == 'sub'
828 range = this.createRange(this.getSelection());
829 var wrappingNode = this.doc.createElement(tg.toLowerCase());
830 wrappingNode.appendChild(range.extractContents());
831 range.insertNode(wrappingNode);
838 this.execCmd("formatblock", tg);
839 this.undoManager.addEvent();
842 insertText : function(txt)
846 var range = this.createRange();
847 range.deleteContents();
848 //alert(Sender.getAttribute('label'));
850 range.insertNode(this.doc.createTextNode(txt));
851 this.undoManager.addEvent();
857 * Executes a Midas editor command on the editor document and performs necessary focus and
858 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
859 * @param {String} cmd The Midas command
860 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
862 relayCmd : function(cmd, value)
868 case 'justifycenter':
869 // if we are in a cell, then we will adjust the
870 var n = this.getParentElement();
871 var td = n.closest('td');
873 var bl = Roo.htmleditor.Block.factory(td);
874 bl.textAlign = cmd.replace('justify','');
876 this.owner.fireEvent('editorevent', this);
879 this.execCmd('styleWithCSS', true); //
883 // if there is no selection, then we insert, and set the curson inside it..
884 this.execCmd('styleWithCSS', false);
894 this.execCmd(cmd, value);
895 this.owner.fireEvent('editorevent', this);
896 //this.updateToolbar();
897 this.owner.deferFocus();
901 * Executes a Midas editor command directly on the editor document.
902 * For visual commands, you should use {@link #relayCmd} instead.
903 * <b>This should only be called after the editor is initialized.</b>
904 * @param {String} cmd The Midas command
905 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
907 execCmd : function(cmd, value){
908 this.doc.execCommand(cmd, false, value === undefined ? null : value);
915 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
917 * @param {String} text | dom node..
919 insertAtCursor : function(text)
926 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
930 // from jquery ui (MIT licenced)
934 if (win.getSelection && win.getSelection().getRangeAt) {
936 // delete the existing?
938 this.createRange(this.getSelection()).deleteContents();
939 range = win.getSelection().getRangeAt(0);
940 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
941 range.insertNode(node);
942 range = range.cloneRange();
943 range.collapse(false);
945 win.getSelection().removeAllRanges();
946 win.getSelection().addRange(range);
950 } else if (win.document.selection && win.document.selection.createRange) {
951 // no firefox support
952 var txt = typeof(text) == 'string' ? text : text.outerHTML;
953 win.document.selection.createRange().pasteHTML(txt);
956 // no firefox support
957 var txt = typeof(text) == 'string' ? text : text.outerHTML;
958 this.execCmd('InsertHTML', txt);
966 mozKeyPress : function(e){
968 var c = e.getCharCode(), cmd;
971 c = String.fromCharCode(c).toLowerCase();
985 // this.cleanUpPaste.defer(100, this);
1003 fixKeys : function(){ // load time branching for fastest keydown performance
1008 var k = e.getKey(), r;
1011 r = this.doc.selection.createRange();
1014 r.pasteHTML('    ');
1019 /// this is handled by Roo.htmleditor.KeyEnter
1022 r = this.doc.selection.createRange();
1024 var target = r.parentElement();
1025 if(!target || target.tagName.toLowerCase() != 'li'){
1027 r.pasteHTML('<br/>');
1034 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1035 // this.cleanUpPaste.defer(100, this);
1041 }else if(Roo.isOpera){
1047 this.execCmd('InsertHTML','    ');
1051 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1052 // this.cleanUpPaste.defer(100, this);
1057 }else if(Roo.isSafari){
1063 this.execCmd('InsertText','\t');
1067 this.mozKeyPress(e);
1069 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1070 // this.cleanUpPaste.defer(100, this);
1078 getAllAncestors: function()
1080 var p = this.getSelectedNode();
1083 a.push(p); // push blank onto stack..
1084 p = this.getParentElement();
1088 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1092 a.push(this.doc.body);
1096 lastSelNode : false,
1099 getSelection : function()
1101 this.assignDocWin();
1102 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1106 * @param {DomElement} node the node to select
1108 selectNode : function(node, collapse)
1110 var nodeRange = node.ownerDocument.createRange();
1112 nodeRange.selectNode(node);
1114 nodeRange.selectNodeContents(node);
1116 if (collapse === true) {
1117 nodeRange.collapse(true);
1120 var s = this.win.getSelection();
1121 s.removeAllRanges();
1122 s.addRange(nodeRange);
1125 getSelectedNode: function()
1127 // this may only work on Gecko!!!
1129 // should we cache this!!!!
1133 var range = this.createRange(this.getSelection()).cloneRange();
1136 var parent = range.parentElement();
1138 var testRange = range.duplicate();
1139 testRange.moveToElementText(parent);
1140 if (testRange.inRange(range)) {
1143 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1146 parent = parent.parentElement;
1151 // is ancestor a text element.
1152 var ac = range.commonAncestorContainer;
1153 if (ac.nodeType == 3) {
1157 var ar = ac.childNodes;
1160 var other_nodes = [];
1161 var has_other_nodes = false;
1162 for (var i=0;i<ar.length;i++) {
1163 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1166 // fullly contained node.
1168 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1173 // probably selected..
1174 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1175 other_nodes.push(ar[i]);
1179 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1184 has_other_nodes = true;
1186 if (!nodes.length && other_nodes.length) {
1189 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1197 createRange: function(sel)
1199 // this has strange effects when using with
1200 // top toolbar - not sure if it's a great idea.
1201 //this.editor.contentWindow.focus();
1202 if (typeof sel != "undefined") {
1204 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1206 return this.doc.createRange();
1209 return this.doc.createRange();
1212 getParentElement: function()
1215 this.assignDocWin();
1216 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1218 var range = this.createRange(sel);
1221 var p = range.commonAncestorContainer;
1222 while (p.nodeType == 3) { // text node
1233 * Range intersection.. the hard stuff...
1237 * [ -- selected range --- ]
1241 * if end is before start or hits it. fail.
1242 * if start is after end or hits it fail.
1244 * if either hits (but other is outside. - then it's not
1250 // @see http://www.thismuchiknow.co.uk/?p=64.
1251 rangeIntersectsNode : function(range, node)
1253 var nodeRange = node.ownerDocument.createRange();
1255 nodeRange.selectNode(node);
1257 nodeRange.selectNodeContents(node);
1260 var rangeStartRange = range.cloneRange();
1261 rangeStartRange.collapse(true);
1263 var rangeEndRange = range.cloneRange();
1264 rangeEndRange.collapse(false);
1266 var nodeStartRange = nodeRange.cloneRange();
1267 nodeStartRange.collapse(true);
1269 var nodeEndRange = nodeRange.cloneRange();
1270 nodeEndRange.collapse(false);
1272 return rangeStartRange.compareBoundaryPoints(
1273 Range.START_TO_START, nodeEndRange) == -1 &&
1274 rangeEndRange.compareBoundaryPoints(
1275 Range.START_TO_START, nodeStartRange) == 1;
1279 rangeCompareNode : function(range, node)
1281 var nodeRange = node.ownerDocument.createRange();
1283 nodeRange.selectNode(node);
1285 nodeRange.selectNodeContents(node);
1289 range.collapse(true);
1291 nodeRange.collapse(true);
1293 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1294 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1296 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1298 var nodeIsBefore = ss == 1;
1299 var nodeIsAfter = ee == -1;
1301 if (nodeIsBefore && nodeIsAfter) {
1304 if (!nodeIsBefore && nodeIsAfter) {
1305 return 1; //right trailed.
1308 if (nodeIsBefore && !nodeIsAfter) {
1309 return 2; // left trailed.
1315 cleanWordChars : function(input) {// change the chars to hex code
1318 [ 8211, "–" ],
1319 [ 8212, "—" ],
1328 Roo.each(swapCodes, function(sw) {
1329 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1331 output = output.replace(swapper, sw[1]);
1341 cleanUpChild : function (node)
1344 new Roo.htmleditor.FilterComment({node : node});
1345 new Roo.htmleditor.FilterAttributes({
1347 attrib_black : this.ablack,
1348 attrib_clean : this.aclean,
1349 style_white : this.cwhite,
1350 style_black : this.cblack
1352 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1353 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1359 * Clean up MS wordisms...
1360 * @deprecated - use filter directly
1362 cleanWord : function(node)
1364 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1365 new Roo.htmleditor.FilterKeepChildren({node : node ? node : this.doc.body, tag : [ 'FONT', ':' ]} );
1372 * @deprecated - use filters
1374 cleanTableWidths : function(node)
1376 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1383 applyBlacklists : function()
1385 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1386 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1388 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1389 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1390 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1394 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1395 if (b.indexOf(tag) > -1) {
1398 this.white.push(tag);
1402 Roo.each(w, function(tag) {
1403 if (b.indexOf(tag) > -1) {
1406 if (this.white.indexOf(tag) > -1) {
1409 this.white.push(tag);
1414 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1415 if (w.indexOf(tag) > -1) {
1418 this.black.push(tag);
1422 Roo.each(b, function(tag) {
1423 if (w.indexOf(tag) > -1) {
1426 if (this.black.indexOf(tag) > -1) {
1429 this.black.push(tag);
1434 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1435 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1439 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1440 if (b.indexOf(tag) > -1) {
1443 this.cwhite.push(tag);
1447 Roo.each(w, function(tag) {
1448 if (b.indexOf(tag) > -1) {
1451 if (this.cwhite.indexOf(tag) > -1) {
1454 this.cwhite.push(tag);
1459 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1460 if (w.indexOf(tag) > -1) {
1463 this.cblack.push(tag);
1467 Roo.each(b, function(tag) {
1468 if (w.indexOf(tag) > -1) {
1471 if (this.cblack.indexOf(tag) > -1) {
1474 this.cblack.push(tag);
1479 setStylesheets : function(stylesheets)
1481 if(typeof(stylesheets) == 'string'){
1482 Roo.get(this.iframe.contentDocument.head).createChild({
1493 Roo.each(stylesheets, function(s) {
1498 Roo.get(_this.iframe.contentDocument.head).createChild({
1510 updateLanguage : function()
1512 if (!this.iframe || !this.iframe.contentDocument) {
1515 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1519 removeStylesheets : function()
1523 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1528 setStyle : function(style)
1530 Roo.get(this.iframe.contentDocument.head).createChild({
1539 // hide stuff that is not compatible
1557 * @cfg {String} fieldClass @hide
1560 * @cfg {String} focusClass @hide
1563 * @cfg {String} autoCreate @hide
1566 * @cfg {String} inputType @hide
1569 * @cfg {String} invalidClass @hide
1572 * @cfg {String} invalidText @hide
1575 * @cfg {String} msgFx @hide
1578 * @cfg {String} validateOnBlur @hide
1582 Roo.HtmlEditorCore.white = [
1583 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1585 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1586 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1587 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1588 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1589 'TABLE', 'UL', 'XMP',
1591 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1594 'DIR', 'MENU', 'OL', 'UL', 'DL',
1600 Roo.HtmlEditorCore.black = [
1601 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1603 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1604 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1605 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1606 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1607 //'FONT' // CLEAN LATER..
1608 'COLGROUP', 'COL' // messy tables.
1612 Roo.HtmlEditorCore.clean = [ // ?? needed???
1613 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1615 Roo.HtmlEditorCore.tag_remove = [
1620 Roo.HtmlEditorCore.ablack = [
1624 Roo.HtmlEditorCore.aclean = [
1625 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1629 Roo.HtmlEditorCore.pwhite= [
1630 'http', 'https', 'mailto'
1633 // white listed style attributes.
1634 Roo.HtmlEditorCore.cwhite= [
1635 // 'text-align', /// default is to allow most things..
1641 // black listed style attributes.
1642 Roo.HtmlEditorCore.cblack= [
1643 // 'font-size' -- this can be set by the project