labelinput.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. // Copyright 2006 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 This behavior is applied to a text input and it shows a text
  16. * message inside the element if the user hasn't entered any text.
  17. *
  18. * This uses the HTML5 placeholder attribute where it is supported.
  19. *
  20. * This is ported from http://go/labelinput.js
  21. *
  22. * Known issue: Safari does not allow you get to the window object from a
  23. * document. We need that to listen to the onload event. For now we hard code
  24. * the window to the current window.
  25. *
  26. * Known issue: We need to listen to the form submit event but we attach the
  27. * event only once (when created or when it is changed) so if you move the DOM
  28. * node to another form it will not be cleared correctly before submitting.
  29. *
  30. * @author arv@google.com (Erik Arvidsson)
  31. * @see ../demos/labelinput.html
  32. */
  33. goog.provide('goog.ui.LabelInput');
  34. goog.require('goog.Timer');
  35. goog.require('goog.a11y.aria');
  36. goog.require('goog.a11y.aria.State');
  37. goog.require('goog.asserts');
  38. goog.require('goog.dom');
  39. goog.require('goog.dom.InputType');
  40. goog.require('goog.dom.TagName');
  41. goog.require('goog.dom.classlist');
  42. goog.require('goog.events.EventHandler');
  43. goog.require('goog.events.EventType');
  44. goog.require('goog.ui.Component');
  45. goog.require('goog.userAgent');
  46. /**
  47. * This creates the label input object.
  48. * @param {string=} opt_label The text to show as the label.
  49. * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
  50. * @extends {goog.ui.Component}
  51. * @constructor
  52. */
  53. goog.ui.LabelInput = function(opt_label, opt_domHelper) {
  54. goog.ui.Component.call(this, opt_domHelper);
  55. /**
  56. * The text to show as the label.
  57. * @type {string}
  58. * @private
  59. */
  60. this.label_ = opt_label || '';
  61. };
  62. goog.inherits(goog.ui.LabelInput, goog.ui.Component);
  63. goog.tagUnsealableClass(goog.ui.LabelInput);
  64. /**
  65. * Variable used to store the element value on keydown and restore it on
  66. * keypress. See {@link #handleEscapeKeys_}
  67. * @type {?string}
  68. * @private
  69. */
  70. goog.ui.LabelInput.prototype.ffKeyRestoreValue_ = null;
  71. /**
  72. * The label restore delay after leaving the input.
  73. * @type {number} Delay for restoring the label.
  74. * @protected
  75. */
  76. goog.ui.LabelInput.prototype.labelRestoreDelayMs = 10;
  77. /** @private {boolean} */
  78. goog.ui.LabelInput.prototype.inFocusAndSelect_;
  79. /** @private {boolean} */
  80. goog.ui.LabelInput.prototype.formAttached_;
  81. /**
  82. * Indicates whether the browser supports the placeholder attribute, new in
  83. * HTML5.
  84. * @type {?boolean}
  85. * @private
  86. */
  87. goog.ui.LabelInput.supportsPlaceholder_;
  88. /**
  89. * Checks browser support for placeholder attribute.
  90. * @return {boolean} Whether placeholder attribute is supported.
  91. * @private
  92. */
  93. goog.ui.LabelInput.isPlaceholderSupported_ = function() {
  94. if (!goog.isDefAndNotNull(goog.ui.LabelInput.supportsPlaceholder_)) {
  95. goog.ui.LabelInput.supportsPlaceholder_ =
  96. ('placeholder' in goog.dom.createElement(goog.dom.TagName.INPUT));
  97. }
  98. return goog.ui.LabelInput.supportsPlaceholder_;
  99. };
  100. /**
  101. * @type {goog.events.EventHandler}
  102. * @private
  103. */
  104. goog.ui.LabelInput.prototype.eventHandler_;
  105. /**
  106. * @type {boolean}
  107. * @private
  108. */
  109. goog.ui.LabelInput.prototype.hasFocus_ = false;
  110. /**
  111. * Creates the DOM nodes needed for the label input.
  112. * @override
  113. */
  114. goog.ui.LabelInput.prototype.createDom = function() {
  115. this.setElementInternal(
  116. this.getDomHelper().createDom(
  117. goog.dom.TagName.INPUT, {'type': goog.dom.InputType.TEXT}));
  118. };
  119. /**
  120. * Decorates an existing HTML input element as a label input. If the element
  121. * has a "label" attribute then that will be used as the label property for the
  122. * label input object.
  123. * @param {Element} element The HTML input element to decorate.
  124. * @override
  125. */
  126. goog.ui.LabelInput.prototype.decorateInternal = function(element) {
  127. goog.ui.LabelInput.superClass_.decorateInternal.call(this, element);
  128. if (!this.label_) {
  129. this.label_ = element.getAttribute('label') || '';
  130. }
  131. // Check if we're attaching to an element that already has focus.
  132. if (goog.dom.getActiveElement(goog.dom.getOwnerDocument(element)) ==
  133. element) {
  134. this.hasFocus_ = true;
  135. var el = this.getElement();
  136. goog.asserts.assert(el);
  137. goog.dom.classlist.remove(el, this.labelCssClassName);
  138. }
  139. if (goog.ui.LabelInput.isPlaceholderSupported_()) {
  140. this.getElement().placeholder = this.label_;
  141. }
  142. var labelInputElement = this.getElement();
  143. goog.asserts.assert(
  144. labelInputElement, 'The label input element cannot be null.');
  145. goog.a11y.aria.setState(
  146. labelInputElement, goog.a11y.aria.State.LABEL, this.label_);
  147. };
  148. /** @override */
  149. goog.ui.LabelInput.prototype.enterDocument = function() {
  150. goog.ui.LabelInput.superClass_.enterDocument.call(this);
  151. this.attachEvents_();
  152. this.check_();
  153. // Make it easy for other closure widgets to play nicely with inputs using
  154. // LabelInput:
  155. this.getElement().labelInput_ = this;
  156. };
  157. /** @override */
  158. goog.ui.LabelInput.prototype.exitDocument = function() {
  159. goog.ui.LabelInput.superClass_.exitDocument.call(this);
  160. this.detachEvents_();
  161. this.getElement().labelInput_ = null;
  162. };
  163. /**
  164. * Attaches the events we need to listen to.
  165. * @private
  166. */
  167. goog.ui.LabelInput.prototype.attachEvents_ = function() {
  168. var eh = new goog.events.EventHandler(this);
  169. eh.listen(this.getElement(), goog.events.EventType.FOCUS, this.handleFocus_);
  170. eh.listen(this.getElement(), goog.events.EventType.BLUR, this.handleBlur_);
  171. if (goog.ui.LabelInput.isPlaceholderSupported_()) {
  172. this.eventHandler_ = eh;
  173. return;
  174. }
  175. if (goog.userAgent.GECKO) {
  176. eh.listen(
  177. this.getElement(),
  178. [
  179. goog.events.EventType.KEYPRESS, goog.events.EventType.KEYDOWN,
  180. goog.events.EventType.KEYUP
  181. ],
  182. this.handleEscapeKeys_);
  183. }
  184. // IE sets defaultValue upon load so we need to test that as well.
  185. var d = goog.dom.getOwnerDocument(this.getElement());
  186. var w = goog.dom.getWindow(d);
  187. eh.listen(w, goog.events.EventType.LOAD, this.handleWindowLoad_);
  188. this.eventHandler_ = eh;
  189. this.attachEventsToForm_();
  190. };
  191. /**
  192. * Adds a listener to the form so that we can clear the input before it is
  193. * submitted.
  194. * @private
  195. */
  196. goog.ui.LabelInput.prototype.attachEventsToForm_ = function() {
  197. // in case we have are in a form we need to make sure the label is not
  198. // submitted
  199. if (!this.formAttached_ && this.eventHandler_ && this.getElement().form) {
  200. this.eventHandler_.listen(
  201. this.getElement().form, goog.events.EventType.SUBMIT,
  202. this.handleFormSubmit_);
  203. this.formAttached_ = true;
  204. }
  205. };
  206. /**
  207. * Stops listening to the events.
  208. * @private
  209. */
  210. goog.ui.LabelInput.prototype.detachEvents_ = function() {
  211. if (this.eventHandler_) {
  212. this.eventHandler_.dispose();
  213. this.eventHandler_ = null;
  214. }
  215. };
  216. /** @override */
  217. goog.ui.LabelInput.prototype.disposeInternal = function() {
  218. goog.ui.LabelInput.superClass_.disposeInternal.call(this);
  219. this.detachEvents_();
  220. };
  221. /**
  222. * The CSS class name to add to the input when the user has not entered a
  223. * value.
  224. */
  225. goog.ui.LabelInput.prototype.labelCssClassName =
  226. goog.getCssName('label-input-label');
  227. /**
  228. * Handler for the focus event.
  229. * @param {goog.events.Event} e The event object passed in to the event handler.
  230. * @private
  231. */
  232. goog.ui.LabelInput.prototype.handleFocus_ = function(e) {
  233. this.hasFocus_ = true;
  234. var el = this.getElement();
  235. goog.asserts.assert(el);
  236. goog.dom.classlist.remove(el, this.labelCssClassName);
  237. if (goog.ui.LabelInput.isPlaceholderSupported_()) {
  238. return;
  239. }
  240. if (!this.hasChanged() && !this.inFocusAndSelect_) {
  241. var me = this;
  242. var clearValue = function() {
  243. // Component could be disposed by the time this is called.
  244. if (me.getElement()) {
  245. me.getElement().value = '';
  246. }
  247. };
  248. if (goog.userAgent.IE) {
  249. goog.Timer.callOnce(clearValue, 10);
  250. } else {
  251. clearValue();
  252. }
  253. }
  254. };
  255. /**
  256. * Handler for the blur event.
  257. * @param {goog.events.Event} e The event object passed in to the event handler.
  258. * @private
  259. */
  260. goog.ui.LabelInput.prototype.handleBlur_ = function(e) {
  261. // We listen to the click event when we enter focusAndSelect mode so we can
  262. // fake an artificial focus when the user clicks on the input box. However,
  263. // if the user clicks on something else (and we lose focus), there is no
  264. // need for an artificial focus event.
  265. if (!goog.ui.LabelInput.isPlaceholderSupported_()) {
  266. this.eventHandler_.unlisten(
  267. this.getElement(), goog.events.EventType.CLICK, this.handleFocus_);
  268. this.ffKeyRestoreValue_ = null;
  269. }
  270. this.hasFocus_ = false;
  271. this.check_();
  272. };
  273. /**
  274. * Handler for key events in Firefox.
  275. *
  276. * If the escape key is pressed when a text input has not been changed manually
  277. * since being focused, the text input will revert to its previous value.
  278. * Firefox does not honor preventDefault for the escape key. The revert happens
  279. * after the keydown event and before every keypress. We therefore store the
  280. * element's value on keydown and restore it on keypress. The restore value is
  281. * nullified on keyup so that {@link #getValue} returns the correct value.
  282. *
  283. * IE and Chrome don't have this problem, Opera blurs in the input box
  284. * completely in a way that preventDefault on the escape key has no effect.
  285. *
  286. * @param {goog.events.BrowserEvent} e The event object passed in to
  287. * the event handler.
  288. * @private
  289. */
  290. goog.ui.LabelInput.prototype.handleEscapeKeys_ = function(e) {
  291. if (e.keyCode == 27) {
  292. if (e.type == goog.events.EventType.KEYDOWN) {
  293. this.ffKeyRestoreValue_ = this.getElement().value;
  294. } else if (e.type == goog.events.EventType.KEYPRESS) {
  295. this.getElement().value = /** @type {string} */ (this.ffKeyRestoreValue_);
  296. } else if (e.type == goog.events.EventType.KEYUP) {
  297. this.ffKeyRestoreValue_ = null;
  298. }
  299. e.preventDefault();
  300. }
  301. };
  302. /**
  303. * Handler for the submit event of the form element.
  304. * @param {goog.events.Event} e The event object passed in to the event handler.
  305. * @private
  306. */
  307. goog.ui.LabelInput.prototype.handleFormSubmit_ = function(e) {
  308. if (!this.hasChanged()) {
  309. this.getElement().value = '';
  310. // allow form to be sent before restoring value
  311. goog.Timer.callOnce(this.handleAfterSubmit_, 10, this);
  312. }
  313. };
  314. /**
  315. * Restore value after submit
  316. * @private
  317. */
  318. goog.ui.LabelInput.prototype.handleAfterSubmit_ = function() {
  319. if (!this.hasChanged()) {
  320. this.getElement().value = this.label_;
  321. }
  322. };
  323. /**
  324. * Handler for the load event the window. This is needed because
  325. * IE sets defaultValue upon load.
  326. * @param {Event} e The event object passed in to the event handler.
  327. * @private
  328. */
  329. goog.ui.LabelInput.prototype.handleWindowLoad_ = function(e) {
  330. this.check_();
  331. };
  332. /**
  333. * @return {boolean} Whether the control is currently focused on.
  334. */
  335. goog.ui.LabelInput.prototype.hasFocus = function() {
  336. return this.hasFocus_;
  337. };
  338. /**
  339. * @return {boolean} Whether the value has been changed by the user.
  340. */
  341. goog.ui.LabelInput.prototype.hasChanged = function() {
  342. return !!this.getElement() && this.getElement().value != '' &&
  343. this.getElement().value != this.label_;
  344. };
  345. /**
  346. * Clears the value of the input element without resetting the default text.
  347. */
  348. goog.ui.LabelInput.prototype.clear = function() {
  349. this.getElement().value = '';
  350. // Reset ffKeyRestoreValue_ when non-null
  351. if (this.ffKeyRestoreValue_ != null) {
  352. this.ffKeyRestoreValue_ = '';
  353. }
  354. };
  355. /**
  356. * Clears the value of the input element and resets the default text.
  357. */
  358. goog.ui.LabelInput.prototype.reset = function() {
  359. if (this.hasChanged()) {
  360. this.clear();
  361. this.check_();
  362. }
  363. };
  364. /**
  365. * Use this to set the value through script to ensure that the label state is
  366. * up to date
  367. * @param {string} s The new value for the input.
  368. */
  369. goog.ui.LabelInput.prototype.setValue = function(s) {
  370. if (this.ffKeyRestoreValue_ != null) {
  371. this.ffKeyRestoreValue_ = s;
  372. }
  373. this.getElement().value = s;
  374. this.check_();
  375. };
  376. /**
  377. * Returns the current value of the text box, returning an empty string if the
  378. * search box is the default value
  379. * @return {string} The value of the input box.
  380. */
  381. goog.ui.LabelInput.prototype.getValue = function() {
  382. if (this.ffKeyRestoreValue_ != null) {
  383. // Fix the Firefox from incorrectly reporting the value to calling code
  384. // that attached the listener to keypress before the labelinput
  385. return this.ffKeyRestoreValue_;
  386. }
  387. return this.hasChanged() ? /** @type {string} */ (this.getElement().value) :
  388. '';
  389. };
  390. /**
  391. * Sets the label text as aria-label, and placeholder when supported.
  392. * @param {string} label The text to show as the label.
  393. */
  394. goog.ui.LabelInput.prototype.setLabel = function(label) {
  395. var labelInputElement = this.getElement();
  396. if (goog.ui.LabelInput.isPlaceholderSupported_()) {
  397. if (labelInputElement) {
  398. labelInputElement.placeholder = label;
  399. }
  400. this.label_ = label;
  401. } else if (!this.hasChanged()) {
  402. // The this.hasChanged() call relies on non-placeholder behavior checking
  403. // prior to setting this.label_ - it also needs to happen prior to the
  404. // this.restoreLabel_() call.
  405. if (labelInputElement) {
  406. labelInputElement.value = '';
  407. }
  408. this.label_ = label;
  409. this.restoreLabel_();
  410. }
  411. // Check if this has been called before DOM structure building
  412. if (labelInputElement) {
  413. goog.a11y.aria.setState(
  414. labelInputElement, goog.a11y.aria.State.LABEL, this.label_);
  415. }
  416. };
  417. /**
  418. * @return {string} The text to show as the label.
  419. */
  420. goog.ui.LabelInput.prototype.getLabel = function() {
  421. return this.label_;
  422. };
  423. /**
  424. * Checks the state of the input element
  425. * @private
  426. */
  427. goog.ui.LabelInput.prototype.check_ = function() {
  428. var labelInputElement = this.getElement();
  429. goog.asserts.assert(
  430. labelInputElement, 'The label input element cannot be null.');
  431. if (!goog.ui.LabelInput.isPlaceholderSupported_()) {
  432. // if we haven't got a form yet try now
  433. this.attachEventsToForm_();
  434. } else if (this.getElement().placeholder != this.label_) {
  435. this.getElement().placeholder = this.label_;
  436. }
  437. goog.a11y.aria.setState(
  438. labelInputElement, goog.a11y.aria.State.LABEL, this.label_);
  439. if (!this.hasChanged()) {
  440. if (!this.inFocusAndSelect_ && !this.hasFocus_) {
  441. var el = this.getElement();
  442. goog.asserts.assert(el);
  443. goog.dom.classlist.add(el, this.labelCssClassName);
  444. }
  445. // Allow browser to catchup with CSS changes before restoring the label.
  446. if (!goog.ui.LabelInput.isPlaceholderSupported_()) {
  447. goog.Timer.callOnce(this.restoreLabel_, this.labelRestoreDelayMs, this);
  448. }
  449. } else {
  450. var el = this.getElement();
  451. goog.asserts.assert(el);
  452. goog.dom.classlist.remove(el, this.labelCssClassName);
  453. }
  454. };
  455. /**
  456. * This method focuses the input and selects all the text. If the value hasn't
  457. * changed it will set the value to the label so that the label text is
  458. * selected.
  459. */
  460. goog.ui.LabelInput.prototype.focusAndSelect = function() {
  461. // We need to check whether the input has changed before focusing
  462. var hc = this.hasChanged();
  463. this.inFocusAndSelect_ = true;
  464. this.getElement().focus();
  465. if (!hc && !goog.ui.LabelInput.isPlaceholderSupported_()) {
  466. this.getElement().value = this.label_;
  467. }
  468. this.getElement().select();
  469. // Since the object now has focus, we won't get a focus event when they
  470. // click in the input element. The expected behavior when you click on
  471. // the default text is that it goes away and allows you to type...so we
  472. // have to fire an artificial focus event when we're in focusAndSelect mode.
  473. if (goog.ui.LabelInput.isPlaceholderSupported_()) {
  474. return;
  475. }
  476. if (this.eventHandler_) {
  477. this.eventHandler_.listenOnce(
  478. this.getElement(), goog.events.EventType.CLICK, this.handleFocus_);
  479. }
  480. // set to false in timer to let IE trigger the focus event
  481. goog.Timer.callOnce(this.focusAndSelect_, 10, this);
  482. };
  483. /**
  484. * Enables/Disables the label input.
  485. * @param {boolean} enabled Whether to enable (true) or disable (false) the
  486. * label input.
  487. */
  488. goog.ui.LabelInput.prototype.setEnabled = function(enabled) {
  489. this.getElement().disabled = !enabled;
  490. var el = this.getElement();
  491. goog.asserts.assert(el);
  492. goog.dom.classlist.enable(
  493. el, goog.getCssName(this.labelCssClassName, 'disabled'), !enabled);
  494. };
  495. /**
  496. * @return {boolean} True if the label input is enabled, false otherwise.
  497. */
  498. goog.ui.LabelInput.prototype.isEnabled = function() {
  499. return !this.getElement().disabled;
  500. };
  501. /**
  502. * @private
  503. */
  504. goog.ui.LabelInput.prototype.focusAndSelect_ = function() {
  505. this.inFocusAndSelect_ = false;
  506. };
  507. /**
  508. * Sets the value of the input element to label.
  509. * @private
  510. */
  511. goog.ui.LabelInput.prototype.restoreLabel_ = function() {
  512. // Check again in case something changed since this was scheduled.
  513. // We check that the element is still there since this is called by a timer
  514. // and the dispose method may have been called prior to this.
  515. if (this.getElement() && !this.hasChanged() && !this.hasFocus_) {
  516. this.getElement().value = this.label_;
  517. }
  518. };