missing settings for sourcegraph(MSFS)

add extra settings to sourcegraph

// ==UserScript==
// @name         missing settings for sourcegraph(MSFS)
// @namespace    https://greasyfork.org
// @version      1.0.0
// @description  add extra settings to sourcegraph
// @author       pot-code
// @match        https://sourcegraph.com/*
// @grant        none
// ==/UserScript==

// TODO:
//  - 切换主题时,需要重新绑定
//  - 切换文件时,重新绑定

;(function() {
  'use strict'
  const PREFIX = 'MSFS'
  const noop = function() {}
  const { logger, LogLevels } = (function() {
    let defaultLevel = 3 // warn level
    let logMethods = ['trace', 'debug', 'info', 'warn', 'error']

    function Logger() {
      const self = this

      function replaceLogMethod(level) {
        logMethods.forEach((methodName, index) => {
          this[methodName] =
            index < level
              ? noop
              : console[methodName === 'debug' ? 'log' : methodName].bind(
                  console,
                  `[${PREFIX}][${methodName.toUpperCase()}]:`
                )
        })
      }

      self.setLevel = level => {
        replaceLogMethod.call(self, level)
      }

      replaceLogMethod.call(self, defaultLevel)
    }
    return {
      logger: new Logger(),
      LogLevels: logMethods.reduce((acc, cur, index) => {
        acc[cur] = index
        return acc
      }, {})
    }
  })()
  function offsetToBody(element) {
    let offsetTop = 0,
      offsetLeft = 0

    while (element !== document.body) {
      offsetLeft += element.offsetLeft
      offsetTop += element.offsetTop
      element = element.offsetParent
    }

    return {
      offsetLeft,
      offsetTop
    }
  }

  /**
   * hepler function to iterate map object
   * @param {Map|Object} mapLikeObject
   * @param {(key, value)=>any} fn function to call on map entry
   */
  function mapIterator(mapLikeObject, fn) {
    if (Object.prototype.toString.call(mapLikeObject) === '[object Map]') {
      mapLikeObject.forEach((value, key) => fn(key, value))
    } else {
      Object.keys(mapLikeObject).forEach(key => fn(key, mapLikeObject[key]))
    }
  }

  function mapAddEntry(mapLikeObject, key, value) {
    if (Object.prototype.toString.call(mapLikeObject) === '[object Map]') {
      mapLikeObject.set(key, value)
    } else {
      mapLikeObject[key] = value
    }
  }

  function createFontController({ codeArea, dropdown }) {
    const DEFAULT_SIZE = '12'
    const fontSizeController = document.createElement('div'),
      label = document.createElement('span'),
      input = document.createElement('input'),
      indicator = document.createElement('span')

    label.innerText = 'font-size'

    input.type = 'range'
    input.min = DEFAULT_SIZE
    input.max = '17'
    input.step = '1'
    input.value = DEFAULT_SIZE
    input.style.margin = '0 0.5rem'

    indicator.style.display = 'inline-block'
    // prevent layout from changing while the character is not monospaced
    indicator.style.width = '17px'
    indicator.innerText = DEFAULT_SIZE

    fontSizeController.appendChild(label)
    fontSizeController.appendChild(input)
    fontSizeController.appendChild(indicator)

    function sizeChangeHandlerFactory(_codeArea) {
      return function() {
        const newSize = input.value

        indicator.innerText = newSize
        _codeArea.style.fontSize = `${newSize}px`
        // logger.debug(`new font size is ${newSize}`);
      }
    }

    let sizeChangeHandler = sizeChangeHandlerFactory(codeArea)

    input.addEventListener('input', sizeChangeHandler)
    dropdown.dom.appendChild(fontSizeController)
    return {
      root: fontSizeController,
      reload: function({ codeArea }) {
        input.removeEventListener('input', sizeChangeHandler)

        sizeChangeHandler = sizeChangeHandlerFactory(codeArea)
        input.addEventListener('input', sizeChangeHandler)
        sizeChangeHandler()
      }
    }
  }

  function initNavItem(navRoot) {
    function create_nav_button(actionName) {
      return `
<div class="popover-button popover-button__btn popover-button__anchor" style="margin: .425rem .175rem;padding: 0 .425rem;color: #566e9f;">
  <span class="popover-button__container">
    ${actionName}
    <svg class="mdi-icon icon-inline popover-button__icon" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
      <path d="M7,10L12,15L17,10H7Z"></path>
    </svg>
  </span>
</div>`
    }

    const navItem = document.createElement('li')

    navItem.classList.add('nav-item')
    navItem.innerHTML = create_nav_button('settings')
    navRoot.appendChild(navItem)

    return {
      dom: navItem
    }
  }

  function initDropdownMenu(navItem) {
    let openState = false // dropdown state

    const container = document.createElement('div')
    const { offsetTop } = offsetToBody(navItem.dom)

    // create container
    container.className = `popover popover-button2__popover rounded ${PREFIX}-settings`
    container.style.transform = `translateY(${offsetTop + navItem.dom.offsetHeight}px)`

    const toggle = event => {
      if (event) {
        event.stopPropagation()
      }
      container.style.display = ((openState = !openState), openState) ? 'block' : 'none'
    }

    // document.body click fix
    document.body.addEventListener('click', event => {
      if (event.target === container) {
        toggle()
      } else {
        if (openState === true) {
          toggle()
        }
      }
    })
    navItem.dom.addEventListener('click', toggle)
    container.addEventListener('click', event => {
      event.stopPropagation()
    })

    logger.debug('dropdown container initialized')
    // insert DOM
    document.body.appendChild(container)
    return {
      dom: container,
      toggle
    }
  }

  function getNavRoot() {
    return document.querySelector(
      '#root > div.layout > div.layout__app-router-container.layout__app-router-container--full-width > div > nav > ul:nth-child(7)'
    )
  }

  function getCodeArea() {
    return document.querySelector('div.blob.blob-page__blob')
  }

  function patch_style(definition) {
    const style = document.createElement('style')

    style.innerHTML = definition
    document.head.appendChild(style)
    logger.debug('======================patched styles======================')
    logger.debug(definition)
    logger.debug('==========================================================')
  }

  const plugin = (function createPlugin() {
    function Plugin() {
      const self = this
      const defaultStyles = `
  .${PREFIX}-settings{
    padding: 0.3em 0.5em;
    position: absolute;
    display: none;
    top: 0;
    right: 10px;
  }
  .${PREFIX}-settings__item{
    display: flex;
    align-items: center;
    margin-bottom: 6px;
  }
  .${PREFIX}-settings__item:last-child{
    margin-bottom: 0;
  }
  div.blob.blob-page__blob{
    font-size: 12px;
  }
  code.blob__code.e2e-blob{
    font-size: inherit;
    line-height: 1.33;
  }
      `

      const definitions = new Map()
      const components = new Map()
      const reloadListeners = []
      const styles = [defaultStyles]

      let stylePatched = false
      let initialized = false
      let navRoot = getNavRoot()
      let codeArea = getCodeArea()
      let navItem = null
      let dropdown = null

      function reloadComponent(newDep, name, component) {
        component.reload && component.reload(newDep)
        logger.debug(`component ${name} is reloaded`)
      }

      function initComponent(dep, name, definition, context) {
        let component = definition(dep, context)

        styles.push(component.style)
        components.set(name, component)
        component.root && component.root.classList.add(`${PREFIX}-settings__item`)
        logger.debug(`component ${name} is initialized`)
      }

      function ensureCriticalElement(reload = false) {
        const MAX_RETRY_COUNT = 10
        const RETRY_DELAY = 1000

        let tried = 0
        let lastNavRoot = navRoot
        let lastCodeArea = codeArea

        return new Promise((res, rej) => {
          function queryElement() {
            ;[navRoot, codeArea] = [reload ? navRoot : getNavRoot(), getCodeArea()]

            if (navRoot && codeArea) {
              if (reload && lastCodeArea === codeArea) {
                if (++tried > MAX_RETRY_COUNT) {
                  logger.warn('max retry count reached, plugin failed to reload or reload is not needed')
                  return
                }
                // supress log
                // logger.debug('failed to detect critical element changes, retrying...')
                setTimeout(queryElement, RETRY_DELAY, reload)
                return
              }
              tried = 0
              ;[lastNavRoot, lastCodeArea] = [navRoot, codeArea]
              res({ navRoot, codeArea })
              logger.debug('critical path created, initializing plugin...')
            } else {
              logger.debug('failed to detect critical element, retrying...')
              if (++tried > MAX_RETRY_COUNT) {
                rej('max retry count reached, plugin failed to initialize')
                return
              }
              setTimeout(queryElement, RETRY_DELAY)
            }
          }
          queryElement()
        })
      }

      /**
       * @param name {string} component name
       * @param definition {(dep, plugin:Plugin)=>{uninstall?:Function, reload?:Function, style?: string, root?:HTMLElement}} component object
       */
      this.registerComponent = (name, definition) => {
        if (name in definition || name in components) {
          logger.warn(`component ${name} is already registered, registration cancelled`)
          return
        }
        mapAddEntry(definitions, name, definition)
      }

      this.reload = () => {
        ensureCriticalElement(true)
          .then(({ codeArea }) => {
            return { navItem, codeArea, dropdown }
          })
          .then(newDep => {
            mapIterator(components, function(name, component) {
              reloadComponent(newDep, name, component)
            })
            logger.debug('plugin reloaded')
          })
          .catch(err => {
            logger.error(err)
          })
      }

      const initComponents = dep => {
        mapIterator(definitions, function(name, definition) {
          initComponent(dep, name, definition, self)
        })
      }

      this.init = () => {
        return ensureCriticalElement()
          .then(({ navRoot, codeArea }) => {
            navItem = initNavItem(navRoot)
            dropdown = initDropdownMenu(navItem)

            return { navItem, codeArea, dropdown }
          })
          .then(dep => {
            initComponents(dep)
            patch_style(styles.join('\n'))

            stylePatched = true
            initialized = true
          })
      }

      /**
       * @param target {HTMLElement} event source element which may change the dependencies
       * @param event {string} event name
       * @param shouldReload {(event:Event)=>boolean} should trigger the reload
       * @param capture should event be capture type
       */
      this.addReloadListener = (target, event, shouldReload, capture = false) => {
        shouldReload = shouldReload.bind(target)

        let handler = _event => {
          if (shouldReload(_event)) {
            this.reload()
            logger.debug(`plugin will reload due to the ${event} event on ${target}`)
          }
        }

        target.addEventListener(event, handler, capture)
        reloadListeners.push({
          target,
          event,
          handler,
          capture,
          dispose: function() {
            target.removeEventListener(event, handler, capture)
          }
        })
      }
    }

    return new Plugin()
  })()

  // logger.setLevel(LogLevels.debug)
  plugin.registerComponent('font-controller', createFontController)

  //===============================Danger Zone===============================
  plugin
    .init()
    .then(() => {
      plugin.addReloadListener(
        document.querySelector('#explorer>div.tree>table>tbody>tr>td>div>table'),
        'click',
        function(event) {
          let target = event.target

          return target.tagName === 'A' && target.className === 'tree__row-contents'
        }
      )
    })
    .catch(err => {
      logger.error(err)
    })
  //=========================================================================
})()