<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>army8735 &#187; 富文本编辑</title>
	<atom:link href="http://army8735.org/tag/%e5%af%8c%e6%96%87%e6%9c%ac%e7%bc%96%e8%be%91/feed" rel="self" type="application/rss+xml" />
	<link>http://army8735.org</link>
	<description>我可以A，我也可以-A，我可以同时A和-A。</description>
	<lastBuildDate>Fri, 25 Nov 2011 04:07:52 +0000</lastBuildDate>
	<generator>http://wordpress.org/?v=2.9.2</generator>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
			<item>
		<title>浏览器中的HTML富文本编辑（二）</title>
		<link>http://army8735.org/2009/09/27/156.html</link>
		<comments>http://army8735.org/2009/09/27/156.html#comments</comments>
		<pubDate>Sun, 27 Sep 2009 01:44:56 +0000</pubDate>
		<dc:creator>army8735</dc:creator>
				<category><![CDATA[前端开发]]></category>
		<category><![CDATA[翻译]]></category>
		<category><![CDATA[rich html]]></category>
		<category><![CDATA[富文本编辑]]></category>

		<guid isPermaLink="false">http://www.army8735.org/?p=156</guid>
		<description><![CDATA[原文标题：《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）页面作为画布：
&#60;iframe id="editorFrame" src="blank.html"&#62;&#60;/iframe&#62;
我们可能会这样用：清除源文件中的代码从而得到一个body里完全没有任何元素的空白页，但是我更倾向于创造一个自定义“空白页”，里面放入一个空的段落，就像：
&#60;title&#62;&#60;/title&#62;
&#60;body&#62;&#60;p&#62;&#60;/p&#62;&#60;/body&#62;
这样做自然有它可取之处，因为使用p元素作为开始来包含内容的话，Mozilla便能够和其它浏览器兼容了。（倘若不这样做，Mozilla将直接进入body元素的内容区。）使用可编辑内容属性（contentEditable attribute），我们能够避免使用框架而直接在页面上创建一个可编辑的div区域，但是Firefox 2并不支持，所以为了兼容性最好还是以内嵌页面（IFrame）为基础来制作跨浏览器的编辑器。
激活编辑模式
当页面加载完成时，我们使用下面的方法来激活编辑模式（editor.js中）。
function createEditor() {
  var editFrame = document.getElementById("editorFrame");
 ...]]></description>
			<content:encoded><![CDATA[<p>原文标题：《Rich HTML editing in the browser: part 2》</p>
<p>原文地址：<a href="http://dev.opera.com/articles/view/rich-html-editing-in-the-browser-part-2/" target="_blank">http://dev.opera.com/articles/view/rich-html-editing-in-the-browser-part-2/</a></p>
<h3>介绍</h3>
<p>在本系列文章的<a href="/2009/09/25/126.html" target="_blank">第一篇</a>中，我详细阐述了如何利用javascript语言在设计模式（designMode）和可编辑内容（contentEditable）中创建html富文本编辑器的理论知识。这些文档对象模型（DOM）已经成为HTML5标准的一部分，并且现代主流浏览器也陆续地开始支持。本篇文章作为第二部分，我将化理论为实践，带你走进制作一款简单而跨浏览器的在线文本编辑器的世界。<br />
你可以<a href="http://olav.dk/code/dev.opera.com/edit/" target="_blank">在这里</a>看到在线完成的版本，点<a href="http://olav.dk/code/dev.opera.com/edit/source.zip" target="_blank">这里能下载</a>它的源码。下面列出的将是代码中最重要的部分，我们将详细地来叙说它，其余乏味的地方就被省略了。<br />
所有代码被分割为3个文件：</p>
<ul>
<li>editor.js：主要的应用程序结构</li>
<li>editlib.js：一个修改选区的方法集合</li>
<li>util.js：一些有用的方法</li>
</ul>
<h3>框架</h3>
<p>我们将使用一个空白的内嵌（IFrame）页面作为画布：</p>
<pre class="brush:html;">&lt;iframe id="editorFrame" src="blank.html"&gt;&lt;/iframe&gt;</pre>
<p>我们可能会这样用：清除源文件中的代码从而得到一个body里完全没有任何元素的空白页，但是我更倾向于创造一个自定义“空白页”，里面放入一个空的段落，就像：</p>
<pre class="brush:html;">&lt;title&gt;&lt;/title&gt;
&lt;body&gt;&lt;p&gt;&lt;/p&gt;&lt;/body&gt;</pre>
<p>这样做自然有它可取之处，因为使用p元素作为开始来包含内容的话，Mozilla便能够和其它浏览器兼容了。（倘若不这样做，Mozilla将直接进入body元素的内容区。）使用可编辑内容属性（contentEditable attribute），我们能够避免使用框架而直接在页面上创建一个可编辑的div区域，但是Firefox 2并不支持，所以为了兼容性最好还是以内嵌页面（IFrame）为基础来制作跨浏览器的编辑器。</p>
<h3>激活编辑模式</h3>
<p>当页面加载完成时，我们使用下面的方法来激活编辑模式（editor.js中）。</p>
<pre class="brush:js">function createEditor() {
  var editFrame = document.getElementById("editorFrame");
  editFrame.contentWindow.document.designMode="on";
}
  bindEvent(window, "load", createEditor);</pre>
<p>bindEvent是个很有用的方法（在util.js中定义），用来绑定事件响应的方法。JQuery框架中也有类似的绑定方法，你可能很喜欢这样用。<br />
下一步是创建一个具有格式化文本功能的工具栏（toolbar）。</p>
<h3>工具栏</h3>
<p>我们先从简单的开始：创建一个“粗体（bold）”按钮，它能将当前选区的文字变粗。当然我们也希望这个按钮能够跟踪文档的状态——当插入点或者选区在粗体文本上时，它能够高亮显示。<br />
主要逻辑被分为两块：一是创建一个命令对象（command object），用以包装当前文档上的实际操作，当然也能查询选区的状态；二是创建一个控制器对象（controller object），能够作为句柄（handler）绑定到点击事件（click event）上，同时更新html按钮的外观。这样分开来比较合乎情理，因为不同的命令拥有相似的控制逻辑。关于这一点我们晚些就会看到。<br />
事件流有两个方向——当工具栏上的控制按钮被点击后，控制器告诉命令在文档上执行；而当光标在文档上移动时，我们想要工具栏上的按钮能及时更新状态。我们追踪所有的控制器，当选区被修改时，查询命令获得其状态并更新相应的按钮外观。</p>
<h3>命令和控制器的实现</h3>
<p>在粗体命令实现之前，命令对象之上只做了个很小的包装：</p>
<pre class="brush:js">function Command(command, editDoc) {
  this.execute = function() {
    editDoc.execCommand(command, false, null);
  };
  this.queryState = function() {
    return editDoc.queryCommandState(command)
  };
}</pre>
<blockquote><p>为什么要进行包装呢？因为我们希望能够像内建的命令一样，让自定义命令拥有统一的接口。</p></blockquote>
<p>实际上按钮仅仅是一个span元素。</p>
<pre class="brush:html">&lt;span id="boldButton"&gt;Bold&lt;/span&gt;</pre>
<p>这个span元素通过控制器（controller）和命令对象（command object）关联。</p>
<pre class="brush:js">function TogglCommandController(command, elem) {
  this.updateUI = function() {
    var state = command.queryState();
    elem.className = state?"active":"";
  }
  bindEvent(elem, "click", function(evt) {
    command.execute();
    updateToolbar();
  });
}</pre>
<p>列出来的代码中忽略了一些附加代码，用以确保按下按钮后，窗口（window）仍然在聚焦（focus）状态中。<br />
我们将上面那个方法命名为TogglCommandController，是因为它利用两种不同状态将将一个双状态命令（two-state commands）连接到一个按钮上。当点击按钮时，命令就被运行了。而后updateUI命令被调用，span元素上添加active样式类（class），按钮从而改变其外观。下面是为每种按钮所定义的不同样式：</p>
<pre class="brush:css">.toolbar span {
  border: outset;
}

.toolbar span.active {
  border: inset;
}</pre>
<p>组件是如此链接的：</p>
<pre class="brush:js">var command = Command("Bold", editDoc);
var elem = document.getElementById(îboldButton);
var controller = new TogglCommandController(command, elem);
updateListeners.push(controller);</pre>
<p>updateListeners收集器为工具栏提供控制。updateToolbar方法通过迭代列表并且调用每个控制器上的updateUI方法来确保所有控制按钮都被更新了。为了确保在文档的选区范围发生改变之后updateToolbar能够及时运行，我们绑定了如下事件：</p>
<pre class="brush:js">bindEvent(editDoc, "keyup", updateToolbar);
bindEvent(editDoc, "mouseup", updateToolbar);</pre>
<p>当一个命令执行时，updateToolbar也会被调用，如同上面列出的命令代码一样。为什么在命令执行时，我们更新整个工具栏而非相应的控制按钮呢？这是因为其它命令的状态也可能会改变。例如：右对齐（justify-right）命令运行后，左对齐（justify-left）按钮也需要更新。为了避免追踪所有类似这种互斥状态，我们更新整个工具栏。<br />
现在，我们有了双状态命令的基础架构。粗体、斜体、左对齐、右对齐、居中对齐都可以此实现。</p>
<h3>链接</h3>
<p>在实现了一些基本文本格式化命令之后，我决定给站点访问者们以在文档中添加链接的能力。在内建的createLink命令没有完全按照我们所想的工作之前，链接控制（link control）需要更多的自定义命令逻辑。内建的命令虽然也能创建链接，但是并不返回选区是否在链接之内的信息。我们需要这一特性来为工具栏提供一致性。<br />
那么我们怎么来检查选区是否在一个链接中呢？答案是创建一个功能方法：getContaining，它能够遍历当前选区的DOM树，直到找到我们想要的节点（找不到就返回none）。我们用它来检查是否包含a元素，如果包含的话，那么当前选区就处于一个链接之中。<br />
另一个扩展是我们需要让用户输入链接URL。一个新颖的设计或许会用自定义对话框来完成，但为了简单起见，我们仅用内建的window.prompt方法。如果选区在链接中，我们想在对话框中显示当前链接，这样用户就能检查或者修改它了。或者我们只显示默认的前缀http://。<br />
以下是Linkcommand方法的代码：</p>
<pre class="brush:js">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)
	};
}</pre>
<p>这个方法的逻辑是：</p>
<ol>
<li>方法首先检查一个元素是否是我们正要找的。tagName总是以大写字母从DOM那里返回，不管源代码中的是大小写。</li>
<li>getContaining方法寻找拥有特殊name的元素，包括当前选区。如果找不到就返回null。</li>
<li>如果找到一个链接，我们在对话框中插入href属性；否则插入默认的http://。</li>
<li>prompt返回null如果用户点击了取消按钮。它将放弃执行命令。</li>
<li>如果用户删除了url并且点了确定按钮，我们假定用户想要完全删除此链接。我们使用内建的unlink命令来完成它。</li>
<li>如果用户提供了一个url并且点了确定按钮，我们使用内建的createLink命令来创建链接。（假如链接已经存在，命令将使用新的url值更新链接的href属性）</li>
<li>双惊叹号的作用是将结果转换为boolean值——如果找到元素为true，找不到为false。</li>
</ol>
<p>我们可以将LinkCommand和ToggleCommandController联合起来，因为所有的工具栏控制接口都是一样的：执行（execute）和查询状态（queryState）方法。</p>
<h3>获得包含（GetContaining）</h3>
<p>现在让我们看看getContaining方法吧（editlib.js文件中），它能告诉我们当前选区在元素中的哪个部位。<br />
事情稍微变得有些复杂，因为IE的API和其它浏览器还不一样。那样的话，我们需要创建两个独立的实现，并根据检查getSelection属性来决定使用哪一个，就像这样：</p>
<pre class="brush:js">var getContaining = (window.getSelection)?w3_getContaining:iegetContaining;</pre>
<p>IE的实现相当有趣，因为它详细展示了IE选区API的微妙之处。</p>
<pre class="brush:js">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);
}</pre>
<p>它是这样工作的：</p>
<ol>
<li>选区的type属性可能为“Control”或者“Text”中的一种。什么时候会出现Control情况呢？不止一个控制区被选中时就会如此（例如，用户按下ctrl键选取了几个不连续的图像）。</li>
<li>我们并不处理多个选区的情况，此时会退出命令不执行，这样什么都不会发生。</li>
<li>如果有且仅有一个选区，就高亮它。</li>
<li>如果它是一个文本选区，我们就这样获取它的容器元素（container element）。</li>
</ol>
<p>其它浏览器使用的API比较简单：</p>
<pre class="brush:js">function w3_getContaining(editWindow, filter) {
	var range = editWindow.getSelection().getRangeAt(0); //(1)
	var container = range.commonAncestorContainer;	//(2)
	return getAncestor(container, filter);
}</pre>
<p>它是这样工作的：</p>
<ol>
<li>如果API允许选择多个选区，但是UI只接受一个，那么我们就把第一个选区看作是唯一的范围（range）。</li>
<li>这个方法可以得到包含当前选区的元素。</li>
</ol>
<p>getAncestor方法很简单——查看整个元素体系，找到我们需要寻找的目标，或者找不到的话返回null。</p>
<pre class="brush:js">/* 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;
}</pre>
<h3>多值命令</h3>
<p>像编辑字体、大小等需要不同的途径，因为它们各自都有好几个不同的可选项。对于UI窗口来说，此时就不应该还像前面一样用双状态按钮（two-state button），而是使用下拉框（select box）了。当然替代简单的开关状态（on/off state），我们还需要一系列命令和控制器（Command and Controller）来处理多值情况。<br />
这是字体选择器的html代码：</p>
<pre class="brush:html">&lt;select id="fontSelector"&gt;
  &lt;option value=""&gt;Default&lt;/option&gt;
  &lt;option value="Courier"&gt;Courier&lt;/option&gt;
  &lt;option value="Verdana"&gt;Verdana&lt;/option&gt;
  &lt;option value="Georgia"&gt;Georgia&lt;/option&gt;
&lt;/select&gt;</pre>
<p>command对象依然很简单，因为它基于内建的FontName命令：</p>
<pre class="brush:js">function ValueCommand(command, editDoc) {
  this.execute = function(value) {
    editDoc.execCommand(command, false, value);
  };
  this.queryValue = function() {
    return editDoc.queryCommandValue(command)
  };
}</pre>
<p>值命令（ValueCommand）和前面提到的双状态命令不同之处在于，它有一个queryValue方法，能够返回字符串类型的当前值。当用户选择下拉列表中的某个值时，控制器就开始执行命令。</p>
<pre class="brush:js">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();
  });
}</pre>
<p>控制器很简单，因为我们直接将选项的值映射到命令值上。<br />
字体大小选择使用相同的方式实现。我们只是用内建的FontSize方法来代替，并且使用1-7作为size的选项值。</p>
<h3>自定义命令</h3>
<p>直到现在，所有的html修改都已经通过内建的命令完成了。但是有时候你可能需要内建命令并不支持的修改行为。这时候我们就要用到DOM和Range API了。<br />
作为一个例子，我们创建一个命令用来在插入点插入一些自定义html代码。简单起见，我们只插入一个text为“Hello World”的span元素。你可以以此为基础扩展插入任何你喜欢的html代码。<br />
这个命令是这样的：</p>
<pre class="brush:js">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;
  }
}</pre>
<p>奇迹发生在overwriteWithNode那里，它会在插入点插入一个元素。（它的名字也指出，如果当前是一个非空选区，被选择的内容会被覆盖）。显而易见的是，IE和DOM 范围标准浏览器之间的实现也不同。让我们先来看看DOM标准浏览器：</p>
<pre class="brush:js">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);
  }
}</pre>
<p>range.deleteContents方法具有如下作用：如果选区是非折叠的，它将删除选区的内容。（如果选区是空的，它不会删除任何东西）。<br />
DOM的Range对象允许我们定位插入点的位置：startContainer是包含插入点的节点，startOffset是插入点在父节点中的偏移量。<br />
例如：如果startContainer是个元素并且startOffset等于3，那么插入点就位于这个节点的第3个子节点和第4个子节点之间。如果startContainer是个文本节点，startOffset则标识插入点的字符位置。比如说startOffset等于3就是说插入点在第3个和第4个字符之间。</p>
<blockquote><p>endContainer和endOffset以相同方式标识插入点的结束位置。如果选区为空，它们和startContainer、startOffset相等。</p></blockquote>
<p>如果插入点在文本中间，那么文本就被一分为二，我们可以在中间插入想要的内容。rightPart是个有用的方法，它将文本节点分成两半并返回右边那一部分。这样就可以使用insertBefore在合适的位置插入新节点了。<br />
IE的版本多少有些棘手。IE的Range对象并不能立即获得插入点的精确位置，另外我们只能用pasteHTML方法——它只专横地接受字符串格式的html代码为参数而非dom节点。<br />
基本上，IE的范围API和DOMAPI完全孤立。<br />
然而，我们仍然有办法将这两大体系链接起来：使用pasteHTML方法插入一个标记元素并为之赋予唯一的ID，随后可以用这个ID在DOM中找到它：</p>
<pre class="brush:js">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 = "<span id='" + id + "'></span>";
  range.pasteHTML(html);
  var node = editWindow.document.getElementById(id);
  return node;
}</pre>
<p>注意在完成后我们移除了标记节点，这是为了防止html被这些废弃节点弄乱。<br />
我们现在有了在插入点插入任意html代码的命令，并使用工具栏按钮和ToggleCommandController方法将它们链接到UI上。</p>
<h3>总结</h3>
<p>在这篇文章中，我们整体结构上了解并制作了一款简单的html编辑器框架。这些代码可以作为你的起点，用以开发更加高级和自定义的编辑器。</p>
]]></content:encoded>
			<wfw:commentRss>http://army8735.org/2009/09/27/156.html/feed</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>浏览器中的HTML富文本编辑（一）</title>
		<link>http://army8735.org/2009/09/25/126.html</link>
		<comments>http://army8735.org/2009/09/25/126.html#comments</comments>
		<pubDate>Fri, 25 Sep 2009 07:40:29 +0000</pubDate>
		<dc:creator>army8735</dc:creator>
				<category><![CDATA[前端开发]]></category>
		<category><![CDATA[翻译]]></category>
		<category><![CDATA[rich html]]></category>
		<category><![CDATA[富文本编辑]]></category>

		<guid isPermaLink="false">http://www.army8735.org/?p=126</guid>
		<description><![CDATA[原文标题：《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...]]></description>
			<content:encoded><![CDATA[<p>原文标题：《Rich HTML editing in the browser: part 1》</p>
<p>原文地址：<a href="http://dev.opera.com/articles/view/rich-html-editing-in-the-browser-part-1/" target="_blank">http://dev.opera.com/articles/view/rich-html-editing-in-the-browser-part-1/</a></p>
<h3>介绍</h3>
<p>时光倒流。在世界上最早的浏览器——提姆·伯纳·李（Tim Berners-Lee）发布于1990年——诞生的时候，我们可以直接在所见即所得模式下编辑网页的内容，那时的网页被构思为一种可读可写的媒介。后来经过一段时间的发展，浏览器基本上变得只读了，除了只能在form控件中输入一些纯文本而已。</p>
<p>Internate Explorer 5的发布将浏览器的所见即所得编辑特性带回了主流：新的设计模式（designMode）属性允许用户编辑整个文档（document）。一开始这一特性显得多少有些疏忽，因为它最初是Windows操作系统下IE专有的。</p>
<p>近些年来，另一些可以和IE抗衡的浏览器——Mozilla、Safari和Opera——跟随并实现了这一IE独有的特性。排版引擎比较组织（WHATWG-group）也致力于编辑系统标准的建立——HTML5中设计模式和可编辑内容文档对象模型属性（contentEditable DOM properties）的介绍。看起来在浏览器中，所见即所得编辑最终将成为网页整体的一部分。</p>
<p>这篇文章利用HTML5的可编辑特性来评审现今浏览器的基本概念和挑战。这些科目包括：</p>
<ul>
<li>不同的可编辑模式</li>
<li>编辑命令</li>
<li>编辑产生的HTML代码</li>
<li>和DOM的配合</li>
</ul>
<p>这篇文章是两篇系列文章的第一篇，第二篇内容将覆盖到一个详细的例子来实现一个编辑器。</p>
<p>注意：我只考虑到最近的主流浏览器所支持的特性：Opera 9.5、Firefox 2+和Safari 3，较旧的版本有太多的bug和不稳定的地方了。IE中的实现直到5.5版本才有了明显的变化。</p>
<h3>可编辑系统预览</h3>
<p>可编辑系统允许用户编辑页面或者页面中的一部分，它有如下几个方面：</p>
<ul>
<li>光标（caret）标识当前的插入点。用户可以输入、删除等。使用键盘或鼠标可以移动光标或者选区。</li>
<li>一些浏览器提供UI组件用户缩放重定位图片、表格和其它可重定位的元素（elements）。</li>
<li>内置了一系列独立的编辑命令：粗体、斜体、插入链接、粘贴、撤消等。这些可以被快捷键、脚本命令接口（script command API）调用。使用API可以轻易实现一个编辑器的工具栏（toolbar）。</li>
<li>使用范围和选区接口（range and selection API），你可以写出自己的脚本来修改任意html代码。这一特性通常用来实现自定义编辑命令。</li>
<li>可编辑系统允许你改变html代码。一旦你创建了它，它并不会真正地修改你网页的内容。举例来说，除非你写了保存修改的脚本命令，否则是无法将修改的内容存回服务器的。</li>
</ul>
<p>这里还有两条关于可编辑系统的警告：</p>
<ul>
<li>命令和编辑行为的产生是不可预知的，而产生的html代码结果在不同的浏览器中可能大相径庭。</li>
<li>直到2000年5.5版本的出现，IE浏览器中的实现才有了大幅度的变化。系统产生的html代码可能会让一些敏感的人颤栗——如果你看到最后的字体节点（&lt;font&gt;tag），你肯定会大吃一惊！</li>
</ul>
<h3>开启可编辑特性</h3>
<p>这里有两种方法在网页上来创建一块可编辑的选区——设计模式（designMode）和可编辑内容（contentEditable）属性。</p>
<p>一个窗口（window）或者框架（frame）是以设置文档（document）对象上的设计模式属性为真（true）来开启的。（警告：在IE中，这个属性在文档上是不存在的，必须从窗口对象上获取。）典型的可编辑框是将一个内嵌框架（iframe）的设置为设计模式。</p>
<p>一个包含文本的元素想要可编辑的话必须设置设计模式属性为真。（Firefox 2并不支持设计模式属性，但是在Firefox 3中却得到了支持。当然IE、Safari、Opera都早已支持。）</p>
<h3>可编辑按键</h3>
<p>在一个简单编辑器中，你或多或少会期望能够使用键盘和鼠标来进行编辑内容。当文档获得焦点（focus）时，光标就会显示，它可以上下左右移动。键入或者删除字符也会改变光标的位置。文本选区可以被移动、删除以及覆盖。</p>
<p>一个人性化的设定是所有按键编辑行为都可以自动被记录并且撤消。（后面会提到如何使用撤消工功能。）</p>
<p>复杂的问题出现了：当我们按下了回车键该怎么办？这样做的话并不会立刻显示所产生html代码，而且各个浏览器的结果还会根据上下文有所不同。如果光标在一个非空的p元素内，所有浏览器都会关闭这个元素，并且产生一个拥有相同属性的新元素，最后把光标定位到它里面。（Mozilla会在光标后附带插入一个多余的br换行元素。）例如（这些例子中都用竖线来表示光标所在位置）：</p>
<pre class="brush:html">&lt;p&gt;bla bla|&lt;/p&gt;</pre>
<p>按下回车键后IE和Safari会变为：</p>
<pre class="brush:html">&lt;p&gt;bla bla&lt;/p&gt;
&lt;p&gt;|&lt;/p&gt;</pre>
<p>假如光标在一个非空的h1元素中，所有的浏览器都会关闭它。但是IE和Opera却会插入一个新的p元素，并且将光标位置置入其中。Safari会插入一个新的h1<br />
元素并将光标位置设定在里面。Mozilla不会产生任何元素，但却会插入两个换行元素在光标后面。例如：</p>
<pre class="brush:html">&lt;h1&gt;bla bla|&lt;/h1&gt;</pre>
<p>按下回车键后Opera会变为：</p>
<pre class="brush:html">&lt;h1&gt;bla bla|&lt;/h1&gt;
&lt;p&gt;|&lt;/p&gt;</pre>
<p>但在Mozilla中却会是：</p>
<pre class="brush:html">&lt;h1&gt;bla bla|&lt;/h1&gt;
|&lt;br&gt;&lt;br&gt;</pre>
<p>在Safari中则是：</p>
<pre class="brush:html">&lt;h1&gt;bla bla|&lt;/h1&gt;
&lt;h1&gt;|&lt;/h1&gt;</pre>
<p>如果你直接在body元素中输入文本（并不包含其它元素），再按下回车键的话，Mozilla会插入一个br元素，IE和Opera会转换前面的文本放入一个p元素中并且插入一个新的p元素，Safari会插入一个新的div元素。<br />
当在一个div元素中键入时，Safari、Opera和IE将关闭当前div元素并且插入一个新的div元素，Mozilla将插入一个br元素并且仍然呆在这个div元素中。<br />
如果在光标外有嵌套的块级（block）元素，所有浏览器都会关闭（并且复制）最深的元素。光标依然会呆在外部块级元素内。<br />
最关键的：令人惊讶的是，在块级元素的处理上，IE反而是最标准的！Mozilla会在部分情况下使用br元素代替块级元素，从而使得原文的显示保持正常。</p>
<h3>光标位置</h3>
<p>光标是在字符之间移动的。它在标签之间的定位是不可见的。逻辑上看起来所有浏览器是一致的。有关块级元素：光标总会被定位到最深的块级元素上，没法让光标位于两端之间。<br />
例如，看下面，竖线代表所有光标可能处于的位置：</p>
<pre class="brush:html">&lt;p&gt;|P|1|&lt;/p&gt;&lt;p&gt;|p|2|&lt;/p&gt;
&lt;div&gt;&lt;p&gt;|P|3|&lt;/p&gt;&lt;div&gt;&lt;p&gt;|P|4|&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;</pre>
<p>关于行级（inline）元素，假如光标在文字左侧，它会在所有行级元素边界之外；假如光标在文字右侧，则会在边界之内。例如：</p>
<pre class="brush:html">&lt;p&gt;|A|&lt;strong&gt;&lt;em&gt;B|&lt;/em&gt;&lt;/strong&gt;C|&lt;/p&gt;</pre>
<p>所以假如你直接在粗体文本左侧输入字符的话，新输入的字符并不会是粗体；相反在右侧输入则会是粗体。</p>
<h3>删除</h3>
<p>如果你直接删除一个段落的边界，结果可想而知：左边的块（block）“获胜”了，块右边的内容被包含在了左边：</p>
<pre class="brush:html">&lt;h1&gt;Overskrift&lt;/h1&gt;&lt;p&gt;|Text&lt;/p&gt;</pre>
<p>如果按下了回退键（backspace），结果是：</p>
<pre class="brush:html">&lt;h1&gt;Overskrift|Text&lt;/h1&gt;</pre>
<p>然而Safari却耍了个小聪明（或者说让人讨厌，这完全取决于你的心情），让右边段落的内容保持原来的风格（style）：</p>
<pre class="brush:html">&lt;h1&gt;Overskrift|&lt;span class="Apple-style-span" style="font-size:16px;font-weight:normal;"&gt;Text&lt;/span&gt;&lt;/h1&gt;</pre>
<h3>对象编辑</h3>
<p>浏览器支持一些特殊的UI特性编辑。<br />
IE允许你用鼠标拖动对象的4个角来拉伸图像、表格、表单控制元素或者完全重定位元素（当对象获得焦点并被选择后，鼠标靠近句柄就会出现）。<br />
Mozilla也允许你拉伸表和和图像，只是附带地允许用户创建新行新列。Mozilla还附带允许你重定位绝对定位的元素。但这些特性都是浏览器完全私有的，并且不能被自定义。</p>
<h3>编辑命令</h3>
<p>不同浏览器支持不同编辑命令。由命令所产生的html代码在不同浏览器中并不一样，且不一定符合标准。例如，在IE中，粗体命令会产生这样代码：</p>
<pre class="brush:html">&lt;strong&gt;Hello!&lt;/strong&gt;</pre>
<p>Safari则会产生这样的：</p>
<pre class="brush:html">&lt;span class="Apple-style-span" style="font-weight:bold;"&gt;Hello!&lt;/span&gt;</pre>
<p>所产生的代码有些过时，至少在IE中是这样。令人畏惧的标签（如&lt;FONT color=#ff0000&gt;123&lt;/FONT&gt;）会被一系列命令产生，并且生成的html代码并不符合xhtml标准有时甚至不符合html标准！<br />
Opera的html实现接近（并不相等）IE，使用font元素等等。Safari产生格式化的span元素并用内联css。Safari这一行为的优点是所产生的html代码能通过HTML 4.01 Strict校验。<br />
Mozilla支持两种模式——既可以产生像IE/Opera的元素也可以像Safari那样使用style属性。<br />
如果你担心html校验的话，可以在服务器端做一些过滤清理，将那些标签杂烩转换为标准的xhtml（你可能很需要这样做，以防XSS漏洞攻击）。</p>
<h3>快捷键</h3>
<p>一些编辑命令支持快捷键，如Ctrl/Cmd + B是粗体、Ctrl/Cmd + B是撤消等等。然而这些快捷键变量在不同浏览器中位置还不完全一样。<br />
快捷键的对照关系图不能被修改，但是却可以在在按键事件中截取到来重写它们。</p>
<h3>命令程序接口</h3>
<p>你可能想要实现一个工具栏来允许用户运行一些编辑命令，这些可以用API来完成。这些API看起来并不像典型的DOM API，它实际上是实现了IOleCommandTarget接口——这是一个微软应用程序中应用的COM接口，用来同步编辑文档的工具。<br />
命令API基于文档对象之上并且是由一个叫执行命令（execCommand）方法实现的，并且一串以“query”开始的方法会返回命令的信息。<br />
所有的方法都以命令ID为第一个参数——代表命令名称的字符串，剩余的是要执行的方法。</p>
<h3>执行命令（ExecCommand）</h3>
<p>在当前选取上执行命令，一些命令会切换状态，例如你在选取一段粗体文本之后执行粗体命令，它们就会变回普通样式。其它一些命令需要传入值参，例如foreclor需要颜色代码。<br />
一些命令提供独立的对话框，例如链接（link）命令显示一个对话框来输入url地址，这些对话框不能被自定义，但是却可以被禁止掉。例如：</p>
<pre class="brush:js">result = document.execCommand(command, useDialog, value);</pre>
<p>不同参数的含义：</p>
<ul>
<li> command：String，命令的名字。</li>
<li>useDialog：Boolean，显示内建对话框（并不是所有命令都有对话框）。</li>
<li>value：命令所需要的值，并不是所有命令都需要值；假如一个内建的对话框被显示了，那么值是从对话框里获取的。</li>
<li>结果：如果命令成功执行，则返回true；如果被用户所取消（用户取消了对话框）或者执行失败，则返回false。</li>
</ul>
<p>当没有选择文字的情况下（只有一个光标），所有浏览器都支持文本格式化命令。假如光标在一个单词中间，IE会将整个单词格式化掉，而其它浏览器仅仅格式化下一个将要输入的字符，除非光标提前移动了。</p>
<h3>查询命令（QueryCommand）</h3>
<p>相对于和文档选区（document selection）相关的工具栏按钮来说，使用查询命令来查询它们的状态，这可能是所有浏览器中最健全的支持了。<br />
<strong>开启查询命令（QueryCommandEnabled）</strong><br />
根据当前选区的命令是否可以运行，查询命令处于开启或关闭的状态下。例如：“解除链接（unlink）”只有当光标在一个链接（link）选区内才可用。而当光标处于不可编辑的区域中的话，所有命令都不可用。<br />
<strong>查询命令状态（QueryCommandState）</strong><br />
它标识着当前选区内是否已经运行过相关命令了。例如在一个粗体选区中，粗体命令（bold command）的状态就是true。<br />
<strong>查询命令返回值（QueryCommandValue）</strong><br />
它是目标选区执行命令后返回的值，对应于执行命令中传入的参数值。如foreColor返回当前选区的颜色代码（String）。</p>
<p>不同浏览器之间的格式化命令是不同的。例如，foreColor命令在IE中会返回一个16进制的颜色代码（如#ff0000），其它浏览器则返回一个rbg表达式（如rgb(255,0,0)）。<br />
一些返回值是建立在浏览器本地基础上的。例如在IE中，格式化块（FormatBlock）会根据浏览器的UI语言返回段落的名称。<br />
像粗体这样的命令，返回值总是false。（api包含两个附加的方法，queryCommandSupported和queryCommandIndeterminate，但是它们用起来极不可靠。）</p>
<h3>范围和选区API</h3>
<p>内建的命令经常被用在实际的选区上，但是却不能修改它们的行为或者自定义实现。使用范围和选区（Range and Selection）API，你可以实现自主的html转换，就像模拟自定义实现一样。</p>
<p>需要说明的是，一些转换破坏了经常被撤销/重做（undo/redo）命令使用的撤销栈（undo stack），这是非常不友好的。但是它却实现了自定义命令的功能，是否值得这样做取决于你的你的页面建立目标。</p>
<p>范围和选区API包含两个核心类：</p>
<ul>
<li>范围——一个文档中连续的字符串范围。范围可能会在元素边界上重叠。一个范围拥有开始点（start point）和结束点（end point）。如果开始点的位置和结束点相等的话，我们就说这个范围是折叠的。</li>
<li>选区——描述当前用户选取的文档选区。一个选区包括唯一的一个高亮范围。如果范围是折叠的，那么选区就以光标形态显示。</li>
</ul>
<p>（范围和选区可以用在可编辑区域之外，你可以在只读的文档上创建选区，但是这样的话它不能被折叠，并且只读选区并不显示光标。）</p>
<p>这些概念在所有浏览器中通用，但是可用的API却在IE和其它浏览器中有区别。IE使用它私有的范围选区API，其它浏览器则使用W3C标准的范围API和非标准的选区API。</p>
<p>最主要的差别还是：IE的范围是以存储包括html标记在内的String为基础的；而W3C标准则是存储DOM节点树。</p>
<h3>范围例子：</h3>
<p>为了展示二者不同之处，这有一个在当前选区插入行内元素code的代码。</p>
<p>IE中（editWindow是个对处于designMode的frame的引用）：</p>
<pre class="brush:js">var rng = editWindow.document.seletion.createRange();
rng.parseHTML("" + rng.htmlText + "");</pre>
<p>Mozilla中：</p>
<pre class="brush:js">var rng = editWindow.getSelection().getRangeAt(0);
rng.surroundContents(document.createElement("code"));</pre>
<h3>控制选区（Control selection）</h3>
<p>IE支持控制选区，它和普通的范围选区不一样。当你在一副图片上、表单上、表格的边线上点击的时候，控制选区就产生了。</p>
<p>并且你可以按下Ctrl键来达到一次性选择多个控制选区的目的，其它浏览器并不支持——它们仍会当作一个文本选区来看待。</p>
<h3>总结</h3>
<p>本文带你浏览了基于浏览器的可编辑概念。系列文章的<a href="http://www.army8735.org/2009/09/27/156.html" target="_blank">第二篇</a>将给你展示一个很有价值例子——如何利用这些API来实现可编辑的页面。</p>
]]></content:encoded>
			<wfw:commentRss>http://army8735.org/2009/09/25/126.html/feed</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
	</channel>
</rss>

