Google Books APIでISBNコードから書籍検索するスクリプト

昨日、書籍検索APIについて少し調べたので記事を書きました。

書籍検索API

どうやら、Google Books APIに関してはAPIKeyが無くても情報を取得できるようです。必須ではないみたいですね。

 

ということで、ISBNコードを入力して簡単な書籍情報を取得できるスクリプトを書いてみました。

一応、送信先サーバへの負荷を軽減するために、5秒間は再検索ができないようにしています。効果あるかわからんですけれども。

タイトル: 

著者: 

出版社: 

発売日: 

 

なお、書籍によっては出版社が取得できないデータもあるようです。

それと、空文字で検索すると「出版流通合理化構想の検証」が固定で取得できるようです。芸が細かい。

JavaScriptで特殊文字を削除

JavaScriptで任意の文字列から特定の文字を削除する場合、普通はreplaceを使います。

var txtBefore = "abcde";
var txtAfter = txtBefore.replace(/c/g,"");

txtBeforeから'c'という文字を""(空白)に置換することで、削除を実現します。

/の後ろにあるgを付与することで、文字列から全ての指定文字を削除するモードになります。

 

今回は、これだと削除できない(と思う)特殊文字の削除についてです。

たとえば「ゼロ幅スペース」。

  は半角スペースではないというお話 (フェンリル | デベロッパーズブログ)

スペース無し あいうえお
ゼロ幅スペース あ​い​う​え​お
半角スペース あ い う え お

ゼロ幅スペースは、見た目上はスペース無しと同じですが、間には確かにスペース文字が存在します。

上記の3つの「あいうえお」をコピーして適当なテキストエディタにペーストしてもらえるとわかると思います。

このゼロ幅スペースは、ASCIIコードのUnicodeの「8203」で入力することができます。htmlに出力するには「​」(10進数)または「​」(16進数)と書きます。

 

また、たとえば「垂直タブ」。

xmlに垂直タブが含まれていて解析エラーになる件 - LET__IT__RIDE

垂直タブ  

これもブラウザによっては見た目で判別できず、しかしコピー&ペーストで間に特殊文字が出力されます。IEだと通常のスペースに変換されているかもしれませんが。

この垂直タブは、ASCIIコードの「11」で、html出力は「」(10進数)または「」(16進数)となります。

 

これらの特殊文字を削除するために、ASCIIコードを指定して削除するスクリプトを組んでみました。

var spStr = [
11, // 垂直タブ
8203 // ゼロ幅スペース
];

var txtBefore = document.getElementById("テキストエリアのID属性").value;
var txtAfter = "";
for (var i=0; i<txtBefore.length; i++) {
  var chr = txtBefore.charCodeAt(i);
  if (spStr.indexOf(chr) == -1) {
    txtAfter += String.fromCharCode(chr);
  }
}
document.getElementById("テキストエリアのID属性").value = txtAfter;

String.charCodeAt(n)でn番目の文字のASCIIコードを取得し、spStrで定義した特殊文字に該当するかをチェックします。

該当しなかった場合、String.fromCharCode(ASCIIcode)でASCIIコードから元の文字を指定し出力文字に連結します。

これで、spStrで定義した特殊文字だけがふるい落とされるようになります。

追記(2017/05/08)

2年近く前の記事への追記となってしまい恐縮ですが、単なるreplaceだけでも特殊文字削除ができたので補足しておきます。

var txt = document.getElementById("テキストエリアのID属性").value;
txt = txt.replace(/\u200B/g, "");  
document.getElementById("テキストエリアのID属性").value = txt;

まず、ゼロ幅スペースの「8203」はASCIIコードではなくUnicodeでした。ASCIIコードは制御文字を含めても128文字しかありませんので、8203とかあるわけないですね。

垂直タブの「11」は制御文字としてASCIIコードにも存在しますが、同時にUnicodeでも10進数で11です。

で、JavaScriptにはUnicodeエスケープという概念があるようです。

Lexical grammar - JavaScript | MDN

\uXXXX」という形で16進数のUnicodeを書くと、JavaScript内で特殊文字を扱うことができます。

前述のように、10進数の「8203」は16進数だと「200B」です。

なので、「\u200B」をreplaceに渡してあげればOKでした。

CSSでレート評価(★)を表現してみる

よくアプリのレート評価などで星マーク(★)を使った表現が使われます。

無評価の0から5つ星の6段階であれば、星マークをただ並べれば良いのですが、評価人数の母体が増えると、平均して3.7つ星なんていう状況も出てきます。

 

