批量修改style采取哪种方式好(续篇)

前篇见[url=http://www.iteye.com/topic/164500]批量修改style采取哪种方式好[/url],主要是回答fins的提问。

下面我来说说我们实际期望怎样的编程方式。

假设一个这样的需求:

页面上有一些文本是highlight的。例如,javaeye的文章如果是点击[url=http://www.google.cn/search?q=%E6%89%B9%E9%87%8F+%E4%BF%AE%E6%94%B9+style+fins+%E5%B1%80%E5%9F%9F%E5%8C%96+site%3Aiteye.com]google搜索结果[/url]过来的,javaeye的后台会自动判断出关键字,并为这些关键字包裹上标记(<span class="hilite1">关键字</span>)。

我们现在希望有这样一个功能,就是允许开启/关闭highlight。

如果关闭的话,那么大家通常可以想到的做法,就是检索所有的.hilite1的元素,然后去掉这个class。

$('span.hilite1').forEach(function(e){e.className=''});

但是这样做好之后,我们就无法再次开启highlight了!所以,我们要换种做法。一种方式是遍历所有的.hilite1然后替换为.hilite0。这样下次就可以找回来。不过正如上一篇文章中所说的,我们其实有更加经济的做法。

我们在body上加入一个class: <body class="hiliteEnabled"...> ,然后把样式改为: body.hiliteEnabled span.hilite1 {...} ,这样,当要关闭highlight的时候,差不多就只需要 document.body.className = '' ,就可以了。

Ok,我们完成这个功能了。

这个时候,有用户希望我们提供另一个很cool的功能,即在页面下方加入一个slider,然后用户可以拖动slider改变highlight部分文字的字体大小。

这就类似于我上一篇中提到的批量修改的问题了。我们有两种做法:

A.
$('#fontSizeSlider').onchange = function() {
var size = this.value;
$('span.hilite1').forEach(function(e){
e.style.fontSize = size;
});
}

B.
$('#fontSizeSlider').onchange = function() {
var size = this.value;
getStyle('span.hilite1').fontSize = size;
}
function getStyle(selector){
//示意代码
return document.styleSheets[0].cssRules[0].style;
}


单纯看的话,托各种支持selector query的library的福,A做法是很简单的。B做法也不复杂。

但是还记得我们第一个开启关闭的功能么?如果highlight被关闭了,显然,从用户的角度上说,应该也禁用修改font size的效果。乍一看这个很简单,改成select出 body.hiliteEnabled span.hilite1 然后遍历就好了。

对于B做法来说,确实如此,你只需确保getStyle()返回的是针对 body.hiliteEnabled span.hilite1 的样式对象即可。

但是注意这点对于A做法是不够的!因为你给所有的body.hiliteEnabled span.hilite1都加上了inline style,这个是不会自动消失的。所以你需要在 document.body.className = '' 之后加上清理语句 resetFontSize(false) 而在再次开启的语句 document.body.className = 'hiliteEnabled' 之后也需要加上 resetFontSize(true) 。该函数的代码如下:

function resetFontSize(flag) {
var v = flag ? $('#fontSizeSlider').value : null;
$('span.hilite1').forEach(function(e){
e.style.fontSize = v;
});
}


这个味道就不太好。这倒不是因为两个功能被交织了起来。我们原来修改body.className其实隐含了关闭/开启highlight的语义,所以fontSize生效与否受到它的影响是正常的。你可以把“document.body.className = ''; resetFontSize(false)”纳入一个disableHilite()函数中。

这里的问题实际是,每次你加入一个与highlight有关的新功能(例如我们下次可能允许大家定制hilite的颜色),你就需要修改disableHilite()/enableHilite(),加上新功能的清理和初始化代码的调用。这显然味道很不好。

各位可能会想到观察者模式了!

是的,你可以把hilite启用和禁用做成一个事件,然后其他功能都来订阅这个事件,并调用各自的初始化和清理代码。

不错不是嘛。问题都迎刃而解了!

不过还有一个问题。在这里,我们开启/禁用,只是一个二选一的问题。但是我们也可能遇到(制造出)更复杂的需求。比如假设是论坛帖子,除了整个页面的开启/禁用之外,每个回复都可以单独开启禁用hilite(即每个article元素上可以有.hiliteEnabled或.hiliteDisabled,如果没有任何一个class,则看body上是否有.hiliteEnabled),局域的设置override上层设置。这时,你就惨了,因为你的初始化/清理代码是针对整个页面写的,你必须改造成针对一个区域进行初始化和清理。你可能需要把用于遍历的selector作为事件的一个信息来传递。你的事件触发也需要重新写过,可能要让disableHilite()/enableHilite()能够接受一个参数指定操作范围,显然这个参数最好也用css selector。

Ok,这是我生造的需求,所以你会觉得不合理,不过对于程序员来说,需求一般总是不合理的。呵呵。我们这里只是举例。

其实我们从上面可以看到一个线索,那就是hilite启用与否,实际上可以取决于某个selector的模式匹配,因为我们通常把带有语义的开关存放在元素的class属性中。对于最初的简单需求来说:
匹配body.hiliteEnabled span.hilite1,就启用hilite以及hilite相关的功能,
不匹配body.hiliteEnabled span.hilite1,就禁用hilite以及hilite相关的功能。
每个hilite功能(如动态改变fontSize)去监听我们自制的hilite事件来进行初始化和清理,其实也可以等价于监听这一匹配的变化(如果我们能够监听的话)。

对于我们下面人为制造的需求,其实可以转化为:
(注:article元素表示整个页面中每个单独的帖子)
匹配body article.hiliteEnabled span.hilite1,启用hilite,
匹配body.hiliteEnabled article span.hilite1,也启用hilite,
除非匹配body article.hiliteDisabled span.hilite1,则禁用hilite。

如果写成一个单一的selector,就是:
article.hiliteEnabled span.hilite1, body.hiliteEnabled article:not(.hiliteDisabled) span.hilite1

一连串复杂的逻辑,其实就可以简化为对于这一模式匹配的监听。

这样我们期望中的代码就呼之欲出了:

首先,启用和禁用hilite,就是简单的直接对元素(body或article)上设置className。

然后我们这样写:

var hiliteSelector = new Selector('article.hiliteEnabled span.hilite1, body.hiliteEnabled article:not(.hiliteDisabled) span.hilite1');

function initHiliteFontSizeFeature() {
hiliteSelector.addEventListener('match', function(evt){
var hiliteSpan = evt.target;
hiliteSpan._syncFontSize = function(evt) {
hiliteSpan.style.fontSize = evt.target.value;
};
hiliteSpan.style.fontSize = $('#fontSizeSlider').value;
$('#fontSizeSlider').addEventListener('change', hiliteSpan._syncFontSize, false);
}, false);
hiliteSelector.addEventListener('unmatch', function(evt){
var hiliteSpan = evt.target;
$('#fontSizeSlider').removeEventListener('change', hiliteSpan._syncFontSize, false);
hiliteSpan._syncFontSize = null;
hiliteSpan.style.fontSize = null;
}, false);
}


也就是,如果有一个Selector API提供给我们监听match/unmatch事件的话,要做的事情就非常简单了!

理想中,Selector会产生match/unmatch事件,并自动dispatch到所有匹配的节点上。

不过,我们现在并没有这样的API……querySelector及各种library提供的,都是一次性取出符合条件的节点,而没有监听的功能。

实际上,在现有浏览器内使用JavaScript来实现这一API,是相当困难的。但是,我们知道,浏览器内部肯定有等价的功能,因为stylesheet的应用就是遵循这样的机制的。而且我们知道,IE的htc和Mozilla的XBL,正是利用这样一种机制的!未来的XBL2规范,也是如此!

在下一篇blog中,我会拿htc、xbl1和xbl2,来实现我们上面提到的例子。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值