5.7. 案例:Zoom Textarea

添加缩放 textareas 的按钮

Zoom Textarea 改变了网页中的表单,在每个 <textarea> 元素(用于输入多行文本)上方加上了一个工具栏。这个工具栏可以放大或缩小 <textarea>中文本的大小,而不会改变页面中其他地方的样式。这些按钮也可以完全用键盘来操作的;用 tab 移动焦点到按钮上然后按 回车键,代替鼠标点击操作。(我前面提到的是由于易用性问题(accessibility matters),然而实际上,用起来比听起来难)

例 5.7.  zoomtextarea.user.js

// ==UserScript==
// @name          Zoom Textarea
// @namespace     http://diveintogreasemonkey.org/download/
// @description   add controls to zoom textareas
// @include       *
// ==/UserScript==

var textareas, textarea;

textareas = document.getElementsByTagName('textarea');
if (!textareas.length) { return; }

function textarea_zoom_in(event) {
	var link, textarea, s;
	link = event.currentTarget;
	textarea = link._target;
	s = getComputedStyle(textarea, "");
	textarea.style.width = (parseFloat(s.width) * 1.5) + "px";
	textarea.style.height = (parseFloat(s.height) * 1.5) + "px";
	textarea.style.fontSize = (parseFloat(s.fontSize) + 7.0) + 'px';
	event.preventDefault();
}

function textarea_zoom_out(event) {
	var link, textarea, s;
	link = event.currentTarget;
	textarea = link._target;
	s = getComputedStyle(textarea, "");
	textarea.style.width = (parseFloat(s.width) * 2.0 / 3.0) + "px";
	textarea.style.height = (parseFloat(s.height) * 2.0 / 3.0) + "px";
	textarea.style.fontSize = (parseFloat(s.fontSize) - 7.0) + "px";
	event.preventDefault();
}

function createButton(target, func, title, width, height, src) {
	var img, button;
	img = document.createElement('img');
	img.width = width;
	img.height = height;
	img.style.borderTop = img.style.borderLeft = "1px solid #ccc";
	img.style.borderRight = img.style.borderBottom = "1px solid #888";
	img.style.marginRight = "2px";
	img.src = src;
	button = document.createElement('a');
	button._target = target;
	button.title = title;
	button.href = '#';
	button.onclick = func;
	button.appendChild(img);
	return button;
}

