九月 28th, 2009

jssc5 beta版中将发生哪些变化?

No Comments, jssc, by army8735.

似乎终于到了消停的日子。只买到30号的票所以请1天假,外加国庆中秋长假一共9天,够爽的了。此外hax在此篇中计划将放出史上最快的Web语法高亮引擎,给俺带来不少压力和动力啊。于是乎十一的休息就用来做jssc5 beta版的开发吧!
功能上jssc5 beta将发生以下变化:

  1. 去除鸡肋的异常处理功能。写在web上的代码基本都是本地测试运行过的,做那一点语法纠错功能没多大用,浪费功夫。这个功能还是放到以后的JAse上去吧。
  2. 重构框架。整体结构将发生一定改变,主要还是继承方面。分化更细致,有利于最终swf文件体积的减少。
  3. 增加自动格式化功能。这其实是以前同事提过的一点,在此准备加上。因web输入等原因造成录入代码缩进问题的话,就不必担心了,因为最终显示会计算缩进量(当然诸如python这样的语言就不行了)。
  4. 改善算法,优化性能。这也是最重要的!即使jssc5 alpha已经大大改善了性能,dojo的9k行代码在2秒内跑完,但理论上还有挖掘的潜力!到时候要和hax的PK一番,哈哈!

至于添加语法种类就需要广大爱好者的帮助了。我也准备陆续写关于高亮的文章,分享jssc5的核心算法和具体思路。虽然一直开源,但貌似从未有人对源代码的改善提过建议,我还是写点教程服务人民群众吧。

另外,发现alpha2里的说明alert居然显示的是beta版。上次修改改错,成超前发布了……

原文标题:《Rich HTML editing in the browser: part 2》

原文地址:http://dev.opera.com/articles/view/rich-html-editing-in-the-browser-part-2/

介绍

在本系列文章的第一篇中,我详细阐述了如何利用javascript语言在设计模式(designMode)和可编辑内容(contentEditable)中创建html富文本编辑器的理论知识。这些文档对象模型(DOM)已经成为HTML5标准的一部分,并且现代主流浏览器也陆续地开始支持。本篇文章作为第二部分,我将化理论为实践,带你走进制作一款简单而跨浏览器的在线文本编辑器的世界。
你可以在这里看到在线完成的版本,点这里能下载它的源码。下面列出的将是代码中最重要的部分,我们将详细地来叙说它,其余乏味的地方就被省略了。
所有代码被分割为3个文件:

  • editor.js:主要的应用程序结构
  • editlib.js:一个修改选区的方法集合
  • util.js:一些有用的方法

框架

我们将使用一个空白的内嵌(IFrame)页面作为画布:

<iframe id="editorFrame" src="blank.html"></iframe>

我们可能会这样用:清除源文件中的代码从而得到一个body里完全没有任何元素的空白页,但是我更倾向于创造一个自定义“空白页”,里面放入一个空的段落,就像:

<title></title>
<body><p></p></body>

这样做自然有它可取之处,因为使用p元素作为开始来包含内容的话,Mozilla便能够和其它浏览器兼容了。(倘若不这样做,Mozilla将直接进入body元素的内容区。)使用可编辑内容属性(contentEditable attribute),我们能够避免使用框架而直接在页面上创建一个可编辑的div区域,但是Firefox 2并不支持,所以为了兼容性最好还是以内嵌页面(IFrame)为基础来制作跨浏览器的编辑器。

激活编辑模式

当页面加载完成时,我们使用下面的方法来激活编辑模式(editor.js中)。

function createEditor() {
  var editFrame = document.getElementById("editorFrame");
  editFrame.contentWindow.document.designMode="on";
}
  bindEvent(window, "load", createEditor);

bindEvent是个很有用的方法(在util.js中定义),用来绑定事件响应的方法。JQuery框架中也有类似的绑定方法,你可能很喜欢这样用。
下一步是创建一个具有格式化文本功能的工具栏(toolbar)。

工具栏

