You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

placeholders.js 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. /*
  2. * The MIT License
  3. *
  4. * Copyright (c) 2012 James Allardice
  5. *
  6. * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
  7. * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
  8. * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
  9. *
  10. * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
  11. *
  12. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  13. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  14. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  15. * THE SOFTWARE.
  16. */
  17. // Defines the global Placeholders object along with various utility methods
  18. (function (global) {
  19. "use strict";
  20. // Cross-browser DOM event binding
  21. function addEventListener(elem, event, fn) {
  22. if (elem.addEventListener) {
  23. return elem.addEventListener(event, fn, false);
  24. }
  25. if (elem.attachEvent) {
  26. return elem.attachEvent("on" + event, fn);
  27. }
  28. }
  29. // Check whether an item is in an array (we don't use Array.prototype.indexOf so we don't clobber any existing polyfills - this is a really simple alternative)
  30. function inArray(arr, item) {
  31. var i, len;
  32. for (i = 0, len = arr.length; i < len; i++) {
  33. if (arr[i] === item) {
  34. return true;
  35. }
  36. }
  37. return false;
  38. }
  39. // Move the caret to the index position specified. Assumes that the element has focus
  40. function moveCaret(elem, index) {
  41. var range;
  42. if (elem.createTextRange) {
  43. range = elem.createTextRange();
  44. range.move("character", index);
  45. range.select();
  46. } else if (elem.selectionStart) {
  47. elem.focus();
  48. elem.setSelectionRange(index, index);
  49. }
  50. }
  51. // Attempt to change the type property of an input element
  52. function changeType(elem, type) {
  53. try {
  54. elem.type = type;
  55. return true;
  56. } catch (e) {
  57. // You can't change input type in IE8 and below
  58. return false;
  59. }
  60. }
  61. // Expose public methods
  62. global.Placeholders = {
  63. Utils: {
  64. addEventListener: addEventListener,
  65. inArray: inArray,
  66. moveCaret: moveCaret,
  67. changeType: changeType
  68. }
  69. };
  70. }(this));
  71. (function (global) {
  72. "use strict";
  73. var validTypes = [
  74. "text",
  75. "search",
  76. "url",
  77. "tel",
  78. "email",
  79. "password",
  80. "number",
  81. "textarea"
  82. ],
  83. // The list of keycodes that are not allowed when the polyfill is configured to hide-on-input
  84. badKeys = [
  85. // The following keys all cause the caret to jump to the end of the input value
  86. 27, // Escape
  87. 33, // Page up
  88. 34, // Page down
  89. 35, // End
  90. 36, // Home
  91. // Arrow keys allow you to move the caret manually, which should be prevented when the placeholder is visible
  92. 37, // Left
  93. 38, // Up
  94. 39, // Right
  95. 40, // Down
  96. // The following keys allow you to modify the placeholder text by removing characters, which should be prevented when the placeholder is visible
  97. 8, // Backspace
  98. 46 // Delete
  99. ],
  100. // Styling variables
  101. placeholderStyleColor = "#ccc",
  102. placeholderClassName = "placeholdersjs",
  103. classNameRegExp = new RegExp("(?:^|\\s)" + placeholderClassName + "(?!\\S)"),
  104. // These will hold references to all elements that can be affected. NodeList objects are live, so we only need to get those references once
  105. inputs, textareas,
  106. // The various data-* attributes used by the polyfill
  107. ATTR_CURRENT_VAL = "data-placeholder-value",
  108. ATTR_ACTIVE = "data-placeholder-active",
  109. ATTR_INPUT_TYPE = "data-placeholder-type",
  110. ATTR_FORM_HANDLED = "data-placeholder-submit",
  111. ATTR_EVENTS_BOUND = "data-placeholder-bound",
  112. ATTR_OPTION_FOCUS = "data-placeholder-focus",
  113. ATTR_OPTION_LIVE = "data-placeholder-live",
  114. ATTR_MAXLENGTH = "data-placeholder-maxlength",
  115. // Various other variables used throughout the rest of the script
  116. test = document.createElement("input"),
  117. head = document.getElementsByTagName("head")[0],
  118. root = document.documentElement,
  119. Placeholders = global.Placeholders,
  120. Utils = Placeholders.Utils,
  121. hideOnInput, liveUpdates, keydownVal, styleElem, styleRules, placeholder, timer, form, elem, len, i;
  122. // No-op (used in place of public methods when native support is detected)
  123. function noop() {}
  124. // Avoid IE9 activeElement of death when an iframe is used.
  125. // More info:
  126. // http://bugs.jquery.com/ticket/13393
  127. // https://github.com/jquery/jquery/commit/85fc5878b3c6af73f42d61eedf73013e7faae408
  128. function safeActiveElement() {
  129. try {
  130. return document.activeElement;
  131. } catch (err) {}
  132. }
  133. // Hide the placeholder value on a single element. Returns true if the placeholder was hidden and false if it was not (because it wasn't visible in the first place)
  134. function hidePlaceholder(elem, keydownValue) {
  135. var type,
  136. maxLength,
  137. valueChanged = (!!keydownValue && elem.value !== keydownValue),
  138. isPlaceholderValue = (elem.value === elem.getAttribute(ATTR_CURRENT_VAL));
  139. if ((valueChanged || isPlaceholderValue) && elem.getAttribute(ATTR_ACTIVE) === "true") {
  140. elem.removeAttribute(ATTR_ACTIVE);
  141. elem.value = elem.value.replace(elem.getAttribute(ATTR_CURRENT_VAL), "");
  142. elem.className = elem.className.replace(classNameRegExp, "");
  143. // Restore the maxlength value
  144. maxLength = elem.getAttribute(ATTR_MAXLENGTH);
  145. if (parseInt(maxLength, 10) >= 0) { // Old FF returns -1 if attribute not set (see GH-56)
  146. elem.setAttribute("maxLength", maxLength);
  147. elem.removeAttribute(ATTR_MAXLENGTH);
  148. }
  149. // If the polyfill has changed the type of the element we need to change it back
  150. type = elem.getAttribute(ATTR_INPUT_TYPE);
  151. if (type) {
  152. elem.type = type;
  153. }
  154. return true;
  155. }
  156. return false;
  157. }
  158. // Show the placeholder value on a single element. Returns true if the placeholder was shown and false if it was not (because it was already visible)
  159. function showPlaceholder(elem) {
  160. var type,
  161. maxLength,
  162. val = elem.getAttribute(ATTR_CURRENT_VAL);
  163. if (elem.value === "" && val) {
  164. elem.setAttribute(ATTR_ACTIVE, "true");
  165. elem.value = val;
  166. elem.className += " " + placeholderClassName;
  167. // Store and remove the maxlength value
  168. maxLength = elem.getAttribute(ATTR_MAXLENGTH);
  169. if (!maxLength) {
  170. elem.setAttribute(ATTR_MAXLENGTH, elem.maxLength);
  171. elem.removeAttribute("maxLength");
  172. }
  173. // If the type of element needs to change, change it (e.g. password inputs)
  174. type = elem.getAttribute(ATTR_INPUT_TYPE);
  175. if (type) {
  176. elem.type = "text";
  177. } else if (elem.type === "password") {
  178. if (Utils.changeType(elem, "text")) {
  179. elem.setAttribute(ATTR_INPUT_TYPE, "password");
  180. }
  181. }
  182. return true;
  183. }
  184. return false;
  185. }
  186. function handleElem(node, callback) {
  187. var handleInputsLength, handleTextareasLength, handleInputs, handleTextareas, elem, len, i;
  188. // Check if the passed in node is an input/textarea (in which case it can't have any affected descendants)
  189. if (node && node.getAttribute(ATTR_CURRENT_VAL)) {
  190. callback(node);
  191. } else {
  192. // If an element was passed in, get all affected descendants. Otherwise, get all affected elements in document
  193. handleInputs = node ? node.getElementsByTagName("input") : inputs;
  194. handleTextareas = node ? node.getElementsByTagName("textarea") : textareas;
  195. handleInputsLength = handleInputs ? handleInputs.length : 0;
  196. handleTextareasLength = handleTextareas ? handleTextareas.length : 0;
  197. // Run the callback for each element
  198. for (i = 0, len = handleInputsLength + handleTextareasLength; i < len; i++) {
  199. elem = i < handleInputsLength ? handleInputs[i] : handleTextareas[i - handleInputsLength];
  200. callback(elem);
  201. }
  202. }
  203. }
  204. // Return all affected elements to their normal state (remove placeholder value if present)
  205. function disablePlaceholders(node) {
  206. handleElem(node, hidePlaceholder);
  207. }
  208. // Show the placeholder value on all appropriate elements
  209. function enablePlaceholders(node) {
  210. handleElem(node, showPlaceholder);
  211. }
  212. // Returns a function that is used as a focus event handler
  213. function makeFocusHandler(elem) {
  214. return function () {
  215. // Only hide the placeholder value if the (default) hide-on-focus behaviour is enabled
  216. if (hideOnInput && elem.value === elem.getAttribute(ATTR_CURRENT_VAL) && elem.getAttribute(ATTR_ACTIVE) === "true") {
  217. // Move the caret to the start of the input (this mimics the behaviour of all browsers that do not hide the placeholder on focus)
  218. Utils.moveCaret(elem, 0);
  219. } else {
  220. // Remove the placeholder
  221. hidePlaceholder(elem);
  222. }
  223. };
  224. }
  225. // Returns a function that is used as a blur event handler
  226. function makeBlurHandler(elem) {
  227. return function () {
  228. showPlaceholder(elem);
  229. };
  230. }
  231. // Functions that are used as a event handlers when the hide-on-input behaviour has been activated - very basic implementation of the "input" event
  232. function makeKeydownHandler(elem) {
  233. return function (e) {
  234. keydownVal = elem.value;
  235. //Prevent the use of the arrow keys (try to keep the cursor before the placeholder)
  236. if (elem.getAttribute(ATTR_ACTIVE) === "true") {
  237. if (keydownVal === elem.getAttribute(ATTR_CURRENT_VAL) && Utils.inArray(badKeys, e.keyCode)) {
  238. if (e.preventDefault) {
  239. e.preventDefault();
  240. }
  241. return false;
  242. }
  243. }
  244. };
  245. }
  246. function makeKeyupHandler(elem) {
  247. return function () {
  248. hidePlaceholder(elem, keydownVal);
  249. // If the element is now empty we need to show the placeholder
  250. if (elem.value === "") {
  251. elem.blur();
  252. Utils.moveCaret(elem, 0);
  253. }
  254. };
  255. }
  256. function makeClickHandler(elem) {
  257. return function () {
  258. if (elem === safeActiveElement() && elem.value === elem.getAttribute(ATTR_CURRENT_VAL) && elem.getAttribute(ATTR_ACTIVE) === "true") {
  259. Utils.moveCaret(elem, 0);
  260. }
  261. };
  262. }
  263. // Returns a function that is used as a submit event handler on form elements that have children affected by this polyfill
  264. function makeSubmitHandler(form) {
  265. return function () {
  266. // Turn off placeholders on all appropriate descendant elements
  267. disablePlaceholders(form);
  268. };
  269. }
  270. // Bind event handlers to an element that we need to affect with the polyfill
  271. function newElement(elem) {
  272. // If the element is part of a form, make sure the placeholder string is not submitted as a value
  273. if (elem.form) {
  274. form = elem.form;
  275. // If the type of the property is a string then we have a "form" attribute and need to get the real form
  276. if (typeof form === "string") {
  277. form = document.getElementById(form);
  278. }
  279. // Set a flag on the form so we know it's been handled (forms can contain multiple inputs)
  280. if (!form.getAttribute(ATTR_FORM_HANDLED)) {
  281. Utils.addEventListener(form, "submit", makeSubmitHandler(form));
  282. form.setAttribute(ATTR_FORM_HANDLED, "true");
  283. }
  284. }
  285. // Bind event handlers to the element so we can hide/show the placeholder as appropriate
  286. Utils.addEventListener(elem, "focus", makeFocusHandler(elem));
  287. Utils.addEventListener(elem, "blur", makeBlurHandler(elem));
  288. // If the placeholder should hide on input rather than on focus we need additional event handlers
  289. if (hideOnInput) {
  290. Utils.addEventListener(elem, "keydown", makeKeydownHandler(elem));
  291. Utils.addEventListener(elem, "keyup", makeKeyupHandler(elem));
  292. Utils.addEventListener(elem, "click", makeClickHandler(elem));
  293. }
  294. // Remember that we've bound event handlers to this element
  295. elem.setAttribute(ATTR_EVENTS_BOUND, "true");
  296. elem.setAttribute(ATTR_CURRENT_VAL, placeholder);
  297. // If the element doesn't have a value and is not focussed, set it to the placeholder string
  298. if (hideOnInput || elem !== safeActiveElement()) {
  299. showPlaceholder(elem);
  300. }
  301. }
  302. Placeholders.nativeSupport = test.placeholder !== void 0;
  303. if (!Placeholders.nativeSupport) {
  304. // Get references to all the input and textarea elements currently in the DOM (live NodeList objects to we only need to do this once)
  305. inputs = document.getElementsByTagName("input");
  306. textareas = document.getElementsByTagName("textarea");
  307. // Get any settings declared as data-* attributes on the root element (currently the only options are whether to hide the placeholder on focus or input and whether to auto-update)
  308. hideOnInput = root.getAttribute(ATTR_OPTION_FOCUS) === "false";
  309. liveUpdates = root.getAttribute(ATTR_OPTION_LIVE) !== "false";
  310. // Create style element for placeholder styles (instead of directly setting style properties on elements - allows for better flexibility alongside user-defined styles)
  311. styleElem = document.createElement("style");
  312. styleElem.type = "text/css";
  313. // Create style rules as text node
  314. styleRules = document.createTextNode("." + placeholderClassName + " { color:" + placeholderStyleColor + "; }");
  315. // Append style rules to newly created stylesheet
  316. if (styleElem.styleSheet) {
  317. styleElem.styleSheet.cssText = styleRules.nodeValue;
  318. } else {
  319. styleElem.appendChild(styleRules);
  320. }
  321. // Prepend new style element to the head (before any existing stylesheets, so user-defined rules take precedence)
  322. head.insertBefore(styleElem, head.firstChild);
  323. // Set up the placeholders
  324. for (i = 0, len = inputs.length + textareas.length; i < len; i++) {
  325. elem = i < inputs.length ? inputs[i] : textareas[i - inputs.length];
  326. // Get the value of the placeholder attribute, if any. IE10 emulating IE7 fails with getAttribute, hence the use of the attributes node
  327. placeholder = elem.attributes.placeholder;
  328. if (placeholder) {
  329. // IE returns an empty object instead of undefined if the attribute is not present
  330. placeholder = placeholder.nodeValue;
  331. // Only apply the polyfill if this element is of a type that supports placeholders, and has a placeholder attribute with a non-empty value
  332. if (placeholder && Utils.inArray(validTypes, elem.type)) {
  333. newElement(elem);
  334. }
  335. }
  336. }
  337. // If enabled, the polyfill will repeatedly check for changed/added elements and apply to those as well
  338. timer = setInterval(function () {
  339. for (i = 0, len = inputs.length + textareas.length; i < len; i++) {
  340. elem = i < inputs.length ? inputs[i] : textareas[i - inputs.length];
  341. // Only apply the polyfill if this element is of a type that supports placeholders, and has a placeholder attribute with a non-empty value
  342. placeholder = elem.attributes.placeholder;
  343. if (placeholder) {
  344. placeholder = placeholder.nodeValue;
  345. if (placeholder && Utils.inArray(validTypes, elem.type)) {
  346. // If the element hasn't had event handlers bound to it then add them
  347. if (!elem.getAttribute(ATTR_EVENTS_BOUND)) {
  348. newElement(elem);
  349. }
  350. // If the placeholder value has changed or not been initialised yet we need to update the display
  351. if (placeholder !== elem.getAttribute(ATTR_CURRENT_VAL) || (elem.type === "password" && !elem.getAttribute(ATTR_INPUT_TYPE))) {
  352. // Attempt to change the type of password inputs (fails in IE < 9)
  353. if (elem.type === "password" && !elem.getAttribute(ATTR_INPUT_TYPE) && Utils.changeType(elem, "text")) {
  354. elem.setAttribute(ATTR_INPUT_TYPE, "password");
  355. }
  356. // If the placeholder value has changed and the placeholder is currently on display we need to change it
  357. if (elem.value === elem.getAttribute(ATTR_CURRENT_VAL)) {
  358. elem.value = placeholder;
  359. }
  360. // Keep a reference to the current placeholder value in case it changes via another script
  361. elem.setAttribute(ATTR_CURRENT_VAL, placeholder);
  362. }
  363. }
  364. } else if (elem.getAttribute(ATTR_ACTIVE)) {
  365. hidePlaceholder(elem);
  366. elem.removeAttribute(ATTR_CURRENT_VAL);
  367. }
  368. }
  369. // If live updates are not enabled cancel the timer
  370. if (!liveUpdates) {
  371. clearInterval(timer);
  372. }
  373. }, 100);
  374. }
  375. Utils.addEventListener(global, "beforeunload", function () {
  376. Placeholders.disable();
  377. });
  378. // Expose public methods
  379. Placeholders.disable = Placeholders.nativeSupport ? noop : disablePlaceholders;
  380. Placeholders.enable = Placeholders.nativeSupport ? noop : enablePlaceholders;
  381. }(this));