こんにちは、
アプリケーションの設計
第9回で取り上げたTwitter検索を行うJavaScriptをベースに、
機能は以下のとおりです。
- 任意のキーワードで検索
- 60秒おきに自動で新しい検索結果を取得
- @ユーザー名はTwitterにリンク
- URLと思われるところはリンクに
- ハッシュタグをクリックしたときはそのハッシュタグで検索
- 短縮されたURLを展開
なお、
検索の骨組み
まずは任意のキーワードで検索する部分を見ていきましょう。まずはHTMLです。
<form id="searchform">
<input type="search" id="searchvalue" value="JavaScript">
<select id="searchlang">
<option value="all">all</option>
<option value="ja">ja</option>
</select>
<input type="submit" value="検索">
</form>
続いて、
var searchform = document.getElementById('searchform');
var searchvalue= document.getElementById('searchvalue');
var searchlang = document.getElementById('searchlang');
var timer = document.getElementById('timer');
var lang = '';
var TwitterAPI = 'http://search.twitter.com/search.json';
function getJSONP(query){
var script = document.createElement('script');
script.src = TwitterAPI + query;
document.body.appendChild(script);
}
searchform.onsubmit = function(){
lang = searchlang.value;
getJSONP('?callback=TwitterCallback&lang='+lang+'&q=' +
encodeURIComponent(searchvalue.value));
return false; // submitをキャンセル(ページ遷移させない)
};
フォームのsubmitを使用している点がポイントで、
なお、
var addEvent = (document.addEventListener) ?
function(node,type,handler){
node.addEventListener(type,handler,false);
}
: function(node,type,handler){
node.attachEvent('on' + type, function(evt){
handler.call(node, evt);
});
};
addEvent(searchform,'submit',function(evt){
lang = searchlang.value;
getJSONP('?callback=TwitterCallback&lang='+lang+'&q=' +
encodeURIComponent(searchvalue.value));
// submitをキャンセル(ページ遷移させない)
if (evt.preventDefault) {
evt.preventDefault();
} else {
evt.returnValue = false;
}
});
今回のようなシンプルなアプリケーションではaddEventListener、
タイムラインの実装
続いて、
<div id="search-result" class="twsrrlt"></div>
<div id="timer"></div>
<ul id="search-tmpl" style="display:none;">
<li>
<a class="usr" target="_blank">
<img width="48" height="48"><br>
<span></span>
</a>
<p class="entry"></p>
<div class="time">
<a class="source" target="_blank"></a>
<a class="username" target="_blank"></a>
<a class="timelink" target="_blank"></a>
</div>
</li>
</ul>
id="search-result" が検索結果を表示する入れ物で、
さてコールバックの処理ですが、
var tmpl = document.getElementById('search-tmpl').
getElementsByTagName('li')[0];
var timeline = document.getElementById('search-result');
var tree;
var prev_result;
function TwitterCallback(data){
if (prev_result){
// 前回の結果に継ぎ足す場合
write_timeline(data);
} else {
clear();
write_timeline(data);
start_timer();
}
prev_result = data;
}
function clear(){
if(tree){
timeline.removeChild(tree);
tree = null;
}
}
prev_
自動更新の実装
先にstart_
var TIME = 60, timerID;
function start_timer(){
var time = TIME;
timerID = setInterval(function(){
time--;
timer.innerHTML = time;
if(time === 0){
time = TIME;
if (prev_result){
getJSONP(prev_result.refresh_url +
'&callback=TwitterCallback&lang='+lang);
}
}
}, 1000);
}
setIntervalで1秒おきにカウントダウンを行い、
なお、
searchform.onsubmit=function(){
prev_result = null;
clearInterval(timerID);
lang = searchlang.value;
getJSONP('?callback=TwitterCallback&lang='+lang+'&q=' +
encodeURIComponent(searchvalue.value));
// submitをキャンセル(ページ遷移させない)
return false;
};
HTMLの生成
では最後にwrite_
function write_timeline(data){
var keyword = searchvalue.value;
var results = data.results;
if (!tree) {
tree = document.createElement('ul');
tree.className = 'twl';
}
var items = tree.childNodes.length;
results.reverse();
if(prev_result){// 前回の結果からnewクラスを取り除く
var len = prev_result.results.length;
for (var i = 0;i < len; i++){
var _li = tree.childNodes[i];
_li.className = _li.className.
replace(/(\s)+new(\s*|$)/,'');
}
}
for (i = 0, len = results.length;i < len; i++){
var usr = results[i];
var user = usr.from_user;
/* 要素を作る */
var li = tmpl.cloneNode(true);
var link = li.getElementsByTagName('a')[0];
var icon = link.getElementsByTagName('img')[0];
var name = link.getElementsByTagName('span')[0];
var entry = li.getElementsByTagName('p')[0];
var time = li.getElementsByTagName('div')[0];
var source = time.getElementsByTagName('a')[0];
var username = time.getElementsByTagName('a')[1];
var timelink = time.getElementsByTagName('a')[2];
/* CSS用にclassを設定 */
li.className = (((i+1+items)%2) ?
'odd' : 'even') + ' new';
/* リンクや画像などの属性を設定 */
username.href = link.href =
'http://twitter.com/' + user;
var src = usr.profile_image_url;
if (src.indexOf('http') === 0) {
icon.src = src;
}
icon.width = 48;
icon.height = 48;
timelink.href = 'http://twitter.com/' +
user +'/status/' + usr.id;
var d = new Date(usr.created_at);
var date = d.getFullYear() + '/' + (d.getMonth()+1) +
'/' + d.getDate() + ' ' + d.getHours() + ':' +
('0'+d.getMinutes()).slice(-2);
/* エスケープされた文字を戻す */
var node = document.createTextNode(usr.text.
replace(/&(lt|gt|quot|amp);/g,function(_$,_1){
return {lt:'<', gt:'>', quot:'"', amp:'&'}[_1];
})
);
/* テキストノードの挿入 */
entry.appendChild(node);
linkfy(entry, '@(\\w+)', '[^\\w@]|$',
'http://twitter.com/');
linkfy(entry, '#(\\w+)', '[^\\w#]|$',
'http://search.twitter.com/search?q=%23');
linkfy(entry, '(https?://.*)',
'[ \\)\\]\'\"\n]|$', '');
expandUrl(entry);
highlight(entry, keyword);
if (usr.source){
var match = usr.source.match(/"(http.*?)"/);
if(match){
source.href = match[1];
source.appendChild(document.createTextNode(
'from '+usr.source.match(/>(.*?)</)[1]
));
}
}
var at_usr = '@' + user;
username.appendChild(document.createTextNode(at_usr));
timelink.appendChild(document.createTextNode(date));
name.appendChild(document.createTextNode(user));
/* 要素の組み立て */
tree.insertBefore(li, tree.firstChild);
}
/* 画面に反映 */
if (!tree.parentNode || !tree.parentNode.parentNode){
timeline.appendChild(tree);
}
}
順番に見ていきましょう。
if (!tree) {
tree = document.createElement('ul');
tree.className = 'twl';
}
まず、
続いて、
if(prev_result){// 前回の結果からnewクラスを取り除く
var len = prev_result.results.length;
for (var i = 0;i < len; i++){
var _li = tree.childNodes[i];
_li.className = _li.className.
replace(/(\s)+new(\s*|$)/,'');
}
}
新しく追加した発言にnewクラスをつけるようにしていますが、
var items = tree.childNodes.length;
results.reverse();
for (i = 0, len = results.length;i < len; i++){
var usr = results[i];
var user = usr.from_user;
/* 要素を作る */
var li = tmpl.cloneNode(true);
var link = li.getElementsByTagName('a')[0];
var icon = link.getElementsByTagName('img')[0];
var name = link.getElementsByTagName('span')[0];
var entry = li.getElementsByTagName('p')[0];
var time = li.getElementsByTagName('div')[0];
var source = time.getElementsByTagName('a')[0];
var username = time.getElementsByTagName('a')[1];
var timelink = time.getElementsByTagName('a')[2];
/* CSS用にclassを設定 */
li.className = (((i+1+items)%2) ?
'odd' : 'even') + ' new';
/* 後述 */
}
新しい発言が上にくるようにreverseをした上で、
TwitterのAPIでは<や&などがエスケープされた状態
/* エスケープされた文字を戻す */
var node = document.createTextNode(usr.text.
replace(/&(lt|gt|quot|amp);/g,function(_$,_1){
return {lt:'<', gt:'>', quot:'"', amp:'&'}[_1];
})
);
/* テキストノードの挿入 */
entry.appendChild(node);
linkfy(entry, '@(\\w+)', '[^\\w@]|$',
'http://twitter.com/');
linkfy(entry, '#(\\w+)', '[^\\w#]|$',
'http://search.twitter.com/search?q=%23');
linkfy(entry, '(https?://.*)',
'[ \\)\\]\'\"\n]|$', '');
expandUrl(entry);
highlight(entry, keyword);
さて、
function linkfy(element, start, end, prefix){
for (var i =0,l = element.childNodes.length;i < l;i++){
var node = element.childNodes[i];
if(node.nodeType !== 3){
continue;
}
if (node.nodeValue.search(start) >= 0) {
var text = node.nodeValue, index;
var parent = node.parentNode;
while (text && (index=text.search(start)) >= 0 ){
// テキストを分割し、後ろ側のノードを取得
var _txt = node.splitText(index);
// キーワードの終わりで再度分割
var _end = _txt.nodeValue.search(end);
var __txt = _txt.splitText(_end);
var a = document.createElement('a');
a.href = prefix + _txt.nodeValue.match(start)[1];
a.target = '_blank';
a.appendChild(_txt);
if (!__txt.nodeValue || !__txt.parentNode){
parent.appendChild(a);
} else {
parent.insertBefore(a, __txt);
}
// ループ用に初期化
text = __txt.nodeValue;
node = __txt;
}
}
}
}
最後は短縮URLの展開処理です。こちらも第11回のJSONPの活用例で短縮URLを展開するAPIを紹介していますが、
function expandUrl(element){
var links = element.getElementsByTagName('a');
for (var i =0,l = links.length;i < l;i++){
var a = links[i];
// 長いURLやtwitter.comの内部リンクは対象外
if (a.href.length < 30 && a.host !== 'twitter.com'){
getCrossSiteXhrOrJsonP(a);
}
}
}
function getCrossSiteXhrOrJsonP(a){
// 現在のURLとAPIのURLが同一オリジンかチェック
var same_origin = location.hostname === 'ss-o.net' &&
(location.port==='' || location.port==='80') &&
location.protocol === 'http:';
var xhr;
var onload = function(){
var data = JSON.parse(xhr.responseText);
if (data.url && data.url !== a.href){
a.textContent = data.url;
a.href = data.url;
}
};
if (same_origin) {
xhr = new XMLHttpRequest();
} else if(window.XDomainRequest){
xhr = new XDomainRequest();
} else if(window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
if (!('withCredentials' in xhr)){
xhr = {
open:function(method, url){
var s = document.createElement('script');
xhr.__script = s;
var callback = ('JSONP_' + new Date().getTime() +
Math.random()).replace(/\W/,'');
s.src = url + '&callback=' + callback;
window[callback] = function(data){
xhr.responseText = JSON.stringify(data);
onload();
document.body.removeChild(s);
delete window[callback];
};
},
send:function(){
document.body.appendChild(xhr.__script);
}
};
}
}
xhr.open('GET', 'http://ss-o.net/api/reurl.json?url=' +
encodeURIComponent(a.href), true);
if (!('onload' in xhr)){
xhr.onreadystatechange = function(){
if(xhr.readyState === 4 && xhr.status === 200){
onload();
}
};
} else {
xhr.onload = onload;
};
xhr.send(null);
}
まず、
続いて、
オリジンが異なる場合はXMLHttpRequest level 2かXDomainRequestを使用しますが、
xhr = {
open:function(method, url){
var s = document.createElement('script');
xhr.__script = s;
var callback = ('JSONP_' + new Date().getTime() +
Math.random()).replace(/\W/,'');
s.src = url + '&callback=' + callback;
window[callback] = function(data){
xhr.responseText = JSON.stringify(data);
onload();
document.body.removeChild(s);
delete window[callback];
};
},
send:function(){
document.body.appendChild(xhr.__script);
}
};
このようにXMLHttpRequestのインタフェースを実装したオブジェクトで、
- simple twitter search - ss-o.
net (URL展開APIと同一オリジン版) - simple twitter search - jsdo.
it (jsdo. it版)
まとめ
今回はここまでの復習として12回で取り上げた内容を使って簡単なアプリケーションを作成してみました。もし忘れているところなどがあったら是非復習してみてください。次回からはJavaScriptの基礎に再び戻って、