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 Array.from(bd.getElementsByTagName('img')).forEach(function(img) {
380 var fig = img.closest('figure');
382 var bf = new Roo.htmleditor.BlockFigure({
389 new Roo.htmleditor.FilterBlock({ node : div });
392 var html = div.innerHTML;
395 if (this.autoClean) {
396 new Roo.htmleditor.FilterBlack({ node : div, tag : this.black});
397 new Roo.htmleditor.FilterAttributes({
407 'data-caption-display',
420 attrib_clean : ['href', 'src' ]
422 new Roo.htmleditor.FilterEmpty({ node : div});
424 var tidy = new Roo.htmleditor.TidySerializer({
427 html = tidy.serialize(div);
433 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
434 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
436 html = '<div style="'+m[0]+'">' + html + '</div>';
439 html = this.cleanHtml(html);
440 // fix up the special chars.. normaly like back quotes in word...
441 // however we do not want to do this with chinese..
442 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
444 var cc = match.charCodeAt();
446 // Get the character value, handling surrogate pairs
447 if (match.length == 2) {
448 // It's a surrogate pair, calculate the Unicode code point
449 var high = match.charCodeAt(0) - 0xD800;
450 var low = match.charCodeAt(1) - 0xDC00;
451 cc = (high * 0x400) + low + 0x10000;
453 (cc >= 0x4E00 && cc < 0xA000 ) ||
454 (cc >= 0x3400 && cc < 0x4E00 ) ||
455 (cc >= 0xf900 && cc < 0xfb00 )
460 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
461 return "&#" + cc + ";";
468 if(this.owner.fireEvent('beforesync', this, html) !== false){
469 this.el.dom.value = html;
470 this.owner.fireEvent('sync', this, html);
476 * TEXTAREA -> EDITABLE
477 * Protected method that will not generally be called directly. Pushes the value of the textarea
478 * into the iframe editor.
480 pushValue : function()
482 //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
483 if(this.initialized){
484 var v = this.el.dom.value.trim();
487 if(this.owner.fireEvent('beforepush', this, v) !== false){
488 var d = (this.doc.body || this.doc.documentElement);
491 this.el.dom.value = d.innerHTML;
492 this.owner.fireEvent('push', this, v);
494 if (this.autoClean) {
495 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
496 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
498 if (this.enableBlocks) {
499 Roo.htmleditor.Block.initAll(this.doc.body);
502 this.updateLanguage();
504 var lc = this.doc.body.lastChild;
505 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
506 // add an extra line at the end.
507 this.doc.body.appendChild(this.doc.createElement('br'));
515 deferFocus : function(){
516 this.focus.defer(10, this);
521 if(this.win && !this.sourceEditMode){
528 assignDocWin: function()
530 var iframe = this.iframe;
533 this.doc = iframe.contentWindow.document;
534 this.win = iframe.contentWindow;
536 // if (!Roo.get(this.frameId)) {
539 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
540 // this.win = Roo.get(this.frameId).dom.contentWindow;
542 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
546 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
547 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
552 initEditor : function(){
553 //console.log("INIT EDITOR");
558 this.doc.designMode="on";
560 this.doc.write(this.getDocMarkup());
563 var dbody = (this.doc.body || this.doc.documentElement);
564 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
565 // this copies styles from the containing element into thsi one..
566 // not sure why we need all of this..
567 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
569 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
570 //ss['background-attachment'] = 'fixed'; // w3c
571 dbody.bgProperties = 'fixed'; // ie
572 dbody.setAttribute("translate", "no");
574 //Roo.DomHelper.applyStyles(dbody, ss);
575 Roo.EventManager.on(this.doc, {
577 'mouseup': this.onEditorEvent,
578 'dblclick': this.onEditorEvent,
579 'click': this.onEditorEvent,
580 'keyup': this.onEditorEvent,
585 Roo.EventManager.on(this.doc, {
586 'paste': this.onPasteEvent,
590 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
593 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
594 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
596 this.initialized = true;
599 // initialize special key events - enter
600 new Roo.htmleditor.KeyEnter({core : this});
604 this.owner.fireEvent('initialize', this);
607 // this is to prevent a href clicks resulting in a redirect?
609 onPasteEvent : function(e,v)
611 // I think we better assume paste is going to be a dirty load of rubish from word..
613 // even pasting into a 'email version' of this widget will have to clean up that mess.
614 var cd = (e.browserEvent.clipboardData || window.clipboardData);
616 // check what type of paste - if it's an image, then handle it differently.
617 if (cd.files && cd.files.length > 0 && cd.types.indexOf('text/html') < 0) {
619 var urlAPI = (window.createObjectURL && window) ||
620 (window.URL && URL.revokeObjectURL && URL) ||
621 (window.webkitURL && webkitURL);
623 var r = new FileReader();
625 r.addEventListener('load',function()
628 var d = (new DOMParser().parseFromString('<img src="' + r.result+ '">', 'text/html')).body;
630 if (t.enableBlocks) {
632 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
633 if (img.closest('figure')) { // assume!! that it's aready
636 var fig = new Roo.htmleditor.BlockFigure({
639 fig.updateElement(img); // replace it..
643 t.insertAtCursor(d.innerHTML.replace(/ /g,' '));
644 t.owner.fireEvent('paste', this);
646 r.readAsDataURL(cd.files[0]);
652 if (cd.types.indexOf('text/html') < 0 ) {
656 var html = cd.getData('text/html'); // clipboard event
657 if (cd.types.indexOf('text/rtf') > -1) {
658 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
659 images = parser.doc ? parser.doc.getElementsByType('pict') : [];
664 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable|footerf)/); }) // ignore headers/footers etc.
665 .map(function(g) { return g.toDataURL(); })
666 .filter(function(g) { return g != 'about:blank'; });
669 html = this.cleanWordChars(html);
671 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
674 var sn = this.getParentElement();
675 // check if d contains a table, and prevent nesting??
676 //Roo.log(d.getElementsByTagName('table'));
678 //Roo.log(sn.closest('table'));
679 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
681 this.insertAtCursor("You can not nest tables");
682 //Roo.log("prevent?"); // fixme -
688 if (images.length > 0) {
689 // replace all v:imagedata - with img.
690 var ar = Array.from(d.getElementsByTagName('v:imagedata'));
691 Roo.each(ar, function(node) {
692 node.parentNode.insertBefore(d.ownerDocument.createElement('img'), node );
693 node.parentNode.removeChild(node);
697 Roo.each(d.getElementsByTagName('img'), function(img, i) {
698 img.setAttribute('src', images[i]);
701 if (this.autoClean) {
702 new Roo.htmleditor.FilterWord({ node : d });
704 new Roo.htmleditor.FilterStyleToTag({ node : d });
705 new Roo.htmleditor.FilterAttributes({
714 /* THESE ARE NOT ALLWOED FOR PASTE
716 'data-caption-display',
730 attrib_clean : ['href', 'src' ]
732 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
734 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', ':' ]} );
735 new Roo.htmleditor.FilterParagraph({ node : d });
736 new Roo.htmleditor.FilterHashLink({node : d});
737 new Roo.htmleditor.FilterSpan({ node : d });
738 new Roo.htmleditor.FilterLongBr({ node : d });
739 new Roo.htmleditor.FilterComment({ node : d });
740 new Roo.htmleditor.FilterEmpty({ node : d});
744 if (this.enableBlocks) {
746 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
747 if (img.closest('figure')) { // assume!! that it's aready
750 var fig = new Roo.htmleditor.BlockFigure({
753 fig.updateElement(img); // replace it..
759 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
760 if (this.enableBlocks) {
761 Roo.htmleditor.Block.initAll(this.doc.body);
766 this.owner.fireEvent('paste', this);
768 // default behaveiour should be our local cleanup paste? (optional?)
769 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
770 //this.owner.fireEvent('paste', e, v);
773 onDestroy : function(){
779 //for (var i =0; i < this.toolbars.length;i++) {
780 // // fixme - ask toolbars for heights?
781 // this.toolbars[i].onDestroy();
784 //this.wrap.dom.innerHTML = '';
785 //this.wrap.remove();
790 onFirstFocus : function(){
793 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
795 this.activated = true;
798 if(Roo.isGecko){ // prevent silly gecko errors
800 var s = this.win.getSelection();
801 if(!s.focusNode || s.focusNode.nodeType != 3){
802 var r = s.getRangeAt(0);
803 r.selectNodeContents((this.doc.body || this.doc.documentElement));
808 this.execCmd('useCSS', true);
809 this.execCmd('styleWithCSS', false);
812 this.owner.fireEvent('activate', this);
816 adjustFont: function(btn){
817 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
818 //if(Roo.isSafari){ // safari
821 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
822 if(Roo.isSafari){ // safari
823 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
824 v = (v < 10) ? 10 : v;
825 v = (v > 48) ? 48 : v;
826 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
831 v = Math.max(1, v+adjust);
833 this.execCmd('FontSize', v );
836 onEditorEvent : function(e)
840 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
841 return; // we do not handle this.. (undo manager does..)
843 // clicking a 'block'?
845 // in theory this detects if the last element is not a br, then we try and do that.
846 // its so clicking in space at bottom triggers adding a br and moving the cursor.
848 e.target.nodeName == 'BODY' &&
849 e.type == "mouseup" &&
850 this.doc.body.lastChild
852 var lc = this.doc.body.lastChild;
853 // gtx-trans is google translate plugin adding crap.
854 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
855 lc = lc.previousSibling;
857 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
858 // if last element is <BR> - then dont do anything.
860 var ns = this.doc.createElement('br');
861 this.doc.body.appendChild(ns);
862 range = this.doc.createRange();
863 range.setStartAfter(ns);
864 range.collapse(true);
865 var sel = this.win.getSelection();
866 sel.removeAllRanges();
873 this.fireEditorEvent(e);
874 // this.updateToolbar();
875 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
878 fireEditorEvent: function(e)
880 this.owner.fireEvent('editorevent', this, e);
883 insertTag : function(tg)
885 // could be a bit smarter... -> wrap the current selected tRoo..
886 if (tg.toLowerCase() == 'span' ||
887 tg.toLowerCase() == 'code' ||
888 tg.toLowerCase() == 'sup' ||
889 tg.toLowerCase() == 'sub'
892 range = this.createRange(this.getSelection());
893 var wrappingNode = this.doc.createElement(tg.toLowerCase());
894 wrappingNode.appendChild(range.extractContents());
895 range.insertNode(wrappingNode);
902 this.execCmd("formatblock", tg);
903 this.undoManager.addEvent();
906 insertText : function(txt)
910 var range = this.createRange();
911 range.deleteContents();
912 //alert(Sender.getAttribute('label'));
914 range.insertNode(this.doc.createTextNode(txt));
915 this.undoManager.addEvent();
921 * Executes a Midas editor command on the editor document and performs necessary focus and
922 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
923 * @param {String} cmd The Midas command
924 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
926 relayCmd : function(cmd, value)
932 case 'justifycenter':
933 // if we are in a cell, then we will adjust the
934 var n = this.getParentElement();
935 var td = n.closest('td');
937 var bl = Roo.htmleditor.Block.factory(td);
938 bl.textAlign = cmd.replace('justify','');
940 this.owner.fireEvent('editorevent', this);
943 this.execCmd('styleWithCSS', true); //
948 // if there is no selection, then we insert, and set the curson inside it..
949 this.execCmd('styleWithCSS', false);
959 this.execCmd(cmd, value);
960 this.owner.fireEvent('editorevent', this);
961 //this.updateToolbar();
962 this.owner.deferFocus();
966 * Executes a Midas editor command directly on the editor document.
967 * For visual commands, you should use {@link #relayCmd} instead.
968 * <b>This should only be called after the editor is initialized.</b>
969 * @param {String} cmd The Midas command
970 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
972 execCmd : function(cmd, value){
973 this.doc.execCommand(cmd, false, value === undefined ? null : value);
980 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
982 * @param {String} text | dom node..
984 insertAtCursor : function(text)
991 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
995 // from jquery ui (MIT licenced)
999 if (win.getSelection && win.getSelection().getRangeAt) {
1001 // delete the existing?
1003 this.createRange(this.getSelection()).deleteContents();
1004 range = win.getSelection().getRangeAt(0);
1005 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
1006 range.insertNode(node);
1007 range = range.cloneRange();
1008 range.collapse(false);
1010 win.getSelection().removeAllRanges();
1011 win.getSelection().addRange(range);
1015 } else if (win.document.selection && win.document.selection.createRange) {
1016 // no firefox support
1017 var txt = typeof(text) == 'string' ? text : text.outerHTML;
1018 win.document.selection.createRange().pasteHTML(txt);
1021 // no firefox support
1022 var txt = typeof(text) == 'string' ? text : text.outerHTML;
1023 this.execCmd('InsertHTML', txt);
1031 mozKeyPress : function(e){
1033 var c = e.getCharCode(), cmd;
1036 c = String.fromCharCode(c).toLowerCase();
1050 // this.cleanUpPaste.defer(100, this);
1058 //this.execCmd(cmd);
1059 //this.deferFocus();
1068 fixKeys : function(){ // load time branching for fastest keydown performance
1073 var k = e.getKey(), r;
1076 r = this.doc.selection.createRange();
1079 r.pasteHTML('    ');
1084 /// this is handled by Roo.htmleditor.KeyEnter
1087 r = this.doc.selection.createRange();
1089 var target = r.parentElement();
1090 if(!target || target.tagName.toLowerCase() != 'li'){
1092 r.pasteHTML('<br/>');
1099 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1100 // this.cleanUpPaste.defer(100, this);
1106 }else if(Roo.isOpera){
1112 this.execCmd('InsertHTML','    ');
1116 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1117 // this.cleanUpPaste.defer(100, this);
1122 }else if(Roo.isSafari){
1128 this.execCmd('InsertText','\t');
1132 this.mozKeyPress(e);
1134 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1135 // this.cleanUpPaste.defer(100, this);
1143 getAllAncestors: function()
1145 var p = this.getSelectedNode();
1148 a.push(p); // push blank onto stack..
1149 p = this.getParentElement();
1153 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1157 a.push(this.doc.body);
1161 lastSelNode : false,
1164 getSelection : function()
1166 this.assignDocWin();
1167 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1171 * @param {DomElement} node the node to select
1173 selectNode : function(node, collapse)
1175 var nodeRange = node.ownerDocument.createRange();
1177 nodeRange.selectNode(node);
1179 nodeRange.selectNodeContents(node);
1181 if (collapse === true) {
1182 nodeRange.collapse(true);
1185 var s = this.win.getSelection();
1186 s.removeAllRanges();
1187 s.addRange(nodeRange);
1190 getSelectedNode: function()
1192 // this may only work on Gecko!!!
1194 // should we cache this!!!!
1198 var range = this.createRange(this.getSelection()).cloneRange();
1201 var parent = range.parentElement();
1203 var testRange = range.duplicate();
1204 testRange.moveToElementText(parent);
1205 if (testRange.inRange(range)) {
1208 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1211 parent = parent.parentElement;
1216 // is ancestor a text element.
1217 var ac = range.commonAncestorContainer;
1218 if (ac.nodeType == 3) {
1222 var ar = ac.childNodes;
1225 var other_nodes = [];
1226 var has_other_nodes = false;
1227 for (var i=0;i<ar.length;i++) {
1228 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1231 // fullly contained node.
1233 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1238 // probably selected..
1239 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1240 other_nodes.push(ar[i]);
1244 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1249 has_other_nodes = true;
1251 if (!nodes.length && other_nodes.length) {
1254 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1262 createRange: function(sel)
1264 // this has strange effects when using with
1265 // top toolbar - not sure if it's a great idea.
1266 //this.editor.contentWindow.focus();
1267 if (typeof sel != "undefined") {
1269 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1271 return this.doc.createRange();
1274 return this.doc.createRange();
1277 getParentElement: function()
1280 this.assignDocWin();
1281 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1283 var range = this.createRange(sel);
1286 var p = range.commonAncestorContainer;
1287 while (p.nodeType == 3) { // text node
1298 * Range intersection.. the hard stuff...
1302 * [ -- selected range --- ]
1306 * if end is before start or hits it. fail.
1307 * if start is after end or hits it fail.
1309 * if either hits (but other is outside. - then it's not
1315 // @see http://www.thismuchiknow.co.uk/?p=64.
1316 rangeIntersectsNode : function(range, node)
1318 var nodeRange = node.ownerDocument.createRange();
1320 nodeRange.selectNode(node);
1322 nodeRange.selectNodeContents(node);
1325 var rangeStartRange = range.cloneRange();
1326 rangeStartRange.collapse(true);
1328 var rangeEndRange = range.cloneRange();
1329 rangeEndRange.collapse(false);
1331 var nodeStartRange = nodeRange.cloneRange();
1332 nodeStartRange.collapse(true);
1334 var nodeEndRange = nodeRange.cloneRange();
1335 nodeEndRange.collapse(false);
1337 return rangeStartRange.compareBoundaryPoints(
1338 Range.START_TO_START, nodeEndRange) == -1 &&
1339 rangeEndRange.compareBoundaryPoints(
1340 Range.START_TO_START, nodeStartRange) == 1;
1344 rangeCompareNode : function(range, node)
1346 var nodeRange = node.ownerDocument.createRange();
1348 nodeRange.selectNode(node);
1350 nodeRange.selectNodeContents(node);
1354 range.collapse(true);
1356 nodeRange.collapse(true);
1358 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1359 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1361 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1363 var nodeIsBefore = ss == 1;
1364 var nodeIsAfter = ee == -1;
1366 if (nodeIsBefore && nodeIsAfter) {
1369 if (!nodeIsBefore && nodeIsAfter) {
1370 return 1; //right trailed.
1373 if (nodeIsBefore && !nodeIsAfter) {
1374 return 2; // left trailed.
1380 cleanWordChars : function(input) {// change the chars to hex code
1383 [ 8211, "–" ],
1384 [ 8212, "—" ],
1393 Roo.each(swapCodes, function(sw) {
1394 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1396 output = output.replace(swapper, sw[1]);
1406 cleanUpChild : function (node)
1409 new Roo.htmleditor.FilterComment({node : node});
1410 new Roo.htmleditor.FilterAttributes({
1412 attrib_black : this.ablack,
1413 attrib_clean : this.aclean,
1414 style_white : this.cwhite,
1415 style_black : this.cblack
1417 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1418 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1424 * Clean up MS wordisms...
1425 * @deprecated - use filter directly
1427 cleanWord : function(node)
1429 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1430 new Roo.htmleditor.FilterKeepChildren({node : node ? node : this.doc.body, tag : [ 'FONT', ':' ]} );
1437 * @deprecated - use filters
1439 cleanTableWidths : function(node)
1441 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1448 applyBlacklists : function()
1450 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1451 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1453 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1454 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1455 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1459 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1460 if (b.indexOf(tag) > -1) {
1463 this.white.push(tag);
1467 Roo.each(w, function(tag) {
1468 if (b.indexOf(tag) > -1) {
1471 if (this.white.indexOf(tag) > -1) {
1474 this.white.push(tag);
1479 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1480 if (w.indexOf(tag) > -1) {
1483 this.black.push(tag);
1487 Roo.each(b, function(tag) {
1488 if (w.indexOf(tag) > -1) {
1491 if (this.black.indexOf(tag) > -1) {
1494 this.black.push(tag);
1499 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1500 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1504 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1505 if (b.indexOf(tag) > -1) {
1508 this.cwhite.push(tag);
1512 Roo.each(w, function(tag) {
1513 if (b.indexOf(tag) > -1) {
1516 if (this.cwhite.indexOf(tag) > -1) {
1519 this.cwhite.push(tag);
1524 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1525 if (w.indexOf(tag) > -1) {
1528 this.cblack.push(tag);
1532 Roo.each(b, function(tag) {
1533 if (w.indexOf(tag) > -1) {
1536 if (this.cblack.indexOf(tag) > -1) {
1539 this.cblack.push(tag);
1544 setStylesheets : function(stylesheets)
1546 if(typeof(stylesheets) == 'string'){
1547 Roo.get(this.iframe.contentDocument.head).createChild({
1558 Roo.each(stylesheets, function(s) {
1563 Roo.get(_this.iframe.contentDocument.head).createChild({
1575 updateLanguage : function()
1577 if (!this.iframe || !this.iframe.contentDocument) {
1580 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1584 removeStylesheets : function()
1588 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1593 setStyle : function(style)
1595 Roo.get(this.iframe.contentDocument.head).createChild({
1604 // hide stuff that is not compatible
1622 * @cfg {String} fieldClass @hide
1625 * @cfg {String} focusClass @hide
1628 * @cfg {String} autoCreate @hide
1631 * @cfg {String} inputType @hide
1634 * @cfg {String} invalidClass @hide
1637 * @cfg {String} invalidText @hide
1640 * @cfg {String} msgFx @hide
1643 * @cfg {String} validateOnBlur @hide
1647 Roo.HtmlEditorCore.white = [
1648 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1650 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1651 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1652 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1653 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1654 'TABLE', 'UL', 'XMP',
1656 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1659 'DIR', 'MENU', 'OL', 'UL', 'DL',
1665 Roo.HtmlEditorCore.black = [
1666 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1668 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1669 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1670 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1671 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1672 //'FONT' // CLEAN LATER..
1673 'COLGROUP', 'COL' // messy tables.
1677 Roo.HtmlEditorCore.clean = [ // ?? needed???
1678 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1680 Roo.HtmlEditorCore.tag_remove = [
1685 Roo.HtmlEditorCore.ablack = [
1689 Roo.HtmlEditorCore.aclean = [
1690 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1694 Roo.HtmlEditorCore.pwhite= [
1695 'http', 'https', 'mailto'
1698 // white listed style attributes.
1699 Roo.HtmlEditorCore.cwhite= [
1700 // 'text-align', /// default is to allow most things..
1706 // black listed style attributes.
1707 Roo.HtmlEditorCore.cblack= [
1708 // 'font-size' -- this can be set by the project