

if (typeof Hatena == 'undefined')
    var Hatena = {};

if (typeof Hatena.Bookmark == 'undefined')
    Hatena.Bookmark = {};

Hatena.Bookmark.isLocal = (location.port == '3000' && location.hostname.indexOf('hatena.ne.jp') != -1);
Hatena.Bookmark.debug = Hatena.Bookmark.isLocal || (location.search.indexOf('_debug=1') != -1);
Hatena.Bookmark.cookie = new Ten.Cookie;

var p = (function() {
    if (!Hatena.Bookmark.debug) {
        return  function() {};
    } else {
        var l = new Ten.Logger;
        return function() {
            l.info.call(l, Array.prototype.slice.call(arguments, 0, arguments.length));
        }
    }
})();

p.c = (function() {
    if (!Hatena.Bookmark.debug) {
        return  function() {};
    } else {
        return function() {
            var ary = Array.prototype.slice.call(arguments, 0, arguments.length);
            setTimeout( function() {
                   console.log(ary);
            }, 100);
        }
    }
})();

p.b = function(func, name) {
    name = 'Benchmark ' + (name || '') + ': ';
    var now = new Date * 1;
    func();
    var t = (new Date * 1) - now;
    p(name + t);
    return t;
}

p.e = function(arg) {
    if (!Hatena.Bookmark.debug) return;
    if (window.uneval) {
        p(window.uneval(arg));
    } else if (window.alert.toSource) {
        p(arg.toSource());
    } else {
        p(arg.toString());
    }
}

Ten.DOM.orig_getElementsByTagAndClassName = Ten.DOM.getElementsByTagAndClassName;
if (document.querySelectorAll) {
    Ten.DOM.getElementsByTagAndClassName = function(tag, klass, parent) {
        var selector = tag || '';
        if (klass) selector += '.' + klass;
        if (!tag && !klass) return [];
        try {
            return Ten.querySelectorAll(selector, parent);
        } catch(e) {
            p('query selector error:' + e + ' selector: '  + selector);
            return Ten.DOM.orig_getElementsByTagAndClassName(tag, klass, parent);
        }
    }
}


Ten.Browser.isSupportsXPath = !!document.evaluate;
Ten.Browser.isChrome = navigator.userAgent.indexOf('Chrome/') != -1;
Ten.Browser.isWebKit = Ten.Browser.isChrome || Ten.Browser.isSafari;
Ten.Browser.isSafari3 = Ten.Browser.isSafari && window.openDatabese && !window.postMessage; // d:id:os0x:20090226:1235616936
Ten.Browser.isFirefox = navigator.userAgent.indexOf('Firefox/') != -1;
Ten.Browser.isOSX = navigator.userAgent.indexOf('OS X ') != -1;
Ten.Browser.isIPhone = Ten.Browser.isSafari && (navigator.userAgent.indexOf('iPod;') != -1 || navigator.userAgent.indexOf('iPhone;') != -1);
Ten.Browser.isAndorid = Ten.Browser.isSafari && (navigator.userAgent.indexOf('Android ') != -1);
Ten.Browser.isTouch = Ten.Browser.isIPhone || Ten.Browser.isAndorid;

if (Ten.Browser.isIE && !window.XMLHttpRequest) {
    Ten.Browser.isIE6 = true;
} else {
    Ten.Browser.isIE6 = navigator.userAgent.indexOf('MSIE 6.0') != -1;
}

if (!Ten.Browser.isIE)
    Ten.JSONP.MaxBytes = 7000;

Ten.Event.onKeyDown = ((Ten.Browser.isFirefox && Ten.Browser.isOSX) || Ten.Browser.isOpera) ? 'onkeypress' : 'onkeydown';

Ten.Style.StyleSheet = new Ten.Class({
    initialize: function() {
        this.cssTexts = [];
        this._cache = [];
    },
    factory: function() {
        var klass;
        if (Ten.Browser.isIE) {
            klass = Ten.Style.StyleSheet.IE;
        } else if (Ten.Browser.isOpera) {
            klass = Ten.Style.StyleSheet.Opera;
        } else if (Ten.Browser.isSafari) {
            klass = Ten.Style.StyleSheet.Safari;
        } else {
            klass = Ten.Style.StyleSheet;
        }
        return new klass;
    }
}, {
    createCSSText: function(selector, text) {
        if (typeof text == 'object') {
            var res = [];
            for (var prop in text) {
                res.push('' + prop + ':' + text[prop] + ';');
            }
            text = res.join("\n");
        }
        return selector + ' { ' + text  + " } \n";
    },
    clear: function() {
        if (this.styleSheet) {
            var ss = this.styleSheet.sheet;
            ss.disabled = true;
            while (ss.cssRules.length) {
                ss.deleteRule(0);
            }
            ss.disabled = false;
        }
        while (this.cssTexts.length) {
            // XXX...
            var css = this.cssTexts.pop();
            //css.cssText = '';
            //this._cssTexts.push(css);
        }
    },
    addRule: function(selector, text) {
         var css = this.createCSSText(selector, text);
         this._cache.push(css);
    },
    applyRules: function() {
        if (!this.styleSheet) {
            var style = new Ten.Element('style', { type: 'text/css' });
            style.appendChild(document.createTextNode('')); // for safari
            document.getElementsByTagName('head')[0].appendChild(style);
            this.styleSheet = style;
        }

        var ss = this.styleSheet.sheet;
        ss.disabled = true;
        var css;
        while (css = this._cache.shift()) {
            p(css);
            ss.insertRule(css, ss.length);
            this.cssTexts.push(css);
        }
        ss.disabled = false;
    }
});

Ten.Style.StyleSheet.Safari = new Ten.Class({
    base: [Ten.Style.StyleSheet],
    initialize: function() {
        this.constructor.SUPER.call(this);
    }
}, {
    applyRules: function() {
        if (!this.styleSheet) {
            var style = new Ten.Element('style', { type: 'text/css' });
            style.appendChild(document.createTextNode('')); // for safari
            document.getElementsByTagName('head')[0].appendChild(style);
            this.styleSheet = style;
        }

        var ss = this.styleSheet.sheet;
        var css;
        var _tmp = '';
        while (css = this._cache.shift()) {
            // very slowly
            //ss.insertRule(css, ss.length);
            _tmp += css + "\n";
            this.cssTexts.push(css);
        }
        Ten.DOM.removeAllChildren(this.styleSheet);
        this.styleSheet.appendChild(document.createTextNode(_tmp));
    }
});

Ten.Style.StyleSheet.Opera = new Ten.Class({
    base: [Ten.Style.StyleSheet],
    initialize: function() {
        this.constructor.SUPER.call(this);
    }
}, {
    clear: function() {
        while (this.cssTexts.length) {
            var css = this.cssTexts.pop();
            Ten.DOM.removeAllChildren(css);
            if (css.parentNode) {
                css.parentNode.removeChild(css);
            }
        }
    },
    addRule: function(selector, text) {
        var style = new Ten.Element('style', { type: 'text/css' });
        var css = this.createCSSText(selector, text);
        style.appendChild(document.createTextNode(css));
        this._cache.push(style);
    },
    applyRules: function() {
        var style;
        while (style = this._cache.shift()) {
            this.cssTexts.push(style);
            document.getElementsByTagName('head')[0].appendChild(style);
        }
    }
});


Ten.Style.StyleSheet.IE = new Ten.Class({
    base: [Ten.Style.StyleSheet],
    initialize: function() {
        this.constructor.SUPER.call(this);
        this._cssText =  document.createStyleSheet();
        //this._cssTexts = [];
    }
}, {
    clear: function() {
        this._cssText.cssText = '';
        while (this.cssTexts.length) {
            // XXX...
            var css = this.cssTexts.pop();
            //css.cssText = '';
            //this._cssTexts.push(css);
        }
    },
    addRule: function(selector, text) {
         var css = this.createCSSText(selector, text);
         this._cache.push(css);
    },
    applyRules: function() {
        var style;
        var text = '';
        while (css = this._cache.shift()) {
            text += css;
            this.cssTexts.push(css);
        }
        this._cssText.cssText = text;
    }
});

Ten.DOM.someParentNode = function(el, func) {
    if (el.parentNode) {
        if (func(el.parentNode)) {
            return true;
        } else {
            return Ten.DOM.someParentNode(el.parentNode, func);
        }
    } else {
        return false;
    }
}

Ten.DOM.clearSelection = function() {
    if (window.getSelection) {
        window.getSelection().collapse(document.body, 0);
    } else if (document.getSelection) {
        document.getSelection().collapse(document.body, 0);
    } else {
        var selection = document.selection.createRange();
        selection.setEndPoint("EndToStart", selection);
        selection.select();
    }
}

Ten.Geometry.getMousePosition = function(pos) {
    // pos should have clientX, clientY same as mouse event
    if (!Ten.Browser.isChrome && (navigator.userAgent.indexOf('Safari') > -1) &&
        (navigator.userAgent.indexOf('Version/') < 0)) {
        return {
            x: pos.clientX,
            y: pos.clientY
        };
    } else {
        var scroll = Ten.Geometry.getScroll();
        return {
            x: pos.clientX + scroll.x,
            y: pos.clientY + scroll.y
        };
    }
}

/* Ten.Timer */
Ten.Timer = new Ten.Class({
    base: [Ten.EventDispatcher],
    initialize: function(intarval, repeatCount) {
        this.constructor.SUPER.call(this);
        this.currentCount = 0;
        this.intarval = intarval || 60; // ms
        this.repeatCount = repeatCount || 0;
    }
}, {
    start: function() {
        this.running = true;
        var self = this;
        setTimeout(function() {
            self.loop();
        }, self.intarval);
    },
    reset: function() {
        this.stop();
        this.currentCount = 0;
    },
    loop: function() {
        if (!this.running) return;
        this.currentCount++;
        if (this.repeatCount && this.currentCount >= this.repeatCount) {
            this.stop();
            this.dispatchEvent('timer');
            this.dispatchEvent('timerComplete');
            return;
        }
        var self = this;
        this.dispatchEvent('timer', this.currentCount);
        setTimeout(function() {
            self.loop();
        }, self.intarval);
    },
    stop: function() {
        this.running = false;
    }
});

/* Ten.IFrameMessenger */
if (typeof Ten.IFrameMessenger == 'undefined') {
Ten.IFrameMessenger = {};
Ten.IFrameMessenger.Base = new Ten.Class({
    initialize: function(interval) {
        Ten.EventDispatcher.implementEventDispatcher(this);
        this.queue = [];
        this.timer = new Ten.Timer(interval || 60);
        var self = this;
        this.timer.addEventListener('timer', function(repeat) {
            self.timerHandler(repeat);
        });
    }
}, {
    sendMessage: function(eventName, args/* args shoud be null or string or hash */) {
        if (this.canSendMessage()) {
            this._implSendMessage(eventName, args);
        } else {
            this.queue.push([eventName, args]);
        }
    },
    _implSendMessage: function(evemtName, args) {
    },
    timerHandler: function(repeatCount) {
        this.sendEvent();
    },
    sendEvent: function() {
        var rawMessage = this.getMessage();
        if (rawMessage) {
            var obj = this.unseriarize(rawMessage);
            this.dispatchEvent(obj.eventName, obj.args);
        }
    },
    unseriarize: function(raw) {
        var tmp = raw.split('?', 2);
        var res = {};
        res.eventName = tmp[0];
        if (tmp[1]) {
            if (tmp[1].indexOf('=') == -1) {
                res.args = decodeURIComponent(tmp[1]);
            } else {
                var ary = tmp[1].split('&');
                args = {};
                for (var i = 0;  i < ary.length; i++) {
                    var query = ary[i].split('=', 2);
                    if (query.length == 2)
                        args[decodeURIComponent(query[0])] = decodeURIComponent(query[1]);
                }
                res.args = args;
            }
        }
        return res;
    },
    seriarize: function(args) {
        if (!args) return '';

        if (typeof args == 'string') {
            return encodeURIComponent(args);
        } else {
            var res = [];
            for (var prop in args) {
                if (!args.hasOwnProperty(prop)) continue;
                res.push( encodeURIComponent(prop) + '=' + encodeURIComponent(args[prop]) );
            }
            return res.join('&');
        }
    },
    setScroll: function(pos) {
        var de = document.documentElement;
        var b = document.body;
        de.scrollLeft = b.scrollLeft = pos.x;
        de.scrollTop  = b.scrollTop  = pos.y;
    },
    canSendMessage: function() {
        /* return bool */
        return true;
    }
});

Ten.IFrameMessenger.Manager = new Ten.Class({
    base: [Ten.IFrameMessenger.Base],
    initialize: function(url, interval) {
        this.constructor.SUPER.call(this, interval);
        this.url = url;
        this.iframeId = '__iframe_messenger';
    },
    onLoad: function() {
        Ten.IFrameMessenger.Manager.dispatchEvent('onload');
        if (Ten.Browser.isSafari || Ten.Browser.isIE) {
            // XXX
            var pos = Ten._stash.lastPos;
            if (pos) {
                setTimeout(function() {
                    Ten.Geometry.setScroll(pos);
                }, 100);
            }
        }
    }
}, {
    _implSendMessage: function(eventName, args) {
        this.replaceURL(this.url + '#' + eventName + '?' + this.seriarize(args));
    },
    getMessage: function() {
        var tmp = location.href.split('#');
        var hash, otherHash;
        if (tmp.length >= 3) {
            hash = tmp.pop();
            tmp.shift();
            otherHash = tmp;
        } else {
            hash = tmp.pop();
        }
        var re = /^Message-/;
        if (hash && hash.length && re.test(hash)) {
            hash = hash.replace(re, '');
            var pos = Ten.Geometry.getScroll();
            if (otherHash) {
                location.replace(location.href.split("#")[0] + "#" + otherHash.join('#'));
            } else {
                location.replace(location.href.split("#")[0] + "#");
            }
            return hash;
        }
    },
    getIFrame: function() {
        return document.getElementById(this.iframeId);
    },
    replaceURL: function(url) {
        if (Ten.Browser.isSafari || Ten.Browser.isIE) var pos = Ten.Geometry.getScroll();
        this.iframe.contentWindow.location.replace(url);
        if (Ten.Browser.isSafari || Ten.Browser.isIE) {
            Ten._stash.lastPos = pos;
            Ten.Geometry.setScroll(pos);
        }
    },
    observe: function(parentContainer) {
        if (!this.iframe) {
            var div = document.createElement('div');
            div.innerHTML = "<iframe onload='Ten.IFrameMessenger.Manager.onLoad();' frameborder='0' id='" + this.iframeId + "' style=''></iframe>";
            (parentContainer || document.body).appendChild(div);
            this.iframe = document.getElementById(this.iframeId);
            this.replaceURL(this.url + '#');
        }
        this.timer.start();
    }
});
Ten.EventDispatcher.implementEventDispatcher(Ten.IFrameMessenger.Manager);

Ten.IFrameMessenger.Client = new Ten.Class({
    base: [Ten.IFrameMessenger.Base],
    initialize: function(interval) {
        this.constructor.SUPER.call(this, interval);
        var self = this;
        this.href = location.href;
    }
}, {
    _implSendMessage: function(eventName, args) {
        // hmm...
        //location.replace = this.href + '#' + (new Date).getTime();
        //window.name = 'hoge';//[eventName, args.toSource()].join("\n");
    },
    getMessage: function() {
        var hash = location.href.split('#')[1];
        if (hash && hash.length) {
            location.replace(location.href.split("#")[0] + "#");
            return hash;
        }
    },
    observe: function() {
        this.timer.start();
    }
});
/* end IFrameMessenger */

Hatena.DropDownSelector = new Ten.Class({
    initialize: function(options) {
        Ten.EventDispatcher.implementEventDispatcher(this);
        options = options || {};
        this.selectedKey = options.selectedKey || '';
        this.showEvent   = options.showEvent   || 'onclick';
        this.hideEvent   = options.hideEvent   || 'onclick';
        this.itemEvent   = options.itemEvent   || 'onclick';
        this.data        = options.data || [];
        if (options.button)   this.registButtonElement(options.button);
        if (options.onclick)  this.addOnClickListener(options.onclick);
        this.createElements();
    }
}, {
    registButtonElement: function(element) {
        if (!this.button && element) {
            this.button = element;
        }
        this.buttonClick = new Ten.Observer(this.button, this.showEvent, this, 'show');
    },
    addOnClickListener: function(meth) {
        this.addEventListener('select', meth);
    },
    createElements: function() {
        this.ul = Ten.Element('ul', {
            className: 'dropdown-list',
            style:{zIndex: 99999, position: 'absolute', display: 'none'}
        });
        for (var i = 0; i < this.data.length; i++) {
            var dropdownName = 'dropdown' + (this.data[i].key ? '-' + this.data[i].key : '') + (this.selectedKey == this.data[i].key ? ' selected' : '');
            var li = Ten.Element('li', {
                className: dropdownName,
                onclick: this.onSelect(this.data[i].key)},
                Ten.Element('a',{ href: '' }, this.data[i].name)
            );
            this.ul.appendChild(li);
            this._data[this.data[i].key] = li;
        }
    },
    show: function(e) {
        if (e && e.stop) e.stop();
        document.body.appendChild(this.ul);
        this.ul.style.display = 'block';
        this.setPosition();
        this.bodyClick   = new Ten.Observer(document.body, this.hideEvent, this, 'hide');
        this.onresize    = new Ten.Observer(window, 'onresize', this, 'setPosition');
        this.ulMouseOut  = new Ten.Observer(this.ul, 'onmouseover', this, 'clearHideTimer');
        this.ulMouseOver = new Ten.Observer(this.ul, 'onmouseout',  this, 'setHideTimer');

        this.buttonClick.stop();
        delete this.buttonClick;
        this.buttonClick = new Ten.Observer(this.button, this.showEvent, this, 'hide');
    },
    hide: function(e) {
        if (e && e.stop) e.stop();
        this.ul.style.display = 'none';
        this.bodyClick.stop();
        this.onresize.stop();
        this.ulMouseOut.stop();
        this.ulMouseOver.stop();
        delete this.bodyClick;
        delete this.onresize;
        delete this.ulMouseOut;
        delete this.ulMouseOver;

        this.buttonClick.stop();
        delete this.buttonClick;
        this.buttonClick = new Ten.Observer(this.button, this.showEvent, this, 'show');
        this.clearHideTimer();
    },
    setHideTimer: function(e) {
        var t = e.event.relatedTarget || e.event.toElement;
        do { if (t == this.ul) return; } while (t = t.parentNode);
        if (!this.hideTimer) {
            var self = this;
            this.hideTimer = setTimeout(
                function() {
                    self.hide();
                }
            , 1000);
        }
    },
    clearHideTimer: function(e) {
        if (this.hideTimer) {
            clearTimeout(this.hideTimer);
            delete this.hideTimer;
        }
    },
    setPosition: function() {
        var p = Ten.Geometry.getElementPosition(this.button);
        var x;
        if (p.x + this.ul.offsetWidth > Ten.Geometry.getWindowSize().w) {
            x = p.x - this.ul.offsetWidth + this.button.offsetWidth;
        }
        this.ul.style.top  = p.y + this.button.offsetHeight + 'px';
        this.ul.style.left = x + 'px';
    },
    onSelect: function(item) {
        var self = this;
        return function(e) {
            e.stop();
            self.dispatchEvent('select', item);
            var li = self.ul.getElementsByTagName('li');
            for (var i = 0; i < li.length; i++) {
                if (Ten.DOM.hasClassName(li[i], 'selected')) {
                    Ten.DOM.removeClassName(li[i], 'selected');
                }
            }
            if (self._data[item]) {
                Ten.DOM.addClassName(self._data[item], 'selected');
            }
        };
    },
    _data: {}
});
} //* end Ten.IFrameMessenger *//

Hatena.CSSChanger = new Ten.Class({
    cssFiles: { },
    registerFiles: function(files) {
        for(var i=0; i<files.length; i++) {
            if(files[i].key && files[i].src) {
                this.cssFiles[files[i].key] = files[i].src;
            }
        }
    },
    hideDropDown: function() {
        Hatena.Bookmark.AfterHeader.addEventListener('onload', function() {
            var el = document.getElementById('colorselecter');
            if (el)
                el.style.display = 'none';
        });
    },
    setFixedColor: function(name) {
        this._fixedColor = name;
        this.hideDropDown();
    },
    init: function(name) {
        if (typeof name == 'undefined')
            name = this.getColorName();

        if (this.cssFiles[name]) {
            var self = this;
            document.write('<link rel="stylesheet" href="' + this.cssFiles[name] + '" title="' + name + '" />');
            Ten.DOM.addEventListener( 'DOMContentLoaded',
                function() {
                    self.replaceDefault()
                }
            );
        }
    },
    change: function(name, noChangeCookie) {
        if (name && !(name in this.cssFiles))
            return;

        var found = false;
        var links = document.getElementsByTagName('link');
        for (var i = 0; i < links.length; i++) {
            if ((links[i].getAttribute('rel') || '').toLowerCase() != 'stylesheet')
                continue;
            var title = links[i].getAttribute('title');
            if (!title)
                continue;
            if (title == name) {
                found = true;
                links[i].disabled = false;
            } else {
                links[i].disabled = true;
            }
        }
        if (name && !found) {
            var link = Ten.Element('link', { href: this.cssFiles[name], title: name, rel: 'stylesheet' });
            document.getElementsByTagName('head')[0].appendChild(link);
            link.disabled = false;
        }

        this.changeImages(name);
        if (!noChangeCookie) this.setColorName(name);
        this.dispatchEvent('change', name);
    },
    changeImages: function(name) {
        var change = function (elems) {
            for (var i = 0; i < elems.length; i++) {
                if ((' ' + elems[i].className + ' ').indexOf(' csschanger ') > -1) {
                    elems[i].src = elems[i].src.replace(/(?:-..)?(\.\w+)$/, (name ? '-' + name : '') + '$1');
                }
            }
        }
        change(document.getElementsByTagName('img'));
        change(document.getElementsByTagName('input'));
        this.dispatchEvent('change-images', name);
    },
    replaceDefault: function () {
        var name = this.getColorName();
        if (this.cssFiles[name]) this.changeImages(name)
    },
    getColorName: function () {
        if (typeof this._fixedColor != 'undefined')
            return this._fixedColor;

        this.cookie = this.cookie || new Ten.Cookie;
        if(!this.cookie.has('_b_hatena_csschanger_name')) return null;
        return this.cookie.get('_b_hatena_csschanger_name');
    },
    setColorName: function (name) {
        this.cookie = this.cookie || new Ten.Cookie;
        this.cookie.set('_b_hatena_csschanger_name', name, { expires: '+10y', path: '/' });
    }
});
Ten.EventDispatcher.implementEventDispatcher(Hatena.CSSChanger);


/* DOM Level3 like EventDispatcher */
Hatena.Bookmark.EventDispatcher = new Ten.Class({
    initialize: function() {
        this._eventListeners = {};
    },
    implementEventDispatcher: function(obj) {
        if (obj._eventListeners) return;

        Ten.Class.inherit(obj, Hatena.Bookmark.EventDispatcher.prototype);
        obj._eventListeners = {};
    }
}, {
    _name: 'Hatena.Bookmark.EventDispatcher',
    hasEventListener: function(type) {
        if (!this._eventListeners) {
            this._eventListeners = {};
        }

        return (this._eventListeners[type] instanceof Array && this._eventListeners[type].length > 0);
    },
    oldAddEventListener: function(type, func) {
       var listener = function(e) {
           func.apply(e.target, e.args);
       }
       this.addEventListener(type, listener);
    },
    addEventListener: function(type, listener, useCapture, priority, useWeakReference) {
        if (!this.hasEventListener(type))
            this._eventListeners[type] = [];
        if (!priority)
            priority = 0;
        if (!useCapture)
            useCapture = false;

        var listeners = this._eventListeners[type];

        for (var i = 0;  i < listeners.length; i++) {
            if (listeners[i][0] == listener && listeners[i][1] == useCapture)
                return;
        }

        this._eventListeners[type].push([listener, useCapture, priority, useWeakReference]);

        // for safari sorting...
        var started = listeners[0][2];
        for (var i = 1;  i < listeners.length; i++) {
            if (started != listeners[i][2]) {
                break;
            }
            if (i == listeners.length - 1) {
                return;
            }
        }

        // sort by priority
        this._eventListeners[type] = this._eventListeners[type].sort(function(a, b) {
            if (a[2] > b[2]) {
                return -1;
            } else if (a[2] < b[2]) {
                return 1;
            } else {
                return 0;
            }
        });
    },

    removeEventListener: function(type, listener, useCapture) {
        if (!useCapture)
            useCapture = false;

        if (this.hasEventListener(type)) {
            var listeners = this._eventListeners[type];
            for (var i = 0;  i < listeners.length; i++) {
                if (listeners[i][0] == listener && listeners[i][1] == useCapture)
                    listeners.splice(i, 1);
            }
        }
    },
    E: function() {
        return new Hatena.Bookmark.Event3(arguments[0], arguments[1] || false, arguments[2] || false);
    },
    dispatchEvent: function(e) {
        if (typeof e == 'string') {
            var args = Array.prototype.slice.call(arguments, 1);
            e = new Hatena.Bookmark.Event3(e);
            e.args = args;
        } else if (typeof e.type == 'undefined') {
            // If e is String, Set event obeject.
            e = new Hatena.Bookmark.Event3(e, arguments[1] || false, arguments[2] || false);
        } else {
            e = e.clone();
        }
        //Hatena.Bookmark.EventDispatcher.watcher.dispatchEvent('throw_event', e.type, e);

        var type = e.type;
        if (!this.hasEventListener(type)) return false;

        e.target = this;
        e.currentTarget = this;

        var listeners = this._eventListeners[type];
        for (var i = 0;  i < listeners.length; i++) {
            if (listeners[i][1] == e.bubbles) {
                listeners[i][0].call(this, e);
                if (e._callStopPropagation || e._callStopImmediatePropagation) break;
            }
        }

        return !e._callPreventDefault;
    }
});
Hatena.Bookmark.EventDispatcher.watcher = new Hatena.Bookmark.EventDispatcher();

Hatena.Bookmark.Event3 = new Ten.Class({
    initialize: function(type, bubbles, cancelable) {
        this.type = type;
        this.bubbles = bubbles || false;
        this.cancelable = bubbles || false;
        this.target = null;
        this.currentTarget = null;
        this.eventPhase = null; // not implements...

        this._callStopPropagation = false;
        this._callStopImmediatePropagation = false;
        this._callPreventDefault = false;
    }
}, {
    clone: function() {
         var e = new Hatena.Bookmark.Event3(this.type, this.bubbles, this.cancelable);
         for (var prop in this) {
             if (!this.hasOwnProperty(prop)) continue;
             e[prop] = this[prop];
         }
         return e;
    },
    isDefaultPrevented :function() {
        return this._callPreventDefault;
    },
    preventDefault: function() {
        if (this.cancelable)
            this._callPreventDefault = true;
    },
    stopPropagation: function() {
        this._callStopPropagation = true;
    },
    stopImmediatePropagation: function() {
        this._callStomImmediatePropagation = true;
    }
});

Hatena.Bookmark.Class = function(klass, prototype) {
    klass = klass || { };
    //klass.base = [Hatena.Bookmark.EventDispatcher].concat(klass.base || []);
    klass.base = (klass.base || []).concat(Hatena.Bookmark.EventDispatcher);
    if (!prototype) prototype = {};
    prototype.lazy = function(func, time, args) {
        var self = this;
        setTimeout(function() {
            self[func].apply(self, args || []);
        }, time || 10);
    }
    return Ten.Class(klass, prototype);
};

Hatena.Bookmark.documentWrites = function (/* arys */) {
    var ary = Array.prototype.slice.apply(arguments);
    for (var i = 0;  i < ary.length; i++) {
        var src = ary[i];
        document.write(unescape("%3Cscript src='" + src + "' charset='utf-8' type='text/javascript'%3E%3C/script%3E"));
    }
}

Hatena.Bookmark.User = new Hatena.Bookmark.Class({
    _users: {},
    API_ENDPOINT: {
        user_info               : '/my.name',
        entry_favorites         : '/my.entry_favorites',
        fast_entry_data         : '/my.fast_entry_data',
        edit                    : 'add.edit.json',
        tags                    : 'tags.json',
        entry                   : 'entry.json',
        information_html        : 'partial.information',
        services_html           : 'partial.services',
        calendar_html           : 'partial.calendar',
        infoversion             : 'api.infoversion.json',
        close_calendar          : 'api.close_calendar.json',
        delete_bookmark         : 'api.delete_bookmark.json',
        interesting             : 'api.interesting.json',
        not_interesting         : 'api.not_interesting.json',
        ignore                  : 'api.ignore.json',
        unignore                : 'api.unignore.json',
        ignoring                : 'api.ignoring.json',
        unfollow                : 'api.unfollow.json',
        follow                  : 'api.follow.json',
        following               : 'api.following.json',
        follow_suggest_ignore   : 'api.follow_suggest_ignore.json',
        unfollow_suggest_ignore : 'api.unfollow_suggest_ignore.json',
        usertag                 : 'api.usertag.json',
        unusertag               : 'api.unusertag.json',
        analysis                : '/entry.analysis.json',
        user_config             : 'config',
        delete_relword          : 'api.delete_relword.json',
        delete_location         : 'api.delete_location.json',
        tutorial                : '/api/tutorial.unlock.json'
    },
    initialize: function(name, rks, options) {
        this.name = name;
        this.rks = rks;
        this.options = options || {};
        this.setURLs();
        Hatena.Bookmark.User._users[name] = this;
    },
    getUserNameByIcon: function(img) {
        var src = img.src;
        if (src) {
            var res = src.match(/\/[^\/]{2}\/([^\/]+)\/profile/);
            if (res) {
                return res[1];
            } else if (img.alt) {
                return img.alt;
            } else {
                return '';
            }
        } else {
            return '';
        }
    },
    getInstance: function(name) {
        if (!Hatena.Bookmark.User._users[name]) {
            new Hatena.Bookmark.User(name);
        }
        return Hatena.Bookmark.User._users[name];
    },
    createByIcon: function(img) {
        var name = Hatena.Bookmark.User.getUserNameByIcon(img);
        if (name) {
            return new Hatena.Bookmark.User(name);
        }
    }
}, {
    isAuthor: function() {
        if (location.pathname.indexOf(this.link) == 0) {
            return true;
        } else {
            return false;
        }
    },
    isAuthorAndPlususer: function() {
        return this.isAuthor() && this.plususer;
    },
    setURLs: function() {
        var name = this.name;
        var link = this.link = '/' + name + '/';
        this.setAPIs(link);
    },
    setAPIs: function(link) {
        var apis = this.constructor.API_ENDPOINT;
        for (var name in apis) {
            var u = apis[name];
            if (u.indexOf('/') == 0) {
                this[this.createAPIName(name)] = u;
            } else {
                this[this.createAPIName(name)] = link + u;
            }
        }
    },
    rksInput: function() {
        return Ten.Element('input', {type: 'hidden', name: 'rks', value: this.rks});
    },
    createAPIName: function(name) {
        return name + 'API';
    },
    getAPIURL : function(name) {
        var apis = this.constructor.API_ENDPOINT;
        return this[this.createAPIName(name)];
    },
    tagLink: function(tagName) {
        return this.link + encodeURIComponent(tagName) + '/';
    },
    getProfileIcon: function(isLarge) {
        return Hatena.Bookmark.ViewHelper.profileIcon(this.name, isLarge);
    },
    getUserLink: function(isLarge) {
        return new Ten.Element('a', { className: 'username', href: this.link }, this.name);
    },
    getUserLinkWithIcon: function(isLarge) {
        return new Ten.Element('a', { href: this.link }, this.getProfileIcon(isLarge));
    },
    loadUserInfo: function(callback) {
        if (this._userInfo) {
            callback && callback(this._userInfo);
        } else if (this._userInfoXHR) {
            callback && this._userInfoXHR.addEventListener('complete', Ten.Function.bind(function() {
                callback(this._userInfo);
            }, this));
        } else {
            this._userInfoXHR = this.xhr('user_info').onComplete(Ten.Function.bind(function(res) {
                this._userInfoXHR = null;
                this._userInfo = eval('(' + res.responseText + ')');
                callback && callback(this._userInfo);
            }, this));
            this._userInfoXHR.load();
        }
    },
    loadTags: function(callback) {
        if (this._tags) {
            callback(this._tags);
        } else {
            this.xhr('tags').onComplete(Ten.Function.bind(function(res) {
                this._tags = (eval('(' + res.responseText + ')')).tags;
                callback(this._tags);
            }, this)).load();
        }
    },
    loadInformationHTML: function(callback) {
        if (this._informationHTML) {
            callback(this._informationHTML);
        } else {
            this.xhr('information_html', 'GET', null).onComplete(Ten.Function.bind(function(res) {
                this.createInformationByText(res.responseText);
                callback(this._informationHTML);
            }, this)).load();
        }
    },
    loadServicesHTML: function(callback) {
        if (this._servicesHTML) {
            callback(this._servicesHTML);
        } else {
            this.xhr('services_html', 'GET', null).onComplete(Ten.Function.bind(function(res) {
                var div = document.createElement('div');
                div.innerHTML = res.responseText;
                this._servicesHTML = div;
                callback(this._servicesHTML);
            }, this)).load();
        }
    },
    createInformationByText: function(text) {
        var E = Ten.Element;
        var div = E('div', { className: 'information', id: 'infomation-' + this.name });
        div.innerHTML = text;
        var follow_link = Ten.DOM.getElementsByTagAndClassName('a', 'follow-link', div)[0];
        Hatena.Bookmark.FavoriteEdit.TextLink.init(follow_link, this.name);

        var ignore_link = Ten.DOM.getElementsByTagAndClassName('a', 'ignore-link', div)[0];
        Hatena.Bookmark.IgnoreEdit.TextLink.init(ignore_link, this.name);

        div.follow_link = follow_link;
        div.ignore_link = ignore_link;
        if (Ten.DOM.getElementsByTagAndClassName('a', 'remove-favorite', div)) {
            Ten.DOM.addClassName(div, 'favorite');
        }
        this._informationHTML = div;
    },
    xhr: function(apiname, method) {
        var endpoint = this.getAPIURL(apiname);
        if (endpoint)
            return new Hatena.Bookmark.XHR(endpoint, method || 'POST', this.rks); 
    }
});

Hatena.Bookmark.FragmentsCache= new Hatena.Bookmark.Class({
    initialize: function(options) {
        this.options = options;
    }
}, {
    load: function() {
        if (!this._loaded) {
            this._loaded = 'loading';
            this.targetElement = document.getElementById(this.options.html_id);
            this.targetElement.appendChild(Hatena.Bookmark.ViewHelper.loadingIcon(this.options.loading_message || 'ロード中…'));
            if (this.targetElement) {
                this.xhr = new Hatena.Bookmark.XHR(this.options.uri);
                this.xhr.timeout = 20 * 1000;
                this.xhr.onError(this, 'errorback');
                this.xhr.onComplete(this, 'completeHandler');
                this.xhr.load();
            }
        }
    },
    errorback: function() {
        if (window.console && window.console.error) {
            console.error('fail fragment XHR:' + this.options.uri);
        }
        // silent...
        Ten.DOM.removeElement(this.targetElement);
    },
    completeHandler: function(res) {
        var div = document.createElement('div');
        div.innerHTML = res.responseText;
        var frag = document.createDocumentFragment();
        for (var i = 0, len = div.childNodes.length; i < len; i++) {
            var node = div.childNodes[i].cloneNode(true);
            frag.appendChild(node);
        };

        Ten.DOM.insertBefore(frag, this.targetElement);
        Ten.DOM.removeElement(this.targetElement);
    }
});


