Dojo のリッチ・テキスト・エディターをマスターする

Dojo のリッチ・テキスト・エディターをプラグインによって拡張する

Dojo のリッチ・テキスト・エディターは Dojo のウィジェットの中で最もよく使われるものの 1 つです。しかし、このエディターは多くの優れた機能を持ちながら、特定の要件に対応できるほど強力ではない場合があります。幸いなことに、このエディターはプラガバブルなアーキテクチャーを採用しているため、プラグインを使って機能を強化することができます。この記事では、Dojo のリッチ・テキスト・エディターの使い方について、またこのエディターを必要に応じて拡張する方法について学びます。

He Gu Yi, Software Engineer, IBM

Photo of He Gu YiHe Gu Yi は上海にある IBM China Development Lab のソフトウェア技術者であり、Web 2.0 関連のプロジェクトに従事しています。彼の専門は Ajax と Dojo ライブラリーです。



Zhu Xiao Wen, Software Engineer, IBM

Photo of Zhu Xioa WenZhu Xiao Wen は上海にある IBM China Development Lab のソフトウェア技術者です。彼は Dojo ライブラリーのさまざまな新機能に関する業務に従事しています。彼は Ajax その他の Web 2.0 技術に関心を持ち、またそれらの領域を専門としています。



2011年 1月 11日

はじめに

Dojo のリッチ・テキスト・エディターである Dijit.Editor は、ワード・プロセッサーのような外観を持ち、ワード・プロセッサーのような動作をするように設計されています。Dijit.Editor にはツールバーと HTML 出力があり、そのプラグイン・アーキテクチャーによって新しいコマンド、新しいボタン、その他の機能がサポートされます。また、ブラウザーへの対応については心配する必要がありません。Dijit.Editor は、Internet Explorer (IE) 6/7/8、Firefox3、Chrome、Safari を含む、いくつかのブラウザーをサポートしています。

この記事では、Dojo のリッチ・テキスト・エディターの基本について、また Dojo に用意されたプラグインを使ってパフォーマンスを高める方法について学びます。さらに、独自のプラグインを作成し、エディターのツールバーを拡張する方法も学びます。

Dojo のリッチ・テキスト・エディター

図 1 は Dojo の昔ながらのリッチ・テキスト・エディターを示しています。このエディターのツールバーには、切り取り、コピー、貼り付けなど、いくつかの便利なコマンドがあります。コンテンツの編集は WYSIWYG 形式で行うことができます。

図 1. Dojo のリッチ・テキスト・エディター
Dojo の昔ながらのリッチ・テキスト・エディターのスクリーンショット

このエディターの使い方は非常に簡単です。最初のステップとして、エディターを使う対象となるページで dijit.Editor を読み込みます。これを、通常 dojo.require を呼び出す場所と同じ場所 (通常は <head> の <script> タグ) で行います。また、テーマ (Claro など) を選択するための CSS ファイルをインポートする必要があります。リスト 1 のように、必ず <body> 要素の CSS クラスを “claro” に設定します。

リスト 1. エディターのクラス定義をインポートする
<html>
<head>
	<title>Dojo Rich Text Editor</title>
	<style type="text/css">
		@import "../../../dijit/themes/claro/claro.css";
	</style>
	<script type="text/javascript">
		dojo.require("dijit.Editor");
	</script>
</head>
<body class="claro">

</body>
</html>

エディターを作成する方法には、プログラムによる方法と宣言による方法の 2 つの方法があります。どちらの方法でも結果は同じです。リスト 2 は宣言によってエディターを作成する方法を示しています。

リスト 2. 宣言によってエディターを作成する
<div dojoType="dijit.Editor" id="editor1">
    <p>
        This is the content of the editor.
    </p>
</div>

作成されたエディターの DOM ツリーを調べてみると、このエディターがツールバーと iframe 領域の 2 つの部分で構成されていることがわかります。iframe の文書の designMode 属性は on に設定されているため、iframe のコンテンツを直接編集することができます。この機能はこのエディターが強力たる所以になっています。