そんな表現をCSSだけで出来ないかなー、と思って書いてみました。

レート評価っぽいこと

解説はすっ飛ばして、作ってみたのがこちら。

 

html5で追加されているinputタグの"range"でゲージを操作すると、レート評価が変わります。

変更させているのはwidth属性のみです。ベースとなるCSSは以下を定義。

.rate:after {
  content: '★★★★★';
  display: inline-block;
  overflow: hidden;
  color: orange;
  width: 40px;
}

「擬似要素」と呼ばれるCSSの仕組みで、class属性に"rate"を指定した要素の後ろに★5つを付与しています。

inline-blockでインラインブロックとしwidthを指定できるようにして、はみ出た部分はoverflowで隠します。

 

基本骨子はあっさりできたのですが、問題点が2つありました。

CSSのafterが動的に変更できない

通常、CSSを指定するには<style>タグをヘッダに付けるか、要素に対しstyle属性を書きます。

ところが、後者だとafterって書けないんじゃないか。よしんば、書けたとしてもどうやって変更すればよいのか。

調べてみたら以下の記事を見つけました。

jQuery - :afterなどのCSS擬似要素のStyleを動的に変更する - Qiita

どうやら、beforeやafterなどの擬似要素はDOMではないためセレクタでアクセスできないようです。

今回は、記事内の方法を参考にwidthをむりやり動的に変更してみました。

input:rangeのトリガーがよくわからない

初めてinputタグの"range"を使ってみたのですが、動かしたときのトリガーがよくわかりません。

こちらも調べてみましたが、ブラウザによって発火のイベントもタイミングも異なるそうです。

input:range のスライダー変更検知イベントは oninput? - 犬ターネット

上記事の通りonchangeとoninputの両方に変更イベントを書いてみました。一応FirefoxGoogle Chromeの最新版では動確済み。

 

 

どちらの問題も、今回の記事向けに動的変更要素を足したために発生した問題で、本題とは無関係です。

単純にレートを表現するのであれば、冒頭のCSSspanタグにでも埋めてあげて、適当にwidthを指定してあげれば良いです。

スライダー横の数字は動的に指定したwidthの数値です。参考資料にしてみてください。なお、px指定なのでフォントサイズで変わると思います。(←意味ない)

Firefoxで「右クリック禁止」を回避?

一昔前のWebサイトなどで「右クリック禁止」がありました。

方法自体は単純で、oncontextmenuというイベントでreturn falseなどを返してあげればよいです。

以下に実装してみます。四角い枠の中では右クリックが無効化され、コンテキストメニューが表示されません。

 

ところが、何故かFirefoxの場合、Shiftキーを押しながら右クリックすると、禁止しているにも関わらずコンテキストメニューが表示されるようです。

IEChromeではそんなことありませんでした。

 

ちょっと不思議なFirefoxの仕様なのでしょうか。

はてなブログでたまにJavaScriptが効かなくなる?

アクセス解析を確認しつつ、たまに自分のブログをチェックしています。

すると、いくつかの記事でJavaScriptが動かなくなっているものを見つけました。

 

その原因もさまざまで、scriptタグに変な属性が付与されていたり、onblur属性が消去されていたり、analytics.jsの変更が妙な悪さをしていたりといった感じです。

特に、onblur属性は以前だと普通に使えていた気がしますが、現在はHTML編集で属性を付与したのち、見たまま編集に戻ると消えてしまいます。Twitterウィジェットなどを使うときもそうですが、タグに使えない属性が意外と多いようです。

 

一応、一通り見直して修正しておきました。onblurは属性ではなくjQueryblurで後付けすると使えるようです。

jQueryプラグイン「jqGrid」を勉強しました その2

前回はほとんど導入のみだった気もするので、今回は編集メインで組んでみました。

行の追加、行の編集、行の削除、それと簡易絞込み検索を実装しています。

参考:jQueryプラグイン「jqGrid」を勉強しました

 

jqGridデモ

jqGrid Demo2

※TOK2で404となっていました。すみません…

主な機能

  • 行選択での各種セル編集
  • edittype:'custom'による独自定義
  • ナビゲート機能による行追加、行削除
  • ナビゲート機能による簡易絞込み検索

なお、簡易絞込みで部分一致と思われる「次に含まれる」は完全一致検索をしている気がしてなりません。

解説