Hatena.Bookmark.Element = new Hatena.Bookmark.Class({
    initialize: function(element) {
        if (!element.tagName)
            element = Ten.Element.apply(Ten.Element, Array.prototype.slice.call(arguments));
        this.element = element;
        this.observers = [];
    }
}, {
    init: function() {
        this.registerEventListeners();
    },
    destroy: function() {
        for (var i = 0;  i < this.observers.length; i++)
            this.observers[i].stop();
    },
    registerEventListeners: function() {
        var re = /(\w+)Handler$/;
        for (var prop in this) {
            if (prop.match(re)) {
                var name = RegExp.$1;
                this[name + 'Observer'] = new Ten.Observer(this.element, name, this, prop); 
            }
        }
    }
});


Hatena.Bookmark.ViewHelper = {
    tag: function(tagName, link) {
        return new Ten.Element('a', {
            className : 'user-tag',
            href      : link,
            rel       : 'tag'
        }, tagName);
    },
    profileIcon: function(name, isLarge) {
        var pre = name.substr(0, 2);
        var size = isLarge ? 32 : 16;
        var img = new Ten.Element('img', {
            src: 'http://www.st-hatena.com/users/' + pre + '/' + name + '/profile' + (isLarge ? '' : '_s') + '.gif',
            alt: name,
            title: name,
            width: size,
            height: size,
            className: 'profile-image'
        });
        return img;
    },
    loadingIcon: function(str) {
        return new Ten.Element('span', {className:'hatena-bookmark-loadingicon'}, new Ten.Element('img', {
            style: { margin: '0 5px 0 0' },
            src: '/images/loading.gif'
        }), str || '');
    },
    _prevSize: { w: 0, h: 0 },
    resizeHandler: function() {
        // IE で body 要素の大きさが変わったときに
        // 発生する resize イベントを無視
        var size = Ten.Geometry.getWindowSize();
        if (size.w === this._prevSize.w && size.h === this._prevSize.h) return;
        this._prevSize = size;
        this.dispatchEvent('resize');
    }
};
Ten.EventDispatcher.implementEventDispatcher(Hatena.Bookmark.ViewHelper);
new Ten.Observer(window, 'onresize', Hatena.Bookmark.ViewHelper, 'resizeHandler');

Hatena.Bookmark.StringHelper = {
    countCommentToBytes: function(comment) {
        var bytes = 0;
        for (var i = 0;  i < comment.length; i++) {
            bytes += comment.charCodeAt(i) <= 255 ? 1 : 3;
        }
        return bytes;
    },
    cutoffComment: function(str) {
        if (!str) str = '';
        var re = /\[([^\[\]]+)\]/g;
        var match;
        var tags = [];
        var lastIndex = 0;
        while ((match = re.exec(str))) {
            lastIndex += match[0].length;
            //if (lastIndex == re.lastIndex)
                tags.push(match[1]);
        }
        var comment = str.substring(lastIndex) || '';
        return [comment, tags];
    },
    randomString: function(len) {
        var str = '01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ';
        var sLen = str.length;
        var res = '';
        for (var i = 0; i < len; i++) {
            res += str.charAt(Math.floor(Math.random() * sLen));
        }
        return res;
    },
    entryURL: function(url) {
        return '/entry/' + url.replace('#', '%23');
    },
    truncate: function(str, size, suffix) {
        if (!size) size = 32;
        if (!suffix) suffix = '...';
        var b = 0;
        for (var i = 0;  i < str.length; i++) {
            b += str.charCodeAt(i) <= 255 ? 1 : 2;
            if (b > size) {
                return str.substr(0, i) + suffix;
            }
        }
        return str;
    },
    timestampToYMD: function(str) {
        // '2009051122122240' -> '2009/05/11'
        return str.substr(0,4) + '/' + str.substr(4,2) + '/' + str.substr(6,2);
    },
    ljust: function(str, len, char) {
        str = str.toString();
        char = char.toString();
        if (char.length == 0)
            char = '0';

        while (str.length < len) {
            str = char + str;
        }
        return str;
    },
    faviconURL: function(url) {
        return "http://favicon.hatena.ne.jp/?url=" + encodeURIComponent(url);
    },
    coolURL: function(url, len) {
        var u = url.
          replace(/^https?:\/\//, '').
          replace(/\.[^\/]+$/, '');
        u = Hatena.Bookmark.StringHelper.truncate(u, len || 40);
        return Hatena.Bookmark.StringHelper.escapeHTML(u);
    },
    highlightText: function(text, words) {
        // text はエスケープされた文字列じゃないときけんだよ
        if (typeof word == 'string')
            words = [words];

        for (var i = 0;  i < words.length; i++) {
            words[i] = words[i].replace(/[\[\]]/, '');
        }
        var re = new RegExp('(' + words.join('|') + ')', 'gi');
        return text.replace(re, function(m) { return '<span class="highlight">' + m + "</span>"; });
    },
    searchWords: function(word) {
        var words = word.split(/[\s　]+/);
        var res = [];
        for (var i = 0;  i < words.length; i++) {
            if (words[i].length) res.push(words[i]);
        }
        return res;
    },
    escapeHTML: function(str) {
        return str.replace(/&/g, '&amp;').
                   replace(/</g, '&lt;').
                   replace(/>/g, '&gt;');
    }
};

/*
 * Fixed Ten's Bug
 * - 空白な value だと post できないよ
 * - 値に配列を渡すことで同名のキーを複数持てるように
 */
Ten.XHR.makePostData = function(data) {
    var regexp = /%20/g;
    if (typeof data == 'string' || (data instanceof String)) {
        return data.replace(regexp, '+');
    }
    var pairs = [];
    for (var k in data) {
        if (typeof data[k] == 'undefined') continue;
        var prefix = encodeURIComponent(k).replace(regexp, '+') + '=';
        var values = Array.prototype.concat(data[k]);
        for (var i = 0; i < values.length; i++) {
            var pair = prefix + encodeURIComponent(values[i]).replace(regexp, '+');
            pairs.push(pair);
        }
    }
    return pairs.join('&');
};

Hatena.Bookmark.XHR = new Hatena.Bookmark.Class({
    canCrossDomainXHR: function() {
        return false;
        if (typeof Hatena.Bookmark.XHR._cdxhr == 'undefined') {
            if (window.XDomainRequest) {
                // XDomainRequest で setRequestHeader セットできないのでとりあえず
                Hatena.Bookmark.XHR._cdxhr = false;
            } else if (Ten.Browser.isChrome) {
                var ver = parseInt((/Chrome\/(\d+)/).exec(navigator.userAgent)[1]);
                if (ver >= 2) {
                    Hatena.Bookmark.XHR._cdxhr = true;
                } else {
                    Hatena.Bookmark.XHR._cdxhr = false;
                }
            } else if (window.XMLHttpRequest) {
                var xhr = new window.XMLHttpRequest;
                if (typeof xhr.withCredentials == 'boolean') {
                    Hatena.Bookmark.XHR._cdxhr = true;
                } else {
                    Hatena.Bookmark.XHR._cdxhr = false;
                }
            } else {
                Hatena.Bookmark.XHR._cdxhr = false;
            }
        }
        return Hatena.Bookmark.XHR._cdxhr;
    },
    getXDomainRequest: function() {
        if (Hatena.Bookmark.XHR.canCrossDomainXHR()) {
            return new (window.XDomainRequest ? window.XDomainRequest : window.XMLHttpRequest);
        } else {
            throw new Error('XDomainRequest not support browser');
        }
    },
    base: [Ten.XHR],
    initialize: function(url, method, rks) {
        this.constructor.SUPER.call(this);
        if (method) this.method = method;
        this.url = url;
        this.rks = rks;
        this.timeout = Ten.Browser.isTouch ? 30 * 1000 : 10 * 1000;
        this.retry = 0;
        this.retryWait = 1000;
        this.oldAddEventListener('error', Ten.Function.method(this, 'retryHandler'));
    }
}, {
    load: function(params) {
        var url = this.url;
        this._lastParams = params;

        if (typeof params == 'undefined')
            params = this.params;

        if (this.rks) {
            if (typeof params == 'undefined')
                params = {};
            params.rks = this.rks;
        }

        // IE で XHR の同一URI時に複数回 POST 時の挙動がおかしい...
        //if (this.ieFlag) {
        //    this.method = 'GET';
        //    params.__date = (new Date()).getTime();
        //}

        if (this.method == 'GET') {
            url += params ? '?' + Ten.XHR.makePostData(params) : '';
            params = '';
        }

        if (this.timeout) {
            if (this.timer) {
                this.timer.reset();
            } else {
                this.timer = new Ten.Timer(this.timeout, 1);
                this.timer.addEventListener('timer', Ten.Function.method(this, 'timerHandler'));
            }
            this.timer.start();
        }

        this.dispatchEvent('open');
        if (Ten.Browser.isIE6) {
           this.SUPER.load.call(this, url, params || '');
        } else {
           var req;
           if (this.crossdomain) {
               req = Hatena.Bookmark.XHR.getXDomainRequest();
           } else {
               req = Ten.XHR.getXMLHttpRequest();
           }
           this.request = req;

           var self = this;
           req.onreadystatechange = function() {
               self.stateChangeHandler.call(self, req);
           };
           var params = params ? Ten.XHR.makePostData(params) : null;

           req.open(this.method, url, true);
           if (this.crossdomain)
               req.withCredentials = true;

           if (this.method == 'POST' && req.setRequestHeader) {
               req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
           }
           req.send(params);
        }
    },
    retryHandler: function() {
        if (this.retry > 0) {
            this.retry--;
            this.dispatchEvent('retry');
            setTimeout(Ten.Function.bind(function() {
                if (this._lastParams) {
                    this.load(this._lastParams);
                } else {
                    this.load();
                }
            }, this), this.retryWait);
        } else {
            this.cancel();
            this.dispatchEvent('retryError', this.request);
        }
    },
    timerHandler: function() {
        this.cancel();
        this.dispatchEvent('error', this.request);
    },
    cancel: function() {
        if (this.timer) {
            this.timer.stop();
        }
        if (this.request) {
            this.request.abort();
            this.dispatchEvent('cancel', this.request);
        }
    },
    stateChangeHandler: function(req) {
        this.dispatchEvent('state_change');

        if (req.readyState == 4) {
            this.dispatchEvent('ready', req.status.toString());

            if (req.status >= 200 && req.status < 300) {
                this.dispatchEvent('complete', req);
                if (this.timer) this.timer.stop();
            } else if (req.status == 0) {
                // abort?
            } else {
                this.dispatchEvent('error', req);
            }
        }
    },
    onError: function(callback, method) {
        if (typeof(callback) == 'function' && typeof(method) == 'undefined') {
            //
        } else {
            callback = Ten.Function.method(callback, method);
        }
        this.oldAddEventListener('error', callback);
        return this;
    },
    onComplete: function(callback, method) {
        if (typeof(callback) == 'function' && typeof(method) == 'undefined') {
            //
        } else {
            callback = Ten.Function.method(callback, method);
        }
        this.oldAddEventListener('complete', callback);
        return this;
    }
});

Hatena.Bookmark.TagComplete = new Hatena.Bookmark.Class({
    initialize: function() {
        this.constructor.SUPER.call(this);
        this.tags = {};
        this.tagsKeys = [];
        this.tagsArray = [];
        this.inputedTags = [];
        this.suggestView = new Hatena.Bookmark.TagComplete.DropDownList(this);
    }
}, {
    addTags: function(tags) {
        for (var tag in tags) {
            if (tags[tag].count) {
                this.tagsArray.push([tag, parseInt(tags[tag].count), tags[tag].timestamp]);
                this.tags[tag] = tags[tag];
                this.tagsKeys.push(tag);
            }
        }
        this.tagsKeys.sort(function(a, b) {
            if (a.toUpperCase() > b.toUpperCase() ) {
                return 1;
            } else if (a.toUpperCase() < b.toUpperCase() ) {
                return -1;
            } else {
                return 0;
            }
        });
        //this.createSuffixArray();
    },
    createSuffixArray: function() {
        var text = [];
        for (var key in this.tags) {
            text.push(key);
        }
        text = text.join("\n");

        var saTmp = [];
        var len = text.length;
        var o, w;
        for (var i = 0;  i < len; i++) {
            o = new String(text.substr(i));
            o.i = i;
            saTmp.push(o);
        }
        saTmp.sort();
        var r = [];
        for (var i = 0;  i < len; i++) {
            r.push(saTmp[i].i);
        }
        this.suffixArray = saTmp;
        this.suffixArrayR = r;
    },
    //suffixArray: function(text) {
        // saTmp.sort(function(a, b) {
        //     _a = text.substr(a, 20);
        //     _b = text.substr(b, 20);
        //     if (_a > _b) {
        //         return 1;
        //     } else if (_a < _b) {
        //         return -1;
        //     } else {
        //         return 0;
        //     }
        // });
    //},
    sortByCount: function() {
        // tags と密結合は設計がわるい
        this.tagsArray.sort(function(a, b) {
            if (a[1] > b[1]) {
                return -1;
            } else if (a[1] < b[1]) {
                return 1;
            } else {
                return 0;
            }
        });
    },
    observeInput: function(element) {
        if (this.keydownObserver) return;
        this.targetInput = element;
        if (this.touchMode) {
        } else {
            this.keyupObserver = new Ten.Observer(element, 'onkeyup', this, 'keyupHandler');
            this.keydownObserver = new Ten.Observer(element, Ten.Event.onKeyDown, this, 'keydownHandler');
        }

        this.registerInputTags(element.value);
    },
    registerInputTags: function(str) {
        var lastInputTags = this.inputedTags.join('_');
        var r = Hatena.Bookmark.StringHelper.cutoffComment(str);
        var comment = r[0];
        this.inputedTags = r[1];
        this.commentByteCounter(comment);
        var nowInputTags = this.inputedTags.join('_');
        if (nowInputTags != lastInputTags) {
            this.dispatchEvent('update_input_tags', this.inputedTags);
            if (!this.touchMode)
                this.targetInput.focus();
        }
    },
    commentByteCounter: function(comment) {
        var bytes = Hatena.Bookmark.StringHelper.countCommentToBytes(comment);
        this.dispatchEvent('update_comment_byte', Math.ceil(bytes/3));
    },
    updateInputedTags: function() {
        this.registerInputTags(this.targetInput.value);
    },
    suggest: function() {
        this.updateInputedTags();

        if (this.getInputValue().indexOf('[') == -1) {
            this.finish();
            return;
        }

        var pos = this.getTargetWordPos();
        if (pos[0] == -1) {
            this.finish();
            return;
        }
        var val = this.getInputValue();
        var word = val.substring(pos[0], pos[1]);
        word = word.replace(/\]$/, '');
        this.suggestWord(word);
    },
    getTargetWordPos: function() {
        var val = this.getInputValue();
        var pos = this.getTextCaretPos();
        var firstPos = val.lastIndexOf('[', pos - 1);
        var lastPos = val.indexOf(']', firstPos);
        //p([pos, firstPos + 1, lastPos]);
        //if (firstPos <= 1) {
            var io = val.indexOf('[', firstPos + 1) - 1;
            if (io > 0) lastPos = Math.min(lastPos, io);
        //}
        if (lastPos == -1) {
            // lastPos = val.length;
            lastPos = pos;
        } else {
            lastPos += 1;
        }
        p(['tw', pos, firstPos + 1, lastPos, val.lastIndexOf(']')].toString());
        if (pos > lastPos
            || (val.substr(pos -1 , 1) == ']')
            || (val.substr(pos, 1) == '[')
            ) {
            return [-1, -1];
        } else {
            p([lastPos, val.length]);
            return [firstPos + 1, lastPos];
        }
    },
    suggestWord: function(word) {
        if (word && word.indexOf('[') == -1) {
            if (word == this.lastWord) return;
            this.lastWord = word;
            this.currentWords = this.createWords(word, 10);
            if (!this.hasCaret())
                this.caret = -1;
            p('suggest words', word);
            this.dispatchEvent('suggest', this.currentWords);
            if (word.indexOf(']') >= 0) {
                this.finish();
            }
        } else {
            this.finish();
        }
    },
    hasCaret: function() {
        return ('caret' in this);
    },
    createWords: function(word, limit) {
        var words = [];
        var w = word.toUpperCase();

        var spaceMatched = function(tKey, index, ws, first) {
            if (ws.length == 0) return true;
            var i;
            if (((i = tKey.indexOf(ws.shift(), index)) >= 0) && !(first && i != 0)) {
                return spaceMatched(tKey, i+2, ws, false);
            }
            return false;
        }

        var sep = w.split(/\s+/);
        var ws = [];
        for (var i = 0;  i < sep.length; i++) {
            if (sep[i]) ws.push(sep[i]);
        }

        for (var i = 0, len = this.tagsKeys.length;  i < len; i++) {
            var tKey = this.tagsKeys[i];

            if (spaceMatched(tKey.toUpperCase(), 0, Array.prototype.slice.apply(ws), true)) {
                words.push({
                    name: tKey,
                    count: this.tags[tKey].count
                });
            }
            if (words.length >= limit) break;
        }
        return words;
    },
    getTextCaretPos: function() {
        var element = this.targetInput;
        var add = this.oneMode() ? 1 : 0;
        if (typeof element.selectionStart === 'number') {
            return element.selectionStart + add;
        }
        if (document.selection) {
            var range = document.selection.createRange();
            var textRange;
            if (element.nodeName.toLowerCase() === 'input') {
                textRange = element.createTextRange();
            } else {
                textRange = document.body.createTextRange();
                textRange.moveToElementText(element);
            }
            textRange.setEndPoint('EndToStart', range);
            return textRange.text.length + add;
        }
        return 0;
    },
    setTextCaretPos: function(pos) {
        var element = this.targetInput;
        if (element.setSelectionRange) {
            element.setSelectionRange(pos, pos);
        } else if (element.createTextRange) {
            var range = element.createTextRange();
            range.move('character', pos);
            range.select();
        }
    },
    updateCaret: function(pos) {
        if (this.hasCaret()) {
            if (this.caret != pos) {
                this.caret = pos;
                this.dispatchEvent('update', this.caret);
            }
        }
    },
    caretNext: function() {
        if (this.hasCaret()) {
            var caret = this.caret;
            caret++;
            if (caret >= this.currentWords.length) caret = 0;
            this.updateCaret(caret);
        }
    },
    caretPrev: function() {
        if (this.hasCaret()) {
            var caret = this.caret;
            caret--;
            if (caret < 0) caret = this.currentWords.length - 1;
            this.updateCaret(caret);
        }
    },
    getInputValue: function() {
        var value = this.targetInput.value;
        if (this.oneMode())
            return '[' + value;
        return value;
    },
    oneMode: function() {
        return this.mode == 'onetag';
    },
    appendTag: function(tagName) {
        var val = this.targetInput.value;
        var lastIndex = val.lastIndexOf(']');
        if (lastIndex == -1) {
            this.targetInput.value = '[' + tagName + ']' + val;
        } else {
            var prefix = val.substring(0, lastIndex + 1);
            var suffix = val.substr(lastIndex + 1);
            this.targetInput.value = prefix + '[' + tagName + ']' + suffix;
        }
        this.updateInputedTags();
        this.setTextCaretPos(lastIndex + tagName.length + 3);
    },
    removeTag: function(tagName) {
        var val = this.targetInput.value;
        this.targetInput.value = val.replace(new RegExp('\\[' + tagName.replace(/([\|\-\^\$\+\*\(\)\{\}])/g, "\\$1") + '\\]', 'gi'), '');
        this.updateInputedTags();
        this.setTextCaretPos(this.targetInput.value.length);
    },
    complete: function() {
        var val = this.getInputValue();
        var pos = this.getTargetWordPos();
        if (this.hasCaret() ) {
            word = this.currentWords[this.caret] || this.currentWords[0];
            if (!word) {
                if (this.oneMode()) delete this.caret;
                return;
            }

            word = word.name;
            var prefix = val.substring(0, pos[0]);
            var suffix = val.substr(pos[1]);
            p('complete', word, prefix, suffix);
            var newValue = prefix + word + ']' + suffix;
            if (this.oneMode())
                newValue = newValue.replace(/\[/g, '').replace(/\]/g, '');
            if (Ten.Browser.isFirefox && Ten.Browser.isOSX) {
                // OSX FIREFOX + IME is LOVEEEEEEEEEEEEEEEEEEEEEEE
                // for osx Firefox, thx id:yksk
                setTimeout(Ten.Function.bind(function() {
                    this.targetInput.value = newValue;
                    this.targetInput.select();
                    this.setTextCaretPos((prefix + word).length + 1);
                    this.finish();
                    this.dispatchEvent('complete');
                    this.targetInput.focus();
                    if (this.oneMode()) delete this.caret;
                }, this), 1);
            } else {
                this.targetInput.value = newValue;
                this.targetInput.select();
                this.setTextCaretPos((prefix + word).length + 1);
                this.finish();
                this.dispatchEvent('complete');
                this.targetInput.focus();
                    if (this.oneMode()) delete this.caret;
            }
        }
    },
    finish: function() {
        delete this.caret;
        delete this.currentWords;
        delete this.lastWord;
        this.dispatchEvent('suggest_hide');
    },
    keydownHandler: function(e) {
        //if (this.hasCaret() && (e.isKey('enter') || e.isKey('tab'))) {
        if (this.hasCaret()) {
            var keyCode = e.event.keyCode;
            switch( keyCode ) {
                case 13: // enter
                    this.complete();
                    e.stop();
                    break;
                case 9: // tab
                    if (this.currentWords.length == 1) {
                        this.complete();
                    } else if (e.shiftKey) {
                        this.caretPrev();
                    } else {
                        this.caretNext();
                    }
                    if ( !this.lastWord && this.oneMode() ) {
                        //
                    } else if( this.lastWord && this.lastWord.indexOf(']') >= 0 ) { // XXX...
                        //
                    } else {
                        e.stop();
                    }
                    if (Ten.Browser.isOpera) {
                        setTimeout(function() {
                            e.target.focus();
                        }, 10);
                    }
                    break;
                case 38: // cursor up
                    this.caretPrev();
                    e.stop();
                    break;
                case 40: // cursor down
                    this.caretNext();
                    e.stop();
                    break;
            }
        }
    },
    keyupHandler: function(e) {
        if (!e.isKey('enter'))
            this.suggest();
    }
});

Hatena.Bookmark.TagComplete.FlatList = new Hatena.Bookmark.Class({
    initialize: function(tagComplete, tagList) {
        this.constructor.SUPER.call(this);
        this.tagComplete = tagComplete;
        this.list = tagList;
        this.applyTagCompleteInput();
        this.registerEventListeners(tagComplete);
    }
}, {
    applyTagCompleteInput: function() {
        if (this.tagComplete.inputedTags) this.update(this.tagComplete.inputedTags);
    },
    registerEventListeners: function(target) {
        var self = this;
        new Ten.Observer(this.list, 'onclick', this, 'listClickHandler');
        target.oldAddEventListener('update_input_tags', function(tags) { self.update(tags); });
    },
    listClickHandler: function(e) {
        if (Ten.DOM.hasClassName(e.target, 'tag')) {
            var t = e.target;
            if (Ten.DOM.hasClassName(t, 'tag-selected')) {
                this.tagComplete.removeTag(Ten.DOM.scrapeText(t));
            } else {
                this.tagComplete.appendTag(Ten.DOM.scrapeText(t));
            }
        }
    },
    update: function(tags) {
        var tTags = this.list.getElementsByTagName('span');
        for (var i = 0;  i < tTags.length; i++) {
            var tag = tTags[i];
            var flag = false;
            for (var j = 0;  j < tags.length; j++) {
                if (Ten.DOM.scrapeText(tag).toUpperCase() == tags[j].toUpperCase()) {
                    flag = true;
                    break;
                }
            }
            if (flag) {
                Ten.DOM.addClassName(tag, 'tag-selected');
            } else {
                Ten.DOM.removeClassName(tag, 'tag-selected');
            }
        }
    }
});

Hatena.Bookmark.TagComplete.DropDownList = new Hatena.Bookmark.Class({
    initialize: function(tagComplete) {
        this.constructor.SUPER.call(this);
        this.tagComplete = tagComplete;
        this.registerEventListeners(tagComplete);
    }
}, {
    registerEventListeners: function(target) {
        var self = this;
        target.oldAddEventListener('suggest', function(words) {
            self.suggestWord(words);
        });
        target.oldAddEventListener('suggest_hide', function() { self.hide(); });
        target.oldAddEventListener('update', function(pos) { self.update(pos); });
    },
    suggestWord: function(words) {
        if (words.length) {
            this.showSuggest(words);
        } else {
            this.hide();
        }
    },
    show: function() {
        if (this.list) this.list.style.display = 'block';
    },
    hide: function() {
        if (this.list) this.list.style.display = 'none';
    },
    update: function(pos) {
        var list = this.list;
        if (!list) return;

        for (var i = 0;  i < list.childNodes.length; i++) {
            var li = list.childNodes[i];
            if (pos == i) {
                li.className = 'hatena-bookmark-suggest-curret';
            } else {
                li.className = '';
            }
        }
    },
    showSuggest: function(words) {
        this.createList();
        this.show();
        this.updatePosition();
        this.replaceList(words);
    },
    clearList: function() {
        Ten.DOM.removeAllChildren(this.list);
    },
    replaceList: function(words) {
        this.clearList();
        var E = Ten.Element;
        for (var i = 0;  i < words.length; i++) {
            var word = words[i];
            var li = E('li', {},
                E('span', {className: 'hatena-bookmark-suggest-count'}, word.count),
                word.name
            );
            this.list.appendChild(li);
        }
    },
    createList: function() {
        if (!this.list) {
            this.list = Ten.Element('ul', {className: 'hatena-bookmark-tag-list'});
            document.body.appendChild(this.list);
            this.mouseoverObserver = new Ten.Observer(this.list, 'onmouseover', this, 'mouseoverHandler');
            this.mousedownObserver = new Ten.Observer(this.list, 'onmousedown', this, 'mousedownHandler');
            this.keydownObserver = new Ten.Observer(this.list, 'onkeydown', this.tagComplete, 'keydownHandler');
        }
        return this.list;
    },
    mousedownHandler: function(e) {
        this.tagComplete.complete();
    },
    mouseoverHandler: function(e) {
        var target = e.target;
        var list = this.list;
        for (var i = 0;  i < list.childNodes.length; i++) {
            var li = list.childNodes[i];
             if (li == target || li == target.parentNode) {
                this.tagComplete.updateCaret(i);
            }
        }
    },
    updatePosition: function() {
        var list = this.list;
        var target = this.tagComplete.targetInput;
        var pos = Ten.Geometry.getElementPosition(target);
        if (target.type === 'textarea') {
            if (document.selection) {
                this.updatePositionByTextRange(list, target, pos);
            } else {
                this.updatePositionByDummyNodes(list, target, pos);
            }
        } else {
            list.style.top = pos.y + target.offsetHeight + 'px';
            list.style.left = pos.x + 'px';
        }
    },
    updatePositionByTextRange: function(list, target, pos) {
        var range = target.createTextRange();
        var caretPos = this.tagComplete.getTextCaretPos();
        var index = target.value.lastIndexOf('[', caretPos) + 1;
        range.move('character', index);
        var scroll = Ten.Geometry.getScroll();
        // Element#offsetLeft/Top が文書辺基準なのに対し、
        // TextRange#offsetLeft/Top は表示域基準のようので、
        // スクロールした分を足す必要がある。
        list.style.left = scroll.x + range.offsetLeft - 4 + 'px';
        list.style.top = scroll.y + range.offsetTop + range.boundingHeight + 2 + 'px';
    },
    updatePositionByDummyNodes: function(list, target, pos) {
        var dummies = this._dummyNodes;
        if (!dummies) {
            dummies = {
                box: Ten.Element('div'),
                caret: Ten.Element('span'),
                caretText: document.createTextNode(''),
                preText: document.createTextNode(''),
                postText: document.createTextNode('')
            };
            var boxStyle = dummies.box.style;
            var inputStyle = getComputedStyle(target, null);
            boxStyle.cssText = 'position: absolute; left: 0; top: 0; white-space: -moz-pre-wrap; white-space: pre-wrap; visibility: hidden;';
            var props = 'borderTopStyle borderTopWidth borderRightStyle borderRightWidth borderBottomStyle borderBottomWidth borderLeftStyle borderLeftWidth paddingTop paddingRight paddingBottom paddingLeft fontSize fontFamily lineHeight'.split(' ');
            for (var i = 0; i < props.length; i++)
                boxStyle[props[i]] = inputStyle[props[i]];
            dummies.widthDiff = parseFloat(inputStyle.paddingLeft) + parseFloat(inputStyle.paddingRight);
            dummies.box.appendChild(dummies.preText);
            dummies.box.appendChild(dummies.caret);
            dummies.caret.appendChild(dummies.caretText);
            dummies.box.appendChild(dummies.postText);
            document.body.appendChild(dummies.box);
            this._dummyNodes = dummies;
        }
        var caretPos = this.tagComplete.getTextCaretPos();
        var text = target.value;
        var index = text.lastIndexOf('[', caretPos) + 1;
        dummies.preText.nodeValue = text.substring(0, index);
        dummies.caretText.nodeValue = text.charAt(index) || '';
        dummies.postText.nodeValue = text.substring(index + 1);
        dummies.box.style.width = target.clientWidth - dummies.widthDiff + 'px';
        list.style.left = pos.x + dummies.caret.offsetLeft - 1 + 'px';
        list.style.top = pos.y + dummies.caret.offsetTop + dummies.caret.offsetHeight - target.scrollTop + 5 + 'px';
    }
});

Hatena.Bookmark.AllTagsSelector = new Hatena.Bookmark.Class({
    DEFAULT_SHOW_LIMITL: 30,
    initialize: function(tagComplete, allTagsContainer) {
        this.tagComplete = tagComplete;
        this.dt = allTagsContainer.getElementsByTagName('dt')[0];
        this.dd = allTagsContainer.getElementsByTagName('dd')[0];
        this.cookie = new Ten.Cookie;
        this.flatList = new Hatena.Bookmark.TagComplete.FlatList(this.tagComplete, this.dd);
        var all_show = this.cookie.get('add_alltags_show');
        if (all_show) {
            // tmpl 側でやる
            // this.showAll();
        } else {
            this.showLimit();
            if (this.limitChildrenNum < this.constructor.DEFAULT_SHOW_LIMITL) {
                // リミットより小さいので、すべてのタグなどの toggle を表示しない
                return;
            }
        }
        var E = Ten.Element;
        var toggleContainer = E('span', {className: 'toggle-container'});
        this.dt.appendChild(toggleContainer);
        this.toggle = new Hatena.Bookmark.Toggle(toggleContainer, [
          this.someTag = E('span', {}, '一部タグのみ表示'),
          this.allTag = E('span', {}, 'すべてのタグを表示')
        ], all_show ? 0 : 1);
        this.toggle.oldAddEventListener('toggle', Ten.Function.method(this, 'toggleHandler'));
    }
}, {
    toggleHandler: function(newEL) {
        if (newEL == this.someTag) {
            this.cookie.set('add_alltags_show', 1, '+10y');
            this.showAll();
        } else {
            this.cookie.clear('add_alltags_show');
            this.showLimit();
        }
    },
    showAll: function() {
        var E = Ten.Element;
        var list = this.dd;
        var tags = list.getElementsByTagName('span');
        for (var i = 0;  i < tags.length; i++) {
            var tag = tags[i];
            tag.style.display = 'inline';
        }
        this.flatList.applyTagCompleteInput();
    },
    showLimit: function() {
        var E = Ten.Element;
        var limit = this.constructor.DEFAULT_SHOW_LIMITL;
        this.tagComplete.sortByCount();
        var a = this.tagComplete.tagsArray;
        var list = this.dd;
        var i;
        var tags = list.getElementsByTagName('span');
        for (i = 0;  i < tags.length; i++) {
            var tag = tags[i];
            tag.style.display = 'none';
        }

        var l = Math.min(a.length, limit);
        for (i = 0;  i < l; i++) {
            var key = Hatena.Bookmark.StringHelper.escapeHTML(a[i][0]);
            var el = document.getElementById('user-tag-' + key);
            if (el) {
                el.style.display = 'inline';
            }
        }
        this.limitChildrenNum = i;
        this.flatList.applyTagCompleteInput();
    }
});

Hatena.Bookmark.Toggle = new Hatena.Bookmark.Class({
    initialize: function(container, toggleElements, defaultIndex) {

        if (!defaultIndex) defaultIndex = 0;

        this.stopEvent = true;
        this.container = container;
        this.toggleElements = Array.prototype.slice.call(toggleElements, 0);
        for (var i = 0;  i < toggleElements.length; i++) {
            var el = toggleElements[i];
            if (el.parentNode)
                el.parentNode.removeChild(el);
            if (i == defaultIndex) this.setCurrent(el);
        }
        this.registerEventListeners();
    }
}, {
    registerEventListeners: function() {
        this.clickObserver = new Ten.Observer(this.container, 'onclick', this, 'clickHandler');
    },
    setCurrent: function(el) {
        this.current = el;
        Ten.DOM.removeAllChildren(this.container);
        this.container.appendChild(el);
    },
    next: function() {

        var toggleElements = this.toggleElements;

        var currentEL = this.current;
        for (var i = 0;  i < toggleElements.length; i++) {
            var el = toggleElements[i];
            if (currentEL == el) {
                el.parentNode.removeChild(el);
                var newEL;
                if (i == toggleElements.length - 1) {
                    newEL = toggleElements[0];
                } else {
                    newEL = toggleElements[i + 1];
                }
                this.setCurrent(newEL);
                this.dispatchEvent('toggle', newEL, el);
                break;
            }
        }
    },
    clickHandler: function(e) {
        if (this.stopEvent)
            e.stop();

        this.next();
        return;
        var toggleElements = this.toggleElements;
        for (var i = 0;  i < toggleElements.length; i++) {
            var el = toggleElements[i];
            if (e.target == el) {
                this.next();
                break;
            }
        }
    }
});

