クロスブラウザでRangeオブジェクト扱う2
選択範囲を任意の要素で囲う関数がネック。
Operaは、要素またいだsurroundContentsエラーになる。
テキストノードだけ抜き出して、それぞれ囲む必要がある。
もう少し汎用的になったら、どっかに上げる予定。
/** * exRange JavaScript Library v0.3 * どのブラウザでも簡単にRangeオブジェクトを扱えるライブラリを目指してます。 * ブログで更新状況公開中 * http://d.hatena.ne.jp/TakiTake * * Author: TakiTake <TakiTake.create@gmail.com> * Under the NYSL licenses. * http://www.kmonos.net/nysl/ */ (function() { window.exRange = function(selection) { var // 共通して使うメソッド common = { _from: 0, // 先頭からの選択開始位置 _to: 0, // 先頭からの選択終了位置 /** * 制御文字を除いた文字列の長さを返す * <s>String.replaceで改行コードが削除できないので、一文字ずつ見て文字列の長さを数える</s> * 上の文は、間違い。RegExpの第二引数に"g"オプション渡してなかったorz 修正中 * @param str 長さを数えたい文字列 * @return 文字列の長さ */ count: function(str) { var len = 0, unnecessity = '[' + String.fromCharCode(0) + '-' + String.fromCharCode(31) + ']', // ASCIIコードの0-31は制御文字なので排除 pattern = new RegExp(unnecessity); for (var i = 0, l = str.length; i < l; i++) { if (!pattern.test(str.charAt(i))) len++; } return len; }, /** * 先頭から選択開始位置までの情報を(取得|設定) */ from: function() { switch (arguments.length) { case 0: return this._from; case 1: this.setFrom(arguments[0]); } return this; }, /** * 先頭から選択終了位置までの情報を(取得|設定) */ to: function() { switch (arguments.length) { case 0: return this._to; case 1: this.setTo(arguments[0]); } return this; }, /** * 先頭からの選択位置情報を取得 * @return [選択開始位置, 選択終了位置] */ pos: function() { return [this._from, this._to]; }, /** * 選択開始ノード、位置を(取得|設定) */ start: function() { switch (arguments.length) { case 0: return [this.startContainer, this.startOffset]; case 1: case 2: this._setStart.apply(this, arguments); } return this; }, /** * 選択終了ノード、位置を(取得|設定) */ end: function() { switch (arguments.length) { case 0: return [this.endContainer, this.endOffset]; case 1: case 2: this._setEnd.apply(this, arguments); } return this; } }, // DOM Level 2 Rangeをサポートしているかどうかでラッパーを変える exclusive = (function() { var m; // DOM Level 2 Rangeをサポートしているブラウザ向けメソッド if (document.createRange) { m = { _setStart: function(node, offset) { // 作業用Range作成 var range = document.createRange(); // テキストノードじゃないとsetStartできないのでテキストノード出るまで子ノード見てく // 改善する必要あり while (node.nodeType == 1) { node = node.firstChild; } this.setStart(node, offset || 0); // body要素を選択し、選択終了位置を_setStartの引数で再設定する // 文字数を数えると先頭からの位置が割り出せるという寸法 range.selectNode(document.body); range.setEnd(node, offset || 0); this._from = this.count(range.toString()); }, _setEnd: function(node, offset) { var range = document.createRange(); while (node.nodeType == 1) { node = node.firstChild; } this.setEnd(node, offset || 0); range.selectNode(document.body); range.setEnd(node, offset || 0); this._to = this.count(range.toString()); }, /** * 引数で渡された要素を選択する * @param node html要素 * @return 拡張Rangeオブジェクト */ sel: function(node) { this.selectNode(node); return this; }, /** * 選択範囲内のテキストを引数で指定した要素で囲む * @param elem テキストを囲むための要素 * @return 拡張Rangeオブジェクト */ wrap: function(elem) { if (this.startContainer == this.endContainer) this.surroundContents(elem); else { // ここがキモ。作成中 } return this; } } // IE用 } else if (document.body.createTextRange) { m = { collapsed: false, // Rangeのstartとendの位置が同じかどうか真偽値を返す startContainer: null, startOffset: 0, endContainer: null, endOffset: 0, setCollapsed: function() { if ( (this.startContainer && this.endContainer) && (this.startContainer == this.endContainer) && (this.startOffset == this.endOffset) ) this.collapsed = true; else this.collapsed = false; }, _setStart: function(node, offset) { // 作業用Range作成 var range = document.body.createTextRange(); // 選択終了位置が未設定の場合は設定する if (this.endContainer == null) { this.moveToElementText(node); this.endContainer = node; this.endOffset = node.innerText.length; } // 選択開始ノードと位置を保持 this.startContainer = node; this.startOffset = offset || 0; // 選択開始ノードと位置を実際に設定 range.moveToElementText(node); range.moveStart('character', this.startOffset); this.setEndPoint('StartToStart', range); // ページ先頭からの位置を取得 range.setEndPoint('EndToStart', range); range.setEndPoint('StartToStart', document.body.createTextRange()); this._from = this.count(range.text); this.setCollapsed(); }, _setEnd: function(node, offset) { // 作業用Range作成 var range = document.body.createTextRange(); // 選択開始位置が未設定の場合は設定する if (this.startContainer == null) { this.moveToElementText(node); this.startContainer = node; this.startOffset = 0; } // 選択終了ノードと位置を保持 this.endContainer = node; this.endOffset = offset || 0; // 選択終了ノードと位置を実際に設定 range.moveToElementText(node); range.moveStart('character', this.endOffset); this.setEndPoint('EndToStart', range); // ページ先頭からの位置を取得 range.setEndPoint('EndToStart', range); range.setEndPoint('StartToStart', document.body.createTextRange()); this._to = this.count(range.text); this.setCollapsed(); }, sel: function(node) { this.moveToElementText(node); this.startContainer = node; this.startOffset = 0; this.endContainer = node; this.endOffset = node.innerText.length; this.setCollapsed(); return this; }, wrap: function(elem) { // ここもキモ elem.innerHTML = this.text; this.pasteHTML(elem.outerHTML); return this; }, detach: function() { this.setEndPoint('EndToStart', this); } }; } else { alert('Rangeをサポートしていないブラウザです。\nFirefox, Operaなどのブラウザで閲覧してください。'); throw 'Can not use exRange.js'; } return m; })(), // Rangeオブジェクト取得 range = (function() { var r = new Object(); if (document.createRange) r = (selection) ? selection.getRangeAt(0) : document.createRange(); else if (document.body.createTextRange) r = (selection) ? selection.createRange() : document.body.createTextRange(); return r; })(); // 共通メソッドを設定 for (var o in common) range[o] = common[o]; // ブラウザ別ラッパーメソッドを設定 for (var o in exclusive) range[o] = exclusive[o]; return range; }; })();