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

https://bitbucket.org/srogerf/javascript · JavaScript · 1328 lines · 878 code · 112 blank · 338 comment · 124 complexity · 669b3642a11e8260a2d9019a7cfb8113 MD5 · raw file

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