デザイン・モードがオンの場合、ブラウザーに組み込みのコマンドを呼び出すことで iframe のコンテンツを変更することができます。例えば、選択されたテキストを太字にするためには、Firefox の execCommand('bold',false, null) を呼び出します。組み込みのコマンドはブラウザーごとに異なります。幸いなことに、エディターによって正規化が行われるため、エディターに用意された統一された API を呼び出せばコンテンツを変更することができます。

Dojo のリッチ・テキスト・エディターを使うためには、ツールバーのボタン・アイコンをクリックし、イベント (例えばクリック・イベント) をトリガーします (図 2)。このイベントはエディターまたはエディターのプラグインによって捕捉されます。iframe の文書の DOM ツリーを変更するためには、組み込みのコマンドを呼び出すか、または DOM ノードを直接変更します。

図 2. 編集リクエストを処理するためのワークフロー
この図は、アイコンをクリックするとクリック・イベントがエディターによって捕捉され、編集領域 (iframe) に導かれることを示しています。

プラグイン

Dojo のリッチ・テキスト・エディターにはプラグイン・アーキテクチャーが採用されているため、エディターにプラグインを実装、登録することでエディターの機能を拡張することができます。プラグインというのは、エディターに機能を追加するもの、あるいはエディターの動作を変更するものです。Dojo にはエディター用のプラグインがいくつか含まれており、また開発者が独自のプラグインを作成することもできます。

一部のプラグイン (Undo (取り消し)、Redo (再実行)、Cut (切り取り)、Copy (コピー) など) はデフォルトで有効になっています。ほとんどのプラグインにはツールバー・ボタンが関連付けられています (FindReplace プラグインなど)。EnterKeyHandling などの一部のプラグインは、エディターの動作に影響を与えますが、ツールバーを変更することはありません。エディターは以下のようにしてプラグインをロードし、初期化します。

  1. 必要な CSS ファイルをインポートします。

    一部のプラグインには CSS ファイルが関連付けられています。プラグインの UI レイアウトは CSS ファイルによって定義されるため、最初に CSS ファイルを文書にインポートする必要があります。

  2. extraPlugins リストの中でプラグインを宣言します。

    プラグインは extraPlugins リストの中で宣言されるので、エディターはこの宣言を基にプラグインのインスタンスを作成することができます (リスト 3)。

    リスト 3. プラグインを宣言する
    <div dojoType="dijit.Editor" id="editor1" extraPlugins="['FindReplace']">
        <p>
            This is the content of the editor.
        </p>
    </div>

    リスト 3 の例では、FindReplace という名前のプラグインを宣言しています。

  3. 宣言されたプラグインをプラグイン・リストに追加し、完全なプラグイン・リストを作成します。

    エディターは宣言されたプラグインをプラグイン・リストに追加し、デフォルトで提供されているプラグインと併せて完全なプラグイン・リストを作成します。この段階で、エディターはプラグインの名前のリストを保持することになります。実際のプラグインは以下のステップに従って作成されます。

  4. エディターのツールバーを作成します。

    エディターは単純に dijit.Toolbar のインスタンスを作成し、このインスタンスをエディターの DOM ノードの子に追加します (リスト 4)。

    リスト 4. エディターのツールバーを作成する
    if(!this.toolbar){
    	// if you haven't been assigned a toolbar, create one
    	this.toolbar = new dijit.Toolbar({
    		dir: this.dir,
    		lang: this.lang
    	});
    	this.header.appendChild(this.toolbar.domNode);
    }
  5. プラグイン・リストに従ってプラグインのインスタンスを作成します。

    すべての魔法はここで行われます。エディターはプラグインの名前リストからプラグインを列挙し、これらの名前の 1 つ 1 つに対して addPlugin を実行します。addPlugin メソッドの中で、プラグインが作成され、それらのプラグインの setEditor メソッドと setToolbar メソッドが呼び出されてプラグインのコンテキストが初期化されます。

    リスト 5. プラグインのインスタンスを作成する
    postCreate: function(){
    	...
    	// Create the plug-in one by one
    	dojo.forEach(this.plugins, this.addPlugin, this);
    	...
    }
    addPlugin: function(/*String||Object*/plugin, /*Integer?*/index){
    	...
    	// Get the plug-in instance that is referenced by paremeter o.plugin.
    	var o={"args":args,"plugin":null,"editor":this};
    	dojo.publish(dijit._scopeName + ".Editor.getPlugin",[o]);
    	// Set the plug-in's context
    	plugin.setEditor(this); 
    	if(dojo.isFunction(plugin.setToolbar)){
    		plugin.setToolbar(this.toolbar);
    	}
    	...
    }

