- OS
- Windows 7
- サーバー言語
- PHP
- 言語バージョン
- ローカル 5.38
リモート 5.6 - 実行環境
- Xampp 1.7.7
- ローカルサーバー
- Apache 2.2.21
リアルタイム・チャットシステム 公開ソースコード
開発/実行環境
ディレクトリ・ファイル構成
online-chat/ (http://wiz-code.digick.jp/dev/html5/stream/online-chat/room.php) │ │ room.php │ send-chat-data.php │ chat-data-stream.php │ functions.php │ Data_Stream.php │ tree.txt │ ├─js │ online-chat.js │ └─tmp library/ │ ├─ PEAR Cache_Lite │ ├─ jQuery 2.2.2 ├─ Underscore.js 1.8.3 └─ Bootstrap 3.3.1
Cache_Liteによるデータ管理
- Cache_Lite
- PHP PEARライブラリ
ファイル名 | グループ名 | オプションの詳細 | 内容 |
---|---|---|---|
chat_data | online_chat |
// ファイルロックをかける fileLocking: true // 読み込み中に競合がないか検出する readControl: true // 書き込み中に競合がないか検出する writeControl: true // ファイル名を暗号化する fileNameProtection: true |
JSON id: チャットID name: チャット送信者 message: メッセージ内容 create_time: 送信時間(ミリ秒) |
ソースコード
room.php
<?php date_default_timezone_set('UTC'); /* スクリプトの実行時間 */ set_time_limit(60); /* セッション・クッキーはブラウザ終了で破棄。HTTPSであれば第4引数はtrueを指定する */ session_set_cookie_params(0, '/', '', false, true); /* キャッシュを無効化とブラウザバック対応 */ session_cache_expire(0); session_cache_limiter('private_no_expire'); require_once('Cache/Lite.php'); require_once('functions.php'); define('APP_NAME', 'online_chat'); header('Content-type: text/html; charset=utf-8'); session_start(); /* ページの初回訪問者への処理 */ if (!isset($_SESSION['client_id'])) { $_SESSION['client_id'] = random_str(); } /* ノンスを作成 */ $nonce = session_id(); /* Cache_Liteの初期化時に指定するオプション。チャットデータの保存期間は24時間とする */ $options = get_cache_lite_options(array('lifeTime' => 86400)); try { $cache = new Cache_Lite($options); $chat_data = $cache->get('chat_data', APP_NAME); /* チャットデータが存在しなければ新規作成してファイルに保存する */ if ($chat_data === false || !is_array($chat_data)) { $chat_data = array(); $cache->save($chat_data, 'chat_data', APP_NAME); } /* 読み込みエラー時に備えて直前のデータをキャッシュする */ $_SESSION['chat_data_cache'] = $chat_data; /* クライアントに最後に送ったチャットデータのIDを格納 */ $_SESSION['last_chat_id'] = null; session_write_close(); } catch (Exception $e) { format_error_log($e->getMessage, __FILE__, __LINE__); } ?> <!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>チャットルームのテストページ - Server-Sent EventsとAjax・PHPのリアルタイム・チャットシステム | ウィザード・コード - WIZARD-CODE</title> <link href="/lib/bootstrap/3.3.1/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <div class="container"> <h1 class="h2">チャットルームのテストページ</h1> <p><strong>Server-Sent Events</strong>/<strong>Ajax</strong>/<strong>PHP</strong>を組み合わせたリアルタイム・チャットシステムです。SSEが実装されていない<strong>Internet Explorer</strong>では利用できません。リアルタイムとありますが、ユーザー間で同期していないので実際には擬似リアルタイムです。</p> <p>セキュリティについて、Ajax通信はトークンで<strong>CSRF</strong>対策を行います。入力文字のバリデーションはクライアント側で行い、制御文字や特殊文字は入力不可として送信しません。サーバー側の対策は出力時のエスケープ処理のみです。</p> <p>Google ChromeのデベロッパーツールやFirebugなどでストリーミングの中身が確認できます。「ネットワーク」のタブを開くとchat-data-stream.phpがコネクションを張り続けているのがわかると思います。書き込まれた内容はキャッシュファイルに24時間保存され、その後に自動的に削除されます。</p> <p>ソースコードを<a href="http://wiz-code.digick.jp/dev/html5/stream/online-chat/online-chat.html">こちらのページ</a>で公開しています。ライセンスフリーです。</p> <form id="chat-form"> <div id="name-group" class="form-group"> <label for="chat-name" class="control-label">名前を入れてください</label> <input type="text" class="form-control" id="chat-name" name="chat-name" placeholder="3文字以上20文字まで入力できます(空欄でもOK)。"> </div> <div id="message-group" class="form-group"> <label for="chat-message" class="control-label">メッセージをどうぞ</label> <input type="text" class="form-control" id="chat-message" name ="chat-message" placeholder="最大60文字まで入力できます。" autocomplete="off"> </div> <input type="hidden" id="nonce" name="nonce" value="<?=h($nonce)?>"> <button id="submit-button" type="submit" class="btn btn-info">チャットを送信</button> </form> <table class="table"> <thead> <tr> <th class="col-xs-2 col-sm-2 col-md-2 col-lg-2">名前</th> <th class="col-xs-8 col-sm-8 col-md-8 col-lg-8">メッセージ</th> <th class="col-xs-2 col-sm-2 col-md-2 col-lg-2 text-center">送信時間</th> </tr> </thead> <tbody id="chat-list"></tbody> </table> </div> <script src="/lib/jquery/jquery-2.2.2.js"></script> <script src="/lib/bootstrap/3.3.1/js/bootstrap.min.js"></script> <script src="/lib/underscore/1.8.3/underscore.js"></script> <script src="/js/online-chat.js"></script> </body> </html>
send-chat-data.php
date_default_timezone_set('UTC'); set_time_limit(60); session_set_cookie_params(0, '/', '', false, true); require_once('Cache/Lite.php'); require_once('functions.php'); define('APP_NAME', 'online_chat'); define('MAX_CHAT_LIMIT', 100); /* レスポンスはテキストデータとして返す */ header('Content-Type: text/plain; charset=utf-8'); header('X-Content-Type-Options: nosniff'); session_start(); /* クライアントに渡す変数(真偽値) */ $response = false; /* Ajax通信でなければ終了 */ if (@$_SERVER['HTTP_X_REQUESTED_WITH'] !== 'XMLHttpRequest') { $message = 'invalid access'; format_error_log($message, __FILE__, __LINE__); echo $response; exit; } /* アクセスが room.php 経由でない場合終了 */ if (!isset($_SESSION['client_id'])) { $message = 'client id not found'; format_error_log($message, __FILE__, __LINE__); echo $response; exit; } /* POSTデータを読み込む */ $raw_post_data = @file_get_contents('php://input'); parse_str($raw_post_data, $params); $chat_name = $params['chat-name']; $chat_message = $params['chat-message']; $nonce = $params['nonce']; /* ノンスが異なっていたら終了 */ if (session_id() !== $nonce) { $message = 'invalid nonce'; format_error_log($message, __FILE__, __LINE__); echo $response; exit; } /* チャットデータの保存期間は24時間とする */ $options = get_cache_lite_options(array('lifeTime' => 86400)); try { /* Cache_Liteライブラリを初期化 */ $cache = new Cache_Lite($options); /* チャットデータを読み込む */ $chat_data = $cache->get('chat_data', APP_NAME); if ($chat_data === false || !is_array($chat_data)) { /* データが壊れている可能性があるので直前のデータで上書き */ $cache->save($_SESSION['chat_data_cache'], 'chat_data', APP_NAME); $message = 'chat data not found'; format_error_log($message, __FILE__, __LINE__); echo $response; exit; } /* ランダムな文字列を生成してチャットIDにする */ $chat_id = random_str(); $new_chat = array( 'id' => $chat_id, 'name' => $chat_name, 'message' => $chat_message, 'create_time' => floor(microtime(true) * 1000), ); array_unshift($chat_data, $new_chat); /* データ数が最大数を超えたら古い順に削除 */ $chat_data = array_slice($chat_data, 0, MAX_CHAT_LIMIT); /* セッション変数に直前のチャットデータを格納 */ $_SESSION['chat_data_cache'] = $chat_data; /* チャットデータを保存 */ $result = $cache->save($chat_data, 'chat_data', APP_NAME); if ($result === false || is_object($result)) { $message = 'failed to save'; format_error_log($message, __FILE__, __LINE__); echo $response; exit; } $response = true; echo $response; } catch (Exception $e) { format_error_log($e->getMessage, __FILE__, __LINE__); echo $response; }
chat-data-stream.php
date_default_timezone_set('UTC'); set_time_limit(60); session_set_cookie_params(0, '/', '', false, true); mb_http_output('pass'); require_once('Cache/Lite.php'); require_once('Data_Stream.php'); require_once('functions.php'); /* UPDATE_FREQUENCY: 何秒ごとにデータストリームを送るか */ define('UPDATE_FREQUENCY', 1); /* ストリーミングのタイムアウト時間を指定 */ define('TIMEOUT', 30); define('APP_NAME', 'online_chat'); /* ページロード時、一括して読み込むチャットデータの数 */ define('INIT_CHAT_OUTPUT', 30); /* セッションはストリーミング中の排他ロックを防ぐため明示的に終了させる */ session_start(); /* アクセスが room.php 経由でない場合終了 */ if (!isset($_SESSION['client_id'])) { $message = 'client id not found'; abort_stream($message); exit; } /* Last-Event-IDをチェックする */ $last_event_id = @$_SERVER['HTTP_LAST_EVENT_ID']; $last_event_id = is_null($last_event_id) ? 0 : intval($last_event_id) + 1; /* 最後に送ったチャットデータのIDをセッション変数から取り出す */ $last_chat_id = $_SESSION['last_chat_id']; session_write_close(); /* Cache_Liteの初期化時に指定するオプション。チャットデータの保存期間は24時間とする */ $options = get_cache_lite_options(array('lifeTime' => 86400)); try { $cache = new Cache_Lite($options); $timeout = TIMEOUT * 1000; $frequency = UPDATE_FREQUENCY * 1000 * 1000; /* データストリーミング用のクラスを初期化 */ $ds = new Data_Stream; $ds->start(); /* イベントIDは再接続時に前回からの連番にする */ if ($last_event_id > 0) { $ds->setId($last_event_id); } /* タイムアウト時間までループ処理 */ while ($ds->getElapsedTime() < $timeout) { /* チャットデータを読み込む */ $chat_data = $cache->get('chat_data', APP_NAME); if ($chat_data === false || !is_array($chat_data)) { $message = 'chat data not found'; format_error_log($message, __FILE__, __LINE__); /* データが壊れている可能性があるので直前のデータで上書きする */ @session_start(); $cache->save($_SESSION['chat_data_cache'], 'chat_data', APP_NAME); session_write_close(); $ds->flush('', 'chat-data'); usleep($frequency); continue; } /* 初回だけ一括してチャットデータを送る */ if (is_null($last_chat_id)) { $sliced_data = array_slice($chat_data, 0, INIT_CHAT_OUTPUT); for ($i = 0, $l = count($sliced_data); $i < $l; $i++) { $data = $sliced_data[$i]; if ($i === 0) { $last_chat_id = $data['id']; } /* 文字列データはエスケープする */ $data = escapeStreamData($data); $sliced_data[$i] = $data; } /* 初回の送信は配列データを送る */ if (!empty($sliced_data)) { $json = json_safe_encode($sliced_data); $ds->storeData($json); } /* 以降は新規のチャットだけを送る */ } else { $chat_ids = array_column($chat_data, 'id'); $last_chat_index = array_search($last_chat_id, $chat_ids, true); if ($last_chat_index > 0) { $sliced_data = array_slice($chat_data, 0, $last_chat_index); for ($i = 0, $l = count($sliced_data); $i < $l; $i++) { $data = $sliced_data[$i]; if ($i === 0) { $last_chat_id = $data['id']; } /* 文字列データはエスケープする */ $data = escapeStreamData($data); $json = json_safe_encode($data); /* Data_Stream::storeData()の第2引数にtrueを渡すと配列に要素をunshiftで入れる。作成日時の古い順にスタックするため */ $ds->storeData($json, true); } } } /* チャットデータをフラッシュして一定時間スリープ */ $ds->output('chat-data'); usleep($frequency); } $ds->end(); /* セッションを再開して変数の値を更新する */ @session_start(); $_SESSION['chat_data_cache'] = $chat_data; $_SESSION['last_chat_id'] = $last_chat_id; session_write_close(); } catch (Exception $e) { format_error_log($e->getMessage(), __FILE__, __LINE__); abort_stream(); exit; } /* クライアントにストリーミングの中断を指示する */ function abort_stream($error_log = false) { if ($error_log !== false) { format_error_log($error_log, __FILE__, __LINE__); } $ds = new Data_Stream; $ds->start(); $ds->flush('failed to stream', 'abort-chat-data-stream'); $ds->end(); }
functions.php
/* PHP 5.5.0未満のバージョンではarray_column()がサポートされておらず、下記のコードが必要 */ /** * This file is part of the array_column library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) 2013 Ben Ramsey <http://benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ if (!function_exists('array_column')) { /** * Returns the values from a single column of the input array, identified by * the $columnKey. * * Optionally, you may provide an $indexKey to index the values in the returned * array by the values from the $indexKey column in the input array. * * @param array $input A multi-dimensional array (record set) from which to pull * a column of values. * @param mixed $columnKey The column of values to return. This value may be the * integer key of the column you wish to retrieve, or it * may be the string key name for an associative array. * @param mixed $indexKey (Optional.) The column to use as the index/keys for * the returned array. This value may be the integer key * of the column, or it may be the string key name. * @return array */ function array_column($input = null, $columnKey = null, $indexKey = null) { // Using func_get_args() in order to check for proper number of // parameters and trigger errors exactly as the built-in array_column() // does in PHP 5.5. $argc = func_num_args(); $params = func_get_args(); if ($argc < 2) { trigger_error("array_column() expects at least 2 parameters, {$argc} given", E_USER_WARNING); return null; } if (!is_array($params[0])) { trigger_error('array_column() expects parameter 1 to be array, ' . gettype($params[0]) . ' given', E_USER_WARNING); return null; } if (!is_int($params[1]) && !is_float($params[1]) && !is_string($params[1]) && $params[1] !== null && !(is_object($params[1]) && method_exists($params[1], '__toString')) ) { trigger_error('array_column(): The column key should be either a string or an integer', E_USER_WARNING); return false; } if (isset($params[2]) && !is_int($params[2]) && !is_float($params[2]) && !is_string($params[2]) && !(is_object($params[2]) && method_exists($params[2], '__toString')) ) { trigger_error('array_column(): The index key should be either a string or an integer', E_USER_WARNING); return false; } $paramsInput = $params[0]; $paramsColumnKey = ($params[1] !== null) ? (string) $params[1] : null; $paramsIndexKey = null; if (isset($params[2])) { if (is_float($params[2]) || is_int($params[2])) { $paramsIndexKey = (int) $params[2]; } else { $paramsIndexKey = (string) $params[2]; } } $resultArray = array(); foreach ($paramsInput as $row) { $key = $value = null; $keySet = $valueSet = false; if ($paramsIndexKey !== null && array_key_exists($paramsIndexKey, $row)) { $keySet = true; $key = (string) $row[$paramsIndexKey]; } if ($paramsColumnKey === null) { $valueSet = true; $value = $row; } elseif (is_array($row) && array_key_exists($paramsColumnKey, $row)) { $valueSet = true; $value = $row[$paramsColumnKey]; } if ($valueSet) { if ($keySet) { $resultArray[$key] = $value; } else { $resultArray[] = $value; } } } return $resultArray; } } function get_cache_lite_options($options = null) { $default_options = array( 'cacheDir' => './tmp/', 'caching' => true, 'fileLocking' => true, 'readControl' => true, 'writeControl' => true, 'fileNameProtection' => true, 'automaticSerialization' => true, 'automaticCleaningFactor' => 200, 'lifeTime' => 3600, 'hashedDirectoryLevel' => 1, ); if (is_null($options)) { return $default_options; } foreach ($default_options as $key => $value) { if (!array_key_exists($key, $options)) { $options[$key] = $value; } } return $options; } function escapeStreamData($data) { foreach ($data as $key => $value) { if (is_string($value)) { $data[$key] = h($value); } } return $data; } function json_safe_encode($val) { return json_encode($val, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); } function h($str) { return htmlspecialchars($str, ENT_QUOTES, 'UTF-8'); } function random_str($length = 32) { return strtr(substr(base64_encode(openssl_random_pseudo_bytes($length)), 0, $length), '/+', '_-'); } function format_error_log($message, $file = null, $line = null) { $message = is_string($message) ? $message : strval($message); $file = !is_null($file) ? basename($file) . ' ' : ''; $line = !is_null($line) ? "line {$line} " : ''; $message = $file . $line . $message; $date = new DateTime(); $date->setTimezone(new DateTimeZone('Asia/Tokyo')); $message = $message . ' [' . $date->format(DATE_ISO8601) . ']' . PHP_EOL; /* ログファイルの場所をフルパスで指定 */ error_log($message, '3', '/home/*****/log/debug.log'); }
Data_Stream.php
class Data_Stream { private $id = 1; private $options = array( 'defaultEvent' => 'message', 'retry' => 0, ); private $started = false; private $data; private $elapsedTime; private $startTime; public function __construct($options = NULL) { if (!is_null($options)) { $this->setOptions($options); } if (headers_sent() === true) { header_remove('Content-type'); header_remove('Cache-Control'); } header('Content-type: text/event-stream; charset=utf-8'); header('Cache-Control: no-cache'); } public function setOption($options) { foreach ($options as $key => $value) { if (isset($value)) { $this->options[$key] = $value; } } } public function getId() { return $this->id; } public function setId($id) { $this->id = is_int($id) ? $id : intval($id); return $this->id; } public function start() { ob_end_clean(); ob_start(); $this->data = array(); $this->elapsedTime = 0; $this->startTime = microtime(true) * 1000; $this->started = true; } public function end() { ob_end_clean(); $this->started = false; } public function getElapsedTime() { $result = 0; if ($this->started === true) { $result = $this->elapsedTime = (microtime(true) * 1000) - $this->startTime; return $result; } return $result; } public function storeData($data, $reverse = false) { if (is_string($data)) { if ($reverse === false) { array_push($this->data, $data); } else { array_unshift($this->data, $data); } return true; } return false; } public function output($event = null, $id = null) { $event = !is_null($event) ? $event : $this->options['defaultEvent']; $id = !is_null($id) ? $id : $this->id; $data_list = ""; if (!empty($this->data)) { for ($i = 0, $l = count($this->data); $i < $l; $i++) { $value = $this->data[$i]; $data_list .= "id: {$id}" . PHP_EOL . "event: {$event}" . PHP_EOL . "data: {$value}" . PHP_EOL . PHP_EOL; $id = ++$this->id; } } if ($this->started === true && is_string($data_list)) { /* ヒアドキュメントの終わりにある二つの改行はデータの終了を示す */ $message = <<< EOM : keep alive retry: {$this->options['retry']} {$data_list} EOM; echo $message; @ob_flush(); @flush(); $this->data = array(); } } public function flush($data, $event = null, $id = null) { $event = !is_null($event) ? $event : $this->options['defaultEvent']; if (is_null($id)) { $id = $this->id; $this->id++; } if ($this->started === true && is_string($data)) { if (!empty($data)) { $data = "data: {$data}"; } else { $data = ''; } /* ヒアドキュメントの終わりにある二つの改行はデータの終了を示す */ $message = <<< EOM : keep alive id: {$id} event: {$event} retry: {$this->options['retry']} {$data} EOM; echo $message; @ob_flush(); @flush(); } } }
online-chat.js
/* このファイルはコメントを削除したり圧縮しないでください */ ;(function () { var heredocReg, validReg, fragment, MIN_NAME_LIMIT, MAX_NAME_LIMIT, MAX_MESSAGE_LIMIT, DATE_UPDATE_INTERVAL; heredocReg = /^function\s+\([^)]*\)\s*\{\s*|\s*\/\*\s*|\s*\*\/\s*\}$/g; validReg = /^[\x20\x21\x23-\x25\x28-\x3B\x3D\x3F-\x7E\u2010-\u2027\u3000-\u3036\u303F\u3041-\u3094\u3099-\u309E\u30A1-\u30F6\u30FB-\u30FE\u4E00-\u9FA5\uFF01-\uFF5E\uFF61-\uFF9F\uFFE0-\uFFE6]+$/; fragment = {}; /* チャット表示要素のフラグメント */ fragment.chat = (function () { /* <tr> <td class="chat-from"><%= name %></td> <td class="chat-body"><%= message %></td> <td class="chat-time"><%= time %></td> </tr> */ }).toString().replace(heredocReg, ''); MIN_NAME_LIMIT = 3; MAX_NAME_LIMIT = 20; MAX_MESSAGE_LIMIT = 60; DATE_UPDATE_INTERVAL = 20; $(function () { var chatList, chatForm, chatName, chatMessage, submitButton, chatDataStream, chatData, dateUpdateInterval; chatForm = $('#chat-form'); chatName = $('#chat-name'); chatMessage = $('#chat-message'); chatList = $('#chat-list'); submitButton = $('#submit-button'); chatData = []; chatForm.on('submit', function (e) { e.preventDefault(); }); chatName.on('keypress', function (e) { /* テキストフォームのエンターキー押し下げによるイベントを抑止 */ if (e.key === 'Enter') { e.preventDefault(); } }); submitButton.on('click', sendChatMessage); /* 新規のチャットメッセージを確認するストリーム */ chatDataStream = new EventSource('/dev/html5/stream/online-chat/chat-data-stream.php'); /* サーバーからのストリームデータを受信 */ chatDataStream.addEventListener('chat-data', function (e) { var data, time, element; data = JSON.parse(e.data); /* ページロード時はチャットデータが配列にまとめられて送られてくる */ if (_.isArray(data)) { _.each(data, function (d) { element = insertChatData(d); highlightElement(element); }); chatData = chatData.concat(data.reverse()); /* 以降は一件ずつ送られてくる */ } else { element = insertChatData(data, 'prependTo'); highlightElement(element); chatData.push(data); } }); chatDataStream.addEventListener('error', function (e) { console.info('チャットデータのストリーミングを再開します'); }, false); chatDataStream.addEventListener('abort-chat-data-stream', function (e) { console.warn('チャットデータのストリーミングを中断しました'); chatDataStream.close(); }, false); $(window).on('unload', function (e) { chatDataStream.close(); }, false); /* チャットの作成時間を一定時間ごとに更新 */ dateUpdateInterval = DATE_UPDATE_INTERVAL * 1000; window.setInterval(dateUpdater, dateUpdateInterval); function sendChatMessage(e) { var name, message, label, messageGroup; e.preventDefault(); name = chatName.val(); message = chatMessage.val(); messageGroup = chatForm.children('#message-group'); label = messageGroup.children('label'); /* ユーザー名のバリデーション */ if (_.isEmpty(name) || name.length < MIN_NAME_LIMIT || name.length > MAX_NAME_LIMIT || !validReg.test(name)) { name = 'NO NAME'; } /* メッセージ内容のバリデーション */ if (_.isEmpty(message)) { if (!messageGroup.hasClass('has-error')) { messageGroup.addClass('has-error'); } label.text('メッセージを入力してください'); chatMessage.focus(); return; } if (message.length > MAX_MESSAGE_LIMIT) { if (!messageGroup.hasClass('has-error')) { messageGroup.addClass('has-error'); } label.text('メッセージの入力文字数は60文字までです'); chatMessage.focus(); return; } if (!validReg.test(message)) { if (!messageGroup.hasClass('has-error')) { messageGroup.addClass('has-error'); } label.text('特殊文字(& < > " \'など)は使用できません'); chatMessage.focus(); return; } if (messageGroup.hasClass('has-error')) { messageGroup.removeClass('has-error'); label.text('チャットツール'); } /* 名前欄が空欄のとき代替名を入れる */ chatName.val(name); $(this).prop('disabled', true).blur(); /* メッセージを送信 */ $.post( '/dev/html5/stream/online-chat/send-chat-data.php', chatForm.serialize(), function (data) { submitButton.prop('disabled', false); if (_.isEmpty(data)) { if (!messageGroup.hasClass('has-error')) { messageGroup.addClass('has-error'); } label.text('メッセージの送信に失敗しました'); chatMessage.focus(); return; } chatMessage.val(''); }, 'text' ); } function dateUpdater() { var chatRows; chatRows = chatList.children('tr'); _.each(chatData, function (data) { var time; time = formatUpdateTime(data.create_time); chatRows.filter('[id="' + data.id + '"]'). find('.chat-time').text(time); }); } function highlightElement(element) { var color, duration; color = 50; duration = 700; $(element).css({backgroundColor: 'hsl(60,100%,' + color + '%)'}). animate({backgroundColor: 'transparent'}, { duration: duration, progress: function (animation, progress) { $(this).css({backgroundColor: 'hsl(60,100%,' + Math.floor(progress * (100 - color) + color) + '%)'}); } } ); } function insertChatData(data, method) { var time, element, temp, rendered; method = !_.isUndefined(method) ? method : 'appendTo'; time = formatUpdateTime(Math.floor(data.create_time)); temp = _.template(fragment.chat); rendered = temp({ name: data.name, message: data.message, time: time, }); element = $(rendered).attr('id', data.id)[method](chatList); return element; } function formatUpdateTime(startTime) { var elapsedTime, remainder, years, months, days, hours, minutes, seconds; elapsedTime = _.now() - startTime; elapsedTime = Math.max(elapsedTime, 0); if (Math.floor(elapsedTime / 1000) === 0) { return 'たった今'; } years = Math.floor(elapsedTime / 3.1536e+10); if (years > 0) { elapsedTime = years + '年前'; } else { remainder = elapsedTime % 3.1536e+10; months = Math.floor(remainder / 2.592e+9); if (months > 0) { elapsedTime = months + 'ヶ月前'; } else { remainder = elapsedTime % 2.592e+9; days = Math.floor(remainder / 8.64e+7); if (days > 0) { elapsedTime = days + '日前'; } else { remainder = elapsedTime % 8.64e+7; hours = Math.floor(remainder / 3.6e+6); if (hours > 0) { elapsedTime = hours + '時間前'; } else { remainder = elapsedTime % 3.6e+6; minutes = Math.floor(remainder / 60000); if (minutes > 0) { elapsedTime = minutes + '分前'; } else { seconds = Math.floor(elapsedTime / 1000); elapsedTime = seconds + '秒前'; } } } } } return elapsedTime; } }); }());