Last active
August 29, 2015 14:03
-
-
Save Leko/a286b79a986511127293 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @class ChatworkExtension | |
*/ | |
var ChatworkExtension = (function() { | |
var config = { | |
ids: { | |
roomInfo: { | |
name: '_roomInfoName', | |
description: '_roomInfoDescription' | |
} | |
}, | |
classes: { | |
room: { | |
title: 'chatListTitleArea', | |
pin: { | |
on: 'ico19PinOn', | |
off: 'ico19PinOff', | |
} | |
}, | |
message: { | |
el: '_message', | |
mine: 'chatTimeLineMessageMine', | |
speaker: '_speaker', | |
speakerName: '_speakerName', | |
orgname: 'chatNameOrgname', | |
timestamp: '_timeStamp' | |
}, | |
menu: { | |
message: { | |
box: '_cwABAction linkStatus', | |
text: '_showAreaText showAreatext' | |
} | |
} | |
} | |
}; | |
var _uid = 0, | |
$emoticonGallery = $("#_emoticonGallery img"), | |
$timeline = $('#_timeLine'), | |
$chat = $('#_chatText'), | |
$send = $('#_sendButton'), | |
$roomList = $('#_roomListItems li._roomLink'), | |
$roomInfoIconPreset = $("#_roomInfoIconPreset img"); | |
/** | |
* チャットルームが追加された際のコールバック | |
* $roomListの更新とchatRoomsの更新を行う | |
*/ | |
var roomObserver = new DOMObserver($roomList.parent().get(0), { childList: true }); | |
// roomObserver.on('add', function() { | |
// console.log('add', arguments); | |
// }); | |
// roomObserver.on('add', function() { | |
// console.log('add2', arguments); | |
// }); | |
// roomObserver.on('remove', function() { | |
// console.log('remove', arguments); | |
// }); | |
function generateMessageMenu(name, icon) { | |
var $menuBox = $('<li></li>'), | |
$menu = $('<span></span>'); | |
// メニュー用のHTMLを作成 | |
$menu.text(name); | |
$menu.addClass(config.classes.menu.message.text); | |
$menuBox.attr('role', 'button'); | |
$menuBox.addClass(config.classes.menu.message.box); | |
if(typeof icon !== 'undefined') { | |
var $icon = $('<span></span>').addClass(icon); | |
$menuBox.append($icon); | |
} | |
$menuBox.append($menu); | |
return $menuBox; | |
} | |
/** | |
* メッセージをパースする | |
* メッセージのメニューがクリックされて、そのメッセージの情報を修得することを想定している | |
*/ | |
function parseMessage($el) { | |
var $root = $el.parents('.' + config.classes.message.el), | |
time = +$root.find('.' + config.classes.message.timestamp).data('tm'); | |
data = { | |
'$root': $root, | |
rid: $root.data('rid'), | |
mid: $root.data('mid'), | |
aid: $root.find('.' + config.classes.message.speaker + ' img').data('aid'), | |
mine: $root.hasClass(config.classes.message.mine), | |
name: $root.find('.' + config.classes.message.speakerName + ' span').text(), | |
orgname: $root.find('.' + config.classes.message.orgname + ' span').text(), | |
content: $root.find('pre:first'), | |
timestamp: time, | |
date: new Date(time) | |
}; | |
return data; | |
} | |
function getChatRooms() { | |
return Array.prototype.slice.call($roomList).reduce(function(memo, roomEl) { | |
var $el = $(roomEl), | |
room = { | |
id: $el.data('rid'), | |
name: $el.find('.' + config.classes.room.title).text(), | |
pinned: !!$el.find('.' + config.classes.room.pin.on).size() | |
}; | |
memo.push(room); | |
return memo; | |
}, []); | |
} | |
/** | |
* @constructor | |
*/ | |
function ChatworkExtension() { | |
this.targets = {}; | |
} | |
// エモーティコンを取得して一覧をキャッシュ | |
ChatworkExtension._emoticons = Array.prototype.slice.call($emoticonGallery).reduce(function(memo, img) { | |
var name = img.src.replace(/(.*?emo_)/, '').replace('.gif', ''), | |
token = img.alt; | |
memo[name] = token; | |
return memo; | |
}, {}); | |
// チャットルーム一覧をオブジェクトの配列化したものを格納し外部へ公開 | |
ChatworkExtension.chatRooms = getChatRooms(); | |
// ################################ | |
// static methods | |
// ################################ | |
/** | |
* 新しくチャットルームを作成する | |
* @param string name チャットルーム名 | |
* @param string description チャットルームの詳細説明文 | |
* @param array [members=[]] チャットルームのメンバーのアカウントIDを配列で指定。デフォルトでは自分のみ | |
* @param opts [opts={}] チャットルームのオプション、デフォルトでは何もなし | |
* @return void | |
*/ | |
ChatworkExtension.createRoom = function(name, description, members, opts) { | |
members = members || []; | |
opts = opts || {}; | |
// TODO: セレクタを設定値化 | |
$("#_addButton").trigger('click'); | |
$('[data-cwui-dd-value="addchat"]').click(); | |
$('#' + config.ids.roomInfo.name).val(name); | |
$('#' + config.ids.roomInfo.description).val(description); | |
// アイコンの指定があればそれを指定 | |
if(typeof opts.icon === 'string') { | |
$roomInfoIconPreset.each(function() { | |
var src = $(this).attr('src'); | |
if(src.indexOf('heart') >= 0) $(this).click(); | |
}); | |
} | |
// TODO: メンバー一覧の操作 | |
// NOTE: 予めキャッシュしているとボタンの要素が取得できない | |
var create = $("#_addRoom").prev().find('[aria-label="作成する"]') | |
create.trigger('click'); | |
} | |
/** | |
* 現在開いているチャットルームを変更する | |
* @param int roomId 開くチャットルームのID | |
* @param function callback 指定したチャットルームが開かれたら実行されるコールバック | |
* @return void | |
*/ | |
ChatworkExtension.changeRoom = function(roomId, callback) { | |
}; | |
/** | |
* チャットルームを検索する、なければnullを返す | |
* @param object query 検索に使用したい{プロパティ: 値} | |
* @return any 検索に一致するチャットルームがあればその情報、無ければnull | |
*/ | |
ChatworkExtension.findRoom = function(query) { | |
var found = null; | |
ChatworkExtension.chatRooms.some(function(room) { | |
var ok = true; | |
for(var prop in query) { | |
if(!query.hasOwnProperty(prop)) continue; | |
ok = ok && room[prop] === query[prop]; | |
} | |
if(ok) { | |
found = room; | |
return true; | |
} | |
}); | |
return found; | |
} | |
/** | |
* チャットワークへ投稿する | |
* チャットワーク記法が使えます。詳しくは http://developer.chatwork.com/ja/messagenotation.html を参照。 | |
* ChatworkExtension.notationsにチャットワーク記法ヘルパメソッドを用意しています。合わせてそちらも御覧ください。 | |
* @param string body 投稿する本文。 | |
* @param int roomId 投稿するチャットルーム。省略された場合は現在開いているチャットルームへ投稿する | |
* @return void | |
*/ | |
ChatworkExtension.send = function(body, roomId) { | |
}; | |
/** | |
* ChatworkExtension.notations.emoticonで使用できる名前と返却される文字列の一覧をコンソールに出力する | |
* @return void | |
*/ | |
ChatworkExtension.emoticons = function() { | |
for(var name in ChatworkExtension._emoticons) { | |
console.log(name + ' => ' + ChatworkExtension._emoticons[name]); | |
} | |
}; | |
// ################################ | |
// chatwork notations | |
// ################################ | |
ChatworkExtension.notations = { | |
/** | |
* [info]タグを使用する | |
* @param string body [info]タグの中身 | |
* @param string [title=null] [title]タグを設定する場合に使用する。省略された場合titleはつけない | |
* @return string | |
*/ | |
info: function(body, title) { | |
var ret = '[info]'; | |
title = title || null; | |
// タイトルがセットされていれば[title]タグを付与 | |
if(title !== null) ret += '[title]' + title + '[/title]'; | |
ret += body + '[/info]'; | |
return ret; | |
}, | |
/** | |
* 水平線([hr]タグ)を使用する | |
* @return string | |
*/ | |
hr: function() { | |
return '[hr]'; | |
}, | |
/** | |
* [To:{account_id}]タグを使用する | |
* @param object accounts アカウントID: 表示名というオブジェクトでToを複数指定する | |
* @param string suffix 人名につける敬称。省略するとさんを付与する | |
* @return string | |
*/ | |
to: function(accounts, suffix) { | |
suffix = suffix || 'さん'; | |
var ret = ''; | |
for(var accountId in accounts) { | |
var name = accounts[accountId] || ''; | |
if(!accounts.hasOwnProperty(accountId)) continue; | |
ret += '[To:' + accountId + '] ' + name + suffix + "\n"; | |
} | |
return ret; | |
}, | |
/** | |
* [rp]タグを使用する | |
* @param number accountId 返信するアカウントID | |
* @param number roomId 返信するチャットルームのID | |
* @param number messageId 返信するメッセージのID | |
*/ | |
reply: function(accountId, roomId, messageId) { | |
var reply = '[rp aid=' + accountId + ' to=' + roomId + '-' + messageId + ']'; | |
return reply; | |
}, | |
/** | |
* [qt]タグを使用する | |
* @param number accountId 引用元のアカウントID | |
* @param string body 引用するテキスト | |
* @param number [timestamp=null] 囲繞するメッセージのUNIXタイムスタンプ。省略された場合は時間を表示しない | |
* @return string | |
*/ | |
quote: function(accountId, body, timestamp) { | |
timestamp = timestamp || null; | |
var quote = '[qt]'; | |
if(timestamp !== null) { | |
quote += '[qtmeta aid=' + accountId + ' time=' + timestamp + ']'; | |
} else { | |
quote += '[qtmeta aid=' + accountId + ']'; | |
} | |
quote += body + '[/qt]'; | |
return quote; | |
}, | |
/** | |
* [picon]タグ(アカウントのアイコン表示)を使用する | |
* @param number accountId 表示するアイコンのアカウントID | |
* @return string | |
*/ | |
picon: function(accountId) { | |
return '[picon:' + accountId + ']'; | |
}, | |
/** | |
* [piconname]タグ(アカウントのアイコンと名前表示)を使用する | |
* @param number accountId 表示するアイコンのアカウントID | |
* @return string | |
*/ | |
piconname: function(accountId) { | |
return '[piconname:' + accountId + ']'; | |
}, | |
/** | |
* エモーティコンを使用する | |
* @param string name エモーティコンの名前。画像名がemo_xxx.gifとなっていて、xxxの部分を指定する | |
* @param number repeat 繰り返し回数。デフォルトは1 | |
*/ | |
emoticon: function(name, repeat) { | |
repeat = repeat || 1; | |
return new Array(repeat + 1).join(ChatworkExtension._emoticons[name]); | |
} | |
}; | |
/** | |
* 監視を行うメソッド一覧 | |
* @param string type change,clickなどを考え中。とりあえずchangeのみ受付 | |
*/ | |
var observers = { | |
room: function(type, action) { | |
// チャット一覧の追加を監視 | |
var observe = new MutationObserver(function() { | |
action.apply(null); | |
}); | |
observe.observe($timeline.get(0), { childList: true }); | |
// changeはDOMイベントじゃないのでカスタムイベントを作成する | |
if(type === 'change') { | |
// チャットルーム一覧がクリックされ、チャットルームの変更が起こったらactionを呼び出す | |
$roomList.on('click', function(e) { | |
// 現在開いているチャットルームなら何もしない | |
var isSelected = $(this).hasClass('_roomSelected'); | |
if(isSelected) return false; | |
}); | |
} | |
} | |
}; | |
// ################################ | |
// instance methods | |
// ################################ | |
ChatworkExtension.prototype = { | |
/** | |
* 各アクションに対してフックを仕掛ける | |
* アクションの指定は`[eventType]:[target]`という書式で指定する。 | |
* 例えばclick:messageのような感じ。 | |
* @param string event 上記の書式の文字列 | |
* @param function action 指定したアクションが起こった場合のイベントハンドラ | |
*/ | |
hook: function(event, action) { | |
var events = event.split(':'), | |
type = events[0], | |
target = events[1]; | |
observers[target](type, action); | |
}, | |
/** | |
* マウスでメッセージをドラッグしたときに出るメニューを拡張 | |
* @method addQuoteMenu | |
* @param string name メニューに表示する文字列 | |
* @param function action クリックされた際のイベント | |
* @param string id メニューに設定するID。省略すると自動で一意なIDが振られる | |
* @return ChatworkExtension 自分自身のインスタンスを返す | |
*/ | |
addQuoteMenu: function(name, action, id) { | |
return this; | |
}, | |
/** | |
* 1つ1つのメッセージに表示するメニューを拡張する | |
* @param string name メニューに表示する文字列 | |
* @param function action クリックされた際のイベント | |
* @param string icon メニューに設定するアイコンのクラス名。省略するとなし | |
*/ | |
addMessageMenu: function(name, action, icon) { | |
this.hook('change:room', function() { | |
var $messages = $timeline.find('._message'); | |
$.each($messages, function() { | |
var $el = $(this), | |
$menu = generateMessageMenu(name, icon); | |
// イベントの設定 | |
$menu.on('click', function() { | |
var parsed = parseMessage($(this)); | |
action.apply(this, [parsed]); | |
}); | |
// NOTE: mouseenterを発火しないとメッセージのメニューが取れないので意図的に発火 | |
$el.trigger('mouseenter'); | |
// 埋め込み | |
$el.find('ul.actionNav').prepend($menu); | |
}); | |
}); | |
} | |
}; | |
return ChatworkExtension; | |
}()); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var DOMObserver = (function() { | |
function callbackStructure() { | |
return { add: [], remove: [] }; | |
} | |
function DOMObserver(el, opts) { | |
// ユーザ指定のコールバックを発火する用のコールバック | |
var fire = function(records) { | |
records = records[0]; | |
// 追加されたノードが存在すればaddイベントを発火 | |
if(records.addedNodes.length > 0) | |
this.trigger('add', [records], records.target); | |
// 削除されたノードが存在すればremoveイベントを発火 | |
if(records.removedNodes.length > 0) | |
this.trigger('remove', [records], records.target); | |
}; | |
// コールバックを初期化 | |
this.callbacks = callbackStructure(); | |
this.callbacksOnce = callbackStructure(); | |
// 変更を検知した際のコールバックを設定 | |
this.observer = new MutationObserver(fire.bind(this)); | |
// 監視設定 | |
this.observer.observe(el, opts); | |
} | |
DOMObserver.prototype = { | |
/** | |
* イベントにコールバックを設定する | |
* @param string type 指定するイベント名 | |
* @param function callback 指定するコールバック | |
* @param any [context=null] コールバックに指定するコンテキスト。省略するとnullが指定される | |
* @return DOMObserver 自分自身のインスタンスを返す | |
*/ | |
on: function(type, callback, context) { | |
context = context || null; | |
this.callbacks[type].push(callback.bind(context)); | |
return this; | |
}, | |
/** | |
* イベントに一度だけ実行されるコールバックを設定する | |
* @param string type 指定するイベント名 | |
* @param function callback 指定するコールバック | |
* @param any [context=null] コールバックに指定するコンテキスト。省略するとnullが指定される | |
* @return DOMObserver 自分自身のインスタンスを返す | |
*/ | |
once: function(type, callback, context) { | |
context = context || null; | |
this.callbacksOnce[type].push(callback.bind(context)); | |
return this; | |
}, | |
/** | |
* イベント、コールバックの購読を解除する | |
* @param string [type] 購読を解除するイベントタイプ。省略されたら全てのイベントの購読を解除する | |
* @param function [callback] 購読を解除するコールバック。省略されたら指定されたイベントタイプの全てのコールバックを解除する | |
* @return DOMObserver 自分自身のインスタンスを返す | |
*/ | |
off: function(type, callback) { | |
// typeがなければ全て解除 | |
if(typeof type === 'undefined') { | |
this.callbacks = callbackStructure(); | |
this.callbacksOnce = callbackStructure(); | |
} else { | |
// callbackが指定されていなければ全て解除 | |
if(typeof callback === 'undefined') { | |
this.callbacks[type] = []; | |
this.callbacksOnce[type] = []; | |
// callbackが指定されていたらそれだけ消去 | |
} else { | |
this.callbacks[type] = this.callbacks[type].filter(function(func) { | |
return func !== callback; | |
}); | |
this.callbacksOnce[type] = this.callbacksOnce[type].filter(function(func) { | |
return func !== callback; | |
}); | |
} | |
} | |
}, | |
/** | |
* イベントに設定されたコールバックを実行する | |
* @param string type 発火するイベント名 | |
* @param array args コールバックへ渡す引数の配列 | |
* @param any context コールバックにバインドするコンテキスト。省略されたらnullが指定される | |
* @return DOMObserver 自分自身のインスタンスを返す | |
*/ | |
trigger: function(type, args, context) { | |
context = context || null; | |
this.callbacks[type].concat(this.callbacksOnce[type]).forEach(function(func) { | |
func.apply(context, args); | |
}); | |
// onceで登録されているものは1度実行されたら消去 | |
this.callbacksOnce[type] = []; | |
}, | |
/** | |
* DOMの監視を解除し、全てのコールバックを解除する | |
* @return void | |
*/ | |
release: function() { | |
this.observer.disconnect(); | |
// NOTE: 明示的にGCに回収してもらう | |
this.observer = null; | |
this.callbacks = null; | |
this.callbacksOnce = null; | |
} | |
}; | |
return DOMObserver; | |
}()); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function(global) { | |
'use strict'; | |
// constants | |
var FAV_ROOM_NAME = 'ふぁぼ'; | |
var FAV_ROOM_DESCRIPTION = 'あなたのふぁぼ一覧です。'; | |
var LOCALSTORAGE_KEY = 'favChatFavRoomId'; | |
var favChat = new ChatworkExtension(); | |
// ルーム一覧の中に「ふぁぼ」というチャットがあるか | |
var favRoom = ChatworkExtension.findRoom({ name: FAV_ROOM_NAME }), | |
savedLocalStorage = localStorage.getItem(LOCALSTORAGE_KEY) !== null; | |
// TODO: ローカルストレージ周りの処理を簡素化 | |
// NOTE: ふぁぼルームの名前を変えても良いように、 | |
// 既にローカルストレージにふぁぼルームのIDが保存されていたら上書きしない | |
if(!savedLocalStorage) { | |
// ふぁぼルームがあるがローカルストレージに保存されていない場合保存 | |
if(favRoom !== null) { | |
localStorage.setItem(LOCALSTORAGE_KEY, favRoom.id); | |
// ふぁぼルームがなければ作成してローカルストレージに保存 | |
} else { | |
ChatworkExtension.createRoom(FAV_ROOM_NAME, FAV_ROOM_DESCRIPTION, [], { pinned: true, icon: 'heart' }); | |
// FIXME: createRoomにdeferred等入れて確実にルームIDを取る | |
setTimeout(function() { | |
ChatworkExtension.chatRooms.some(function(room) { | |
if(room.name === FAV_ROOM_NAME) { | |
favRoom = room; | |
return true; | |
} | |
}); | |
localStorage.setItem(LOCALSTORAGE_KEY, favRoom.id); | |
}, 1000); | |
} | |
} | |
// 各メッセージのメニューに「ふぁぼ」を追加し、クリックされた投稿をふぁぼチャットに投げつける | |
favChat.addMessageMenu('ふぁぼ', function(data) { | |
console.log('clicked', data); | |
}, 'icoFontAddBtn'); | |
}(this)); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var $timeline = $('#_timeLine'), | |
$chat = $('#_chatText'), | |
$send = $('#_sendButton'), | |
MY_ROOM_ID = 22166935; | |
function favorite(e) { | |
e.preventDefault(); | |
var $root = $(this).parents('._message'), | |
room_id = $root.data('rid'), | |
account_id = $root.find('._speaker img').data('aid'), | |
timestamp = +$root.find('._timeStamp').data('tm'), | |
body = $root.find('pre').text(); | |
body = '[qt][qtmeta aid=' + account_id + ' time=' + timestamp + ']' + body + '[/qt]'; | |
$('[data-rid=' + MY_ROOM_ID + ']').click(); | |
$chat.val(body); | |
$send.click(); | |
$('[data-rid=' + room_id + ']').click(); | |
} | |
$('#_roomListItems').find('li._roomLink').on('click', function() { | |
function appendFavButton() { | |
$timeline.find('._message').each(function() { | |
var $el = $(this); | |
$el.ready(function() { | |
$el.trigger('mouseenter'); | |
// mouseenterしてから要素を取得 | |
var $actions = $el.find('ul.actionNav'), | |
$favButton = $('<li></li>'); | |
$favButton.on('click', favorite); | |
$favButton.addClass('_cwABAction linkStatus'); | |
$favButton.append($('<span></span>').addClass('_showAreaText showAreatext').text('ふぁぼ')); | |
$actions.prepend($favButton); | |
console.log('追加したで'); | |
}); | |
}); | |
} | |
// FIXME: 40件とれてしまうチャットからチャットへ写った時に、リセット判定してくれず無限ループ。 | |
// Ajax読み込み完了を待つ | |
var num = $timeline.find('._message').size(), | |
reseted = false; | |
function wait() { | |
var currentNum = $timeline.find('._message').size(); | |
console.log(num, currentNum); | |
if(currentNum != 0 && num != currentNum) { | |
appendFavButton(); | |
} else { | |
setTimeout(wait, 50); | |
} | |
} | |
wait(); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment