I have been writing things with frame before, and I have never built any wheels, so I always want to write something with native JS, but my own level is limited, so I can only find the wheel made by others on the Internet, and then do my own research. This project is not original, but a case study. This article is used to record my learning summary of the project.

I. Declaration:

  • This project is all usedes6/es7Syntax is written, and packaged and compiled using a multi-page development environment. So it can serve as a front-end advance for a project that is recommended if you are new to ithere
  • The original project article is very good, the component writing process is very clear, see the original project address for details
  • Change (zhao) create (Chao) project address (with lots of comments)
  • What the original text involves is not mentioned here. This article mainly refers to something that is not mentioned in the original text
  • Before reading this article, look at the original project
  • This article is only their own understanding of the project source code, if there is any wrong, please timely point out
  • See here how to use a multi-page development environment
  • Demo address only supports mobile terminal

Ii. Knowledge points

1. The change event

First look at the introduction on MDN:

  • specificationHTML5
  • interfaceEvent
  • The bubbling Yes
  • Can cancel the No
  • The target Element
  • Default behavior undefined

From the above introduction, the change event can bubble, so you can use event proxies on form elements. Let’s look at this code:

<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <div id="handler"> <label> <input type="radio" name="mode" value="check" id="checkmode"> Verify password </label> <label> <input type="radio" Name ="mode" value="update" checked> Set password </label> </div> <script type="text/javascript"> var handler = document.querySelector("#handler"); var checkmode = document.querySelector("#checkmode") handler.addEventListener("change",function (){ console.log("success"); }) setTimeout(function (){ checkmode.checked = 'checked' },2000) </script> </body> </html>Copy the code

Make the set password radio button selected first, then make the Verify password button selected 2s later, triggering the change event, where the change event is proxyed. After two seconds the verify password radio button is selected, but the change event callback is not triggered, WTF? . Here’s what happened. Look at the description on MDN:

and when the default options are modified (via a click or keyboard event); 2. When the user completes the submission action (e.g., clicking an option in < SELECT >, selecting a date from the tag, uploading a file with the tag, etc.); 3. When the value of the tag is modified and out of focus, but not committed (e.g., after editing the value of ). .

Checkmode. checked = ‘Checked’ The change event of the verify password radio button is triggered, but it does not bubble. The change event will only bubble if the mouse or keyboard event of the radio button is triggered. So the solution is to use the click method:

The click method can be used to simulate a left mouse click on an element. Checkmode.click () When used on an element that supports the click method, the element's click event is emitted.Copy the code
2. Implement a square that ADAPTS height to width
Use margin or padding <! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <style type="text/css"> #container{  width: 400px; } #main{ width: 100%; padding-bottom: 100%; height: 0; background: red; } </style> </head> <body> <div id="container"> <div id="main"></div> </div> </body> </html>Copy the code
3. The touch event cancels the default behavior
container.addEventListener('touchstart',(evt)=>{
            evt.preventDefault();
    },{passive:false})
Copy the code

The default for passive is false. If passive=true, the listener should never call preventDefault(). If the listener still calls this function, the client ignores it and throws a console warning.

Iii. Implementation details:

1. Switch status

First picture:

Switch between the three states:

  1. Verify password status: Returns the verify password status again if the verified password is less than four digits (four circles) or does not match the set
  2. First password setting: If the verified password is less than four digits (four circles), return to the first password setting state again; otherwise, repeat password setting for the second time
  3. Set password status for the second time: If the verified password is less than four digits (four circles) or does not match the password set for the first time, the password state is set again. Otherwise, the password state is changed to verify password
import Recorder from './recorder.js'; import {defaultFunctions} from './config.js' export default class Locker extends Recorder{ static get ERR_NOT_MISMATCH(){ return "not mismath" } constructor(options){ options.check = Object.assign({},defaultFunctions.check,options.check); options.update = Object.assign({},defaultFunctions.update,options.update); /* super keyword: In the constructor of a subclass, the this keyword can only be used after super is called, otherwise an error will be reported. */ super(options); */ super(options); } async check(password){ let checked = this.options.check.checked; let res = await this.record(); if(! res.err&&password! ==res.records){ res.err = Locker.ERR_NOT_MISMATCH } checked.call(this,res); this.check(password); } async update(){ let beforeRepeat = this.options.update.beforeRepeat, afterRepeat = this.options.update.afterRepeat; let first = await this.record(); beforeRepeat.call(this,first); if(first.err){ return this.update(); } let second = await this.record(); if(! second.err&&second.records! ==first.records){ second.err = Locker.ERR_NOT_MISMATCH } afterRepeat.call(this,second); this.update(); }}Copy the code
2. ReadRecoderIn the parent classrecordmethods

Record method mainly has touchstart, Touchmove and TouchEnd three events callback functions. And record is an asynchronous operation, so call it in async/await.

  • handlerThe touchStart and TouchMove event callbacks are used to draw fixed lines, circles, and moving lines. See the notes below for details