我们先从简单的开始:创建一个“粗体(bold)”按钮,它能将当前选区的文字变粗。当然我们也希望这个按钮能够跟踪文档的状态——当插入点或者选区在粗体文本上时,它能够高亮显示。
主要逻辑被分为两块:一是创建一个命令对象(command object),用以包装当前文档上的实际操作,当然也能查询选区的状态;二是创建一个控制器对象(controller object),能够作为句柄(handler)绑定到点击事件(click event)上,同时更新html按钮的外观。这样分开来比较合乎情理,因为不同的命令拥有相似的控制逻辑。关于这一点我们晚些就会看到。
事件流有两个方向——当工具栏上的控制按钮被点击后,控制器告诉命令在文档上执行;而当光标在文档上移动时,我们想要工具栏上的按钮能及时更新状态。我们追踪所有的控制器,当选区被修改时,查询命令获得其状态并更新相应的按钮外观。

命令和控制器的实现

在粗体命令实现之前,命令对象之上只做了个很小的包装:

function Command(command, editDoc) {
  this.execute = function() {
    editDoc.execCommand(command, false, null);
  };
  this.queryState = function() {
    return editDoc.queryCommandState(command)
  };
}

为什么要进行包装呢?因为我们希望能够像内建的命令一样,让自定义命令拥有统一的接口。

实际上按钮仅仅是一个span元素。

<span id="boldButton">Bold</span>

这个span元素通过控制器(controller)和命令对象(command object)关联。

function TogglCommandController(command, elem) {
  this.updateUI = function() {
    var state = command.queryState();
    elem.className = state?"active":"";
  }
  bindEvent(elem, "click", function(evt) {
    command.execute();
    updateToolbar();
  });
}

列出来的代码中忽略了一些附加代码,用以确保按下按钮后,窗口(window)仍然在聚焦(focus)状态中。
我们将上面那个方法命名为TogglCommandController,是因为它利用两种不同状态将将一个双状态命令(two-state commands)连接到一个按钮上。当点击按钮时,命令就被运行了。而后updateUI命令被调用,span元素上添加active样式类(class),按钮从而改变其外观。下面是为每种按钮所定义的不同样式:

.toolbar span {
  border: outset;
}

.toolbar span.active {
  border: inset;
}

组件是如此链接的:

var command = Command("Bold", editDoc);
var elem = document.getElementById(îboldButton);
var controller = new TogglCommandController(command, elem);
updateListeners.push(controller);

updateListeners收集器为工具栏提供控制。updateToolbar方法通过迭代列表并且调用每个控制器上的updateUI方法来确保所有控制按钮都被更新了。为了确保在文档的选区范围发生改变之后updateToolbar能够及时运行,我们绑定了如下事件:

bindEvent(editDoc, "keyup", updateToolbar);
bindEvent(editDoc, "mouseup", updateToolbar);

当一个命令执行时,updateToolbar也会被调用,如同上面列出的命令代码一样。为什么在命令执行时,我们更新整个工具栏而非相应的控制按钮呢?这是因为其它命令的状态也可能会改变。例如:右对齐(justify-right)命令运行后,左对齐(justify-left)按钮也需要更新。为了避免追踪所有类似这种互斥状态,我们更新整个工具栏。
现在,我们有了双状态命令的基础架构。粗体、斜体、左对齐、右对齐、居中对齐都可以此实现。

链接

在实现了一些基本文本格式化命令之后,我决定给站点访问者们以在文档中添加链接的能力。在内建的createLink命令没有完全按照我们所想的工作之前,链接控制(link control)需要更多的自定义命令逻辑。内建的命令虽然也能创建链接,但是并不返回选区是否在链接之内的信息。我们需要这一特性来为工具栏提供一致性。
那么我们怎么来检查选区是否在一个链接中呢?答案是创建一个功能方法:getContaining,它能够遍历当前选区的DOM树,直到找到我们想要的节点(找不到就返回none)。我们用它来检查是否包含a元素,如果包含的话,那么当前选区就处于一个链接之中。
另一个扩展是我们需要让用户输入链接URL。一个新颖的设计或许会用自定义对话框来完成,但为了简单起见,我们仅用内建的window.prompt方法。如果选区在链接中,我们想在对话框中显示当前链接,这样用户就能检查或者修改它了。或者我们只显示默认的前缀http://。
以下是Linkcommand方法的代码:

function LinkCommand(editDoc) {
	var tagFilter = function(elem){ return elem.tagName=="A"; }; //(1)
	this.execute = function() {
		var a = getContaining(editWindow, tagFilter); //(2)
		var initialUrl = a ? a.href : "http://"; //(3)
		var url = window.prompt("Enter an URL:", initialUrl);
		if (url===null) return; //(4)
		if (url==="") {
			editDoc.execCommand("unlink", false, null); //(5)
		} else {
			editDoc.execCommand("createLink", false, url); //(6)
		}
	};
	this.queryState = function() {
		return !!getContaining(editWindow, tagFilter);  //(7)
	};
}

这个方法的逻辑是:

  1. 方法首先检查一个元素是否是我们正要找的。tagName总是以大写字母从DOM那里返回,不管源代码中的是大小写。
  2. getContaining方法寻找拥有特殊name的元素,包括当前选区。如果找不到就返回null。
  3. 如果找到一个链接,我们在对话框中插入href属性;否则插入默认的http://。
  4. prompt返回null如果用户点击了取消按钮。它将放弃执行命令。
  5. 如果用户删除了url并且点了确定按钮,我们假定用户想要完全删除此链接。我们使用内建的unlink命令来完成它。
  6. 如果用户提供了一个url并且点了确定按钮,我们使用内建的createLink命令来创建链接。(假如链接已经存在,命令将使用新的url值更新链接的href属性)
  7. 双惊叹号的作用是将结果转换为boolean值——如果找到元素为true,找不到为false。

我们可以将LinkCommand和ToggleCommandController联合起来,因为所有的工具栏控制接口都是一样的:执行(execute)和查询状态(queryState)方法。

获得包含(GetContaining)

现在让我们看看getContaining方法吧(editlib.js文件中),它能告诉我们当前选区在元素中的哪个部位。
事情稍微变得有些复杂,因为IE的API和其它浏览器还不一样。那样的话,我们需要创建两个独立的实现,并根据检查getSelection属性来决定使用哪一个,就像这样:

var getContaining = (window.getSelection)?w3_getContaining:iegetContaining;

IE的实现相当有趣,因为它详细展示了IE选区API的微妙之处。

function ie_getContaining(editWindow, filter) {
	var selection = editWindow.document.selection;
	if (selection.type=="Control") { //(1)
		// control selection
		var range = selection.createRange();
		if (range.length==1) {
			var elem = range.item(0); //(3)
		}
		else {
			// multiple control selection
			return null; //(2)
		}
	} else {
		var range = selection.createRange(); //(4)
		var elem = range.parentElement();
	}
	return getAncestor(elem, filter);
}

它是这样工作的:

  1. 选区的type属性可能为“Control”或者“Text”中的一种。什么时候会出现Control情况呢?不止一个控制区被选中时就会如此(例如,用户按下ctrl键选取了几个不连续的图像)。
  2. 我们并不处理多个选区的情况,此时会退出命令不执行,这样什么都不会发生。
  3. 如果有且仅有一个选区,就高亮它。
  4. 如果它是一个文本选区,我们就这样获取它的容器元素(container element)。

其它浏览器使用的API比较简单:

function w3_getContaining(editWindow, filter) {
	var range = editWindow.getSelection().getRangeAt(0); //(1)
	var container = range.commonAncestorContainer;	//(2)
	return getAncestor(container, filter);
}

它是这样工作的:

  1. 如果API允许选择多个选区,但是UI只接受一个,那么我们就把第一个选区看作是唯一的范围(range)。
  2. 这个方法可以得到包含当前选区的元素。

getAncestor方法很简单——查看整个元素体系,找到我们需要寻找的目标,或者找不到的话返回null。

/* walks up the hierachy until an element with the tagName if found.
Returns null if no element is found before BODY */
function getAncestor(elem, filter) {
	while (elem.tagName!="BODY") {
		if (filter(elem)) return elem;
		elem = elem.parentNode;
	}
	return null;
}

多值命令

像编辑字体、大小等需要不同的途径,因为它们各自都有好几个不同的可选项。对于UI窗口来说,此时就不应该还像前面一样用双状态按钮(two-state button),而是使用下拉框(select box)了。当然替代简单的开关状态(on/off state),我们还需要一系列命令和控制器(Command and Controller)来处理多值情况。
这是字体选择器的html代码:

<select id="fontSelector">
  <option value="">Default</option>
  <option value="Courier">Courier</option>
  <option value="Verdana">Verdana</option>
  <option value="Georgia">Georgia</option>
</select>

command对象依然很简单,因为它基于内建的FontName命令:

function ValueCommand(command, editDoc) {
  this.execute = function(value) {
    editDoc.execCommand(command, false, value);
  };
  this.queryValue = function() {
    return editDoc.queryCommandValue(command)
  };
}

值命令(ValueCommand)和前面提到的双状态命令不同之处在于,它有一个queryValue方法,能够返回字符串类型的当前值。当用户选择下拉列表中的某个值时,控制器就开始执行命令。

function ValueSelectorController(command, elem) {
  this.updateUI = function() {
    var value = command.queryValue();
    elem.value = value;
  }
  bindEvent(elem, "change", function(evt) {
    editWindow.focus();
    command.execute(elem.value);
    updateToolbar();
  });
}

控制器很简单,因为我们直接将选项的值映射到命令值上。
字体大小选择使用相同的方式实现。我们只是用内建的FontSize方法来代替,并且使用1-7作为size的选项值。

自定义命令

直到现在,所有的html修改都已经通过内建的命令完成了。但是有时候你可能需要内建命令并不支持的修改行为。这时候我们就要用到DOM和Range API了。
作为一个例子,我们创建一个命令用来在插入点插入一些自定义html代码。简单起见,我们只插入一个text为“Hello World”的span元素。你可以以此为基础扩展插入任何你喜欢的html代码。
这个命令是这样的:

function HelloWorldCommand() {
  this.execute = function() {
    var elem = editWindow.document.createElement("SPAN");
    elem.style.backgroundColor = "red";
    elem.innerHTML = "Hello world!";
    overwriteWithNode(elem);
  }
  this.queryState = function() {
    return false;
  }
}

奇迹发生在overwriteWithNode那里,它会在插入点插入一个元素。(它的名字也指出,如果当前是一个非空选区,被选择的内容会被覆盖)。显而易见的是,IE和DOM 范围标准浏览器之间的实现也不同。让我们先来看看DOM标准浏览器:

function w3_overwriteWithNode(node) {
  var rng = editWindow.getSelection().getRangeAt(0);
  rng.deleteContents();
  if (isTextNode(rng.startContainer)) {
    var refNode = rightPart(rng.startContainer, rng.startOffset)
    refNode.parentNode.insertBefore(node, refNode);
  } else {
    var refNode = rng.startContainer.childNodes[rng.startOffset];
    rng.startContainer.insertBefore(node, refNode);
  }
}

range.deleteContents方法具有如下作用:如果选区是非折叠的,它将删除选区的内容。(如果选区是空的,它不会删除任何东西)。
DOM的Range对象允许我们定位插入点的位置:startContainer是包含插入点的节点,startOffset是插入点在父节点中的偏移量。
例如:如果startContainer是个元素并且startOffset等于3,那么插入点就位于这个节点的第3个子节点和第4个子节点之间。如果startContainer是个文本节点,startOffset则标识插入点的字符位置。比如说startOffset等于3就是说插入点在第3个和第4个字符之间。

endContainer和endOffset以相同方式标识插入点的结束位置。如果选区为空,它们和startContainer、startOffset相等。

如果插入点在文本中间,那么文本就被一分为二,我们可以在中间插入想要的内容。rightPart是个有用的方法,它将文本节点分成两半并返回右边那一部分。这样就可以使用insertBefore在合适的位置插入新节点了。
IE的版本多少有些棘手。IE的Range对象并不能立即获得插入点的精确位置,另外我们只能用pasteHTML方法——它只专横地接受字符串格式的html代码为参数而非dom节点。
基本上,IE的范围API和DOMAPI完全孤立。
然而,我们仍然有办法将这两大体系链接起来:使用pasteHTML方法插入一个标记元素并为之赋予唯一的ID,随后可以用这个ID在DOM中找到它:

function ie_overwriteWithNode(node) {
  var range = editWindow.document.selection.createRange();
  var marker = writeMarkerNode(range);
  marker.appendChild(node);
  marker.removeNode(); // removes node but not children
}