for (var i = 0; i < textareas.length; i++) {
	textarea = textareas[i];
	textarea.parentNode.insertBefore(
		createButton(
			textarea,
			textarea_zoom_in,
			'Increase textarea size',
			20,
			20,
			'data:image/gif;base64,'+
			'R0lGODlhFAAUAOYAANPS1tva3uTj52NjY2JiY7KxtPf3%2BLOys6WkpmJiYvDw8fX19vb'+
			'296Wlpre3uEZFR%2B%2Fv8aqpq9va3a6tr6Kho%2Bjo6bKytZqZml5eYMLBxNra21JSU3'+
			'Jxc3RzdXl4emJhZOvq7KamppGQkr29vba2uGBgYdLR1dLS0lBPUVRTVYB%2Fgvj4%2BYK'+
			'Bg6SjptrZ3cPDxb69wG1tbsXFxsrJy29vccDAwfT09VJRU6uqrFlZW6moqo2Mj4yLjLKy'+
			's%2Fj4%2BK%2Busu7t783Nz3l4e19fX7u6vaalqNPS1MjHylZVV318ftfW2UhHSG9uccv'+
			'KzfHw8qqqrNPS1eXk5tvb3K%2BvsHNydeLi40pKS2JhY2hnalpZWlVVVtDQ0URDRJmZm5'+
			'mYm11dXp2cnm9vcFxcXaOjo0pJSsC%2FwuXk6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'+
			'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC'+
			'H5BAAAAAAALAAAAAAUABQAAAeagGaCg4SFhoeIiYqKTSQUFwgwi4JlB0pOCkEiRQKKRxM'+
			'gKwMGDFEqBYpPRj4GAwwLCkQsijwQBAQJCUNSW1mKSUALNiVVJzIvSIo7GRUaGzUOPTpC'+
			'igUeMyNTIWMHGC2KAl5hCBENYDlcWC7gOB1LDzRdWlZMAZOEJl83VPb3ggAfUnDo5w%2F'+
			'AFRQxJPj7J4aMhYWCoPyASFFRIAA7'),
		textarea);
	textarea.parentNode.insertBefore(
		createButton(
			textarea,
			textarea_zoom_out,
			'Decrease textarea size',
			20,
			20,
			'data:image/gif;base64,'+
			'R0lGODlhFAAUAOYAANPS1uTj59va3vDw8bKxtGJiYrOys6Wkpvj4%2BPb29%2FX19mJiY'+
			'%2Ff3%2BKqqrLe3uLKytURDRFpZWqmoqllZW9va3aOjo6Kho4KBg729vWJhZK%2BuskZF'+
			'R4B%2FgsLBxHNydY2Mj%2Ff396amptLS0l9fX9fW2dDQ0W1tbpmZm8DAwfT09fHw8n18f'+
			'uLi49LR1V5eYOjo6VBPUa6tr769wEhHSNra20pJStPS1KuqrNPS1ZmYm%2B7t77Kys8rJ'+
			'y%2Fj4%2BaSjpm9uca%2BvsMjHyqalqHRzdVJRU8PDxVRTVcvKzc3Nz0pKS9rZ3evq7MC'+
			'%2FwsXFxp2cnnl4e1VVVu%2Fv8ba2uM7Oz29vcbu6vZqZmnJxc9vb3PHx8uXk5mhnamJh'+
			'Y1xcXZGQklZVV29vcHl4eoyLjKqpq6Wlpl1dXuXk6AAAAAAAAAAAAAAAAAAAAAAAAAAAA'+
			'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'+
			'AAACH5BAAAAAAALAAAAAAUABQAAAeZgGaCg4SFhoeIiYqKR1IWVgcyi4JMBiQqA0heQgG'+
			'KQTFLPQgMCVocBIoNNqMgCQoDVReKYlELCwUFI1glEYorOgopWSwiTUVfih8dLzRTKA47'+
			'Ek%2BKBGE8GEAhFQYuPooBOWAHY2ROExBbSt83QzMbVCdQST8Ck4QtZUQe9faCABlGrvD'+
			'rB4ALDBMU%2BvnrUuOBQkE4NDycqCgQADs%3D'),
		textarea);
	textarea.parentNode.insertBefore(
		document.createElement('br'),
		textarea);
}

这段代码看起来很复杂,而且它确实复杂,但是有特殊原因的。看起来很复杂是因为它中间有大段的乱码似的字符串。这些是data: URIs,它们看起来像地狱却很容易生成。真正复杂的地方还在别处。

首先,我获取了页面上所有的 <textarea> 元素集。此模式的详情,请阅读操作特定 HTML 元素的所有实例。如果没有,那就没必要继续了,所以 return(返回)。

textareas = document.getElementsByTagName('textarea');
if (!textareas.length) { return; }

现在,我们跑下题。我们的“工具栏”看起来像是一行按钮,但是每个按钮就是看起来可以点的某些图片,外面包上一个能执行我们的 Javascript 函数的链接。

由于我们要创建多个按钮(尽管这个脚本只有两个按钮,但是您可以便利地扩展出更多功能),我创建了一个函数来封装所有的制造按钮的逻辑。

