コンピュータクワガタ

かっぱのかっぱによるコンピュータ関連のサイトです

JavaScriptでテキストの装飾をしてみる

いろいろな理由があって、Googleドキュメントのようにリッチテキストをブラウザ上で行う方法を調べてみました。

一応、

WYSIWYGエディタに夢中になったときのメモ

http://www.fourmeisters.com/blog/yoshi/2007/09/wysiwyg.html

にあるように、document.designModeというものがあり、これで簡単にできそうな感じです。

ただし、今回は単純にマーキングとかアンダーラインだけを考慮したいので、テキスト自体の編集をされてしまうと困るので違う方法がないか探してみたところ、SelectionオブジェクトとRangeオブジェクトを使うことで実現することができました。
まず、RangeオブジェクトはDOMインターフェースとして定義されています。

Document Object Model Range

http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html

Selectionオブジェクトを取得する方法の定義やrangeの日本語訳等は以下のサイトを参考にしました。
https://developer.mozilla.org/ja/DOM/Selection
https://developer.mozilla.org/ja/DOM/range

実際に使用したメソッド等を簡単に説明します。
まず、通常のWebページに置いて画面上選択されている部分を取得するためには、Selectionオブジェクトを取得する必要があります。Selectionオブジェクトは、以下のように取得します。Chromeではdocument.getSelection()でも取得できましたが、Firefoxでは取得できなかったためwindowからgetSelectionしました。

var selection = window.getSelection();

そして、選択範囲を取得するためにはSelectionオブジェクトからさらにRangeオブジェクトを取得する必要があります。FirefoxのようにCtrlキーを押しながら複数箇所を選択できるブラウザもあるため、Rangeオブジェクトはそのインデックス(何番目の選択範囲か)を指定して取得します。具体的には以下の様になります。

var range = selection.getRangeAt(index);

取得したrangeの内容は、以下のメソッドで複製を取得できます。

range.cloneContents()

また、選択している部分の内容を削除するためには、以下のメソッドを使用します。

range.deleteContents();

さらに、選択している部分にNodeを追加するには以下のメソッドを使用します。

range.insertNode(node);

これらのメソッドを使用して、簡単な装飾ツールを作成することができます。多少バグってますが、気にしない。ソースは以下。

<!DOCTYPE html>
<html>
 <head>
  <meta charset="utf-8">
  <title>テキストの装飾</title>
  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
  <script type="text/javascript">
$(function() {
    $('#button1').click(function() {
        designString('background-color: #ffff00;');
    });

    $('#button2').click(function() {
        designString('text-decoration: underline;');
    });
});

function designString(styleString) {
    var selection = window.getSelection();
    if (!selection || selection.rangeCount == 0) {
        return;
    }
    var range = selection.getRangeAt(0);

    var contentsId = 'div1';

    var start = $(range.startContainer).parents('#' + contentsId).attr('id') ==  contentsId ? true : false;
    var end = $(range.endContainer).parents('#' + contentsId).attr('id') == contentsId ? true : false;

    if (start && end) {
        var add = $('<span style="' + styleString + '"></span>').append(range.cloneContents());
        range.deleteContents();
        range.insertNode(add.get(0));
    }

    selection.collapseToStart();
}
  </script>
 </head>
 <body style="line-height: 1.5em;">
  <div id="div1">あいうえおかきくけこさしすせそたちつてとなにぬねの<br>
はひふへほまみむめもやゆよらりるれろわをん<br>
あいうえおかきくけこさしすせそたちつてとなにぬねの<br>
はひふへほまみむめもやゆよらりるれろわをん<br>
あいうえおかきくけこさしすせそたちつてとなにぬねの<br>
はひふへほまみむめもやゆよらりるれろわをん<br>
あいうえおかきくけこさしすせそたちつてとなにぬねの<br>
はひふへほまみむめもやゆよらりるれろわをん</div>
  <button id="button1">マーカー</button><br>
  <button id="button2">下線</button>
  <div id="result"></div>
 </body>
</html>

動く物は以下に置いてあります。一応、Firefox 3.6、Chrome 8、Safari 5では確認しています。
http://www.kuwalab.net/html5/range1.html
IEの対応等もできなくはないのですが、ここまでにしておきます。

jQueryオブジェクトの比較

