主要内容
1 .实际功能就是父组件点击弹出一个隐藏的下拉框,点击选择元素,还可以按鼠标上下键切换选项。
2 .这个其实就是之前显示文件的逻辑,但是好像这个部分的代码是另一个程序写的,看了下github的提交人,发现是一个程序员,但是不知道为什么是两种风格,还是有更多我不知道的细节
3 .还用了一个boradcast 方法,来监听别的组件给的方法
created(){
this.$on('on-update-popper',this.update);
this.$on('on-destroy-popper', this.destroy);
},
//组件创建的时候就监听这俩个事件
//然后外部给派发事件
methods:{
update(){
console.log("需要更新")
},
destory(){
console.log("需要销毁组件")
}
}
4 .源码好像是使用给的这个部分的代码 popper.js。我决定还是按照自己的方法实现。
5 .为什么他这个代码这么复杂。。。我怎么感觉这个功能还是很简单的。。
6 .我还是先按照自己的方法实现在看他的源码是怎么个操作方法吧
7 .
里面的内容直接slot传进来
1 .他没有嵌套那种复杂结构,所以可以直接传一个对象进来,不需要在单独写一个组件了吧,不知道为啥他还会有一个option的组件
{
value:"New York",
label:"Nwe York",
},
{
value:"London",
label:"London",
},
{
value:"上海",
label:"上海"
},
{
value:"hot",
label:"hot",
type:'title'
},
// 这里是标题
{
value:"广州",
label:"广州",
}
//所有的都是这个样子,如果要有标题的话就加上一个type的字段
2 .这里并不需要双向绑定,所以我不要在这里做v-moedel绑定.而是给一个回调事件
3 .div绑定 keydown事件,我原来以为是需要给这个div加个focus()事件,或者直接给全局挂事件,或者里面隐藏一个input来触发,但是发现用的是tab-index
tabIndex 用法概述
1 .可以设置键盘的TAB键在控件种的移动顺序,即焦点的顺序
2 .把控件的tabIndex属性设置为1-32767的一个值,就可以把这个控件加入TAB的序列中,当浏览者使用TAB键在网页种移动时,首先移动到最小的tabIndex属性值上,然后在具有最大tabIndex属性值的空间上结束
3 .默认的index属性为0,将排列在所有执行tabIndex的控件之后
4 .如果把他设置为一个负值,那么这个链接将被排除在TAB键的序列之外。但是onfocus,onblur事件任然会触发,onkeydown这些就不会触发了。不能通过tab导航来访问改元素,可以通过js获取
5 .通常使用 tab 键移动焦点,使用空格键激活焦点
6 .指示元素是否可以聚焦,以及在何处参与顺序键盘导航
7 .html5已经支持所有元素。
8 .如果多个元素拥有相同的tabIndex,他的相对顺序按照他们在当前DOM中的先后顺序决定
9 .document.activeElement:返回当前页面中获得焦点的元素,该属性只读。不支持IE
10 .设置焦点
1 .使用tab键来根据tabindex的定义来切换焦点
2 .document.getElementById("id").focus({preventScroll:false})
3 .preventScroll默认为false,表示当触犯的时候,浏览器会将元素滚动到视图中
11 .判断焦点 hasFocus()
1 .如果当前页面的活动元素获得了焦点,Document.hasFocus为true,否则为false。可以判断用户是否正在和页面进行交互
12 .focus:元素获取焦点时触发,不支持冒泡
13 .focusin:元素获取焦点时触发,支持冒泡
14 .blur:在元素失去焦点时触发,不支持冒泡
15 .focusout:在元素失去焦点时触发,支持冒泡
全部代码
<template>
<div
:class="classes"
>
<div
:class="selectCls"
@blur="toggleHeaderFocus"
@focus="toggleHeaderFocus"
@click="toggleMenu"
@keydown.esc="handleKeydown"
@keydown.enter="handleKeydown"
@keydown.up.prevent="handleKeydown"
@keydown.down.prevent="handleKeydown"
@keydown.tab="handleKeydown"
@keydown.delete="handleKeydown"
@mouseenter="hasMouseHoverHead=true"
@mouseleave="hasMouseHoverHead=false"
:tabindex="selectTabindex"
>
<slot name="input">
<input type="hidden" :name="name" :value="publicValue">
<!-- <input type="text" :name="name" :value="publicValue" ref="liSelect"> -->
<SelectHeader
:filterable="filterable"
:multiple="multiple"
:values="values"
:clearable="canBeCleared"
:prefix="prefix"
:disabled="disabled"
:remote="remote"
:input-element-id="elementId"
:initial-label="initialLabel"
:placeholder="placeholder"
:query-prop="query"
:max-tag-count="maxTagCount"
:max-tag-placeholder="maxTagPlaceholder"
:allow-create="allowCreate"
:show-create-item="showCreateItem"
@on-query-change="onQueryChange"
@on-input-focus="isFocused=true"
@on-input-blur="isFocused=false"
@on-clear="clearSingleSelect"
@on-enter="handleCreateItem"
>
</SelectHeader>
</slot>
{{query}}
</div>
<transition name="transition-drop">
<Drop
:class="dropdownCls"
v-show="dropVisible"
:placement="placement"
:transfer="transform"
>
<!-- 有的就直接传下去了,有的却选择了slot操作 -->
<ul
:class="[pre+'-not-found']"
v-show="showNotFoundLabel && !allowCreate">
<li>{{notFountText}}</li>
</ul>
<ul
v-show="query"
>
<li
@click="handleQueryClick(query)"
>{{query}}</li>
</ul>
<ul v-show="!this.query">
<li
v-for="(c,index) in selectOptions"
@click="handLelabelClick(c,index)"
:disabled="c.disabled===true"
:class="methodClasses(c,index)"
v-if="(!remote)||(remote&&!loading)"
>
{{c.value}}
</li>
</ul>
{{currentIndex}}
</Drop>
</transition>
</div>
</template>
<script>
// v-click-outside="handleClickOutside"
// v-click-outside.mousedown="handleClickOutside"
// v-click-outside.touchstart="handleClickOutside"
import ClickOutside from '../../../direction/outsideclick'
import SelectHeader from './selectHead'
import Emitter from '../../../utils/emitter'
import Drop from './drop'
export default {
name:'Select',
mixins:[Emitter],
data:function(){
return {
pre:"li-select",
values:[],
// 选好的
isFocused:false,
visiable:false,
// 这个现在也不知道是干啥的。
hasMouseHoverHead:false,
active:false,
// 这个为什么没有定义,而且没有人该他,应该是别的mixins里面有定义,最后再看下代码吧
focusIndex:-1,
query:'',
unchangeQuery:false,
filterQueryChange:false,
initialLabel:this.label,
lastRemoteQuery:'',
transform:{
left:0,
top:0,
width:200,
},
currentIndex:0,
// 当前正在选择的元素
indexSelectOptions:[],
}
},
props:{
capture: {
type: Boolean,
default () {
return !this.$IVIEW ? true : this.$IVIEW.capture;
}
},
// 是否开启capture模式,目前还不支持
value:{
type:[String,Number,Array],
default:''
},
// 当前选中项目的value值,可以使用v-model双向绑定。单选时只接受String或Number,多选时只接受Array
label:{
type:[String,Number,Array],
default:'',
},
// remote模式下,初始化使用
multiple:{
type:Boolean,
default:true,
},
// 是否支持多选
disabled:{
type:Boolean,
default:false,
},
// 是否禁用
clearable:{
type:Boolean,
default:true,
},
// 是否可以清空选项,单选时有效
placeholder:{
type:String,
},
// 选择框默认文字
filterable:{
type:Boolean,
default:false,
},
// 是否支持搜索
remote:{
type:Boolean,
default:false
},
// 是否开启远程搜索
filterMethod:{
type:Function,
},
// 远程搜索的方法
loading:{
type:Boolean,
default:false,
},
// 是否正在远程搜索
loadingText:{
type:String,
},
// 远程搜索中的文字提示
size:{
validator(value){
return ['small','large','default'].includes(value)
},
default:'default',
},
labelInValue:{
type:Boolean,
default:false,
},
// 在返回时,是否将label和value一并返回,默认只返回value
notFountText:{
type:String,
default:'没有匹配的选项'
},
// 下拉列表为空时显示的内容
placement:{
validator(value){
return ['top','bottom','top-satrt','bottom-start','bottom-end'].includes(value)
},
default:'bottom-start',
},
// 弹窗展开的方向
autoComplete:{
type:Boolean,
default:false,
},
transfer:{
type:Boolean,
default:false,
},
// 是否将弹层置放于body内,不受父级元素影响,从而达到更好的效果
name:{
type:String,
},
elementId:{
type:String,
},
// 给表元素添加id
prefix:{
type:String,
},
// select内显示的图标
maxTagCount:{
type:Number,
},
// 多选时最多显示几个图标
maxTagPlaceholder:{
type:Function,
},
// 隐藏tag时显示的内容
allowCreate:{
type:Boolean,
default:true,
},
// 是否允许用户创建新条目
selectOptions:{
type:Array,
default:function(){
return [
{
value:"New York",
label:"Nwe York",
},
{
value:"hot",
label:"hot",
type:'title',
disabled:true
},
// 这里是标题
{
value:"广州",
label:"广州",
disabled:true,
},
{
value:"杭州",
label:"杭州",
},
{
value:"苏州",
label:"苏州",
}
]
}
}
},
computed:{
classes(){
return [
`${this.pre}`,
{
[`${this.pre}-visible`]:this.visiable,
[`${this.pre}-disabled`]:this.disabled,
[`${this.pre}-multiple`]:this.multiple,
[`${this.pre}-single`]:!this.multiple,
[`${this.pre}-show-clear`]:this.showCloseIcon,
[`${this.pre}-${this.size}`]:!!this.size,
}
]
},
selectCls(){
return [
{
[`${this.pre}-selection`]:!this.autoComplete,
[`${this.pre}-selection-focused`]:this.isFocused
}
]
},
publicValue(){
return ''
},
canBeCleared(){
const uiStateMatch=this.hasMouseHoverHead||this.active
const qualifiesForClear=!this.multiple&&!this.disabled&&this.clearable
return uiStateMatch&&qualifiesForClear&&this.reset
},
showCreateItem(){
let state=false
if(this.allowCreate&&this.query!==''){
state=true
}
return state;
},
dropVisible(){
let status=true;
if(!this.visiable){
status=false
}
// const noOptions=!this.selectOptions||this.selectOptions.length===0
return status;
},
dropdownCls(){
return [
`${this.pre}-list`
]
},
showNotFoundLabel(){
return this.selectOptions&&this.selectOptions.length===0&&(!this.remote||(this.remote&&!this.loading))
},
selectTabindex(){
return this.disabled||this.filterable?-1:0;
}
},
directives:{
ClickOutside
},
methods:{
handleClickOutside(e){
if(this.visiable){
console.log("点击外部要关掉")
}else{
this.isFocused=false;
}
},
toggleHeaderFocus({type}){
if(this.disabled){
return
}
// this.isFocused=type==='focus'
this.isFocused=!this.isFocused
},
toggleMenu(e,force){
if(this.disabled)return false
this.visiable= typeof force !=='undefined'?force:!this.visiable;
if(this.visiable){
console.log("展开下面的东西")
this.getSelectPosition()
this.broadcast("li-drop",'on-update-popper')
}
},
getSelectPosition(){
let el=this.$el.getBoundingClientRect()
let top=el.top+document.documentElement.scrollTop+el.height
let left=el.left+document.documentElement.scrollLeft
let width=el.width
this.transform={top,left,width}
},
hiadeMenu(){
console.log("关闭菜单")
this.toggleMenu(null,false)
},
NavigateOptions(value){
let index=this.currentIndex
if(value>0){
console.log("向下")
if(index+1>this.selectOptions.length){
// 检查一下一个是否能加到里面去
index=0
this.currentIndex=this.checkDisabledAndTitleDownP(index,false)
}else{
index++
console.log('lala')
this.currentIndex=this.checkDisabledAndTitleDownP(index,false)
}
}else{
console.log('向上')
if(index-1<0){
index=this.selectOptions.length
this.currentIndex=this.checkDisabledAndTitleDownP(index,true)
}else{
index--
this.currentIndex=this.checkDisabledAndTitleDownP(index,true)
}
}
},
handleKeydown(e){
const key=e.key||e.computed
if(key==='Backspace')return
if(this.visiable){
e.preventDefault()
console.log(key)
// switch (key){
// case "Tab":
// console.log('Tag')
// e.stopPropagation();
// case "Escape":
// e.stopPropagation();
// this.hideMenu()
// case "ArrowUp":
// this.NavigateOptions(-1)
// case 'ArrowDown':
// this.NavigateOptions(1)
// case 'Enter':
// console.log('enter')
// }
// 虽然看起来是可以的,但是好像没有走全等的逻辑,满足上面的条件,也满足下面的条件,会两个一起执行
if(key==='ArrowDown'){
this.NavigateOptions(1)
}else if(key==='ArrowUp'){
this.NavigateOptions(-1)
}else if(key==="Tab"){
e.stopPropagation()
}else if(key==="Enter"){
this.handleEnter()
}else if(key=="Esc"){
console.log("关闭菜单")
this.hiadeMenu()
}
}else{
}
},
reset(){
this.query=''
this.focusIndex=-1;
this.unchangeQuery=true
this.values=[]
this.filterQueryChange=false
},
cleaSingleSelect(){
console.log("clear")
},
onQueryChange(query){
if(query.length>0&&query!==this.query){
// 重复的值不操作
if(this.autoComplete){
let isInputFocused=
document.hasFocus&&
document.hasFocus()&&
document.activeElement==this.$el.querySelector('input')
this.visiable=isInputFocused
}else{
this.visiable=true
}
}
this.query=query;
this.unchangeQuery=this.visiable;
this.filterQueryChange=true;
},
clearSingleSelect(){
this.$emit("on-clear")
this.hiadeMenu()
if(this.clearable)this.reset()
},
handleQueryClick(option){
this.handleCreateItem(option)
},
handleCreateItem(option){
if(this.allowCreate&&this.query!==''&&this.showCreateItem){
let query=''
if(option){
console.log(option)
query=option
}
query=this.query
// 他是在开了query的基础上,如果发生搜索的时候自己按下enter,这个时候就会触发进行创建新的item
this.$emit("on-create",query)
this.query=''
const option={
value:query,
label:query,
tag:undefined,
}
if(this.multiple){
this.onOptionClick(option)
}else{
this.$nextTick(()=>{
this.onOptionClick(option)
})
}
}else{
this.handleEnter()
}
},
onOptionClick(option){
if(this.multiple){
if(this.remote){
this.lastRemoteQuery=this.lastRemoteQuery||this.query
}else{
this.lastRemoteQuery=''
}
const valueIsSelect=this.values.find(
({value})=> value===option.value
)
const selectIsSelect=this.selectOptions.find(
({value})=>value===option.value
)
if(valueIsSelect){
this.values=this.values.filter(({value})=>{
value!==option.value
})
// 如果新加的值是之前已经选过的,那就过滤显示即可
}else{
this.values=[...this.values,option]
// 如果不是就直接加进去
if(selectIsSelect){
console.log('之前就有的enter不输入了')
return
}else{
this.selectOptions.push(option)
}
console.log('emit')
}
this.isFocused=true
this.initIndexOptions()
}else{
console.log("options")
this.query=String(option.label).trim();
this.values=[option]
this.lastRemoteQuery=''
const selectIsSelect=this.selectOptions.find(
({value})=>value===option.value
)
if(selectIsSelect){
console.log('原来就有')
}else{
console.log('已添加')
this.selectOptions.push(option)
console.log(this.selectOptions)
}
this.initIndexOptions()
this.hiadeMenu();
this.query=''
}
},
handLelabelClick(e,index){
if(e.type&&e.type=='title'||e.disabled){
console.log("点击了一个标题,不做处理")
return false
}else{
if(this.multiple){
if(this.remote){
this.lastRemoteQuery=this.lastRemoteQuery||this.query
}else{
this.lastRemoteQuery=''
}
const valueIsSelect=this.values.find(
({value})=> value===e.value
)
if(valueIsSelect){
this.currentIndex=index
return
}else{
this.values=[...this.values,e]
// 如果不是就直接加进去
this.$emit("on-select-change",this.values)
this.currentIndex=index
}
this.isFocused=true
}else{
console.log("options")
// this.query=String(e.label).trim();
this.values=[e]
this.lastRemoteQuery=''
this.hiadeMenu();
this.$emit("on-select-change",this.values)
this.index=0
}
}
},
checkDisabledAndTitleDownP(index,isUp){
console.log(index)
let result=[]
if(!isUp){
let end=this.indexSelectOptions.slice(index,this.indexSelectOptions.length)
let start=this.indexSelectOptions.slice(0,index)
result=[...end,...start]
}else{
let end=this.indexSelectOptions.slice(index,this.indexSelectOptions.length).reverse()
let start=this.indexSelectOptions.slice(0,index+1).reverse()
result=[...start,...end]
}
console.log(result)
for(let i=0;i<result.length;i++){
if(!result[i].type&&!result[i].disabled){
console.log('输出',result[i]['index'])
return result[i]['index']
}
}
},
initIndexOptions(){
this.indexSelectOptions=[]
if(this.indexSelectOptions.length){
this.indexSelectOptions=[]
}
for(let i=0;i<this.selectOptions.length;i++){
let item=this.selectOptions[i]
item['index']=i
this.indexSelectOptions.push(item)
}
},
handleEnter(){
if(this.allowCreate&&this.query!=='')return
console.log('enter-添加')
const options=this.selectOptions[this.currentIndex]
if(this.multiple){
if(this.remote){
this.lastRemoteQuery=this.lastRemoteQuery||this.query
}else{
this.lastRemoteQuery=''
}
const valueIsSelect=this.values.find(
({value})=> value===options.value
)
if(valueIsSelect){
return
}else{
this.values=[...this.values,options]
// 如果不是就直接加进去
this.$emit("on-select-change",this.values)
}
this.isFocused=true
}else{
this.query=String(options.label).trim();
this.values=[options]
this.lastRemoteQuery=''
this.hiadeMenu();
this.$emit("on-select-change",this.values)
this.index=0
console.log('单选')
}
},
methodClasses(c,index){
const valueIsSelect=this.values.find(
({value})=> value===c.value
)
return [
`${this.pre}-item`,
{
[`${this.pre}-item-disabled`]:!(c.type&&c.type=='title')&&c.disabled,
[`${this.pre}-item-selected`]:valueIsSelect,
[`${this.pre}-item-title`]:c.type&&c.type=='title',
[`${this.pre}-item-focus`]:this.currentIndex==index,
}
]
},
remove(e){
console.log('remove')
console.log(e)
}
},
components:{
SelectHeader,Drop,
},
mounted(){
this.initIndexOptions()
this.$on('on-remove-select',this.remove)
}
}
</script>
<style lang="less" src="./select.less">
</style>
drop组件
1 .用来装所有在本元素下面的那个下拉元素
<template>
<div
:class="className"
:style="computedStyle"
class="li-select-drop">
<slot></slot>
</div>
</template>
<script>
export default {
name:'li-drop',
props:{
className:{
type:String,
},
placement:{
type:String,
default:'bottom-start'
},
transfer:{
type:Object,
default:{
left:100,
top:100,
width:200,
}
}
},
computed:{
},
computed:{
computedStyle(){
let left=this.transfer.left
let top=this.transfer.top
let width=this.transfer.width
return {
left:`${left}px`,
top:`${top}px`,
width:`${width}px`,
}
}
}
}
</script>
less文件
@name:.li-select;
.hover(){
border-color:#57a3f3;
}
.active(){
border-color: #57a3f3;
outline: 0;
box-shadow: 0 0 0 2px rgba(45,140,240,.2)
}
.disabled(){
background-color: #f3f3f3;
opacity: 1;
cursor: not-allowed;
color: #ccc;
}
// 这些事全局没有搜到的样式,从css编出来的代码反编译出来的
@{name}{
display: inline-block;
width: 100%;
box-sizing: border-box;
vertical-align: middle;
color:@text-color;
font-size: @font-size-base;
&-selection{
display: block;
box-sizing: border-box;
outline:none;
user-select: none;
cursor: pointer;
position: relative;
background-color: #fff;
border-radius: @btn-border-radius;
border:1px solid @border-color-base;
transition:all @transition-time @ease-in-out;
&:hover,&-focused{
.hover()
}
}
.inner-arrow(){
position: absolute;
top: 50%;
right: 8px;
line-height: 1;
transform: translateY(-50%);
font-size: @font-size-base;
color: @subsidiary-color;
transition: all @transition-time @ease-in-out;
}
&-arrow{
.inner-arrow();
}
&-visible{
@{name}-selection{
.active()
}
@{name}-arrow{
transform:translateY(-50%) rotate(-90deg);
display:block;
}
}
&-disabled{
color:red;
@{name}-selection{
.disabled();
color:red;
@{name}-arrow{
color:@slider-disabled-color;
}
&:hover{
border-color:@border-color-base;
box-shadow:none;
@{name}-arrow{
display: inline-block;
}
}
}
}
// 单个样式
&-single &-selection{
height:@input-height-base;
position: relative;
@{name}-placeholder{
color:@input-placeholder-color;
}
@{name}-placeholder, @{name}-selected-value{
display: block;
height:@input-height-base - 2px;
line-height: @input-height-base - 2px;
font-size: @font-size-base;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-left:8px;
padding-right:24px;
text-align: start;
}
}
&-single &-input{
width: 100%;
}
// 多选样式
&-multiple &-selection{
padding: 0 24px 0 4px;
@{name}-placeholder{
display: block;
height: @input-height-base - 2px;
line-height: @input-height-base - 2px;
color:@input-placeholder-color;
font-size:@font-size-base;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-left: 4px;
padding-right: 22px;
}
@{name}-tag{
height: 24px;
line-height: 22px;
margin: 3px 4px 3px 0;
max-width: 99%;
position: relative;
}
}
&-tag{
display: inline-block;
height: 22px;
line-height: 22px;
margin: 2px 4px 2px 0;
padding: 0 8px;
border: 1px solid #e8eaec;
border-radius: 3px;
background: #f7f7f7;
font-size: 12px;
vertical-align: middle;
opacity: 1;
overflow: hidden;
&-wrapper{
display: flex;
}
}
&-default &-multiple &selection{
min-height: @input-height-base;
}
// head部分
&-head-flex{
display: flex;
align-items: center;
}
&-prefix{
display: inline-block;
vertical-align: middle;
}
&-head-with-prefix{
display: inline-block !important;
vertical-align: middle;
}
&-input{
display: inline-block;
height: @input-height-base;
line-height: @input-height-base;
padding:0 24px 0 8px;
outline: none;
border:none;
box-sizing: border-box;
color:@input-color;
background-color: transparent;
position: relative;
cursor:pointer;
&[disabled]{
cursor:@cursor-disabled;
color:#ccc;
-webkit-text-fill-color:#ccc;
}
}
&-list{
// min-width: 100%;
list-style-type:none;
width: inherit;
max-height: 200px;
overflow: auto;
margin: 2px 0;
padding: 5px 0;
background-color: #fff;
box-sizing: border-box;
border-radius: 4px;
box-shadow: 0 1px 6px rgba(0,0,0,.2);
position: absolute;
z-index: 900;
}
&-item{
margin: 0;
line-height: normal;
padding:7px 16px;
clear:both;
color:@text-color;
font-size:@font-size-base !important;
white-space: normal;
list-style: none;
cursor: pointer;
transition: background @transition-time @ease-in-out;
text-align: start;
&:hover{
background:@background-color-select-hover;
}
&-focus{
background:@background-color-select-hover;
color:@primary-color;
}
&-disabled{
color:@btn-disable-color;
cursor:@cursor-disabled;
&:hover{
color:@btn-disable-color !important;
background-color:#fff !important;
cursor:@cursor-disabled;
}
}
&-selected,&-selected:hover{
color:@primary-color;
background:#f3f3f3;
}
&-title{
font-size: 14px;
padding-left: 8px;
color:#999;
height:30px;
line-height: 30px;
}
}
}