Enhanced Douban Collecting Dialog

By wuyuntao Last update Sep 19, 2008 — Installed 370 times. Daily Installs: 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0

There are 7 previous versions of this script.

// {{{ === License ===  
// Enhanced Douban Collecting Dialog
// a greasemonkey script offers del.icio.us-style douban subject
// collecting experience
// Version: 0.3.3
// Copyright (c) 2008 Wu Yuntao <http://luliban.com/blog/>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
// --------------------------------------------------------------------
//
// This is a Greasemonkey user script.
//
// To install, you need Greasemonkey: http://greasemonkey.mozdev.org/
// Then restart Firefox and revisit this script.
// Under Tools, there will be a new menu item to "Install User Script".
// Accept the default configuration and install.
//
// --------------------------------------------------------------------
//
// ==UserScript==
// @name            Enhanced Douban Collecting Dialog 
// @namespace       http://blog.luliban.com/
// @description     a greasemonkey script offers del.icio.us-style douban subject collecting experience
// @include         http://www.douban.com/*
// @require         http://www.douban.com/js/jquery5685.js
// ==/UserScript==
//
// }}}

/* {{{ === Global variables ===  
 */
var console = unsafeWindow.console || { log: function() {} };
var parser = null;
/* }}} */

/* {{{ === Collecting dialog ===  
 */
$.fn.collect = function(options) {
    if (!this.length) return this;

    var options = $.extend({
        tagFolder: true
    }, options || {});

    this.click(function() {
        dialog_wait();
    });

    /* Wait until dialog loaded
     */
    function dialog_wait() {
        var dialog = $('#dialog');
        if (dialog.html() == null || dialog.children().attr('class') == 'loadpop') {
            window.setTimeout(dialog_wait, 100);
        } else {
            console.log("Dialog loaded");
            if (options.tagFolder == false) {
                $('#showtags').html('缩起 ▲');
                $('#advtags').show();
                $('#foldcollect').val('U');
                refine_dialog();
            };
            if (!unsafeWindow.DOUBAN) {
                console.log("Fetch douban api and parser");
                $.ajax({ async: false, dataType: 'script', url: '/js/api.js?v=2' });
                $.ajax({ async: false, dataType: 'script', url: '/js/api-parser.js?v=1' });
                window.DOUBAN = unsafeWindow.DOUBAN;
                DOUBAN.apikey = '9e339c9d951cdb9cda9ad5d8bf5692d4';
            }
            new TagSuggest(dialog);
        }
    }

    // Copied from /js/douban13359.js
    function refine_dialog() {
        var top = ($(window).height() - $('#dialog').height()) / 2 + 140;
        console.log(top);
	    $('#dialog,#overlay').css('top', top);
        var oheight = ($.browser.msie?11:26),
            dialog = $('#dialog')[0],
            w = dialog.offsetWidth,
            left = (document.body.offsetWidth - w) / 2 + 'px';
    
        $('#overlay').css({ height: dialog.offsetHeight+oheight,
                            width: w+26, left:left });
        dialog.style.left=left;
    }
}

/* }}} */

/* {{{ === Douban tag suggestion ===  
 * add tag suggestion
 */
function TagSuggest(dialog) {
    var tag_list = new Array();
    var done = false;
    var category = parser.category();

    // start fetching
    tag_fetch(1);
    tag_wait();
    tag_style();

    // fetch tags using douban api
    function tag_fetch(index) {
        var method_dict = {
            "book": DOUBAN.getUserBookTags,
            "movie": DOUBAN.getUserMovieTags,
            "music": DOUBAN.getUserMusicTags
        };

        var params = {
            id: parser.user_id(),
            startindex: index,
            maxresults: 50,
            callback: tag_parse
        };
        method_dict[category](params);
    }

    // parse titles of tags
    function tag_parse(result) {
        var tags = DOUBAN.parseTags(result);
        $.each(tags.entries, function() {
            tag_list.push(this.title);
        });
        if ((tags.startIndex + 49) < tags.totalResults) {
            tag_fetch(tags.startIndex + 50);
        } else {
            done = true;
        }
    }

    // add tag style
    function tag_style() {
        if ($('style#tag-style').html() == null) {
            var style = 'ul.tag-matches { position: fixed; width: 180px; border-left: 1px solid #eeffee; border-right: 1px solid #eeffee; background: #ffffff; } '
                      + 'ul.tag-matches li { padding: 2px 5px; color: #060; border-bottom: 1px solid #eeffee; } '
                      + 'ul.tag-matches li:hover { background: #eeffee; cursor: pointer; } '
            $('head').append('<style type="text/css" id="tag-style">' + style + '</style>');
        }
    }

    // check if tags are ready
    function tag_wait() {
        if (!done) {
            window.setTimeout(tag_wait, 100);
        } else {
            console.log('All tags have been fetched. Ready to suggest');
            dialog.find('input[name="tags"]').attr('autocomplete', 'off');
            dialog.find('input[name="tags"]').tagSuggest({
                tags: tag_list,
                matchClass: 'tag-matches',
                tagContainer : 'ul', 
                tagWrap : 'li', 
                delay : 1.0
            });
        }
    }
}
/* }}} */

