Better Google Search

Adds useful features for google searching. English support only. Tested on Brave Browser. Intended to work with light mode and dark mode. Doesn't work on mobile view.

当前为 2021-12-28 提交的版本,查看 最新版本

// ==UserScript==
// @name          Better Google Search
// @namespace     Better Google Search
// @match         https://www.google.com/search?*
// @grant         GM_addStyle
// @require       https://openuserjs.org/src/libs/sizzle/GM_config.js
// @grant         GM_getValue
// @grant         GM_setValue
// @version       1.3
// @author        kyosukyuu
// @description   Adds useful features for google searching. English support only. Tested on Brave Browser. Intended to work with light mode and dark mode. Doesn't work on mobile view.
// @license       MIT
// ==/UserScript==

GM_addStyle (`
  .btns--container {
    display: flex;
    position: absolute;
    width: 100%;
    left: 100%;
    top: 0;
    margin-left: 20px;
    z-index: 1;
    background: #202124;
  }
  .btns--container-light {
    background: #fff;
  }
  .btn {
    margin-right: 8px;
    cursor: pointer;
  }
  .btn--container {
    position: relative;
    padding: 4px;
    margin: 0 8px;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    -o-user-select: none;
    user-select: none;
  }
  .btn--container-light {
    color: #70757a !important;
  }
  .btn--container:hover {
    color: #ddd
  }
  .btn--container-light:hover {
    color: #202124 !important;
  }
/*   .btn--container:hover .caret-dropdown {
    border-color: #ddd transparent;
  }
  .btn--container-light:hover .caret-dropdown {
    border-color: #202124 transparent !important;
  }
 */
  .btn--active {
    color: #e8eaed !important;
  }
  .btn--active-light {
    color: #202124;
  }
  .caret-dropdown--active {
    border-color: #ddd transparent !important;
  }

  .btn--caret::after{
    border-color: #9aa0a6 transparent;
    border-style: solid;
    border-width: 5px 4px 0 4px;
    width: 0;
    height: 0;
    margin-left: 2px;
    top: 13px;
    margin-top: -2px;
    position: absolute;
    right: 0;
    content: " "
  }
  .btn--caret:hover::after, .btn--caret:focus::after {
    border-color: #ddd transparent !important;
  }
  .btn--container:hover > .btn--caret::after {
    border-color: #ddd transparent !important;
  }
  .btn--container-light:hover > .btn--caret::after {
    border-color: #202124 transparent !important;
  }

/*   .caret-dropdown {
    border-color: #9aa0a6 transparent;
    border-style: solid;
    border-width: 5px 4px 0 4px;
    width: 0;
    height: 0;
    margin-left: 2px;
    top: 13px;
    margin-top: -2px;
    position: absolute;
    right: 0;
  } */

  .dropdown--container {
    z-index: 10;
    padding: 5px 0;
    border-radius: 8px;
    box-shadow: 1px 1px 15px 0px #171717;
    background-color: #202124;
    position: absolute;
    width: 100%;
    min-width: 80px;
    max-width: 105px;
    top: 25px;
    overflow: hidden;
    list-style-type: none;
  }
  .dropdown--container-light {
    box-shadow: 0 2px 10px 0 rgb(0 0 0 / 20%);
    background-color: #fff;
  }
  .show {
    display: block !important;
  }
  .hide {
    display: none;
  }

  .dropdown--items {
    line-height: 16px;
    padding: 7px 8px;
    color: #bdc1c6;
    display: flex;
    align-items: center;
    cursor: pointer;
    margin: auto;
  }
  .dropdown--items-light {
    color: #5f6368;
  }
  .dropdown--items:hover, .dropdown--items:focus {
    background-color: rgba(255,255,255,0.1);
  }
  .dropdown--items-light:hover, .dropdown--items-light:focus{
    background-color: rgba(0,0,0,0.1) !important;
  }

  @media only screen and (max-width: 1450px) {
    .btns--container {
      flex-direction: column;
      max-width: 125px;
      box-shadow: 1px 1px 3px 2px #0000003b;
    }
    .dropdown--container {
      left: 113px;
      top: 0;
    }
  }
`);

