樣式處理也許是瀏覽器中最複雜的部份,將所有細節予以封裝一定是個不錯的想法,為此,可以建立一個 Style-1.0.0.js,首先來看看 css
函式,它可以使用物件來一次設定想要的樣式:
// 可透過物件以 key : value(CSS)形式來設定樣式
function css(elem, props) {
Object.keys(props)
.forEach(name => elem.style[name] = props[name]);
}
接著將〈存取元素位置〉中的 offset
放進去:
// 取得元素確實位置
function offset(elem) {
let x = 0;
let y = 0;
for(let e = elem; e; e = e.offsetParent) {
x += e.offsetLeft;
y += e.offsetTop;
}
// 修正捲軸區域的量
for(let e = elem.parentNode; e && e != document.body; e = e.parentNode) {
if(e.scrollLeft) {
x -= e.scrollLeft;
}
if(e.scrollTop) {
y -= e.scrollTop;
}
}
return {
x,
y,
toString() {
return `(${this.x}, ${this.y})`;
}
};
}
然後準備處理〈顯示、可見度與透明度〉中 hide
、show
函式,不過在這之前要想想,原本的 hide
、show
函式直接在原生元素上新增了特性,這並不是個建議的方式(除非是為了相容性而修補物件,使之有相容於標準的新功能)。
必須要有個方法,可以為元素儲存相關特性,然而並非在元素本身,這時可以用上〈Set 與 Map〉中談過的 WeakMap
:
// 儲存元素對應的資料
let elemData = new WeakMap();
function storage(elem, data) {
if(data === undefined) {
return elemData.get(elem);
} else {
elemData.set(elem, data);
}
}
// 設定元素的相關屬性,但此實作不是直接儲存在元素上
function prePropOf(elem, prop, value) {
if(value === undefined) {
let data = storage(elem);
return data === undefined ? undefined : data[prop];
} else {
let data = storage(elem);
if(data) {
data[prop] = value;
}
else {
data = {[prop] : value};
}
storage(elem, data);
}
}
使用 WeakMap
的原因在於,如果元素已經不再被程式其他部份參考住,就可以直接 GC,WeakMap
也不會再有該元素,這可以避免記憶體洩漏的問題。
接著,就可以實現 hide
、show
函式,以及 fadeOut
、fadeIn
函式:
function computedStyle(elem, name, pseudoClz = null) {
return window.getComputedStyle(elem, pseudoClz)[name];
}
// 顯示元素
function show(elem, pseudoClz = null) {
elem.style.display = prePropOf(elem, 'display') || '';
if(computedStyle(elem, 'display', pseudoClz) === 'none') {
// 在 DOM 樹上建立元素,取得 display 預設值後移除
let node = document.createElement(elem.nodeName);
document.body.appendChild(node);
elem.style.display = style(node, 'display');
document.body.removeChild(node);
}
}
// 隱藏元素
function hide(elem, pseudoClz = null) {
let display = computedStyle(elem, 'display', pseudoClz);
prePropOf(elem, 'display', display);
elem.style.display = 'none';
}
// 取得透明度的數字
function opacity(elem, pseudoClz = null) {
let opt = computedStyle(elem, 'opacity', pseudoClz);
return opt === '' ? 1 : parseFloat(opt);
}
//speed 是動畫總時間,step 是動畫數
// 淡出
function fadeOut(elem, speed = 5000, steps = 10, pseudoClz = null) {
let preOpacity = opacity(elem, pseudoClz);
prePropOf(elem, 'opacity', preOpacity);
let timeInterval = speed / steps;
let valueStep = preOpacity / steps;
let opt = preOpacity;
setTimeout(function next() {
opt -= valueStep;
if(opt > 0) {
elem.style.opacity = opt;
setTimeout(next, timeInterval);
}
else {
elem.style.opacity = 0;
}
}, timeInterval);
}
// 淡入
function fadeIn(elem, speed = 5000, steps = 10, pseudoClz = null) {
let targetValue = prePropOf(elem, 'opacity') || 1;
let timeInterval = speed / steps;
let valueStep = targetValue / steps;
let opt = 0;
setTimeout(function next() {
opt += valueStep;
if(opt < targetValue) {
elem.style.opacity = opt;
setTimeout(next, timeInterval);
}
else {
elem.style.opacity = targetValue;
}
}, timeInterval);
}
接著就是將一些先前文件中看過的其他樣式相關函式放進去了:
// 是否有指定類別
function hasClass(elem, clz) {
let clzs = elem.className;
if(!clzs) {
return false;
} else if(clzs === clz) {
return true;
}
return clzs.search(`\\b${clz}\\b`) !== -1;
}
// 新增類別
function addClass(elem, clz) {
if(!hasClass(elem, clz)) {
if(elem.className) {
clz = ` ${clz}`;
}
elem.className += clz;
}
}
// 移除類別
function removeClass(elem, clz) {
elem.className = elem.className.replace(
new RegExp(`\\b${clz}\\b\\s*`, 'g'), '');
}
function toggleClass(elem, clz1, clz2) {
if(hasClass(elem, clz1)) {
removeClass(elem, clz1);
addClass(elem, clz2);
}
else if(hasClass(elem, clz2)) {
removeClass(elem, clz2);
addClass(elem, clz1);
}
}
// 集中取得維度用的方法
class Dimension {
static screen() {
return {
width: screen.width,
height: screen.height
};
}
static screenAvail() {
return {
width: screen.availWidth,
height: screen.availHeight
};
}
static browser() {
return {
width: window.outerWidth,
height: window.outerHeight
};
}
static html() {
return {
width: window.documentElement.scrollWidth,
height: window.documentElement.scrollHeight
};
}
static body() {
return {
width: window.body.scrollWidth,
height: window.body.scrollHeight
};
}
static viewport() {
return {
width: window.innerWidth,
height: window.innerHeight
};
}
}
// 集中取得座標用的方法
class Coordinate {
static browser() {
return {
x: window.screenX,
y: window.screenY
};
}
static scroll() {
return {
x: window.pageXOffset,
y: window.pageYOffset
};
}
}
匯出的名稱有以下這些:
export {css, offset, hide, show, fadeOut, fadeIn};
export {hasClass, addClass, removeClass, toggleClass};
export {Dimension, Coordinate};
再來就是處理 XD-1.2.0.js 了,首先匯入相關名稱:
import {css, offset, hide, show, fadeOut, fadeIn} from './Style-1.0.0.js';
import {hasClass, addClass, removeClass, toggleClass} from './Style-1.0.0.js';
在 ElemCollection
上添增一些方法:
class ElemCollection {
...
// 如果 value 為 undefined,取得元素 style 特性上對應的樣式
// 否則在元素的 style 上設定特性
style(name, value) {
let elems = this.elems;
let propName = PROPS.has(name) ? PROPS.get(name) : name;
if(value === undefined) {
return elems[0] ? elems[0].style[propName] : null;
} else {
elems.filter(elem => !isTextNode(elems[0]) && !isCommentNode(elems[0]))
.forEach(elem => elem.style[propName] = value);
return this;
}
}
// 取得計算樣式,不寫在 style() 方法中的理由在於
// 從計算樣式與 style() 方法傳回值是否為 undefined
// 可以知道樣式是來自樣式表或者是 style 設定
// 明確化來源是其目的
computedStyle(name, pseudoClz = null) {
let elems = this.elems;
let propName = PROPS.has(name) ? PROPS.get(name) : name;
return elems[0] && !isTextNode(elems[0]) && !isCommentNode(elems[0]) ?
window.getComputedStyle(elems[0], pseudoClz)[propName] : null;
}
// 可透過物件以 key : value(CSS)形式來設定樣式
css(props) {
let standardized =
Object.keys(props)
.reduce((acc, name) => {
acc[PROPS.has(name) ? PROPS.get(name) : name] = props[name];
return acc;
}, {});
this.elems.forEach(elem => css(elem, standardized));
return this;
}
// 取得元素確實位置
offset() {
let elems = this.elems;
return elems[0] ? offset(elems[0]) : null;
}
// 隱藏元素
hide(pesudoClz = null) {
this.elems.forEach(elem => hide(elem, pesudoClz));
return this;
}
// 顯示元素
show(pesudoClz = null) {
this.elems.forEach(elem => show(elem, pesudoClz));
return this;
}
// 淡出
fadeOut(speed = 5000, steps = 10, pseudoClz = null) {
this.elems.forEach(elem => fadeOut(elem, speed, steps, pseudoClz));
return this;
}
// 淡入
fadeIn(speed = 5000, steps = 10, pseudoClz = null) {
this.elems.forEach(elem => fadeIn(elem, speed, steps, pseudoClz));
return this;
}
// 第一個元素是否有指定類別
hasClass(clz) {
let elems = this.elems;
return elems[0] ? hasClass(elems[0], clz) : null;
}
// 加入類別
addClass(clz) {
this.elems.forEach(elem => addClass(elem, clz));
return this;
}
// 移除類別
removeClass() {
this.elems.forEach(elem => removeClass(elem, clz));
return this;
}
// 切換類別
toggleClass(clz1, clz2) {
this.elems.forEach(elem => toggleClass(elem, clz1, clz2));
return this;
}
}
XD-1.2.0.js 實際上是作為一個門戶(Facade),對於 Style-1.0.0.js 中的 Dimension
與 Coordinate
直接匯出就可以了:
export {Dimension, Coordinate} from './Style-1.0.0.js';
然而,XD-1.2.0.js 作為一個門戶,也必須考量到的是,ElemCollection
會不會擔負了太多職責了?在未來你可能繼續在上頭添增一些方便的方法,而使得 ElemCollection
成為一個無所不能的超級或上帝類別(God class)?
這是個必須考量的問題,就目前來說,為了簡化範例才這麼做,然而,實際上,應該讓 ElemCollection
只處理一些基礎事務,像 hide
、show
、fadeOut
、fadeIn
這些是基礎事務嗎?雖然目前都放在 ElemCollection
的話,寫起程式來會很爽,然而,它們應該不太算是基礎事務,而算是特效之類的東西。
因此比較好的作法是,基於 XD-1.2.0.js 上,建構一個 Effect
模組來專門處理特效,將 Style-1.0.0.js 中 hide
、show
、fadeOut
、fadeIn
函式放到 Effect
模組中,而 ElemCollection
上的 hide
、show
、fadeOut
、fadeIn
方法,在 Effect
模組中設計一個物件或者是相關函式來處理,在必須用到特效時,可以從 ElemCollection
建構出特效物件,將特效的職責分離出來。
這也有助於 Style-1.0.0.js 瘦身,ES6 並不鼓勵一個模組匯出太多東西,而成為一個超級模組,就目前來說,Style-1.0.0.js 匯出的東西算是比較多一些了,只是為了簡化範例,暫且沒將特效的東西分離出來而已,未來 Style-1.0.0.js 中的東西越來越多時,應避免它成為一個超級模組。
分離職責的東西,就交給你自己來試試了,你可以在 XD-1.2.0.js、Style-1.0.0.js 與 Evt-1.0.0.js 下載到目前已封裝之程式庫。
現在先來看看,基於目前的程式庫封裝,可以如何簡化先前看過的範例,首先是〈存取樣式資訊〉中第一個範例的改寫(你的瀏覽器必須支援 ES6 模組):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
</head>
<body>
<div id="message">這是一段訊息</div>
<script type="module">
import {elemsById} from './js/XD-1.2.0.js';
elemsById('message').css({
color : '#ffffff',
backgroundColor : '#ff0000',
width : '300px',
height : '200px',
paddingLeft : '250px',
paddingTop : '150px'
});
</script>
</body>
</html>
再來是〈存取樣式資訊〉中最後一個範例:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<style type="text/css">
#message {
color: #ffffff;
background-color: #ff0000;
width: 500px;
height: 200px;
padding-left: 250px;
padding-top: 150px;
}
</style>
</head>
<body>
<div id="message">這是一段訊息</div>
<span id="console"></span>
<script type="module">
import {elemsById} from './js/XD-1.2.0.js';
let color = elemsById('message').computedStyle('backgroundColor');
elemsById('console').html(color);
</script>
</body>
</html>
以下是〈存取元素位置〉的第三個範例改寫:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<style type="text/css">
#container {
color: #ffffff;
background-color: #ff0000;
height: 50px;
position: absolute;
top: -100px;
left:-100px;
}
</style>
</head>
<body>
<div id="container">這是一段訊息</div>
<hr>
搜尋:<input id="search" type="text">
<script type="module">
import x from './js/XD-1.2.0.js';
let doc = x(document);
let input = doc.elemsById('search');
let offsetWidth = input.attr('offsetWidth');
let offsetHeight = input.attr('offsetHeight');
let search = input.offset();
doc.elemsById('container')
.css({
left : `${search.x}px`,
top : `${search.y + offsetHeight}px`,
width : `${offsetWidth}px`
});
</script>
</body>
</html>
以下是〈顯示、可見度與透明度〉第一個範例的改寫:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<style type="text/css">
#message {
color: #ffffff;
background-color: #ff0000;
border-width: 10px;
border-color: black;
border-style: solid;
width: 100px;
height: 50px;
padding: 50px;
}
</style>
</head>
<body>
<button id='toggle'>切換顯示狀態</button>
<hr>
這是一些文字!這是一些文字!這是一些文字!這是一些文字!這是一些文字!
<div id="message">這是訊息一</div>
這是其他文字!這是其他文字!這是其他文字!這是其他文字!這是其他文字!
<script type="module">
import {elemsById} from './js/XD-1.2.0.js';
elemsById('toggle').addEvt('click', evt => {
let message = elemsById('message');
if(message.computedStyle('display') === 'none') {
message.show();
} else {
message.hide();
}
});
</script>
</body>
</html>
以下是〈顯示、可見度與透明度〉第二個範例的改寫:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
</head>
<body>
<button id='fadeOut'>淡出</button>
<button id='fadeIn'>淡入</button><br>
<img id="image" src="https://openhome.cc/Gossip/images/caterpillar_small.jpg">
<script type="module">
import {elemsById} from './js/XD-1.2.0.js';
let image = elemsById('image');
elemsById('fadeOut').addEvt('click', evt => {
image.fadeOut();
});
elemsById('fadeIn').addEvt('click', evt => {
image.fadeIn();
});
</script>
</body>
</html>
以下是〈操作 class 屬性〉中的範例改寫:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<style type="text/css">
.released {
border-width: 1px;
border-color: red;
border-style: dashed;
}
.pressed {
border-width: 5px;
border-color: black;
border-style: solid;
}
</style>
</head>
<body>
<img id="logo" class='released'
src="https://openhome.cc/Gossip/images/caterpillar_small.jpg">
<script type="module">
import {elemsById} from './js/XD-1.2.0.js';
let logo = elemsById('logo');
logo.addEvt('click', evt => {
logo.toggleClass('released', 'pressed');
});
</script>
</body>
</html>
以下是〈取得視窗維度資訊〉中的範例改寫:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<style type="text/css">
#message1 {
text-align: center;
vertical-align: middle;
color: #ffffff;
background-color: #ff0000;
width: 100px;
height: 50px;
position: absolute;
top: 0px;
left: 0px;
}
</style>
</head>
<body>
這些是一些文字<br>這些是一些文字<br>這些是一些文字<br>
<button>其他元件</button>
<div id="message1">
看點廣告吧!<br><br>
<button id="confirm">確定</button>
</div>
<script type="module">
import {elemsById, Dimension} from './js/XD-1.2.0.js';
let {width, height} = Dimension.viewport();
let message1 = elemsById('message1');
message1.css({
opacity : 0.5,
width : `${width}px`,
height : `${height / 2}px`,
paddingTop : `${height / 2}px`
});
elemsById('confirm').addEvt('click', evt => {
message1.css({
width : '0px',
height : '0px',
paddingTop : '0px',
display : 'none'
});
});
</script>
</body>
</html>