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> ``` ## 事件 > - 觸發響應機制 > - 事件三元素: > - 事件源 > - 事件名稱 > - 事件處理程序 點擊前 ![](https://i.imgur.com/fQ7C8lt.png) 點擊後 ![](https://i.imgur.com/b4gyrjn.png) ```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 > - ![](https://i.imgur.com/jBJAEeo.png) ```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> ```