/ext-4.0.7/src/form/field/HtmlEditor.js
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) ? ' ' : '​',
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(' ');
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',' ');
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