// writes a marker node on a range and returns the node.
function writeMarkerNode(range) {
  var id = editWindow.document.uniqueID;
  var html = "";
  range.pasteHTML(html);
  var node = editWindow.document.getElementById(id);
  return node;
}

注意在完成后我们移除了标记节点,这是为了防止html被这些废弃节点弄乱。
我们现在有了在插入点插入任意html代码的命令,并使用工具栏按钮和ToggleCommandController方法将它们链接到UI上。

总结

在这篇文章中,我们整体结构上了解并制作了一款简单的html编辑器框架。这些代码可以作为你的起点,用以开发更加高级和自定义的编辑器。

原文标题:《Rich HTML editing in the browser: part 1》

原文地址:http://dev.opera.com/articles/view/rich-html-editing-in-the-browser-part-1/

介绍

时光倒流。在世界上最早的浏览器——提姆·伯纳·李(Tim Berners-Lee)发布于1990年——诞生的时候,我们可以直接在所见即所得模式下编辑网页的内容,那时的网页被构思为一种可读可写的媒介。后来经过一段时间的发展,浏览器基本上变得只读了,除了只能在form控件中输入一些纯文本而已。

Internate Explorer 5的发布将浏览器的所见即所得编辑特性带回了主流:新的设计模式(designMode)属性允许用户编辑整个文档(document)。一开始这一特性显得多少有些疏忽,因为它最初是Windows操作系统下IE专有的。

近些年来,另一些可以和IE抗衡的浏览器——Mozilla、Safari和Opera——跟随并实现了这一IE独有的特性。排版引擎比较组织(WHATWG-group)也致力于编辑系统标准的建立——HTML5中设计模式和可编辑内容文档对象模型属性(contentEditable DOM properties)的介绍。看起来在浏览器中,所见即所得编辑最终将成为网页整体的一部分。

这篇文章利用HTML5的可编辑特性来评审现今浏览器的基本概念和挑战。这些科目包括:

  • 不同的可编辑模式
  • 编辑命令
  • 编辑产生的HTML代码
  • 和DOM的配合

这篇文章是两篇系列文章的第一篇,第二篇内容将覆盖到一个详细的例子来实现一个编辑器。

注意:我只考虑到最近的主流浏览器所支持的特性:Opera 9.5、Firefox 2+和Safari 3,较旧的版本有太多的bug和不稳定的地方了。IE中的实现直到5.5版本才有了明显的变化。

可编辑系统预览

可编辑系统允许用户编辑页面或者页面中的一部分,它有如下几个方面:

  • 光标(caret)标识当前的插入点。用户可以输入、删除等。使用键盘或鼠标可以移动光标或者选区。
  • 一些浏览器提供UI组件用户缩放重定位图片、表格和其它可重定位的元素(elements)。
  • 内置了一系列独立的编辑命令:粗体、斜体、插入链接、粘贴、撤消等。这些可以被快捷键、脚本命令接口(script command API)调用。使用API可以轻易实现一个编辑器的工具栏(toolbar)。
  • 使用范围和选区接口(range and selection API),你可以写出自己的脚本来修改任意html代码。这一特性通常用来实现自定义编辑命令。
  • 可编辑系统允许你改变html代码。一旦你创建了它,它并不会真正地修改你网页的内容。举例来说,除非你写了保存修改的脚本命令,否则是无法将修改的内容存回服务器的。

这里还有两条关于可编辑系统的警告:

  • 命令和编辑行为的产生是不可预知的,而产生的html代码结果在不同的浏览器中可能大相径庭。
  • 直到2000年5.5版本的出现,IE浏览器中的实现才有了大幅度的变化。系统产生的html代码可能会让一些敏感的人颤栗——如果你看到最后的字体节点(<font>tag),你肯定会大吃一惊!

开启可编辑特性

这里有两种方法在网页上来创建一块可编辑的选区——设计模式(designMode)和可编辑内容(contentEditable)属性。

一个窗口(window)或者框架(frame)是以设置文档(document)对象上的设计模式属性为真(true)来开启的。(警告:在IE中,这个属性在文档上是不存在的,必须从窗口对象上获取。)典型的可编辑框是将一个内嵌框架(iframe)的设置为设计模式。

一个包含文本的元素想要可编辑的话必须设置设计模式属性为真。(Firefox 2并不支持设计模式属性,但是在Firefox 3中却得到了支持。当然IE、Safari、Opera都早已支持。)

