Reddit - Quick RES user tagging

Quickly tag multiple users with the same tag and open the tag popup using the keyboard

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name           Reddit - Quick RES user tagging
// @description    Quickly tag multiple users with the same tag and open the tag popup using the keyboard
// @author         James Skinner <[email protected]> (http://github.com/spiralx)
// @namespace      http://spiralx.org/
// @version        2.2.0
// @license        MIT
// @icon           data:image/png;base64,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
// @supportURL     https://greasyfork.org/en/scripts/370256-reddit-quick-user-tagging/feedback
// @match          *://*.reddit.com/r/*
// @match          *://*.reddit.com/user/*
// @grant          none
// @run-at         document-end
// @require        https://unpkg.com/jquery@3/dist/jquery.min.js
// @require        https://greasyfork.org/scripts/389748-console-message-v2/code/console-message-v2.js?version=730537
// ==/UserScript==

/* jshint asi: true, esnext: true, laxbreak: true */
/* global jQuery, message */

/**

==== 2.2.0 (2022.07.24) ====
* Change name and description
* Add Q keyboard shortcut to open tag dialog
* Removed references to unused Watcher object
* Tidy up some code

==== 2.1.2 (2022.07.01) ====
* Update icon and add license metadata field

==== 2.1.1 (2022.04.16) ====
* Clicking 'Clear tag' closes tag popup

==== 2.1.0 (2021.11.05) ====
* Fix when ConsoleMessage not available

==== 2.0.5 (2021.06.08) ====
* Update icons

==== 2.0.4 (2021.06.02) ====
* Set width of tag dialog to 800px

==== 2.0.0 (2019.09.03) ====
* Rename script and update version to 2.0.0

==== 1.0.0 (2018.10.09) ====
* Rewrite code base
* Set previous tag on preset tag clicked

==== 0.9.0 (2018.10.09) ====
* Made tag modal wider and text smaller to accomodate more preset tags
* Added clear tag link to the right of the colour drop-down

==== 0.8.0 (2018.07.13) ====
* Changed console.message require to use GreasyFork

==== 0.7.0 (2018.02.13) ====
* Changed the rendering of the tag preview to match how RES does it
* Moved output to all use console.message

==== 0.6.0 (2018.02.11) ====
* Use unpkg.com for jQuery
* Add console.message for logging
* Use localstorage to save tags
* Use new Tag class to store tag info
* Updated ID for text field in tag popup

==== 0.5.1 (2018.02.11) ====
* Update icons to match other Reddit script

==== 0.5.0 (26.08.2017) ====
* Change to simple use of GM_getValue and GM_setValue for storage

==== 0.4.0 (21.08.2017) ====
* Update all other tags correctly when changing a user's tag
* Handle removing all other tags when clearing a user's tag

==== 0.3.0 (13.07.2017) ====

* Checks tag link to see if tag set, always overwrites if not
* Updates other tags for same user on current page

==== 0.2.1 (27.06.2017) ====
* Changed timeout of field set function to 250ms

==== 0.2.0 (31.05.2017) ====
* Updated jQuery to v3.2.1
* Added timeout before overriding tag/colour fields
* Update preview when setting tag/colour

*/

