abstractdialogplugin.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. // Copyright 2008 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview An abstract superclass for TrogEdit dialog plugins. Each
  16. * Trogedit dialog has its own plugin.
  17. *
  18. * @author nicksantos@google.com (Nick Santos)
  19. */
  20. goog.provide('goog.editor.plugins.AbstractDialogPlugin');
  21. goog.provide('goog.editor.plugins.AbstractDialogPlugin.EventType');
  22. goog.require('goog.dom');
  23. goog.require('goog.dom.Range');
  24. goog.require('goog.editor.Field');
  25. goog.require('goog.editor.Plugin');
  26. goog.require('goog.editor.range');
  27. goog.require('goog.events');
  28. goog.require('goog.ui.editor.AbstractDialog');
  29. // *** Public interface ***************************************************** //
  30. /**
  31. * An abstract superclass for a Trogedit plugin that creates exactly one
  32. * dialog. By default dialogs are not reused -- each time execCommand is called,
  33. * a new instance of the dialog object is created (and the old one disposed of).
  34. * To enable reusing of the dialog object, subclasses should call
  35. * setReuseDialog() after calling the superclass constructor.
  36. * @param {string} command The command that this plugin handles.
  37. * @constructor
  38. * @extends {goog.editor.Plugin}
  39. */
  40. goog.editor.plugins.AbstractDialogPlugin = function(command) {
  41. goog.editor.plugins.AbstractDialogPlugin.base(this, 'constructor');
  42. /**
  43. * The command that this plugin handles.
  44. * @private {string}
  45. */
  46. this.command_ = command;
  47. /** @private {function()} */
  48. this.restoreScrollPosition_ = function() {};
  49. /**
  50. * The current dialog that was created and opened by this plugin.
  51. * @private {?goog.ui.editor.AbstractDialog}
  52. */
  53. this.dialog_ = null;
  54. /**
  55. * Whether this plugin should reuse the same instance of the dialog each time
  56. * execCommand is called or create a new one.
  57. * @private {boolean}
  58. */
  59. this.reuseDialog_ = false;
  60. /**
  61. * Mutex to prevent recursive calls to disposeDialog_.
  62. * @private {boolean}
  63. */
  64. this.isDisposingDialog_ = false;
  65. /**
  66. * SavedRange representing the selection before the dialog was opened.
  67. * @private {?goog.dom.SavedRange}
  68. */
  69. this.savedRange_ = null;
  70. };
  71. goog.inherits(goog.editor.plugins.AbstractDialogPlugin, goog.editor.Plugin);
  72. /** @override */
  73. goog.editor.plugins.AbstractDialogPlugin.prototype.isSupportedCommand =
  74. function(command) {
  75. return command == this.command_;
  76. };
  77. /**
  78. * Handles execCommand. Dialog plugins don't make any changes when they open a
  79. * dialog, just when the dialog closes (because only modal dialogs are
  80. * supported). Hence this method does not dispatch the change events that the
  81. * superclass method does.
  82. * @param {string} command The command to execute.
  83. * @param {...*} var_args Any additional parameters needed to
  84. * execute the command.
  85. * @return {*} The result of the execCommand, if any.
  86. * @override
  87. */
  88. goog.editor.plugins.AbstractDialogPlugin.prototype.execCommand = function(
  89. command, var_args) {
  90. return this.execCommandInternal.apply(this, arguments);
  91. };
  92. // *** Events *************************************************************** //
  93. /**
  94. * Event type constants for events the dialog plugins fire.
  95. * @enum {string}
  96. */
  97. goog.editor.plugins.AbstractDialogPlugin.EventType = {
  98. // This event is fired when a dialog has been opened.
  99. OPENED: 'dialogOpened',
  100. // This event is fired when a dialog has been closed.
  101. CLOSED: 'dialogClosed'
  102. };
  103. // *** Protected interface ************************************************** //
  104. /**
  105. * Creates a new instance of this plugin's dialog. Must be overridden by
  106. * subclasses.
  107. * Implementations should expect that the editor is inactive and cannot be
  108. * focused, nor will its caret position (or selection) be determinable until
  109. * after the dialogs goog.ui.PopupBase.EventType.HIDE event has been handled.
  110. * @param {!goog.dom.DomHelper} dialogDomHelper The dom helper to be used to
  111. * create the dialog.
  112. * @param {*=} opt_arg The dialog specific argument. Concrete subclasses should
  113. * declare a specific type.
  114. * @return {goog.ui.editor.AbstractDialog} The newly created dialog.
  115. * @protected
  116. */
  117. goog.editor.plugins.AbstractDialogPlugin.prototype.createDialog =
  118. goog.abstractMethod;
  119. /**
  120. * Returns the current dialog that was created and opened by this plugin.
  121. * @return {goog.ui.editor.AbstractDialog} The current dialog that was created
  122. * and opened by this plugin.
  123. * @protected
  124. */
  125. goog.editor.plugins.AbstractDialogPlugin.prototype.getDialog = function() {
  126. return this.dialog_;
  127. };
  128. /**
  129. * Sets whether this plugin should reuse the same instance of the dialog each
  130. * time execCommand is called or create a new one. This is intended for use by
  131. * subclasses only, hence protected.
  132. * @param {boolean} reuse Whether to reuse the dialog.
  133. * @protected
  134. */
  135. goog.editor.plugins.AbstractDialogPlugin.prototype.setReuseDialog = function(
  136. reuse) {
  137. this.reuseDialog_ = reuse;
  138. };
  139. /**
  140. * Handles execCommand by opening the dialog. Dispatches
  141. * {@link goog.editor.plugins.AbstractDialogPlugin.EventType.OPENED} after the
  142. * dialog is shown.
  143. * @param {string} command The command to execute.
  144. * @param {*=} opt_arg The dialog specific argument. Should be the same as
  145. * {@link createDialog}.
  146. * @return {*} Always returns true, indicating the dialog was shown.
  147. * @protected
  148. * @override
  149. */
  150. goog.editor.plugins.AbstractDialogPlugin.prototype.execCommandInternal =
  151. function(command, opt_arg) {
  152. // If this plugin should not reuse dialog instances, first dispose of the
  153. // previous dialog.
  154. if (!this.reuseDialog_) {
  155. this.disposeDialog_();
  156. }
  157. // If there is no dialog yet (or we aren't reusing the previous one), create
  158. // one.
  159. if (!this.dialog_) {
  160. this.dialog_ = this.createDialog(
  161. // TODO(user): Add Field.getAppDomHelper. (Note dom helper will
  162. // need to be updated if setAppWindow is called by clients.)
  163. goog.dom.getDomHelper(this.getFieldObject().getAppWindow()), opt_arg);
  164. }
  165. // Since we're opening a dialog, we need to clear the selection because the
  166. // focus will be going to the dialog, and if we leave an selection in the
  167. // editor while another selection is active in the dialog as the user is
  168. // typing, some browsers will screw up the original selection. But first we
  169. // save it so we can restore it when the dialog closes.
  170. // getRange may return null if there is no selection in the field.
  171. var tempRange = this.getFieldObject().getRange();
  172. // saveUsingDom() did not work as well as saveUsingNormalizedCarets(),
  173. // not sure why.
  174. this.restoreScrollPosition_ = this.saveScrollPosition();
  175. this.savedRange_ =
  176. tempRange && goog.editor.range.saveUsingNormalizedCarets(tempRange);
  177. goog.dom.Range.clearSelection(
  178. this.getFieldObject().getEditableDomHelper().getWindow());
  179. // Listen for the dialog closing so we can clean up.
  180. goog.events.listenOnce(
  181. this.dialog_, goog.ui.editor.AbstractDialog.EventType.AFTER_HIDE,
  182. this.handleAfterHide, false, this);
  183. this.getFieldObject().setModalMode(true);
  184. this.dialog_.show();
  185. this.dispatchEvent(goog.editor.plugins.AbstractDialogPlugin.EventType.OPENED);
  186. // Since the selection has left the document, dispatch a selection
  187. // change event.
  188. this.getFieldObject().dispatchSelectionChangeEvent();
  189. return true;
  190. };
  191. /**
  192. * Cleans up after the dialog has closed, including restoring the selection to
  193. * what it was before the dialog was opened. If a subclass modifies the editable
  194. * field's content such that the original selection is no longer valid (usually
  195. * the case when the user clicks OK, and sometimes also on Cancel), it is that
  196. * subclass' responsibility to place the selection in the desired place during
  197. * the OK or Cancel (or other) handler. In that case, this method will leave the
  198. * selection in place.
  199. * @param {goog.events.Event} e The AFTER_HIDE event object.
  200. * @protected
  201. */
  202. goog.editor.plugins.AbstractDialogPlugin.prototype.handleAfterHide = function(
  203. e) {
  204. this.getFieldObject().setModalMode(false);
  205. this.restoreOriginalSelection();
  206. this.restoreScrollPosition_();
  207. if (!this.reuseDialog_) {
  208. this.disposeDialog_();
  209. }
  210. this.dispatchEvent(goog.editor.plugins.AbstractDialogPlugin.EventType.CLOSED);
  211. // Since the selection has returned to the document, dispatch a selection
  212. // change event.
  213. this.getFieldObject().dispatchSelectionChangeEvent();
  214. // When the dialog closes due to pressing enter or escape, that happens on the
  215. // keydown event. But the browser will still fire a keyup event after that,
  216. // which is caught by the editable field and causes it to try to fire a
  217. // selection change event. To avoid that, we "debounce" the selection change
  218. // event, meaning the editable field will not fire that event if the keyup
  219. // that caused it immediately after this dialog was hidden ("immediately"
  220. // means a small number of milliseconds defined by the editable field).
  221. this.getFieldObject().debounceEvent(
  222. goog.editor.Field.EventType.SELECTIONCHANGE);
  223. };
  224. /**
  225. * Restores the selection in the editable field to what it was before the dialog
  226. * was opened. This is not guaranteed to work if the contents of the field
  227. * have changed.
  228. * @protected
  229. */
  230. goog.editor.plugins.AbstractDialogPlugin.prototype.restoreOriginalSelection =
  231. function() {
  232. this.getFieldObject().restoreSavedRange(this.savedRange_);
  233. this.savedRange_ = null;
  234. };
  235. /**
  236. * Cleans up the structure used to save the original selection before the dialog
  237. * was opened. Should be used by subclasses that don't restore the original
  238. * selection via restoreOriginalSelection.
  239. * @protected
  240. */
  241. goog.editor.plugins.AbstractDialogPlugin.prototype.disposeOriginalSelection =
  242. function() {
  243. if (this.savedRange_) {
  244. this.savedRange_.dispose();
  245. this.savedRange_ = null;
  246. }
  247. };
  248. /** @override */
  249. goog.editor.plugins.AbstractDialogPlugin.prototype.disposeInternal =
  250. function() {
  251. this.disposeDialog_();
  252. goog.editor.plugins.AbstractDialogPlugin.base(this, 'disposeInternal');
  253. };
  254. // *** Private implementation *********************************************** //
  255. /**
  256. * Disposes of the dialog if needed. It is this abstract class' responsibility
  257. * to dispose of the dialog. The "if needed" refers to the fact this method
  258. * might be called twice (nested calls, not sequential) in the dispose flow, so
  259. * if the dialog was already disposed once it should not be disposed again.
  260. * @private
  261. */
  262. goog.editor.plugins.AbstractDialogPlugin.prototype.disposeDialog_ = function() {
  263. // Wrap disposing the dialog in a mutex. Otherwise disposing it would cause it
  264. // to get hidden (if it is still open) and fire AFTER_HIDE, which in
  265. // turn would cause the dialog to be disposed again (closure only flags an
  266. // object as disposed after the dispose call chain completes, so it doesn't
  267. // prevent recursive dispose calls).
  268. if (this.dialog_ && !this.isDisposingDialog_) {
  269. this.isDisposingDialog_ = true;
  270. this.dialog_.dispose();
  271. this.dialog_ = null;
  272. this.isDisposingDialog_ = false;
  273. }
  274. };