jqGridの基本コード

  $('#tableのid').jqGrid({// [jqGridのプロパティ] http://www.jqueryhelp.net/basicgrids/properties/
    datatype: 'local' // データタイプ
    , colNames: ['テキスト', 'テキストエリア', 'チェック', 'セレクト', 'カスタム(日付)'] // カラム名
    , colModel: [
  {name:'text', index:'text', sorttype:'text', editable:true, edittype:'text'}
 ,{name:'textarea', index:'textarea', sorttype:'textarea', editable:true, edittype:'textarea', editoptions:{rows:'2', cols:'10'}}
 ,{name:'check', index:'check', sorttype:'checkbox', editable:true, edittype:'checkbox', editoptions:{value:'1:0'}}
  ,{name:'select', index:'select', sorttype:'select', editable:true, edittype:'select', editoptions:{value:'1:1;2:2;3:3'}}
 ,{name:'date', index:'date', sorttype:'date', editable:true, edittype:'custom', editoptions:{custom_element:elmDate, custom_value:valDate}}
] // カラム設定
    , data: [
               {id:'1', text:'文字列1', textarea:'複数文字列1', check:'1', select:'1'}
               ,{id:'2', text:'文字列2', textarea:'複数文字列2', check:'0', select:'2'}
               ,{id:'3', text:'文字列3', textarea:'複数文字列3', check:'3', select:'3'}
    ] // 読み込むデータ
    , rowNum: 10 // 1ページの表示件数
    , rowList: [5, 10, 15] // rowNumの選択リスト
    , height: 150 // グリッドテーブルの高さ
    , width: 640 // グリッドテーブルの幅
    , pager: 'ページャdivタグのid' // ページャ
    , shrinkToFit: true // カラム幅の自動調整
    , viewrecords: true // フッタの右下に件数情報を表示
    , multiselect: true // グリッドの左端選択チェック
    //, cellEdit: true // セル編集の許可 ※セル編集を許可するとテキストエリアで改行よりも値確定が優先されるっぽい
    , cellsubmit: 'clientArray' // セル編集時にsubmitする先。ローカルの場合は'clientArray'
    , editurl: 'clientArray'
    , loadonce:true
    , onSelectRow: onSelectRowFunc
  });

基本的には前回とほとんど似たような定義です。

グリッドテーブルをローカルで使用する場合、cellSubmitediturl'clientArray'を定義してあげる必要があります。

グリッドテーブルの各行を選択した際に、cellEdittrueだと自動的に編集モードになります。

ただし、コメントに記載したとおり、編集モードでテキストエリアに改行を挿入しようとすると値確定されてしまいます。

そのため、今回は行選択時にonSelectRow内で手動で編集モードにしています。

行選択での各種セル編集

var lastSel;
function onSelectRowFunc(id) {
  if(id && id!==lastSel){ // 別の行選択時
     //$(this).restoreRow(lastSel); // 前の編集行をキャンセル
     $(this).saveRow(lastSel); // 前の編集行を確定
     lastSel = id;
  } 
  $(this).editRow(id, true);
}

前述したように、onSelectRowonSelectRowFuncという関数を呼んでいます。

関数の中身は以下を参考にしています。

参考:wiki:inline_editing - jqGrid Wiki

edittype:'custom'による独自定義

/**
 * edittype:customの要素を生成する関数
 * @param value 現在の値
 * @param options 要素のオプション
 */
function elmDate(value, options) {
  var elm = $('<input>', {type:'text', value:value}).attr('readonly',true).css('width', '80').datepicker({dateFormat:"yy/mm/dd"});
  return elm;
}
/**
 * edittype:customの値確定時に送信する値
 * @param elem 編集中の要素
 * @param operation "get"or"set"
 * @param value ? operationがsetのときに何かくる
 */
function valDate(elem, operation, value) {
  return $(elem).val();
}

colModelで最後の列はカスタムの編集モードを定義しています。

定義の際には、edittype:'custom', editoptions:{custom_element:生成関数, custom_value:返却関数}を設定します。

editoptionsの2つの関数では、custom_elementで編集モードの際に要素を生成し、custom_valueで値確定時に参照モードへ渡す値を返却します。

今回はjQuery UIのDatepickerを使って、日付入力を行うテキストボックスを用意しました。

ナビゲート機能

  $("#tableのid").jqGrid("navGrid", "#pagerのid", {
      edit:false
      , add:true
      , del:true
      , refresh:true
      , search:true
      , addfunc:addFunc
      , delfunc:delFunc
    }
    , {} // editパラメータ
    , {reloadAfterSubmit:false} // addパラメータ
    , {reloadAfterSubmit:false} // delパラメータ
    , {multipleSearch:false, multipleGroup:false, showQuery: false} // searchパラメータ
    , {} // viewパラメータ
  );