; (($, message) => {

  const STYLES = {
    func: { color: '#c41', 'font-weight': 'bold' },
    attr: { color: '#1a2', 'font-weight': 'bold' },
    value: { color: '#05f' },
    punc: { 'font-weight': 'bold' },
    // comment: { color: 'c1007f' },
    // error: { color: '#f4f', 'font-weight': 'bold' },
    // link: { color: '#05f', 'text-decoration': 'underline' },
  }

  // --------------------------------------------------------------------------

  if (message) {
    message().extend({
      tag({ text, colour }) {
        return this.text(text, {
          fontSize: '0.9em',
          padding: '0 4px',
          border: 'solid 1px rgb(199, 199, 199)',
          borderRadius: 3,
          ...getStyleForColour(colour)
        })
      }
    })
  }

  // --------------------------------------------------------------------------

  const TAG_STORAGE_KEY = 'resPreviousUserTag'

  const TAG_DIALOG_OPEN_TIMEOUT = 200

  const BACKGROUND_TO_TEXT_COLOUR_MAP = {
    none: 'inherit',
    aqua: 'black',
    lime: 'black',
    pink: 'black',
    silver: 'black',
    white: 'black',
    yellow: 'black'
  }

  const IGNORE_TAGS = new Set([ 'A', 'BUTTON', 'INPUT', 'TEXTAREA' ])

  // --------------------------------------------------------------------

  const CLEAR_TAG_LINK = `
    <a href="javascript:void(0)"
      title="Clear tag">
      clear tag
    </a>
  `

  const NO_PREVIEW_TAG = `
    <span class="RESUserTag">
      <a href="javascript:void 0"
         title="Set a tag"
         class="RESUserTagImage userTagLink truncateTag"
      >&nbsp;</a>
    </span>
  `

  const QUICK_REPEAT_TAG = `
    <span class="RESUserTag" style="filter: hue-rotate(90deg) saturate(0.5);">
      <a href="javascript:void 0"
         title="Repeat previous tag"
         class="RESUserTagImage userTagLink truncateTag"
      >&nbsp;</a>
    </span>
  `

  // --------------------------------------------------------------------------

  function getStyleForColour (colour) {
    return {
      color: BACKGROUND_TO_TEXT_COLOUR_MAP[ colour ] || 'white',
      backgroundColor: colour === 'none'
        ? 'transparent'
        : colour
    }
  }

  // --------------------------------------------------------------------------

  const getPreviewForTag = ({ text, colour }) => {
    const { color, backgroundColor } = getStyleForColour(colour)

    return `
      <span class="RESUserTag">
        <a href="javascript:void 0"
           title="${text}"
           class="userTagLink hasTag truncateTag"
           style="color: ${color}; background-color: ${backgroundColor};"
        >${text}</a>
      </span>
    `
  }

  // --------------------------------------------------------------------------

  const EMPTY_TAG = Object.freeze({
    text: '',
    colour: 'none'
  })

  const isEmptyTag = ({ text, colour } = EMPTY_TAG) => text === '' && colour === 'none'

  const areEqualTags = (a, b) => a.text === b.text && a.colour === b.colour

  // --------------------------------------------------------------------------

  let previousUserTag = EMPTY_TAG
  let ctx = null

  if (localStorage[ TAG_STORAGE_KEY ]) {
    previousUserTag = JSON.parse(localStorage[ TAG_STORAGE_KEY ])
  }

  if (message) {
    let msg = message()
      .text('reddit-save-res-tag.init', STYLES.func)
      .text(': ', STYLES.punc)

    if (!isEmptyTag(previousUserTag)) {
      msg = msg
        .text('previousUserTag', STYLES.attr)
        .text(' = ', STYLES.punc)
        .tag(previousUserTag)
    } else {
      msg = msg.text('No previous tag found')
    }

    msg.print()
  } else if (previousUserTag !== EMPTY_TAG) {
    console.log(`previousUserTag = %o`, previousUserTag)
  }

  // --------------------------------------------------------------------------

  class TagContext {
    constructor ($thingElem) {
      this.$thingElem = $thingElem

      this.authorId = $thingElem.data('author-fullname')

      this.$dialog = $('.userTagger-dialog-head').closest('.RESHover')

      this.user = $('.RESHover .RESHoverTitle .res-icon + span').text() || null

      this.$textField = $('#userTaggerText')
      this.$colourField = $('#userTaggerColor')
      this.$previewElem = $('#userTaggerPreview')
      this.$presetTags = $('#userTaggerPresetTags')
    }

    clearFields () {
      this.$textField.val('').focus()
      this.$colourField.val('none')
      this.$previewElem.html(NO_PREVIEW_TAG)
    }

    setFields ({ text, colour }) {
      this.$textField.val(text)
      this.$colourField.val(colour)
      this.$previewElem.html(getPreviewForTag({ text, colour }))
    }

    getTag () {
      return {
        text: this.$textField.val().trim(),
        colour: this.$colourField.val()
      }
    }
  }

  // --------------------------------------------------------------------------

  function onTagSelected (tag) {
    // console.info(`onTagSelected: tag = %o, ctx = %o, prevTag = %o`, tag, ctx, previousUserTag)

    if (message) {
      message()
        .text('onTagSelected', STYLES.func)
        .text('(', STYLES.punc)
        .text('tag', STYLES.attr)
        .text(': ', STYLES.punc)
        .tag(tag)
        .text('): ', STYLES.punc)
        .text('previousUserTag', STYLES.attr)
        .text(' = ', STYLES.punc)
        .tag(previousUserTag)
        .text(', ', STYLES.punc)
        .text('ctx', STYLES.attr)
        .text(' = ', STYLES.punc)
        .object(ctx)
        .print()
    }

    const $tagLinks = $(`.id-${ctx.authorId}`)
      .next()
      .children(0)

    if (isEmptyTag(tag)) {
      $tagLinks
        .html('&nbsp;')
        .css('background-color', 'transparent')
        .removeClass('hasTag')
        .addClass('RESUserTagImage')
    } else if (!areEqualTags(tag, previousUserTag)) {
      previousUserTag = tag
      localStorage[TAG_STORAGE_KEY] = JSON.stringify(previousUserTag)

      $tagLinks
        .text(tag.text)
        .css(getStyleForColour(tag.colour))
        .addClass('hasTag')
        .removeClass('RESUserTagImage')
    }

    ctx = null
  }

  // --------------------------------------------------------------------------

  function onTagClicked ($tagLink) {
      const $thing = $tagLink.closest('.thing')

      // console.info(`a.userTagLink.click`, this, $thing)

      setTimeout(() => {
        ctx = new TagContext($thing)
        ctx.$dialog.width(800)

        const authorHasTag = $tagLink.hasClass('hasTag')

        if (!authorHasTag && !isEmptyTag(previousUserTag)) {
          ctx.setFields(previousUserTag)
        }

        $(CLEAR_TAG_LINK)
          .click(function () {
            ctx.clearFields()
            $('#userTaggerSave').click()
            return false
          })
          .css({ marginLeft: 'auto' })
          .insertAfter(ctx.$colourField)

        const $srcField = ctx.$previewElem.parent().next()

        ctx.$presetTags
          .one('click.resrem', '.userTagLink', function () {
            const $clicked = $(this)
            const clickedTag = {
              text: $clicked.text(),
              colour: $clicked.css('background-color')
            }

            // console.info(`preset .userTagLink.click`, this, clickedTag, ctx)
            onTagSelected(clickedTag)
          })
          .parent()
            .insertBefore($srcField)

        $srcField.next().css({ float: 'left', width: '50%' })
      }, TAG_DIALOG_OPEN_TIMEOUT)
  }

  // --------------------------------------------------------------------------

  $('body')
    .on('click.resrem', '.RESUserTag', function () {
      const $tagLink = $(this).children('a.userTagLink')
      onTagClicked($tagLink)
    })
    .on('click.resrem', '#userTaggerSave', function () {
      if (ctx) {
        onTagSelected(ctx.getTag())
      }
    })
    .on('keypress.resrem', event => {
      if (ctx || event.key !== 'q' || IGNORE_TAGS.has(event.target.tagName)) {
        return
      }

      $('div.thing.comment.noncollapsed.res-selected .entry.res-selected .RESUserTag').click()

      return false
    })

  // --------------------------------------------------------------------------

  /*
  <div class="RESHover RESHoverInfoCard RESDialogSmall" style="top: 387.918px; left: 215.767px; width: 350px; display: block; opacity: 1;">
    <h3 class="RESHoverTitle" data-hover-element="0">
      <div>
        <span class="res-icon"></span>&nbsp;<span>Plan-Six</span>
      </div>
    </h3>

    <div class="RESCloseButton">x</div>

    <div class="RESHoverBody RESDialogContents" data-hover-element="1">
      <form id="userTaggerToolTip">
        <div class="fieldPair">
          <label class="fieldPair-label" for="userTaggerText">Text</label>

          <input class="fieldPair-text" type="text" id="userTaggerText">
        </div>

        <div class="fieldPair">
          <label class="fieldPair-label" for="userTaggerColor">Color</label>

          <select id="userTaggerColor">
            <option style="color: inherit; background-color: none" value="none">none</option>
            <option style="color: black; background-color: aqua" value="aqua">aqua</option>
            <option style="color: white; background-color: black" value="black">black</option>
            <option style="color: white; background-color: blue" value="blue">blue</option>
            <option style="color: white; background-color: cornflowerblue" value="cornflowerblue">cornflowerblue</option>
            <option style="color: white; background-color: fuchsia" value="fuchsia">fuchsia</option>
            <option style="color: white; background-color: gray" value="gray">gray</option>
            <option style="color: white; background-color: green" value="green">green</option>
            <option style="color: black; background-color: lime" value="lime">lime</option>
            <option style="color: white; background-color: maroon" value="maroon">maroon</option>
            <option style="color: white; background-color: navy" value="navy">navy</option>
            <option style="color: white; background-color: olive" value="olive">olive</option>
            <option style="color: white; background-color: orange" value="orange">orange</option>
            <option style="color: white; background-color: orangered" value="orangered">orangered</option>
            <option style="color: black; background-color: pink" value="pink">pink</option>
            <option style="color: white; background-color: purple" value="purple">purple</option>
            <option style="color: white; background-color: red" value="red">red</option>
            <option style="color: black; background-color: silver" value="silver">silver</option>
            <option style="color: white; background-color: teal" value="teal">teal</option>
            <option style="color: black; background-color: white" value="white">white</option>
            <option style="color: black; background-color: yellow" value="yellow">yellow</option>
          </select>
        </div>

        <div class="fieldPair">
          <label class="fieldPair-label" for="userTaggerPreview">Preview</label>

          <span id="userTaggerPreview" style="color: white; background-color: olive;">
            <span class="RESUserTag">
              <a class="userTagLink hasTag truncateTag" style="background-color: olive; color: white !important;" title="Sexist" href="javascript:void 0">Sexist</a>
            </span>
          </span>
        </div>
        <a class="userTagLink hasTag truncateTag" style="background-color: olive; color: white !important;" title="Feminist" href="javascript:void 0">Feminist</a>

        <div class="fieldPair res-usertag-ignore">
          <label class="fieldPair-label" for="userTaggerIgnore">Ignore</label>

          <div id="userTaggerIgnoreContainer" class="toggleButton ">
            <span class="toggleThumb"></span>
            <div class="toggleLabel res-icon" data-enabled-text="" data-disabled-text=""></div>
            <input id="userTaggerIgnore" name="userTaggerIgnore" type="checkbox">
          </div>

          <a class="gearIcon" href="#res:settings/userTagger/hardIgnore" title="RES Settings > User Tagger > hardIgnore"> configure </a>
        </div>

        <div class="fieldPair">
          <label class="fieldPair-label" for="userTaggerLink">
            <span class="userTaggerOpenLink">
              <a title="open link" href="javascript:void 0">Source URL</a>
            </span>
          </label>

          <input class="fieldPair-text" type="text" id="userTaggerLink" value="https://www.reddit.com/r/giantbomb/comments/7x251d/all_systems_goku_02/du5fpwr/">
        </div>

        <div class="fieldPair">
          <label class="fieldPair-label" for="userTaggerVotesUp" title="Upvotes you have given this redditor">Upvotes</label>

          <input type="number" style="width: 50px;" id="userTaggerVotesUp" value="0">
        </div>

        <div class="fieldPair">
          <label class="fieldPair-label" for="userTaggerVotesDown" title="Downvotes you have given this redditor">Downvotes</label>

          <input type="number" style="width: 50px;" id="userTaggerVotesDown" value="0">
        </div>

        <div class="res-usertagger-footer">
          <a href="/r/dashboard#userTaggerContents" target="_blank" rel="noopener noreferer">View tagged users</a>

          <input type="submit" id="userTaggerSave" value="✓ save tag">
        </div>
      </form>
    </div>
  </div>

  */

})(jQuery, typeof message !== 'undefined' ? message : null)

jQuery.noConflict(true)