Browse Source

feat(touch): 优化 slider, layer, carousel, laydate, rate, colorpicker 移动端滑动操作 (#1446)

pull/1458/head
贤心 11 months ago committed by GitHub
parent
commit
31028ea1c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      src/css/layui.css
  2. 18
      src/modules/carousel.js
  3. 26
      src/modules/colorpicker.js
  4. 109
      src/modules/lay.js
  5. 9
      src/modules/laydate.js
  6. 24
      src/modules/layer.js
  7. 52
      src/modules/rate.js
  8. 147
      src/modules/slider.js

3
src/css/layui.css

@ -1520,7 +1520,8 @@ body .layui-util-face .layui-layer-content{padding:0; background-color:#fff; co
.layui-rate li{margin-top: 0 !important;}
.layui-rate li i.layui-icon{ font-size: 20px; color: #ffb800;}
.layui-rate li i.layui-icon{margin-right: 5px; transition: all .3s; -webkit-transition: all .3s;}
.layui-rate li i:hover{cursor: pointer; transform: scale(1.12); -webkit-transform: scale(1.12);}
.layui-rate li i:hover,
.layui-rate-hover{cursor: pointer; transform: scale(1.12); -webkit-transform: scale(1.12);}
.layui-rate[readonly] li i:hover{cursor: default; transform: scale(1);}
/** 颜色选择器 **/

18
src/modules/carousel.js

@ -331,13 +331,27 @@ layui.define(['jquery', 'lay'], function(exports){
if(options.elem.data('haveEvents')) return;
// 移入移出容器
options.elem.on('mouseenter', function(){
options.elem.on('mouseenter touchstart', function(){
if (that.config.autoplay === 'always') return;
clearInterval(that.timer);
}).on('mouseleave', function(){
}).on('mouseleave touchend', function(){
if (that.config.autoplay === 'always') return;
that.autoplay();
});
var touchEl = options.elem;
var isVertical = options.anim === 'updown';
lay.touchSwipe(touchEl, {
onTouchEnd: function(e, state){
var duration = Date.now() - state.timeStart;
var distance = isVertical ? state.distanceY : state.distanceX;
var speed = distance / duration;
var shouldSwipe = Math.abs(speed) > 0.25 || Math.abs(distance) > touchEl[isVertical ? 'height' : 'width']() / 3;
if(shouldSwipe){
that.slide(distance > 0 ? '' : 'sub');
}
}
})
options.elem.data('haveEvents', true);
};

26
src/modules/colorpicker.js

@ -567,6 +567,32 @@ layui.define(['jquery', 'lay'], function(exports){
change(hsb.h, hsb.s, hsb.b, a);
})
});
if(!lay.touchEventsSupported()) return;
// 触摸事件模拟
layui.each([
{elem: side, eventType: 'click'},
{elem: alphacolor, eventType: 'click'},
{elem: basis, eventType: 'mousedown'}
], function(i, obj){
lay.touchSwipe(obj.elem, {
onTouchMove: function(e){
touchHandler(e, obj.eventType)
}
})
})
function touchHandler(event, eventType) {
var pointer = event.touches[0];
var simulatedEvent = document.createEvent("MouseEvent");
simulatedEvent.initMouseEvent(eventType,
true, true, window, 1,
pointer.screenX, pointer.screenY,pointer.clientX, pointer.clientY,
false, false, false, false, 0, null
);
pointer.target.dispatchEvent(simulatedEvent);
}
};
//颜色选择器hsb转换

109
src/modules/lay.js

@ -505,6 +505,115 @@
}
};
/**
* 检测是否支持 Passive Event Listeners
* 引用自 https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
* @type {boolean}
*/
lay.passiveSupported = function(){
var passiveSupported = false;
try {
var opts = Object.defineProperty({}, 'passive', {
get: function() {
passiveSupported = true;
}
});
window.addEventListener('test', null, opts);
window.removeEventListener('test', null, opts);
} catch (err) {}
return passiveSupported;
}();
/**
* 是否支持 touch 事件
*/
lay.touchEventsSupported = function(){
return 'ontouchstart' in window;
};
/**
* @typedef touchSwipeState
* @prop {{x: number,y: number}} pointerStart - 初始坐标
* @prop {{x: number,y: number}} pointerEnd - 结束坐标
* @prop {number} distanceX - X 轴移动距离
* @prop {number} distanceY - Y 轴移动距离
* @prop {'none'|'right'|'left'|'up'|'down'} direction - 滑动方向
* @prop {Date} timeStart 开始时间
*/
/**
* @callback touchSwipeCallback
* @param {TouchEvent} e 滑动事件
* @param {touchSwipeState} state 滑动相关的状态
*/
/**
* 基于 touch 事件的触摸滑动
* @param {string | HTMLElement | JQuery} elem - HTML 元素
* @param {{onTouchStart?: touchSwipeCallback, onTouchMove?: touchSwipeCallback, onTouchEnd?: touchSwipeCallback}} opts - 配置项
*/
lay.touchSwipe = function(elem, opts){
var options = opts
var targetElem = lay(elem)[0];
if(!targetElem || !lay.touchEventsSupported()) return;
var state = {
pointerStart: {x:0, y:0},
pointerEnd: {x:0, y:0},
distanceX: 0,
distanceY: 0,
direction:'none', // 'up','down','left','right','none
timeStart: null
}
var onStart = function(e){
if(e.touches.length !== 1) return;
bindEvents();
state.timeStart = Date.now();
state.pointerStart.x = state.pointerEnd.x = e.touches[0].clientX;
state.pointerStart.y = state.pointerEnd.y = e.touches[0].clientY;
options.onTouchStart && options.onTouchStart(e, state);
}
var onMove = function(e){
e.preventDefault();
state.pointerEnd.x = e.touches[0].clientX;
state.pointerEnd.y = e.touches[0].clientY;
state.distanceX = state.pointerStart.x - state.pointerEnd.x;
state.distanceY = state.pointerStart.y - state.pointerEnd.y;
if(Math.abs(state.distanceX) > Math.abs(state.distanceY)){
state.direction = state.distanceX > 0 ? 'left' : 'right';
}else{
state.direction = state.distanceY > 0 ? 'up' : 'down';
}
options.onTouchMove && options.onTouchMove(e, state);
}
var onEnd = function(e){
options.onTouchEnd && options.onTouchEnd(e, state);
unbindEvents();
}
var bindEvents = function(){
targetElem.addEventListener('touchmove', onMove, lay.passiveSupported ? { passive: false} : false);
targetElem.addEventListener('touchend', onEnd);
targetElem.addEventListener('touchcancel', onEnd);
}
var unbindEvents = function(){
targetElem.removeEventListener('touchmove', onMove);
targetElem.removeEventListener('touchend', onEnd, lay.passiveSupported ? { passive: false} : false);
targetElem.removeEventListener('touchcancel', onEnd);
}
// 防止事件重复绑定
if(targetElem.__lay_touchswipe_cb_){
targetElem.removeEventListener('touchstart', targetElem.__lay_touchswipe_cb_);
}
targetElem.__lay_touchswipe_cb_ = onStart;
targetElem.addEventListener('touchstart', onStart);
}
/*
* lay 元素操作

9
src/modules/laydate.js

@ -1514,7 +1514,8 @@
if(haveSpan[0]) haveSpan.remove();
elemHeader[2].appendChild(span);
lay(ul).find('ol').each(function(i){
var olElem = lay(ul).find('ol');
olElem.each(function(i){
var ol = this;
//选择时分秒
lay(ol).find('li').on('click', function(){
@ -1537,6 +1538,12 @@
that.setBtnStatus();
});
});
if(lay.touchEventsSupported()){
olElem.on('touchstart', function(){
this.style['overflow-y'] = 'auto';
})
}
}
return that;

24
src/modules/layer.js

@ -1604,6 +1604,28 @@ layer.photos = function(options, loop, key){
e.preventDefault();
});
// 滑动切换图片事件,仅限 layui 中
if(window.layui || window.lay){
var lay = window.layui.lay || window.lay;
var touchEndCallback = function(e, state){
var duration = Date.now() - state.timeStart;
var speed = state.distanceX / duration;
var threshold = win.width() / 3;
var shouldSwipe = Math.abs(speed) > 0.25 || Math.abs(state.distanceX) > threshold;
if(!shouldSwipe) return;
if(state.direction === 'left'){
dict.imgnext(true);
}else if(state.direction === 'right'){
dict.imgprev(true);
}
}
$.each([that.shadeo, dict.main], function(i, elem){
lay.touchSwipe(elem, {
onTouchEnd: touchEndCallback
})
})
}
};
// 图片预加载
@ -1764,7 +1786,7 @@ ready.run = function(_$){
// 加载方式
window.layui && layui.define ? (
layer.ready(),
layui.define('jquery', function(exports){ // layui
layui.define(['jquery','lay'], function(exports){ // layui
layer.path = layui.cache.dir;
ready.run(layui.$);

52
src/modules/rate.js

@ -155,8 +155,9 @@ layui.define(['jquery', 'lay'],function(exports){
var options = that.config;
var _ul = that.elemTemp;
var wide = _ul.find("i").width();
var liElems = _ul.children("li");
_ul.children("li").each(function(index){
liElems.each(function(index){
var ind = index + 1;
var othis = $(this);
@ -212,6 +213,55 @@ layui.define(['jquery', 'lay'],function(exports){
})
})
lay.touchSwipe(_ul, {
onTouchMove: function(e, state){
if(Date.now() - state.timeStart <= 200) return;
var pageX = e.touches[0].pageX;
var rateElemWidth = _ul.width();
var itemElemWidth = rateElemWidth / options.length; // 单颗星的宽度
var offsetX = pageX - _ul.offset().left;
var num = offsetX / itemElemWidth; // 原始值
var remainder = num % 1;
var integer = num - remainder;
// 最终值
var score = remainder <= 0.5 && options.half ? integer + 0.5 : Math.ceil(num);
if(score > options.length) score = options.length;
if(score < 0) score = 0;
liElems.each(function(index){
var iconElem = $(this).children('i');
var isActiveIcon = (Math.ceil(score) - index === 1);
var needSelect = Math.ceil(score) > index;
var shouldHalfIcon = (score - index === 0.5);
if(needSelect){
// 设置选中样式
iconElem.addClass(ICON_RATE_SOLID).removeClass(ICON_HALF_RATE);
if(options.half && shouldHalfIcon){
iconElem.addClass(ICON_RATE_HALF).removeClass(ICON_RATE_SOLID);
}
}else{
// 恢复初始样式
iconElem.addClass(ICON_RATE).removeClass(ICON_SOLID_HALF);
}
// 设置缩放样式
iconElem.toggleClass('layui-rate-hover', isActiveIcon);
});
// 更新最终值
options.value = score;
options.setText && options.setText(options.value);
},
onTouchEnd: function(e, state){
if(Date.now() - state.timeStart <= 200) return;
_ul.find('i').removeClass('layui-rate-hover');
options.choose && options.choose(options.value);
options.setText && options.setText(options.value);
}
});
};
//事件处理

147
src/modules/slider.js

@ -10,26 +10,26 @@ layui.define(['jquery', 'lay'], function(exports){
// 外部接口
var slider = {
config: {}
,index: layui.slider ? (layui.slider.index + 10000) : 0
config: {},
index: layui.slider ? (layui.slider.index + 10000) : 0,
// 设置全局项
,set: function(options){
set: function(options){
var that = this;
that.config = $.extend({}, that.config, options);
return that;
}
},
// 事件
,on: function(events, callback){
on: function(events, callback){
return layui.onevent.call(this, MOD_NAME, events, callback);
}
};
// 操作当前实例
var thisSlider = function(){
var that = this
,options = that.config;
var that = this;
var options = that.config;
return {
setValue: function(value, index){ // 设置值
@ -37,8 +37,8 @@ layui.define(['jquery', 'lay'], function(exports){
value = value < options.min ? options.min : value;
options.value = value;
return that.slide('set', value, index || 0);
}
,config: options
},
config: options
}
};
@ -65,18 +65,18 @@ layui.define(['jquery', 'lay'], function(exports){
// 默认配置
Class.prototype.config = {
type: 'default' //滑块类型,垂直:vertical
,min: 0 //最小值
,max: 100 //最大值,默认100
,value: 0 //初始值,默认为0
,step: 1 //间隔值
,showstep: false //间隔点开启
,tips: true //文字提示,开启
,input: false //输入框,关闭
,range: false //范围选择,与输入框不能同时开启,默认关闭
,height: 200 //配合 type:"vertical" 使用,默认200px
,disabled: false //滑块禁用,默认关闭
,theme: '#16baaa' //主题颜色
type: 'default', //滑块类型,垂直:vertical
min: 0, //最小值
max: 100, //最大值,默认100
value: 0, //初始值,默认为0
step: 1, //间隔值
showstep: false, //间隔点开启
tips: true, //文字提示,开启
input: false, //输入框,关闭
range: false, //范围选择,与输入框不能同时开启,默认关闭
height: 200, //配合 type:"vertical" 使用,默认200px
disabled: false, //滑块禁用,默认关闭
theme: '#16baaa' //主题颜色
};
//滑块渲染
@ -116,9 +116,9 @@ layui.define(['jquery', 'lay'], function(exports){
options.value[0] = Math.min(options.value[0],options.max);
options.value[1] = Math.min(options.value[1],options.max);
var scaleFir = Math.floor((options.value[0] - options.min) / (options.max - options.min) * 100)
,scaleSec = Math.floor((options.value[1] - options.min) / (options.max - options.min) * 100)
,scale = scaleSec - scaleFir + '%';
var scaleFir = Math.floor((options.value[0] - options.min) / (options.max - options.min) * 100);
var scaleSec = Math.floor((options.value[1] - options.min) / (options.max - options.min) * 100);
var scale = scaleSec - scaleFir + '%';
scaleFir = scaleFir + '%';
scaleSec = scaleSec + '%';
} else {
@ -143,8 +143,8 @@ layui.define(['jquery', 'lay'], function(exports){
'<div class="layui-slider-bar" style="background:'+ theme +'; '+ (options.type === 'vertical' ? 'height' : 'width') +':'+ scale +';'+ (options.type === 'vertical' ? 'bottom' : 'left') +':'+ (scaleFir || 0) +';"></div><div class="layui-slider-wrap" style="'+ (options.type === 'vertical' ? 'bottom' : 'left') +':'+ (scaleFir || scale) +';">' +
'<div class="layui-slider-wrap-btn" style="border: 2px solid '+ theme +';"></div></div>'+ (options.range ? '<div class="layui-slider-wrap" style="'+ (options.type === 'vertical' ? 'bottom' : 'left') +':'+ scaleSec +';"><div class="layui-slider-wrap-btn" style="border: 2px solid '+ theme +';"></div></div>' : '') +'</div>';
var othis = $(options.elem)
,hasRender = othis.next('.' + ELEM_VIEW);
var othis = $(options.elem);
var hasRender = othis.next('.' + ELEM_VIEW);
//生成替代元素
hasRender[0] && hasRender.remove(); //如果已经渲染,则Rerender
that.elemTemp = $(temp);
@ -204,12 +204,12 @@ layui.define(['jquery', 'lay'], function(exports){
//划过滑块显示数值
var timer;
that.elemTemp.find('.' + SLIDER_WRAP_BTN).on('mouseover', function(){
var sliderWidth = options.type === 'vertical' ? options.height : that.elemTemp[0].offsetWidth
,sliderWrap = that.elemTemp.find('.' + SLIDER_WRAP)
,tipsLeft = options.type === 'vertical' ? (sliderWidth - $(this).parent()[0].offsetTop - sliderWrap.height()) : $(this).parent()[0].offsetLeft
,left = tipsLeft / sliderWidth * 100
,value = $(this).parent().data('value')
,tipsTxt = options.setTips ? options.setTips(value) : value;
var sliderWidth = options.type === 'vertical' ? options.height : that.elemTemp[0].offsetWidth;
var sliderWrap = that.elemTemp.find('.' + SLIDER_WRAP);
var tipsLeft = options.type === 'vertical' ? (sliderWidth - $(this).parent()[0].offsetTop - sliderWrap.height()) : $(this).parent()[0].offsetLeft;
var left = tipsLeft / sliderWidth * 100;
var value = $(this).parent().data('value');
var tipsTxt = options.setTips ? options.setTips(value) : value;
that.elemTemp.find('.' + SLIDER_TIPS).html(tipsTxt);
clearTimeout(timer);
@ -235,17 +235,17 @@ layui.define(['jquery', 'lay'], function(exports){
//滑块滑动
Class.prototype.slide = function(setValue, value, i){
var that = this
,options = that.config
,sliderAct = that.elemTemp
,sliderWidth = function(){
var that = this;
var options = that.config;
var sliderAct = that.elemTemp;
var sliderWidth = function(){
return options.type === 'vertical' ? options.height : sliderAct[0].offsetWidth
}
,sliderWrap = sliderAct.find('.' + SLIDER_WRAP)
,sliderTxt = sliderAct.next('.' + SLIDER_INPUT)
,inputValue = sliderTxt.children('.' + SLIDER_INPUT_TXT).children('input').val()
,step = 100 / ((options.max - options.min) / Math.ceil(options.step))
,change = function(offsetValue, index, from){
};
var sliderWrap = sliderAct.find('.' + SLIDER_WRAP);
var sliderTxt = sliderAct.next('.' + SLIDER_INPUT);
var inputValue = sliderTxt.children('.' + SLIDER_INPUT_TXT).children('input').val();
var step = 100 / ((options.max - options.min) / Math.ceil(options.step));
var change = function(offsetValue, index, from){
if(Math.ceil(offsetValue) * step > 100){
offsetValue = Math.ceil(offsetValue) * step
}else{
@ -254,8 +254,8 @@ layui.define(['jquery', 'lay'], function(exports){
offsetValue = offsetValue > 100 ? 100: offsetValue;
offsetValue = offsetValue < 0 ? 0: offsetValue;
sliderWrap.eq(index).css((options.type === 'vertical' ?'bottom':'left'), offsetValue + '%');
var firLeft = valueTo(sliderWrap[0].offsetLeft)
,secLeft = options.range ? valueTo(sliderWrap[1].offsetLeft) : 0;
var firLeft = valueTo(sliderWrap[0].offsetLeft);
var secLeft = options.range ? valueTo(sliderWrap[1].offsetLeft) : 0;
if(options.type === 'vertical'){
sliderAct.find('.' + SLIDER_TIPS).css({"bottom":offsetValue + '%', "margin-bottom":"20px"});
firLeft = valueTo(sliderWidth() - sliderWrap[0].offsetTop - sliderWrap.height());
@ -281,8 +281,8 @@ layui.define(['jquery', 'lay'], function(exports){
//如果开启范围选择,则返回数组值
if(options.range){
var arrValue = [
sliderWrap.eq(0).data('value')
,sliderWrap.eq(1).data('value')
sliderWrap.eq(0).data('value'),
sliderWrap.eq(1).data('value')
];
if(arrValue[0] > arrValue[1]) arrValue.reverse(); //如果前面的圆点超过了后面的圆点值,则调换顺序
}
@ -292,27 +292,40 @@ layui.define(['jquery', 'lay'], function(exports){
// 值完成选中的事件
if(from === 'done') options.done && options.done(that.value);
}
,valueTo = function(value){
var oldLeft = value / sliderWidth() * 100 / step
,left = Math.round(oldLeft) * step;
};
var valueTo = function(value){
var oldLeft = value / sliderWidth() * 100 / step;
var left = Math.round(oldLeft) * step;
if(value == sliderWidth()){
left = Math.ceil(oldLeft) * step;
}
return left;
}
};
//拖拽元素
,elemMove = $(['<div class="layui-auxiliar-moving" id="LAY-slider-moving"></div'].join(''))
,createMoveElem = function(move, up){
var elemMove = $(['<div class="layui-auxiliar-moving" id="LAY-slider-moving"></div'].join(''));
var createMoveElem = function(sliderBtnElem, move, up){
var upCall = function(){
up && up();
// 移动端延时一秒关闭
up && up(lay.touchEventsSupported() ? 1000 : 0);
elemMove.remove();
options.done && options.done(that.value);
// 移动端
if (lay.touchEventsSupported()) {
sliderBtnElem[0].removeEventListener('touchmove', move, lay.passiveSupported ? { passive: false } : false);
sliderBtnElem[0].removeEventListener('touchend', upCall);
sliderBtnElem[0].removeEventListener('touchcancel', upCall);
}
};
$('#LAY-slider-moving')[0] || $('body').append(elemMove);
elemMove.on('mousemove', move);
elemMove.on('mouseup', upCall).on('mouseleave', upCall);
// 移动端
if (lay.touchEventsSupported()) {
sliderBtnElem[0].addEventListener('touchmove', move, lay.passiveSupported ? { passive: false } : false);
sliderBtnElem[0].addEventListener('touchend', upCall);
sliderBtnElem[0].addEventListener('touchcancel', upCall);
}
};
//动态赋值
@ -321,11 +334,15 @@ layui.define(['jquery', 'lay'], function(exports){
//滑块滑动
sliderAct.find('.' + SLIDER_WRAP_BTN).each(function(index){
var othis = $(this);
othis.on('mousedown', function(e){
othis.on('mousedown touchstart', function(e){
e = e || window.event;
if(e.type === 'touchstart'){
e.clientX = e.originalEvent.touches[0].clientX;
e.clientY = e.originalEvent.touches[0].clientY;
}
var oldleft = othis.parent()[0].offsetLeft
,oldx = e.clientX;
var oldleft = othis.parent()[0].offsetLeft;
var oldx = e.clientX;
if(options.type === 'vertical'){
oldleft = sliderWidth() - othis.parent()[0].offsetTop - sliderWrap.height()
oldx = e.clientY;
@ -333,6 +350,10 @@ layui.define(['jquery', 'lay'], function(exports){
var move = function(e){
e = e || window.event;
if (e.type === 'touchmove') {
e.clientX = e.touches[0].clientX;
e.clientY = e.touches[0].clientY;
}
var left = oldleft + (options.type === 'vertical' ? (oldx - e.clientY) : (e.clientX - oldx));
if(left < 0)left = 0;
if(left > sliderWidth())left = sliderWidth();
@ -343,12 +364,14 @@ layui.define(['jquery', 'lay'], function(exports){
e.preventDefault();
};
var up = function(){
var up = function(delay){
othis.removeClass(ELEM_HOVER);
sliderAct.find('.' + SLIDER_TIPS).hide();
setTimeout(function(){
sliderAct.find('.' + SLIDER_TIPS).hide();
}, delay);
};
createMoveElem(move, up)
createMoveElem(othis, move, up)
});
});
@ -417,8 +440,8 @@ layui.define(['jquery', 'lay'], function(exports){
//事件处理
Class.prototype.events = function(){
var that = this
,options = that.config;
var that = this;
var options = that.config;
};
//核心入口

Loading…
Cancel
Save