angular-shims-placeholder
Advanced tools
Comparing version 0.3.1 to 0.3.2
{ | ||
"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.3.1", | ||
"version": "0.3.2", | ||
"homepage": "https://github.com/jrief/angular-shims-placeholder", | ||
@@ -6,0 +6,0 @@ "authors": [{ |
@@ -1,2 +0,2 @@ | ||
/*! angular-shims-placeholder - v0.3.1 - 2014-11-03 | ||
/*! angular-shims-placeholder - v0.3.2 - 2014-11-19 | ||
* https://github.com/jrief/angular-shims-placeholder | ||
@@ -9,3 +9,3 @@ * Copyright (c) 2014 Jacob Rief; Licensed MIT */ | ||
function ($document) { | ||
this.hasPlaceholder = function () { | ||
this.emptyClassName = 'empty', this.hasPlaceholder = function () { | ||
var test = $document[0].createElement('input'); | ||
@@ -17,12 +17,14 @@ return test.placeholder !== void 0; | ||
'$timeout', | ||
'$document', | ||
'placeholderSniffer', | ||
function ($timeout, placeholderSniffer) { | ||
function ($timeout, $document, placeholderSniffer) { | ||
if (placeholderSniffer.hasPlaceholder()) | ||
return {}; | ||
var documentListenersApplied = false; | ||
return { | ||
restrict: 'A', | ||
require: '?ngModel', | ||
priority: 1, | ||
priority: 110, | ||
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 = 'empty', clone; | ||
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 (!text || !isInput) { | ||
@@ -54,2 +56,11 @@ return; | ||
} | ||
if (!documentListenersApplied) { | ||
$document.on('selectstart', function (e) { | ||
var elmn = angular.element(e.target); | ||
if (elmn.hasClass(emptyClassName) && elmn.prop('disabled')) { | ||
e.preventDefault(); | ||
} | ||
}); | ||
documentListenersApplied = true; | ||
} | ||
function updateValue(e) { | ||
@@ -60,8 +71,11 @@ var val = elem.val(); | ||
} | ||
conditionalDefer(function () { | ||
setValue(val); | ||
}); | ||
} | ||
function conditionalDefer(callback) { | ||
if (document.documentMode <= 11) { | ||
$timeout(function () { | ||
setValue(val); | ||
}, 0); | ||
$timeout(callback, 0); | ||
} else { | ||
setValue(val); | ||
callback(); | ||
} | ||
@@ -72,5 +86,3 @@ } | ||
elem.addClass(emptyClassName); | ||
if (is_pwd) { | ||
showPasswordPlaceholder(); | ||
} else { | ||
if (!is_pwd) { | ||
elem.val(text); | ||
@@ -80,7 +92,7 @@ } | ||
elem.removeClass(emptyClassName); | ||
if (is_pwd) { | ||
hidePasswordPlaceholder(); | ||
} | ||
elem.val(val); | ||
} | ||
if (is_pwd) { | ||
updatePasswordPlaceholder(); | ||
} | ||
} | ||
@@ -100,19 +112,48 @@ function getValue() { | ||
} | ||
function setAttrUnselectable(elmn, enable) { | ||
if (enable) { | ||
elmn.attr('unselectable', 'on'); | ||
} else { | ||
elmn.removeAttr('unselectable'); | ||
} | ||
} | ||
function setupPasswordPlaceholder() { | ||
clone = angular.element('<input type="text" value="' + text + '"/>'); | ||
stylePasswordPlaceholder(); | ||
clone.addClass(emptyClassName).addClass('ng-hide').bind('focus', hidePasswordPlaceholderAndFocus); | ||
clone.addClass(emptyClassName).addClass(hiddenClassName).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], updatePasswordPlaceholder); | ||
} | ||
} | ||
} | ||
function updatePasswordPlaceholder() { | ||
stylePasswordPlaceholder(); | ||
if (isNgHidden()) { | ||
clone.addClass(hiddenClassName); | ||
} else if (elem.hasClass(emptyClassName) && domElem !== document.activeElement) { | ||
showPasswordPlaceholder(); | ||
} else { | ||
hidePasswordPlaceholder(); | ||
} | ||
} | ||
function stylePasswordPlaceholder() { | ||
clone.val(text).attr('class', elem.attr('class') || '').attr('style', elem.attr('style') || ''); | ||
clone.val(text).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 showPasswordPlaceholder() { | ||
stylePasswordPlaceholder(); | ||
elem.addClass('ng-hide'); | ||
clone.removeClass('ng-hide'); | ||
elem.addClass(hiddenClassName); | ||
clone.removeClass(hiddenClassName); | ||
} | ||
function hidePasswordPlaceholder() { | ||
clone.addClass('ng-hide'); | ||
elem.removeClass('ng-hide'); | ||
clone.addClass(hiddenClassName); | ||
elem.removeClass(hiddenClassName); | ||
} | ||
@@ -123,2 +164,10 @@ function hidePasswordPlaceholderAndFocus() { | ||
} | ||
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; | ||
} | ||
} | ||
} | ||
@@ -125,0 +174,0 @@ }; |
@@ -1,4 +0,4 @@ | ||
/*! angular-shims-placeholder - v0.3.1 - 2014-11-03 | ||
/*! angular-shims-placeholder - v0.3.2 - 2014-11-19 | ||
* https://github.com/jrief/angular-shims-placeholder | ||
* Copyright (c) 2014 Jacob Rief; Licensed MIT */ | ||
!function(a,b){"use strict";a.module("ng.shims.placeholder",[]).service("placeholderSniffer",["$document",function(a){this.hasPlaceholder=function(){var b=a[0].createElement("input");return void 0!==b.placeholder}}]).directive("placeholder",["$timeout","placeholderSniffer",function(c,d){return d.hasPlaceholder()?{}:{restrict:"A",require:"?ngModel",priority:1,link:function(d,e,f,g){function h(){var a=e.val();e.hasClass(x)&&a===w||(b.documentMode<=11?c(function(){i(a)},0):i(a))}function i(a){a||s===b.activeElement?(e.removeClass(x),v&&o(),e.val(a)):(e.addClass(x),v?n():e.val(w))}function j(){return g?d.$eval(f.ngModel)||"":k()||""}function k(){var a=e.val();return a===f.placeholder&&(a=""),a}function l(){q=a.element('<input type="text" value="'+w+'"/>'),m(),q.addClass(x).addClass("ng-hide").bind("focus",p),s.parentNode.insertBefore(q[0],s)}function m(){q.val(w).attr("class",e.attr("class")||"").attr("style",e.attr("style")||"")}function n(){m(),e.addClass("ng-hide"),q.removeClass("ng-hide")}function o(){q.addClass("ng-hide"),e.removeClass("ng-hide")}function p(){o(),s.focus()}var q,r=j(),s=e[0],t=s.nodeName.toLowerCase(),u="input"===t||"textarea"===t,v="password"===f.type,w=f.placeholder,x="empty";w&&u&&(v&&l(),i(r),e.bind("focus",function(){e.hasClass(x)&&(e.val(""),e.removeClass(x),s.select())}),e.bind("blur",h),g||e.bind("change",h),g&&(g.$render=function(){i(g.$viewValue),s!==b.activeElement||e.val()||s.select()}))}}}])}(window.angular,window.document); | ||
!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","placeholderSniffer",function(c,d,e){if(e.hasPlaceholder())return{};var f=!1;return{restrict:"A",require:"?ngModel",priority:110,link:function(g,h,i,j){function k(){var a=h.val();h.hasClass(E)&&a===D||l(function(){m(a)})}function l(a){b.documentMode<=11?c(a,0):a()}function m(a){a||z===b.activeElement?(h.removeClass(E),h.val(a)):(h.addClass(E),C||h.val(D)),C&&r()}function n(){return j?g.$eval(i.ngModel)||"":o()||""}function o(){var a=h.val();return a===i.placeholder&&(a=""),a}function p(a,b){b?a.attr("unselectable","on"):a.removeAttr("unselectable")}function q(){x=a.element('<input type="text" value="'+D+'"/>'),s(),x.addClass(E).addClass(F).bind("focus",v),z.parentNode.insertBefore(x[0],z);for(var b=[i.ngDisabled,i.ngReadonly,i.ngRequired,i.ngShow,i.ngHide],c=0;c<b.length;c++)b[c]&&g.$watch(b[c],r)}function r(){s(),w()?x.addClass(F):h.hasClass(E)&&z!==b.activeElement?t():u()}function s(){x.val(D).attr("class",h.attr("class")||"").attr("style",h.attr("style")||"").prop("disabled",h.prop("disabled")).prop("readOnly",h.prop("readOnly")).prop("required",h.prop("required")),p(x,"on"===h.attr("unselectable"))}function t(){h.addClass(F),x.removeClass(F)}function u(){x.addClass(F),h.removeClass(F)}function v(){u(),z.focus()}function w(){var a="undefined"!=typeof i.ngShow,b="undefined"!=typeof i.ngHide;return a||b?a&&!g.$eval(i.ngShow)||b&&g.$eval(i.ngHide):!1}var x,y=n(),z=h[0],A=z.nodeName.toLowerCase(),B="input"===A||"textarea"===A,C="password"===i.type,D=i.placeholder,E=e.emptyClassName,F="ng-hide";D&&B&&(C&&q(),m(y),h.bind("focus",function(){h.hasClass(E)&&(h.val(""),h.removeClass(E),z.select())}),h.bind("blur",k),j||h.bind("change",k),j&&(j.$render=function(){m(j.$viewValue),z!==b.activeElement||h.val()||z.select()}),f||(d.on("selectstart",function(b){var c=a.element(b.target);c.hasClass(E)&&c.prop("disabled")&&b.preventDefault()}),f=!0))}}}])}(window.angular,window.document); |
@@ -17,2 +17,3 @@ /* | ||
.service('placeholderSniffer', function($document){ | ||
this.emptyClassName = 'empty', | ||
this.hasPlaceholder = function() { | ||
@@ -24,5 +25,7 @@ // test for native placeholder support | ||
}) | ||
.directive('placeholder', function($timeout, placeholderSniffer) { | ||
.directive('placeholder', function($timeout, $document, placeholderSniffer) { | ||
if (placeholderSniffer.hasPlaceholder()) return {}; | ||
var documentListenersApplied = false; | ||
// No native support for attribute placeholder | ||
@@ -32,3 +35,3 @@ return { | ||
require: '?ngModel', | ||
priority: 1, | ||
priority: 110, // run after ngModel (0) and BOOLEAN_ATTR (100) directives | ||
link: function(scope, elem, attrs, ngModel) { | ||
@@ -41,3 +44,4 @@ var orig_val = getValue(), | ||
text = attrs.placeholder, | ||
emptyClassName = 'empty', | ||
emptyClassName = placeholderSniffer.emptyClassName, | ||
hiddenClassName = 'ng-hide', | ||
clone; | ||
@@ -65,2 +69,3 @@ | ||
// handler for model-less inputs to interact with non-angular code | ||
// TODO: vs `$watch(function(){return elem.val()})` | ||
if (!ngModel) { | ||
@@ -86,2 +91,15 @@ elem.bind('change', updateValue); | ||
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.on('selectstart', function (e) { | ||
var elmn = angular.element(e.target); | ||
if (elmn.hasClass(emptyClassName) && elmn.prop('disabled')) { | ||
e.preventDefault(); | ||
} | ||
}); | ||
documentListenersApplied = true; | ||
} | ||
function updateValue(e) { | ||
@@ -93,16 +111,18 @@ var val = elem.val(); | ||
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 updateValue from firing before the keydown handler, | ||
// 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 timeout path when tab key behavior is fixed in | ||
// TODO: remove this function when tab key behavior is fixed in | ||
// angular core | ||
if (document.documentMode <= 11) { | ||
$timeout(function(){ | ||
setValue(val); | ||
},0); | ||
$timeout(callback, 0); | ||
} else { | ||
setValue(val); | ||
callback(); | ||
} | ||
@@ -115,5 +135,3 @@ } | ||
elem.addClass(emptyClassName); | ||
if (is_pwd) { | ||
showPasswordPlaceholder(); | ||
} else { | ||
if (!is_pwd) { | ||
elem.val(text); | ||
@@ -124,7 +142,7 @@ } | ||
elem.removeClass(emptyClassName); | ||
if (is_pwd) { | ||
hidePasswordPlaceholder(); | ||
} | ||
elem.val(val); | ||
} | ||
if (is_pwd) { | ||
updatePasswordPlaceholder(); | ||
} | ||
} | ||
@@ -134,2 +152,4 @@ | ||
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) || ''; | ||
@@ -143,2 +163,4 @@ } | ||
// http://stackoverflow.com/q/11208417/490592 | ||
// I believe IE is persisting the field value across refreshes | ||
// TODO: vs `elem.attr('value')` | ||
function getDomValue() { | ||
@@ -152,2 +174,10 @@ var val = elem.val(); | ||
function setAttrUnselectable(elmn, enable) { | ||
if (enable) { | ||
elmn.attr('unselectable', 'on'); | ||
} else { | ||
elmn.removeAttr('unselectable'); | ||
} | ||
} | ||
// IE8: password inputs cannot display text, and inputs cannot | ||
@@ -159,22 +189,56 @@ // change type, so create a new element to display placeholder | ||
clone.addClass(emptyClassName) | ||
.addClass('ng-hide') | ||
.addClass(hiddenClassName) | ||
.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], updatePasswordPlaceholder); | ||
} | ||
} | ||
} | ||
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 | ||
clone.addClass(hiddenClassName); | ||
} else if (elem.hasClass(emptyClassName) && domElem !== document.activeElement) { | ||
showPasswordPlaceholder(); | ||
} else { | ||
hidePasswordPlaceholder(); | ||
} | ||
} | ||
function stylePasswordPlaceholder() { | ||
clone.val(text) | ||
.attr('class', elem.attr('class') || '') | ||
.attr('style', elem.attr('style') || ''); | ||
.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 showPasswordPlaceholder() { | ||
stylePasswordPlaceholder(); | ||
elem.addClass('ng-hide'); | ||
clone.removeClass('ng-hide'); | ||
elem.addClass(hiddenClassName); | ||
clone.removeClass(hiddenClassName); | ||
} | ||
function hidePasswordPlaceholder() { | ||
clone.addClass('ng-hide'); | ||
elem.removeClass('ng-hide'); | ||
clone.addClass(hiddenClassName); | ||
elem.removeClass(hiddenClassName); | ||
} | ||
@@ -187,2 +251,13 @@ | ||
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; | ||
} | ||
} | ||
} | ||
@@ -189,0 +264,0 @@ }; |
{ | ||
"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.3.1", | ||
"version": "0.3.2", | ||
"homepage": "https://github.com/jrief/angular-shims-placeholder", | ||
@@ -34,5 +34,5 @@ "author": { | ||
"grunt": ">= 0.4.0", | ||
"grunt-bump": "0.0.16", | ||
"grunt-bump": "latest", | ||
"grunt-contrib-concat": "latest", | ||
"grunt-contrib-connect": "^0.8.0", | ||
"grunt-contrib-connect": "latest", | ||
"grunt-contrib-jshint": "latest", | ||
@@ -42,3 +42,3 @@ "grunt-contrib-uglify": "latest", | ||
"grunt-ngmin": "0.0.2", | ||
"grunt-open": "^0.2.3", | ||
"grunt-open": "latest", | ||
"karma": "0.8.5" | ||
@@ -45,0 +45,0 @@ }, |
angular-shims-placeholder | ||
========================= | ||
[](http://badge.fury.io/bo/angular-shims-placeholder) | ||
[](http://badge.fury.io/js/angular-shims-placeholder) | ||
[](https://travis-ci.org/jrief/angular-shims-placeholder) | ||
[](https://david-dm.org/jrief/angular-shims-placeholder#info=devDependencies) | ||
@@ -14,9 +18,7 @@ Angular directive to emulate the `placeholder` attribute on text and password input fields for | ||
Demo | ||
---- | ||
[View Demo](http://jrief.github.io/angular-shims-placeholder) | ||
Build status | ||
------------ | ||
[](https://travis-ci.org/jrief/angular-shims-placeholder) | ||
Usage | ||
@@ -50,2 +52,26 @@ ----- | ||
Compatibility | ||
------------- | ||
This directive is compatible with ngModel, ngDisabled, ngReadonly, ngRequired, ngShow, and ngHide. | ||
If you modify a shimmed input from outside of Angular, use the 'change' event to update the placeholder display. e.g. `elem.triggerHandler('change')` | ||
Known Issues | ||
------------ | ||
* Ignores text input from drag and drop | ||
* Does not support modern-style placeholders that persist until text is entered | ||
* IE8/9: Disabled textareas show the text insertion cursor on hover. This is due to an IE bug. | ||
* IE8/9: Clearing a filled input while its text is selected can cause the resulting placeholder text to appear selected | ||
* No way for an individual input to opt out | ||
* Not tested with ngAnimate | ||
* Not tested with ngSubmit | ||
* Not tested with ngClass | ||
* Not tested with dynamic placeholders e.g. placeholder="{{val}}" | ||
Authors | ||
------- | ||
Written by [Jacob Rief](https://github.com/jrief). | ||
Maintained by [Chad von Nau](https://github.com/cvn). | ||
License | ||
@@ -52,0 +78,0 @@ ------- |
@@ -284,2 +284,26 @@ 'use strict'; | ||
}); | ||
describe('on a empty password input with unselectable=on attribute', function() { | ||
var pwd_field, pwd_clone; | ||
beforeEach(function() { | ||
pwd_field = angular.element('<input type="password" name="userpwd" placeholder="Password" unselectable="on" value="" />'); | ||
$compile(pwd_field)(scope); | ||
pwd_clone = angular.element(pwd_field[0].previousElementSibling); | ||
}); | ||
describe('the placeholder element', function() { | ||
it('should be unselectable', function() { | ||
expect(pwd_clone.val()).toBe('Password'); | ||
expect(pwd_clone.attr('unselectable')).toBe('on'); | ||
}); | ||
it('should become selectable when disabled attribute is removed', function() { | ||
pwd_field.removeAttr('unselectable'); | ||
pwd_field.triggerHandler('change'); | ||
expect(pwd_clone.val()).toBe('Password'); | ||
expect(pwd_clone.attr('unselectable')).toBeUndefined(); | ||
}); | ||
}); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
50764
856
80