原文链接:Styling & Customizing File Inputs the Smart Way(作者:Osvaldas Valutis)
这份教程主要介绍如何将<input type="file" />与<label>配合、以保持语义化+可访问性为目标辅以少量Javascript来美化。
网上流传过一些“自定义”<input type="file" />
元素的方法,试验过但都实现不了像Readerrr上那种上传文件即时显示到内容流里的效果。兴许最烂的做法是:把input直接放进模拟按钮的容器、100%大填满容器、点击容器的任意区域即等于点击input
。听起来可行但实际又比较奇怪对吧?并且实际上这样做也会带来一些不可接受的缺失,比如可用性、比如对touch事件的支持等等。
后来我在StackOverflow网站上发现一条隐藏得很深的灵感,有些人顶它但是被淹没在该主题的回复里。这条回复的重点只有一个——<label>
!好用在于,它可以跟input的click事件关联上,这就实现了语义化解决方案。
<input type="file" name="file" id="file" class="inputfile" />
<label for="file">Choose a file</label>
因此点击这两个元素的任何一个都能得到相同的结果——弹出文件上传选择对话框。这意味着最难的部分解决了。没有JavaScript,不需要其他比如捕捉鼠标位置之类的麻烦方案,只需要两行代码。如图:
现在剩下的工作就是将它当成一个普通按钮来尽情美化。
隐藏<input>
首先,部分CSS属性对隐藏<input type="file" />
无效:
display: none;
visibility: hidden;
原因是:这样做之后input
将不能提交数据,也会被tab顺序跳过(键盘可用性一下子就完蛋了)。为了完美实现视觉不可见但浏览器可辨认,我测试出了这样的CSS组合:
.inputfile {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
你可能会好奇为什么宽和高会设成0.1px
而不是0px
,因为在某些浏览器下0宽高将会让<input>
元素被tab键忽略。而position: absolute
的目的是不干扰随后元素的位置。
美化<label>
对<label>的修饰就随意了,先来几行简单的效果:
.inputfile + label {
font-size: 1.25em;
font-weight: 700;
color: white;
background-color: black;
display: inline-block;
}
.inputfile:focus + label,
.inputfile + label:hover {
background-color: red;
}
可用性
一个用户要如何知道网页上的某个元素是“可点”?首先它要传达出一种可摁或者可点的暗示;其次hover时光标应该直接体现。而<label>
默认没有变小手的效果,那么加上:
键盘导航
如果用户不能使用键盘访问到这个按钮,设计就失败了。除了做到优雅隐藏外,还应该在focus状态时有所反馈:
.inputfile:focus + label {
outline: 1px dotted #000;
outline: -webkit-focus-ring-color auto 5px;
}
-webkit-focus-ring-color auto 5px
是Chrome/Opera/Safari上的一个默认支持的小技巧。
Touch事件上可能存在的问题
如果你正在使用FastClick(一个消除可触屏上的300毫秒反馈延迟的库)之类的东西,并且想对label
增加一些额外属性,按钮有可能出现问题,这时候就需要加上:
pointer-events: none;
JavaScript辅助
最后还不能忘记一件事:判断文件是否已选中。<input type="file" />
既然已经被隐藏,你就看不到选上了的文件名。这个时候就只能再依赖一小段JavaScript,使<label>
的文本显示为选中的文件名。如果有多个文件被选中,文本还应该显示出文件总数。
<input type="file" name="file" id="file" class="inputfile" data-multiple-caption="{count} files selected" multiple />
var inputs = document.querySelectorAll( '.inputfile' );
Array.prototype.forEach.call( inputs, function( input ) {
var label = input.nextElementSibling,
labelVal = label.innerHTML;
input.addEventListener( 'change', function( e ) {
var fileName = '';
if( this.files && this.files.length > 1 )
fileName = ( this.getAttribute( 'data-multiple-caption' ) || '' ).replace( '{count}', this.files.length );
else
fileName = e.target.value.split( '\\' ).pop();
if( fileName )
label.querySelector( 'span' ).innerHTML = fileName;
else label.innerHTML = labelVal;
});
});
jQuery的话:
$( '.inputfile' ).each( function() {
var $input = $( this ),
$label = $input.next( 'label' ),
labelVal = $label.html();
$input.on( 'change', function( e ) {
var fileName = '';
if( this.files && this.files.length > 1 )
fileName = ( this.getAttribute( 'data-multiple-caption' ) || '' ).replace( '{count}', this.files.length );
else if( e.target.value )
fileName = e.target.value.split( '\\' ).pop();
if( fileName )
$label.find( 'span' ).html( fileName );
else
$label.html( labelVal );
});
});
部分解释:
- 保留原生
[multiple]
特性允许一次上传多个文件,而[data-multiple-caption]
是自定义特性,用于显示被选中的文件数,同时也可以设置自定义文本。{count}
则是可选项,文件数将显示在内部。没有使用JS变量而是直接增加HTML特性的目的是,后者在同一页内多次使用时比较便利。 -
[multiple]
在IE9及以下不被支持,也不属于JavaScript的原生files
属性。对于后者,我们可以简单地使用value
(值),比如c:\path\filename.jpg
这样的路径,split( '\\' ).pop()
就能一步到位获取到文件名。 - 有个有趣的细节:在弹出文件选择对话框后你可以摁ESC键重设/清空
input
的值,虽然有可能限于Chrome/Opera。因此设计出labelVal
这个变量来装这个值,并且随时回头利用。
效果如图:
如果不支持JavaScript怎么办?
鉴于没有JavaScript以外的方式能获取文件是否选中的状态,最好的办法就是依赖<input type="file" />
的原生外观。只需要对<html>
增加一个.no-js
的class名,再用JS去改变为.js
——以辨识出JS是否得到支持。
<html class="no-js">
<head>
<!-- remove this if you use Modernizr -->
<script>
(function(e,t,n) {
var r=e.querySelectorAll("html")[0];
r.className=r.className.replace(/(^|\s)no-js(\s|$)/,"$1js$2")
})(document,window,0);
</script>
</head>
</html>
CSS相应为:
.js .inputfile {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
.no-js .inputfile + label {
display: none;
}
Firefox bug
可能大家都没想到,Firefox完全忽略input[type="file"]:focus
这样的定义,却完美支持:hover
和:active
!更意外的是,Firefox竟然支持在JavaScript里获取焦点,因此解决方法就是多加两行:
input.addEventListener( 'focus', function() {
input.classList.add( 'has-focus' );
});
input.addEventListener( 'blur', function() {
input.classList.remove( 'has-focus' );
});
.inputfile:focus + label,
.inputfile.has-focus + label {
outline: 1px dotted #000;
outline: -webkit-focus-ring-color auto 5px;
}
DEMO:http://tympanus.net/Tutorials/CustomFileInputs/
打包下载:http://tympanus.net/Tutorials/CustomFileInputs/CustomFileInputs.zip
题外话
有用户指出这样实现的上传按钮无法通过拖拽的方式来上传——毕竟这是<input type="file" />的原生功能。有人建议可以辅用Plupload或者Dropzone.js之类的插件,但此人反驳道本来就支持的功能现在失去了,再找其他东西来修补是一种脱裤放屁的行为。楼主回复认为,上传按钮的拖拽支持本来就非常隐蔽,并不是明面上可见的功能,除了一些习惯泡在网上的技术男,绝大部分普通用户看到一个形象的上传按钮时肯定是点击它而不是往上面拖文件。
除了设宽高为0,有人给出隐藏input
但不让它失效的其他方法:
input {
position: absolute;
opacity: 0.01;
}
也就是把透明度设为一个极小的值。目前未测试不确定适用范围(该人声称已在其公司的项目里广泛应用)。
还有一些人提出一个比较常见的理解:
input {
position: absolute;
width: 容器宽;
height: 容器高;
opacity: 0;
user-select: none;
}
透明度为0,同时不允许被选中,绝对定位覆盖填满容器,然后点击的时候就肯定点击在input
上。看似很好理解并且实现简单,但作者指出在IE10及以下这种做法可能导致双击才能生效:在线测试。
进而有人提出需求:如果还能清除选中的文件名就好了。作者链接到另一个StackOverflow上的帖子,那边有人问如何用jQuery清空<input type="file" />
的值,因为这样做无效:
$('#control').attr({ value: '' });
那边帖子有一个顶得最高的解决方法,颇为巧妙:
function resetFormElement(e) {
e.wrap('<form>').closest('form').get(0).reset();
e.unwrap();
// Prevent form submission
e.stopPropagation();
e.preventDefault();
}
具体做法就是产生一个外包<form>
,然后原生reset掉其内的<input>
,再去掉这个外壳。最后添加preventDefault
的目的是,假如表单里存在一个触发reset的button,这样做可以避免上面的JS触发它。
也有人提出把<input>
放在<label>
里,显得更天然更合理:
<label class="upload">
Choose a file...
<input type="file" />
</label>
但作者指出如果需要捕捉<input>
是否有:focus
就只能依赖JS了,因此在楼例里作者把<label>
放在<input>
后面,利用CSSinput + label
去选择到它。
最后有人提出如果动态添加多个<input type="file" />
,以上的方法就不够全面,希望作者能给出一个思路,到目前为止作者没有回复。