Skip to content

Instantly share code, notes, and snippets.

@Leko
Last active August 29, 2015 14:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Leko/a286b79a986511127293 to your computer and use it in GitHub Desktop.
Save Leko/a286b79a986511127293 to your computer and use it in GitHub Desktop.
/**
* @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;
}());
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;
}());
(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));
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