1 //<script type="text/javascript">
5 * Copyright(c) 2006-2007, Ext JS, LLC.
8 * http://www.extjs.com/license
14 * Default CSS appears to render it as fixed text by default (should really be Sans-Serif)
15 * - IE ? - no idea how much works there.
23 * @class Ext.form.HtmlEditor
24 * @extends Ext.form.Field
25 * Provides a lightweight HTML Editor component.
26 * WARNING - THIS CURRENTlY ONLY WORKS ON FIREFOX - USE FCKeditor for a cross platform version
28 * <br><br><b>Note: The focus/blur and validation marking functionality inherited from Ext.form.Field is NOT
29 * supported by this editor.</b><br/><br/>
30 * An Editor is a sensitive component that can't be used in all spots standard fields can be used. Putting an Editor within
31 * any element that has display set to 'none' can cause problems in Safari and Firefox.<br/><br/>
33 Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
35 * @cfg {Array} toolbars Array of toolbars. - defaults to just the Standard one
39 * @cfg {String} createLinkText The default text for the create link prompt
41 createLinkText : 'Please enter the URL for the link:',
43 * @cfg {String} defaultLinkValue The default value for the create link prompt (defaults to http:/ /)
45 defaultLinkValue : 'http:/'+'/',
48 * @cfg {String} resizable 's' or 'se' or 'e' - wrapps the element in a
53 * @cfg {Number} height (in pixels)
57 * @cfg {Number} width (in pixels)
64 validationEvent : false,
68 sourceEditMode : false,
69 onFocus : Roo.emptyFn,
73 defaultAutoCreate : { // modified by initCompnoent..
75 style:"width:500px;height:300px;",
80 initComponent : function(){
84 * Fires when the editor is fully initialized (including the iframe)
85 * @param {HtmlEditor} this
90 * Fires when the editor is first receives the focus. Any insertion must wait
91 * until after this event.
92 * @param {HtmlEditor} this
97 * Fires before the textarea is updated with content from the editor iframe. Return false
99 * @param {HtmlEditor} this
100 * @param {String} html
105 * Fires before the iframe editor is updated with content from the textarea. Return false
106 * to cancel the push.
107 * @param {HtmlEditor} this
108 * @param {String} html
113 * Fires when the textarea is updated with content from the editor iframe.
114 * @param {HtmlEditor} this
115 * @param {String} html
120 * Fires when the iframe editor is updated with content from the textarea.
121 * @param {HtmlEditor} this
122 * @param {String} html
126 * @event editmodechange
127 * Fires when the editor switches edit modes
128 * @param {HtmlEditor} this
129 * @param {Boolean} sourceEdit True if source edit, false if standard editing.
131 editmodechange: true,
134 * Fires when on any editor (mouse up/down cursor movement etc.) - used for toolbar hooks.
135 * @param {HtmlEditor} this
139 this.defaultAutoCreate = {
141 style:'width: ' + this.width + 'px;height: ' + this.height + 'px;',
147 * Protected method that will not generally be called directly. It
148 * is called when the editor creates its toolbar. Override this method if you need to
149 * add custom toolbar buttons.
150 * @param {HtmlEditor} editor
152 createToolbar : function(editor){
153 if (!editor.toolbars || !editor.toolbars.length) {
154 editor.toolbars = [ new Roo.form.HtmlEditor.ToolbarStandard() ]; // can be empty?
157 for (var i =0 ; i < editor.toolbars.length;i++) {
158 editor.toolbars[i] = Roo.factory(editor.toolbars[i], Roo.form.HtmlEditor);
159 editor.toolbars[i].init(editor);
166 * Protected method that will not generally be called directly. It
167 * is called when the editor initializes the iframe with HTML contents. Override this method if you
168 * want to change the initialization markup of the iframe (e.g. to add stylesheets).
170 getDocMarkup : function(){
171 return '<html><head><style type="text/css">body{border:0;margin:0;padding:3px;height:98%;cursor:text;}</style></head><body></body></html>';
175 onRender : function(ct, position)
178 Roo.form.HtmlEditor.superclass.onRender.call(this, ct, position);
179 this.el.dom.style.border = '0 none';
180 this.el.dom.setAttribute('tabIndex', -1);
181 this.el.addClass('x-hidden');
182 if(Roo.isIE){ // fix IE 1px bogus margin
183 this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
185 this.wrap = this.el.wrap({
186 cls:'x-html-editor-wrap', cn:{cls:'x-html-editor-tb'}
189 if (this.resizable) {
190 this.resizeEl = new Roo.Resizable(this.wrap, {
194 minHeight : this.height,
196 handles : this.resizable,
199 resize : function(r, w, h) {
200 _t.onResize(w,h); // -something
207 this.frameId = Roo.id();
209 this.createToolbar(this);
213 var iframe = this.el.insertAfter({
218 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
221 // console.log(iframe);
222 //this.wrap.dom.appendChild(iframe);
224 this.iframe = iframe.dom;
228 this.doc.designMode = 'on';
231 this.doc.write(this.getDocMarkup());
235 var task = { // must defer to wait for browser to be ready
237 //console.log("run task?" + this.doc.readyState);
239 if(this.doc.body || this.doc.readyState == 'complete'){
241 this.doc.designMode="on";
245 Roo.TaskMgr.stop(task);
246 this.initEditor.defer(10, this);
253 Roo.TaskMgr.start(task);
256 this.setSize(this.wrap.getSize());
259 this.resizeEl.resizeTo.defer(100, this.resizeEl,[ this.width,this.height ] );
260 // should trigger onReize..
265 onResize : function(w, h)
267 //Roo.log('resize: ' +w + ',' + h );
268 Roo.form.HtmlEditor.superclass.onResize.apply(this, arguments);
269 if(this.el && this.iframe){
270 if(typeof w == 'number'){
271 var aw = w - this.wrap.getFrameWidth('lr');
272 this.el.setWidth(this.adjustWidth('textarea', aw));
273 this.iframe.style.width = aw + 'px';
275 if(typeof h == 'number'){
277 for (var i =0; i < this.toolbars.length;i++) {
278 // fixme - ask toolbars for heights?
279 tbh += this.toolbars[i].tb.el.getHeight();
285 var ah = h - this.wrap.getFrameWidth('tb') - tbh;// this.tb.el.getHeight();
286 ah -= 10; // knock a few pixes off for look..
287 this.el.setHeight(this.adjustWidth('textarea', ah));
288 this.iframe.style.height = ah + 'px';
290 (this.doc.body || this.doc.documentElement).style.height = (ah - (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){
307 this.iframe.className = 'x-hidden';
308 this.el.removeClass('x-hidden');
309 this.el.dom.removeAttribute('tabIndex');
314 this.iframe.className = '';
315 this.el.addClass('x-hidden');
316 this.el.dom.setAttribute('tabIndex', -1);
319 this.setSize(this.wrap.getSize());
320 this.fireEvent('editmodechange', this, this.sourceEditMode);
323 // private used internally
324 createLink : function(){
325 var url = prompt(this.createLinkText, this.defaultLinkValue);
326 if(url && url != 'http:/'+'/'){
327 this.relayCmd('createlink', url);
331 // private (for BoxComponent)
332 adjustSize : Roo.BoxComponent.prototype.adjustSize,
334 // private (for BoxComponent)
335 getResizeEl : function(){
339 // private (for BoxComponent)
340 getPositionEl : function(){
345 initEvents : function(){
346 this.originalValue = this.getValue();
350 * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide
353 markInvalid : Roo.emptyFn,
355 * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide
358 clearInvalid : Roo.emptyFn,
360 setValue : function(v){
361 Roo.form.HtmlEditor.superclass.setValue.call(this, v);
366 * Protected method that will not generally be called directly. If you need/want
367 * custom HTML cleanup, this is the method you should override.
368 * @param {String} html The HTML to be cleaned
369 * return {String} The cleaned HTML
371 cleanHtml : function(html){
374 if(Roo.isSafari){ // strip safari nonsense
375 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
378 if(html == ' '){
385 * Protected method that will not generally be called directly. Syncs the contents
386 * of the editor iframe with the textarea.
388 syncValue : function(){
389 if(this.initialized){
390 var bd = (this.doc.body || this.doc.documentElement);
392 var html = bd.innerHTML;
394 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
395 var m = bs.match(/text-align:(.*?);/i);
397 html = '<div style="'+m[0]+'">' + html + '</div>';
400 html = this.cleanHtml(html);
401 if(this.fireEvent('beforesync', this, html) !== false){
402 this.el.dom.value = html;
403 this.fireEvent('sync', this, html);
409 * Protected method that will not generally be called directly. Pushes the value of the textarea
410 * into the iframe editor.
412 pushValue : function(){
413 if(this.initialized){
414 var v = this.el.dom.value;
419 if(this.fireEvent('beforepush', this, v) !== false){
420 var d = (this.doc.body || this.doc.documentElement);
423 this.el.dom.value = d.innerHTML;
424 this.fireEvent('push', this, v);
430 deferFocus : function(){
431 this.focus.defer(10, this);
436 if(this.win && !this.sourceEditMode){
443 assignDocWin: function()
445 var iframe = this.iframe;
448 this.doc = iframe.contentWindow.document;
449 this.win = iframe.contentWindow;
451 if (!Roo.get(this.frameId)) {
454 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
455 this.win = Roo.get(this.frameId).dom.contentWindow;
460 initEditor : function(){
461 //console.log("INIT EDITOR");
466 this.doc.designMode="on";
468 this.doc.write(this.getDocMarkup());
471 var dbody = (this.doc.body || this.doc.documentElement);
472 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
473 // this copies styles from the containing element into thsi one..
474 // not sure why we need all of this..
475 var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
476 ss['background-attachment'] = 'fixed'; // w3c
477 dbody.bgProperties = 'fixed'; // ie
478 Roo.DomHelper.applyStyles(dbody, ss);
479 Roo.EventManager.on(this.doc, {
480 'mousedown': this.onEditorEvent,
481 'dblclick': this.onEditorEvent,
482 'click': this.onEditorEvent,
483 'keyup': this.onEditorEvent,
488 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
490 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
491 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
493 this.initialized = true;
495 this.fireEvent('initialize', this);
500 onDestroy : function(){
506 for (var i =0; i < this.toolbars.length;i++) {
507 // fixme - ask toolbars for heights?
508 this.toolbars[i].onDestroy();
511 this.wrap.dom.innerHTML = '';
517 onFirstFocus : function(){
522 this.activated = true;
523 for (var i =0; i < this.toolbars.length;i++) {
524 this.toolbars[i].onFirstFocus();
527 if(Roo.isGecko){ // prevent silly gecko errors
529 var s = this.win.getSelection();
530 if(!s.focusNode || s.focusNode.nodeType != 3){
531 var r = s.getRangeAt(0);
532 r.selectNodeContents((this.doc.body || this.doc.documentElement));
537 this.execCmd('useCSS', true);
538 this.execCmd('styleWithCSS', false);
541 this.fireEvent('activate', this);
545 adjustFont: function(btn){
546 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
547 //if(Roo.isSafari){ // safari
550 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
551 if(Roo.isSafari){ // safari
552 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
553 v = (v < 10) ? 10 : v;
554 v = (v > 48) ? 48 : v;
555 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
560 v = Math.max(1, v+adjust);
562 this.execCmd('FontSize', v );
565 onEditorEvent : function(e){
566 this.fireEvent('editorevent', this, e);
567 // this.updateToolbar();
571 insertTag : function(tg)
573 // could be a bit smarter... -> wrap the current selected tRoo..
575 this.execCmd("formatblock", tg);
579 insertText : function(txt)
583 range = this.createRange();
584 range.deleteContents();
585 //alert(Sender.getAttribute('label'));
587 range.insertNode(this.doc.createTextNode(txt));
591 relayBtnCmd : function(btn){
592 this.relayCmd(btn.cmd);
596 * Executes a Midas editor command on the editor document and performs necessary focus and
597 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
598 * @param {String} cmd The Midas command
599 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
601 relayCmd : function(cmd, value){
603 this.execCmd(cmd, value);
604 this.fireEvent('editorevent', this);
605 //this.updateToolbar();
610 * Executes a Midas editor command directly on the editor document.
611 * For visual commands, you should use {@link #relayCmd} instead.
612 * <b>This should only be called after the editor is initialized.</b>
613 * @param {String} cmd The Midas command
614 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
616 execCmd : function(cmd, value){
617 this.doc.execCommand(cmd, false, value === undefined ? null : value);
623 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
625 * @param {String} text
627 insertAtCursor : function(text){
633 var r = this.doc.selection.createRange();
640 }else if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
642 this.execCmd('InsertHTML', text);
647 mozKeyPress : function(e){
649 var c = e.getCharCode(), cmd;
652 c = String.fromCharCode(c).toLowerCase();
663 this.cleanUpPaste.defer(100, this);
679 fixKeys : function(){ // load time branching for fastest keydown performance
682 var k = e.getKey(), r;
685 r = this.doc.selection.createRange();
688 r.pasteHTML('    ');
695 r = this.doc.selection.createRange();
697 var target = r.parentElement();
698 if(!target || target.tagName.toLowerCase() != 'li'){
700 r.pasteHTML('<br />');
706 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
707 this.cleanUpPaste.defer(100, this);
713 }else if(Roo.isOpera){
719 this.execCmd('InsertHTML','    ');
722 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
723 this.cleanUpPaste.defer(100, this);
728 }else if(Roo.isSafari){
734 this.execCmd('InsertText','\t');
738 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
739 this.cleanUpPaste.defer(100, this);
747 getAllAncestors: function()
749 var p = this.getSelectedNode();
752 a.push(p); // push blank onto stack..
753 p = this.getParentElement();
757 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
761 a.push(this.doc.body);
768 getSelection : function()
771 return Roo.isIE ? this.doc.selection : this.win.getSelection();
774 getSelectedNode: function()
776 // this may only work on Gecko!!!
778 // should we cache this!!!!
783 var range = this.createRange(this.getSelection());
786 var parent = range.parentElement();
788 var testRange = range.duplicate();
789 testRange.moveToElementText(parent);
790 if (testRange.inRange(range)) {
793 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
796 parent = parent.parentElement;
802 var ar = range.endContainer.childNodes;
804 ar = range.commonAncestorContainer.childNodes;
808 var other_nodes = [];
809 var has_other_nodes = false;
810 for (var i=0;i<ar.length;i++) {
811 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
814 // fullly contained node.
816 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
821 // probably selected..
822 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
823 other_nodes.push(ar[i]);
826 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
831 has_other_nodes = true;
833 if (!nodes.length && other_nodes.length) {
836 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
842 createRange: function(sel)
844 // this has strange effects when using with
845 // top toolbar - not sure if it's a great idea.
846 //this.editor.contentWindow.focus();
847 if (typeof sel != "undefined") {
849 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
851 return this.doc.createRange();
854 return this.doc.createRange();
857 getParentElement: function()
861 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
863 var range = this.createRange(sel);
866 var p = range.commonAncestorContainer;
867 while (p.nodeType == 3) { // text node
879 // BC Hacks - cause I cant work out what i was trying to do..
880 rangeIntersectsNode : function(range, node)
882 var nodeRange = node.ownerDocument.createRange();
884 nodeRange.selectNode(node);
887 nodeRange.selectNodeContents(node);
890 return range.compareBoundaryPoints(Range.END_TO_START, nodeRange) == -1 &&
891 range.compareBoundaryPoints(Range.START_TO_END, nodeRange) == 1;
893 rangeCompareNode : function(range, node) {
894 var nodeRange = node.ownerDocument.createRange();
896 nodeRange.selectNode(node);
898 nodeRange.selectNodeContents(node);
900 var nodeIsBefore = range.compareBoundaryPoints(Range.START_TO_START, nodeRange) == 1;
901 var nodeIsAfter = range.compareBoundaryPoints(Range.END_TO_END, nodeRange) == -1;
903 if (nodeIsBefore && !nodeIsAfter)
905 if (!nodeIsBefore && nodeIsAfter)
907 if (nodeIsBefore && nodeIsAfter)
913 // private? - in a new class?
914 cleanUpPaste : function()
916 // cleans up the whole document..
917 // console.log('cleanuppaste');
918 this.cleanUpChildren(this.doc.body);
922 cleanUpChildren : function (n)
924 if (!n.childNodes.length) {
927 for (var i = n.childNodes.length-1; i > -1 ; i--) {
928 this.cleanUpChild(n.childNodes[i]);
935 cleanUpChild : function (node)
938 if (node.nodeName == "#text") {
939 // clean up silly Windows -- stuff?
942 if (node.nodeName == "#comment") {
943 node.parentNode.removeChild(node);
944 // clean up silly Windows -- stuff?
948 if (Roo.form.HtmlEditor.black.indexOf(node.tagName.toLowerCase()) > -1) {
950 node.parentNode.removeChild(node);
954 if (Roo.form.HtmlEditor.remove.indexOf(node.tagName.toLowerCase()) > -1) {
955 this.cleanUpChildren(node);
956 // inserts everything just before this node...
957 while (node.childNodes.length) {
958 var cn = node.childNodes[0];
959 node.removeChild(cn);
960 node.parentNode.insertBefore(cn, node);
962 node.parentNode.removeChild(node);
966 if (!node.attributes || !node.attributes.length) {
967 this.cleanUpChildren(node);
971 function cleanAttr(n,v)
974 if (v.match(/^\./) || v.match(/^\//)) {
977 if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/)) {
980 Roo.log("(REMOVE)"+ node.tagName +'.' + n + '=' + v);
981 node.removeAttribute(n);
985 function cleanStyle(n,v)
987 if (v.match(/expression/)) { //XSS?? should we even bother..
988 node.removeAttribute(n);
993 var parts = v.split(/;/);
994 Roo.each(parts, function(p) {
995 p = p.replace(/\s+/g,'');
999 var l = p.split(':').shift().replace(/\s+/g,'');
1001 if (Roo.form.HtmlEditor.cwhite.indexOf(l) < 0) {
1002 Roo.log('(REMOVE)' + node.tagName +'.' + n + ':'+l + '=' + v);
1003 node.removeAttribute(n);
1013 for (var i = node.attributes.length-1; i > -1 ; i--) {
1014 var a = node.attributes[i];
1016 if (Roo.form.HtmlEditor.ablack.indexOf(a.name.toLowerCase()) > -1) {
1017 node.removeAttribute(a.name);
1020 if (Roo.form.HtmlEditor.aclean.indexOf(a.name.toLowerCase()) > -1) {
1021 cleanAttr(a.name,a.value); // fixme..
1024 if (a.name == 'style') {
1025 cleanStyle(a.name,a.value);
1027 /// clean up MS crap..
1028 if (a.name == 'class') {
1029 if (a.value.match(/^Mso/)) {
1030 node.className = '';
1040 this.cleanUpChildren(node);
1046 // hide stuff that is not compatible
1064 * @cfg {String} fieldClass @hide
1067 * @cfg {String} focusClass @hide
1070 * @cfg {String} autoCreate @hide
1073 * @cfg {String} inputType @hide
1076 * @cfg {String} invalidClass @hide
1079 * @cfg {String} invalidText @hide
1082 * @cfg {String} msgFx @hide
1085 * @cfg {String} validateOnBlur @hide
1089 Roo.form.HtmlEditor.white = [
1090 'area', 'br', 'img', 'input', 'hr', 'wbr',
1092 'address', 'blockquote', 'center', 'dd', 'dir', 'div',
1093 'dl', 'dt', 'h1', 'h2', 'h3', 'h4',
1094 'h5', 'h6', 'hr', 'isindex', 'listing', 'marquee',
1095 'menu', 'multicol', 'ol', 'p', 'plaintext', 'pre',
1096 'table', 'ul', 'xmp',
1098 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
1101 'dir', 'menu', 'ol', 'ul', 'dl',
1107 Roo.form.HtmlEditor.black = [
1108 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1110 'base', 'basefont', 'bgsound', 'blink', 'body',
1111 'frame', 'frameset', 'head', 'html', 'ilayer',
1112 'iframe', 'layer', 'link', 'meta', 'object',
1113 'script', 'style' ,'title', 'xml' // clean later..
1115 Roo.form.HtmlEditor.clean = [
1116 'script', 'style', 'title', 'xml'
1118 Roo.form.HtmlEditor.remove = [
1123 Roo.form.HtmlEditor.ablack = [
1127 Roo.form.HtmlEditor.aclean = [
1128 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1132 Roo.form.HtmlEditor.pwhite= [
1133 'http', 'https', 'mailto'
1136 Roo.form.HtmlEditor.cwhite= [