Key Event Listener, audio play and toggle class.
window.addEventListener('keydown', playSound)
playSound()
is a listener forkeydown
events registered usingwindow.addEventListener
.window
is the global object in a browser, or the root object of the DOM. Anddocument
stands for DOM.
const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
const key = document.querySelector(`div[data-key="${e.keyCode}"]`);
keyCode
property is the KEY to connect our buttons(<div>
s) and sounds(<audio>
s).keyCode
's value is same asASCII
code (in lowercase letter ), check keycodes here.- NOTE:
keyCode
is DEPRECATED. I'll update this in the future. data-key
is set for mapping buttons and audios to get thekeyCode
s viakeydown
event.- the whole
querySelector
expression has to be in back ticks (```). ${}
is syntactic sugar for template literals, read more aboutExpression interpolation
here
How do we prevent delay playing sound, if we keep hitting a key?
just add audio.currentTime = 0;
before audio.play();
-
use
item.classList.add('className')
to add class when key pressed. (same aselement.addClass('className')
in jQuery) -
use
transitionend
event to removeplay
class. since we want to just removetransform
property, so add a condition to skip others.
if(e.propertyName != 'transform') return;
this.classList.remove('playing'); // `event.target.classList.remove('playing');`
-
items.forEach()
instead of justforEach
, which means it's a property of an array. -
Arrow functions is ES6 syntax,
keys.forEach(key => key.addEventListener());
pointers rotate animation, get times, changing pointer positions.
transform-origin: 100%; // transform-origin: right;
transition-timing-function: cubic-bezier();
-
transform-origin
moves the origin of rotation along x-axis. check it here. -
transition-timing-function
here is for the real clock tic-tock-like effect.
setInterval(setDate, 1000);
-
the
setInterval
function runs a function passed to it every interval specified which to implement the second pointer's rotating effect. -
create
Date()
to getnow.getSeconds()
,now.getMinutes()
andnow.getHours()
. -
culculating angles of pointers
const secondDegrees = ((seconds / 60) * 360) + 90;
(the initial state of pointers are 90 degrees)
Due to there is a glitch that occurs at every 0th second and our transition is set at 0.05s. When hand transition from final state to initial state, because the number of degrees reduce, the hand makes a (reverse) anti-clockwise motion to reach the 0 degree mark, so we'll see it occurs.
To bypass it, we remove the transition
property at the specified degrees (where glitch occurs) via JavaScript.
if (secondsDegrees === 90) secondHand.style.transition = 'all 0s';
else secondHand.style.transition = 'all 0.05s';
if (minsDegrees === 90) minHand.style.transition = 'all 0s';
else minHand.style.transition = 'all 0.1s';
data-
attribute, :root
, CSS Variables definition var(--xxx)
, filter: blur()
, change
event and mousemove
event
-
dataset
property allows to custom data attributes likedata-xxx
on the element, either in HTML or in the DOM. It's a map of DOMString, one entry for each custom data attribute. -
:root
selector matches the document's root element is always the html element and it's also where we declare the variable for the base element in HTML. -
once we declare CSS Variables, then we can add it to our specific elements, like
img
below, check how to declare it here. -
CSS Variable declare syntax is
--
, just like$
in SASS.
:root {
--spacing: 10px;
}
img {
padding: var(--spacing);
}
-
CSS
filter
provides such asblur
,bightness
and so on, take a look at it here. -
NodeList v.s. Array : NodeList is NOT an Array. You can open the
proto
in dev tool and see its methods, there areforEach()
,keys()
..., and Array's prototype hasmap()
,pop()
...etc.
use dataset
to deal with suffix px
by adding data-sizing: px
as an attribute on input element.
<input type="range" name="blur" min="0" max="25" value="10" data-sizing="px">
and the get the suffix by dataset.sizing
via JS
const suffix = this.dataset.sizing || '';
and don't forget a condition with || ''
for <input type=color>
which has no px
.
document.documentElement
is the root element in JS, so we can change the global CSS variables by JS is just setProperty
to style
like so:
document.documentElement.style.setProperty('--base', '#000');
console.table()
, filter()
, map()
, sort()
, reduce()
Array.prototype.filter()
creates a new array with all elements that pass the test implemented by the provided function.
- here I learned a compact way to return a value instead of an if-statement returning
true
.
const fifteens = inventors.filter(inventor => (inventor.year >= 1500 && inventor.year < 1600));
- and I also learned about
console.table()
instead ofconsole.log()
to display result pretty.
Array.prototype.map()
creates a new array with the results of calling a provided function on every element in this array. (takes in an array, and modifies it and returns a new array)
- use
+
for concatenation in JS.
const fullNames = inventors.map(inventor => inventor.first + ' ' + inventor.last);
above code in a ES6 syntax way:
const fullNames = inventors.map(inventor => `${inventor.first} ${inventor.last}`);
see, you don't event need to use +
for concatenation!
Array.prototype.sort()
sorts the elements of an array in place and returns the array.
-
the default sort order is according to string Unicode code points.
-
sort()
also accepts the specific function that defines the sort order.
const ordered = inventors.sort((a, b) => (a.year > b.year) ? 1 : -1);
in this case, we can also write it more shortly for an ascending order just like:
const ordered = inventors.sort((a, b) => a.year - b.year);
const de = links
.map(link => link.textContent)
.filter(streetName => streetName.includes('de'));
- [NOTICE]: since
nodeList
is NOT anarray
, so we need to turn it into an array first for manipulate array methods.
const links = Array.from(document.querySelectorAll('.mw-category a'));
above code can rewrite into ES6 syntax like:
const links = [...(document.querySelectorAll('.mw-category a'))];
Array.prototype.reduce()
method applies a function against an accumulator and each value of the array(from left-to-right) to reduce it to a single value.
const transportation = data.reduce(function(obj, item) {
if(!obj[item]) {
obj[item] = 0;
}
obj[item] ++;
return obj;
}, {});
obj
is an element passed in to the reduce()
function which will gather data over each iteration. and the result is just reduced the "numbers" collection into the "total" variables. which means every time you find yourself going from a list of values to one value (reducing), then you can use this method.
const sum = [0, 1, 2, 3, 4].reduce((a, b) => a + b, 0);
console.log(sum); // 10
CSS flex
, toggle()
, includes()
, transitionend
there are bunch of articles about CSS flexbox layout, and I hightly recommend this one written by Chris Coyier on CSS-Tricks if you are new to this fearture.
Safari transitionend event.propertyName === flex */
Chrome + FF transitionend event.propertyName === flex-grow */
due to there are different words between browsers, so we use .includes()
to find the key word 'flex'
here, for matches them.
if (e.propertyName.includes('flex')) {
this.classList.toggle('open-active');
}
change
&keyup
events- Promise:
fetch()
,then()
,json()
- Array:
filter()
,map()
,push()
,join()
- Regexp:
match()
,replace()
change
can also be an event in addEventListener
for inputs, but the change
only fires when we step outside that input. so we need to tie the element up with the keyup
event as well. for better user experience.
searchInput.addEventListener('change', displayMatches);
searchInput.addEventListener('keyup', displayMatches);
Fetch API provides an interface for fetching resources(including across the network). It will seem familiar to anyone who has used XMLHttpRequest, but the new API provides a more powerful and flexible feature set.
fetch() is one of GlobalFetch API method used to start the process of fetching a resource.
fetch(input, init).then(function(response) {...});
in MDN's basic fetch example(see Examples
section) like:
var myImage = document.querySelector('.my-image');
fetch('flowers.jpg')
.then(function(response) {
if (!response.ok) return new Error(response);
return response.blob();
})
.then(function(myBlob) {
var objectURL = URL.createObjectURL(myBlob);
myImage.src = objectURL;
})
in ES6 syntax will be like:
const myImage = document.querySelector('img');
fetch('flowers.jpg')
.then(response => response.blob())
.then(myBlob => {
const objectURL = URL.createObjectURL(myblob);
myImage.src = objectURL;
});
above example shows that it use the blob()
to fetch image. and there are many other ways as well. we use json()
it this case.
Spread syntax allows an expression to be expanded in places where multiple arguments(for function calls) or multiple elements(for array literals) or multiple variables(for destructing assignment) are expected.
For function calls:
myFunction(...iterableObj);
For array literals:
[...iterableObj, 4, 5, 6]
usually, we use Function.prototype.apply
in cases like:
function myFunction(x, y, z) {}
var args = [0, 1, 2];
myFunction.apply(null, args);
but in ES6 we can now write the above as:
function myFunction(x, y, z) {}
var args = [0, 1, 2];
myFunction(...args);
const regex = new RegExp(wordToMatch, 'gi');
g
is for global and i
is for case insensitive,
wordToMatch
is our variable, then do element.match(regex)
or element.replace(regex)
.
in RegExp, the match()
executes for matching what we search, and then combine with Array.filter()
so that we can filter out all the results that we expect.
Array.prototype.some()
, Array.prototype.every()
, Array.prototype.find()
, Array.prototype.findIndex()
, Array.prototype.splice()
, Array.prototype.slice()
The some()
method tests whether some element in the array passes the test implemented by the provided function. which means that it checks at least one thing in the array matches something. just like OR
operation.
const isAdult = people.some(function(person) {
const currentYear = (new Date()).getFullYear();
if (currentYear - person.year >= 19) {
return true;
}
});
rewrite above in ES6 syntax:
const isAdult = people.some(person => (new Date()).getFullYear() - person.year >= 19);
- [NOTICE]:
getFullYear()
is a function of Date, not a property.
some()
example:
function isBiggerThan10(e) {
return e > 10;
}
console.log([2, 5, 8, 1, 4].some(isBiggerThan10)); // false
console.log([12, 5, 8, 1, 4].some(isBiggerThan10)); // true
The every()
method tests whether all elements in the array pass the test implemented by the provided function. like AND
operation.
const everyAdult = people.every(person => (new Date()).getFullYear() - person.year >= 19);
every()
example:
function isBigEnough(e) {
return e >= 10;
}
console.log([12, 5, 8, 130, 44].every(isBigEnough)); // false
console.log([12, 54, 18, 130, 44].every(isBigEnough)); // true
console.log(allAdult) // gives the value of allAdult variable
console.log({allAdult}) // gives the allAdult object itself
The find()
method returns a value of the first element in the array that satisfies the provided testing function. Otherwise undefined
is returned.
find()
is like filter but instead of returning a subset of the array it returns the first item it finds (or undefined
).
const comment = comments.find(comment => comment.id == 823423);
find()
example:
function isBigEnough(e) {
return e >= 15;
}
[12, 5, 8, 130, 44].find(isBigEnough); // 130
The findIndex()
method returns an index of the first element in the array that satisfies the provided testing function. Otherwise -1
is returned.
const index = comments.findIndex(comment => comment.id == 823423);
findIndex()
example:
function isBigEnough(e) {
return e >= 15;
};
[12, 5, 8, 130, 44].findIndex(isBigEnough); // 3 (the value 130's index of the array is 3)
find()
andfindIndex()
are the new features of ES6
The splice()
method changes the content of an array by removing existing elements and/or adding new elements.
comments.splice(index, 1); // will change content of the origin array
splice()
syntax:
array.splice(start)
array.splice(start, deleteCount)
array.splice(start, deleteCount, item1, item2, ...)
start
: index at which to start changing the array (with origin 0)- if greater than the length of the array, actual starting index will be set to the length of the array.
- if negative, will begin that many elements from the end of the array.
deleteCount
: an integer indicating the number of old array elements to remove.- if deleteCount is 0, no elements are removed. in this case, you should specify at least one new element.
- if deleteCount is omitted, deleteCount will be equal to (arr.length - start)
item1, item2, ...
: the elements to add to the array, beginning at the start index. If you don't specify any elements, splice() will only remove elements from the array.
now let's try it
slice()
starts fromindex = 2
,deleteCount = 0
and add new element15
var nums = [0, 1, 2, 3, 4, 5];
nums.splice(2,0,15);
console.log(nums); // [0, 1, 15, 2, 3, 4, 5]
slice()
starts fromindex = 2
,deleteCount = 2
and add new element15
var nums = [0, 1, 2, 3, 4, 5];
nums.splice(2,2,15);
console.log(nums); // [0, 1, 15, 4, 5]
The slice()
method returns shallow copy of a protion of an array into a new array object selected from begin to end (end NOT included). The original array will not be modified.
const newComments = [
...comments.slice(0, index),
...comments.slice(index + 1)
];
in above ...
is the ES6 spread syntax
let's take a look those two ...comments.slice()
above, what do we get for our newComments
array:
- now we know that the
index = 1
which is the element we want to delete.
slice()
syntax:
arr.slice()
arr.slice(begin)
arr.slice(begin, end)
begin
(optional): zero-based index at which to begin extraction.- as a negative index, begin indicates an offset from the end of the sequence.
slice(-2)
extracts the last two elements in the sequence.
- if begin is undefined, slice begins from index 0.
- as a negative index, begin indicates an offset from the end of the sequence.
end
(optional): zero-based index at which to end extraction. slice extracts up to but NOT including end.slice(1,4)
extracts the second element through the fourth element (elements indexed 1, 2, and 3).- as a negative index, end indicates an offset from the end of the sequence.
slice(2,-1)
extracts the third element through the second-to-last element in the sequence.
- if end is omitted, slice extracts through the end of the sequence (arr.length).
- returns a new array containing the extracted elements.
now let's try it
var nums = [0, 1, 2, 3, 4, 5];
var newNums = nums.slice(2,4);
console.log(nums); // [0, 1, 2, 3, 4, 5]
console.log(newNums); // [2, 3]
results in our tutorial:
Canvas, HSL, mouse events
Canvas
is added in HTML5, the HTML <canvas>
element can be used to draw graphics via scripting in JavaScript. It's also used by WebGL to draw hardware-accelerated 3D.
-
Implementing basic Canvas
-
in HTML
<canvas id="draw" width="800" height="800"></canvas>
-
in JavaScript
var canvas = document.getElementById('canvas'); var ctx = canvas.getContext('2d');
-
we use these:
-
Properties
ctx.lineCap
: the shape of the stroke,round
|butt
|square
.ctx.lineJoin
: determines how two connecting segments (of lines, arcs or curves) with non-zero lengths in a shape are joined together),bevel
|round
|miter
.ctx.lineWidth
: sets the thickness of lines in space units.ctx.strokeStyle
: specifies the color or style to use for the lines around shapes. The default is#000
(black).ctx.fillStyle
: specifies the color or style to use inside shapes. The default is#000
(black).
-
Methods
ctx.beginPath()
: starts a new path by emptying the list of sub-paths. Call this method when you want to create a new path.ctx.stroke()
: strokes the current or given path with the current stroke style using the non-zero winding rule. -ctx.moveTo()
: moves the starting point of a new sub-path to the (x, y) coordinates. -ctx.lineTo()
: connects the last point in the sub-path to the x, y coordinates with a straight line(but does not actually draw it).
mothereffinghsl.com website shows you the figure of HSL.
The HSL(seel the "hsl()" section) is the Hue-saturation-lightness model using the hsl()
function notation.
- H (hue): is represented as an angle of the color circle.
- value
0~360
- red = 0 = 360
- green = 120
- blue = 240
- value
- S (saturation): represented as percentages.
- value
0~1
or percentages - 100% is full saturation
- 0% is a shade of grey
- value
- L (lightness): represented as percentages.
- value
0~1
or percentages - 100% lightness is white
- 0% lightness is black
- 50% lightness is "normal"
- value
hsl(0, 100%,50%) /* red */
hsl(120,100%,50%) /* green */
hsl(240,100%,50%) /* blue */
The advantage of HSL over RGB is that it is far more intuitive: you can guess at the colors you want, and then tweak. It is also easier to create sets of matching colors (by keeping the hue the same and varying the lightness/darkness, and saturation).
in our tutorial
- how to implement a rainbow-like gredient color?
ctx.strokeStyle = `hsl(${hue}, 100%, 50%)`;
hue++;
if (hue >= 360) {
hue = 0;
}
↑↑↑ just to restore its value when it is more than 360 to 0 to re-accumulate.
- register eventListeners
let isDrawing = false;
canvas.addEventListener('mousedown', isDrawing = true); // ready to draw when mouse down
canvas.addEventListener('mousemove', draw); // drawing when mouse move
canvas.addEventListener('mouseup', () => isDrawing = false); // stop drawing when mouse up
canvas.addEventListener('mouseout', () => isDrawing = false); // stop drawing when mouse out of canvas
- defining drawing lines
ctx.beginPath();
// start from
ctx.moveTo(lastX, lastY);
// go to
ctx.lineTo(e.offsetX, e.offsetY);
ctx.stroke();
[lastX, lastY] = [e.offsetX, e.offsetY];
[NOTICE]: [lastX, lastY] = [e.offsetX, e.offsetY]
-
it must be at the bottom of "go to" section in the function, or it will have a slight problem occurs.
-
this is in the ES6 syntax to define multiple variables in one statement, it's also equals like:
lastX = e.offsetX; lastY = e.offsetY;
-
↑↑↑ this way called destructuring assignment (see "Assignment seperate from declaration" section)
-
example:
var a, b; [a, b] = [1, 2]; console.log(a); // 1 console.log(b); // 2
-
controlling line width of stroke
if (ctx.lineWidth >= 50 || ctx.lineWidth <= 1) {
direction = !direction;
}
if(direction) {
ctx.lineWidth++;
} else {
ctx.lineWidth--;
}
- drawing on mobile?
try
// dealing with touch screen
if (e.type != "mousemove") {
x = e.changedTouches[0].clientX;
y = e.changedTouches[0].clientY;
}
- Now you can change the color, the line width, clear the canvas and even go into "dynamic mode (hsl + change line width)
const checkboxes = document.querySelectorAll('.inbox input[type="checkbox"]');
checkboxes.forEach(checkbox => checkbox.addEventListener( 'click', handleCheck )); // `click` also fire when use keyboard
-
destructuring steps
- check an input a <- will be the
lastChecked
- hold shift key
- check an input b <- will be
this
- then we want to all the inputs between a and b will also be checked <-
inBetween
's inputs.checked = true
;
- check an input a <- will be the
-
after searching:
let lastChecked;
function handleCheck(e) {
let inBetween = false;
if(e.shiftKey && this.checked) {
checkboxes.forEach(checkbox => {
if(checkbox === this || checkbox === lastChecked) {
inBetween = !inBetween;
}
if(inBetween) {
checkbox.checked = true;
}
});
}
lastChecked = this;
}
-
we defines the range of
inBetween
bycheckbox === this
andcheckbox === lastChecked
-
checking all inputs, if it's one of the two inputs we checked, then flip the
inBetween = true
, and set all theinBetween = true
inputs '.checked = true
let's take a look in a pseudo-code way
let inBetween = false;
// first seleted b, then hold shiftKey and slected d
//start checking
[ ] a <- inBetween = false, it doesn't event get in the if condition
[v] b <- inBetween = true, b is the checked input 'lastChecked', and inBetween starts to flip to true
[v] c <- inBetween = true
[v] d <- inBetween = false, d is the checked input 'this', and its inBetween is fliped from true to false, then the checking ended as well.
[ ] e <- inBetween = x, it doesn't get in the if condition
I've got stuck in a long time for that iteraling to the input c, how come its inBetween
is true, seems like it doesn't match either checkbox === this
or checkbox === lastChecked
, is beacuse the inBetween
had fliped to true so that it's true when checking on input c, right ?
hope this way will help you understand much better like I did.
- problem 1: if you reload page and hold shift key directly, then select one input, the rest of inputs those are after the one you selected will be selected too.
- problem 2: if you shift-selected input b to input d, and then you unselect the input c, then again you hold shift key and select the input c, you'll get the others after input d will be selected too.
I guess the above two problems both are the same logic issues.
The figure above shows the problem 1 result I'd tried, and I think that is because in this case only has a seleted input just right equals the checkbox === lastChecked
and some how it treat the last input as the checkbox === this
, so it will iteral over all the rest of inputs (after the one we seleted), and set the inBetween = true
till the end.
Here is one of solutions I found on stack overflow: How can I shift-select multiple checkboxes like GMail?
- step 1: turn the NodeList into an Array
const checkboxes = document.querySelectorAll('.inbox input[type="checkbox"]');
const checkboxesArray = [...checkboxes]; // fixup-step-1: turn the NodeList into an Array
- step 2: when
e.shfitKey
is true, usearray.indexOf()
to get the index of seleted inputs in the array to define the range (say the range containts the start point likecheckbox === lastChecked
and end point likecheckbox === this
)
let start = checkboxesArray.indexOf(lastChecked);
let end = checkboxesArray.indexOf(this);
- step 3:
let
thecheckState
variable isfalse
, it represents the inputs in the range which are checked or not
let checkState = false;
- step 4: use
array.slice()
to take all the elements between the range and change theircheckState
to checked
checkboxesArray
.slice(Math.min(start, end), Math.max(start, end) + 1)
.forEach(input => input.checked = checkState);
- combine them all together
const checkboxes = document.querySelectorAll('.inbox input[type="checkbox"]');
const checkboxesArray = [...checkboxes]; // fixup-step-1
let checkState = false; // fixup-step-3
function handleCheck(e) {
if(!lastChecked) { lastChecked = this; } // mark the lastChecked to redefine the range
checkState = lastChecked.checked ? true : false; // checked or unchecked
if(e.shiftKey) {
// fixup-step-2
let start = checkboxesArray.indexOf(lastChecked);
let end = checkboxesArray.indexOf(this);
// fixup-step-4
checkboxesArray
.slice(Math.min(start, end), Math.max(start, end) + 1)
.forEach(input => input.checked = checkState);
if(start - end < 0) {
console.log(`from first selected input ${start} to second selected input ${end} are checked`);
} else {
console.log(`[Backforwad]form first selected input ${start} to second selected input ${end} are checked`);
}
}
lastChecked = this;
}
well then...now it seems much better, but I think there are some other tiny problems... I have not updated the code yet, just tested the fix.
video.paused
, video.currentTime
, dataset
of .data-
attribute, parseFloat()
user .querySelector
or .querySelectorAll
to get the elements we need to build up the panel for video player
const player = document.querySelector('.player');
const video = document.querySelector('.viewer');
const progress = document.querySelector('.progress');
const progressBar = document.querySelector('.progress__filled');
const toggle = document.querySelector('.toggle');
const skipButtons = document.querySelectorAll('[data-skip]');
const ranges = document.querySelectorAll('.player__slider');
-
function togglePlay()
- click the video to play
.paused
is the property ofvideo
and there is no
.playing
property live onvideo
function togglePlay() { const method = video.paused ? 'play' : 'pause'; video[method](); }
above code equals to:
video[vdeo.paused ? 'play' : 'pause']();
and
if(video.paused) { video.play(); } else { video.pause(); }
-
function updateButton()
- toggle the play button during the video plays or pauses
to change icon, in this case is change the
textContent
propertyconst icon = this.paused ? '►' : '❚ ❚'; // `this` is the `video` toggle.textContent = icon; console.log({toggle}); // log the `{toggle}` out to see where the `textContent` is
-
function skip()
- click the skip buttons to skip
the two skip buttons are:
<button data-skip="-10"></button>
and<button data-skip="25"></button>
console.log(this.dataset.skip); video.currentTime += parseFloat(this.dataset.skip);
console.log(this.dataset)
can get the information which is the value we just added asdata-skip
attribute on HTML like:then we use its
skip
property andparseFloat
into a float number to-10s
or+25s
thecurrentTime
-
function handleRangeUpdate()
- handle the two input sliders
the two input sliders are:
<input type="range" name="volume">
and<input type="range" name="playbackRate">
console.log(`${this.name}: ${this.value}`); video[this.name] = this.value;
the
name
ofthis.name
is thevolume
orplaybackRate
, just what we define thename
attributes of the inputs on HTML -
function handleProgress()
- update the progress bar when the video plays
percent
defines the width ofprogressBa
r'sflexBasis
const percent = (video.currentTime / video.duration) * 100; progressBar.style.flexBasis = `${percent}%`; console.log(percent);
-
function scrub(e)
- change the progress bar width when drag or click on it
to
console.log(e)
theMouseEvent
out and you will find theoffsetX
which is relative to the progressoffsetWidth
, use them to calculate thescrubTime
and then update the video'scurrentTime
const scrubTime = (e.offsetX / progress.offsetWidth) * video.duration; video.currentTime = scrubTime; console.log(e);
-
click the video to play
video.addEventListener('click', togglePlay);
-
toggle the play button icon when the video plays or pauses
video.addEventListener('play', updateButton); video.addEventListener('pause', updateButton);
-
update the progress bar when the video plays
video.addEventListener('timeupdate', handleProgress);
-
toggle the play butotn to play or pause
toggle.addEventListener('click', togglePlay);
-
click to skip (to
-10s
or+25s
)skipButtons.forEach(button => button.addEventListener('click', skip));
-
handle range input sliders
add
mousemove
event for updating real-time, rather than just when we let go of the buttonranges.forEach(range => range.addEventListener('change', handleRangeUpdate)); ranges.forEach(range => range.addEventListener('mousemove', handleRangeUpdate));
-
change the progress bar width when we click or drag on it
progress.addEventListener('click', scrub);
keyup
, array.push()
, array.join()
, array.includes()
Let's take a one more look the .join()
The .join()
methond joins all elements of an array (or an array-like object
) into a string.
-
syntax
If an element is undefined or null, it is converted to the empty string.
array.join() array.join(seperator)
seperator
(optional): Specifies a string to separate each element of the array. Defaults to,
.
-
example
var a = ['Wind', 'Rain', 'Fire']; a.join(); // 'Wind,Rain,Fire' a.join(', '); // 'Wind, Rain, Fire' a.join(' + '); // 'Wind + Rain + Fire' a.join(''); // 'WindRainFire' a.join(' '); // 'Wind Rain Fire'
- we can
console.log()
thee.key
out to see the name of key we pressed
console.log(e.key);
- then use
.push()
to combine key names into an array
pressed.push(e.key);
- it starts to push out the first one item in the array, if length is over the budget of
secretCode.length
letters
pressed.splice(- secretCode.length - 1, pressed.length - secretCode.length);
-
check the array to see if pressed keys matches the
secretCode
, and then add cornify effect if matched -
.join()
to turn the array into a string
if(pressed.join('').includes(secretCode)) {
console.log('DING DING!');
}
window.scrollY
, window.innerHeight
, offsetTop
use debounce function provided to avoid performance issue, just wrap the checkSlide
function into the debounce()
function
window.addEventListener('scroll', debounce(checkSlide));
if we don't do debounce, then it will too much like:
function checkSlide(e) {
sliderImages.forEach(sliderImage => {
// half way through the image
const slideInAt = (window.scrollY + window.innerHeight) - sliderImage.height / 2;
// bottom of the image
const imageBottom = sliderImage.offsetTop + sliderImage.height;
const isHalfShown = slideInAt > sliderImage.offsetTop;
const isNotScrolledPast = window.scrollY < imageBottom;
if(isHalfShown && isNotScrolledPast) {
sliderImage.classList.add('active');
} else {
sliderImage.classList.remove('active');
}
}
the .offsetTop
tells the top of image is how far from the top of the actual window
Debouncing in JavaScript is a practice used to improve browser performance. There might be some functionality in a web page which requires time-consuming computations. If such a method is invoked frequently, it might greatly affect the performance of the browser, as JavaScript is a single threaded language.
let age = 100;
let age2 = age;
age2 = 200;
let name = 'Kyle';
let name2 = name;
name2 = 'Chad';
it won't change the original one, does make sense
let players = ['Wes', 'Sarah', 'Ryan', 'Poppy'];
const team = players;
if we update the team[3]
team[3] = 'Chad';
that will also update the players[3]
too, and that is not what we want
to fix it, we take a copy instead
const team2 = players.slice();
const team3 = [].concat(players); // same way as team2
team2[3] = "Chad";
team3[3] = "Chad";
so that it won't change the original one (players)
use ES6 spread syntax
const team4 = [...players]; // just like take a copy
team4[3] = "Hello Kitty~ Meow";
or you can use Array.from()
as well
const team5 = Array.from(players); // same as team4
team5[3] = "Hello Kitty~ Meow";
think we make a copy of person
object and want to add number
property to only man
object
const person = {
name: "Tom",
age: 30
};
const man = person;
man.number = 100;
does it will also change the person
object ?
unfortunately ...yes, and that's not what we want
we can use Object.assign()
to fix this
Object.assign()
: first argument is an empty object ({}
), second is the object (person
) to fold in, third is the values we want to additionally fold in ({ number: 100 }
), it difference betweenslice()
andsplice()
in Arrays
const man2 = Object.assign({}, person, { number: 100 });
but there's a problem is the Object.assign()
only copy one level deep... so if you try:
const tom = {
name: 'Tom',
age: 30,
social: {
twitter: '@tomyes',
facebook: 'tomyes.coolman'
}
};
const tom2 = Object.assign({}, tom);
tom2.social.twitter = '@tom2_nobody';
the tom.social.twitter
is changed as well
if we need to get a clone deep (i.e. second level deep), we have to run a function and go online and find it where, it's called clone deep and that will clone every level as deep as you want. and before doing it, we might ask ourselves that is do we really need to do this?
there is some cheating way to do a clone deep by using JSON.parse(JSON.stringify())
, just pass in the tom
like:
const tom3 = JSON.parse(JSON.stringify(tom));
tom3.social.twitter = '@tom3_nobody';
so the tom.social.twitter
won't be changed
too see what's going on here, we can console.log()
...
through the JSON.stringify()
to turn the tom
object into a string
and then pass it to JSON.parse()
to construct into an object
-
THe
JSON.stringify()
methods converts a JavaScript value to a JSON string -
The
JSON.parse()
method parses a JSON string, constructing the JavaScript value or object described by the string
localStorage
, e.preventDefault()
const addItems = document.querySelector('.add-items');
const itemsList = document.querySelector('.plates');
const items = JSON.parse(localStorage.getItem('items')) || [];
// const items = [];
const items
is to check if there is something in localStorage
and then we fall back to an empty array
- the
localStorage
property allows you to access a local Storage object
function addItem(e) {
e.preventDefault();
const item = {
text: text, // or in ES6 syntax: `text,`
done: false
};
items.push(item);
populateList(items, itemsList);
// localStorage.setItem ('items', items);
localStorage.setItem ('items', JSON.stringify(items));
this.reset();
}
e.preventDefault()
-> cancels the event if it is cancelable, without stopping further propagation of the eventitems.push(item);
-> takeitem
and put it into theitems
arraythis.reset();
->this
is theform
,reset()
is the form method to clear the input
[NOTICE]
localStorage.setItem ('items', items);`
will just get string
as return
that's because browser doesn't know how to handle it so it will use toString()
method that exists on the number or the object (in this case is an array), therefore we need to do is to JSON.stringify()
it before we convert like so
localStorage.setItem ('items', JSON.stringify(items));
use populateList()
this way is much more resilient than just reaching outside the items and grabbing them the place where we will dump them
- the
populateList()
needs two things:- a list of plates to populateList:
plates = []
- don't forget to set the default
plates
as an empty array(or object), otherwise it will break up the javascript sometimes (in this case theplates
is an array)
- don't forget to set the default
- a place to put the HTML:
plateList
- a list of plates to populateList:
function populateList(plates = [], plateList) {
plateList.innerHTML = plates.map((plate, i) => {
return `
<li>
<input type="checkbox" data-index=${i} id="item${i}" ${plate.done ? 'checked' : ''}>
<label for="item${i}">${plate.text}</label>
</li>
`
}).join('');
}
here the .join('')
takes the array (which is places.map()
made) and turn into a string and then pass it to innerHTML
function toggleDone(e) {
if(!e.target.matches('input')) return;
const el = e.target;
const index = el.dataset.index;
items[index].done = !items[index].done;
localStorage.setItem ('items', JSON.stringify(items))
populateList(items, itemsList);
}
let's take look
- skip this unless it's an input
if(!e.target.matches('input')) return;
- flip-floping between true and false
items[index].done = !items[index].done;
- everytime update will mirror to the localStorage
localStorage.setItem ('items', JSON.stringify(items));
- update the actual visibility part on html
populateList(items, itemsList);
addItems.addEventListener('submit', addItem);
itemsList.addEventListener('click', toggleDone);
populateList(items, itemsList);
everytime we create an item, it calls populateList()
and rerendering the entire list again instead of just update one single line, in this case is OK on performance, but practically just update one single line by using React or other frameworks is more efficient and helpful
our mousemove
event hooked up on hero
element, and we want to do text shadow effect on its text, right in the h1
tag
const hero = document.querySelector('.hero');
const text = hero.querySelector('h1');
hero.addEventListener('mousemove', shadow);
walk
is defined to calculate the spacings between shadows, the value is more higher, the spacing is more bigger
const walk = 500; // 500px
set the width
and height
of hero
in ES6 syntax
const { offsetWidth: width, offsetHeight: height } = hero;
let { offsetX: x, offsetY: y } = e;
above code equals in this way:
const width = hero.offsetWidth;
const height = hero.offsetHeight;
let x = e.offsetX;
let y = e.offsetY;
now we console.log()
out will see that this
is .hero
and e.target
is h1
console.log(this, e.target);
calculate offset positions
if(this !== e.target) {
x = x + e.target.offsetLeft;
y = y + e.target.offsetTop;
}
const xWalk = Math.round((x / width * walk) - (walk / 2));
const yWalk = Math.round((y / height * walk) - (walk / 2));
console.log(xWalk, yWalk);
log the xWalk
and yWalk
out to see the offsets after calculating
and the CSS part, add the textShadow
effect
text.style.textShadow = `
${xWalk}px ${yWalk}px 0 rgba(255, 0, 255, 0.7),
${xWalk * -1}px ${yWalk}px 0 rgba(0, 255, 255, 0.6),
${yWalk}px ${xWalk * -1}px 0 rgba(0, 255, 0, 0.5),
${yWalk * -1}px ${xWalk}px 0 rgba(0, 0, 255, 0.4)
`;
-
the
offsetLeft
read-only property returns the number of pixels that the upper left corner of the current element is offset to the left within the.offsetParent
node. -
the
offsetTop
property read-only property returns the distance of the current element relative to the top of theoffsetParent
node.
write in just one hot line
const sortedBands = bands.sort((a, b) => strip(a) > strip(b) ? 1 : -1);
equals
if(strip(a) > strip(b)) {
return 1;
} else {
return -1;
}
by default, it will sort by alphabetical order
to strip out the specified words which are not articles
function strip(bandName) {
return bandName.replace(/^(a |the |an )/i, '').trim();
}
test it to see if it works
[NOTICE] we are only using strip()
in if statement, and we are not actually going to be modify our data (it's not neccessary to do so)
then now it's sorted by alphabetical order after strip()
the array
document.querySelector('#bands').innerHTML =
sortedBands
.map(band => `<li>${band}</li>`)
.join('');
it takes the element and sets to the innerHTML
, and that's going to return an array with commas (,
) by default, so we want to join('')
it into one big string rather than a bunch of string with with a comma in between
if without join('')
:
so we need to 'join('')' to remove commas:
don't forget to turn the nodeList into an array
const timeNodes = [...document.querySelectorAll('[data-time]')];
get the dataset.time
const seconds = timeNodes
.map(timeNode => timeNode.dataset.time)
console.log(seconds);
will be value of data-time
attributes we set on html
then we turn the values to seconds unit, and use parseFloat
to turn it to an actual number of array
const seconds = timeNodes
.map(timeNode => timeNode.dataset.time)
.map(timeCode => {
const [mins, secs] = timeCode.split(':').map(parseFloat);
return (mins * 60) + secs;
})
console.log(seconds);
The parseFloat
function parses a string argument and returns a floating point number.
finally, let's reduce
the array to get the total seconds
const seconds = timeNodes
.map(timeNode => timeNode.dataset.time)
.map(timeCode => {
const [mins, secs] = timeCode.split(':').map(parseFloat);
return (mins * 60) + secs;
})
.reduce((total, vidSeconds) => total + vidSeconds); // total seconds 17938
console.log(seconds);
use the seconds
(total seconds) variable to calculate the hours
and mins
, use Math.floor
to remove decimal point
let secondsLeft = seconds;
const hours = Math.floor(secondsLeft / 3600);
secondsLeft = secondsLeft % 3600;
const mins = Math.floor(secondsLeft / 60);
secondsLeft = secondsLeft % 60;
I add a <h1>
tag to place the result total time
const totalTime = document.querySelector('.total');
totalTime.innerHTML = `<span>Total time <b>${hours}</b>:<b>${mins}</b>:<b>${secondsLeft}</span>`;
for accessing our webcam which is must be tied to secure origin means that a website is HTTPS
, and localhost
in our tutorial is also a secure origin. we use npm
(npm install
& npm start
) to run our small server to build the page.
const video = document.querySelector('.player');
const canvas = document.querySelector('.photo');
const ctx = canvas.getContext('2d');
const strip = document.querySelector('.strip');
const snap = document.querySelector('.snap');
first of all, we need to get the real video source
function getVideo() {
navigator.mediaDevices.getUserMedia({ video: true, audio: false })
.then(localMediaStream => {
console.log(localMediaStream);
video.srcObject = localMediaStream;
video.play();
})
.catch(err => {
console.error(`OH NO!!!`, err);
});
}
the .catch
is to handle the error.
check out the HTML page and you will see that the video
's src
is a blob:http://XXX
. blob
means a raw data being piped in off this webcam right on the page.
take a frame from video (on the upper-right corner), and to paint it onto the actual canvas right on the page
function paintToCanavas() {
const width = video.videoWidth;
const height = video.videoHeight;
canvas.width = width;
canvas.height = height;
return setInterval(() => {
ctx.drawImage(video, 0, 0, width, height);
// take the pixels out
let pixels = ctx.getImageData(0, 0, width, height);
// try some effects
// pixels = redEffect(pixels);
pixels = rgbSplit(pixels);
// ctx.globalAlpha = 0.8;
// pixels = greenScreen(pixels);
// put them back
ctx.putImageData(pixels, 0, 0);
}, 16);
}
make sure the canvas width and height equals webcam's width and height to properly rendering
const width = video.videoWidth;
const height = video.videoHeight;
canvas.width = width;
canvas.height = height;
function takePhoto() {
// played the sound
snap.currentTime = 0;
snap.play();
// take the data out of the canvas
const data = canvas.toDataURL('image/jpeg');
const link = document.createElement('a');
link.href = data;
link.setAttribute('download', 'handsome');
link.innerHTML = `<img src="${data}" alt="snap shot" />`;
strip.insertBefore(link, strip.firsChild);
}
finally, basic webcam just done!
getVideo();
video.addEventListener('canplay', paintToCanvas);
window.SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const recognition = new SpeechRecognition();
recognition.interimResults = true;
-
window.SpeechRecognition
is aWeb Speech API
. -
recognition.interimResults = true;
makes sure that results are available while we are speaking
let p = document.createElement('p');
const words = document.querySelector('.words');
words.appendChild(p);
- use
document.createElement
to create a paragraph andappend
it to the.words
div
recognition.addEventListener('result', e => {
const transcript = Array.from(e.results)
.map(result => result[0])
.map(result => result.transcript)
.join('');
const poopScript = transcript.replace(/poop|poo|shit|dump/gi, '💩');
p.textContent = poopScript;
if (e.results[0].isFinal) {
p = document.createElement('p');
words.appendChild(p);
}
});
recognition.addEventListener('end', recognition.start);
recognition.start();
-
add an
eventListener
onresult
event of SpeechRecognition, in the event we will gete.results
and assign to thetranscript
variable. -
e.results
is a list NOT an array -
each
0th
element of the list is the text data we need, so we have tomap
transcript onresult[0]
-
return
transcript
andjoin
everything so that it forms a single string. -
this only works for one paragraph so we need to set
recognition.addEventListener('end', recognition.start)
again -
to avoid the
<p>
get replaced in the DOM, we need to runcreateElement
andappendChild
inside theresult
event again so that it creates a new paragraph instead.
The Geolocation.watchPosition()
method is used to register a handler function that will be called automatically each time the position of the device changes. You can also, optionally, specify an error handling callback function.
const arrow = document.querySelector('.arrow');
const speed = document.querySelector('.speed-value');
navigator.geolocation.watchPosition((data) => {
// success callback
// console.log(data);
speed.textContent = data.coords.speed;
arrow.style.transform = `rotate(${data.coords.heading}deg)`;
// error callback
}, (err) => {
console.log(err);
alert('Oh NO...you gotta allow that to happen!!');
});
function(data) {
speed.textContent = data.coords.speed;
arrow.style.transform = `rotate(${data.coords.heading}deg)`;
}
function(err) {
console.log(err);
alert('Oh NO...you gotta allow that to happen!!');
}
const triggers = document.querySelectorAll('a');
const highlight = document.createElement('span');
highlight.classList.add('highlight');
document.body.append(highlight);
function highlightLink() {
const linkCoordinates = this.getBoundingClientRect();
// console.log(this); // <a> itself
console.log(linkCoords);
const coordinates = {
width: linkCoordinates.width,
height: linkCoordinates.height,
top: linkCoordinates.top + window.scrollY,
left: linkCoordinates.left + window.scrollX
};
highlight.style.width =`${coordinates.width}px`;
highlight.style.height =`${coordinates.height}px`;
highlight.style.transform = `translate(${coordinates.left}px, ${coordinates.top}px)`;
}
triggers.forEach(a => a.addEventListener('mouseenter', highlightLink));
[NOTE] need to add window.scrollX
and window.scrollX
to prevent wrong position while scroll occured
top: linkCoords.top + window.scrollY,
left: linkCoords.left + window.scrollX
this
: every single<a>
element itself- to
console.log(linkCoords);
will get
we can see what we have here
because we don't want it "slide" in from the (X,Y) = (0,0)
of window's coordinates, so let's set it start from the first <li>
element of <nav>
const initStart = {
left: initCoord.left + window.scrollX,
top: initCoord.top + window.scrollY
};
highlight.style.transform = `translate(${initStart.left}px, ${initStart.top}px)`;
const message = new SpeechSynthesisUtterance();
let voices = [];
const voicesDropdown = document.querySelector('[name="voice"]');
const options = document.querySelectorAll('[type="range"], [name="text"]');
const speakButton = document.querySelector('#speak');
const stopButton = document.querySelector('#stop');
message.text = document.querySelector('[name="text"]').value;
function populateVoices() {
voices = this.getVoices(); // get all the voices
console.log(voices);
// select dropdown
voicesDropdown.innerHTML = voices
// only want en ver. voices
.filter(voice => voice.lang.includes('en'))
.map(voice => `<option value="${voice.name}">${voice.name} (${voice.lang})</option>`)
.join('');
}
- to
console.log(voices)
will get all the voice synthesis
voices = this.getVoices();
console.log(voices);
- for select dropdown
because we only want just english versions so we filter
array with includes()
voicesDropdown.innerHTML = voices
.filter(voice => voice.lang.includes('en'))
.map(voice => `<option value="${voice.name}">${voice.name} (${voice.lang})</option>`)
.join('');
set the voice equals the value of select option
function setVoice() {
msg.voice = voices.find(voice => voice.name === this.value);
toggle();
}
change voice while talking, and don't forget to call this function in setVoice()
and setOption()
function toggle(startOver = true) {
speechSynthesis.cancel();
if (startOver) {
speechSynthesis.speak(message);
}
}
change the value of Rate
, Pitch
options and textarea
function setOption() {
console.log(this.name, this.value);
message[this.name] = this.value;
toggle();
}
speechSynthesis.addEventListener('voiceschanged', populateVoices);
voicesDropdown.addEventListener('change', setVoice);
options.forEach(option => option.addEventListener('change', setOption));
speakButton.addEventListener('click', toggle);
stopButton.addEventListener('click', () => toggle(false));
get nav's top position related to the top of window
const nav = document.querySelector('#main');
const topOfNav = nav.offsetTop; // 320
function fixNav() {
if (window.scrollY >= topOfNav) {
document.body.style.paddingTop = `${nav.offsetHeight}px`; // 77px (nav's height)
nav.classList.add('fixed-nav');
} else {
document.body.style.paddingTop = 0;
nav.classList.remove('fixed-nav');
}
}
window.addEventListener('scroll', fixNav);
e.stopPropagation()
, capture
, once
The event.stopPropagation()
prevents further propagation of the current event in the capturing and bubbling phases.
To bubble up which means that it's triggering that events as you go up, so use e.stopPropagation();
to stop bubbling that event up.
function logText(e) {
console.log(this.classList.value);
e.stopPropagation();
}
document.body.addEventListener('click', logText);
divs.forEach(div => div.addEventListener('click', logText));
- if we don't set
e.stopPropagation();
andconsole.log(this.classList.value);
will get when we click on just the "three"<div>
refernce: here -> EventTarget.addEventListener()
capture
is a boolean that indicates that events of this type will be dispatched to the registered listener before being dispatched to any EventTarget beneath it in the DOM tree.
function logText(e) {
console.log(this.classList.value);
}
divs.forEach(div => div.addEventListener('click', logText, {
capture: false
}));
- set
capture
istrue
orfalse
without settinge.stopPropagation();
once
is a boolean indicating that the listener should be invoked at most once after being added. If it is true, the listener would be removed automatically when it is invoked.
button.addEventListener('click', () => {
console.log('Click!!!');
}, {
once: false
});
- set
once
isture
orfalse
and click multiple times
function handleEnter() {
this.classList.add('trigger-enter');
setTimeout(() => this.classList.contains('trigger-enter') && this.classList.add('trigger-enter-active'), 150);
background.classList.add('open');
const dropdown = this.querySelector('.dropdown');
const dropdownCoords = dropdown.getBoundingClientRect();
const navCoords = nav.getBoundingClientRect();
const coords = {
height: dropdownCoords.height,
width: dropdownCoords.width,
top: dropdownCoords.top - navCoords.top,
left: dropdownCoords.left - navCoords.left
};
background.style.setProperty('width', `${coords.width}px`);
background.style.setProperty('height', `${coords.height}px`);
background.style.setProperty('transform', `translate(${coords.left}px, ${coords.top}px)`);
}
- the
setTimeout()
here is if hastrigger-enter
class and it equals true then will excutethis.classList.add('trigger-enter-active')
, it will prevent the weirdtrigger-enter-active
when you hover quickly between li items
setTimeout(() => this.classList.contains('trigger-enter') && this.classList.add('trigger-enter-active'), 150);
above code use ES6 arrow function to properly inherit from it's parent instead, otherwise this
will be the window
and will throut an error
- figure out the nav's position as a initial coords
const navCoords = nav.getBoundingClientRect();
- to prevent wrong position when the nav has be pushed down or moved offset on X-asis, so
- navCoords.
top/left
top: dropdownCoords.top - navCoords.top,
left: dropdownCoords.left - navCoords.left
function handleLeave() {
this.classList.remove('trigger-enter', 'trigger-enter-active');
background.classList.remove('open');
}
triggers.forEach(trigger => trigger.addEventListener('mouseenter', handleEnter));
triggers.forEach(trigger => trigger.addEventListener('mouseleave', handleLeave));
const slider = document.querySelector('.items');
let isDown = false;
let startX;
let scrollLeft;
isDown
: mouse is clicked down or notstartX
: start point from the sliderscrollLeft
: store slider's scrollLeft when scroll occured
- set the
isDown
istrue
- add
active
class to theslider
- get the start point when mousedown
- get the previous
scrollLeft
if scrolled
slider.addEventListener('mousedown', (e) => {
isDown = true;
slider.classList.add('active');
startX = e.pageX - slider.offsetLeft;
scrollLeft = slider.scrollLeft;
});
isDown
: settrue
when mouse downstartX = e.pageX - slider.offsetLeft;
: prevent the initial start point of slider isn't start from 0 (offsetLeft
) of page (maybe there is padding or margin around the slider), so we need to subtract the value ofslider.offsetLeft
to get the real point of position
- set
isDown
back tofalse
- remove the
active
classList
slider.addEventListener('mouseleave', () => {
isDown = false;
slider.classList.remove('active');
});
- set
isDown
back tofalse
- remove the
active
classList
same as mouseleave()
slider.addEventListener('mouseup', () => {
isDown = false;
slider.classList.remove('active');
});
the important part
slider.addEventListener('mousemove', (e) => {
if(!isDown) return;
e.preventDefault();
const x = e.pageX - slider.offsetLeft;
const walk = (x - startX) * 3;
// slider.scrollLeft = walk;
slider.scrollLeft = scrollLeft - walk;
if(!isDown) return;
: stop the function from running if not mouse downe.preventDefault();
: stop the browser think that we might want to select text
small details:
const walk = (x - startX) * 3;
: how far we deviated from the initial point, and add with* 3
(px) to make slider scroll smoothly
// slider.scrollLeft = walk;
slider.scrollLeft = scrollLeft - walk;
slider.scrollLeft = walk;
seems work but still jumpy, so recalculating like
slider.scrollLeft = scrollLeft -walk;
every single time can fix this jumpy.
function handleSpeed(e) {
const y = e.pageY - this.offsetTop;
const percent = y / this.offsetHeight;
const min = 0.4;
const max = 4;
const height = Math.round(percent * 100) + '%';
const playbackRate = percent * (max - min) + min;
bar.style.height = height;
bar.textContent = playbackRate.toFixed(2) + 'x';
video.playbackRate = playbackRate;
}
y = e.pageY - this.offsetTop;
: take the offset at which the top of the parent is, and subtract the offset from the y coordinate, which gives us just how much of the bar is to be filledpercent = y / this.offsetHeight;
: calculate the height(%), divide Y by the total height of the parent,y/offsetHeight
will give us the decimal %
const min = 0.4;
const max = 4;
const height = Math.round(percent * 100) + '%';
- define the boundaries of
min
andmax
our own, and multiply by 100 and get how much % of space is to be filled byspeed-bar
.
const playbackRate = percent * (max - min) + min;
-
find the number associated with that much height and use it as playback rate. at 0 height should be 0.4, and at 100 height it should be 2.5, so we do
percent * (max - min) + min
to match it, and assig it tovideo.playbackRate
-
toFixed(2);
displays the number with 2 decimal places
let countdown;
const timerDisplay = document.querySelector('.display__time-left');
const endTime = document.querySelector('.display__end-time');
const buttons = document.querySelectorAll('[data-time]');
function timer(seconds) {
clearInterval(countdown);
const now = Date.now();
const then = now + seconds * 1000;
displayTimeLeft(seconds);
displayEndTime(then);
countdown = setInterval(() => {
const secondsLeft = Math.round((then - Date.now()) / 1000);
// check if we should stop it
if (secondsLeft < 0) {
clearInterval(countdown);
return;
}
// display it
// console.log(secondsLeft);
displayTimeLeft(secondsLeft);
}, 1000);
}
clearInterval(countdown);
: when start a timer, clear existing timers, and it always needs a variable name of asetInterval()
to stop it.- remember to
clearInterval()
the timer at the beginning of the timer function. const now = Date.now();
: will get us currenttimestamp
in millisecondsconst then = now + seconds * 1000;
:now
plus the number of seconds that you wish to run the timer for.now
is in milliseconds, butseconds
is not, so we need to multiple by 1000 to be in milliseconds as well
countdown = setInterval(() => {
const secondsLeft = Math.round((then - Date.now()) / 1000);
// check if we should stop it
if (secondsLeft < 0) {
clearInterval(countdown);
return;
}
// display it
displayTimeLeft(secondsLeft);
}, 1000);
-
setInterval()
does not run immediately, it needs 1 second to start -
[NOTICE] we can't use like:
setInterval(seconds, {
seconds—;
});
because sometimes when the browser is not active, it might pause the setInterval()
, and also pauses while scrolling in iOS.
function displayTimeLeft(seconds) {
const minutes = Math.floor(seconds / 60);
const remainderSeconds = seconds % 60;
const display = `${minutes}:${remainderSeconds < 10 ? '0' : ''}${remainderSeconds}`;
document.title = display;
timerDisplay.textContent = display;
}
- use
textContent
overinnerText
.innerText
is IE specific and does not cover all elements document.title = display;
:document.title
can be dynamically set in JS, it updates the title of the webpage(the<title>
tag on HTML) like:
function displayEndTime(timestamp) {
const end = new Date(timestamp);
const hour = end.getHours();
const adjustedHour = hour > 12 ? hour - 12 : hour;
const minutes = end.getMinutes();
endTime.textContent = `Be Back At ${adjustedHour}:${minutes < 10 ? '0' : ''}${minutes}`;
}
const adjustedHour = hour > 12 ? hour - 12 : hour;
: adjust the time format as in 12-hours instead of 24-hours
function startTimer() {
const seconds = parseInt(this.dataset.time);
timer(seconds);
}
const seconds = parseInt(this.dataset.time);
: change the value ofdata-time
attribute (dataset
) of an element into a real number (say from"20"
into20
) byparseInt()
buttons.forEach(button => button.addEventListener('click', startTimer));
document.customForm.addEventListener('submit', function(e) {
e.preventDefault();
const mins = this.minutes.value;
timer(mins * 60);
this.reset(); // clear form input value
});
- [NOTICE] we can directly select as
document.elementName
if an element has aname attribute
in the DOM of HTML (in this case isdocument.customForm
, thecustomForm
is aname attribute
of<form>
element) this.reset();
: clear form input value (this
is theform
)
const holes = document.querySelectorAll('.hole');
const scoreBoard = document.querySelector('.score');
const moles = document.querySelectorAll('.mole');
let lastHole;
let timeUp = false;
let score = 0;
function randomTime(min, max) {
return Math.round(Math.random() * (max - min) + min);
}
The Math.random(min, max)
function returns a floating-point, pseudo-random number in the range (0, 1) that is, from 0 (inclusive) up to but not including 1 (exclusive), which you can then scale to your desired range.
- the final
+ min
ensures that the minimum possible value if the difference between max and min is 0, will be the min value itself, so it basically offsets the value of max-min by the value of min itself, then we can get a valid number.
function randomHole(holes) {
const idx = Math.floor(Math.random() * holes.length);
const hole = holes[idx];
if (hole === lastHole) {
console.log("Ah that is the same one bud");
return randomHole(holes);
}
lastHole = hole;
return hole;
}
- randomly defines the hole to pop up mole.
- if the
hole === lastHole
then re-execute the function again
if (hole === lastHole) {
console.log("Ah that is the same one bud");
return randomHole(holes);
}
lastHole = hole;
function peep() {
const time = randomTime(200, 1000);
const hole = randomHole(holes);
hole.classList.add('up');
setTimeout(() => {
hole.classList.remove('up');
if(!timeUp) peep();
}, time);
}
setTimeout()
to remove theup
class if time up, otherwise keeppeep()
ing
on HTML
<button onClick="startGame()">START</button>
function startGame() {
scoreBoard.textContent = 0;
timeUp = false; // in case page reload
score = 0;
peep();
setTimeout(() => timeUp = true, 10000);
}
- define the game time which is 10 secs and set the
timeUp = true
function bonk(e) {
if(!e.isTrusted) return; // cheater
score ++;
this.classList.remove('up');
scoreBoard.textContent = score;
}
moles.forEach(mole => mole.addEventListener('click', bonk));
e.isTrusted
property which can check for fake clicks generated by javascript, in our game we need it to betrue
so that this actually came from the user's mouse input