ふと、jQueryオブジェクトの比較はそのまま行えるのかどうか疑問だったので確認しました。
Googleに聞くとすでに検証されているようで、以下の2つのサイトを参考に検証してみました。

jQueryオブジェクトの比較

http://diary.flowind.jp/program/ajax/jquery/flowind61.html

$("body")==$("body") がfalseになるのはなぜなんだー

http://gyauza.egoism.jp/clip/archives/2009/05/090526-bodybody-false/

基本的にはjQueryオブジェクトを単純に比較、例えば$('#id') == $('#id')はfalseになるということです。通常のオブジェクト生成と同様で、内部的に作成されるオブジェクトはキャッシュされないので別のインスタンスが生成されるためfalseになります。
そのため、jQueryオブジェクトのget(0)メソッドでDOM要素を取得しそれを比較することで同一のノードを指し示しているかを確認することができます。

サンプルとして以下のようなソースで確認しました。

<!DOCTYPE html>
<html>
 <head>
  <meta charset="utf-8">
  <title>jQueryオブジェクトの比較</title>
  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
  <script type="text/javascript">
$(function() {
    var result = $('#result');
    var span1 = $('#compare > span:eq(0)');
    var span2 = $('#compare > span:eq(1)');

    result.html('(span1 == span2)=' + (span1 == span2) + '<br>');
    result.append('(span1.get(0) == span2.get(0))=' + (span1.get(0) == span2.get(0)) + '<br>');
    result.append('(span1 == span1)=' + (span1 == span1) + '<br>');
    result.append('(span1 == $(\'#compare > span:eq(0)\'))=' + (span1 == $('#compare > span:eq(0)')) + '<br>');
    result.append('(span1.get(0) == $(\'#compare > span:eq(0)\').get(0))=' + (span1.get(0) == $('#compare > span:eq(0)').get(0)) + '<br>');
});
  </script>
 </head>
 <body>
  <div id="compare">
   <span></span>
   <span></span>
  </div>
  <div id="result"></div>
 </body>
</html>

実行結果は以下のようになる。わざとjQueryオブジェクトの生成時に変数に代入してます。同じ変数同士の比較だとtrueになり(当たり前だ!!)、同じ条件で生成したjQueryオブジェクトと比較するとfalseになっているのがわかります。また、get(0)で取得したDOM要素が同じであればtrueになることも確認できます。

(span1 == span2)=false
(span1.get(0) == span2.get(0))=false
(span1 == span1)=true
(span1 == $('#compare > span:eq(0)'))=false
(span1.get(0) == $('#compare > span:eq(0)').get(0))=true

FirefoxでOfffsetX、OffsetYを実現する。

jQuery.Eventオブジェクトで持っているプロパティはpageX、pageYである。その他に、標準のeventもコピーされているので、いろいろ持っているが、それは以下のサイトが詳しい。
http://www.openspc2.org/JavaScript/Ajax/Ajax_study/chapter05/013/index.html
上記サイトで、FirefoxでoffsetX、offsetYに対応していないことが確認できる。実際に試しても動作しない。
まず、使いそうな座標指定を以下にまとめる。

プロパティ 説明
pageX
pageY
ドキュメント内の相対座用を示す。
clientX
clientY
表示されているウィンドウ内の座標を示す。
screenX
screenY
PC等の表示している端末上の座標の絶対位置。
offsetX
offsetY
イベントが発生した要素内の座標。

offsetXとoffsetYをFirefoxでも使えるようにするには、以下のようにすると同じ値が取れる。

var off = $(this).offset();
alert('offsetX=' + (e.pageX - off.left) + ' offsetY=' + (e.pageY - off.top));

簡単に試すサンプルを以下に示す。一応IE 8、Firefox 3.6、Chrome 8では動きそう。

<!DOCTYPE html>
<html>
 <head>
  <meta charset="UTF-8" />
  <title>jQuery.Eventの属性3</title>
  <script type="text/javascript" src="jquery.js"></script>
  <script type="text/javascript">
