]> git.proxmox.com Git - extjs.git/blame - extjs/classic/classic/src/form/field/HtmlEditor.js
add extjs 6.0.1 sources
[extjs.git] / extjs / classic / classic / src / form / field / HtmlEditor.js
CommitLineData
6527f429
DM
1/**\r
2 * Provides a lightweight HTML Editor component. Some toolbar features are not supported by Safari and will be\r
3 * automatically hidden when needed. These are noted in the config options where appropriate.\r
4 *\r
5 * The editor's toolbar buttons have tooltips defined in the {@link #buttonTips} property, but they are not\r
6 * enabled by default unless the global {@link Ext.tip.QuickTipManager} singleton is\r
7 * {@link Ext.tip.QuickTipManager#init initialized}.\r
8 *\r
9 * An Editor is a sensitive component that can't be used in all spots standard fields can be used. Putting an\r
10 * Editor within any element that has display set to 'none' can cause problems in Safari and Firefox due to their\r
11 * default iframe reloading bugs.\r
12 *\r
13 * # Example usage\r
14 *\r
15 * Simple example rendered with default options:\r
16 *\r
17 * @example\r
18 * Ext.tip.QuickTipManager.init(); // enable tooltips\r
19 * Ext.create('Ext.form.HtmlEditor', {\r
20 * width: 580,\r
21 * height: 250,\r
22 * renderTo: Ext.getBody()\r
23 * });\r
24 *\r
25 * Passed via xtype into a container and with custom options:\r
26 *\r
27 * @example\r
28 * Ext.tip.QuickTipManager.init(); // enable tooltips\r
29 * new Ext.panel.Panel({\r
30 * title: 'HTML Editor',\r
31 * renderTo: Ext.getBody(),\r
32 * width: 550,\r
33 * height: 250,\r
34 * frame: true,\r
35 * layout: 'fit',\r
36 * items: {\r
37 * xtype: 'htmleditor',\r
38 * enableColors: false,\r
39 * enableAlignments: false\r
40 * }\r
41 * });\r
42 * \r
43 * # Reflow issues\r
44 * \r
45 * In some browsers, a layout reflow will cause the underlying editor iframe to be reset. This\r
46 * is most commonly seen when using the editor in collapsed panels with animation. In these cases\r
47 * it is best to avoid animation. More information can be found here: https://bugzilla.mozilla.org/show_bug.cgi?id=90268 \r
48 */\r
49Ext.define('Ext.form.field.HtmlEditor', {\r
50 extend: 'Ext.form.FieldContainer',\r
51 mixins: {\r
52 field: 'Ext.form.field.Field'\r
53 },\r
54 alias: 'widget.htmleditor',\r
55 alternateClassName: 'Ext.form.HtmlEditor',\r
56 requires: [\r
57 'Ext.tip.QuickTipManager',\r
58 'Ext.picker.Color',\r
59 'Ext.layout.container.VBox',\r
60 'Ext.toolbar.Item',\r
61 'Ext.toolbar.Toolbar',\r
62 'Ext.util.Format',\r
63 'Ext.layout.component.field.HtmlEditor',\r
64 'Ext.util.TaskManager',\r
65 'Ext.layout.container.boxOverflow.Menu'\r
66 ],\r
67 \r
68 focusable: true,\r
69 componentLayout: 'htmleditor',\r
70\r
71 /**\r
72 * @private\r
73 */\r
74 textareaCls: Ext.baseCSSPrefix + 'htmleditor-textarea',\r
75\r
76 componentTpl: [\r
77 '{beforeTextAreaTpl}',\r
78 '<textarea id="{id}-textareaEl" data-ref="textareaEl" name="{name}" tabindex="-1" {inputAttrTpl}',\r
79 ' class="{textareaCls}" autocomplete="off">',\r
80 '{[Ext.util.Format.htmlEncode(values.value)]}',\r
81 '</textarea>',\r
82 '{afterTextAreaTpl}',\r
83 '{beforeIFrameTpl}',\r
84 '<iframe id="{id}-iframeEl" data-ref="iframeEl" name="{iframeName}" frameBorder="0" {iframeAttrTpl}',\r
85 ' src="{iframeSrc}" class="{iframeCls}"></iframe>',\r
86 '{afterIFrameTpl}',\r
87 {\r
88 disableFormats: true\r
89 }\r
90 ],\r
91 \r
92 stretchInputElFixed: true,\r
93\r
94 subTplInsertions: [\r
95 /**\r
96 * @cfg {String/Array/Ext.XTemplate} beforeTextAreaTpl\r
97 * An optional string or `XTemplate` configuration to insert in the field markup\r
98 * before the textarea element. If an `XTemplate` is used, the component's\r
99 * {@link Ext.form.field.Base#getSubTplData subTpl data} serves as the context.\r
100 */\r
101 'beforeTextAreaTpl',\r
102\r
103 /**\r
104 * @cfg {String/Array/Ext.XTemplate} afterTextAreaTpl\r
105 * An optional string or `XTemplate` configuration to insert in the field markup\r
106 * after the textarea element. If an `XTemplate` is used, the component's\r
107 * {@link Ext.form.field.Base#getSubTplData subTpl data} serves as the context.\r
108 */\r
109 'afterTextAreaTpl',\r
110\r
111 /**\r
112 * @cfg {String/Array/Ext.XTemplate} beforeIFrameTpl\r
113 * An optional string or `XTemplate` configuration to insert in the field markup\r
114 * before the iframe element. If an `XTemplate` is used, the component's\r
115 * {@link Ext.form.field.Base#getSubTplData subTpl data} serves as the context.\r
116 */\r
117 'beforeIFrameTpl',\r
118\r
119 /**\r
120 * @cfg {String/Array/Ext.XTemplate} afterIFrameTpl\r
121 * An optional string or `XTemplate` configuration to insert in the field markup\r
122 * after the iframe element. If an `XTemplate` is used, the component's\r
123 * {@link Ext.form.field.Base#getSubTplData subTpl data} serves as the context.\r
124 */\r
125 'afterIFrameTpl',\r
126\r
127 /**\r
128 * @cfg {String/Array/Ext.XTemplate} iframeAttrTpl\r
129 * An optional string or `XTemplate` configuration to insert in the field markup\r
130 * inside the iframe element (as attributes). If an `XTemplate` is used, the component's\r
131 * {@link Ext.form.field.Base#getSubTplData subTpl data} serves as the context.\r
132 */\r
133 'iframeAttrTpl',\r
134\r
135 // inherited\r
136 'inputAttrTpl'\r
137 ],\r
138\r
139 /**\r
140 * @cfg {Boolean} enableFormat\r
141 * Enable the bold, italic and underline buttons\r
142 */\r
143 enableFormat: true,\r
144 /**\r
145 * @cfg {Boolean} enableFontSize\r
146 * Enable the increase/decrease font size buttons\r
147 */\r
148 enableFontSize: true,\r
149 /**\r
150 * @cfg {Boolean} enableColors\r
151 * Enable the fore/highlight color buttons\r
152 */\r
153 enableColors: true,\r
154 /**\r
155 * @cfg {Boolean} enableAlignments\r
156 * Enable the left, center, right alignment buttons\r
157 */\r
158 enableAlignments: true,\r
159 /**\r
160 * @cfg {Boolean} enableLists\r
161 * Enable the bullet and numbered list buttons. Not available in Safari 2.\r
162 */\r
163 enableLists: true,\r
164 /**\r
165 * @cfg {Boolean} enableSourceEdit\r
166 * Enable the switch to source edit button. Not available in Safari 2.\r
167 */\r
168 enableSourceEdit: true,\r
169 /**\r
170 * @cfg {Boolean} enableLinks\r
171 * Enable the create link button. Not available in Safari 2.\r
172 */\r
173 enableLinks: true,\r
174 /**\r
175 * @cfg {Boolean} enableFont\r
176 * Enable font selection. Not available in Safari 2.\r
177 */\r
178 enableFont: true,\r
179 //<locale>\r
180 /**\r
181 * @cfg {String} createLinkText\r
182 * The default text for the create link prompt\r
183 */\r
184 createLinkText: 'Please enter the URL for the link:',\r
185 //</locale>\r
186 /**\r
187 * @cfg {String} [defaultLinkValue='http://']\r
188 * The default value for the create link prompt\r
189 */\r
190 defaultLinkValue: 'http:/'+'/',\r
191 /**\r
192 * @cfg {String[]} fontFamilies\r
193 * An array of available font families\r
194 */\r
195 fontFamilies: [\r
196 'Arial',\r
197 'Courier New',\r
198 'Tahoma',\r
199 'Times New Roman',\r
200 'Verdana'\r
201 ],\r
202 /**\r
203 * @cfg {String} defaultValue\r
204 * A default value to be put into the editor to resolve focus issues.\r
205 *\r
206 * Defaults to (Non-breaking space) in Opera,\r
207 * (Zero-width space) in all other browsers.\r
208 */\r
209 defaultValue: Ext.isOpera ? '&#160;' : '&#8203;',\r
210\r
211 /**\r
212 * @private\r
213 */\r
214 extraFieldBodyCls: Ext.baseCSSPrefix + 'html-editor-wrap',\r
215\r
216 /**\r
217 * @cfg {String} defaultButtonUI\r
218 * A default {@link Ext.Component#ui ui} to use for the HtmlEditor's toolbar\r
219 * {@link Ext.button.Button Buttons}\r
220 */\r
221 defaultButtonUI: 'default-toolbar',\r
222\r
223 /**\r
224 * @private\r
225 */\r
226 initialized: false,\r
227\r
228 /**\r
229 * @private\r
230 */\r
231 activated: false,\r
232\r
233 /**\r
234 * @private\r
235 */\r
236 sourceEditMode: false,\r
237\r
238 /**\r
239 * @private\r
240 */\r
241 iframePad:3,\r
242\r
243 /**\r
244 * @private\r
245 */\r
246 hideMode:'offsets',\r
247\r
248 maskOnDisable: true,\r
249\r
250 containerElCls: Ext.baseCSSPrefix + 'html-editor-container',\r
251\r
252 // This will strip any number of single or double quotes (in any order) from a string at the anchors.\r
253 reStripQuotes: /^['"]*|['"]*$/g,\r
254 \r
255 textAlignRE: /text-align:(.*?);/i,\r
256 safariNonsenseRE: /\sclass="(?:Apple-style-span|Apple-tab-span|khtml-block-placeholder)"/gi,\r
257 nonDigitsRE: /\D/g,\r
258\r
259 /**\r
260 * @event initialize\r
261 * Fires when the editor is fully initialized (including the iframe)\r
262 * @param {Ext.form.field.HtmlEditor} this\r
263 */\r
264\r
265 /**\r
266 * @event activate\r
267 * Fires when the editor is first receives the focus. Any insertion must wait until after this event.\r
268 * @param {Ext.form.field.HtmlEditor} this\r
269 */\r
270\r
271 /**\r
272 * @event beforesync\r
273 * Fires before the textarea is updated with content from the editor iframe. Return false to cancel the\r
274 * sync.\r
275 * @param {Ext.form.field.HtmlEditor} this\r
276 * @param {String} html\r
277 */\r
278\r
279 /**\r
280 * @event beforepush\r
281 * Fires before the iframe editor is updated with content from the textarea. Return false to cancel the\r
282 * push.\r
283 * @param {Ext.form.field.HtmlEditor} this\r
284 * @param {String} html\r
285 */\r
286\r
287 /**\r
288 * @event sync\r
289 * Fires when the textarea is updated with content from the editor iframe.\r
290 * @param {Ext.form.field.HtmlEditor} this\r
291 * @param {String} html\r
292 */\r
293\r
294 /**\r
295 * @event push\r
296 * Fires when the iframe editor is updated with content from the textarea.\r
297 * @param {Ext.form.field.HtmlEditor} this\r
298 * @param {String} html\r
299 */\r
300\r
301 /**\r
302 * @event editmodechange\r
303 * Fires when the editor switches edit modes\r
304 * @param {Ext.form.field.HtmlEditor} this\r
305 * @param {Boolean} sourceEdit True if source edit, false if standard editing.\r
306 */\r
307\r
308 /**\r
309 * @private\r
310 */\r
311 initComponent: function(){\r
312 var me = this;\r
313\r
314 me.items = [me.createToolbar(), me.createInputCmp()];\r
315\r
316 me.layout = {\r
317 type: 'vbox',\r
318 align: 'stretch'\r
319 };\r
320\r
321 // No value set, we must report empty string\r
322 if (me.value == null) {\r
323 me.value = '';\r
324 }\r
325\r
326 me.callParent(arguments);\r
327 me.initField();\r
328 },\r
329 \r
330 createInputCmp: function(){\r
331 this.inputCmp = Ext.widget(this.getInputCmpCfg());\r
332 return this.inputCmp;\r
333 },\r
334 \r
335 getInputCmpCfg: function(){\r
336 var me = this,\r
337 id = me.id + '-inputCmp',\r
338 data = {\r
339 id : id,\r
340 name : me.name,\r
341 textareaCls : me.textareaCls + ' ' + Ext.baseCSSPrefix + 'hidden',\r
342 value : me.value,\r
343 iframeName : Ext.id(),\r
344 iframeSrc : Ext.SSL_SECURE_URL,\r
345 iframeCls : Ext.baseCSSPrefix + 'htmleditor-iframe'\r
346 };\r
347 \r
348 me.getInsertionRenderData(data, me.subTplInsertions);\r
349 \r
350 return {\r
351 flex: 1,\r
352 xtype: 'component',\r
353 tpl: me.getTpl('componentTpl'),\r
354 childEls: ['iframeEl', 'textareaEl'],\r
355 id: id, \r
356 cls: Ext.baseCSSPrefix + 'html-editor-input',\r
357 data: data \r
358 }; \r
359 },\r
360\r
361 /**\r
362 * Called when the editor creates its toolbar. Override this method if you need to\r
363 * add custom toolbar buttons.\r
364 * @param {Ext.form.field.HtmlEditor} editor\r
365 * @protected\r
366 */\r
367 createToolbar: function(){\r
368 this.toolbar = Ext.widget(this.getToolbarCfg());\r
369 return this.toolbar;\r
370 },\r
371 \r
372 getToolbarCfg: function(){\r
373 var me = this,\r
374 items = [], i,\r
375 tipsEnabled = Ext.quickTipsActive && Ext.tip.QuickTipManager.isEnabled(),\r
376 baseCSSPrefix = Ext.baseCSSPrefix,\r
377 fontSelectItem, undef;\r
378\r
379 function btn(id, toggle, handler){\r
380 return {\r
381 itemId: id,\r
382 cls: baseCSSPrefix + 'btn-icon',\r
383 iconCls: baseCSSPrefix + 'edit-'+id,\r
384 enableToggle:toggle !== false,\r
385 scope: me,\r
386 handler:handler||me.relayBtnCmd,\r
387 clickEvent: 'mousedown',\r
388 tooltip: tipsEnabled ? me.buttonTips[id] || undef : undef,\r
389 overflowText: me.buttonTips[id].title || undef,\r
390 tabIndex: -1\r
391 };\r
392 }\r
393\r
394\r
395 if (me.enableFont && !Ext.isSafari2) {\r
396 fontSelectItem = Ext.widget('component', {\r
397 itemId: 'fontSelect',\r
398 renderTpl: [\r
399 '<select id="{id}-selectEl" data-ref="selectEl" class="' + baseCSSPrefix + 'font-select">',\r
400 '</select>'\r
401 ],\r
402 childEls: ['selectEl'],\r
403 afterRender: function() {\r
404 me.fontSelect = this.selectEl;\r
405 Ext.Component.prototype.afterRender.apply(this, arguments);\r
406 },\r
407 onDisable: function() {\r
408 var selectEl = this.selectEl;\r
409 if (selectEl) {\r
410 selectEl.dom.disabled = true;\r
411 }\r
412 Ext.Component.prototype.onDisable.apply(this, arguments);\r
413 },\r
414 onEnable: function() {\r
415 var selectEl = this.selectEl;\r
416 if (selectEl) {\r
417 selectEl.dom.disabled = false;\r
418 }\r
419 Ext.Component.prototype.onEnable.apply(this, arguments);\r
420 },\r
421 listeners: {\r
422 change: function() {\r
423 me.win.focus();\r
424 me.relayCmd('fontName', me.fontSelect.dom.value);\r
425 me.deferFocus();\r
426 },\r
427 element: 'selectEl'\r
428 }\r
429 });\r
430\r
431 items.push(\r
432 fontSelectItem,\r
433 '-'\r
434 );\r
435 }\r
436\r
437 if (me.enableFormat) {\r
438 items.push(\r
439 btn('bold'),\r
440 btn('italic'),\r
441 btn('underline')\r
442 );\r
443 }\r
444\r
445 if (me.enableFontSize) {\r
446 items.push(\r
447 '-',\r
448 btn('increasefontsize', false, me.adjustFont),\r
449 btn('decreasefontsize', false, me.adjustFont)\r
450 );\r
451 }\r
452\r
453 if (me.enableColors) {\r
454 items.push(\r
455 '-', {\r
456 itemId: 'forecolor',\r
457 cls: baseCSSPrefix + 'btn-icon',\r
458 iconCls: baseCSSPrefix + 'edit-forecolor',\r
459 overflowText: me.buttonTips.forecolor.title,\r
460 tooltip: tipsEnabled ? me.buttonTips.forecolor || undef : undef,\r
461 tabIndex:-1,\r
462 menu: Ext.widget('menu', {\r
463 plain: true,\r
464\r
465 items: [{\r
466 xtype: 'colorpicker',\r
467 allowReselect: true,\r
468 focus: Ext.emptyFn,\r
469 value: '000000',\r
470 plain: true,\r
471 clickEvent: 'mousedown',\r
472 handler: function(cp, color) {\r
473 me.relayCmd('forecolor', Ext.isWebKit || Ext.isIE ? '#'+color : color);\r
474 this.up('menu').hide();\r
475 }\r
476 }]\r
477 })\r
478 }, {\r
479 itemId: 'backcolor',\r
480 cls: baseCSSPrefix + 'btn-icon',\r
481 iconCls: baseCSSPrefix + 'edit-backcolor',\r
482 overflowText: me.buttonTips.backcolor.title,\r
483 tooltip: tipsEnabled ? me.buttonTips.backcolor || undef : undef,\r
484 tabIndex:-1,\r
485 menu: Ext.widget('menu', {\r
486 plain: true,\r
487\r
488 items: [{\r
489 xtype: 'colorpicker',\r
490 focus: Ext.emptyFn,\r
491 value: 'FFFFFF',\r
492 plain: true,\r
493 allowReselect: true,\r
494 clickEvent: 'mousedown',\r
495 handler: function(cp, color) {\r
496 if (Ext.isGecko) {\r
497 me.execCmd('useCSS', false);\r
498 me.execCmd('hilitecolor', '#'+color);\r
499 me.execCmd('useCSS', true);\r
500 me.deferFocus();\r
501 } else {\r
502 me.relayCmd(Ext.isOpera ? 'hilitecolor' : 'backcolor', Ext.isWebKit || Ext.isIE || Ext.isOpera ? '#'+color : color);\r
503 }\r
504 this.up('menu').hide();\r
505 }\r
506 }]\r
507 })\r
508 }\r
509 );\r
510 }\r
511\r
512 if (me.enableAlignments) {\r
513 items.push(\r
514 '-',\r
515 btn('justifyleft'),\r
516 btn('justifycenter'),\r
517 btn('justifyright')\r
518 );\r
519 }\r
520\r
521 if (!Ext.isSafari2) {\r
522 if (me.enableLinks) {\r
523 items.push(\r
524 '-',\r
525 btn('createlink', false, me.createLink)\r
526 );\r
527 }\r
528\r
529 if (me.enableLists) {\r
530 items.push(\r
531 '-',\r
532 btn('insertorderedlist'),\r
533 btn('insertunorderedlist')\r
534 );\r
535 }\r
536 if (me.enableSourceEdit) {\r
537 items.push(\r
538 '-',\r
539 btn('sourceedit', true, function(){\r
540 me.toggleSourceEdit(!me.sourceEditMode);\r
541 })\r
542 );\r
543 }\r
544 }\r
545 \r
546 // Everything starts disabled.\r
547 for (i = 0; i < items.length; i++) {\r
548 if (items[i].itemId !== 'sourceedit') {\r
549 items[i].disabled = true;\r
550 }\r
551 }\r
552\r
553 // build the toolbar\r
554 // Automatically rendered in Component.afterRender's renderChildren call\r
555 return {\r
556 xtype: 'toolbar',\r
557 defaultButtonUI: me.defaultButtonUI,\r
558 cls: Ext.baseCSSPrefix + 'html-editor-tb',\r
559 enableOverflow: true,\r
560 items: items,\r
561\r
562 // stop form submits\r
563 listeners: {\r
564 click: function(e){\r
565 e.preventDefault();\r
566 },\r
567 element: 'el'\r
568 }\r
569 }; \r
570 },\r
571 \r
572 getMaskTarget: function(){\r
573 // Can't be the body td directly because of issues with absolute positioning\r
574 // inside td's in FF\r
575 return Ext.isGecko ? this.inputCmp.el : this.bodyEl;\r
576 },\r
577\r
578 /**\r
579 * Sets the read only state of this field.\r
580 * @param {Boolean} readOnly Whether the field should be read only.\r
581 */\r
582 setReadOnly: function(readOnly) {\r
583 var me = this,\r
584 textareaEl = me.textareaEl,\r
585 iframeEl = me.iframeEl,\r
586 body;\r
587\r
588 me.readOnly = readOnly;\r
589\r
590 if (textareaEl) {\r
591 textareaEl.dom.readOnly = readOnly;\r
592 }\r
593\r
594 if (me.initialized) {\r
595 body = me.getEditorBody();\r
596 if (Ext.isIE) {\r
597 // Hide the iframe while setting contentEditable so it doesn't grab focus\r
598 iframeEl.setDisplayed(false);\r
599 body.contentEditable = !readOnly;\r
600 iframeEl.setDisplayed(true);\r
601 } else {\r
602 me.setDesignMode(!readOnly);\r
603 }\r
604 if (body) {\r
605 body.style.cursor = readOnly ? 'default' : 'text';\r
606 }\r
607 me.disableItems(readOnly);\r
608 }\r
609 },\r
610\r
611 /**\r
612 * Called when the editor initializes the iframe with HTML contents. Override this method if you\r
613 * want to change the initialization markup of the iframe (e.g. to add stylesheets).\r
614 *\r
615 * **Note:** IE8-Standards has unwanted scroller behavior, so the default meta tag forces IE7 compatibility.\r
616 * Also note that forcing IE7 mode works when the page is loaded normally, but if you are using IE's Web\r
617 * Developer Tools to manually set the document mode, that will take precedence and override what this\r
618 * code sets by default. This can be confusing when developing, but is not a user-facing issue.\r
619 * @protected\r
620 */\r
621 getDocMarkup: function() {\r
622 var me = this,\r
623 h = me.iframeEl.getHeight() - me.iframePad * 2;\r
624\r
625 // - IE9+ require a strict doctype otherwise text outside visible area can't be selected.\r
626 // - Opera inserts <P> tags on Return key, so P margins must be removed to avoid double line-height.\r
627 // - On browsers other than IE, the font is not inherited by the IFRAME so it must be specified.\r
628 return Ext.String.format(\r
629 '<!DOCTYPE html>' +\r
630 '<html><head><style type="text/css">' +\r
631 (Ext.isOpera || Ext.isIE ? 'p{margin:0;}' : '') +\r
632 'body{border:0;margin:0;padding:{0}px;direction:' + (me.rtl ? 'rtl;' : 'ltr;') +\r
633 (Ext.isIE8 ? Ext.emptyString : 'min-') +\r
634 'height:{1}px;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;cursor:text;background-color:white;' +\r
635 (Ext.isIE ? '' : 'font-size:12px;font-family:{2}') +\r
636 '}</style></head><body></body></html>',\r
637 me.iframePad, h, me.defaultFont);\r
638 },\r
639\r
640 /**\r
641 * @private\r
642 */\r
643 getEditorBody: function() {\r
644 var doc = this.getDoc();\r
645 return doc.body || doc.documentElement;\r
646 },\r
647\r
648 /**\r
649 * @private\r
650 */\r
651 getDoc: function() {\r
652 return this.iframeEl.dom.contentDocument || this.getWin().document;\r
653 },\r
654\r
655 /**\r
656 * @private\r
657 */\r
658 getWin: function() {\r
659 // using window.frames[id] to access the the iframe's window object in FF creates\r
660 // a global variable with name == id in the global scope that references the iframe\r
661 // window. This is undesirable for unit testing because that global variable\r
662 // is readonly and cannot be deleted. To avoid this, we use contentWindow if it\r
663 // is available (and it is in all supported browsers at the time of this writing)\r
664 // and fall back to window.frames if contentWindow is not available.\r
665 return this.iframeEl.dom.contentWindow || window.frames[this.iframeEl.dom.name];\r
666 },\r
667 \r
668 initDefaultFont: function(){\r
669 // It's not ideal to do this here since it's a write phase, but we need to know\r
670 // what the font used in the textarea is so that we can setup the appropriate font\r
671 // options in the select box. The select box will reflow once we populate it, so we want\r
672 // to do so before we layout the first time.\r
673 \r
674 var me = this,\r
675 selIdx = 0,\r
676 fonts, font, select,\r
677 option, i, len, lower;\r
678 \r
679 if (!me.defaultFont) {\r
680 font = me.textareaEl.getStyle('font-family');\r
681 font = Ext.String.capitalize(font.split(',')[0]);\r
682 fonts = Ext.Array.clone(me.fontFamilies);\r
683 Ext.Array.include(fonts, font);\r
684 fonts.sort();\r
685 me.defaultFont = font;\r
686 \r
687 select = me.down('#fontSelect').selectEl.dom;\r
688 for (i = 0, len = fonts.length; i < len; ++i) {\r
689 font = fonts[i];\r
690 lower = font.toLowerCase();\r
691 option = new Option(font, lower);\r
692 if (font === me.defaultFont) {\r
693 selIdx = i;\r
694 }\r
695 option.style.fontFamily = lower;\r
696 \r
697 if (Ext.isIE) {\r
698 select.add(option);\r
699 } else {\r
700 select.options.add(option); \r
701 }\r
702 }\r
703 // Old IE versions have a problem if we set the selected property\r
704 // in the loop, so set it after.\r
705 select.options[selIdx].selected = true;\r
706 } \r
707 },\r
708 \r
709 isEqual: function(value1, value2){\r
710 return this.isEqualAsString(value1, value2);\r
711 },\r
712\r
713 /**\r
714 * @private\r
715 */\r
716 afterRender: function() {\r
717 var me = this,\r
718 inputCmp = me.inputCmp;\r
719\r
720 me.callParent(arguments);\r
721 \r
722 me.iframeEl = inputCmp.iframeEl;\r
723 me.textareaEl = inputCmp.textareaEl;\r
724 \r
725 // The input element is interrogated by the layout to extract height when labelAlign is 'top'\r
726 // It must be set, and then switched between the iframe and the textarea\r
727 me.inputEl = me.iframeEl;\r
728\r
729 if (me.enableFont) { \r
730 me.initDefaultFont();\r
731 }\r
732\r
733 // Start polling for when the iframe document is ready to be manipulated\r
734 me.monitorTask = Ext.TaskManager.start({\r
735 run: me.checkDesignMode,\r
736 scope: me,\r
737 interval: 100\r
738 });\r
739 },\r
740\r
741 initFrameDoc: function() {\r
742 var me = this,\r
743 doc, task;\r
744\r
745 Ext.TaskManager.stop(me.monitorTask);\r
746\r
747 doc = me.getDoc();\r
748 me.win = me.getWin();\r
749\r
750 doc.open();\r
751 doc.write(me.getDocMarkup());\r
752 doc.close();\r
753\r
754 task = { // must defer to wait for browser to be ready\r
755 run: function() {\r
756 var doc = me.getDoc();\r
757 if (doc.body || doc.readyState === 'complete') {\r
758 Ext.TaskManager.stop(task);\r
759 me.setDesignMode(true);\r
760 Ext.defer(me.initEditor, 10, me);\r
761 }\r
762 },\r
763 interval: 10,\r
764 duration:10000,\r
765 scope: me\r
766 };\r
767 Ext.TaskManager.start(task);\r
768 },\r
769\r
770 checkDesignMode: function() {\r
771 var me = this,\r
772 doc = me.getDoc();\r
773 if (doc && (!doc.editorInitialized || me.getDesignMode() !== 'on')) {\r
774 me.initFrameDoc();\r
775 }\r
776 },\r
777\r
778 /**\r
779 * @private\r
780 * Sets current design mode. To enable, mode can be true or 'on', off otherwise\r
781 */\r
782 setDesignMode: function(mode) {\r
783 var me = this,\r
784 doc = me.getDoc();\r
785 if (doc) {\r
786 if (me.readOnly) {\r
787 mode = false;\r
788 }\r
789 doc.designMode = (/on|true/i).test(String(mode).toLowerCase()) ?'on':'off';\r
790 }\r
791 },\r
792\r
793 /**\r
794 * @private\r
795 */\r
796 getDesignMode: function() {\r
797 var doc = this.getDoc();\r
798 return !doc ? '' : String(doc.designMode).toLowerCase();\r
799 },\r
800\r
801 disableItems: function(disabled) {\r
802 var items = this.getToolbar().items.items,\r
803 i,\r
804 iLen = items.length,\r
805 item;\r
806\r
807 for (i = 0; i < iLen; i++) {\r
808 item = items[i];\r
809\r
810 if (item.getItemId() !== 'sourceedit') {\r
811 item.setDisabled(disabled);\r
812 }\r
813 }\r
814 },\r
815\r
816 /**\r
817 * Toggles the editor between standard and source edit mode.\r
818 * @param {Boolean} [sourceEditMode] True for source edit, false for standard\r
819 */\r
820 toggleSourceEdit: function(sourceEditMode) {\r
821 var me = this,\r
822 iframe = me.iframeEl,\r
823 textarea = me.textareaEl,\r
824 hiddenCls = Ext.baseCSSPrefix + 'hidden',\r
825 btn = me.getToolbar().getComponent('sourceedit');\r
826\r
827 if (!Ext.isBoolean(sourceEditMode)) {\r
828 sourceEditMode = !me.sourceEditMode;\r
829 }\r
830 me.sourceEditMode = sourceEditMode;\r
831\r
832 if (btn.pressed !== sourceEditMode) {\r
833 btn.toggle(sourceEditMode);\r
834 }\r
835 if (sourceEditMode) {\r
836 me.disableItems(true);\r
837 me.syncValue();\r
838 iframe.addCls(hiddenCls);\r
839 textarea.removeCls(hiddenCls);\r
840 textarea.dom.removeAttribute('tabIndex');\r
841 textarea.focus();\r
842 me.inputEl = textarea;\r
843 } else {\r
844 if (me.initialized) {\r
845 me.disableItems(me.readOnly);\r
846 }\r
847 me.pushValue();\r
848 iframe.removeCls(hiddenCls);\r
849 textarea.addCls(hiddenCls);\r
850 textarea.dom.setAttribute('tabIndex', -1);\r
851 me.deferFocus();\r
852 me.inputEl = iframe;\r
853 }\r
854 me.fireEvent('editmodechange', me, sourceEditMode);\r
855 me.updateLayout();\r
856 },\r
857\r
858 /**\r
859 * @private\r
860 */\r
861 createLink: function() {\r
862 var url = prompt(this.createLinkText, this.defaultLinkValue);\r
863 if (url && url !== 'http:/'+'/') {\r
864 this.relayCmd('createlink', url);\r
865 }\r
866 },\r
867\r
868 clearInvalid: Ext.emptyFn,\r
869\r
870 setValue: function(value) {\r
871 var me = this,\r
872 textarea = me.textareaEl;\r
873\r
874 if (value === null || value === undefined) {\r
875 value = '';\r
876 }\r
877\r
878 // Only update the field if the value has changed\r
879 if (me.value !== value) {\r
880 if (textarea) {\r
881 textarea.dom.value = value;\r
882 }\r
883 me.pushValue();\r
884\r
885 if (!me.rendered && me.inputCmp) {\r
886 me.inputCmp.data.value = value;\r
887 }\r
888 me.mixins.field.setValue.call(me, value);\r
889 }\r
890\r
891 return me;\r
892 },\r
893\r
894 /**\r
895 * If you need/want custom HTML cleanup, this is the method you should override.\r
896 * @param {String} html The HTML to be cleaned\r
897 * @return {String} The cleaned HTML\r
898 * @protected\r
899 */\r
900 cleanHtml: function(html) {\r
901 html = String(html);\r
902 if (Ext.isWebKit) { // strip safari nonsense\r
903 html = html.replace(this.safariNonsenseRE, '');\r
904 }\r
905\r
906 /*\r
907 * Neat little hack. Strips out all the non-digit characters from the default\r
908 * value and compares it to the character code of the first character in the string\r
909 * because it can cause encoding issues when posted to the server. We need the\r
910 * parseInt here because charCodeAt will return a number.\r
911 */\r
912 if (html.charCodeAt(0) === parseInt(this.defaultValue.replace(this.nonDigitsRE, ''), 10)) {\r
913 html = html.substring(1);\r
914 }\r
915 \r
916 return html;\r
917 },\r
918\r
919 /**\r
920 * Syncs the contents of the editor iframe with the textarea.\r
921 * @protected\r
922 */\r
923 syncValue: function(){\r
924 var me = this,\r
925 body, changed, html, bodyStyle, match, textElDom;\r
926\r
927 if (me.initialized) {\r
928 body = me.getEditorBody();\r
929 html = body.innerHTML;\r
930 textElDom = me.textareaEl.dom;\r
931\r
932 if (Ext.isWebKit) {\r
933 bodyStyle = body.getAttribute('style'); // Safari puts text-align styles on the body element!\r
934 match = bodyStyle.match(me.textAlignRE);\r
935 if (match && match[1]) {\r
936 html = '<div style="' + match[0] + '">' + html + '</div>';\r
937 }\r
938 }\r
939\r
940 html = me.cleanHtml(html);\r
941\r
942 if (me.fireEvent('beforesync', me, html) !== false) {\r
943 // Gecko inserts single <br> tag when input is empty\r
944 // and user toggles source mode. See https://sencha.jira.com/browse/EXTJSIV-8542\r
945 if (Ext.isGecko && textElDom.value === '' && html === '<br>') {\r
946 html = '';\r
947 }\r
948\r
949 if (textElDom.value !== html) {\r
950 textElDom.value = html;\r
951 changed = true;\r
952 }\r
953\r
954 me.fireEvent('sync', me, html);\r
955\r
956 if (changed) {\r
957 // we have to guard this to avoid infinite recursion because getValue\r
958 // calls this method...\r
959 me.checkChange();\r
960 }\r
961 }\r
962 }\r
963 },\r
964\r
965 getValue: function() {\r
966 var me = this,\r
967 value;\r
968 if (!me.sourceEditMode) {\r
969 me.syncValue();\r
970 }\r
971 value = me.rendered ? me.textareaEl.dom.value : me.value;\r
972 me.value = value;\r
973 return value;\r
974 },\r
975\r
976 /**\r
977 * Pushes the value of the textarea into the iframe editor.\r
978 * @protected\r
979 */\r
980 pushValue: function() {\r
981 var me = this,\r
982 v;\r
983 if(me.initialized){\r
984 v = me.textareaEl.dom.value || '';\r
985 if (!me.activated && v.length < 1) {\r
986 v = me.defaultValue;\r
987 }\r
988 if (me.fireEvent('beforepush', me, v) !== false) {\r
989 me.getEditorBody().innerHTML = v;\r
990 if (Ext.isGecko) {\r
991 // Gecko hack, see: https://bugzilla.mozilla.org/show_bug.cgi?id=232791#c8\r
992 me.setDesignMode(false); //toggle off first\r
993 me.setDesignMode(true);\r
994 }\r
995 me.fireEvent('push', me, v);\r
996 }\r
997 }\r
998 },\r
999\r
1000 focus: function(selectText, delay) {\r
1001 var me = this,\r
1002 value, focusEl;\r
1003\r
1004 if (delay) {\r
1005 if (!me.focusTask) {\r
1006 me.focusTask = new Ext.util.DelayedTask(me.focus);\r
1007 }\r
1008 me.focusTask.delay(Ext.isNumber(delay) ? delay : 10, null, me, [selectText, false]);\r
1009 }\r
1010 else {\r
1011 if (selectText) {\r
1012 if (me.textareaEl && me.textareaEl.dom) {\r
1013 value = me.textareaEl.dom.value;\r
1014 }\r
1015 if (value && value.length) { // Make sure there is content before calling SelectAll, otherwise the caret disappears.\r
1016 me.execCmd('selectall', true);\r
1017 }\r
1018 }\r
1019 focusEl = me.getFocusEl();\r
1020 if (focusEl && focusEl.focus) {\r
1021 focusEl.focus();\r
1022 }\r
1023 }\r
1024 return me;\r
1025 },\r
1026\r
1027 /**\r
1028 * @private\r
1029 */\r
1030 initEditor: function(){\r
1031\r
1032 var me = this,\r
1033 dbody, ss, doc, docEl, fn;\r
1034\r
1035 //Destroying the component during/before initEditor can cause issues.\r
1036 if (me.destroying || me.destroyed) {\r
1037 return;\r
1038 }\r
1039\r
1040 dbody = me.getEditorBody();\r
1041\r
1042 // IE has a null reference when it first comes online.\r
1043 if (!dbody) {\r
1044 setTimeout(function () {\r
1045 me.initEditor();\r
1046 }, 10);\r
1047 return;\r
1048 }\r
1049\r
1050 ss = me.textareaEl.getStyle(['font-size', 'font-family', 'background-image', 'background-repeat', 'background-color', 'color']);\r
1051\r
1052 ss['background-attachment'] = 'fixed'; // w3c\r
1053 dbody.bgProperties = 'fixed'; // ie\r
1054\r
1055 Ext.DomHelper.applyStyles(dbody, ss);\r
1056\r
1057 doc = me.getDoc();\r
1058 docEl = Ext.get(doc);\r
1059\r
1060 if (docEl) {\r
1061 try {\r
1062 docEl.clearListeners();\r
1063 } catch(e) {}\r
1064\r
1065 /*\r
1066 * We need to use createDelegate here, because when using buffer, the delayed task is added\r
1067 * as a property to the function. When the listener is removed, the task is deleted from the function.\r
1068 * Since onEditorEvent is shared on the prototype, if we have multiple html editors, the first time one of the editors\r
1069 * is destroyed, it causes the fn to be deleted from the prototype, which causes errors. Essentially, we're just anonymizing the function.\r
1070 */\r
1071 fn = me.onEditorEvent.bind(me);\r
1072 docEl.on({\r
1073 mousedown: fn,\r
1074 dblclick: fn,\r
1075 click: fn,\r
1076 keyup: fn,\r
1077 delegated: false,\r
1078 buffer:100\r
1079 });\r
1080\r
1081 // These events need to be relayed from the inner document (where they stop\r
1082 // bubbling) up to the outer document. This has to be done at the DOM level so\r
1083 // the event reaches listeners on elements like the document body. The effected\r
1084 // mechanisms that depend on this bubbling behavior are listed to the right\r
1085 // of the event.\r
1086 fn = me.onRelayedEvent;\r
1087 docEl.on({\r
1088 mousedown: fn, // menu dismisal (MenuManager) and Window onMouseDown (toFront)\r
1089 mousemove: fn, // window resize drag detection\r
1090 mouseup: fn, // window resize termination\r
1091 click: fn, // not sure, but just to be safe\r
1092 dblclick: fn, // not sure again\r
1093 delegated: false,\r
1094 scope: me\r
1095 });\r
1096\r
1097 if (Ext.isGecko) {\r
1098 docEl.on('keypress', me.applyCommand, me);\r
1099 }\r
1100\r
1101 if (me.fixKeys) {\r
1102 docEl.on('keydown', me.fixKeys, me, {delegated: false});\r
1103 }\r
1104\r
1105 if (me.fixKeysAfter) {\r
1106 docEl.on('keyup', me.fixKeysAfter, me, {delegated: false});\r
1107 }\r
1108\r
1109 if (Ext.isIE9) {\r
1110 Ext.get(doc.documentElement).on('focus', me.focus, me);\r
1111 }\r
1112\r
1113 // In old IEs, clicking on a toolbar button shifts focus from iframe\r
1114 // and it loses selection. To avoid this, we save current selection\r
1115 // and restore it.\r
1116 if (Ext.isIE8) {\r
1117 docEl.on('focusout', function() {\r
1118 me.savedSelection = doc.selection.type !== 'None' ? doc.selection.createRange() : null;\r
1119 }, me);\r
1120\r
1121 docEl.on('focusin', function() {\r
1122 if (me.savedSelection) {\r
1123 me.savedSelection.select();\r
1124 }\r
1125 }, me);\r
1126 }\r
1127\r
1128 // We need to be sure we remove all our events from the iframe on unload or we're going to LEAK!\r
1129 Ext.getWin().on('beforeunload', me.beforeDestroy, me);\r
1130 doc.editorInitialized = true;\r
1131\r
1132 me.initialized = true;\r
1133 me.pushValue();\r
1134 me.setReadOnly(me.readOnly);\r
1135 me.fireEvent('initialize', me);\r
1136 }\r
1137 },\r
1138\r
1139 /**\r
1140 * @private\r
1141 */\r
1142 beforeDestroy: function(){\r
1143 var me = this,\r
1144 monitorTask = me.monitorTask,\r
1145 doc, prop;\r
1146\r
1147 if (monitorTask) {\r
1148 Ext.TaskManager.stop(monitorTask);\r
1149 }\r
1150 if (me.rendered) {\r
1151 Ext.getWin().un(me.beforeDestroy, me);\r
1152\r
1153 doc = me.getDoc();\r
1154 if (doc) {\r
1155 // removeAll() doesn't currently know how to handle iframe document,\r
1156 // so for now we have to wrap it in an Ext.Element,\r
1157 // or else IE6/7 will leak big time when the page is refreshed.\r
1158 // TODO: this may not be needed once we find a more permanent fix.\r
1159 // see EXTJSIV-5891.\r
1160 Ext.get(doc).destroy();\r
1161\r
1162 if (doc.hasOwnProperty) {\r
1163\r
1164 for (prop in doc) {\r
1165 try {\r
1166 if (doc.hasOwnProperty(prop)) {\r
1167 delete doc[prop];\r
1168 }\r
1169 } catch(e) {\r
1170 // clearing certain props on document MAY throw in IE\r
1171 }\r
1172 }\r
1173 }\r
1174 }\r
1175\r
1176 delete me.iframeEl;\r
1177 delete me.textareaEl;\r
1178 delete me.toolbar;\r
1179 delete me.inputCmp;\r
1180 }\r
1181 me.callParent();\r
1182 },\r
1183\r
1184 /**\r
1185 * @private\r
1186 */\r
1187 onRelayedEvent: function (event) {\r
1188 // relay event from the iframe's document to the document that owns the iframe...\r
1189\r
1190 var iframeEl = this.iframeEl,\r
1191 iframeXY = Ext.fly(iframeEl).getTrueXY(),\r
1192 originalEventXY = event.getXY(),\r
1193\r
1194 eventXY = event.getXY();\r
1195\r
1196 // the event from the inner document has XY relative to that document's origin,\r
1197 // so adjust it to use the origin of the iframe in the outer document:\r
1198 event.xy = [iframeXY[0] + eventXY[0], iframeXY[1] + eventXY[1]];\r
1199\r
1200 event.injectEvent(iframeEl); // blame the iframe for the event...\r
1201\r
1202 event.xy = originalEventXY; // restore the original XY (just for safety)\r
1203 },\r
1204\r
1205 /**\r
1206 * @private\r
1207 */\r
1208 onFirstFocus: function(){\r
1209 var me = this,\r
1210 selection, range;\r
1211\r
1212 me.activated = true;\r
1213 me.disableItems(me.readOnly);\r
1214 if (Ext.isGecko) { // prevent silly gecko errors\r
1215 me.win.focus();\r
1216 selection = me.win.getSelection();\r
1217\r
1218 // If the editor contains a <br> tag, clicking on the editor after the text where\r
1219 // the <br> broke the line will produce nodeType === 1 (the body tag).\r
1220 // It's better to check the length of the selection.focusNode's content.\r
1221 //\r
1222 // If htmleditor.value = ' ' (note the space)\r
1223 // 1. nodeType === 1\r
1224 // 2. nodeName === 'BODY'\r
1225 // 3. selection.focusNode.textContent.length === 1\r
1226 //\r
1227 // If htmleditor.value = '' (no chars) nodeType === 3 && nodeName === '#text'\r
1228 // 1. nodeType === 3\r
1229 // 2. nodeName === '#text'\r
1230 // 3. selection.focusNode.textContent.length === 1 (yes, that's right, 1)\r
1231 //\r
1232 // The editor inserts Unicode code point 8203, a zero-width space when\r
1233 // htmleditor.value === '' (call selection.focusNode.textContent.charCodeAt(0))\r
1234 // http://www.fileformat.info/info/unicode/char/200b/index.htm\r
1235 // So, test with framework method to normalize.\r
1236 if (selection.focusNode && !me.getValue().length) {\r
1237 range = selection.getRangeAt(0);\r
1238 range.selectNodeContents(me.getEditorBody());\r
1239 range.collapse(true);\r
1240 me.deferFocus();\r
1241 }\r
1242 try {\r
1243 me.execCmd('useCSS', true);\r
1244 me.execCmd('styleWithCSS', false);\r
1245 } catch(e) {\r
1246 // ignore (why?)\r
1247 }\r
1248 }\r
1249 me.fireEvent('activate', me);\r
1250 },\r
1251\r
1252 /**\r
1253 * @private\r
1254 */\r
1255 adjustFont: function(btn) {\r
1256 var adjust = btn.getItemId() === 'increasefontsize' ? 1 : -1,\r
1257 size = this.getDoc().queryCommandValue('FontSize') || '2',\r
1258 isPxSize = Ext.isString(size) && size.indexOf('px') !== -1,\r
1259 isSafari;\r
1260 size = parseInt(size, 10);\r
1261 if (isPxSize) {\r
1262 // Safari 3 values\r
1263 // 1 = 10px, 2 = 13px, 3 = 16px, 4 = 18px, 5 = 24px, 6 = 32px\r
1264 if (size <= 10) {\r
1265 size = 1 + adjust;\r
1266 }\r
1267 else if (size <= 13) {\r
1268 size = 2 + adjust;\r
1269 }\r
1270 else if (size <= 16) {\r
1271 size = 3 + adjust;\r
1272 }\r
1273 else if (size <= 18) {\r
1274 size = 4 + adjust;\r
1275 }\r
1276 else if (size <= 24) {\r
1277 size = 5 + adjust;\r
1278 }\r
1279 else {\r
1280 size = 6 + adjust;\r
1281 }\r
1282 size = Ext.Number.constrain(size, 1, 6);\r
1283 } else {\r
1284 isSafari = Ext.isSafari;\r
1285 if (isSafari) { // safari\r
1286 adjust *= 2;\r
1287 }\r
1288 size = Math.max(1, size + adjust) + (isSafari ? 'px' : 0);\r
1289 }\r
1290 this.relayCmd('FontSize', size);\r
1291 },\r
1292\r
1293 /**\r
1294 * @private\r
1295 */\r
1296 onEditorEvent: function() {\r
1297 this.updateToolbar();\r
1298 },\r
1299\r
1300 /**\r
1301 * Triggers a toolbar update by reading the markup state of the current selection in the editor.\r
1302 * @protected\r
1303 */\r
1304 updateToolbar: function() {\r
1305 var me = this,\r
1306 i, l, btns, doc, name, queriedName, fontSelect,\r
1307 toolbarSubmenus;\r
1308\r
1309 if (me.readOnly) {\r
1310 return;\r
1311 }\r
1312\r
1313 if (!me.activated) {\r
1314 me.onFirstFocus();\r
1315 return;\r
1316 }\r
1317\r
1318 btns = me.getToolbar().items.map;\r
1319 doc = me.getDoc();\r
1320\r
1321 if (me.enableFont && !Ext.isSafari2) {\r
1322 // When querying the fontName, Chrome may return an Array of font names\r
1323 // with those containing spaces being placed between single-quotes.\r
1324 queriedName = doc.queryCommandValue('fontName');\r
1325 name = (queriedName ? queriedName.split(",")[0].replace(me.reStripQuotes, '') : me.defaultFont).toLowerCase();\r
1326 fontSelect = me.fontSelect.dom;\r
1327 if (name !== fontSelect.value || name !== queriedName) {\r
1328 fontSelect.value = name;\r
1329 }\r
1330 }\r
1331\r
1332 function updateButtons() {\r
1333 var state;\r
1334 \r
1335 for (i = 0, l = arguments.length, name; i < l; i++) {\r
1336 name = arguments[i];\r
1337 \r
1338 // Firefox 18+ sometimes throws NS_ERROR_INVALID_POINTER exception\r
1339 // See https://sencha.jira.com/browse/EXTJSIV-9766\r
1340 try {\r
1341 state = doc.queryCommandState(name);\r
1342 }\r
1343 catch (e) {\r
1344 state = false;\r
1345 }\r
1346 \r
1347 btns[name].toggle(state);\r
1348 }\r
1349 }\r
1350 if(me.enableFormat){\r
1351 updateButtons('bold', 'italic', 'underline');\r
1352 }\r
1353 if(me.enableAlignments){\r
1354 updateButtons('justifyleft', 'justifycenter', 'justifyright');\r
1355 }\r
1356 if(!Ext.isSafari2 && me.enableLists){\r
1357 updateButtons('insertorderedlist', 'insertunorderedlist');\r
1358 }\r
1359\r
1360 // Ensure any of our toolbar's owned menus are hidden.\r
1361 // The overflow menu must control itself.\r
1362 toolbarSubmenus = me.toolbar.query('menu');\r
1363 for (i = 0; i < toolbarSubmenus.length; i++) {\r
1364 toolbarSubmenus[i].hide();\r
1365 }\r
1366 me.syncValue();\r
1367 },\r
1368\r
1369 /**\r
1370 * @private\r
1371 */\r
1372 relayBtnCmd: function(btn) {\r
1373 this.relayCmd(btn.getItemId());\r
1374 },\r
1375\r
1376 /**\r
1377 * Executes a Midas editor command on the editor document and performs necessary focus and toolbar updates.\r
1378 * **This should only be called after the editor is initialized.**\r
1379 * @param {String} cmd The Midas command\r
1380 * @param {String/Boolean} [value=null] The value to pass to the command\r
1381 */\r
1382 relayCmd: function(cmd, value) {\r
1383 Ext.defer(function() {\r
1384 var me = this;\r
1385 \r
1386 if (!this.destroyed) {\r
1387 me.win.focus();\r
1388 me.execCmd(cmd, value);\r
1389 me.updateToolbar();\r
1390 }\r
1391 }, 10, this);\r
1392 },\r
1393\r
1394 /**\r
1395 * Executes a Midas editor command directly on the editor document. For visual commands, you should use\r
1396 * {@link #relayCmd} instead. **This should only be called after the editor is initialized.**\r
1397 * @param {String} cmd The Midas command\r
1398 * @param {String/Boolean} [value=null] The value to pass to the command\r
1399 */\r
1400 execCmd: function(cmd, value){\r
1401 var me = this,\r
1402 doc = me.getDoc();\r
1403 doc.execCommand(cmd, false, (value === undefined ? null : value));\r
1404 me.syncValue();\r
1405 },\r
1406\r
1407 /**\r
1408 * @private\r
1409 */\r
1410 applyCommand: function(e){\r
1411 if (e.ctrlKey) {\r
1412 var me = this,\r
1413 c = e.getCharCode(), cmd;\r
1414 if (c > 0) {\r
1415 c = String.fromCharCode(c);\r
1416 switch (c) {\r
1417 case 'b':\r
1418 cmd = 'bold';\r
1419 break;\r
1420 case 'i':\r
1421 cmd = 'italic';\r
1422 break;\r
1423 case 'u':\r
1424 cmd = 'underline';\r
1425 break;\r
1426 }\r
1427 if (cmd) {\r
1428 me.win.focus();\r
1429 me.execCmd(cmd);\r
1430 me.deferFocus();\r
1431 e.preventDefault();\r
1432 }\r
1433 }\r
1434 }\r
1435 },\r
1436\r
1437 /**\r
1438 * Inserts the passed text at the current cursor position.\r
1439 * __Note:__ the editor must be initialized and activated to insert text.\r
1440 * @param {String} text\r
1441 */\r
1442 insertAtCursor : function(text){\r
1443 // adapted from http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div/6691294#6691294\r
1444 var me = this,\r
1445 win = me.getWin(),\r
1446 doc = me.getDoc(),\r
1447 sel, range, el, frag, node, lastNode, firstNode;\r
1448\r
1449 if (me.activated) {\r
1450 win.focus();\r
1451 if (win.getSelection) {\r
1452 sel = win.getSelection();\r
1453 if (sel.getRangeAt && sel.rangeCount) {\r
1454 range = sel.getRangeAt(0);\r
1455 range.deleteContents();\r
1456\r
1457 // Range.createContextualFragment() would be useful here but is\r
1458 // only relatively recently standardized and is not supported in\r
1459 // some browsers (IE9, for one)\r
1460 el = doc.createElement("div");\r
1461 el.innerHTML = text;\r
1462 frag = doc.createDocumentFragment();\r
1463 while ((node = el.firstChild)) {\r
1464 lastNode = frag.appendChild(node);\r
1465 }\r
1466 firstNode = frag.firstChild;\r
1467 range.insertNode(frag);\r
1468\r
1469 // Preserve the selection\r
1470 if (lastNode) {\r
1471 range = range.cloneRange();\r
1472 range.setStartAfter(lastNode);\r
1473 range.collapse(true);\r
1474 sel.removeAllRanges();\r
1475 sel.addRange(range);\r
1476 }\r
1477 }\r
1478 } else if (doc.selection && sel.type !== 'Control') {\r
1479 sel = doc.selection;\r
1480 range = sel.createRange();\r
1481 range.collapse(true);\r
1482 sel.createRange().pasteHTML(text);\r
1483 }\r
1484 me.deferFocus();\r
1485 }\r
1486 },\r
1487\r
1488 /**\r
1489 * @private\r
1490 * Load time branching for fastest keydown performance.\r
1491 */\r
1492 fixKeys: (function () {\r
1493 var tag;\r
1494\r
1495 if (Ext.isIE10m) {\r
1496 return function (e) {\r
1497 var me = this,\r
1498 k = e.getKey(),\r
1499 doc = me.getDoc(),\r
1500 readOnly = me.readOnly,\r
1501 range, target;\r
1502\r
1503 if (k === e.TAB) {\r
1504 e.stopEvent();\r
1505\r
1506 // TODO: add tab support for IE 11.\r
1507 if (!readOnly) {\r
1508 range = doc.selection.createRange();\r
1509 if (range){\r
1510 if (range.collapse) {\r
1511 range.collapse(true);\r
1512 range.pasteHTML('&#160;&#160;&#160;&#160;');\r
1513 }\r
1514\r
1515 me.deferFocus();\r
1516 }\r
1517 }\r
1518 }\r
1519 };\r
1520 }\r
1521\r
1522 if (Ext.isOpera) {\r
1523 return function(e) {\r
1524 var me = this,\r
1525 k = e.getKey(),\r
1526 readOnly = me.readOnly;\r
1527\r
1528 if (k === e.TAB) {\r
1529 e.stopEvent();\r
1530\r
1531 if (!readOnly) {\r
1532 me.win.focus();\r
1533 me.execCmd('InsertHTML','&#160;&#160;&#160;&#160;');\r
1534 me.deferFocus();\r
1535 }\r
1536 }\r
1537 };\r
1538 }\r
1539\r
1540 // Not needed, so null.\r
1541 return null;\r
1542 }()),\r
1543\r
1544 /**\r
1545 * @private\r
1546 */\r
1547 fixKeysAfter: (function () {\r
1548 if (Ext.isIE) {\r
1549 return function (e) {\r
1550 var me = this,\r
1551 k = e.getKey(),\r
1552 doc = me.getDoc(),\r
1553 readOnly = me.readOnly,\r
1554 innerHTML;\r
1555\r
1556 if (!readOnly && (k === e.BACKSPACE || k === e.DELETE)) {\r
1557 innerHTML = doc.body.innerHTML;\r
1558\r
1559 // If HtmlEditor had some input and user cleared it, IE inserts <p>&nbsp;</p>\r
1560 // which makes an impression that there is still some text, and creeps\r
1561 // into source mode when toggled. We don't want this.\r
1562 //\r
1563 // See https://sencha.jira.com/browse/EXTJSIV-8542\r
1564 //\r
1565 // N.B. There is **small** chance that user could go to source mode,\r
1566 // type '<p>&nbsp;</p>', switch back to visual mode, type something else\r
1567 // and then clear it -- the code below would clear the <p> tag as well,\r
1568 // which could be considered a bug. However I see no way to distinguish\r
1569 // between offending markup being entered manually and generated by IE,\r
1570 // so this can be considered a nasty corner case.\r
1571 //\r
1572 if (innerHTML === '<p>&nbsp;</p>' || innerHTML === '<P>&nbsp;</P>') {\r
1573 doc.body.innerHTML = '';\r
1574 }\r
1575 }\r
1576 };\r
1577 }\r
1578\r
1579 return null;\r
1580 }()),\r
1581\r
1582 /**\r
1583 * Returns the editor's toolbar. **This is only available after the editor has been rendered.**\r
1584 * @return {Ext.toolbar.Toolbar}\r
1585 */\r
1586 getToolbar: function(){\r
1587 return this.toolbar;\r
1588 },\r
1589\r
1590 //<locale>\r
1591 /**\r
1592 * @property {Object} buttonTips\r
1593 * Object collection of toolbar tooltips for the buttons in the editor. The key is the command id associated with\r
1594 * that button and the value is a valid QuickTips object. For example:\r
1595 *\r
1596 * {\r
1597 * bold: {\r
1598 * title: 'Bold (Ctrl+B)',\r
1599 * text: 'Make the selected text bold.',\r
1600 * cls: 'x-html-editor-tip'\r
1601 * },\r
1602 * italic: {\r
1603 * title: 'Italic (Ctrl+I)',\r
1604 * text: 'Make the selected text italic.',\r
1605 * cls: 'x-html-editor-tip'\r
1606 * }\r
1607 * // ...\r
1608 * }\r
1609 */\r
1610 buttonTips: {\r
1611 bold: {\r
1612 title: 'Bold (Ctrl+B)',\r
1613 text: 'Make the selected text bold.',\r
1614 cls: Ext.baseCSSPrefix + 'html-editor-tip'\r
1615 },\r
1616 italic: {\r
1617 title: 'Italic (Ctrl+I)',\r
1618 text: 'Make the selected text italic.',\r
1619 cls: Ext.baseCSSPrefix + 'html-editor-tip'\r
1620 },\r
1621 underline: {\r
1622 title: 'Underline (Ctrl+U)',\r
1623 text: 'Underline the selected text.',\r
1624 cls: Ext.baseCSSPrefix + 'html-editor-tip'\r
1625 },\r
1626 increasefontsize: {\r
1627 title: 'Grow Text',\r
1628 text: 'Increase the font size.',\r
1629 cls: Ext.baseCSSPrefix + 'html-editor-tip'\r
1630 },\r
1631 decreasefontsize: {\r
1632 title: 'Shrink Text',\r
1633 text: 'Decrease the font size.',\r
1634 cls: Ext.baseCSSPrefix + 'html-editor-tip'\r
1635 },\r
1636 backcolor: {\r
1637 title: 'Text Highlight Color',\r
1638 text: 'Change the background color of the selected text.',\r
1639 cls: Ext.baseCSSPrefix + 'html-editor-tip'\r
1640 },\r
1641 forecolor: {\r
1642 title: 'Font Color',\r
1643 text: 'Change the color of the selected text.',\r
1644 cls: Ext.baseCSSPrefix + 'html-editor-tip'\r
1645 },\r
1646 justifyleft: {\r
1647 title: 'Align Text Left',\r
1648 text: 'Align text to the left.',\r
1649 cls: Ext.baseCSSPrefix + 'html-editor-tip'\r
1650 },\r
1651 justifycenter: {\r
1652 title: 'Center Text',\r
1653 text: 'Center text in the editor.',\r
1654 cls: Ext.baseCSSPrefix + 'html-editor-tip'\r
1655 },\r
1656 justifyright: {\r
1657 title: 'Align Text Right',\r
1658 text: 'Align text to the right.',\r
1659 cls: Ext.baseCSSPrefix + 'html-editor-tip'\r
1660 },\r
1661 insertunorderedlist: {\r
1662 title: 'Bullet List',\r
1663 text: 'Start a bulleted list.',\r
1664 cls: Ext.baseCSSPrefix + 'html-editor-tip'\r
1665 },\r
1666 insertorderedlist: {\r
1667 title: 'Numbered List',\r
1668 text: 'Start a numbered list.',\r
1669 cls: Ext.baseCSSPrefix + 'html-editor-tip'\r
1670 },\r
1671 createlink: {\r
1672 title: 'Hyperlink',\r
1673 text: 'Make the selected text a hyperlink.',\r
1674 cls: Ext.baseCSSPrefix + 'html-editor-tip'\r
1675 },\r
1676 sourceedit: {\r
1677 title: 'Source Edit',\r
1678 text: 'Switch to source editing mode.',\r
1679 cls: Ext.baseCSSPrefix + 'html-editor-tip'\r
1680 }\r
1681 },\r
1682 //</locale>\r
1683\r
1684 // hide stuff that is not compatible\r
1685 /**\r
1686 * @event blur\r
1687 * @private\r
1688 */\r
1689 /**\r
1690 * @event focus\r
1691 * @private\r
1692 */\r
1693 /**\r
1694 * @event specialkey\r
1695 * @private\r
1696 */\r
1697 /**\r
1698 * @cfg {String} fieldCls\r
1699 * @private\r
1700 */\r
1701 /**\r
1702 * @cfg {String} focusCls\r
1703 * @private\r
1704 */\r
1705 /**\r
1706 * @cfg {String} autoCreate\r
1707 * @private\r
1708 */\r
1709 /**\r
1710 * @cfg {String} inputType\r
1711 * @private\r
1712 */\r
1713 /**\r
1714 * @cfg {String} invalidCls\r
1715 * @private\r
1716 */\r
1717 /**\r
1718 * @cfg {String} invalidText\r
1719 * @private\r
1720 */\r
1721 /**\r
1722 * @cfg {Boolean} allowDomMove\r
1723 * @private\r
1724 */\r
1725 /**\r
1726 * @cfg {String} readOnly\r
1727 * @private\r
1728 */\r
1729 /**\r
1730 * @cfg {String} tabIndex\r
1731 * @private\r
1732 */\r
1733 /**\r
1734 * @method validate\r
1735 * @private\r
1736 */\r
1737\r
1738 privates: {\r
1739 deferFocus: function(){\r
1740 this.focus(false, true);\r
1741 },\r
1742\r
1743 getFocusEl: function() {\r
1744 return this.sourceEditMode ? this.textareaEl : this.iframeEl;\r
1745 }\r
1746 }\r
1747});\r
1748\r