HTML5 Type Detection and Plugin Initialization
PART A:
I know there are a lot of things out there that tell you if a browser supports a certain HTML5 attribute, for example http://diveintohtml5.info/detect.html, but they don't tell you how to acquire the type from individual elements and use that info to init your plugins.
So I tried:
alert($("input:date"));
//returns "[object Object]"
alert($("input[type='date']"));
//returns "[object Object]"
alert($("input").attr("type"));
//returns "text" ... which is a lie. it should have been "date"
None those worked.
I eventually came up with this (that does work):
var inputAttr = $('<div>').append($(this).clone()).remove().html().toLowerCase();
alert(inputAttr);
// returns "<input min="-365" max="365" type="date">"
Thanks: http://jquery-howto.blogspot.com/2009/02/how-to-get-full-html-string-including.html
So my first question: 1. Why can I not read the "type" attribute in browsers that don't support html5? You can make up any other attribute and bogus value and read it. 2. Why does my solution work? Why does it matter if its in the DOM or not?
PART B:
Below is a basic example of what I am using the detector for:
<script type="text/javascript" >
$(function () {
var tM = document.createElement("input");
tM.setAttribute("type", "date");
if (tM.type == "text") {
alert("No date type support on a browser level. Start adding date, week, month, and time fallbacks");
// Thanks: http://diveintohtml5.ep.io/detect.html
$("input").each(function () {
// If we read the type attribute directly from the DOM (some browsers) will return unknown attributes as text (like the first detection). Making a clone allows me to read the input as a clone in a variable. I don't know why.
var inputAttr = $('<div>').append($(this).clone()).remove().html().toLowerCase();
alert(inputAttr);
if ( inputAttr.indexOf( "month" ) !== -1 )
{
//get HTML5 attributes from element
var tMmindate = $(this).attr('min');
var tMmaxdate = $(this).attr('max');
//add datepicker with attributes support and no animation (so we can use -ms-filter gradients for ie)
$(this).datepick({
renderer: $.datepick.weekOfYearRenderer,
onShow: $.datepick.monthOnly,
minDate: tMmindate,
开发者_如何学C maxDate: tMmaxdate,
dateFormat: 'yyyy-mm',
showAnim: ''});
}
else
{
$(this).css('border', '5px solid red');
// test for more input types and apply init to them
}
});
}
});
</script>
Live example: http://joelcrawfordsmith.com/sandbox/html5-type-detection.html
And a favor/question: Can anyone help me cut some fat in my HTML5 input type fixer?
I have the functionality down (adds fallbacks to IE6-IE8, and FF with out adding classes to init off of)
Are there are more efficient methods for iterating over the DOM for mystery input types? And should I be using an If Else, or a function, or a case in my example?
Thanks All,
Joel
A far superior method for detecting supported input types is to simply create an input element and loop through all of the different input types available and check if the type
change sticks:
var supported = { date: false, number: false, time: false, month: false, week: false },
tester = document.createElement('input');
for (var i in supported){
try {
tester.type = i;
if (tester.type === i){
supported[i] = true;
}
} catch (e) {
// IE raises an exception if you try to set the type to
// an invalid value, so we just swallow the error
}
}
This actually makes use of the fact that browsers which do not support that particular input type will fall back to using text, thereby allowing you to test if they're supported or not.
You can then use supported['week']
, for instance, to check for the availability of the week
input type, and do your fallbacks through this. See a simple demo of this here: http://www.jsfiddle.net/yijiang/r5Wsa/2/. You might also consider using Modernizr for more robust HTML5 feature detection.
And finally, a better way to get outerHTML
is to, believe it or not, use outerHTML
. Instead of
var inputAttr = $('<div>').append($(this).clone()).remove().html().toLowerCase();
Why not just use:
var inputAttr = this.outerHTML || new XMLSerializer().serializeToString(this);
(Yes, as you can see, there is a caveat - outerHTML
isn't supported by Firefox, so we're going to need a simple workaround, from this Stack Overflow question).
Edit: Found a method to do testing for native form UI support, from this page: http://miketaylr.com/code/html5-forms-ui-support.html. Browsers that support the UI for these types in some way should also prevent invalid values from been entered into these fields, so the logical extension to the test we're doing above would be this:
var supported = {date: false, number: false, time: false, month: false, week: false},
tester = document.createElement('input');
for(var i in supported){
tester.type = i;
tester.value = ':(';
if(tester.type === i && tester.value === ''){
supported[i] = true;
}
}
Again, not 100% reliable - this is only good for types that have certain restrictions on their values, and definitely not very good, but it's a step in the right direction, and certainly would solve your problem now.
See the updated demo here: http://www.jsfiddle.net/yijiang/r5Wsa/3/
Asking for the type attribute does not work in all Android stock browsers. They pretend that they support inputType="date", but they do not offer an UI (datepicker e.g.) for date inputs.
This feature detection worked for me:
(function() {
var el = document.createElement('input'),
notADateValue = 'not-a-date';
el.setAttribute('type','date');
el.setAttribute('value', notADateValue);
return el.value !== notADateValue;
})();
The trick is to set an illegal value into a date field. If the browser sanitises this input, it could also offer a datepicker.
The type attribute isn't a "made-up" element, it's defined here:
http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.4
...and browsers only "know" about the @type values defined there (unless they are HTML5 aware -- which has defined some new values like "date", "email" etc)
When you query the type attribute some browsers return "text" to you because if a browser doesn't support the "date" type (or anything it doesn't understand) then it falls back to the default value -- which is type="text"
Have you thought of adding a classname (class="date") to the inputs as well then you can just $('.date').each() and then do you detection on that set
I would argue this is a bug in JQuery! If you look at the attr() function in the JQuery code itself, JQuery first trys to get a value for the name you passed in using bracket notation. If it is not undefined, then it returns that value. If it is undefined, then it usess the getAttribute() method instead.
Jquery does something similar to this for $("#elem").attr(name):
if (elem[ name ] !== undefined)
{
return elem[name];
}
else
{
return elem.getAttribute( name )
}
The problem is Jquery is assuming if elem[name] is not undefined, then elem[name] is correct.
Consider the following example:
<input type="date" id="myInput" name="myInput" joel="crawford" />
var myInput = document.getElementById('myInput');
alert(myInput['type']);//returns text
alert(myInput.getAttribute('type'));//returns date
alert($("#myInput").attr('type'));//returns text
alert(myInput['joel']);//returns undefined
alert(myInput.getAttribute('joel'));//returns crawford
alert($("#myInput").attr('joel'));//returns crawford
When you pass in .attr("type"), myInput['type'] returns "text", so Jquery returns "text". If you passed in .attr("joel"), myInput['joel'] returns undefined, so then Jquery uses getAttribute('joel') instead which returns "crawford".
You cannot get type="date" in a browser that doesn't support this. If a browser detects a type-attribute it doesn't understand it overrides it with the type="text" (default).
A way around this (using jQuery) is simply to add the class date as well.
Then you can do something like
$('input.date').each(function() {
var $this = $(this);
if($this.attr('type') != 'date') $this.datepicker();
});
Just tester.type = i;
throws exception in IE. Fixed version:
var get_supported_html5_input_types = function() {
var supported = {
date: false,
number: false,
time: false,
datetime: false,
'datetime-local':false,
month: false,
week: false
},
tester = document.createElement('input');
for(var i in supported){
// Do nothing - IE throws, FF/Chrome just ignores
try { tester.type = i; } catch (err) {}
if(tester.type === i){
supported[i] = true;
}
}
return supported;
};
console.log(get_supported_html5_input_types());
Always test, never blindly copy-paste !
Here's a jQuery script that detects whether the browser supports HTML5 date
format, and if so, it changes all date
field values to yyyy-mm-dd
format, and all datetime
field values to yyyy-mm-dd hh:mm:ss
format.
// From https://stackoverflow.com/a/10199306
// If the browser supports HTML5 input type="date", change all the values from y/m/dddd format to yyyy-mm-dd format, so they appear properly:
function fix_date_inputs() {
try {
var input = document.createElement('input');
input.setAttribute('type','date');
var notADateValue = 'not-a-date';
input.setAttribute('value', notADateValue);
var r = (input.value !== notADateValue);
if ( r ) {
$( 'input' ).each( function() {
if (
$(this).attr( 'type' ).match( /^date/ ) // date or datetime
) {
var m_d_y = $(this).context.attributes.value.value; // Because $(this).val() does not work (returns '')
var d = new Date( m_d_y );
var month = '' + (d.getMonth() + 1);
var day = '' + d.getDate();
var year = d.getFullYear();
if (month.length < 2) month = '0' + month;
if (day.length < 2) day = '0' + day;
var yyyy_mm_dd = [ year, month, day ].join( '-' );
// If we're processing a datetime, add the time:
if (
$(this).attr( 'type' ) == 'datetime'
) {
var h = '' + d.getHours();
var i = '' + d.getMinutes();
var s = '' + d.getSeconds();
if (h.length < 2) h = '0' + h;
if (i.length < 2) i = '0' + i;
if (s.length < 2) s = '0' + s;
yyyy_mm_dd += ' ' + [ h, i, s ].join( ':' );
}
$(this).val( yyyy_mm_dd ); // Here, val() works to set the new value. Go figure.
}
});
}
} catch( e ) {
alert( 'Internal error: ' + e.message );
}
}
Ok I think that the methods described here to detect if a browser supports the date inpu type are to complicated, if you'd like an easier approach you could just do something like this:
/*
* Instead of body, you could use the closest parent where you know your input is going to be
*/
$("body").on("focus", "input[type='date']", function(){
let attribute, property;
attribute= $(this).attr("type").toUpperCase();
property= $(this).prop("type").toUpperCase();
if(attribute!== property){
console.log("This browser doe not support type='date'");
//Pop up your own calendar or use a plugin
}
});
This function will be executed once you focus an input with type="date", it doesnt matter if it doesn't exist on the page yet, the listener will be watching until there's a new input[type="date"] on body, but as suggested on the comments, if you have a closer container that you know will always contain the inputs, well you could change it instead of having "body".
JS Listeners are fast anyway
精彩评论