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();
208 this.createToolbar(this);
215 var iframe = this.wrap.createChild({
220 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
223 // console.log(iframe);
224 //this.wrap.dom.appendChild(iframe);
226 this.iframe = iframe.dom;
230 this.doc.designMode = 'on';
233 this.doc.write(this.getDocMarkup());
237 var task = { // must defer to wait for browser to be ready
239 //console.log("run task?" + this.doc.readyState);
241 if(this.doc.body || this.doc.readyState == 'complete'){
243 this.doc.designMode="on";
247 Roo.TaskMgr.stop(task);
248 this.initEditor.defer(10, this);
255 Roo.TaskMgr.start(task);
258 this.setSize(this.wrap.getSize());
261 this.resizeEl.resizeTo.defer(100, this.resizeEl,[ this.width,this.height ] );
262 // should trigger onReize..
267 onResize : function(w, h)
269 //Roo.log('resize: ' +w + ',' + h );
270 Roo.form.HtmlEditor.superclass.onResize.apply(this, arguments);
271 if(this.el && this.iframe){
272 if(typeof w == 'number'){
273 var aw = w - this.wrap.getFrameWidth('lr');
274 this.el.setWidth(this.adjustWidth('textarea', aw));
275 this.iframe.style.width = aw + 'px';
277 if(typeof h == 'number'){
279 for (var i =0; i < this.toolbars.length;i++) {
280 // fixme - ask toolbars for heights?
281 tbh += this.toolbars[i].tb.el.getHeight();
287 var ah = h - this.wrap.getFrameWidth('tb') - tbh;// this.tb.el.getHeight();
288 ah -= 10; // knock a few pixes off for look..
289 this.el.setHeight(this.adjustWidth('textarea', ah));
290 this.iframe.style.height = ah + 'px';
292 (this.doc.body || this.doc.documentElement).style.height = (ah - (this.iframePad*2)) + 'px';
299 * Toggles the editor between standard and source edit mode.
300 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
302 toggleSourceEdit : function(sourceEditMode){
304 this.sourceEditMode = sourceEditMode === true;
306 if(this.sourceEditMode){
309 this.iframe.className = 'x-hidden';
310 this.el.removeClass('x-hidden');
311 this.el.dom.removeAttribute('tabIndex');
316 this.iframe.className = '';
317 this.el.addClass('x-hidden');
318 this.el.dom.setAttribute('tabIndex', -1);
321 this.setSize(this.wrap.getSize());
322 this.fireEvent('editmodechange', this, this.sourceEditMode);
325 // private used internally
326 createLink : function(){
327 var url = prompt(this.createLinkText, this.defaultLinkValue);
328 if(url && url != 'http:/'+'/'){
329 this.relayCmd('createlink', url);
333 // private (for BoxComponent)
334 adjustSize : Roo.BoxComponent.prototype.adjustSize,
336 // private (for BoxComponent)
337 getResizeEl : function(){
341 // private (for BoxComponent)
342 getPositionEl : function(){
347 initEvents : function(){
348 this.originalValue = this.getValue();
352 * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide
355 markInvalid : Roo.emptyFn,
357 * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide
360 clearInvalid : Roo.emptyFn,
362 setValue : function(v){
363 Roo.form.HtmlEditor.superclass.setValue.call(this, v);
368 * Protected method that will not generally be called directly. If you need/want
369 * custom HTML cleanup, this is the method you should override.
370 * @param {String} html The HTML to be cleaned
371 * return {String} The cleaned HTML
373 cleanHtml : function(html){
376 if(Roo.isSafari){ // strip safari nonsense
377 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
380 if(html == ' '){
387 * Protected method that will not generally be called directly. Syncs the contents
388 * of the editor iframe with the textarea.
390 syncValue : function(){
391 if(this.initialized){
392 var bd = (this.doc.body || this.doc.documentElement);
394 var html = bd.innerHTML;
396 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
397 var m = bs.match(/text-align:(.*?);/i);
399 html = '<div style="'+m[0]+'">' + html + '</div>';
402 html = this.cleanHtml(html);
403 if(this.fireEvent('beforesync', this, html) !== false){
404 this.el.dom.value = html;
405 this.fireEvent('sync', this, html);
411 * Protected method that will not generally be called directly. Pushes the value of the textarea
412 * into the iframe editor.
414 pushValue : function(){
415 if(this.initialized){
416 var v = this.el.dom.value;
421 if(this.fireEvent('beforepush', this, v) !== false){
422 var d = (this.doc.body || this.doc.documentElement);
425 this.el.dom.value = d.innerHTML;
426 this.fireEvent('push', this, v);
432 deferFocus : function(){
433 this.focus.defer(10, this);
438 if(this.win && !this.sourceEditMode){
445 assignDocWin: function()
447 var iframe = this.iframe;
450 this.doc = iframe.contentWindow.document;
451 this.win = iframe.contentWindow;
453 if (!Roo.get(this.frameId)) {
456 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
457 this.win = Roo.get(this.frameId).dom.contentWindow;
462 initEditor : function(){
463 //console.log("INIT EDITOR");
468 this.doc.designMode="on";
470 this.doc.write(this.getDocMarkup());
473 var dbody = (this.doc.body || this.doc.documentElement);
474 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
475 // this copies styles from the containing element into thsi one..
476 // not sure why we need all of this..
477 var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
478 ss['background-attachment'] = 'fixed'; // w3c
479 dbody.bgProperties = 'fixed'; // ie
480 Roo.DomHelper.applyStyles(dbody, ss);
481 Roo.EventManager.on(this.doc, {
482 'mousedown': this.onEditorEvent,
483 'dblclick': this.onEditorEvent,
484 'click': this.onEditorEvent,
485 'keyup': this.onEditorEvent,
490 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
492 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
493 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
495 this.initialized = true;
497 this.fireEvent('initialize', this);
502 onDestroy : function(){
508 for (var i =0; i < this.toolbars.length;i++) {
509 // fixme - ask toolbars for heights?
510 this.toolbars[i].onDestroy();
513 this.wrap.dom.innerHTML = '';
519 onFirstFocus : function(){
524 this.activated = true;
525 for (var i =0; i < this.toolbars.length;i++) {
526 this.toolbars[i].onFirstFocus();
529 if(Roo.isGecko){ // prevent silly gecko errors
531 var s = this.win.getSelection();
532 if(!s.focusNode || s.focusNode.nodeType != 3){
533 var r = s.getRangeAt(0);
534 r.selectNodeContents((this.doc.body || this.doc.documentElement));
539 this.execCmd('useCSS', true);
540 this.execCmd('styleWithCSS', false);
543 this.fireEvent('activate', this);
547 adjustFont: function(btn){
548 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
549 //if(Roo.isSafari){ // safari
552 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
553 if(Roo.isSafari){ // safari
554 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
555 v = (v < 10) ? 10 : v;
556 v = (v > 48) ? 48 : v;
557 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
562 v = Math.max(1, v+adjust);
564 this.execCmd('FontSize', v );
567 onEditorEvent : function(e){
568 this.fireEvent('editorevent', this, e);
569 // this.updateToolbar();
573 insertTag : function(tg)
575 // could be a bit smarter... -> wrap the current selected tRoo..
577 this.execCmd("formatblock", tg);
581 insertText : function(txt)
585 range = this.createRange();
586 range.deleteContents();
587 //alert(Sender.getAttribute('label'));
589 range.insertNode(this.doc.createTextNode(txt));
593 relayBtnCmd : function(btn){
594 this.relayCmd(btn.cmd);
598 * Executes a Midas editor command on the editor document and performs necessary focus and
599 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
600 * @param {String} cmd The Midas command
601 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
603 relayCmd : function(cmd, value){
605 this.execCmd(cmd, value);
606 this.fireEvent('editorevent', this);
607 //this.updateToolbar();
612 * Executes a Midas editor command directly on the editor document.
613 * For visual commands, you should use {@link #relayCmd} instead.
614 * <b>This should only be called after the editor is initialized.</b>
615 * @param {String} cmd The Midas command
616 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
618 execCmd : function(cmd, value){
619 this.doc.execCommand(cmd, false, value === undefined ? null : value);
625 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
627 * @param {String} text
629 insertAtCursor : function(text){
635 var r = this.doc.selection.createRange();
642 }else if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
644 this.execCmd('InsertHTML', text);
649 mozKeyPress : function(e){
651 var c = e.getCharCode(), cmd;
654 c = String.fromCharCode(c).toLowerCase();
665 this.cleanUpPaste.defer(100, this);
681 fixKeys : function(){ // load time branching for fastest keydown performance
684 var k = e.getKey(), r;
687 r = this.doc.selection.createRange();
690 r.pasteHTML('    ');
697 r = this.doc.selection.createRange();
699 var target = r.parentElement();
700 if(!target || target.tagName.toLowerCase() != 'li'){
702 r.pasteHTML('<br />');
708 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
709 this.cleanUpPaste.defer(100, this);
715 }else if(Roo.isOpera){
721 this.execCmd('InsertHTML','    ');
724 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
725 this.cleanUpPaste.defer(100, this);
730 }else if(Roo.isSafari){
736 this.execCmd('InsertText','\t');
740 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
741 this.cleanUpPaste.defer(100, this);
749 getAllAncestors: function()
751 var p = this.getSelectedNode();
754 a.push(p); // push blank onto stack..
755 p = this.getParentElement();
759 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
763 a.push(this.doc.body);
770 getSelection : function()
773 return Roo.isIE ? this.doc.selection : this.win.getSelection();
776 getSelectedNode: function()
778 // this may only work on Gecko!!!
780 // should we cache this!!!!
785 var range = this.createRange(this.getSelection());
788 var parent = range.parentElement();
790 var testRange = range.duplicate();
791 testRange.moveToElementText(parent);
792 if (testRange.inRange(range)) {
795 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
798 parent = parent.parentElement;
804 var ar = range.endContainer.childNodes;
806 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) {
896 var nodeRange = node.ownerDocument.createRange();
898 nodeRange.selectNode(node);
900 nodeRange.selectNodeContents(node);
902 var nodeIsBefore = range.compareBoundaryPoints(Range.START_TO_START, nodeRange) == 1;
903 var nodeIsAfter = range.compareBoundaryPoints(Range.END_TO_END, nodeRange) == -1;
905 if (nodeIsBefore && !nodeIsAfter)
907 if (!nodeIsBefore && nodeIsAfter)
909 if (nodeIsBefore && nodeIsAfter)
915 // private? - in a new class?
916 cleanUpPaste : function()
918 // cleans up the whole document..
919 // console.log('cleanuppaste');
920 this.cleanUpChildren(this.doc.body);
924 cleanUpChildren : function (n)
926 if (!n.childNodes.length) {
929 for (var i = n.childNodes.length-1; i > -1 ; i--) {
930 this.cleanUpChild(n.childNodes[i]);
937 cleanUpChild : function (node)
940 if (node.nodeName == "#text") {
941 // clean up silly Windows -- stuff?
944 if (node.nodeName == "#comment") {
945 node.parentNode.removeChild(node);
946 // clean up silly Windows -- stuff?
950 if (Roo.form.HtmlEditor.black.indexOf(node.tagName.toLowerCase()) > -1) {
952 node.parentNode.removeChild(node);
956 if (Roo.form.HtmlEditor.remove.indexOf(node.tagName.toLowerCase()) > -1) {
957 this.cleanUpChildren(node);
958 // inserts everything just before this node...
959 while (node.childNodes.length) {
960 var cn = node.childNodes[0];
961 node.removeChild(cn);
962 node.parentNode.insertBefore(cn, node);
964 node.parentNode.removeChild(node);
968 if (!node.attributes || !node.attributes.length) {
969 this.cleanUpChildren(node);
973 function cleanAttr(n,v)
976 if (v.match(/^\./) || v.match(/^\//)) {
979 if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/)) {
982 Roo.log("(REMOVE)"+ node.tagName +'.' + n + '=' + v);
983 node.removeAttribute(n);
987 function cleanStyle(n,v)
989 if (v.match(/expression/)) { //XSS?? should we even bother..
990 node.removeAttribute(n);
995 var parts = v.split(/;/);
996 Roo.each(parts, function(p) {
997 p = p.replace(/\s+/g,'');
1001 var l = p.split(':').shift().replace(/\s+/g,'');
1003 if (Roo.form.HtmlEditor.cwhite.indexOf(l) < 0) {
1004 Roo.log('(REMOVE)' + node.tagName +'.' + n + ':'+l + '=' + v);
1005 node.removeAttribute(n);
1015 for (var i = node.attributes.length-1; i > -1 ; i--) {
1016 var a = node.attributes[i];
1018 if (Roo.form.HtmlEditor.ablack.indexOf(a.name.toLowerCase()) > -1) {
1019 node.removeAttribute(a.name);
1022 if (Roo.form.HtmlEditor.aclean.indexOf(a.name.toLowerCase()) > -1) {
1023 cleanAttr(a.name,a.value); // fixme..
1026 if (a.name == 'style') {
1027 cleanStyle(a.name,a.value);
1029 /// clean up MS crap..
1030 if (a.name == 'class') {
1031 if (a.value.match(/^Mso/)) {
1032 node.className = '';
1042 this.cleanUpChildren(node);
1048 // hide stuff that is not compatible
1066 * @cfg {String} fieldClass @hide
1069 * @cfg {String} focusClass @hide
1072 * @cfg {String} autoCreate @hide
1075 * @cfg {String} inputType @hide
1078 * @cfg {String} invalidClass @hide
1081 * @cfg {String} invalidText @hide
1084 * @cfg {String} msgFx @hide
1087 * @cfg {String} validateOnBlur @hide
1091 Roo.form.HtmlEditor.white = [
1092 'area', 'br', 'img', 'input', 'hr', 'wbr',
1094 'address', 'blockquote', 'center', 'dd', 'dir', 'div',
1095 'dl', 'dt', 'h1', 'h2', 'h3', 'h4',
1096 'h5', 'h6', 'hr', 'isindex', 'listing', 'marquee',
1097 'menu', 'multicol', 'ol', 'p', 'plaintext', 'pre',
1098 'table', 'ul', 'xmp',
1100 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
1103 'dir', 'menu', 'ol', 'ul', 'dl',
1109 Roo.form.HtmlEditor.black = [
1110 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1112 'base', 'basefont', 'bgsound', 'blink', 'body',
1113 'frame', 'frameset', 'head', 'html', 'ilayer',
1114 'iframe', 'layer', 'link', 'meta', 'object',
1115 'script', 'style' ,'title', 'xml' // clean later..
1117 Roo.form.HtmlEditor.clean = [
1118 'script', 'style', 'title', 'xml'
1120 Roo.form.HtmlEditor.remove = [
1125 Roo.form.HtmlEditor.ablack = [
1129 Roo.form.HtmlEditor.aclean = [
1130 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1134 Roo.form.HtmlEditor.pwhite= [
1135 'http', 'https', 'mailto'
1138 Roo.form.HtmlEditor.cwhite= [