これで、宣言されたプラグインがすべて作成され、初期化されます。


独自のプラグインを作成する

Dojo ツールキットに用意されたプラグインでは要求に応えることができない場合には、独自のプラグインを作成することができます。Dojo のリッチ・テキスト・エディターでは、エディターの機能を拡張することができます。そのためには、dijit._editor._Plugin を継承する新しいプラグイン・クラスを作成し、そのクラスをエディターのプラグイン・リストに追加します。

エディターのプラグインのライフサイクル

dijit._editor._Plugin (エディターに対するプラグインの基底クラス) は通常、ツールバー上の 1 つのボタンと、そのボタンに関連付けられたコードで構成されます。dijit._editor._Plugin によってプラグインのライフサイクルが定義されます。

プラグインのライフサイクルは、そのプラグインが addPlugin 関数によってエディターに追加されたときに開始され、この時点で Dojo のトピックが公開されるため、プラグインが自動的に作成されます。次にプラグインの setEditor 関数と setToolbar 関数がそれぞれ呼び出され、プラグインがエディターにインストールされます。setEditor 関数はデフォルトで以下の処理を行います。

  1. エディターに対するローカル参照を作成します。
  2. コマンド・ボタンを作成します。
  3. エディターの execCommand メソッドをボタンに接続します。
  4. プラグインの updateState メソッドをエディターに接続します。

このように非常に単純です。setToolbar のデフォルトの動作はさらに単純で、先ほど作成したコマンド・ボタンをツールバーに追加するだけです。

この時点で初期化は完了し、プラグインのライフサイクルは「エンドレスの」イベント駆動フェーズに入ります。このフェーズは、誰かがエディター全体を破棄してプラグインのライフサイクルを終了させるまで続きます。プラグインのライフサイクルを終了するにはプラグインの destroy メソッドを呼び出します。

要約すると、ライフサイクルに関係する関数は以下のとおりです。

  • Editor.addPlugin
    • プラグインのコンストラクター
    • plugin.setEditor
    • plugin.setToolbar
  • イベント駆動フェーズ。常に plugin.updateState を呼び出します。
  • Editor.destroy
    • plugin.destroy

さらに詳しく学ぶために、以下のセクションでは FindReplace という検索/置換プラグインを作成します。FindReplace は指定されたキーワードをエディターのテキストの中で検索し、見つかった場合にはそのキーワードを強調表示します。また、見つかったキーワードを新しい文字列で置き換えることもできます。このプラグインを実装するために、コマンド・ボタンがクリックされると表示される特別なツールバーを作成します。

dijit._editor._Plugin を継承する

最初のステップとして、dijit._editor._Plugin 基底クラスを継承して空のコード・フレームワークを作成します (リスト 6)。

リスト 6. 新しいプラグインの基本コード・フレームワーク
dojo.provide("dojox.editor.plugins.FindReplace");

dojo.require("dijit._editor._Plugin");

