放大镜
前置知识
- 鼠标焦点丢失的问题:
- 鼠标移动过快,鼠标会脱离拖拽的盒子,在盒子外面鼠标移动无法触发盒子的 mousemove,盒子不会再跟着计算最新的位置;
- 在盒子外面松开鼠标,也不会触发盒子的 mouseup,导致 mousemove 事件没有被移除,鼠标重新进入盒子,不管是否按住,盒子都会跟着走;
- 解决鼠标焦点丢失的问题:
- IE 和火狐浏览器中的解决方案:setCapture/releaseCapture 可以实现把元素焦点和鼠标绑定在一起 (或者移除绑定) 的效果,来防止鼠标焦点丢失;
- 谷歌中的解决方案:孙猴子 (鼠标) 蹦的在欢快,也逃离不了如来佛祖 (document) 的五指山,所以在项目中 move 和 up 方法绑定给 document 而不是盒子;
- 注意:在不确定当前元素的某个事件行为是否可能绑定多个方法的情况下,尽可能使用 DOM2 事件绑定的方式来实现;
- 图解:
- 示例代码:
HTMLCSSJavaScript
<div class="box" id="box"></div>
.box { position: absolute; top: 0; left: 0; width: 100px; height: 100px; background: lightcoral; cursor: move; }
let box = document.getElementById('box'); box.addEventListener('mousedown', down); // 鼠标按下做的事情 function down(ev) { // 禁止右键 if (ev.which === 3 || ev.which === 2) return; // 1.记录鼠标和盒子的起始位置,把值都记录在元素的自定义属性上 this.startX = ev.clientX; this.startY = ev.clientY; this.startL = this.offsetLeft; this.startT = this.offsetTop; // move.bind() 会返回一个代理函数,this 指向 盒子本身 this._MOVE = move.bind(this); this._UP = up.bind(this); // 「谷歌中的解决方案」 // this 是点击的事件源,不可以是 document,需要用 bind 改变 this 指向 document.addEventListener('mousemove', this._MOVE); document.addEventListener('mouseup', this._UP); // 「IE 和火狐浏览器中的解决方案」 // this.setCapture(); } // 鼠标移动的时候做的事情 function move(ev) { if (ev.which === 3 || ev.which === 2) return; let curL = ev.clientX - this.startX + this.startL, curT = ev.clientY - this.startY + this.startT; // this.style.cssText = `left:${curL}px;top:${curT}px;`; this.style.left = curL + 'px'; this.style.top = curT + 'px'; } // 鼠标抬起时候做的事情 function up(ev) { if (ev.which === 3 || ev.which === 2) return; document.removeEventListener('mousemove', this._MOVE); document.removeEventListener('mouseup', this._UP); // this.releaseCapture(); }
具体实现
-
图解
-
示例代码
HTMLCSSJavaScript<section class="magnifier clearfix"> <!-- 左侧缩略图 --> <div class="abbre"> <img src="images/1.jpg" alt=""> <div class="mark"></div> </div> <!-- 右侧原图(大图) --> <div class="origin"> <img src="images/2.jpg" alt=""> </div> </section> <script src="js/jquery.min.js"></script>
.magnifier { box-sizing: border-box; margin: 20px auto; width: 500px; } .magnifier .abbre, .magnifier .origin { float: left; } .magnifier .abbre { position: relative; box-sizing: border-box; width: 200px; height: 200px; } .magnifier .abbre img { width: 100%; height: 100%; } .magnifier .abbre .mark { display: none; position: absolute; top: 0; left: 0; width: 60px; height: 60px; background: rgba(255, 0, 0, .3); cursor: move; } .magnifier .origin { display: none; position: relative; box-sizing: border-box; width: 300px; height: 300px; overflow: hidden; } .magnifier .origin img { position: absolute; top: 0; left: 0; }
/* 首先计算大图的大小 */ let $abbre = $('.abbre'), $mark = $abbre.find('.mark'), $origin = $('.origin'), $originImg = $origin.find('img'); let abbreW = $abbre.outerWidth(), abbreH = $abbre.outerHeight(), //=>获取当前元素距离body的偏移 =>{top:xxx,left:xxx} abbreOffset = $abbre.offset(), markW = $mark.outerWidth(), markH = $mark.outerHeight(), originW = $origin.outerWidth(), originH = $origin.outerHeight(), originImgW = abbreW / markW * originW, originImgH = abbreH / markH * originH; $originImg.css({ width: originImgW, height: originImgH }); /* 鼠标进入和离开完成的事情 */ // 计算 MARK 盒子的位置和控制大图的移动 function computedMark(ev) { // MARK 盒子的偏移量 let markL = ev.clientX - abbreOffset.left - markW / 2, markT = ev.clientY - abbreOffset.top - markH / 2; // 最小偏移量 let minL = 0, minT = 0, // 最大偏移量(不能在照片外移动) maxL = abbreW - markW, maxT = abbreH - markH; markL = markL < minL ? minL : (markL > maxL ? maxL : markL); markT = markT < minT ? minT : (markT > maxT ? maxT : markT); $mark.css({ top: markT, left: markL }); // $originImg.css({ top: -markT / abbreH * originImgH, left: -markL / abbreW * originImgW }); } $abbre.on('mouseenter', function (ev) { $mark.css('display', 'block'); $origin.css('display', 'block'); computedMark(ev); }).on('mouseleave', function (ev) { $mark.css('display', 'none'); $origin.css('display', 'none'); }).on('mousemove', computedMark);
-
效果展示
模态框
模态框封装
/*
* 把写好的方法重载到内置的 window.alert 上
* alert('你好世界!'); => 弹出提升框 可以关闭,3S 后自动消失
* alert('你好世界!',{ => 支持自定义配置项
* title: '系统温馨提示', 控制标题的提示内容
* confirm: false, 是否显示确认和取消按钮
* handled: null 再点击确认/取消/×按钮的时候,触发的回调函数
* });
*/
window.alert = (function () {
// DIALOG:模态框类(每一个模态框都是创建这个类的实例)
class Dialog {
constructor(content, options) {
// 把后续在各个方法中用到的内容全部挂载到实例上
this.content = content;
this.options = options;
// 初始化
this.init();
}
// 创建元素
create(type, cssText) {
let element = document.createElement(type);
// style属性值
element.style.cssText = cssText;
return element;
}
createElement() {
this.$DIALOG = this.create('div', `
position: fixed;
top: 0;
left: 0;
z-index: 9998;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, .8);
user-select: none;
opacity: 0;
transition: opacity .3s;
`);
this.$MAIN = this.create('div', `
position: absolute;
top: 100px;
left: 50%;
margin-left: -200px;
z-index: 9999;
width: 400px;
background: #FFF;
border-radius: 3px;
overflow: hidden;
transform: translateY(-1000px);
transition: transform .3s;
`);
this.$HEADER = this.create('div', `
position: relative;
box-sizing: border-box;
padding: 0 10px;
height: 40px;
line-height: 40px;
background: #2299EE;
cursor: move;
`);
this.$TITLE = this.create('h3', `
font-size: 18px;
color: #FFF;
font-weight: normal;
`);
this.$CLOSE = this.create('i', `
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
font-size: 24px;
font-style: normal;
color: #FFF;
font-family: 'Courier New';
cursor: pointer;
`);
this.$BODY = this.create('div', `
padding: 30px 10px;
line-height: 30px;
font-size: 16px;
`);
this.$FOOTER = this.create('div', `
text-align: right;
padding: 10px 10px;
border-top: 1px solid #EEE;
`);
this.$CONFIRM = this.create('button', `
margin: 0 5px;
padding: 0 15px;
height: 28px;
line-height: 28px;
border: none;
font-size: 14px;
cursor: pointer;
color: #FFF;
background: #2299EE;
`);
this.$CANCEL = this.create('button', `
margin: 0 5px;
padding: 0 15px;
height: 28px;
line-height: 28px;
border: none;
font-size: 14px;
cursor: pointer;
color: #000;
background: #DDD;
`);
// 把创建的元素按照层级合成(从里向外合成)
// ES6中的解构赋值
let { title, confirm } = this.options;
this.$TITLE.innerHTML = title;
this.$CLOSE.innerHTML = 'X';
this.$HEADER.appendChild(this.$TITLE);
this.$HEADER.appendChild(this.$CLOSE);
this.$BODY.innerHTML = this.content;
this.$MAIN.appendChild(this.$HEADER);
this.$MAIN.appendChild(this.$BODY);
if (confirm) {
// 显示底部确定和取消按钮
this.$CONFIRM.innerHTML = '确定';
this.$CANCEL.innerHTML = '取消';
this.$FOOTER.appendChild(this.$CONFIRM);
this.$FOOTER.appendChild(this.$CANCEL);
this.$MAIN.appendChild(this.$FOOTER);
}
this.$DIALOG.appendChild(this.$MAIN);
// 插入到页面中:全部处理完后在插入,只引发一次回流
document.body.appendChild(this.$DIALOG);
}
// 显示模态框
show() {
// opacity=1 透明度是1
this.$DIALOG.style.opacity = 1;
// y轴坐标偏移改外0,隐藏时偏移-1000px
this.$MAIN.style.transform = 'translateY(0)';
// 如果没有确定和取消按钮,让其显示3000MS后消失
if (!confirm) {
this.$timer = setTimeout(() => {
this.hide();
clearTimeout(this.$timer);
}, 3000);
}
}
// 隐藏模态框 lx='CONFIRM/CANCEL'
// lx默认值是 CANCEL
hide(lx = 'CANCEL') {
this.$MAIN.style.transform = 'translateY(-1000px)';
this.$DIALOG.style.opacity = 0;
let fn = () => {
// 触发handled回调函数执行
if (typeof this.options.handled === "function") {
this.options.handled.call(this, lx);
}
// 移除创建的元素
document.body.removeChild(this.$DIALOG);
// 当前方法只绑定一次
this.$DIALOG.removeEventListener('transitionend', fn);
};
// transitionend 事件在 CSS 动画完成后触发
this.$DIALOG.addEventListener('transitionend', fn);
}
// 拖拽实现
down(ev) {
this.startX = ev.clientX;
this.startY = ev.clientY;
this.startT = this.$MAIN.offsetTop;
this.startL = this.$MAIN.offsetLeft;
this._MOVE = this.move.bind(this);
this._UP = this.up.bind(this);
document.addEventListener('mousemove', this._MOVE);
document.addEventListener('mouseup', this._UP);
}
move(ev) {
let curL = ev.clientX - this.startX + this.startL,
curT = ev.clientY - this.startY + this.startT;
let minL = 0,
minT = 0,
maxL = this.$DIALOG.offsetWidth - this.$MAIN.offsetWidth,
maxT = this.$DIALOG.offsetHeight - this.$MAIN.offsetHeight;
curL = curL < minL ? minL : (curL > maxL ? maxL : curL);
curT = curT < minT ? minT : (curT > maxT ? maxT : curT);
this.$MAIN.style.left = curL + 'px';
this.$MAIN.style.top = curT + 'px';
this.$MAIN.style.marginLeft = 0;
}
up(ev) {
document.removeEventListener('mousemove', this._MOVE);
document.removeEventListener('mouseup', this._UP);
}
// 执行INIT可以创建元素,让其显示,并且实现对应的逻辑操作
init() {
this.createElement();
//=>阻断渲染队列,让上述代码立即先渲染
// 如果不加此行代码,createElement和show都是写操作都会触发回流,
// 会加入到渲染队列中,等到全部执行完在显示,则没有动画效果,
// 加上此行读取代码,阻断渲染队列
this.$DIALOG.offsetWidth;
this.show();
// 基于事件委托实现关闭/确定/取消按钮的点击操作
this.$DIALOG.addEventListener('click', ev => {
let target = ev.target;
// button 或者 i标签匹配 target.tagName
if (/^(BUTTON|I)$/i.test(target.tagName)) {
// 取消自动消失
clearTimeout(this.$timer);
this.hide(target.innerHTML === '确定' ? 'CONFIRM' : 'CANCEL');
}
});
// 实现拖拽效果
// 改变 this 指向为当前实例
this.$HEADER.addEventListener('mousedown', this.down.bind(this));
}
}
// proxy:就是alert执行的函数
// =>插件封装的时候,如果需要传递多个配置项,我们一般都让其传递一个对象,
// 而不是单独一项项让其传递,这样处理的好处:
// 不需要考虑是否必传以及传递信息的顺序了、方便后期的扩展和升级...
return function proxy(content, options = {}) {
// 传参验证
if (typeof content === 'undefined') {
throw new Error("错误:提示内容必须传递!");
}
if (options === null || typeof options !== "object") {
throw new Error("错误:参数配置项必须是一个对象!");
}
// 参数默认值和替换 (Object.assign合并对象)
options = Object.assign({
title: '系统温馨提示',
confirm: false,
handled: null
}, options);
return new Dialog(content, options);
}
})();
使用
<button id="btn">点我有惊喜</button>
<script src="dialog-plugin.js"></script>
<script>
btn.onclick = function () {
alert(`
用户名:<input type='text' id='AA'/>
<br>
密码:<input type='password' id='BB'/>
`,
{
title: '用户登录',
confirm: true,
handled: lx => {
if (lx !== 'CONFIRM') return;
let AA = document.getElementById('AA');
let BB = document.getElementById('BB');
console.log(AA.value, BB.value);
}
});
};
</script>
图片瀑布流
瀑布流,又称瀑布流式布局,是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部;
制作思路
-
首先第一步,仔细观察瀑布流图片,会发现他们都是定宽不定高的,既然定宽,那么一共显示几列也就能够计算出来,如下图所示:
-
列数出来之后,用一个数组来保存一行的高度;举例说明:按照 4 列来算,一开始一张图片都没有放,每一列的高度都为 0,所以数组里面是 4 个 0;
-
接下来放入第一张图片,找数组里面最小的,目前都是 0,就放在第一列,放完之后需要更新数组里面的最小值;
-
然后依此类推,找数组最小的,会找到第二个 0,往第二列放入图片,更新数组,找到第三个 0,往第三列放入图片,更新数组…;
-
目前第一行满了,该放在第二行了,实际上和上面的算法是一样的,找数组的最小值即可,哪个最小就放在哪一列,放完之后更新数组;
-
计算公式:
- 新的高度的计算公式:
这一列新的高度 = 这一列高度(数组里面存储的有) + 间隙 + 新的图片高度
- 然而这只是计算了 top 值,还有 left 值需要计算,每张图片的 left 值只和该图片所在的列有关;
- 新的高度的计算公式:
代码附件下载
五子棋游戏
前置知识
-
HTML5 新增查询 API
- querySelector:找到满足条件的第一个元素,参数是 css 选择器
- querySelectorAll:找到满足条件的所有个元素,参数是 css 选择器
-
clientWidth:表示元素的内部宽度,该属性包括内边距 padding,但不包括边框 border、外边距 margin 和垂直滚动条;
制作思路
-
棋盘的绘制
- 绘制棋盘,其实就是一个 14 行 x 14 列 的 table 表格;
- 每个 td 会记录当前的格子是第几行第几列,通过自定义属性来进行记录,data-row 记录行,data-line 记录列;
-
确定落子的坐标
- 当用户在棋盘上点击鼠标时,通过 e.target.dataset 能够获取到用户点击的是哪一个 td;
- 再从 td 上获取位置信息;
-
用户点击了哪一个 td 后,需要判断落子应该在什么位置
- 通过 e.offsetX、e.offsetY,就可以获取到事件发生时鼠标相对于事件源元素的坐标,所谓事件源元素就是绑定事件的那个元素;
- 采用下面的方式来进行评判
- 首先计算出一个格子的宽高,然后用户点击的 e.offsetX 小于格子宽度的一半,e.offsetY 小于格子高度的一半,则是左上区域;e.offsetX 小于格子宽度的一半但是 e.offsetY 大于格子高度的一半,则是左下,依此类推。
//定位落子的位置是在四个角的哪一个 var positionX = e.offsetX > tdw / 2; var positionY = e.offsetY > tdw / 2; // 生成点击的坐标点,包含 3 个属性 // x 坐标、y 坐标和 c 落子方 var chessPoint = { x: positionX ? parseInt(temp.line) + 1 : parseInt(temp.line), y: positionY ? parseInt(temp.row) + 1 : parseInt(temp.row), c: whichOne }
- 这里重新组装了格子信息,得到类似于下面的对象。
{x: 0, y: 0, c: "white"} // 第一个格子左上 {x: 1, y: 0, c: "black"} // 第一个格子右上 {x: 0, y: 1, c: "white"} // 第一个格子左下 {x: 1, y: 1, c: "white"} // 第一个格子右下
- 通过 e.offsetX、e.offsetY,就可以获取到事件发生时鼠标相对于事件源元素的坐标,所谓事件源元素就是绑定事件的那个元素;
-
绘制棋子
-
绘制棋子其实就是在 td 单元格里面添加一个 div,例如第一行第一个棋子:
<td data-row="0" data-line="0"> <div style="" class="white" data-row="0" data-line="0"></div> </td>
-
需要注意的是每一行和每一列的最后两个棋子共用一个单元格,例如第一行最后两个棋子:
<td data-row="0" data-line="13"> <!-- 最后一个棋子 --> <div style="left: 50%;" class="black" data-row="0" data-line="14"></div> <!-- 倒数第二个棋子 --> <div style="" class="white" data-row="0" data-line="13"></div> </td>
-
根据上面的描述得知,最右下角的格子,会放 4 个 div,如下:
<td data-row="13" data-line="13"> <div style="" class="white" data-row="13" data-line="13"></div> <div style="left: 50%;" class="black" data-row="13" data-line="14"></div> <div style="top: 50%;" class="white" data-row="14" data-line="13"></div> <div style="top: 50%; left: 50%;" class="black" data-row="14" data-line="14"></div> </td>
-
-
胜负判定
- 具体的方案就是每一次落子都将这个棋子的坐标对象存储入数组,然后每次落子遍历数组进行判断即可;
- 核心代码块:
// 检查横着有没有连着的 5 个 chess2 = chessArr.find(function (item) { return curChess.x === item.x + 1 && item.y === curChess.y && item.c === curChess.c; }) chess3 = chessArr.find(function (item) { return curChess.x === item.x + 2 && item.y === curChess.y && item.c === curChess.c; }) chess4 = chessArr.find(function (item) { return curChess.x === item.x + 3 && item.y === curChess.y && item.c === curChess.c; }) chess5 = chessArr.find(function (item) { return curChess.x === item.x + 4 && item.y === curChess.y && item.c === curChess.c; }) if (chess2 && chess3 && chess4 && chess5) { end(curChess, chess2, chess3, chess4, chess5); } // 检查竖着有没有连着的 5 个 chess2 = chessArr.find(function (item) { return curChess.x === item.x && item.y + 1 === curChess.y && item.c === curChess.c; }) chess3 = chessArr.find(function (item) { return curChess.x === item.x && item.y + 2 === curChess.y && item.c === curChess.c; }) chess4 = chessArr.find(function (item) { return curChess.x === item.x && item.y + 3 === curChess.y && item.c === curChess.c; }) chess5 = chessArr.find(function (item) { return curChess.x === item.x && item.y + 4 === curChess.y && item.c === curChess.c; }) if (chess2 && chess3 && chess4 && chess5) { end(curChess, chess2, chess3, chess4, chess5); } // 检查斜线 1 有没有连着的 5 个 chess2 = chessArr.find(function (item) { return curChess.x === item.x + 1 && item.y + 1 === curChess.y && item.c === curChess.c; }) chess3 = chessArr.find(function (item) { return curChess.x === item.x + 2 && item.y + 2 === curChess.y && item.c === curChess.c; }) chess4 = chessArr.find(function (item) { return curChess.x === item.x + 3 && item.y + 3 === curChess.y && item.c === curChess.c; }) chess5 = chessArr.find(function (item) { return curChess.x === item.x + 4 && item.y + 4 === curChess.y && item.c === curChess.c; }) if (chess2 && chess3 && chess4 && chess5) { end(curChess, chess2, chess3, chess4, chess5); } // 检查斜线 2 有没有连着的 5 个 chess2 = chessArr.find(function (item) { return curChess.x === item.x - 1 && item.y + 1 === curChess.y && item.c === curChess.c; }) chess3 = chessArr.find(function (item) { return curChess.x === item.x - 2 && item.y + 2 === curChess.y && item.c === curChess.c; }) chess4 = chessArr.find(function (item) { return curChess.x === item.x - 3 && item.y + 3 === curChess.y && item.c === curChess.c; }) chess5 = chessArr.find(function (item) { return curChess.x === item.x - 4 && item.y + 4 === curChess.y && item.c === curChess.c; }) if (chess2 && chess3 && chess4 && chess5) { end(curChess, chess2, chess3, chess4, chess5); }
代码附件下载
拖拽(重力加速度)
示例代码
<div id='demo'></div>
div {
position: absolute;
left: 0px;
top: 0px;
background: orange;
width: 100px;
height: 100px;
}
var oDiv = document.getElementById('demo');
var lastX = 0;
var lastY = 0;
var iSpeedX = 0;
var iSpeedY = 0;
oDiv.onmousedown = function (e) {
clearInterval(this.timer);
var event = event || e;
// 点击位置,距离盒子左上角的距离
var disX = event.clientX - this.offsetLeft;
var disY = event.clientY - this.offsetTop;
var self = this;
document.onmousemove = function (e) {
var event = event || e;
// 计算此时盒子左上角的坐标
var newLeft = event.clientX - disX;
var newTop = event.clientY - disY;
// 计算拖动距离,作为 x、y 方向的速度
iSpeedX = newLeft - lastX;
iSpeedY = newTop - lastY;
// 记录拖动停止位置
lastX = newLeft;
lastY = newTop;
self.style.left = newLeft + 'px';
self.style.top = newTop + 'px';
}
document.onmouseup = function () {
document.onmouseup = null;
document.onmousemove = null;
startMove(self, iSpeedX, iSpeedY);
}
}
function startMove(dom, iSpeedX, iSpeedY) {
clearInterval(dom.timer);
var g = 3;
dom.timer = setInterval(function () {
iSpeedY += g;
var newTop = dom.offsetTop + iSpeedY;
var newLeft = dom.offsetLeft + iSpeedX;
// 容器底部碰撞检测
if (newTop >= document.documentElement.clientHeight - dom.clientHeight) {
iSpeedY *= -1;
iSpeedY *= 0.8;
iSpeedX *= 0.8;
newTop = document.documentElement.clientHeight - dom.clientHeight;
}
// 容器顶部碰撞检测
if (newTop <= 0) {
iSpeedY *= -1;
iSpeedY *= 0.8;
iSpeedX *= 0.8;
newTop = 0;
}
// 容器右侧碰撞检测
if (newLeft >= document.documentElement.clientWidth - dom.clientWidth) {
iSpeedX *= -1;
iSpeedY *= 0.8;
iSpeedX *= 0.8;
newLeft = document.documentElement.clientWidth - dom.clientWidth;
}
// 容器左侧碰撞检测
if (newLeft <= 0) {
iSpeedX *= -1;
iSpeedY *= 0.8;
iSpeedX *= 0.8;
newLeft = 0;
}
// 运动速度绝对值小于 1,直接停止
if (Math.abs(iSpeedX) < 1) iSpeedX = 0;
if (Math.abs(iSpeedY) < 1) iSpeedY = 0;
if (iSpeedX == 0 && iSpeedY == 0 && newTop == document.documentElement.clientHeight - dom.clientHeight) {
clearInterval(dom.timer);
console.log('over')
} else {
dom.style.top = newTop + 'px';
dom.style.left = newLeft + 'px';
}
}, 30);
}
代码附件下载
第 3️⃣ 座大山:h4 拖拽
上一篇