dimensionpickerrenderer.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  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 The default renderer for a goog.dom.DimensionPicker. A
  16. * dimension picker allows the user to visually select a row and column count.
  17. * It looks like a palette but in order to minimize DOM load it is rendered.
  18. * using CSS background tiling instead of as a grid of nodes.
  19. *
  20. * @author robbyw@google.com (Robby Walker)
  21. */
  22. goog.provide('goog.ui.DimensionPickerRenderer');
  23. goog.require('goog.a11y.aria.Announcer');
  24. goog.require('goog.a11y.aria.LivePriority');
  25. goog.require('goog.dom');
  26. goog.require('goog.dom.TagName');
  27. goog.require('goog.i18n.bidi');
  28. goog.require('goog.style');
  29. goog.require('goog.ui.ControlRenderer');
  30. goog.require('goog.userAgent');
  31. /**
  32. * Default renderer for {@link goog.ui.DimensionPicker}s. Renders the
  33. * palette as two divs, one with the un-highlighted background, and one with the
  34. * highlighted background.
  35. *
  36. * @constructor
  37. * @extends {goog.ui.ControlRenderer}
  38. */
  39. goog.ui.DimensionPickerRenderer = function() {
  40. goog.ui.ControlRenderer.call(this);
  41. /** @private {goog.a11y.aria.Announcer} */
  42. this.announcer_ = new goog.a11y.aria.Announcer();
  43. };
  44. goog.inherits(goog.ui.DimensionPickerRenderer, goog.ui.ControlRenderer);
  45. goog.addSingletonGetter(goog.ui.DimensionPickerRenderer);
  46. /**
  47. * Default CSS class to be applied to the root element of components rendered
  48. * by this renderer.
  49. * @type {string}
  50. */
  51. goog.ui.DimensionPickerRenderer.CSS_CLASS =
  52. goog.getCssName('goog-dimension-picker');
  53. /**
  54. * Return the underlying div for the given outer element.
  55. * @param {Element} element The root element.
  56. * @return {Element} The underlying div.
  57. * @private
  58. */
  59. goog.ui.DimensionPickerRenderer.prototype.getUnderlyingDiv_ = function(
  60. element) {
  61. return /** @type {Element} */ (element.firstChild.childNodes[1]);
  62. };
  63. /**
  64. * Return the highlight div for the given outer element.
  65. * @param {Element} element The root element.
  66. * @return {Element} The highlight div.
  67. * @private
  68. */
  69. goog.ui.DimensionPickerRenderer.prototype.getHighlightDiv_ = function(element) {
  70. return /** @type {Element} */ (element.firstChild.lastChild);
  71. };
  72. /**
  73. * Return the status message div for the given outer element.
  74. * @param {Element} element The root element.
  75. * @return {Element} The status message div.
  76. * @private
  77. */
  78. goog.ui.DimensionPickerRenderer.prototype.getStatusDiv_ = function(element) {
  79. return /** @type {Element} */ (element.lastChild);
  80. };
  81. /**
  82. * Return the invisible mouse catching div for the given outer element.
  83. * @param {Element} element The root element.
  84. * @return {Element} The invisible mouse catching div.
  85. * @private
  86. */
  87. goog.ui.DimensionPickerRenderer.prototype.getMouseCatcher_ = function(element) {
  88. return /** @type {Element} */ (element.firstChild.firstChild);
  89. };
  90. /**
  91. * Overrides {@link goog.ui.ControlRenderer#canDecorate} to allow decorating
  92. * empty DIVs only.
  93. * @param {Element} element The element to check.
  94. * @return {boolean} Whether if the element is an empty div.
  95. * @override
  96. */
  97. goog.ui.DimensionPickerRenderer.prototype.canDecorate = function(element) {
  98. return element.tagName == goog.dom.TagName.DIV && !element.firstChild;
  99. };
  100. /**
  101. * Overrides {@link goog.ui.ControlRenderer#decorate} to decorate empty DIVs.
  102. * @param {goog.ui.Control} control goog.ui.DimensionPicker to decorate.
  103. * @param {Element} element The element to decorate.
  104. * @return {Element} The decorated element.
  105. * @override
  106. */
  107. goog.ui.DimensionPickerRenderer.prototype.decorate = function(
  108. control, element) {
  109. var palette = /** @type {goog.ui.DimensionPicker} */ (control);
  110. goog.ui.DimensionPickerRenderer.superClass_.decorate.call(
  111. this, palette, element);
  112. this.addElementContents_(palette, element);
  113. this.updateSize(palette, element);
  114. return element;
  115. };
  116. /**
  117. * Scales various elements in order to update the palette's size.
  118. * @param {goog.ui.DimensionPicker} palette The palette object.
  119. * @param {Element} element The element to set the style of.
  120. */
  121. goog.ui.DimensionPickerRenderer.prototype.updateSize = function(
  122. palette, element) {
  123. var size = palette.getSize();
  124. element.style.width = size.width + 'em';
  125. var underlyingDiv = this.getUnderlyingDiv_(element);
  126. underlyingDiv.style.width = size.width + 'em';
  127. underlyingDiv.style.height = size.height + 'em';
  128. if (palette.isRightToLeft()) {
  129. this.adjustParentDirection_(palette, element);
  130. }
  131. };
  132. /**
  133. * Adds the appropriate content elements to the given outer DIV.
  134. * @param {goog.ui.DimensionPicker} palette The palette object.
  135. * @param {Element} element The element to decorate.
  136. * @private
  137. */
  138. goog.ui.DimensionPickerRenderer.prototype.addElementContents_ = function(
  139. palette, element) {
  140. // First we create a single div containing three stacked divs. The bottom div
  141. // catches mouse events. We can't use document level mouse move detection as
  142. // we could lose events to iframes. This is especially important in Firefox 2
  143. // in which TrogEdit creates iframes. The middle div uses a css tiled
  144. // background image to represent deselected tiles. The top div uses a
  145. // different css tiled background image to represent selected tiles.
  146. var mouseCatcherDiv = palette.getDomHelper().createDom(
  147. goog.dom.TagName.DIV,
  148. goog.getCssName(this.getCssClass(), 'mousecatcher'));
  149. var unhighlightedDiv =
  150. palette.getDomHelper().createDom(goog.dom.TagName.DIV, {
  151. 'class': goog.getCssName(this.getCssClass(), 'unhighlighted'),
  152. 'style': 'width:100%;height:100%'
  153. });
  154. var highlightedDiv = palette.getDomHelper().createDom(
  155. goog.dom.TagName.DIV, goog.getCssName(this.getCssClass(), 'highlighted'));
  156. element.appendChild(
  157. palette.getDomHelper().createDom(
  158. goog.dom.TagName.DIV, {'style': 'width:100%;height:100%'},
  159. mouseCatcherDiv, unhighlightedDiv, highlightedDiv));
  160. // Lastly we add a div to store the text version of the current state.
  161. element.appendChild(
  162. palette.getDomHelper().createDom(
  163. goog.dom.TagName.DIV, goog.getCssName(this.getCssClass(), 'status')));
  164. };
  165. /**
  166. * Creates a div and adds the appropriate contents to it.
  167. * @param {goog.ui.Control} control Picker to render.
  168. * @return {!Element} Root element for the palette.
  169. * @override
  170. */
  171. goog.ui.DimensionPickerRenderer.prototype.createDom = function(control) {
  172. var palette = /** @type {goog.ui.DimensionPicker} */ (control);
  173. var classNames = this.getClassNames(palette);
  174. // Hide the element from screen readers so they don't announce "1 of 1" for
  175. // the perceived number of items in the palette.
  176. var element = palette.getDomHelper().createDom(
  177. goog.dom.TagName.DIV,
  178. {'class': classNames ? classNames.join(' ') : '', 'aria-hidden': 'true'});
  179. this.addElementContents_(palette, element);
  180. this.updateSize(palette, element);
  181. return element;
  182. };
  183. /**
  184. * Initializes the control's DOM when the control enters the document. Called
  185. * from {@link goog.ui.Control#enterDocument}.
  186. * @param {goog.ui.Control} control Palette whose DOM is to be
  187. * initialized as it enters the document.
  188. * @override
  189. */
  190. goog.ui.DimensionPickerRenderer.prototype.initializeDom = function(control) {
  191. var palette = /** @type {goog.ui.DimensionPicker} */ (control);
  192. goog.ui.DimensionPickerRenderer.superClass_.initializeDom.call(this, palette);
  193. // Make the displayed highlighted size match the dimension picker's value.
  194. var highlightedSize = palette.getValue();
  195. this.setHighlightedSize(
  196. palette, highlightedSize.width, highlightedSize.height);
  197. this.positionMouseCatcher(palette);
  198. };
  199. /**
  200. * Get the element to listen for mouse move events on.
  201. * @param {goog.ui.DimensionPicker} palette The palette to listen on.
  202. * @return {Element} The element to listen for mouse move events on.
  203. */
  204. goog.ui.DimensionPickerRenderer.prototype.getMouseMoveElement = function(
  205. palette) {
  206. return /** @type {Element} */ (palette.getElement().firstChild);
  207. };
  208. /**
  209. * Returns the x offset in to the grid for the given mouse x position.
  210. * @param {goog.ui.DimensionPicker} palette The table size palette.
  211. * @param {number} x The mouse event x position.
  212. * @return {number} The x offset in to the grid.
  213. */
  214. goog.ui.DimensionPickerRenderer.prototype.getGridOffsetX = function(
  215. palette, x) {
  216. // TODO(robbyw): Don't rely on magic 18 - measure each palette's em size.
  217. return Math.min(palette.maxColumns, Math.ceil(x / 18));
  218. };
  219. /**
  220. * Returns the y offset in to the grid for the given mouse y position.
  221. * @param {goog.ui.DimensionPicker} palette The table size palette.
  222. * @param {number} y The mouse event y position.
  223. * @return {number} The y offset in to the grid.
  224. */
  225. goog.ui.DimensionPickerRenderer.prototype.getGridOffsetY = function(
  226. palette, y) {
  227. return Math.min(palette.maxRows, Math.ceil(y / 18));
  228. };
  229. /**
  230. * Sets the highlighted size. Does nothing if the palette hasn't been rendered.
  231. * @param {goog.ui.DimensionPicker} palette The table size palette.
  232. * @param {number} columns The number of columns to highlight.
  233. * @param {number} rows The number of rows to highlight.
  234. */
  235. goog.ui.DimensionPickerRenderer.prototype.setHighlightedSize = function(
  236. palette, columns, rows) {
  237. var element = palette.getElement();
  238. // Can't update anything if DimensionPicker hasn't been rendered.
  239. if (!element) {
  240. return;
  241. }
  242. // Style the highlight div.
  243. var style = this.getHighlightDiv_(element).style;
  244. style.width = columns + 'em';
  245. style.height = rows + 'em';
  246. // Explicitly set style.right so the element grows to the left when increase
  247. // in width.
  248. if (palette.isRightToLeft()) {
  249. style.right = '0';
  250. }
  251. /**
  252. * @desc The dimension of the columns and rows currently selected in the
  253. * dimension picker, as text that can be spoken by a screen reader.
  254. */
  255. var MSG_DIMENSION_PICKER_HIGHLIGHTED_DIMENSIONS = goog.getMsg(
  256. '{$numCols} by {$numRows}',
  257. {'numCols': String(columns), 'numRows': String(rows)});
  258. this.announcer_.say(
  259. MSG_DIMENSION_PICKER_HIGHLIGHTED_DIMENSIONS,
  260. goog.a11y.aria.LivePriority.ASSERTIVE);
  261. // Update the size text.
  262. goog.dom.setTextContent(
  263. this.getStatusDiv_(element),
  264. goog.i18n.bidi.enforceLtrInText(columns + ' x ' + rows));
  265. };
  266. /**
  267. * Position the mouse catcher such that it receives mouse events past the
  268. * selectedsize up to the maximum size. Takes care to not introduce scrollbars.
  269. * Should be called on enter document and when the window changes size.
  270. * @param {goog.ui.DimensionPicker} palette The table size palette.
  271. */
  272. goog.ui.DimensionPickerRenderer.prototype.positionMouseCatcher = function(
  273. palette) {
  274. var mouseCatcher = this.getMouseCatcher_(palette.getElement());
  275. var doc = goog.dom.getOwnerDocument(mouseCatcher);
  276. var body = doc.body;
  277. var position = goog.style.getRelativePosition(mouseCatcher, body);
  278. // Hide the mouse catcher so it doesn't affect the body's scroll size.
  279. mouseCatcher.style.display = 'none';
  280. // Compute the maximum size the catcher can be without introducing scrolling.
  281. var xAvailableEm = (palette.isRightToLeft() && position.x > 0) ?
  282. Math.floor(position.x / 18) :
  283. Math.floor((body.scrollWidth - position.x) / 18);
  284. // Computing available height is more complicated - we need to check the
  285. // window's inner height.
  286. var height;
  287. if (goog.userAgent.IE) {
  288. // Offset 20px to make up for scrollbar size.
  289. height = goog.style.getClientViewportElement(body).scrollHeight - 20;
  290. } else {
  291. var win = goog.dom.getWindow(doc);
  292. // Offset 20px to make up for scrollbar size.
  293. height = Math.max(win.innerHeight, body.scrollHeight) - 20;
  294. }
  295. var yAvailableEm = Math.floor((height - position.y) / 18);
  296. // Resize and display the mouse catcher.
  297. mouseCatcher.style.width = Math.min(palette.maxColumns, xAvailableEm) + 'em';
  298. mouseCatcher.style.height = Math.min(palette.maxRows, yAvailableEm) + 'em';
  299. mouseCatcher.style.display = '';
  300. // Explicitly set style.right so the mouse catcher is positioned on the left
  301. // side instead of right.
  302. if (palette.isRightToLeft()) {
  303. mouseCatcher.style.right = '0';
  304. }
  305. };
  306. /**
  307. * Returns the CSS class to be applied to the root element of components
  308. * rendered using this renderer.
  309. * @return {string} Renderer-specific CSS class.
  310. * @override
  311. */
  312. goog.ui.DimensionPickerRenderer.prototype.getCssClass = function() {
  313. return goog.ui.DimensionPickerRenderer.CSS_CLASS;
  314. };
  315. /**
  316. * This function adjusts the positioning from 'left' and 'top' to 'right' and
  317. * 'top' as appropriate for RTL control. This is so when the dimensionpicker
  318. * grow in width, the containing element grow to the left instead of right.
  319. * This won't be necessary if goog.ui.SubMenu rendering code would position RTL
  320. * control with 'right' and 'top'.
  321. * @private
  322. *
  323. * @param {goog.ui.DimensionPicker} palette The palette object.
  324. * @param {Element} element The palette's element.
  325. */
  326. goog.ui.DimensionPickerRenderer.prototype.adjustParentDirection_ = function(
  327. palette, element) {
  328. var parent = palette.getParent();
  329. if (parent) {
  330. var parentElement = parent.getElement();
  331. // Anchors the containing element to the right so it grows to the left
  332. // when it increase in width.
  333. var right = goog.style.getStyle(parentElement, 'right');
  334. if (right == '') {
  335. var parentPos = goog.style.getPosition(parentElement);
  336. var parentSize = goog.style.getSize(parentElement);
  337. if (parentSize.width != 0 && parentPos.x != 0) {
  338. var visibleRect =
  339. goog.style.getBounds(goog.style.getClientViewportElement());
  340. var visibleWidth = visibleRect.width;
  341. right = visibleWidth - parentPos.x - parentSize.width;
  342. goog.style.setStyle(parentElement, 'right', right + 'px');
  343. }
  344. }
  345. // When a table is inserted, the containing elemet's position is
  346. // recalculated the next time it shows, set left back to '' to prevent
  347. // extra white space on the left.
  348. var left = goog.style.getStyle(parentElement, 'left');
  349. if (left != '') {
  350. goog.style.setStyle(parentElement, 'left', '');
  351. }
  352. } else {
  353. goog.style.setStyle(element, 'right', '0px');
  354. }
  355. };