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
78 // at this point this.owner is set, so we can start working out the whitelisted / blacklisted elements
80 // defaults : white / black...
81 this.applyBlacklists();
88 Roo.extend(Roo.HtmlEditorCore, Roo.Component, {
92 * @cfg {Roo.form.HtmlEditor|Roo.bootstrap.HtmlEditor} the owner field
98 * @cfg {String} resizable 's' or 'se' or 'e' - wrapps the element in a
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.
127 * @cfg {boolean} allowComments - default false - allow comments in HTML source
128 * - by default they are stripped - if you are editing email you may need this.
130 allowComments: false,
134 // private properties
135 validationEvent : false,
139 sourceEditMode : false,
140 onFocus : Roo.emptyFn,
146 // blacklist + whitelisted elements..
155 * Protected method that will not generally be called directly. It
156 * is called when the editor initializes the iframe with HTML contents. Override this method if you
157 * want to change the initialization markup of the iframe (e.g. to add stylesheets).
159 getDocMarkup : function(){
163 // inherit styels from page...??
164 if (this.stylesheets === false) {
166 Roo.get(document.head).select('style').each(function(node) {
167 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
170 Roo.get(document.head).select('link').each(function(node) {
171 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
174 } else if (!this.stylesheets.length) {
176 st = '<style type="text/css">' +
177 'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
180 for (var i in this.stylesheets) {
181 if (typeof(this.stylesheets[i]) != 'string') {
184 st += '<link rel="stylesheet" href="' + this.stylesheets[i] +'" type="text/css">';
189 st += '<style type="text/css">' +
190 'IMG { cursor: pointer } ' +
193 var cls = 'roo-htmleditor-body';
195 if(this.bodyCls.length){
196 cls += ' ' + this.bodyCls;
199 return '<html><head>' + st +
200 //<style type="text/css">' +
201 //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
203 ' </head><body contenteditable="true" data-enable-grammerly="true" class="' + cls + '"></body></html>';
207 onRender : function(ct, position)
210 //Roo.HtmlEditorCore.superclass.onRender.call(this, ct, position);
211 this.el = this.owner.inputEl ? this.owner.inputEl() : this.owner.el;
214 this.el.dom.style.border = '0 none';
215 this.el.dom.setAttribute('tabIndex', -1);
216 this.el.addClass('x-hidden hide');
220 if(Roo.isIE){ // fix IE 1px bogus margin
221 this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
225 this.frameId = Roo.id();
229 var iframe = this.owner.wrap.createChild({
231 cls: 'form-control', // bootstrap..
235 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
240 this.iframe = iframe.dom;
244 this.doc.designMode = 'on';
247 this.doc.write(this.getDocMarkup());
251 var task = { // must defer to wait for browser to be ready
253 //console.log("run task?" + this.doc.readyState);
255 if(this.doc.body || this.doc.readyState == 'complete'){
257 this.doc.designMode="on";
262 Roo.TaskMgr.stop(task);
263 this.initEditor.defer(10, this);
270 Roo.TaskMgr.start(task);
275 onResize : function(w, h)
277 Roo.log('resize: ' +w + ',' + h );
278 //Roo.HtmlEditorCore.superclass.onResize.apply(this, arguments);
282 if(typeof w == 'number'){
284 this.iframe.style.width = w + 'px';
286 if(typeof h == 'number'){
288 this.iframe.style.height = h + 'px';
290 (this.doc.body || this.doc.documentElement).style.height = (h - (this.iframePad*2)) + 'px';
297 * Toggles the editor between standard and source edit mode.
298 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
300 toggleSourceEdit : function(sourceEditMode){
302 this.sourceEditMode = sourceEditMode === true;
304 if(this.sourceEditMode){
306 Roo.get(this.iframe).addClass(['x-hidden','hide', 'd-none']); //FIXME - what's the BS styles for these
309 Roo.get(this.iframe).removeClass(['x-hidden','hide', 'd-none']);
310 //this.iframe.className = '';
313 //this.setSize(this.owner.wrap.getSize());
314 //this.fireEvent('editmodechange', this, this.sourceEditMode);
321 * Protected method that will not generally be called directly. If you need/want
322 * custom HTML cleanup, this is the method you should override.
323 * @param {String} html The HTML to be cleaned
324 * return {String} The cleaned HTML
326 cleanHtml : function(html){
329 if(Roo.isSafari){ // strip safari nonsense
330 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
333 if(html == ' '){
340 * HTML Editor -> Textarea
341 * Protected method that will not generally be called directly. Syncs the contents
342 * of the editor iframe with the textarea.
344 syncValue : function()
346 //Roo.log("HtmlEditorCore:syncValue (EDITOR->TEXT)");
347 if(this.initialized){
349 this.undoManager.addEvent();
352 var bd = (this.doc.body || this.doc.documentElement);
356 var div = document.createElement('div');
357 div.innerHTML = bd.innerHTML;
360 if (this.enableBlocks) {
361 new Roo.htmleditor.FilterBlock({ node : div });
366 var html = div.innerHTML;
368 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
369 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
371 html = '<div style="'+m[0]+'">' + html + '</div>';
374 html = this.cleanHtml(html);
375 // fix up the special chars.. normaly like back quotes in word...
376 // however we do not want to do this with chinese..
377 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
379 var cc = match.charCodeAt();
381 // Get the character value, handling surrogate pairs
382 if (match.length == 2) {
383 // It's a surrogate pair, calculate the Unicode code point
384 var high = match.charCodeAt(0) - 0xD800;
385 var low = match.charCodeAt(1) - 0xDC00;
386 cc = (high * 0x400) + low + 0x10000;
388 (cc >= 0x4E00 && cc < 0xA000 ) ||
389 (cc >= 0x3400 && cc < 0x4E00 ) ||
390 (cc >= 0xf900 && cc < 0xfb00 )
395 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
396 return "&#" + cc + ";";
403 if(this.owner.fireEvent('beforesync', this, html) !== false){
404 this.el.dom.value = html;
405 this.owner.fireEvent('sync', this, html);
411 * TEXTAREA -> EDITABLE
412 * Protected method that will not generally be called directly. Pushes the value of the textarea
413 * into the iframe editor.
415 pushValue : function()
417 //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
418 if(this.initialized){
419 var v = this.el.dom.value.trim();
422 if(this.owner.fireEvent('beforepush', this, v) !== false){
423 var d = (this.doc.body || this.doc.documentElement);
426 this.el.dom.value = d.innerHTML;
427 this.owner.fireEvent('push', this, v);
429 if (this.autoClean) {
430 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
431 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
434 Roo.htmleditor.Block.initAll(this.doc.body);
436 var lc = this.doc.body.lastChild;
437 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
438 // add an extra line at the end.
439 this.doc.body.appendChild(this.doc.createElement('br'));
447 deferFocus : function(){
448 this.focus.defer(10, this);
453 if(this.win && !this.sourceEditMode){
460 assignDocWin: function()
462 var iframe = this.iframe;
465 this.doc = iframe.contentWindow.document;
466 this.win = iframe.contentWindow;
468 // if (!Roo.get(this.frameId)) {
471 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
472 // this.win = Roo.get(this.frameId).dom.contentWindow;
474 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
478 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
479 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
484 initEditor : function(){
485 //console.log("INIT EDITOR");
490 this.doc.designMode="on";
492 this.doc.write(this.getDocMarkup());
495 var dbody = (this.doc.body || this.doc.documentElement);
496 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
497 // this copies styles from the containing element into thsi one..
498 // not sure why we need all of this..
499 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
501 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
502 //ss['background-attachment'] = 'fixed'; // w3c
503 dbody.bgProperties = 'fixed'; // ie
504 //Roo.DomHelper.applyStyles(dbody, ss);
505 Roo.EventManager.on(this.doc, {
506 //'mousedown': this.onEditorEvent,
507 'mouseup': this.onEditorEvent,
508 'dblclick': this.onEditorEvent,
509 'click': this.onEditorEvent,
510 'keyup': this.onEditorEvent,
515 Roo.EventManager.on(this.doc, {
516 'paste': this.onPasteEvent,
520 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
523 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
524 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
526 this.initialized = true;
529 // initialize special key events - enter
530 new Roo.htmleditor.KeyEnter({core : this});
534 this.owner.fireEvent('initialize', this);
538 onPasteEvent : function(e,v)
540 // I think we better assume paste is going to be a dirty load of rubish from word..
542 // even pasting into a 'email version' of this widget will have to clean up that mess.
543 var cd = (e.browserEvent.clipboardData || window.clipboardData);
545 // check what type of paste - if it's an image, then handle it differently.
546 if (cd.files.length > 0) {
548 var urlAPI = (window.createObjectURL && window) ||
549 (window.URL && URL.revokeObjectURL && URL) ||
550 (window.webkitURL && webkitURL);
552 var url = urlAPI.createObjectURL( cd.files[0]);
553 this.insertAtCursor('<img src=" + url + ">');
557 var html = cd.getData('text/html'); // clipboard event
558 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
559 var images = parser.doc ? parser.doc.getElementsByType('pict') : [];
563 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable)/); }) // ignore headers
564 .map(function(g) { return g.toDataURL(); });
567 html = this.cleanWordChars(html);
569 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
572 var sn = this.getParentElement();
573 // check if d contains a table, and prevent nesting??
574 //Roo.log(d.getElementsByTagName('table'));
576 //Roo.log(sn.closest('table'));
577 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
579 this.insertAtCursor("You can not nest tables");
580 //Roo.log("prevent?"); // fixme -
584 if (images.length > 0) {
585 Roo.each(d.getElementsByTagName('img'), function(img, i) {
586 img.setAttribute('src', images[i]);
589 if (this.autoClean) {
590 new Roo.htmleditor.FilterStyleToTag({ node : d });
591 new Roo.htmleditor.FilterAttributes({
593 attrib_white : ['href', 'src', 'name', 'align'],
594 attrib_clean : ['href', 'src' ]
596 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
598 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT' ]} );
599 new Roo.htmleditor.FilterParagraph({ node : d });
600 new Roo.htmleditor.FilterSpan({ node : d });
601 new Roo.htmleditor.FilterLongBr({ node : d });
603 if (this.enableBlocks) {
605 Array.from(d.getElementByTagType('img')).forEach(function(img) {
606 if (img.closest('figure')) { // assume!! that it's aready
609 var fig = Roo.htmleditor.BlockFigure({
617 this.insertAtCursor(d.innerHTML);
618 Roo.htmleditor.Block.initAll(this.doc.body);
623 // default behaveiour should be our local cleanup paste? (optional?)
624 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
625 //this.owner.fireEvent('paste', e, v);
628 onDestroy : function(){
634 //for (var i =0; i < this.toolbars.length;i++) {
635 // // fixme - ask toolbars for heights?
636 // this.toolbars[i].onDestroy();
639 //this.wrap.dom.innerHTML = '';
640 //this.wrap.remove();
645 onFirstFocus : function(){
648 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
650 this.activated = true;
653 if(Roo.isGecko){ // prevent silly gecko errors
655 var s = this.win.getSelection();
656 if(!s.focusNode || s.focusNode.nodeType != 3){
657 var r = s.getRangeAt(0);
658 r.selectNodeContents((this.doc.body || this.doc.documentElement));
663 this.execCmd('useCSS', true);
664 this.execCmd('styleWithCSS', false);
667 this.owner.fireEvent('activate', this);
671 adjustFont: function(btn){
672 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
673 //if(Roo.isSafari){ // safari
676 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
677 if(Roo.isSafari){ // safari
678 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
679 v = (v < 10) ? 10 : v;
680 v = (v > 48) ? 48 : v;
681 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
686 v = Math.max(1, v+adjust);
688 this.execCmd('FontSize', v );
691 onEditorEvent : function(e)
694 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
695 return; // we do not handle this.. (undo manager does..)
697 // in theory this detects if the last element is not a br, then we try and do that.
698 // its so clicking in space at bottom triggers adding a br and moving the cursor.
700 e.target.nodeName == 'BODY' &&
701 e.type == "mouseup" &&
702 this.doc.body.lastChild
704 var lc = this.doc.body.lastChild;
705 // gtx-trans is google translate plugin adding crap.
706 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
707 lc = lc.previousSibling;
709 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
710 // if last element is <BR> - then dont do anything.
712 var ns = this.doc.createElement('br');
713 this.doc.body.appendChild(ns);
714 range = this.doc.createRange();
715 range.setStartAfter(ns);
716 range.collapse(true);
717 var sel = this.win.getSelection();
718 sel.removeAllRanges();
725 this.fireEditorEvent(e);
726 // this.updateToolbar();
727 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
730 fireEditorEvent: function(e)
732 this.owner.fireEvent('editorevent', this, e);
735 insertTag : function(tg)
737 // could be a bit smarter... -> wrap the current selected tRoo..
738 if (tg.toLowerCase() == 'span' ||
739 tg.toLowerCase() == 'code' ||
740 tg.toLowerCase() == 'sup' ||
741 tg.toLowerCase() == 'sub'
744 range = this.createRange(this.getSelection());
745 var wrappingNode = this.doc.createElement(tg.toLowerCase());
746 wrappingNode.appendChild(range.extractContents());
747 range.insertNode(wrappingNode);
754 this.execCmd("formatblock", tg);
755 this.undoManager.addEvent();
758 insertText : function(txt)
762 var range = this.createRange();
763 range.deleteContents();
764 //alert(Sender.getAttribute('label'));
766 range.insertNode(this.doc.createTextNode(txt));
767 this.undoManager.addEvent();
773 * Executes a Midas editor command on the editor document and performs necessary focus and
774 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
775 * @param {String} cmd The Midas command
776 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
778 relayCmd : function(cmd, value)
784 case 'justifycenter':
785 // if we are in a cell, then we will adjust the
786 var n = this.getParentElement();
787 var td = n.closest('td');
789 var bl = Roo.htmleditor.Block.factory(td);
790 bl.textAlign = cmd.replace('justify','');
792 this.owner.fireEvent('editorevent', this);
795 this.execCmd('styleWithCSS', true); //
799 // if there is no selection, then we insert, and set the curson inside it..
800 this.execCmd('styleWithCSS', false);
810 this.execCmd(cmd, value);
811 this.owner.fireEvent('editorevent', this);
812 //this.updateToolbar();
813 this.owner.deferFocus();
817 * Executes a Midas editor command directly on the editor document.
818 * For visual commands, you should use {@link #relayCmd} instead.
819 * <b>This should only be called after the editor is initialized.</b>
820 * @param {String} cmd The Midas command
821 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
823 execCmd : function(cmd, value){
824 this.doc.execCommand(cmd, false, value === undefined ? null : value);
831 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
833 * @param {String} text | dom node..
835 insertAtCursor : function(text)
842 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
846 // from jquery ui (MIT licenced)
850 if (win.getSelection && win.getSelection().getRangeAt) {
852 // delete the existing?
854 this.createRange(this.getSelection()).deleteContents();
855 range = win.getSelection().getRangeAt(0);
856 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
857 range.insertNode(node);
858 range = range.cloneRange();
859 range.collapse(false);
861 win.getSelection().removeAllRanges();
862 win.getSelection().addRange(range);
866 } else if (win.document.selection && win.document.selection.createRange) {
867 // no firefox support
868 var txt = typeof(text) == 'string' ? text : text.outerHTML;
869 win.document.selection.createRange().pasteHTML(txt);
872 // no firefox support
873 var txt = typeof(text) == 'string' ? text : text.outerHTML;
874 this.execCmd('InsertHTML', txt);
882 mozKeyPress : function(e){
884 var c = e.getCharCode(), cmd;
887 c = String.fromCharCode(c).toLowerCase();
901 // this.cleanUpPaste.defer(100, this);
919 fixKeys : function(){ // load time branching for fastest keydown performance
924 var k = e.getKey(), r;
927 r = this.doc.selection.createRange();
930 r.pasteHTML('    ');
935 /// this is handled by Roo.htmleditor.KeyEnter
938 r = this.doc.selection.createRange();
940 var target = r.parentElement();
941 if(!target || target.tagName.toLowerCase() != 'li'){
943 r.pasteHTML('<br/>');
950 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
951 // this.cleanUpPaste.defer(100, this);
957 }else if(Roo.isOpera){
963 this.execCmd('InsertHTML','    ');
967 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
968 // this.cleanUpPaste.defer(100, this);
973 }else if(Roo.isSafari){
979 this.execCmd('InsertText','\t');
985 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
986 // this.cleanUpPaste.defer(100, this);
994 getAllAncestors: function()
996 var p = this.getSelectedNode();
999 a.push(p); // push blank onto stack..
1000 p = this.getParentElement();
1004 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1008 a.push(this.doc.body);
1012 lastSelNode : false,
1015 getSelection : function()
1017 this.assignDocWin();
1018 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1022 * @param {DomElement} node the node to select
1024 selectNode : function(node, collapse)
1026 var nodeRange = node.ownerDocument.createRange();
1028 nodeRange.selectNode(node);
1030 nodeRange.selectNodeContents(node);
1032 if (collapse === true) {
1033 nodeRange.collapse(true);
1036 var s = this.win.getSelection();
1037 s.removeAllRanges();
1038 s.addRange(nodeRange);
1041 getSelectedNode: function()
1043 // this may only work on Gecko!!!
1045 // should we cache this!!!!
1050 var range = this.createRange(this.getSelection()).cloneRange();
1053 var parent = range.parentElement();
1055 var testRange = range.duplicate();
1056 testRange.moveToElementText(parent);
1057 if (testRange.inRange(range)) {
1060 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1063 parent = parent.parentElement;
1068 // is ancestor a text element.
1069 var ac = range.commonAncestorContainer;
1070 if (ac.nodeType == 3) {
1074 var ar = ac.childNodes;
1077 var other_nodes = [];
1078 var has_other_nodes = false;
1079 for (var i=0;i<ar.length;i++) {
1080 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1083 // fullly contained node.
1085 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1090 // probably selected..
1091 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1092 other_nodes.push(ar[i]);
1096 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1101 has_other_nodes = true;
1103 if (!nodes.length && other_nodes.length) {
1106 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1112 createRange: function(sel)
1114 // this has strange effects when using with
1115 // top toolbar - not sure if it's a great idea.
1116 //this.editor.contentWindow.focus();
1117 if (typeof sel != "undefined") {
1119 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1121 return this.doc.createRange();
1124 return this.doc.createRange();
1127 getParentElement: function()
1130 this.assignDocWin();
1131 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1133 var range = this.createRange(sel);
1136 var p = range.commonAncestorContainer;
1137 while (p.nodeType == 3) { // text node
1148 * Range intersection.. the hard stuff...
1152 * [ -- selected range --- ]
1156 * if end is before start or hits it. fail.
1157 * if start is after end or hits it fail.
1159 * if either hits (but other is outside. - then it's not
1165 // @see http://www.thismuchiknow.co.uk/?p=64.
1166 rangeIntersectsNode : function(range, node)
1168 var nodeRange = node.ownerDocument.createRange();
1170 nodeRange.selectNode(node);
1172 nodeRange.selectNodeContents(node);
1175 var rangeStartRange = range.cloneRange();
1176 rangeStartRange.collapse(true);
1178 var rangeEndRange = range.cloneRange();
1179 rangeEndRange.collapse(false);
1181 var nodeStartRange = nodeRange.cloneRange();
1182 nodeStartRange.collapse(true);
1184 var nodeEndRange = nodeRange.cloneRange();
1185 nodeEndRange.collapse(false);
1187 return rangeStartRange.compareBoundaryPoints(
1188 Range.START_TO_START, nodeEndRange) == -1 &&
1189 rangeEndRange.compareBoundaryPoints(
1190 Range.START_TO_START, nodeStartRange) == 1;
1194 rangeCompareNode : function(range, node)
1196 var nodeRange = node.ownerDocument.createRange();
1198 nodeRange.selectNode(node);
1200 nodeRange.selectNodeContents(node);
1204 range.collapse(true);
1206 nodeRange.collapse(true);
1208 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1209 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1211 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1213 var nodeIsBefore = ss == 1;
1214 var nodeIsAfter = ee == -1;
1216 if (nodeIsBefore && nodeIsAfter) {
1219 if (!nodeIsBefore && nodeIsAfter) {
1220 return 1; //right trailed.
1223 if (nodeIsBefore && !nodeIsAfter) {
1224 return 2; // left trailed.
1230 cleanWordChars : function(input) {// change the chars to hex code
1233 [ 8211, "–" ],
1234 [ 8212, "—" ],
1243 Roo.each(swapCodes, function(sw) {
1244 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1246 output = output.replace(swapper, sw[1]);
1256 cleanUpChild : function (node)
1259 new Roo.htmleditor.FilterComment({node : node});
1260 new Roo.htmleditor.FilterAttributes({
1262 attrib_black : this.ablack,
1263 attrib_clean : this.aclean,
1264 style_white : this.cwhite,
1265 style_black : this.cblack
1267 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1268 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1274 * Clean up MS wordisms...
1275 * @deprecated - use filter directly
1277 cleanWord : function(node)
1279 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1286 * @deprecated - use filters
1288 cleanTableWidths : function(node)
1290 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1297 applyBlacklists : function()
1299 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1300 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1302 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1303 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1304 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1308 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1309 if (b.indexOf(tag) > -1) {
1312 this.white.push(tag);
1316 Roo.each(w, function(tag) {
1317 if (b.indexOf(tag) > -1) {
1320 if (this.white.indexOf(tag) > -1) {
1323 this.white.push(tag);
1328 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1329 if (w.indexOf(tag) > -1) {
1332 this.black.push(tag);
1336 Roo.each(b, function(tag) {
1337 if (w.indexOf(tag) > -1) {
1340 if (this.black.indexOf(tag) > -1) {
1343 this.black.push(tag);
1348 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1349 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1353 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1354 if (b.indexOf(tag) > -1) {
1357 this.cwhite.push(tag);
1361 Roo.each(w, function(tag) {
1362 if (b.indexOf(tag) > -1) {
1365 if (this.cwhite.indexOf(tag) > -1) {
1368 this.cwhite.push(tag);
1373 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1374 if (w.indexOf(tag) > -1) {
1377 this.cblack.push(tag);
1381 Roo.each(b, function(tag) {
1382 if (w.indexOf(tag) > -1) {
1385 if (this.cblack.indexOf(tag) > -1) {
1388 this.cblack.push(tag);
1393 setStylesheets : function(stylesheets)
1395 if(typeof(stylesheets) == 'string'){
1396 Roo.get(this.iframe.contentDocument.head).createChild({
1407 Roo.each(stylesheets, function(s) {
1412 Roo.get(_this.iframe.contentDocument.head).createChild({
1423 removeStylesheets : function()
1427 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1432 setStyle : function(style)
1434 Roo.get(this.iframe.contentDocument.head).createChild({
1443 // hide stuff that is not compatible
1461 * @cfg {String} fieldClass @hide
1464 * @cfg {String} focusClass @hide
1467 * @cfg {String} autoCreate @hide
1470 * @cfg {String} inputType @hide
1473 * @cfg {String} invalidClass @hide
1476 * @cfg {String} invalidText @hide
1479 * @cfg {String} msgFx @hide
1482 * @cfg {String} validateOnBlur @hide
1486 Roo.HtmlEditorCore.white = [
1487 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1489 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1490 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1491 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1492 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1493 'TABLE', 'UL', 'XMP',
1495 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1498 'DIR', 'MENU', 'OL', 'UL', 'DL',
1504 Roo.HtmlEditorCore.black = [
1505 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1507 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1508 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1509 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1510 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1511 //'FONT' // CLEAN LATER..
1512 'COLGROUP', 'COL' // messy tables.
1515 Roo.HtmlEditorCore.clean = [ // ?? needed???
1516 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1518 Roo.HtmlEditorCore.tag_remove = [
1523 Roo.HtmlEditorCore.ablack = [
1527 Roo.HtmlEditorCore.aclean = [
1528 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1532 Roo.HtmlEditorCore.pwhite= [
1533 'http', 'https', 'mailto'
1536 // white listed style attributes.
1537 Roo.HtmlEditorCore.cwhite= [
1538 // 'text-align', /// default is to allow most things..
1544 // black listed style attributes.
1545 Roo.HtmlEditorCore.cblack= [
1546 // 'font-size' -- this can be set by the project