function addFunc() {
  $("#tableのid").addRowData(undefined, {}, "last");
}
function delFunc() {
  var selectRows = $("#tableのid").getGridParam('selarrrow');
  var delRowNum = selectRows.length;
  for (var i=delRowNum-1; i>=0; i--) {
    $("#taboeのid").delRowData(selectRows[i]);
  }
  $("#tableのid").resetSelection()
}

グリッドテーブル下部にあるページャにナビゲート機能を付与しています。

今回は追加、削除、簡易絞込み検索、更新のボタンを有効にしています。

各引数内で使用できるオプションは以下に掲載されています。

参考:wiki:navigator - jqGrid Wiki

 

本来は、追加や削除ボタンを押したときには標準のダイアログが表示されますが、今回はaddfuncdelfuncで処理を上書きしています。

というのも、ローカルで全てを完結するために'clientArray'を使用しているのに、標準のナビゲート機能を使った追加・削除では上手くローカルで機能しなかったためです。意味わかんないです。

 

上書きした追加・削除処理は以下のサイトを参考にさせてもらいました。

参考:HARD DAY'S NIGHT 別館 - jqGrid グリッド行の追加・削除サンプル

ただ、上記のサンプルでは行削除後にソートを行うと、削除したデータが復活します。

正常に削除しつつ、ソートが効くようにresetSelectionを最後に呼ぶようにしています。

参考:jqGridでdelRowData後にソートが効かなくなる

 

 

と、こんな感じで、いろいろできるようになっています。

jqGridでdelRowData後にソートが効かなくなる

たまにjqGridに触れる機会があります。

関連:jQueryプラグイン「jqGrid」を勉強しました

 

前回のサンプルでは単純なグリッドテーブルで行の追加や削除は実装していませんでした。

jqGridのデータはサーバから取得することが多いですが、ローカルにデータを持って追加や削除を行うことも可能です。

datatype: 'local'
loadonce: true

 

行の追加、編集、削除はそれぞれ以下のメソッドを使います。

行追加
jQuery("#grid_id").addRowData( rowid, data, position, srcrowid );
行編集
jQuery("#grid_id").editRow(rowid, keys, oneditfunc, successfunc, url, extraparam, aftersavefunc, errorfunc, afterrestorefunc);

urlには'clientArray'を指定することでローカル編集ができます。

行削除
jQuery("#grid_id").delRowData( rowid );

 

注意点として、rowidにユニークな値を入れておかないと、ソート時にリロードされてローカルで編集したデータがリセットされます。

これで、ある程度さっくりとローカル編集可能なグリッドテーブルが実現できました。サンプルは近いうちに公開します。

 

ただ、このときにdelRowDataを行うとソートが効かなくなってしまいます。

 

 

調べてみると、ソートを行う際にjqGridの以下のメソッドが呼ばれているのが分かりました。

(省略)
  sortData = function (index, idxcol,reload,sor, obj){
    if(!ts.p.colModel[idxcol].sortable) { return; }
    if(ts.p.savedRow.length > 0) {return;}
    if(!reload) {
      if( ts.p.lastsort === idxcol ) {
        if( ts.p.sortorder === 'asc') {
          ts.p.sortorder = 'desc';
        } else if(ts.p.sortorder === 'desc') { ts.p.sortorder = 'asc';}
      } else { ts.p.sortorder = ts.p.colModel[idxcol].firstsortorder || 'asc'; }
      ts.p.page = 1;
    }
(省略)

delRowDataを行うと、削除したデータがsavedRowに格納されており、lengthが0より大きくなってreturnされています。

これは、どうやら復旧用のデータのようで、保存時または復旧時に削除されるものだそうです。

参考:wiki:inline_editing - jqGrid Wiki

 

一応、getGridParamでアクセス可能な領域ではあるのですが、読み取り専用でsetGridParamでは編集できません。

参考:wiki:options - jqGrid Wiki

 

 

仕方なしにjqGridのソースを追っていると、いくつかのメソッドで削除している記述がありました。

その1つがresetSelectionというメソッドです。

本来は、multiselectのプロパティで表示されるチェックボックスの行選択を解除するメソッドなようです。

delRowDataで行を削除後、resetSelectionで行選択を解除しつつsavedRowに格納された復旧データを空にしてあげることで、ソートが効くようになりました。

jQuery("#grid_id").delRowData( rowid );
jQuery("#grid_id").resetSelection();

いずれ、サンプル作ります。

サンプル作りました。

jQueryプラグイン「jqGrid」を勉強しました その2