Hatena.Bookmark.Protector = new Hatena.Bookmark.Class({
    base: [Hatena.Bookmark.Toggle],
    initialize: function(lock_img, cancel_img) {
        var E = Ten.Element;
        this.lockImg = E('img', { src: lock_img, alt: '変更する' });
        this.lockCancelImg = E('img', { src: cancel_img, className: 'bookmarklet-lock-cancel', alt: 'キャンセル' });
        this.container = E('span');
        this.constructor.SUPER.call(this, this.container, [this.lockImg, this.lockCancelImg]);
        this.oldAddEventListener('toggle', Ten.Function.method(this, 'toggleHandler'));
    }
}, {
    lock: function() {
        if (this.currentEL != this.lockImg) {
            this.setCurrent(this.lockImg);
            this.dispatchEvent('lock');
        }
    },
    unlock: function() {
        if (this.currentEL != this.lockCancelImg) {
            this.setCurrent(this.lockCancelImg);
            this.dispatchEvent('unlock');
        }
    },
    toggleHandler: function(newEL, el) {
        if (this.lockImg == newEL) {
            this.dispatchEvent('lock');
        } else {
            this.dispatchEvent('unlock');
        }
    }
});


Hatena.Bookmark.TitleProtector = new Hatena.Bookmark.Class({
    initialize: function(title, lockImageContainer) {
        this.title = title;
        this.titleSpan = title.getElementsByTagName('span')[0];
        this.lasteditor = document.getElementById('lasteditor');
        this.lockImageContainer = lockImageContainer;
        this.protector = new Hatena.Bookmark.Protector(
            '/images/bookmarklet_lock.gif',
            '/images/bookmarklet_lock_cancel.gif'
        );
        this.protector.stopEvent = false;
        this.lockImageContainer.appendChild(this.protector.container);
        this.registerEventListeners();
    }
}, {
    registerEventListeners: function() {
        this.protector.oldAddEventListener('unlock', Ten.Function.method(this, 'unlockHandler'));
        this.protector.oldAddEventListener('lock', Ten.Function.method(this, 'lockHandler'));
    },
    setTitleButton: function(titleButton) {
        this.titleButton = titleButton;
        titleButton.clickObserver = new Ten.Observer(titleButton, 'onclick', this, 'titleButtonClickHandler');
        titleButton.style.cursor = 'pointer';
    },
    titleButtonClickHandler: function() {
        if (this.inputTitle && this.inputTitle.parentNode) {
            this.protector.lock();
        } else {
            this.protector.unlock();
        }
    },
    unlockHandler: function(e) {
        if (this.titleSpan.parentNode)
            this.title.removeChild(this.titleSpan);
        var E = Ten.Element;
        this.inputTitle = E('input', {
            'type': 'text',
            value: this.titleSpan.innerHTML,
            name: 'title',
            size: '40'
        });
        this.notice = E('p', {
            className: 'bookmarklet-notice notice'
        }, 'エントリーのタイトルは全ユーザ共通です',E('br'), E('a', {href:'/help/guideline_title'}, ' タイトル変更のガイドライン'));
        Ten.DOM.insertBefore(this.inputTitle, this.title.firstChild);
        this.title.appendChild(this.notice);
        if (this.lasteditor) this.lasteditor.style.display = 'block';
    },
    lockHandler: function(e) {
        if (this.inputTitle.parentNode)
            this.title.removeChild(this.inputTitle);
        if (this.notice.parentNode)
            this.title.removeChild(this.notice);

        Ten.DOM.insertBefore(this.titleSpan, this.title.firstChild);
        if (this.lasteditor) this.lasteditor.style.display = 'none';
    }
});

Hatena.Bookmark.ImageSelector = new Hatena.Bookmark.Class({
    initialize: function(container) {
        this.container = container;
        this.images = [];
        this.registerEventListeners();
    }
}, {
    registerEventListeners: function() {
        this.clickObserver = new Ten.Observer(this.container, 'onclick', this, 'clickHandler');
        //this.clickObserver.stop();
    },
    clickHandler: function(e) {
        e.stop();
        if (this.nowLocking()) {
            this.dispatchEvent('lockingClick');
        } else {
            if (e.target.tagName.toUpperCase() != 'IMG') return;
            this.setCurrentImage(e.target);
            this.hide();
            if (this.protector) this.protector.lock();
        }
    },
    getImage: function(imgSrc) {
        for (var i = 0;  i < this.images.length; i++) {
            var img = this.images[i];
            if (imgSrc == img.src) {
                return img;
            }
        }
        return false;
    },
    initDefaultImage: function(defaultImageSrc) {
        this.noImage = this.addImage('/images/noimages.gif');
        var image;
        if (defaultImageSrc) {
            image = this.addImage(defaultImageSrc);
        } else {
            image = this.noImage;
        }
        this.setDefaultImage(image);
        this.setCurrentImage(image);
        this.container.parentNode.style.display = 'none';
        this.hide();
    },
    hide: function() {
        //this.clickObserver.stop();
        var images = this.images;
        for (var i = 0;  i < images.length; i++) {
            var image = images[i];
            if (this.currentImage.src == image.src) {
                image.style.display = 'inline';
            } else {
                image.style.display = 'none';
            }
        }
    },
    showAll: function() {
        var images = this.images;
        for (var i = 0;  i < images.length; i++) {
            var image = images[i];
            image.style.display = 'inline';
        }
        this.clickObserver.start();
    },
    setCurrentImage: function(targetImage) {
        var images = this.images;
        for (var i = 0;  i < images.length; i++) {
            var image = images[i];
            if (image == targetImage) {
                Ten.DOM.addClassName(image, 'thumb-selected');
            } else {
                Ten.DOM.removeClassName(image, 'thumb-selected');
            }
        }
        this.currentImage = targetImage;
    },
    setDefaultImage: function(image) {
        Ten.DOM.addClassName(image, 'thumb-selected');
        this.defaultImage = image;
    },
    addImage: function(imageSrc) {
        var image = this.getImage(imageSrc);
        this.container.parentNode.style.display = 'block';
        if (!image) {
            image = new Ten.Element('img', { src: imageSrc, className: 'thumbnail' });
            image.style.display = 'none';
            this.container.appendChild(image);
            this.images.push(image);
        }
        return image;
    },
    getSelectedImage: function() {
        if (this.currentImage.src != this.defaultImage.src) {
            return this.currentImage;
        }
    },
    nowLocking: function() {
        var parentContainer = document.getElementById('image-container');
        return Ten.DOM.hasClassName(parentContainer, 'locking');
    },
    lock: function() {
        var parentContainer = document.getElementById('image-container');
        Ten.DOM.addClassName(parentContainer, 'locking');
        this.hide();
    },
    unlock: function() {
        var parentContainer = document.getElementById('image-container');
        Ten.DOM.removeClassName(parentContainer, 'locking');
        this.showAll();
    }
});

if (typeof Hatena.Bookmark.BookmarkLet == 'undefined')
    Hatena.Bookmark.BookmarkLet = {};

Hatena.Bookmark.BookmarkLet.Client = new Hatena.Bookmark.Class({
    initialize: function() {
        this.messenger = new Ten.IFrameMessenger.Client;
        if (document.getElementById('image-selector')) {
            this.imageSelector = new Hatena.Bookmark.ImageSelector(
              document.getElementById('image-selector')
            );
        }
    }
}, {
    setProcector: function(container) {
        var protector = new Hatena.Bookmark.Protector(
            '/images/image-selector-lock.gif',
            '/images/appender-cancel.gif'
        );
        protector.stopEvent = false;
        var E = Ten.Element;
        container.appendChild(this.infos = E('div', {className: 'images-info'} ));
        this.infos.appendChild(this.notice = E('p', {
            className: 'bookmarklet-notice notice'
        }, 'エントリーの画像は全ユーザ共通です', E('a', {href:'/help/guideline_image'}, ' ガイドライン')));
        this.notice.style.display = 'none';

        this.infos.appendChild(protector.container);
        protector.oldAddEventListener('lock', Ten.Function.method(this, 'lockHandler'));
        protector.oldAddEventListener('unlock', Ten.Function.method(this, 'unlockHandler'));
        this.protector = protector;
        this.imageSelector.protector = protector;
        this.imageSelector.oldAddEventListener('lockingClick', function() {
            this.protector.unlock();
        });
    },
    setImageLastModifier: function(username) {
        if (username && this.protector) {
            Ten.DOM.insertBefore(
                this.imageLastModifier = new Ten.Element('p', {
                    className: 'image-last-modifier'
                },
                '最終変更:',
                Hatena.Bookmark.ViewHelper.profileIcon(username),
                Ten.Element('a', { href:'/' + username + '/', target:'_blank', className:'username' }, username)),
                this.notice
            );
            this.imageLastModifier.style.display = 'none';
        }
    },
    lockHandler: function() {
        this.imageSelector.lock();
        if (this.notice)
            this.notice.style.display = 'none';
        if (this.imageLastModifier)
            this.imageLastModifier.style.display = 'none';
    },
    unlockHandler: function() {
        this.imageSelector.unlock();
        if (this.imageLastModifier)
            this.imageLastModifier.style.display = 'block';
        if (this.notice)
            this.notice.style.display = 'block';
    },
    registerEventListeners: function() {
        var self = this;
        this.messenger.addEventListener('addImage', function(args) {
            var srcs = args.srcs.split('@');
            for (var i = 0;  i < srcs.length; i++) {
                self.imageSelector.addImage(srcs[i]);
            }
        });
    },
    init: function() {
        var form = document.getElementById('bookmarklet-form');
        new Ten.Observer(form, 'onsubmit', this, 'submitHandler');
        new Ten.Observer(form, Ten.Event.onKeyDown, this, 'keydownHandler');
        var elements = document.getElementsByTagName('a');
        for (var i = 0;  i < elements.length; i++) {
            var a = elements[i];
            if (a.href && a.href.length && !a.target) {
                //new Ten.Observer(a, 'onclick', this, 'clickOpenTopHandler');
                a.target = '_blank'; // XXX
            }
        }
        if (this.imageSelector) {
            this.lockHandler();
        }
        this.messenger.observe();
    },
    keydownHandler: function(e) {
        if (e.event.keyCode !== 13 ||
            e.target.nodeName.toLowerCase() !== 'textarea' ||
            e.target.name !== 'comment')
            return;
        e.stop();
        var stopped = false;
        this.submitHandler({
            target: e.target.form,
            stop: function () { stopped = true; }
        });
        if (!stopped)
            e.target.form.submit();
        e.target.blur();
    },
    submitHandler: function(e) {
        this.createShadowLayer();
        var form = e.target;
        if (this.imageSelector) {
            var selectedImage = this.imageSelector.getSelectedImage();
            if (selectedImage) {
                var hidden = new Ten.Element('input', {
                    type: 'hidden',
                    value: selectedImage.src,
                    name: 'image'
                });
                form.appendChild(hidden);
            }
        }
        var params = this.seriarizeForm(form);
        if (!params.old) {
            this.ajaxSubmit(form, params);
            e.stop();
        }
    },
    seriarizeForm: function(form) {
        // FIXME: multipul や radio が無い場合のみのコード
        var inputs = form.elements;
        var params = {};
        for (var i = 0;  i < inputs.length; i++) {
            var input = inputs[i];
            var nodeName = input.nodeName.toLowerCase();
            if ((nodeName === 'input' || nodeName === 'textarea') &&
                !input.disabled && input.name) {
                if (input.type.toLowerCase() == 'checkbox' && !input.checked) {
                    //
                } else {
                    params[input.name] = input.value;
                }
            }
        }
        return params;
    },
    ajaxSubmit: function(form, params) {
        var url = form.action;
        url += '.json?_date=' + (new Date()).getTime();
        var xhr = new Hatena.Bookmark.XHR(url, 'POST');
        xhr.retry = 3;
        xhr.oldAddEventListener('retryError', Ten.Function.method(this, 'errorHandler'));
        xhr.oldAddEventListener('retry', Ten.Function.method(this, 'retryHandler'));
        xhr.onComplete(this, 'completeHandler');
        this.xhr = xhr;
        xhr.load(params);
    },
    completeHandler: function(res) {
        var json = eval('(' + res.responseText + ')');
        var url = json.url;
        try {
            top.location.replace(url);
        } catch (e) {
            top.location = url;
        }
    },
    retryHandler: function() {
        this.shadowLayer.errorMessage.innerHTML = '現在保存作業を再試行中です';
    },
    errorHandler: function() {
        alert('申し訳ありません、ブックマークに失敗しました。');
        this.shadowLayer.style.display = 'none';
    },
    createShadowLayer: function() {
        if (!this.shadowLayer) {
            var div = new Ten.Element('div', {className: 'shadow-layer'});
            div.appendChild(div.message = Hatena.Bookmark.ViewHelper.loadingIcon('保存しています'));
            div.appendChild(div.errorMessage = new Ten.Element('span', {className: 'error-message'}, ''));
            var w = Ten.Geometry.getWindowSize();
            document.body.appendChild(div);
            div.style.width = w.w + 'px';
            div.style.height = Math.max(300, w.h) + 'px';
            this.shadowLayer = div;
        } else {
            this.shadowLayer.style.display = 'inline';
            this.shadowLayer.errorMessage.innerHTML = '';
        }
    }
});


Hatena.Bookmark.Editor = new Hatena.Bookmark.Class({
    initialize: function() {
        this.constructor.SUPER.call(this);
        this.plusMode = false;
    }
}, {
    update: function(url, comment, private, postTwitter) {
        this.dispatchEvent('open');
        var data = {
            url: url,
            comment: comment || ''
        };
        if (this.plusMode) {
            data.with_status_op = 1;
            data.private = private ? 1 : 0;
        }
        if (postTwitter) {
            data.post_twitter = 1;
        }
        var xhr = this.getXHR('edit');
        xhr.load(data);
        return xhr;
    },
    remove: function(eid) {
        this.dispatchEvent('open');
        var xhr = this.getXHR('delete_bookmark');
        if (eid.indexOf('http') != -1) {
            // url delete
            xhr.load({
                url: eid
            });
        } else {
            xhr.load({
                eid: eid
            });
        }
        return xhr;
    },
    getXHR: function(apiname) {
        var xhr = Hatena.Bookmark.user.xhr(apiname).onComplete(this, 'callback');
        xhr.onError(this, 'errorback');
        xhr.retry = 5;
        xhr.retryWait = 1000;
        return xhr;
    },
    errorback: function(res) {
        var json = null;
        try { json = eval('(' + res.responseText + ')'); } catch(e) {};
        this.dispatchEvent('error', json);
    },
    callback: function(res) {
        var json = eval('(' + res.responseText + ')');
        this.dispatchEvent('complete', json);
    }
});

Hatena.Bookmark.CommentEditorBase = new Hatena.Bookmark.Class({
    initialize: function() {
        this.plusMode = false;
    }
}, {
    createTagComplete: function(input) {
        this.tagComplete = new Hatena.Bookmark.TagComplete();
        this.dispatchEvent('create_tag_complete', this.tagComplete);
        Hatena.Bookmark.user.loadTags(Ten.Function.method(this, 'loadTagsHandler'));
        this.tagComplete.observeInput(input);
    },
    loadTagsHandler: function(tags) {
        this.tagComplete.addTags(tags);
    },
    formSubmitHandler: function(e) {
        e.stop();
        var editor = new Hatena.Bookmark.Editor();
        var m = Ten.Function.method;
        editor.oldAddEventListener('open', m(this, 'openHandler'));
        editor.oldAddEventListener('complete', m(this, 'completeHandler'));
        if (this.plusMode && this.privateInput) {
            editor.plusMode = true;
        }
        if (this.postTwitterInput) {
            Hatena.Bookmark.cookie.set('post_twitter', +this.postTwitterInput.checked, { expires: '+1m' });
        }
        editor.update(this.url, this.input.value,
                      this.privateInput && this.privateInput.checked,
                      this.postTwitterInput && this.postTwitterInput.checked);
    },
    openHandler: function() {
        this.hide();
        this.showLoading();
    },
    hide: function() {
        // impl
    },
    replaceTags: function(user ,el, tags) {
        Ten.DOM.removeAllChildren(el);
        if (!user) user = new Hatena.Bookmark.User('my');
        for (var i = 0;  i < tags.length; i++) {
            var tag = tags[i];
            el.appendChild(Hatena.Bookmark.ViewHelper.tag(tag, user.tagLink(tag)));
            /*
            el.appendChild(new Ten.Element('a', {
                    className : 'user-tag',
                    href      : Hatena.Bookmark.user.tagLink(tag),
                    rel       : 'tag'
                }, tag)
            );
            */
            if (i + 1 < tags.length)
                el.appendChild(document.createTextNode(', '));
        }
    },
    replaceComment: function(el, comment) {
        Ten.DOM.removeAllChildren(el);
        el.appendChild(document.createTextNode(comment));
    },
    keypressHandler: function(e) {
        if (e.isKey('escape')) {
            this.cancelHandler();
        }
    },
    cancelHandler: function(e) {
        if (e) e.stop();
        this.hide();
        this.dispatchEvent('cancel');
    },
    showLoading: function() {
        if (!this.loadingElement) {
            // 画面が一瞬すぎる？
            var E = Ten.Element;
            this.loadingElement = Hatena.Bookmark.ViewHelper.loadingIcon('保存中...');
        }
        this.dispatchEvent('show_loading');
    },
    removeLoading: function() {
        if (this.loadingElement)
            Ten.DOM.removeElement(this.loadingElement);
    }
});

Hatena.Bookmark.CommentEditor = new Hatena.Bookmark.Class({
    base: [Hatena.Bookmark.CommentEditorBase],
    initialize: function(editImg, url) {
        this.constructor.SUPER.call(this);
        if (editImg)
            this.container = editImg.parentNode;
        this.cookie = new Ten.Cookie;
        this.url = url;
        this.editImg = editImg;
        this.deleteImg = Ten.DOM.getElementsByTagAndClassName('img', 'inplace-delete-icon', this.container)[0];
        this.registerEventListeners();
    },
    deleteIconClick: function(deleteImg, container) {
        var editor = new Hatena.Bookmark.CommentEditor();
        editor.deleteImg = deleteImg;
        editor.container = container || deleteImg.parentNode;
        editor.deleteHandler({
            stop: function() {},
            target: deleteImg
        });
    },
    edit: function(editImg, url, hide) {
        if (editImg && !editImg.commentEditor)
            editImg.commentEditor = new Hatena.Bookmark.CommentEditor(editImg, url);
        if (!hide) editImg.commentEditor.show();
        return editImg.commentEditor;
    }
}, {
    registerEventListeners: function() {
        this.oldAddEventListener('show_loading', Ten.Function.method(this, 'loadImageInsertion'));
        if (this.editImg) {
            new Ten.Observer(this.editImg, 'onclick', this, 'show');
        }
        if (this.deleteImg) {
            new Ten.Observer(this.deleteImg, 'onclick', this, 'deleteHandler');
        }
    },
    deleteHandler: function(e) {
        e.stop();
        var elID = e.target.id;
        var eid;
        var m;
        if (m = elID.match(/delete-(\d+)/)) {
            eid = m[1];
        }
        if (eid && window.confirm('このブックマークを削除しますか？')) {
            Ten.DOM.removeAllChildren(this.container);
            this.container.appendChild(Hatena.Bookmark.ViewHelper.loadingIcon('削除中...'));
            Hatena.Bookmark.user.xhr('delete_bookmark').onComplete(this, 'deleteCompleteHandler').load({eid: eid});
        }
    },
    deleteCompleteHandler: function(e) {
        Ten.DOM.removeAllChildren(this.container);
        this.container.appendChild(new Ten.Element('span', {className: 'delete-bookmark'}, 'ブックマークを削除しました ',
                                           Ten.Element('a', { href:location.href, onclick: function () { location.reload() } },
                                               '再読み込み') ));
    },
    loadImageInsertion: function() {
        Ten.DOM.insertBefore(this.loadingElement, this.editImg);
    },
    hide: function() {
        if (this.deleteImg) {
            Ten.DOM.insertBefore(this.deleteImg, this.form);
        }
        Ten.DOM.insertBefore(this.editImg, this.form);
        Ten.DOM.removeElement(this.form);
        var el = this.editImg;
        while (el) {
            if (el.className != 'hatena-star-comment-container' && el.tagName.toUpperCase() != 'NOSCRIPT')
                el.style.display = 'inline';

            el = Ten.DOM.nextElement(el);
        };
    },
    show: function() {
        var el = this.editImg;
        var tags = [];
        var comment = '';
        while (el) {
            el.style.display = 'none';
            if (Ten.DOM.hasClassName(el, 'tags')) tags = this.detectTagBySpan(el);
            if (Ten.DOM.hasClassName(el, 'comment')) comment = Ten.DOM.scrapeText(el);
            if (Ten.DOM.hasClassName(el, 'timestamp'))
                break;
            el = Ten.DOM.nextElement(el);
        }
        if (!this.form) {
            this.form = this.createForm(comment, tags);
            if (this.privateInput) {
                // 初回のみ設定する
                var pEL = this.container;
                while (pEL) {
                    if (Ten.DOM.hasClassName(pEL, 'noshare')) {
                        this.privateInput.checked = true;
                    }
                    pEL = pEL.parentNode;
                 }
            }
        }
        if(this.editImg.parentNode)
            Ten.DOM.insertBefore(this.form, this.editImg);

        if (this.deleteImg && this.deleteImg.parentNode)
            Ten.DOM.removeElement(this.deleteImg);
        if (this.editImg.parentNode)
            Ten.DOM.removeElement(this.editImg);
        this.input.focus();
    },
    detectTagBySpan: function(span) {
        var els = span.getElementsByTagName('a');
        var tags = [];
        for (var i = 0;  i < els.length; i++) {
            tags.push(Ten.DOM.scrapeText(els[i]));
        }
        return tags;
    },
    createForm: function(comment, tags) {
        var E = Ten.Element;
        var form = E('form', { className: 'inplace-form' },
            (this.input = E('input', { type: 'text', className: 'comment', name: 'comment', size: 40 })),
            (this.saveButton = E('input', { type: 'submit', value: '保存' })),
            (this.cancel = E('span', {className:'bookmark-appender-cancel'}, 'キャンセル')),
            (this.commentCounter = new Hatena.Bookmark.CommentCounter).span
        );
        if (Hatena.Bookmark.user && Hatena.Bookmark.user.plususer) {
            this.plusMode = true;
            var span; Ten.DOM.insertBefore(span = E('span', { title: 'ブックマークを他のユーザーに公開しない場合はチェックを入れてください。(閲覧許可ユーザーにも公開されません。) ', className: 'bookmark-appender-private' }, '非公開'), this.cancel);
            var private; Ten.DOM.insertBefore(private = E('input', { type: 'checkbox', className: 'private', name: 'private', value: '1'}), span);
            new Ten.Observer(span, 'onclick', function() {
                private.click();
            });
            this.privateInput = private;
        }

        this.cancel.observer = new Ten.Observer(this.cancel, 'onclick', this, 'cancelHandler');
        this.input.keypressObserver = new Ten.Observer(this.input, 'onkeypress', this, 'keypressHandler');
        this.input.setAttribute('autocomplete', 'off');

        var tagString = '';
        if (tags.length > 0) tagString = '[' + tags.join('][') + ']';
        this.input.value = tagString + comment;
        var self = this;
        this.oldAddEventListener('create_tag_complete', function(tagComplete) {
            self.commentCounter.registTagComplete(tagComplete);
        });
        this.createTagComplete(this.input);
        form.observer = new Ten.Observer(form, 'onsubmit', this, 'formSubmitHandler');
        return form;
    },
    completeHandler: function(res) {
        this.removeLoading();
        if (this.privateInput) {
            if (this.privateInput.checked) {
                Ten.DOM.addClassName(this.container, 'noshare');
                var image = Ten.DOM.getElementsByTagAndClassName('img', 'noshare', this.container)[0];
                if (!image) {
                    var image = Ten.Element('img', {src: '/images/noshare.gif', className: 'noshare'});
                    Ten.DOM.insertAfter(image, this.container.firstChild);
                }
            } else {
                Ten.DOM.removeClassName(this.container, 'noshare');
                var image = Ten.DOM.getElementsByTagAndClassName('img', 'noshare', this.container)[0];
                if (image) image.parentNode.removeChild(image);
            }
        }

        var el = this.editImg;
        while (el) {
            if (Ten.DOM.hasClassName(el, 'tags')) {
                this.replaceTags(Hatena.Bookmark.user, el, res.tags);
            }
            if (Ten.DOM.hasClassName(el, 'comment')) {
                this.replaceComment(el, res.comment);
            }
            if (Ten.DOM.hasClassName(el, 'timestamp')) {
                el.innerHTML = res.timestamp;
                break;
            }
            el.style.display = 'inline';
            el = Ten.DOM.nextElement(el);
        }
    }
});

Hatena.Bookmark.CommentEditor.Hover = new Hatena.Bookmark.Class({
    initialize: function(element, url) {
        this.element = element;
        this.editImg = Ten.DOM.getElementsByTagAndClassName('img', 'inplace-edit-icon', element)[0];
        if (!this.editImg) {
            this.createEditImage();
        }
        this.deleteImg = Ten.DOM.getElementsByTagAndClassName('img', 'inplace-delete-icon', element)[0];
        if (!this.deleteImg) {
            this.createDeleteImage();
        }
        this.editor = Hatena.Bookmark.CommentEditor.edit(this.editImg, url, true);
        if (this.editImg) {
            this.registerEventListeners();
        }
    },
    create: function(element, url) {
        if (!element.hover)
            element.hover = new Hatena.Bookmark.CommentEditor.Hover(element, url);
        element.hover.show();
    }
}, {
    createEditImage: function() {
        var E = Ten.Element;
        this.editImg = E('img', {
            className:"inplace-edit-icon",
            title:"コメントを編集する",
            alt:"コメントを編集する",
            src:"/images/edit.gif",
            style:"display: none;"
        });
        var uname = Ten.DOM.getElementsByTagAndClassName('a', 'username', this.element)[0];
        if (uname)  {
            Ten.DOM.insertAfter(this.editImg, uname);
        } else {
            Ten.DOM.insertBefore(this.editImg, this.element.firstChild);
        }
    },
    createDeleteImage: function() {
        var E = Ten.Element;
        this.deleteImg = E('img', {
            id: 'delete-' + Hatena.Bookmark.entry.eid,
            className:"inplace-delete-icon",
            title:"ブックマークを削除する",
            alt:"ブックマークを削除する",
            src:"/images/delete.gif",
            style:"display: none;"
        });
        Ten.DOM.insertAfter(this.deleteImg, this.editImg);
    },
    registerEventListeners: function() {
        new Ten.Observer(this.element, 'onclick', this, 'clickHandler');
        new Ten.Observer(this.element, 'onmouseout', this, 'mouseoutHandler');
    },
    clickHandler: function(e) {
        var tag = e.target.tagName.toUpperCase();
        if (tag == 'A') {
            //
        } else if (tag == 'IMG') {
            //
        } else {
            //if(this.editor)
            //this.editor.show();
        }
    },
    mouseoutHandler: function(e) {
        this.editImg.style.display = 'none';
        if (this.deleteImg)
            this.deleteImg.style.display = 'none';
        //Ten.DOM.removeClassName(this.element, 'comment-editor-hover');
    },
    show: function() {
        //Ten.DOM.addClassName(this.element, 'comment-editor-hover');
        if (this.editImg) {
            this.editImg.style.display = 'inline';
        }
        if (this.deleteImg) {
            this.deleteImg.style.display = 'inline';
        }
    }
});


Hatena.Bookmark.EntryAppender = new Hatena.Bookmark.Class({
    base: [Hatena.Bookmark.CommentEditorBase],
    initialize: function(link, url, message, options) {
        this.constructor.SUPER.call(this);
        this.link = link;
        this.cookie = new Ten.Cookie;
        this.message = message || '保存しました';
        this.url = url;
        this.createElement();
        this.oldAddEventListener('show_loading', Ten.Function.method(this, 'loadImageInsertion'));
        if (Hatena.Bookmark.user) {
            // あらかじめ Twitter へ投稿できるか確認できるようにしておく
            Hatena.Bookmark.user.loadUserInfo();
        }
    },
    create: function(link, url, message) {
        if (!link.appender ) {
            link.appender = new Hatena.Bookmark.EntryAppender(link, url, message);
        }
        link.appender.show();
    }
}, {
    loadImageInsertion: function() {
        Ten.DOM.replaceNode(this.loadingElement, this.form);
    },
    completeHandler: function(res) {
        Ten.DOM.replaceNode(this.form, this.loadingElement);
        this.layer.close();
        this.createConfirmLink(res);
        this.dispatchEvent('complete', res);
        Hatena.Bookmark.user.xhr('entry').onComplete(this, 'xhrCallback').load({url: this.url});
    },
    createConfirmLink: function(res) {
        if (this.deletedLink) {
            this.deletedLink.style.display = 'none';
        }
        if (this.confirmLink) {
            this.confirmLink.style.display = 'inline';
            return;
        }
        var E = Ten.Element;
        var url = res.permalink;
        this.confirmLink = E('span',{className: 'appender-confirm-link', style:{marginLeft: '0.5em'}}, this.message, E('a', {className: 'bookmark-appender-confirm', href: url}, '(ブックマークを確認する)'));
        Ten.DOM.insertAfter(this.confirmLink, this.link);
    },
    createDeletedLink: function() {
        if (this.confirmLink) {
            this.confirmLink.style.display = 'none';
        }
        if (this.deletedLink) {
            this.deletedLink.style.display = 'inline';
            return;
        }
        var E = Ten.Element;
        this.deletedLink = E('span',{className: 'appender-confirm-link', style:{marginLeft: '0.5em'}}, 'ブックマークを削除しました');
        Ten.DOM.insertAfter(this.deletedLink, this.link);
    },
    createElement: function() {
        this.layer = new Hatena.Bookmark.LayerWindow;
        Ten.DOM.addClassName(this.layer.div, 'bookmark-appender');
        if (Hatena.Bookmark.user) {
            this.createElementUser();
        } else {
            this.createElementLogin();
        }
    },
    createElementLogin: function() {
        var E = Ten.Element;
        var form = this.form = E('form', {className: 'appender-form'},
            E('div', {style: {'paddingRight': '15px'}, className: 'bookmark-appender-control'},
                E('p', {}, 'ブックマークを追加するには、ログインが必要です。'),
                E('p', {style: {'textAlign': 'center'}}, E('a', {target: 'blank', href:'/guide'}, E('img', {src:'/images/guide_banner_white.gif', alt:'はてなブックマークってなに？', title:'はてなブックマークってなに？'}))),
                (this.login = E('a', {target: 'blank', href:'https://www.hatena.ne.jp/login?location='+encodeURIComponent(this.link.href || location.href), className:'bookmark-appender-login'}, 'ログイン')),
                (this.cancel = E('span', {className:'bookmark-appender-cancel'}, 'キャンセル'))
            )
        );
        this.cancel.observer = new Ten.Observer(this.cancel, 'onclick', this, 'cancelHandler');
        this.layer.div.appendChild(this.form);
        this.fixedPosition();
    },
    createElementUser: function() {
        var E = Ten.Element;
        var form = this.form = E('form', {className: 'appender-form'},
                Hatena.Bookmark.user.getProfileIcon(),
                E('span', {className:'username'}),
                (this.input = E('input', {type: 'text', className: 'comment inputtext', size: '30', name: 'comment'})),
                E('div', {className: 'bookmark-appender-control'},
                E('div', {className: 'bookmark-appender-control-inner'},
                    E('div', {className: 'bookmark-appender-control-inner-right'},
                        this.showTagContainer = E('span', {className: 'bookmark-appender-show-tag'}),
                        ' ',
                        this.charCount = (this.CommentCounter = new Hatena.Bookmark.CommentCounter).span
                    ),
                    (this.saveButton = E('input', {type: 'submit', className: 'inputbutton', value: '保存'})),
                    (this.cancel = E('span', {className:'bookmark-appender-cancel'}, 'キャンセル')),
                    (this.deleter = E('span', {style: {display: 'none'}, className:'bookmark-appender-delete'}, '削除する'))
                ),
                this.allTagsContainer = E('div', {className: 'bookmark-appender-all-tags', style: { display: 'none'} })
            )
        );
        if (Hatena.Bookmark.user && Hatena.Bookmark.user.plususer) {
            this.plusMode = true;
            var span; Ten.DOM.insertBefore(span = E('span', { title: 'ブックマークを他のユーザーに公開しない場合はチェックを入れてください。(閲覧許可ユーザーにも公開されません。) ', className: 'bookmark-appender-private' }, '非公開'), this.cancel);
            var private; Ten.DOM.insertBefore(private = E('input', { type: 'checkbox', className: 'private', name: 'private', value: '1'}), span);
            new Ten.Observer(span, 'onclick', function() {
                private.click();
            });
            this.privateInput = private;
        }
        Hatena.Bookmark.user && Hatena.Bookmark.user.loadUserInfo(Ten.Function.bind(function(info) {
            if (!info.is_oauth_twitter) return;
            var postTwitterChecked = (info.twitter_checked === 'on' ||
                                      (info.twitter_checked === 'inherit' &&
                                       !!+this.cookie.get('post_twitter')));
            var postTwitterInput = E('input', { type: 'checkbox', name: 'post_twitter', value: '1', defaultChecked: postTwitterChecked });
            var postTwitterLabel = E('label', { title: 'ブックマークした内容をTwitterへ投稿する場合はチェックを入れてください。', className: 'bookmark-appender-post-twitter' }, postTwitterInput, E('img', { src: '/images/add-twitter.png', alt: 'Twitter', width: 16, height: 16 }));
            Ten.DOM.insertAfter(postTwitterLabel, this.saveButton);
            this.postTwitterInput = postTwitterInput;
            Hatena.Bookmark.FormOutput.createTwitterCheck(this.privateInput || null, postTwitterInput);
        }, this));
        this.preLoadingElement = Hatena.Bookmark.ViewHelper.loadingIcon('ロード中...');
        this.layer.div.appendChild(this.preLoadingElement);
        Hatena.Bookmark.user.xhr('entry').onComplete(this, 'xhrCallback').load({url: this.url});
        this.input.setAttribute('autocomplete', 'off');
        this.input.keydownObserver = new Ten.Observer(this.input, 'onkeydown', this, 'inputKeydownHandler');
        this.createTagComplete(this.input);

        form.observer = new Ten.Observer(form, 'onsubmit', this, 'formSubmitHandler');

        if (!Hatena.Bookmark.user.tagsuggest) {
            this.showTagContainer.style.display = 'none';
            return;
        }
        var appender_show_tag = this.cookie.get('appender_show_tag');
        this.toggleShowTags = new Hatena.Bookmark.Toggle(this.showTagContainer, [
          this.showTag = E('span', {}, 'タグを表示'),
          this.hideTag = E('span', {}, 'タグを隠す')
        ], appender_show_tag ? 1 : 0);
        this.toggleShowTags.oldAddEventListener('toggle', Ten.Function.method(this, 'toggleHandler'));
        if (appender_show_tag) {
            var self = this;
            Hatena.Bookmark.user.loadTags(function() {
                  setTimeout(function() {
                      self.showTagHandler();
                  }, 30);
            });
        }
    },
    toggleHandler: function(newEL) {
        if (newEL == this.showTag) {
            this.cookie.clear('appender_show_tag');
            this.hideTagHandler();
        } else {
            this.cookie.set('appender_show_tag', 1, { expires: '+10y', path: '/' } );
            this.showTagHandler();
        }
    },
    showTagHandler: function() {
        if (!this.flatList) {
            var E = Ten.Element;
            var tags = this.tagComplete.tagsKeys;
            var cnt = this.allTagsContainer;
            for (var i = 0;  i < tags.length; i++) {
                var tag = tags[i];
                cnt.appendChild(E('span', {className: 'tag'}, tag));
                cnt.appendChild(document.createTextNode(' '));
            }
            this.flatList = new Hatena.Bookmark.TagComplete.FlatList(this.tagComplete, this.allTagsContainer);
        }
        this.allTagsContainer.style.display = 'block';
    },
    hideTagHandler: function() {
        this.allTagsContainer.style.display = 'none';
    },
    inputKeydownHandler: function(e) {
        if (e.isKey('escape'))
            this.cancelHandler();
    },
    deleteHandler: function(e) {
        e.stop();
        var elID = e.target.id;
        var eid;
        var m;
        if (m = elID.match(/delete-appender-(\d+)/)) {
            eid = m[1];
        }
        if (eid && window.confirm('このブックマークを削除しますか？')) {
            this.deletedElement = Hatena.Bookmark.ViewHelper.loadingIcon('削除中...');
            Ten.DOM.replaceNode(this.deletedElement, this.form);
            Hatena.Bookmark.user.xhr('delete_bookmark').onComplete(this, 'deleteCompleteHandler').load({eid: eid});
        }
    },
    deleteCompleteHandler: function(e) {
        Hatena.Bookmark.user.xhr('entry').onComplete(this, 'xhrCallback').load({url: this.url});
        Ten.DOM.replaceNode(this.form, this.deletedElement);
        this.layer.close();
        this.createDeletedLink();
        this.dispatchEvent('complete', res);
    },
    xhrCallback: function(res) {
        var r = eval('(' + res.responseText + ')');
        if (r && r.eid) {
            this.deleter.style.display = 'inline';
            this.deleter.id = 'delete-appender-' + r.eid;
            if (!this.deleter.clickHandler)
                this.deleter.clickHandler = new Ten.Observer(this.deleter, 'onclick', this, 'deleteHandler');
        } else if (this.deleter) {
            this.deleter.style.display = 'none';
        }
        if (r.comment_raw)
            this.input.value = r.comment_raw;
        if (this.privateInput && r.private == 1)
            this.privateInput.checked = true;
        Ten.DOM.replaceNode(this.form, this.preLoadingElement);
        this.cancel.observer = new Ten.Observer(this.cancel, 'onclick', this, 'cancelHandler');
        this.charCount.counter.registTagComplete(this.tagComplete);
        this.fixedPosition();
        this.input.focus();
    },
    toggle: function() {
        if (this.layer.shown) {
            this.cancelHandler();
        } else {
            this.show();
        }
    },
    cancelHandler: function() {
        if (this.form.parentNode)
            this.layer.close();
    },
    fixedPosition: function() {
        var t = this.layer.div;
        var pos = Ten.Geometry.getElementPosition(this.link);
        var y = pos.y + this.link.offsetHeight + 6;
        var l = pos.x - 20;
        var w = Ten.Geometry.getWindowSize().w;
        if (w < (t.offsetWidth + l)) {
            l = w - t.offsetWidth - 20;
        }
        var x = l;
        this.layer.moveTo(x, y);
    },
    show: function() {
        this.layer.open();
        this.fixedPosition();
    }
});

