X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/0494b8d9b9bb03ab6c22b34dae81261e3cd7e3e6..7a654f8d43fdb43d78b63d90528bed6e86b608cc:/src/form/field/HtmlEditor.js?ds=sidebyside diff --git a/src/form/field/HtmlEditor.js b/src/form/field/HtmlEditor.js new file mode 100644 index 00000000..80674d7c --- /dev/null +++ b/src/form/field/HtmlEditor.js @@ -0,0 +1,1315 @@ +/** + * @class Ext.form.field.HtmlEditor + * @extends Ext.Component + * + * Provides a lightweight HTML Editor component. Some toolbar features are not supported by Safari and will be + * automatically hidden when needed. These are noted in the config options where appropriate. + * + * The editor's toolbar buttons have tooltips defined in the {@link #buttonTips} property, but they are not + * enabled by default unless the global {@link Ext.tip.QuickTipManager} singleton is {@link Ext.tip.QuickTipManager#init initialized}. + * + * An Editor is a sensitive component that can't be used in all spots standard fields can be used. Putting an Editor within + * any element that has display set to 'none' can cause problems in Safari and Firefox due to their default iframe reloading bugs. + * + * {@img Ext.form.HtmlEditor/Ext.form.HtmlEditor1.png Ext.form.HtmlEditor component} + * + * ## Example usage + * + * {@img Ext.form.HtmlEditor/Ext.form.HtmlEditor2.png Ext.form.HtmlEditor component} + * + * // Simple example rendered with default options: + * Ext.tip.QuickTips.init(); // enable tooltips + * Ext.create('Ext.form.HtmlEditor', { + * width: 580, + * height: 250, + * renderTo: Ext.getBody() + * }); + * + * {@img Ext.form.HtmlEditor/Ext.form.HtmlEditor2.png Ext.form.HtmlEditor component} + * + * // Passed via xtype into a container and with custom options: + * Ext.tip.QuickTips.init(); // enable tooltips + * new Ext.panel.Panel({ + * title: 'HTML Editor', + * renderTo: Ext.getBody(), + * width: 550, + * height: 250, + * frame: true, + * layout: 'fit', + * items: { + * xtype: 'htmleditor', + * enableColors: false, + * enableAlignments: false + * } + * }); + * + * @constructor + * Create a new HtmlEditor + * @param {Object} config + * @xtype htmleditor + */ +Ext.define('Ext.form.field.HtmlEditor', { + extend:'Ext.Component', + mixins: { + labelable: 'Ext.form.Labelable', + field: 'Ext.form.field.Field' + }, + alias: 'widget.htmleditor', + alternateClassName: 'Ext.form.HtmlEditor', + requires: [ + 'Ext.tip.QuickTipManager', + 'Ext.picker.Color', + 'Ext.toolbar.Item', + 'Ext.toolbar.Toolbar', + 'Ext.util.Format', + 'Ext.layout.component.field.HtmlEditor' + ], + + fieldSubTpl: [ + '
', + '', + '', + { + compiled: true, + disableFormats: true + } + ], + + /** + * @cfg {Boolean} enableFormat Enable the bold, italic and underline buttons (defaults to true) + */ + enableFormat : true, + /** + * @cfg {Boolean} enableFontSize Enable the increase/decrease font size buttons (defaults to true) + */ + enableFontSize : true, + /** + * @cfg {Boolean} enableColors Enable the fore/highlight color buttons (defaults to true) + */ + enableColors : true, + /** + * @cfg {Boolean} enableAlignments Enable the left, center, right alignment buttons (defaults to true) + */ + enableAlignments : true, + /** + * @cfg {Boolean} enableLists Enable the bullet and numbered list buttons. Not available in Safari. (defaults to true) + */ + enableLists : true, + /** + * @cfg {Boolean} enableSourceEdit Enable the switch to source edit button. Not available in Safari. (defaults to true) + */ + enableSourceEdit : true, + /** + * @cfg {Boolean} enableLinks Enable the create link button. Not available in Safari. (defaults to true) + */ + enableLinks : true, + /** + * @cfg {Boolean} enableFont Enable font selection. Not available in Safari. (defaults to true) + */ + enableFont : true, + /** + * @cfg {String} createLinkText The default text for the create link prompt + */ + createLinkText : 'Please enter the URL for the link:', + /** + * @cfg {String} defaultLinkValue The default value for the create link prompt (defaults to http:/ /) + */ + defaultLinkValue : 'http:/'+'/', + /** + * @cfg {Array} fontFamilies An array of available font families + */ + fontFamilies : [ + 'Arial', + 'Courier New', + 'Tahoma', + 'Times New Roman', + 'Verdana' + ], + defaultFont: 'tahoma', + /** + * @cfg {String} defaultValue A default value to be put into the editor to resolve focus issues (defaults to (Non-breaking space) in Opera and IE6, (Zero-width space) in all other browsers). + */ + defaultValue: (Ext.isOpera || Ext.isIE6) ? ' ' : '', + + fieldBodyCls: Ext.baseCSSPrefix + 'html-editor-wrap', + + componentLayout: 'htmleditor', + + // private properties + initialized : false, + activated : false, + sourceEditMode : false, + iframePad:3, + hideMode:'offsets', + + maskOnDisable: true, + + // private + initComponent : function(){ + var me = this; + + me.addEvents( + /** + * @event initialize + * Fires when the editor is fully initialized (including the iframe) + * @param {Ext.form.field.HtmlEditor} this + */ + 'initialize', + /** + * @event activate + * Fires when the editor is first receives the focus. Any insertion must wait + * until after this event. + * @param {Ext.form.field.HtmlEditor} this + */ + 'activate', + /** + * @event beforesync + * Fires before the textarea is updated with content from the editor iframe. Return false + * to cancel the sync. + * @param {Ext.form.field.HtmlEditor} this + * @param {String} html + */ + 'beforesync', + /** + * @event beforepush + * Fires before the iframe editor is updated with content from the textarea. Return false + * to cancel the push. + * @param {Ext.form.field.HtmlEditor} this + * @param {String} html + */ + 'beforepush', + /** + * @event sync + * Fires when the textarea is updated with content from the editor iframe. + * @param {Ext.form.field.HtmlEditor} this + * @param {String} html + */ + 'sync', + /** + * @event push + * Fires when the iframe editor is updated with content from the textarea. + * @param {Ext.form.field.HtmlEditor} this + * @param {String} html + */ + 'push', + /** + * @event editmodechange + * Fires when the editor switches edit modes + * @param {Ext.form.field.HtmlEditor} this + * @param {Boolean} sourceEdit True if source edit, false if standard editing. + */ + 'editmodechange' + ); + + me.callParent(arguments); + + // Init mixins + me.initLabelable(); + me.initField(); + }, + + /* + * Protected method that will not generally be called directly. It + * is called when the editor creates its toolbar. Override this method if you need to + * add custom toolbar buttons. + * @param {Ext.form.field.HtmlEditor} editor + */ + createToolbar : function(editor){ + var me = this, + items = [], + tipsEnabled = Ext.tip.QuickTipManager && Ext.tip.QuickTipManager.isEnabled(), + baseCSSPrefix = Ext.baseCSSPrefix, + fontSelectItem, toolbar, undef; + + function btn(id, toggle, handler){ + return { + itemId : id, + cls : baseCSSPrefix + 'btn-icon', + iconCls: baseCSSPrefix + 'edit-'+id, + enableToggle:toggle !== false, + scope: editor, + handler:handler||editor.relayBtnCmd, + clickEvent:'mousedown', + tooltip: tipsEnabled ? editor.buttonTips[id] || undef : undef, + overflowText: editor.buttonTips[id].title || undef, + tabIndex:-1 + }; + } + + + if (me.enableFont && !Ext.isSafari2) { + fontSelectItem = Ext.widget('component', { + renderTpl: [ + '' + ], + renderData: { + cls: baseCSSPrefix + 'font-select', + fonts: me.fontFamilies, + defaultFont: me.defaultFont + }, + renderSelectors: { + selectEl: 'select' + }, + onDisable: function() { + var selectEl = this.selectEl; + if (selectEl) { + selectEl.dom.disabled = true; + } + Ext.Component.superclass.onDisable.apply(this, arguments); + }, + onEnable: function() { + var selectEl = this.selectEl; + if (selectEl) { + selectEl.dom.disabled = false; + } + Ext.Component.superclass.onEnable.apply(this, arguments); + } + }); + + items.push( + fontSelectItem, + '-' + ); + } + + if (me.enableFormat) { + items.push( + btn('bold'), + btn('italic'), + btn('underline') + ); + } + + if (me.enableFontSize) { + items.push( + '-', + btn('increasefontsize', false, me.adjustFont), + btn('decreasefontsize', false, me.adjustFont) + ); + } + + if (me.enableColors) { + items.push( + '-', { + itemId: 'forecolor', + cls: baseCSSPrefix + 'btn-icon', + iconCls: baseCSSPrefix + 'edit-forecolor', + overflowText: editor.buttonTips.forecolor.title, + tooltip: tipsEnabled ? editor.buttonTips.forecolor || undef : undef, + tabIndex:-1, + menu : Ext.widget('menu', { + plain: true, + items: [{ + xtype: 'colorpicker', + allowReselect: true, + focus: Ext.emptyFn, + value: '000000', + plain: true, + clickEvent: 'mousedown', + handler: function(cp, color) { + me.execCmd('forecolor', Ext.isWebKit || Ext.isIE ? '#'+color : color); + me.deferFocus(); + this.up('menu').hide(); + } + }] + }) + }, { + itemId: 'backcolor', + cls: baseCSSPrefix + 'btn-icon', + iconCls: baseCSSPrefix + 'edit-backcolor', + overflowText: editor.buttonTips.backcolor.title, + tooltip: tipsEnabled ? editor.buttonTips.backcolor || undef : undef, + tabIndex:-1, + menu : Ext.widget('menu', { + plain: true, + items: [{ + xtype: 'colorpicker', + focus: Ext.emptyFn, + value: 'FFFFFF', + plain: true, + allowReselect: true, + clickEvent: 'mousedown', + handler: function(cp, color) { + if (Ext.isGecko) { + me.execCmd('useCSS', false); + me.execCmd('hilitecolor', color); + me.execCmd('useCSS', true); + me.deferFocus(); + } else { + me.execCmd(Ext.isOpera ? 'hilitecolor' : 'backcolor', Ext.isWebKit || Ext.isIE ? '#'+color : color); + me.deferFocus(); + } + this.up('menu').hide(); + } + }] + }) + } + ); + } + + if (me.enableAlignments) { + items.push( + '-', + btn('justifyleft'), + btn('justifycenter'), + btn('justifyright') + ); + } + + if (!Ext.isSafari2) { + if (me.enableLinks) { + items.push( + '-', + btn('createlink', false, me.createLink) + ); + } + + if (me.enableLists) { + items.push( + '-', + btn('insertorderedlist'), + btn('insertunorderedlist') + ); + } + if (me.enableSourceEdit) { + items.push( + '-', + btn('sourceedit', true, function(btn){ + me.toggleSourceEdit(!me.sourceEditMode); + }) + ); + } + } + + // build the toolbar + toolbar = Ext.widget('toolbar', { + renderTo: me.toolbarWrap, + enableOverflow: true, + items: items + }); + + if (fontSelectItem) { + me.fontSelect = fontSelectItem.selectEl; + + me.mon(me.fontSelect, 'change', function(){ + me.relayCmd('fontname', me.fontSelect.dom.value); + me.deferFocus(); + }); + } + + // stop form submits + me.mon(toolbar.el, 'click', function(e){ + e.preventDefault(); + }); + + me.toolbar = toolbar; + }, + + onDisable: function() { + this.bodyEl.mask(); + this.callParent(arguments); + }, + + onEnable: function() { + this.bodyEl.unmask(); + this.callParent(arguments); + }, + + /** + * Sets the read only state of this field. + * @param {Boolean} readOnly Whether the field should be read only. + */ + setReadOnly: function(readOnly) { + var me = this, + textareaEl = me.textareaEl, + iframeEl = me.iframeEl, + body; + + me.readOnly = readOnly; + + if (textareaEl) { + textareaEl.dom.readOnly = readOnly; + } + + if (me.initialized) { + body = me.getEditorBody(); + if (Ext.isIE) { + // Hide the iframe while setting contentEditable so it doesn't grab focus + iframeEl.setDisplayed(false); + body.contentEditable = !readOnly; + iframeEl.setDisplayed(true); + } else { + me.setDesignMode(!readOnly); + } + if (body) { + body.style.cursor = readOnly ? 'default' : 'text'; + } + me.disableItems(readOnly); + } + }, + + /** + * Protected method that will not generally be called directly. It + * is called when the editor initializes the iframe with HTML contents. Override this method if you + * want to change the initialization markup of the iframe (e.g. to add stylesheets). + * + * Note: IE8-Standards has unwanted scroller behavior, so the default meta tag forces IE7 compatibility. + * Also note that forcing IE7 mode works when the page is loaded normally, but if you are using IE's Web + * Developer Tools to manually set the document mode, that will take precedence and override what this + * code sets by default. This can be confusing when developing, but is not a user-facing issue. + */ + getDocMarkup: function() { + var me = this, + h = me.iframeEl.getHeight() - me.iframePad * 2; + return Ext.String.format('', me.iframePad, h); + }, + + // private + getEditorBody: function() { + var doc = this.getDoc(); + return doc.body || doc.documentElement; + }, + + // private + getDoc: function() { + return (!Ext.isIE && this.iframeEl.dom.contentDocument) || this.getWin().document; + }, + + // private + getWin: function() { + return Ext.isIE ? this.iframeEl.dom.contentWindow : window.frames[this.iframeEl.dom.name]; + }, + + // private + onRender: function() { + var me = this, + renderSelectors = me.renderSelectors; + + Ext.applyIf(renderSelectors, me.getLabelableSelectors()); + + Ext.applyIf(renderSelectors, { + toolbarWrap: 'div.' + Ext.baseCSSPrefix + 'html-editor-tb', + iframeEl: 'iframe', + textareaEl: 'textarea' + }); + + me.callParent(arguments); + + me.textareaEl.dom.value = me.value || ''; + + // Start polling for when the iframe document is ready to be manipulated + me.monitorTask = Ext.TaskManager.start({ + run: me.checkDesignMode, + scope: me, + interval:100 + }); + + me.createToolbar(me); + me.disableItems(true); + }, + + initRenderTpl: function() { + var me = this; + if (!me.hasOwnProperty('renderTpl')) { + me.renderTpl = me.getTpl('labelableRenderTpl'); + } + return me.callParent(); + }, + + initRenderData: function() { + return Ext.applyIf(this.callParent(), this.getLabelableRenderData()); + }, + + getSubTplData: function() { + var cssPrefix = Ext.baseCSSPrefix; + return { + toolbarWrapCls: cssPrefix + 'html-editor-tb', + textareaCls: cssPrefix + 'hidden', + iframeName: Ext.id(), + iframeSrc: Ext.SSL_SECURE_URL, + size: 'height:100px;' + }; + }, + + getSubTplMarkup: function() { + return this.getTpl('fieldSubTpl').apply(this.getSubTplData()); + }, + + getBodyNaturalWidth: function() { + return 565; + }, + + initFrameDoc: function() { + var me = this, + doc, task; + + Ext.TaskManager.stop(me.monitorTask); + + doc = me.getDoc(); + me.win = me.getWin(); + + doc.open(); + doc.write(me.getDocMarkup()); + doc.close(); + + task = { // must defer to wait for browser to be ready + run: function() { + var doc = me.getDoc(); + if (doc.body || doc.readyState === 'complete') { + Ext.TaskManager.stop(task); + me.setDesignMode(true); + Ext.defer(me.initEditor, 10, me); + } + }, + interval : 10, + duration:10000, + scope: me + }; + Ext.TaskManager.start(task); + }, + + checkDesignMode: function() { + var me = this, + doc = me.getDoc(); + if (doc && (!doc.editorInitialized || me.getDesignMode() !== 'on')) { + me.initFrameDoc(); + } + }, + + /* private + * set current design mode. To enable, mode can be true or 'on', off otherwise + */ + setDesignMode: function(mode) { + var me = this, + doc = me.getDoc(); + if (doc) { + if (me.readOnly) { + mode = false; + } + doc.designMode = (/on|true/i).test(String(mode).toLowerCase()) ?'on':'off'; + } + }, + + // private + getDesignMode: function() { + var doc = this.getDoc(); + return !doc ? '' : String(doc.designMode).toLowerCase(); + }, + + disableItems: function(disabled) { + this.getToolbar().items.each(function(item){ + if(item.getItemId() !== 'sourceedit'){ + item.setDisabled(disabled); + } + }); + }, + + /** + * Toggles the editor between standard and source edit mode. + * @param {Boolean} sourceEditMode (optional) True for source edit, false for standard + */ + toggleSourceEdit: function(sourceEditMode) { + var me = this, + iframe = me.iframeEl, + textarea = me.textareaEl, + hiddenCls = Ext.baseCSSPrefix + 'hidden', + btn = me.getToolbar().getComponent('sourceedit'); + + if (!Ext.isBoolean(sourceEditMode)) { + sourceEditMode = !me.sourceEditMode; + } + me.sourceEditMode = sourceEditMode; + + if (btn.pressed !== sourceEditMode) { + btn.toggle(sourceEditMode); + } + if (sourceEditMode) { + me.disableItems(true); + me.syncValue(); + iframe.addCls(hiddenCls); + textarea.removeCls(hiddenCls); + textarea.dom.removeAttribute('tabIndex'); + textarea.focus(); + } + else { + if (me.initialized) { + me.disableItems(me.readOnly); + } + me.pushValue(); + iframe.removeCls(hiddenCls); + textarea.addCls(hiddenCls); + textarea.dom.setAttribute('tabIndex', -1); + me.deferFocus(); + } + me.fireEvent('editmodechange', me, sourceEditMode); + me.doComponentLayout(); + }, + + // private used internally + createLink : function() { + var url = prompt(this.createLinkText, this.defaultLinkValue); + if (url && url !== 'http:/'+'/') { + this.relayCmd('createlink', url); + } + }, + + clearInvalid: Ext.emptyFn, + + // docs inherit from Field + setValue: function(value) { + var me = this, + textarea = me.textareaEl; + me.mixins.field.setValue.call(me, value); + if (value === null || value === undefined) { + value = ''; + } + if (textarea) { + textarea.dom.value = value; + } + me.pushValue(); + return this; + }, + + /** + * Protected method that will not generally be called directly. If you need/want + * custom HTML cleanup, this is the method you should override. + * @param {String} html The HTML to be cleaned + * @return {String} The cleaned HTML + */ + cleanHtml: function(html) { + html = String(html); + if (Ext.isWebKit) { // strip safari nonsense + html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, ''); + } + + /* + * Neat little hack. Strips out all the non-digit characters from the default + * value and compares it to the character code of the first character in the string + * because it can cause encoding issues when posted to the server. + */ + if (html.charCodeAt(0) === this.defaultValue.replace(/\D/g, '')) { + html = html.substring(1); + } + return html; + }, + + /** + * @protected method that will not generally be called directly. Syncs the contents + * of the editor iframe with the textarea. + */ + syncValue : function(){ + var me = this, + body, html, bodyStyle, match; + if (me.initialized) { + body = me.getEditorBody(); + html = body.innerHTML; + if (Ext.isWebKit) { + bodyStyle = body.getAttribute('style'); // Safari puts text-align styles on the body element! + match = bodyStyle.match(/text-align:(.*?);/i); + if (match && match[1]) { + html = '
+{
+ bold : {
+ title: 'Bold (Ctrl+B)',
+ text: 'Make the selected text bold.',
+ cls: 'x-html-editor-tip'
+ },
+ italic : {
+ title: 'Italic (Ctrl+I)',
+ text: 'Make the selected text italic.',
+ cls: 'x-html-editor-tip'
+ },
+ ...
+
+ * @type Object
+ */
+ buttonTips : {
+ bold : {
+ title: 'Bold (Ctrl+B)',
+ text: 'Make the selected text bold.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ italic : {
+ title: 'Italic (Ctrl+I)',
+ text: 'Make the selected text italic.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ underline : {
+ title: 'Underline (Ctrl+U)',
+ text: 'Underline the selected text.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ increasefontsize : {
+ title: 'Grow Text',
+ text: 'Increase the font size.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ decreasefontsize : {
+ title: 'Shrink Text',
+ text: 'Decrease the font size.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ backcolor : {
+ title: 'Text Highlight Color',
+ text: 'Change the background color of the selected text.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ forecolor : {
+ title: 'Font Color',
+ text: 'Change the color of the selected text.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ justifyleft : {
+ title: 'Align Text Left',
+ text: 'Align text to the left.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ justifycenter : {
+ title: 'Center Text',
+ text: 'Center text in the editor.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ justifyright : {
+ title: 'Align Text Right',
+ text: 'Align text to the right.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ insertunorderedlist : {
+ title: 'Bullet List',
+ text: 'Start a bulleted list.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ insertorderedlist : {
+ title: 'Numbered List',
+ text: 'Start a numbered list.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ createlink : {
+ title: 'Hyperlink',
+ text: 'Make the selected text a hyperlink.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ sourceedit : {
+ title: 'Source Edit',
+ text: 'Switch to source editing mode.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ }
+ }
+
+ // hide stuff that is not compatible
+ /**
+ * @event blur
+ * @hide
+ */
+ /**
+ * @event change
+ * @hide
+ */
+ /**
+ * @event focus
+ * @hide
+ */
+ /**
+ * @event specialkey
+ * @hide
+ */
+ /**
+ * @cfg {String} fieldCls @hide
+ */
+ /**
+ * @cfg {String} focusCls @hide
+ */
+ /**
+ * @cfg {String} autoCreate @hide
+ */
+ /**
+ * @cfg {String} inputType @hide
+ */
+ /**
+ * @cfg {String} invalidCls @hide
+ */
+ /**
+ * @cfg {String} invalidText @hide
+ */
+ /**
+ * @cfg {String} msgFx @hide
+ */
+ /**
+ * @cfg {Boolean} allowDomMove @hide
+ */
+ /**
+ * @cfg {String} applyTo @hide
+ */
+ /**
+ * @cfg {String} readOnly @hide
+ */
+ /**
+ * @cfg {String} tabIndex @hide
+ */
+ /**
+ * @method validate
+ * @hide
+ */
+});