missing settings for sourcegraph(MSFS)

add extra settings to sourcegraph

  1. // ==UserScript==
  2. // @name missing settings for sourcegraph(MSFS)
  3. // @namespace https://greasyfork.org
  4. // @version 1.0.0
  5. // @description add extra settings to sourcegraph
  6. // @author pot-code
  7. // @match https://sourcegraph.com/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. // TODO:
  12. // - 切换主题时,需要重新绑定
  13. // - 切换文件时,重新绑定
  14.  
  15. ;(function() {
  16. 'use strict'
  17. const PREFIX = 'MSFS'
  18. const noop = function() {}
  19. const { logger, LogLevels } = (function() {
  20. let defaultLevel = 3 // warn level
  21. let logMethods = ['trace', 'debug', 'info', 'warn', 'error']
  22.  
  23. function Logger() {
  24. const self = this
  25.  
  26. function replaceLogMethod(level) {
  27. logMethods.forEach((methodName, index) => {
  28. this[methodName] =
  29. index < level
  30. ? noop
  31. : console[methodName === 'debug' ? 'log' : methodName].bind(
  32. console,
  33. `[${PREFIX}][${methodName.toUpperCase()}]:`
  34. )
  35. })
  36. }
  37.  
  38. self.setLevel = level => {
  39. replaceLogMethod.call(self, level)
  40. }
  41.  
  42. replaceLogMethod.call(self, defaultLevel)
  43. }
  44. return {
  45. logger: new Logger(),
  46. LogLevels: logMethods.reduce((acc, cur, index) => {
  47. acc[cur] = index
  48. return acc
  49. }, {})
  50. }
  51. })()
  52. function offsetToBody(element) {
  53. let offsetTop = 0,
  54. offsetLeft = 0
  55.  
  56. while (element !== document.body) {
  57. offsetLeft += element.offsetLeft
  58. offsetTop += element.offsetTop
  59. element = element.offsetParent
  60. }
  61.  
  62. return {
  63. offsetLeft,
  64. offsetTop
  65. }
  66. }
  67.  
  68. /**
  69. * hepler function to iterate map object
  70. * @param {Map|Object} mapLikeObject
  71. * @param {(key, value)=>any} fn function to call on map entry
  72. */
  73. function mapIterator(mapLikeObject, fn) {
  74. if (Object.prototype.toString.call(mapLikeObject) === '[object Map]') {
  75. mapLikeObject.forEach((value, key) => fn(key, value))
  76. } else {
  77. Object.keys(mapLikeObject).forEach(key => fn(key, mapLikeObject[key]))
  78. }
  79. }
  80.  
  81. function mapAddEntry(mapLikeObject, key, value) {
  82. if (Object.prototype.toString.call(mapLikeObject) === '[object Map]') {
  83. mapLikeObject.set(key, value)
  84. } else {
  85. mapLikeObject[key] = value
  86. }
  87. }
  88.  
  89. function createFontController({ codeArea, dropdown }) {
  90. const DEFAULT_SIZE = '12'
  91. const fontSizeController = document.createElement('div'),
  92. label = document.createElement('span'),
  93. input = document.createElement('input'),
  94. indicator = document.createElement('span')
  95.  
  96. label.innerText = 'font-size'
  97.  
  98. input.type = 'range'
  99. input.min = DEFAULT_SIZE
  100. input.max = '17'
  101. input.step = '1'
  102. input.value = DEFAULT_SIZE
  103. input.style.margin = '0 0.5rem'
  104.  
  105. indicator.style.display = 'inline-block'
  106. // prevent layout from changing while the character is not monospaced
  107. indicator.style.width = '17px'
  108. indicator.innerText = DEFAULT_SIZE
  109.  
  110. fontSizeController.appendChild(label)
  111. fontSizeController.appendChild(input)
  112. fontSizeController.appendChild(indicator)
  113.  
  114. function sizeChangeHandlerFactory(_codeArea) {
  115. return function() {
  116. const newSize = input.value
  117.  
  118. indicator.innerText = newSize
  119. _codeArea.style.fontSize = `${newSize}px`
  120. // logger.debug(`new font size is ${newSize}`);
  121. }
  122. }
  123.  
  124. let sizeChangeHandler = sizeChangeHandlerFactory(codeArea)
  125.  
  126. input.addEventListener('input', sizeChangeHandler)
  127. dropdown.dom.appendChild(fontSizeController)
  128. return {
  129. root: fontSizeController,
  130. reload: function({ codeArea }) {
  131. input.removeEventListener('input', sizeChangeHandler)
  132.  
  133. sizeChangeHandler = sizeChangeHandlerFactory(codeArea)
  134. input.addEventListener('input', sizeChangeHandler)
  135. sizeChangeHandler()
  136. }
  137. }
  138. }
  139.  
  140. function initNavItem(navRoot) {
  141. function create_nav_button(actionName) {
  142. return `
  143. <div class="popover-button popover-button__btn popover-button__anchor" style="margin: .425rem .175rem;padding: 0 .425rem;color: #566e9f;">
  144. <span class="popover-button__container">
  145. ${actionName}
  146. <svg class="mdi-icon icon-inline popover-button__icon" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
  147. <path d="M7,10L12,15L17,10H7Z"></path>
  148. </svg>
  149. </span>
  150. </div>`
  151. }
  152.  
  153. const navItem = document.createElement('li')
  154.  
  155. navItem.classList.add('nav-item')
  156. navItem.innerHTML = create_nav_button('settings')
  157. navRoot.appendChild(navItem)
  158.  
  159. return {
  160. dom: navItem
  161. }
  162. }
  163.  
  164. function initDropdownMenu(navItem) {
  165. let openState = false // dropdown state
  166.  
  167. const container = document.createElement('div')
  168. const { offsetTop } = offsetToBody(navItem.dom)
  169.  
  170. // create container
  171. container.className = `popover popover-button2__popover rounded ${PREFIX}-settings`
  172. container.style.transform = `translateY(${offsetTop + navItem.dom.offsetHeight}px)`
  173.  
  174. const toggle = event => {
  175. if (event) {
  176. event.stopPropagation()
  177. }
  178. container.style.display = ((openState = !openState), openState) ? 'block' : 'none'
  179. }
  180.  
  181. // document.body click fix
  182. document.body.addEventListener('click', event => {
  183. if (event.target === container) {
  184. toggle()
  185. } else {
  186. if (openState === true) {
  187. toggle()
  188. }
  189. }
  190. })
  191. navItem.dom.addEventListener('click', toggle)
  192. container.addEventListener('click', event => {
  193. event.stopPropagation()
  194. })
  195.  
  196. logger.debug('dropdown container initialized')
  197. // insert DOM
  198. document.body.appendChild(container)
  199. return {
  200. dom: container,
  201. toggle
  202. }
  203. }
  204.  
  205. function getNavRoot() {
  206. return document.querySelector(
  207. '#root > div.layout > div.layout__app-router-container.layout__app-router-container--full-width > div > nav > ul:nth-child(7)'
  208. )
  209. }
  210.  
  211. function getCodeArea() {
  212. return document.querySelector('div.blob.blob-page__blob')
  213. }
  214.  
  215. function patch_style(definition) {
  216. const style = document.createElement('style')
  217.  
  218. style.innerHTML = definition
  219. document.head.appendChild(style)
  220. logger.debug('======================patched styles======================')
  221. logger.debug(definition)
  222. logger.debug('==========================================================')
  223. }
  224.  
  225. const plugin = (function createPlugin() {
  226. function Plugin() {
  227. const self = this
  228. const defaultStyles = `
  229. .${PREFIX}-settings{
  230. padding: 0.3em 0.5em;
  231. position: absolute;
  232. display: none;
  233. top: 0;
  234. right: 10px;
  235. }
  236. .${PREFIX}-settings__item{
  237. display: flex;
  238. align-items: center;
  239. margin-bottom: 6px;
  240. }
  241. .${PREFIX}-settings__item:last-child{
  242. margin-bottom: 0;
  243. }
  244. div.blob.blob-page__blob{
  245. font-size: 12px;
  246. }
  247. code.blob__code.e2e-blob{
  248. font-size: inherit;
  249. line-height: 1.33;
  250. }
  251. `
  252.  
  253. const definitions = new Map()
  254. const components = new Map()
  255. const reloadListeners = []
  256. const styles = [defaultStyles]
  257.  
  258. let stylePatched = false
  259. let initialized = false
  260. let navRoot = getNavRoot()
  261. let codeArea = getCodeArea()
  262. let navItem = null
  263. let dropdown = null
  264.  
  265. function reloadComponent(newDep, name, component) {
  266. component.reload && component.reload(newDep)
  267. logger.debug(`component ${name} is reloaded`)
  268. }
  269.  
  270. function initComponent(dep, name, definition, context) {
  271. let component = definition(dep, context)
  272.  
  273. styles.push(component.style)
  274. components.set(name, component)
  275. component.root && component.root.classList.add(`${PREFIX}-settings__item`)
  276. logger.debug(`component ${name} is initialized`)
  277. }
  278.  
  279. function ensureCriticalElement(reload = false) {
  280. const MAX_RETRY_COUNT = 10
  281. const RETRY_DELAY = 1000
  282.  
  283. let tried = 0
  284. let lastNavRoot = navRoot
  285. let lastCodeArea = codeArea
  286.  
  287. return new Promise((res, rej) => {
  288. function queryElement() {
  289. ;[navRoot, codeArea] = [reload ? navRoot : getNavRoot(), getCodeArea()]
  290.  
  291. if (navRoot && codeArea) {
  292. if (reload && lastCodeArea === codeArea) {
  293. if (++tried > MAX_RETRY_COUNT) {
  294. logger.warn('max retry count reached, plugin failed to reload or reload is not needed')
  295. return
  296. }
  297. // supress log
  298. // logger.debug('failed to detect critical element changes, retrying...')
  299. setTimeout(queryElement, RETRY_DELAY, reload)
  300. return
  301. }
  302. tried = 0
  303. ;[lastNavRoot, lastCodeArea] = [navRoot, codeArea]
  304. res({ navRoot, codeArea })
  305. logger.debug('critical path created, initializing plugin...')
  306. } else {
  307. logger.debug('failed to detect critical element, retrying...')
  308. if (++tried > MAX_RETRY_COUNT) {
  309. rej('max retry count reached, plugin failed to initialize')
  310. return
  311. }
  312. setTimeout(queryElement, RETRY_DELAY)
  313. }
  314. }
  315. queryElement()
  316. })
  317. }
  318.  
  319. /**
  320. * @param name {string} component name
  321. * @param definition {(dep, plugin:Plugin)=>{uninstall?:Function, reload?:Function, style?: string, root?:HTMLElement}} component object
  322. */
  323. this.registerComponent = (name, definition) => {
  324. if (name in definition || name in components) {
  325. logger.warn(`component ${name} is already registered, registration cancelled`)
  326. return
  327. }
  328. mapAddEntry(definitions, name, definition)
  329. }
  330.  
  331. this.reload = () => {
  332. ensureCriticalElement(true)
  333. .then(({ codeArea }) => {
  334. return { navItem, codeArea, dropdown }
  335. })
  336. .then(newDep => {
  337. mapIterator(components, function(name, component) {
  338. reloadComponent(newDep, name, component)
  339. })
  340. logger.debug('plugin reloaded')
  341. })
  342. .catch(err => {
  343. logger.error(err)
  344. })
  345. }
  346.  
  347. const initComponents = dep => {
  348. mapIterator(definitions, function(name, definition) {
  349. initComponent(dep, name, definition, self)
  350. })
  351. }
  352.  
  353. this.init = () => {
  354. return ensureCriticalElement()
  355. .then(({ navRoot, codeArea }) => {
  356. navItem = initNavItem(navRoot)
  357. dropdown = initDropdownMenu(navItem)
  358.  
  359. return { navItem, codeArea, dropdown }
  360. })
  361. .then(dep => {
  362. initComponents(dep)
  363. patch_style(styles.join('\n'))
  364.  
  365. stylePatched = true
  366. initialized = true
  367. })
  368. }
  369.  
  370. /**
  371. * @param target {HTMLElement} event source element which may change the dependencies
  372. * @param event {string} event name
  373. * @param shouldReload {(event:Event)=>boolean} should trigger the reload
  374. * @param capture should event be capture type
  375. */
  376. this.addReloadListener = (target, event, shouldReload, capture = false) => {
  377. shouldReload = shouldReload.bind(target)
  378.  
  379. let handler = _event => {
  380. if (shouldReload(_event)) {
  381. this.reload()
  382. logger.debug(`plugin will reload due to the ${event} event on ${target}`)
  383. }
  384. }
  385.  
  386. target.addEventListener(event, handler, capture)
  387. reloadListeners.push({
  388. target,
  389. event,
  390. handler,
  391. capture,
  392. dispose: function() {
  393. target.removeEventListener(event, handler, capture)
  394. }
  395. })
  396. }
  397. }
  398.  
  399. return new Plugin()
  400. })()
  401.  
  402. // logger.setLevel(LogLevels.debug)
  403. plugin.registerComponent('font-controller', createFontController)
  404.  
  405. //===============================Danger Zone===============================
  406. plugin
  407. .init()
  408. .then(() => {
  409. plugin.addReloadListener(
  410. document.querySelector('#explorer>div.tree>table>tbody>tr>td>div>table'),
  411. 'click',
  412. function(event) {
  413. let target = event.target
  414.  
  415. return target.tagName === 'A' && target.className === 'tree__row-contents'
  416. }
  417. )
  418. })
  419. .catch(err => {
  420. logger.error(err)
  421. })
  422. //=========================================================================
  423. })()