2008/05/09
Gmail 2.0 でテンプレートを切り替える

僕は普段Gmailを使っていて、Greasemonkeyスクリプトの「Gmail Template Switch」を愛用しています。
複数のアカウントで使用したり、差出人によって署名・挨拶文を変えたい場合に非常に便利なGreasemonkeyスクリプトです。
Gmail Template Switch大抵皆そうなんだろうけど、メールを書く場合決まった形があって、あいさつ文・本文・締め言葉・署名という順番で書いている。以前作ったスクリプト で、署名は差出人に応じて自動的に切り替わるようになったけど、あいさつ文や締め言葉は辞書に登録したりして、毎回入力してたわけです。会社で使っていることもあり、社内と社外で定型文が変わってくるので、辞書に登録した語句を忘れたりしてかなり不便だった。これはさすがに面倒なので、さらに Gmail を快適にすべく、テンプレートを切り替えられる Greasemonkey スクリプトを作ってみた。これで署名が複数あっても、定型文が複数あっても平気ですな。
しかしながら、Gmail Template Switch は、Gmail 2.0では動作しないため、動作が快速でラベルの色分け等便利な機能は我慢し、日本語版Gmailを使っていたわけですが、先日ついに、日本語版Gmailも 2.0になってしまいました。
というわけで、Gmail Template Switch を、Gmail 2.0 で動作するようにハックしてみました。
Gmail Template Switcher - v 2.0
既存のデータは生かしたかったので、コードはほとんど流用しています。なお、jQueryの使用で意味不明なエラーが出たので、使わないようにハックしてます。
また、Gmail 2.0は、Greasemonkeyを正式にサポートするらしく、GmailGreasemonkey10APIが公開されているので、せっかくなので使ってみました。
はじめは軽い気持ちでやってみたのですが、Greasemonkeyは初めて、かつ、Gmail 2.0 になってずいぶん変わっているようで、とても苦戦しました・・・(汗
とくに、ElementのIDがランダムで変わるのと、Reply時の入力フォームが動的に挿入される点でかなりはまりました・・。
なお、操作方法等はオリジナルとかわりません。もちろん新しい機能なんてありませんw
以下、コード全文です。
gmailtemplateswitcherv20.user.js// ==UserScript==
// @name Gmail Template Switcher - v 2.0
// @namespace http://www.r-stone.net/blogs/ishikawa
// @description Append the function to apply the mail template, when writing a mail. Modify source code of [Gmail Template Switch] from http://d.hatena.ne.jp/re_guzy by re_guzy
// @version 0.1.20080510.0
// @include http://mail.google.com/*
// @include https://mail.google.com/*
// @exclude http://mail.google.com/mail/help/*
// @exclude https://mail.google.com/mail/help/*
//
// Copyright (c) 2007-2008, re_guzy <goodspeed.xii@gmail.com>
// Distributed under the MIT license
// http://opensource.org/licenses/mit-license.php
// http://sourceforge.jp/projects/opensource/wiki/licenses%2FMIT_license
//
// Notice : To Uninstall this script, remove "gtssettings0-9" from Gmail contact list.
// Feature: When writing a mail, append a combobox to action. By selecting action,
// apply template to mail, or add template or remove template. Template
// is saved to "contact list" named starting with "gtssettings".
// Require: Greasemonkey 0.7.20080121.0
// ==/UserScript==
const DEBUG = false;
const KEY_TOKEN = "gts_token";
const KEY_CACHE = "gts_cache";
const CONTACT_NAME = "gtssettings";
const CONTACT_ID_RE = /\["\w+","(\w+)","gtssettings\d","gtssettings\d",/;
const MSGBODY_RE = /([\s\S]*)\n?(?:^---)(\n[\s\S]+)/m;
const LOCATION_RE = /(https?:\/\/[^\/]+\/(a\/[^\/]+\/)?).*/;
const SELECTOR = {
'gts' : 'descendant::*[local-name() = "select" or local-name() = "SELECT"][@id = "id_gts_template"]',
'input_form' : 'descendant::*[local-name() = "form" or local-name() = "FORM"]',
'body' : 'descendant::*[local-name() = "textarea" or local-name() = "TEXTAREA"][@name = "body"]',
'subject' : 'descendant::*[local-name() = "input" or local-name() = "INPUT"][@name = "subject"]',
'to' : 'descendant::*[local-name() = "textarea" or local-name() = "TEXTAREA"][@name = "to"]',
'from' : 'descendant::*[local-name() = "select" or local-name() = "SELECT"][@name = "from"] | descendant::*[local-name() = "input" or local-name() = "INPUT"][@name = "from"]',
'cc' : 'descendant::*[local-name() = "textarea" or local-name() = "TEXTAREA"][@name = "cc"]',
'bcc' : 'descendant::*[local-name() = "textarea" or local-name() = "TEXTAREA"][@name = "bcc"]',
'gts_undo' : 'descendant::*[contains(concat(" ",@class," "), " gts_undo_option ")]',
'gts_first' : 'descendant::*[contains(concat(" ",@class," "), " gts_option_first ")]',
'discard' : 'descendant::*[local-name() = "button" or local-name() = "BUTTON"][count(preceding-sibling::*[local-name() = "button" or local-name() = "BUTTON"]) = 2]',
'labels' : 'descendant::*[local-name() = "span" or local-name() = "SPAN"]',
'msg' : 'div/div/div/div/div/div/div/div/div/div/div[3]/div/div[2]/div[2]/div/div[2]/div[2]/div/table/tbody/tr[2]/td[2]'
}
var T = new Array(10);
T[0] = { 'id' : -1, 'num' : 0 };
for (var i=1;i < T.length;i++) {
T[i] = {
'id' : -1,
'num' : i,
'from' : '',
'to' : '',
'cc' : '',
'bcc' : '',
'subject' : '',
'body' : '',
'body_latter' : ''
}
}
var Ja = false;
var recentView;
//Initialize gmail and gmonkey objects
window.addEventListener('load', function() {
if (unsafeWindow.gmonkey) {
unsafeWindow.gmonkey.load('1.0', function(gmail) {
function getViewType() {
var str = '';
switch (gmail.getActiveViewType()) {
case 'tl': str = 'Threadlist'; break;
case 'cv': str = 'Conversation'; break;
case 'co': str = 'Compose'; break;
case 'ct': str = 'Contacts'; break;
case 's': str = 'Settings'; break;
default: str = 'Unknown';
}
return str;
}
function getView() {
return gmail.getActiveViewElement();
}
function getMailForm() {
var a = xpath(SELECTOR['input_form'], getView());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}
function getGts() {
var a = xpath(SELECTOR['gts'], getView());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}
function getGtsOptUndos() {
return a = xpath(SELECTOR['gts_undo'], getGts());
}
function getGtsOptFirst() {
var a = xpath(SELECTOR['gts_first'], getGts());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}
function getFrom(form) {
var a = xpath(SELECTOR['from'], form || getMailForm());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}
function getTo(form) {
var a = xpath(SELECTOR['to'], form || getMailForm());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}
function getCc(form) {
var a = xpath(SELECTOR['cc'], form || getMailForm());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}
function getBcc(form) {
var a = xpath(SELECTOR['bcc'], form || getMailForm());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}
function getSubject(form) {
var a = xpath(SELECTOR['subject'], form || getMailForm());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}
function getBody(form) {
var a = xpath(SELECTOR['body'], form || getMailForm());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}
function getDiscard() {
var a = xpath(SELECTOR['discard'], getMailForm().parentNode.parentNode);
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}
function getLabels(form) {
return a = xpath(SELECTOR['labels'], form || getMailForm());
}
function getCcLabel() {
var a = getLabels();
for (var i=0; i<a.length; i++) {
if (/Cc/.exec(a[i].innerHTML)) {
return a[i];
}
if (i>1) {
break;
}
}
return null;
}
function getBccLabel() {
var a = getLabels();
for (var i=0; i<a.length; i++) {
if (/Bcc/.exec(a[i].innerHTML)) {
return a[i];
}
if (i>1) {
break;
}
}
return null;
}
function getChangeLabel() {
var a = getLabels();
for (var i=0; i<a.length; i++) {
if (/change/.exec(a[i].innerHTML) || /変更/.exec(a[i].innerHTML)) {
return a[i];
}
if (i>1) {
break;
}
}
return null;
}
function switcher() {
str = getViewType();
if (str != "Compose" && str != "Conversation") {
if (recentView) {
recentView.removeEventListener('DOMNodeInserted', nodeInsertedHandler, false);
}
return;
}
recentView = getView();
window.setTimeout(function() {
initialize();
}, 600);
}
function nodeInsertedHandler(event) {
target = event.target;
if (target.nodeType == 1) {
tagName = target.tagName.toLowerCase();
if (tagName == 'form') {
log('form inserted');
window.setTimeout(function() {
initialize();
}, 500);
//getView().removeEventListener('DOMNodeInserted', nodeInsertedHandler, false);
} else if (tagName == 'table') {
log('table inserted');
window.setTimeout(function() {
initialize();
}, 500);
//getView().removeEventListener('DOMNodeInserted', nodeInsertedHandler, false);
}
}
}
function initialize() {
log('initialize');
try {
if (getGts()) {
log('already initialized');
return;
}
var form = getMailForm();
if (!form) {
log('form not found');
getView().addEventListener('DOMNodeInserted', nodeInsertedHandler, false);
return;
}
//getView().removeEventListener('DOMNodeInserted', nodeInsertedHandler, false);
var discard_button = getDiscard();
var label = discard_button.innerHTML;
Ja = (label == '破棄');
discard_button.parentNode.insertBefore(createSelectElement(), discard_button.nextSibling);
composeCommand(form);
} catch(e) {
log('add combobox failure because : ' + e);
return;
}
}
function createSelectElement() {
var content = document.createElement('select');
content.setAttribute("id", "id_gts_template");
content.setAttribute("style", "margin-left:10px;font-size:.8em;");
content.innerHTML = toOption('please wait...' , false , true);
content.addEventListener('change', function(event) {doCommand(event.target)}, true);
return content
}
function toOption(text, value, selected, cls, isDom) {
if (isDom) {
var attr = {
'style' : value ? null : 'color: rgb(119, 119, 119);',
'disabled' : value ? null : 'disabled',
'selected' : selected ? 'selected' : null,
'value' : value ? value : null,
'class' : cls ? cls : null
};
var elm = document.createElement('option');
for (var i in attr) {
if (attr[i]) {
elm.setAttribute(i, attr[i]);
}
}
elm.innerHTML = text;
return elm;
} else {
var attr = {
'style' : value ? null : '"color: rgb(119, 119, 119);"',
'disabled' : value ? null : '"disabled"',
'selected' : selected ? '"selected"' : null,
'value' : value ? '"' + value + '"' : null,
'class' : cls ? cls : null
};
var a = [];
for (var i in attr) {
if (attr[i]) {
a.push(i + "=" + attr[i]);
}
}
return "<option " + a.join(' ') + ">" + text + "</option>";
}
}
function composeCommand(form) {
getTemplates(function /*parseTemplate*/(notes, use_cache) {
for (var i in notes) {
if (use_cache) {
T[i] = notes[i];
} else {
var note = notes[i].note ? decode(notes[i].note, false) : "{}";
try {
T[notes[i].num] = eval(note);
T[notes[i].num].id = notes[i].id;
T[notes[i].num] = decode(T[notes[i].num], true);
} catch(e) {log("eval failed : " + e);}
}
}
recomposeSelectElement(form);
applyDefault(form);
if (notes.length == 0) {
save();
} else if (!use_cache) {
var caches = [];
for (var i in T) {
var encoded = encode(T[i], true);
if (encoded) {
caches.push(encoded.toSource());
}
}
GM_setValue(KEY_CACHE, "[" + caches.join(", ") + "]");
}
});
}
function applyDefault(form) {
var from = getFrom();
if (from) {
var fromvalue = from.value;
matched = grep(T, function(i) {
return (i.name + '').indexOf('#') == 0 && i.from == fromvalue;
});
if (matched.length > 0) {
applyTemplate(matched[0].num, form);
}
}
}
function recomposeSelectElement(form) {
var options = [];
options.push(toOption(trans("Template actions...") , "init" , true, "gts_option_first"));
var enables = grep(T, function(o) {return (o.name && o.num != 0);});
var expand = function(arrays, cmd) {
var hash = {};
for (var i in enables) {
hash[enables[i].from] = hash[enables[i].from] || [];
hash[enables[i].from].push(enables[i]);
}
for (var i in hash) {
arrays.push(toOption(' <' + (i || trans('No from')) + '>'));
for (var j in hash[i]) {
arrays.push(toOption(' ' + hash[i][j].name , cmd + '_' + hash[i][j].num));
}
}
};
var tmp = [
{'cmd':'apply', 'exp':trans("Apply"), 'func':expand},
{'cmd':'add','exp':trans("Append"), 'func':function(arrays, cmd) {
var used = grep(T , function(o) { return (o.name && o.num != 0) });
if (used.length < 9) {
arrays.push(toOption(' ' + trans("Includes from") , cmd));
arrays.push(toOption(' ' + trans("Excludes from") , cmd + '_ignore_from'));
} else {
arrays.push(toOption(' ' + trans('Quantity limit is 9')));
}
}},
{'cmd':'delete','exp':trans('Remove'), 'func':expand}
];
for (var i in tmp) {
if (tmp[i].func != expand || enables.length != 0) {
options.push(toOption('-------'));
options.push(toOption(trans('verbs', tmp[i].exp) + ':'));
tmp[i].func(options, tmp[i].cmd);
}
}
var gts = getGts();
gts.innerHTML = options.join('');
gts.value = 'init';
}
function doCommand(selectNode) {
var form = getMailForm();
if (form) {
if (selectNode.value == 'add') {
addTemplate(form, true);
} else if (selectNode.value == 'add_ignore_from') {
addTemplate(form, false);
} else if (selectNode.value.match(/apply_(\d+)/)) {
applyTemplate(RegExp.$1 , form);
} else if (selectNode.value.match(/delete_(\d+)/)) {
deleteTemplate(RegExp.$1 , form);
} else if (selectNode.value == 'undo') {
if (unsafeWindow.gts_undo) { unsafeWindow.gts_undo(); }
}
selectNode.value= 'init';
} else {
log('form not found');
}
}
function addTemplate(form, contain_from) {
var m = trans('Please input the template name.');
if (contain_from) {
m += '\n';
m += trans('If the name is started from "#", it becomes default of the corresponding "from".');
}
var name = window.prompt(m, "");
if (!name) {return;}
if (grep(T , function(o) { return (o.name == name) }).length > 0) {
alert(trans('The name already exists.'));
return;
}
var empties = grep(T , function(o) { return (o.name || o.num == 0) }, true);
var t = empties[0];
var to = getTo(form);
var b = getBody(form);
var c = getCc(form);
var bc = getBcc(form);
var f = getFrom(form);
var s = getSubject(form);
T[t.num] = {
'num' : t.num,
'id' : t.id,
'name' : name,
'from' : contain_from ? f.options[f.selectedIndex].value : "",
'to' : to.innerHTML,
'cc' : c.innerHTML,
'bcc' : bc.innerHTML,
'subject' : s.value,
'body' : b.innerHTML
};
if (MSGBODY_RE.exec(T[t.num].body)) {
T[t.num].body = RegExp.$1;
T[t.num].body_latter = RegExp.$2;
}
editContact(T[t.num], function() {
recomposeSelectElement(form);
msg(trans('appended', name));
});
}
function applyTemplate(num , form) {
if (typeof T[0]._init == 'undefined') {
T[0]._init = {};
var selectors = x('f,s,t,c,bc,bo');
for (var i in selectors) {
var j = xpath(selectors[i], form)[0];
T[0]._init[j.name] = trim(j.value);
}
}
var selectors = x('f,s,t,c,bc');
for (var i in selectors) {
var j = xpath(selectors[i], form)[0];
var tmp = T[0]._init[j.name];
if (j.name != 'from' || j.value != T[num][j.name]) {
if (j.type == 'textarea') {
var targetaddrs = tmp || "";
//既に含まれているものは追加しない
var notcontains = grep(T[num][j.name].split(','), function(i) {
if (trim(i).length == 0) { return; }//空白は無視
if (/([\w\.+-]+@[\w+-]+(\.[\w+-]+)+)/.exec(i)) {
return targetaddrs.indexOf(RegExp.$1) < 0;
} else {
return targetaddrs.indexOf(i) < 0;
}
});
if (tmp) { notcontains.unshift(tmp); }
j.value = notcontains.join(', ');
} else {
j.value = (j.name == 'subject' && tmp) ? tmp : T[num][j.name];
}
if (j.name == 'cc' && T[num][j.name]) {
var cc_label = getCcLabel();
if (cc_label) {
emulate_click(cc_label);
}
}
if (j.name == 'bcc' && T[num][j.name]) {
var bcc_label = getBccLabel();
if (bcc_label) {
emulate_click(bcc_label);
}
}
}
}
var change_label = getChangeLabel();
if (change_label) {
emulate_click(change_label);
}
var b = getBody(form);
b.value = grep([
T[num].body, T[0]._init.body, T[num].body_latter
], function(i) { return i; }).join("\n\n");
var selectors = x('bo,s,t');
for (var i in selectors) {
var j = xpath(selectors[i], form)[0];
if ((j.name == 'body') || !j.value) {
j.focus();
j.selectionStart = 0;
j.selectionEnd = 0;
}
}
var undos = [
toOption('-------', null, null, "gts_undo_option", true),
toOption(' ' + trans("Undo"), 'undo', null, "gts_undo_option", true)
];
var gts_undos = getGtsOptUndos();
for (var i=0; i<gts_undos.length; i++) {
gts_undos[i].parentNode.removeChild(gts_undos[i]);
}
var gts_first = getGtsOptFirst();
if (gts_first) {
gts_first.parentNode.insertBefore( undos[1], gts_first.nextSibling );
gts_first.parentNode.insertBefore( undos[0], gts_first.nextSibling );
}
msg(trans('applied', T[num].name), function() {
undo(form);
msg(trans("To apply template was canceled."));
}, true);
}
function undo(form) {
if (typeof T[0]._init != 'undefined') {
var selectors = x('f,t,c,bc,bo');
for (var i in selectors) {
var j = xpath(selectors[i], form)[0];
j.value = T[0]._init[j.name];
}
}
delete T[0]._init;
var gts_undos = getGtsOptUndos();
for (var i=0; i<gts_undos.length; i++) {
gts_undos[i].parentNode.removeChild(gts_undos[i]);
}
}
function emulate_click(target) {
if (target.dispatchEvent) {
var e = unsafeWindow.document.createEvent("MouseEvents");
e.initEvent("click", true, true);
target.dispatchEvent(e);
}
}
function deleteTemplate(num , form) {
var name = T[num].name;
if (confirm(trans('remove confirm', name)) != true) {
return;
}
T[num] = {'id' : T[num].id, 'num' : num};
editContact(T[num], function() {
recomposeSelectElement(form);
msg(trans("removed", name));
});
}
function getTemplates(f_parseTemplate) {
var queryUrl = 'mail/contacts/data/contacts?thumb=false&groups=false&show=ALL&psort=Name&max=300&out=js&rf=&jsx=true';
ajax(queryUrl, function(req){
contactPage = req.responseText.replace('while (true); ', '').replace(/&&&START&&&([^&&&]+)&&&END&&&/, "$1");
response = eval("(" + contactPage + ")");
if (response.Success) {
var contacts = response.Body.Contacts;
var notes = [];
for(i=0; i<contacts.length; i++) {
if (contacts[i].Name && /gtssettings(\d)/.exec(contacts[i].Name) ) {
num = RegExp.$1;
note = contacts[i].Notes;
id = contacts[i].Id;
notes.push({'num' : num, 'note' : note, 'id' : id});
}
}
var authtoken = response.Body.AuthToken.Value;
GM_setValue(KEY_TOKEN, authtoken);
f_parseTemplate(notes);
} else {
log("Contacts Request Failed: " + response.Errors[0].Text);
}
});
}
function encode(tmpl, by_escape) {
if (by_escape) {
var escaped = {};
//encodeURIだと「'」がエンコードされないので、escapeを使う
for (var i in tmpl) {
if (i.indexOf('_') != 0) {
escaped[i] = escape(tmpl[i]);
}
}
return escaped;
} else {
//連絡先は「"」で囲まれるため、JSONデータを表すのに「"」を使えない。
//連絡先から復元するときに使うデータを、REGEXでマッチさせるため「"」を「'」に置換しておく。
return tmpl.toSource().replace(/\"/g, "'");
}
}
function decode(tmpl, by_unescape) {
if (by_unescape) {
var unescaped = {};
for (var i in tmpl) {
unescaped[i] = unescape(tmpl[i]);
}
return unescaped;
} else {
//連絡先に格納するために、「'」に変換しておいた「"」を戻す
return tmpl.replace(/\\'/g, "\"");
}
}
function editContact(tmpl, f_completed) {
var authtoken = GM_getValue(KEY_TOKEN);
if (!authtoken) {
log('token not found');
return;
}
var escaped = encode(tmpl, true);
var post_data = param({
"token" : authtoken,
"tok" : authtoken,
"out" : "js",
"id" : tmpl.id,
"action" : "SET",
"Name" : CONTACT_NAME + tmpl.num,
"Emails.0.Address" : CONTACT_NAME + tmpl.num + "@gmail.com",
"Notes" : encode(escaped, false)
});
ajax("mail/contacts/update/contact", function(req) {
var response = eval("(" + req.responseText.replace('while (true); ', '').replace(/&&&START&&&([^&&&]+)&&&END&&&/, "$1") + ")");
if (response.Success) {
if (tmpl.id == -1) {
if (CONTACT_ID_RE.exec(req.responseText)) {
tmpl.id = RegExp.$1;
editContact(tmpl, f_completed);
}
} else {
if (tmpl.num != 0) {
save(f_completed);
} else {
f_completed();
}
}
} else {
log("Update Contact Request Failed: " + response.Errors[0].Text);
}
}, 'POST', {'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'}, post_data);
}
function log(message) {
if (unsafeWindow && unsafeWindow.console && DEBUG) {
unsafeWindow.console.log(message);
}
}
function getCookie(name) {
var re = new RegExp(name + "=([^;]+)");
var value = re.exec(document.cookie);
return (value != null) ? decodeURI(value[1]) : null;
}
function msg(message, f_clicked, is_undo) {
unsafeWindow.gts_undo = f_clicked;
var a = xpath(SELECTOR['msg'], getView().ownerDocument.body);
if (a && a.length > 0) {
var td = a[0];
var div = td.parentNode.parentNode.parentNode.parentNode;
div.style.visibility = "visible";
td.innerHTML = "GTS : " + message;
if (is_undo) {
var span = document.createElement('span');
span.setAttribute("id", "gts_und");
span.setAttribute("class", "lk");
span.innerHTML = trans('Undo link');
span.addEventListener('click', f_clicked, true);
td.appendChild(span);
}
window.setTimeout(function() {
div.style.visibility = "hidden";
}, 60000);
}
}
function save(f_saved) {
T[0]['num'] = 0;
T[0].date = new Date();
editContact(T[0], f_saved ? f_saved : function() {});
}
function ajax(request_path, f_load, get_or_post, headers, data) {
window.setTimeout(function() {
GM_xmlhttpRequest({
'method': get_or_post ? get_or_post : "GET",
'url': getBaseLocation() + request_path,
'data': data,
'headers': headers,
'onload': f_load,
'onerror': function(req) {
log("Request Failed in error code: " + req.status);
}
});
}, 0);
}
function getBaseLocation() {
if (LOCATION_RE.exec(document.location)) {//for Google Apps
return RegExp.$1;
} else {
return 'http://mail.google.com/';
}
}
function trans(msg_id, opt) {
return {
'Template actions...' : Ja ? 'テンプレートの操作...' : msg_id,
'Apply' : Ja ? '適用' : msg_id,
'Append' : Ja ? '追加' : msg_id,
'Includes from' : Ja ? '差出人を含む' : msg_id,
'Excludes from' : Ja ? '差出人を除く' : msg_id,
'Quantity limit is 9' : Ja ? '最大9個です' : msg_id,
'Remove' : Ja ? '削除' : msg_id,
'verbs' : Ja ? (opt + 'するテンプレート') : (opt + ' template'),
'Please input the template name.' : Ja ? 'テンプレート名を入力してください。' : msg_id,
'If the name is started from "#", it becomes default of the corresponding "from".' :
Ja ? '名前を「#」から始めると、対応する差出人のデフォルトになります。' : msg_id,
'The name already exists.' : Ja ? 'その名前は既に存在します。' : msg_id,
'appended' : Ja ? ("テンプレート「" + opt + "」を追加しました。")
: ('Template "' + opt + '" was appended.'),
'applied' : Ja ? ("テンプレート「"+opt+"」を適用しました。")
: ('Template "' + opt + '" was applied. '),
'remove confirm' : Ja ? ("テンプレート「" + opt + "」を削除しますか?")
: ('Is template "' + opt + '" removed?'),
'removed' : Ja ? ("テンプレート「" + opt + "」を削除しました。")
: ('Template "' + opt + '" was removed.'),
'To apply template was canceled.' : Ja ? "テンプレートの適用は取り消されました。" : msg_id,
'Undo' : Ja ? "適用の取り消し" : msg_id,
'Undo link' : Ja ? "適用取り消し" : "Undo applied",
'No from' : Ja ? "差出人なし" : msg_id
}[msg_id] || msg_id;
}
function x(prefix) {
var result = [];
for (var i in SELECTOR) {
if (typeof prefix != 'undefined') {
var a = grep(prefix.split(','), function(j) { return i.indexOf(j) == 0; });
if (a.length != 0) { result.push(SELECTOR[i]); }
} else {
result.push(SELECTOR[i]);
}
}
return result;
}
/*
* this 'grep' function from jquery-1.2.2.js
* jQuery 1.2.2 - New Wave Javascript
*/
function grep( elems, callback, inv ) {
// If a string is passed in for the function, make a function
// for it (a handy shortcut)
if ( typeof callback == "string" )
callback = eval("false||function(a,i){return " + callback + "}");
var ret = [];
// Go through the array, only saving the items
// that pass the validator function
for ( var i = 0, length = elems.length; i < length; i++ )
if ( !inv && callback( elems[ i ], i ) || inv && !callback( elems[ i ], i ) )
ret.push( elems[ i ] );
return ret;
}
/*
* this 'param' function from jquery-1.2.2.js
* jQuery 1.2.2 - New Wave Javascript
*/
function param(a) {
var s = [];
// Serialize the key/values
for ( var j in a )
// If the value is an array then the key names need to be repeated
s.push( encodeURIComponent(j) + "=" + encodeURIComponent( a[j] ) );
// Return the resulting serialization
return s.join("&").replace(/%20/g, "+");
}
/*
* this 'trim' function from jquery-1.2.2.js
* jQuery 1.2.2 - New Wave Javascript
*/
function trim(text) {
return (text || "").replace( /^\s+|\s+$/g, "" );
}
function xpath(query, target) {
var results = document.evaluate(query, target || document, null,
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
var nodes = [];
for (var i=0; i<results.snapshotLength; i++) {
nodes.push(results.snapshotItem(i));
}
return nodes;
}
gmail.registerViewChangeCallback(switcher);
switcher();
});
}
}, true);
ラベル: firefox, gmail, google, greasemonkey, javascript
automatically translated by Google Translate Hack!
2008/04/25
ホスト名とOpenID

Rails で OpenID を試してみようと思って思わぬところで躓いたのでメモです。
ホスト名に使用できる文字は、英数字文字(a-zA-Z0-9)および、ハイフン(-)となっています。
ホスト名についてホスト名(hostname)は,[RFC1034]の3.及び[RFC1123]の2.1 で示される形式をとる。すなわち,"."によって分離されたドメインラベルの列であって,各々のドメインラベルは,英数字文字(alphanum)で開始及び終了し,"-"文字を含んでもよい。完全限定ドメイン名の最も右にあるドメインラベルは,数字で始まってはならない。そのために,IPv4アドレスとは構文的に区別されるドメイン名になる。
Uniform Resource Identifiers (URI): Generic Syntax: Main(日本語)
テストのために自分のOpenIDを取得したのですが、これがまた、上で述べたルールに反したものを取得してしまいました・・。
取得したIDは、「ishikawa_rs.openid.ne.jp」なのですが、アンダーバーが入っています><
このIDをOpenIDのプラグインであるopen_id_authenticationに通すと、「ishikawa_rs.openid.ne.jp is not an OpenID URL」と言われてしまいます。
エラーが発生するところをirbで再現してみると下記のようになります。
irb(main):001:0> require 'uri'
=> true
irb(main):002:0> url = "ishikawa_rs.openid.ne.jp"
=> "ishikawa_rs.openid.ne.jp"
irb(main):003:0> uri = URI.parse(url.to_s.strip)
=> #
irb(main):004:0> uri = URI.parse("http://#{uri}") unless uri.scheme
URI::InvalidURIError: the scheme http does not accept registry part: ishikawa_rs
.openid.ne.jp (or bad hostname?)
from c:/ruby/lib/ruby/1.8/uri/generic.rb:195:in `initialize'
from c:/ruby/lib/ruby/1.8/uri/http.rb:78:in `initialize'
from c:/ruby/lib/ruby/1.8/uri/common.rb:488:in `new'
from c:/ruby/lib/ruby/1.8/uri/common.rb:488:in `parse'
from (irb):4
irb(main):005:0>
bad hostnameです。
そもそも、なぜこのIDになったかというと、「ishikawa.rs」にしようとしたところ、「ドット(.)はだめです。アンダーバー(_)は使えるよ。」と言われたからでした・・。
OpenIDを取得する際、ユーザIDがホスト名になる場合はご注意ください。
※そして、困ったことにopenid.ne.jpではアカウントの削除ができないようです。
automatically translated by Google Translate Hack!
2008/03/28
Rails と AIR で、付箋紙アプリ
Rails の最新は 2.0.2 ですし、AIR は正式版の 1.0 が公開されました。
情報はある程度追ってはいるものの、やはり実際に試してみないとなかなか身につきません。
というわけで、以前から気になっていた、[Think IT] 第1回:付箋紙アプリケーションを作ろう!を参考に、Ruby on RailsとAIRによるデスクトップ付箋紙アプリケーションを作ってみました。
Adobe AIR のインストールはこちらから。
サンプル付箋紙アプリをお試しいただく場合は、こちらから。
ちなみに、このアプリはユーザ管理はしておりません。ので、大変ソーシャルな付箋アプリです(汗
基本的な動作は、[Think IT] 第1回:付箋紙アプリケーションを作ろう!と、Ruby on RailsとAdobe AIRでデスクトップアプリを作る - Pokeal.COMをベースに、なんとなくタスクトレイアイコンも使ってみました。ついでに、常に前面表示も可能です。
Railsに関しては、実質、次の2行のみです。これでRestfulなバックエンドアプリのできあがりです・・。
$ ruby script/generate scaffold sticky body:text x:float y:float width:integer height:integer $ rake db:migrate
なお、Railsアプリは、先日紹介した、Herokuで構築しています。
AIRでは、透過するTextFieldを作るのにちょっと苦労しました。
ポイントは、blendModeをLAYERにしたSpriteでした。
I am not sure if this is quite what you are looking for but here is a solution I found for adjusting alpha on a TextField object.
// create an empty sprite var rect:Sprite = new Sprite(); rect.blendMode = BlendMode.LAYER; addChild(rect); var txtField:TextField = new TextField(); txtField.txtColor = 0x000000; txtField.alpha = .5; txtField.appendText("SOME TEXT"); rect.addChild(txtField);the key is to set the blendMode property on the sprite to LAYER.
以下、参考までにソースコードです。
Menu.mxml
<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" width="300" height="200" creationComplete="onCreationComplete();" closing="closing(event);">
<mx:Script>
<![CDATA[
import mx.core.BitmapAsset;
[Embed(source="icons/broken-16x16.png")]
private var icon16:Class;
private var stickies:Array = [];
private function create():void {
setStatus(">> Create new sticky");
var sticky:Sticky = new Sticky();
sticky.setAlwaysInFront(isAlwaysInFront.selected);
sticky.save();
sticky.show();
stickies.push(sticky);
}
private function onCreationComplete():void {
setStatus(">> Initializing...");
// is supports system tray icon
if (NativeApplication.supportsSystemTrayIcon) {
var images:Array = [];
images.push((new icon16() as BitmapAsset).bitmapData);
nativeApplication.icon.bitmaps = images;
var systemTrayIcon:SystemTrayIcon = (nativeApplication.icon as SystemTrayIcon);
systemTrayIcon.tooltip = "Stickynotes";
var nativeMenu:NativeMenu = new NativeMenu();
var menuItemNew:NativeMenuItem = new NativeMenuItem("New Stickynote");
menuItemNew.addEventListener(Event.SELECT, function(e:Event):void {
create();
});
var menuItemReload:NativeMenuItem = new NativeMenuItem("Reload Stickynotes");
menuItemReload.addEventListener(Event.SELECT, function(e:Event):void {
load();
});
var menuItemRestore:NativeMenuItem = new NativeMenuItem("Restore window");
menuItemRestore.addEventListener(Event.SELECT, function(e:Event):void {
restore();
});
var menuItemExit:NativeMenuItem = new NativeMenuItem("Exit");
menuItemExit.addEventListener(Event.SELECT, function(e:Event):void {
exit();
});
nativeMenu.addItem(menuItemNew);
nativeMenu.addItem(menuItemReload);
nativeMenu.addItem(menuItemRestore);
nativeMenu.addItem(menuItemExit);
systemTrayIcon.menu = nativeMenu;
}
load();
}
private function load():void {
setStatus(">> Loading from http://stickynotes.heroku.com ...");
closing(null);
var request:URLRequest = new URLRequest("http://stickynotes.heroku.com/stickies.xml");
request.method = 'GET';
var loader:URLLoader = new URLLoader();
loader.addEventListener(Event.COMPLETE, function(e:Event):void {
messages.text += e.target.data + "\n";
var xml:XML = new XML(e.target.data);
for each (var element:Object in xml.sticky) {
var sticky:Sticky = new Sticky();
sticky.id = element.id;
sticky.editor.text = element.body;
sticky.window.x = element.x * Capabilities.screenResolutionX;
sticky.window.y = element.y * Capabilities.screenResolutionY;
sticky.window.width = element.width;
sticky.window.height = element.height;
sticky.setAlwaysInFront(isAlwaysInFront.selected);
sticky.updateStatus();
sticky.show();
stickies.push(sticky);
}
clearStatus();
});
loader.load(request);
}
private function setStatus(s:String):void {
this.status = s;
}
private function clearStatus():void {
this.status = "";
}
private function closing(e:Event):void {
for each (var sticky:Sticky in stickies) {
if (sticky) {
sticky.window.close();
}
}
stickies = [];
}
private function saveAll():void {
setStatus(">> Save stickies");
for each(var sticky:Sticky in stickies) {
sticky.save();
}
}
private function onChangeHandle():void {
for each (var sticky:Sticky in stickies) {
sticky.setAlwaysInFront(isAlwaysInFront.selected);
}
}
]]>
</mx:Script>
<mx:Button left="10" top="10" label="New" id="new_btn" click="create();" />
<mx:Button left="60" top="10" label="Reload" id="load_btn" click="load();" />
<mx:Button right="10" top="10" label="Save" id="save_btn" click="saveAll();" />
<mx:CheckBox left="10" top="40" label="always in front" id="isAlwaysInFront" change="onChangeHandle();" />
<mx:TextArea right="10" top="70" left="10" bottom="10" id="messages" />
</mx:WindowedApplication>
Sticky.as
package {
import flash.text.*;
import flash.display.*;
import flash.events.*;
import flash.system.*;
import flash.net.*;
import mx.controls.*;
public class Sticky {
public var window:NativeWindow; // sticky window
public var editor:TextField; // sticky edit area
public var id:Number; // primary key
private var button:SimpleButton = new SimpleButton(); // cloase button
private var resizeHandle:SimpleButton = new SimpleButton(); // resize handle
private var x:Number; // sticky x
private var y:Number; // sticky y
private var height:Number; // sticky height
private var width:Number; // sticky width
private var body:String; // sticky body
private var sprite:Sprite = new Sprite();
private static var RESIZE_HANDLE_SIZE:int = 20;
/* create sticky window */
public function Sticky():void {
var initOptions:NativeWindowInitOptions = new NativeWindowInitOptions();
initOptions.systemChrome = NativeWindowSystemChrome.NONE;
initOptions.transparent = true;
initOptions.type = NativeWindowType.LIGHTWEIGHT;
window = new NativeWindow(initOptions);
window.alwaysInFront = false;
window.stage.align = StageAlign.TOP_LEFT;
window.stage.scaleMode = StageScaleMode.NO_SCALE;
// layer for transparent editor
sprite.blendMode = BlendMode.LAYER;
window.stage.addChild(sprite);
// for edit area
editor = new TextField();
editor.x = editor.y = 0;
editor.selectable = true;
editor.border = false;
editor.type = TextFieldType.INPUT;
editor.multiline = true;
editor.background = true;
editor.wordWrap = true;
editor.backgroundColor = 0xE6E082;
editor.alpha = 1;
sprite.addChild(editor);
// window position of center
window.x = 0.5 * Capabilities.screenResolutionX - 300 * 0.5
window.y = 0.5 * Capabilities.screenResolutionY - 100 * 0.5
// size for window and edit area
window.width = editor.width = 300;
window.height = editor.height = 100;
// resize for window and edit area
window.stage.addEventListener(Event.RESIZE, function(e:Event):void {
resized();
});
// move and resize
window.stage.addEventListener(MouseEvent.MOUSE_DOWN, function(e:MouseEvent):void {
var x:Number = e.stageX;
var y:Number = e.stageY;
if (y > window.height - RESIZE_HANDLE_SIZE && x > window.width - RESIZE_HANDLE_SIZE) {
//window.startResize(NativeWindowResize.BOTTOM_RIGHT);
} else {
window.startMove();
}
});
window.stage.addEventListener(MouseEvent.MOUSE_UP, function(e:MouseEvent):void {
if (isChanged()) {
save();
}
});
editor.addEventListener(FocusEvent.FOCUS_OUT, function(e:FocusEvent):void {
if (isChanged()) {
save();
}
});
} // end of function Sticky
// show window
public function show():void {
// create close button
button.x = window.width - 15;
button.y = 5;
button.upState = createBox(0xE6E082, 10, 0.3);
button.overState = createBox(0xE6E082, 10, 1);
button.downState = createBox(0xCCCCCC, 10, 1);
button.hitTestState = button.upState;
button.addEventListener(MouseEvent.CLICK, function(e:MouseEvent):void {
var request:URLRequest = new URLRequest("http://stickynotes.heroku.com/stickies/" + id + ".xml");
request.method = 'DELETE';
var loader:URLLoader = new URLLoader();
loader.load(request);
window.close();
});
window.stage.addChild(button);
// create resize handle
resizeHandle.x = window.width - 15;
resizeHandle.y = window.height - 15;
resizeHandle.upState = createResizeHandle(0xE6E082, 10, 0.3);
resizeHandle.overState = createResizeHandle(0xE6E082, 10, 1);
resizeHandle.downState = createResizeHandle(0xCCCCCC, 10, 1);
resizeHandle.hitTestState = button.upState;
resizeHandle.addEventListener(MouseEvent.MOUSE_DOWN, function(e:MouseEvent):void {
window.startResize(NativeWindowResize.BOTTOM_RIGHT);
});
window.stage.addChild(resizeHandle);
window.visible = true;
}
// call after window resized
public function resized():void {
editor.width = window.width;
editor.height = window.height;
button.x = window.width - 15;
button.y = 5;
resizeHandle.x = window.width - 15;
resizeHandle.y = window.height - 15;
}
// save sticky
public function save():void {
// create URL of sticky
var request:URLRequest
if (!id) {
request = new URLRequest("http://stickynotes.heroku.com/stickies.xml");
request.method = 'POST';
} else {
request = new URLRequest("http://stickynotes.heroku.com/stickies/" + id + ".xml");
request.method = 'PUT';
}
// create paramater for edit
var variables:URLVariables = new URLVariables();
variables['sticky[x]'] = window.x / Capabilities.screenResolutionX;
variables['sticky[y]'] = window.y / Capabilities.screenResolutionY;
variables['sticky[width]'] = window.width;
variables['sticky[height]'] = window.height;
variables['sticky[body]'] = editor.text;
request.data = variables;
// send
var loader:URLLoader = new URLLoader();
loader.addEventListener(Event.COMPLETE, function(e:Event):void {
var xml:XML = new XML(e.target.data);
if (!id) {
id = xml.id;
}
updateStatus();
});
loader.load(request);
}
public function setAlwaysInFront(value:Boolean):void {
window.alwaysInFront = value;
if (value) {
editor.alpha = 0.85;
} else {
editor.alpha = 1;
}
}
public function updateStatus():void {
// sticky status
x = window.x;
y = window.y;
width = window.width;
height = window.height;
body = editor.text;
}
/* private methods ***********/
// close button
private function createBox(color:uint, radius:Number, vis:Number):Shape {
var xShape:Shape = new Shape();
xShape.graphics.lineStyle(1, 0x000000);
xShape.graphics.beginFill(color);
xShape.graphics.drawRect(0, 0, radius, radius);
xShape.graphics.moveTo(0, radius);
xShape.graphics.lineTo(radius, 0);
xShape.graphics.moveTo(radius, radius);
xShape.graphics.lineTo(0, 0);
xShape.graphics.endFill();
xShape.alpha = vis;
return xShape;
}
private function createResizeHandle(color:uint, radius:Number, vis:Number):Shape {
var xShape:Shape = new Shape();
xShape.graphics.lineStyle(1, 0x000000);
xShape.graphics.beginFill(color);
xShape.graphics.moveTo(0, radius);
xShape.graphics.lineTo(radius, 0);
xShape.graphics.lineTo(radius, radius);
xShape.graphics.lineTo(0, radius);
xShape.graphics.endFill();
xShape.alpha = vis;
return xShape;
}
// is paramater changed
private function isChanged():Boolean {
return (x != window.x || y != window.y ||
width != window.width || height != window.height ||
body != editor.text);
}
} // end of class Sticky
} // end of package
automatically translated by Google Translate Hack!

