DOM & BOM – 冷知识 (新手)
JS 无法 query select 到伪元素
参考: 使用JS控制伪元素的几种方法
JS style remove property 是 kebab-case
set property 是 camelCase
navList.style.maxHeight = `${window.innerHeight - navHeight}px`;
navList.style.overflowY = 'auto';
remove property 是 kebab-case
navList.style.removeProperty('max-height');
navList.style.removeProperty('overflow-y');
camelCase 是 remove 不掉的哦, 小心.
JS getComputedStyle property camelCase and kebab-case
window.getComputedStyle(div).getPropertyValue('padding-top')
window.getComputedStyle(div).paddingTop
注意哦, 一个是 kebab-case 一个是 camelCase.
当 getComputedStyle 遇上 inline 返回值会是 auto
通常发生在想获取 span 的 width 的时候, 可以改拿 element.offsetWidth, 或者将 element display 改成 inline-block.
querySelector Child Layer Only
参考: Stack Overflow – Using querySelectorAll to retrieve direct children
swiperContainer.querySelector<HTMLElement>('> .swiper')!;
直接写 > 是不行的. 要加一个 :scrope, 支持度还不错, 只有 ie 不支持.
const swiper = swiperContainer.querySelector<HTMLElement>(':scope > .swiper')!;
这样就可以了.
Custom Event 默认是不冒泡的
dispatch 的时候要开启, 默认是不冒泡的哦
ellipsis.dispatchEvent(new CustomEvent('ellipsisopen', { bubbles: true }));
有些 event 默认是不冒泡的
比如 focus、blur。
HTMLScriptElement text vs textContent
参考: MDN – HTMLScriptElement, Node.textContent
set 的时候, 它的表现和 textContent 是一摸一样的.
get 的时候, textContent 会把子孙的 text node 也拿出来, 但是 text 则不会. 当然 script 里面是不应该出现子孙 element 的丫.
测试代码
const script = document.createElement("script"); const text = document.createTextNode(`console.log('Hello World');`); script.appendChild(text); const comment = document.createComment("my comment"); script.appendChild(comment); const div = document.createElement("div"); div.appendChild(document.createTextNode(`Hello World`)); script.appendChild(div); console.log(script.text); // console.log('Hello World'); console.log(script.textContent); // console.log('Hello World');Hello World document.head.appendChild(script);
body.offsetWidth 不包含 vertical scrollbar width
一般的 div.offsetWidth 是包含 scrollbar width 的,div.clientWidth 则不包含。
但 body 和 html 很奇怪,它的 offsetWidth 是不包含 scrollbar width 的。
比如我的屏幕 1920px,当 scrollbar 出现后,offsetWidth 变成了 1903px。
所以呢,如果想要获取 body scrollbar width 可以用另一招:
拿 window.innerWidth 减掉 body.offsetWidth。
window.innerWidth 是 viewport width 包含了 scrollbar width,而 body.offsetWidth 则没有包含 scrollbar width。
提醒:body.offsetHeight 是有包含 horizontal scrollbar height 的哦😂,想获取 viewport height without scrollbar 通常是用 document.documentElement.clientHeight。
Input Date
原生 input type="date" 的 value format 是 yyyy-mm-dd. 其它不支持哦.
比如 input.value = '20-01-2023' 会完全被无视掉. 参考这篇
Browser Auto-fill Password
很多 browser 都有账号密码管理功能. 当网站有 input password 的时候, browser 会自动帮用户填写.
为了安全, browser 不会让 JS 获取到 autofill 的内容, autofill 时也不会触发任何 input event.
一直到用户 first interact (e.g. click, focus) 之后, JS 才可以获取到 value.
JS 获取不到 value 有时候会破坏体验, 比如 floating label.
这时我们需要一些小技巧来解决问题.
CSS selector :-webkit-autofill 或者 :autofill 可以查找出被 autofill 的 input.
虽然我们依然拿不到 value, 但至少是可以把 label 做一个 floating 了.
Keydown event keep fire
当 user 长按一个键时,keydown 和 keypress event 会一直触发。
我们可以通过 e.repeat 判断 event 是第一下 fire,还是后续的连续 fire.
window.addEventListener('keydown', e => {
console.log(e.repeat);
});
input 事件 の keydown, keypress, compositionstart, input compositionend, keyup, change
input 事件顺序和细节:
-
keydown
event.key 可以获知用户按了哪一个键。
-
keypress
keypress 只有在按字符键才会触发,如果用户按 Shift, Alt 或 Ctrl 这些键是不会触发的。
-
长按一个键,keydown 和 keypress 会连续触发,通过 event.repeat 可以知道是第一次触发还是连续触发。
-
如果用户开启中午输入法,它按键时 compositionstart 会触发,如下图
-
input 事件只有在 input value changes 时触发。
Shift, Alt 或 Ctrl 都不会触发。
但是它和 keypress 是有区别的哦,比如 backspace 键 keypress 是不触发的,因为它不是字符,
但 input 是可能会触发的,比如 'abc' backspace 变成 'ab' 那 input 会触发。
不过如果 input 是 empty string backspace 后任然是 empty string,那就不会触发了。
总之,keypress 依据键是不是字符,input 依据 value 有没有变更。
另外,keydown, keypress, compositionstart 触发时,input.value 还没有新值,直到 input 事件 input.value 才有新值。
-
compositionend 在关闭中午输入法时触发。
此时,如果我按下 1 号键,首先会触发 keydown,event.key 是 'Process'
接着触发 input 事件,再来是 compositionend 事件
-
keyup 在手指离开按键时触发。
-
在 input blur 以后如果 value 有变更那会触发 change 事件。
:tel, :mailto 不需要 target="_blank"
:tel 和 :mailto 是直接开启 App,所以不需要 new tab。
如果是 link to WhatsApp 和 Google Map 就需要 target="_blank",因为它是开启游览器 new tab 然后才 trigger 开启 App。
remove DOM === remove event listener?
body 里有一个 button
<body> <button>click</button> </body>
添加一个点击事件给 button
const button = document.querySelector('button')!;
button.addEventListener('click', () => console.log('hello world'));
1 秒后把 button 从 body 移除
window.setTimeout(() => {
document.body.removeChild(button);
}, 1000);
2 秒后 dispatch event
window.setTimeout(() => { button.dispatchEvent(new Event('click')); }, 2000);
请问:会 log 'hello world' 吗?
答案是:会!
click event target not same as mousedown or mouseup
有一个 div,里面有一个 input
<div class="search-box"> <input> </div>
监听 div 和 input 的 mousedown, mouseup, click 事件
const searchBox = document.querySelector<HTMLElement>('.search-box')!; const input = document.querySelector('input')!; input.addEventListener('click', event => { console.log('input click', event.target); }); input.addEventListener('mousedown', event => { console.log('input mousedown', event.target); }); input.addEventListener('mouseup', event => { console.log('input mouseup', event.target); }); searchBox.addEventListener('click', event => { console.log('div click', event.target); }); searchBox.addEventListener('mousedown', event => { console.log('div mousedown', event.target); }); searchBox.addEventListener('mouseup', event => { console.log('div mouseup', event.target); });
点击 input 的效果
input 先触发,然后冒泡到 div,这个很好理解。
点击 div 的效果
只有 div 触发,这个也很好理解。
长点击 input 然后移动到 div 才放开。
效果
注意看,mouseup 和 click 的 target 是 div。
长点击 div 然后移动到 div 才放开。
效果
注意看,mouseup target 是 input,但是 click 却是 div 哦。
上面这 2 个移动的例子 mousedown 和 mouseup 虽然不同,但至少是上下层关系,如果是 sibling 那 click event 将完全不会触发。
总结:
-
mousedown 和 mouseup 好理解,你鼠标在哪里 target 就是那个。
-
click 不太好理解
mousedown 和 mouseup 哪一个比较在高层,哪一个就是 click 的 target。比如上面例子
input -> div,target = div
div -> input, target 还是 div,因为 div 比 input 高层如果 mousedown 和 mouseup 不是上下层 (比如它们是 sibling),那 click event 将完全不会触发。
switch browser tab will trigger blur & focus event
一开始 focus input, blur body 正常。
接着 focus input,然后 switch browser tab。
此时会触发 blur,document.activeElement 依然是 input。
然后 switch 回来时会 trigger focus,document.activeElement 依然是 input。
分开监听事件,触发时机会不同
我们分 2 次监听 button click 事件
document.querySelector('button')!.addEventListener('click', () => { console.log('first click start'); // 1 queueMicrotask(() => { console.log('first click end'); // 2 }); requestAnimationFrame(() => console.log('first click animation')); // 5 }); document.querySelector('button')!.addEventListener('click', () => { console.log('second click start'); // 3 queueMicrotask(() => { console.log('second click end'); // 4 }); });
虽然在 callback 函数里,我们写了 queueMicrotask 延迟 console end,但是第二个 button click 事件依然后于第一个的 console end。
也就是说,虽然用户是同一时间点击,但分开监听事件的触发不是同步的,而是有间隔的。
不过 requestAnimationFrame 依然是最后执行的。
getComputedStyle transform 会得到 Matrix
参考:
Stack Overflow – How to get value translateX by javascript
Stack Overflow – Get the value of -webkit-transform of an element with jquery
有一个 CSS transform,里面有 translate,我们想用 JavaScript DOM API 拿到最终的 translateX 和 translateY 值。
transform: rotateZ(2deg) translate(70px, 20px);
使用 getComputedStyle
const h1 = document.querySelector<HTMLElement>('h1')!; const style = window.getComputedStyle(h1); console.log('transform', style.transform); // matrix(0.999391, 0.0348995, -0.0348995, 0.999391, 69.2594, 22.4308) console.log('transform', style.getPropertyValue('transform')); // matrix(0.999391, 0.0348995, -0.0348995, 0.999391, 69.2594, 22.4308)
它返回的是一个 string,里面有 6 个号码,最后 2 个便是 translateX 和 translateY。
我们可以自己 parse 这个 string,或者用 built-in 的方法 -- DOMMatrixReadOnly
const matrix = new DOMMatrixReadOnly(style.transform); console.log('matrix', matrix.e); // 2d translateX console.log('matrix', matrix.m41); // 3d translateX console.log('matrix', matrix.f); // 2d translateY console.log('matrix', matrix.m42); // 3d translateY console.log('matrix', matrix.a); // 2d scaleX console.log('matrix', matrix.m11); // 3d scaleX console.log('matrix', matrix.d); // 2d scaleY console.log('matrix', matrix.m22); // 3d scaleY
一个是 2d 一个是 3d,我没有认真研究它的区别,毕竟我没有用 3d,有兴趣的可以看上面的参考链接。
Right click 会触发 mousedown
click 只能监听 left click,要监听 right click 要监听 contextmenu 事件。
但 mousedown 却可以监听到 left click 和 right click。
那如何分辨是 left 还是 right click 呢?
用 event.button,它是一个 number,
0 代表 left click
1 代表 wheel click
2 代表 right click
button.addEventListener('mousedown', e => console.log(e.button));
HTMLElement.contains 和 closest 都算自己
document.body.contains(document.body);
body 包含 body,因为自己也算。
document.body.closest('body') === document.body;
body 往上找可以找到 body,因为自己也算。
Mouse Click or Keyboard Enter?
我们知道不仅仅是 mouse click,keyboard enter 和 space 也能触发 click 事件。
那从 PointerEvent 中,我们是否可以识别出来是真的 mouse click 还是 keyboard enter 触发的呢?
可以,判断 screenX or screenY 的值是否等于 0,等于 0 表示是 keyboard 触发的 click 事件。
<button>click me</button>
Scripts
document.querySelector('button').addEventListener('click', e => console.log(e.screenX === 0 ? 'keyboard' : 'mouse'));
效果
这招是从 Angular Material 源码里学来的。
<img> 原生 drag 功能
左边是游览器里的 <img> element,右边是 Windows 的 notepad 软件。
游览器的图片可以直接 drag and drop 到 notepad,会得出图片的 URL 地址。
这个功能有时候会妨碍我们做事件监听,比如 mousemove 事件。
一旦开始 drag,mousemove 事件就会停止发布。
若想关闭这个原生 drag img 功能,我们可以添加一个属性给 img
<img draggable="false">
这样就可以了。
Scrollable div 自动成为 tabbable
在 Chrome,假如一个 div 有 scrollbar,可以 scroll 的话,它会自动成为 tabbable。
我猜是 Chrome 的交互体验,目的是让 keyboard 操作的顺,因为假如不能 tab,那就无法操控 div 的 scrollbar,那就看不到内容了。
这个 div 没有设置 tabindex
但却可以 tab 到它。
Radio Group 原生行为
<form> <label> <input type="radio" name="gender" value="male" required> Male </label> <label> <input type="radio" name="gender" value="female" required> Female </label> <label> <input type="radio" name="gender" value="preferNotToSay"> Prefer not to say </label> </form>
有几个行为,我觉得蛮特别的:
-
tab 只能去到第一个或最后一个,中间是 tab 不进去的。
-
上下左右会直接 checked,不需要 space 确认。
- 无法 uncheck,在 checked 的 radio 上点击或者 space 都无法 uncheck 它了。
-
每当 radio checked 会触发 click 事件,用 keyboard 上下左右自动选也会触发 click 事件。
- enter 不是选择,也不会触发 click,只有 space 才是选择。(对比我们常用的 button,它就是 enter 和 space 都等于 click)
How to detect file input open and close?
参考:Stack Overflow – How to detect when cancel is clicked on file input?
要感知 file input 开启和关闭 dialog 并没有直接的方法,我们只能透过一些间接的手法。
首先,detect 打开可以透过监听 click 事件,click 触发就打开了,于此同时,它还会触发 blur 事件,click > open dialog > blur。
关闭 dialog,我们可以透过监听 focus 事件。当 dialog close 以后,它会 focus 回来 file input。
InputEvent or Event?
const input = document.querySelector('input')!;
input.addEventListener('input', event => console.log(event));
请问 event 的类型是什么?
input.addEventListener('input', event => { console.log(event instanceof InputEvent); // true });
随便测试一下,答案是 class InputEvent。
但是!查看 TypeScript,它的类型是 Event,而不是 InputEvent。难道是 TypeScript 不够 smart?
其实不是的,正在的原因是,当 input 使用 browser 自带的 autocomplete 功能时,选择以后触发的 input 时间,event 类型会是 Event 而非 InputEvent。
所以当我们 listen / dispatch input event 时一定要考虑这个情况哦,比如
inputElement.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
默认的 input event 是会冒泡和穿越 shadow dom 的。