Soybooru Ext

Add autocomplete to the Soybooru upload form

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        Soybooru Ext
// @namespace   https://github.com/thoughever
// @match       https://booru.soy/*
// @match       http://booru.soy/*
// @match       https://www.booru.soy/*
// @match       http://www.booru.soy/*
// @grant       GM_setClipboard
// @version     0.1.2
// @author      thoughever
// @license     MIT
// @description Add autocomplete to the Soybooru upload form
// ==/UserScript==

(function () {

  "use strict";

  /* Global settings */
  const g_autocomplete_query_delay_ms = 300;

  /* Utils */
  function add_page_styles(styles) {
    let tag_styles = document.createElement('style');
    tag_styles.type = 'text/css';
    tag_styles.innerHTML = styles;
    let head = document.getElementsByTagName('head')[0];
    head.appendChild(tag_styles);
  }

  function insert_after(reference_elem, new_elem) {
      reference_elem.parentNode.insertBefore(new_elem, reference_elem.nextSibling);
  }

  function reverse_str(str){
      return [...str].reverse().join("");
  }

  function insert_mid_str(str, i,  j, insert_str) {
    return `${str.slice(0, i)}${insert_str}${str.slice(j, str.length)}`;
  }

  function legacy_copy_to_clipboard(text) {
    let textarea = document.createElement("textarea");
    textarea.classList.add("shimmieext-legacy-clipboard-textarea");
    textarea.value = text;

    document.body.appendChild(textarea);
    textarea.focus();
    textarea.select();
    try {
      document.execCommand("copy");
    } catch (e) {}
    document.body.removeChild(textarea);
  }

  function copy_to_clipboard(text) {
    try {
      GM_setClipboard(text);
    } catch(e) {
      if(navigator.clipboard) {
        navigator.clipboard.writeText(text);
      } else {
        legacy_copy_to_clipboard(text);
      }
    }
  }

  const KEYCODE_TAB = 9;
  const KEYCODE_ENTER = 13;
  const KEYCODE_ARROW_LEFT = 37;
  const KEYCODE_ARROW_UP = 38;
  const KEYCODE_ARROW_RIGHT = 39;
  const KEYCODE_ARROW_DOWN = 40;

  /* Autocomplete elements */
  class AutocompleteDropdown {
    constructor() {
      this.node = document.createElement("ul");
      this.node.classList.add("shimmieext-autocomplete");
      this.node.tabIndex = "-1";
      let _this = this;
      this.node.addEventListener("keydown", function(e) {
        let key_code = e.keyCode || e.which;
        switch(key_code) {
          case KEYCODE_ARROW_UP:
            _this.focus_prev();
            e.preventDefault();
            break;
          case KEYCODE_ARROW_DOWN:
            _this.focus_next();
            e.preventDefault();
            break;
          case KEYCODE_ARROW_RIGHT:
          case KEYCODE_TAB:
            _this.select_focused();
            e.preventDefault();
        }
      }, false);
      this.items = [];
    }

    focus_prev() {
      let prev_li = document.activeElement.previousSibling;
      if(prev_li) {
        prev_li.focus();
      }
    }

    focus_next() {
      let next_li = document.activeElement.nextSibling;
      if(next_li) {
        next_li.focus();
      }
    }

    set_focus(i) {
      let li = this.items[i];
      if(li) {
        li.focus();
      }
    }

    select_focused() {
      let focused_elem = document.activeElement;
      if(this.items.includes(focused_elem)) {
        focused_elem.click();
      }
    }

    select(i) {
      let li = this.items[i];
      if(li) {
        li.click();
      }
    }

    clear() {
      this.node.replaceChildren();
      this.items = [];
    }

    show() {
      this.node.classList.remove("shimmieext-hidden");
    }

    hide() {
      this.node.classList.add("shimmieext-hidden");
    }

    _add_li(li) {
      this.node.appendChild(li);
      this.items.push(li);
    }

    add_item(tag, count, on_select) {
      let new_li = document.createElement("li");
      new_li.classList.add("shimmieext-autocomplete");
      new_li.innerHTML = `${tag} (${count})`;
      new_li.tabIndex = "0";

      let _this = this;
      let hide_select = function() {
        _this.hide();
        on_select(tag);
      };
      new_li.addEventListener("click", function() {
        hide_select();
      }, false);
      new_li.addEventListener("keydown", function(e) {
        let key_code = e.keyCode || e.which;
        if(key_code === KEYCODE_ENTER) {
          hide_select();
          e.preventDefault();
        }
      }, false);

      this._add_li(new_li);
    }
  }

  class AutocompleteField {
    constructor(elem_input, delay_query, shimmie_api) {
      this.root = elem_input;
      this.delay = delay_query;
      this.api = shimmie_api;
      this.timer = undefined;

      const _this = this;
      this.root.addEventListener("input", function(e) {
        let text = _this.root.value;
        if(text) {
          // Reset delay if another key pressed before finished
          _this.clear_timer();
          _this.timer = setTimeout(function() {
            let cursor_i = e.target.selectionStart;
            // If at end of input or space in front
            let cursor_at_end = text[cursor_i] === undefined;
            if(cursor_at_end || text[cursor_i] === " ") {
              // Get word behind cursor
              let i = cursor_i - 1;
              let c = text[i];
              let word_behind = "";
              while(c !== undefined && c !== " ") {
                word_behind += c;
                i--;
                c = text[i];
              }
              let word_start_i = i + 1;
              if(word_behind) {
                word_behind = reverse_str(word_behind);
                _this.api_get_autocomplete(word_behind, function(res_text) {
                  _this.populate_dropdown(JSON.parse(res_text), word_start_i, cursor_i, cursor_at_end);
                });
              }
            }
          }, _this.delay);
        }
      }, false);

      this.root.addEventListener("keydown", function(e) {
        let key_code = e.keyCode || e.which;
        switch(key_code) {
          case KEYCODE_TAB:
          case KEYCODE_ARROW_DOWN:
          case KEYCODE_ENTER:
            _this.dropdown.set_focus(0);
            _this.clear_timer();
            e.preventDefault();
            break;
        }
      }, false);

      this.dropdown = new AutocompleteDropdown();
      this.dropdown.hide();
      insert_after(this.root, this.dropdown.node);
    }

    clear_timer() {
      if(this.timer) {
        clearTimeout(this.timer);
      }
    }

    populate_dropdown(res_json, insert_start_i, insert_end_i, add_space) {
      this.dropdown.clear();
      let _this = this;
      for (const [k, v] of Object.entries(res_json)) {
        this.dropdown.add_item(k, v, function(selected_tag) {
          _this.root.value = insert_mid_str(_this.root.value, insert_start_i, insert_end_i, selected_tag);
          if(add_space) {
            _this.root.value += " ";
          }
          _this.root.focus();
        });
      }
      this.dropdown.show();
    }

    api_get_autocomplete(query_text, on_load) {
      const xhttp = new XMLHttpRequest();
      const _this = this;
      xhttp.onload = function() {
        on_load(this.responseText);
      };
      xhttp.open("GET", `${_this.api}/api/internal/autocomplete?s=${query_text}`, true);
      xhttp.send();
    }
  }

  /* Upload form autocomplete */
  function page_upload(shimmie_api) {
    let upload_form = document.getElementById("file_upload");
    // Prevent form submit on enter press
    upload_form.addEventListener("keydown", function(e) {
      let key_code = e.keyCode || e.which;
      if(key_code === KEYCODE_ENTER){
        e.preventDefault();
      }
    }, false);

    // Add enter press submit back to post button
    let upload_button = document.getElementById("uploadbutton");
    upload_button.addEventListener("keydown", function(e) {
      let key_code = e.keyCode || e.which;
      if(key_code === KEYCODE_ENTER){
        upload_form.submit();
      }
    }, false);

    let autocomplete_fields = [];
    let autocomplete_inputs = upload_form.getElementsByClassName('autocomplete_tags');
    for (const input_node of autocomplete_inputs) {
      autocomplete_fields.push(new AutocompleteField(input_node, g_autocomplete_query_delay_ms, shimmie_api));
    }

    // Close dropdown on click anywhere else
    document.body.addEventListener("click", function() {
      for (const field of autocomplete_fields) {
        field.dropdown.hide();
      }
    }, false);
  }

  /* Single page copy tags and autocomplete tag editor */
  function page_single_image(shimmie_api) {
    let tag_editor_input = document.getElementById("tag_editor");
    let tag_editor_autocomplete_field = new AutocompleteField(tag_editor_input, g_autocomplete_query_delay_ms, shimmie_api);

    let tags = tag_editor_input.value;
    let info_table = document.querySelector(".image_info tbody");

    let new_tr = document.createElement("tr");
    let new_td = document.createElement("td");
    let new_button = document.createElement("input");

    new_td.colSpan = "4";

    new_button.value = "Copy Tags";
    new_button.type = "button";
    new_button.classList.add("shimmieext-button-copy-tags");
    new_button.addEventListener("click", function() {
      copy_to_clipboard(tags);
    }, false);

    new_td.appendChild(new_button);
    new_tr.appendChild(new_td);
    info_table.appendChild(new_tr);
  }

  function main() {
    let url = window.location.href.split('/');
    let shimmie_api = `${url[0]}//${url[2]}`;

    let page;
    if(url[3] === "post" && url[4] === "view" || url[3] === "random_image" && url[4] === "view") {
      page = page_single_image;
    } else if(url[3] === "upload") {
      page = page_upload;
    }

    if(page) {
      add_page_styles("ul.shimmieext-autocomplete {list-style: none; padding: 2px; margin: 0; display: block; outline: none; color: #444444; border: 1px solid #dddddd; z-index: 100;\
font-size: 1.1em; font-family: Helvetica,Arial,sans-serif; text-align: left; background-color: #ffffff; position: absolute; cursor: pointer;}\
li.shimmieext-autocomplete {list-style: none;}\
li.shimmieext-autocomplete:hover, li.shimmieext-autocomplete:focus {font-weight: bold; background-color: #0a78eb; color: #ffffff; }\
.shimmieext-hidden {display: none !important;}\
.shimmieext-button-copy-tags {cursor: pointer;} .shimmieext-button-copy-tags:active  {background-color:#E9E9ED;}\
.shimmieext-legacy-clipboard-textarea {top: 0; left: 0; position: fixed;}");

      page(shimmie_api);
    }
  }

  main();

})();