Hatena.Bookmark.CommentCounter = new Hatena.Bookmark.Class({
    MAX_COMMENT_SIZE: 100,
    initialize: function(span) {
        if (span) {
            this.span = span;
        } else {
            this.createSpan();
        }
        this.spanInit();
    }
}, {
    registTagComplete: function(tagComplete) {
        tagComplete.oldAddEventListener('update_comment_byte', Ten.Function.method(this, 'updateCount'));
    },
    spanInit: function() {
        if (!this.span.counter) {
            this.span.counter = this;
            this.updateCount(0);
        }
    },
    createSpan: function() {
        this.span = new Ten.Element('span', { className: 'char-count' });
    },
    updateCount: function(num) {
        var m = this.constructor.MAX_COMMENT_SIZE;
        var s = this.span;
        s.innerHTML = '' + num + ' / ' + m;
        if (num > m) {
            if (!Ten.DOM.hasClassName(s, 'char-count-over'))
                Ten.DOM.addClassName(s, 'char-count-over');
        } else {
            if (Ten.DOM.hasClassName(s, 'char-count-over'))
                Ten.DOM.removeClassName(s, 'char-count-over');
        }
    }
});

Hatena.Bookmark.ToggleEditBase = new Hatena.Bookmark.Class({
    initialize: function(username, checked, options) {
        this.api = {};
        this.options = options || {};
        this.username = username;
        this.loading = false;
        if (typeof checked == 'undefined') {
            this.check();
        } else {
            this.checked = checked;
        }
    }
}, {
    check: function() {
        this.loading = true;
        Hatena.Bookmark.user.xhr(this.api.check).onComplete(this, 'checkHander').load(this.getParameter());
    },
    getParameter: function() {
       return {username: this.username};
    },
    checkHander: function(res) {
        this.loading = false;
        this.checkRes(res);
        this.dispatchEvent('update', this);
    },
    addHandler: function(res) {
        this.loading = false;
        this.checkRes(res);
        this.dispatchEvent('update', this);
    },
    removeHandler: function(res) {
        this.loading = false;
        this.checkRes(res);
        this.dispatchEvent('update', this);
    },
    checkRes: function(res) {
        res = eval('(' + res.responseText + ')');
        Hatena.Bookmark.Entry.Storage.clearStorage(); // 設定項目のキャッシュを消す
        if (res['error']) {
            this.error(res);
        } else {
            if (res['checked']) {
                this.checked = true;
            } else {
                this.checked = false;
            }
        }
    },
    error: function(res) {
    },
    loadXHR: function(apiname, handler) {
        this.loading = true;
        var xhr = Hatena.Bookmark.user.xhr(apiname).onComplete(this, handler);
        if (this.nowLoading) this.nowLoading.registerTarget(xhr);
        xhr.load(this.getParameter());
    },
    toggle: function() {
        if (!this.loading) {
            if (!this.checked) {
                this.loadXHR(this.api.add, 'addHandler');
            } else {
                this.loadXHR(this.api.remove, 'removeHandler');
            }
        }
    }
});

Hatena.Bookmark.FavoriteEdit = new Hatena.Bookmark.Class({
    base: [Hatena.Bookmark.ToggleEditBase],
    initialize: function(username, following) {
        this.constructor.SUPER.call(this, username, following);
        this.api.check = 'following';
        this.api.add = 'follow';
        this.api.remove = 'unfollow';
    }
}, {
    error: function(res) {
        alert('お気に入りの追加上限 500 人に達しました');
    }
});

Hatena.Bookmark.FollowSuggestIgnore = new Hatena.Bookmark.Class({
    base: [Hatena.Bookmark.ToggleEditBase],
    initialize: function(username, following) {
        this.constructor.SUPER.call(this, username, following);
        this.api.add = 'follow_suggest_ignore';
        this.api.remove = 'unfollow_suggest_ignore';
    }
});

Hatena.Bookmark.IgnoreEdit= new Hatena.Bookmark.Class({
    base: [Hatena.Bookmark.ToggleEditBase],
    initialize: function(username, following) {
        this.constructor.SUPER.call(this, username, following);
        this.api.check = 'ignoring';
        this.api.add = 'ignore';
        this.api.remove = 'unignore';
    }
});

Hatena.Bookmark.ToggleEditBase.TextLink = new Hatena.Bookmark.Class({
    initialize: function(el, username, withoutOnLoad, editOptions) {
        this.el = el;
        this.username = username;
        this.edit = new this.constructor.EDIT_CLASS(username, !!Ten.DOM.hasClassName(el, this.remove.className), editOptions);
        this.edit.oldAddEventListener('update', Ten.Function.method(this, 'updateHandler'));

        var E = Ten.Element;
        var img = Hatena.Bookmark.ViewHelper.loadingIcon('保存中...');
        img.style.display = 'none';
        Ten.DOM.insertAfter(img, el);
        var loading = new Hatena.Bookmark.NowLoading(img, el);
        this.edit.nowLoading = loading;

        this.registerEventListeners();
        if (withoutOnLoad) {
            this.edit.check();
        } else {
            this.edit.toggle();
        }
    }
}, {
    updateHandler: function(target) {
        if (target.checked) {
            Ten.DOM.removeClassName(this.el, this.add.className);
            Ten.DOM.addClassName(this.el, this.remove.className);
            this.el.innerHTML = this.remove.label;
        } else {
            Ten.DOM.removeClassName(this.el, this.remove.className);
            Ten.DOM.addClassName(this.el, this.add.className);
            this.el.innerHTML = this.add.label;
        }
    },
    registerEventListeners: function() {
        this.el.clickObserver = new Ten.Observer(this.el, 'onclick', this, 'clickHandler');
    },
    clickHandler: function(e) {
        e.stop();
        this.edit.toggle();
    }
});

Hatena.Bookmark.FavoriteEdit.TextLink = new Hatena.Bookmark.Class({
    base: [Hatena.Bookmark.ToggleEditBase.TextLink],
    EDIT_CLASS: Hatena.Bookmark.FavoriteEdit,
    create: function(el, username, withoutOnLoad) {
        if (!el.textLink) {
            el.textLink = new Hatena.Bookmark.FavoriteEdit.TextLink(el, username, withoutOnLoad);
        }
        return el.textLink;
    },
    init: function(el, username, withoutOnLoad) {
        if (!el.textLink) {
            new Ten.Observer(el, 'onclick', function(e) {
                e.stop();
                Hatena.Bookmark.FavoriteEdit.TextLink.create(el, username, withoutOnLoad);
            });
        }
    },
    initialize: function(el, username, withoutOnLoad) {
        this.remove = {
            className: 'remove-favorite',
            label: '<img src="/images/follow_on.gif" alt="お気に入りを解除" />'
        };
        this.add = {
            className: 'add-favorite',
            label: '<img src="/images/follow.gif" alt="お気に入りに追加" />'
        };
        this.constructor.SUPER.call(this, el, username, withoutOnLoad);
    }
});

Hatena.Bookmark.FavoriteEdit.OfficialTextLink = new Hatena.Bookmark.Class({
    base: [Hatena.Bookmark.ToggleEditBase.TextLink],
    EDIT_CLASS: Hatena.Bookmark.FavoriteEdit,
    create: function(el, username, withoutOnLoad) {
        if (!el.textLink) {
            el.textLink = new Hatena.Bookmark.FavoriteEdit.OfficialTextLink(el, username, withoutOnLoad);
        }
        return el.textLink;
    },
    init: function(el, username, withoutOnLoad) {
        if (!el.textLink) {
            new Ten.Observer(el, 'onclick', function(e) {
                e.stop();
                Hatena.Bookmark.FavoriteEdit.OfficialTextLink.create(el, username, withoutOnLoad);
            });
        }
    },
    initialize: function(el, username, withoutOnLoad) {
        this.remove = {
            className: 'remove-favorite',
            label: '<img src="/images/follow_on.gif" alt="お気に入りを解除" />'
        };
        this.add = {
            className: 'add-favorite',
            label: '<img src="/images/sponsored/addbutton.gif" alt="お気に入りに追加" />'
        };
        this.constructor.SUPER.call(this, el, username, withoutOnLoad);
    }
});

Hatena.Bookmark.FollowSuggestIgnore.TextLink = new Hatena.Bookmark.Class({
    base: [Hatena.Bookmark.ToggleEditBase.TextLink],
    EDIT_CLASS: Hatena.Bookmark.FollowSuggestIgnore,
    create: function(el, username, withoutOnLoad) {
        if (!el.textLink) {
            el.textLink = new Hatena.Bookmark.FollowSuggestIgnore.TextLink(el, username, withoutOnLoad);
        }
        return el.textLink;
    },
    init: function(el, username, withoutOnLoad) {
        if (!el.textLink) {
            new Ten.Observer(el, 'onclick', function(e) {
                e.stop();
                Hatena.Bookmark.FollowSuggestIgnore.TextLink.create(el, username, withoutOnLoad);
            });
        }
    },
    initialize: function(el, username, withoutOnLoad) {
        this.remove = {
            className: 'remove-favorite',
            label: '<img src="/images/follow_suggest_ignore_on.gif" alt="おすすめして" />'
        };
        this.add = {
            className: 'add-favorite',
            label: '<img src="/images/follow_suggest_ignore.gif" alt="おすすめしないで" />'
        };
        this.constructor.SUPER.call(this, el, username, withoutOnLoad);
    }
});

Hatena.Bookmark.UserTagEdit = new Hatena.Bookmark.Class({
    base: [Hatena.Bookmark.ToggleEditBase],
    initialize: function(username, checked, options) {
        this.constructor.SUPER.call(this, username, checked, options);
        this.api.add = 'usertag';
        this.api.remove = 'unusertag';
    }
}, {
    getParameter: function() {
       return {
           username: this.username,
           usertag: this.options.usertag
       };
    }
});

Hatena.Bookmark.UserTagEdit.TextLink = new Hatena.Bookmark.Class({
    base: [Hatena.Bookmark.ToggleEditBase.TextLink],
    EDIT_CLASS: Hatena.Bookmark.UserTagEdit,
    create: function(el, username, withoutOnLoad, usertag) {
        if (!el.textLink) {
            el.textLink = new Hatena.Bookmark.UserTagEdit.TextLink(el, username, withoutOnLoad, usertag);
        }
        return el.textLink;
    },
    init: function(el, username, withoutOnLoad, usertag) {
        if (!el.textLink) {
            new Ten.Observer(el, 'onclick', function(e) {
                e.stop();
                Hatena.Bookmark.UserTagEdit.TextLink.create(el, username, withoutOnLoad, usertag);
            });
        }
    },
    initialize: function(el, username, withoutOnLoad, usertag) {
        if (!username) {
            // el の link から取る
            var link = el.href;
            var re = new RegExp('[^/]/([^/]+)/');
            if (link.match(re)) {
                username = RegExp.$1;
            }
        }
        var uHTML;
        if (!usertag) {
            usertag = Ten.DOM.scrapeText(el);
            uHTML = Hatena.Bookmark.StringHelper.escapeHTML(usertag);
        }
        this.remove = {
            className: 'remove-usertag',
            label: uHTML
        };
        this.add = {
            className: 'add-usertag',
            label: uHTML
        };
        this.constructor.SUPER.call(this, el, username, withoutOnLoad, {usertag: usertag});
    }
}
);

Hatena.Bookmark.IgnoreEdit.TextLink = new Hatena.Bookmark.Class({
    base: [Hatena.Bookmark.ToggleEditBase.TextLink],
    EDIT_CLASS: Hatena.Bookmark.IgnoreEdit,
    create: function(el, username, withoutOnLoad) {
        if (!el.textLink) {
            el.textLink = new Hatena.Bookmark.IgnoreEdit.TextLink(el, username, withoutOnLoad);
        }
        return el.textLink;
    },
    init: function(el, username, withoutOnLoad) {
        if (!el.textLink) {
            new Ten.Observer(el, 'onclick', function(e) {
                e.stop();
                Hatena.Bookmark.IgnoreEdit.TextLink.create(el, username, withoutOnLoad);
            });
        }
    },
    initialize: function(el, username, withoutOnLoad) {
        this.remove = {
            className: 'remove-ignore',
            label: '<img src="/images/ignore_on.gif" alt="ユーザーを表示" />'
        };
        this.add = {
            className: 'add-ignore',
            label: '<img src="/images/ignore.gif" alt="ユーザーを非表示" />'
        };
        this.constructor.SUPER.call(this, el, username, withoutOnLoad);
    }
});

Hatena.Bookmark.NowLoading = new Hatena.Bookmark.Class({
    initialize: function(loadingElement, elements) {
        this.loadingElement = loadingElement;
        this.display = 'block';
        this.elementDisplay = 'block';
        this.elements = (elements instanceof Array) ? elements : [elements];
    }
}, {
    registerTarget: function(xhr) {
        var m = Ten.Function.method;
        xhr.oldAddEventListener('open', m(this, 'openHandler'));
        xhr.oldAddEventListener('complete', m(this, 'finishHandler'));
        xhr.oldAddEventListener('error', m(this, 'finishHandler'));
    },
    openHandler: function() {
        this.loadingElement.style.display = this.display;
        var els = this.elements;
        for (var i = 0;  i < els.length; i++) {
            var el = els[i];
            el._origDisplay = el.style.display;
            el.style.display = 'none';
        }
    },
    finishHandler: function() {
        this.loadingElement.style.display = 'none';
        var els = this.elements;
        for (var i = 0;  i < els.length; i++) {
            var el = els[i];
            if (el._origDisplay) {
                el.style.display = el._origDisplay;
            } else {
                el.style.display = this.elementDisplay;
            }
        }
    }
});

Hatena.Bookmark.Tooltip = {};

Hatena.Bookmark.Tooltip.Help = new Hatena.Bookmark.Class({
    initialize: function(el, helpBody) {
        this.el = el;
        this.layer = new Hatena.Bookmark.LayerWindow;
        this.layer.div.appendChild(helpBody);
        helpBody.style.display = 'block';
        if(Ten.Browser.isIE) {
            var iframe = Ten.Element('iframe', {
                className :'iefix',
                style: {
                  display: 'block',
                  position: 'absolute',
                  top: 0,
                  left: 0
                }
            });
            this.layer.div.appendChild(iframe);
        }
        this.registerEventListeners();
    },
    create: function(el, comment) {
        if (!el.tooltip) {
            el.tooltip = new Hatena.Bookmark.Tooltip.Help(el, Ten.DOM.nextElement(el));
        }
        el.tooltip.show();
    }
}, {
    registerEventListeners: function() {
        this.bodyClickObserver = new Ten.Observer(document.body, 'onclick', this, 'bodyClickHandler');
        this.bodyClickObserver.stop();
        if (Hatena.Bookmark.TabMenu._instance) {
            Hatena.Bookmark.TabMenu._instance.oldAddEventListener('change', Ten.Function.method(this, 'hide'));
        }
    },
    bodyClickHandler: function(e) {
        if (e.target.tagName.toUpperCase() == 'A') {
            //
        } else {
            e.stop();
            this.hide();
        }
    },
    hide: function() {
        this.bodyClickObserver.stop();
        this.layer.close();
    },
    fixedPosition: function() {
        var pos = Ten.Geometry.getElementPosition(this.el);
        this.layer.moveTo(pos.x - 15, pos.y + 20);
    },
    show: function() {
        if (this.layer.shown) return;

        this.fixedPosition();
        this.layer.open();
        var self = this;
        setTimeout(function() {
            self.bodyClickObserver.start();
        }, 0);
    }
});

Hatena.Bookmark.Tooltip.BookmarkEntry = new Hatena.Bookmark.Class({
    /* signleton + flyweight のほうがいいかなー */
    initialize: function(el, comment, isSmall) {
        this.el = el;
        this.comment = comment;
        this.isSmall = isSmall;
        this.registerEventListeners();
    },
    create: function(el, comment) {
        if (!el.tooltip) {
            el.tooltip = new Hatena.Bookmark.Tooltip.BookmarkEntry(el, comment);
        }
        el.tooltip.show();
    },
    createByURI: function(el, url) {
        if (!el.tooltip && !el.loading) {
            el.loading = true;
            el.onmouseout = function() {
                el.mouseOut = true;
            };
            var xhr = Hatena.Bookmark.User.createByIcon(el).xhr('entry');
            xhr.oldAddEventListener('complete', function(res) {
                el.loading = false;
                var res = eval('(' + res.responseText + ')');
                el.tooltip = new Hatena.Bookmark.Tooltip.BookmarkEntry(el, res, true);
                if (!el.mouseOut) el.tooltip.show();
            });
            xhr.load({url: url});
        }
        if (el.tooltip) {
          el.tooltip.show();
        }
    },
    createByURI: function(el, url) {
        if (!el.tooltip && !el.loading) {
            el.loading = true;
            el.onmouseout = function() {
                el.mouseOut = true;
            };
            var xhr = Hatena.Bookmark.User.createByIcon(el).xhr('entry');
            xhr.oldAddEventListener('complete', function(res) {
                el.loading = false;
                var res = eval('(' + res.responseText + ')');
                el.tooltip = new Hatena.Bookmark.Tooltip.BookmarkEntry(el, res, true);
                if (!el.mouseOut) el.tooltip.show();
            });
            xhr.load({url: url});
        }
        if (el.tooltip) {
          el.tooltip.show();
        }
    }
}, {
    registerEventListeners: function() {
        this.doubleClickCount = 0;
        // new Ten.Observer(this.el, 'ondblclick', this, 'clickHandler');
    },
    clickHandler: function(e) {
        if ( this.doubleClickCount++ < 1 ) return;
        if (!window._mouse_move_observer) {
            window._mouse_position = e.mousePosition();
            window._mouse_move_observer = new Ten.Observer(window, 'onmousemove', function(_e) {
                window._mouse_position = _e.mousePosition();
            });
        }
        this.mouseoutHandler();
        var element = this.el;
        var pos = Ten.Geometry.getElementPosition(element);
        element.style.top = Math.round(pos.y) + 'px';
        element.style.left = Math.round(pos.x) + 'px';
        element.style.position = 'absolute';
        Ten.DOM.removeElement(element);
        document.body.appendChild(element);

        this.loopTween();
    },
    loopTween: function() {
          var element = this.el;
          var mp = window._mouse_position;
          JSTweener.addTween(element.style, {
             time: 0.2 + 0.5*Math.random(),
             onComplete: Ten.Function.method(this, 'loopTween'),
             suffix: { left: 'px', top: 'px' },
             left: Math.round(mp.x - 100 + Math.random() * 200),
             top: Math.round(mp.y - 100 + Math.random() * 200)
         });
    },
    show: function() {
        if (!this.layer)
            this.createTooltip();
        this.layer.open();
        var highlightEL = this.getHighlightId();
        if (highlightEL)
            Ten.DOM.addClassName(highlightEL, 'bookmark-list-highlight');
        this.fixedPosition();
        if (!this.mouseoutObserver) {
            this.mouseoutObserver = new Ten.Observer(this.el, 'onmouseout', this, 'mouseoutHandler');
        } else {
            this.mouseoutObserver.start();
        }
    },
    getHighlightId: function() {
        return document.getElementById('bookmark-user-' + this.comment.user);
    },
    mouseoutHandler: function(e) {
        this.layer.close();
        var highlightEL = this.getHighlightId();
        if (highlightEL)
            Ten.DOM.removeClassName(highlightEL, 'bookmark-list-highlight');
        this.mouseoutObserver.stop();
    },
    fixedPosition: function() {
        var t = this.layer.div;
        var pos = Ten.Geometry.getElementPosition(this.el);
        var y = pos.y + this.el.offsetHeight + (this.isSmall ? 6 : 14);
        var l = pos.x + (this.isSmall ? 6 : 10)
        var w = Ten.Geometry.getWindowSize().w;
        if (w < (t.offsetWidth + l)) {
            l = w - t.offsetWidth - 20;
        }
        var x = l;
        this.layer.moveTo(x, y);
    },
    createTooltip: function() {
        var E = Ten.Element;
        var comment = this.comment;
        this.layer = new Hatena.Bookmark.LayerWindow;
        Ten.DOM.addClassName(this.layer.div, 'tooltip bookmark-entry-tooltip');
        this.layer.div.appendChild(E('div', { className: 'bookmark-entry-tooltip-header'},
             Hatena.Bookmark.ViewHelper.profileIcon(comment.user),
             E('span', { className:'username' }, comment.user)
        ));

        if (comment.tags && comment.tags.length) {
            var tags = E('span', { className: 'tags' });
            Hatena.Bookmark.CommentEditorBase.prototype.replaceTags(Hatena.Bookmark.user, tags, comment.tags);
            this.layer.div.appendChild(tags);
        }
        if (comment.comment)
            this.layer.div.appendChild(E('span', { className: 'comment' }, comment.comment));
        if (comment.timestamp)
            this.layer.div.appendChild(E('span', { className: 'timestamp' }, comment.timestamp));
    }
});

Hatena.Bookmark.Tooltip.TagSponsor = new Hatena.Bookmark.Class({
    base: [Hatena.Bookmark.Tooltip.BookmarkEntry]
}, {
});

Hatena.Bookmark.LayerWindow = new Hatena.Bookmark.Class({
    initialize: function(options/* parent */) {
        if (!options) options = {};
        this.parent = options.parent || document.body;
        this.createCloseButton = options.createCloseButton;
        this.createWindow();
        this.registerEventListeners();
        this.shown = false;
    }
}, {
    moveTo: function(x, y) {
        this.div.style.left = '' + parseInt(x) + 'px';
        this.div.style.top = '' + parseInt(y) + 'px';
        this.dispatchEvent('move');
    },
    registerEventListeners: function() {
        if (this.closeButton)
            this.closeButtonObserver = new Ten.Observer(this.closeButton, 'onclick', this, 'closeButtonHandler');
    },
    closeButtonHandler: function() {
        this.dispatchEvent('click_close_button');
        this.close();
    },
    createWindow: function() {
        var E = Ten.Element;
        this.div = E('div', {className: 'layer-window'});
        if (this.createCloseButton) {
            this.closeButton = E('img', {src: this.createCloseButton, className: 'layer-window-close-button'})
            this.div.appendChild(this.closeButton);
        }
    },
    open: function() {
        if (this.insertNode) {
            Ten.DOM.insertAfter(this.div, this.insertNode);
        } else {
            this.parent.appendChild(this.div);
        }
        this.shown = true;
        this.dispatchEvent('show');
    },
    close: function() {
        this.shown = false;
        if (this.div.parentNode)
            this.div.parentNode.removeChild(this.div);
            this.dispatchEvent('close');
    }
});

/*
 * フォームの入力に応じて動的に出力 (表示) を変化させる
 */
Hatena.Bookmark.FormOutput = new Hatena.Bookmark.Class({
    initialize: function(input, output, action) {
        this.input = input;
        this.output = output;
        this.action = action;
        this.registerEventListeners();
        this.rebuild();
    },
    create: function(input, output, action) {
        if (input[0] && input[0].type === 'radio')
            return new this.Radios(input, output, action);
        if (input.type === 'checkbox')
            return new this.Checkbox(input, output, action);
        return new this(input, output, action);
    },
    createTwitterCheck: function(input, output) {
        if (!input || !output) return null;
        return this.create(input, output, this.twitterCheckAction);
    },
    twitterCheckAction: function(input, output) {
        var label = output.parentNode;
        if (!label.disabledTitle) {
            label.enabledTitle = label.title;
            label.disabledTitle = label.title + '(非公開ブックマークはTwitterへ投稿されません。)';
            output.defaultChecked = output.checked;
        }
        if (input.checked) {
            output.defaultChecked = output.checked;
            output.checked = false;
            output.disabled = true;
            label.title = label.disabledTitle;
            Ten.DOM.addClassName(label, 'disabled');
        } else {
            output.checked = output.defaultChecked;
            output.disabled = false;
            label.title = label.enabledTitle;
            Ten.DOM.removeClassName(label, 'disabled');
        }
    }
}, {
    registerEventListeners: function() {
        new Ten.Observer(this.input, 'onchange', this, 'rebuild');
    },
    getInput: function() {
        return this.input;
    },
    rebuild: function() {
        this.action(this.getInput(), this.output);
    }
});

Hatena.Bookmark.FormOutput.Radios = new Hatena.Bookmark.Class({
    base: [Hatena.Bookmark.FormOutput]
}, {
    registerEventListeners: function() {
        for (var i = 0; i < this.input.length; i++)
            new Ten.Observer(this.input[i], 'onclick', this, 'rebuild');
    },
    getInput: function() {
        for (var i = 0; i < this.input.length; i++)
            if (this.input[i].checked)
                return this.input[i];
        return null;
    }
});

Hatena.Bookmark.FormOutput.Checkbox = new Hatena.Bookmark.Class({
    base: [Hatena.Bookmark.FormOutput]
}, {
    registerEventListeners: function() {
        new Ten.Observer(this.input, 'onclick', this, 'rebuild');
    }
});

/*
 * ユーザ情報表示, follow/unfollow/表示/非表示
 */
Hatena.Bookmark.UserNavigator = new Hatena.Bookmark.Class({
    initialize: function(userImage) {
        this.username = Hatena.Bookmark.User.getUserNameByIcon(userImage);
        if (Hatena.Bookmark.user && Hatena.Bookmark.user.name == this.username)
            return;

        this.userImage = userImage;
        this.user = Hatena.Bookmark.User.getInstance(this.username);
        this.viewer = Hatena.Bookmark.UserNavigator.Viewer.getInstance();
        this.registerEventListeners();
    },
    registerOnLoadBySelector: function(cssSelector) {
        var elements = Ten.Selector.getElementsBySelector(cssSelector);
        for (var i = 0;  i < elements.length; i++) {
            Hatena.Bookmark.UserNavigator.registerOnLoad(elements[i]);
        }
    },
    registerOnLoad: function(parent) {
        if (!Hatena.Bookmark.user) return;

        if (typeof parent == 'string') {
            parent = document.getElementById(parent);
        } else if (parent) {
            //
        } else {
            parent = document.body;
        }
        Ten.DOM.addEventListener('onload', function() {
            if (!parent) return;
            var elements = Ten.DOM.getElementsByTagAndClassName('img', 'profile-image', parent);
            for (var i = 0;  i < elements.length; i++) {
                var element = elements[i];
                element.userNavigator = new Hatena.Bookmark.UserNavigator(element);
            }
        });
    }
}, {
    registerEventListeners: function() {
        this.mouseoverObserver = new Ten.Observer(this.userImage, 'onmouseover', this, 'mouseoverHandler');
    },
    /*
    getElement: function() {
        if (!Hatena.Bookmark.user || Hatena.Bookmark.user.name == this.username) return;

        return this.user.getInformationHTML();
    },
    getFollowElement: function() {
        if (!Hatena.Bookmark.user || Hatena.Bookmark.user.name == this.username) return;

        if (!this._followElement) {
            var span = new Ten.Element('span');
            this._followElement = new Ten.Element('a', {}, span);
            Hatena.Bookmark.FavoriteEdit.TextLink.create(span, this.username, true);
        }
        return this._followElement;
    },
    getIgnoreElement: function() {
        if (!Hatena.Bookmark.user || Hatena.Bookmark.user.name == this.username) return;

        if (!this._ignoreElement) {
            var span = new Ten.Element('span');
            this._ignoreElement = new Ten.Element('a', {}, span);
            Hatena.Bookmark.IgnoreEdit.TextLink.create(span, this.username, true);
        }
        return this._ignoreElement;
    },
    */
    mouseoverHandler: function() {
        if (this.viewer.canUpdate()) {
            this.viewer.setNavigator(this);
            this.viewer.fixedPosition();
            this.viewer.show();
        }
    }
});

/* singleton + flyweight */
Hatena.Bookmark.UserNavigator.Viewer = new Hatena.Bookmark.Class({
    initialize: function() {
        this.createElement();
        this.timer = new Ten.Timer(1000, 1);
        this.timer.addEventListener('timer', Ten.Function.method(this, 'timerHandler'));
    },
    getInstance: function() {
        if (!Hatena.Bookmark.UserNavigator.Viewer._instance) {
            Hatena.Bookmark.UserNavigator.Viewer._instance = new Hatena.Bookmark.UserNavigator.Viewer;
        }
        return Hatena.Bookmark.UserNavigator.Viewer._instance;
    }
}, {
    mouseoverHandler: function(e) {
        this.timer.reset();
        if (this.mouseoutObserver) {
            this.mouseoutObserver.start();
        } else {
            this.mouseoutObserver = new Ten.Observer(this.element, 'onmouseout', this, 'mouseoutHandler');
        }
    },
    mouseoutHandler: function(e) {
        this.mouseoutObserver.stop();
        this.timer.reset();
        this.timer.start();
    },
    setNavigator: function(navigator) {
        this.navigator = navigator;
        this.target = navigator.userImage;
        if (navigator.userImage.src.indexOf('profile.gif') > 0) {
            this.large = true;
            this.largeSize = navigator.userImage.offsetWidth;
        } else {
            this.large = false;
        }
        this.element.title = this.navigator.username;
    },
    createElement: function() {
        var E = Ten.Element;
        this.element = E('div',
           { className: 'user-navigator', id: 'user-navigator' },
           this.button = E('span', {id: 'user-navigator-button'}, '*')
        );
        this.button.clickObserver = new Ten.Observer(this.button, 'onmousedown', this, 'mousedownHandler');
        this.subWindow = E('div', {id: 'user-navigator-subwindow'});
        this.mouseoverObserver = new Ten.Observer(this.element, 'onmouseover', this, 'mouseoverHandler');
    },
    mousedownHandler: function(e) {
        e.stop();
        if (this.subWindow.parentNode && this.subWindow.parentNode.tagName) {
            this.subWindow.parentNode.removeChild(this.subWindow);
            Ten.DOM.removeClassName(this.button, 'navigator-open');
        } else {
            this.updateSubWindow();
            document.body.appendChild(this.subWindow);
            Ten.DOM.addClassName(this.button, 'navigator-open');
        }
    },
    canUpdate: function() {
        // IE だと document.body に appendChild したあと removeChild しても、何故か parentNode への参照が存在する
        return !(this.subWindow.parentNode && this.subWindow.parentNode.tagName);
    },
    updateSubWindow: function() {
        Ten.DOM.removeAllChildren(this.subWindow);
        Ten.DOM.removeClassName(this.subWindow, 'favorite');
        if (!Hatena.Bookmark.user || Hatena.Bookmark.user.name == this.navigator.username) return;
        if (!this.loadingIcon) {
            this.loadingIcon = Hatena.Bookmark.ViewHelper.loadingIcon('ロード中…');
        }
        this.subWindow.appendChild(this.loadingIcon);

        var self = this;
        if (!this.loadingIcon2) {
            this.loadingIcon2 = Hatena.Bookmark.ViewHelper.loadingIcon('ロード中…');
            this.loadingIcon2.style.display = 'block';
            this.loadingIcon2.style.marginTop = '3px';
        }

        this.navigator.user.loadInformationHTML(function(html) {
            Ten.DOM.removeAllChildren(self.subWindow);
            if (Ten.DOM.hasClassName(html, 'favorite')) {
                Ten.DOM.addClassName(self.subWindow, 'favorite');
            }
            self.subWindow.appendChild(html);
            html.appendChild(self.loadingIcon2);
            self.navigator.user.loadServicesHTML(function(servicesHTML) {
                html.appendChild(servicesHTML);
                if (self.loadingIcon2.parentNode) self.loadingIcon2.parentNode.removeChild(self.loadingIcon2);
            });
        });
        /*
        var followElement = this.navigator.getFollowElement();
        if (followElement) this.subWindow.appendChild(followElement);
        var ignoreElement = this.navigator.getIgnoreElement();
        if (ignoreElement) this.subWindow.appendChild(ignoreElement);
        */
    },
    fixedPosition: function() {
        if (this.large) {
            Ten.DOM.addClassName(this.element, 'user-navigator-large');
            if (this.largeSize == 24) {
                Ten.DOM.addClassName(this.element, 'user-navigator-large-24');
            }
        } else {
            Ten.DOM.removeClassName(this.element, 'user-navigator-large');
            Ten.DOM.removeClassName(this.element, 'user-navigator-large-24');
        }
        var pos = Ten.Geometry.getElementPosition(this.target);
        this.element.style.top = pos.y - 1 + 'px';
        this.element.style.left = pos.x - 15 + 'px';
        if (this.large) {
            if (this.largeSize == 24) {
                this.subWindow.style.top = pos.y + (18 + 8) + 'px';
            } else {
                this.subWindow.style.top = pos.y + (18 + 16) + 'px';
            }
        } else {
            this.subWindow.style.top = pos.y + 18 + 'px';
        }
        var w = Ten.Geometry.getWindowSize().w;
        var l = pos.x - 15;
        var t = 250;
        if (w < (t + l)) {
            l = w - t - 20;
        }
        this.subWindow.style.left = l + 'px';

        this.element.style.width  = this.navigator.userImage.offsetWidth + 17 + 'px';
        this.element.style.height = this.navigator.userImage.offsetHeight + 4 + 'px';

    },
    show: function() {
        document.body.appendChild(this.element);
        if (this.bodyClickObserver) {
            this.bodyClickObserver.start();
        } else {
            this.bodyClickObserver = new Ten.Observer(document.body, 'onclick', this, 'bodyClickHandler');
        }
    },
    bodyClickHandler: function(e) {
        if (e.target == this.button) {
            //
        } else if (e.target == this.element && Ten.DOM.hasClassName(this.target, 'profile-image')) {
            location.href = Hatena.Bookmark.User.createByIcon(this.target).link;
        } else {
            this.hide();
        }
    },
    hide: function() {
        Ten.DOM.removeClassName(this.button, 'navigator-open');
        this.timer.reset();
        this.bodyClickObserver.stop();
        if (this.element.parentNode && this.element.parentNode.tagName) {
            this.element.parentNode.removeChild(this.element);
        }
        if (this.subWindow.parentNode && this.subWindow.parentNode.tagName) {
            this.subWindow.parentNode.removeChild(this.subWindow);
        }
    },
    timerHandler: function() {
        if (!this.subWindow.parentNode) {
            // subwindow 表示時には、timer では表示にしない
            this.hide();
        }
    }
});