function createButton(target, func, title, width, height, src) {

The createButton function takes 6 arguments:

target
元素对象,按钮控制的 <textarea> 元素
func
函数对象,当用户用鼠标点击或键盘激活这个按钮时,调用的 Javascript 函数
title
字符串,鼠标移动到按钮上显示的提示文字
width
整数,按钮的宽度。它应该与 src 参数提供的图形实际宽度一致。
height
整数,按钮的高度。它应该与 src 参数提供的图形实际高度一致。
src
字符串,URL、路径或者按钮图形的 data: URI

创建一个按钮分为两个步骤:创建 <img> 元素,然后创建其外的 <a> 元素。

我调用 document.createElement 来创建 <img> 元素,并且设置了一小部分属性,包含样式的属性。关于此模式的更多信息,请阅读设置元素样式

img = document.createElement('img');
img._target = target;
img.width = width;
img.height = height;
img.style.borderTop = img.style.borderLeft = "1px solid #ccc";
img.style.borderRight = img.style.borderBottom = "1px solid #888";
img.style.marginRight = "2px";
img.src = src;

这里有个很酷的技巧,您可以使用下面的语法同时为两个属性赋上相同的值:

img.style.borderTop = img.style.borderLeft = "1px solid #ccc";

OK, 继续前进。创建按钮的第二部分是创建链接(<a> 元素),然后把 <img> 元素放在里面。

button = document.createElement('a');
button._target = target;
button.title = title;
button.href = '#';
button.onclick = func;
button.appendChild(img);

我想指出两点。第一,我需要赋给链接伪造的 href 属性,否则 Firefox 会把它当作命名锚并且不会把它加到 tab 索引中(也就是您无法通过 tab 移动它上面,也就无法通过键盘使用)。第二,我设置了 _target 属性来保存目标 <textarea> 的引用。这在 Javascript 中是完全合法的;您只需要给新属性赋值,就可以给对象创建新属性。稍后,在 onclick 的事件句柄中,我会访问这个自定义的 _target 属性。

现在,让我们跳回到 onclick 句柄。每个句柄都是一个函数,有一个参数:event

function textarea_zoom_in(event)

event 对象有许多属性,现在我感兴趣的只有一个:currentTarget

link = event.currentTarget;

如果您阅读过关于 Event 对象的文档,您可以读到很多目标相关的属性,包含一个简单的:target。您可能很想使用 event.target 去获取已访问的链接,但是它表现(依我看来)不一致。当用户 tab 到按钮上然后敲回车键event.target 是这个链接,但是当用户用鼠标点击按钮时,event.target 是链接中的图片!我想这肯定有个合理的解释,但是它超过了我对 DOM 事件模型的理解层次。无论如何,event.currentTarget 在所有情况下都返回链接,所以我使用它。

下一步我取回了目前打算缩放的 <textarea> 的引用,通过自定义的 _target 属性,我在创建按钮时设置了这个属性。

    textarea = link._target;

现在好戏刚刚开始。(您认为您已经找到乐趣了吗!)我需要获得 <textarea> 当前的长宽和字体大小,这样我就可以把它们放大。简单的从textarea.style (textarea.style.width, textarea.style.heighttextarea.style.fontSize)中获得适合的属性并管用,因为只有在 <textarea>style 属性中定义它们的时候才能得到值。这不是我想要的;我要得到真实的当前样式。我需要的是 getComputedStyle. 关于此函数的更多信息,请阅读获取元素样式

s = getComputedStyle(textarea, "");
textarea.style.width = (parseFloat(s.width) * 1.5) + "px";
textarea.style.height = (parseFloat(s.height) * 1.5) + "px";
textarea.style.fontSize = (parseFloat(s.fontSize) + 7.0) + 'px';

最后,您还记得按钮链接上为了键盘易用新而加的伪造的 href 值吗?嗯,它已经变成了烦恼,因为 Firefox 执行完 onclick 句柄后,它就会尝试跳转到这个链接。因为它指向了不存在的锚,无论按钮在什么地方,Firefox 都会跳到页面的顶端。这是件恼人的事,要阻止它,我需要在完成 onclick 句柄前调用 event.preventDefault()

event.preventDefault();

脚本的其余部分很简单。我遍历了所有的 <textarea> 元素,为他们各自创建缩放按钮(每个都有它自己的 onclick 句柄和按钮图片),然后在 <textarea> 前插入缩放按钮。对于每张图片,我使用 data: URI 来创建内嵌的图片,所以用户不需要访问远端服务器去获取按钮图片。关于此模式的更多信息,请阅读在元素前插入内容在没有服务器的情况下添加图片

for (var i = 0; i < textareas.length; i++) {
	textarea = textareas[i];
	textarea.parentNode.insertBefore(
		createButton(
			textarea,
			textarea_zoom_in,
			'Increase textarea size',
			20,
			20,
			'data:image/gif;base64,'+
			'R0lGODlhFAAUAOYAANPS1tva3uTj52NjY2JiY7KxtPf3%2BLOys6WkpmJiYvDw8fX19vb'+
			'296Wlpre3uEZFR%2B%2Fv8aqpq9va3a6tr6Kho%2Bjo6bKytZqZml5eYMLBxNra21JSU3'+
			'Jxc3RzdXl4emJhZOvq7KamppGQkr29vba2uGBgYdLR1dLS0lBPUVRTVYB%2Fgvj4%2BYK'+
			'Bg6SjptrZ3cPDxb69wG1tbsXFxsrJy29vccDAwfT09VJRU6uqrFlZW6moqo2Mj4yLjLKy'+
			's%2Fj4%2BK%2Busu7t783Nz3l4e19fX7u6vaalqNPS1MjHylZVV318ftfW2UhHSG9uccv'+
			'KzfHw8qqqrNPS1eXk5tvb3K%2BvsHNydeLi40pKS2JhY2hnalpZWlVVVtDQ0URDRJmZm5'+
			'mYm11dXp2cnm9vcFxcXaOjo0pJSsC%2FwuXk6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'+
			'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC'+
			'H5BAAAAAAALAAAAAAUABQAAAeagGaCg4SFhoeIiYqKTSQUFwgwi4JlB0pOCkEiRQKKRxM'+
			'gKwMGDFEqBYpPRj4GAwwLCkQsijwQBAQJCUNSW1mKSUALNiVVJzIvSIo7GRUaGzUOPTpC'+
			'igUeMyNTIWMHGC2KAl5hCBENYDlcWC7gOB1LDzRdWlZMAZOEJl83VPb3ggAfUnDo5w%2F'+
			'AFRQxJPj7J4aMhYWCoPyASFFRIAA7'),
		textarea);
	textarea.parentNode.insertBefore(
		createButton(
			textarea,
			textarea_zoom_out,
			'Decrease textarea size',
			20,
			20,
			'data:image/gif;base64,'+
			'R0lGODlhFAAUAOYAANPS1uTj59va3vDw8bKxtGJiYrOys6Wkpvj4%2BPb29%2FX19mJiY'+
			'%2Ff3%2BKqqrLe3uLKytURDRFpZWqmoqllZW9va3aOjo6Kho4KBg729vWJhZK%2BuskZF'+
			'R4B%2FgsLBxHNydY2Mj%2Ff396amptLS0l9fX9fW2dDQ0W1tbpmZm8DAwfT09fHw8n18f'+
			'uLi49LR1V5eYOjo6VBPUa6tr769wEhHSNra20pJStPS1KuqrNPS1ZmYm%2B7t77Kys8rJ'+
			'y%2Fj4%2BaSjpm9uca%2BvsMjHyqalqHRzdVJRU8PDxVRTVcvKzc3Nz0pKS9rZ3evq7MC'+
			'%2FwsXFxp2cnnl4e1VVVu%2Fv8ba2uM7Oz29vcbu6vZqZmnJxc9vb3PHx8uXk5mhnamJh'+
			'Y1xcXZGQklZVV29vcHl4eoyLjKqpq6Wlpl1dXuXk6AAAAAAAAAAAAAAAAAAAAAAAAAAAA'+
			'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'+
			'AAACH5BAAAAAAALAAAAAAUABQAAAeZgGaCg4SFhoeIiYqKR1IWVgcyi4JMBiQqA0heQgG'+
			'KQTFLPQgMCVocBIoNNqMgCQoDVReKYlELCwUFI1glEYorOgopWSwiTUVfih8dLzRTKA47'+
			'Ek%2BKBGE8GEAhFQYuPooBOWAHY2ROExBbSt83QzMbVCdQST8Ck4QtZUQe9faCABlGrvD'+
			'rB4ALDBMU%2BvnrUuOBQkE4NDycqCgQADs%3D'),
		textarea);
	textarea.parentNode.insertBefore(
		document.createElement('br'),
		textarea);
}
← 案例:Frownies
案例:Access Bar →