9-22提交的备案申请,10-22,也就是今天,审核通过。这速率真得很快……

废话不多说,重装上阵!

十月 14th, 2009

发现个firebug的失误

No Comments, 前端开发, by army8735.

其实更应该叫做“firebug的bug”,这名字听上去挺有趣儿,仿佛是专门挑别人bug的自己有了bug。

言归正传,具体是今天做相册的ajax无刷新多文件上传时发现的,mootools中form数组进行each遍历,将每个form的input[type=hidden]的value更改,改完后居然发现firebug没有显示!

郁闷,仔细检查一遍,alert出来的结果是已经更改的。后来发现:当前input节点为展开状态时,更改后firebug并不显示;而如果是并合的,更改后再展开它查看就有显示了。

真是奇怪的现象。

十月 10th, 2009

jssc 5 beta释放~

No Comments, jssc, by army8735.

国庆期间的努力,TAT~

http://code.google.com/p/jssc/

这里有预览:

http://ff9.ffsky.cn/temp/jssc5/index.html

改进:

  1. 相对于alpha版性能至少提升1个数量级而言,beta版也提升了数倍,其中个别逻辑性能提升近10倍。
  2. 整体架构有明显变化,体积减小约10%。
  3. 改进了对php的支持。
  4. 修正了细微的bug,删改细节功能体验。

九月 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编辑器框架。这些代码可以作为你的起点,用以开发更加高级和自定义的编辑器。