charpicker.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924
  1. // Copyright 2009 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 Character Picker widget for picking any Unicode character.
  16. *
  17. * @see ../demos/charpicker.html
  18. */
  19. goog.provide('goog.ui.CharPicker');
  20. goog.require('goog.a11y.aria');
  21. goog.require('goog.a11y.aria.State');
  22. goog.require('goog.array');
  23. goog.require('goog.asserts');
  24. goog.require('goog.dom');
  25. goog.require('goog.dom.TagName');
  26. goog.require('goog.dom.classlist');
  27. goog.require('goog.events');
  28. goog.require('goog.events.Event');
  29. goog.require('goog.events.EventHandler');
  30. goog.require('goog.events.EventType');
  31. goog.require('goog.events.InputHandler');
  32. goog.require('goog.events.KeyCodes');
  33. goog.require('goog.events.KeyHandler');
  34. goog.require('goog.i18n.CharListDecompressor');
  35. goog.require('goog.i18n.CharPickerData');
  36. goog.require('goog.i18n.uChar');
  37. goog.require('goog.i18n.uChar.NameFetcher');
  38. goog.require('goog.structs.Set');
  39. goog.require('goog.style');
  40. goog.require('goog.ui.Button');
  41. goog.require('goog.ui.Component');
  42. goog.require('goog.ui.ContainerScroller');
  43. goog.require('goog.ui.FlatButtonRenderer');
  44. goog.require('goog.ui.HoverCard');
  45. goog.require('goog.ui.LabelInput');
  46. goog.require('goog.ui.Menu');
  47. goog.require('goog.ui.MenuButton');
  48. goog.require('goog.ui.MenuItem');
  49. goog.require('goog.ui.Tooltip');
  50. /**
  51. * Character Picker Class. This widget can be used to pick any Unicode
  52. * character by traversing a category-subcategory structure or by inputing its
  53. * hex value.
  54. *
  55. * See charpicker.html demo for example usage.
  56. * @param {goog.i18n.CharPickerData} charPickerData Category names and charlist.
  57. * @param {!goog.i18n.uChar.NameFetcher} charNameFetcher Object which fetches
  58. * the names of the characters that are shown in the widget. These names
  59. * may be stored locally or come from an external source.
  60. * @param {Array<string>=} opt_recents List of characters to be displayed in
  61. * resently selected characters area.
  62. * @param {number=} opt_initCategory Sequence number of initial category.
  63. * @param {number=} opt_initSubcategory Sequence number of initial subcategory.
  64. * @param {number=} opt_rowCount Number of rows in the grid.
  65. * @param {number=} opt_columnCount Number of columns in the grid.
  66. * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
  67. * @constructor
  68. * @extends {goog.ui.Component}
  69. * @final
  70. */
  71. goog.ui.CharPicker = function(
  72. charPickerData, charNameFetcher, opt_recents, opt_initCategory,
  73. opt_initSubcategory, opt_rowCount, opt_columnCount, opt_domHelper) {
  74. goog.ui.Component.call(this, opt_domHelper);
  75. /**
  76. * Object used to retrieve character names.
  77. * @type {!goog.i18n.uChar.NameFetcher}
  78. * @private
  79. */
  80. this.charNameFetcher_ = charNameFetcher;
  81. /**
  82. * Object containing character lists and category names.
  83. * @type {goog.i18n.CharPickerData}
  84. * @private
  85. */
  86. this.data_ = charPickerData;
  87. /**
  88. * The category number to be used on widget init.
  89. * @type {number}
  90. * @private
  91. */
  92. this.initCategory_ = opt_initCategory || 0;
  93. /**
  94. * The subcategory number to be used on widget init.
  95. * @type {number}
  96. * @private
  97. */
  98. this.initSubcategory_ = opt_initSubcategory || 0;
  99. /**
  100. * Number of columns in the grid.
  101. * @type {number}
  102. * @private
  103. */
  104. this.columnCount_ = opt_columnCount || 10;
  105. /**
  106. * Number of entries to be added to the grid.
  107. * @type {number}
  108. * @private
  109. */
  110. this.gridsize_ = (opt_rowCount || 10) * this.columnCount_;
  111. /**
  112. * Number of the recently selected characters displayed.
  113. * @type {number}
  114. * @private
  115. */
  116. this.recentwidth_ = this.columnCount_ + 1;
  117. /**
  118. * List of recently used characters.
  119. * @type {Array<string>}
  120. * @private
  121. */
  122. this.recents_ = opt_recents || [];
  123. /**
  124. * Handler for events.
  125. * @type {goog.events.EventHandler<!goog.ui.CharPicker>}
  126. * @private
  127. */
  128. this.eventHandler_ = new goog.events.EventHandler(this);
  129. /**
  130. * Decompressor used to get the list of characters from a base88 encoded
  131. * character list.
  132. * @type {Object}
  133. * @private
  134. */
  135. this.decompressor_ = new goog.i18n.CharListDecompressor();
  136. };
  137. goog.inherits(goog.ui.CharPicker, goog.ui.Component);
  138. /**
  139. * The last selected character.
  140. * @type {?string}
  141. * @private
  142. */
  143. goog.ui.CharPicker.prototype.selectedChar_ = null;
  144. /**
  145. * Set of formatting characters whose display need to be swapped with nbsp
  146. * to prevent layout issues.
  147. * @type {goog.structs.Set}
  148. * @private
  149. */
  150. goog.ui.CharPicker.prototype.layoutAlteringChars_ = null;
  151. /**
  152. * The top category menu.
  153. * @type {goog.ui.Menu}
  154. * @private
  155. */
  156. goog.ui.CharPicker.prototype.menu_ = null;
  157. /**
  158. * The top category menu button.
  159. * @type {goog.ui.MenuButton}
  160. * @private
  161. */
  162. goog.ui.CharPicker.prototype.menubutton_ = null;
  163. /**
  164. * The subcategory menu.
  165. * @type {goog.ui.Menu}
  166. * @private
  167. */
  168. goog.ui.CharPicker.prototype.submenu_ = null;
  169. /**
  170. * The subcategory menu button.
  171. * @type {goog.ui.MenuButton}
  172. * @private
  173. */
  174. goog.ui.CharPicker.prototype.submenubutton_ = null;
  175. /** @type {number} */
  176. goog.ui.CharPicker.prototype.itempos;
  177. /** @type {!Array<string>} */
  178. goog.ui.CharPicker.prototype.items;
  179. /** @private {!goog.events.KeyHandler} */
  180. goog.ui.CharPicker.prototype.keyHandler_;
  181. /**
  182. * Category index used to index the data tables.
  183. * @type {number}
  184. */
  185. goog.ui.CharPicker.prototype.category;
  186. /** @private {Element} */
  187. goog.ui.CharPicker.prototype.stick_ = null;
  188. /**
  189. * The element representing the number of rows visible in the grid.
  190. * This along with goog.ui.CharPicker.stick_ would help to create a scrollbar
  191. * of right size.
  192. * @type {HTMLElement}
  193. * @private
  194. */
  195. goog.ui.CharPicker.prototype.stickwrap_ = null;
  196. /**
  197. * The component containing all the buttons for each character in display.
  198. * @type {goog.ui.Component}
  199. * @private
  200. */
  201. goog.ui.CharPicker.prototype.grid_ = null;
  202. /**
  203. * The component used for extra information about the character set displayed.
  204. * @type {goog.ui.Component}
  205. * @private
  206. */
  207. goog.ui.CharPicker.prototype.notice_ = null;
  208. /**
  209. * Grid displaying recently selected characters.
  210. * @type {goog.ui.Component}
  211. * @private
  212. */
  213. goog.ui.CharPicker.prototype.recentgrid_ = null;
  214. /**
  215. * Input field for entering the hex value of the character.
  216. * @type {goog.ui.Component}
  217. * @private
  218. */
  219. goog.ui.CharPicker.prototype.input_ = null;
  220. /**
  221. * OK button for entering hex value of the character.
  222. * @private {goog.ui.Button}
  223. */
  224. goog.ui.CharPicker.prototype.okbutton_ = null;
  225. /**
  226. * Element displaying character name in preview.
  227. * @type {Element}
  228. * @private
  229. */
  230. goog.ui.CharPicker.prototype.charNameEl_ = null;
  231. /**
  232. * Element displaying character in preview.
  233. * @type {Element}
  234. * @private
  235. */
  236. goog.ui.CharPicker.prototype.zoomEl_ = null;
  237. /**
  238. * Element displaying character number (codepoint) in preview.
  239. * @type {Element}
  240. * @private
  241. */
  242. goog.ui.CharPicker.prototype.unicodeEl_ = null;
  243. /**
  244. * Hover card for displaying the preview of a character.
  245. * Preview would contain character in large size and its U+ notation. It would
  246. * also display the name, if available.
  247. * @type {goog.ui.HoverCard}
  248. * @private
  249. */
  250. goog.ui.CharPicker.prototype.hc_ = null;
  251. /**
  252. * Gets the last selected character.
  253. * @return {?string} The last selected character.
  254. */
  255. goog.ui.CharPicker.prototype.getSelectedChar = function() {
  256. return this.selectedChar_;
  257. };
  258. /**
  259. * Gets the list of characters user selected recently.
  260. * @return {Array<string>} The recent character list.
  261. */
  262. goog.ui.CharPicker.prototype.getRecentChars = function() {
  263. return this.recents_;
  264. };
  265. /** @override */
  266. goog.ui.CharPicker.prototype.createDom = function() {
  267. goog.ui.CharPicker.superClass_.createDom.call(this);
  268. this.decorateInternal(
  269. this.getDomHelper().createElement(goog.dom.TagName.DIV));
  270. };
  271. /** @override */
  272. goog.ui.CharPicker.prototype.disposeInternal = function() {
  273. goog.dispose(this.hc_);
  274. this.hc_ = null;
  275. goog.dispose(this.eventHandler_);
  276. this.eventHandler_ = null;
  277. goog.ui.CharPicker.superClass_.disposeInternal.call(this);
  278. };
  279. /** @override */
  280. goog.ui.CharPicker.prototype.decorateInternal = function(element) {
  281. goog.ui.CharPicker.superClass_.decorateInternal.call(this, element);
  282. // The chars below cause layout disruption or too narrow to hover:
  283. // \u0020, \u00AD, \u2000 - \u200f, \u2028 - \u202f, \u3000, \ufeff
  284. var chrs = this.decompressor_.toCharList(':2%C^O80V1H2s2G40Q%s0');
  285. this.layoutAlteringChars_ = new goog.structs.Set(chrs);
  286. this.menu_ = new goog.ui.Menu(this.getDomHelper());
  287. var categories = this.data_.categories;
  288. for (var i = 0; i < this.data_.categories.length; i++) {
  289. this.menu_.addChild(this.createMenuItem_(i, categories[i]), true);
  290. }
  291. this.menubutton_ = new goog.ui.MenuButton(
  292. 'Category Menu', this.menu_,
  293. /* opt_renderer */ undefined, this.getDomHelper());
  294. this.addChild(this.menubutton_, true);
  295. this.submenu_ = new goog.ui.Menu(this.getDomHelper());
  296. this.submenubutton_ = new goog.ui.MenuButton(
  297. 'Subcategory Menu', this.submenu_, /* opt_renderer */ undefined,
  298. this.getDomHelper());
  299. this.addChild(this.submenubutton_, true);
  300. // The containing component for grid component and the scroller.
  301. var gridcontainer = new goog.ui.Component(this.getDomHelper());
  302. this.addChild(gridcontainer, true);
  303. var stickwrap = new goog.ui.Component(this.getDomHelper());
  304. gridcontainer.addChild(stickwrap, true);
  305. this.stickwrap_ = /** @type {!HTMLElement} */ (stickwrap.getElement());
  306. var stick = new goog.ui.Component(this.getDomHelper());
  307. stickwrap.addChild(stick, true);
  308. this.stick_ = stick.getElement();
  309. this.grid_ = new goog.ui.Component(this.getDomHelper());
  310. gridcontainer.addChild(this.grid_, true);
  311. this.notice_ = new goog.ui.Component(this.getDomHelper());
  312. this.notice_.setElementInternal(
  313. this.getDomHelper().createDom(goog.dom.TagName.DIV));
  314. this.addChild(this.notice_, true);
  315. // The component used for displaying 'Recent Selections' label.
  316. /**
  317. * @desc The text label above the list of recently selected characters.
  318. */
  319. var MSG_CHAR_PICKER_RECENT_SELECTIONS = goog.getMsg('Recent Selections:');
  320. var recenttext = new goog.ui.Component(this.getDomHelper());
  321. recenttext.setElementInternal(
  322. this.getDomHelper().createDom(
  323. goog.dom.TagName.SPAN, null, MSG_CHAR_PICKER_RECENT_SELECTIONS));
  324. this.addChild(recenttext, true);
  325. this.recentgrid_ = new goog.ui.Component(this.getDomHelper());
  326. this.addChild(this.recentgrid_, true);
  327. // The component used for displaying 'U+'.
  328. var uplus = new goog.ui.Component(this.getDomHelper());
  329. uplus.setElementInternal(
  330. this.getDomHelper().createDom(goog.dom.TagName.SPAN, null, 'U+'));
  331. this.addChild(uplus, true);
  332. /**
  333. * @desc The text inside the input box to specify the hex code of a character.
  334. */
  335. var MSG_CHAR_PICKER_HEX_INPUT = goog.getMsg('Hex Input');
  336. this.input_ =
  337. new goog.ui.LabelInput(MSG_CHAR_PICKER_HEX_INPUT, this.getDomHelper());
  338. this.addChild(this.input_, true);
  339. this.okbutton_ = new goog.ui.Button(
  340. 'OK', /* opt_renderer */ undefined, this.getDomHelper());
  341. this.addChild(this.okbutton_, true);
  342. this.okbutton_.setEnabled(false);
  343. this.zoomEl_ = this.getDomHelper().createDom(
  344. goog.dom.TagName.DIV,
  345. {id: 'zoom', className: goog.getCssName('goog-char-picker-char-zoom')});
  346. this.charNameEl_ = this.getDomHelper().createDom(
  347. goog.dom.TagName.DIV,
  348. {id: 'charName', className: goog.getCssName('goog-char-picker-name')});
  349. this.unicodeEl_ = this.getDomHelper().createDom(
  350. goog.dom.TagName.DIV,
  351. {id: 'unicode', className: goog.getCssName('goog-char-picker-unicode')});
  352. var card = this.getDomHelper().createDom(
  353. goog.dom.TagName.DIV, {'id': 'preview'}, this.zoomEl_, this.charNameEl_,
  354. this.unicodeEl_);
  355. goog.style.setElementShown(card, false);
  356. this.hc_ = new goog.ui.HoverCard(
  357. {'DIV': 'char'},
  358. /* opt_checkDescendants */ undefined, this.getDomHelper());
  359. this.hc_.setElement(card);
  360. var self = this;
  361. /**
  362. * Function called by hover card just before it is visible to collect data.
  363. */
  364. function onBeforeShow() {
  365. var trigger = self.hc_.getAnchorElement();
  366. var ch = self.getChar_(trigger);
  367. if (ch) {
  368. goog.dom.setTextContent(self.zoomEl_, self.displayChar_(ch));
  369. goog.dom.setTextContent(self.unicodeEl_, goog.i18n.uChar.toHexString(ch));
  370. // Clear the character name since we don't want to show old data because
  371. // it is retrieved asynchronously and the DOM object is re-used
  372. goog.dom.setTextContent(self.charNameEl_, '');
  373. self.charNameFetcher_.getName(ch, function(charName) {
  374. if (charName) {
  375. goog.dom.setTextContent(self.charNameEl_, charName);
  376. }
  377. });
  378. }
  379. }
  380. goog.events.listen(
  381. this.hc_, goog.ui.HoverCard.EventType.BEFORE_SHOW, onBeforeShow);
  382. goog.asserts.assert(element);
  383. goog.dom.classlist.add(element, goog.getCssName('goog-char-picker'));
  384. goog.dom.classlist.add(
  385. goog.asserts.assert(this.stick_), goog.getCssName('goog-stick'));
  386. goog.dom.classlist.add(
  387. goog.asserts.assert(this.stickwrap_), goog.getCssName('goog-stickwrap'));
  388. goog.dom.classlist.add(
  389. goog.asserts.assert(gridcontainer.getElement()),
  390. goog.getCssName('goog-char-picker-grid-container'));
  391. goog.dom.classlist.add(
  392. goog.asserts.assert(this.grid_.getElement()),
  393. goog.getCssName('goog-char-picker-grid'));
  394. goog.dom.classlist.add(
  395. goog.asserts.assert(this.recentgrid_.getElement()),
  396. goog.getCssName('goog-char-picker-grid'));
  397. goog.dom.classlist.add(
  398. goog.asserts.assert(this.recentgrid_.getElement()),
  399. goog.getCssName('goog-char-picker-recents'));
  400. goog.dom.classlist.add(
  401. goog.asserts.assert(this.notice_.getElement()),
  402. goog.getCssName('goog-char-picker-notice'));
  403. goog.dom.classlist.add(
  404. goog.asserts.assert(uplus.getElement()),
  405. goog.getCssName('goog-char-picker-uplus'));
  406. goog.dom.classlist.add(
  407. goog.asserts.assert(this.input_.getElement()),
  408. goog.getCssName('goog-char-picker-input-box'));
  409. goog.dom.classlist.add(
  410. goog.asserts.assert(this.okbutton_.getElement()),
  411. goog.getCssName('goog-char-picker-okbutton'));
  412. goog.dom.classlist.add(
  413. goog.asserts.assert(card), goog.getCssName('goog-char-picker-hovercard'));
  414. this.hc_.className = goog.getCssName('goog-char-picker-hovercard');
  415. this.grid_.buttoncount = this.gridsize_;
  416. this.recentgrid_.buttoncount = this.recentwidth_;
  417. this.populateGridWithButtons_(this.grid_);
  418. this.populateGridWithButtons_(this.recentgrid_);
  419. this.updateGrid_(this.recentgrid_, this.recents_);
  420. this.setSelectedCategory_(this.initCategory_, this.initSubcategory_);
  421. new goog.ui.ContainerScroller(this.menu_);
  422. new goog.ui.ContainerScroller(this.submenu_);
  423. goog.dom.classlist.add(
  424. goog.asserts.assert(this.menu_.getElement()),
  425. goog.getCssName('goog-char-picker-menu'));
  426. goog.dom.classlist.add(
  427. goog.asserts.assert(this.submenu_.getElement()),
  428. goog.getCssName('goog-char-picker-menu'));
  429. };
  430. /** @override */
  431. goog.ui.CharPicker.prototype.enterDocument = function() {
  432. goog.ui.CharPicker.superClass_.enterDocument.call(this);
  433. var inputkh = new goog.events.InputHandler(this.input_.getElement());
  434. this.keyHandler_ = new goog.events.KeyHandler(this.input_.getElement());
  435. // Stop the propagation of ACTION events at menu and submenu buttons.
  436. // If stopped at capture phase, the button will not be set to normal state.
  437. // If not stopped, the user widget will receive the event, which is
  438. // undesired. User widget should receive an event only on the character
  439. // click.
  440. this.eventHandler_
  441. .listen(
  442. this.menubutton_, goog.ui.Component.EventType.ACTION,
  443. goog.events.Event.stopPropagation)
  444. .listen(
  445. this.submenubutton_, goog.ui.Component.EventType.ACTION,
  446. goog.events.Event.stopPropagation)
  447. .listen(
  448. this, goog.ui.Component.EventType.ACTION, this.handleSelectedItem_,
  449. true)
  450. .listen(
  451. inputkh, goog.events.InputHandler.EventType.INPUT, this.handleInput_)
  452. .listen(
  453. this.keyHandler_, goog.events.KeyHandler.EventType.KEY,
  454. this.handleEnter_)
  455. .listen(
  456. this.recentgrid_, goog.ui.Component.EventType.FOCUS,
  457. this.handleFocus_)
  458. .listen(this.grid_, goog.ui.Component.EventType.FOCUS, this.handleFocus_);
  459. goog.events.listen(
  460. this.okbutton_.getElement(), goog.events.EventType.MOUSEDOWN,
  461. this.handleOkClick_, true, this);
  462. goog.events.listen(
  463. this.stickwrap_, goog.events.EventType.SCROLL, this.handleScroll_, true,
  464. this);
  465. };
  466. /**
  467. * Handles the button focus by updating the aria label with the character name
  468. * so it becomes possible to get spoken feedback while tabbing through the
  469. * visible symbols.
  470. * @param {goog.events.Event} e The focus event.
  471. * @private
  472. */
  473. goog.ui.CharPicker.prototype.handleFocus_ = function(e) {
  474. var button = e.target;
  475. var element = button.getElement();
  476. var ch = this.getChar_(element);
  477. // Clear the aria label to avoid speaking the old value in case the button
  478. // element has no char attribute or the character name cannot be retrieved.
  479. goog.a11y.aria.setState(element, goog.a11y.aria.State.LABEL, '');
  480. if (ch) {
  481. // This is working with screen readers because the call to getName is
  482. // synchronous once the values have been prefetched by the RemoteNameFetcher
  483. // and because it is always synchronous when using the LocalNameFetcher.
  484. // Also, the special character itself is not used as the label because some
  485. // screen readers, notably ChromeVox, are not able to speak them.
  486. // TODO(user): Consider changing the NameFetcher API to provide a
  487. // method that lets the caller retrieve multiple character names at once
  488. // so that this asynchronous gymnastic can be avoided.
  489. this.charNameFetcher_.getName(ch, function(charName) {
  490. if (charName) {
  491. goog.a11y.aria.setState(element, goog.a11y.aria.State.LABEL, charName);
  492. }
  493. });
  494. }
  495. };
  496. /**
  497. * On scroll, updates the grid with characters correct to the scroll position.
  498. * @param {goog.events.Event} e Scroll event to handle.
  499. * @private
  500. */
  501. goog.ui.CharPicker.prototype.handleScroll_ = function(e) {
  502. var height = e.target.scrollHeight;
  503. var top = e.target.scrollTop;
  504. var itempos =
  505. Math.ceil(top * this.items.length / (this.columnCount_ * height)) *
  506. this.columnCount_;
  507. if (this.itempos != itempos) {
  508. this.itempos = itempos;
  509. this.modifyGridWithItems_(this.grid_, this.items, itempos);
  510. }
  511. e.stopPropagation();
  512. };
  513. /**
  514. * On a menu click, sets correct character set in the grid; on a grid click
  515. * accept the character as the selected one and adds to recent selection, if not
  516. * already present.
  517. * @param {goog.events.Event} e Event for the click on menus or grid.
  518. * @private
  519. */
  520. goog.ui.CharPicker.prototype.handleSelectedItem_ = function(e) {
  521. var parent = /** @type {goog.ui.Component} */ (e.target).getParent();
  522. if (parent == this.menu_) {
  523. this.menu_.setVisible(false);
  524. this.setSelectedCategory_(e.target.getValue());
  525. } else if (parent == this.submenu_) {
  526. this.submenu_.setVisible(false);
  527. this.setSelectedSubcategory_(e.target.getValue());
  528. } else if (parent == this.grid_) {
  529. var button = e.target.getElement();
  530. this.selectedChar_ = this.getChar_(button);
  531. this.updateRecents_(this.selectedChar_);
  532. } else if (parent == this.recentgrid_) {
  533. this.selectedChar_ = this.getChar_(e.target.getElement());
  534. }
  535. };
  536. /**
  537. * When user types the characters displays the preview. Enables the OK button,
  538. * if the character is valid.
  539. * @param {goog.events.Event} e Event for typing in input field.
  540. * @private
  541. */
  542. goog.ui.CharPicker.prototype.handleInput_ = function(e) {
  543. var ch = this.getInputChar();
  544. if (ch) {
  545. goog.dom.setTextContent(this.zoomEl_, ch);
  546. goog.dom.setTextContent(this.unicodeEl_, goog.i18n.uChar.toHexString(ch));
  547. goog.dom.setTextContent(this.charNameEl_, '');
  548. var coord =
  549. new goog.ui.Tooltip.ElementTooltipPosition(this.input_.getElement());
  550. this.hc_.setPosition(coord);
  551. this.hc_.triggerForElement(this.input_.getElement());
  552. this.okbutton_.setEnabled(true);
  553. } else {
  554. this.hc_.cancelTrigger();
  555. this.hc_.setVisible(false);
  556. this.okbutton_.setEnabled(false);
  557. }
  558. };
  559. /**
  560. * On OK click accepts the character and updates the recent char list.
  561. * @param {goog.events.Event=} opt_event Event for click on OK button.
  562. * @return {boolean} Indicates whether to propagate event.
  563. * @private
  564. */
  565. goog.ui.CharPicker.prototype.handleOkClick_ = function(opt_event) {
  566. var ch = this.getInputChar();
  567. if (ch && ch.charCodeAt(0)) {
  568. this.selectedChar_ = ch;
  569. this.updateRecents_(ch);
  570. return true;
  571. }
  572. return false;
  573. };
  574. /**
  575. * Behaves exactly like the OK button on Enter key.
  576. * @param {goog.events.KeyEvent} e Event for enter on the input field.
  577. * @return {boolean} Indicates whether to propagate event.
  578. * @private
  579. */
  580. goog.ui.CharPicker.prototype.handleEnter_ = function(e) {
  581. if (e.keyCode == goog.events.KeyCodes.ENTER) {
  582. return this.handleOkClick_() ?
  583. this.dispatchEvent(goog.ui.Component.EventType.ACTION) :
  584. false;
  585. }
  586. return false;
  587. };
  588. /**
  589. * Gets the character from the event target.
  590. * @param {Element} e Event target containing the 'char' attribute.
  591. * @return {string} The character specified in the event.
  592. * @private
  593. */
  594. goog.ui.CharPicker.prototype.getChar_ = function(e) {
  595. return e.getAttribute('char');
  596. };
  597. /**
  598. * Creates a menu entry for either the category listing or subcategory listing.
  599. * @param {number} id Id to be used for the entry.
  600. * @param {string} caption Text displayed for the menu item.
  601. * @return {!goog.ui.MenuItem} Menu item to be added to the menu listing.
  602. * @private
  603. */
  604. goog.ui.CharPicker.prototype.createMenuItem_ = function(id, caption) {
  605. var item = new goog.ui.MenuItem(caption, /* model */ id, this.getDomHelper());
  606. item.setVisible(true);
  607. return item;
  608. };
  609. /**
  610. * Sets the category and updates the submenu items and grid accordingly.
  611. * @param {number} category Category index used to index the data tables.
  612. * @param {number=} opt_subcategory Subcategory index used with category index.
  613. * @private
  614. */
  615. goog.ui.CharPicker.prototype.setSelectedCategory_ = function(
  616. category, opt_subcategory) {
  617. this.category = category;
  618. this.menubutton_.setCaption(this.data_.categories[category]);
  619. while (this.submenu_.hasChildren()) {
  620. this.submenu_.removeChildAt(0, true).dispose();
  621. }
  622. var subcategories = this.data_.subcategories[category];
  623. for (var i = 0; i < subcategories.length; i++) {
  624. var item = this.createMenuItem_(i, subcategories[i]);
  625. this.submenu_.addChild(item, true);
  626. }
  627. this.setSelectedSubcategory_(opt_subcategory || 0);
  628. };
  629. /**
  630. * Sets the subcategory and updates the grid accordingly.
  631. * @param {number} subcategory Sub-category index used to index the data tables.
  632. * @private
  633. */
  634. goog.ui.CharPicker.prototype.setSelectedSubcategory_ = function(subcategory) {
  635. var subcategories = this.data_.subcategories;
  636. var name = subcategories[this.category][subcategory];
  637. this.submenubutton_.setCaption(name);
  638. this.setSelectedGrid_(this.category, subcategory);
  639. };
  640. /**
  641. * Updates the grid according to a given category and subcategory.
  642. * @param {number} category Index to the category table.
  643. * @param {number} subcategory Index to the subcategory table.
  644. * @private
  645. */
  646. goog.ui.CharPicker.prototype.setSelectedGrid_ = function(
  647. category, subcategory) {
  648. var charLists = this.data_.charList;
  649. var charListStr = charLists[category][subcategory];
  650. var content = this.decompressor_.toCharList(charListStr);
  651. this.charNameFetcher_.prefetch(charListStr);
  652. this.updateGrid_(this.grid_, content);
  653. };
  654. /**
  655. * Updates the grid with new character list.
  656. * @param {goog.ui.Component} grid The grid which is updated with a new set of
  657. * characters.
  658. * @param {Array<string>} items Characters to be added to the grid.
  659. * @private
  660. */
  661. goog.ui.CharPicker.prototype.updateGrid_ = function(grid, items) {
  662. if (grid == this.grid_) {
  663. /**
  664. * @desc The message used when there are invisible characters like space
  665. * or format control characters.
  666. */
  667. var MSG_PLEASE_HOVER =
  668. goog.getMsg('Please hover over each cell for the character name.');
  669. goog.dom.setTextContent(
  670. this.notice_.getElement(),
  671. this.charNameFetcher_.isNameAvailable(items[0]) ? MSG_PLEASE_HOVER :
  672. '');
  673. this.items = items;
  674. if (this.stickwrap_.offsetHeight > 0) {
  675. this.stick_.style.height =
  676. this.stickwrap_.offsetHeight * items.length / this.gridsize_ + 'px';
  677. } else {
  678. // This is the last ditch effort if height is not avaialble.
  679. // Maximum of 3em is assumed to the the cell height. Extra space after
  680. // last character in the grid is OK.
  681. this.stick_.style.height =
  682. 3 * this.columnCount_ * items.length / this.gridsize_ + 'em';
  683. }
  684. this.stickwrap_.scrollTop = 0;
  685. }
  686. this.modifyGridWithItems_(grid, items, 0);
  687. };
  688. /**
  689. * Updates the grid with new character list for a given starting point.
  690. * @param {goog.ui.Component} grid The grid which is updated with a new set of
  691. * characters.
  692. * @param {Array<string>} items Characters to be added to the grid.
  693. * @param {number} start The index from which the characters should be
  694. * displayed.
  695. * @private
  696. */
  697. goog.ui.CharPicker.prototype.modifyGridWithItems_ = function(
  698. grid, items, start) {
  699. for (var buttonpos = 0, itempos = start;
  700. buttonpos < grid.buttoncount && itempos < items.length;
  701. buttonpos++, itempos++) {
  702. this.modifyCharNode_(
  703. /** @type {!goog.ui.Button} */ (grid.getChildAt(buttonpos)),
  704. items[itempos]);
  705. }
  706. for (; buttonpos < grid.buttoncount; buttonpos++) {
  707. grid.getChildAt(buttonpos).setVisible(false);
  708. }
  709. };
  710. /**
  711. * Creates the grid for characters to displayed for selection.
  712. * @param {goog.ui.Component} grid The grid which is updated with a new set of
  713. * characters.
  714. * @private
  715. */
  716. goog.ui.CharPicker.prototype.populateGridWithButtons_ = function(grid) {
  717. for (var i = 0; i < grid.buttoncount; i++) {
  718. var button = new goog.ui.Button(
  719. ' ', goog.ui.FlatButtonRenderer.getInstance(), this.getDomHelper());
  720. // Dispatch the focus event so we can update the aria description while
  721. // the user tabs through the cells.
  722. button.setDispatchTransitionEvents(goog.ui.Component.State.FOCUSED, true);
  723. grid.addChild(button, true);
  724. button.setVisible(false);
  725. var buttonEl = button.getElement();
  726. goog.asserts.assert(buttonEl, 'The button DOM element cannot be null.');
  727. // Override the button role so the user doesn't hear "button" each time he
  728. // tabs through the cells.
  729. goog.a11y.aria.removeRole(buttonEl);
  730. }
  731. };
  732. /**
  733. * Updates the grid cell with new character.
  734. * @param {goog.ui.Button} button This button is popped up for new character.
  735. * @param {string} ch Character to be displayed by the button.
  736. * @private
  737. */
  738. goog.ui.CharPicker.prototype.modifyCharNode_ = function(button, ch) {
  739. var text = this.displayChar_(ch);
  740. var buttonEl = button.getElement();
  741. goog.dom.setTextContent(buttonEl, text);
  742. buttonEl.setAttribute('char', ch);
  743. button.setVisible(true);
  744. };
  745. /**
  746. * Adds a given character to the recent character list.
  747. * @param {string} character Character to be added to the recent list.
  748. * @private
  749. */
  750. goog.ui.CharPicker.prototype.updateRecents_ = function(character) {
  751. if (character && character.charCodeAt(0) &&
  752. !goog.array.contains(this.recents_, character)) {
  753. this.recents_.unshift(character);
  754. if (this.recents_.length > this.recentwidth_) {
  755. this.recents_.pop();
  756. }
  757. this.updateGrid_(this.recentgrid_, this.recents_);
  758. }
  759. };
  760. /**
  761. * Gets the user inputed unicode character.
  762. * @return {string} Unicode character inputed by user.
  763. */
  764. goog.ui.CharPicker.prototype.getInputChar = function() {
  765. var text = this.input_.getValue();
  766. var code = parseInt(text, 16);
  767. return /** @type {string} */ (goog.i18n.uChar.fromCharCode(code));
  768. };
  769. /**
  770. * Gets the display character for the given character.
  771. * @param {string} ch Character whose display is fetched.
  772. * @return {string} The display of the given character.
  773. * @private
  774. */
  775. goog.ui.CharPicker.prototype.displayChar_ = function(ch) {
  776. return this.layoutAlteringChars_.contains(ch) ? '\u00A0' : ch;
  777. };