2 * FeedBlog SearchScript Ext Version
4 * @copyright 2009 FeedBlog Project (http://sourceforge.jp/projects/feedblog/)
5 * @author Kureha Hisame (http://lunardial.sakura.ne.jp/) & Yui Naruse (http://airemix.com/)
9 // ブログ本体のHTMLファイルの名前を記入してください
10 var blogUrl = "./index.html"
12 // ログのリストが書かれたXMLのファイルパスを記入してください
13 var logXmlUrl = "./xml/loglist.xml";
15 // Ext jsパネルのサイズを記述してください
16 var extPanelWidth = 750;
18 // 結果表示エリアの幅のサイズを指定してください
19 var resultAreaWidth = 784;
21 // 検索フォームの幅のサイズを記述してください
22 var searchPanelWidth = 774;
24 // ログを表示するコンボボックスのサイズを記述してください
25 var extComboWidth = 150;
27 // 日記間のスパン(間隔)をPIXEL単位で記述してください
31 * XMLファイルから読み込んだファイルのバリデートモードを選択します。
32 * 0 = 改行コード部分に<br/>を挿入
33 * 1 = 改行コード部分に<br/>を挿入しない
35 var validateMode = "1";
38 var currentSearchWords;
40 // fetchEntries 用のセマフォ
41 var fetchEntriesSemaphore = new Semaphore();
43 // ログのファイルリストを格納するグローバル変数です
46 // コンボボックスのオブジェクトを格納するグローバル変数です
56 * Extへのイベント登録です。すべてのDOMが利用可能になった時点で実行されます。
58 $(document).ready(function(){
61 // テキストボックスをExt js化し、空欄入力を拒否します
62 var searchTextBox = new Ext.form.TextField({
63 applyTo: "searchWord",
68 logData = logXMLLoader();
72 * 日記の描画を行います。この部分を編集することでデザインを変更可能です。
73 * @param {String} title パネルのタイトル部分に表示する文字列
74 * @param {String} drawitem パネルの本文を格納したDIV要素のid
75 * @param {String} renderto 「タイトル・更新日時・本文」の1日分の日記データを焼き付けるDIV要素のid
76 * @param {String} closed (Ext jsパネルオプション)日記をクローズ状態で生成するか否か
78 function generatePanel(title, drawitem, renderto, closed){
84 hideCollapseTool: false,
93 * 検索フォーム及び結果表示フォームを生成するメソッドです。
95 function generateForm(){
96 var formBuffer = "<table align='center'><tbody><tr><td class='searchform' style='width: " + searchPanelWidth + "px'>" +
97 "<form name='searchForm' onsubmit='searchDiary(); return false'>▼ 検索語句を入力してください (語句を半角で区切るとAND、|で区切るとORで検索します)<br/>" +
98 "<input type='text' id='searchWord' style='width: " +
99 (searchPanelWidth * 0.7) +
100 "px;'> <input type='submit' name='execSearch' value='検索'><br/>" +
101 "<input type='checkbox' id='regexpOptionI' checked='checked'/><label for='regexpOptionI'>大文字、小文字を区別しない</label><br/>" +
102 "<input type='checkbox' id='isAsyncOn'/><label for='isAsyncOn'>非同期通信モードで検索を行う</label><br/>" +
103 "<span style='font-weight: bold;'>[ 注意 ]</span>非同期通信モードをオンにすると速度は上昇しますが、検索の順序が保障されません。<br/><br/>" +
104 "▼ 検索対象ログ選択<br/><div id='logSelecter'/></div><input type='checkbox' id='allSearchCheck' checked='checked'/><label for='allSearchCheck'>すべてのログに対して検索を行う</label>" +
107 "'>トップページへ戻る</a><br/></form></td></tr></tbody></table>"
108 document.getElementById("genForm").innerHTML = formBuffer;
110 var resultAreaBuffer = "<table align='center'><tbody><tr><td class='resultarea' style='width: " + resultAreaWidth + "px;'>" +
111 "<div id='resultWriteArea'><b>検索結果</b></div></td></tr><tr><td class='resultarea' style='width: " +
114 "<table align='center'><tbody><tr><td><div id='writeArea'></div></td></tr>" +
115 "</tbody></table></td></tr><tr><td><div id='pagerAreaBottom'></div></td></tr></tbody></table>"
117 document.getElementById("resultArea").innerHTML = resultAreaBuffer;
122 * @param {Object} obj entry 要素の DOM オブジェクト
125 this.title = $("title:first", obj).text();
126 if (this.title == "")
127 requiredElementError(obj, "title");
128 this.title = "<span>" + validateText(this.title) + "</span>";
129 this.content = $("content:first", obj).text();
130 this.content = "<span>" + validateText(this.content) + "</span>";
131 this.id = $("id:first", obj).text();
133 requiredElementError(obj, "id");
134 this.date = $("updated:first", obj).text();
136 requiredElementError(obj, "updated");
137 this.date = validateData(this.date);
142 * @param {Array} keywords 単語群
143 * @param {String} regexpType 正規表現の検索モードを示す文字列
144 * @return {boolean} bool 全て含んでいれば true、さもなくば false
146 Entry.prototype.hasKeywords = function(keywords, regexpType){
147 // 正規表現が一致するかという判定"のみ"を行います
148 for (var i = 0; i < keywords.length; i++) {
149 // 正規表現チェック用のオブジェクトを用意します(OR条件は一時的に条件を置換)
150 var reg = new RegExp('(?:' + keywords[i] + ')(?![^<>]*>)', regexpType);
152 if (!reg.test(this.content) && !reg.test(this.title))
159 * 呼び出すとDIV:id名:writeArea上のHTMLを削除し、ロードエフェクトを表示します
161 function loadingEffect(){
162 document.getElementById("writeArea").innerHTML = '<div id="drawPanel"><div id="drawItem" class="code" style="text-align: center;"><\/div><\/div>';
163 document.getElementById("drawItem").innerHTML = '<br/><img src="./js/ext/resources/images/default/shared/blue-loading.gif"><br/>長時間画面が切り替わらない場合はページをリロードしてください。<br/><br/>';
166 generatePanel("Now Loading .....", "drawItem", "drawPanel", false);
172 function showError(){
173 document.getElementById("writeArea").innerHTML = '<div id="drawPanel"><div id="drawItem" class="code" style="text-align: center;"><\/div><\/div>';
174 document.getElementById("drawItem").innerHTML = '<br/>日記ファイルのロードに失敗しました!<br/><br/>';
177 generatePanel("Error!", "drawItem", "drawPanel", false);
179 Ext.Msg.alert("Error!", "日記ファイルが読み込めません!");
185 function notFoundError(){
186 document.getElementById("writeArea").innerHTML = '<div id="drawPanel"><div id="drawItem" class="code" style="text-align: center;"><\/div><\/div>';
187 document.getElementById("drawItem").innerHTML = '<br/>検索条件に一致する日記は見つかりませんでした。<br/><br/>';
190 generatePanel("Not Found!", "drawItem", "drawPanel", false);
196 function requiredElementError(parent, name){
197 Ext.Msg.alert("Error!", parent.ownerDocument.URL + ": 必須な要素 " +
205 * 日付のHTML表示用バリデーション処理を行います
206 * @param {String} data RFC3339形式のdate-time文字列
208 function validateData(data){
209 data = data.replace(/T/g, " ");
212 data = data.substring(0, 19);
218 * 日記本文のバリデーション処理を行います
219 * @param {String} contents 日記の本文が格納されている文字列
221 function validateText(contents){
223 if (validateMode == 0) {
224 contents = contents.replace(/[\n\r]|\r\n/g, "<br />");
232 * @param {String} str エスケープを行いたい文字列
234 function xmlAttrContentEscape(str){
235 // return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
236 return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/^[ ]+/mg, " ").replace(/^[\t]+/mg, "");
241 * @param {String} contents 日記の本文が格納されている文字列
242 * @param {String} id 日記の初公開日を示す日付文字列
244 function contentsWithid(contents, id){
246 var hashTag = '<br/><div style="text-align: right;"><a href="' +
247 xmlAttrContentEscape(blogUrl) +
249 xmlAttrContentEscape(id) +
250 '" target="_blank">- この日の記事にリンクする -<\/a><\/span>';
251 return contents + hashTag;
256 * @param {String} word 強調したい語句
258 function emphasizeWord(word){
259 return '<span style="background-color: red;">' + word + '</span>';
264 * @param {String} a 比較対象(1)
265 * @param {String} b 比較対象(2)
267 function compareLengthDecrease(a, b){
270 return a > b ? -1 : a < b ? 1 : 0;
276 function Semaphore(){
286 Semaphore.prototype.init = function(){
287 while (this.xhrs.length > 0) {
288 this.xhrs.shift().abort();
290 this.id = Math.random();
296 * ログファイル選択用のコンボボックスをid名:logSelecterに生成します
298 function logXMLLoader(){
300 var PathRecord = new Ext.data.Record.create([{
309 var logXMLData = new Ext.data.Store();
314 success: function(xmlData){
315 var separateTag = xmlData.getElementsByTagName("file");
316 logData = new Array(separateTag.length);
318 // 読み込んだ要素をStoreに格納して表示
319 for (var i = 0; i < separateTag.length; i++) {
321 logData[i] = separateTag[i].getElementsByTagName("path")[0].firstChild.nodeValue;
323 var record = new PathRecord({
324 path: separateTag[i].getElementsByTagName("path")[0].firstChild.nodeValue,
325 display: separateTag[i].getElementsByTagName("display")[0].firstChild.nodeValue
327 logXMLData.add(record);
331 document.getElementById("logSelecter").innerHTML = "<input type='text' id='logbox' style='width: " + extComboWidth + "px'/>";
334 comboBox = new Ext.form.ComboBox({
337 displayField: "display",
340 triggerAction: "all",
341 emptyText: "ログを選択してください..."
350 function getSearchWords(){
351 var searchWord = document.getElementById("searchWord").value;
352 if (searchWord == "")
354 var searchWords = [];
358 searchWord = xmlAttrContentEscape(searchWord);
360 searchWord = searchWord.replace(/([$()*+.?\[\\\]^{}])/g, '\\$1');
362 searchWords = searchWord.replace(/^\s+|\+$/g, '').split(/\s+/);
363 // 正規表現の選択を長い順に並び替えます(AND条件)
364 searchWords.sort(compareLengthDecrease);
366 return searchWords.length == 0 ? null : searchWords;
370 * 文章内を特定の単語で検索し、一致した部分を強調表示タグで置き換えます
371 * @param {String} searchWord 探索する単語
372 * @param {String} plainText 探索を行う文章
373 * @param {String} regexpType 正規表現の検索モードを示す文字列
375 function complexEmphasize(searchWord, plainText, regexpType){
377 searchWord = searchWord.split('|').sort(compareLengthDecrease).join('|');
378 // タグの内側でないことを確認する正規表現を追加
379 var pattern = new RegExp('(?:' + searchWord + ')(?![^<>]*>)', regexpType);
382 var currentIndex = -1; // 現在マッチしている部分文字列の開始位置
383 var currentLastIndex = -1; // 現在マッチしている部分文字列の、現在の末尾
384 var m; // 正規表現マッチの結果配列
385 while (m = pattern.exec(plainText)) {
386 if (m.index > currentLastIndex) {
387 // 新しい部分文字列へのマッチが始まったので、そこまでの文字列をバッファに書き出す
388 if (currentIndex < currentLastIndex)
389 result.push(emphasizeWord(plainText.substring(currentIndex, currentLastIndex)));
390 result.push(plainText.substring(currentLastIndex, m.index));
392 currentIndex = m.index;
395 currentLastIndex = pattern.lastIndex;
396 // 次の正規表現マッチは今マッチした文字の次の文字から
397 pattern.lastIndex = m.index + 1;
400 if (currentIndex < currentLastIndex)
401 result.push(emphasizeWord(plainText.substring(currentIndex, currentLastIndex)));
402 result.push(plainText.substring(currentLastIndex));
405 return result.join('');
409 * 検索結果を分割して表示します(2回目以降呼び出し)
410 * @param {int} showLength 一回の画面に表示する記事数
411 * @param {int} startIndex 表示を開始する日記のインデックス
413 function showEntriesRange(showLength, startIndex){
415 var entries = loadedEntries;
417 // 表示インデックスが範囲外の場合はエラーパネルを表示して終了
418 if (startIndex < 0 || entries.length <= startIndex) {
423 var stringBuffer = [];
426 var loopLimit = (showLength + startIndex > entries.length) ? entries.length : showLength + startIndex;
427 var indexShowEntries = loopLimit + 1;
429 for (var i = startIndex; i < loopLimit; i++) {
430 stringBuffer.push('<div id="drawPanel');
431 stringBuffer.push(i);
432 stringBuffer.push('"><div id="drawItem');
433 stringBuffer.push(i);
434 stringBuffer.push('" class="code"><\/div><\/div><div style="line-height: ' + entrySpan + 'px;"><br/></div>');
436 document.getElementById("writeArea").innerHTML = stringBuffer.join('');
438 stringBuffer.length = 0;
439 for (i = startIndex; i < loopLimit; i++) {
440 var entry = entries[i];
441 document.getElementById("drawItem" + i).innerHTML = contentsWithid(entry.content, entry.id);
442 generatePanel(entry.title + " / " + entry.date, "drawItem" + i, "drawPanel" + i, false);
447 menuBuffer.push("<table width='100%' class='pager'><tbody><tr>");
449 if (startIndex - showLength >= 0) {
450 menuBuffer.push("\<td align='left'><a href='' onclick='showEntriesRange(" +
453 (startIndex - showLength) +
454 "); return false;'>\<\<\< 前の" +
459 menuBuffer.push("\<td align='left'>\<\<\< 前の" +
465 menuBuffer.push("<td align='center'>[ ");
466 var menuNumbers = Math.ceil(entries.length / showLength);
467 for (i = 0; i < menuNumbers; i++) {
468 if (startIndex / showLength == i) {
469 menuBuffer.push(i + " ");
472 menuBuffer.push("<a href='' onclick='showEntriesRange(" +
476 "); return false;'>");
478 menuBuffer.push("</a> ");
481 menuBuffer.push("]</td>");
484 if (entries.length > startIndex + showLength) {
485 menuBuffer.push("\<td align='right'><a href='' onclick='showEntriesRange(" +
488 (startIndex + showLength) +
489 "); return false;'>次の" +
491 "件を表示 \>\>\></a\></td>");
494 menuBuffer.push("\<td align='right'>次の" +
496 "件を表示 \>\>\></a\></td>");
498 menuBuffer.push("</tr></tbody></table>");
501 document.getElementById("resultWriteArea").innerHTML = "\<b\>検索結果\</b\>\<br/\>" +
510 document.getElementById("pagerAreaBottom").innerHTML = menuBuffer.join("");
514 * 検索時のjQuery.ajaxのcallback関数
516 function fetchEntries(xmlData){
517 // 大文字小文字を区別するかを取得します
518 var regexpOptionI = document.searchForm.regexpOptionI.checked;
519 var regexpType = regexpOptionI ? "ig" : "g";
522 var entries = xmlData.getElementsByTagName("entry");
525 for (var j = 0; j < entries.length; j++) {
526 var entry = new Entry(entries[j]);
528 // 正規表現が一致した場合は、強調表現処理を行います
529 if (entry.hasKeywords(currentSearchWords, regexpType)) {
531 entry.title = complexEmphasize(currentSearchWords.join("|"), entry.title, regexpType);
532 entry.content = complexEmphasize(currentSearchWords.join("|"), entry.content, regexpType);
534 fetchEntriesSemaphore.buf.push(entry);
538 // セマフォのカウンタを減少させます (Ajaxとの同期のため)
539 fetchEntriesSemaphore.count--;
542 if (fetchEntriesSemaphore.count == 0) {
543 var entries = fetchEntriesSemaphore.buf;
545 // 一軒も検索にヒットしなかった場合は専用のパネルを表示して終了
546 if (entries.length == 0) {
552 entries = entries.sort(function(a, b){
555 return a > b ? -1 : a < b ? 1 : 0
558 loadedEntries = entries;
561 showEntriesRange(showLength, 0);
566 * 「探索」ボタンを押されたときに呼び出されるメソッドです
568 function searchDiary(){
570 document.getElementById("writeArea").innerHTML = "";
573 currentSearchWords = getSearchWords();
574 if (!currentSearchWords) {
575 Ext.Msg.alert("ERROR", "検索対象の単語が入力されていません");
577 document.getElementById("resultWriteArea").innerHTML = "\<b\>検索結果\</b\>";
585 var allCheckedFlag = document.searchForm.allSearchCheck.checked;
588 fetchEntriesSemaphore.init();
589 // 日記が全検索モードか否かをチェックします
590 var isAsyncOn = null;
592 if (allCheckedFlag == true) {
593 // 全文検索時、通信のモードが非同期か否か
594 isAsyncOn = document.searchForm.isAsyncOn.checked;
599 // 単独日記探索なので、選んだログのURL
600 urls = [comboBox.getValue()];
602 fetchEntriesSemaphore.urls = urls;
603 fetchEntriesSemaphore.count = urls.length;
604 for (i = 0; i < urls.length; i++) {
605 var xhr = new jQuery.ajax({
609 success: fetchEntries,
612 fetchEntriesSemaphore.xhrs.push(xhr);