wired-pattern-lock
Version:
A lightweight div-based gesture pattern lock library using SVG lines.
246 lines (205 loc) • 7.4 kB
JavaScript
import { md5 } from 'js-md5';
export default class Wired {
constructor(container, options = {}) {
this.container = typeof container === 'string' ? document.querySelector(container) : container;
// 从元素属性读取配置
const attrOptions = {};
[...this.container.attributes].forEach(attr => {
let val = attr.value;
if (val === 'true') val = true;
else if (val === 'false') val = false;
attrOptions[attr.name.toLowerCase()] = val;
});
this.options = Object.assign({
rows: 3,
cols: 3,
size: 300,
pointRadius: 20,
lineColor: '#2196F3',
lineWidth: 4,
lineDash: [],
correctpassword: null,
onsuccess: null,
onfail: null,
}, attrOptions, options);
// 判断md5是否启用,只有存在 md5 属性且为 true 或空字符串才启用
const md5Attr = this.container.getAttribute('md5');
this.md5Enabled = (md5Attr === '' || md5Attr === 'true' || md5Attr === true);
this.successHandler = this._getHandler(this.options.onsuccess);
this.failHandler = this._getHandler(this.options.onfail);
this.points = [];
this.selected = [];
this.mouseDown = false;
this._init();
}
static enable(selector) {
const el = typeof selector === 'string' ? document.querySelector(selector) : selector;
if (el && !el._wiredInstance) {
el._wiredInstance = new Wired(el);
}
}
static disable(selector) {
const el = typeof selector === 'string' ? document.querySelector(selector) : selector;
if (el && el._wiredInstance) {
el._wiredInstance._destroy();
delete el._wiredInstance;
}
}
static boot() {
document.querySelectorAll('[wired]').forEach(el => {
Wired.enable(el);
});
}
_getHandler(fn) {
if (typeof fn === 'function') return fn;
if (typeof fn === 'string' && typeof window[fn] === 'function') {
return window[fn];
}
return () => {};
}
_init() {
const { size, rows, cols, pointRadius } = this.options;
this._unbindEvents();
this.container.innerHTML = '';
this.container.classList.add('wired-container');
this.container.style.width = size + 'px';
this.container.style.height = size + 'px';
this.container.style.position = 'relative';
const margin = size / (Math.max(rows, cols) + 1);
this.points = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const x = (c + 1) * margin;
const y = (r + 1) * margin;
const div = document.createElement('div');
div.classList.add('wired-dot');
div.style.left = `${x}px`;
div.style.top = `${y}px`;
div.dataset.index = r * cols + c;
this.container.appendChild(div);
this.points.push({ x, y, el: div, index: r * cols + c });
}
}
this._svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
this._svg.setAttribute('width', size);
this._svg.setAttribute('height', size);
this._svg.classList.add('wired-svg');
this.container.appendChild(this._svg);
// 保存事件处理函数引用,方便解绑
this._startHandler = this._start.bind(this);
this._moveHandler = this._move.bind(this);
this._endHandler = this._end.bind(this);
this.container.addEventListener('mousedown', this._startHandler);
this.container.addEventListener('mousemove', this._moveHandler);
this.container.addEventListener('mouseup', this._endHandler);
this.container.addEventListener('touchstart', this._startHandler, { passive: false });
this.container.addEventListener('touchmove', this._moveHandler, { passive: false });
this.container.addEventListener('touchend', this._endHandler, { passive: false });
}
_unbindEvents() {
if (this._startHandler) {
this.container.removeEventListener('mousedown', this._startHandler);
this.container.removeEventListener('mousemove', this._moveHandler);
this.container.removeEventListener('mouseup', this._endHandler);
this.container.removeEventListener('touchstart', this._startHandler);
this.container.removeEventListener('touchmove', this._moveHandler);
this.container.removeEventListener('touchend', this._endHandler);
}
}
_start(e) {
e.preventDefault();
this.mouseDown = true;
this.selected = [];
this._handleInput(e);
}
_move(e) {
if (!this.mouseDown) return;
this._handleInput(e);
}
_end() {
this.mouseDown = false;
this._render();
let pwd = this.selected.map(p => p.index).join('');
if (this.md5Enabled) {
pwd = md5(pwd);
}
const correct = this.options.correctpassword;
const eventDetail = { detail: pwd };
if (correct !== null) {
if (pwd === correct) {
this.successHandler(pwd);
this.container.dispatchEvent(new CustomEvent('unlock-success', eventDetail));
} else {
this.failHandler(pwd);
this.container.dispatchEvent(new CustomEvent('unlock-fail', eventDetail));
}
}
this._resetAfterDelay();
}
_handleInput(e) {
const rect = this.container.getBoundingClientRect();
const x = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left;
const y = (e.touches ? e.touches[0].clientY : e.clientY) - rect.top;
const radius = this.options.pointRadius;
for (const p of this.points) {
if (this.selected.includes(p)) continue;
if (Math.hypot(p.x - x, p.y - y) < radius) {
this.selected.push(p);
break;
}
}
this._render(x, y);
}
_render(currentX, currentY) {
this.points.forEach(p =>
p.el.classList.toggle('wired-dot-selected', this.selected.includes(p))
);
this._svg.innerHTML = '';
if (this.selected.length === 0) return;
const ns = "http://www.w3.org/2000/svg";
const polyline = document.createElementNS(ns, 'polyline');
const pointsArr = this.selected.map(p => [p.x, p.y]);
if (
this.mouseDown &&
currentX !== undefined &&
currentY !== undefined &&
this.selected.length > 0
) {
const last = this.selected[this.selected.length - 1];
const dx = currentX - last.x;
const dy = currentY - last.y;
const dist = Math.hypot(dx, dy);
if (dist > this.options.pointRadius) {
pointsArr.push([currentX, currentY]);
}
}
const pointsStr = pointsArr.map(p => `${p[0]},${p[1]}`).join(' ');
polyline.setAttribute('points', pointsStr);
polyline.setAttribute('fill', 'none');
polyline.setAttribute('stroke', this.options.lineColor);
polyline.setAttribute('stroke-width', this.options.lineWidth);
polyline.setAttribute('stroke-linecap', 'round');
polyline.setAttribute('stroke-linejoin', 'round');
if (this.options.lineDash.length > 0) {
polyline.setAttribute('stroke-dasharray', this.options.lineDash.join(','));
}
this._svg.appendChild(polyline);
}
_resetAfterDelay() {
setTimeout(() => {
this.selected = [];
this._render();
}, 500);
}
_destroy() {
this._unbindEvents();
this.container.innerHTML = '';
this.container.classList.remove('wired-container');
}
}
// 自动初始化所有含 wired 属性的元素
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => Wired.boot());
} else {
Wired.boot();
}