(() => {
  "use strict"
  
  const fieldDefs = {
    "bypass-csp": {
      "section": [GM_config.create("Functional Settings"), "Affects the behavior of the script"],
      "label": "Bypass CSP",
      "type": "checkbox",
      "default": false,
    },
  }
  
  GM_config.init({
    "id": "settings",
    "title": "Better Google Search Settings",
    "fields": fieldDefs,
  });
  
  const BYPASS_CSP = JSON.parse(GM_getValue("settings"))["bypass-csp"] || false
  
  const qSelect = (selector) => document.querySelector(selector)
  const qSelectAll = (selectors) => document.querySelectorAll(selectors)
  const qAllCallback = (selector, callback) => Array.from(qSelectAll(selector)).forEach((el, i) => callback(el, i))  
  
  let relativeParent = qSelect("input").parentElement.parentElement.parentElement.parentElement
  
  // check for theme
  const getDefaultTheme = () => {
    if (window?.matchMedia("(prefers-color-scheme: dark)").matches) return true
    return false
  };
  const isDark = getDefaultTheme();
  
  // bypass content-security-policy (CSP) to allow the script to work on google images
  // WARNING: this makes innerHTML vulnerable to injection
  if (window.trustedTypes?.createPolicy && BYPASS_CSP) {
    relativeParent = qSelect("input").parentElement.parentElement.parentElement
    window.trustedTypes.createPolicy("default", {
      createHTML: (string, sink) => string
    });
  }
  
  const FILE_TYPE = "FILE_TYPE"
  const EXCLUDE = "EXCLUDE"
  const SITE = "SITE"
  const EXACT_QUERY = "EXACT_QUERY"
  const TERM_APPEARS = "TERM_APPEARS"
  const BEFORE = "BEFORE"
  const AFTER = "AFTER"
  const SETTINGS = "SETTINGS"
  
  // contains all possible search actions you can perform
  const actions = [
    {name: "File Type", action: FILE_TYPE, data: "filetype:", 
     choices: [{name: "PDF", data: "pdf"}, {name: "DOC", data: "doc"}, {name: "DOCX", data: "docx"}, 
               {name: "TXT", data: "txt"}, {name: "LOG", data: "log"}, {name: "PPT", data: "ppt"}, 
               {name: "PPTX", data: "pptx"}, {name: "XML", data: "xml"}, {name: "TORRENT", data: "torrent"},
               {name: "RTF", data: "rtf"}]}, 
    {name: "Exclude", action: EXCLUDE, data: "-", isUnique: false},
    {name: "Site", action: SITE, data: "site:", 
     choices: [{name: "reddit", data: "reddit.com"}, {name: "stack overflow", data: "stackoverflow.com"}, {name: "youtube", data: "youtube.com"}, 
               {name: "twitter", data: "twitter.com"}, {name: "facebook"}, {name: "custom", data: "facebook.com"}]},
    {name: "Exact Query", action: EXACT_QUERY, data: `""`, isUnique: false},
    {name: "Term Appears", action: TERM_APPEARS, 
     choices: [{name: "anywhere in page", data: ""}, {name: "in the title of the page", data: "allintitle:"}, 
               {name: "in the text of the page", data: "allintext:"}, {name: "in the URL of the page", data: "allinurl:"}, 
               {name: "in links to the page", data: "allinanchor:"}]},
    {name: "Before", action: BEFORE, data: "before:", isUnique: true},
    {name: "After", action: AFTER, data: "after:", isUnique: true},
    {name: "Settings", action: SETTINGS, isUnique: true}
  ]
  
  const toggleDropdown = (evt) => {
    if (evt.target !== evt.currentTarget){
      evt.currentTarget.lastElementChild.classList.toggle("show")
      // theme dependent styles
      if (isDark) {
        evt.currentTarget.classList.toggle("btn--active")
      }
      else{
        evt.currentTarget.classList.toggle("btn--active-light")
      }
    }
  }
  
  const createButtons = () => {
    relativeParent.style.position = "relative"

    const buttonsContainer = document.createElement("section")
    relativeParent.appendChild(buttonsContainer)
    buttonsContainer.classList.add("btns--container")
    actions.forEach((action) => {
      let caretDropdown = ""
      let dropdownMenu = ""
      let hasDropdown = false
      
      if (action?.choices) {
        hasDropdown = true
        caretDropdown = `<span class="caret-dropdown"></span>`
        
        // let parentAction = action.data
        let dropdownItems = action.choices.map((item) => `<li class="dropdown--items" data-action="${item.data}" data-parent-action="${action.data ? action.data : ""}">${item.name}</li>`).join("")
            
        dropdownMenu = `
          <ul class="hide dropdown--container">
            ${dropdownItems}
          </ul>
        `
      }
      
      if (hasDropdown){
        buttonsContainer.innerHTML += `
          <section class="btn--container">
            <div class="btn">${action.name}</div>
            ${dropdownMenu}
          </section>
        `
      }
      else{
        buttonsContainer.innerHTML += `
          <section class="btn--container">
            <div class="btn">${action.name}</div>
          </section>
        `
      }
    })
  }
  
  createButtons()
  
  // attach buttons with dropdown function
  qAllCallback(".btn--container", (btn) => btn.addEventListener("click", toggleDropdown))
  qAllCallback(".dropdown--container", (el) => {
    el.addEventListener("click", (evt) => evt.stopPropagation())
    // add caret to buttons with dropdown
    el.previousElementSibling.classList.add("btn--caret")
  })
  
  
  // close dropdown menus when clicked on somewhere other than the dropdown areas
  window.onclick = (evt) => {
    if (!evt.target.matches(".btn")) {
      qAllCallback(".dropdown--container", ({classList}) => classList.contains("show") && classList.remove("show"))
      qAllCallback(".btn--active", ({classList}) => classList.remove("btn--active"))
    }
  }
  
  const attachActions = () => {
    // attach dropdown items with click handler
    qAllCallback(".dropdown--items", (item) => {
      item.addEventListener("click", (evt) => {
        const parentAction = evt.currentTarget.getAttribute("data-parent-action")
        const action = evt.currentTarget.getAttribute("data-action")

        const input = qSelect("input")
        
        // check if unused query already exists
        const expression = new RegExp(`(${parentAction}\\w+\\.\\w+|${parentAction}\\w+|${parentAction})`, "g")
        if(parentAction && input.value.match(expression)) {
          input.value = input.value.replace(expression, (search) => `${parentAction}${action}`)
          
          if (parentAction === "site:"){
            const searchPos = input.value.search(/(site:)/)
            input.setSelectionRange(searchPos + parentAction.length, searchPos + parentAction.length)
          }
          
          input.focus()
          return
        }
        // remove excess spaces
        input.value = input.value.replace(/\s{2,}/g, (search) => " ")
        
        // apply changes to search bar value
        input.value += ` ${parentAction}${action} `
        
        // trim and remove excess spaces
        input.value = input.value.trim().replace(/\s{2,}/g, (search) => " ")
        
        // focus on search bar
        input.focus()
      })
    })
    
    // attach non-having dropdown items with click handler
    qAllCallback(".btn--container", (el, i) => {
      if(el.childElementCount === 1) {
        el.firstElementChild.addEventListener("click", (evt) => {
          const action = actions[i].data
          
          if (actions[i].action === SETTINGS) {
            GM_config.open()
            return
          }
          
          const input = qSelect("input")
          
          // check if query is unique (only one of it should exist)
          const queryExists = new RegExp(`${action}`)
          if(actions[i].isUnique && input.value.match(queryExists)){
            const searchPos = input.value.search(queryExists)
            input.setSelectionRange(searchPos + action.length, searchPos + action.length)
            input.focus()
            return
          }
          
          // check if unused query is already in search bar
          const expression = new RegExp(`(${action}\\W)|(\\W${action}\\B)`, "g")
          if(input.value.match(expression)){
            // set cursor at unused query
            const searchPos = input.value.search(expression)
            
            if(action.length > 2) input.setSelectionRange(searchPos + action.length + 1, searchPos + action.length + 1)
            else input.setSelectionRange(searchPos + 2, searchPos + 2)
            
          }
          else{
            input.value = input.value.trim()
            // apply changes to search bar value
            input.value += ` ${action} `
            input.value = input.value.trim()
          }
          
          if(action === `""`) {
            const quotationsPos = input.value.search(`""`)
            input.setSelectionRange(quotationsPos + 1, quotationsPos + 1)
          }
          //focus on search bar
          input.focus()
        })
      }
    })
  }
  
  attachActions()
  
  if(!isDark){
    qAllCallback(".btns--container", (el) => el.classList.add("btns--container-light"))
    qAllCallback(".btn--container", (el) => el.classList.add("btn--container-light"))
    qAllCallback(".dropdown--container", (el) => el.classList.add("dropdown--container-light"))
    qAllCallback(".dropdown--items", (el) => el.classList.add("dropdown--items-light"))
  }
  
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址