$(function() {
    var result1 = $('#result1');
    var result2 = $('#result2');
    var result3 = $('#result3');
    var result4 = $('#result4');

    $('#div1').mousemove(function(e) {
        result1.html('pageX=' + e.pageX + ' pageY=' + e.pageY);
        result2.html('clientX=' + e.clientX + ' clientY=' + e.clientY);
        result3.html('screenX=' + e.screenX + ' screenY=' + e.screenY);
        if ($.browser.mozilla) {
            var off = $(this).offset();
            result4.html('offsetX=' + (e.pageX - off.left) + ' offsetY=' + (e.pageY - off.top));
        } else {
            result4.html('offsetX=' + e.offsetX + ' offsetY=' + e.offsetY);
        }
    });
});
  </script>
 </head>
 <body>
  <div id="div1" style="width: 1000px; height: 1000px; background-color: #ffff00; margin: 30px;">ここでマウスを動かす</div>
  <div id="result1" style="margin-left: 50px;"></div>
  <div id="result2" style="margin-left: 50px;"></div>
  <div id="result3" style="margin-left: 50px;"></div>
  <div id="result4" style="margin-left: 50px;"></div>
 </body>
</html>

Dojoのcombobox

Dojoでcomboboxを設定してみる。
まず、Dojoのダウンロード
http://dojotoolkit.org/downloads
から、Dojo Toolkit 1.2.3: Dojo + Dijit + DojoXをダウンロードし、展開する。
展開すると、

  • dijit
  • dojo
  • dojox

という3つのフォルダができる。
同じ階層に、以下のHTMLを置くとコンボボックスができる。これはマニュアル通り(http://dojotoolkit.org/book/dojo-book-0-9/part-2-dijit/form-validation-specialized-input/auto-completer)のHTMLだが、cssJavaScriptのファイルはローカルを見るように変更している。また、JavaScriptファイルの読み込みで、dojo/dojo.xd.jsとなっているが、ここはdojo/dojo.jsとしている。一応ネットワーク越しにアクセスされても困るのでネットワークケーブルを抜いて実行してみる。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
            "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Simple ComboBox</title>
    <style type="text/css">
        @import "dijit/themes/tundra/tundra.css";
        @import "dojo/resources/dojo.css"
    </style>
    <script type="text/javascript" src="dojo/dojo.js"
        djConfig="parseOnLoad: true"></script>
    <script type="text/javascript">
       dojo.require("dojo.parser");
       dojo.require("dijit.form.ComboBox");
       function setVal1(value) {
           console.debug("Selected "+value);
       }
   </script>
</head>
<body class="tundra">
        <select name="state1"
                dojoType="dijit.form.ComboBox"
                autocomplete="false"
                value="California"
                onChange="setVal1">
                <option selected="selected">California</option>
                <option >Illinois</option>
                <option >New York</option>
                <option >Texas</option>
        </select>
</body></html>

モーダルダイアログ

あんまりブラウザ依存のものは使いたくなけど、仕方がないか。
何かといえば、モーダルダイアログ。
手軽には使えるけど、意外とくせがある。
参考は以下。
http://msdn.microsoft.com/ja-jp/library/cc428178.aspx
http://park14.wakwak.com/~kimihiko/web-maniacs/javascript/modal/001/
素直に、JavaScriptライブラリーでダイアログを出したほうがいい気もしてきた。

出羽さんのブログより

出羽ブログよりJavaScriptの記事。
http://d.hatena.ne.jp/dewa/20080110#1199944760
最近、JavaScriptでクラスを作ることがあって、動きがいまいちつかめなかったけど、これでわかった。
前は、クラスの定義で

var Test = function(a) {
  this.a = a;
  a = new String("置き換え");
  
  function func() {
    alert("a= " + a);
    alert("this.a= " + this.a);
  }
}

としていました。この状態で、

var t1 = new Test("para");
t1.func

としても、this.aの値がnullでした。
勘違いは、クラスの中のfunctionもthisで定義するところで、以下のようにしてうまく行きました。

var Test = function(a) {
  this.a = a;
  a = new String("置き換え");
  
  this.func = function() {
    alert("a= " + a);
    alert("this.a= " + this.a);
  }
}

先ほどとおなじく

var t1 = new Test("para");
t1.func

とすると、

a-置き換え
this.a=para

となります。
だいぶ見えました。
JavaScriptは簡単なようで難しいです。

外部ファイルの読み込み

ちょっとはまったのでメモ。
XHTMLから外部JavaScriptファイルを読み込む際に、次のように書くとうまく動かない。

<script type="text/javascript" src="js/dom.js" />

これは、終わりタグを明示して書いてあげないといけない。

<script type="text/javascript" src="js/dom.js"></script>

2時間くらいはまってしまった。