let handler = evt => { let {clientX, clientY} = evt.changedTouches[0], {bgColor, focusColor, innerRadius, outerRadius, touchRadius} = options, touchPoint = getCanvasPoint(moveCanvas, clientX, clientY); /* The steps for drawing fixed lines, circles and moving lines are as follows: 1. Walk through nine points and find the distance from touchPoint. If it is smaller than outerRaius, then this point is the point touched by the gesture. Determine whether the password record array has any value. If so, take the last value of the array as the starting point for drawing a fixed line, and the point traversed in the first step as the end point of the fixed line 3. Delete the point traversed in the first step from this. Circles and add it to the Records array (the array used to record passwords) 4. If the length of the records array is greater than 0, the last point in the array is used as the starting point of the moving line, and the moving point is used as the end point. When drawing the moving line, the canvas should be cleared first and then redrawn. i < this.circles.length; i++){ let point = this.circles[i], x0 = point.x, y0 = point.y; if(distance(point, touchPoint) < outerRadius){ drawSolidCircle(circleCtx, bgColor, x0, y0, outerRadius); // Draw a blank solid circle drawSolidCircle(circleCtx, focusColor, x0, y0, innerRadius); // Draw a red solid circle drawHollowCircle(circleCtx, focusColor, x0, y0, outerRadius); If (record.length){let p2 = records[record.length - 1], x1 = p2.x, y1 = p2.y; drawLine(lineCtx, focusColor, x1, y1, x0, y0); } let circle = this.circles.splice(i, 1); records.push(circle[0]); break; If (record.length){let point = records[record.length - 1], x0 = point. X, y0 = point. Y, x1 = touchPoint.x, y1 = touchPoint.y; moveCtx.clearRect(0, 0, moveCanvas.width, moveCanvas.height); drawLine(moveCtx, focusColor, x0, y0, x1, y1); }}; circleCanvas.addEventListener('touchstart', handler); circleCanvas.addEventListener('touchmove', handler);Copy the code
  • doneMainly used to remove event callbacks andresolveThe result of asynchronous operations, because mainlytouchendDetermines the outcome of an asynchronous operation, so thedoneMethods are encapsulated in onepromiseIn the
let done; Touchend let PROMISE = new Promise((resolve, reject) => {done = evt => {movectx.clearRect (0, 0, movectx.clearRect (0, 0, movectx.clearRect)) moveCanvas.width, moveCanvas.height); if(! records.length) return; / / click on the blank space does not perform the following circleCanvas. RemoveEventListener (' touchstart ', handler); circleCanvas.removeEventListener('touchmove', handler); circleCanvas.removeEventListener('touchend', done); let err = records.length < options.minPoints ? Recorder.ERR_NOT_ENOUGH_POINTS : null; Let res = {err, records: records.map(o => o.pos.join(" ")).join(" ")}; resolve(res); }; circleCanvas.addEventListener('touchend', done); });Copy the code
3

Here’s an example:

As shown in the figure:

At first, the radio button was in verify password, we didn’t do any drawing, and when we clicked set password to do so, there was an extra ray from the circle we clicked at the beginning. This is because the check method is called when the password is verified at the beginning, while the Record method is called in the check method, and each record will call back to the Canvas binding event once, so that when click set password to draw, Update (record); update (Record); update (Record); update (Record);

Solutions:

Before each record method is executed, the event callback bound by the last Record method to canvas should be removed. However, how to remove the event callback bound by the last Record method in this record method? Use a closure at the bottom of the Record method and use removeEventListener in the closure to “close” the handler and done, And assign the closure to an instance property (which is an object) that removes the event callback added by the last Record.

cancel(){ this.recordingTask&&this.recordingTask.cancel(); } record(){ let { circleCanvas, moveCanvas, circleCtx, lineCtx, moveCtx, options } = this; let { focusColor, bgColor, innerRadius, outerRadius, minPoint } = options; this.cancel(); circleCanvas.addEventListener("touchstart",(evt)=>{ this.clearPath(); }); let records = []; const handler = (evt)=>{ let {clientX,clientY} = evt.touches[0], touchPoint = getCanvasPoint(circleCanvas,clientX,clientY); for(let i=0; i<this.circles.length; i++){ let point = this.circles[i]; let x0 = point.x, y0 = point.y; if(distancePoint(point,touchPoint)<outerRadius){ drawSolidCircle(circleCtx,bgColor,x0,y0,outerRadius); drawSolidCircle(circleCtx,focusColor,x0,y0,innerRadius); drawHollowCircle(circleCtx,focusColor,x0,y0,outerRadius); if(records.length){ let p2 = records[records.length-1], x1 = p2.x, y1 = p2.y; drawLine(lineCtx,focusColor,x1,y1,x0,y0); } let circle = this.circles.splice(i,1); records.push(circle[0]) break; } } if(records.length){ let point = records[records.length-1], x0 = point.x, y0 = point.y, x1 = touchPoint.x, y1 = touchPoint.y; MoveCtx. ClearRect (0, 0, moveCanvas. Width, moveCanvas height) drawLine (moveCtx focusColor, x0, y0, x1, y1)}}; circleCanvas.addEventListener('touchstart',handler); circleCanvas.addEventListener('touchmove',handler); let done; Touchend let promise = new promise (resolve=>{done = ()=>{) MoveCtx. ClearRect (0, 0, moveCanvas. Width, moveCanvas height); if(! records.length) return ; circleCanvas.removeEventListener('touchstart', handler); circleCanvas.removeEventListener('touchmove', handler); circleCanvas.removeEventListener('touchend', done); let err = records.length<minPoint? Recorder.ERR_NOT_ENOUGH_POINTS:null; let res = {err,records:records.map(item=>item.pos.join('')).join('')}; resolve(res) }; circleCanvas.addEventListener('touchend',done); }); this.recordingTask = {}; this.recordingTask.cancel = ()=>{ circleCanvas.removeEventListener('touchstart', handler); circleCanvas.removeEventListener('touchmove', handler); circleCanvas.removeEventListener('touchend', done); }; return promise }Copy the code

This. RecordingTask = {}; The purpose of the cancel method is to avoid the first record when the cancel method does not exist