可编辑按键

在一个简单编辑器中,你或多或少会期望能够使用键盘和鼠标来进行编辑内容。当文档获得焦点(focus)时,光标就会显示,它可以上下左右移动。键入或者删除字符也会改变光标的位置。文本选区可以被移动、删除以及覆盖。

一个人性化的设定是所有按键编辑行为都可以自动被记录并且撤消。(后面会提到如何使用撤消工功能。)

复杂的问题出现了:当我们按下了回车键该怎么办?这样做的话并不会立刻显示所产生html代码,而且各个浏览器的结果还会根据上下文有所不同。如果光标在一个非空的p元素内,所有浏览器都会关闭这个元素,并且产生一个拥有相同属性的新元素,最后把光标定位到它里面。(Mozilla会在光标后附带插入一个多余的br换行元素。)例如(这些例子中都用竖线来表示光标所在位置):

<p>bla bla|</p>

按下回车键后IE和Safari会变为:

<p>bla bla</p>
<p>|</p>

假如光标在一个非空的h1元素中,所有的浏览器都会关闭它。但是IE和Opera却会插入一个新的p元素,并且将光标位置置入其中。Safari会插入一个新的h1
元素并将光标位置设定在里面。Mozilla不会产生任何元素,但却会插入两个换行元素在光标后面。例如:

<h1>bla bla|</h1>

按下回车键后Opera会变为:

<h1>bla bla|</h1>
<p>|</p>

但在Mozilla中却会是:

<h1>bla bla|</h1>
|<br><br>

在Safari中则是:

<h1>bla bla|</h1>
<h1>|</h1>

如果你直接在body元素中输入文本(并不包含其它元素),再按下回车键的话,Mozilla会插入一个br元素,IE和Opera会转换前面的文本放入一个p元素中并且插入一个新的p元素,Safari会插入一个新的div元素。
当在一个div元素中键入时,Safari、Opera和IE将关闭当前div元素并且插入一个新的div元素,Mozilla将插入一个br元素并且仍然呆在这个div元素中。
如果在光标外有嵌套的块级(block)元素,所有浏览器都会关闭(并且复制)最深的元素。光标依然会呆在外部块级元素内。
最关键的:令人惊讶的是,在块级元素的处理上,IE反而是最标准的!Mozilla会在部分情况下使用br元素代替块级元素,从而使得原文的显示保持正常。

光标位置

光标是在字符之间移动的。它在标签之间的定位是不可见的。逻辑上看起来所有浏览器是一致的。有关块级元素:光标总会被定位到最深的块级元素上,没法让光标位于两端之间。
例如,看下面,竖线代表所有光标可能处于的位置:

<p>|P|1|</p><p>|p|2|</p>
<div><p>|P|3|</p><div><p>|P|4|</p></div></div>

关于行级(inline)元素,假如光标在文字左侧,它会在所有行级元素边界之外;假如光标在文字右侧,则会在边界之内。例如:

<p>|A|<strong><em>B|</em></strong>C|</p>

所以假如你直接在粗体文本左侧输入字符的话,新输入的字符并不会是粗体;相反在右侧输入则会是粗体。

删除

如果你直接删除一个段落的边界,结果可想而知:左边的块(block)“获胜”了,块右边的内容被包含在了左边:

<h1>Overskrift</h1><p>|Text</p>

如果按下了回退键(backspace),结果是:

<h1>Overskrift|Text</h1>

然而Safari却耍了个小聪明(或者说让人讨厌,这完全取决于你的心情),让右边段落的内容保持原来的风格(style):

<h1>Overskrift|<span class="Apple-style-span" style="font-size:16px;font-weight:normal;">Text</span></h1>

对象编辑

浏览器支持一些特殊的UI特性编辑。
IE允许你用鼠标拖动对象的4个角来拉伸图像、表格、表单控制元素或者完全重定位元素(当对象获得焦点并被选择后,鼠标靠近句柄就会出现)。
Mozilla也允许你拉伸表和和图像,只是附带地允许用户创建新行新列。Mozilla还附带允许你重定位绝对定位的元素。但这些特性都是浏览器完全私有的,并且不能被自定义。

编辑命令

