PageRenderTime 64ms CodeModel.GetById 26ms app.highlight 30ms RepoModel.GetById 0ms app.codeStats 1ms

/ext-4.0.7/src/form/field/HtmlEditor.js

https://bitbucket.org/srogerf/javascript
JavaScript | 1328 lines | 878 code | 112 blank | 338 comment | 124 complexity | 669b3642a11e8260a2d9019a7cfb8113 MD5 | raw file
   1/*
   2
   3This file is part of Ext JS 4
   4
   5Copyright (c) 2011 Sencha Inc
   6
   7Contact:  http://www.sencha.com/contact
   8
   9GNU General Public License Usage
  10This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
  11
  12If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
  13
  14*/
  15/**
  16 * Provides a lightweight HTML Editor component. Some toolbar features are not supported by Safari and will be
  17 * automatically hidden when needed. These are noted in the config options where appropriate.
  18 *
  19 * The editor's toolbar buttons have tooltips defined in the {@link #buttonTips} property, but they are not
  20 * enabled by default unless the global {@link Ext.tip.QuickTipManager} singleton is
  21 * {@link Ext.tip.QuickTipManager#init initialized}.
  22 *
  23 * An Editor is a sensitive component that can't be used in all spots standard fields can be used. Putting an
  24 * Editor within any element that has display set to 'none' can cause problems in Safari and Firefox due to their
  25 * default iframe reloading bugs.
  26 *
  27 * # Example usage
  28 *
  29 * Simple example rendered with default options:
  30 *
  31 *     @example
  32 *     Ext.tip.QuickTipManager.init();  // enable tooltips
  33 *     Ext.create('Ext.form.HtmlEditor', {
  34 *         width: 580,
  35 *         height: 250,
  36 *         renderTo: Ext.getBody()
  37 *     });
  38 *
  39 * Passed via xtype into a container and with custom options:
  40 *
  41 *     @example
  42 *     Ext.tip.QuickTipManager.init();  // enable tooltips
  43 *     new Ext.panel.Panel({
  44 *         title: 'HTML Editor',
  45 *         renderTo: Ext.getBody(),
  46 *         width: 550,
  47 *         height: 250,
  48 *         frame: true,
  49 *         layout: 'fit',
  50 *         items: {
  51 *             xtype: 'htmleditor',
  52 *             enableColors: false,
  53 *             enableAlignments: false
  54 *         }
  55 *     });
  56 */
  57Ext.define('Ext.form.field.HtmlEditor', {
  58    extend:'Ext.Component',
  59    mixins: {
  60        labelable: 'Ext.form.Labelable',
  61        field: 'Ext.form.field.Field'
  62    },
  63    alias: 'widget.htmleditor',
  64    alternateClassName: 'Ext.form.HtmlEditor',
  65    requires: [
  66        'Ext.tip.QuickTipManager',
  67        'Ext.picker.Color',
  68        'Ext.toolbar.Item',
  69        'Ext.toolbar.Toolbar',
  70        'Ext.util.Format',
  71        'Ext.layout.component.field.HtmlEditor'
  72    ],
  73
  74    fieldSubTpl: [
  75        '<div id="{cmpId}-toolbarWrap" class="{toolbarWrapCls}"></div>',
  76        '<textarea id="{cmpId}-textareaEl" name="{name}" tabIndex="-1" class="{textareaCls}" ',
  77            'style="{size}" autocomplete="off"></textarea>',
  78        '<iframe id="{cmpId}-iframeEl" name="{iframeName}" frameBorder="0" style="overflow:auto;{size}" src="{iframeSrc}"></iframe>',
  79        {
  80            compiled: true,
  81            disableFormats: true
  82        }
  83    ],
  84
  85    /**
  86     * @cfg {Boolean} enableFormat
  87     * Enable the bold, italic and underline buttons
  88     */
  89    enableFormat : true,
  90    /**
  91     * @cfg {Boolean} enableFontSize
  92     * Enable the increase/decrease font size buttons
  93     */
  94    enableFontSize : true,
  95    /**
  96     * @cfg {Boolean} enableColors
  97     * Enable the fore/highlight color buttons
  98     */
  99    enableColors : true,
 100    /**
 101     * @cfg {Boolean} enableAlignments
 102     * Enable the left, center, right alignment buttons
 103     */
 104    enableAlignments : true,
 105    /**
 106     * @cfg {Boolean} enableLists
 107     * Enable the bullet and numbered list buttons. Not available in Safari.
 108     */
 109    enableLists : true,
 110    /**
 111     * @cfg {Boolean} enableSourceEdit
 112     * Enable the switch to source edit button. Not available in Safari.
 113     */
 114    enableSourceEdit : true,
 115    /**
 116     * @cfg {Boolean} enableLinks
 117     * Enable the create link button. Not available in Safari.
 118     */
 119    enableLinks : true,
 120    /**
 121     * @cfg {Boolean} enableFont
 122     * Enable font selection. Not available in Safari.
 123     */
 124    enableFont : true,
 125    /**
 126     * @cfg {String} createLinkText
 127     * The default text for the create link prompt
 128     */
 129    createLinkText : 'Please enter the URL for the link:',
 130    /**
 131     * @cfg {String} [defaultLinkValue='http://']
 132     * The default value for the create link prompt
 133     */
 134    defaultLinkValue : 'http:/'+'/',
 135    /**
 136     * @cfg {String[]} fontFamilies
 137     * An array of available font families
 138     */
 139    fontFamilies : [
 140        'Arial',
 141        'Courier New',
 142        'Tahoma',
 143        'Times New Roman',
 144        'Verdana'
 145    ],
 146    defaultFont: 'tahoma',
 147    /**
 148     * @cfg {String} defaultValue
 149     * A default value to be put into the editor to resolve focus issues (defaults to (Non-breaking space) in Opera
 150     * and IE6, ​(Zero-width space) in all other browsers).
 151     */
 152    defaultValue: (Ext.isOpera || Ext.isIE6) ? '&#160;' : '&#8203;',
 153
 154    fieldBodyCls: Ext.baseCSSPrefix + 'html-editor-wrap',
 155
 156    componentLayout: 'htmleditor',
 157
 158    // private properties
 159    initialized : false,
 160    activated : false,
 161    sourceEditMode : false,
 162    iframePad:3,
 163    hideMode:'offsets',
 164
 165    maskOnDisable: true,
 166
 167    // private
 168    initComponent : function(){
 169        var me = this;
 170
 171        me.addEvents(
 172            /**
 173             * @event initialize
 174             * Fires when the editor is fully initialized (including the iframe)
 175             * @param {Ext.form.field.HtmlEditor} this
 176             */
 177            'initialize',
 178            /**
 179             * @event activate
 180             * Fires when the editor is first receives the focus. Any insertion must wait until after this event.
 181             * @param {Ext.form.field.HtmlEditor} this
 182             */
 183            'activate',
 184             /**
 185             * @event beforesync
 186             * Fires before the textarea is updated with content from the editor iframe. Return false to cancel the
 187             * sync.
 188             * @param {Ext.form.field.HtmlEditor} this
 189             * @param {String} html
 190             */
 191            'beforesync',
 192             /**
 193             * @event beforepush
 194             * Fires before the iframe editor is updated with content from the textarea. Return false to cancel the
 195             * push.
 196             * @param {Ext.form.field.HtmlEditor} this
 197             * @param {String} html
 198             */
 199            'beforepush',
 200             /**
 201             * @event sync
 202             * Fires when the textarea is updated with content from the editor iframe.
 203             * @param {Ext.form.field.HtmlEditor} this
 204             * @param {String} html
 205             */
 206            'sync',
 207             /**
 208             * @event push
 209             * Fires when the iframe editor is updated with content from the textarea.
 210             * @param {Ext.form.field.HtmlEditor} this
 211             * @param {String} html
 212             */
 213            'push',
 214             /**
 215             * @event editmodechange
 216             * Fires when the editor switches edit modes
 217             * @param {Ext.form.field.HtmlEditor} this
 218             * @param {Boolean} sourceEdit True if source edit, false if standard editing.
 219             */
 220            'editmodechange'
 221        );
 222
 223        me.callParent(arguments);
 224
 225        // Init mixins
 226        me.initLabelable();
 227        me.initField();
 228    },
 229
 230    /**
 231     * Called when the editor creates its toolbar. Override this method if you need to
 232     * add custom toolbar buttons.
 233     * @param {Ext.form.field.HtmlEditor} editor
 234     * @protected
 235     */
 236    createToolbar : function(editor){
 237        var me = this,
 238            items = [],
 239            tipsEnabled = Ext.tip.QuickTipManager && Ext.tip.QuickTipManager.isEnabled(),
 240            baseCSSPrefix = Ext.baseCSSPrefix,
 241            fontSelectItem, toolbar, undef;
 242
 243        function btn(id, toggle, handler){
 244            return {
 245                itemId : id,
 246                cls : baseCSSPrefix + 'btn-icon',
 247                iconCls: baseCSSPrefix + 'edit-'+id,
 248                enableToggle:toggle !== false,
 249                scope: editor,
 250                handler:handler||editor.relayBtnCmd,
 251                clickEvent:'mousedown',
 252                tooltip: tipsEnabled ? editor.buttonTips[id] || undef : undef,
 253                overflowText: editor.buttonTips[id].title || undef,
 254                tabIndex:-1
 255            };
 256        }
 257
 258
 259        if (me.enableFont && !Ext.isSafari2) {
 260            fontSelectItem = Ext.widget('component', {
 261                renderTpl: [
 262                    '<select id="{id}-selectEl" class="{cls}">',
 263                        '<tpl for="fonts">',
 264                            '<option value="{[values.toLowerCase()]}" style="font-family:{.}"<tpl if="values.toLowerCase()==parent.defaultFont"> selected</tpl>>{.}</option>',
 265                        '</tpl>',
 266                    '</select>'
 267                ],
 268                renderData: {
 269                    cls: baseCSSPrefix + 'font-select',
 270                    fonts: me.fontFamilies,
 271                    defaultFont: me.defaultFont
 272                },
 273                childEls: ['selectEl'],
 274                onDisable: function() {
 275                    var selectEl = this.selectEl;
 276                    if (selectEl) {
 277                        selectEl.dom.disabled = true;
 278                    }
 279                    Ext.Component.superclass.onDisable.apply(this, arguments);
 280                },
 281                onEnable: function() {
 282                    var selectEl = this.selectEl;
 283                    if (selectEl) {
 284                        selectEl.dom.disabled = false;
 285                    }
 286                    Ext.Component.superclass.onEnable.apply(this, arguments);
 287                }
 288            });
 289
 290            items.push(
 291                fontSelectItem,
 292                '-'
 293            );
 294        }
 295
 296        if (me.enableFormat) {
 297            items.push(
 298                btn('bold'),
 299                btn('italic'),
 300                btn('underline')
 301            );
 302        }
 303
 304        if (me.enableFontSize) {
 305            items.push(
 306                '-',
 307                btn('increasefontsize', false, me.adjustFont),
 308                btn('decreasefontsize', false, me.adjustFont)
 309            );
 310        }
 311
 312        if (me.enableColors) {
 313            items.push(
 314                '-', {
 315                    itemId: 'forecolor',
 316                    cls: baseCSSPrefix + 'btn-icon',
 317                    iconCls: baseCSSPrefix + 'edit-forecolor',
 318                    overflowText: editor.buttonTips.forecolor.title,
 319                    tooltip: tipsEnabled ? editor.buttonTips.forecolor || undef : undef,
 320                    tabIndex:-1,
 321                    menu : Ext.widget('menu', {
 322                        plain: true,
 323                        items: [{
 324                            xtype: 'colorpicker',
 325                            allowReselect: true,
 326                            focus: Ext.emptyFn,
 327                            value: '000000',
 328                            plain: true,
 329                            clickEvent: 'mousedown',
 330                            handler: function(cp, color) {
 331                                me.execCmd('forecolor', Ext.isWebKit || Ext.isIE ? '#'+color : color);
 332                                me.deferFocus();
 333                                this.up('menu').hide();
 334                            }
 335                        }]
 336                    })
 337                }, {
 338                    itemId: 'backcolor',
 339                    cls: baseCSSPrefix + 'btn-icon',
 340                    iconCls: baseCSSPrefix + 'edit-backcolor',
 341                    overflowText: editor.buttonTips.backcolor.title,
 342                    tooltip: tipsEnabled ? editor.buttonTips.backcolor || undef : undef,
 343                    tabIndex:-1,
 344                    menu : Ext.widget('menu', {
 345                        plain: true,
 346                        items: [{
 347                            xtype: 'colorpicker',
 348                            focus: Ext.emptyFn,
 349                            value: 'FFFFFF',
 350                            plain: true,
 351                            allowReselect: true,
 352                            clickEvent: 'mousedown',
 353                            handler: function(cp, color) {
 354                                if (Ext.isGecko) {
 355                                    me.execCmd('useCSS', false);
 356                                    me.execCmd('hilitecolor', color);
 357                                    me.execCmd('useCSS', true);
 358                                    me.deferFocus();
 359                                } else {
 360                                    me.execCmd(Ext.isOpera ? 'hilitecolor' : 'backcolor', Ext.isWebKit || Ext.isIE ? '#'+color : color);
 361                                    me.deferFocus();
 362                                }
 363                                this.up('menu').hide();
 364                            }
 365                        }]
 366                    })
 367                }
 368            );
 369        }
 370
 371        if (me.enableAlignments) {
 372            items.push(
 373                '-',
 374                btn('justifyleft'),
 375                btn('justifycenter'),
 376                btn('justifyright')
 377            );
 378        }
 379
 380        if (!Ext.isSafari2) {
 381            if (me.enableLinks) {
 382                items.push(
 383                    '-',
 384                    btn('createlink', false, me.createLink)
 385                );
 386            }
 387
 388            if (me.enableLists) {
 389                items.push(
 390                    '-',
 391                    btn('insertorderedlist'),
 392                    btn('insertunorderedlist')
 393                );
 394            }
 395            if (me.enableSourceEdit) {
 396                items.push(
 397                    '-',
 398                    btn('sourceedit', true, function(btn){
 399                        me.toggleSourceEdit(!me.sourceEditMode);
 400                    })
 401                );
 402            }
 403        }
 404
 405        // build the toolbar
 406        toolbar = Ext.widget('toolbar', {
 407            renderTo: me.toolbarWrap,
 408            enableOverflow: true,
 409            items: items
 410        });
 411
 412        if (fontSelectItem) {
 413            me.fontSelect = fontSelectItem.selectEl;
 414
 415            me.mon(me.fontSelect, 'change', function(){
 416                me.relayCmd('fontname', me.fontSelect.dom.value);
 417                me.deferFocus();
 418            });
 419        }
 420
 421        // stop form submits
 422        me.mon(toolbar.el, 'click', function(e){
 423            e.preventDefault();
 424        });
 425
 426        me.toolbar = toolbar;
 427    },
 428
 429    onDisable: function() {
 430        this.bodyEl.mask();
 431        this.callParent(arguments);
 432    },
 433
 434    onEnable: function() {
 435        this.bodyEl.unmask();
 436        this.callParent(arguments);
 437    },
 438
 439    /**
 440     * Sets the read only state of this field.
 441     * @param {Boolean} readOnly Whether the field should be read only.
 442     */
 443    setReadOnly: function(readOnly) {
 444        var me = this,
 445            textareaEl = me.textareaEl,
 446            iframeEl = me.iframeEl,
 447            body;
 448
 449        me.readOnly = readOnly;
 450
 451        if (textareaEl) {
 452            textareaEl.dom.readOnly = readOnly;
 453        }
 454
 455        if (me.initialized) {
 456            body = me.getEditorBody();
 457            if (Ext.isIE) {
 458                // Hide the iframe while setting contentEditable so it doesn't grab focus
 459                iframeEl.setDisplayed(false);
 460                body.contentEditable = !readOnly;
 461                iframeEl.setDisplayed(true);
 462            } else {
 463                me.setDesignMode(!readOnly);
 464            }
 465            if (body) {
 466                body.style.cursor = readOnly ? 'default' : 'text';
 467            }
 468            me.disableItems(readOnly);
 469        }
 470    },
 471
 472    /**
 473     * Called when the editor initializes the iframe with HTML contents. Override this method if you
 474     * want to change the initialization markup of the iframe (e.g. to add stylesheets).
 475     *
 476     * **Note:** IE8-Standards has unwanted scroller behavior, so the default meta tag forces IE7 compatibility.
 477     * Also note that forcing IE7 mode works when the page is loaded normally, but if you are using IE's Web
 478     * Developer Tools to manually set the document mode, that will take precedence and override what this
 479     * code sets by default. This can be confusing when developing, but is not a user-facing issue.
 480     * @protected
 481     */
 482    getDocMarkup: function() {
 483        var me = this,
 484            h = me.iframeEl.getHeight() - me.iframePad * 2;
 485        return Ext.String.format('<html><head><style type="text/css">body{border:0;margin:0;padding:{0}px;height:{1}px;box-sizing: border-box; -moz-box-sizing: border-box; -webkit-box-sizing: border-box;cursor:text}</style></head><body></body></html>', me.iframePad, h);
 486    },
 487
 488    // private
 489    getEditorBody: function() {
 490        var doc = this.getDoc();
 491        return doc.body || doc.documentElement;
 492    },
 493
 494    // private
 495    getDoc: function() {
 496        return (!Ext.isIE && this.iframeEl.dom.contentDocument) || this.getWin().document;
 497    },
 498
 499    // private
 500    getWin: function() {
 501        return Ext.isIE ? this.iframeEl.dom.contentWindow : window.frames[this.iframeEl.dom.name];
 502    },
 503
 504    // private
 505    onRender: function() {
 506        var me = this;
 507
 508        me.onLabelableRender();
 509
 510        me.addChildEls('toolbarWrap', 'iframeEl', 'textareaEl');
 511
 512        me.callParent(arguments);
 513
 514        me.textareaEl.dom.value = me.value || '';
 515
 516        // Start polling for when the iframe document is ready to be manipulated
 517        me.monitorTask = Ext.TaskManager.start({
 518            run: me.checkDesignMode,
 519            scope: me,
 520            interval:100
 521        });
 522
 523        me.createToolbar(me);
 524        me.disableItems(true);
 525    },
 526
 527    initRenderTpl: function() {
 528        var me = this;
 529        if (!me.hasOwnProperty('renderTpl')) {
 530            me.renderTpl = me.getTpl('labelableRenderTpl');
 531        }
 532        return me.callParent();
 533    },
 534
 535    initRenderData: function() {
 536        return Ext.applyIf(this.callParent(), this.getLabelableRenderData());
 537    },
 538
 539    getSubTplData: function() {
 540        var cssPrefix = Ext.baseCSSPrefix;
 541        return {
 542            cmpId: this.id,
 543            id: this.getInputId(),
 544            toolbarWrapCls: cssPrefix + 'html-editor-tb',
 545            textareaCls: cssPrefix + 'hidden',
 546            iframeName: Ext.id(),
 547            iframeSrc: Ext.SSL_SECURE_URL,
 548            size: 'height:100px;'
 549        };
 550    },
 551
 552    getSubTplMarkup: function() {
 553        var data = this.getSubTplData();
 554        return this.getTpl('fieldSubTpl').apply(data);
 555    },
 556
 557    getBodyNaturalWidth: function() {
 558        return 565;
 559    },
 560
 561    initFrameDoc: function() {
 562        var me = this,
 563            doc, task;
 564
 565        Ext.TaskManager.stop(me.monitorTask);
 566
 567        doc = me.getDoc();
 568        me.win = me.getWin();
 569
 570        doc.open();
 571        doc.write(me.getDocMarkup());
 572        doc.close();
 573
 574        task = { // must defer to wait for browser to be ready
 575            run: function() {
 576                var doc = me.getDoc();
 577                if (doc.body || doc.readyState === 'complete') {
 578                    Ext.TaskManager.stop(task);
 579                    me.setDesignMode(true);
 580                    Ext.defer(me.initEditor, 10, me);
 581                }
 582            },
 583            interval : 10,
 584            duration:10000,
 585            scope: me
 586        };
 587        Ext.TaskManager.start(task);
 588    },
 589
 590    checkDesignMode: function() {
 591        var me = this,
 592            doc = me.getDoc();
 593        if (doc && (!doc.editorInitialized || me.getDesignMode() !== 'on')) {
 594            me.initFrameDoc();
 595        }
 596    },
 597
 598    /**
 599     * @private
 600     * Sets current design mode. To enable, mode can be true or 'on', off otherwise
 601     */
 602    setDesignMode: function(mode) {
 603        var me = this,
 604            doc = me.getDoc();
 605        if (doc) {
 606            if (me.readOnly) {
 607                mode = false;
 608            }
 609            doc.designMode = (/on|true/i).test(String(mode).toLowerCase()) ?'on':'off';
 610        }
 611    },
 612
 613    // private
 614    getDesignMode: function() {
 615        var doc = this.getDoc();
 616        return !doc ? '' : String(doc.designMode).toLowerCase();
 617    },
 618
 619    disableItems: function(disabled) {
 620        this.getToolbar().items.each(function(item){
 621            if(item.getItemId() !== 'sourceedit'){
 622                item.setDisabled(disabled);
 623            }
 624        });
 625    },
 626
 627    /**
 628     * Toggles the editor between standard and source edit mode.
 629     * @param {Boolean} sourceEditMode (optional) True for source edit, false for standard
 630     */
 631    toggleSourceEdit: function(sourceEditMode) {
 632        var me = this,
 633            iframe = me.iframeEl,
 634            textarea = me.textareaEl,
 635            hiddenCls = Ext.baseCSSPrefix + 'hidden',
 636            btn = me.getToolbar().getComponent('sourceedit');
 637
 638        if (!Ext.isBoolean(sourceEditMode)) {
 639            sourceEditMode = !me.sourceEditMode;
 640        }
 641        me.sourceEditMode = sourceEditMode;
 642
 643        if (btn.pressed !== sourceEditMode) {
 644            btn.toggle(sourceEditMode);
 645        }
 646        if (sourceEditMode) {
 647            me.disableItems(true);
 648            me.syncValue();
 649            iframe.addCls(hiddenCls);
 650            textarea.removeCls(hiddenCls);
 651            textarea.dom.removeAttribute('tabIndex');
 652            textarea.focus();
 653        }
 654        else {
 655            if (me.initialized) {
 656                me.disableItems(me.readOnly);
 657            }
 658            me.pushValue();
 659            iframe.removeCls(hiddenCls);
 660            textarea.addCls(hiddenCls);
 661            textarea.dom.setAttribute('tabIndex', -1);
 662            me.deferFocus();
 663        }
 664        me.fireEvent('editmodechange', me, sourceEditMode);
 665        me.doComponentLayout();
 666    },
 667
 668    // private used internally
 669    createLink : function() {
 670        var url = prompt(this.createLinkText, this.defaultLinkValue);
 671        if (url && url !== 'http:/'+'/') {
 672            this.relayCmd('createlink', url);
 673        }
 674    },
 675
 676    clearInvalid: Ext.emptyFn,
 677
 678    // docs inherit from Field
 679    setValue: function(value) {
 680        var me = this,
 681            textarea = me.textareaEl;
 682        me.mixins.field.setValue.call(me, value);
 683        if (value === null || value === undefined) {
 684            value = '';
 685        }
 686        if (textarea) {
 687            textarea.dom.value = value;
 688        }
 689        me.pushValue();
 690        return this;
 691    },
 692
 693    /**
 694     * If you need/want custom HTML cleanup, this is the method you should override.
 695     * @param {String} html The HTML to be cleaned
 696     * @return {String} The cleaned HTML
 697     * @protected
 698     */
 699    cleanHtml: function(html) {
 700        html = String(html);
 701        if (Ext.isWebKit) { // strip safari nonsense
 702            html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
 703        }
 704
 705        /*
 706         * Neat little hack. Strips out all the non-digit characters from the default
 707         * value and compares it to the character code of the first character in the string
 708         * because it can cause encoding issues when posted to the server.
 709         */
 710        if (html.charCodeAt(0) === this.defaultValue.replace(/\D/g, '')) {
 711            html = html.substring(1);
 712        }
 713        return html;
 714    },
 715
 716    /**
 717     * Syncs the contents of the editor iframe with the textarea.
 718     * @protected
 719     */
 720    syncValue : function(){
 721        var me = this,
 722            body, html, bodyStyle, match;
 723        if (me.initialized) {
 724            body = me.getEditorBody();
 725            html = body.innerHTML;
 726            if (Ext.isWebKit) {
 727                bodyStyle = body.getAttribute('style'); // Safari puts text-align styles on the body element!
 728                match = bodyStyle.match(/text-align:(.*?);/i);
 729                if (match && match[1]) {
 730                    html = '<div style="' + match[0] + '">' + html + '</div>';
 731                }
 732            }
 733            html = me.cleanHtml(html);
 734            if (me.fireEvent('beforesync', me, html) !== false) {
 735                me.textareaEl.dom.value = html;
 736                me.fireEvent('sync', me, html);
 737            }
 738        }
 739    },
 740
 741    //docs inherit from Field
 742    getValue : function() {
 743        var me = this,
 744            value;
 745        if (!me.sourceEditMode) {
 746            me.syncValue();
 747        }
 748        value = me.rendered ? me.textareaEl.dom.value : me.value;
 749        me.value = value;
 750        return value;
 751    },
 752
 753    /**
 754     * Pushes the value of the textarea into the iframe editor.
 755     * @protected
 756     */
 757    pushValue: function() {
 758        var me = this,
 759            v;
 760        if(me.initialized){
 761            v = me.textareaEl.dom.value || '';
 762            if (!me.activated && v.length < 1) {
 763                v = me.defaultValue;
 764            }
 765            if (me.fireEvent('beforepush', me, v) !== false) {
 766                me.getEditorBody().innerHTML = v;
 767                if (Ext.isGecko) {
 768                    // Gecko hack, see: https://bugzilla.mozilla.org/show_bug.cgi?id=232791#c8
 769                    me.setDesignMode(false);  //toggle off first
 770                    me.setDesignMode(true);
 771                }
 772                me.fireEvent('push', me, v);
 773            }
 774        }
 775    },
 776
 777    // private
 778    deferFocus : function(){
 779         this.focus(false, true);
 780    },
 781
 782    getFocusEl: function() {
 783        var me = this,
 784            win = me.win;
 785        return win && !me.sourceEditMode ? win : me.textareaEl;
 786    },
 787
 788    // private
 789    initEditor : function(){
 790        //Destroying the component during/before initEditor can cause issues.
 791        try {
 792            var me = this,
 793                dbody = me.getEditorBody(),
 794                ss = me.textareaEl.getStyles('font-size', 'font-family', 'background-image', 'background-repeat', 'background-color', 'color'),
 795                doc,
 796                fn;
 797
 798            ss['background-attachment'] = 'fixed'; // w3c
 799            dbody.bgProperties = 'fixed'; // ie
 800
 801            Ext.DomHelper.applyStyles(dbody, ss);
 802
 803            doc = me.getDoc();
 804
 805            if (doc) {
 806                try {
 807                    Ext.EventManager.removeAll(doc);
 808                } catch(e) {}
 809            }
 810
 811            /*
 812             * We need to use createDelegate here, because when using buffer, the delayed task is added
 813             * as a property to the function. When the listener is removed, the task is deleted from the function.
 814             * Since onEditorEvent is shared on the prototype, if we have multiple html editors, the first time one of the editors
 815             * is destroyed, it causes the fn to be deleted from the prototype, which causes errors. Essentially, we're just anonymizing the function.
 816             */
 817            fn = Ext.Function.bind(me.onEditorEvent, me);
 818            Ext.EventManager.on(doc, {
 819                mousedown: fn,
 820                dblclick: fn,
 821                click: fn,
 822                keyup: fn,
 823                buffer:100
 824            });
 825
 826            // These events need to be relayed from the inner document (where they stop
 827            // bubbling) up to the outer document. This has to be done at the DOM level so
 828            // the event reaches listeners on elements like the document body. The effected
 829            // mechanisms that depend on this bubbling behavior are listed to the right
 830            // of the event.
 831            fn = me.onRelayedEvent;
 832            Ext.EventManager.on(doc, {
 833                mousedown: fn, // menu dismisal (MenuManager) and Window onMouseDown (toFront)
 834                mousemove: fn, // window resize drag detection
 835                mouseup: fn,   // window resize termination
 836                click: fn,     // not sure, but just to be safe
 837                dblclick: fn,  // not sure again
 838                scope: me
 839            });
 840
 841            if (Ext.isGecko) {
 842                Ext.EventManager.on(doc, 'keypress', me.applyCommand, me);
 843            }
 844            if (me.fixKeys) {
 845                Ext.EventManager.on(doc, 'keydown', me.fixKeys, me);
 846            }
 847
 848            // We need to be sure we remove all our events from the iframe on unload or we're going to LEAK!
 849            Ext.EventManager.on(window, 'unload', me.beforeDestroy, me);
 850            doc.editorInitialized = true;
 851
 852            me.initialized = true;
 853            me.pushValue();
 854            me.setReadOnly(me.readOnly);
 855            me.fireEvent('initialize', me);
 856        } catch(ex) {
 857            // ignore (why?)
 858        }
 859    },
 860
 861    // private
 862    beforeDestroy : function(){
 863        var me = this,
 864            monitorTask = me.monitorTask,
 865            doc, prop;
 866
 867        if (monitorTask) {
 868            Ext.TaskManager.stop(monitorTask);
 869        }
 870        if (me.rendered) {
 871            try {
 872                doc = me.getDoc();
 873                if (doc) {
 874                    Ext.EventManager.removeAll(doc);
 875                    for (prop in doc) {
 876                        if (doc.hasOwnProperty(prop)) {
 877                            delete doc[prop];
 878                        }
 879                    }
 880                }
 881            } catch(e) {
 882                // ignore (why?)
 883            }
 884            Ext.destroyMembers(me, 'tb', 'toolbarWrap', 'iframeEl', 'textareaEl');
 885        }
 886        me.callParent();
 887    },
 888
 889    // private
 890    onRelayedEvent: function (event) {
 891        // relay event from the iframe's document to the document that owns the iframe...
 892
 893        var iframeEl = this.iframeEl,
 894            iframeXY = iframeEl.getXY(),
 895            eventXY = event.getXY();
 896
 897        // the event from the inner document has XY relative to that document's origin,
 898        // so adjust it to use the origin of the iframe in the outer document:
 899        event.xy = [iframeXY[0] + eventXY[0], iframeXY[1] + eventXY[1]];
 900
 901        event.injectEvent(iframeEl); // blame the iframe for the event...
 902
 903        event.xy = eventXY; // restore the original XY (just for safety)
 904    },
 905
 906    // private
 907    onFirstFocus : function(){
 908        var me = this,
 909            selection, range;
 910        me.activated = true;
 911        me.disableItems(me.readOnly);
 912        if (Ext.isGecko) { // prevent silly gecko errors
 913            me.win.focus();
 914            selection = me.win.getSelection();
 915            if (!selection.focusNode || selection.focusNode.nodeType !== 3) {
 916                range = selection.getRangeAt(0);
 917                range.selectNodeContents(me.getEditorBody());
 918                range.collapse(true);
 919                me.deferFocus();
 920            }
 921            try {
 922                me.execCmd('useCSS', true);
 923                me.execCmd('styleWithCSS', false);
 924            } catch(e) {
 925                // ignore (why?)
 926            }
 927        }
 928        me.fireEvent('activate', me);
 929    },
 930
 931    // private
 932    adjustFont: function(btn) {
 933        var adjust = btn.getItemId() === 'increasefontsize' ? 1 : -1,
 934            size = this.getDoc().queryCommandValue('FontSize') || '2',
 935            isPxSize = Ext.isString(size) && size.indexOf('px') !== -1,
 936            isSafari;
 937        size = parseInt(size, 10);
 938        if (isPxSize) {
 939            // Safari 3 values
 940            // 1 = 10px, 2 = 13px, 3 = 16px, 4 = 18px, 5 = 24px, 6 = 32px
 941            if (size <= 10) {
 942                size = 1 + adjust;
 943            }
 944            else if (size <= 13) {
 945                size = 2 + adjust;
 946            }
 947            else if (size <= 16) {
 948                size = 3 + adjust;
 949            }
 950            else if (size <= 18) {
 951                size = 4 + adjust;
 952            }
 953            else if (size <= 24) {
 954                size = 5 + adjust;
 955            }
 956            else {
 957                size = 6 + adjust;
 958            }
 959            size = Ext.Number.constrain(size, 1, 6);
 960        } else {
 961            isSafari = Ext.isSafari;
 962            if (isSafari) { // safari
 963                adjust *= 2;
 964            }
 965            size = Math.max(1, size + adjust) + (isSafari ? 'px' : 0);
 966        }
 967        this.execCmd('FontSize', size);
 968    },
 969
 970    // private
 971    onEditorEvent: function(e) {
 972        this.updateToolbar();
 973    },
 974
 975    /**
 976     * Triggers a toolbar update by reading the markup state of the current selection in the editor.
 977     * @protected
 978     */
 979    updateToolbar: function() {
 980        var me = this,
 981            btns, doc, name, fontSelect;
 982
 983        if (me.readOnly) {
 984            return;
 985        }
 986
 987        if (!me.activated) {
 988            me.onFirstFocus();
 989            return;
 990        }
 991
 992        btns = me.getToolbar().items.map;
 993        doc = me.getDoc();
 994
 995        if (me.enableFont && !Ext.isSafari2) {
 996            name = (doc.queryCommandValue('FontName') || me.defaultFont).toLowerCase();
 997            fontSelect = me.fontSelect.dom;
 998            if (name !== fontSelect.value) {
 999                fontSelect.value = name;
1000            }
1001        }
1002
1003        function updateButtons() {
1004            Ext.Array.forEach(Ext.Array.toArray(arguments), function(name) {
1005                btns[name].toggle(doc.queryCommandState(name));
1006            });
1007        }
1008        if(me.enableFormat){
1009            updateButtons('bold', 'italic', 'underline');
1010        }
1011        if(me.enableAlignments){
1012            updateButtons('justifyleft', 'justifycenter', 'justifyright');
1013        }
1014        if(!Ext.isSafari2 && me.enableLists){
1015            updateButtons('insertorderedlist', 'insertunorderedlist');
1016        }
1017
1018        Ext.menu.Manager.hideAll();
1019
1020        me.syncValue();
1021    },
1022
1023    // private
1024    relayBtnCmd: function(btn) {
1025        this.relayCmd(btn.getItemId());
1026    },
1027
1028    /**
1029     * Executes a Midas editor command on the editor document and performs necessary focus and toolbar updates.
1030     * **This should only be called after the editor is initialized.**
1031     * @param {String} cmd The Midas command
1032     * @param {String/Boolean} [value=null] The value to pass to the command
1033     */
1034    relayCmd: function(cmd, value) {
1035        Ext.defer(function() {
1036            var me = this;
1037            me.focus();
1038            me.execCmd(cmd, value);
1039            me.updateToolbar();
1040        }, 10, this);
1041    },
1042
1043    /**
1044     * Executes a Midas editor command directly on the editor document. For visual commands, you should use
1045     * {@link #relayCmd} instead. **This should only be called after the editor is initialized.**
1046     * @param {String} cmd The Midas command
1047     * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
1048     */
1049    execCmd : function(cmd, value){
1050        var me = this,
1051            doc = me.getDoc(),
1052            undef;
1053        doc.execCommand(cmd, false, value === undef ? null : value);
1054        me.syncValue();
1055    },
1056
1057    // private
1058    applyCommand : function(e){
1059        if (e.ctrlKey) {
1060            var me = this,
1061                c = e.getCharCode(), cmd;
1062            if (c > 0) {
1063                c = String.fromCharCode(c);
1064                switch (c) {
1065                    case 'b':
1066                        cmd = 'bold';
1067                    break;
1068                    case 'i':
1069                        cmd = 'italic';
1070                    break;
1071                    case 'u':
1072                        cmd = 'underline';
1073                    break;
1074                }
1075                if (cmd) {
1076                    me.win.focus();
1077                    me.execCmd(cmd);
1078                    me.deferFocus();
1079                    e.preventDefault();
1080                }
1081            }
1082        }
1083    },
1084
1085    /**
1086     * Inserts the passed text at the current cursor position.
1087     * Note: the editor must be initialized and activated to insert text.
1088     * @param {String} text
1089     */
1090    insertAtCursor : function(text){
1091        var me = this,
1092            range;
1093
1094        if (me.activated) {
1095            me.win.focus();
1096            if (Ext.isIE) {
1097                range = me.getDoc().selection.createRange();
1098                if (range) {
1099                    range.pasteHTML(text);
1100                    me.syncValue();
1101                    me.deferFocus();
1102                }
1103            }else{
1104                me.execCmd('InsertHTML', text);
1105                me.deferFocus();
1106            }
1107        }
1108    },
1109
1110    // private
1111    fixKeys: function() { // load time branching for fastest keydown performance
1112        if (Ext.isIE) {
1113            return function(e){
1114                var me = this,
1115                    k = e.getKey(),
1116                    doc = me.getDoc(),
1117                    range, target;
1118                if (k === e.TAB) {
1119                    e.stopEvent();
1120                    range = doc.selection.createRange();
1121                    if(range){
1122                        range.collapse(true);
1123                        range.pasteHTML('&nbsp;&nbsp;&nbsp;&nbsp;');
1124                        me.deferFocus();
1125                    }
1126                }
1127                else if (k === e.ENTER) {
1128                    range = doc.selection.createRange();
1129                    if (range) {
1130                        target = range.parentElement();
1131                        if(!target || target.tagName.toLowerCase() !== 'li'){
1132                            e.stopEvent();
1133                            range.pasteHTML('<br />');
1134                            range.collapse(false);
1135                            range.select();
1136                        }
1137                    }
1138                }
1139            };
1140        }
1141
1142        if (Ext.isOpera) {
1143            return function(e){
1144                var me = this;
1145                if (e.getKey() === e.TAB) {
1146                    e.stopEvent();
1147                    me.win.focus();
1148                    me.execCmd('InsertHTML','&nbsp;&nbsp;&nbsp;&nbsp;');
1149                    me.deferFocus();
1150                }
1151            };
1152        }
1153
1154        if (Ext.isWebKit) {
1155            return function(e){
1156                var me = this,
1157                    k = e.getKey();
1158                if (k === e.TAB) {
1159                    e.stopEvent();
1160                    me.execCmd('InsertText','\t');
1161                    me.deferFocus();
1162                }
1163                else if (k === e.ENTER) {
1164                    e.stopEvent();
1165                    me.execCmd('InsertHtml','<br /><br />');
1166                    me.deferFocus();
1167                }
1168            };
1169        }
1170
1171        return null; // not needed, so null
1172    }(),
1173
1174    /**
1175     * Returns the editor's toolbar. **This is only available after the editor has been rendered.**
1176     * @return {Ext.toolbar.Toolbar}
1177     */
1178    getToolbar : function(){
1179        return this.toolbar;
1180    },
1181
1182    /**
1183     * @property {Object} buttonTips
1184     * Object collection of toolbar tooltips for the buttons in the editor. The key is the command id associated with
1185     * that button and the value is a valid QuickTips object. For example:
1186     *
1187     *     {
1188     *         bold : {
1189     *             title: 'Bold (Ctrl+B)',
1190     *             text: 'Make the selected text bold.',
1191     *             cls: 'x-html-editor-tip'
1192     *         },
1193     *         italic : {
1194     *             title: 'Italic (Ctrl+I)',
1195     *             text: 'Make the selected text italic.',
1196     *             cls: 'x-html-editor-tip'
1197     *         },
1198     *         ...
1199     */
1200    buttonTips : {
1201        bold : {
1202            title: 'Bold (Ctrl+B)',
1203            text: 'Make the selected text bold.',
1204            cls: Ext.baseCSSPrefix + 'html-editor-tip'
1205        },
1206        italic : {
1207            title: 'Italic (Ctrl+I)',
1208            text: 'Make the selected text italic.',
1209            cls: Ext.baseCSSPrefix + 'html-editor-tip'
1210        },
1211        underline : {
1212            title: 'Underline (Ctrl+U)',
1213            text: 'Underline the selected text.',
1214            cls: Ext.baseCSSPrefix + 'html-editor-tip'
1215        },
1216        increasefontsize : {
1217            title: 'Grow Text',
1218            text: 'Increase the font size.',
1219            cls: Ext.baseCSSPrefix + 'html-editor-tip'
1220        },
1221        decreasefontsize : {
1222            title: 'Shrink Text',
1223            text: 'Decrease the font size.',
1224            cls: Ext.baseCSSPrefix + 'html-editor-tip'
1225        },
1226        backcolor : {
1227            title: 'Text Highlight Color',
1228            text: 'Change the background color of the selected text.',
1229            cls: Ext.baseCSSPrefix + 'html-editor-tip'
1230        },
1231        forecolor : {
1232            title: 'Font Color',
1233            text: 'Change the color of the selected text.',
1234            cls: Ext.baseCSSPrefix + 'html-editor-tip'
1235        },
1236        justifyleft : {
1237            title: 'Align Text Left',
1238            text: 'Align text to the left.',
1239            cls: Ext.baseCSSPrefix + 'html-editor-tip'
1240        },
1241        justifycenter : {
1242            title: 'Center Text',
1243            text: 'Center text in the editor.',
1244            cls: Ext.baseCSSPrefix + 'html-editor-tip'
1245        },
1246        justifyright : {
1247            title: 'Align Text Right',
1248            text: 'Align text to the right.',
1249            cls: Ext.baseCSSPrefix + 'html-editor-tip'
1250        },
1251        insertunorderedlist : {
1252            title: 'Bullet List',
1253            text: 'Start a bulleted list.',
1254            cls: Ext.baseCSSPrefix + 'html-editor-tip'
1255        },
1256        insertorderedlist : {
1257            title: 'Numbered List',
1258            text: 'Start a numbered list.',
1259            cls: Ext.baseCSSPrefix + 'html-editor-tip'
1260        },
1261        createlink : {
1262            title: 'Hyperlink',
1263            text: 'Make the selected text a hyperlink.',
1264            cls: Ext.baseCSSPrefix + 'html-editor-tip'
1265        },
1266        sourceedit : {
1267            title: 'Source Edit',
1268            text: 'Switch to source editing mode.',
1269            cls: Ext.baseCSSPrefix + 'html-editor-tip'
1270        }
1271    }
1272
1273    // hide stuff that is not compatible
1274    /**
1275     * @event blur
1276     * @hide
1277     */
1278    /**
1279     * @event change
1280     * @hide
1281     */
1282    /**
1283     * @event focus
1284     * @hide
1285     */
1286    /**
1287     * @event specialkey
1288     * @hide
1289     */
1290    /**
1291     * @cfg {String} fieldCls @hide
1292     */
1293    /**
1294     * @cfg {String} focusCls @hide
1295     */
1296    /**
1297     * @cfg {String} autoCreate @hide
1298     */
1299    /**
1300     * @cfg {String} inputType @hide
1301     */
1302    /**
1303     * @cfg {String} invalidCls @hide
1304     */
1305    /**
1306     * @cfg {String} invalidText @hide
1307     */
1308    /**
1309     * @cfg {String} msgFx @hide
1310     */
1311    /**
1312     * @cfg {Boolean} allowDomMove @hide
1313     */
1314    /**
1315     * @cfg {String} applyTo @hide
1316     */
1317    /**
1318     * @cfg {String} readOnly  @hide
1319     */
1320    /**
1321     * @cfg {String} tabIndex  @hide
1322     */
1323    /**
1324     * @method validate
1325     * @hide
1326     */
1327});
1328