missing settings for sourcegraph(MSFS)

add extra settings to sourcegraph

As of 27.05.2019. See ბოლო ვერსია.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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)
    })
  //=========================================================================
})()