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.wrap.createChild({
218 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
222 // console.log(iframe);
223 //this.wrap.dom.appendChild(iframe);
225 this.iframe = iframe.dom;
229 this.doc.designMode = 'on';
232 this.doc.write(this.getDocMarkup());
236 var task = { // must defer to wait for browser to be ready
238 //console.log("run task?" + this.doc.readyState);
240 if(this.doc.body || this.doc.readyState == 'complete'){
242 this.doc.designMode="on";
246 Roo.TaskMgr.stop(task);
247 this.initEditor.defer(10, this);
254 Roo.TaskMgr.start(task);
257 this.setSize(this.wrap.getSize());
260 this.resizeEl.resizeTo.defer(100, this.resizeEl,[ this.width,this.height ] );
261 // should trigger onReize..
266 onResize : function(w, h)
268 //Roo.log('resize: ' +w + ',' + h );
269 Roo.form.HtmlEditor.superclass.onResize.apply(this, arguments);
270 if(this.el && this.iframe){
271 if(typeof w == 'number'){
272 var aw = w - this.wrap.getFrameWidth('lr');
273 this.el.setWidth(this.adjustWidth('textarea', aw));
274 this.iframe.style.width = aw + 'px';
276 if(typeof h == 'number'){
278 for (var i =0; i < this.toolbars.length;i++) {
279 // fixme - ask toolbars for heights?
280 tbh += this.toolbars[i].tb.el.getHeight();
281 if (this.toolbars[i].footer) {
282 tbh += this.toolbars[i].footer.el.getHeight();
289 var ah = h - this.wrap.getFrameWidth('tb') - tbh;// this.tb.el.getHeight();
290 ah -= 5; // knock a few pixes off for look..
291 this.el.setHeight(this.adjustWidth('textarea', ah));
292 this.iframe.style.height = ah + 'px';
294 (this.doc.body || this.doc.documentElement).style.height = (ah - (this.iframePad*2)) + 'px';
301 * Toggles the editor between standard and source edit mode.
302 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
304 toggleSourceEdit : function(sourceEditMode){
306 this.sourceEditMode = sourceEditMode === true;
308 if(this.sourceEditMode){
311 this.iframe.className = 'x-hidden';
312 this.el.removeClass('x-hidden');
313 this.el.dom.removeAttribute('tabIndex');
318 this.iframe.className = '';
319 this.el.addClass('x-hidden');
320 this.el.dom.setAttribute('tabIndex', -1);
323 this.setSize(this.wrap.getSize());
324 this.fireEvent('editmodechange', this, this.sourceEditMode);
327 // private used internally
328 createLink : function(){
329 var url = prompt(this.createLinkText, this.defaultLinkValue);
330 if(url && url != 'http:/'+'/'){
331 this.relayCmd('createlink', url);
335 // private (for BoxComponent)
336 adjustSize : Roo.BoxComponent.prototype.adjustSize,
338 // private (for BoxComponent)
339 getResizeEl : function(){
343 // private (for BoxComponent)
344 getPositionEl : function(){
349 initEvents : function(){
350 this.originalValue = this.getValue();
354 * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide
357 markInvalid : Roo.emptyFn,
359 * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide
362 clearInvalid : Roo.emptyFn,
364 setValue : function(v){
365 Roo.form.HtmlEditor.superclass.setValue.call(this, v);
370 * Protected method that will not generally be called directly. If you need/want
371 * custom HTML cleanup, this is the method you should override.
372 * @param {String} html The HTML to be cleaned
373 * return {String} The cleaned HTML
375 cleanHtml : function(html){
378 if(Roo.isSafari){ // strip safari nonsense
379 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
382 if(html == ' '){
389 * Protected method that will not generally be called directly. Syncs the contents
390 * of the editor iframe with the textarea.
392 syncValue : function(){
393 if(this.initialized){
394 var bd = (this.doc.body || this.doc.documentElement);
396 var html = bd.innerHTML;
398 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
399 var m = bs.match(/text-align:(.*?);/i);
401 html = '<div style="'+m[0]+'">' + html + '</div>';
404 html = this.cleanHtml(html);
405 if(this.fireEvent('beforesync', this, html) !== false){
406 this.el.dom.value = html;
407 this.fireEvent('sync', this, html);
413 * Protected method that will not generally be called directly. Pushes the value of the textarea
414 * into the iframe editor.
416 pushValue : function(){
417 if(this.initialized){
418 var v = this.el.dom.value;
423 if(this.fireEvent('beforepush', this, v) !== false){
424 var d = (this.doc.body || this.doc.documentElement);
427 this.el.dom.value = d.innerHTML;
428 this.fireEvent('push', this, v);
434 deferFocus : function(){
435 this.focus.defer(10, this);
440 if(this.win && !this.sourceEditMode){
447 assignDocWin: function()
449 var iframe = this.iframe;
452 this.doc = iframe.contentWindow.document;
453 this.win = iframe.contentWindow;
455 if (!Roo.get(this.frameId)) {
458 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
459 this.win = Roo.get(this.frameId).dom.contentWindow;
464 initEditor : function(){
465 //console.log("INIT EDITOR");
470 this.doc.designMode="on";
472 this.doc.write(this.getDocMarkup());
475 var dbody = (this.doc.body || this.doc.documentElement);
476 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
477 // this copies styles from the containing element into thsi one..
478 // not sure why we need all of this..
479 var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
480 ss['background-attachment'] = 'fixed'; // w3c
481 dbody.bgProperties = 'fixed'; // ie
482 Roo.DomHelper.applyStyles(dbody, ss);
483 Roo.EventManager.on(this.doc, {
484 //'mousedown': this.onEditorEvent,
485 'mouseup': this.onEditorEvent,
486 'dblclick': this.onEditorEvent,
487 'click': this.onEditorEvent,
488 'keyup': this.onEditorEvent,
493 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
495 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
496 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
498 this.initialized = true;
500 this.fireEvent('initialize', this);
505 onDestroy : function(){
511 for (var i =0; i < this.toolbars.length;i++) {
512 // fixme - ask toolbars for heights?
513 this.toolbars[i].onDestroy();
516 this.wrap.dom.innerHTML = '';
522 onFirstFocus : function(){
527 this.activated = true;
528 for (var i =0; i < this.toolbars.length;i++) {
529 this.toolbars[i].onFirstFocus();
532 if(Roo.isGecko){ // prevent silly gecko errors
534 var s = this.win.getSelection();
535 if(!s.focusNode || s.focusNode.nodeType != 3){
536 var r = s.getRangeAt(0);
537 r.selectNodeContents((this.doc.body || this.doc.documentElement));
542 this.execCmd('useCSS', true);
543 this.execCmd('styleWithCSS', false);
546 this.fireEvent('activate', this);
550 adjustFont: function(btn){
551 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
552 //if(Roo.isSafari){ // safari
555 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
556 if(Roo.isSafari){ // safari
557 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
558 v = (v < 10) ? 10 : v;
559 v = (v > 48) ? 48 : v;
560 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
565 v = Math.max(1, v+adjust);
567 this.execCmd('FontSize', v );
570 onEditorEvent : function(e){
571 this.fireEvent('editorevent', this, e);
572 // this.updateToolbar();
576 insertTag : function(tg)
578 // could be a bit smarter... -> wrap the current selected tRoo..
580 this.execCmd("formatblock", tg);
584 insertText : function(txt)
588 range = this.createRange();
589 range.deleteContents();
590 //alert(Sender.getAttribute('label'));
592 range.insertNode(this.doc.createTextNode(txt));
596 relayBtnCmd : function(btn){
597 this.relayCmd(btn.cmd);
601 * Executes a Midas editor command on the editor document and performs necessary focus and
602 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
603 * @param {String} cmd The Midas command
604 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
606 relayCmd : function(cmd, value){
608 this.execCmd(cmd, value);
609 this.fireEvent('editorevent', this);
610 //this.updateToolbar();
615 * Executes a Midas editor command directly on the editor document.
616 * For visual commands, you should use {@link #relayCmd} instead.
617 * <b>This should only be called after the editor is initialized.</b>
618 * @param {String} cmd The Midas command
619 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
621 execCmd : function(cmd, value){
622 this.doc.execCommand(cmd, false, value === undefined ? null : value);
628 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
630 * @param {String} text
632 insertAtCursor : function(text){
638 var r = this.doc.selection.createRange();
645 }else if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
647 this.execCmd('InsertHTML', text);
652 mozKeyPress : function(e){
654 var c = e.getCharCode(), cmd;
657 c = String.fromCharCode(c).toLowerCase();
668 this.cleanUpPaste.defer(100, this);
684 fixKeys : function(){ // load time branching for fastest keydown performance
687 var k = e.getKey(), r;
690 r = this.doc.selection.createRange();
693 r.pasteHTML('    ');
700 r = this.doc.selection.createRange();
702 var target = r.parentElement();
703 if(!target || target.tagName.toLowerCase() != 'li'){
705 r.pasteHTML('<br />');
711 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
712 this.cleanUpPaste.defer(100, this);
718 }else if(Roo.isOpera){
724 this.execCmd('InsertHTML','    ');
727 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
728 this.cleanUpPaste.defer(100, this);
733 }else if(Roo.isSafari){
739 this.execCmd('InsertText','\t');
743 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
744 this.cleanUpPaste.defer(100, this);
752 getAllAncestors: function()
754 var p = this.getSelectedNode();
757 a.push(p); // push blank onto stack..
758 p = this.getParentElement();
762 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
766 a.push(this.doc.body);
773 getSelection : function()
776 return Roo.isIE ? this.doc.selection : this.win.getSelection();
779 getSelectedNode: function()
781 // this may only work on Gecko!!!
783 // should we cache this!!!!
788 var range = this.createRange(this.getSelection());
791 var parent = range.parentElement();
793 var testRange = range.duplicate();
794 testRange.moveToElementText(parent);
795 if (testRange.inRange(range)) {
798 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
801 parent = parent.parentElement;
807 var ar = range.commonAncestorContainer.childNodes;
810 var other_nodes = [];
811 var has_other_nodes = false;
812 for (var i=0;i<ar.length;i++) {
813 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
816 // fullly contained node.
818 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
823 // probably selected..
824 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
825 other_nodes.push(ar[i]);
828 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
833 has_other_nodes = true;
835 if (!nodes.length && other_nodes.length) {
838 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
844 createRange: function(sel)
846 // this has strange effects when using with
847 // top toolbar - not sure if it's a great idea.
848 //this.editor.contentWindow.focus();
849 if (typeof sel != "undefined") {
851 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
853 return this.doc.createRange();
856 return this.doc.createRange();
859 getParentElement: function()
863 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
865 var range = this.createRange(sel);
868 var p = range.commonAncestorContainer;
869 while (p.nodeType == 3) { // text node
881 // BC Hacks - cause I cant work out what i was trying to do..
882 rangeIntersectsNode : function(range, node)
884 var nodeRange = node.ownerDocument.createRange();
886 nodeRange.selectNode(node);
889 nodeRange.selectNodeContents(node);
892 return range.compareBoundaryPoints(Range.END_TO_START, nodeRange) == -1 &&
893 range.compareBoundaryPoints(Range.START_TO_END, nodeRange) == 1;
895 rangeCompareNode : function(range, node)
897 var nodeRange = node.ownerDocument.createRange();
899 nodeRange.selectNode(node);
901 nodeRange.selectNodeContents(node);
903 var nodeIsBefore = range.compareBoundaryPoints(Range.START_TO_START, nodeRange) == 1;
904 var nodeIsAfter = range.compareBoundaryPoints(Range.END_TO_END, nodeRange) == -1;
906 if (nodeIsBefore && !nodeIsAfter)
908 if (!nodeIsBefore && nodeIsAfter)
910 if (nodeIsBefore && nodeIsAfter)
916 // private? - in a new class?
917 cleanUpPaste : function()
919 // cleans up the whole document..
920 // console.log('cleanuppaste');
921 this.cleanUpChildren(this.doc.body);
925 cleanUpChildren : function (n)
927 if (!n.childNodes.length) {
930 for (var i = n.childNodes.length-1; i > -1 ; i--) {
931 this.cleanUpChild(n.childNodes[i]);
938 cleanUpChild : function (node)
941 if (node.nodeName == "#text") {
942 // clean up silly Windows -- stuff?
945 if (node.nodeName == "#comment") {
946 node.parentNode.removeChild(node);
947 // clean up silly Windows -- stuff?
951 if (Roo.form.HtmlEditor.black.indexOf(node.tagName.toLowerCase()) > -1) {
953 node.parentNode.removeChild(node);
957 if (Roo.form.HtmlEditor.remove.indexOf(node.tagName.toLowerCase()) > -1) {
958 this.cleanUpChildren(node);
959 // inserts everything just before this node...
960 while (node.childNodes.length) {
961 var cn = node.childNodes[0];
962 node.removeChild(cn);
963 node.parentNode.insertBefore(cn, node);
965 node.parentNode.removeChild(node);
969 if (!node.attributes || !node.attributes.length) {
970 this.cleanUpChildren(node);
974 function cleanAttr(n,v)
977 if (v.match(/^\./) || v.match(/^\//)) {
980 if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/)) {
983 Roo.log("(REMOVE)"+ node.tagName +'.' + n + '=' + v);
984 node.removeAttribute(n);
988 function cleanStyle(n,v)
990 if (v.match(/expression/)) { //XSS?? should we even bother..
991 node.removeAttribute(n);
996 var parts = v.split(/;/);
997 Roo.each(parts, function(p) {
998 p = p.replace(/\s+/g,'');
1002 var l = p.split(':').shift().replace(/\s+/g,'');
1004 if (Roo.form.HtmlEditor.cwhite.indexOf(l) < 0) {
1005 Roo.log('(REMOVE)' + node.tagName +'.' + n + ':'+l + '=' + v);
1006 node.removeAttribute(n);
1016 for (var i = node.attributes.length-1; i > -1 ; i--) {
1017 var a = node.attributes[i];
1019 if (Roo.form.HtmlEditor.ablack.indexOf(a.name.toLowerCase()) > -1) {
1020 node.removeAttribute(a.name);
1023 if (Roo.form.HtmlEditor.aclean.indexOf(a.name.toLowerCase()) > -1) {
1024 cleanAttr(a.name,a.value); // fixme..
1027 if (a.name == 'style') {
1028 cleanStyle(a.name,a.value);
1030 /// clean up MS crap..
1031 if (a.name == 'class') {
1032 if (a.value.match(/^Mso/)) {
1033 node.className = '';
1043 this.cleanUpChildren(node);
1049 // hide stuff that is not compatible
1067 * @cfg {String} fieldClass @hide
1070 * @cfg {String} focusClass @hide
1073 * @cfg {String} autoCreate @hide
1076 * @cfg {String} inputType @hide
1079 * @cfg {String} invalidClass @hide
1082 * @cfg {String} invalidText @hide
1085 * @cfg {String} msgFx @hide
1088 * @cfg {String} validateOnBlur @hide
1092 Roo.form.HtmlEditor.white = [
1093 'area', 'br', 'img', 'input', 'hr', 'wbr',
1095 'address', 'blockquote', 'center', 'dd', 'dir', 'div',
1096 'dl', 'dt', 'h1', 'h2', 'h3', 'h4',
1097 'h5', 'h6', 'hr', 'isindex', 'listing', 'marquee',
1098 'menu', 'multicol', 'ol', 'p', 'plaintext', 'pre',
1099 'table', 'ul', 'xmp',
1101 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
1104 'dir', 'menu', 'ol', 'ul', 'dl',
1110 Roo.form.HtmlEditor.black = [
1111 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1113 'base', 'basefont', 'bgsound', 'blink', 'body',
1114 'frame', 'frameset', 'head', 'html', 'ilayer',
1115 'iframe', 'layer', 'link', 'meta', 'object',
1116 'script', 'style' ,'title', 'xml' // clean later..
1118 Roo.form.HtmlEditor.clean = [
1119 'script', 'style', 'title', 'xml'
1121 Roo.form.HtmlEditor.remove = [
1126 Roo.form.HtmlEditor.ablack = [
1130 Roo.form.HtmlEditor.aclean = [
1131 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1135 Roo.form.HtmlEditor.pwhite= [
1136 'http', 'https', 'mailto'
1139 Roo.form.HtmlEditor.cwhite= [