Mefiquote (updated for redesign)

Adds "quote" links to Metafilter comments. Updated for the 2014 redesign.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name           Mefiquote (updated for redesign)
// @namespace      http://plutor.org/
// @description    Adds "quote" links to Metafilter comments. Updated for the 2014 redesign.
// @include        https://metafilter.com/*
// @include        https://*.metafilter.com/*
// @version 0.0.1.20180505182510
// ==/UserScript==

//
// DONE 2011-02-23
// * Use MeFi's own jquery object (properly this time)
// * Use a content scope injector instead of unsafeWindow
// * Handle new ajax comments - big thanks to pb
//
// TODO
// * Indicate when "quote" will quote the selection
// * Make it work with epiphany and opera 
// * Ability to link to the original post on preview

// Content Scope 
var script = document.createElement('script');
script.appendChild(document.createTextNode('('+ everything.toString() +')();'));
script.setAttribute("type", "application/javascript");
(document.body || document.head || document.documentElement).appendChild(script);

/* ======================================================================== */
function everything() {
    var BUTTONTEXT = 'quote';
    var QUOTEFORMAT = '<a href="%l">%n</a>: "<i>%q</i>"';

    /* ======================================================================== */

    function mq_quotethis(evt) {
        var commenttextarea = $("#comment");
        if (commenttextarea.length < 1) return;

        var quotelink = $(this);
        var metadata = quotelink.parent();
        var comment = metadata.parent();


        // Get all of the data to fill in placeholders
        var quotebits = new Object;
        quotebits['%'] = '%';

        if (mq_selection_within_comment(comment)) {
            quotebits.q = document.getSelection().toString();
        } else {
            quotebits.q = new String(comment.html());
            quotebits.q = quotebits.q.replace(/<br>/ig, '');

            // Remove the trailing metadata
            if (quotebits.q.lastIndexOf('<span class="smallcopy">posted by') > -1)
                quotebits.q = quotebits.q.slice(0,
                                    quotebits.q.lastIndexOf('<span class="smallcopy">posted by'));

            // Remove the player from music
            if (quotebits.q.lastIndexOf('<object ') > -1)
                quotebits.q = quotebits.q.slice(0,
                                    quotebits.q.lastIndexOf('<object '));

            // Remove the more inside junk
            quotebits.q = quotebits.q.replace(/<\/?div[^>]*>/g, '');
            quotebits.q = quotebits.q.replace(/^[ \t\n]*/, '');
            quotebits.q = quotebits.q.replace(/[ \t\n]*$/, '');
        }

        // Default to top of the thread, just in case
        quotebits.l = "" + location.protocol + "//" + location.host + location.pathname;

        // The rest of the data
        metadata.children('a').each( function(i) {
            var url = $(this).attr('href');
            var path = url.replace(/https?:\/\/([^\/]*\.)?metafilter.com/, '');
            if (url == path && path.match('^/'))
                url = "" + location.protocol + "//" + location.host + path;

            if (path.match(/^\/user\/(\d+)/)) {
                quotebits.i = RegExp.$1;
                quotebits.n = $(this).html();
                quotebits.n = quotebits.n.replace(/<.*/, ''); 
                quotebits.p = url;
            } else if (path.match(/#\d+$/)) {
                quotebits.l = url;
            }
        } );

        // Replace all of the placeholders
        var quoteregex = new RegExp('%(.)', 'g');
        var quotehtml = new String();
        var lastIndex = 0;
        while ( quoteregex.exec(QUOTEFORMAT) ) {
            var thisIndex = quoteregex.lastIndex;
            quotehtml = quotehtml.concat( QUOTEFORMAT.substr(lastIndex, thisIndex-lastIndex-2) );
            var val = quotebits[QUOTEFORMAT.substr(thisIndex-1, 1)];
            if (val != undefined) {
                quotehtml = quotehtml.concat( quotebits[QUOTEFORMAT.substr(thisIndex-1, 1)] );
            } else {
                quotehtml = quotehtml.concat( '%' + QUOTEFORMAT.substr(thisIndex-1, 1) );
            }

            lastIndex = thisIndex;
        }
        quotehtml = quotehtml.concat( QUOTEFORMAT.substr(lastIndex) );

        // GM_log( quotehtml );
        var commentval = commenttextarea.val() || "";
        if (commentval != "" && !commentval.match(/\n\n$/)) {
            commentval += "\n\n";
        }
        commentval += quotehtml + "\n\n";

        commenttextarea.val(commentval);
    }

    /* ======================================================================== */

    function mq_load_preferences() {
        BUTTONTEXT = Cookie.get('mefiquote_buttontext') || BUTTONTEXT;
        QUOTEFORMAT = Cookie.get('mefiquote_quoteformat') || QUOTEFORMAT;
    }

    function mq_save_preferences() {
        var buttontext_el = $('#mq_buttontext');
        var quoteformat_el = $('#mq_quoteformat');

        Cookie.set('mefiquote_buttontext', (buttontext_el.val() || BUTTONTEXT),
            24*365*10, '', 'metafilter.com', false);
        Cookie.set('mefiquote_quoteformat', (quoteformat_el.val() || QUOTEFORMAT),
            24*365*10, '', 'metafilter.com', false);

        return true; /* So it actually submits, too */
    }

    /* ======================================================================== */

    function mq_escape(str) {
        return str.replace(/"/g, '&quot;');
    }

    /* ======================================================================== */

    function mq_selection_within_comment(comment) {
        var selection = window.getSelection();

        // an empty selection doesn't count as being within the comment, otherwise
        // the quote button tries to quote it and things behave counterintuitively
        if (selection == null || selection.isCollapsed) {
            return false;
        }

        // check to see if the selection is inside this comment
        var rangeStart = selection.getRangeAt(0).startContainer;
        var rangeEnd = selection.getRangeAt(0).endContainer;

        if (rangeStart != null && rangeEnd != null) {
            var start_found = false;
            var end_found = false;

            // TODO - Why doesn't parents().index() do the right thing?
            $(rangeStart).parents().andSelf().each( function() {
                if (this == comment.get(0)) start_found = true;
            });
            $(rangeEnd).parents().andSelf().each( function() {
                if (this == comment.get(0)) end_found = true;
            });

            return (start_found && end_found);
        }
        return false;
    }

    /* ======================================================================== */
        
    function mq_init_preferences() {
        var inputs = $('input');
        var submit_button = $('input[type=submit]').filter( function() {
            return $(this).val().match(/Save your Preferences/);
        } );
        if (inputs.length < 1 || submit_button.length < 1) return;

        // Create the fieldset
        var mefiquote_fieldset = $('<fieldset>'
                + '<legend>MefiQuote preferences</legend>'
                + '<label for="mq_buttontext">Quote button text: </label>'
                + '<input type="text" id="mq_buttontext" name="mq_buttontext" value="'
                + mq_escape(BUTTONTEXT)
                + '" maxlength="200" size="30" onfocus="this.style.background=\'#ddd\';" onblur="this.style.background=\'#ccc\';" /><br />'
                + '<label for="mq_quoteformat">Quote format:<br />'
                + '<span class="smallcopy" style="text-align: left">%i - commenter\'s user id<br />%l - url of comment<br />%n - commenter\'s name<br />%p - url of commenter\'s profile<br />%q - comment text<br />%% - an actual percent ("%")</span></label>'
                + '<textarea name="mq_quoteformat" id="mq_quoteformat" cols="60" rows="8" wrap="VIRTUAL" style="width:400px;height:200px;" onfocus="this.style.background=\'#ddd\';" onblur="this.style.background=\'#ccc\';">'
                + mq_escape(QUOTEFORMAT) 
                + '</textarea>'
                + '</fieldset>')
            .insertBefore(submit_button);

        // Add javascript to the form
        submit_button.parents('form').submit( mq_save_preferences );
    }

    function mq_init_thread() {
        console.log("init-thread");

        var commenttextarea = $("#comment");
        if (commenttextarea.length < 1) return;

        var n = 0;
        $('span').each( function(i) {
            var curr = $(this);
            if (curr.hasClass('smallcopy') && curr.html().match(/^posted by/) &&
                curr.parents('#prevDiv2, form').length == 0 &&
                curr.find('.quotebutton').length == 0) {
                // Skip the first (post) quote link on preview
                if (location.pathname.match('^/contribute/post_comment_preview.mefi') && n++ == 0)
                    return;

                // Add the button
                var quotebutton = $('<a href="#comment">' + BUTTONTEXT + '</a>')
                    .attr('target', '_self')
                    .addClass('quotebutton');

                curr.append(' [').append( quotebutton ).append(']');
            }
        } );
    }

    function mq_init_newcomments() {
        $("#newcomments").bind('mefi-comments',
            function() {
                console.log("event!");
                mq_init_thread();
            } );
        console.log("init-newcomments");
    }

    /**
     * Modified from cookie-js 0.4 by Maxime Haineault ([email protected])
     * <http://code.google.com/p/cookie-js/> 
     */
    Cookie = {      
        /** Get a cookie's value */
        get: function(key) {
                // Still not sure that "[a-zA-Z0-9.()=|%/_]+($|;)" match *all* allowed characters in cookies
                tmp =  document.cookie.match((new RegExp(key +'=[a-zA-Z0-9.()=|%/_]+($|;)','g')));
                if(!tmp || !tmp[0]) return null;
                else return unescape(tmp[0].substring(key.length+1,tmp[0].length).replace(';','')) || null;
                
        },      
        
        /** Set a cookie */
        set: function(key, value, ttl, path, domain, secure) {
                cookie = [key+'='+    escape(value),
                                  'path='+    ((!path   || path=='')  ? '/' : path),
                                  'domain='+  ((!domain || domain=='')?  window.location.host : domain)];
                
                if (ttl)         cookie.push('expires=' + Cookie.hoursToExpireDate(ttl));
                if (secure)      cookie.push('secure');
                return document.cookie = cookie.join('; ');
        },
        
        /** Unset a cookie */
        unset: function(key, path, domain) {
                path   = (!path   || typeof path   != 'string') ? '' : path;
        domain = (!domain || typeof domain != 'string') ? '' : domain;
                if (Cookie.get(key)) Cookie.set(key, '', 'Thu, 01-Jan-70 00:00:01 GMT', path, domain);
        },

        /** Return GTM date string of "now" + time to live */
        hoursToExpireDate: function(ttl) {
                if (parseInt(ttl) == 'NaN' ) return '';
                else {
                        now = new Date();
                        now.setTime(now.getTime() + (parseInt(ttl) * 60 * 60 * 1000));
                        return now.toGMTString();                       
                }
        }
    }

    function mq_init() {
        mq_load_preferences();

        var url = location.pathname;

        if (url.match(/^\/(\d+)/) || url.match('^/contribute/post_comment_preview.mefi')) {
            mq_init_thread();
            mq_init_newcomments();

            // Attach a listener to clicks -- just once            
            $(".content").on("click", "a.quotebutton", mq_quotethis);
        } else if (url.match('^/contribute/customize.cfm')) {
            mq_init_preferences();
        }
    }

    mq_init();
}