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,' '));
629 if (cd.types.indexOf('text/html') < 0 ) {
633 var html = cd.getData('text/html'); // clipboard event
634 if (cd.types.indexOf('text/rtf') > -1) {
635 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
636 images = parser.doc ? parser.doc.getElementsByType('pict') : [];
641 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable|footerf)/); }) // ignore headers/footers etc.
642 .map(function(g) { return g.toDataURL(); })
643 .filter(function(g) { return g != 'about:blank'; });
646 html = this.cleanWordChars(html);
648 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
651 var sn = this.getParentElement();
652 // check if d contains a table, and prevent nesting??
653 //Roo.log(d.getElementsByTagName('table'));
655 //Roo.log(sn.closest('table'));
656 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
658 this.insertAtCursor("You can not nest tables");
659 //Roo.log("prevent?"); // fixme -
665 if (images.length > 0) {
666 // replace all v:imagedata - with img.
667 var ar = Array.from(d.getElementsByTagName('v:imagedata'));
668 Roo.each(ar, function(node) {
669 node.parentNode.insertBefore(d.ownerDocument.createElement('img'), node );
670 node.parentNode.removeChild(node);
674 Roo.each(d.getElementsByTagName('img'), function(img, i) {
675 img.setAttribute('src', images[i]);
678 if (this.autoClean) {
679 new Roo.htmleditor.FilterWord({ node : d });
681 new Roo.htmleditor.FilterStyleToTag({ node : d });
682 new Roo.htmleditor.FilterAttributes({
684 attrib_white : ['href', 'src', 'name', 'align', 'colspan', 'rowspan', 'data-display', 'data-width', 'start'],
685 attrib_clean : ['href', 'src' ]
687 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
689 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', ':' ]} );
690 new Roo.htmleditor.FilterParagraph({ node : d });
691 new Roo.htmleditor.FilterSpan({ node : d });
692 new Roo.htmleditor.FilterLongBr({ node : d });
693 new Roo.htmleditor.FilterComment({ node : d });
697 if (this.enableBlocks) {
699 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
700 if (img.closest('figure')) { // assume!! that it's aready
703 var fig = new Roo.htmleditor.BlockFigure({
706 fig.updateElement(img); // replace it..
712 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
713 if (this.enableBlocks) {
714 Roo.htmleditor.Block.initAll(this.doc.body);
719 this.owner.fireEvent('paste', this);
721 // default behaveiour should be our local cleanup paste? (optional?)
722 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
723 //this.owner.fireEvent('paste', e, v);
726 onDestroy : function(){
732 //for (var i =0; i < this.toolbars.length;i++) {
733 // // fixme - ask toolbars for heights?
734 // this.toolbars[i].onDestroy();
737 //this.wrap.dom.innerHTML = '';
738 //this.wrap.remove();
743 onFirstFocus : function(){
746 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
748 this.activated = true;
751 if(Roo.isGecko){ // prevent silly gecko errors
753 var s = this.win.getSelection();
754 if(!s.focusNode || s.focusNode.nodeType != 3){
755 var r = s.getRangeAt(0);
756 r.selectNodeContents((this.doc.body || this.doc.documentElement));
761 this.execCmd('useCSS', true);
762 this.execCmd('styleWithCSS', false);
765 this.owner.fireEvent('activate', this);
769 adjustFont: function(btn){
770 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
771 //if(Roo.isSafari){ // safari
774 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
775 if(Roo.isSafari){ // safari
776 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
777 v = (v < 10) ? 10 : v;
778 v = (v > 48) ? 48 : v;
779 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
784 v = Math.max(1, v+adjust);
786 this.execCmd('FontSize', v );
789 onEditorEvent : function(e)
793 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
794 return; // we do not handle this.. (undo manager does..)
796 // in theory this detects if the last element is not a br, then we try and do that.
797 // its so clicking in space at bottom triggers adding a br and moving the cursor.
799 e.target.nodeName == 'BODY' &&
800 e.type == "mouseup" &&
801 this.doc.body.lastChild
803 var lc = this.doc.body.lastChild;
804 // gtx-trans is google translate plugin adding crap.
805 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
806 lc = lc.previousSibling;
808 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
809 // if last element is <BR> - then dont do anything.
811 var ns = this.doc.createElement('br');
812 this.doc.body.appendChild(ns);
813 range = this.doc.createRange();
814 range.setStartAfter(ns);
815 range.collapse(true);
816 var sel = this.win.getSelection();
817 sel.removeAllRanges();
824 this.fireEditorEvent(e);
825 // this.updateToolbar();
826 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
829 fireEditorEvent: function(e)
831 this.owner.fireEvent('editorevent', this, e);
834 insertTag : function(tg)
836 // could be a bit smarter... -> wrap the current selected tRoo..
837 if (tg.toLowerCase() == 'span' ||
838 tg.toLowerCase() == 'code' ||
839 tg.toLowerCase() == 'sup' ||
840 tg.toLowerCase() == 'sub'
843 range = this.createRange(this.getSelection());
844 var wrappingNode = this.doc.createElement(tg.toLowerCase());
845 wrappingNode.appendChild(range.extractContents());
846 range.insertNode(wrappingNode);
853 this.execCmd("formatblock", tg);
854 this.undoManager.addEvent();
857 insertText : function(txt)
861 var range = this.createRange();
862 range.deleteContents();
863 //alert(Sender.getAttribute('label'));
865 range.insertNode(this.doc.createTextNode(txt));
866 this.undoManager.addEvent();
872 * Executes a Midas editor command on the editor document and performs necessary focus and
873 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
874 * @param {String} cmd The Midas command
875 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
877 relayCmd : function(cmd, value)
883 case 'justifycenter':
884 // if we are in a cell, then we will adjust the
885 var n = this.getParentElement();
886 var td = n.closest('td');
888 var bl = Roo.htmleditor.Block.factory(td);
889 bl.textAlign = cmd.replace('justify','');
891 this.owner.fireEvent('editorevent', this);
894 this.execCmd('styleWithCSS', true); //
898 // if there is no selection, then we insert, and set the curson inside it..
899 this.execCmd('styleWithCSS', false);
909 this.execCmd(cmd, value);
910 this.owner.fireEvent('editorevent', this);
911 //this.updateToolbar();
912 this.owner.deferFocus();
916 * Executes a Midas editor command directly on the editor document.
917 * For visual commands, you should use {@link #relayCmd} instead.
918 * <b>This should only be called after the editor is initialized.</b>
919 * @param {String} cmd The Midas command
920 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
922 execCmd : function(cmd, value){
923 this.doc.execCommand(cmd, false, value === undefined ? null : value);
930 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
932 * @param {String} text | dom node..
934 insertAtCursor : function(text)
941 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
945 // from jquery ui (MIT licenced)
949 if (win.getSelection && win.getSelection().getRangeAt) {
951 // delete the existing?
953 this.createRange(this.getSelection()).deleteContents();
954 range = win.getSelection().getRangeAt(0);
955 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
956 range.insertNode(node);
957 range = range.cloneRange();
958 range.collapse(false);
960 win.getSelection().removeAllRanges();
961 win.getSelection().addRange(range);
965 } else if (win.document.selection && win.document.selection.createRange) {
966 // no firefox support
967 var txt = typeof(text) == 'string' ? text : text.outerHTML;
968 win.document.selection.createRange().pasteHTML(txt);
971 // no firefox support
972 var txt = typeof(text) == 'string' ? text : text.outerHTML;
973 this.execCmd('InsertHTML', txt);
981 mozKeyPress : function(e){
983 var c = e.getCharCode(), cmd;
986 c = String.fromCharCode(c).toLowerCase();
1000 // this.cleanUpPaste.defer(100, this);
1008 //this.execCmd(cmd);
1009 //this.deferFocus();
1018 fixKeys : function(){ // load time branching for fastest keydown performance
1023 var k = e.getKey(), r;
1026 r = this.doc.selection.createRange();
1029 r.pasteHTML('    ');
1034 /// this is handled by Roo.htmleditor.KeyEnter
1037 r = this.doc.selection.createRange();
1039 var target = r.parentElement();
1040 if(!target || target.tagName.toLowerCase() != 'li'){
1042 r.pasteHTML('<br/>');
1049 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1050 // this.cleanUpPaste.defer(100, this);
1056 }else if(Roo.isOpera){
1062 this.execCmd('InsertHTML','    ');
1066 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1067 // this.cleanUpPaste.defer(100, this);
1072 }else if(Roo.isSafari){
1078 this.execCmd('InsertText','\t');
1082 this.mozKeyPress(e);
1084 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1085 // this.cleanUpPaste.defer(100, this);
1093 getAllAncestors: function()
1095 var p = this.getSelectedNode();
1098 a.push(p); // push blank onto stack..
1099 p = this.getParentElement();
1103 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1107 a.push(this.doc.body);
1111 lastSelNode : false,
1114 getSelection : function()
1116 this.assignDocWin();
1117 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1121 * @param {DomElement} node the node to select
1123 selectNode : function(node, collapse)
1125 var nodeRange = node.ownerDocument.createRange();
1127 nodeRange.selectNode(node);
1129 nodeRange.selectNodeContents(node);
1131 if (collapse === true) {
1132 nodeRange.collapse(true);
1135 var s = this.win.getSelection();
1136 s.removeAllRanges();
1137 s.addRange(nodeRange);
1140 getSelectedNode: function()
1142 // this may only work on Gecko!!!
1144 // should we cache this!!!!
1148 var range = this.createRange(this.getSelection()).cloneRange();
1151 var parent = range.parentElement();
1153 var testRange = range.duplicate();
1154 testRange.moveToElementText(parent);
1155 if (testRange.inRange(range)) {
1158 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1161 parent = parent.parentElement;
1166 // is ancestor a text element.
1167 var ac = range.commonAncestorContainer;
1168 if (ac.nodeType == 3) {
1172 var ar = ac.childNodes;
1175 var other_nodes = [];
1176 var has_other_nodes = false;
1177 for (var i=0;i<ar.length;i++) {
1178 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1181 // fullly contained node.
1183 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1188 // probably selected..
1189 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1190 other_nodes.push(ar[i]);
1194 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1199 has_other_nodes = true;
1201 if (!nodes.length && other_nodes.length) {
1204 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1212 createRange: function(sel)
1214 // this has strange effects when using with
1215 // top toolbar - not sure if it's a great idea.
1216 //this.editor.contentWindow.focus();
1217 if (typeof sel != "undefined") {
1219 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1221 return this.doc.createRange();
1224 return this.doc.createRange();
1227 getParentElement: function()
1230 this.assignDocWin();
1231 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1233 var range = this.createRange(sel);
1236 var p = range.commonAncestorContainer;
1237 while (p.nodeType == 3) { // text node
1248 * Range intersection.. the hard stuff...
1252 * [ -- selected range --- ]
1256 * if end is before start or hits it. fail.
1257 * if start is after end or hits it fail.
1259 * if either hits (but other is outside. - then it's not
1265 // @see http://www.thismuchiknow.co.uk/?p=64.
1266 rangeIntersectsNode : function(range, node)
1268 var nodeRange = node.ownerDocument.createRange();
1270 nodeRange.selectNode(node);
1272 nodeRange.selectNodeContents(node);
1275 var rangeStartRange = range.cloneRange();
1276 rangeStartRange.collapse(true);
1278 var rangeEndRange = range.cloneRange();
1279 rangeEndRange.collapse(false);
1281 var nodeStartRange = nodeRange.cloneRange();
1282 nodeStartRange.collapse(true);
1284 var nodeEndRange = nodeRange.cloneRange();
1285 nodeEndRange.collapse(false);
1287 return rangeStartRange.compareBoundaryPoints(
1288 Range.START_TO_START, nodeEndRange) == -1 &&
1289 rangeEndRange.compareBoundaryPoints(
1290 Range.START_TO_START, nodeStartRange) == 1;
1294 rangeCompareNode : function(range, node)
1296 var nodeRange = node.ownerDocument.createRange();
1298 nodeRange.selectNode(node);
1300 nodeRange.selectNodeContents(node);
1304 range.collapse(true);
1306 nodeRange.collapse(true);
1308 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1309 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1311 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1313 var nodeIsBefore = ss == 1;
1314 var nodeIsAfter = ee == -1;
1316 if (nodeIsBefore && nodeIsAfter) {
1319 if (!nodeIsBefore && nodeIsAfter) {
1320 return 1; //right trailed.
1323 if (nodeIsBefore && !nodeIsAfter) {
1324 return 2; // left trailed.
1330 cleanWordChars : function(input) {// change the chars to hex code
1333 [ 8211, "–" ],
1334 [ 8212, "—" ],
1343 Roo.each(swapCodes, function(sw) {
1344 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1346 output = output.replace(swapper, sw[1]);
1356 cleanUpChild : function (node)
1359 new Roo.htmleditor.FilterComment({node : node});
1360 new Roo.htmleditor.FilterAttributes({
1362 attrib_black : this.ablack,
1363 attrib_clean : this.aclean,
1364 style_white : this.cwhite,
1365 style_black : this.cblack
1367 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1368 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1374 * Clean up MS wordisms...
1375 * @deprecated - use filter directly
1377 cleanWord : function(node)
1379 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1380 new Roo.htmleditor.FilterKeepChildren({node : node ? node : this.doc.body, tag : [ 'FONT', ':' ]} );
1387 * @deprecated - use filters
1389 cleanTableWidths : function(node)
1391 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1398 applyBlacklists : function()
1400 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1401 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1403 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1404 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1405 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1409 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1410 if (b.indexOf(tag) > -1) {
1413 this.white.push(tag);
1417 Roo.each(w, function(tag) {
1418 if (b.indexOf(tag) > -1) {
1421 if (this.white.indexOf(tag) > -1) {
1424 this.white.push(tag);
1429 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1430 if (w.indexOf(tag) > -1) {
1433 this.black.push(tag);
1437 Roo.each(b, function(tag) {
1438 if (w.indexOf(tag) > -1) {
1441 if (this.black.indexOf(tag) > -1) {
1444 this.black.push(tag);
1449 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1450 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1454 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1455 if (b.indexOf(tag) > -1) {
1458 this.cwhite.push(tag);
1462 Roo.each(w, function(tag) {
1463 if (b.indexOf(tag) > -1) {
1466 if (this.cwhite.indexOf(tag) > -1) {
1469 this.cwhite.push(tag);
1474 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1475 if (w.indexOf(tag) > -1) {
1478 this.cblack.push(tag);
1482 Roo.each(b, function(tag) {
1483 if (w.indexOf(tag) > -1) {
1486 if (this.cblack.indexOf(tag) > -1) {
1489 this.cblack.push(tag);
1494 setStylesheets : function(stylesheets)
1496 if(typeof(stylesheets) == 'string'){
1497 Roo.get(this.iframe.contentDocument.head).createChild({
1508 Roo.each(stylesheets, function(s) {
1513 Roo.get(_this.iframe.contentDocument.head).createChild({
1525 updateLanguage : function()
1527 if (!this.iframe || !this.iframe.contentDocument) {
1530 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1534 removeStylesheets : function()
1538 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1543 setStyle : function(style)
1545 Roo.get(this.iframe.contentDocument.head).createChild({
1554 // hide stuff that is not compatible
1572 * @cfg {String} fieldClass @hide
1575 * @cfg {String} focusClass @hide
1578 * @cfg {String} autoCreate @hide
1581 * @cfg {String} inputType @hide
1584 * @cfg {String} invalidClass @hide
1587 * @cfg {String} invalidText @hide
1590 * @cfg {String} msgFx @hide
1593 * @cfg {String} validateOnBlur @hide
1597 Roo.HtmlEditorCore.white = [
1598 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1600 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1601 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1602 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1603 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1604 'TABLE', 'UL', 'XMP',
1606 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1609 'DIR', 'MENU', 'OL', 'UL', 'DL',
1615 Roo.HtmlEditorCore.black = [
1616 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1618 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1619 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1620 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1621 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1622 //'FONT' // CLEAN LATER..
1623 'COLGROUP', 'COL' // messy tables.
1627 Roo.HtmlEditorCore.clean = [ // ?? needed???
1628 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1630 Roo.HtmlEditorCore.tag_remove = [
1635 Roo.HtmlEditorCore.ablack = [
1639 Roo.HtmlEditorCore.aclean = [
1640 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1644 Roo.HtmlEditorCore.pwhite= [
1645 'http', 'https', 'mailto'
1648 // white listed style attributes.
1649 Roo.HtmlEditorCore.cwhite= [
1650 // 'text-align', /// default is to allow most things..
1656 // black listed style attributes.
1657 Roo.HtmlEditorCore.cblack= [
1658 // 'font-size' -- this can be set by the project