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({
612 fig.updateElement(img); // replace it..
618 this.insertAtCursor(d.innerHTML);
619 if (this.enableBlocks) {
620 Roo.htmleditor.Block.initAll(this.doc.body);
626 // default behaveiour should be our local cleanup paste? (optional?)
627 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
628 //this.owner.fireEvent('paste', e, v);
631 onDestroy : function(){
637 //for (var i =0; i < this.toolbars.length;i++) {
638 // // fixme - ask toolbars for heights?
639 // this.toolbars[i].onDestroy();
642 //this.wrap.dom.innerHTML = '';
643 //this.wrap.remove();
648 onFirstFocus : function(){
651 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
653 this.activated = true;
656 if(Roo.isGecko){ // prevent silly gecko errors
658 var s = this.win.getSelection();
659 if(!s.focusNode || s.focusNode.nodeType != 3){
660 var r = s.getRangeAt(0);
661 r.selectNodeContents((this.doc.body || this.doc.documentElement));
666 this.execCmd('useCSS', true);
667 this.execCmd('styleWithCSS', false);
670 this.owner.fireEvent('activate', this);
674 adjustFont: function(btn){
675 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
676 //if(Roo.isSafari){ // safari
679 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
680 if(Roo.isSafari){ // safari
681 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
682 v = (v < 10) ? 10 : v;
683 v = (v > 48) ? 48 : v;
684 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
689 v = Math.max(1, v+adjust);
691 this.execCmd('FontSize', v );
694 onEditorEvent : function(e)
697 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
698 return; // we do not handle this.. (undo manager does..)
700 // in theory this detects if the last element is not a br, then we try and do that.
701 // its so clicking in space at bottom triggers adding a br and moving the cursor.
703 e.target.nodeName == 'BODY' &&
704 e.type == "mouseup" &&
705 this.doc.body.lastChild
707 var lc = this.doc.body.lastChild;
708 // gtx-trans is google translate plugin adding crap.
709 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
710 lc = lc.previousSibling;
712 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
713 // if last element is <BR> - then dont do anything.
715 var ns = this.doc.createElement('br');
716 this.doc.body.appendChild(ns);
717 range = this.doc.createRange();
718 range.setStartAfter(ns);
719 range.collapse(true);
720 var sel = this.win.getSelection();
721 sel.removeAllRanges();
728 this.fireEditorEvent(e);
729 // this.updateToolbar();
730 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
733 fireEditorEvent: function(e)
735 this.owner.fireEvent('editorevent', this, e);
738 insertTag : function(tg)
740 // could be a bit smarter... -> wrap the current selected tRoo..
741 if (tg.toLowerCase() == 'span' ||
742 tg.toLowerCase() == 'code' ||
743 tg.toLowerCase() == 'sup' ||
744 tg.toLowerCase() == 'sub'
747 range = this.createRange(this.getSelection());
748 var wrappingNode = this.doc.createElement(tg.toLowerCase());
749 wrappingNode.appendChild(range.extractContents());
750 range.insertNode(wrappingNode);
757 this.execCmd("formatblock", tg);
758 this.undoManager.addEvent();
761 insertText : function(txt)
765 var range = this.createRange();
766 range.deleteContents();
767 //alert(Sender.getAttribute('label'));
769 range.insertNode(this.doc.createTextNode(txt));
770 this.undoManager.addEvent();
776 * Executes a Midas editor command on the editor document and performs necessary focus and
777 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
778 * @param {String} cmd The Midas command
779 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
781 relayCmd : function(cmd, value)
787 case 'justifycenter':
788 // if we are in a cell, then we will adjust the
789 var n = this.getParentElement();
790 var td = n.closest('td');
792 var bl = Roo.htmleditor.Block.factory(td);
793 bl.textAlign = cmd.replace('justify','');
795 this.owner.fireEvent('editorevent', this);
798 this.execCmd('styleWithCSS', true); //
802 // if there is no selection, then we insert, and set the curson inside it..
803 this.execCmd('styleWithCSS', false);
813 this.execCmd(cmd, value);
814 this.owner.fireEvent('editorevent', this);
815 //this.updateToolbar();
816 this.owner.deferFocus();
820 * Executes a Midas editor command directly on the editor document.
821 * For visual commands, you should use {@link #relayCmd} instead.
822 * <b>This should only be called after the editor is initialized.</b>
823 * @param {String} cmd The Midas command
824 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
826 execCmd : function(cmd, value){
827 this.doc.execCommand(cmd, false, value === undefined ? null : value);
834 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
836 * @param {String} text | dom node..
838 insertAtCursor : function(text)
845 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
849 // from jquery ui (MIT licenced)
853 if (win.getSelection && win.getSelection().getRangeAt) {
855 // delete the existing?
857 this.createRange(this.getSelection()).deleteContents();
858 range = win.getSelection().getRangeAt(0);
859 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
860 range.insertNode(node);
861 range = range.cloneRange();
862 range.collapse(false);
864 win.getSelection().removeAllRanges();
865 win.getSelection().addRange(range);
869 } else if (win.document.selection && win.document.selection.createRange) {
870 // no firefox support
871 var txt = typeof(text) == 'string' ? text : text.outerHTML;
872 win.document.selection.createRange().pasteHTML(txt);
875 // no firefox support
876 var txt = typeof(text) == 'string' ? text : text.outerHTML;
877 this.execCmd('InsertHTML', txt);
885 mozKeyPress : function(e){
887 var c = e.getCharCode(), cmd;
890 c = String.fromCharCode(c).toLowerCase();
904 // this.cleanUpPaste.defer(100, this);
922 fixKeys : function(){ // load time branching for fastest keydown performance
927 var k = e.getKey(), r;
930 r = this.doc.selection.createRange();
933 r.pasteHTML('    ');
938 /// this is handled by Roo.htmleditor.KeyEnter
941 r = this.doc.selection.createRange();
943 var target = r.parentElement();
944 if(!target || target.tagName.toLowerCase() != 'li'){
946 r.pasteHTML('<br/>');
953 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
954 // this.cleanUpPaste.defer(100, this);
960 }else if(Roo.isOpera){
966 this.execCmd('InsertHTML','    ');
970 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
971 // this.cleanUpPaste.defer(100, this);
976 }else if(Roo.isSafari){
982 this.execCmd('InsertText','\t');
988 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
989 // this.cleanUpPaste.defer(100, this);
997 getAllAncestors: function()
999 var p = this.getSelectedNode();
1002 a.push(p); // push blank onto stack..
1003 p = this.getParentElement();
1007 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1011 a.push(this.doc.body);
1015 lastSelNode : false,
1018 getSelection : function()
1020 this.assignDocWin();
1021 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1025 * @param {DomElement} node the node to select
1027 selectNode : function(node, collapse)
1029 var nodeRange = node.ownerDocument.createRange();
1031 nodeRange.selectNode(node);
1033 nodeRange.selectNodeContents(node);
1035 if (collapse === true) {
1036 nodeRange.collapse(true);
1039 var s = this.win.getSelection();
1040 s.removeAllRanges();
1041 s.addRange(nodeRange);
1044 getSelectedNode: function()
1046 // this may only work on Gecko!!!
1048 // should we cache this!!!!
1053 var range = this.createRange(this.getSelection()).cloneRange();
1056 var parent = range.parentElement();
1058 var testRange = range.duplicate();
1059 testRange.moveToElementText(parent);
1060 if (testRange.inRange(range)) {
1063 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1066 parent = parent.parentElement;
1071 // is ancestor a text element.
1072 var ac = range.commonAncestorContainer;
1073 if (ac.nodeType == 3) {
1077 var ar = ac.childNodes;
1080 var other_nodes = [];
1081 var has_other_nodes = false;
1082 for (var i=0;i<ar.length;i++) {
1083 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1086 // fullly contained node.
1088 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1093 // probably selected..
1094 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1095 other_nodes.push(ar[i]);
1099 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1104 has_other_nodes = true;
1106 if (!nodes.length && other_nodes.length) {
1109 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1115 createRange: function(sel)
1117 // this has strange effects when using with
1118 // top toolbar - not sure if it's a great idea.
1119 //this.editor.contentWindow.focus();
1120 if (typeof sel != "undefined") {
1122 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1124 return this.doc.createRange();
1127 return this.doc.createRange();
1130 getParentElement: function()
1133 this.assignDocWin();
1134 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1136 var range = this.createRange(sel);
1139 var p = range.commonAncestorContainer;
1140 while (p.nodeType == 3) { // text node
1151 * Range intersection.. the hard stuff...
1155 * [ -- selected range --- ]
1159 * if end is before start or hits it. fail.
1160 * if start is after end or hits it fail.
1162 * if either hits (but other is outside. - then it's not
1168 // @see http://www.thismuchiknow.co.uk/?p=64.
1169 rangeIntersectsNode : function(range, node)
1171 var nodeRange = node.ownerDocument.createRange();
1173 nodeRange.selectNode(node);
1175 nodeRange.selectNodeContents(node);
1178 var rangeStartRange = range.cloneRange();
1179 rangeStartRange.collapse(true);
1181 var rangeEndRange = range.cloneRange();
1182 rangeEndRange.collapse(false);
1184 var nodeStartRange = nodeRange.cloneRange();
1185 nodeStartRange.collapse(true);
1187 var nodeEndRange = nodeRange.cloneRange();
1188 nodeEndRange.collapse(false);
1190 return rangeStartRange.compareBoundaryPoints(
1191 Range.START_TO_START, nodeEndRange) == -1 &&
1192 rangeEndRange.compareBoundaryPoints(
1193 Range.START_TO_START, nodeStartRange) == 1;
1197 rangeCompareNode : function(range, node)
1199 var nodeRange = node.ownerDocument.createRange();
1201 nodeRange.selectNode(node);
1203 nodeRange.selectNodeContents(node);
1207 range.collapse(true);
1209 nodeRange.collapse(true);
1211 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1212 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1214 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1216 var nodeIsBefore = ss == 1;
1217 var nodeIsAfter = ee == -1;
1219 if (nodeIsBefore && nodeIsAfter) {
1222 if (!nodeIsBefore && nodeIsAfter) {
1223 return 1; //right trailed.
1226 if (nodeIsBefore && !nodeIsAfter) {
1227 return 2; // left trailed.
1233 cleanWordChars : function(input) {// change the chars to hex code
1236 [ 8211, "–" ],
1237 [ 8212, "—" ],
1246 Roo.each(swapCodes, function(sw) {
1247 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1249 output = output.replace(swapper, sw[1]);
1259 cleanUpChild : function (node)
1262 new Roo.htmleditor.FilterComment({node : node});
1263 new Roo.htmleditor.FilterAttributes({
1265 attrib_black : this.ablack,
1266 attrib_clean : this.aclean,
1267 style_white : this.cwhite,
1268 style_black : this.cblack
1270 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1271 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1277 * Clean up MS wordisms...
1278 * @deprecated - use filter directly
1280 cleanWord : function(node)
1282 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1289 * @deprecated - use filters
1291 cleanTableWidths : function(node)
1293 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1300 applyBlacklists : function()
1302 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1303 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1305 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1306 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1307 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1311 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1312 if (b.indexOf(tag) > -1) {
1315 this.white.push(tag);
1319 Roo.each(w, function(tag) {
1320 if (b.indexOf(tag) > -1) {
1323 if (this.white.indexOf(tag) > -1) {
1326 this.white.push(tag);
1331 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1332 if (w.indexOf(tag) > -1) {
1335 this.black.push(tag);
1339 Roo.each(b, function(tag) {
1340 if (w.indexOf(tag) > -1) {
1343 if (this.black.indexOf(tag) > -1) {
1346 this.black.push(tag);
1351 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1352 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1356 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1357 if (b.indexOf(tag) > -1) {
1360 this.cwhite.push(tag);
1364 Roo.each(w, function(tag) {
1365 if (b.indexOf(tag) > -1) {
1368 if (this.cwhite.indexOf(tag) > -1) {
1371 this.cwhite.push(tag);
1376 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1377 if (w.indexOf(tag) > -1) {
1380 this.cblack.push(tag);
1384 Roo.each(b, function(tag) {
1385 if (w.indexOf(tag) > -1) {
1388 if (this.cblack.indexOf(tag) > -1) {
1391 this.cblack.push(tag);
1396 setStylesheets : function(stylesheets)
1398 if(typeof(stylesheets) == 'string'){
1399 Roo.get(this.iframe.contentDocument.head).createChild({
1410 Roo.each(stylesheets, function(s) {
1415 Roo.get(_this.iframe.contentDocument.head).createChild({
1426 removeStylesheets : function()
1430 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1435 setStyle : function(style)
1437 Roo.get(this.iframe.contentDocument.head).createChild({
1446 // hide stuff that is not compatible
1464 * @cfg {String} fieldClass @hide
1467 * @cfg {String} focusClass @hide
1470 * @cfg {String} autoCreate @hide
1473 * @cfg {String} inputType @hide
1476 * @cfg {String} invalidClass @hide
1479 * @cfg {String} invalidText @hide
1482 * @cfg {String} msgFx @hide
1485 * @cfg {String} validateOnBlur @hide
1489 Roo.HtmlEditorCore.white = [
1490 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1492 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1493 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1494 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1495 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1496 'TABLE', 'UL', 'XMP',
1498 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1501 'DIR', 'MENU', 'OL', 'UL', 'DL',
1507 Roo.HtmlEditorCore.black = [
1508 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1510 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1511 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1512 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1513 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1514 //'FONT' // CLEAN LATER..
1515 'COLGROUP', 'COL' // messy tables.
1518 Roo.HtmlEditorCore.clean = [ // ?? needed???
1519 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1521 Roo.HtmlEditorCore.tag_remove = [
1526 Roo.HtmlEditorCore.ablack = [
1530 Roo.HtmlEditorCore.aclean = [
1531 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1535 Roo.HtmlEditorCore.pwhite= [
1536 'http', 'https', 'mailto'
1539 // white listed style attributes.
1540 Roo.HtmlEditorCore.cwhite= [
1541 // 'text-align', /// default is to allow most things..
1547 // black listed style attributes.
1548 Roo.HtmlEditorCore.cblack= [
1549 // 'font-size' -- this can be set by the project