/* {{{ === Douban parser ===  
 */
// Douban Page Parser which may change frequently
function Parser() {
    // initialize parameters necessary
    this._subject_category = null;
    this._user_id = null;
    this.category_dict = { 'C': 'book', 'M': 'movie', 'W': 'music' };
};

$.extend(Parser.prototype, {
    // get user id
    user_id: function() {
        if (!this._user_id) {
            var user_id = null;
            // access a subject page to get the user's id
            $.ajax({ async: false, url: '/subject/3024234/', success:
                function(data) {
                    user_id = data.match(/href=\"\/people\/(\w+)\/recs\?add=W(\d+)\"\ class=\"j\ a_rec_btn/)[1];
                }
            });
            this._user_id = user_id;
        }
        return this._user_id;
    },

    // get category
    category: function() {
        // parse category from window.location.href where urls match:
        // '^/(movie|book/music)/mine?status=(wish|collect|do)',
        // '^/(movie|book/music)/(recommended|top250|(tag/.*))',
        // '^/people/.*/(movie|book/music)tags/.*',
        var cate = window.location.href.match(/(book|movie|music)/);
        if (cate != null) {
            this._subject_category = cate[1];
        }
        // parse category from name of recommand button where urls match:
        // '^/subject/.*',
        else if ($('.a_rec_btn').html() != null) {
            // a quick solution to get category code via name of recommand button;
            // like 'rbtn-M-2078864-'
            var code = $('.a_rec_btn').attr('name').split('-')[1];
            this._subject_category = this.category_dict[code];
        }
        // parse title of dialog
        // like '我看过这本书...'
        else {
            var title = $('#dialog').find('h2').html();
            if (title.match(/(书|杂志)/)) {
                this._subject_category = 'book';
            } else if (title.match(/电影|电视剧/)) {
                this._subject_category = 'movie';
            } else if (title.match(/唱片/)) {
                this._subject_category = 'music';
            } else {
                throw new Error("CategoryNotFound: Can not find the category of the subject which is needed for tag suggestion. Please report the URL, " + location.href + "to douban group, http://www.douban.com/group/topic/3293752/");
            }
        }
        // console.log(this._subject_category);
        return this._subject_category;
    },
});
/* }}} */

/* {{{ === Main entry ===  
 */
$(function() {
    parser = new Parser();
    $('.a_collect_btn').collect({ tagFolder: false });
});
/* }}} */

/* {{{ === jQuery tag suggestion plugin ===  
  @author: remy sharp / http://remysharp.com
  @url: http://remysharp.com/2007/12/28/jquery-tag-suggestion/
  @usage: setGlobalTags(['javascript', 'jquery', 'java', 'json']); 
          // applied tags to be used for all implementations
          $('input.tags').tagSuggest(options);
          
          The selector is the element that the user enters their tag list
  @params:
    matchClass - class applied to the suggestions, defaults to 'tagMatches'
    tagContainer - the type of element uses to contain the suggestions, defaults to 'span'
    tagWrap - the type of element the suggestions a wrapped in, defaults to 'span'
    sort - boolean to force the sorted order of suggestions, defaults to false
    url - optional url to get suggestions if setGlobalTags isn't used.  Must return array of suggested tags
    tags - optional array of tags specific to this instance of element matches
    delay - optional sets the delay between keyup and the request - can help throttle ajax requests, defaults to zero delay
    separator - optional separator string, defaults to ' ' (Brian J. Cardiff)
  @license: Creative Commons License - ShareAlike http://creativecommons.org/licenses/by-sa/3.0/
  @version: 1.4
  @changes: fixed filtering to ajax hits
*/

(function ($) {
    var globalTags = [];

    // creates a public function within our private code.
    // tags can either be an array of strings OR
    // array of objects containing a 'tag' attribute
    window.setGlobalTags = function(tags /* array */) {
        globalTags = getTags(tags);
    };
    
    function getTags(tags) {
        var tag, i, goodTags = [];
        for (i = 0; i < tags.length; i++) {
            tag = tags[i];
            if (typeof tags[i] == 'object') {
                tag = tags[i].tag;
            } 
            goodTags.push(tag.toLowerCase());
        }
        
        return goodTags;
    }
    
    $.fn.tagSuggest = function (options) {
        var defaults = { 
            'matchClass' : 'tagMatches', 
            'tagContainer' : 'span', 
            'tagWrap' : 'span', 
            'sort' : true,
            'tags' : null,
            'url' : null,
            'delay' : 0,
            'separator' : ' '
        };

        var i, tag, userTags = [], settings = $.extend({}, defaults, options);

        if (settings.tags) {
            userTags = getTags(settings.tags);
        } else {
            userTags = globalTags;
        }

        return this.each(function () {
            var tagsElm = $(this);
            var elm = this;
            var matches, fromTab = false;
            var suggestionsShow = false;
            var workingTags = [];
            var currentTag = {"position": 0, tag: ""};
            var tagMatches = document.createElement(settings.tagContainer);
            
            function showSuggestionsDelayed(el, key) {
                if (settings.delay) {
                    if (elm.timer) clearTimeout(elm.timer);
                    elm.timer = setTimeout(function () {
                        showSuggestions(el, key);
                    }, settings.delay);
                } else {
                    showSuggestions(el, key);
                }
            }

            function showSuggestions(el, key) {
                workingTags = el.value.split(settings.separator);
                matches = [];
                var i, html = '', chosenTags = {}, tagSelected = false;

                // we're looking to complete the tag on currentTag.position (to start with)
                currentTag = { position: currentTags.length-1, tag: '' };
                
                for (i = 0; i < currentTags.length && i < workingTags.length; i++) {
                    if (!tagSelected && 
                        currentTags[i].toLowerCase() != workingTags[i].toLowerCase()) {
                        currentTag = { position: i, tag: workingTags[i].toLowerCase() };
                        tagSelected = true;
                    }
                    // lookup for filtering out chosen tags
                    chosenTags[currentTags[i].toLowerCase()] = true;
                }

                if (currentTag.tag) {
                    // collect potential tags
                    if (settings.url) {
                        $.ajax({
                            'url' : settings.url,
                            'dataType' : 'json',
                            'data' : { 'tag' : currentTag.tag },
                            'async' : false, // wait until this is ajax hit is complete before continue
                            'success' : function (m) {
                                matches = m;
                            }
                        });
                    } else {
                        for (i = 0; i < userTags.length; i++) {
                            if (userTags[i].indexOf(currentTag.tag) === 0) {
                                matches.push(userTags[i]);
                            }
                        }                        
                    }
                    
                    matches = $.grep(matches, function (v, i) {
                        return !chosenTags[v.toLowerCase()];
                    });

                    if (settings.sort) {
                        matches = matches.sort();
                    }                    

                    for (i = 0; i < matches.length; i++) {
                        html += '<' + settings.tagWrap + ' class="_tag_suggestion">' + matches[i] + '</' + settings.tagWrap + '>';
                    }

                    tagMatches.html(html);
                    suggestionsShow = !!(matches.length);
                } else {
                    hideSuggestions();
                }
            }

            function hideSuggestions() {
                tagMatches.empty();
                matches = [];
                suggestionsShow = false;
            }

            function setSelection() {
                var v = tagsElm.val();

                // tweak for hintted elements
                // http://remysharp.com/2007/01/25/jquery-tutorial-text-box-hints/
                if (v == tagsElm.attr('title') && tagsElm.is('.hint')) v = '';

                currentTags = v.split(settings.separator);
                hideSuggestions();
            }

            function chooseTag(tag) {
                var i, index;
                for (i = 0; i < currentTags.length; i++) {
                    if (currentTags[i].toLowerCase() != workingTags[i].toLowerCase()) {
                        index = i;
                        break;
                    }
                }

                if (index == workingTags.length - 1) tag = tag + settings.separator;

                workingTags[i] = tag;

                tagsElm.val(workingTags.join(settings.separator));
                tagsElm.blur().focus();
                setSelection();
            }

            function handleKeys(ev) {
                fromTab = false;
                var type = ev.type;
                var resetSelection = false;
                
                switch (ev.keyCode) {
                    case 37: // ignore cases (arrow keys)
                    case 38:
                    case 39:
                    case 40: {
                        hideSuggestions();
                        return true;
                    }
                    case 224:
                    case 17:
                    case 16:
                    case 18: {
                        return true;
                    }

                    case 8: {
                        // delete - hide selections if we're empty
                        if (this.value == '') {
                            hideSuggestions();
                            setSelection();
                            return true;
                        } else {
                            type = 'keyup'; // allow drop through
                            resetSelection = true;
                            showSuggestionsDelayed(this);
                        }
                        break;
                    }

                    case 9: // return and tab
                    case 13: {
                        if (suggestionsShow) {
                            // complete
                            chooseTag(matches[0]);
                            
                            fromTab = true;
                            return false;
                        } else {
                            return true;
                        }
                    }
                    case 27: {
                        hideSuggestions();
                        setSelection();
                        return true;
                    }
                    case 32: {
                        setSelection();
                        return true;
                    }
                }

                if (type == 'keyup') {
                    switch (ev.charCode) {
                        case 9:
                        case 13: {
                            return true;
                        }
                    }

                    if (resetSelection) { 
                        setSelection();
                    }
                    showSuggestionsDelayed(this, ev.charCode);            
                }
            }

            tagsElm.after(tagMatches).keypress(handleKeys).keyup(handleKeys).blur(function () {
                if (fromTab == true || suggestionsShow) { // tweak to support tab selection for Opera & IE
                    fromTab = false;
                    tagsElm.focus();
                }
            });

            // replace with jQuery version
            tagMatches = $(tagMatches).click(function (ev) {
                if (ev.target.nodeName == settings.tagWrap.toUpperCase() && $(ev.target).is('._tag_suggestion')) {
                    chooseTag(ev.target.innerHTML);
                }                
            }).addClass(settings.matchClass);

            // initialise
            setSelection();
        });
    };
})($);
/* }}} */