前两天在写原生上传文件时用到了input的 type="file"
按钮,写完以后就感觉别扭的一批:这玩意自己的样式实在难看,但是它又不支持自定义样式。最后“灵光一现”,想到:可以设置其透明度为 opacity: 0;
然后拿其他的标签覆盖上去:比如 input type="text"
、比如 div
、比如 label
。。。
相关项目:http://htmlpreview.github.io/?https://github.com/1314mxc/compress/blob/master/index.html
最近正好在搞这个“图片上传”、“压缩”的事情:H5以后,input就支持了accept —— 选择文件类型,还有一些值比如:“multiple
” 可设置“只选择文件夹”。
这两个问题也造就了这篇文章:
先说下input中的一些问题:
- 不是所有的 input 都支持“placeholder”,比如:
type="date"
。尝试了一下,虽然H5原生的date功能强大,但对placeholder还真没有什么解决方案。不过,参照本文开篇的思路,我们同样可以先写一个type="text"
,然后用0级DOM事件 onfocus (在触焦时)将其变为date:onfocus="(this.type='date')"
。
这种方法在PC端简直完美,但在一些手机上需要【点击两次】才有效 —— 猜测可能还是移动端响应click的问题。
所以还有一种方案:在input上覆盖一个div,当点击时去操控 input 的事件和响应!
我们都知道,在input中,当输入过一次时,下一次输入会有提示 —— autocomplete 。但这也会带来一些效果:input 将背景“自动”变为黄色。
哦,这可不是什么bug。是 input 对 paste 的响应样式罢了。我们可以用“autocomplate='off'
” / 伪类:
input:-webkit-autofill{
-webkit-box-shadow: 0 0 0px 1000px white inset;
}
来去除效果。
上面这段CSS代码意思为:将边框阴影设为白色,然后向内扩展,覆盖原来应该显示的“黄色”。
一些input
input file类型控件有一个属性,名为accept。可以用来指定浏览器接受的文件类型,也就是的那个我们打开系统的选择文件弹框的时候,默认界面中呈现的文件类型。例如:accept="image/jpeg"
,则界面中只有jpg图片。
实际开发的时候,很少只允许传jpg图片,应该都是只能传图片类型,此时,可以使用:
accept="image/*"
但是,需要注意的是,虽然使用 accept=“image/*” 很方便,但是在Chrome浏览器下,可能会有文件选择窗口打开非常慢的问题,因此,如果仅仅是上传图片,建议使用:
accept="image/png, image/jpeg, image/gif, image/jpg"
多个属性值使用逗号分隔。再比如:
<input accept="audio/*,video/*,image/*">
accept属性在部分Android设备下可能是无效的。
input file文件选择框还支持capture属性,允许开发者调用一些设备的媒体功能。
调用前置摄像头:
<input type="file" name="image" accept="image/*" capture="user">
调用后置摄像头:
<input type="file" name="image" accept="image/*" capture="environment">
调用麦克风和摄像头:
<input type="file" name="image" accept="audio/*" capture>
2020-09-09更新
除了H5-input的capture属性(user & environment),浏览器还单独提供了getUserMedia API,它的优势是“不借助任何(第三方)插件”。但据我研究,他也有很大的缺点:必须要配合其他HTML元素显示。
这其实也不算缺点,但确实很麻烦!
目前最好的实践应该是vedio+canvas+getUserMedia API 了,它基于这样一个思想:将getUserMedia所得画面通过createObjectURL API转为流元素(实时)展示到video中,点击“拍照”时,将“当前帧”绘制到canvas中。
<video id="video" autoplay style="width:480px;height: 320px"></video>
<div><button id="capture">拍照</button></div>
<canvas id="canvas" width="480" height="320"></canvas>
function getUserMedia(constraints,success,error){
if(navigator.mediaDevices.getUserMedia){
//最新的标准API
navigator.mediaDevices.getUserMedia(constraints).then(success).catch(error)
}else if(navigator.webkitGetUserMedia){
//webkit核心浏览器
navigator.webkitGetUserMedia(constraints,success,error)
}else if(navigator.mozGetUserMedia){
//Firefox浏览器
navigator.mozGetUserMedia(constraints,success,error)
}
}
var video=document.getElementById("video")
var canvas=document.getElementById("canvas")
var context=canvas.getContext("2d")
function success(stream){
var URL=window.URL || window.webkitURL
if("srcObject" in video){
video.srcObject=stream
}else{
video.src=URL.createObjectURL(stream)
}
video.play()
}
function error(err){
console.log('访问用户媒体设备失败',err.name,err.message)
}
if(navigator.mediaDevices.getUserMedia || navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia){
getUserMedia({video:{width: 480,height: 320}},success,error)
}else{
alert('您的浏览器不支持用户媒体设备!')
}
document.getElementById("capture").addEventListener("click",function(){
context.drawImage(video,0,0,480,320)
})
2020-09-15更新
。。。我错了,没想到input这么强:如果你想在手机页面上使用相机(相机!不是视频),你完全可以只用input元素的accept属性,他有两个选项:
但如果是视频流,建议还是用webRTC API吧,上面所说 getUserMedia
API实在是坑(经常会发生诸如摄像头被占而不能用的情况,贼尴尬),还是谨慎点好。
HTML5约束验证
HTML5对于input增加了很多标签属性,和事件。其中最著名的莫过于“表单验证validate”了:
当你获取到validitestate对象(通过DOM.validity
)后,这里面有几个很重要的属性:
- willValidate:元素约束是否“被符合”,无则返回false
- validity:当前元素验证状态
- validationMessage:描述相关约束失败 / 错误信息
- checkValidity():有没有满足任一约束(常被用在submit事件中)
- setCustomValidity():设置自定义验证提示信息
里面还有许多属性都是和input中的属性(字段)一一对应的:
validitestate对象属性 | input属性字段 |
---|---|
valueMissing | required(设置非空) |
typeMismatch | type |
patternMismatch | pattern |
tooLong | maxlength |
step | 用于type="number"中,可指定“数字精确规范”:如step=“0.01”,则最后可获取到两位小数(否则number默认只能获取整数值) |
input还有一个比较“特别”的:search。这种类型的input,在输入时右侧会有一个“带圆圈的×”,那么肯定会有人觉得不想要 or 不好看了,我们也可以用伪元素来将其去掉:
input[type="search"]::-webkit-search-cancel-button{
-webkit-appearence: none;
//下面可自定义样式
}
同样的还有input的button、普通input的边框阴影都可以用类似代码去除!
看好了,上面是 -webkit前缀!几乎不用想,在手机上一定会出现一些“似乎莫名其妙的问题”:比较推荐的是,用div+absolute来重新写一个“小叉号”,用JS控制对应事件。
这里“比较推荐”是“在解决问题的办法”中比较而得。事实上,还是推荐用原生的“取消按钮”。
哦对了,既然有了maxlength,为什么W3C还保留了max?
因为在 type=“number” 中,maxlength是没啥用的。。。(就很尴尬)maxlength属性对于type=password, search, tel, text, url
这五种生效。 更尴尬的是:max也只能控制“上限值” —— 比如只能输入5位,则写为:max="99999"
,而且他的效果还是体现到“获取到的值”上。这也就是说:你在没有任何提示下,在你眼睛所能看到的input里,你可以输入无限长度。
input的高度可以用height或者padding来改变 —— 事实上,几乎所有的行内(非替换)元素都是用padding改变高度的(行内替换元素可以设置height)
对于input标签
type=number
,你可以借助js截取:
<input type="number" placeholder="请设置最多11位密码" oninput="if(value.length>11)value=value.slice(0,11)
其实说了这么多,HTML+CSS就可以完成简单的“表单校验”:
伪类“:valid
”、“:invalid
”直接作用到对应input上即可 —— 基于pattern + required的基础功能验证。
//HTML部分
<input id="mail" type="email" required placeholder="your email" />
<span class="title">邮箱</span>
<label for="mail"></label>
//css部分
input:focus,input:hover{text-indent: 2px;}
input:focus +span.title,input:hover +span.title{transform: translateX(-120%);}
input:valid ~label::after{content: '很棒,您输入的是一个邮箱!';}
input:invalid ~label::after{content: '邮箱格式不对,这是邮箱吗?';}
input:valid{border: 1px solid grey;}
input:invalid{border: 1px solid red;}
甚至是用伪类 optional 、required 设置“选填”和“必填”的简单样式:
input:required +label::after{content:'(必填)';}
input:optional +label::after{content:'(选填)';}
input:required:focus{box-shadow: 0 0 3px 1px #aa0088;}
input:optional:focus{box-shadow: 0 0 3px 1px #999;}
你甚至可以在input中加入0级DOM事件oninvalid,在其中直接调用 this.setCustomValidity("...")
来作为填错时提示。这很方便。
做完这些,你很满意。但是你的UI设计师可能要发火了 —— 不同的提示框UI实在是太…
我们需要自定义提示气泡!
- 阻止自带默认气泡:
function stopBubble(form){
form.addEventListener("invalid",function(e){
e.preventDefault()
},true)
//submit事件中监听:如果提交时验证不通过
form.addEventListener("submit",function(e){
if(!this.checkValidity()){
e.preventDefault()
}
},true)
}
- 自定义气泡、JS生成DOM、插入(appendChild)与input同级、并触发focus(
xxx.focus()
)
扩展Tip:
如何设置了验证(required/pattern/…)但不让浏览器去验证?
——两个HTML属性:novalidate(放在input上) / formnovalidate(放在提交按钮上)
如何清除表单的值?
form.reset()
—— reset事件可直接触发,不过一般是作为form表单button按钮的type值存在:<button type="reset"></button>