Hatena.Bookmark.Navigator = new Hatena.Bookmark.Class({
    initialize: function(elements) {
        if (this.constructor.instance) return;
        this.elementPositions = [];
        if (!elements) elements = [];
        var h1 = document.getElementsByTagName('h1')[0];
        if (h1) {
            elements.unshift(h1);
        }
        this.setTargetElements(elements);
        this.caretIndex = -1;
        if (Hatena.Bookmark.isLocal) {
            // debug
            //this.wheel = new Hatena.Bookmark.Navigator.Wheel(this);
        }
        this.constructor.instance = this;
    }
}, {
    setElementsBySelector: function(cssClassNames, parent) {
        var elements = [];
        for (var i = 0;  i < cssClassNames.length; i++) {
          try {
              var el = Ten.Selector.getElementsBySelector(cssClassNames[i], parent);
              for (var j = 0;  j < el.length; j++) {
                  elements.push(el[j]);
              }
          } catch(e) {
              // Ten の CSS セレクターがエラー…
          }
        }

        this.addElements(elements);
    },
    registerEventListeners: function() {
        this.keyboard = new Hatena.Bookmark.Navigator.Keyboard(this);
        this.scrollObserver = new Ten.Observer(window, 'onscroll', this, 'scrollHandler');
    },
    scrollHandler: function(e) {
        var els = this.getNearElements();
        var el = els[1] || els[0] || els[2];
        if (!el) return;
        var url = this.getURL(el);
        if (url) {
            this.checkChangeURL(url);
        }
    },
    checkChangeURL:function (url) {
        if (this._lastURL != url) {
            this.dispatchEvent('change_url', url, this._lastURL);
            this._lastURL = url;
        }
    },
    setGlobalNavigator: function(gNavi) {
        this.gNavi = gNavi;
    },
    addTargetElements: function(es) {
        for (var i = 0;  i < es.length; i++) {
            this.addElement(es[i]);
            var el = this.targetElements[i];
            el.targetCaret = i;
        }
    },
    setTargetElements: function(es) {
        this.targetElements = [];
        this.addTargetElements(es);
    },
    addElement: function(e) {
        e.targetCaret = this.targetElements.length;
        this.targetElements.push(e);
    },
    addElements: function(es) {
        for (var i = 0;  i < es.length; i++) {
            this.addElement(es[i]);
        }
    },
    setCaret: function(num) {
        this.caretIndex = Math.max(Math.min(this.targetElements.length - 1, num), 0);
        return this.caretIndex;
    },
    getCurrentElement: function() {
        return this.targetElements[this.caretIndex];
    },
    addCurrentClassName: function() {
        var t = this.getCurrentElement();
        if (t) Ten.DOM.addClassName(t, 'current-element');
    },
    removeCurrentClassName: function() {
        for (var i = 0;  i < this.targetElements.length; i++) {
            var el = this.targetElements[i];
            Ten.DOM.removeClassName(el, 'current-element');
        }
    },
    getAppendButton: function() {
        var el = this.getCurrentElement();
        var eln;
        if (el && (eln = Ten.DOM.nextElement(el))) { // (()) for no warnings
            var imgs = eln.getElementsByTagName('img');
            for (var i = 0;  i < imgs.length; i++) {
                if (imgs[i].src.indexOf('/images/append.gif') >= 0) {
                    return imgs[i];
                }
            }
        }
    },
    showAppender: function() {
        var img = this.getAppendButton();
        if (!img) return;
        var a = img.parentNode;
        if (a && a.onclick) {
            a.onclick();
        }
    },
    getElementPosition: function(el) {
        // cache
        var tn = el.targetCaret;
        if (!this.elementPositions[tn])
            this.elementPositions[tn] = Ten.Geometry.getElementPosition(el);

        return this.elementPositions[tn];
    },
    getNearElements: function() {
        var sy = window.scrollY;
        var els = this.targetElements;
        for (var i = 0;  i < els.length; i++) {
            var el = els[i];
            var pos = this.getElementPosition(el);
            if (pos.y > sy) {
                return [els[i], el, els[i+1]];
            }
        }
        return [];
    },
    move: function(num) {
        // 最大で 1
        num = Math.max(Math.min(1, num), -1);
        this.setCaret(this.caretIndex + num);
        this.currentize();
    },
    next: function() {
        this.move(1);
    },
    prev: function() {
        this.move(-1);
    },
    globalSearch: function() {
        if (this.gNavi) this.gNavi.search();
    },
    currentize: function() {
        var t = this.getCurrentElement();
        if (t) {
            this.removeCurrentClassName();
            this.addCurrentClassName();
            this.moveTo(t);
            return t;
        }
    },
    moveTo: function(element) {
        var pos;
        if (element.tagName.toUpperCase() == 'H1') {
            pos = {x:0, y:0};
        } else {
            pos = this.getElementPosition(element);
        }

        var pix = 20;
        if (Ten.DOM.hasClassName(document.body, 'fixed-header')) {
            pix += 36;
        }
        var y = pos.y - pix;
        this.heightSpacer(y);
        window.scrollTo(0, y);
    },
    heightSpacer: function(y) {
        var g = Ten.Geometry;
        var docY = g.getDocumentSize().h;
        var winY = g.getWindowSize().h;
        if (winY + y > docY) {
            var spacerHeight = docY - y;
            if (!this.spacer) {
                var s = new Ten.Element('div', {className: 'spacer'}, ' ');
                document.body.appendChild(s);
                this.spacer = s;
            }
            this.spacer.style.height = spacerHeight + 'px';
        } else {
        }
    },
    getNextURL: function() {
        var el = Ten.Selector.getElementsBySelector('a.pager-next')[0];
        if (el && el.href) return el.href;
    },
    open: function(entry) {
        var t = this.getCurrentElement();
        if (t) {
            var url = this.getURL(t);
            if (url) {
                if (entry) {
                    url = Hatena.Bookmark.StringHelper.entryURL(url);
                }
                this.openURL(url);
            }
        }
    },
    getURL: function(t) {
        if (!t) return;
        var a = t.getElementsByTagName('a')[0];
        if (a) {
            var url = a.href;
            return url;
        } else {
            if (t.tagName && t.tagName.toUpperCase() == 'A') {
                return t.href;
            }
        }
    },
    openURL: function(url) {
        window.open(url, '');
    },
    setNearCaret: function(num) {
        var els = this.getNearElements();
        var el;
        if (num < 0) {
            el = els[0] || els[1];
        } else {
            el = els[1] || els[0];
        }
        if (el)
            this.setCaret(el.targetCaret);
    },
    moveToNear: function() {
        this.setNearCaret();
        var el = this.currentize();
    }
});

Hatena.Bookmark.Navigator.Keyboard = new Hatena.Bookmark.Class({
    initialize: function(navigator, target) {
        this.navigator = navigator;
        if (!target) {
            if (Ten.Browser.isMozilla || Ten.Browser.isOpera) {
                target = document;
            } else {
                target = document.body;
            }
        }
        this.target = target;
        this.registerEventListeners();
        this.y = 0;
    }
}, {
    registerEventListeners: function() {
        var t = this.target;
        this.keypressObserver = new Ten.Observer(t, 'onkeypress', this, 'keypressHandler');
    },
    keypressHandler: function(e) {
        var tag = e.target.tagName.toUpperCase();
        var pos = Ten.Geometry.getScroll();
        if (tag == 'HTML' ||
            tag == 'BODY' ||
            tag == 'DIV') {
            var ev = e.event;
            if (ev.ctrlKey || ev.shiftKey || ev.altKey || ev.metaKey) return;

            var keyCode = e.event.charCode || e.event.keyCode;
            var c = String.fromCharCode(e.event.charCode || e.event.keyCode);
            if (keyCode == 13) c = 'o'; // enter
            var pressed = false;
            switch( c ) {
                case 'j':
                    pressed = true;
                    if (this.y != pos.y) {
                        this.navigator.getNearElements();
                        this.navigator.moveToNear();
                        this.y = pos.y;
                    } else {
                        this.navigator.next();
                        pos = Ten.Geometry.getScroll();
                    }
                    this.y = pos.y;
                    break;
                case 'k':
                    pressed = true;
                    if (this.y != pos.y) {
                        this.navigator.getNearElements();
                        this.navigator.moveToNear();
                        this.y = pos.y;
                    } else {
                        this.navigator.prev();
                        pos = Ten.Geometry.getScroll();
                    }
                    this.y = pos.y;
                    break;
                case 's':
                    pressed = true;
                    this.navigator.globalSearch();
                    break;
                case 'b':
                    pressed = true;
                    this.navigator.showAppender();
                    break;
                case 'o':
                    pressed = true;
                    this.navigator.open();
                    break;
                case 'e':
                    pressed = true;
                    this.navigator.open(true);
                    break;
            }
            if (pressed) {
                e.stop();
            }
        }
    }
});

Hatena.Bookmark.Navigator.Wheel = new Hatena.Bookmark.Class({
    initialize: function(navigator) {
        this.navigator = navigator;
        this.timer = new Ten.Timer(400, 1);
        this.timer.addEventListener('timerComplete', Ten.Function.method(this, 'timerCompleteHandler'));
        this.bodyDBLClickObserver = new Ten.Observer(document.body, 'ondblclick', this, 'bodyDBLClickHandler');

        this.cookie = new Ten.Cookie;
        if (this.cookie.get('wheel_view')) {
          this.show();
          var pos_s = this.cookie.get('wheel_view_pos');
          if (pos_s) {
              //this.cookie.set('wheel_view_pos', '');
              var pos_ary = pos_s.split(',');
              this.fixedPosition({x: Number(pos_ary[0]), y: Number(pos_ary[1])});
          }
        }
    }
}, {
    registerEventListeners: function() {
    },
    timerCompleteHandler: function() {
        if (this.bodyClickObserver) this.bodyClickObserver.stop();
    },
    bodyDBLClickHandler: function(e) {
        e.stop();
        this.timer.start();
        if (!this.bodyClickObserver) {
            this.bodyClickObserver =  new Ten.Observer(document.body, 'onclick', this, 'bodyClickHandler');
        } else {
            this.bodyClickObserver.start();
        }
    },
    bodyClickHandler: function(e) {
        this.timer.reset();
        e.stop();

        // テキスト選択の解除 Firefox only
        var selection = window.getSelection();
        selection.collapse(document.body, 0);

        this.bodyClickObserver.stop();
        this.show();
        this.savePosition(e.mousePosition());
        this.fixedPosition(e.mousePosition());
    },
    savePosition: function(pos) {
        this.cookie.set('wheel_view_pos', pos.x + ',' + pos.y);
    },
    fixedPosition: function(pos) {
        this.wheelEL.style.top = pos.y - window.scrollY + 3 + 'px';
        this.wheelEL.style.left = pos.x + 3 + 'px';
    },
    show: function() {
        this.cookie.set('wheel_view', 1, '+10y');
        if (!this.wheelEL) {
            this.createNavigator();
        }
        if (!this.wheelEL.parentNode)
            document.body.appendChild(this.wheelEL);
    },
    hide: function() {
        this.cookie.clear('wheel_view');
        this.cookie.clear('wheel_view_pos');
        if (this.wheelEL.parentNode)
            document.body.removeChild(this.wheelEL);
    },
    mouseWheelHandler: function(e) {
        e.stop();
        var move = e.event.detail;
        if (this.outed) {
            this.navigator.currentize();
            this.outed = 0;
            return;
        }

        this.navigator.move(move);
    },
    createNavigator: function() {
        var E = Ten.Element;
        this.wheelEL = E('div', {id: 'wheel-navigator'},
            this.wheelTarget = E('div', {id: 'wheel-target'})
        );
        if (this.navigator.getNextURL()) {
            this.wheelEL.appendChild(this.nextLink = E('img', {
                alt: 'next',
                title: 'next',
                src: '/images/refered.gif',
                style: '' }));
        }
        this.wheelEL.appendChild(this.closeButton = E('img', {
            alt: 'close',
            title: 'close',
            src: '/images/bookmarklet_close.gif',
            style: '' }));
        var self = this; // zantei
        this.closeButton.onclick = function() { self.hide() };

        this.registerEventListeners();
    },
    registerEventListeners: function() {
        this.mouseWheelObserver = new Ten.Observer(this.wheelTarget, 'DOMMouseScroll', this, 'mouseWheelHandler');

        var mt = this.wheelTarget;
        this.clickObserver = new Ten.Observer(mt, 'onmousedown', this, 'clickHandler');
        //this.dblclickObserver = new Ten.Observer(mt, 'ondblclick', this, 'dblclickHandler');
        this.mouseoutObserver = new Ten.Observer(this.wheelEL, 'onmouseout', this, 'mouseoutHandler');
        this.mouseoverObserver = new Ten.Observer(this.wheelEL, 'onmouseover', this, 'mouseoverHandler');
        if (this.nextLink)
           this.nextLinkObserver = new Ten.Observer(this.nextLink, 'onclick', this, 'nextLinkClickHandler');

    },
    nextLinkClickHandler: function(e) {
        e.stop();
        location.href = this.navigator.getNextURL();
    },
    dblclickHandler: function(e) {
        e.stop();
        this.navigator.open(true);
    },
    clickHandler: function(e) {
        e.stop();
        if (e.event.button == 2) {
            this.navigator.openNext();
        } else {
            this.navigator.open();
            this.navigator.open(true);
        }
    },
    mouseoutHandler: function() {
        this.outed = 1;
        this.navigator.removeCurrentClassName();
    },
    mouseoverHandler: function() {
        this.navigator.addCurrentClassName();
        this.navigator.setNearCaret();
    }
});

Hatena.Bookmark.AutoPagerize = new Hatena.Bookmark.Class({
    initialize: function(selector, insertionSelector, pagerSelector) {
        if (this.constructor.instance) return;

        this.selector = selector;
        this.insertionSelector = insertionSelector;
        this.beforeFilterTriggers = [];

        if (!this.init()) return; // init が false = autopagerize らない
        this.constructor.instance = this;

        this.cookie = new Ten.Cookie;
        if (this.cookie.get('autopagerize')) {
            this.active();
        } else {
            this.inactive();
        }
        //this.registerEventListeners();
    }
}, {
    registerEventListeners: function() {
        // firefox only?
        if (window.addEventListener) {
            if (this.scrollObserver) {
                this.scrollObserver.start();
            } else {
                this.scrollObserver = new Ten.Observer(window, 'onscroll', this, 'scrollHandler');
            }
        } else {
            if (this.scrollObserver) {
                this.scrollObserver.start();
            } else {
                if (Ten.Browser.isIE6 && document.documentElement) {
                    document.documentElement.onscroll = Ten.Function.method(this, 'scrollHandler');
                } else {
                    this.scrollObserver = new Ten.Observer(window, 'onscroll', this, 'scrollHandler');
                }
            }
        }
    },
    removeEventLisners: function() {
        if (this.scrollObserver) this.scrollObserver.stop();

         if (Ten.Browser.isIE6 && document.documentElement && document.documentElement.onscroll) {
             document.documentElement.onscroll = function() {};
         }
    },
    active: function() {
        this.registerEventListeners();
        this.scrollHandler();
        this.pager.button.active();
        this.cookie.set('autopagerize', 1, '+10y');
    },
    inactive: function() {
        this.removeEventLisners();
        this.pager.button.inactive();
        this.cookie.clear('autopagerize');
    },
    init: function() {
        this.nextURL = this.linkRelNext(document);
        var pager = this.insertionElement(document.body);
        if (pager && pager.getElementsByTagName('a').length) {
            this.setPager(pager);
            return true;
        } else {
            return false;
        }
    },
    setPager: function(pager) {
        if (pager) {
            this.pager = pager;
            Ten.DOM.addClassName(this.pager, 'pager-autopagerize');
            if (!pager.activeButton) {
                pager.button = new Hatena.Bookmark.AutoPagerize.ToggleButton;
                pager.button.oldAddEventListener('active', function () {
                    Hatena.Bookmark.Tutorial.check('use_autoload_page');
                });
                pager.button.oldAddEventListener('active', Ten.Function.method(this, 'active'));
                pager.button.oldAddEventListener('inactive', Ten.Function.method(this, 'inactive'));
                Ten.DOM.unshiftChild(pager, pager.button.container);
            }
        }
    },
    toggle: function(toggle) {
    },
    linkRelNext: function(el) {
        var links = el.getElementsByTagName('link');
        if (links.length == 0) {
            var source = el.innerHTML;
            var m = source.match(/prev.+?href="([^"]+)".+\d年/i);
            if (m && m[1]) return m[1];

            m = source.match(/next.+?href="([^"]+)"/i);
            if (m && m[1]) return m[1];
        } else {
            for (var i = links.length - 1;  i >= 0; i--) {
                var link = links[i];
                if (link.rel == 'next') {
                    return link.href;
                }
            }

        }
    },
    scrollHandler: function() {
        var w = window;
        if (!this.xhr) {
           var insertion = this.insertionElement();
           var bottom = this.getElementBottom(insertion);
           var g = Ten.Geometry;
           g._initialized = false;
           var y = g.getScroll().y;
           var h = g.getWindowSize().h;
           if ((bottom - y - h) < 1000) {
               this.next();
           }
        }
    },
    getElementBottom: function(elem) {
        var cStyle = elem.currentStyle;

        if (Ten.Browser.isSafari) {
            var offsetTrail = elem;
            var offsetLeft  = 0;
            var offsetTop   = 0;
            while (offsetTrail) {
                    offsetLeft += offsetTrail.offsetLeft;
                    offsetTop  += offsetTrail.offsetTop;
                    offsetTrail = offsetTrail.offsetParent;
            }
            offsetTop = offsetTop || null;
            offsetLeft = offsetLeft || null;
            return offsetTop;
        }

        if (!cStyle) {
            var s = document.defaultView.getComputedStyle(elem, null);
        }
        var height = 0;
        var prop = ['height', 'borderTopWidth', 'borderBottomWidth', 'paddingTop', 'paddingBottom', 'marginTop', 'marginBottom']
        for (var i = 0;  i < prop.length; i++) {
            var h;
            if (cStyle) {
                h = parseInt(cStyle[prop[i]]);
            } else {
                h = parseInt(s.getPropertyValue([prop[i]]));
            }
            if (typeof h == 'number' && !isNaN(h)) {
                height += h
            }
        }
        var top = elem.offsetTop;
        return top ? (top + height) : null;
    },
    next: function() {
        if (this.xhr) return;
        if (!this.nextURL) return;
        this.pagerLoad();
        this.dispatchEvent('load');
        var xhr = new Hatena.Bookmark.XHR(this.nextURL);
        xhr.timeout = 20000;
        xhr.retry = 3;
        this.xhr = xhr;
        xhr.onComplete(this, 'nextCallback');
        xhr.load(this.query);
    },
    insertionElement: function(el) {
        return Ten.Selector.getElementsBySelector(this.insertionSelector, el || document.body)[0];
    },
    pagerLoad: function() {
        if (!this.pager.info) {
            var E = Ten.Element;
            var info = E('span', {className: 'info'});
            Ten.DOM.unshiftChild(this.pager, info);
            this.pager.info = info;
        }
        Ten.DOM.removeAllChildren(this.pager.info);
        this.pager.info.appendChild(Hatena.Bookmark.ViewHelper.loadingIcon(''));
    },
    pagerComplete: function() {
        Ten.DOM.addClassName(this.pager, 'pager-autopagerize-complete');
        if (this.pager.info) {
            Ten.DOM.removeAllChildren(this.pager.info);
        }
    },
    nextCallback: function(res) {
        this.pagerComplete();
        this.xhr = null;
        var div = document.createElement('div');
        div.innerHTML = res.responseText;
        this.nextURL = this.linkRelNext(div);
        var el = Ten.Selector.getElementsBySelector(this.selector, div)[0];
        el.style.clear = 'both';

        this.beforeFilterTrigger(el);
        el = this.filterElements(el);
        Ten.DOM.insertAfter(el, this.pager);
        this.lastInsertElements = el;

        var pager = this.insertionElement(div);
        this.setPager(pager);
        if (pager) {
            Ten.DOM.insertAfter(pager, el);
        }
        var hn = Hatena.Bookmark.Navigator.instance;
        if (hn) {
            hn.addElements(el.getElementsByTagName('h3'));
        }
        Ten.Geometry._initialized = false;

        Hatena.Bookmark.Star.loadStar('ul.comment li', el);
        // XXX: dirty
        if (location.href.indexOf('/articles') >= 0) {
            Hatena.Bookmark.Star.loadStar('div.section h3', el);
        }
        this.dispatchEvent('complete');
    },
    addBeforeFilterTrigger: function(func) {
        this.beforeFilterTriggers.push(func);
    },
    beforeFilterTrigger: function(el) {
        for (var i = 0;  i < this.beforeFilterTriggers.length; i++) {
            this.beforeFilterTriggers[i](el);
        }
    },
    filterElements: function(el) {
        if (!this.lastInsertElements) return el;

        var urls = {};
        var _els = Ten.Selector.getElementsBySelector('h3 > a:first-child', this.lastInsertElements);
        for (var i = 0;  i < _els.length; i++) {
            urls[_els[i].href] = true;
        }
        var els = Ten.Selector.getElementsBySelector('h3 > a:first-child', el);
        for (var i = 0;  i < els.length; i++) {
            var e = els[i];
            if (urls[e.href]) {
                if (e.parentNode.parentNode.tagName.toUpperCase() == 'LI') {
                    Ten.DOM.removeElement(e.parentNode.parentNode);
                } else if (e.parentNode.parentNode.parentNode.tagName.toUpperCase() == 'LI') {
                    Ten.DOM.removeElement(e.parentNode.parentNode.parentNode);
                }
            }
        }
        return el;
    },
    detect: function() {
    }
});

Hatena.Bookmark.AutoPagerize.ToggleButton = new Hatena.Bookmark.Class({
    LOCK_IMG: '/images/auto_pagerize_on.gif',
    LOCK_CANCEL_IMG: '/images/auto_pagerize.gif',
    BUTTONS: [],
    base: [Hatena.Bookmark.Toggle],
    initialize: function() {
        var E = Ten.Element;
        this.activeImg = E('img', { src: this.constructor.LOCK_IMG, className: 'pointer', alt: '自動ロード有効', title: 'ページ自動ロードを無効にする', width: 16, height: 16});
        this.inactiveImg = E('img', { src: this.constructor.LOCK_CANCEL_IMG, className: 'pointer', alt: '自動ロード無効', title:'ページ自動ロードを有効にする', width: 16, height: 16 });
        this.container = E('span');
        this.constructor.SUPER.call(this, this.container, [this.activeImg, this.inactiveImg]);
        this.constructor.BUTTONS.push(this);
        this.oldAddEventListener('toggle', Ten.Function.method(this, 'toggleHandler'));
    }
}, {
    active: function() {
        for (var i = 0;  i < this.constructor.BUTTONS.length; i++) {
            var b = this.constructor.BUTTONS[i];
            b.setCurrent(b.activeImg);
        }
    },
    inactive: function() {
        for (var i = 0;  i < this.constructor.BUTTONS.length; i++) {
            var b = this.constructor.BUTTONS[i];
            b.setCurrent(b.inactiveImg);
        }
    },
    toggleHandler: function(newEL, el) {
        if (this.activeImg == newEL) {
            this.dispatchEvent('active');
        } else {
            this.dispatchEvent('inactive');
        }
    }
});

Hatena.Bookmark.SearchQuery = new Hatena.Bookmark.Class({
    initialize: function(q, list) {
        this.q = document.getElementById(q);
        this.list = [];
        for (var i=0; i < list.length; i++) {
            this.list.push(document.getElementById(list[i]));
        }
        new Ten.Observer(this.q, 'onchange', this, 'updateLinks');
        new Ten.Observer(this.q, 'onkeyup', this, 'updateLinks');
    }
}, {
    updateLinks: function() {
        var query = encodeURIComponent(this.q.value);
        query = query.replace(/%20/g, '+').replace(/&/g, '%26');
        for (var i=0; i < this.list.length; i++) {
            this.updateLink(query, this.list[i]);
        }
    },
    updateLink: function(query, link) {
        var url = new String(link.href)
            .replace(/([?&]q=)[^&]*(&?)/, '$1'+query+'$2')
            .replace(/([?&])or=[^&]*&?/, '$1')
            .replace(/([?&])ex=[^&]*&?/, '$1')
            .replace(/([?&])url=[^&]*&?/, '$1')
            .replace(/&$/, '');
        link.href = url;
    }
});

Hatena.Bookmark.TagSearch = new Hatena.Bookmark.Class({
    initialize: function(form, list, targetURL) {
        this.form = form;
        this.input = form.getElementsByTagName('input')[0];
        this.list = list;
        this.targetURL = targetURL;
        this.registerEventListeners();
        this.lastWord = '';
    }
}, {
    showLink: function(el) {
        this.showLinkEL = el;
        var span = el.getElementsByTagName('span')[0];
        span.onclick = Ten.Function.method(this, 'showAll');
    },
    initSearch: function() {
        if (!this._initSearch) {
            this._initSearch = true;
            this.showAll();
        }
    },
    showAll: function() {
        if (this.list[0].parentNode) {
            Ten.DOM.addClassName(this.list[0].parentNode, 'searching');
        }
        if (this.showLinkEL)
            Ten.DOM.removeElement(this.showLinkEL);
    },
    registerEventListeners: function() {
        this.input.keyupObserver = new Ten.Observer(this.input, 'onkeyup', this, 'keyupHandler');
        this.form.submitObserver = new Ten.Observer(this.form, 'onsubmit', this, 'submitHandler');
    },
    submitHandler: function(e) {
        e.stop();
        // 一件なら redirect る
        if (this.oneWord)
            location.href = this.targetURL + encodeURIComponent(this.oneWord) + '/';
    },
    keyupHandler: function(e) {
        this.initSearch();
        var word = this.input.value;
        if (word == this.lastWord) return;
        this.lastWord = word;
        this.detect(word);
    },
    detect: function(word) {
        var list = this.list;
        word = word.toUpperCase();
        var words = [];
        for (var i = 0;  i < list.length; i++) {
            var el = list[i];
            if (typeof el['rawText'] == 'undefined') {
                el['rawText'] = Ten.DOM.scrapeText(el.getElementsByTagName('a')[0]);
                el['rawTextUC'] = el['rawText'].toUpperCase(); // cache
            }
            var text = el.rawTextUC;
            if (text.indexOf(word) >= 0) {
                el.style.display = '';
                words.push(el.rawText);
            } else {
                el.style.display = 'none';
            }
        }
        if (words.length == 1) {
            this.oneWord = words.pop();
        } else {
            delete this.oneWord;
        }
    },
    setToggle: function(tagCloudSetting) {
        tagCloudSetting.oldAddEventListener('toggle', Ten.Function.method(this, 'showAll'));
    }
});

Hatena.Bookmark.Toggle.TagCloudSetting = new Hatena.Bookmark.Class({
    base: [Hatena.Bookmark.Toggle],
    initialize: function(target) {
        this.target = target;
        this.container = document.getElementById('tag-cloud-setting');
        this.container.style.display = 'block';
        this.flat = document.getElementById('flat-setting-button');
        this.cloud = document.getElementById('cloud-setting-button');
        this.cookie = new Ten.Cookie;
        var i = 1;
        if (this.cookie.get('tag_cloud'))
            i = 0;

        this.constructor.SUPER.call(this, this.container, [this.flat, this.cloud], i);
        this.oldAddEventListener('toggle', Ten.Function.method(this, 'toggleHandler'));
    }
}, {
    toggleHandler: function(newEL, el) {
        if (this.flat != newEL) {
            this.cookie.clear('tag_cloud');
            Ten.DOM.removeClassName(this.target, 'tag-cloud');
        } else {
            this.cookie.set('tag_cloud', 1, '+10y');
            Ten.DOM.addClassName(this.target, 'tag-cloud');
        }
    }
});

if (typeof Hatena.Bookmark.Pager == 'undefined')
    Hatena.Bookmark.Pager = {};

Hatena.Bookmark.Pager.Binary = new Hatena.Bookmark.Class({
    initialize: function(container) {
        this.hasNext = false;
        this.hasPrev = false;
        this.createPager();
        if (container)
            container.appendChild(this.pager);
        this.registerEventListeners();
    }
}, {
    registerEventListeners: function() {
        this.prevClickObserver = new Ten.Observer(this.prev, 'onclick', this, 'clickHandler');
        this.nextClickObserver = new Ten.Observer(this.next, 'onclick', this, 'clickHandler');
    },
    clear: function() {
        this.hasNext = false;
        this.hasPrev = false;
        this.update();
    },
    clickHandler: function(e) {
        if (e.target == this.prev) {
            this.dispatchEvent('prev');
        } else if (e.target == this.next) {
            this.dispatchEvent('next');
        }
    },
    createPager: function() {
        var E = Ten.Element;
        this.pager = E('div', {className: 'pager'},
          this.prev = E('span', {className: 'pager-prev'}, '< 前のページ'),
          ' ',
          this.next = E('span', {className: 'pager-next'}, '次のページ >'));
        this.update();
    },
    update: function() {
        if (this.hasPrev) {
            Ten.DOM.addClassName(this.prev, 'shown');
        } else {
            Ten.DOM.removeClassName(this.prev, 'shown');
        }
        if (this.hasNext) {
            Ten.DOM.addClassName(this.next, 'shown');
        } else {
            Ten.DOM.removeClassName(this.next, 'shown');
        }
        //this.prev.style.display = this.hasPrev ? '' : 'none';
        //this.next.style.display = this.hasNext ? '' : 'none';
    }
});

Hatena.Bookmark.UserCalendar = new Hatena.Bookmark.Class({
    initialize: function() {
        this.registerEventListeners();
    },
    close: function() {
        if (window.confirm('カレンダーを非表示にしますか？設定 ＞ 表示から、いつでもカレンダーを元に戻せます。')) {
            var el = document.getElementById('user-sidebar-calendar');
            var loading = Ten.Element('p', {}, Hatena.Bookmark.ViewHelper.loadingIcon('処理中…'));
            Ten.DOM.removeAllChildren(el);
            el.appendChild(loading);
            Hatena.Bookmark.user.xhr('close_calendar').onComplete(function(res) {
                if (el && el.parentNode) {
                    el.parentNode.parentNode.removeChild(el.parentNode);
                }
            }).load();
        }
    }
}, {
    registerEventListeners: function() {
        var next = document.getElementById('user-calendar-next');
        var prev = document.getElementById('user-calendar-prev');
        var getDate = function(el) {
            return parseInt(el.className.split('-')[1]);
        }
        var self = this;
        if (next && ! next.clickObserver) {
            next.clickObserver = new Ten.Observer(next, 'onclick', function(e) {
                e.stop();
                var date = getDate(next);
                self.load(date);
            });
        }
        if (prev && ! prev.clickObserver) {
            prev.clickObserver = new Ten.Observer(prev, 'onclick', function(e) {
                e.stop();
                var date = getDate(prev);
                self.load(date);
            });
        }
    },
    load: function(date) {
        var author = Hatena.Bookmark.author;
        if (author) {
            var self = this;
            author.xhr('calendar_html').onComplete(function(res) {
                document.getElementById('user-sidebar-calendar').innerHTML = res.responseText;
                self.registerEventListeners();
            }).load({ date: date });
        }
    }
});

if (typeof Hatena.Bookmark.UserBookmarks == 'undefined')
    Hatena.Bookmark.UserBookmarks  = {};