dojo.declare("dojox.editor.plugins.FindReplace", dijit._editor._Plugin, {
	// When you click the command button, you show a toolbar, 
	// instead of executing some editor commands.
	useDefaultCommand: false,
	
	// You can just use the original method.	
	// setEditor: function(editor){},
	
	setToolbar: function(editorToolbar){
		this.inherited(arguments);
		
		//TODO: Create your additional find/replace toolbar here, 
		// and append it after the editor toolbar.
	},
	
	updateState: function(){
		// You don't need to handle anything when editor state is updated. 
		// So just leave this empty.
	},
	
	destroy: function(){
		this.inherited(arguments);
		
		//TODO: Remember to destroy the toolbar you created.
	}
});

// Register this plug-in so it can construct itself when the editor publishes a topic.
dojo.subscribe(dijit._scopeName + ".Editor.getPlugin", null, function(o){
	if(!o.plugin && o.args.name.toLowerCase() === "findreplace"){
		o.plugin = new dojox.editor.plugins.FindReplace({});
	}
});

ツールバーを拡張する

2 番目のステップとして、エディターのツールバー上にボタンを作成します。これは setEditor メソッドによって自動的に行われます。必要なことは、別のラベルと別のアイコンを使うことだけです。最も単純な方法をリスト 7 に示します。

リスト 7. エディターのツールバーに ToggleButton を作成する
...
dojo.declare("dojox.editor.plugins.FindReplace", dijit._editor._Plugin, {
	...
	// You'd like to use a toggle button to show/hide your own find/replace toolbar.
	buttonClass: dijit.form.ToggleButton,
	
	// As long as you provide a command, the CSS class of the button icon will be 
	// generated automatically. For this one, it will be "dijitEditorFindReplace".
	command: "findReplace",

	// You can also use localization here.
	getLabel: function(){
		return "Find and Replace";
	},
	...
});
...

dijitEditorFindReplace という CSS クラスの背景画像が用意できると、ボタンのサンプルが図 3 のように表示されます。

図 3. サンプル・プラグインのための新しいボタン
新しいプラグインのための検索/置換アイコンとして、ツールバー上に新しいボタンが表示されています。

次のステップとして、検索/置換ツールバーを作成し、コマンド・ボタンにバインドします。必要なものは、検索ストリング用のフィールド、置換ストリング用の別のフィールド、検索ボタン、そして置換ボタンです。もっと強力にしたい場合には、他にも多くの機能を追加して市販品のような外観にすることができます (例えば「Replace All (すべて置換)」ボタンや、「大文字と小文字を区別して検索」および「後方検索」用のチェックボックスなど)。これらのすべてを保持する単純なウィジェットを別途作成します (リスト 8)。

リスト 8. 検索/置換ツールバーを作成する
dojo.declare("dojox.editor.plugins.FindReplacePane", [dijit._Widget, dijit._Templated], {
	// With templates, you don't need to create all the stuff manually.
	templateString: dojo.cache("dojox.editor.plugins", "FindReplacePane.html"),
	
	// There are widgets in template. Tell the parser to parse them.
	widgetsInTemplate: true
});

FindReplacePane.html の中で、dijit.Toolbar や他のフォーム・ウィジェットを直接使用することができます。リスト 9 の例ではラベルとテキスト・フィールド (またはチェックボックス) とを組み合わせて新しいウィジェットを作成し、コードを単純化しています。

リスト 9. 検索/置換ツールバーの内容
<div><div dojotype="dijit.Toolbar" dojoattachpoint="frToolbar">
  <div dojoattachpoint="findField" dojotype="dojox.editor.plugins._FindReplaceTextBox" 
    label="Find:"></div>
  <div dojoattachpoint="replaceField" dojotype="dojox.editor.plugins._FindReplaceTextBox" 
    label="Replace with:"></div>
  <div dojotype="dojox.editor.plugins._ToolbarLineBreak"></div>
  <div dojoattachpoint="findButton" dojotype="dijit.form.Button" 
    label="Find" showLable="true"></div>
  <div dojoattachpoint="replaceButton" dojotype="dijit.form.Button" 
    label="Replace" showLable="true"></div>
  <div dojoattachpoint="replaceAllButton" dojotype="dijit.form.Button" 
    label="Replace All" showLable="true"></div>
  <div dojoattachpoint="matchCase" dojotype="dojox.editor.plugins._FindReplaceCheckBox" 
    label="Match case"></div>
  <div dojoattachpoint="backwards" dojotype="dojox.editor.plugins._FindReplaceCheckBox" 
    label="Backwards"></div>
