BOM( Brower Object Model) === > - 與瀏覽器進行交互的對象( 把瀏覽器當成對象 ) > - vs. DOM 把文檔當成對象 > - 核心為 window > - vs. DOM 核心為 document > - 沒有統一標準, 由各家瀏覽器自行定義( 最早為網景 ), 所以兼容性差 > - vs. DOM 由 W3C 規範 > - vs. JS 由 ECMA 規範 > - BOM 是 JS 訪問瀏覽器的接口 > - 全局變量和函數都會變成 window 的對象和方法 > - 注意! window 有些特殊屬性, 例如 `window.name` > - 所以聲明變量沒事儘量不要聲明 `var name`, 可能會遇到一些麻煩, 因為變量會被存在瀏覽器, 而不會重新整理後清除, 要關掉該網頁才會清掉 ```javascript= console.log(window); var name = 123; console.log(name); // 123 console.log(typeof name); // string -> name 屬性是字符串! var top = 456; console.log(top); // Window console.log(typeof top); // object -> top 就是 window 對象, 不能被賦值 ``` > - 以前常用的方法 > - `alert('提示')` : 彈出提示, 沒有返回值 > - `prompt('提示', '默認')` : 請用戶輸入一些東西, 返回 string > - `confirm('提示')` : 請用戶點擊是或否, 返回 boolean > - 現在少用的原因 > - 太醜, 且每個瀏覽器呈現的方式不一樣 > - 現在常見自己寫, 操作 display 就好了 ```javascript= var btn = document.querySelectorAll('button'); btn[0].addEventListener('click', function () { var al = alert('hihi') console.log(al); // undefined console.log(typeof al); // undefined }) btn[1].addEventListener('click', function () { var pr = prompt('hihi', 'unhihi') console.log(pr); // unhihi console.log(typeof pr); // string }) btn[2].addEventListener('click', function () { var co = confirm('hihi') console.log(co); // true console.log(typeof co); // boolean }) ``` > - BOM 組成 > - window (BOM) > - document (DOM) > - BOM 包含了 DOM > - location > - navigation > - screen > - history ```javascript= function fn() { console.log(10); } // DOM 是 BOM 的組成元素一 //document.addEventListener('click', fn); window.document.addEventListener('click', fn); // 全局變量和函數都會變成 window 的對象和方法 var num = 100; console.log(num); // 100 window.console.log(num); // 100 fn(); // 10 window.fn(); // 10 // alert 為 window 的方法 alert(1000); window.alert(1000); ``` ## window 常見事件 ### 頁面加載 > #### `window.onload` > - 頁面 「完全加載完(包含圖片, css等)」後會觸發 onload 事件 , > 也就是說以前一定要把 JS 放在最下面寫, 不然找不到對象可以操作, > 用 onload 就可以把 JS放在任何地方, 反正最後才觸發加載 > - `window.onload = function () {}` > - 傳統註冊一樣有後面蓋前面的問題 > - `window.addEventListener('load', function () {})` > - vs. `onunload` : 頁面卸載後執行 > - 所有對話框都無法使用, 因為 window 要被釋放掉了 > #### `document.addEventListener('DOMContentLoaded', fn)` > - DOM 加載完後即觸發, 不用等照片, 樣式表, flash.. > - 這東西只有掛載在 window 或 document 上,並且要用 addEventListener 來註冊 > - 優點: 如果頁面有太多資源要加載, 那 onload 功能就要等很久才能用 > - 缺點: 兼容問題( IE9+ ) ```htmlembedded= <body> <div></div> <script> document.querySelector('div').addEventListener('DOMContentLoaded', () => { console.log(12312312) }) document.addEventListener('DOMContentLoaded', () => { console.log(99999) }) window.addEventListener('DOMContentLoaded', () => { console.log(88888) }) console.log(document.onDOMContentLoaded, window.onDOMContentLoaded) // undefined undefined // 99999 // 88888 // 沒有 123 </script> </body> ``` > #### `document.onreadystatechange` > - 這東西掛載在 document 上,當文檔狀態改變時觸發 > - 文檔狀態屬性 `document.readyState` 有三個值 > - loading: 文檔讀取中 > - interactive: 文檔已經搞定,但是其他瀏覽器線程的資源還沒搞定,例如 scripts, images, stylesheets and frames > - complete: 通通搞定 > #### `pageshow` > - 在 onload 後面調用 ```htmlmixed= <body> <script> console.log(document.readyState) window.onload = function () { console.log('onload', '圖片寬度:' + getComputedStyle(document.querySelector('div img')).width) } document.addEventListener('DOMContentLoaded', function () { console.log('DOMContentLoaded', '圖片寬度:' + getComputedStyle(document.querySelector('div img')).width) }) document.onreadystatechange = function () { console.log(document.readyState, '圖片寬度:' + getComputedStyle(document.querySelector('div img')).width) } window.addEventListener('pageshow', function () { console.log('pageshow', '圖片寬度:' + getComputedStyle(document.querySelector('div img')).width) }) // loading // interactive 圖片寬度:0px => 在這裏 DOM 都已經拿到了,但是裡面的圖片還沒加載完成,所以寬度是 0 // DOMContentLoaded 圖片寬度:0px // complete 圖片寬度:94px => 在這裏就都搞定了 // onload 圖片寬度:94px // pageshow 圖片寬度:94px </script> <div><img src="./1.png" alt=""></div> </body> ``` ### 頁面顯示與隱藏 > - 文件:https://developers.google.com/web/updates/2018/07/page-lifecycle-api > #### `visibilityChange`: 顯示狀態改變時 > - document.visibilityState 頁面狀態 > - `visible` 當頁面正在被讀取時 > - `hidden` 當頁面不被讀取時,例如縮小或離開該頁面的 tab > - document.hidden 頁面是否被隱藏 boolean ```htmlembedded= document.onvisibilitychange = function () { console.log(document.visibilityState, document.hidden) } ``` ### 調整視窗大小事件 > #### `window.onresize` > - 只要瀏覽器有被調整大小, 就會觸發事件 > - `window.onresize = function () {}` > - `window.addEventListener('resize', function () {})` > - 經常用於響應式, 利用 `window.innerWidth` 屬性 ( 當前螢幕寬度 ) 來控制佈局 ```htmlmixed= <body> <script> document.addEventListener('DOMContentLoaded', function () { var div = document.querySelector('div'); window.addEventListener('resize', function (e) { // console.log('視窗拉動'); // 注意, e 是指向 resize, window就是指向window // console.log(e); // console.log(window); console.log(window.innerWidth); // 當螢幕寬度小於 900 時 if (window.innerWidth < 900) { div.style.display = 'none'; // 隱藏 div } else { div.style.display = 'block'; } }) }) </script> <div style='height: 100px; width: 100px; border: 1px solid red;'></div> </body> ``` ### 定時器 > #### `window.setTimeout(function[, 延遲毫秒數])` > - 顧名思義, 這就是個計時器, 時間到了就會觸發函數 > - 延遲毫秒數就是計時, 默認為 0 > - 寫法很多種, 其實就是一般調用傳參的各種做法 > - 方法可以 匿名, 函數名, 還可以用 '函數名()' 來傳 > - 如果需要設置各種定時器, 可以用變量來存 > - 只調用==一次==就結束這個定時器 > - 這是一種 **回調函數(callback)** > - 回調函數: 完成一件事情後, 回頭調用函數 > - `setTimeout` : 完成倒數後, 回頭執行 fn() > - `onclick` : 完成點擊後, 回頭執行 fn() ```javascript= /* 可以直接寫匿名函數 window.setTimeout(function () { console.log(1); } , 5000) */ function fn() { console.log(1); } /* 也可以寫函數, 且 window 可以省略*/ setTimeout(fn, 3000); /* 還可以寫 '函數名()', 另外頁面可能會需要多個計時器, 此時可以用變量來存, 比較省事 */ var time2 = setTimeout('fn()', 5000); ``` ### 五秒後隱藏廣告 ```htmlmixed= <head> <meta charset='utf-8'> <style> div { height: 100px; width: 100px; border: 1px solid red; } </style> </head> <body> <div></div> <script> setTimeout(function () { var div = document.querySelector('div'); div.style.display = 'none'; }, 5000) </script> </body> ``` ### 清除定時器 > #### `window.clearTimeout(timeoutID)` > - 取消計時器 > - window可省, 參數就是 timeout 的變量 ```htmlmixed= <body> <input type='button'/> <script> // 計時器 var timeOut = setTimeout(function () { console.log(1); }, 5000) // 按鈕按下去 var btn = document.querySelector('input'); btn.addEventListener('click', function () { clearTimeout(timeOut); // 清除計時器 }) </script> </body> ``` ### 重複定時器 > #### `window.setInterval(fn[, 間隔毫秒數])` > - 用法跟 `setTimeout` 幾乎一樣 > 差別在於 setInterval 會重複調用 ```javascript= setInterval(function () { console.log(1); }, 3000) ``` ### 練習: 模仿京東倒數計時器樣式-改成跨年倒數 > - 用 setInterval 每秒鐘都調用一次現在時間來跟跨年時間計算 > - 再賦值給顯示時間的盒子 > - 在 setInterval 之前要先調用一次計算時間的函數, 否則會有一秒鐘的空白(延遲時間) ```htmlmixed= <head> <meta charset='utf-8'> <style> * { margin: 0; padding: 0; } .timeBack { height: 300px; width: 200px; background-color: #E44536; margin: 100px auto; overflow: hidden; /* 觸發BFC, 不然子元素的margin會被吃掉 */ color: white; } .timeBack h3 { font-size: 30px; margin-top: 30px; text-align: center; } .timeBack h4 { font-size: 20px; text-align: center; opacity: 0.8; } .timeBack .imgBox { height: 80px; width: 150px; text-align: center; margin: auto; } .timeBack .imgBox img { margin: 25px auto; } .timeBack p { text-align: center; line-height: 15px; } .timeBack .time { height: 110px; text-align: center; } .timeBack .time .timeIn { height: 30px; width: 30px; display: inline-block; background-color: #363A34; margin-top: 35px; font-size: 28px; } </style> </head> <body> <div class='timeBack'> <h3>跨年倒數</h3> <h4>HappyNewYear</h4> <div class='imgBox'> <img src='https://api.fnkr.net/testimg/30x30/ccc'></img> </div> <p>距離倒數還剩</p> <div class='time'> <div class='timeIn'>1</div> <div class='timeIn'>1</div> <div class='timeIn'>2</div> <div class='timeIn'>3</div> </div> </div> <script> var t2 = new Date(2020, 0, 1); // 全局變量, 不怕抓不到 // 定義的東西拿出來全局定義, 每則每秒都要開一個空間, 效率非常差 var time = document.querySelectorAll('.timeIn'); var timeConsole; var year, month, day, hour, minute, second, t1, sub; countDown(); // 先調用一次, 否則會有一秒鐘的間隔 setInterval(countDown, 1000); function countDown() { // 調用現在時間 t1 = new Date(); // 計算相差時間 sub = t2-t1; sub /= 1000; //console.log(sub); // 換算時間 second = Math.floor(sub % 60); minute = Math.floor(sub / 60 % 60); hour = Math.floor(sub / 60 / 60 % 24); day = Math.floor(sub / 60 / 60 / 24); // 補0 second = second<10?'0'+second: second; minute = minute<10?'0'+minute: minute; hour = hour<10?'0'+hour: hour; day = day<10?'0'+day: day; timeConsole = day + '天' + hour + '時' + minute + '分' + second + '秒' console.log(timeConsole); // 賦值 //console.log(time) time[0].innerText = day; time[1].innerText = hour; time[2].innerText = minute; time[3].innerText = second; } </script> </body> ``` ### 清除重複定時器 > #### `window.clearInterval(IntervalID)` > - 跟清除定時器一樣 ```htmlmixed= <body> <input type='button' value='start'/> <input type='button' value='end'/> <script> var start = document.querySelector('input[value=start]'); var end = document.querySelector('input[value=end]'); // var tmp; // null 是一個空對象, setInterval 也要賦值給一個對象 // 為了避免 undefined 遇到問題, 所以操作 null 好一點 var tmp = null; start.addEventListener('click', function () { tmp = setInterval(function () { console.log(1); }, 1000) }) end.addEventListener('click', function () { clearInterval(tmp); }) </script> </body> ``` ### 練習: 發送驗證碼倒數 ```htmlmixed= <body> <input type='button' value='送出'/> <script> var btn = document.querySelector('input'); var time = 4; btn.addEventListener('click', function() { btn.disabled= true; btn.value = 5; // 避免延遲一秒的空窗 var tmp = setInterval(function () { if (time === 0) { btn.value = '送出'; btn.disabled = false; time = 4; clearInterval(tmp) } else { btn.value = time; time--; } }, 1000) console.log(1) // 我只是想測試是不是跑完setInterval才執行, 結果不是 }) </script> </body> ``` ```javascript= // 在寫這個的時候遇到的問題紀錄 var tmp = setInterval(function () { // 一開始我是這樣寫, // 這有個邏輯問題, time=1 的時候, 按鈕會變成 1 後瞬間變回送出 // 因為 賦值後-1 =0 , 馬上進入判斷 true , 造成 1 只顯示了一瞬間 // 所以應該是先判斷不等於 0 再賦值 btn.value = time; time--; if (time === 0) { btn.value = '送出'; btn.disabled = false; time = 4; clearInterval(tmp) } } ``` ### this > - this 一般情況下指向的是調用對象 ```htmlembedded= <body> <input type='button'/> <script> // 全局作用域 this 指向 window // window.console.log(this) -> window 調用的 console.log(this); // window {} function fn() { console.log(this); } // window.fn() -> window 調用的 fn() // window // 方法調用後, this 指向調用方法的對象 var a = { haha: 'haha', aa: function () { console.log(this) } } a.aa() // {haha: "haha", aa: ƒ} -> this 指向 a 指向的這個對象 var b = a; b.aa(); // {haha: "haha", aa: ƒ} var btn = document.querySelector('input'); btn.onclick = function () { console.log(this); // <input type='button'/> -> 這個按鈕調用的方法 } // 構建函數後, this 指向實例對象, 就是 python 的 self function Cl() { console.log(this); } var c = new Cl(); // Cl() --> this 指向了Cl() 創建得利對象, 也就是c // Q. 以下產生的問題 var btn = document.querySelector('input'); var time = 4; btn.addEventListener('click', function() { this.disabled= true; // 沒有問題, this 就是指向 btn this.value = 5; // 沒有問題, this 就是指向btn var tmp = setInterval(function () { if (time === 0) { // this指向的事window // 所以這裡都是改window的屬性值, 而不是 btn , // 所以 btn 會一直關閉且值=5 this.value = '送出'; this.disabled = false; time = 4; clearInterval(tmp) } else { this.value = time; time--; } }, 1000) console.log(1) // 我只是想測試是不是跑完setInterval才執行, 結果不是 }) ``` ## JS 的同步與異步 > - JS 是單線程語言 > - 為了解決效率問題, HTML5 提出了 Web Worker > - 允許 JS 建立多線程, 因而出現同步與異步 > - 同步 -> 主線程 > - 異步 -> 任務隊列 > - JS 異步是透過 callback 函數來實現 > - 普通事件 `click` `resize` > - 資源加載 `load` `error` > - 定時器 `setInterval` `setTimeout` > - 先執行主線程, 再處理異步任務 ```javascript= console.log(1); setTimeout(function () { console.log(2); }, 0) console.log(3); // 如果是同步任務, 應該是123 // 但執行的結果是 132, 因為 setTimeout 被丟到了任務隊列中 ``` ### 事件循環(Event Loop) ![](https://i.imgur.com/RB0ADqq.gif) > - JS 執行過程 > - 開始 > - 先執行主線程執行線, > - 遇到異步任務時, 交給對應的異步進程處理 (web API) > - ajax > - DOM > - timer > - 異步任務完畢後, 丟到任務隊列(Callback Queue)中 > - 主線成執行完畢後, 從任務隊列取出任務丟到主線程 > 處理完後再回任務隊列找任務, 不斷循環, 此循環稱為**事件循環** ```javascript= console.log(1); document.addEventListener('click', function () { console.log(2) }) console.log(3); setInterval(function () { console.log(4); }, 3000) // 執行結果: // 開始後跳出 13 如果我沒有點擊畫面, 每三秒跳一次 4, 我有點擊畫面就跳 2 // 執行過程: // 開始後先執行 1, 接著將click丟給 web API 的 DOM 後, 執行 3 // 然後再把 setInterval 丟給 timer // 如果我有點擊畫面, DOM 就會把 2 丟給 Callback Queue // timer 每三秒都會丟一次 4 給 Callback Queue // 而 JS 會不停地去 Callback Queue 拿任務 ( Event Loop ) ``` ## loaction > - window裡的屬性,用於獲取、設置與解析URL > - 返回值為一個對象, 所以稱為 location 對象 ### URL (Uniform Resource Locator) > - 網上每個文件都有一個唯一的 URL > - URL語法 : `protocol://host[:port]/path/[?query]#fragment` > - `protocol` : 通信協議, 例如 http, ftp, maito 等 > - `host` : 主機域名 > - `port` : 端口, 可省( http默認80 ) > - `path` : 路徑 > - `query` : 參數, 鍵與值的形式, 以 & 區隔, k=v&k=v > - `fragment` : 片段, 常用於錨點, #後面 ### loaction 屬性 > - `location.href` : 整個 URL > - `loaction.host` : 主機域名 > - `location.port` : 端口 > - `loaction.pathname` : 路徑 > - `loaction.search` : 參數 > - `loaction.fragment` : 片段 ### 練習: 5秒後跳轉頁面 ```htmlmixed= <body> <div>你將在5秒後跳轉頁面</div> <script> var time = 4; setInterval(fn, 1000); function fn() { var div = document.querySelector('div'); if (time === 0) { // 看一下這個頁面的URL // console.log(location.href); // 改變 URL location.href = 'https://www.youtube.com/' } div.innerText = '你將在' + time + '秒後跳轉頁面' time-- } </script> </body> ``` ### 獲取參數 > - 傳遞參數 > - `form.action` : 指定程序的URL > - `input.name` : URL 的參數 KEY 值 > - `input.value` : URL 的參數 VALUE 值 > - `?<input.name>=<input.value>` `$ vi login.html` ```htmlmixed= <body> <!-- 提交後送到index.html--> <form action='index.html'> <!-- 這欄的 URL參數 key 是 username--> <input type='text' name='username'/> <input type='submit'/> </form> </body> ``` `$ vi index.html` ```javascript= var body = document.querySelector('body'); // 獲取參數 console.log(location.search) // ?username=Racc // 我只想要後面val值, 用split切開後取 [1] var arr = location.search.split('='); console.log(arr) // (2) ["?username", "Racc"] var Uname = arr[1]; console.log(Uname); // Racc -> 拿到 Racc 就好辦了 body.innerText = Uname + '歡迎光臨' ``` ### location 常用方法 > - `loaction.assign()` : 跳轉 > - 有歷史紀錄, 可以回上一頁 > - `location.replace()` : 跳轉 > - 沒有歷史紀錄, 不能回上一頁 > - `location.reload([Boolean])` : 重整 > - 參數默認為 false, 一般的重整, 如果瀏覽器有暫存, 先從暫存獲取 > - 參數設定為 true, 強制重整, 直接從服務器獲取頁面 ```htmlmixed= <body> <input type='button' value='assign'/> <input type='button' value='replace'/> <input type='button' value='reload'/> <script> var btn = document.querySelectorAll('input'); // 跳轉後記錄歷史功能, 所以有上一頁的功能 btn[0].addEventListener('click', function () { location.assign('https://www.youtube.com/') }) // 跳轉後不記錄歷史紀錄, 所以沒有上一頁的功能 btn[1].addEventListener('click', function () { location.replace('https://www.youtube.com/') }) // 重新整理, 相當於 f5, 默認參數為false // 如果參數為 true, 那就是強制重新整理, 相當於 ctrl+R(ctrl+f5) btn[2].addEventListener('click', function () { location.reload(true); }) </script> </body> ``` ## navigator > - 包含瀏覽器信息的對象 > - `navigator.userAgent` > - 返回客戶端發送給服務器的 user-agent 的值 > - 說白話就是瀏覽器跟系統的版本 > - 常用於判斷客戶使用的設備, 已導入不同頁面(手機版或電腦版) ```javascript= console.log(navigator.userAgent) // Mozilla/5.0 .... ``` ### 實現判斷跳轉 > - 用正則判斷拿到的返回值是不是手機端, 如果是就跳手機端頁面, 反之跳電腦版 ```javascript= if ((navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Andriod|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i))) { location.href = 'https://m.youtube.com/'; } else { location.href = 'https://www.youtube.com/'; } ``` ## history `window.history` > - 與瀏覽器歷史紀錄交互的對象 > - 常用方法 > - `history.back()` : 上一頁 > - `history.forward` : 下一頁 > - `history.go(num)` > - num=1: 下一頁; num=2: 下兩頁; ... > - num=-1: 上一頁; num=-2: 上兩頁; ... > - 一般開發可能不常用到, 可是特殊情況, 如 OA系統 就可能會用上 `$ test.html` ```htmlmixed= <body> <input type='button'/> <a href='index.html'>Index</a> <script> var btn = document.querySelector('input'); btn.addEventListener('click', function () { // 下一頁 // history.forward(); history.go(1); }) </script> </body> ``` `$ index.html` ```htmlmixed= <body> <input type='button'/> <script> var btn = document.querySelector('input'); btn.addEventListener('click', function () { // 上一頁 // history.back(); history.go(-1); }) </script> </body> ``` ## offset > - 動態得到元素的偏移量, 大小, 老爸等 > - `elem.offsetParent` : 帶有定位的才是老爸 > - vs. `parentNode` : 不管有沒有定位都是老爸 > - `elem.offsetTop` : 距離帶有定位老爸的上面 > - `elem.offsetLeft` : 距離帶有定位老爸的左邊 > - `elem.offsetWidth` : 包含padding, border的寬 > - `elem.offsetHeight` : 包含padding, border的高 > - 返回值都沒有單位 > - 沒有 `offsetRight` 跟 `offsetBottom` ```htmlmixed= <head> <meta charset='utf-8'> <style> * { margin: 0; padding: 0; } .fa { height: 200px; width: 200px; border: 1px solid blue; margin: 100px; } .son { height: 100px; width: 100px; border: 1px solid black; margin: 30px; } </style> </head> <body> <div class='fa'> <div class='son'></div> </div> <script> var fa = document.querySelector('.fa'); console.log(fa.offsetTop); // 100 console.log(fa.offsetLeft); // 100 var son = document.querySelector('.son'); console.log(son.offsetTop); // 131 son.margin + fa.border + fa.margin // (30+1+100) console.log(son.offsetLeft); // 131 console.log(son.offsetWidth); // 102 (100+1+1) console.log(son.offsetHeight); // 102 (100+1+1) console.log(son.offsetParent); // <body></body> fa 沒有定位 console.log(son.parentNode); // <div class='fa' ...></div> console.log('---relative---') fa.style.position = 'relative'; console.log(son.offsetTop); // 30 console.log(son.offsetLeft); // 30 console.log(son.offsetParent); // <div class='fa' ...></div> console.log(son.parentNode); // <div class='fa' ...></div> </script> </body> ``` ### style 與 offset 區別 > - style > - 只能得到行內式樣式 > - 有單位 > - 不含 padding, border > - 可以讀寫 > - offset > - 在哪都可以得到 > - 沒有單位 > - 含 padding, border > - 只能讀 > - 說白了 style 就是code裡的屬性 > - offset 是瀏覽器經過計算的結果 ```htmlmixed= <head> <meta charset='utf-8'> <style> * { padding: 0; margin: 0; } div { height: 100px; width: 100px; border: 1px solid red; } </style> </head> <body> <div style='width: 100px;'></div> <script> var div = document.querySelector('div'); console.log(div.offsetWidth); // 102 console.log(div.style.width); // 100px </script> </body> ``` ### 練習: 獲取滑鼠在盒子內的相對座標 > - 利用 `elem.pageX` 跟 `elem.pageY` 跟 `mousemove` 來找到盒子在可視區的座標 > - 利用 `offsetTop` 跟 `offsetLeft` 找到盒子相對於可視區的位置 > - 兩個相減就好了 ```htmlmixed= <head> <meta charset='utf-8'> <style> * { margin: 0; padding: 0; } div { height: 300px; width: 300px; background: skyblue; margin: 100px; } </style> </head> <body> <div></div> <script> var div = document.querySelector('div'); div.addEventListener('click', function (e) { // event 兼容 var e = e || window.event; // 滑鼠點擊的位置 - 盒子的位置 = 滑鼠在盒子的位置 var x = getPage(e).pageX - this.offsetLeft; var y = getPage(e).pageY - this.offsetTop; console.log(x, y); }) // scroll 兼容 function getScroll() { var scrollTop = document.body.scrollTop || document.documentElement.scrollTop; var scrollLeft = document.body.scrollLeft || document.documentElement.scrollLeft; return { scrollTop: scrollTop, scrollLeft: scrollLeft, } } // 距離兼容 function getPage(e) { var pageX = e.pageX || e.clientX + getScroll().scrollTop; var pageY = e.pageY || e.clientY + getScroll().scrollLeft; return { pageX: pageX, pageY: pageY, } } </script> </body> ``` ### 練習: 拖拉盒子 > - 拖拉盒子三動作: 左鍵(mousedown) -> 拖拉(mousemove) -> 放開左鍵(mouseup) > - 特點: > - 滑鼠按下左鍵後, 鼠標相對於盒子的位置都是相同的 > - 如果只是把滑鼠的位置=盒子的話, 滑鼠會在盒子的左上角 > - 應先計算滑鼠在盒子的哪, 盒子的位置減去滑鼠的偏移量 = 盒子的位置 > - `elem.e.pageX(Y)` - `elem.offsetLeft(Top)` = 滑鼠按下時的位置 > - `document.e.pageX(Y)` - 滑鼠的偏移 = 盒子最終的偏移 > - 注意, 如果盒子有設margin的話, 要把margin 減回來, 否則會偏移 ```htmlmixed= <head> <meta charset='utf-8'> <style> * { padding: 0; margin: 0; } body { display: relative } div { height: 250px; width: 500px; background-color: skyblue; position: absolute; top: 100px; left: 100px; display: none; cursor: move; } div i { display: inline-block; position: absolute; line-height: 50px; width: 50px; background: pink; text-align: center; right: -25px; top: -25px; border-radius: 50%; } </style> </head> <body> <h1>點我</h1> <div> <i>關閉</i> </div> <script> var h1 = document.querySelector('h1'); var div = document.querySelector('div'); // 點擊顯示盒子 h1.addEventListener('click', function () { div.style.display = 'block'; }) // 點擊關閉鈕隱藏盒子 var close = document.querySelector('div i'); close.addEventListener('click', function () { div.style.display = 'none'; }) // 左鍵按下後需要偵測兩件事: 滑鼠的位置 與 左鍵放開 div.addEventListener('mousedown', function (e) { // 先計算鼠標相對於盒子的位置 // 按下時的XY - 盒子距離可視區的上左 var downBoxX = e.pageX - div.offsetLeft; var downBoxY = e.pageY - div.offsetTop; // 因為鼠標鬆開後需要移除 mousemove 的事件, 否則盒子會一直跟著滑鼠 // 所以為了使用變量名來移除事件比較方便, 所以拿出來寫 // 滑鼠在螢幕上的位置 - 滑鼠相對於盒子的位置 就是盒子現在的位置 function move(e) { div.style.left = e.pageX - downBoxX + 'px'; div.style.top = e.pageY - downBoxY + 'px'; } // 偵測滑鼠位置, 且利用滑鼠位置的改變來修改盒子的位置 document.addEventListener('mousemove', move); // 偵測左鍵放開後要執行取消移動事件 document.addEventListener('mouseup', function () { document.removeEventListener('mousemove', move); }) }) </script> </body> ``` > - 盒子有 margin ```htmlmixed= <head> <meta charset='utf-8'> <style> div { height: 100px; width: 100px; background: red; position: absolute; left: 50%; top: 50%; /* 盒子有設 margin */ margin-left: -50px; margin-top: -50px } </style> </head> <body> <div></div> <script> var box = document.querySelector('div'); box.addEventListener('mousedown', function (e) { var mouseInBoxX = e.pageX - box.offsetLeft; var mouseInBoxY = e.pageY - box.offsetTop; console.log(mouseInBoxX, mouseInBoxY) document.addEventListener('mousemove', mouseMove) function mouseMove(e) { // 這裡要弄回來, 某則定位盒子後還會偏移 margin 的量 box.style.left = e.pageX - mouseInBoxX + 50 + 'px'; box.style.top = e.pageY - mouseInBoxY + 50 + 'px'; } document.addEventListener('mouseup', function (e) { document.removeEventListener('mousemove', mouseMove); }) }) </script> </body> ``` ### 練習: 圖片放大 > - 滑鼠碰到盒子, 顯示遮蓋區跟放大圖, 離開則隱藏 > - `elem.onmouseover` `elem.onmouseout` `display` > - 遮蓋區的位置 = 滑鼠在盒子的位置 位移遮蓋區的寬高一半(置中) > - 避免讓遮蓋區跑出盒子 > - 位置為負數: 強制=0 > - 位置超過可移動區: 強制=可移動區 > - 可移動區: 盒子的大小 - 遮蓋區的大小 > - 移動放大圖 > - 遮蓋區移動的距離(現在位置) / 遮蓋區可移動距離 = 放大圖移動的距離 / 放大圖可移動距離 > - 放大圖可移動距離 = 圖片的大小 - 放大盒的大小 > - 放大圖移動的距離 = 遮蓋區移動的距離 / 遮蓋區可移動距離 * 放大圖可移動距離 > - 圖片移動的方向應該跟遮蓋圖相反的, 所以加個負號 ```htmlmixed= <head> <meta charset='utf-8'> <style> * { padding: 0; margin: 0; } .imgBox { height: 200px; width: 200px; /* 註1 */ border: 1px solid blue; margin: 200px; position: relative; cursor: move; } .mask { height: 100px; width: 100px; background-color: yellow; position: absolute; top: 0; left: 0; opacity: .5; display: none; } .big { position: absolute; left: 210px; top: 0; height: 300px; width: 300px; border: 1px solid skyblue; overflow: hidden; display: none; } .big img { position: absolute; top: 0; left: 0; } </style> </head> <body> <div class='imgBox'> <img src='https://api.fnkr.net/testimg/200x200/ccc'/> <!-- 放大區(塊) --> <div class='mask'></div> <!-- 放大圖的盒子 --> <div class='big'> <!-- 放大圖--> <img src='https://api.fnkr.net/testimg/600x600/ccc'/> </div> </div> <script> var imgBox = document.querySelector('.imgBox'); var mask = document.querySelector('.mask'); var big = document.querySelector('.big'); var bigImg = document.querySelector('.big img'); // 鼠標碰到盒子會顯示放大區跟放大圖 imgBox.addEventListener('mouseover', function () { mask.style.display = 'block'; big.style.display = 'block'; }) // 鼠標離開盒子會隱藏放大區跟放大圖 imgBox.addEventListener('mouseout', function () { mask.style.display = 'none'; big.style.display = 'none'; }) // 移動放大區跟移動放大圖 imgBox.addEventListener('mousemove', function (e) { // 先找到鼠標在盒子的位置( 在螢幕上的位置 - 盒子距離螢幕的位置 ) var mouseXInBox = e.pageX - imgBox.offsetLeft; var mouseYInBox = e.pageY - imgBox.offsetTop; // 如果直接把鼠標的位置當成放大區的位置, 那滑鼠會在放大區的左上角 // 為了讓鼠標在放大區的中間, 必須讓盒子的位置往上跟往左移動盒子的一半 var maskTop = mouseYInBox - mask.offsetHidth / 2; var maskLeft = mouseXInBox - mask.offsetWidth / 2; // 為了讓放大區不跑出盒子, // 盒子的上、左值小於 0 的時候就要等於 0 // 盒子的上、左值大於最大可移動的空間值, 就要等於最大可移動空間 // 最大可移動空間就 = 盒子寬高 - 放大區的寬高 var maxMaskMoveX = imgBox.offsetWidth - mask.offsetWidth - 2; var maxMaskMoveY = imgBox.offsetHeight - mask.offsetHeight - 2; // 這邊就是判斷有沒超過盒子 // 註2 if (maskTop <= 0) { maskTop = 0; } else if (maskTop > maxMaskMoveY) { maskTop = maxMaskMoveY; } if (maskLeft <= 0) { maskLeft = 0; } else if (maskLeft > maxMaskMoveX) { maskLeft = maxMaskMoveX; } mask.style.top = maskTop + 'px'; mask.style.left = maskLeft + 'px'; // 由於放大區移動 1px 不等於 放大圖移動的距離, // 必須計算放大區移動 1px 等於放大圖移動多少 // 也就是放大區在盒子移動多少比例, 就等於放大圖在放大圖的盒子移動多少比例 // 放大區移動的距離 / 放大圖可移動的距離 = 放大圖移動的距離 / 放大圖可移動的距離 // 計算放大圖可移動的距離, +2 是 border var maxBigImgMoveY = bigImg.offsetHeight - big.offsetHeight + 2; var maxBigImgMoveX = bigImg.offsetWidth - big.offsetWidth + 2; // 放大區移動的距離 / 放大圖可移動的距離 = 放大圖移動的距離 / 放大圖可移動的距離 // 放大圖移動的距離 = 放大區移動的距離 / 放大圖可移動的距離 * 放大圖可移動的距離 var bigImgMoveX = maskLeft / maxMaskMoveX * maxBigImgMoveX; var bigImgMoveY = maskTop / maxMaskMoveY * maxBigImgMoveY; // 圖片移動應該反方向, 所以加個負號 bigImg.style.top = -bigImgMoveY + 'px'; bigImg.style.left = -bigImgMoveX + 'px'; }) </script> </body> ``` ```javascript= // 註一 // width 記得寫清楚, 否則整個畫面都是你的觸發事件區 // 註二 /* 我一開始的寫法 mask.style.top = maskTop + 'px'; mask.style.left = maskLeft + 'px'; if (maskTop <= 0) { maskTop = 0; } else if (maskTop > maxMaskMoveY) { mask.style.top = maxMaskMoveX + 'px'; } if (maskLeft <= 0) { maskLeft = 0; } else if (maskLeft > maxMaskMoveX) { mask.style.left = maxMaskMoveY + 'px'; } * 遇到一個問題: 下面計算大圖比例的時候會需要用到 maxMaskMoveX * 而這邊我沒有把 maxMaskMoveX 卡死, 而是直接屬性改值 * 造成下面計算比例的時候搞很久, 找不到哪裡有問題 ==m */ ``` ### client > - `elem.clientTop` : 元素 border-top 大小 > - `elem.clientLeft` : 元素 border-left 大小 > - `elem.clientWidth` : 元素寬(不含border, 包含padding) > - `elem.clientHeight` : 元素高(不含border, 包含padding) ```htmlmixed= <head> <meta charset='utf-8'> <style> div { width: 100px; height: 100px; border: 5px solid red; padding: 10px } </style> </head> <body> <div></div> <script> var div = document.querySelector('div'); console.log(div.offsetWidth); // 130 console.log(div.clientWidth); // 120 console.log(div.clientLeft); // 5 // 只有上左 console.log(div.clientRight); // undefined </script> </body> ``` ### scroll > - `elem.scrollTop` : 被捲上去的==內容==高度 > - `elem.scrollLeft` : 被捲過去的==內容==寬度 > - `elem.scrollWidth` : > - 實際內容大小 > - 如果內容沒有超出盒子, 那就返回盒子內大小(不含border) > - vs. `elem.clientWidth` : 單純的盒子寬度(不含border) > - `elem.scrollHeight` > - `onscroll`事件: 滾動條發生變化時觸發事件 ```htmlmixed= <head> <meta charset='utf-8'> <style> div { width: 100px; height: 100px; border: 5px solid red; padding: 10px; overflow: auto; } </style> </head> <body> <div></div> <div id='content'> 我是內容 我是內容 我是內容 我是內容 我是內容 我是內容 我是內容 我是內容 我是內容 我是內容 我是內容 我是內容 </div> <script> var div = document.querySelector('div'); var content = document.getElementById('content'); console.log(div.clientHeight); // 120 console.log(div.scrollHeight); // 120 console.log(content.clientHeight); // 120 console.log(content.scrollHeight); // 196 content.addEventListener('scroll', function () { console.log(content.scrollTop); // 捲到底 76 (196-120) }) </script> </body> ``` ### 側邊欄變化(window.pageOffset) > - `window.pageYOffset`: 頁面被捲去的頭部部分 > - vs. 元素被捲去的部分: `elem.scrollTop` > - `window.pageXOffset` : 頁面被捲去的左側部分 ```htmlmixed= <!DOCTYPE html> <!-- DTD--> <html lang='en'> <head> <meta charset='utf-8'> <style> * { padding: 0; margin: 0; } /* 版心 */ .w { width: 960px; margin: auto; } .head { height: 300px; background-color: blue; } .banner { height: 300px; background-color: skyblue; } .foot { height: 1200px; background-color: darkblue; } .slide { height: 100px; width: 30px; background-color: orange; position: absolute; top: 450px; /* 位置卡在banner中間下面 */ left: 50%; margin-left: 480px; } span { position: absolute; bottom: 0; display: none; } </style> </head> <body> <div class='head w'></div> <div class='banner w'></div> <div class='foot w'></div> <div class='slide'> <span>Go Back</span> </div> <script> // 為了拿到 slide 一開始距離 window 的值, 所以寫外面 var slide = document.querySelector('.slide'); var slideTop = slide.offsetTop; document.addEventListener('scroll', function () { var banner = document.querySelector('.banner'); var foot = document.querySelector('.foot'); var span = document.querySelector('span'); // console.log(slide.offsetTop, banner.offsetTop); // var slideFixTop = slide.offsetTop - banner.offsetTop; // 必須將 slide.offsetTop 的值固定, 否則每次滾動都會減一次 300 // (450, 300) -> (150, 300) -> (-150, 300) -> ... // 所以在事件函數外存起來 var slideFixTop = slideTop - banner.offsetTop; // pageYOffset -> 螢幕滾動掉的頭部 if (window.pageYOffset >= banner.offsetTop) { slide.style.position = 'fixed'; slide.style.top = slideFixTop + 'px'; } else if (window.pageYOffset < banner.offsetTop) { slide.style.position = 'absolute'; slide.style.top = '450px' } if (window.pageYOffset >= foot.offsetTop) { span.style.display = 'block'; } else if (window.pageYOffset < foot.offsetTop) { span.style.display = 'none'; } }) </script> </body> </html> ``` > - 兼容性問題 > - 如果有聲明 DTD: > - `document.documentElement.scrollTop` > - `document.documentElement.scrollLeft` > - 如果沒聲明 DTD: > - `document.body.scrollTop` > - `document.body.scrollTop` > - IE9 以後支持 > - `window.pageYOffset` > - `window.pageXOffset` ```javascript= function getScroll() { return { left: window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0, top: window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0 }; } getScroll().left; getScroll().top; ``` ### 比較 > - `offset`: > - 返回包含border, padding > - 常用 `offsetTop`, `offsetLeft` 來找元素位置 > - `client`: > - 返回包含padding, 不含border > - 常用 `clientWidth`, `clientHeight` 來找元素寬高 > - `scroll`: > - 返回包含padding, 不含border, > - 如內容大於盒子, 返回內容 > - 常用 `scrollTop`, `scrollLeft` 來找元素滾動多少 > - `window.pageXOffset`, `window.pageYOffset` 常用於找螢幕滾動多少 ## mouseenter > - 當鼠標觸經過元素就會觸發事件 > - 與 `mouseover` 類似 > - 兩者差別在於 `mouseenter` 不冒泡 > - 亦即在父元素設 `mouseenter` , 鼠標經過子元素不會觸發 > - 鼠標經過設置 `mouseover` 的子元素會觸發, 因為冒泡會從target到document, > 子元素沒有事件後會往上找父元素有沒有, 所以會觸發 > - 搭配 `mouseenter` 的離開元素為 `mouseleave` > - `mouseover` <-> `mouseout` > - 冒泡在某些地方常常會是個問題, 例如放大圖片的案例, 滑鼠進到觸發區時, 會不斷觸發而造成閃爍, 因為觸發區觸發, 遮蓋圖也觸發(冒泡), 所以有需要冒泡功能的時候再用 `mouseover` 跟 `mouseout` 就好了 ```htmlmixed= <head> <meta charset='utf-8'> <style> .fa { height: 200px; width: 200px; border: 1px solid blue; } .son { height: 100px; width: 100px; border: 1px solid red; } </style> </head> <body> <div class='fa'> <div class='son'></div> </div> <div class='fa'> <div class='son'></div> </div> <script> var fa = document.querySelectorAll('.fa'); fa[0].addEventListener('mouseenter', function () { console.log('0'); }) fa[1].addEventListener('mouseover', function () { console.log('1'); }) </script> </body> ``` ### 模擬滾動條 > - 特點: > - 滾動條的長短跟內容的多寡成反比 > - 先判斷內容有沒有超過盒子, 沒超過就隱藏滾動條 > - 滾動條的長度 / 滾動條盒的長度 = 內容盒的長度 / 內容的長度 > - `mousedown` `mouseover` `mouseup` 操作 > - `mousedown` 點擊時判斷滑鼠在滾動條的位置 > - `mouseover` > - 滑鼠滑動的位置 - 滑鼠的位置 = 滾動條的位置 > - 判斷不能超出 > - 內容移動距離 / 內容可移動距離 = 滾動條移動距離 / 滾動條可移動距離 ```htmlmixed= <head> <meta charset='utf-8'> <style> .box { height: 300px; width: 300px; border: 1px solid red; margin: 100px auto; position: relative; overflow: hidden; } .content { height: 100%; width: 270px; position: absolute; } .scroll { width: 30px; height: 100%; background: gray; position: absolute; right: 0; top: 0; } .scroll .bar { height: 30px; width: 30px; background: skyblue; position: absolute; } </style> </head> <body> <div class='box'> <div class='content'> 靜靜凝視窗前 腦海的思緒像花 在冬季睡眠 天邊已泛彩霞 這一刻都市像畫 但始終一個歸家 轉眼間 太多的變化 人潮內聽這鬧市的笑話 瞬間所有悲傷 都只覺渺小 慢慢學會 世界若暫停 仍不再重要 其實 命運就像大廈 如都市幻化 凌亂如燈火中的密碼 露台 看看世界吧 這個天下 閃爍風光不再嗎 經過幾多變化 夜幕來襲森林 這朵花 不會盛開 但始終想再喧嘩 花與花 再不懂去愛嗎 活在大世界化做敵人 如此冷酷嗎 其實 命運就像大廈 如都市幻化 凌亂如燈火中的密碼 露台 看看世界吧 這個天下 用盡方法建立這一個家 每朵 高高低低的花 為都市腐化 零落時找到它的代價 但仍有個美態吧 不怕倒下 天邊的一顆碎花 為誰而腐化 其實 命運就像大廈 如都市幻化 凌亂如燈火中的密碼 露台 看看世界吧. 這個天下 用盡方法建立這一個家 卑躬屈膝的花 為都市腐化 零落時擦亮這個家 但仍有個美態吧 不怕倒下 花開花飛花似畫 經得起變化 </div> <div class='scroll'> <div class='bar'></div> </div> </div> <script> var content = document.querySelector('.content'); var bar = document.querySelector('.bar'); var scroll = document.querySelector('.scroll'); var box = document.querySelector('.box'); // 取得滾動條的高度 /* 如果內容不超過盒子, 隱藏滾動 */ if (content.scrollHeight <= content.clientHeight) { scroll.style.display = 'none'; } else { // 滾動條與內容應該是反比關係(內容越多, 滾動條越小) // 內容盒子高度 / 內容高度 = 滾動高度 / 滾動盒子高度 // 滾動高度 = 內容盒子高度 / 內容高度 * 滾動盒子高度 bar.style.height = content.clientHeight / content.scrollHeight * scroll.clientHeight + 'px'; // console.log(content.clientHeight,content.scrollHeight, scroll.clientHeight ) // console.log(bar.style.height); // console.log(bar.clientHeight); } // 操作滾動條 var maxBarMove = scroll.clientHeight - bar.clientHeight; bar.addEventListener('mousedown', function (e) { // 找到滑鼠點下滾動條的相對位置 var mouseInBarY = e.pageY - bar.offsetTop - box.offsetTop; // console.log(bar.offsetTop, e.pageY, mouseInBarY); // 放開滑鼠會移除事件, 所以拿出來寫 function mouseMove(e) { // 滾動條的位置 var barY = e.pageY - mouseInBarY - box.offsetTop; // 避免滾動條超出去 if (barY < 0) { barY = 0; } else if (barY > maxBarMove) { barY = maxBarMove; } bar.style.top = barY + 'px' // 移動滾動條時, 與內容的關係 // 滾動條移動的距離 / 滾動條最大可移動距離 = 內容移動的距離 / 內容最大可移動的距離; var maxContentMove = content.scrollHeight - content.clientHeight; // console.log(maxContentMove); // console.log(barY, bar.offsetTop); // 滾動條跟內容的移動方向相反(負號) content.style.top = -(bar.offsetTop / maxBarMove * maxContentMove) + 'px'; } document.addEventListener('mousemove', mouseMove); document.addEventListener('mouseup', function () { document.removeEventListener('mousemove', mouseMove); }) }) </script> </body> ``` ## JS 動畫 ### 原理 > - 利用 `setInterval()` 來重複操作 > - 利用 `clearInterval()` 來停止重複 > - 利用 `elem.style.top`, `elem.style.left` 來改變元素位置 > - 利用 `elem.offsetTop`, `elem.offsetLeft` 來查找元素位置 ```htmlmixed= <head> <meta charset='utf-8'> <style> * { padding: 0; margin: 0; } div { height: 100px; width: 100px; background-color: skyblue; position: absolute; left: 0px; top: 10px; } </style> </head> <body> <div></div> <script> var div = document.querySelector('div'); // 必須設變量, 因為要設清除定時條件 // 每 30 毫秒走 1px, 走到 200px 就停 var timer = setInterval(function () { if (div.offsetLeft >= 200) { clearInterval(timer); } else { div.style.left = div.offsetLeft + 1 + 'px'; } }, 30) </script> </body> ``` ### 簡易封裝動畫 `function animate(elem, target) {}` > - 簡單的動畫操作一般需要: > - 指定的元素要做移動 > - 停損點 ```htmlmixed= <head> <meta charset='utf-8'> <style> div { height: 200px; width: 200px; background-color: skyblue; position: absolute; left: 0; } .one { top: 0; } .two { top: 300px; } </style> </head> <body> <div class='one'></div> <div class='two'></div> <script> var one = document.querySelector('.one'); var two = document.querySelector('.two'); function animate(elem, target) { // 兩個參數: 指定元素, 停損目標 var timer = setInterval(function () { if (elem.offsetLeft >= target) { clearInterval(timer); } else { elem.style.left = elem.offsetLeft + 1 + 'px'; } }, 30) } animate(one, 200); animate(two, 100); </script> </body> ``` > - 問題1. 如果有一百個函數調用, 那就要創100個 timer 變量, 浪費空間 > - 既然 one, two 等 DOM 已經是對象了, 何不直接塞變量進去對象裡就好了? > - 邏輯 > ``` > a = {} > a.apple = 100 > a // {apple: 100} > ``` > - 實作: 把 timer 當成 key 塞進去就好了 > - 問題2. > - 如果我設條件事件去執行, 例如click後做動畫, > - 如果我多次觸發事件( 例如一直按元素來觸發 ), 會導致動畫函數(計時器)多次調用 > - 解決: 函數的第一件事應該先清除計時器, 避免重複調用 ```htmlmixed= <head> <meta charset='utf-8'> <style> div { height: 200px; width: 200px; background-color: skyblue; position: absolute; left: 0; } .one { top: 0; } .two { top: 300px; } </style> </head> <body> <div class='one'></div> <div class='two'></div> <script> var one = document.querySelector('.one'); var two = document.querySelector('.two'); function animate(obj, target) { // 第一件事就是清除計時器, 避免重複調用 // 否則每次點擊就會快一倍, 因為 // animate(two, 100); -> 30毫秒+1 // animate(two, 100); -> 30毫秒+1 --> 30毫秒+2 // ... // n 個 animate() 同時在運算 obj.offsetLeft + 1 的值 if (obj.timer) { clearInterval(obj.timer); obj.timer = null; } // 把 var timer 創建新變量來放函數 // 改成把函數塞到 DOM 的對象中 // 以減少創建新空間的機會 obj.timer = setInterval(function () { if (obj.offsetLeft >= target) { clearInterval(obj.timer); } else { obj.style.left = obj.offsetLeft + 1 + 'px'; } }, 30) } animate(one, 200); two.addEventListener('click', function () { animate(two, 100); }) </script> </body> ``` ### 緩動動畫 > - 讓動畫越走越慢到結束 > - 參考公式: 位置 = 現在位置 + (目標位置 - 現在位置) / 10 > - 假設目標100 > - 0 + ( 100-0 ) / 10 = 10 > - 10 + ( 100-10 ) / 10 = 19 > - 19 + ( 100-19 ) / 10 = 27.1 > - ... > - 如果目標位置在後面, 結果就會是負值, 所以判斷語句可以不要 > 跟 < , > 直接改成等於來判斷即可, 這樣可以達成往後走的效果 > - 問題1: > 由於過多位數的小數運算轉二進制有問題(應該吧), > 又或者公式只能達到趨近於目標(應該吧), > 造成無法達到目標的結果(目標100, 結果96.4) > - 解決辦法就是直接取整 > - 問題2: 如何取整 > - 為了必須讓盒子動, 所以取整的值不能為0, 否則會卡住 > - Math.ceil(-0.1) = -0 > - Math.floor(0.1) = 0 > - 所以取整必須至少是 1 或 -1 > - Math.ceil(0.1) = 1 > - Math.floor(-0.1) = -1 > - 也就是正數向上取整(ceil), 負數項下取整(floor) ```htmlmixed= <head> <meta charset='utf-8'> <style> div { height: 200px; width: 200px; background-color: skyblue; position: absolute; left: 0; } .one { top: 0; } .two { top: 300px; } </style> </head> <body> <button id='oneH'>100</button> <button id='twoH'>200</button> <div class='two'></div> <script> var two = document.querySelector('.two'); var btnOH = document.querySelector('#oneH'); var btnTH = document.querySelector('#twoH'); function animate(obj, target) { clearInterval(obj.timer); obj.timer = setInterval(function () { // 我把 >= 改成 ==, 為了讓盒子能倒著走 // 如果 >= 的話就無法倒著走了, 因為目標值在後面 if (obj.offsetLeft == target) { console.log('0'); clearInterval(obj.timer); } else { // 勻速度= 現在位置移動固定值 // obj.style.left = obj.offsetLeft + 1 + 'px'; // 緩速度= 現在位置移動的速度越來越慢, // 參考公式: (目標-現在) / 10 // BUG, 須取整 // obj.style.left = obj.offsetLeft + (target - obj.offsetLeft) / 10 + 'px' // 解決辦法: 全部每個步數都取整 // 正數向上取整(ceil), 負數向下取整(floor) var step = (target-obj.offsetLeft) / 10; step = step > 0 ? Math.ceil(step): Math.floor(step); obj.style.left = obj.offsetLeft + step + 'px'; } }, 30) } btnOH.addEventListener('click', function () { animate(two, 100); }) btnTH.addEventListener('click', function () { animate(two, 200); }) </script> </body> ``` ### 緩動加回調 > - 回調函數: 達成觸發條件時調用, 例如點擊 onclick 的元素 > - 作法: if判斷條件達成時, 調用函數 > - 實作: 在剛剛的緩動到目標時, 改變元素的背景色 ```htmlmixed= <head> <meta charset='utf-8'> <style> div { height: 200px; width: 200px; background-color: skyblue; position: absolute; left: 0; } .one { top: 0; } .two { top: 300px; } </style> </head> <body> <button id='oneH'>100</button> <button id='twoH'>200</button> <div class='two'></div> <script> var two = document.querySelector('.two'); var btnOH = document.querySelector('#oneH'); var btnTH = document.querySelector('#twoH'); // 讓函數傳進來 function animate(obj, target, callback) { clearInterval(obj.timer); obj.timer = setInterval(function () { if (obj.offsetLeft == target) { console.log('0'); clearInterval(obj.timer); // 進來表示目標已達成, 這個if就很適合當成觸發條件 // 如果有傳函數進來就調用 if (callback) { callback() } } else { var step = (target-obj.offsetLeft) / 10 step = step > 0 ? Math.ceil(step): Math.floor(step); obj.style.left = obj.offsetLeft + step + 'px'; } }, 30) } btnOH.addEventListener('click', function () { // 把函數傳進去 animate(two, 100, function() { two.style.backgroundColor = 'orange'; }); }) btnTH.addEventListener('click', function () { animate(two, 200, function () { two.style.backgroundColor = 'skyblue'; }); }) </script> </body> ``` ### 解藕 > - 為了讓函數能重複使用, 可以把函數拆出來獨立成一個 js 檔 > - 要使用的html再導入就好了 `$ vi animate.js` ```javascript= function animate(obj, target, callback) { clearInterval(obj.timer); obj.timer = setInterval(function () { if (obj.offsetLeft == target) { console.log('0'); clearInterval(obj.timer); /* 下面那種寫法更簡潔 if (callback) { callback() } */ // 利用 && 需兩者為真的特性 // 如果 callback 有東西傳進來 -> 調用函數 // 如果 callback 沒傳進來 -> 結束 callback && callback(); } else { var step = (target-obj.offsetLeft) / 10 step = step > 0 ? Math.ceil(step): Math.floor(step); obj.style.left = obj.offsetLeft + step + 'px'; } }, 30) } ``` `$ vi test.html` ```htmlmixed= <!DOCTYPE html> <html lang='en'> <head> <meta charset='utf-8'> <style> div { height: 200px; width: 200px; background-color: skyblue; position: absolute; left: 0; } .one { top: 0; } .two { top: 300px; } </style> <!--引入函數--> <script src='animate.js'></script> </head> <body> <button id='oneH'>100</button> <button id='twoH'>200</button> <div class='two'></div> <script> var two = document.querySelector('.two'); var btnOH = document.querySelector('#oneH'); var btnTH = document.querySelector('#twoH'); btnOH.addEventListener('click', function () { // 調用函數 animate(two, 100) }) btnTH.addEventListener('click', function () { animate(two, 200) }) </script> </body> </html> ``` `$ vi test2.html` ```htmlmixed= <head> <meta charset='utf-8'> <style> .slideBar { height: 100px; width: 60px; background-color: skyblue; position: fixed; right: 0; bottom: 30px; } .row { height: 60px; width: 100px; background-color: darkblue; position: absolute; top: 0; left: 0; } span { position: absolute; top: 10px; left: 20px; z-index: 999; color: white; font-size: 30px; } </style> <script src='test.js'></script> </head> <body> <div class='slideBar'> <span>&lt</span> <div class='row'></div> </div> <script> var slideBar = document.querySelector('.slideBar'); var row = document.querySelector('.row'); var span = document.querySelector('span'); slideBar.addEventListener('mouseenter', function () { var moveX = row.clientWidth - slideBar.clientWidth; animate(row, -moveX, function () { span.innerHTML = '&gt'; }); }) slideBar.addEventListener('mouseleave', function () { var moveX = row.clientWidth - slideBar.clientWidth; animate(row, 0, function () { span.innerHTML = '&lt'; }); }) </script> </body> ``` ### relative 做動畫的問題 > - 解法: 不要用 relative 或 不要讓 offsetLeft(Top) 跟 style.left(top) 不同 ```htmlmixed= <head> <meta charset='utf-8'> <style> div { height: 100px; width: 100px; position: relative; background: blue; } </style> </head> <body> <button class='one'></button> <button class='two'></button> <div></div> <script> var div = document.querySelector('div'); var btnO = document.querySelector('.one'); var btnT = document.querySelector('.two'); btnO.addEventListener('click', function () { div.style.left = div.offsetLeft + 10 + 'px'; console.log(div.style.left, div.offsetLeft) /* 預期中的位置應該是 18 / 28 / 38 / 48 * 由於 relative 是相對於自己的原先位置坐 left / top 值 * 原本的位置在 left: 8px 的地方(預設) * 所以第一次移動時, 8+10後, 整個盒子向右移了 18, 也就是 26的位置 * 第二次移動從 26+10, 向右移了 36 而座落 44 的位置 * 每次事實上都移了 18px 18px 26 36px 44 54px 62 72px 80 */ }) btnT.addEventListener('click', function () { div.style.left = div.offsetLeft - 10 + 'px'; console.log(div.style.left, div.offsetLeft) /* 相反的, 每次都左移了 2px 70px 78 68px 76 66px 74 64px 72 */ }) </script> </body> ``` ### 輪播 <img src='https://i.imgur.com/kuCqN89.png' style='width: 200px;'/> <img src='https://i.imgur.com/19Ykh7N.png' style='width: 200px;'/><img src='https://i.imgur.com/20KLMih.png' style='width: 200px;'/> > - hover 顯示與隱藏左右按鈕 > - `overenter` 與 `overleave` > - `display: none` 與 `display: block` > - 動態生成小 li ( 幾張圖就生成幾個小點 ) > - for img 張數 > - `document.createElement('TagName');` > - `appendChild(child)` > - 第一個 `li.className = 'cur'`; > - 點擊小點點, 變成指定顏色, 且播放相應圖片 > - 變成指定顏色 > - 排他 > - `className = 'cur'` > - 播放相應圖片 > - 輪播的是裝照片的容器, 不是個別照片本身 > - 容器需定位才能動 `style.left` > - 導入動畫函數, 函數必須在調用的上面 > - 容器偏移量 = 小li 的索引 * 一張圖的寬度(盒子的寬度) > - 小 li 索引 = 0( 第一顆小 li ), 表示容器都還沒動過 `left: 0;` > - 小 li 索引 = 1( 第二顆小 li ), 表示移了一張圖距離 `left= 1 * picWidth` > - ... > - 小 li 的索引可以在創建的時候順便添加, (自定義屬性或內置屬性) > - 自定義屬性 > - `elem.setAttribute('k', v)` 與 `elem.getAttribute('k')` > - 內置屬性 > - `elem.k = 'v'` 與 `elem.k` > - 點擊左右按鈕, 圖片左右跑 > - 設一個運算變量, 每次點擊都 +1 或 -1 (點左點右) > - 運算變量 * picWidth = 偏移量 > - ps. 額外思路: > - 當我點擊右邊時, 表示按下下一個 小li > - 所以我只要調用按下一個 小li 就好了 > - 也就是說點擊後 `num++` `circle.children[num].click()` (突然想到, 還沒試過) > - 無縫滾動 > - 點擊到最後一張時, 看不出跳回第一張圖的效果 > - 如果只有正常的圖, 到最後一張圖要跳回第一張時, 會有一個明顯的跳轉效果 > - 而無縫滾動則是減少跳轉的效果 > - 概念作法: > - 在最後一張圖的後面添加第一張圖, 也就是最後一張的下一張其實沒有跳轉 > 而是再往後跑一張 > - 從添加的那張要跳到第二張時, 先將圖片位置瞬間改為第一張的位置, > 然後才往後跳到第二張 > - 代碼作法: > - 作法很多種, 可以直接在HTML結構添加一張, > 但記得在動態添加 小li 的循環 -1, 否則會多一顆 小li > - 直接 `node.cloneNode(true)` 複製一張 > 可以在動態添加完 小li 後複製, 省去一些麻煩, 邏輯也比較好 > - 由於照片數與 小li 數不同, 所以可以多設一個 小li 用的運算變量來輔助 小li 的 cur > - 點擊小點點與左右按鈕不同步 > - 因為點擊左右按鈕是由計算變量控制偏移量的, 而 小li 是由自定義屬性控制偏移量 > 造成兩者沒有任何關聯 > - 解決辦法: 小li 的 index 賦值給計算變量, > 讓計算變量知道現在是第幾張圖與第幾顆小li > - 自動輪播 > - `setInterval(fn[,howsecond])` > - 自動調用手動事件 `right.click()` > - 由於自動輪播的效果跟點擊右邊的事件一樣, > 所以直接在定時器裡調用 click 就行了, > 亦即每次觸發定時器, 都會調用一次右側按鈕 click > - hover 暫停與繼續輪播 > - 上面有寫 hover display 的效果, 直接在裡面寫清除與開啟函數即可 > - 後來發現的小Bug > - 當 小li 的計算變量 = 0, 且圖片的計算變量 = 最大值時, 也就是 > <img src='https://i.imgur.com/i8F4Zgk.png' style='width: 200px;'/> 轉 <img src='https://i.imgur.com/mrhu1e1.png' style='width: 200px;'/> > - 此時再次點擊第一科按鈕(黃色那顆), 會有一個跳轉的動作, > 因為現在圖片是clone的那張, 而按第一顆按鈕對應的是第一張 > - 解決: 讓這個動作不要動作即可 > - 判斷是不是按第一顆後, 接著判斷是不是上面那個條件, > 完全符合時, 按那個按鈕就不要動作 `return false` > - 節流 ( throttle ) > - 節流函式不管事件觸發有多頻繁,都會保證在規定時間內一定會執行一次真正的事件處理函式。 > - 說白話就是我點擊小li或左右按鈕時, 我快速點擊, 圖片就會快速切換 > 而沒有依據 animate 函數裡的緩動跑完 > - 核心: 利用回調函數, 控制一個開啟與關閉的變量 > - 點擊時關閉, 跑完後開啟 ```htmlmixed= <head> <meta charset='utf-8'> <style> * { margin: 0; padding: 0; } .box { height: 100px; width: 200px; margin: 200px auto; border: 2px solid red; position: relative; } ul { list-style: none; } li { float: left; } .photo { width: 400%; /* 隨便設, 裝得下圖就好了 */ position: absolute; /* 這個ul要做輪播, 所以要定位 */ } .left, .right { position: absolute; top: 50%; background-color: skyblue; color: orange; line-height: 30px; width: 20px; transform: translateY(-50%); padding: 3px; box-sizing: border-box; display: none; } .left { left: 0; } .right { right: 0; } .circle { width: ; height: ; position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%); } .circle li { height: 10px; width: 10px; border-radius: 50%; background-color: red; margin-right: 5px; } .circle li:last-child { margin-right: 0; } .circle .cur { background-color: yellow; } </style> </head> <body> <div class='box'> <ul class='photo'> <li> <img src='https://api.fnkr.net/testimg/200x100/ccc'/> </li> <li> <img src='https://api.fnkr.net/testimg/200x100/cf0'/> </li> <li> <img src='https://api.fnkr.net/testimg/200x100/04c'/> </li> </ul> <div class='left'>&lt</div> <div class='right'>&gt</div> <!-- li 動態添加 --> <ul class='circle'></ul> </div> <!-- 把上面寫的緩動動畫導入 --> <script src='animate.js'></script> <script> var box = document.querySelector('.box'); var left = document.querySelector('.left'); var right = document.querySelector('.right'); box.addEventListener('mouseenter', function () { // 鼠標經過時, 左右箭頭的顯示 left.style.display = 'block'; right.style.display = 'block'; // 鼠標經過box時, 停止輪播 clearInterval(timer); timer = null; }) box.addEventListener('mouseleave', function () { // 鼠標離開時, 左右箭頭的隱藏 left.style.display = 'none'; right.style.display = 'none'; // 鼠標離開時, 開啟輪播 timer = setInterval(function () { right.click(); }, 1000); }) // 動態插入小 li // 為了縮小範圍, 所以選擇的節點直接從 box 開始找, // 不過從 document 找其實也沒什麼問題 var photo = box.querySelector('.photo'); // 輪播的 ul var circle = box.querySelector('.circle'); // 裝小 li 的 ul for (i=0; i<photo.children.length; i++) { // 動態創建 li (有幾張圖, 創幾個 li) var li = document.createElement('li'); circle.appendChild(li); // 添加到裝小 li 的 ul 中 // 設置自定義屬性, 因為點擊小 li 來改變輪播圖位置需要標記 li.setAttribute('index', i); // 或者設置內置屬性 // circle.children[i].index = i; // 既然這裡在循環小 li, // 那就順便在這綁定 小li 點擊事件, 不用再出去寫一個循環 circle.children[i].addEventListener('click', function () { // console.log(num , cir, this.getAttribute('index')); // 為了避免當圖片在最後一張 (clone) 時, 按一下按鈕會有跳到第一張的動作 // 所以在這裡判斷是否按第一顆且 num 是不是最後一張, // 如果完全符合, 那按按鈕就不要有反應( 阻止默認行為 ) if (this.getAttribute('index') == 0) { if (cir == 0 && num == photo.children.length-1){ return false } } if (throt) { throt = false; // 點擊到其中一個圈時, 先清掉所有cur(排他) for (i=0; i<circle.children.length; i++) { circle.children[i].className = ''; } // 觸發的上色 this.className = 'cur'; // 設置點擊後移動圖片 // 邏輯: 小 li 的 index * 盒子寬度(圖片寬度) = photo的偏移量 // 點擊第一顆小 li -> index = 0 -> 偏移量就是 0 // 點擊第二顆小 li -> index = 1 -> 偏移一張圖的距離 // 自定義屬性移動圖片的寫法 // console.log(this.getAttribute('index')); var moveX = this.getAttribute('index') * box.clientWidth; // 內置屬性移動圖片的寫法 // console.log(this.index); // var moveX = this.index * box.clientWidth; // 連結點擊小 li 與點擊左右箭頭的關係 num = this.getAttribute('index'); cir = this.getAttribute('index'); animate(photo, -moveX, function () { throt = true; }) } }) } // 開網頁後第一顆小 li 上選擇色 circle.children[0].className = 'cur'; // 作法1 // 另外作法就是在HTML最後手動加第一張圖的li, // 且避免小li多一顆, 再上面 for 循環 -1 // 作法2 // 為了不改變HTML結構 // 直接在 circle 創建完的「後」用 clone 添加 li // 如此就能做到不會增加小 li 且增加一張圖片的效果 var cloneLi = photo.children[0].cloneNode(true); photo.appendChild(cloneLi); // 兩邊箭頭開始 var num = 0; // 控制圖片 var cir = 0; // 控制小 li var throt = true; // 節流閥 // 右箭頭 right.addEventListener('click', function () { if (throt) { throt = false; // 執行一次馬上關起來 // num 的邏輯在於判斷圖片是否最後一張(clone) // 按下去時, 如果是最後一張, 則瞬間回到第一張, 且 num 歸零 if (num == photo.children.length -1) { photo.style.left = 0; num = 0; } num++; // 此時++, num = 1 // 從第一張移動到第二張的效果 var moveX = num * box.clientWidth; animate(photo, -moveX, function () { throt = true; // 執行完再打開 }); // cir 的邏輯在於避免 cur 附於不存在的小 li 上 // 先+1 判斷有沒有超過小 li 的數量, // 如果超過就表示應該要回到第一顆小 li 了 // 而此時小 li 應該是對應 clone 的照片 // 再按一次時, 圖片瞬間從 clone 的照片回到第一張且移動到第二張 // 而 li 也到了第二顆 cir++; if (cir == circle.children.length) { cir = 0; } // 排他 for (i=0; i<circle.children.length; i++){ circle.children[i].className = ''; } circle.children[cir].className = 'cur'; } }) // 左箭頭 left.addEventListener('click', function () { if (throt) { throt = false; // 0 1 2 3(clone) // 0 -> 3 // 按下去時判斷等於 0 就要瞬間移到 clone 那張(3)的座標 // 也就是 left = -3 * width + px // left = -(length - 1) * width + px if (num == 0) { num = photo.children.length -1; photo.style.left = -num * box.clientWidth + 'px'; } num--; var moveX = num * box.clientWidth; animate(photo, -moveX, function () { throt = true; }); // 依樣的邏輯, // -1 看有沒有超過, 有超過表示應該回到最後一顆 cir--; if (cir < 0) { cir = circle.children.length -1 ; } // 排他 for (i=0; i<circle.children.length; i++){ circle.children[i].className = ''; } circle.children[cir].className = 'cur'; } }) // 自動輪播 var timer = setInterval(function () { /* 事實上就是點擊右邊的事件 if (num == 3) { photo.style.left = 0; num = 0; } num++ var moveX = num * box.clientWidth; animate(photo, -moveX); cir++ if (cir == circle.children.length) { cir = 0; } for (i=0; i<circle.children.length; i++){ circle.children[i].className = ''; } circle.children[cir].className = 'cur'; */ // 超強絕招, 直接調用 right.click(); }, 1000); </script> </body> ``` > - 自動調用手動觸發事件 ```htmlmixed= <body> <button>123</button> <script> var btn = document.querySelector('button'); btn.addEventListener('click', function () { console.log(1); }) setInterval(function () { btn.click(); }, 1000) </script> </body> ``` ### 緩動返頂 > - `window.scroll(x,y)` > - 滾動窗口至頁面的某個位置 (x,y) > - 沒有 px! > - `animate(window, 0)` > - 利用先前寫的動畫函數 > - 由於是改變頁面上下滾動而非盒子左右移動 > - 把盒子距離左邊的距離 `elem.offsetLeft` > - 改成頁面距離上面的距離 `window.pageYOffset` > - 把移動盒子 `elem.style.width` 改成滾動頁面 `window.scroll()` ```htmlmixed= <head> <meta charset='utf-8'> <style> * { margin: 0; padding: 0; } .w { width: 960px; margin: auto; } .head { height: 300px; background-color: blue; } .banner { height: 300px; background-color: yellow; } .foot { height: 1200px; background-color: darkblue; } .slide { height: 100px; width: 50px; background-color: orange; position: absolute; top: 450px; left: 50%; margin-left: 480px; } span { position: absolute; bottom: 0; display: none; background-color: red; width: 100%; } </style> </head> <body> <div class='head w'></div> <div class='banner w'></div> <div class='foot w'></div> <div class='slide'> <span>Go Back</span> </div> <script> var slide = document.querySelector('.slide'); var banner = document.querySelector('.banner'); var slideTop = slide.offsetTop; var goback = document.querySelector('span'); var foot = document.querySelector('.foot'); document.addEventListener('scroll', function () { // console.log(window.pageYOffset); // console.log(slide.offsetTop, banner.offsetTop); if (window.pageYOffset >= banner.offsetTop) { var fixY = slideTop - banner.offsetTop; // console.log(1); slide.style.position = 'fixed'; slide.style.top = fixY + 'px'; } else if (window.pageYOffset < banner.offsetTop){ slide.style.position = 'absolute'; slide.style.top = '450px'; } if (window.pageYOffset >= foot.offsetTop) { goback.style.display = 'block'; } else if (window.pageYOffset < foot.offsetTop) { goback.style.display = 'none'; } }) // 點擊 goback 後, 動畫返回頂部 goback.addEventListener('click', function() { //window.scroll(0,0); animate(window, 0); }) function animate(obj, target, callback) { clearInterval(obj.timer) obj.timer = setInterval(function() { // console.log(window.pageYOffset); var step = (target - window.pageYOffset) / 10; // console.log(step); step = step > 0 ? Math.ceil(step): Math.floor(step); // console.log(step); if (window.pageYOffset == 0){ console.log('end'); clearInterval(timer); callback && callback(); } else { // obj.style.left = obj.offsetLeft + step + 'px'; // 把移動盒子改成頁面滾動 window.scroll(x,y) // window.scroll(x,y) 沒有單位! window.scroll(0, window.pageYOffset + step); } }, 30) } </script> </body> ``` ### hover + click > - 鼠標對盒子有hover選擇的效果 > - 點擊後改變盒子的初始位置 > - 作法一: 改變 className ```htmlmixed= <head> <meta charset='utf-8'> <style> * { margin: 0; padding: 0; } nav { width: 500px; height: 300px; display: flex; } div { flex-grow: 1; border: 1px solid red; } .blue { background-color: blue; } </style> </head> <body> <nav> <div class='blue'>1</div> <div>2</div> <div>3</div> </nav> <script> var nav = document.querySelector('nav'); var divs = document.querySelectorAll('div'); var tmp = 0; for (i=0; i<divs.length; i++) { // 自定義屬性 divs[i].setAttribute('index', i); // 很普通的 hover divs[i].addEventListener('mouseenter', function () { for (i=0; i<divs.length; i++) { divs[i].className = ''; } this.className = 'blue'; }) // 很普通的 hover divs[i].addEventListener('mouseleave', function () { for (i=0; i<divs.length; i++) { divs[i].className = ''; } // hover 離開時回到『初始位置』 divs[tmp].className = 'blue'; }) // 點擊後, 儲存新的初始位置 divs[i].addEventListener('click', function () { tmp = this.getAttribute('index'); }) } </script> </body> ``` > - 作法二: 動畫版 ```htmlmixed= <!DOCTYPE html> <html lang='en'> <head> <meta charset='utf-8'> <style> * { margin: 0; padding: 0; } nav { width: 600px; height: 300px; position: relative; } .tmp { position: absolute; display: flex; height: 100%; width: 100%; } .tmp div { flex-grow: 1; border: 1px solid red; } .move { display: inline-block; width: 200px; height: 100%; position: absolute; left: 0; top: 0; background-color: blue; } </style> </head> <body> <nav> <span class='move'></span> <nav class='tmp'> <div>1</div> <div>2</div> <div>3</div> </nav> </nav> <script> var move = document.querySelector('.move'); var divs = document.querySelectorAll('div'); function animate(obj, target, callback) { clearInterval(obj.timer); obj.timer = setInterval(function () { if (obj.offsetLeft == target) { clearInterval(obj.timer); callback && callback(); } else { var step = ( target - obj.offsetLeft ) / 10; step = step > 0 ? Math.ceil(step) : Math.floor(step); obj.style.left = obj.offsetLeft + step + 'px'; } }, 30) } var cur = 0; for (var i=0; i<divs.length; i++) { // hover 觸發 divs[i].addEventListener('mouseenter', function () { // console.log(this.offsetLeft); animate(move, this.offsetLeft); }) // hover 離開, 回到『初始位置』 divs[i].addEventListener('mouseleave', function () { animate(move, cur) }) // 點擊後記錄『初始位置』 divs[i].addEventListener('click', function () { cur = this.offsetLeft; }) } </script> </body> </html> ``` ### 模擬簡單聊天室 > - CSS我就沒有做了, 主要想模擬聊天室的 JS ```htmlmixed= <head> <meta charset='utf-8'> <style> .box { height: 800px; width: 600px; border: 1px solid red; margin: 100px auto; } .header { width: 100%; height: 700px; } .header ul { width: 100%; } .header ul li { width: 100%; } .left { float: left; } .right { float: left; text-align: right; } .footer { width: 100%; height: 100px; background: skyblue; } .img { float: left; } .conver { float: left; } </style> </head> <body> <div class='box'> <div class='header'> <ul></ul> </div> <div class='footer'> <div class='img'><img id='icon' src='https://api.fnkr.net/testimg/30x30/ccc'/></div> <input type='text' class='conver'/> <button>Enter</button> </div> </div> <script> var img = document.querySelector('.img'); var icon = document.querySelector('#icon') var ul = document.querySelector('ul'); var arr = [ 'https://api.fnkr.net/testimg/30x30/ccc', 'https://api.fnkr.net/testimg/30x30/12a', ] var flag = 0; // 點擊圖片, 切換角色 img.addEventListener('click', function() { // flag0 = ccc; flag1 = 12a if (flag == 0) { icon.src = arr[1]; flag = 1; } else { icon.src = arr[0]; flag = 0; } }) // 送出訊息 var btn = document.querySelector('button'); btn.addEventListener('click', function () { var conver = document.querySelector('.conver'); var text = conver.value; // 判斷訊息是否為空 if (text !== '') { // 有訊息就創一個 li var li = document.createElement('li') li.innerText = text; // 判斷是誰講的 if (flag == 0) { // 訊息就靠哪邊顯示 li.className = 'left'; ul.appendChild(li); } else { li.className = 'right'; ul.appendChild(li); } conver.value = ''; } }) </script> </body> ``` ### 瀑布流生成圖片 <img src='https://i.imgur.com/5nkjwXK.png' style='width: 300px'/> > - 滾動滾輪到某列滾完時, 生成新照片, 每張照片要放在高度最矮的那列下面 > - 完成頁面載入時的照片排序 > - 首先要先決定幾列, 所以先計算 總寬度 / 個別佔用寬度 取整 > - 接著先將第一行照片擺上去, 然後將每列高度存起來, 比較哪列最矮 > - 接著將第二行以後的照片依序插入最矮的列, 每次插入後都要修改該列高度的數據, 讓下一張可以知道要插哪列 > - 完成滾動頁面生成照片並依序插入 > - `window.innerHeight`: 瀏覽器可視區高度 > - `window.pageYOffset`: 滾輪捲上去的內容高度 > - 兩個相加比最矮列的高度高時, 表示最矮列沒照片了, 趕快生成 > - 生成完照片記得也擺好 > - 重點雷區: > - `querySelectAll` 不是動態集合, > 亦即後來生成的標籤不會自己添加到這個方法返回的數組中 > 所以在動態創建節點後, 要再對原數組重新賦值一次 > - `getElements` 系列的是動態集合, 返回的是 `HTMLCollection` > - 存高度的時候別用 `arr.push()` , > 因為生成照片時會再次調用這個排序照片函數, 會不停的==重複==添加數據到數組中 ```htmlmixed= <head> <meta charset='utf-8'> <style> * { margin: 0; padding: 0; } .box { width: 650px; margin: 0 auto; position: relative; } .item { position: absolute; padding: 4px; border: 1px solid red; } </style> </head> <body> <div class='box'> <div class='item'> <img src='https://api.fnkr.net/testimg/200x300/ccc'/> </div> <div class='item'> <img src='https://api.fnkr.net/testimg/200x600/ccc'/> </div> <div class='item'> <img src='https://api.fnkr.net/testimg/200x400/ccc'/> </div> <div class='item'> <img src='https://api.fnkr.net/testimg/200x300/ccc'/> </div> <div class='item'> <img src='https://api.fnkr.net/testimg/200x600/ccc'/> </div> <div class='item'> <img src='https://api.fnkr.net/testimg/200x400/ccc'/> </div> <div class='item'> <img src='https://api.fnkr.net/testimg/200x300/ccc'/> </div> <div class='item'> <img src='https://api.fnkr.net/testimg/200x600/ccc'/> </div> <div class='item'> <img src='https://api.fnkr.net/testimg/200x400/ccc'/> </div> </div> <script> // 因為照片讀取比較慢, 所以用 onload window.onload = function () { var box = document.querySelector('.box'); // 這裡別用 query, 因為動態添加節點的時候, 這個數組不會動態添加 // var items = document.querySelectorAll('.item'); var items = document.getElementsByClassName('item'); var boxW = box.offsetWidth; var itemW = items[0].offsetWidth; // 列數 var column = parseInt(boxW / itemW); // margin var space = (boxW - itemW * column) / (column - 1); // console.log(boxW, itemW, column, space); // 排序照片的函數 var arr = []; function waterfull() { for (var i = 0; i < items.length; i++) { // 先排序第一行 if ( i < column ) { items[i].style.left = i * (itemW + space) + 'px'; // 千萬別用 push(), // 否則後面動態添加節點還會調用這個函數, 每調用一次就多一行數據進去 // arr.push(items[i].offsetHeight); arr[i] = items[i].offsetHeight; } else { // 剩下的照片就依序放到最矮列的下面 items[i].style.left = getMinH(arr).minI * (itemW + space) + 'px'; items[i].style.top = getMinH(arr).minV + space + 'px'; // 放完照片要記得修改該列數據 arr[getMinH(arr).minI] = getMinH(arr).minV + space + items[i].offsetHeight; } } // console.log(arr); } // 找最矮列 function getMinH(arr) { var minV = arr[0]; var minI = 0; // console.log(minV, minI); for (var i = 1; i < arr.length; i++) { if (minV > arr[i]) { minV = arr[i]; minI = i; } } // console.log(minV, minI); return { minV: minV, minI: minI } } // 滾動滾動, 動態添加 window.onscroll = function () { // 頁面滾到最矮列的底時動態添加 if (window.innerHeight + window.pageYOffset > getMinH(arr).minV) { // 數據 var json = [ {'src': 'https://api.fnkr.net/testimg/200x300/ccc'}, {'src': 'https://api.fnkr.net/testimg/200x600/ccc'}, {'src': 'https://api.fnkr.net/testimg/200x400/ccc'}, ] for (var i = 0; i < json.length; i++) { var div = document.createElement('div'); div.className = 'item'; var img = document.createElement('img'); img.src = json[i].src; div.appendChild(img); box.appendChild(div); } // 添加完數據要整理一下位置 // 如果一開始是用query找到items節點, 那這裡要再重找一次 // items = document.querySelectorAll('.item'); waterfull(); } } // 一開始照片整理位置 waterfull() //getMinH(); } </script> </body> ``` > - querySelector() 不是動態集合 ```htmlmixed= <body> <div></div> <div></div> <script> var divs = document.querySelectorAll('div'); var body = document.body; var div = document.createElement('div'); body.appendChild(div); console.log(divs); // NodeList(2) [div, div] </script> </body> ``` ### 模擬閃避障礙物的跳跳小遊戲 > - 跑酷的角色前進, 通常是背景圖的反向移動 > - 也就是操作障礙物的 x 來往角色移動 > - 操作角色的 y 來閃避障礙物 ```htmlmixed= <head> <meta charset='utf-8'> <style> * { margin: 0; padding: 0; } .gameWindow { width: 800px; height: 600px; background: url(https://picsum.photos/800/600?random=1); position: relative; } .player { width: 30px; height: 30px; position: absolute; top: 100px; left: 100px; background: darkblue; } .obstacle { width: 30px; background-color: orange; position: absolute; } </style> </head> <body> <div class='gameWindow'> <div class='player'></div> </div> <script> var gameWindow = document.getElementsByClassName('gameWindow')[0]; var player = document.getElementsByClassName('player')[0]; // 把長改的參數拉出來寫 var gameBgAttr = { x: 0, } // 把長改的參數拉出來寫 var playerAttr = { y: player.offsetTop, speedY: 5, } // 遊戲開始與結束的鎖 var gameStart = true; setInterval(function () { if (gameStart) { // 移動背景, 讓角色前進的感覺 gameBgAttr.x += -5 gameWindow.style.backgroundPositionX = gameBgAttr.x + 'px'; // 為了平衡跳跳數據, 這裡每次都加一, 否則click會直接-10而往上飛 playerAttr.speedY += 1; // 移動角色 playerAttr.y += playerAttr.speedY; // 碰到底版或天花板就停遊戲 if (playerAttr.y < 0) { gameStart = false; // 讓角色留在上面 playerAttr.y = 0; } else if (playerAttr.y + player.offsetHeight > 600) { gameStart = false; playerAttr.y = 600 - player.offsetHeight; // console.log(playerAttr.y); } player.style.top = playerAttr.y + 'px'; } }, 50) // 按一下往上跳 10 document.onclick = function () { playerAttr.speedY = -10; } // 創建障礙物 function createObstacle(x) { var divU = document.createElement('div'); var divD = document.createElement('div'); divU.className = 'obstacle'; divU.style.top = 0; divU.style.left = x + 'px'; // 上障礙物的高度設在[200~300)間 divUHeight = 200 + parseInt(Math.random() * 100) divU.style.height = divUHeight + 'px'; divD.className = 'obstacle'; divD.style.bottom = 0; divD.style.left = x + 'px'; // 下障礙物為整張圖高度 - 上障礙物 - 留空200讓角色通過 divD.style.height = 600 - divUHeight - 200 + 'px'; gameWindow.appendChild(divU); gameWindow.appendChild(divD); // 障礙物移動 setInterval(function () { if (gameStart) { divU.style.left = divU.offsetLeft - 2 + 'px'; divD.style.left = divU.offsetLeft - 2 + 'px'; // 如果障礙物超出螢幕到自己的寬度 if (divU.offsetLeft < -30) { // 回到螢幕外準備再進來 divU.style.left = '800px'; } // 當角色撞到障礙物 if (player.offsetLeft + player.offsetWidth >= divU.offsetLeft && player.offsetLeft < divU.offsetLeft && player.offsetTop < divU.offsetHeight) { gameStart = false; // 遊戲停止 } if (player.offsetLeft + player.offsetWidth >= divU.offsetLeft && player.offsetLeft < divU.offsetLeft && player.offsetTop + player.offsetHeight > divU.offsetHeight + 200) { gameStart = false; } } }, 30) } createObstacle(400) createObstacle(600) createObstacle(800) createObstacle(1000) ``` ### 模擬飛機大戰(未完成, 等我功力好點再回來做) > - 我寫了五個函數, 分別為創飛機, 創子彈, 移動飛機, 移動子彈, 檢查 > - 不過在檢查的時候一直報錯, 而且用了太多的Interval, 感覺很lag > - I will be back ```htmlmixed= <head> <meta charset='utf-8'> <style> * { margin: 0; padding: 0; } .game { width: 600px; height: 800px; background: skyblue; position: relative; } .fly { width: 30px; height: 30px; background: darkblue; position: absolute; display: none; } </style> </head> <body> <div class='game'></div> <div class='fly'></div> <script> var game = document.getElementsByClassName('game')[0]; var fly = document.getElementsByClassName('fly')[0]; var flyX; var flyY; var gameStart = false; document.onmousemove = function (e) { // console.log(e.pageX, e.pageY); if (e.pageX < game.offsetWidth && e.pageY < game.offsetHeight) { fly.style.display = 'block'; gameStart = true; flyX = e.pageX - fly.offsetWidth / 2; flyY = e.pageY - fly.offsetHeight / 2; if (flyX < 0) { flyX = 0; } else if (flyX > (game.offsetWidth - fly.offsetWidth)) { flyX = game.offsetWidth - fly.offsetWidth; } if (flyY < 0) { flyY = 0; } else if (flyY > (game.offsetHeight - fly.offsetHeight)) { flyY = game.offsetHeight - fly.offsetHeight; } fly.style.left = flyX + 'px'; fly.style.top = flyY + 'px'; } else { fly.style.display = 'none'; gameStart = false; }; } var buObj = { width: 6, height: 10, background: 'black', num: 1, arr: [], // 記錄子彈 } function createBullet() { setInterval(function () { if (gameStart) { var length = buObj.arr.length if (length < 10) { var bu = document.createElement('div'); bu.id = 'bubble' + buObj.num; buObj.arr[length] = bu.id + '|'; bu.style.width = buObj.width + 'px'; bu.style.height = buObj.height + 'px'; bu.style.background = buObj.background; bu.style.position = 'absolute'; bu.style.top = fly.offsetTop + 'px'; buObj.arr[length] = buObj.arr[length] + parseInt(bu.style.top) + '|'; bu.style.left = fly.offsetLeft + (fly.offsetWidth / 2 - buObj.width/2)+ 'px'; buObj.arr[length] = buObj.arr[length] + parseInt(bu.style.left); game.appendChild(bu); buObj.num++; } // console.log(buObj.arr); } }, 1000) } function moveBullet() { setInterval(function () { if (gameStart) { for(var i=0; i<buObj.arr.length; i++) { var newArr = buObj.arr[i].split('|'); var bubEle = document.getElementById(newArr[0]); newArr[1] = parseInt(newArr[1]) - 1; newArr[2] = parseInt(newArr[2]); bubEle.style.top = newArr[1] + 'px'; buObj.arr[i] = newArr[0] + '|' + newArr[1] + '|' + newArr[2]; if (newArr[1] < 0) { buObj.arr.splice(i, 1); game.removeChild(bubEle); } } //console.log(buObj.arr); } }, 50) } var enObj = { width: 10, height: 10, background: 'orange', num: 1, arr: [], } function createEnemy() { setInterval(function () { if (gameStart) { var length = enObj.arr.length; if (length < 10) { var en = document.createElement('div'); en.id = 'enermy' + enObj.num; enObj.num++; enObj.arr[length] = en.id + '|'; en.style.width = enObj.width + 'px'; en.style.height = enObj.height + 'px'; en.style.background = enObj.background; en.style.position = 'absolute'; en.style.top = 0; enObj.arr[length] = enObj.arr[length] + parseInt(en.style.top) + '|'; en.style.left = game.offsetWidth * Math.random() + 'px'; enObj.arr[length] = enObj.arr[length] + parseInt(en.style.left); game.appendChild(en); } } }, 1000) } function moveEnemy() { setInterval(function () { if (gameStart) { // console.log(enObj.arr); for (var i = 0; i < enObj.arr.length; i++) { var newArr = enObj.arr[i].split('|'); var enEle = document.getElementById(newArr[0]); newArr[1] = parseInt(newArr[1]) + 1; newArr[2] = parseInt(newArr[2]); enEle.style.top = newArr[1] + 'px'; enObj.arr[i] = newArr[0] + '|' + newArr[1] + '|' + newArr[2]; if (newArr[1] + enEle.offsetHeight >= game.offsetHeight) { enObj.arr.splice(i,1); game.removeChild(enEle); } } // console.log(enObj.arr); } }, 30) } /* function check() { setInterval(function () { if (gameStart) { // console.log(buObj.arr, enObj.arr); for (var i = 0; i < enObj.arr.length; i++) { var newArrEn = enObj.arr[i].split('|'); var enCheck = document.getElementById(newArrEn[0]); var xEL = parseInt(newArrEn[2]); var xER = parseInt(newArrEn[2]) + enObj.width; var yET = parseInt(newArrEn[1]); var yEB = parseInt(newArrEn[1]) + enObj.height; for (var j = 0; j < buObj.arr.length; j++) { var newArrBu = buObj.arr[i].split('|'); var buCheck = document.getElementById(newArrBu[0]); var xBL = parseInt(newArrBu[2]); var xBR = parseInt(newArrBu[2]) + buObj.width; var yBT = parseInt(newArrBu[1]); var yBB = parseInt(newArrBu[1]) + buObj.height; console.log(xBL, xBR, yBT, yBB); } } } }, 10) } */ createBullet(); moveBullet(); createEnemy(); moveEnemy(); check(); </script> </body> ```