守破離でいこう! -Let's go with SyuHaRi!-

2008/05/09

Gmail 2.0 でテンプレートを切り替える

GmailGmail Template Switch

僕は普段Gmailを使っていて、Greasemonkeyスクリプトの「Gmail Template Switch」を愛用しています。

複数のアカウントで使用したり、差出人によって署名・挨拶文を変えたい場合に非常に便利なGreasemonkeyスクリプトです。

Gmail Template Switch

大抵皆そうなんだろうけど、メールを書く場合決まった形があって、あいさつ文・本文・締め言葉・署名という順番で書いている。以前作ったスクリプト で、署名は差出人に応じて自動的に切り替わるようになったけど、あいさつ文や締め言葉は辞書に登録したりして、毎回入力してたわけです。会社で使っていることもあり、社内と社外で定型文が変わってくるので、辞書に登録した語句を忘れたりしてかなり不便だった。これはさすがに面倒なので、さらに Gmail を快適にすべく、テンプレートを切り替えられる Greasemonkey スクリプトを作ってみた。これで署名が複数あっても、定型文が複数あっても平気ですな。

Gmail にテンプレート切り替え機能を付けてみた - 記憶は削除の方向で

しかしながら、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);

ラベル: , , , ,

automatically translated by Google Translate Hack!

naoki 20:57 | 0 comments |
HaloScan: |

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!

naoki 21:57 | 0 comments |
HaloScan: |

2008/03/28

Rails と AIR で、付箋紙アプリ

Stickynotes

Rails の最新は 2.0.2 ですし、AIR は正式版の 1.0 が公開されました。
情報はある程度追ってはいるものの、やはり実際に試してみないとなかなか身につきません。

というわけで、以前から気になっていた、[Think IT] 第1回:付箋紙アプリケーションを作ろう!を参考に、Ruby on RailsとAIRによるデスクトップ付箋紙アプリケーションを作ってみました。

Adobe AIR のインストールはこちらから。

サンプル付箋紙アプリをお試しいただく場合は、こちらから。
ちなみに、このアプリはユーザ管理はしておりません。ので、大変ソーシャルな付箋アプリです(汗

Download Stickynotes 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でした。

dynamic textfield alpha problem - kirupaForum

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!

naoki 21:51 | 0 comments |
HaloScan: |