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:/'+'/',
52 validationEvent : false,
56 sourceEditMode : false,
57 onFocus : Roo.emptyFn,
62 style:"width:500px;height:300px;",
67 initComponent : function(){
71 * Fires when the editor is fully initialized (including the iframe)
72 * @param {HtmlEditor} this
77 * Fires when the editor is first receives the focus. Any insertion must wait
78 * until after this event.
79 * @param {HtmlEditor} this
84 * Fires before the textarea is updated with content from the editor iframe. Return false
86 * @param {HtmlEditor} this
87 * @param {String} html
92 * Fires before the iframe editor is updated with content from the textarea. Return false
94 * @param {HtmlEditor} this
95 * @param {String} html
100 * Fires when the textarea is updated with content from the editor iframe.
101 * @param {HtmlEditor} this
102 * @param {String} html
107 * Fires when the iframe editor is updated with content from the textarea.
108 * @param {HtmlEditor} this
109 * @param {String} html
113 * @event editmodechange
114 * Fires when the editor switches edit modes
115 * @param {HtmlEditor} this
116 * @param {Boolean} sourceEdit True if source edit, false if standard editing.
118 editmodechange: true,
121 * Fires when on any editor (mouse up/down cursor movement etc.) - used for toolbar hooks.
122 * @param {HtmlEditor} this
129 * Protected method that will not generally be called directly. It
130 * is called when the editor creates its toolbar. Override this method if you need to
131 * add custom toolbar buttons.
132 * @param {HtmlEditor} editor
134 createToolbar : function(editor){
135 if (!editor.toolbars || !editor.toolbars.length) {
136 editor.toolbars = [ new Roo.form.HtmlEditor.ToolbarStandard() ]; // can be empty?
139 for (var i =0 ; i < editor.toolbars.length;i++) {
140 editor.toolbars[i] = Roo.factory(editor.toolbars[i], Roo.form.HtmlEditor);
141 editor.toolbars[i].init(editor);
148 * Protected method that will not generally be called directly. It
149 * is called when the editor initializes the iframe with HTML contents. Override this method if you
150 * want to change the initialization markup of the iframe (e.g. to add stylesheets).
152 getDocMarkup : function(){
153 return '<html><head><style type="text/css">body{border:0;margin:0;padding:3px;height:98%;cursor:text;}</style></head><body></body></html>';
157 onRender : function(ct, position){
158 Roo.form.HtmlEditor.superclass.onRender.call(this, ct, position);
159 this.el.dom.style.border = '0 none';
160 this.el.dom.setAttribute('tabIndex', -1);
161 this.el.addClass('x-hidden');
162 if(Roo.isIE){ // fix IE 1px bogus margin
163 this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
165 this.wrap = this.el.wrap({
166 cls:'x-html-editor-wrap', cn:{cls:'x-html-editor-tb'}
169 this.frameId = Roo.id();
170 this.createToolbar(this);
177 var iframe = this.wrap.createChild({
182 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
185 // console.log(iframe);
186 //this.wrap.dom.appendChild(iframe);
188 this.iframe = iframe.dom;
192 this.doc.designMode = 'on';
195 this.doc.write(this.getDocMarkup());
199 var task = { // must defer to wait for browser to be ready
201 //console.log("run task?" + this.doc.readyState);
203 if(this.doc.body || this.doc.readyState == 'complete'){
205 this.doc.designMode="on";
209 Roo.TaskMgr.stop(task);
210 this.initEditor.defer(10, this);
217 Roo.TaskMgr.start(task);
220 this.setSize(this.el.getSize());
225 onResize : function(w, h){
226 Roo.form.HtmlEditor.superclass.onResize.apply(this, arguments);
227 if(this.el && this.iframe){
228 if(typeof w == 'number'){
229 var aw = w - this.wrap.getFrameWidth('lr');
230 this.el.setWidth(this.adjustWidth('textarea', aw));
231 this.iframe.style.width = aw + 'px';
233 if(typeof h == 'number'){
235 for (var i =0; i < this.toolbars.length;i++) {
236 // fixme - ask toolbars for heights?
237 tbh += this.toolbars[i].tb.el.getHeight();
243 var ah = h - this.wrap.getFrameWidth('tb') - tbh;// this.tb.el.getHeight();
244 this.el.setHeight(this.adjustWidth('textarea', ah));
245 this.iframe.style.height = ah + 'px';
247 (this.doc.body || this.doc.documentElement).style.height = (ah - (this.iframePad*2)) + 'px';
254 * Toggles the editor between standard and source edit mode.
255 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
257 toggleSourceEdit : function(sourceEditMode){
259 this.sourceEditMode = sourceEditMode === true;
261 if(this.sourceEditMode){
264 this.iframe.className = 'x-hidden';
265 this.el.removeClass('x-hidden');
266 this.el.dom.removeAttribute('tabIndex');
271 this.iframe.className = '';
272 this.el.addClass('x-hidden');
273 this.el.dom.setAttribute('tabIndex', -1);
276 this.setSize(this.wrap.getSize());
277 this.fireEvent('editmodechange', this, this.sourceEditMode);
280 // private used internally
281 createLink : function(){
282 var url = prompt(this.createLinkText, this.defaultLinkValue);
283 if(url && url != 'http:/'+'/'){
284 this.relayCmd('createlink', url);
288 // private (for BoxComponent)
289 adjustSize : Roo.BoxComponent.prototype.adjustSize,
291 // private (for BoxComponent)
292 getResizeEl : function(){
296 // private (for BoxComponent)
297 getPositionEl : function(){
302 initEvents : function(){
303 this.originalValue = this.getValue();
307 * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide
310 markInvalid : Roo.emptyFn,
312 * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide
315 clearInvalid : Roo.emptyFn,
317 setValue : function(v){
318 Roo.form.HtmlEditor.superclass.setValue.call(this, v);
323 * Protected method that will not generally be called directly. If you need/want
324 * custom HTML cleanup, this is the method you should override.
325 * @param {String} html The HTML to be cleaned
326 * return {String} The cleaned HTML
328 cleanHtml : function(html){
331 if(Roo.isSafari){ // strip safari nonsense
332 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
335 if(html == ' '){
342 * Protected method that will not generally be called directly. Syncs the contents
343 * of the editor iframe with the textarea.
345 syncValue : function(){
346 if(this.initialized){
347 var bd = (this.doc.body || this.doc.documentElement);
348 var html = bd.innerHTML;
350 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
351 var m = bs.match(/text-align:(.*?);/i);
353 html = '<div style="'+m[0]+'">' + html + '</div>';
356 html = this.cleanHtml(html);
357 if(this.fireEvent('beforesync', this, html) !== false){
358 this.el.dom.value = html;
359 this.fireEvent('sync', this, html);
365 * Protected method that will not generally be called directly. Pushes the value of the textarea
366 * into the iframe editor.
368 pushValue : function(){
369 if(this.initialized){
370 var v = this.el.dom.value;
374 if(this.fireEvent('beforepush', this, v) !== false){
375 (this.doc.body || this.doc.documentElement).innerHTML = v;
376 this.fireEvent('push', this, v);
382 deferFocus : function(){
383 this.focus.defer(10, this);
388 if(this.win && !this.sourceEditMode){
395 assignDocWin: function()
397 var iframe = this.iframe;
400 this.doc = iframe.contentWindow.document;
401 this.win = iframe.contentWindow;
403 if (!Roo.get(this.frameId)) {
406 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
407 this.win = Roo.get(this.frameId).dom.contentWindow;
412 initEditor : function(){
413 //console.log("INIT EDITOR");
418 this.doc.designMode="on";
420 this.doc.write(this.getDocMarkup());
423 var dbody = (this.doc.body || this.doc.documentElement);
424 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
425 // this copies styles from the containing element into thsi one..
426 // not sure why we need all of this..
427 var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
428 ss['background-attachment'] = 'fixed'; // w3c
429 dbody.bgProperties = 'fixed'; // ie
430 Roo.DomHelper.applyStyles(dbody, ss);
431 Roo.EventManager.on(this.doc, {
432 'mousedown': this.onEditorEvent,
433 'dblclick': this.onEditorEvent,
434 'click': this.onEditorEvent,
435 'keyup': this.onEditorEvent,
440 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
442 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
443 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
445 this.initialized = true;
447 this.fireEvent('initialize', this);
452 onDestroy : function(){
458 for (var i =0; i < this.toolbars.length;i++) {
459 // fixme - ask toolbars for heights?
460 this.toolbars[i].onDestroy();
463 this.wrap.dom.innerHTML = '';
469 onFirstFocus : function(){
474 this.activated = true;
475 for (var i =0; i < this.toolbars.length;i++) {
476 this.toolbars[i].onFirstFocus();
479 if(Roo.isGecko){ // prevent silly gecko errors
481 var s = this.win.getSelection();
482 if(!s.focusNode || s.focusNode.nodeType != 3){
483 var r = s.getRangeAt(0);
484 r.selectNodeContents((this.doc.body || this.doc.documentElement));
489 this.execCmd('useCSS', true);
490 this.execCmd('styleWithCSS', false);
493 this.fireEvent('activate', this);
497 adjustFont: function(btn){
498 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
499 //if(Roo.isSafari){ // safari
502 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
503 if(Roo.isSafari){ // safari
504 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
505 v = (v < 10) ? 10 : v;
506 v = (v > 48) ? 48 : v;
507 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
512 v = Math.max(1, v+adjust);
514 this.execCmd('FontSize', v );
517 onEditorEvent : function(e){
518 this.fireEvent('editorevent', this, e);
519 // this.updateToolbar();
523 insertTag : function(tg)
525 // could be a bit smarter... -> wrap the current selected tRoo..
527 this.execCmd("formatblock", tg);
531 insertText : function(txt)
535 range = this.createRange();
536 range.deleteContents();
537 //alert(Sender.getAttribute('label'));
539 range.insertNode(this.doc.createTextNode(txt));
543 relayBtnCmd : function(btn){
544 this.relayCmd(btn.cmd);
548 * Executes a Midas editor command on the editor document and performs necessary focus and
549 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
550 * @param {String} cmd The Midas command
551 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
553 relayCmd : function(cmd, value){
555 this.execCmd(cmd, value);
556 this.fireEvent('editorevent', this);
557 //this.updateToolbar();
562 * Executes a Midas editor command directly on the editor document.
563 * For visual commands, you should use {@link #relayCmd} instead.
564 * <b>This should only be called after the editor is initialized.</b>
565 * @param {String} cmd The Midas command
566 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
568 execCmd : function(cmd, value){
569 this.doc.execCommand(cmd, false, value === undefined ? null : value);
575 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
577 * @param {String} text
579 insertAtCursor : function(text){
585 var r = this.doc.selection.createRange();
592 }else if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
594 this.execCmd('InsertHTML', text);
599 mozKeyPress : function(e){
601 var c = e.getCharCode(), cmd;
604 c = String.fromCharCode(c).toLowerCase();
615 this.cleanUpPaste.defer(100, this);
631 fixKeys : function(){ // load time branching for fastest keydown performance
634 var k = e.getKey(), r;
637 r = this.doc.selection.createRange();
640 r.pasteHTML('    ');
647 r = this.doc.selection.createRange();
649 var target = r.parentElement();
650 if(!target || target.tagName.toLowerCase() != 'li'){
652 r.pasteHTML('<br />');
658 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
659 this.cleanUpPaste.defer(100, this);
665 }else if(Roo.isOpera){
671 this.execCmd('InsertHTML','    ');
674 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
675 this.cleanUpPaste.defer(100, this);
680 }else if(Roo.isSafari){
686 this.execCmd('InsertText','\t');
690 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
691 this.cleanUpPaste.defer(100, this);
699 getAllAncestors: function()
701 var p = this.getSelectedNode();
704 a.push(p); // push blank onto stack..
705 p = this.getParentElement();
709 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
713 a.push(this.doc.body);
720 getSelection : function()
723 return Roo.isIE ? this.doc.selection : this.win.getSelection();
726 getSelectedNode: function()
728 // this may only work on Gecko!!!
730 // should we cache this!!!!
735 var range = this.createRange(this.getSelection());
738 var parent = range.parentElement();
740 var testRange = range.duplicate();
741 testRange.moveToElementText(parent);
742 if (testRange.inRange(range)) {
745 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
748 parent = parent.parentElement;
754 var ar = range.endContainer.childNodes;
756 ar = range.commonAncestorContainer.childNodes;
760 var other_nodes = [];
761 var has_other_nodes = false;
762 for (var i=0;i<ar.length;i++) {
763 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
766 // fullly contained node.
768 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
773 // probably selected..
774 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
775 other_nodes.push(ar[i]);
778 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
783 has_other_nodes = true;
785 if (!nodes.length && other_nodes.length) {
788 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
794 createRange: function(sel)
796 // this has strange effects when using with
797 // top toolbar - not sure if it's a great idea.
798 //this.editor.contentWindow.focus();
799 if (typeof sel != "undefined") {
801 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
803 return this.doc.createRange();
806 return this.doc.createRange();
809 getParentElement: function()
813 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
815 var range = this.createRange(sel);
818 var p = range.commonAncestorContainer;
819 while (p.nodeType == 3) { // text node
831 // BC Hacks - cause I cant work out what i was trying to do..
832 rangeIntersectsNode : function(range, node)
834 var nodeRange = node.ownerDocument.createRange();
836 nodeRange.selectNode(node);
839 nodeRange.selectNodeContents(node);
842 return range.compareBoundaryPoints(Range.END_TO_START, nodeRange) == -1 &&
843 range.compareBoundaryPoints(Range.START_TO_END, nodeRange) == 1;
845 rangeCompareNode : function(range, node) {
846 var nodeRange = node.ownerDocument.createRange();
848 nodeRange.selectNode(node);
850 nodeRange.selectNodeContents(node);
852 var nodeIsBefore = range.compareBoundaryPoints(Range.START_TO_START, nodeRange) == 1;
853 var nodeIsAfter = range.compareBoundaryPoints(Range.END_TO_END, nodeRange) == -1;
855 if (nodeIsBefore && !nodeIsAfter)
857 if (!nodeIsBefore && nodeIsAfter)
859 if (nodeIsBefore && nodeIsAfter)
865 // private? - in a new class?
866 cleanUpPaste : function()
868 // cleans up the whole document..
869 // console.log('cleanuppaste');
870 this.cleanUpChildren(this.doc.body)
874 cleanUpChildren : function (n)
876 if (!n.childNodes.length) {
879 for (var i = n.childNodes.length-1; i > -1 ; i--) {
880 this.cleanUpChild(n.childNodes[i]);
887 cleanUpChild : function (node)
890 if (node.nodeName == "#text") {
891 // clean up silly Windows -- stuff?
894 if (node.nodeName == "#comment") {
895 node.parentNode.removeChild(node);
896 // clean up silly Windows -- stuff?
900 if (Roo.form.HtmlEditor.black.indexOf(node.tagName.toLowerCase()) > -1) {
902 node.parentNode.removeChild(node);
906 if (!node.attributes || !node.attributes.length) {
907 this.cleanUpChildren(node);
911 function cleanAttr(n,v)
914 if (v.match(/^\./) || v.match(/^\//)) {
917 if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/)) {
920 Roo.log("(REMOVE)"+ node.tagName +'.' + n + '=' + v);
921 node.removeAttribute(n);
925 function cleanStyle(n,v)
927 if (v.match(/expression/)) { //XSS?? should we even bother..
928 node.removeAttribute(n);
933 var parts = v.split(/;/);
934 Roo.each(parts, function(p) {
935 p = p.replace(/\s+/g,'');
939 var l = p.split(':').shift().replace(/\s+/g,'');
941 if (Roo.form.HtmlEditor.cwhite.indexOf(l) < 0) {
942 Roo.log('(REMOVE)' + node.tagName +'.' + n + ':'+l + '=' + v);
943 node.removeAttribute(n);
952 for (var i = node.attributes.length-1; i > -1 ; i--) {
953 var a = node.attributes[i];
955 if (Roo.form.HtmlEditor.ablack.indexOf(a.name.toLowerCase()) > -1) {
956 node.removeAttribute(a.name);
959 if (Roo.form.HtmlEditor.aclean.indexOf(a.name.toLowerCase()) > -1) {
960 cleanAttr(a.name,a.value); // fixme..
963 if (a.name == 'style') {
964 cleanStyle(a.name,a.value);
966 /// clean up MS crap..
967 if (a.name == 'class') {
968 if (a.value.match(/^Mso/)) {
979 this.cleanUpChildren(node);
985 // hide stuff that is not compatible
1003 * @cfg {String} fieldClass @hide
1006 * @cfg {String} focusClass @hide
1009 * @cfg {String} autoCreate @hide
1012 * @cfg {String} inputType @hide
1015 * @cfg {String} invalidClass @hide
1018 * @cfg {String} invalidText @hide
1021 * @cfg {String} msgFx @hide
1024 * @cfg {String} validateOnBlur @hide
1028 Roo.form.HtmlEditor.white = [
1029 'area', 'br', 'img', 'input', 'hr', 'wbr',
1031 'address', 'blockquote', 'center', 'dd', 'dir', 'div',
1032 'dl', 'dt', 'h1', 'h2', 'h3', 'h4',
1033 'h5', 'h6', 'hr', 'isindex', 'listing', 'marquee',
1034 'menu', 'multicol', 'ol', 'p', 'plaintext', 'pre',
1035 'table', 'ul', 'xmp',
1037 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
1040 'dir', 'menu', 'ol', 'ul', 'dl',
1046 Roo.form.HtmlEditor.black = [
1047 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1049 'base', 'basefont', 'bgsound', 'blink', 'body',
1050 'frame', 'frameset', 'head', 'html', 'ilayer',
1051 'iframe', 'layer', 'link', 'meta', 'object',
1052 'script', 'style' ,'title', 'xml' // clean later..
1054 Roo.form.HtmlEditor.clean = [
1055 'script', 'style', 'title', 'xml'
1060 Roo.form.HtmlEditor.ablack = [
1064 Roo.form.HtmlEditor.aclean = [
1065 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1069 Roo.form.HtmlEditor.pwhite= [
1070 'http', 'https', 'mailto'
1073 Roo.form.HtmlEditor.cwhite= [