Hatena.Bookmark.UserBookmarks.SearchInterface = new Hatena.Bookmark.Class({
    initialize: function(user, form, tags, limit) {
        this.user = user;
        this.form = form;
        this.input = form.getElementsByTagName('input')[0];
        this.createLayer();
        this.limit = limit || this.detectLimit();
        this.initView();
        this.searcher = new Hatena.Bookmark.UserBookmarks.Search(this.user.name, {
            limit: this.limit + 1 // for pager
        });
        this.pager = new Hatena.Bookmark.Pager.Binary();
        this.timer = new Ten.Timer(500, 1);
        // this.timer.addEventListener('timer', Ten.Function.method(this, 'timerHandler'));
        this.registerEventListeners();
        this.offsetStack = [];//...
        this.tags = tags;
    }
}, {
    detectLimit: function() {
        var h = Ten.Geometry.getWindowSize().h;
        if (h < 600) {
            return 5;
        } else if (h < 700) {
            return 6;
        } else if (h < 800) {
            return 7;
        } else if (h < 900) {
            return 8;
        } else {
            return 10;
        }
    },
    createLayer: function() {
        this.layer = new Hatena.Bookmark.LayerWindow({
            createCloseButton: '/images/appender-cancel.gif',
            parent: document.getElementById('user-search-result-container')
        });
        //this.layer.insertNode = this.form;
        Ten.DOM.addClassName(this.layer.div, 'result-window');
        var E = Ten.Element;
        this.layer.div.appendChild(this.layerInnerContainer = E('div', {className: 'result-window-inner-container'}));
        this.layerInnerContainer.appendChild(this.layerInner = E('div', {className: 'result-window-inner'}));
        this.layerInner.appendChild(
           this.progressDiv = E('div', {className: 'progress'},
               this.progressBar = E('div', {className: 'progress-bar'})
           )
        );
        this.layerInner.appendChild(this.messageContainer = E('div', {className: 'message-container'}));
        this.layerInner.appendChild(this.listContainer = E('ul', {className: 'result'}));
        if (Hatena.Bookmark.user && Hatena.Bookmark.user.isAuthorAndPlususer()) {
            this.layerInner.appendChild(this.searchMessage = E('p', {className: 'search-result-after-message'}));
        }
        this.fixedPosition();
    },
    fixedPosition: function() {
        var pos = Ten.Geometry.getElementPosition(this.form);
        var w = Ten.Geometry.getWindowSize();

        //this.layer.div.style.right = w.w - pos.x - this.form.offsetWidth + 'px';
        this.layer.div.style.right = '15px';
        this.layer.div.style.top = pos.y - this.form.offsetHeight - 30 +  'px';
        //this.layer.moveTo(0, pos.y + this.form.offsetHeight);
    },
    setTagComplete: function(tags) {
        this.completer = new Hatena.Bookmark.TagComplete;
        this.completer.addTags(tags);
        this.completer.observeInput(this.input);
        this.completer.oldAddEventListener('complete', Ten.Function.method(this, 'completerCompleteHandler'));
    },
    completerCompleteHandler: function() {
        this.searchByWord(this.input.value);
        this.input.value = this.input.value + ' ';
    },
    registerEventListeners: function() {
        this.form.clickObserver = new Ten.Observer(this.form, 'onclick', this, 'preloadHandler');
        this.form.submitObserver = new Ten.Observer(this.form, 'onsubmit', this, 'preloadSubmitHandler');
        this.input.focusHandler = new Ten.Observer(this.input, 'onfocus', this, 'preloadHandler');
        this.searcher.oldAddEventListener('loaded', Ten.Function.method(this, 'loadedHandler'));
        this.pager.oldAddEventListener('next', Ten.Function.method(this, 'pagerNextHandler'));
        this.pager.oldAddEventListener('prev', Ten.Function.method(this, 'pagerPrevHandler'));

        this.mouseWheelObserver = new Ten.Observer(this.layer.div, (!Ten.Browser.isFirefox) ? 'onmousewheel' : 'DOMMouseScroll', this, 'mouseWheelHandler');
    },
    replaceAction: function() {
        this.submitFulltext = true;
        this.form.action = '/my/search';
        this.form.method = 'GET';
    },
    mouseWheelHandler: function(ev) {
        ev.stop();
        var e = ev.event;
        var delta;
        if (e.wheelDelta) {
            delta = e.wheelDelta / 120;
            if (Ten.Browser.isOpera)
                delta *= -1;
        } else if(e.detail){
            delta = -e.detail / 3;
        }
        if (delta < 0) {
            this.pagerNext();
        } else if (delta > 0) {
            this.pagerPrev();
        }
    },
    pagerNext: function() {
        if (this.pager && this.pager.pager.style.display != 'none' && this.pager.hasNext) {
            this.pagerNextHandler();
        }
    },
    pagerPrev: function() {
        if (this.pager && this.pager.pager.style.display != 'none' && this.pager.hasPrev) {
            this.pagerPrevHandler();
        }
    },
    preloadSubmitHandler: function(e) {
        if (this.submitFulltext)
            return;
        if (e) e.stop();
        if (Ten.DOM.hasClassName(this.form, 'searching')) return;
        this.form.clickObserver.stop();
        this.input.focusHandler.stop();
        this.preload();
    },
    preloadHandler: function(e) {
        if (e) e.stop();
        this.form.clickObserver.stop();
        this.input.focusHandler.stop();
        this.preload();
    },
    focus: function() {
        this.input.focus();
    },
    preload: function() {
        Ten.DOM.addClassName(this.form, 'searching');
        if (this.tags) this.setTagComplete(this.tags);
        this.searcher.initLoad();
    },
    pagerNextHandler: function() {
        this.offsetStack.push(this.lastOffset);
        this.search(this.lastWord, this.searcher.lastOffset);
    },
    pagerPrevHandler: function() {
        this.search(this.lastWord, this.offsetStack.pop());
    },
    loadedHandler: function() {
        var handler = Ten.Function.method(this, 'keypressHandler');
        this.form.submitObserver.stop(); // XXX
        new Ten.Observer(this.form, 'onsubmit', this, '_submitHandler');
        new Ten.Observer(this.input, 'onkeyup', handler);
        new Ten.Observer(this.input, 'onkeydown', this, 'keydownHandler');
        new Ten.Observer(this.input, 'onfocus', this, 'submitHandler');
        Ten.DOM.removeClassName(this.form, 'searching');
        Ten.DOM.insertBefore(this.pager.pager, this.listContainer);

        var word = this.input.value;
        //if (word != this.lastWord)
            this.searchByWord(word);
    },
    _submitHandler: function(e) {
        if (!this.submitPagerDisable) {
            this.submitHandler(e);
        }
    },
    submitHandler: function(e) {
        e.stop();
        if (this.pager && this.pager.hasNext && this.pager.pager.style.display != 'none') {
            this.pagerNextHandler(); // dasai..
        } else {
            var word = this.input.value;
            if (this.messageContainer.style.display == 'block' && this.pager.pager.style.display == 'none') {
                var url = this.bSearchURL(word);
                if (url) {
                    location.href = url;
                }
            } else {
                this.searchByWord(word);
            }
        }
    },
    keydownHandler: function(e) {
        var keyCode = e.event.keyCode;
        if (keyCode == 38) {
            // up
            this.pagerPrev();
        } else if (keyCode == 40) {
            // down
            this.pagerNext();
        }
    },
    keypressHandler: function(e) {
        if (e.isKey('escape')) {
            this.layer.close();
        } else {
            var word = this.input.value;
            if (word != this.lastWord) {
                this.searchByWord(word);
            }
        }
    },
    searchByWord: function(word) {
        this.offsetStack = [];
        this.lastWord = '';
        this.search(word);
    },
    initView: function() {
        this.lists = [];
        for (var i = 0;  i < this.limit; i++) {
            var E = Ten.Element;
            var html = E('li', {style: {display: 'none'}});
            html.appendChild(
               html.head = E('h3', {className: 'entry-search'},
                   html.link = E('a', { target: '_blank' }))
            );
            html.appendChild(
               html.commentDiv = E('div', {className: 'comment'},
                 html.tags      = E('span', {className: 'tags'}), ' ',
                 html.comment   = E('span', {className: 'comment'})
               )
            );
            html.appendChild(
               html.urlDiv = E('div', {className: 'infos'},
                 html.url = E('a', {className: 'url'}), ' - ',
                 html.timestamp = E('span', {className: 'timestamp'}), ' - ',
                 html.count = E('a', {className: 'user'})
               )
            );
            if (Hatena.Bookmark.user) {
                html.urlDiv.appendChild(document.createTextNode(' '));
                var edit = E('a', {style: 'border-width:0px'}, E(
                 'img', {
                        width: 16,
                        height: 12,
                        title: "このエントリーをはてなブックマークに追加、編集",
                        alt: "このエントリーをはてなブックマークに追加、編集",
                        src:"/images/append.gif"
                }));
                html.edit = edit;
                html.urlDiv.appendChild(edit);
            }
            this.lists.push(html);
            this.listContainer.appendChild(html);
        }
    },
    clearView: function() {
        for (var i = 0;  i < this.lists.length; i++) {
            this.lists[i].style.display = 'none';
        }
        this.messageContainer.style.display = 'none';
        this.pager.pager.style.display = 'block';
        this.progressDiv.style.display = 'block';
    },
    renderProgressBar: function(width) {
        this.progressBar.style.width = parseInt(width) + '%';
    },
    timerHandler: function() {
        this.highlightText(this.listContainer, Hatena.Bookmark.StringHelper.searchWords(this.lastWord));
    },
    bUserSearchURL: function(word) {
        return '/my/search?q=' + encodeURIComponent(word);
    },
    bSearchURL: function(word) {
        return '/search?q=' + encodeURIComponent(word);
    },
    navigationToMainSearch: function(word) {
        this.messageContainer.style.display = 'block';
        this.pager.pager.style.display = 'none';
        this.progressDiv.style.display = 'none';
        var E = Ten.Element;
        var mes = E('p', {
        },
        E('p', {className:'bookmark-search-result'}, E('strong', {}, word), 'に一致する結果はありませんでした。'),
        E('a', {className:'bookmark-search-link',  href: this.bSearchURL(word) }, '『' + word + '』をはてなブックマーク全体から検索する')
        );
        if (this.searchMessage) {
            Ten.DOM.removeAllChildren(this.searchMessage);
        }
        if (Hatena.Bookmark.user.isAuthorAndPlususer() ) {
            mes.appendChild(E('br', {style: {paddingBottom: '3px'}}));
            mes.appendChild(
                E('a', {className:'bookmark-search-link',  href: this.bUserSearchURL(word) }, '『' + word + '』を自分のブックマークから全文検索する'));
        }
        Ten.DOM.removeAllChildren(this.messageContainer);
        this.messageContainer.appendChild(mes);
    },
    search: function(word, offset) {
        if (!offset) offset = 0;
        if (word == this.lastWord && this.lastOffset == offset) {
            if (word.length == 0) {
                this.layer.close();
            }
            return;
        }
        this.lastOffset = offset;
        this.lastWord = word;
        this.clearView();
        if (word.length) {
            Hatena.Bookmark.Tutorial.check('search_from_my_bookmarks');
           this.timer.reset();
           var s = (new Date()).getTime(); // debug
           Ten.DOM.addClassName(this.form, 'searching');

           var _s = (new Date()).getTime(); // debug
           var res = this.searcher.search(word, offset);
           // debug
           var _e = (new Date()).getTime();
           var time = (_e-_s);
           p('' + time + 'ms / ' + word);

           if (res.length) {
               for (var i = 0;  i < Math.min(res.length, this.limit); i++) {
                   this.renderView(this.createData(res[i]), i, word);
               }
               this.pager.hasPrev = !!offset;
               this.pager.hasNext = (res.length > this.limit);
               this.pager.update();

               var width = 100;
               if (this.pager.hasNext) {
                   width = this.searcher.lastOffset / this.searcher.bookmarks.length * 100;
               }
               this.renderProgressBar(width);
               if (Hatena.Bookmark.user && Hatena.Bookmark.user.isAuthorAndPlususer())
                   this.fullTextNavigation(word);
               this.layer.open();
               this.timer.start();

               // debug
               var e = (new Date()).getTime();
               var time = (e-s);
               p('search total: ' + time + 'ms / offset: ' +
                 this.searcher.bookmarks.length
                 + ' / ' + this.searcher.lastOffset + ' / ' + word);
           } else {
               this.layer.open();
               this.navigationToMainSearch(word);
           }
           Ten.DOM.removeClassName(this.form, 'searching');
        } else {
           this.layer.close();
        }
    },
    fullTextNavigation: function(word) {
        if (this.searchMessage) {
            Ten.DOM.removeAllChildren(this.searchMessage);
            this.searchMessage.appendChild(
                Ten.Element('a', {target: '_blank', className:'bookmark-search-link-after',  href: this.bUserSearchURL(word) }, '『' + word + '』を自分のブックマークから全文検索する'));
        }
    },
    createData: function(lines) {
        var infos = lines[3].split("\t", 2);
        var tmp = Hatena.Bookmark.StringHelper.cutoffComment(lines[1]);
        var t = infos[1];
        return {
                  title: lines[0],
                  tags: tmp[1],
                  comment: tmp[0],
                  count: infos[0],
                  url: lines[2],
                  timestamp: t.substr(0,4) + '/' + t.substr(4,2) + '/' + t.substr(6,2)
        };
    },
    highlightText: function(nodes, words) {
        var children = [];
        for (var i = 0;  i < nodes.childNodes.length; i++)
            children.push(nodes.childNodes[i]);
        // var children = Array.prototype.slice.call(nodes.childNodes, 0); なぜか IE できかない
        for (var i = 0;  i < children.length; i++) {
            var child = children[i];
            if (child.nodeType == 3) {
                var div = new Ten.Element('div');
                div.innerHTML = Hatena.Bookmark.StringHelper.highlightText(child.nodeValue, words);
                while (div.childNodes.length) {
                    nodes.insertBefore(div.childNodes[0], child);
                }
                nodes.removeChild(child);
                child = null;
            } else {
                this.highlightText(child, words);
            }
        }
    },
    renderView: function(data, index, word) {
        var s = Hatena.Bookmark.StringHelper;
        var words = s.searchWords(word);
        url = data.url;
        var html = this.lists[index];
        html.link.innerHTML = s.escapeHTML(s.truncate(data.title, 56, '...'));
        html.link.href = url;
        html.count.innerHTML = data.count + (data.count <= 1 ? ' user' : ' users');
        html.count.href = s.entryURL(url);
        html.link.title = data.title;
        html.head.style.backgroundImage = "url(" + s.faviconURL(url) + ')';

        html.url.href = url;
        html.url.innerHTML = s.coolURL(url);
        Hatena.Bookmark.CommentEditorBase.prototype.replaceTags(
            this.user, html.tags, data.tags);

        html.comment.innerHTML = s.escapeHTML(data.comment);
        html.timestamp.innerHTML = data.timestamp;

        if (html.edit) {
            html.edit.href = '/my/add.confirm?url=' + encodeURIComponent(url);
            var url2 = url;
            if (html.edit.appender) {
                var ap = html.edit.appender;
                if (ap.confirmLink && ap.confirmLink.parentNode) ap.confirmLink.parentNode.removeChild(ap.confirmLink);
                delete html.edit.appender;
            }
            html.edit.onclick = function() {
                Hatena.Bookmark.EntryAppender.create(html.edit, url2, Hatena.Bookmark.user.isAuthor() ? '変更を保存しました' : '保存しました');
                return false;
            };
        }

        html.style.display = 'block';
    }
});


Hatena.Bookmark.UserBookmarks.Search = new Hatena.Bookmark.Class({
    initialize: function(username, options) {
        if (!options) options = {};

        this.username = username;
        this.firstLimit = 1000;
        this.limit = options.limit || 20;
        this.refresh();
    }
}, {
    initLoad: function() {
        this.load({
            offset: 0,
            limit: this.firstLimit
        });
    },
    getURL: function() {
        return '/' + this.username + '/search.data';
    },
    load: function(query) {
        this.dispatchEvent('load');
        var xhr = new Hatena.Bookmark.XHR(this.getURL(), 'POST');
        xhr.timeout = 60 * 1000;
        xhr.onComplete(this, 'completeHandler').load(query);
    },
    completeHandler: function(res) {
        p(res.responseText.length);
        if (this._loaded) return;
        p('load complete');

        if (!this.infos) {
            this.createDataStructure(res.responseText || '');
            if (this.infos.length == this.firstLimit) {
                this.load({
                    offset: this.firstLimit
                });
            } else {
                this._loaded = 1;
            }
        } else {
            this.updateDataStructure(res.responseText || '');
            this._loaded = 1;
        }
        this.dispatchEvent('loaded');
    },
    updateDataStructure: function(data) {
        var infos = data.split("\n");
        var bookmarks = infos.splice(0, infos.length * 3/4);
        this.bookmarks = this.bookmarks.concat(bookmarks);
        this.infos = this.infos.concat(infos);
    },
    createDataStructure: function(data) {
        var infos = this.infos = data.split("\n");
        this.bookmarks = infos.splice(0, infos.length * 3/4);
    },
    refresh: function() {
        this.lastOffset = 0;
    },
    search: function(word, offset) {
        var res = [];
        var words = Hatena.Bookmark.StringHelper.searchWords(word);
        if (words.length >= 2) {
            res = this.searchWords(words, offset);
        } else if (words.length) {
            res = this.searchOneWord(words[0], offset);
        }
        return res;
    },
    searchWords: function(words, offset) {
        var bookmarks = this.bookmarks;
        var len = bookmarks.length;
        var count = 0;
        var j;
        for (j = 0;  j < words.length; j++) {
            words[j] = words[j].toUpperCase();
        }
        var line, title, comment, info, index, d, flag;
        var results = [];
        for (var i = offset;  i < len; i+=3) {
            lines = (bookmarks[i+2] + bookmarks[i] + bookmarks[i+1]).toUpperCase();
            flag = true;
            for (j = 0;  j < words.length; j++) {
                if (lines.indexOf(words[j]) == -1) {
                    flag = false;
                }
            }
            if (flag) {
                results.push([bookmarks[i], bookmarks[i+1], bookmarks[i+2], this.infos[i/3]]);
                if (++count >= this.limit) {
                    this.lastOffset = i;
                    break;
                }
            }
        }
        return results;
    },
    searchOneWord: function(word, offset) {
        var bookmarks = this.bookmarks;
        var len = bookmarks.length;
        var count = 0;
        word = word.toUpperCase(); // force upper case
        var line, title, comment, info, index, d;
        var results = [];
        for (var i = offset;  i < len; i+=3) {
            lines = (bookmarks[i+2] + bookmarks[i] + bookmarks[i+1]).toUpperCase();
            //if (lines.indexOf(word) > 6) { // http:// は除く
            if (lines.indexOf(word) != -1) {
                results.push([bookmarks[i], bookmarks[i+1], bookmarks[i+2], this.infos[i/3]]);
                if (++count >= this.limit) {
                    this.lastOffset = i;
                    break;
                }
            }
        }
        return results;
    }
});

Hatena.Bookmark.UpdateCategory = {
    showForm: function(parent, form) {
        var ch = parent.childNodes;
        for (var i = 0;  i < ch.length; i++) {
            var el = ch[i];
            if (el.style) el.style.display = 'none';
        }
        form.style.display = 'block';
    },
    hideForm: function(parent, form) {
        var ch = parent.childNodes;
        for (var i = 0;  i < ch.length; i++) {
            var el = ch[i];
            if (el.style) el.style.display = '';
        }
        form.style.display = 'none';
    }
};

Hatena.Bookmark.Interest = new Hatena.Bookmark.Class({
    initialize: function(url, iEL, niEL, icEL, nicEL, imageEL) {
        this.url = url;
        this.iEL = iEL;
        this.niEL = niEL;
        this.icEL = icEL;
        this.nicEL = nicEL;
        this.imageEL = imageEL;
        this.registerEventListeners();
        this.nowPosting = false;
    }
}, {
    registerEventListeners: function() {
        new Ten.Observer(this.iEL, 'onclick', this, 'clickHandler');
        new Ten.Observer(this.niEL, 'onclick', this, 'clickHandler');
    },
    showLoading: function() {
        if (!this.loading) {
            this.loading = Hatena.Bookmark.ViewHelper.loadingIcon('データ送信中…');
        }
        Ten.DOM.insertAfter(this.loading, this.niEL);
    },
    hideLoading: function() {
        if (this.loading && this.loading.parentNode)
            this.loading.parentNode.removeChild(this.loading);
    },
    clickHandler: function(e) {
        e.stop();
        if (!this.nowPosting) {
            this.nowPosting = true;
            this.showLoading();
            var api = ((e.target == this.niEL || e.target.parentNode == this.niEL) ? 'not_interesting' : 'interesting');
            Hatena.Bookmark.user.xhr(api).onComplete(this, api + '_xhrCallback').load({
                url: this.url
            });
        }
    },
    successHandler: function(res) {
        this.hideLoading();
        this.nowPosting = false;
        var res = (eval('(' + res.responseText + ')'));
        this.imageEL.src = res.image;
        this.updateCount(res);
    },
    updateCount: function(res) {
        this.icEL.innerHTML  = res.interesting;
        this.nicEL.innerHTML = res.not_interesting;
    },
    interesting_xhrCallback: function(r) {
        this.successHandler(r);
        Ten.DOM.addClassName(this.iEL.parentNode, 'current-interest');
        Ten.DOM.removeClassName(this.niEL.parentNode, 'current-interest');
        this.iEL.childNodes[0].src = '/images/yes_on.gif';
        this.niEL.childNodes[0].src = '/images/no.gif';
    },
    not_interesting_xhrCallback: function(r) {
        this.successHandler(r);
        Ten.DOM.removeClassName(this.iEL.parentNode, 'current-interest');
        Ten.DOM.addClassName(this.niEL.parentNode, 'current-interest');
        this.iEL.childNodes[0].src = '/images/yes.gif';
        this.niEL.childNodes[0].src = '/images/no_on.gif';
    }
});

Hatena.Bookmark.EntryDescription = new Hatena.Bookmark.Class({
    initialize: function(iconSelector) {
        this.cookie = Hatena.Bookmark.cookie;
        this.iconSelector = iconSelector;
        this.selector = 'div.entry-body > blockquote';

        var hiden = this.cookie.get('hide_entry_desc');
        if (hiden) {
            this.hide();
        }
        Ten.DOM.addEventListener('onload', Ten.Function.method(this, 'onloadHandler'));
    }
}, {
    onloadHandler: function() {
        this.container = Ten.Selector.getElementsBySelector(this.iconSelector)[0];
        var hiden = this.cookie.get('hide_entry_desc');
        var E = Ten.Element;
        var toggleContainer = E('span', {className: 'entry-desc-toggle-container'});
        Ten.DOM.unshiftChild(this.container, toggleContainer);
        this.toggle = new Hatena.Bookmark.Toggle(toggleContainer, [
          this.showButton = E('img', {src: '/images/quote_hide.gif', width:16, height:16, title:'エントリーの概要を表示', alt:'エントリーの概要を表示'}),
          this.hideButton = E('img', {src: '/images/quote_show.gif', width:16, height:16, title:'エントリーの概要を非表示', alt:'エントリーの概要を非表示'})
        ], hiden ? 0 : 1);
        this.toggle.oldAddEventListener('toggle', Ten.Function.method(this, 'toggleHandler'));
    },
    toggleHandler: function(newEL) {
        if (newEL == this.showButton) {
            this.cookie.set('hide_entry_desc', 1, { expires: '+10y', path: '/' } );
            this.hide();
        } else {
            this.cookie.clear('hide_entry_desc');
            this.show();
        }
    },
    changeDisplay: function(val) {
        var els = Ten.Selector.getElementsBySelector(this.selector);
        for (var i = 0;  i < els.length; i++) {
            els[i].style.display = val;
        }
    },
    show: function() {
        // this.changeDisplay('block');
        Ten.DOM.removeClassName(document.body, 'entry-desc-hide');
    },
    hide: function() {
        Ten.DOM.addClassName(document.body, 'entry-desc-hide');
        // this.changeDisplay('none');
    }
});

Hatena.Bookmark.FollowSuggestButton = new Hatena.Bookmark.Class({
    initialize: function(container) {
        this.container = container;
        this.cookie = new Ten.Cookie;
        this.selector = 'div.suggest_list';

        var hiden = this.cookie.get('hide_follow_suggest');
        var E = Ten.Element;
        var toggleContainer = E('span', {className: 'follow-suggest-button-container'});
        Ten.DOM.unshiftChild(this.container, toggleContainer);
        this.toggle = new Hatena.Bookmark.Toggle(toggleContainer, [
          this.showButton = E('img', {src: '/images/quote_hide.gif', width:16, height:16, title:'おすすめユーザを表示', alt:'おすすめユーザを表示'}),
          this.hideButton = E('img', {src: '/images/quote_show.gif', width:16, height:16, title:'おすすめユーザを非表示', alt:'おすすめユーザを非表示'})
        ], hiden ? 0 : 1);
        this.toggle.oldAddEventListener('toggle', Ten.Function.method(this, 'toggleHandler'));
    }
}, {
    toggleHandler: function(newEL) {
        if (newEL == this.showButton) {
            this.cookie.set('hide_follow_suggest', 1, { expires: '+10y', path: '/' } );
            this.hide();
        } else {
            this.cookie.clear('hide_follow_suggest');
            this.show();
        }
    },
    changeDisplay: function(val) {
        var els = Ten.Selector.getElementsBySelector(this.selector);
        for (var i = 0;  i < els.length; i++) {
            els[i].style.display = val;
        }
    },
    show: function() {
        this.changeDisplay('block');
    },
    hide: function() {
        this.changeDisplay('none');
    }
});


Hatena.Bookmark.TabMenu = new Hatena.Bookmark.Class({
  MENU_REGEXP: /^#tabmenu-(.+)$/,
  initialize: function (target, tabs, cookie_name) {
      this.constructor._instance = this;
      this.tabs = tabs;
      this.cookie = new Ten.Cookie;
      this.cookie_name = cookie_name;
      var list = this.createTabButtons(tabs);
      target.appendChild(list);
      //this.submit = document.getElementById('config_submit');
      this.truncator = new Hatena.Bookmark.TabMenuTruncator(list);
  },
  getInstance: function () {
      this.create();
      return this._instance;
  },
  create: function (target, tabtable, cookie_name) {
    if (this._instance) return;

    var cookie = new Ten.Cookie;
    var defaultTab = cookie.get(cookie_name);
    if (this.MENU_REGEXP && location.hash.match(this.MENU_REGEXP)) {
        // override defaultTab
        // location.hash パラメータがあったらチェックし、そちらのタブがあればそっちを優先する
        var hashTab = RegExp.$1;
        for (var i=0; i<tabtable.length; i++) {
            if (tabtable[i].id == hashTab) {
                defaultTab = hashTab;
            }
        }
    }
    if (defaultTab) {
        defaultTab = defaultTab.replace(/_button$/, '');
        for (var i=0; i<tabtable.length; i++) {
            if (tabtable[i].id == defaultTab) {
                tabtable[i].selected = 1;
            } else {
                tabtable[i].selected = null;
            }
        }
    }
    for (var i=0; i<tabtable.length; i++) {
        var tab = document.getElementById(tabtable[i].id);
        if(tab) {
          tabtable[i].dom = tab;
          if(!tabtable[i].selected) {
            tab.style.display = 'none';
          }
        }
    }
    new this(target, tabtable, cookie_name);
  }
},{
  createTabButtons: function (tabs) {
    var E = Ten.Element;
    var ul = E('ul', {className: 'menu'});
    var self = this;
    for(var i=0; i<tabs.length; i++) {
      var tab = tabs[i];
      var a;
      tab.button = E('li', {id: tab.id + '_button', className: tab.selected ? 'selected' : null},
                         a = E('a', {style: {cursor: 'pointer'}}, tab.name));
      if (tab.icon) {
          var icon = E('img', {className: 'tab-icon', src: tab.icon});
          Ten.DOM.unshiftChild(a, icon);
      }
      if (tab.isNew) {
          var icon = E('img', {className: 'tab-icon-new', src: '/images/new.gif'});
          Ten.DOM.unshiftChild(a, icon);
      }
      ul.appendChild(tab.button);
      tab.button.target_id_name = tab.id;
      new Ten.Observer(tab.button, 'onclick', this, 'changeTab');
    }
    return ul;
  },
  enableSubmit: function () {
    this.submit.disabled = false;
  },
  disableSubmit: function () {
    this.submit.disabled = true;
  },
  changeTab: function(e) {
    var targetId = '';
    if (typeof e === 'string') {
        targetId = e;
        // タブのidを指定された場合、ページ下部にいる可能性があるので
        // ページ上部を表示してやる。イベントリスナ経由なら
        // たぶんタブを直接クリックしているのでスクロールの必要はない。
        window.scrollTo(0, 0);
    } else {
        if (e && e.stop) e.stop();
        for (obj = e.target; obj.parentNode; obj = obj.parentNode) {
            if (obj.tagName == 'LI') {
                targetId = obj.target_id_name;
                break;
            }
        }
    }
    for (var i = 0; i < this.tabs.length; i++) {
      var tab = this.tabs[i];
      if (tab.id === targetId) {
        if (tab.dom) tab.dom.style.display = '';
        tab.button.className = 'selected';
        location.hash = 'tabmenu-' + tab.button.id.replace(/_button$/, '');
        this.cookie.set(this.cookie_name, tab.button.id);
      } else {
        if (tab.dom) tab.dom.style.display = 'none';
        tab.button.className = '';
      }
      //this.submit.style.display = obj.hidesubmit ? 'none' : '';
    }
    this.dispatchEvent('change');
  }
});

Hatena.Bookmark.TabMenuTruncator = new Hatena.Bookmark.Class({
    initialize: function(list) {
        this.list = list;
        new Ten.Observer(Hatena.Bookmark.ViewHelper, 'onresize', this, 'truncate');
        this.truncate();
    }
}, {
    truncate: function() {
        this.reset();
        var lengths = [4, 3, 2];
        for (var i = 0; i < lengths.length; i++) {
            if (this.needsTruncate())
                this.truncateTab(lengths[i]);
            else
                break;
        }
    },
    reset: function() {
        var labels = this.list.getElementsByTagName('a');
        for (var i = 0, label; label = labels[i]; i++) {
            if (label.title) {
                label.firstChild.nodeValue = label.title;
                label.title = '';
            }
        }
    },
    needsTruncate: function() {
        // 最後のタブがカラム落ちしているか
        return this.list.lastChild.offsetTop > 3;
    },
    truncateTab: function(length) {
        var labels = this.list.getElementsByTagName('a');
        for (var i = 0, label; label = labels[i]; i++) {
            var text = label.firstChild.nodeValue;
            if (text.length <= length) continue;
            label.firstChild.nodeValue =
                Hatena.Bookmark.StringHelper.truncate(text, 2 * length);
            if (!label.title)
                label.title = text;
        }
    }
});

Hatena.Bookmark.Draggable = new Hatena.Bookmark.Class({
    initialize: function(element,handle) {
        this.constructor.SUPER.call(this);
        this.element = element;
        this.handle = handle || element;
        this.startObserver = new Ten.Observer(this.handle, 'onmousedown', this, 'startDrag');
        this.handlers = [];
        this.dragging = false;
    }
},{
    startDrag: function(e) {
        if (e.targetIsFormElements()) return;
        if (this.dragging)
            return this.endDrag(e);

        this.dragging = true;
        this.dispatchEvent('startDrag');
        this.delta = Ten.Position.subtract(
            e.mousePosition(),
            Ten.Geometry.getElementPosition(this.element)
        );
        this.handlers = [
            new Ten.Observer(document, 'onmousemove', this, 'drag'),
            new Ten.Observer(document, 'onmouseup', this, 'endDrag'),
            new Ten.Observer(this.element, 'onlosecapture', this, 'endDrag')
        ];
        e.stop();
    },
    drag: function(e) {
        var pos = Ten.Position.subtract(e.mousePosition(), this.delta);
        Ten.Style.applyStyle(this.element, {
            left: pos.x + 'px',
            top: Math.max(0, pos.y) + 'px'
        });
        e.stop();
    },
    endDrag: function(e) {
        this.dragging = false;
        this.dispatchEvent('endDrag');
        for (var i = 0; i < this.handlers.length; i++) {
            this.handlers[i].stop();
        }
        if(e) e.stop();
    }
});

