diff --git a/bower.json b/bower.json index 4eb2cfd..b1533b4 100644 --- a/bower.json +++ b/bower.json @@ -1,7 +1,7 @@ { "name": "angular-shims-placeholder", "description": "Angular directive to emulate the `placeholder` attribute on text and password input fields for old browsers, such as IE9, IE8, and below", - "version": "0.4.2", + "version": "0.4.3", "homepage": "https://github.com/cvn/angular-shims-placeholder", "authors": [ { diff --git a/dist/angular-shims-placeholder.js b/dist/angular-shims-placeholder.js index 7d97c10..4840a26 100644 --- a/dist/angular-shims-placeholder.js +++ b/dist/angular-shims-placeholder.js @@ -1,218 +1,299 @@ -/*! angular-shims-placeholder - v0.4.2 - 2015-05-11 +/*! angular-shims-placeholder - v0.4.3 - 2015-06-14 * https://github.com/cvn/angular-shims-placeholder * Copyright (c) 2015 Chad von Nau; Licensed MIT */ -(function (angular, document, undefined) { - 'use strict'; - angular.module('ng.shims.placeholder', []).service('placeholderSniffer', [ - '$document', - function ($document) { - this.emptyClassName = 'empty', this.hasPlaceholder = function () { - var test = $document[0].createElement('input'); - return test.placeholder !== void 0; - }; - } - ]).directive('placeholder', [ - '$timeout', - '$document', - '$interpolate', - '$injector', - 'placeholderSniffer', - function ($timeout, $document, $interpolate, $injector, placeholderSniffer) { - if (placeholderSniffer.hasPlaceholder()) - return {}; - var documentListenersApplied = false, angularVersion = parseFloat(angular.version.full); - try { - var $animate = $injector.get('$animate'); - } catch (e) { - } - return { - restrict: 'A', - require: '?ngModel', - priority: angularVersion >= 1.2 ? 110 : -10, - link: function (scope, elem, attrs, ngModel) { - var orig_val = getValue(), domElem = elem[0], elemType = domElem.nodeName.toLowerCase(), isInput = elemType === 'input' || elemType === 'textarea', is_pwd = attrs.type === 'password', text = attrs.placeholder, emptyClassName = placeholderSniffer.emptyClassName, hiddenClassName = 'ng-hide', clone; - if (!isInput) { - return; - } - attrs.$observe('placeholder', function (newValue) { - changePlaceholder(newValue); - }); - if (is_pwd) { - setupPasswordPlaceholder(); - } - setValue(orig_val); - elem.bind('focus', function () { - if (elem.hasClass(emptyClassName)) { - elem.val(''); - elem.removeClass(emptyClassName); - domElem.select(); - } - }); - elem.bind('blur', updateValue); - if (!ngModel) { - elem.bind('change', function () { - changePlaceholder($interpolate(elem.attr('placeholder'))(scope)); - }); - } - if (ngModel) { - ngModel.$render = function () { - setValue(ngModel.$viewValue); - if (domElem === document.activeElement && !elem.val()) { - domElem.select(); - } - }; - } - if (!documentListenersApplied) { - $document.bind('selectstart', function (e) { - var elmn = angular.element(e.target); - if (elmn.hasClass(emptyClassName) && elmn.prop('disabled')) { - e.preventDefault(); - } - }); - documentListenersApplied = true; - } - function updateValue(e) { - var val = elem.val(); - if (elem.hasClass(emptyClassName) && val && val === text) { - return; - } - conditionalDefer(function () { - setValue(val); - }); - } - function conditionalDefer(callback) { - if (document.documentMode <= 11) { - $timeout(callback, 0); - } else { - callback(); - } - } - function setValue(val) { - if (!val && val !== 0 && domElem !== document.activeElement) { - elem.addClass(emptyClassName); - elem.val(!is_pwd ? text : ''); - } else { - elem.removeClass(emptyClassName); - elem.val(val); - } - if (is_pwd) { - updatePasswordPlaceholder(); - if ($animate) { - asyncUpdatePasswordPlaceholder(); - } - } - } - function getValue() { - if (ngModel) { - return scope.$eval(attrs.ngModel) || ''; - } - return getDomValue() || ''; - } - function getDomValue() { - var val = elem.val(); - if (val === attrs.placeholder) { - val = ''; - } - return val; - } - function changePlaceholder(value) { - if (elem.hasClass(emptyClassName) && elem.val() === text) { - elem.val(''); - } - text = value; - updateValue(); - } - function setAttrUnselectable(elmn, enable) { - if (enable) { - elmn.attr('unselectable', 'on'); - } else { - elmn.removeAttr('unselectable'); - } - } - function setupPasswordPlaceholder() { - clone = angular.element(''); - stylePasswordPlaceholder(); - hideElement(clone); - clone.addClass(emptyClassName).bind('focus', hidePasswordPlaceholderAndFocus); - domElem.parentNode.insertBefore(clone[0], domElem); - var watchAttrs = [ - attrs.ngDisabled, - attrs.ngReadonly, - attrs.ngRequired, - attrs.ngShow, - attrs.ngHide - ]; - for (var i = 0; i < watchAttrs.length; i++) { - if (watchAttrs[i]) { - scope.$watch(watchAttrs[i], flexibleUpdatePasswordPlaceholder); - } - } - } - function updatePasswordPlaceholder() { - stylePasswordPlaceholder(); - if (isNgHidden()) { - hideElement(clone); - } else if (elem.hasClass(emptyClassName) && domElem !== document.activeElement) { - showPasswordPlaceholder(); - } else { - hidePasswordPlaceholder(); - } - } - function asyncUpdatePasswordPlaceholder() { - if (angularVersion >= 1.3) { - $animate.addClass(elem, '').then(updatePasswordPlaceholder); - } else { - $animate.addClass(elem, '', updatePasswordPlaceholder); - } - } - function flexibleUpdatePasswordPlaceholder() { - if ($animate) { - asyncUpdatePasswordPlaceholder(); - } else { - updatePasswordPlaceholder(); - } - } - function stylePasswordPlaceholder() { - clone.val(text); - clone.attr('class', elem.attr('class') || '').attr('style', elem.attr('style') || '').prop('disabled', elem.prop('disabled')).prop('readOnly', elem.prop('readOnly')).prop('required', elem.prop('required')); - setAttrUnselectable(clone, elem.attr('unselectable') === 'on'); - } - function showElement(elmn) { - if (angularVersion >= 1.2) { - elmn.removeClass(hiddenClassName); - } else { - elmn.css('display', ''); - } - } - function hideElement(elmn) { - if (angularVersion >= 1.2) { - elmn.addClass(hiddenClassName); - } else { - elmn.css('display', 'none'); - } - } - function showPasswordPlaceholder() { - hideElement(elem); - showElement(clone); - } - function hidePasswordPlaceholder() { - hideElement(clone); - showElement(elem); - } - function hidePasswordPlaceholderAndFocus() { - hidePasswordPlaceholder(); - domElem.focus(); - } - function isNgHidden() { - var hasNgShow = typeof attrs.ngShow !== 'undefined', hasNgHide = typeof attrs.ngHide !== 'undefined'; - if (hasNgShow || hasNgHide) { - return hasNgShow && !scope.$eval(attrs.ngShow) || hasNgHide && scope.$eval(attrs.ngHide); - } else { - return false; - } - } - } - }; - } - ]); -}(window.angular, window.document)); \ No newline at end of file +(function(angular, document, undefined) { +'use strict'; + +angular.module('ng.shims.placeholder', []) +.service('placeholderSniffer', ["$document", function($document){ + this.emptyClassName = 'empty', + this.hasPlaceholder = function() { + // test for native placeholder support + var test = $document[0].createElement("input"); + return (test.placeholder !== void 0); + }; +}]) +.directive('placeholder', ["$timeout", "$document", "$interpolate", "$injector", "placeholderSniffer", function($timeout, $document, $interpolate, $injector, placeholderSniffer) { + if (placeholderSniffer.hasPlaceholder()) return {}; + + var documentListenersApplied = false, + angularVersion = parseFloat(angular.version.full); + + // load $animate if available, to coordinate with other directives that use it + try { + var $animate = $injector.get('$animate'); + } catch (e) {} + + // No native support for attribute placeholder + return { + restrict: 'A', + require: '?ngModel', + // run after ngModel (0) and BOOLEAN_ATTR (100) directives. + // priority order was reversed in Angular 1.2, so we must account for this + priority: (angularVersion >= 1.2) ? 110 : -10, + link: function(scope, elem, attrs, ngModel) { + var orig_val = getValue(), + domElem = elem[0], + elemType = domElem.nodeName.toLowerCase(), + isInput = elemType === 'input' || elemType === 'textarea', + is_pwd = attrs.type === 'password', + text = attrs.placeholder || '', + emptyClassName = placeholderSniffer.emptyClassName, + hiddenClassName = 'ng-hide', + clone; + + if (!isInput) { return; } + + attrs.$observe('placeholder', function (newValue) { + changePlaceholder(newValue); + }); + + if (is_pwd) { setupPasswordPlaceholder(); } + + // init + setValue(orig_val); + + // on focus, replace auto-label with empty field + elem.bind('focus', function() { + if (elem.hasClass(emptyClassName)) { + elem.val(''); + elem.removeClass(emptyClassName); + domElem.select(); // IE8/9 show text cursor after tabbing in + } + }); + + // on blur, show placeholder if necessary + elem.bind('blur', updateValue); + + // handler for model-less inputs to interact with non-angular code + if (!ngModel) { + elem.bind('change', function () { + changePlaceholder($interpolate(elem.attr('placeholder') || '')(scope)); + }); + } + + // model -> view + if (ngModel) { + ngModel.$render = function() { + setValue(ngModel.$viewValue); + // IE8/9: show text cursor after updating value while + // focused, this happens when tabbing into a field, and the + // deferred keydown handler from the previous field fires + // + // TODO: remove when tab key behavior is fixed in + // angular core + if (domElem === document.activeElement && !elem.val()) { + domElem.select(); + } + }; + } + + if (!documentListenersApplied) { + // cancel selection of placeholder text on disabled elements + // disabled elements do not emit selectstart events in IE8/IE9, + // so bind to $document and catch the event as it bubbles + $document.bind('selectstart', function (e) { + var elmn = angular.element(e.target); + if (elmn.hasClass(emptyClassName) && elmn.prop('disabled')) { + e.preventDefault(); + } + }); + documentListenersApplied = true; + } + + function updateValue(e) { + var val = elem.val(); + + // don't update from placeholder, helps debounce + if (elem.hasClass(emptyClassName) && val && val === text) { return; } + + conditionalDefer(function(){ setValue(val); }); + } + + function conditionalDefer(callback) { + // IE8/9: ngModel uses a keydown handler with deferrered + // execution to check for changes to the input. this $timeout + // prevents callback from firing before the keydown handler, + // which is an issue when tabbing out of an input. + // the conditional tests IE version, matches $sniffer. + // + // TODO: remove this function when tab key behavior is fixed in + // angular core + if (document.documentMode <= 11) { + $timeout(callback, 0); + } else { + callback(); + } + } + + function setValue(val) { + if (!val && val !== 0 && domElem !== document.activeElement) { + // show placeholder when necessary + elem.addClass(emptyClassName); + elem.val(!is_pwd ? text : ''); + } else { + // otherwise set input to actual value + elem.removeClass(emptyClassName); + elem.val(val); + } + if (is_pwd) { + updatePasswordPlaceholder(); + if ($animate) { + asyncUpdatePasswordPlaceholder(); + } + } + } + + function getValue() { + if (ngModel) { + // use eval because $viewValue isn't ready during init + // TODO: this might not to work during unit tests, investigate + return scope.$eval(attrs.ngModel) || ''; + } + return getDomValue() || ''; + } + + // IE8/9: elem.val() on an empty field sometimes returns the + // placeholder value, so return an empty string instead + // http://stackoverflow.com/q/11208417/490592 + // I believe IE is persisting the field value across refreshes + // TODO: vs `elem.attr('value')` + function getDomValue() { + var val = elem.val(); + if (val === attrs.placeholder) { + val = ''; + } + return val; + } + + function changePlaceholder(value) { + if (elem.hasClass(emptyClassName) && elem.val() === text) { + elem.val(''); + } + text = value; + updateValue(); + } + + function setAttrUnselectable(elmn, enable) { + if (enable) { + elmn.attr('unselectable', 'on'); + } else { + elmn.removeAttr('unselectable'); + } + } + + // IE8: password inputs cannot display text, and inputs cannot + // change type, so create a new element to display placeholder + function setupPasswordPlaceholder() { + clone = angular.element(''); + stylePasswordPlaceholder(); + hideElement(clone); + clone.addClass(emptyClassName) + .bind('focus', hidePasswordPlaceholderAndFocus); + domElem.parentNode.insertBefore(clone[0], domElem); + + // keep password placeholder in sync with original element. + // update element after $watches + var watchAttrs = [ + attrs.ngDisabled, + attrs.ngReadonly, + attrs.ngRequired, + attrs.ngShow, + attrs.ngHide + ]; + for (var i = 0; i < watchAttrs.length; i++) { + if (watchAttrs[i]) { + scope.$watch(watchAttrs[i], flexibleUpdatePasswordPlaceholder); + } + } + } + + function updatePasswordPlaceholder() { + stylePasswordPlaceholder(); + if (isNgHidden()) { + // force hide the placeholder when element is hidden by + // ngShow/ngHide. we cannot rely on stylePasswordPlaceholder + // above to copy the ng-hide class, because the ngShow/ngHide + // $watch functions apply the ng-hide class with $animate, + // so the class is not applied when our $watch executes + hideElement(clone); + } else if (elem.hasClass(emptyClassName) && domElem !== document.activeElement) { + showPasswordPlaceholder(); + } else { + hidePasswordPlaceholder(); + } + } + // update element after animation and animation-aware directives + function asyncUpdatePasswordPlaceholder() { + if (angularVersion >= 1.3) { + $animate.addClass(elem, '').then(updatePasswordPlaceholder); + } else { + $animate.addClass(elem, '', updatePasswordPlaceholder); + } + } + function flexibleUpdatePasswordPlaceholder() { + if ($animate) { + asyncUpdatePasswordPlaceholder(); + } else { + updatePasswordPlaceholder(); + } + } + + function stylePasswordPlaceholder() { + clone.val(text); + // chaining was failing in v1.0.8 + clone.attr('class', elem.attr('class') || '') + .attr('style', elem.attr('style') || '') + .prop('disabled', elem.prop('disabled')) + .prop('readOnly', elem.prop('readOnly')) + .prop('required', elem.prop('required')); + setAttrUnselectable(clone, elem.attr('unselectable') === 'on'); + } + + function showElement(elmn) { + if (angularVersion >= 1.2) { + elmn.removeClass(hiddenClassName); + } else { + elmn.css('display', ''); + } + } + + function hideElement(elmn) { + if (angularVersion >= 1.2) { + elmn.addClass(hiddenClassName); + } else { + elmn.css('display', 'none'); + } + } + + function showPasswordPlaceholder() { + hideElement(elem); + showElement(clone); + } + + function hidePasswordPlaceholder() { + hideElement(clone); + showElement(elem); + } + + function hidePasswordPlaceholderAndFocus() { + hidePasswordPlaceholder(); + domElem.focus(); + } + + function isNgHidden() { + var hasNgShow = typeof attrs.ngShow !== 'undefined', + hasNgHide = typeof attrs.ngHide !== 'undefined'; + if (hasNgShow || hasNgHide) { + return (hasNgShow && !scope.$eval(attrs.ngShow)) || + (hasNgHide && scope.$eval(attrs.ngHide)); + } else { + return false; + } + } + + } + }; +}]); + +})(window.angular, window.document); diff --git a/dist/angular-shims-placeholder.min.js b/dist/angular-shims-placeholder.min.js index 62c43af..dcb4664 100644 --- a/dist/angular-shims-placeholder.min.js +++ b/dist/angular-shims-placeholder.min.js @@ -1,4 +1,4 @@ -/*! angular-shims-placeholder - v0.4.2 - 2015-05-11 +/*! angular-shims-placeholder - v0.4.3 - 2015-06-14 * https://github.com/cvn/angular-shims-placeholder * Copyright (c) 2015 Chad von Nau; Licensed MIT */ -!function(a,b){"use strict";a.module("ng.shims.placeholder",[]).service("placeholderSniffer",["$document",function(a){this.emptyClassName="empty",this.hasPlaceholder=function(){var b=a[0].createElement("input");return void 0!==b.placeholder}}]).directive("placeholder",["$timeout","$document","$interpolate","$injector","placeholderSniffer",function(c,d,e,f,g){if(g.hasPlaceholder())return{};var h=!1,i=parseFloat(a.version.full);try{var j=f.get("$animate")}catch(k){}return{restrict:"A",require:"?ngModel",priority:i>=1.2?110:-10,link:function(f,k,l,m){function n(){var a=k.val();k.hasClass(M)&&a&&a===L||o(function(){p(a)})}function o(a){b.documentMode<=11?c(a,0):a()}function p(a){a||0===a||H===b.activeElement?(k.removeClass(M),k.val(a)):(k.addClass(M),k.val(K?"":L)),K&&(v(),j&&w())}function q(){return m?f.$eval(l.ngModel)||"":r()||""}function r(){var a=k.val();return a===l.placeholder&&(a=""),a}function s(a){k.hasClass(M)&&k.val()===L&&k.val(""),L=a,n()}function t(a,b){b?a.attr("unselectable","on"):a.removeAttr("unselectable")}function u(){F=a.element(''),y(),A(F),F.addClass(M).bind("focus",D),H.parentNode.insertBefore(F[0],H);for(var b=[l.ngDisabled,l.ngReadonly,l.ngRequired,l.ngShow,l.ngHide],c=0;c=1.3?j.addClass(k,"").then(v):j.addClass(k,"",v)}function x(){j?w():v()}function y(){F.val(L),F.attr("class",k.attr("class")||"").attr("style",k.attr("style")||"").prop("disabled",k.prop("disabled")).prop("readOnly",k.prop("readOnly")).prop("required",k.prop("required")),t(F,"on"===k.attr("unselectable"))}function z(a){i>=1.2?a.removeClass(N):a.css("display","")}function A(a){i>=1.2?a.addClass(N):a.css("display","none")}function B(){A(k),z(F)}function C(){A(F),z(k)}function D(){C(),H.focus()}function E(){var a="undefined"!=typeof l.ngShow,b="undefined"!=typeof l.ngHide;return a||b?a&&!f.$eval(l.ngShow)||b&&f.$eval(l.ngHide):!1}var F,G=q(),H=k[0],I=H.nodeName.toLowerCase(),J="input"===I||"textarea"===I,K="password"===l.type,L=l.placeholder,M=g.emptyClassName,N="ng-hide";J&&(l.$observe("placeholder",function(a){s(a)}),K&&u(),p(G),k.bind("focus",function(){k.hasClass(M)&&(k.val(""),k.removeClass(M),H.select())}),k.bind("blur",n),m||k.bind("change",function(){s(e(k.attr("placeholder"))(f))}),m&&(m.$render=function(){p(m.$viewValue),H!==b.activeElement||k.val()||H.select()}),h||(d.bind("selectstart",function(b){var c=a.element(b.target);c.hasClass(M)&&c.prop("disabled")&&b.preventDefault()}),h=!0))}}}])}(window.angular,window.document); \ No newline at end of file +!function(a,b,c){"use strict";a.module("ng.shims.placeholder",[]).service("placeholderSniffer",["$document",function(a){this.emptyClassName="empty",this.hasPlaceholder=function(){var b=a[0].createElement("input");return void 0!==b.placeholder}}]).directive("placeholder",["$timeout","$document","$interpolate","$injector","placeholderSniffer",function(c,d,e,f,g){if(g.hasPlaceholder())return{};var h=!1,i=parseFloat(a.version.full);try{var j=f.get("$animate")}catch(k){}return{restrict:"A",require:"?ngModel",priority:i>=1.2?110:-10,link:function(f,k,l,m){function n(a){var b=k.val();k.hasClass(M)&&b&&b===L||o(function(){p(b)})}function o(a){b.documentMode<=11?c(a,0):a()}function p(a){a||0===a||H===b.activeElement?(k.removeClass(M),k.val(a)):(k.addClass(M),k.val(K?"":L)),K&&(v(),j&&w())}function q(){return m?f.$eval(l.ngModel)||"":r()||""}function r(){var a=k.val();return a===l.placeholder&&(a=""),a}function s(a){k.hasClass(M)&&k.val()===L&&k.val(""),L=a,n()}function t(a,b){b?a.attr("unselectable","on"):a.removeAttr("unselectable")}function u(){F=a.element(''),y(),A(F),F.addClass(M).bind("focus",D),H.parentNode.insertBefore(F[0],H);for(var b=[l.ngDisabled,l.ngReadonly,l.ngRequired,l.ngShow,l.ngHide],c=0;c=1.3?j.addClass(k,"").then(v):j.addClass(k,"",v)}function x(){j?w():v()}function y(){F.val(L),F.attr("class",k.attr("class")||"").attr("style",k.attr("style")||"").prop("disabled",k.prop("disabled")).prop("readOnly",k.prop("readOnly")).prop("required",k.prop("required")),t(F,"on"===k.attr("unselectable"))}function z(a){i>=1.2?a.removeClass(N):a.css("display","")}function A(a){i>=1.2?a.addClass(N):a.css("display","none")}function B(){A(k),z(F)}function C(){A(F),z(k)}function D(){C(),H.focus()}function E(){var a="undefined"!=typeof l.ngShow,b="undefined"!=typeof l.ngHide;return a||b?a&&!f.$eval(l.ngShow)||b&&f.$eval(l.ngHide):!1}var F,G=q(),H=k[0],I=H.nodeName.toLowerCase(),J="input"===I||"textarea"===I,K="password"===l.type,L=l.placeholder||"",M=g.emptyClassName,N="ng-hide";J&&(l.$observe("placeholder",function(a){s(a)}),K&&u(),p(G),k.bind("focus",function(){k.hasClass(M)&&(k.val(""),k.removeClass(M),H.select())}),k.bind("blur",n),m||k.bind("change",function(){s(e(k.attr("placeholder")||"")(f))}),m&&(m.$render=function(){p(m.$viewValue),H!==b.activeElement||k.val()||H.select()}),h||(d.bind("selectstart",function(b){var c=a.element(b.target);c.hasClass(M)&&c.prop("disabled")&&b.preventDefault()}),h=!0))}}}])}(window.angular,window.document); \ No newline at end of file diff --git a/package.json b/package.json index 541264c..12fd5b7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "angular-shims-placeholder", "description": "Angular directive to emulate the `placeholder` attribute on text and password input fields for old browsers, such as IE9, IE8, and below", - "version": "0.4.2", + "version": "0.4.3", "homepage": "https://github.com/cvn/angular-shims-placeholder", "author": { "name": "Chad von Nau",