不同浏览器支持不同编辑命令。由命令所产生的html代码在不同浏览器中并不一样,且不一定符合标准。例如,在IE中,粗体命令会产生这样代码:

<strong>Hello!</strong>

Safari则会产生这样的:

<span class="Apple-style-span" style="font-weight:bold;">Hello!</span>

所产生的代码有些过时,至少在IE中是这样。令人畏惧的标签(如<FONT color=#ff0000>123</FONT>)会被一系列命令产生,并且生成的html代码并不符合xhtml标准有时甚至不符合html标准!
Opera的html实现接近(并不相等)IE,使用font元素等等。Safari产生格式化的span元素并用内联css。Safari这一行为的优点是所产生的html代码能通过HTML 4.01 Strict校验。
Mozilla支持两种模式——既可以产生像IE/Opera的元素也可以像Safari那样使用style属性。
如果你担心html校验的话,可以在服务器端做一些过滤清理,将那些标签杂烩转换为标准的xhtml(你可能很需要这样做,以防XSS漏洞攻击)。

快捷键

一些编辑命令支持快捷键,如Ctrl/Cmd + B是粗体、Ctrl/Cmd + B是撤消等等。然而这些快捷键变量在不同浏览器中位置还不完全一样。
快捷键的对照关系图不能被修改,但是却可以在在按键事件中截取到来重写它们。

命令程序接口

你可能想要实现一个工具栏来允许用户运行一些编辑命令,这些可以用API来完成。这些API看起来并不像典型的DOM API,它实际上是实现了IOleCommandTarget接口——这是一个微软应用程序中应用的COM接口,用来同步编辑文档的工具。
命令API基于文档对象之上并且是由一个叫执行命令(execCommand)方法实现的,并且一串以“query”开始的方法会返回命令的信息。
所有的方法都以命令ID为第一个参数——代表命令名称的字符串,剩余的是要执行的方法。

执行命令(ExecCommand)

在当前选取上执行命令,一些命令会切换状态,例如你在选取一段粗体文本之后执行粗体命令,它们就会变回普通样式。其它一些命令需要传入值参,例如foreclor需要颜色代码。
一些命令提供独立的对话框,例如链接(link)命令显示一个对话框来输入url地址,这些对话框不能被自定义,但是却可以被禁止掉。例如:

result = document.execCommand(command, useDialog, value);

不同参数的含义:

  • command:String,命令的名字。
  • useDialog:Boolean,显示内建对话框(并不是所有命令都有对话框)。
  • value:命令所需要的值,并不是所有命令都需要值;假如一个内建的对话框被显示了,那么值是从对话框里获取的。
  • 结果:如果命令成功执行,则返回true;如果被用户所取消(用户取消了对话框)或者执行失败,则返回false。

当没有选择文字的情况下(只有一个光标),所有浏览器都支持文本格式化命令。假如光标在一个单词中间,IE会将整个单词格式化掉,而其它浏览器仅仅格式化下一个将要输入的字符,除非光标提前移动了。

查询命令(QueryCommand)

相对于和文档选区(document selection)相关的工具栏按钮来说,使用查询命令来查询它们的状态,这可能是所有浏览器中最健全的支持了。
开启查询命令(QueryCommandEnabled)
根据当前选区的命令是否可以运行,查询命令处于开启或关闭的状态下。例如:“解除链接(unlink)”只有当光标在一个链接(link)选区内才可用。而当光标处于不可编辑的区域中的话,所有命令都不可用。
查询命令状态(QueryCommandState)
它标识着当前选区内是否已经运行过相关命令了。例如在一个粗体选区中,粗体命令(bold command)的状态就是true。
查询命令返回值(QueryCommandValue)
它是目标选区执行命令后返回的值,对应于执行命令中传入的参数值。如foreColor返回当前选区的颜色代码(String)。

不同浏览器之间的格式化命令是不同的。例如,foreColor命令在IE中会返回一个16进制的颜色代码(如#ff0000),其它浏览器则返回一个rbg表达式(如rgb(255,0,0))。
一些返回值是建立在浏览器本地基础上的。例如在IE中,格式化块(FormatBlock)会根据浏览器的UI语言返回段落的名称。
像粗体这样的命令,返回值总是false。(api包含两个附加的方法,queryCommandSupported和queryCommandIndeterminate,但是它们用起来极不可靠。)

范围和选区API

内建的命令经常被用在实际的选区上,但是却不能修改它们的行为或者自定义实现。使用范围和选区(Range and Selection)API,你可以实现自主的html转换,就像模拟自定义实现一样。

需要说明的是,一些转换破坏了经常被撤销/重做(undo/redo)命令使用的撤销栈(undo stack),这是非常不友好的。但是它却实现了自定义命令的功能,是否值得这样做取决于你的你的页面建立目标。

范围和选区API包含两个核心类:

  • 范围——一个文档中连续的字符串范围。范围可能会在元素边界上重叠。一个范围拥有开始点(start point)和结束点(end point)。如果开始点的位置和结束点相等的话,我们就说这个范围是折叠的。
  • 选区——描述当前用户选取的文档选区。一个选区包括唯一的一个高亮范围。如果范围是折叠的,那么选区就以光标形态显示。

(范围和选区可以用在可编辑区域之外,你可以在只读的文档上创建选区,但是这样的话它不能被折叠,并且只读选区并不显示光标。)

这些概念在所有浏览器中通用,但是可用的API却在IE和其它浏览器中有区别。IE使用它私有的范围选区API,其它浏览器则使用W3C标准的范围API和非标准的选区API。

最主要的差别还是:IE的范围是以存储包括html标记在内的String为基础的;而W3C标准则是存储DOM节点树。

范围例子:

为了展示二者不同之处,这有一个在当前选区插入行内元素code的代码。

IE中(editWindow是个对处于designMode的frame的引用):

var rng = editWindow.document.seletion.createRange();
rng.parseHTML("" + rng.htmlText + "");

Mozilla中:

var rng = editWindow.getSelection().getRangeAt(0);
rng.surroundContents(document.createElement("code"));

控制选区(Control selection)

IE支持控制选区,它和普通的范围选区不一样。当你在一副图片上、表单上、表格的边线上点击的时候,控制选区就产生了。

并且你可以按下Ctrl键来达到一次性选择多个控制选区的目的,其它浏览器并不支持——它们仍会当作一个文本选区来看待。

总结

本文带你浏览了基于浏览器的可编辑概念。系列文章的第二篇将给你展示一个很有价值例子——如何利用这些API来实现可编辑的页面。

九月 24th, 2009

视觉色彩习惯

5 Comments, 用户体验设计, by army8735.

“订阅按钮在哪里?”

这是昨天域名备好案之后,我和最终妄想-零聊天并告知他个人技术博客开通时,他问的第一句话。

仔细看站点底部,有着“Theme based on iStudio Designed by Xu.hel.”的字样。这套主题是下载Xu.hel的iStudio作品,当然在其基础上大肆修改了一番,比如宽度调到960,色调变成蓝色等等等等,几乎是面目全非了。且不说这些,iStudio主题还是蛮清馨简约的,不得不称赞作者的设计功力。然而文章开头的那句提问,却引发了对话者之间一个细节的讨论。

“我一般就找橙色图标来订阅。当你脑里想着‘橙色’的时候,都无视一切灰色物体了。”

04的这句话绝对可以当成金科玉律!的确,提到RSS订阅,人们第一印象就是想起那个橙色的、圆角矩形的图案来,于是乎每个人眼里第一目标就会去寻找它。当然,不乏有创意十足的设计师们设计出其它个性的图标,但他们也是在一定的原型基础上发挥天马行空的想象:譬如矩形变成圆形,颜色改成近色。很少有脱离原本模样完全让你完全认不出它是RSS的设计,除非制作得特别醒目,能引起人的好奇心把鼠标移上去看看是什么东西。而且这样做需冒一定的风险。google搜索rss图片,便能看到许多特别的的创意。

habit

可以看到,以前的RSS订阅图片是灰色的,鼠标移上去才会显示原本的橙色。现在已经改为默认橙色,鼠标移上去变为橙红色。

九月 22nd, 2009

Hello world!

1 Comment, 其它, by army8735.

第一篇系统默认博客,算了不删了,留个纪念吧。 ^_^

日期当然就是开通的时间,这里就用作专门的技术博客,生活博客则使用Qzone,链接见链接框。