</div></div>

dojoattachpoint により、FindReplacePane のプロパティーとして、さまざまなウィジェットを容易に利用することができます。dojox.editor.plugins._ToolbarLineBreak は複数行で構成されるツールバー用として非常に便利なウィジェットです。

これでボタンとツールバーが用意できたので、この 2 つを接続します。必要なことは、適切な場所にツールバーを配置し、トグル・ボタンがクリックされたときに実行される内容を定義することだけです。リスト 10 はその一例を示しています。

リスト 10. ボタンとツールバーを接続する
// This initialization work should be done in setToolbar, 
// because you want to be sure that the editor toolbar is ready.
setToolbar: function(editorToolbar){
	// Super class will add the command button to the editor toolbar for you.
	this.inherited(arguments);
	
	// Create your find/replace toolbar, place it after the editor toolbar,
	// hide it, and start it up.
	var frtb = this.frToolbar = new dojox.editor.plugins.FindReplacePane();
	frtb.placeAt(toolbar.domNode, "after");
	dojo.style(frtb.domNode, "display", "none");
	frtb.startup();
	
	// Toggle it when your toggle button is clicked...
	this.connect(this.button, "onChange", "_toggleFindReplace");
	
	...
},
_toggleFindReplace: function(toShow){
	// Remember the original height.
	var height = dojo.marginBox(this.editor.domNode).h
	
	dojo.style(this.frToolbar.domNode, "display", toShow ? "block" : "none");
	
	// Resize the editor to maintain the height.
	this.editor.resize({h: height});
}

図 4 は、現時点でこの例がどのように表示されるかを示しています。

図 4. 検索/置換ツールバー
このツールバーには、「Find: (検索:)」という名前のテキスト・ボックスと、「Replace with: (置換:)」という名前のテキスト・ボックス言葉があり、「Match case (大/小文字を区別)」用のチェックボックスと「Backward (後方検索)」用のチェックボックスがあります。

イベントを処理し、検索を行う関数を実装する

今度は、検索ボタンと置換ボタンを動作させるための実際の作業を行います。setToolbar メソッドの中で検索ボタンと置換ボタンを設定します (リスト 11)。

リスト 11. イベントを処理する
setToolbar: function(editorToolbar){
	...
	var tb = this._frToolbar = ...
	
	// Connect methods to the onClick events of the buttons.
	this.connect(tb.findButton, "onClick", "_find");
	this.connect(tb.replaceButton, "onClick", "_replace");
	this.connect(tb.replaceAllButton, "onClick", "_replaceAll");
	
	// Make the ENTER key work for the "Find" text field.
	this.connect(tb.findField, "onKeyDown", function(evt){
		if(evt.keyCode == dojo.keys.ENTER){
			this._find();
			dojo.stopEvent(evt);
		}
	});
	
	...
}

これは非常に単純でした。次のステップでは _find 関数を実装します。IE 以外のブラウザーの場合には、単純に window.find を使用することができます。IE の場合は少し複雑です。そこで、ブラウザーの違いを解消するためのアダプター関数を作成する必要があります (リスト 12)。