Hatena.Bookmark.Star = {
    loadStar: function(cssSelector, parentNode) {
        var elements;
        try {
            elements = Ten.Selector.getElementsBySelector(cssSelector, parentNode || document.body);
        } catch(e) {};
        if (!elements) return;
        Hatena.Bookmark.Star.loadElements( elements );
    },
    loadStarBeforeOnLoad: function(cssSelector, parentNode) {
        if (Ten.Browser.isFirefox) {
            Ten.DOM.addEventListener('onload', function() {
                Hatena.Bookmark.Star.loadStar(cssSelector, parentNode);
            });
        } else {
            Hatena.Bookmark.Star.loadStar(cssSelector, parentNode);
        }
    },
    loadElements: function(elements) {
        var entries = [];
        for (var i = 0;  i < elements.length; i++) {
            var element = elements[i];
            var entry = new Hatena.Bookmark.Star.createCommentEntry(element)
            if (!(entry && entry.uri)) {
                entry = new Hatena.Bookmark.Star.createArticleEntry(element);
            }
            if (entry && entry.uri)
                entries.push(entry);
        }
        if (entries.length)
            Hatena.Bookmark.Star.addEntries(entries);
    },
    addEntries: function(entries) {
        var c = Hatena.Star.EntryLoader;

        var entries_org = c.entries;
        c.entries = null;
        c.entries = [];
        if (entries && typeof(entries.length) == 'number') {
            for (var i = 0; i < entries.length; i++) {
                var e = new Hatena.Star.Entry(entries[i]);
                e.showButtons();
                c.entries.push(e);
            }
        }
        c.getStarEntries();
        if (entries_org) {
            c.entries.push(entries_org);
            c.entries = Ten.Array.flatten(c.entries);
        }
    },
    createArticleEntry: function(el) {
        var entry = {};
        var a = el.getElementsByTagName('a')[0];
        if (!a) return;

        entry.uri = a.href;
        var title = Ten.DOM.scrapeText(el);
        entry.title = title;

        Hatena.Bookmark.Star.addStarElement(entry, el);
        return entry;
    },
    createCommentEntry: function(el) {
        // コメントの li に対する Entry の作成
        var entry = {};
        var a = Ten.DOM.getElementsByTagAndClassName('a', 'username', el)[0];
        if (!a) return null;
        var pathname = a.pathname.replace(/^\//, '');
        entry.uri = 'http://b.hatena.ne.jp/' + pathname + a.hash;
        var title = '';
        var tags = Ten.DOM.getElementsByTagAndClassName('a', 'user-tag', el);
        for (var i = 0; i < tags.length; i++) {
            title += '[' + Ten.DOM.scrapeText(tags[i]) + ']';
        }
        var comments = Ten.DOM.getElementsByTagAndClassName('span', 'comment', el)[0];
        if (comments) {
            title += Ten.DOM.scrapeText(comments);
        }
        if (!title) {
            title =  Ten.DOM.scrapeText(a) + 'のブックマーク';
        }
        entry.title = title;

        Hatena.Bookmark.Star.addStarElement(entry, el);
        return entry;
    },
    addStarElement: function(entry, el) {
        entry.comment_container = Hatena.Star.EntryLoader.createCommentContainer();
        entry.star_container = Hatena.Star.EntryLoader.createStarContainer();
        entry.comment_container.style.display = 'none';
        el.appendChild(entry.comment_container);
        el.appendChild(entry.star_container);
    },
    createByArticleTitle: function() {
    },
    createByEntryTitle: function() {
        if (document.getElementById('entry_star_count')) {
            var li = document.getElementById('entry_star_count');
            var h2 = Ten.DOM.getElementsByTagAndClassName('h2', 'entrytitle', document)[0];
            var entries = [];
            if(li && h2) {
                var a = h2.getElementsByTagName('a')[0];
                var entry = {
                    title: Ten.DOM.scrapeText(h2),
                    uri: a.href
                };
                Hatena.Bookmark.Star.addStarElement(entry, li);
                entries.push(entry);
                Hatena.Bookmark.Star.addEntries(entries);
            }
        }
    },
    receiveStarEntries: function(res) {
        var res = eval('(' + res + ')');
        var c = Hatena.Star.EntryLoader;
        c.receiveStarEntries(res);
    },
    createSWF: function() {
        return new SWFObject(
            "/js/StarLoader.swf?081106",
            'starLoaderSWF',
            '1',
            '1',
            '9.0.0',
            '#FFFFFF'
        );
    },
    getSWF: function() {
        return document.getElementsByName('starLoaderSWF')[0];
    },
    swfLoaded: function() {
        setTimeout(function() {
            Hatena.Star.EntryLoader.getStarEntries = function() {
                var c = Hatena.Star.EntryLoader;
                var entries = c.entries;
                if (!entries.length) return;
                var urls = [];
                for (var i = 0; i < entries.length; i++) {
                    urls.push(entries[i].uri);
                }
                Hatena.Bookmark.Star.getSWF().loadStars(urls);
            }
        }, 30);
    },
    getStarEntries: function() {
        // テスト中
        var c = Hatena.Star.EntryLoader;
        var entries = c.entries;
        if (!entries.length) return;
        var endpoint = 'entries.simple.json?';

        var url = Hatena.Star.BaseURL + endpoint;
        var crossdomain = Hatena.Bookmark.XHR.canCrossDomainXHR();
        if (crossdomain) {
            // Ten.JSONP.MaxBytes = 100000000;
            // あまりにロードを大きくするとそれはそれで遅い
        }
        if (entries.length > 5 && location.pathname.indexOf('/entry/') != -1) {
            // それなりの entry 数があるときは
            // b.hatena.ne.jp/username/permalink への star とする
            var regex = /^(http?:\/\/b.hatena.ne.jp\/)([^\/]+\/\d+)#bookmark\-(\d+)/;
            var matched = entries[1].uri.match(regex);
            var eid;
            if (matched) {
                endpoint = 'entries.bookmark.json?';
                url = Hatena.Star.BaseURL + endpoint;

                if (location.pathname.indexOf('/entry/') != -1) {
                    // entry ページは eid を省略して発行できるように
                    eid = matched[3];
                    if (eid) {
                        url += '&eid=' + eid;
                    }
                }

                for (var i = 0; i < entries.length; i++) {
                     if (url.length > Ten.JSONP.MaxBytes) {
                         new Ten.JSONP(url, c, 'receiveStarEntries');
                         url = Hatena.Star.BaseURL + endpoint;
                         if (eid) {
                            url += '&eid=' + eid;
                         }
                     }
                     matched = entries[i].uri.match(regex) || [];
                     var u = matched[2];
                     var e = matched[3];
                     if (u && e) {
                         url += '&u=' + encodeURIComponent(u);
                         if (!eid) {
                             url += '&e=' + encodeURIComponent(e);
                         }
                     } else {
                         url += '&uri=' + encodeURIComponent(entries[i].uri);
                     }
                }
                if (crossdomain) {
                    var tmp = url.split('?', 2);
                    var xhr = new Hatena.Bookmark.XHR(tmp[0], 'POST');
                    xhr.timeout = 60 * 1000;
                    xhr.crossdomain = true;
                    xhr.onComplete(function(res) {
                        var obj = eval('(' + res.responseText + ')');
                        c.receiveStarEntries(obj);
                    });
                    xhr.load(tmp[1]);
                } else {
                    new Ten.JSONP(url, c, 'receiveStarEntries');
                }
                return;
            }
        }

        // normal loading
        for (var i = 0; i < entries.length; i++) {
            if (url.length > Ten.JSONP.MaxBytes) {
                new Ten.JSONP(url, c, 'receiveStarEntries');
                url = Hatena.Star.BaseURL + endpoint;
            }
            url += 'uri=' + encodeURIComponent(entries[i].uri) + '&';
        }
        if (crossdomain) {
            var tmp = url.split('?', 2);
            var xhr = new Hatena.Bookmark.XHR(tmp[0], 'POST');
            xhr.timeout = 60 * 1000;
            xhr.crossdomain = true;
            xhr.onComplete(function(res) {
                var obj = eval('(' + res.responseText + ')');
                c.receiveStarEntries(obj);
            });
            xhr.load(tmp[1]);
        } else {
            new Ten.JSONP(url, c, 'receiveStarEntries');
        }
    }
}

Hatena.Bookmark.Information = new Hatena.Bookmark.Class({
    initialize: function(target, infos) {
        if (!target) return;
        if (!Hatena.Bookmark.user) return;
        this.target = target;
        this.closeButton = target.getElementsByTagName('span')[0];
        this.infos = infos;
        //this.constructor.SUPER.call(this);
        if (this.allShown()) return;
        this.init();
        this.registerEventListeners();
    }
}, {
    allShown: function() {
        var infos = this.infos;
        var infoversion = parseInt(Hatena.Bookmark.user.options.infoversion);
        if (!infos || infos.length == 0) {
            return true;
        } else {
            for (var i = infos.length - 1;  i >= 0; i--) {
                var info_id = parseInt(infos[i].info_id);
                if (info_id > infoversion) {
                    this.infoData = this.infos[i];
                    return false;
                }
            }
            return true;
        }
    },
    init: function() {
        if (!this.infoData) return p('info data not found');
        var a = this.createMessage(this.infoData);
        var self = this;
        new Ten.Observer(a, 'onclick', function() {
            Hatena.Bookmark.Tracker.firstTracker._trackEvent('click-information', self.infoData.url, self.infoData.info_id);
        });
        document.getElementById('info-header-messege').appendChild(a);
        Ten.DOM.addClassName(document.body, 'info-header-visible');
    },
    createMessage: function(data) {
        var E = Ten.Element;
        var link = E('a', {href: data.url, target: '_blank'}, data.title);
        var self = this;
        new Ten.Observer(link, 'onclick', this, 'close');
        return link;
    },
    registerEventListeners: function() {
        this.mousedownHandler = new Ten.Observer(this.closeButton, 'onmousedown', this, 'mousedownHandler');
    },
    mousedownHandler: function(e) {
        if (e) e.stop();
        this.closeButtonHandler();
    },
    close: function() {
        Hatena.Bookmark.user.xhr('infoversion').load({infoversion: this.infoData.info_id});
        Ten.DOM.removeClassName(document.body, 'info-header-visible');
    },
    closeButtonHandler: function() {
        this.close();
    }
});

Hatena.Bookmark.Navigator.Global = new Hatena.Bookmark.Class({
    initialize: function() {
        if (this.constructor.instance) {
            return;
        }
        this.constructor.instance = this;
        Hatena.Bookmark.navigator.setGlobalNavigator(this);
        this.cookie = new Ten.Cookie;
        if (Ten.Browser.isIE6) {
            //
        } else {
            var pinHeader = document.getElementById('pin-header');
            if (pinHeader)
                this.fixedImage = pinHeader.getElementsByTagName('img')[0];
        }
        this.globalSearchInput = document.getElementById('searchtext');
        this.registerEventListeners();
    }
}, {
    setInformation: function(info) {
        if (info) {
            this.info = new Hatena.Bookmark.Information(document.getElementById('info-header'), info);
        }
    },
    search: function() {
        if (this.globalSearchInput && this.globalSearchInput.value) {
            window.open('http://b.hatena.ne.jp/search?q=' + encodeURIComponent(this.globalSearchInput.value), '_blank');
        }
    },
    registerEventListeners: function() {
        if (this.fixedImage) {
            this.fixedImageClickObserver = new Ten.Observer(this.fixedImage, 'onmousedown', this, 'fixedImageClickHandler');
        }
        if (!document.getElementById('hatena-bookmark-search') && this.globalSearchInput) {
            var selectedTextTimer = new Ten.Timer(300);
            selectedTextTimer.addEventListener('timer', Ten.Function.method(this, 'selectedTextHandler'));
            this.selectedTextTimer = selectedTextTimer;
            new Ten.Observer(this.globalSearchInput, 'onfocus', this, 'globalInputFocusHandler');
            new Ten.Observer(this.globalSearchInput, 'onblur', this, 'globalInputBlurHandler');
            selectedTextTimer.start();
        }
    },
    fixedImageClickHandler: function(e) {
        if (!Ten.DOM.hasClassName(document.body, 'fixed-header')) {
            Ten.DOM.addClassName(document.body, 'fixed-header');
            e.target.src = '/images/fixed-off.png';
            this.cookie.clear('no_fixed');
        } else {
            Ten.DOM.removeClassName(document.body, 'fixed-header');
            e.target.src = '/images/fixed-on.png';
            this.cookie.set('no_fixed', 1, { expires: '+10y', path: '/' } );
        }
    },
    globalInputFocusHandler: function() {
        this.selectedTextTimer.stop();
    },
    globalInputBlurHandler: function() {
        this.selectedTextTimer.start();
    },
    selectedTextHandler: function() {
        var text;
        if (text = Ten.DOM.getSelectedText()) {
            this.globalSearchInput.value = text;
        }
    }
});

Hatena.Bookmark.AfterHeader = {
    init: function() {
        this.dispatchEvent('onload');

        this.registerVia();
        this.fixedHeader();
        if (Ten.Browser.isIE6)
            this.hidePin();
        this.truncateHeader();
        if (Hatena.Bookmark.entry && Hatena.Bookmark.entry.url) {
            Hatena.Bookmark.Entry.toggleCommentCssClass();
        }
    },
    entryPageHeader: function(shown) {
        var list = Ten.Selector.getElementsBySelector('#header-navigation > li');
        if (list && list.length) {
            for (var i = 0;  i < list.length; i++) {
                var li = list[i];
                if (Ten.DOM.hasClassName(li, shown ? 'guest' : 'for-login-user')) {
                    li.style.display = 'none';
                } else if (Ten.DOM.hasClassName(li, shown ? 'for-login-user' : 'guest')) {
                    li.style.display = 'inline';
                }
            }
        }
    },
    registerVia: function() {
        var link = document.getElementById('register_link');
        var tc = Hatena.Bookmark.cookie.get('trackcode');
        if (link && Ten.DOM.hasClassName(link, 'register_via') && tc ) {
            link.href += '&via=' + tc;
        }
    },
    fixedHeader: function() {
        var body = document.body;
        if (body) {
            if (Hatena.Bookmark.cookie.get('no_fixed')) {
                Ten.DOM.removeClassName(body, 'fixed-header');
            } else {
                Ten.DOM.addClassName(body, 'fixed-header');
            }
        }
    },
    hidePin: function() {
        var pinHeader = document.getElementById('pin-header');
        if (pinHeader && pinHeader.parentNode)
            pinHeader.parentNode.removeChild(pinHeader);

        Ten.DOM.removeClassName(document.body, 'fixed-header');
    },
    truncateHeader: function() {
        // if (Ten.Browser.isIE) return; // IE6 でハングる

        var header = document.getElementById('header');
        if (header) {
            header.truncater = new Hatena.Bookmark.HeaderTruncater(header);
            header.truncater.truncate();
        }
    }
}
Ten.EventDispatcher.implementEventDispatcher(Hatena.Bookmark.AfterHeader);

Hatena.Bookmark.HeaderTruncater = new Hatena.Bookmark.Class({
    initialize: function(header) {
        this.constructor.SUPER.call(this);
        this.header = header;
        this.ul = header.getElementsByTagName('ul')[0];
        var s = document.getElementById('searchtext');
        if (s && s.parentNode && s.parentNode.parentNode && s.parentNode.parentNode.parentNode && s.parentNode.parentNode.parentNode.id == 'navigation') {
            // XXX...
            this.searchtext = s;
        }
        this.registerEventListeners();
    }
}, {
    registerEventListeners: function() {
        this.resizeObserver = new Ten.Observer(Hatena.Bookmark.ViewHelper, 'onresize', this, 'resizeHandler');
    },
    truncate: function() {
        if (!this.spacer) {
            this.spacer = new Ten.Element('div', {style: {height: '3000px'}}, ' ');
        }
        document.body.appendChild(this.spacer);

        this.truncateHeader();
        this.finish();
    },
    finish: function() {
        if (this.spacer.parentNode) this.spacer.parentNode.removeChild(this.spacer);
    },
    isFlattenHeader: function() {
        if (Ten.Geometry.getWindowSize().w > 880) return true;
        if (Ten.Browser.isIE) {
            return this.ul.clientHeight < 30;
        } else {
            return Ten.Geometry.getElementPosition(this.ul).y < 22;
        }
    },
    resizeHandler: function() {
        this.reset();
    },
    reset: function() {
        if (this.searchtext) {
            this.searchtext.style.width = '';
        }
        this.truncateHeaderMargin(10);

        var ul = this.ul;
        var links = ul.getElementsByTagName('a');
        for (var i = 0;  i < links.length; i++) {
            var link = links[i];
            if (link.origText) {
                link.innerHTML = link.origText;
            }
        }

        this.truncate();
    },
    truncateHeader: function() {
        if (this.isFlattenHeader()) {
            //
        } else {
            if (this.truncateSearchBox(this.searchtext)) {
                return;
            }
            if (this.truncateHeaderUL(3)) {
                return;
            }
            if (this.truncateHeaderMargin(6)) {
                return;
            }
            if (this.truncateHeaderUL(2)) {
                return;
            }
            if (this.truncateHeaderMargin(2)) {
                return;
            }
        }
    },
    truncateSearchBox: function(searchtext) {
        if (!searchtext) {
            return false;
        } else {
            if (parseInt(searchtext.clientWidth) > 100) {
                searchtext.style.width = '80px';
            }
            return this.isFlattenHeader();
        }
    },
    truncateHeaderMargin: function(margin) {
        var lists = this.ul.getElementsByTagName('li');
        for (var i = 0;  i < lists.length; i++) {
            var list = lists[i];
            list.style.marginRight = '' + margin + 'px';
        }
        return this.isFlattenHeader();
    },
    truncateHeaderUL: function(len) {
        var ul = this.ul;
        var links = ul.getElementsByTagName('a');
        for (var i = 0;  i < links.length; i++) {
            var link = links[i];
            var text = link.origText || Ten.DOM.scrapeText(link);
            if (text.length > (len+1)) {
                if (link.origText) {
                    //
                } else {
                    link.title = link.origText = text;
                }
                text = Hatena.Bookmark.StringHelper.truncate(text, len*2);
                link.innerHTML = text;
            }
        }
        return this.isFlattenHeader();
    }
});


Hatena.Bookmark.Tracker = {
    track: function(name) {
        if ((typeof urchinTracker != 'undefined') && urchinTracker) {
            urchinTracker(name);
        } else if ((typeof pageTracker != 'undefined') && pageTracker) {
            pageTracker._trackPageview(name);
        }
    }
}

Hatena.Bookmark.Tracker.Link = {
    track: function() {
        this.observer = new Ten.Observer(window, 'onclick', function(e) {
            try{
                var a = e.target;
                if (a && a.tagName) {
                    if (a.tagName.toUpperCase() == 'IMG') a = a.parentNode;
                    if (a.href && a.tagName && a.tagName.toUpperCase() == 'A') {
                        if (a.href) {
                            var link = a.cloneNode(false).href;
                            Hatena.Bookmark.Tracker.firstTracker._trackEvent('click-' + (Hatena.Bookmark.user ? 'user' : 'guest'), location.href, link);
                        }
                    }
                }
            } catch(er){ p.e(er) };
        });
    }
}

Hatena.Bookmark.Tracker.ScreenSize = {
    normalize: function(size) {
        return Math.round(size / 50.0) * 50;
    },
    track: function() {
        return;
        var size = Ten.Geometry.getWindowSize();
        var width = Math.max(300, Math.min(2000, this.normalize(size.w)));
        var height = Math.max(200, Math.min(2000, this.normalize(size.h)));
        var name = '/_other/display/' + width + '/' + height;
        Hatena.Bookmark.Tracker.track(name);
    }
}

Hatena.Bookmark.closeWithCookie = function(cookieName, hideElements) {
    var cookie =  new Ten.Cookie;
    cookie.set(cookieName, 1, { expires: '+10y', path: '/' });
    for (var i = 0;  i < hideElements.length; i++) {
        var el = hideElements[i];
        if (typeof el.nodeType == 'undefined') {
            el = document.getElementById(el);
        }
        if (el && el.parentNode) el.parentNode.removeChild(el);
    }
}

Hatena.Bookmark.tagSponsorForm = function(input, button) {
    var click = function() {
        document.getElementById('tag-sponsor-tagname-value').value = document.getElementById('tag-sponsor-tagname').value;
        if(document.getElementById('tag-sponsor-tagname-value').value) {
            document.getElementById('tag-sponsor-form').submit();
        }
    };
    new Ten.Observer(input, 'onkeypress' , function(e) {
        if (e.event.keyCode == 13) {
            e.stop();
            click();
        }
    });
    new Ten.Observer(button, 'onclick' , function(e) {
        e.stop();
        click();
    });
}

Hatena.Bookmark.NewEntryConfig = new Hatena.Bookmark.Class({
    initialize: function(inputElement) {
        this.registerEventListeners(inputElement);
    }
},{
    registerEventListeners : function(inputElement) {
        this.observer = new Ten.Observer(inputElement, 'onchange', this,"changeValue");
    },
    changeValue : function(event) {
        this.postConfig(event.target.value);
    },
    postConfig : function(value) {
        Hatena.Bookmark.user.xhr('user_config').onComplete(this, 'xhrCallback').load({
            newentry_limit: value
        });
    },
    xhrCallback : function() {
        location.reload();
    }
});

Hatena.Bookmark.HideNewEntryClickhandler = new Hatena.Bookmark.Class({
    initialize: function(inputElement) {
        this.registerEventListeners(inputElement);
    }
},{
    registerEventListeners : function(inputElement) {
        this.observer = new Ten.Observer(inputElement, 'onclick', this,"toggleDisplay");
    },
    toggleDisplay : function(event) {
        event.stop();
        beDisplayedElement = document.getElementById("newentry_limit_form_container");
        beHiddenElement = document.getElementById("newentry_limit_description");
        if( beHiddenElement.style.display != "none"){
            beHiddenElement.style.display = "none";
            beDisplayedElement.style.display = "";
        }else{
            beHiddenElement.style.display = "";
            beDisplayedElement.style.display = "none";
        }
    }
});


Hatena.Bookmark.getAnalysisData = new Hatena.Bookmark.Class({
    initialize: function(url,showFunc,isMini) {
        this.showFunc = showFunc;
        this.isMini = isMini;
        if (!this.isMini) {
            this.chartListDiv = document.getElementById("chart-list-container");
            this.chartLoadingDiv = document.getElementById("chart-loading");
        }
        this.getAnalysisData(url);
    }
}, {
    getAnalysisData: function(url) {
        if (this.isMini) {
            var guest = new Hatena.Bookmark.User('xx');
            guest.xhr('analysis').onComplete(this, 'xhrCallback').load({
                url: url,
                mini_graph: this.isMini
            });
        } else {
            Hatena.Bookmark.user.xhr('analysis').onComplete(this, 'xhrCallback').load({
                url: url
            });
        }
    },
    xhrCallback: function(res){
        if (!this.isMini) {
            this.chartListDiv.style.display = "block";
            this.chartLoadingDiv.style.display = "none";
        }
        var data_set = eval('('+res.responseText+')');
        this.showFunc(data_set);
    }
});

Hatena.Bookmark.AWS = {
    items: [],
    current: 0,
    resultClickHandler: function(ev) {
        var t = ev.target;
        var tName = t.tagName.toUpperCase();
        if (tName == 'A' || tName == 'IMG') {
            //
        } else {
            var checkAsin = function(el) {
                if (el.asin) {
                    return el;
                } else {
                    if (el.parentNode && el.parentNode.tagName) {
                        return checkAsin(el.parentNode);
                    }
                }
            }
            var res = checkAsin(t);
            if (res) {
                var ul = document.getElementById('asin-sponsor-result');
                var cn = ul.childNodes;
                for (var i = 0;  i < cn.length; i++) {
                    cn[i].className = 'no-selected';
                }
                document.getElementById('asin-sponsor-pager').style.display = 'none';
                res.className = 'selected';
                document.getElementById('asin-sponsor-id').value = res.asin;
                location.hash = '#asin-sponsor-editor';
            }
        }
    },
    formRegister: function(form) {
        form.style.display = 'block';
        new Ten.Observer(form, 'onsubmit', Hatena.Bookmark.AWS.formSubmitHandler);
    },
    formSubmitHandler: function(ev) {
        ev.stop();
        var keyword = document.getElementById('asin-sponsor-form-keyword').value;
        if (keyword) {
            Hatena.Bookmark.AWS.search();
        }
    },
    init: function() {
        var self = Hatena.Bookmark.AWS;
        if (!self._inited) {
            self._inited = true;
            new Ten.Observer(document.getElementById('asin-sponsor-pager-link'), 'onclick', function() {
                Hatena.Bookmark.AWS.search(true);
            });
        }
    },
    search: function(unclear) {
        var keyword = document.getElementById('asin-sponsor-form-keyword').value;
        var type = document.getElementById("asin-type-selector").value;
        // var uri = 'http://webservices.amazon.co.jp/onca/xml?';
        var uri = 'http://ecs.amazonaws.jp/onca/xml?';
        var self = Hatena.Bookmark.AWS;
        self.init();

        if (!unclear) {
            var ul = document.getElementById('asin-sponsor-result');
            Ten.DOM.removeAllChildren(ul);
            self.items = [];
            self.current = 0;
            document.getElementById('asin-sponsor-pager').style.display = 'none';
        }

        self.current++;
        var query = {
            'Keywords'       : keyword,
            'Operation'      : 'ItemSearch',
            'SearchIndex'    : type,
            'ResponseGroup'  : 'Medium',
            'Service'        : 'AWSECommerceService',
            'Version'        : '2009-01-06',
            // 'Version'        : '2005-10-05',
            'ItemPage'       : self.current || 1,
            // 'Style'          : 'http://b.hatena.ne.jp/js/xml2js.xslt',
            'AssociateTag'   : 'hatena-b-22'
        };
        uri += Ten.XHR.makePostData(query);

        var resinfo = document.getElementById('asin-sponsor-search-resultinfo');
        resinfo.innerHTML = '';
        resinfo.appendChild(
          Hatena.Bookmark.ViewHelper.loadingIcon('検索しています...')
        );
        resinfo.style.display = 'block';
        var script = document.createElement('script');
        script.src = '/api.ecs_proxy?uri=' + encodeURIComponent(uri);
        script.type = 'text/javascript';
        document.getElementsByTagName('head')[0].appendChild(script);
    },
    item: function(data) {
        // callback
        var url = data.DetailPageURL;
        var E = Ten.Element;
        var ul = document.getElementById('asin-sponsor-result');
        var li = E('li', {id: 'asin-' + data.ASIN});
        li.asin = data.ASIN;
        if (data.SmallImage) {
            var img = E('img', {src: data.SmallImage.URL});
            li.appendChild(E('a', {target: '_blank', href: url},img));
        }
        var ia = data.ItemAttributes;
        var price = ia.ListPrice;
        if (price && price.FormattedPrice) {
            li.appendChild(E('p', {}, price.FormattedPrice));
        }

        li.appendChild(E('p', {}, E('a', {target: '_blank', href: url}, ia.Title)));
        var author = ia.Author || ia.Artist;
        if (author) {
            li.appendChild(E('p', {}, '作者/アーティスト: ' + author));
        }
        var label = ia.Publisher || ia.Manufacturer || ia.Studio || ia.Label;
        if (label) {
            li.appendChild(E('p', {}, '出版社/メーカー: ' + label));
        }
        var date = ia.PublicationDate;
        if (date && date.getFullYear) {
            li.appendChild(E('p', {}, '発売日: ' + [date.getFullYear(), date.getMonth() + 1, date.getDate()].join('/') ));
        }
        ul.appendChild(li);
        var self = Hatena.Bookmark.AWS;
        self.items.push(data);
        self.itemCheck();
        return data;
    },
    itemCheck: function() {
        var self = Hatena.Bookmark.AWS;
        if (self.items.length >= 10) {
            document.getElementById('asin-sponsor-pager').style.display = 'block';
        } else {
            document.getElementById('asin-sponsor-pager').style.display = 'none';
        }
    },
    result: function(data) {
        var resinfo = document.getElementById('asin-sponsor-search-resultinfo');
        if (data.Items && data.Items.TotalResults) {
            resinfo.innerHTML = '';
            resinfo.style.display = 'none';
            var items = data.Items.Item;
            if (items) for (var i = 0;  i < items.length; i++) {
                Hatena.Bookmark.AWS.item(items[i]);
            }
        } else {
            resinfo.innerHTML = '検索結果はありませんでした。';
        }
        // callback
    }
};

Hatena.Bookmark.FavEntrylistLoader = new Hatena.Bookmark.Class({
    initialize: function(url, target, container, options) {
        this.url = url;
        this.target = target;
        this.container = container;
        this.options = options || {};
    }
}, {
    load: function() {
        var xhr = new Hatena.Bookmark.XHR('/my.entry_favorites_detail_list?self_ignore=1&url=' + encodeURIComponent(this.url));
        var E = Ten.Element;
        var loadingIcon = E('div', {className: 'fragments-loading'}, Hatena.Bookmark.ViewHelper.loadingIcon(this.options.loading_message || 'ロード中…'));
        Ten.DOM.insertBefore(loadingIcon, this.container);
        var self = this;
        xhr.onComplete(function(res) {
            var entries = (eval('(' + res.responseText + ')'));
            if (entries && entries.length) {
                entries.sort(function(a, b) {
                    if (a.favorites.length > b.favorites.length) {
                        return -1;
                    } else if(a.favorites.length < b.favorites.length) {
                        return 1;
                    } else {
                        return 0;
                    }
                });
                var target = self.target;
                var s = Hatena.Bookmark.StringHelper;
                var len = Math.min(entries.length, 6);
                for (var i = 0;  i < len; i++) {
                    var entry = entries[i];
                    var t = entry.timestamp;
                    var li = E('li', {}, E('a', { className: 'favicon entry-link', href: entry.url }, E('img', { src: s.faviconURL(entry.url) })),
                        ' ', E('a', { className: 'entry-link', href: entry.url }, s.truncate(entry.title, 90)), ' ',
                        E('span', { className: 'users' }, E('a', { href: s.entryURL(entry.url) }, '' + entry.count + ' user' + (entry.count > 2 ? 's' : ''))), ' '
                    );
                    var tEL = E('span', { className: 'timestamp' }, t.substr(0,10).replace(/\-/g, '/'))
                    var favoriteContainer = E('span', {className: 'entry-fav-user-container' });
                    var handler = Hatena.Bookmark.LazyFavoriteLoader.prototype.afterHandlers.addFavoriteEntryElements;
                    handler(entry.url, favoriteContainer, entry.favorites);

                    li.appendChild(favoriteContainer);
                    li.appendChild(document.createTextNode(' '));
                    li.appendChild(tEL);
                    target.appendChild(li);
                }
                self.container.style.display = 'block';
            }
            loadingIcon.parentNode.removeChild(loadingIcon);
        }).load();
    }
});

Hatena.Bookmark.Entry = new Hatena.Bookmark.Class({
    initialize: function(url, options) {
        this.url = url;
        this.eid = options.eid;
        this.hasVideo = options.hasVideo;
        this.stylesheet = Ten.Style.StyleSheet.factory();
    }
}, {
});

Hatena.Bookmark.Entry.Storage = {
    checkCache: function() {
    },
    clearStorage: function() {
    }
};

Ten.EventDispatcher.implementEventDispatcher(Hatena.Bookmark.Entry.Storage);

Hatena.Bookmark.Entry.toggleCommentCssClass = function() {
    var el = document.getElementById('hatena-bookmark-entry');
    if (el) {
        var tc = Hatena.Bookmark.cookie.get('toggleComment');
        if (tc && tc == 1) {
            Ten.DOM.addClassName(el, 'nocomment-hide');
        } else {
            Ten.DOM.removeClassName(el, 'nocomment-hide');
        }
    }
};

Hatena.Bookmark.Entry.toggleComment = function() {
    var c = Hatena.Bookmark.cookie;
    var tc = c.get('toggleComment');
    if (tc && tc == 1) {
        c.set('toggleComment', 0, { expires: '+10y', path: '/' });
    } else {
        c.set('toggleComment', 1, { expires: '+10y', path: '/' });
    }
    Hatena.Bookmark.Entry.toggleCommentCssClass();
}

Hatena.Bookmark.LazyFavoriteLoader = new Hatena.Bookmark.Class({
    initialize: function() {
        this.types = {};
    },
    getInstance: function() {
        if (!this.instance)
            this.instance = new Hatena.Bookmark.LazyFavoriteLoader();
        return this.instance;
    }
}, {
    appendType: function(name) {
        if (this.constructor.APPEND_TYPES[name] && !this.types[name]) {
            this.types[name] = (this.constructor.APPEND_TYPES[name]);
            this.types[name].name = name;
        }
    },
    init: function() {
        this.loadFavorites();

        var self = this;
        setTimeout(function() { self.addAutoPager(); }, 4);
    },
    loadFavorites: function(pNode) {
        var self = this;
        var entries = {};
        var urls = [];
        for (var name in this.types) {
            var selector = this.types[name];
            // 遅くなりやすいので注意
            p.b(function() {
                var es = self.getEntries(selector, pNode);
                for (var j = 0;  j < es.length; j++) {
                    var e = es[j];
                    if (e && e.href) {
                        if (!entries[e.href]) {
                            entries[e.href] = [];
                            urls.push(e.href); // so fast load
                        }
                        // 1ページの url に対する entry は複数ある場合がある
                        if (!entries[e.href]) entries[e.href] = [];
                        entries[e.href].push([e, name]);
                    }
                }
            }, 'LazyFavoriteLoader - ' + name);
            // for (var j = 0;  j < entries.length; j++) {
            //     var node = this.getFavNode(entries[j], selector);
            // }
        }
        if (!urls.length) return;
        if (Hatena.Bookmark.user) // XXX
        Hatena.Bookmark.user.xhr('entry_favorites').onComplete(function(res) {
            res = (eval('(' + res.responseText + ')'));
            self.loadEntries(res, entries);
        }).load({entries: urls.join('|')});
    },
    addAutoPager: function() {
        var ap = Hatena.Bookmark.AutoPagerize.instance;
        if (ap) {
            ap.addBeforeFilterTrigger(Ten.Function.method(this, 'autoPagerizeBeforeHandler'));
        }
    },
    autoPagerizeBeforeHandler: function(el) {
        this.loadFavorites(el);
    },
    loadEntries: function(urls, entries) {
        for (var url in urls) {
            var ees;
            if (ees = entries[url]) {
                var favs = urls[url];
                for (var i = 0;  i < ees.length; i++) {
                    var es = ees[i];
                    var entry = es[0];
                    var typeName = es[1];
                    var selector = this.types[typeName];
                    if (!entry.favorite_loaded) {
                        entry.favorite_loaded = true;
                        var favNode = this.getFavNode(entry, selector);
                        if (favNode) {
                            var afterHandler = this.afterHandlers[selector.afterHandler];
                            if (!afterHandler) {
                                throw new Error('after handler not found:' + selector.afterHandler);
                            } else {
                                afterHandler(url, favNode, favs);
                            }
                        }
                    }
                }
            }
        }
    },
    afterHandlers: {
        addFavoriteEntryElements: function(url, favNode, favs) {
           favs.sort(function(a,b) { 
               if (a.timestamp > b.timestamp) {
                   return 1;
               } else if(a.timestamp < b.timestamp) {
                   return -1;
               } else {
                   return 0;
               }
           });
           var createMouseOverHandler = function(img, fav) {
               new Ten.Observer(img, 'onmouseover', function(event) {
                   Hatena.Bookmark.Tooltip.BookmarkEntry.create(img, fav);
               });
               new Ten.Observer(img, 'onclick', function(event) {
                   var link;
                   // BAD...
                   if (fav.timestampParsed) {
                       link = 'http://b.hatena.ne.jp/' + fav.user + '/' + fav.originTimestamp.substr(0,8);
                   } else {
                       link = 'http://b.hatena.ne.jp/' + fav.user + '/' + fav.timestamp.substr(0,8);
                   }
                   location.href = link;
               });
           };
           for (var i = 0;  i < favs.length; i++) {
               var fav = favs[i];
               if (!fav.timestampParsed) {
                   fav.originTimestamp = fav.timestamp;
                   fav.timestamp = Hatena.Bookmark.StringHelper.timestampToYMD(fav.timestamp);
                   fav.timestampParsed = true;
               }
               var img = Hatena.Bookmark.ViewHelper.profileIcon(fav.user);
               img.alt = img.title = fav.user;
               createMouseOverHandler(img, fav);
               favNode.appendChild(img);
           }
           favNode.style.display = '';
        },
        addSearchResult: function(url, favNode, favs) {
           var E = Ten.Element;

           var ul = E('ul', {className: 'favorite'});
           for (var i = 0;  i < favs.length; i++) {
               var fav = favs[i];
               var link = 'http://b.hatena.ne.jp/' + fav.user + '/' + fav.timestamp.substr(0,8) + '#bookmark-' ;
               var img = Hatena.Bookmark.ViewHelper.profileIcon(fav.user);
               var tags = E('span', {className: 'tags'});
               Hatena.Bookmark.CommentEditorBase.prototype.replaceTags(new Hatena.Bookmark.User(fav.user), tags, fav.tags);
               var li = E('li', {},
                   E('a', {href: link}, img), ' ',
                   E('a', {className: 'username', href:link}, fav.user), ' ',
                   tags, ' ',
                   E('span', {className: 'comment'}, fav.comment), ' ',
                   E('span', {className: 'timestamp'}, Hatena.Bookmark.StringHelper.timestampToYMD(fav.timestamp))
               );
               ul.appendChild(li);
           }
           favNode.appendChild(ul);
        },
        addUserSearchResult: function(url, favNode, favs) {
            var E = Ten.Element;
            var el = Ten.Selector.getElementsBySelector('li.mime > a.username',favNode)[0];
            var hash = '#' + el.href.split('#')[1];
            var me = location.pathname.split('/')[1];
            for (var i = 0;  i < favs.length; i++) {
                var fav = favs[i];
                if (me == fav.user) continue;
                var link = 'http://b.hatena.ne.jp/' + fav.user + '/' + fav.timestamp.substr(0,8) + hash;
                var img = Hatena.Bookmark.ViewHelper.profileIcon(fav.user);
                var tags = E('span', {className: 'tags'});
                Hatena.Bookmark.CommentEditorBase.prototype.replaceTags(new Hatena.Bookmark.User(fav.user), tags, fav.tags);
                var li = E('li', {className: 'favorite'},
                    E('a', {href: link}, img), ' ',
                    E('a', {className: 'username', href:link}, fav.user), ' ',
                    tags, ' ',
                    E('span', {className: 'comment'}, fav.comment), ' ',
                    E('span', {className: 'timestamp'}, Hatena.Bookmark.StringHelper.timestampToYMD(fav.timestamp))
                );
                Hatena.Bookmark.Star.loadElements([li]);
                favNode.appendChild(li);
            }
        }

    },
    getFavNode: function(entry, selector) {
        if (!entry || !selector) return;

        var node = entry.parentNode;
        if (selector.simpletree) {
            var tts = selector.simpletree.split(/\s+/);
            for (var i = 0;  i < tts.length; i++) {
                var type = tts[i];
                if (type == 'parent') {
                    node = node.parentNode;
                }
            }
        }
        if (node && selector.append) {
            return Ten.Selector.getElementsBySelector(selector.append, node)[0];
        } else if (node) {
            return node;
        } else {
            return null;
        }
    },
    getEntries: function(selector, node) {
        var elements = Ten.Selector.getElementsBySelector(selector.entry, node || document.body);
        return elements;
    }
});

Hatena.Bookmark.LazyFavoriteLoader.APPEND_TYPES = {
    'search': {
        afterHandler: 'addSearchResult',
        entry: 'li.search-result > h3 > a:first-child',
        append: 'div.entryinfo',
        simpletree: 'parent'
    },
    'usersearch': {
        afterHandler: 'addUserSearchResult',
        entry: 'li > h3.entry > a:first-child',
        append: 'div.curvebox-body > ul.comment',
        simpletree: 'parent'
    },
    '/hotentry': {
        afterHandler: 'addFavoriteEntryElements',
        entry: 'ul.hotentry > li > div.entry-body > h3 > a:first-child',
        append: 'ul.entry-info > li.favorite',
        simpletree: 'parent'
    },
    'top-hotentry': {
        afterHandler: 'addFavoriteEntryElements',
        entry: '#hotentry > ul.hotentry > li > div.entry-body > h3 > a:first-child',
        append: 'ul.entry-info > li.favorite',
        simpletree: 'parent'
    },
    'top-newentry': {
        afterHandler: 'addFavoriteEntryElements',
        entry: '#entrylist > ul.hotentry > li > div.entry-body > h3 > a:first-child',
        append: 'ul.entry-info > li.favorite',
        simpletree: 'parent'
    },
    'top-hotnews': {
        afterHandler: 'addFavoriteEntryElements',
        entry: '#hotnews-es > ul.newhotentry > li > h3 > a:first-child',
        append: 'ul > li.favorite',
        simpletree: 'parent'
    },
    'top-hotvideo': {
        afterHandler: 'addFavoriteEntryElements',
        entry: '#hotvideo > ul.hotvideo > li.entry_video > a:first-child',
        append: 'div > span.favorite'
    },
    'module-wordwatch': {
        afterHandler: 'addFavoriteEntryElements',
        entry: 'ul#bookmarked_user > li > div.entry-body > h3 > a:first-child',
        append: 'ul.entry-info > li.favorite',
        simpletree: 'parent'
    }
};

Hatena.Bookmark.ShortURL = {};

Hatena.Bookmark.ShortURL.Counter = new Ten.Class({
    initialize: function(click, marker) {
        this.click = click;
        this.marker = marker;
    }
}, {
    show: function() {
        Ten.DOM.insertAfter(this.toElement(), this.marker);
        Ten.DOM.addClassName(this.marker.parentNode, 'with-click-count');
    },
    toElement: function() {
        var count = this.click.count;
        return Ten.Element('span', { className: 'click-count' },
                           count + ((count === 1) ? ' click' : ' clicks'));
    }
});

Hatena.Bookmark.ShortURL.CounterLoader = new Ten.Class({
    initialize: function() {
    },
    getInstance: function() {
        if (!this.instance)
            this.instance = new Hatena.Bookmark.ShortURL.CounterLoader();
        return this.instance;
    }
}, {
    init: function() {
        this.markers = {};
        this.loadCounters();
        setTimeout(Ten.Function.method(this, 'addAutoPager'), 7);
    },
    loadCounters: function(container) {
        container = container || document.body;
        var markers = Ten.DOM.getElementsByTagAndClassName('a', 'with-short-url', container);
        var users = {};
        for (var i = 0, marker; marker = markers[i]; i++) {
            var memo = this.getEidAndUser(marker);
            if (!memo) continue;
            var key = memo.eid + ',' + memo.user;
            if (key in this.markers) continue;
            this.markers[key] = marker;
            users[memo.eid] || (users[memo.eid] = []);
            users[memo.eid].push(memo.user);
        }
        var entries = [];
        for (var eid in users)
            entries.push(eid + ',' + users[eid].join('|'));
        if (!entries.length) return;
        var self = this;
        new Hatena.Bookmark.XHR('/api/shorturl.clicks', 'POST').onComplete(function(res) {
            res = eval('(' + res.responseText + ')');
            self.addCounters(res && res.entries);
        }).load({ entry: entries });
    },
    getEidAndUser: function(element) {
        var eid = 0;
        var user = '';
        while (element && element !== document) {
            eid = eid || +element.getAttribute('data-eid');
            user = user || element.getAttribute('data-user');
            if (eid && user)
                return { eid: eid, user: user };
            element = element.parentNode;
        }
        return null;
    },
    addCounters: function(entries) {
        for (var i = 0; i < entries.length; i++) {
            var eid = entries[i].eid;
            var clicks = entries[i].clicks;
            for (var j = 0; j < clicks.length; j++) {
                var marker = this.markers[eid + ',' + clicks[j].user];
                if (!marker) continue;
                new Hatena.Bookmark.ShortURL.Counter(clicks[j], marker).show();
            }
        }
    },
    addAutoPager: function() {
        var ap = Hatena.Bookmark.AutoPagerize.instance;
        if (ap)
            ap.addBeforeFilterTrigger(Ten.Function.method(this, 'autoPagerizeBeforeHandler'));
    },
    autoPagerizeBeforeHandler: function(el) {
        this.loadCounters(el);
    }
});

Hatena.Bookmark.MiniGraph = {};

Hatena.Bookmark.MiniGraph.Settings = {
    containerWidth: 100,
    containerHeight: 93,
    marginLeft: 25,
    marginBottom: 20,
    marginRight: 10,
    marginTop: 18,
    lineColor: '#2C6EBD',
    gridColor: '#333',
    backgroundColor: '#FFF'
}

Hatena.Bookmark.MiniGraph.makeMiniGraphCanvas = function(startDate,endDate,maxBookmark,analysisUrl){
    var containerWidth = Hatena.Bookmark.MiniGraph.Settings.containerWidth;
    var containerHeight = Hatena.Bookmark.MiniGraph.Settings.containerHeight;
    var marginLeft = Hatena.Bookmark.MiniGraph.Settings.marginLeft;
    var marginBottom = Hatena.Bookmark.MiniGraph.Settings.marginBottom;
    var marginRight = Hatena.Bookmark.MiniGraph.Settings.marginRight;
    var marginTop = Hatena.Bookmark.MiniGraph.Settings.marginTop;
    var plotWidth = containerWidth - marginLeft;
    var plotHeight = containerHeight - marginBottom - marginTop;

    var insertTarget = document.getElementById("bookmarked_user");

    var lineColor = Hatena.Bookmark.MiniGraph.Settings.lineColor;
    var gridColor = Hatena.Bookmark.MiniGraph.Settings.gridColor;
    var backgroundColor = Hatena.Bookmark.MiniGraph.Settings.backgroundColor;

    var startDateElms = startDate.split('/');
    var endDateElms = endDate.split('/');

    var firstDate = new Date(startDateElms[0], startDateElms[1]-1, startDateElms[2]);
    var firstHour = startDateElms[3];
    var firstMin = startDateElms[4];
    var lastDate = new Date(endDateElms[0], endDateElms[1]-1, endDateElms[2]);
    var lastHour = endDateElms[3];
    var lastMin = endDateElms[4];
    var today = new Date();

    function isSameDate(date1,date2){
        if(date1.getFullYear() == date2.getFullYear() &&
           date1.getMonth() == date2.getMonth() &&
           date1.getDate() == date2.getDate()){
            return true;
        }else{
            return false;
        }
    }

    function getTwoDigitYear(year){
        return year.toString().substr(2);
    }
    function getTwoDigitMonth(month){
        if(month > 8){
            return month + 1;
        }else{
            return "0" + (month +1);
        }
    }
    function getTwoDigitDate(date){
        if(date > 9){
            return date;
        }else{
            return "0" + date;
        }
    }
    var getTwoDigitMinOrHour = getTwoDigitDate;

    var canvasContainer = new Ten.Element ('div',{
        id: 'miniGraphCanvasContainer',
        width: containerWidth,
        height: containerHeight,
        title: "エントリー詳細情報"
    });

    var canvas = new Ten.Element ('canvas',{
        id: 'miniGraphCanvas',
        width: plotWidth,
        height: plotHeight,
        style: {
            marginLeft: marginLeft + "px",
            marginTop: marginTop+"px",
            marginRight: marginRight+"px",
            marginBottom: marginBottom + "px",
            padding: 0
        }
    });

    var canvasLink = new Ten.Element ('a',{
        'id': 'miniGraphCanvasLink',
        href: analysisUrl
    });

    canvasLink.appendChild(canvas);
    canvasContainer.appendChild(canvasLink);

    Ten.DOM.insertBefore(canvasContainer,insertTarget);
    if (! canvas.getContext ) {
        canvas = G_vmlCanvasManager.initElement(canvas);
    }

    var startDateString ="";
    if( isSameDate(firstDate,today)){
        startDateString = getTwoDigitMinOrHour(firstHour) + ':' + getTwoDigitMinOrHour(firstMin);
    }else{
        var startYear = getTwoDigitYear(firstDate.getFullYear());
        var startMonth = getTwoDigitMonth(firstDate.getMonth());
        var startDay = getTwoDigitDate(firstDate.getDate());


        if(lastDate.getFullYear() != firstDate.getFullYear() || today.getFullYear() != firstDate.getFullYear()){
            startDateString += startYear + '/' + startMonth + '/' + startDay;;
        }else{
            startDateString += (firstDate.getMonth() + 1) + '/' + startDay;
        }
    }
    var originXTickPositionX = 15;
    var originXTick = new Ten.Element ('div',{
        id: 'originXTick',
        style : {
            position: 'absolute',
            top: (plotHeight + marginTop )+"px",
            left: originXTickPositionX +"px"
        }
    },startDateString);


    var lastDateString ="";
    if ( isSameDate(firstDate,today)){
        lastDateString = getTwoDigitMinOrHour(lastHour) + ':' + getTwoDigitMinOrHour(lastMin);
    }else{
        var lastYear = getTwoDigitYear(lastDate.getFullYear());
        var lastMonth = getTwoDigitMonth(lastDate.getMonth());
        var lastDay = getTwoDigitDate(lastDate.getDate());

        if(lastDate.getFullYear() != firstDate.getFullYear() || today.getFullYear() != firstDate.getFullYear()){
            lastDateString += lastYear + '/' + lastMonth + '/' + lastDay;
        }else{
            lastDateString += (lastDate.getMonth() + 1)+ '/' + lastDay;
        }
    }
    var maxXTickPositionX = containerWidth - (lastDateString.length * 3);

    var maxXTick = new Ten.Element ('div',{
        id: 'maxXTick',
        style : {
            position: 'absolute',
            top: (plotHeight + marginTop)+"px",
            left: maxXTickPositionX +"px"
        }
    },lastDateString);



    canvasContainer.appendChild(originXTick);
    canvasContainer.appendChild(maxXTick);

    var ctx = canvas.getContext('2d');
    //background
    ctx.fillStyle = backgroundColor;
    ctx.rect(0,0,plotWidth,plotHeight);
    ctx.fill();

    //grid
    ctx.strokeStyle = gridColor;
    ctx.lineWidth = 1;
    ctx.strokeRect(0,0,plotWidth,plotHeight);


}

Hatena.Bookmark.MiniGraph.drawMiniGraph = function(dataSet){
    var totalCount = dataSet.total_count;
    var timeLentgh = totalCount[totalCount.length-1][0] - totalCount[0][0];
    var startTime = totalCount[0][0];
    var maxBookmark = totalCount[totalCount.length-1][1];
    var containerWidth = Hatena.Bookmark.MiniGraph.Settings.containerWidth;
    var containerHeight = Hatena.Bookmark.MiniGraph.Settings.containerHeight;
    var marginLeft = Hatena.Bookmark.MiniGraph.Settings.marginLeft;
    var marginBottom = Hatena.Bookmark.MiniGraph.Settings.marginBottom;
    var marginRight = Hatena.Bookmark.MiniGraph.Settings.marginRight;
    var marginTop = Hatena.Bookmark.MiniGraph.Settings.marginTop;

    //Y軸Max(maxBookmark)の描画
    var canvasContainer = document.getElementById('miniGraphCanvasContainer');
    var maxBookmarkString = maxBookmark.toString() + " users";

    var maxBookmarkDigits = maxBookmarkString.length;
    var maxYTick = new Ten.Element ('div',{
        id: 'maxYTick',
        style : {
            position: 'absolute',
            top: 0,
            left: 15 + "px"
        }
    });
    maxYTick.appendChild(document.createTextNode(maxBookmarkString));
    canvasContainer.appendChild(maxYTick);

    var plotWidth = containerWidth - marginLeft;
    var plotHeight = containerHeight - marginBottom - marginTop;

    var lineColor = Hatena.Bookmark.MiniGraph.Settings.lineColor;

    function mapRecord (record) {
        var time = record[0];
        var bookmark = record[1];
        var x = ((time - startTime) /timeLentgh) * plotWidth;
        var y = plotHeight - ((bookmark / maxBookmark) * plotHeight);
        return [x,y];
    }

    var canvas = document.getElementById('miniGraphCanvas')
    var ctx = canvas.getContext('2d');

    //grphline
    ctx.strokeStyle = lineColor;
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(0,plotHeight);
    for(var i=1;i<totalCount.length;i++){
        var point = mapRecord(totalCount[i]);
        ctx.lineTo(point[0],point[1]);
    }
    ctx.stroke();
};

Hatena.Bookmark.Video = {};
Hatena.Bookmark.Video.registerLoadErrorHandlers = function(selector) {
    var elements = Ten.querySelectorAll(selector);
    // p(elements);
    for (var i = 0;  i < elements.length; i++) {
        new Ten.Observer('onerror', elements[i], function(e) {
            var img = e.target;
            p(img);
        });
    }
};

Hatena.Bookmark.Video.rescueThumbnail = function(img) {
    if (img.src.indexOf('smilevideo.jp/') != -1) {
        p('nicovideo thumb error:' + img.src);
        img.src = '/images/spacer130x100.gif';
        Ten.DOM.addClassName(img, 'thumb-load-error');
        img.title = img.alt = 'サムネイル画像読み込みに失敗しました';
    }
}

Hatena.Bookmark.Tooltip.Clipboard = new Hatena.Bookmark.Class({
    base: [Hatena.Bookmark.Tooltip.Help]
}, {
    bodyClickHandler: function(e) {
        if (e.target.tagName.toUpperCase() == 'A') {
            //
        } else if (e.target.id.indexOf('ZeroClipboard') >= 0) {
            //
        } else {
            e.stop();
            this.hide();
        }
    }
});

Hatena.Bookmark.LinkTarget = {
    observe: function() {
        if (Hatena.Bookmark.user && Hatena.Bookmark.user.options.linktarget) {
            Hatena.Bookmark.LinkTarget.observer = new Ten.Observer(document, 'onclick', this, 'linkTargetHandler');
        }
    },
    linkTargetHandler: function(e) {
        var target = e.target;
        while (target) {
            if (target.tagName == 'A') break;
            target = target.parentNode;
        }
        if (!target) return;

        if (target.className && Ten.DOM.hasClassName(target, 'entry-link')) {
            target.target = Hatena.Bookmark.user.options.linktarget;
        }
    }
};

Hatena.Bookmark.createIPhoneNavigation = function() {
    if (Ten.Browser.isTouch) {
        var E = Ten.Element;
        if (Hatena.Bookmark.entry && Hatena.Bookmark.entry.url) {
            var url = '/entry.touch/' + Hatena.Bookmark.entry.url.replace('#', '%23');
        } else {
            var url = '/touch';
        }
        var a = E('a', {href: url}, "iPhone \u7528\u30da\u30fc\u30b8\u3067\u95b2\u89a7");//'iPhone 用ページで閲覧');
        var div = E('div', {
            'id': 'iPhone-navigation'
        },
            E('p', {className: 'iPhone-info'}, "\u306f\u3066\u306a\u30d6\u30c3\u30af\u30de\u30fc\u30af\u306b iPhone \u7528\u30da\u30fc\u30b8\u304c\u767b\u5834\uff01"), //'はてなブックマークに iPhone 用ページが登場！'),
            E('p', {className: 'iPhone-link'}, a)
        );

        if (Hatena.Bookmark.entry && Hatena.Bookmark.entry.url) {
            var container = document.getElementById('top-ad-line');
            if (container) Ten.DOM.insertAfter(div, container);
        } else {
            var container = document.getElementById('header');
            if (container) Ten.DOM.insertBefore(div, container);
        }
    }
}

Hatena.Bookmark.Blogparts = new Hatena.Bookmark.Class(
{
    initialize : function(options) {
        options = options || {};
        this.tagArea          = Ten.querySelector(options.tagArea);
        this.widgetTagArea    = Ten.querySelector(options.widgetTagArea);
        this.entryTitle       = (options.entryTitle || '').replace(/^\s*(.+)\s*$/, '$1');
        this.formContainer    = Ten.querySelector(options.formContainer);
        this.previewDiv       = Ten.querySelector(options.previewDiv);
        this.widgetContainer  = Ten.querySelector(options.widgetContainer);
        this.widgetPreviewDiv = Ten.querySelector(options.widgetPreviewDiv);
        this.linkCheckboxUl   = Ten.querySelector(options.linkCheckboxUl);
        this.ovservers        = {};
        this.checkboxConds    = {};
        this.cookie           = new Ten.Cookie;

        ZeroClipboard.setMoviePath( '/js/ZeroClipboard.swf' );
        this.clipboard        = new ZeroClipboard.Client();
        this.clipboardWidget  = new ZeroClipboard.Client();
    }
},{
    clipboardMouseOver :function (clipboard) {
        var message = Ten.Element( 'div', { className: 'clip-notice'}, 'HTMLタグをクリップボードにコピーします' );
        var target = clipboard.domElement;
        target.tooltip = new Hatena.Bookmark.Tooltip.Clipboard(target, message);
        target.tooltip.show();
    },
    clipboardMouseOut :function (clipboard) {
        var target = clipboard.domElement;
        if ( target.tooltip ) {
            target.tooltip.hide();
        }
    },
    clipboardComplete :function (clipboard) {
        var message = Ten.Element( 'div', { className: 'clip-notice'}, 'コピーしました' );
        var target = clipboard.domElement;
        if ( target.tooltip ) {
            target.tooltip.hide();
        }
        target.tooltip = new Hatena.Bookmark.Tooltip.Clipboard(target, message);
        target.tooltip.show();
    },
    clipboardReposition : function() {
        this.clipboard.reposition();
        this.clipboardWidget.reposition();
    },
    initClipboard : function() {
        this.clipboard.setHandCursor(true);
        this.clipboardWidget.setHandCursor(true);

        this.clipboardWidget.setText(this.widgetTagArea.value);

        this.clipboard.addEventListener('onMouseOver', [this, 'clipboardMouseOver']);
        this.clipboard.addEventListener('onMouseOut' , [this, 'clipboardMouseOut']);
        this.clipboard.addEventListener('onComplete' , [this, 'clipboardComplete']);

        this.clipboardWidget.addEventListener('onMouseOver', [this, 'clipboardMouseOver']);
        this.clipboardWidget.addEventListener('onMouseOut' , [this, 'clipboardMouseOut']);
        this.clipboardWidget.addEventListener('onComplete' , [this, 'clipboardComplete']);

        this.clipboard.glue('clip-button');
        this.clipboardWidget.glue('clip-button-widget');
    },
    init : function() {
        this.initClipboard();
        this.ovservers[this.tagArea.className] = new Ten.Observer(this.tagArea, 'onclick', this, 'clickTags');
        this.ovservers[this.widgetTagArea.className] = new Ten.Observer(this.widgetTagArea, 'onclick', this, 'clickTags');
        this.ovservers[this.widgetPreviewDiv.id] = new Ten.Observer(this.widgetPreviewDiv, 'onload', this, 'clipboardReposition');
        if ( this.cookie.has('_b_hatena_entry_blogparts_link_selected') ){
            var cond_text = this.cookie.get('_b_hatena_entry_blogparts_link_selected');
            var conds = cond_text.split(' ');
            for ( var i in this.checkboxConds ) {
                this.checkboxConds[i].checked = false;
            }
            for (var i=0;i<conds.length;i++) {
                var cond = conds[i];
                if ( this.checkboxConds[cond] ) {
                    this.checkboxConds[cond].checked = true;
                }
            }
        }
        this.linkCheckboxClicked();
    },
    registerLinkCheckbox : function(selector) {
        var elements = Ten.querySelectorAll(selector);
        for (var i = 0;  i < elements.length; i++) {
            this.checkboxConds[elements[i].id] = elements[i];
            this.ovservers[elements[i].id]
                = new Ten.Observer(elements[i], 'onclick', this, 'linkCheckboxClicked');
        }
    },
    registerTogglePreview : function(selector) {
        var element = Ten.querySelector(selector);
        this.toggleButton = element;
        this.ovservers[element.id] = new Ten.Observer(element, 'onclick', this, 'togglePreview');
    },
    togglePreview : function(e) {
        if ( this.formContainer.style.display == 'none' ) {
            this.toggleButton.innerHTML = '元に戻す';
            this.formContainer.style.display = 'block';
        } else {
            this.toggleButton.innerHTML = 'プレビュー';
            this.formContainer.style.display = 'none';
        }
        this.clipboardReposition();
    },
    registerToggleWidgetPreview : function(selector) {
        var element = Ten.querySelector(selector);
        this.toggleWidgetButton = element;
        this.ovservers[element.id] = new Ten.Observer(element, 'onclick', this, 'toggleWidgetPreview');
    },
    firstLoad: true,
    toggleWidgetPreview : function(e) {
        if ( this.widgetContainer.style.display == 'none' ) {
            this.toggleWidgetButton.innerHTML = '元に戻す';
            this.widgetContainer.style.display = 'block';
            if ( this.firstLoad ){
                this.clearWidgetPreview();
                this.widgetPreviewDiv.appendChild(this.getWidgetDummy());
                var preview = this.getWidgetTags();
                this.ovservers['widgetIframe'] = new Ten.Observer(preview, 'onload', this, 'showWidgetTags');
                this.widgetPreviewDiv.appendChild(preview);
                this.firstLoad = false;
            }
        } else {
            this.toggleWidgetButton.innerHTML = 'プレビュー';
            this.widgetContainer.style.display = 'none';
        }
        this.clipboardReposition();
    },
    clearPreview: function() {
        Ten.DOM.removeAllChildren(this.previewDiv);
    },
    clearWidgetPreview: function() {
        Ten.DOM.removeAllChildren(this.widgetPreviewDiv);
    },
    clickTags : function(e) {
        e.target.focus();
        e.target.select();
    },
    _allPreview : undefined,
    _previewContainers : undefined,
    makeLinkPreview : function(conds) {
        var containers = {};
        if ( !this._allPreview ) {
            var container = Ten.Element( 'div', {} );
            container.appendChild(this.getTitleTags());
            container.appendChild(document.createTextNode(' '));

            containers['include-users'] = this.getUsersTags();
            containers['include-users'].style.display = "none"
            container.appendChild(containers['include-users']);

            container.appendChild(document.createTextNode(' '));
            containers['b-mark'] = this.getBMarkTags();
            containers['b-mark'].style.display = "none";
            container.appendChild(containers['b-mark']);


            container.appendChild(document.createTextNode(' '));
            containers['b-ex-mark'] = this.getBExMarkTags();
            containers['b-ex-mark'].style.display = "none";
            container.appendChild(containers['b-ex-mark']);

            this._allPreview = container;
            this._previewContainers = containers;

            this.previewDiv.appendChild(this._allPreview);
        }
        for( var i in this._previewContainers){
            this._previewContainers[i].style.display = "none";
        }
        for( var i =0; i < conds.length; i++ ){
            this._previewContainers[conds[i]].style.display = "inline";
        }
    },
    linkCheckboxClicked : function(e) {
        var container = Ten.Element( 'div', {} );
        var conds = [];

        container.appendChild(this.getTitleTags());
        if ( this.checkboxConds['include-users'] && this.checkboxConds['include-users'].checked ) {
            container.appendChild(document.createTextNode(' '));
            container.appendChild(this.getUsersTags());
            conds.push('include-users');
        }
        if ( this.checkboxConds['b-mark'] && this.checkboxConds['b-mark'].checked ) {
            container.appendChild(document.createTextNode(' '));
            container.appendChild(this.getBMarkTags());
            conds.push('b-mark');
        }
        if ( this.checkboxConds['b-ex-mark'] && this.checkboxConds['b-ex-mark'].checked ) {
            container.appendChild(document.createTextNode(' '));
            container.appendChild(this.getBExMarkTags());
            conds.push('b-ex-mark');
        }
        this.cookie.set('_b_hatena_entry_blogparts_link_selected',conds.join(' '),'+10y');

        this.tagArea.value = container.innerHTML;

        this.clipboard.setText( container.innerHTML );
        this.makeLinkPreview(conds);
    },
    entryPageURL : function(){
        var _entryPageURL = 'http://b.hatena.ne.jp/entry/'
        _entryPageURL += Hatena.Bookmark.entry.url.replace(/http(s)?:\/\//,'');
        _entryPageURL = _entryPageURL.replace(/#/,'%23');
        return _entryPageURL;
    },
    appendURL : function(){
        var _appendURL = 'http://b.hatena.ne.jp/my/add.confirm?url='
        _appendURL += encodeURIComponent(Hatena.Bookmark.entry.url);
        return _appendURL;
    },
    makeLinkShareURL : function(id){
        var url = 'http://click.linksynergy.com/fs-bin/stat?';
        var rdParam1 = encodeURIComponent('http://itunes.apple.com/jp/app/id' + id + '?mt=8&uo=6&partnerId=30');

        url += 'id=3mUpcAKsM0g';
        url += '&offerid=94348';
        url += '&type=3';
        url += '&subid=0';
        url += '&tmpid=2192';
        url += '&RD_PARM1=' + rdParam1;
        return url;
    },
    getTitleTags : function(){
        var entryURL = Hatena.Bookmark.entry.url;
        var itsRegExp = new RegExp('^http://itunes.apple.com/app/id(\\d+)$');
        var asinRegExp = new RegExp('^http://www.amazon.co.jp/gp/product/\\w+$');
        if ( itsRegExp.test(entryURL) ) {
            var itsId = itsRegExp.exec(entryURL)[0]
            entryURL = this.makeLinkShareURL(itsId);
        } else if ( asinRegExp.test(entryURL) ) {
            entryURL = entryURL + '?tag=hatena-b-22'
        }

        var container = Ten.Element(
            'span', { "className" : "hatena-bookmark-title" },
            Ten.Element(
                'a',
                { 'href': entryURL },
                this.entryTitle
            )
        );
        return container;
    },
    getUsersTags : function(){
        var container = Ten.Element(
            'span', { "className" : 'hatena-bookmark-users' },
            Ten.Element(
                'a',
                { 'href': this.entryPageURL() },
                Ten.Element('img', {
                    title: this.entryTitle,
                    alt: this.entryTitle,
                    'src': 'http://b.hatena.ne.jp/entry/image/' + Hatena.Bookmark.entry.url.replace(/#/,'%23')
                } )
            ));
        return container;
    },
    getBMarkTags : function(){
        var container = Ten.Element(
            'span', { "className" : 'hatena-bookmark-b-mark' },
            Ten.Element(
                'a',
                { 'href': this.entryPageURL() },
                Ten.Element('img', {
                    title: this.entryTitle,
                    alt: this.entryTitle,
                    'src': 'http://b.st-hatena.com/images/b_entry.gif'
                } )
            ));
        return container;
    },
    getBExMarkTags : function(){
        var container = Ten.Element(
            'span', { "className" : 'hatena-bookmark-b-ex-mark' },
            Ten.Element(
                'a',
                { 'href': this.appendURL() },
                Ten.Element('img', {
                    title: 'このエントリーをはてなブックマークに追加',
                    alt: 'このエントリーをはてなブックマークに追加',
                    'src': 'http://b.st-hatena.com/images/append.gif'
                } )
            ));
        return container;
    },
    showWidgetTags : function(e) {
        this.hideWidgetDummy();
        e.target.width = 500;
        e.target.height = 230;
    },
    getWidgetDummy : function() {
        var container = Ten.Element(
            'div',
            {
                "className" : 'widget-dummy',
                "width" : 500,
                "height": 230
            },
            Ten.Element(
                'span',
                {className:'hatena-bookmark-loadingicon'},
                Ten.Element('img',
                            {
                                style: { margin: '0 5px 0 0' },
                                src: '/images/loading.gif'
                            }),
                '詳細表示画面をロードしています。'
            )
        );
        this.widgetDummy = container;
        return container;
    },
    hideWidgetDummy : function() {
        this.widgetDummy.style.display = 'none';
    },
    getWidgetTags : function() {
        var url = "http://" + location.host + "/entry.parts?url=";
        url += encodeURIComponent(Hatena.Bookmark.entry.url);
        var container = Ten.Element( 'iframe',
                                     { "src"         : url,
                                       "width"       : 0,
                                       "height"      : 0,
                                       "scrolling"   : "no",
                                       "style"       : "border:solid 1px #CCC;",
                                       "frameBorder" : "0",
                                       "marginHeight" : "0",
                                       "marginWidth" : "0"
                                     }
                                   );
        return container;
    }
});
Hatena.Bookmark.SponsoredBookmark = {};
Hatena.Bookmark.SponsoredBookmark.IgnoreLink = new Hatena.Bookmark.Class(
{
    initialize : function(options) {
        this.displayElement = document.getElementById(options.elId);
        this.sponsorName = options.sponsorName;
        this.eid = options.eid;

        this.ovserver = new Ten.Observer(this.displayElement, 'onclick', this, 'clickHandler');
    }
},{
    clickHandler: function(e) {
        e.stop();
        p('clicked',e);
        this.ignore_entry(this.eid);
    },
    ignore_entry: function(eid) {
        var url = 'http://' + location.host + '/sponsored/' + this.sponsorName + '.ignore';

        this.displayElement.innerHTML = '現在保存中です';
        var xhr = new Hatena.Bookmark.XHR( url, 'POST', Hatena.Bookmark.user.rks );
        xhr.onComplete(this, 'completeHandler');
        xhr.onError(this, 'errorHandler');
        xhr.load({ignore: this.eid});
    },
    completeHandler: function(res) {
        this.displayElement.innerHTML = "このエントリーは非表示に設定されました";
    },
    errorHandler: function() {
        this.displayElement.innerHTML = "このエントリーを非表示するのに失敗しました";
    }
});

Hatena.Bookmark.Tutorial = {};
Hatena.Bookmark.Tutorial.Help = {};
Hatena.Bookmark.Tutorial.Help.toggle = function (ele) {
    var next = Ten.DOM.nextElement(ele);
    if (next.style.display == 'none') {
        this.show(next, ele);
    } else {
        this.hide(next, ele);
    }
};
Hatena.Bookmark.Tutorial.Help.show = function (ele, name) {
    //
    ele.style.display = '';
    Ten.DOM.addClassName(ele.parentNode, 'show');
};
Hatena.Bookmark.Tutorial.Help.hide = function (ele, name) {
    ele.style.display = 'none';
    Ten.DOM.removeClassName(ele.parentNode, 'show');
};
Hatena.Bookmark.Tutorial.check = function (name) {
    if (!Hatena.Bookmark.user || arguments.callee[name]) return
    var xhr = Hatena.Bookmark.user.xhr('tutorial');
    xhr.load({
        name : name
    });
    arguments.callee[name] = true;
};


if (typeof Hatena.Star != 'undefined') {
    // ロードするURLを差し替える
    Hatena.Star.EntryLoader.loadEntries = function() {};
    Hatena.Star.EntryLoader.getStarEntries = Hatena.Bookmark.Star.getStarEntries;

    // 現在のスタイルの取得が重いので省く
    // http://hatenadiary.g.hatena.ne.jp/keyword/%E3%81%AF%E3%81%A6%E3%81%AA%E3%82%B9%E3%82%BF%E3%83%BC%E3%81%AE%E3%83%9C%E3%82%BF%E3%83%B3%E7%94%BB%E5%83%8F%E3%82%92%E5%A4%89%E6%9B%B4%E3%81%99%E3%82%8B
    // に従ってスタイルが指定されているのならこれで十分なはず
    Hatena.Star.Button.getImgSrc = function(c,container) {
        var sel = c.ImgSrcSelector;
        if (sel) {
            var prop = Ten.Style.getGlobalStyle(sel,'backgroundImage');
            if (prop) {
                var url = Ten.Style.scrapeURL(prop);
                if (url) return url;
            }
        }
        return c.ImgSrc;
    };

    Ten.Style.getGlobalRule = function(selector) {
        if (Hatena.Bookmark.entry.url) {
            return null;
        }
    };

    Hatena.Star.Button = new Ten.Class({
        _buttons: {},
        createButton: function(args) {
            var img;
            if (args.src && (img = Hatena.Star.Button._buttons[args.src])) {
                return img.cloneNode(false);
            }
            var img = document.createElement('img');
            for (var attr in args) {
                img.setAttribute(attr, args[attr]);
            }
            with (img.style) {
                cursor = 'pointer';
                margin = '0 3px';
                padding = '0';
                border = 'none';
                verticalAlign = 'middle';
            }
            if (args.src) Hatena.Star.Button._buttons[args.src] = img.cloneNode(false);
            return img;
        },
        sels: {},
        getImgSrc: function(c,container) {
            if (Hatena.Bookmark.entry.url) return c.ImgSrc;
            var sels = Hatena.Star.Button.sels;
            var sel = c.ImgSrcSelector;
            if (sel && typeof sels[sel] == 'undefined') {
                if (!Ten.Browser.isSafari3 && container) {
                    var bgimage;
                    var cname = sel.replace(/\./,'');
                    var span = new Ten.Element('span',{
                        className: cname
                    });
                    container.appendChild(span);
                    bgimage = Ten.Style.getElementStyle(span,'backgroundImage');
                    container.removeChild(span);
                    if (bgimage) {
                        var url = Ten.Style.scrapeURL(bgimage);
                        sels[sel] = url;
                        if (url) {
                            return url;
                        }
                    }
                }

                var prop = Ten.Style.getGlobalStyle(sel,'backgroundImage');
                if (prop) {
                    var url = Ten.Style.scrapeURL(prop);
                    if (url) {
                        sels[sel] = url;
                        return url;
                    }
                }
                sels[sel] = c.ImgSrc;
                return sels[sel];
            } else {
                return sels[sel] || c.ImgSrc;
            }
        }
    });

    var orig_addStar = Hatena.Star.AddButton.prototype.addStar;
    Hatena.Star.AddButton.prototype.addStar = function () {
        orig_addStar.apply(this, arguments);
        Hatena.Bookmark.Tutorial.check('use_hatena_star');
    };
}
