UNPKG

wired-pattern-lock

Version:

A lightweight div-based gesture pattern lock library using SVG lines.

246 lines (205 loc) 7.4 kB
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(); }