WebAPI
===
## API( Application Programming Interface )
> - 簡單說就是別人寫好的function給你調
### Web API
> - 瀏覽器提供操作瀏覽器的 API ( BOM & DOM )
## DOM ( Document Object Model )
> - [W3C](https://www.w3.org/) 推薦的 WebAPI 編程街口
> - 把文檔當成對象來處理(增刪改查, 屬性操作, 事件操作), 處理後的結果返回到頁面中
> - 使 HTML 形成一顆 DOM 樹, 包含 text, elem, node
> - 要求: 文檔必須在記憶體中
> #### 創建元素
> - document.write()
> - innerHTML
> - createElement()
> #### 增
> - appendChild()
> - insertBefore()
> #### 刪
> - removeChild()
> #### 改
> - 修改元素屬性: node.attr(proprety?)
> - 修改文本內容: innerHTML, innerText, textContent
> - 修改表單元素: value, type...
> - 修改樣式: style, className
> #### 查
> - 舊方法: getElementById, getElementsByTagName, getElementByClass...
> - H5 新方法: querySelector, queySelectorAll
> - Node: parentNode, children, previousElementSibling, nextElementSibling
> #### Attr 操作
> - getAttribute
> - setAttribute
> - removeAttribute
> #### 事件操作
> - 鼠標事件
> - onclick : 點擊
> - onmouseover : 滑鼠經過
> - mouseover 的 relatedTarget 屬性掛載了從哪個 DOM 來的
> - onmouseout : 滑鼠離開
> - onmouseout 的 relatedTarget 屬性掛載了離開去到哪個 DOM
```htmlembedded=
<body>
<style>
.fa {
padding: 100px;
background: blue;
}
.son {
height: 100px;
width: 100px;
background: red;
}
</style>
<div class="fa">
<div class="son"></div>
</div>
<script>
const oFa = document.querySelector('.fa')
oFa.onmouseover = ev => console.log('onmouseover: 從', ev.relatedTarget, '離開, 進到: ', ev.target)
oFa.onmouseout = ev => console.log('onmouseout: 從', ev.target, '離開,進到', ev.relatedTarget)
</script>
</body>
```
> - 鼠標事件
> - onfocus : 獲得焦點
> - onblur : 失去焦點
> - onmousemove : 滑鼠移動
> - onmouseup : 滑鼠彈起
> - onmousedown : 滑鼠按下
### HTML Tree
> - 節點: 網頁所有內容都是節點(標籤, 屬性, 文本, 註釋...)
> - 元素: 文檔中的標籤
> - 屬性: 標籤中的屬性
```
Doctument
- <html>: Root element
- <head>: Element
- <title>: Element
- ' ': text
- ...
- <body>
- <a>: Element
- href: Attribute
- <p>: Element
- ' ': text
- ...
```
## 獲取元素
### 根據 ID 獲取
> - `document.getElementById(id)`
```javascript=
var main = document.getElementById('main');
console.log(main);
```
> #### 觀念釐清
> - 由於執行code是從上往下, 既然沒有先讀到標籤, 當然JS也無法找到該標籤而指向null
> - 所以 JS 應該是寫在下面
```htmlembedded=
<!--
-->
<head>
<meta charset="utf-8">
<script>
var main = document.getElementById('main');
console.log(main); // null
</script>
</head>
<body>
<div id='main'>123</div>
</body>
```
```htmlmixed=
<head>
<meta charset="utf-8">
</head>
<body>
<div id='main'>123</div> //
<script>
var main = document.getElementById('main');
console.log(main); <!-- <div id="main">123</div>
這不是 str, 而是對象-->
<!-- 因為console.log() 看不清楚他是不是對象
此時可以使用console.dir() 來專門輸出對象 -->
console.dir(main); <!-- div#main -->
</script>
</body>
```
> #### 查看類型
> - __proto__: 類型
```htmlmixed=
<head>
<meta charset="utf-8">
</head>
<body>
<div id='main'>123</div>
<p id='p'>456</p>
<script>
var str = new String('haha');
var main = document.getElementById('main');
var p = document.getElementById('p');
console.log(str); // __proto__: String
console.dir(main); // __proto__: HTMLDivElement
console.dir(p); // __proto__: HTMLParagraphElement
</script>
</body>
```
### 根據標籤名
`document.getElementsByTagName(name)`
> - `document.getElementsByTagName()` 返回的類型為 HTMLCollection
> - HTMLCollection
> - 動態集合
> - 偽陣列 ( 有位置下標(1 2 3), 看起來像陣列的對象 )
> - 由於他有索引跟length, 所以也可以像陣列那樣遍歷元素
> - 集合裡的元素的類型都是該標籤對應的類型 div -> HTMLDivElement
```htmlmixed=
<body>
<div id='main'>123</div>
<div>456</div>
<div>789</div>
<script>
var divs = document.getElementsByTagName('div');
console.log(divs);
// HTMLCollection(3) [div#main, div, div, main: div#main]
// 0: div#main
// __proto__: HTMLDivElement
// 1: div
// __proto__: HTMLDivElement
// 2: div
// __proto__: HTMLDivElement
// length: 3
// main: div#main
// __proto__: HTMLDivElement
// __proto__: HTMLCollection
</script>
</body>
```
> - 動態集合
> - 當瀏覽器讀取到標籤時, 集合也會動態添加
```htmlmixed=
<head>
<meta charset="utf-8">
<script>
var divs = document.getElementsByTagName('div');
console.log(divs) // HTMLCollection []
// 0: div#main
// 1: div
// 2: div
// length: 3
// main: div#main
// __proto__: HTMLCollection
console.log(divs.length); // 0
</script>
</head>
<body>
<div id='main'>123</div>
<div>456</div>
<div>789</div>
<script>
console.log(divs); // 3
console.log(divs.length) // HTMLCollection(3) [div#main, div, div, main: div#main]
</script>
</body>
<!--
HTMLCollection 一開始的確是 [], 長度也是0
展開的那些東西都是後來動態添加進去的
ps. 當 JS 找不到該標籤時, 屬性 length: 0
-->
```
> - 選取區塊內的元素
> - `getElementById` 是找到那個Id的元素
> - `getElementsByTagName` 是找到那個標籤名的所有元素集合
> - 那選指定區塊的元素就可用 `getElementById.getElementsByTagName` 來取得
```htmlmixed=
<body>
<div id='main'>
<div>456</div>
<div>789</div>
</div>
<div id='unmain'>
<div>456</div>
<div>789</div>
</div>
<script>
var main = document.getElementById('main'); // 獲取main的對象
var mainDiv = main.getElementsByTagName('div'); // 獲取main裡面的div
console.log(mainDiv); // HTMLCollection(2) [div, div]
// 對比 document 直接取, 會把全部都取下來
console.log(document.getElementsByTagName('div'));
// HTMLCollection(6) [div#main, div, div, div#unmain, div, div,
// main: div#main, unmain: div#unmain]
</script>
</body>
```
### 根據 name 獲取元素
`getElementsByName`
> - 根據 name 屬性來取
> - IE10 跟以前的
> - 會連同名的 id 一起取回來,
> - 且 Edge 跟 IE 返回的類型是 HTMLCollection 而不是 NodeList
```htmlmixed=
<body>
<div id='main'>12</div>
<div name='main'>34</div>
<script>
var divs = document.getElementsByName('main');
console.log(divs); // NodeList [div]
// 0: div
// length: 1
// __proto__: NodeList
</script>
</body>
```
### 根據 class 獲取元素
`getElementsByClassName(names)`
> - 兼容問題: IE9以後才支援
```htmlmixed=
<body>
<div id='main'>12</div>
<div name='main'>34</div>
<div class='main'>34</div>
<script>
var divs = document.getElementsByClassName('main');
console.log(divs); // HTMLCollection [div.main]
// 0: div.main
// length: 1
// __proto__: HTMLCollection
</script>
</body>
```
### 根據選擇器獲取元素
`querySelector()`
> - 只取第一個匹配到的
`querySelectorAll()`
> - 所有匹配到的
> 兼容性問題: IE8才支援
```htmlmixed=
<body>
<div id='main'>12</div>
<div name='main'>34</div>
<div class='main'>34</div>
<div class='main'>56</div>
<script>
var elem = document.querySelector('.main');
console.log(elem); // <div class='main'>34</div>
var elem2 = document.querySelector('#main');
console.log(elem2); // <div id='main'>12</div>
var elems = document.querySelectorAll('.main');
console.log(elems); // NodeList(2) [div.main, div.main]
// 0: div.main
// 1: div.main
// length: 2
// __proto__: NodeList
</script>
</body>
```
## 事件
> - 觸發響應機制
> - 事件三元素:
> - 事件源
> - 事件名稱
> - 事件處理程序
點擊前

點擊後

```htmlmixed=
<body>
<button id="butn">點我啊笨蛋</button>
<div id='divv'>點我啊笨蛋</div>
<script>
var butn = document.getElementById('butn');
var divv = document.getElementById('divv');
butn.onclick = function () {
console.log('我是button')
}
divv.onclick = function () {
console.log('我是divv')
}
// butn, divv: 事件源;
// click: 事件名稱;
// function: 事件處理程序
</script>
</body>
```
> ### 練習: 點擊按鈕 切換圖片
> - 標籤是屬於HTML的, 標籤的屬性也是屬於HTML的
> - DOM 的元素一般情況下是封裝了對應的標籤屬性
```htmlmixed=
<body>
<button id="butn"></button>
<img id="testP" src='https://api.fnkr.net/testimg/100x100'/>
<script>
var butn = document.getElementById('butn');
var testP = document.getElementById('testP');
var flag = 1; // 當flag=1時, 圖片100x100; flag=2時, 圖片100x50;
butn.onclick = function () {
if (flag===1) {
flag = 2;
// DOM屬性對應標籤屬性, 所以直接改屬性就好了
testP.src = 'https://api.fnkr.net/testimg/100x50';
} else if (flag===2) {
flag = 1;
testP.src = 'https://api.fnkr.net/testimg/100x100';
}
}
</script>
</body>
```
## 屬性操作
### 非表單元素屬性
> - href, src, title, id
```htmlmixed=
<body>
<img id="testP" src='https://api.fnkr.net/testimg/100x100'/>
<a id="goo" href="https://www.google.com" title="Google">Google</a>
<a href="https://www.google.com">沒改過的Google</a>
<script>
var testP = document.getElementById('testP');
var goo = document.getElementById('goo');
// 獲取DOM對象屬性值
console.log(testP.id, testP.src);
// testP https://api.fnkr.net/testimg/100x100
console.log(goo.id, goo.title, goo.href);
// goo Google https://www.google.com/
// 對DOM對象屬性值操作
goo.href = 'https://www.yahoo.com';
testP.src = 'https://api.fnkr.net/testimg/100x50';
</script>
</body>
```
> - className
> - DOM 對應的標籤 ==**class 屬性名為 className**==
> - 因為class是關鍵字,
> - 關鍵字不可以做為變量或屬性名
> - 雖然分類非表單元素屬性, 但這只是表示非特有,
> 亦即表單元素也會有某些屬性, 如 (id ,className)
> #### this 整理
> - 普通函數中的 this: Window
> - 構造函數中的 this: 實例對象 (創誰指誰)
> - 方法中的 this: 對象方法所屬 (誰調指誰)
> - 事件處理函數中的 this: 事件源 (誰調指誰)
> - 例如 `butn.onclick` 的 this 就指向 butn
> - `butn.className` = `this.className`
```htmlmixed=
<head>
<meta charset="utf-8">
<style>
div {
height: 100px;
width: 100px;
background: skyblue;
}
.show {
display: block;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<input id='butn' type="button"value="顯示"/>
<div id='divv' class='show'></div>
<script>
var butn = document.getElementById('butn');
var divv = document.getElementById('divv');
var flag = 1;
butn.onclick = function () {
if (flag === 1) {
flag = 2;
// divv.class='hidden'; 搞半天不叫 class ==
divv.className='hidden';
// butn.value = '隱藏'
this.value = '隱藏'; // this 指向事件源, 也就是 butn
} else if (flag === 2) {
flag = 1;
// divv.class='show';
divv.className='show';
this.value = '顯示';
}
}
</script>
</body>
```
> #### 練習: 點小圖換大圖
```htmlmixed=
<head>
<meta charset="utf-8">
<style>
.hidden {
display: none;
}
.show {
display: block;
}
</style>
</head>
<body>
<div id='photoArr'>
<img id='photo1' src='https://api.fnkr.net/testimg/30x30'/>
<img id='photo2' src='https://api.fnkr.net/testimg/30x30'/>
<img id='photo3' src='https://api.fnkr.net/testimg/30x30'/>
</div>
<img id='photo4' class='hidden' src='https://api.fnkr.net/testimg/100x100'>
<script>
var photo1 = document.getElementById('photo1');
var photo2 = document.getElementById('photo2');
var photo3 = document.getElementById('photo3');
var photo4 = document.getElementById('photo4');
photo1.onclick = function () {
photo4.src = 'https://api.fnkr.net/testimg/100x100/F8F';
photo4.className = 'show';
}
photo2.onclick = function () {
photo4.src = 'https://api.fnkr.net/testimg/100x100/E45';
photo4.className = 'show';
}
photo3.onclick = function () {
photo4.src = 'https://api.fnkr.net/testimg/100x100/A00';
photo4.className = 'show';
}
photo4.onclick = function () {
this.className = 'hidden';
}
</script>
</body>
```
> #### 關掉`<a>`的跳轉
> - `<a>` 自帶跳轉到 href, 關掉的方法就是 return false
```htmlmixed=
<body>
<a id='gooLink' href='https://www.google.com'>Google</a>
<script>
var gooLink = document.getElementById('gooLink');
gooLink.onclick = function () {
console.log('goo')
return false
}
</script>
</body>
```
> #### 練習: 點小圖換大圖2
```htmlmixed=
<body>
<div id='aImg'>
<a href='https://api.fnkr.net/testimg/100x100/F00' title='red'>
<img src='https://api.fnkr.net/testimg/30x30' />
</a>
<a href='https://api.fnkr.net/testimg/100x100/0F0' title='green'>
<img src='https://api.fnkr.net/testimg/30x30' />
</a>
<a href='https://api.fnkr.net/testimg/100x100/00F' title='blue'>
<img src='https://api.fnkr.net/testimg/30x30' />
</a>
</div>
<img id='cgPhoto' src='https://api.fnkr.net/testimg/100x100' /> <br />
<p id='figcap'>圖片描述</p>
<script>
var aImg = document.getElementById('aImg');
var photos = aImg.getElementsByTagName('a');
// console.log(photos);
// HTMLCollection(3) [a, a, a]
for (i=0; i<photos.length; i++) {
var photo = photos[i];
photo.onclick = function () { // 讓每張小圖註冊 function
// - 改圖片
// 點擊後註冊的優點: 一開始打開網頁的速度比較快, 有用到在處理就好了
var cgPhoto = document.getElementById('cgPhoto');
// cgPhoto.src = photo.href // ( 註 )
// console.log(this);
cgPhoto.src = this.href
// - 改圖片描述
var figcap = document.getElementById('figcap');
// console.dir(figcap); // 不知道要改什麼的時候, 可以用這個去查屬性名
figcap.innerHTML = this.title;
return false // - 取消跳轉
}
}
/* 註:
* 這裡使用 photo.href 會永遠顯示藍色,
* 因為這個function是在 photo 被點擊的時候才調用
* 所以被點擊前的code應該長這樣
* var aImg = document.getElementById('aImg');
var photos = aImg.getElementsByTagName('a');
for (i=0; i<photos.length; i++) {
var photo = photos[i];
photo.onclick
}
* 所以 photo 指向的當然是最後一個(藍色)
*
* 所以這裡使用的是 this.href, 因為 this 是指向事件源(按誰指誰)
*/
</script>
</body>
```
> #### innerHTML vs innerText vs textContent
> - innerHTML 跟 innerText 獲取開始標籤跟結束標籤之間的內容
> - innerHTML
> - 如果內容有標籤, 會擷取所有標籤內容
> - 設置有標籤的內容時, 會以HTML來解析
> - 這東西會將原本的東西清掉,然後解析新的字串,
> 解析完後create+append,最後渲染
> - 這個效率比直接操作 DOM 的效率高
> - 因為這是由瀏覽器自己解析完成了
> - DOM 是由客戶端的 JS 搞定的
> - innerText
> - 如果內容有標籤, 會過濾標籤, 且會將前後換行及空白都過濾掉
> - 設置有標籤的內容時, 會將標籤轉成HTML轉義符, 將標籤顯示出來
> - textContent
> - 如果內容有標籤, 會過濾標籤, 但會留換行與空白
> - 選擇障礙: 要操作inner時, 要用哪個?
> - 如果有需要標籤, 就用innerHTML
> - 如果沒有用標籤, 那就用 innerText(textContent),
> 因為innerHTML有個解析過程, 效率比較低
```htmlmixed=
<body>
<div id='box'>
5566 不能亡
<span> 那一年 默默無言 </div>
</div>
<script>
var box = document.getElementById('box');
console.dir(box);
// innerHTML: "↵ 5566 不能亡 ↵ <span> 那一年 默默無言 </span>"
// innerText: "5566 不能亡 那一年 默默無言"
// textContent: "↵ 5566 不能亡 ↵ 那一年 默默無言 "
// box.innerHTML = "<a href='https://www.google.com'>Google</a>";
// 寫是一個Google連結
box.innerText = "<a href='https://www.google.com'>Google</a>";
// 顯示這個str完整內容
</script>
</body>
```
> #### innerText 與 textContent 兼容問題處理
> - 兩個都支持: Google, 新版Firefox, 新版IE(IE9+)
> - 只支持 innerText: 舊版IE(IE8-)
> - 只支持 textContent: 舊版Firefox
```htmlmixed=
<body>
<div id='box'>
5566 不能亡
<span> 那一年 默默無言 </div>
</div>
<script>
var box = document.getElementById('box');
console.log(getInnerText(box));
// 運用:
// 屬性不存在時, 返回undefined
console.log(typeof box.a); // undefined
// 屬性存在時, 返回屬性類型
console.log(typeof box.id); // string
function getInnerText(element) {
if (typeof element.innerText == 'string') { // 返回string 表示有支持, 那就返回
return element.innerText
} else {
return element.textContent
}
}
</script>
</body>
```
### 表單元素屬性
> - 不只一個值: value, type,
> - 只有一個值: disabled(禁用), checked(默認控件), selected(下拉選單)
> - 如果只有一個值, DOM 對應的元素屬性值為 Boolean
```htmlmixed=
<body>
<input type='button' id='butn' value='封印'/>
<input type='text' id='txt'/>
<input type='button' id='butn5566' value='5566'/>
<script>
var butn = document.getElementById('butn');
var flag = 1;
butn.onclick = function () {
var txt = document.getElementById('txt');
console.dir(txt.disabled)
if (flag === 1) {
// txt.disabled = 'disabled'; 這也能用, 只是DOM既然是Boolean, 那就用Boolaen
txt.disabled = true;
butn.value = '封印解除'
flag = 2;
} else if (flag === 2) {
flag = 1;
// txt.disabled = '';
txt.disabled = false;
butn.value = '封印'
}
}
var butn5566 = document.getElementById('butn5566');
butn5566.onclick = function () {
txt.value = '5566不能亡';
}
</script>
</body>
```
> #### 練習
> - 1. 解析完頁面後就依序添加數字到各個文本框中
> - 2. 輸出所有文本框中的值, 以 ` | ` 隔開
```htmlmixed=
<body>
<input type='text' /><br />
<input type='text' /><br />
<input type='text' /><br />
<input type='text' /><br />
<input type='text' /><br />
<input type='text' /><br />
<input type='text' /><br />
<input type='text' /><br />
<input type='text' /><br />
<input type='button' id='butn' />
<script>
var texts = document.getElementsByTagName('input');
for (i=0; i<texts.length; i++) {
if (texts[i].type === 'text') { // 因為有個input是button, 要篩掉
texts[i].value = i
}
}
var butn = document.getElementById('butn');
butn.onclick = function () {
/* 用拼接的如果太大量, 會有效能問題
var outPut = '';
for (i=0; i<texts.length; i++) {
if (texts[i].type === 'text') {
outPut += texts[i].value + ' | ';
}
}
console.log(outPut);
outPut = outPut.substr(0, outPut.length-3);
console.log(outPut);
*/
// 用陣列快很多, 因為他是直接在後面添加, 不用重新開空間
var arr = [];
for (i=0; i<texts.length; i++) {
if (texts[i].type === 'text') {
arr.push(texts[i].value);
}
}
console.log(arr);
str = arr.join(' | ');
console.log(str);
}
</script>
</body>
```
> #### 練習: 檢測登入
> - 帳號限制3-6
> - 密碼限制6-8
> - 按登入後檢測有問題, 紅框
```htmlmixed=
<body>
<input type='text' id='userName'/><br />
<input type='password' id='userPassword'/><br />
<input type='button' id='butn' value='login'/>
<script>
// 按鈕後才檢測, 所以找到按鈕
var butn = document.getElementById('butn');
butn.onclick = function () {
// 要檢測的東西
var userName = document.getElementById('userName');
var userPassword = document.getElementById('userPassword');
// 檢測規則
if (userName.value.length <3 || userName.value.length >6) {
userName.style = 'background: red;';
return // 改背景顏色沒用, 要終止程序才有用
} else {
userName.style = '';
}
if (userPassword.value.length <6 || userPassword.value.length >8) {
userPassword.style = 'background: red;';
return
} else {
userPassword.style = '';
}
// 檢測成功
console.log('登入');
}
</script>
</body>
```
> #### 練習: 顯示密碼
> - 點圖示會顯示密碼, 再點一次會關閉顯示密碼
> - 作法: 點擊圖片, 切換 input.type ( password <-> text )
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
.box {
border: 1px solid red;
width: 330px;
margin: 200px auto;
position: relative;
}
.box input {
width: 300px;
height: 30px;
outline: none;
}
.box label {
width: 15px;
position: absolute;
right: 46px;
top: 3px;
}
</style>
</head>
<body>
<div class='box'>
<label id='photo'><img src='https://api.fnkr.net/testimg/30x30/F00'/></label>
<input type='password' id='open'/>
</div>
<script>
var photo = document.getElementById('photo');
var flag = 1;
photo.onclick = function () {
var open = document.getElementById('open');
if (flag === 1) {
flag = 2;
open.type = 'text';
} else {
flag = 1;
open.type = 'password';
}
}
</script>
</body>
```
> #### 練習: 隨機點餐(seleected)
> - 按下按鈕, 隨機選擇吃什麼
```htmlmixed=
<body>
<input type='button' id='btnOption'/><br />
<select id='eatWhat'>
<option>薯條</option>
<option>漢堡</option>
<option>雞塊</option>
</select>
<script>
// 獲取按鈕
var btnOption = document.getElementById('btnOption');
btnOption.onclick = function () {
// 獲取選項
var eatWhat = document.getElementById('eatWhat');
var eatWhats = eatWhat.getElementsByTagName('option');
// console.log(eatWhats);
// 獲取隨機數字
var randomOption = Math.floor(Math.random() * eatWhats.length);
// console.log(randomOption);
// 改變選擇
// console.log(eatWhats[randomOption]);
eatWhats[randomOption].selected = true;
}
</script>
</body>
```
> #### 練習: 請輸入關鍵字
> - `onfocus` : 獲得焦點事件
> - `onblur` : 失去焦點事件
```htmlmixed=
<body>
<input id='txt' type='text' style='color: gray;' value='請輸入'/>
<script>
var txt = document.getElementById('txt');
// onfocus: 獲得焦點事件
txt.onfocus = function () {
// console.log('hah');
if (this.value === '請輸入') {
this.value = '';
this.style = 'color: black;'
}
}
// onblur: 失去焦點事件
txt.onblur = function () {
if (this.value.length === 0 || this.value === '請輸入') { // 避免使用者輸入'請輸入', 所以加那個 || 條件
this.value = '請輸入';
this.style = 'color: gray;'
}
}
</script>
</body>
```
> #### 練習: 全選與反選
> - 按全選紐時, 選項全部勾起來或全部不勾
> - 個別按鈕如果全部都選中了, 那全選鈕就要勾起來, 有缺就不勾
> - 反選鈕就反選個別扭的選項, 如果反選鈕把全部都勾起來, 那全選就要勾, 反之就不勾
> - ! 這有點難, 以後有空回來多做幾次
```htmlmixed=
<body>
<input type='checkbox' id='checkAll'>
<div id='checkEach'>
<input type='checkbox'/>
<input type='checkbox'/>
<input type='checkbox'/>
</div>
<input type='button' id='btn' value='反選'/>
<script>
// 取得按鈕們的控制
var checkAll = document.getElementById('checkAll');
var checkEach = document.getElementById('checkEach');
var inputs = checkEach.getElementsByTagName('input');
// 1. 把選項們的按鈕來出來跟全選紐連結,
// 按全選時, 子按鈕要全部勾起來,
// 再按一次, 子按鈕就不勾
checkAll.onclick = function () {
// 1-1 先取得子按鈕們
for (i=0; i<inputs.length; i++) {
var input = inputs[i]
// 為了避免盒子裡面的inputs不只checkbox, 所以要特別篩選
if (input.type === 'checkbox') {
// 1-2 連結全選與子按鈕
input.checked = this.checked;
}
}
}
// 2. 按個別扭時要判斷有沒有全選, 如果有,
// 那全選紐要勾起來, 反之如果有缺, 那全選鈕就不能勾
// 2-1 註冊按鈕
for (i=0; i<inputs.length; i++) {
var input = inputs[i];
if (input.type === 'checkbox') {
input.onclick = function () {
// 2-2 按按鈕時, 判斷有沒有人沒有勾選
var isCheckAll = true; // 假設大家都有勾
for (i=0; i<inputs.length; i++) {
var input = inputs[i];
if (input.type === 'checkbox') {
// 如果有沒沒勾, 就改標記為 false
if (input.checked !== true) {
isCheckAll = false;
}
}
}
// 2-3 跑完2-2就知道有沒有人沒有勾, 那全選鈕就跟著標記就行了
checkAll.checked = isCheckAll;
}
}
}
// 3. 反選鈕, 把所有個別選項取反,
// 如果把全部都勾起來時, 全選紐要勾起來, 否則就不勾
// 3-1 取得按鈕控制
var btn = document.getElementById('btn');
btn.onclick = function () {
var isCheckAll = true;
for (i=0; i<inputs.length; i++) {
var input = inputs[i];
if (input.type === 'checkbox') {
// 3-2 對子按鈕反選
input.checked = !input.checked;
// 3-3 確認子按鈕按鈕有沒有全選
if (input.checked !== true) {
isCheckAll = false; // 沒有就標記沒有
}
}
}
// 3-4 全選鈕連結標記
checkAll.checked = isCheckAll;
}
</script>
</body>
```
> #### `select()`
> - 讓 Text Box 裡面的文字獲得選取狀態
```htmlmixed=
<body>
<input type='text' value='tmp'/>
<script src='test.js'></script>
</body>
```
```javascript=
var inp = document.querySelector('input');
// TextBox 獲得焦點時
inp.onfocus = function () {
inp.select(); // 文字選取
}
```
### 樣式屬性操作
### style
`element.style.attr = 'value';`
> - JS 的樣式採用駝峰命名, 例如 backgroundColor
> - JS 修改 style 始於行內樣式 ( 權重較高 )
> - 樣式改的比較少時, 使用 style
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
.box {
border: 1px solid red;
width: 330px;
height: 200px;
margin: 200px auto;
}
</style>
</head>
<body>
<div class='box'></div>
<script>
var div = document.querySelector('div');
div.onclick = function () {
this.style.backgroundColor = 'blue';
this.style.width = '400px';
}
</script>
</body>
```
> #### 練習: 點擊小圖後隱藏整個框
> - 常用於一些附有 x 圖示的廣告
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
.box {
margin: 100px auto;
width: 300px;
}
</style>
</head>
<body>
<div class='box'>
<img src='https://api.fnkr.net/testimg/30x30/'/>
<img src='https://api.fnkr.net/testimg/100x100/'/>
</div>
<script>
var div = document.querySelector('div');
var imgs = div.getElementsByTagName('img');
imgs[0].onclick = function () {
div.style.display = 'none'; // 點擊小框後把整個 div 隱藏
}
</script>
</body>
```
> #### 練習: 精靈圖循環
> - 使用前提: sprite 擺放的位置要有規律
> - 使用效果: 這樣就不用每個 li 個別改位置了, 非常屌的小技巧
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
* {
margin: 0;
padding: 0;
}
div {
width: 150px;
margin: 200px auto;
}
ul {
list-style: none;
}
li {
height: 20px;
width: 30px;
/* background: url(sprite.png) no-repeat*/
/* 背景應該要放精靈圖路徑, 因為打成網路筆記, 所以我沒寫*/
background-color: gray;
margin: 20px;
float: left;
}
</style>
</head>
<body>
<div class='box'>
<ul>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
<script>
var lis = document.getElementsByTagName('li');
for (i=0; i<lis.length; i++) {
// 根據sprite圖的位置去計算,
// 例如每張圖間隔y 10px, -> i * 10px
var index = i * 10;
// 每次循環拼接成出要使用的位置
// 0 0, 0 10, 0 20, 0 30,
lis[i].style.backgroundPosition = '0 -' + index + 'px';
}
</script>
</body>
```
> #### 練習: 開關燈
```htmlmixed=
<body>
<input type='button' value='開關燈'/>
<script>
var input = document.querySelector('input');
var isLight = true;
input.onclick = function () {
var body = document.querySelector('body');
if (isLight === true) {
body.style.backgroundColor = 'black';
isLight = false;
} else {
isLight = true;
body.style.backgroundColor = 'white';
}
}
</script>
</body>
```
### 練習: 鼠標經過表格欄位變色
> - `onmouseover` : 鼠標經過
> - `onmouseout` : 鼠標離開
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
table {
margin: 100px auto;
width: 600px;
}
thead {
background-color: skyblue;
}
</style>
</head>
<body>
<table>
<thead>
<tr>
<td>tead1</td>
<td>tead2</td>
<td>tead3</td>
</tr>
</thead>
<tbody>
<tr>
<td>tbody1</td>
<td>tbody2</td>
<td>tbody3</td>
</tr>
<tr>
<td>tbody4</td>
<td>tbody5</td>
<td>tbody6</td>
</tr>
<tr>
<td>tbody7</td>
<td>tbody8</td>
<td>tbody9</td>
</tr>
</tbody>
</table>
<script>
// 取得 tbody 裡面的 tr
var tr = document.querySelector('tbody').querySelectorAll('tr');
// console.log(tr);
// 註冊
for (i=0; i<tr.length; i++) {
// 鼠標經過
tr[i].onmouseover = function () {
this.style.backgroundColor = 'darkblue';
}
// 鼠標離開
tr[i].onmouseout = function () {
this.style.backgroundColor = '';
}
}
</script>
</body>
```
### className
> - 如果改比較複雜的樣式時, 可以先把css寫好, 然後觸發時添加 class
> - class 是保留字, DOM對應的 attr 是 className
> - 如果原本的標籤已經有類名, 記得觸發時要寫上該類名
> - 例如`<div class='test'>` -> `div.className = 'test test2'`
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
* {
margin: 0;
padding: 0;
}
div {
width: 300px;
margin: 100px auto;
font-size: 10px;
}
div p {
display: inline-block;
}
div input {
vertical-align: middle;
line-height: 100%;
}
/* 先寫好觸發樣式 */
.wrong {
border: 1px solid red;
}
.right {
border: 1px solid green;
}
</style>
</head>
<body>
<div>
<input type='password' id='psd'/>
<p id='pwdFont'>請輸入6-10位密碼</p>
</div>
<script>
var psd = document.getElementById('psd');
psd.onblur = function () {
var pwdFont = document.getElementById('pwdFont');
if (this.value.length < 6 || this.value.length > 10) {
this.className = 'wrong'; // 觸發時添加類
pwdFont.style.color = 'red'; // 比較一下
pwdFont.innerText = '請輸入6-10位密碼';
} else {
this.className = 'right'; // 觸發時添加類
pwdFont.style.color = 'green';
pwdFont.innerText = '輸入正確';
}
}
</script>
</body>
```
### 練習: 點擊換背景色
> - 點擊顏色, 換背景顏色
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
.father {
width: 300px;
height: 300px;
display: flex;
margin: 100px auto;
border: 1px solid red;
}
.red {
background-color: red;
flex: 1;
}
.blue {
background-color: blue;
flex: 1;
}
.green {
background-color: green;
flex: 1;
}
</style>
</head>
<body>
<nav class='father'>
<div class='red'></div>
<div class='blue'></div>
<div class='green'></div>
</nav>
<script>
var divs = document.getElementsByTagName('div');
for (i=0; i<divs.length; i++) {
divs[i].onclick = function () {
// console.log(this.className);
var body = document.querySelector('body');
body.className = this.className;
}
}
</script>
</body>
```
### 自定義屬性操作
> - 有些數據可以設置到頁面而不用保存到數據庫
> - `element.getAttribute('attr')` : 取得屬性
> - vs. 獲取內置屬性 `element.property`
> - `element.setAttribute('attr', 'value')` : 設置屬性
> - vs. 設置內置屬性 `element.property = 'value'`
> - `element.removeAttribute('attr')` : 刪除屬性
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
.grayBorder {
border: 1px solid gray;
}
</style>
</head>
<body>
<div id='box' apple='15' banana='10'>Test</div>
<script>
// 取得屬性
var box = document.getElementById('box');
console.log(box.id); // box
// 直接取是取不到的
console.log(box.apple); // undefined
console.log(box.banana); // undefined
// 自定義屬性存在 getAttribute 裡
console.log(box.getAttribute('apple')); // 15
console.log(box.getAttribute('banana')); // 10
// 新增屬性
box.setAttribute('cat', '100');
console.log(box.getAttribute('cat')); // 100
box.setAttribute('class', 'grayBorder'); // 這裡是 class 不是 className
// 修改屬性
box.setAttribute('apple', '150');
console.log(box.getAttribute('apple')); // 150
// 刪除屬性
box.removeAttribute('apple');
console.log(box.getAttribute('apple')); // null
</script>
</body>
```
### 排他思想
> - 先清除所有, 再設置當前
```htmlmixed=
<body>
<input type='button'/>
<input type='button'/>
<input type='button'/>
<input type='button'/>
<script>
var btns = document.getElementsByTagName('input');
for (i=0; i<btns.length; i++) {
btns[i].onclick = function () {
// 按下按鍵後要設值顏色之前, 要先把所有人的顏色清空, 否則上次onclick的狀態還會在
for (i=0; i<btns.length; i++) {
btns[i].style.backgroundColor = '';
}
this.style.backgroundColor = 'red';
}
}
</script>
</body>
```
### 點擊切換欄位
> - 最難的點: 如何取得被點擊者的位置
> - 解: `this.setAttribute` 賦予位置, 用 `this.getAttribute` 取得
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
/* 初始化 */
* {
margin: 0;
padding: 0;
}
ul {
list-style: none;
}
.clearfl:before,
.clearfl:after {
content: '';
display: table;
}
.clearfl:after {
clear: both;
}
/* 盒子基本設置 */
.box {
width: 600px;
margin: 100px auto;
}
.title {
background: skyblue;
}
li {
float: left;
width: 200px;
}
/* 盒子變化設置 */
.cur {
background: red;
}
.main div {
display: none;
}
</style>
</head>
<body>
<div class='box'>
<div class='title clearfl'>
<ul>
<li class='cur'>li1</li>
<li>li2</li>
<li>li3</li>
</ul>
</div>
<div class='main'>
<div style='display: block;'>choice1</div>
<div>choice2</div>
<div>choice3</div>
</div>
</div>
<script>
// 拿到 tab 欄位控制
var lis = document.querySelector('.title').querySelectorAll('li');
for (i=0; i<lis.length; i++) {
// 最重要的一步, 自定義位置
// 必須知道我點了哪個 tab, 所以為每個 tab 標註位置
lis[i].setAttribute('index', i);
lis[i].onclick = function () {
var divs = document.querySelector('.main').querySelectorAll('div');
// 清除樣式 ( 排他 )
for (i=0; i<lis.length; i++) {
lis[i].className = '';
divs[i].style = '';
}
// 賦予被點擊者樣式
this.className = 'cur';
// 拿到被點擊者位置
var index = this.getAttribute('index');
// 賦予對應的盒子顯示
divs[index].style.display = 'block';
}
}
</script>
</body>
```
### H5 自定義屬性
> - H5 規定自定義屬性以 `data-` 開頭, 以區別內置屬性
> - 例如 `elem.setAttribute('data-index', 0);`
> - H5 新增 dataset 的 proprety 來獲取 data- 開頭的自定義屬性
> - 如果自定義屬性有多個-連接, dataset 採駝峰命名
> - dataset 在 IE 11以上才支援
```htmlmixed=
<body>
<div index='0'></div>
<script>
var div = document.querySelector('div');
// 問題: 難以直接看出他是內置還是自定義
console.log(div.index); // undefined
console.log(div.getAttribute('index')); // 0
// H5 規定 data- 開頭
div.setAttribute('data-index', 0);
console.log(div.getAttribute('data-index')); // 0
// H5 新增的獲取自定義屬性 dataset
// dataset 就是 data- 開頭的自定義屬性集合
console.log(div.dataset); // DOMStringMap {index: "0"}
console.log(div.dataset.index); // 0
console.log(div.dataset['index']); // 0
// 如果自定義屬性有多個-連接, dateset獲取時採駝峰命名
div.setAttribute('data-list-name', 'Racc');
console.log(div.dataset.listName); // Racc
</script>
</body>
```
## 節點
### 模擬 DOM 結構
> - 觀察 DOM 結構
> - 沒有任何 `undefined`, 要馬有東西, 要馬`null`, `''`, `[]`, `{}`
```htmlmixed=
<body>
<div id='tmp'>
123
<span id='tmp2'>456</span>
</div>
<script>
var tmp = document.getElementById('tmp');
var tmp2 = document.getElementById('tmp2');
console.dir(tmp);
console.dir(tmp2);
</script>
</body>
```
> - 測試 js 如果找不到屬性時, 會如何處理
```javascript=
function Node(param) {
this.className = param.className;
}
// 如果參數對象有該屬性, 就會顯示該屬性
var a = new Node({
className: 'haha',
})
console.dir(a);
// className: "haha"
// __proto__:
// constructor: ƒ Node(param)
// __proto__: Object
// 如果參數對象沒有該屬性, 則會顯示 『 undefined 』
var b = new Node({})
console.dir(b);
// className: undefined
// __proto__:
// constructor: ƒ Node(param)
// __proto__: Object
```
> - 所以構造函數必須要做一些判斷處理
```javascript=
function Node(param) {
/* 太囉唆
if (param.className) {
this.className = param.className;
} else {
this.className = '';
}
*/
// 三元運算還是太長
// this.className = param.className ? param.className : '' ;
// 讚讚
this.className = param.className || '';
// 如果有, 返回 <value>
// 如果沒, 返回 ''
}
var a = new Node({
className: 'haha',
})
console.dir(a); // className: "haha"
var b = new Node({})
console.dir(b); // className: ""
```
> - DOM 節點的一些重要基本屬性
```htmlmixed=
<body>
<div>
123
<!-- 456 -->
</div>
</body>
```
```javascript=
console.dir(div)
// nodeName: "DIV"
// nodeType: 1
// nodeValue: null
// childNodes: NodeList(1)
// 0: text
// nodeName: "#text"
// nodeType: 3
// nodeValue: "↵ 123↵ "
// 1: comment
// nodeName: "#comment"
// nodeType: 8
// nodeValue: "456"
// 2: text
// length: 3
// __proto__: NodeList
console.dir(tmp.attributes[0]);
// nodeName: "id"
// nodeType: 2
// nodeValue: "tmp"
// childNodes: NodeList(0)
// length: 0
// __proto__: NodeList
```
```javascript=
// 以設置元素節點為例, 否則太複雜了要寫很久==
function Node(param) {
// 設置 attr.default.value
// nodeName 元素節點默認為標籤名
this.nodeName = param.nodeName || '';
// nodeType 元素節點=1; 屬性節點=2; 文本節點=3
this.nodeType = param.nodeTyle || 1;
// nodeValue 元素節點的值為 null
this.nodeValue param.nodeValue || null;
// 子節點
this.childNodes = param.childNodes || [];
}
var html = new Node({
nodeName: 'html',
})
var body = new Node({
nodeName: 'body',
})
var head = new Node({
nodeName: 'head',
})
html.childNodes.push(body);
html.childNodes.push(head);
console.log(html);
// childNodes: (2) [Node, Node]
// nodeName: "html"
// nodeType: 1
// nodeValue: null
```
> - 節點至少擁有 nodeType, nodeName, nodeValue 三個基本屬性
> - 元素節點: nodeType = 1
> - 屬性節點: nodeType = 2
> - 文本節點(文字, 空格, 換行..等): nodeType = 3
> - 註釋節點(comment): nodeType = 8
> - 瀏覽器內部維護一顆DOM樹
### 父節點
`node.parentNode`
> - parentNode 可以返回某節點的父節點
> - 當指定節點沒有父節點時, 返回 null (#document 的父節點)
```htmlmixed=
<body>
<div id='grandFa'>
<div id='fa'>
<div id='son'></div>
</div>
</div>
<script>
var son = document.getElementById('son');
var fa = document.getElementById('fa');
console.log(fa); // <div id='fa'>...</div>
console.log(son.parentNode); // <div id='fa'>...</div>
console.log(son.parentNode.parentNode); // <div id='grandFa'></div>
console.log(son.parentNode.parentNode.parentNode); // <body></body>
console.log(son.parentNode.parentNode.parentNode.parentNode); // <html lang='en'></html>
console.log(son.parentNode.parentNode.parentNode.parentNode.parentNode); // #document
console.log(son.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode); // null
</script>
</body>
```
### 子節點
`node.childNodes`
> - 返回所有子節點集合, 包含元素節點, 文本節點等
> - 空行也是節點( #text )
> - 但經常情況下都在處理元素節點
> - 解決辦法1 : 利用 nodeType 來找出元素節點
> - 解決辦法2 : `node.children` (非標準)
> - 雖然是非標準, 但是所有瀏覽器都有支援
> - `hasChildNodes()` : 判斷是否有子節點
```htmlmixed=
<body>
<ul id='testUl'>
<li>test</li>
<li>test</li>
<li>test</li>
</ul>
<script>
var ul = document.getElementById('testUl');
var lis = ul.getElementsByTagName('li');
console.log(lis); // HTMLCollection(3) [li, li, li]
// childNodes 返回了所有的子節點集合, 包含元素節點, 文本節點等
console.log(ul.childNodes); // NodeList(7) [text, li, text, li, text, li, text]
console.dir(ul);
// innerHTML: "↵ <li>test</li>↵ <li>test</li>↵ <li>test</li>↵ "
// 解決辦法1. 利用 nodeType 來找到元素節點
console.log(ul.childNodes[0].nodeType); // 3
console.log(ul.childNodes[1].nodeType); // 1
var ulChildNode = [];
for (i=0; i<lis.length; i++) {
if (lis[i].nodeType === 1) {
ulChildNode.push(lis[i]);
}
}
console.log(ulChildNode); // (3) [li, li, li]
// 解決辦法2. node.children
console.log(ul.children); // HTMLCollection(3) [li, li, li]
</script>
</body>
```
> - 簡單案例
```htmlmixed=
<body>
<div id='tmp'>
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
</div>
<script>
var tmp = document.getElementById('tmp');
// 判斷有沒子節點
if (tmp.hasChildNodes()) {
for (var i = 0; i < tmp.children.length; i++) {
if (i % 2 == 0) {
tmp.children[i].style.backgroundColor = 'yellow';
} else {
tmp.children[i].style.backgroundColor = 'orange';
}
}
}
</script>
</body>
```
### 首節點與末節點
> #### 子節點
> - `node.firstChild` : 所有子節點的第一個(包含非元素節點)
> - `node.lastChild` : 所有子節點的最後一個(包含非元素節點)
> - 找無, 返回 `null`
> #### 元素子節點
> - `node.firstElementChild` : 第一個元素子節點
> - `node.firstElementChild` : 最後一個元素子節點
> - 這個有兼容性問題, IE9以上才支援
> #### children
> - `ul.children[0]` : 第一個元素子節點
> - `ul.children[ul.children.length - 1]` : 最後一個元素子節點
```htmlmixed=
<body>
<ul id='testUl'>
<li>test1</li>
<li>test2</li>
<li>test3</li>
</ul>
<script>
var ul = document.getElementById('testUl');
console.log(ul.firstChild); // #text
console.log(ul.lastChild); // #text
console.log(ul.firstElementChild); // <li>test1<li>
console.log(ul.lastElementChild); // <li>test3<li>
console.log(ul.children[0]);
console.log(ul.children[ul.children.length - 1]);
</script>
</body>
```
> - 處理兼容
```javascript=
<body>
<div id='tmp'>
<!--
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
-->
</div>
<script>
var tmp = document.getElementById('tmp');
function getFirstElementChild(elem) {
var node, nodes = elem.childNodes, i = 0;
while (node = nodes[i++]) {
if (node.nodeType === 1) {
return node
}
}
return null
}
console.log(getFirstElementChild(tmp));
</script>
</body>
```
> - 綜合小練習: 被點擊的元素`<a>`改變背景色
> - 讓 a 不跳轉的方法 href 的方法
> - `<a href='javascript:void(0);'>` 或
> `<a href='javascript:;'>` 或
> `<a href='javascript:undefined;'>`
> - 點擊`javascript:` 時, 會執行 URI 的 code, 並返回結果來代替內容,
> 除非結果是 `undefined`
> - void 是一種運算符, 返回 undefined
> - 惟每次點擊都要運算而得到 `undefined` 後不執行, MDN 不推薦此種做法
> - MDN 推薦直接綁定 onclick 來 return false, 點擊不執行
```htmlmixed=
<body>
<div id='tmp'>
<ul>
<li><a href='javascript:;'>1</a></li>
<li><a href='javascript:;'>2</a></li>
<li><a href='javascript:;'>3</a></li>
</ul>
</div>
<script>
// 先拿到最外層父元素
var tmp = document.getElementById('tmp');
// 兼容函數
function getFirstElementChild(elem) {
var node, nodes = elem.childNodes, i = 0;
while (node = nodes[i++]) {
if (node.nodeType === 1) {
return node
}
}
return null
}
// 拿到 ul
var tmpUl = getFirstElementChild(tmp);
// 拿到 ul 裡面的東西
for (var i = 0; i < tmpUl.children.length; i++) {
var li = tmpUl.children[i],
a = getFirstElementChild(li);
// 點擊時調用 aClick
// 不建議在循環內定義 function, 浪費內存, 拉到外面寫後再指向就好了
// 切記不是 aClick(), 這會返回一個返還值
// a.onclick 想要綁定的不是一個值, 是一個函數
a.onclick = aClick;
}
function aClick() {
// 排他
for (var i = 0; i < tmpUl.children.length; i++) {
// 拿到ul裡面所有li
var li = tmpUl.children[i];
// 清掉背景色
li.style.backgroundColor = '';
}
// 為被點擊的<a> 的父節點(li)上背景色
this.parentNode.style.backgroundColor = 'red';
// 被點擊的<a>不執行任何動作(MDN推薦)
return false
}
</script>
</body>
```
### 兄弟節點
> #### 子節點
> - `node.nextSibling` : 下一個節點
> - `node.previousSibling` : 上一個節點
> #### 子元素節點
> - `nextElementSibling` : 下一個兄弟元素節點
> - `previousElementSibling` : 上一個兄弟元素節點
> - 有兼容問題, IE9+
> - 如果沒有就返回 null
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
</style>
</head>
<body>
<div></div>
<nav></nav>
<script>
var div = document.querySelector('div');
// 兄弟節點
console.log(div.nextSibling); // #text
console.log(div.previousSibling); // #text
// 兄弟元素節點(IE9+)
console.log(div.nextElementSibling); // <nav></nav>
console.log(div.previousElementSibling); // null
// 兼容函數
function getElementSibling(elem) {
// 一直往下找, 找到 null 的 Boolean 就是 false 而會停止
while (elem = elem.nextSibling) {
if (elem.nodeType === 1) { // 找到元素就返回
return elem;
}
}
return null // 全部找完都沒就返回 null
}
// 兼容函數
function getPreviousElementSibling(elem) {
while (elem = elem.previousSibling) {
if (elem.nodeType === 1) {
return elem;
}
}
return null
}
console.log(getElementSibling(div)); // <nav></nav>
console.log(getPreviousElementSibling(div)); // null
</script>
</body>
```
### 創建節點與添加節點
> #### 創建節點
> `document.createElement('tagName')`
> - 動態創建元素節點
> #### 添加節點
> - `node.appendChild(child)`
> - 添加到指定父元素的子元素列表最後
> - 類似 css 的 append 的 after偽元素
> - `node.insertBefore(child, 指定元素)`;
> - 添加到指定父元素的指定子元素前面
```htmlmixed=
<body>
<ul>
<li>123</li>
</ul>
<script>
var ul = document.querySelector('ul');
// 創建節點
var li1 = document.createElement('li');
// append 添加
ul.appendChild(li1);
var li2 = document.createElement('li');
// insert 添加
ul.insertBefore(li2, ul.children[0]);
</script>
</body>
```
> #### appendChild 補充
> - 被插入的節點如果已經在文檔中, 則會先從該位置移除後插入指定位置
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
.one {
height: 100px;
background: skyblue;
}
.two {
height: 100px;
background: darkblue;
color: white;
}
</style>
</head>
<body>
<div class='one'>
<span>123</span>
<button>magic</button>
</div>
<div class='two'></div>
<script>
var btn = document.querySelector('button');
btn.addEventListener('click', function () {
var one = document.querySelector('.one');
var two = document.querySelector('.two');
two.appendChild(one.children[0]);
})
</script>
</body>
```
### 練習: 選中跳選單
<img src='https://i.imgur.com/aVxjuXW.png' style='width: 200px;'/>
<img src='https://i.imgur.com/u8AxBVL.png' style='width: 200px;'/>
<img src='https://i.imgur.com/hahdI36.png' style='width: 200px;'/>
> - `option.selected` 跟 `select.multiple` 是 Boolean 值
> - `select.multiple` : 決定是否可以多選, 默認為 false (不可多選)
> - `option.selected` : 有無選中, true 為選中
> - 還有一個BUG還沒解, 就是多次選中跳單後, 順序是亂的
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
select {
width: 100px;
height: 200px;
outline: none;
}
</style>
</head>
<body>
<select multiple='ture'>
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
</select>
<button><<</button>
<button>>></button>
<button><</button>
<button>></button>
<select multiple='true'>
</select>
<script>
var btns = document.querySelectorAll('button');
var selects = document.querySelectorAll('select');
btns[1].addEventListener('click', function() {
/* i=0, 移走1, selects[0]=[2345], length=4;
* i=1, 移走3, selects[0]=[245], length=3;
* i=2, 移走5, selects[0]=[24]. length=2;
* i=3, length=2 跳出
* 所以只移走了135
for (var i=0; i<selects[0].length; i++) {
var option = selects[0].children[i];
console.log(option);
selects[1].appendChild(option);
}
*/
/* 每次都移最後一項
* selects[0] = [12345]
* i=4, 移走5, selects[0]=[1234], length-1=3;
* i=3, 移走4, selects[0]=[123], length-1=2;
* ...
* 全部都移過去了, 可是順序反了
for (var i=selects[0].length -1; i>=0; i--) {
var option = selects[0].children[i];
selects[1].appendChild(option);
}
*/
var len = selects[0].length;
for (var i=0; i < len; i++) {
var option = selects[0].children[0];
selects[1].appendChild(option);
}
})
btns[0].addEventListener('click', function () {
var len = selects[1].length;
for (i=0; i<len; i++) {
option = selects[1].children[0];
selects[0].appendChild(option);
}
})
btns[3].addEventListener('click', function() {
var arr = [];
for (var i = 0; i<selects[0].length; i++) {
var option = selects[0].children[i]
if (option.selected) {
arr.push(option);
option.selected = false;
}
}
console.log(arr);
for (i = 0; i<arr.length; i++) {
console.log(i, arr);
/* 我全部選中的輸出結果
* 這裡是操作 arr 不是操作標籤, 所以 length 不會減少
0 (5) [option, option, option, option, option]
1 (5) [option, option, option, option, option]
2 (5) [option, option, option, option, option]
3 (5) [option, option, option, option, option]
4 (5) [option, option, option, option, option]
*/
selects[1].appendChild(arr[i]);
}
})
btns[2].addEventListener('click', function () {
arr = [];
for (i=0; i<selects[1].children.length; i++) {
if (selects[1].children[i].selected) {
arr.push(selects[1].children[i])
selects[1].children[i].selected = false;
}
}
console.log(arr);
for (i = 0 ; i< arr.length; i++) {
selects[0].appendChild(arr[i]);
}
})
</script>
</body>
```
### 練習: 超陽春留言板
> - 點擊送出時,
> - 讀取textarea的內容,
> - 創建元素,
> - 將內容附加到元素上
> - 將元素丟到留言板父元素裡
```htmlmixed=
<body>
<textarea></textarea>
<input type='button' id='btn' value='送出'>
<ul>
</ul>
<script>
var btn = document.getElementById('btn');
// 點擊送出
btn.onclick = function () {
var ul = document.querySelector('ul');
var text = document.querySelector('textarea');
// 確認有沒有打字
if (text.value.length === 0) {
alert('請打點字');
return false
} else {
// 有打字就創建元素並賦予內容
var li = document.createElement('li');
li.innerHTML = text.value;
// 想怎麼插入都行
// ul.appendChild(li);
ul.insertBefore(li, ul.children[0]);
// 清空留言框
text.value = '';
}
}
</script>
</body>
```
#### `element.insertAdjacentHTML(position, text);`
> - 將 str 解析成 HTML, 並將結果插入指定的DOM位置
> - position:
> - `'beforebegin'` : 指定元素的前面
> - `'afterend'` : 指定元素的後面
> - `'afterbegin'` : 指定元素的最初子節點的前面
> - `'beforeend'` : 指定元素的最後子節點的後面
> - vs. `appendChild(node)`
> - 不支持 str
> - 
```htmlmixed=
<body>
<div>1</div>
<script src='test.js'></script>
</body>
```
```javascript=
var div = document.querySelector('div');
var tmp = '<button>123</button>';
// 這只是要讓效果明顯的css而已
div.style.border = '1px solid blue';
div.style.width = '100px';
// 插入 tmp 到 div 裡面的最後
div.insertAdjacentHTML('beforeend', tmp);
```
### 刪除節點
`node.removeChild(child)`
> - 刪除父節點裡指定的孩子
```htmlmixed=
<body>
<input type='button' value='del'/>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
var btn = document.querySelector('input');
var ul = document.querySelector('ul');
// 每次按按鍵
btn.onclick = function () {
if (ul.children.length === 0) {
return false
} else {
// 刪除ul裡第一個li
ul.removeChild(ul.children[0]);
}
}
</script>
</body>
```
> #### `node.remove();`
> - 從 DOM 刪除元素
```htmlmixed=
<body>
<div>1</div>
<div>2</div>
<div>3</div>
<script src='test.js'></script>
</body>
```
```javascript=
var divs = document.querySelectorAll('div');
for (var i = 0; i < divs.length; i++) {
// 點擊後刪除該觸發元素
divs[i].onclick = function () {
this.remove();
}
}
```
### 練習: 刪除留言
> - 每個留言旁邊都設一個刪除鍵
> - 按下刪除鍵後刪除該留言
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
li {
width: 300px;
background: skyblue;
margin: 15px 0; /* 加大li間的大小, 否則a的高大於li的話會跑版 */
}
li a {
float: right;
}
</style>
</head>
<body>
<textarea></textarea>
<input type='button' id='btn' value='留言'/>
<ul></ul>
<script>
var btn = document.getElementById('btn');
var text = document.querySelector('textarea');
var ul = document.querySelector('ul');
btn.onclick = function () {
if (text.value.length === 0) {
alert('請打點字');
return false
} else {
var li = document.createElement('li');
// 留言增加刪除鍵
li.innerHTML = text.value + "<a href='javascript:;'>刪除</a>";
// ul.appendChild(li);
ul.insertBefore(li, ul.children[0]);
text.value = '';
// 找到留言區裡所有刪除鍵
var as = ul.querySelectorAll('a');
for (i=0; i<as.length; i++) {
// 按下刪除鍵後
as[i].onclick = function () {
// 刪除父元素(留言區)裡的子元素(留言),
// 可是按的是 a , 所以子元素(li)是 this.parentNode
ul.removeChild(this.parentNode)
}
}
}
}
</script>
</body>
```
### 複製節點
`node.cloneNode()`
> - 淺拷貝:
> - 參數為空`cloneNode()` 或為false `cloneNode(false)`
> - 只複製節點本身
> - 深拷貝:
> - 參數為true `cloneNode(true)`
> - 複製節點本身以及裡面所有的子節點
```htmlmixed=
<body>
<input type='button' value='del'/>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
var btn = document.querySelector('input');
var ul = document.querySelector('ul');
btn.onclick = function () {
var cloneLi = ul.children[0].cloneNode(true);
ul.appendChild(cloneLi);
}
</script>
</body>
```
### 練習: 動態生成表格內容
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
* {
margin: 0;
padding: 0;
}
table {
margin: 100px auto;
}
th {
width: 100px;
background: skyblue;
}
tbody td {
border: 1px solid red;
}
</style>
</head>
<body>
<table>
<!-- 表頭 -->
<thead>
<tr>
<th>Data1</th>
<th>Data2</th>
<th>Data3</th>
<th>刪除</th>
</tr>
<!-- 表身的資料動態創建 -->
</thead>
<tbody>
</tbody>
</table>
<script>
// 隨便寫幾個假資料
var data = [
{
Data1: 'test1',
Data2: 'test2',
Data3: 'test3'
}, {
Data1: 'test4',
Data2: 'test5',
Data3: 'test6'
}, {
Data1: 'test7',
Data2: 'test8',
Data3: 'test9'
}
]
// 1. 根據資料個數創建tr (幾行)
var tbody = document.querySelector('tbody');
for (var i=0; i<data.length; i++) {
var tr = document.createElement('tr');
tbody.appendChild(tr);
// 2. 根據資料項目個數創建 td (幾列)
for (var key in data[i]) {
var td = document.createElement('td');
// 2-1 寫入資料 a = {b: 1}; -> a['b'] = 1
td.innerHTML = data[i][key];
tr.appendChild(td);
}
// 3. 根據資料個數創建刪除欄位
var td = document.createElement('td');
td.innerHTML = "<a href='javascript:;'>刪除</a>"
tr.appendChild(td);
}
// 4. 完成刪除功能
var as = tbody.querySelectorAll('a');
for (var i=0; i<as.length; i++) {
as[i].onclick = function () {
// 是要刪除整行(tr)資料,
// 表格結構是 tbody>tr>td>a,
// 所以要刪掉tr的話是 tbody.removeChild(tr)
// 而 tr 是 a 的阿公
tbody.removeChild(this.parentNode.parentNode)
}
}
</script>
</body>
```
> - 191208 完全用JS寫一個
```javascript=
var item = ['Date1', 'Date2', 'Date3'];
var tmpDate = [
{Date1: 'tmp1', Date2: 'tmp2', Date3: 'tmp3'},
{Date1: 'tmp4', Date2: 'tmp5', Date3: 'tmp6'},
{Date1: 'tmp7', Date2: 'tmp8', Date3: 'tmp9'},
]
// <body> <table> <thead> <tr> <th>
var body = document.body;
var table = document.createElement('table');
body.appendChild(table);
table.style.border = '1px solid skyblue';
table.style.textAlign = 'center';
var thead = document.createElement('thead');
table.appendChild(thead);
var tr = document.createElement('tr');
thead.appendChild(tr);
for (var i = 0; i < item.length; i++) {
var th = document.createElement('th');
tr.appendChild(th);
th.innerText = item[i];
th.style.width = '100px';
th.style.backgroundColor = 'blue';
}
th = document.createElement('th');
tr.appendChild(th);
th.innerText = '刪除';
th.style.backgroundColor = 'blue';
th.style.width = '100px';
// <body> <table> <tbody> <tr> <td>
var tbody = document.createElement('tbody');
table.appendChild(tbody);
for (var i = 0; i < tmpDate.length; i++) {
var tr = document.createElement('tr');
tbody.appendChild(tr);
for (key in tmpDate[i]) {
var td = document.createElement('td');
tr.append(td);
td.innerText = tmpDate[i][key];
}
td = document.createElement('td');
tr.append(td);
td.innerHTML = '<a href="javascript:;">Del</a>';
}
var links = tbody.querySelectorAll('a');
for (var i = 0; i<links.length; i++) {
links[i].addEventListener('click', function () {
// <tbody> <tr> <td> <a>
tbody.removeChild(this.parentNode.parentNode);
return false
})
}
```
### document.write()
> - document.write('內容')
> - 簡單說就是在參數寫 innerHTML 後就會加載到頁面
> - 問題: 如果是在頁面加載完畢==後==調用, 整個頁面會被重置成 write 裡的參數
```htmlmixed=
<body>
<input type='button' id='btn'/>
<script>
// 讀完這句後會在頁面加載一個包著 123 的 div 標籤
document.write('<div>123</div>');
var btn = document.getElementById('btn');
btn.onclick = function () {
// 整個頁面加載完後, 點擊body裡面那個button而觸發這個func時
// 整個頁面會重新加載, 並只加載write裡的參數,
// 也就是裝著 123 的 div 標籤
document.write('<div>123</div>');
}
</script>
</body>
```
### 三個創建元素的方法
> - document.write()
> - 有明顯缺點, 不列入比較
> #### 兩者主要差別在於執行速度
> - document.createElement()
> - element.innerHTML
> - 當大量使用拼接時, 效率會比較慢
> - 但如果是先生成一個array後, 一次附值, 那效率比 appendChild 高
> - 以下三種方法皆在div內添加一模一樣的 a 標籤
> - 效率: `innerHTML array` > `createElement()` >>>>>>>> `innerHTML 拼接`
```htmlmixed=
<!-- createElement()-->
<!-- 大概在 100-200 豪秒之間 -->
<body>
<div></div>
<script>
var t1 = new Date();
var div = document.querySelector('div');
for (i=0; i<10000; i++) {
var a = document.createElement('a');
a.innerHTML = '測試';
a.href = 'javascript:;'
div.append(a);
}
var t2 = new Date();
console.log(t2.getTime() - t1.getTime());
</script>
</body>
```
```htmlmixed=
<!-- innerHTML 拼接 -->
<!-- 190690 豪秒 --> <!-- 久到懷疑人生 -->
<body>
<div></div>
<script>
var t1 = new Date();
var div = document.querySelector('div');
for (i=0; i<10000; i++) {
div.innerHTML += '<a href="javascript:;">測試</a>'
}
var t2 = new Date();
console.log(t2 - t1);
</script>
</body>
```
```htmlmixed=
<!-- innerHTML array -->
<!-- 50 豪秒左右 -->
<body>
<div></div>
<script>
var t1 = new Date();
var div = document.querySelector('div');
var arr = [];
for (i=0; i<10000; i++) {
arr.push('<a href="javascript:;">測試</a>')
}
div.innerHTML = arr.join('');
var t2 = new Date();
console.log(t2 - t1);
</script>
</body>
```
> - 對創建的元素做其他操作的時機不同
> - `element.innerHTML` 必須在網頁讀取完後再找到DOM對象來操作
> - `document.createElement()` 可以在創建元素的同時直接操做
```javascript=
var body = document.body;
var arr = [];
// 創建元素
for (var i = 0; i<3; i++) {
arr.push('<button>clickMe</button>');
}
body.innerHTML = arr.join('');
// 獲取元素 DOM
var btn = body.getElementsByTagName('button');
// 操作
for (var i = 0; i < btn.length; i++) {
btn[i].onclick = fn;
}
function fn() {
console.log('hi');
}
```
```javascript=
var body = document.body
for (var i = 0; i < 3; i++) {
// 創建元素
var btn = document.createElement('button')
// 直接操作
btn.innerText = 'clickMe';
btn.onclick = fn;
// 添加到 DOM 樹
body.appendChild(btn);
}
function fn() {
console.log('hi');
}
```
> - 釋放內存問題(內存泄露)
> - 操作節點就是整個在玩他的位置, 清除也就都全部清掉了
> - innerHTML 則是重新刻板, 只能刻出外表, 內在刻不到(事件等),
> 把板清空時, 還可能有遺產(內存洩漏)
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
div {
height: 100px;
}
.one {
background: skyblue;
}
.two {
background: darkblue;
}
</style>
</head>
<body>
<div class='one'>
<button>123</button>
</div>
<div class='two'></div>
<script>
var one = document.querySelector('.one')
var two = document.querySelector('.two')
var btn = document.querySelector('button');
btn.onclick = function () {
console.log(1);
}
/* 這種方式無法帶走他原本有的東西, 如事件
* 還有一個問題是one清空後, 原本的func可能會沒有清掉
one.onclick = function () {
two.innerHTML = one.innerHTML;
one.innerHTML = '';
}
*/
one.onclick = function () {
two.appendChild(btn);
}
</script>
</body>
```
### replaceChild
```htmlembedded=
<body>
<p>1</p>
<div>2</div>
<script>
const p = document.querySelector('p')
const div = document.querySelector('div')
div.parentNode.replaceChild(div, p)
// 拿 div 去把 p 換掉
</script>
</body>
```
### replaceWith
- 用什麼把調用的 DOM 替換掉
- `ChildNode.replaceWith((Node or DOMString))`
```htmlmixed=
<body>
<div><p>1</p></div>
<nav>3</nav>
<script>
const p = document.querySelector('p')
const div = document.querySelector('div')
const nav = document.querySelector('nav')
p.replaceWith(nav)
// 用 nav 把 p 給換了
console.log(div.outerHTML) // <div><nav>3</nav></div>
</script>
</body>
```
## 事件
### 註冊事件
> - 傳統註冊:
> - on開頭
> - 同一元素跟同一事件, 只能設置一個處理函數, 後函數會覆蓋前函數
> - `a.onclick = fn` `a.onclick = fn2`
> - 因為等於重新賦值了
> - 方法監聽
> - `eventTarget.addEventListener(type, listener[, useCapture])`
> - type : 事件類型string( 必須加引號 ), 比如 'click' ( 注意! 沒有on )
> - listener : 函數
> - useCapture : 參數, Boolean, 默認 false
> - w3c推薦
> - 兼容問題: IE9+
> - 同一元素跟同一事件, 可以設置多個處理函數, 按註冊順序依序執行
> - attachEvent
> - `eventTarget.attachEvent(eventNameWithOn, callback)`
> - 只有 IE10- 支持(非標準)
```htmlmixed=
<body>
<input type='button' value='傳統觸發' />
<input type='button' value='事件監聽' />
<script>
var btns = document.querySelectorAll('input');
// 傳統觸發
// 註冊兩個後, 只會觸發後面那個, 也就是 console.log('2');
btns[0].onclick = function () {
console.log('1');
}
btns[0].onclick = function () {
console.log('2');
}
// 事件監聽
// 註冊兩個後, 會依序觸發 3 4
btns[1].addEventListener('click', function () {
console.log('3');
});
btns[1].addEventListener('click', function () {
console.log('4');
});
</script>
</body>
```
> - 兼容方法
```javascript=
function addEventListener(elem, eventName, fn) {
// 判斷瀏覽器是否兼容 addEventListener
if (element.addEventListener) { // 如果瀏覽器裡有 addEventListener
elem.addEventListener(eventName, fn) // 那就調用
} else if (element.attachEvent) { // 如果有 attachEvent
elem.attachEvent('on'+eventName , fn) // 那就調用
} else { // 真的都沒有, 那就用 onEventName
// elem.onclick = fn
elem['on' + eventName] = fn
}
}
```
### 刪除事件
> - 傳統刪除
> - 直接把節點指向 null
> - `eventTarget.eventNameWithOn = null;`
> - `btns[0].onclick = null;`
> - IE9+ 事件監聽刪除
> - `eventTarget.removeEventListener(type, listener[, useCapture])`
> - `btns[1].removeEventListener('click', fn);`
> - 也就是如果需要刪除事件, 那創建的時候不能使用匿名函數
> - IE8- 事件監聽刪除
> - `eventTarget.detachEvent(eventNameWithOn, callback);`
> - `btns[2].detachEvent('onclick', fnIE8);`
```htmlembedded=
<body>
<input type='button' value='傳統觸發' />
<input type='button' value='IE9+事件監聽' />
<input type='button' value='IE8-事件監聽' />
<script>
var btns = document.querySelectorAll('input');
// 傳統刪除
btns[0].onclick = function () {
console.log('1');
btns[0].onclick = null;
}
// IE9+ 事件監聽刪除
// 考慮到刪除時要指定func的變量, 所以建立時不要用匿名函數, 否則刪除時沒有變量可以用
btns[1].addEventListener('click', fn);
function fn() {
console.log('2');
btns[1].removeEventListener('click', fn);
}
// 我沒IE 所以沒有測試可行性
btns[1].attachEvent('click', fnIE8);
function fnIE8() {
console.log('3');
btns[2].detachEvent('onclick', fnIE8);
}
</script>
</body>
```
> - 兼容
```javascript=
function removeEventListener(elem, eventName, fn) {
if (element.removeEventListener) {
elem.removeEventListener(eventName, fn);
} else if (element.detachEvent) {
elem.detachEvent('on'+eventName, fn);
} else {
// elem.onclick = null
elem['on' + eventName] = null;
}
}
```
### DOM 事件流
> - 事件傳播過程, 即「DOM 事件流」
> - 捕獲階段( Capture Phase ): 網景提出, 指從頂層向下傳播的過程
> - `Document` -> `Element html` -> `Element body`
> -> ... -> `Element <Target TagName>`
> - 當前目標階段( Target Phase )
> - `Element <Target TagName>`
> - 冒泡階段( Bubbling Phase ): IE 提出, 指事件接收向上傳播的過程
> - `Element <Target TagName>` -> ... -> `Element body`
> -> `Element html` -> `Document`
> - JS code 只能執行其中一個(捕獲 或 冒泡)
> - 傳統註冊 ( `node.onclick` ) 跟 IE專屬 ( `attachEvent()` ) 只能得到冒泡階段
> 也就是說, 只有 `addEventListener()` 才能控制要使用哪種
> - `true` : 捕獲
> - `false` 或 空 : 冒泡
> - 冒泡例外: focus 和 blur 不會冒泡
> - 取代: focusin 跟 focusout 執行時間點一模一樣,但是會冒泡
> - [DOM 的事件傳遞機制:捕獲與冒泡](https://blog.techbridge.cc/2017/07/15/javascript-event-propagation/)
```htmlmixed=
<body>
<div class='father'>
<div class='son'>1</div>
</div>
<script>
var father = document.querySelector('.father');
var son = document.querySelector('.son');
/* addEventListener 的第三個參數如果是 true
* 表示事件捕獲階段(Document -> Target)調用處理程序 */
// 先執行 father, 再執行 son
/*
father.addEventListener('click', function () {
console.log('father');
}, true)
father.addEventListener('click', function () {
console.log('son');
}, true)
*/
/* addEventListener 的第三個參數如果是 false 或 空
* 表示事件冒泡階段(Target -> Document)調用處理程序 */
// 先執行 son 才執行 father
father.addEventListener('click', function () {
console.log('father');
})
son.addEventListener('click', function () {
console.log('son');
})
</script>
</body>
```
## 事件對象
> - 觸發事件時, 事件會傳一個實參給處理函數, 該參數指向該事件的對象
> - 兼容問題: ie 678 使用 `window.event` 來獲取, 而不是形參
> - 事件對象是一個與該事件相關的數據集合,
> - 例如 滑鼠事件 裡有滑鼠座標等信息
> - 例如 鍵盤事件 裡有鍵盤按鍵信息等
> - 故獲取方法就是在處理函數設一個形參來接收,
> - 常用的形參名有 : event, evt, e
```htmlmixed=
</head>
<body>
<div class='one'>123</div>
<div class='two'>456</div>
<div class='three'>789</div>
<script>
var one = document.querySelector('.one');
// function(event) 的event 接收來自 onlink 的數據
one.onclick = function (event) {
console.log(event);
}
var two = document.querySelector('.two');
two.addEventListener('click', function (event) {
console.log(event)
})
// 兼容 IE 678 寫法
var three = document.querySelector('.three');
three.onclick = function (e) {
// 因為 IE 678 不是傳實參進func, 所以設形參也接收不到東西
// 為了兼容性, 可以讓形參多一個 || window.event 的動作來處理
// 如果瀏覽器沒有接到形參, 那就賦值 wondow.event
e = e || window.event;
console.log(e);
}
</script>
</body>
</body>
```
> - Q. 如果想對綁定函數傳參數怎辦?
```javascript=
function abc(arg1, arg2) {
console.log(arg1, arg2)
}
xxx.addEventListener(`click`, abc, false);
ooo.addEventListener(`click`, ev=>{
abc(123,4812); // 綁一個回調後再回調裡調用就能傳了
}, false);
```
### 常見對象屬性和方法
> - `event.target`
> - vs `this`
> - `event.target` 返回的是觸發事件的對象
> - `this` 返回的是綁定事件的對象
> - `event.target` 有兼容性問題(IE9+)
> - IE 678 使用的是 `event.srcElement`
> - `event.currentTarget`
> - vs. `this`
> - `event.currentTarget` 跟 `this` 非常相似
> 差別在於前者有兼容性問題( IE9+ ), 所以使用上 this 用得比較多
```htmlmixed=
<body>
<div class='one'>123</div>
<ul>
<li>123</li>
<li>456</li>
</ul>
<div class='two'>456</div>
<script>
var one = document.querySelector('.one');
// 這個例子差異看不太出來, 因為可以點擊的跟綁定的對象是同一個
one.onclick = function (event) {
console.log(event.target); // <div class='one'>123</div>
console.log(this); // <div class='one'>123</div>
}
var ul = document.querySelector('ul');
// 這個例子就很明顯
// event.target 返回的是誰觸發對象的, 點擊123就返回123, 點擊456就返回456
// this 返回綁定的對象, 這個事件是 ul 註冊的, 所以 this 指向這個 ul
ul.onclick = function (event) {
console.log(event.target); // <li>123</li> 或 <li>456</li>
console.log(this); // <ul>...</ul>
// currentTarget 與 this 非常相似的屬性, 有兼容問題, 所以使用上較少
console.log(event.currentTarget); // <ul>...</ul>
}
// 兼容 IE678
var two = document.querySelector('.two');
two.onclick = function (e) {
e = e || window.event; // IE678 獲取對象的方法
var target = e.target || e.srcElement; // IE678獲取target屬性的方法
console.log(target);
}
</script>
</body>
```
> - `e.eventPhase` : 事件的階段
> - `1` 是捕獲階段; `2` 是目標階段; `3` 是冒泡階段
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
.one {
height: 100px;
width: 100px;
background: blue;
}
.two {
height: 60px;
width: 60px;
background: skyblue;
}
.three {
height: 30px;
width: 30px;
background: darkblue;
}
</style>
</head>
<body>
<div class='one'>
<div class='two'>
<div class='three'>
</div>
</div>
</div>
<script>
var one = document.querySelector('.one');
one.addEventListener('click', function (e) {
console.log(e.eventPhase);
console.log(e.target);
console.log(e.currentTarget);
console.log(this);
}, true)
// 當 one 是捕獲階段(true)時
// 點擊 <three> 的 eventPhase 為 1; 點擊 <two> 為 1; 點擊 <one> 為 2
// 當 one 是冒泡階段(false)
// 點擊 <three> 的 eventPhase 為 3; 點擊 <two> 為 3; 點擊 <one> 為 2
</script>
</body>
```
> - `event.type` : 返回事件類型(不帶 on )
```htmlmixed=
<body>
<div class='one'>123</div>
<script>
var div = document.querySelector('div');
/*
div.onclick = function (e) {
console.log(e.type); // click 不帶on
}
*/
div.addEventListener('click', fn); // click
div.addEventListener('mouseover', fn); // mouseover
div.addEventListener('mouseout', fn); // mouseout
function fn(e) {
// 返回觸發事件類型
console.log(e.type);
}
</script>
</body>
```
> #### 阻止默認行為
> - `addEventListener()`
> - `event.preventDefault()`
> - 兼容問題( 兼容問題 (IE9+) ) :
> - IE678 本來就不認識 `addEventListener` ,
> - 當然也就不用討論到後面 `preventDefault()`
> - 傳統註冊
> - `event.preventDefault()`
> - 兼容問題( IE9+ )
> - `event.returnValue = false;` (非官方)
> - `return false`
> - 沒有兼容問題
> - return 就返回了, 所以 return 後面的東西就都不會執行
> - IE 678
> - 傳統註冊 + `event.returnValue = false`
> - 傳統註冊 + `return false`
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
</style>
</head>
<body>
<a href='#'>123</a>
<a href='#'>456</a>
<script>
var as = document.querySelectorAll('a');
as[0].addEventListener('click', function (e) {
// addEventListener 註冊方法裡只有這種阻止默認行為的方式
e.preventDefault();
// return false 沒有效用
})
as[1].onclick = function (e) {
e = e || window.event
// e.preventDefault(); // 第一種
// e.returnValue = false; // 第二種
return false // 第三種, 後面語句不執行
console.log('haha');
}
</script>
</body>
```
### 阻止事件冒泡
> - 事件冒泡, 從具體觸發一直到 document, 一路觸發事件到底
> - 阻止事件冒泡就是觸發完後停止, 不再繼續向上觸發
> - `stopPropagation()`(標準)
> - 兼容問題(IE9+)
> - `e.cancelBubble = true;` (非標準),
> - IE8- 也都可以用
> - 但是如果為了兼容 IE678 的話, 記得不能用 addEventListener
```htmlmixed=
<body>
<div class='fa' style='padding: 20px; border: 1px solid red;'>
<div class='son' style='border: 1px solid blue;'>123</div>
</div>
<script>
var fa = document.querySelector('.fa');
var son = document.querySelector('.son');
// 我只有對 son 指定終止冒泡, 所以點擊son 輸出son後會停止
son.addEventListener('click', function (e) {
console.log('son');
// e.stopPropagation();
e.cancelBubble = true;
}, false)
// 我沒有對 father 指定終止冒泡, 所以點擊 father 會輸出 father 跟 document
fa.addEventListener('click', function () {
console.log('father');
}, false)
document.addEventListener('click', function () {
console.log('document');
}, false)
</script>
</body>
```
```javascript=
// 兼容 IE678 寫法參考
fa.onclick = function (e) {
// 如果瀏覽器有使用形參傳對象, 且認識 stopPropagation
if (e && e.stopPropagation) {
e.stopPropagation(); // 調用
} else { // 否則就是 IE678
window.event.cancelBubble = true;
}
}
```
> - `stopImmediatePropagation` 阻止後面同一事件所有即將被執行的 handler 執行 ( IE9+ )
> - 既然後面 handler 不執行了,所以也有阻止冒泡的效果
> - 與 stopPropagation 的區別就在於後者只阻止父層以上的同事件 handler
```htmlembedded=
<body>
<style>
.fa {
padding: 100px;
background: blue;
}
.son {
height: 100px;
width: 100px;
background: red;
}
</style>
<div class="fa">
<div class="son"></div>
</div>
<script>
const oFa = document.querySelector('.fa').addEventListener('click', (ev) => {
console.log('fa')
})
const oSon = document.querySelector('.son')
oSon.addEventListener('click', (ev) => {
console.log('son-1')
// ev.stopPropagation();
ev.stopImmediatePropagation();
// 如果是 stopPropagation, 那麼 son-2 還會執行
// stopImmediatePropagation 直接斷在這, 連 son-2 也不會執行
})
oSon.addEventListener('click', (ev) => {
console.log('son-2')
})
</script>
</body>
```
### 事件委託
> - 以前想要操作子節點時, 都會對每個 node 註冊
> - 事件委託是對父節點操作DOM,
> 如果想對子節點操作, 可利用觸發事件返回對象中的 target 屬性來找到對象
```htmlmixed=
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
/* 以前的作法: 對每個 li 做DOM 操作
var lis = document.querySelectorAll('li');
for (i=0; i<lis.length; i++) {
lis[i].addEventListener('click', function (e) {
console.log(this)
})
}
*/
// 利用冒泡, 只需對父元素操作DOM, 再利用 target 屬性來找到對象
var ul = document.querySelector('ul');
ul.addEventListener('click', function (e) {
console.log(e.target);
e.target.style.backgroundColor = 'skyblue';
})
</script>
</body>
```
### 常用鼠標事件
> - `contextmenu` : 控制上下選單, 對於鼠標事件來說就是右鍵選單
> - `selectstart` : 控制選取
```htmlmixed=
<body>
<p>123</p>
<script>
// 禁止右鍵
// 控制選單 -> 取消 默認行為(可以拉選單)
document.addEventListener('contextmenu', function (e) {
e.preventDefault()
})
// 禁止選取
// 控制開始選取 -> 取消 默認行為(可以選取)
document.addEventListener('selectstart', function (e) {
e.preventDefault()
})
</script>
</body>
```
### 鼠標事件對象
> #### clientX 與 clientY
> - clientX : 返回相對於 頁面 ==可視區== 的 X 座標
> - clientY : 返回相對於 頁面 ==可視區== 的 Y 座標
> #### pageX 與 pageY
> - pageX : 返回相對於 ==文檔頁面== 的 X 座標
> - pageY : 返回相對於 ==文檔頁面== 的 Y 座標
> - 有兼容性問題: IE9 以後才支援
> - 解法就是 client + 頁面滾動距離 = page
> #### screenX 與 screenY
> - screenX : 返回相對於 ==電腦螢幕== 的 X 座標
> - screenY : 返回相對於 ==電腦螢幕== 的 Y 座標
```htmlmixed=
<body>
<script>
var body = document.querySelector('body');
body.style.height = '3000px';
document.addEventListener('mousemove', function (e) {
console.log('--------');
console.log(`e.clientX:${e.clientX}, e.pageX:${e.pageX}, e.screenX: ${e.screenX}`);
console.log(`e.clientY:${e.clientY}, e.pageY:${e.pageY}, e.screenY: ${e.screenY}`);
console.log('--------');
})
</script>
</body>
```
### 案例: 圖片跟隨滑鼠
> - `mousemove` : 滑鼠移動事件
> - document 註冊這個比較方便
> - 圖片不佔位, 且可以控制 XY 移動 -> absolute
> - 透過 mousemove 的 pageXY 或 clientXY 來拿滑鼠座標
> - 圖片left, top 賦予滑鼠座標, 記得加單位(px)
```htmlmixed=
<body>
<img src='https://api.fnkr.net/testimg/20x20/ccc'>
<script>
// 1. 拿到圖片
var img = document.querySelector('img');
// 2. 設定成絕對定位
img.style.position = 'absolute';
// 3. mousemove
document.addEventListener('mousemove', function (e) {
console.log(e.pageX, e.pageY);
// 4. 賦值
img.style.left = e.pageX-10 + 'px';
img.style.top = e.pageY-10 + 'px';
})
</script>
</body>
```
### 滾動距離
> #### 元素滾動距離
> - `elem.scrollTop` `elem.scrollLeft`
> #### 頁面滾動距離
> - DOM
> - `element.body.scrollTop` `element.body.scrollLeft`
> - `document.documentElement.scrollTop` `document.documentElement.scrollLeft`
> - `document.documentElement` 就是 html
> - 這兩種有兼容性問題, 有些瀏覽器上面那個可以, 有些是下面這個可以
> - 191208 我的 `Chrome: Version 78.0.3904.108` 是用從 html 獲取
> - BOM
> - `window.pageXOffset` `window.pageYOffset`
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
body {
height: 3000px;
width: 2000px;
}
div {
height: 100px;
width: 50px;
border: 1px solid red;
overflow: auto;
}
</style>
</head>
<body>
<div>
我是內容
我是內容
我是內容
我是內容
我是內容
我是內容
我是內容
我是內容
我是內容
我是內容
我是內容
</div>
<script>
// 元素滾動距離
var div = document.querySelector('div');
div.addEventListener('click', function () {
console.log(div.scrollTop, div.scrollLeft);
}, true)
// 頁面滾動距離
document.addEventListener('click', function () {
console.log(document.body.scrollTop, document.body.scrollLeft);
console.log(document.documentElement.scrollTop, document.documentElement.scrollLeft);
console.log(window.pageYOffset, window.pageXOffset);
})
// DOM 文檔的根元素, 也就是 html 標籤
console.log(document.documentElement);
</script>
</body>
```
> - DOM.scroll & e.page 兼容
```javascript=
// 測試環境設置而已, 讓頁面有滾動條
document.body.style.width = '2000px';
document.body.style.height = '3000px';
// scroll 兼容
function getScroll() {
scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
scrollLeft = document.body.scrollLeft || document.documentElement.scrollLeft;
return {
scrollTop: scrollTop,
scrollLeft: scrollLeft,
}
}
// e.page 兼容
function getPage(e) {
var pageX = e.pageX || e.clientX + getScroll().scrollLeft;
var pageY = e.pageY || e.clientY + getScroll().scrollTop;
return {
pageX: pageX,
pageY: pageY,
}
}
document.addEventListener('click', function (e) {
// console.log(getScroll().scrollTop, getScroll().scrollLeft);
// console.log(window.pageYOffset, window.pageXOffset);
// event 兼容
var e = e || window.event;
console.log(getPage(e).pageX, getPage(e).pageY)
})
```
> #### `ondblclick`
> - 雙擊觸發
> - `window.getSelection? window.getSelection().removeAllRanges(): document.selection.empty();` : 取消選取文字
```htmlmixed=
<body>
<div>1</div>
<script src='test.js'></script>
</body>
```
```javascript=
var div = document.querySelector('div');
// 雙擊觸發
div.addEventListener('dblclick', function () {
console.log(1);
// 雙集會選取文字, 判斷是否選取, 有就取消
window.getSelection? window.getSelection().removeAllRanges(): document.selection.empty();
})
```
### 常用鍵盤事件
> - `onkeyup` : 按下按鍵鬆開時觸發
> - `onkeydown` : 按下按鍵時觸發
> - `onkeypress` : 按下按鍵時觸發
> - 不識別功能鍵, 如ctrl, shift...等
> - 執行順序: `onkeydown` -> `onkeypress` -> `onkeyup`
```htmlmixed=
<body>
<script>
/*document.onkeyup = function () {
console.log('keyup')
}*/
document.addEventListener('keyup', function () {
console.log('keyup');
})
document.addEventListener('keydown', function () {
console.log('keydown');
})
document.addEventListener('keypress', function () {
console.log('keypress');
})
</script>
</body>
```
### 鍵盤事件對象
> - `keyCode` : 返回按鍵的ASCII
> - `keyup` 跟 `keydown` 不區分大小寫, 但可識別功能鍵
> - `keypress` 能區分大小寫, 但不識別功能鍵
```htmlmixed=
<body>
<input type='text'/>
<script>
document.addEventListener('keyup', function (e) {
console.log('keyup: ' + e.keyCode);
console.log('------')
})
document.addEventListener('keydown', function (e) {
console.log('keydown: ' + e.keyCode);
})
document.addEventListener('keypress', function (e) {
console.log('keypress: ' + e.keyCode);
})
</script>
</body>
```
### 練習: 按 S 鍵, 讓輸入框獲得焦點
> - 輸入框獲得焦點: `input.focus()`
```htmlmixed=
<body>
<input type='text'/>
<script>
var input = document.querySelector('input');
/* 這裡使用 keyup 比較適合,
* 因為 keydown 會連續觸發, 造成按s時, 除了觸發焦點外, 還馬上在上面輸入了一個 s
* keyup 是彈起來才觸發一次
document.addEventListener('keydown', function (e) {
console.log(e.keyCode);
if (e.keyCode === 83) {
input.focus();
}
})
*/
document.addEventListener('keyup', function (e) {
console.log(e.keyCode);
if (e.keyCode === 83) {
input.focus();
}
})
</script>
</body>
```
### 練習: 輸入框的字體放大鏡
<img src='https://i.imgur.com/BCo0q3z.png' style='width: 200px;'/>
> - 利用 keyup 來感應輸入框有沒有在打字
> - 不適合 keydown 跟 keypress
> - 因為這兩個在按下按鈕時就馬上觸發了, input.value 都還沒把內容寫進去
> - 如此會導致少一個字的問題
> <img src='https://i.imgur.com/JUUAee8.png' style='width: 150px;'/>
> - 把 input.value 內容丟到 innerText 裡
> - 如果 `input.value === ''` 就隱藏盒子
> - 失去焦點的時候, 隱藏放大鏡
> - 獲得焦點的時候, 輸入框有東西就顯示放大鏡
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
.fa {
position: relative;
height: 300px;
width: 300px;
margin: 300px auto;
}
/* 放大盒 */
.big {
height: 50px;
width: 100px;
border: 1px solid red;
box-shadow: 0px 5px 8px 0px gray;
margin-bottom: 10px;
position: relative;
display: none; /* 一開始隱藏 */
position: absolute; /* 為了盒子不佔位, 所以做定位 */
top: -60px;
}
/* 三角形小圖標 */
.big::after {
content: '';
width: 0;
height: 0;
line-height: 0;
font-size: 0;
border: 8px solid transparent;
border-top-color: white;
position: absolute;
bottom: -16px;
left: 5px;
}
</style>
</head>
<body>
<div class='fa'>
<div class='big'></div>
<input type='text'></input>
</div>
<script>
var input = document.querySelector('input');
var div = document.querySelector('.big');
/* keydown跟keypress按下按鈕時馬上觸發,
* 字都還沒進到 input.value 裡, 所以會產生少一個字的問題
input.addEventListener('keydown', function () {
div.innerText = input.value;
*/
input.addEventListener('keyup', function () {
// 把 input 框的內容丟到放大盒裏
div.innerText = input.value;
// 判斷一下 input 框有沒有東西
if (input.value.length === 0) {
div.style.display = 'none'; // 沒東西就隱藏
} else {
div.style.display = 'block'; // 有東西就顯示
}
})
// 輸入匡失去焦點的時候隱藏放大盒
input.addEventListener('blur', function () {
div.style.display = 'none';
})
// 獲得焦點得時候, 有東西就顯示
input.addEventListener('focus', function () {
if (input.value !== '') {
div.style.display = 'block';
}
})
</script>
</body>
```
### 練習: 只能輸入數字與刪除的文本框
> - `keyup` 不適合, 因為觸發 keyup前, 內容已經寫進去了
> - 所以用 `keydown` 比較好
`<input type="text"/>`
```javascript=
var txt = document.querySelector('input');
txt.onkeydown = function (e) {
console.log(e.keyCode);
// 0 = 48, 9 = 57, delete = 8
if ((e.keyCode < 48 || e.keyCode > 57) && e.keyCode !== 8) {
return false
}
}
```
## 表單域 [onchange](https://developer.mozilla.org/zh-CN/docs/Web/Events/change) [oninput](https://developer.mozilla.org/zh-CN/docs/Web/Events/input) 事件的差別
- oninput 是只要輸入框一改變, 即觸發事件
- onchange 是輸入框失去焦點時,觸發事件
```htmlmixed=
<body>
<!-- 這裡的東西要失去焦點才觸發, 我感覺 select 沒有差別 -->
<input type="text" onchange="test(event)"/>
<select onchange="test(event)">
<option>1</option>
<option>2</option>
<option>3</option>
</select>
<textarea onchange="test(event)"></textarea> <br/>
<!-- 這裡的東西只要改變文本框的內容馬上觸發 -->
<input type="text" oninput="test(event)"/>
<select oninput="test(event)">
<option>1</option>
<option>2</option>
<option>3</option>
</select>
<textarea oninput="test(event)"></textarea>
</body>
<script>
function test(ev) {
console.log(ev.target.value);
}
</script>
```