リスト 12. 検索を実装する
// An adapter function to make all browsers look the same.
_findText: function(txt, isCaseSensitive, isBackwards){
	if(!txt){ return false; }
	var ed = this.editor, win = ed.window, doc = ed.document;
	var found = false;
	if(win.find){
		found = win.find(txt, isCaseSensitive, isBackwards, 
			false, false, false, false);
	}else if(doc.selection){
		/* IE */
		// Focus to restore position/selection, 
		// then shift to search from current position.
		ed.focus();
		var txtRg = doc.body.createTextRange();
		var curPos = doc.selection ? doc.selection.createRange() : null;
		if(curPos){
			txtRg.setEndPoint(isBackwards ? "EndToStart" : "StartToEnd", 
				curPos);
		}
		var flags = isCaseSensitive ? 4 : 0;
		if(backwards){
			flags = flags | 1;
		}
		found = txtRg.findText(txt, txtRg.text.length, flags);
		if(found){
			txtRg.select();
		}
	}
	return found;
},
_find: function(){
	var tb = this._frToolbar;
	return this._findText(tb.findField.get("value"), 
		tb.matchCase.get("value"), tb.backwards.get("value"));
}

このページを再度実行し、効果を確認します (図 5)。

図 5. テキストを検索する
ブラウザーのウィンドウで list という単語を検索する

これで、最近のほとんどのブラウザーで機能する便利な検索関数が用意できました。

プログラムでコンテンツを編集する

Dojo のリッチ・テキスト・エディターには execCommand という統一された API が用意されており、この API を使って組み込みのコマンドを呼び出すことができます。利用できるコマンドは、Undo (取り消し)、Redo (再実行)、Cut (切り取り)、Copy (コピー)、Paste (貼り付け)、Bold (太字)、Italic (斜体) など、たくさんあります。組み込みコマンドの完全な一覧については dijit.Editor のドキュメントを参照してください (「参考文献」を参照)。

このサンプル・プラグインの場合には、何らかのテキストをエディターに挿入し、選択されたテキストを置換する必要があります。もちろん、そのために inserthtml という組み込みコマンドがあります。必要なことは、以下の内容を実行することだけです。

  1. 選択されたテキストが「Find (検索)」フィールドのテキストと一致するかどうかをチェックします。
  2. 一致する場合には editor.execCommand("inserthtml", replaceText); を呼び出します。

リスト 13 はそのコードを示しています。

リスト 13. 置換の実装
        _replace: function(){
	var tb = this._frToolbar;
	var txt = tb.findField.get("value");
	if(!txt){ return false; }
	
	var repTxt = tb.replaceField.get("value") || "";
	var isCaseSensitive = tb.caseSensitive.get("value");
	var isBackwards = tb.backwards.get("value");
	var ed = this.editor;
	ed.focus();
	
	//Replace the current selected text if it matches the find field.
	var selected = dojo.withGlobal(ed.window, "getSelectedText", 
		dijit._editor.selection, [null]);
	if(selected && (isCaseSensitive ? 
		(selected === txt) : (selected.toLowerCase() === txt.toLowerCase()))){
		ed.execCommand("inserthtml", repTxt);
		return true;
	}
	return false;
}

ページ全体のウィンドウではなく、エディターの iframe ウィンドウで getSelectedText を呼び出す必要があります。この結果を示したものが図 6 です。

図 6. テキストを置換する
ブラウザー・ウィンドウの中で list という単語を検索し、queue という単語で置き換えます。

これで、エディターのプラグインを作成するための基本的な詳細事項を理解することができました。この続きとして、皆さん自身の演習として、引き続き「Replace All (すべて置換)」を実行する関数を作成してみるとよいかもしれません。


まとめ

この記事では、Dojo のリッチ・テキスト・エディターのプラガブルなアーキテクチャーについて学びました。このプラグイン・メカニズムにより、エディターの機能を拡張することができます。また、Dojo のリッチ・テキスト・エディターを利用することで、強力なテキスト・エディターをアプリケーションにデプロイすることができます。そのエディターは容易に使用することができ、また特定の要件を満たすように容易にカスタマイズすることができます。

参考文献

学ぶために

製品や技術を入手するために

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development
ArticleID=620298
ArticleTitle=Dojo のリッチ・テキスト・エディターをマスターする
publish-date=01112011