Google Sheets Stopwatch

1/7/2022, 9:06:21 PM

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

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

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

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.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name        Google Sheets Stopwatch
// @namespace   Violentmonkey Scripts
// @match       https://docs.google.com/spreadsheets/d/*/edit
// @grant       none
// @version     1.0
// @author      -
// @description 1/7/2022, 9:06:21 PM
// @license MIT
// ==/UserScript==
  function mountTimer(mountToEl) {
    const $ = (...args) => {
      if (args.length > 1) args.reverse()
      const [query, parent = document] = args
      return Array.from(parent.querySelectorAll(query))
    }

    $.create = (html) => {
      const div = document.createElement('div')
      div.innerHTML = html
      return div.children[0]
    }

    const unique = (arr) => [...new Set(arr)]

    const uniqueId = (() => {
      let id = 1
      return () => id++
    })()

    const normalizeEventName = (name) =>
      name.trim().toLowerCase().replace(/^on/, '')

    const mount = (root, elem) => {
      root.innerHTML = ''
      root.appendChild(elem)
    }

    const REGEXES = {
      openingHtmlTags: /<[^\/].*?>/gm,
      nameAndFunc: /\s(on[A-Z].+?\})/gm,
    }

    function renderWithEvents(htmlStr, events) {
      const eventStore = {}

      const newHtmlStr = htmlStr.replace(REGEXES.openingHtmlTags, (tag) => {
        const tagId = uniqueId()
        return tag.replace(REGEXES.nameAndFunc, (nameAndFunc) => {
          const [eventNameCamel, funcName] = nameAndFunc
            .replace(/[\{\}]/g, '')
            .split('=')

          eventStore[tagId + '-' + normalizeEventName(eventNameCamel)] =
            funcName
          return ' data-id="' + tagId + '"'
        })
      })

      const root = $.create(newHtmlStr)

      const allEventNames = unique(
        Object.keys(eventStore).map((v) => v.split('-')[1])
      )

      allEventNames.forEach((eventName) => {
        root.addEventListener(eventName, (e) => {
          const funcName =
            eventStore[e.target.getAttribute('data-id') + '-' + eventName]

          funcName && events[funcName] && events[funcName](e)
        })
      })

      return $(root, '[id]').reduce(
        (acc, el) => {
          acc['$' + el.id] = el
          return acc
        },
        { $root: root }
      )
    }
    const padZero = (val, n) =>
      val.toString().length > n ? val : ('0' + val).slice(n * -1)

    const startOfDay = (date) => new Date(new Date(date).setHours(0, 0, 0, 0))

    const secsToTime = (secs, withSeconds = false) => {
      const mmss = new Date(startOfDay(new Date()).getTime() + secs * 1000)
        .toString()
        .substr(19, withSeconds ? 5 : 2)
      const hh = padZero(Math.floor(secs / 60 / 60), 2)

      return [hh, mmss].join(':')
    }

    const createInterval = () => {
      let interval = null
      const start = (cb, delay) =>
        !interval && (interval = setInterval(cb, delay))
      const stop = () => {
        clearInterval(interval)
        interval = null
      }
      return { start, stop }
    }

    const createTimer = (initial, delay = 1000) => {
      const interval = createInterval()
      let duration = initial
      let lastPlay = null

      const play = (cb) => {
        interval.start(() => {
          cb((duration += new Date().getTime() / 1000 - lastPlay))
          lastPlay = new Date().getTime() / 1000
        }, delay)

        lastPlay = new Date().getTime() / 1000
      }
      const pause = () => interval.stop()

      return { play, pause }
    }

    let timer = createTimer(0, 1000)

    const state = {
      status: 'paused',
      duration: '00:00:00',
    }

    function updateState(fresh) {
      Object.assign(state, fresh)
      render(state)
    }

    const DOMEvents = {
      startStop() {
        const newStatus = state.status === 'playing' ? 'paused' : 'playing'

        newStatus === 'playing'
          ? timer.play((seconds) =>
              updateState({ duration: secsToTime(seconds, true) })
            )
          : timer.pause()

        updateState({
          status: newStatus,
        })
      },

      reset() {
        timer.pause()
        timer = createTimer(0, 1000)
        updateState({
          duration: '00:00:00',
          status: 'paused',
        })
      },
    }

    function render(state) {
      const style = {
        base: [
          'border-radius: 100px',
          'border:none',
          'padding: 6px 16px',
          'color: #fff',
          'text-decoration: none',
          'font-size: 16px',
          'cursor: pointer',
          'background: #D2D2D2',
        ].join(';'),
        active: ['background: #76B3FA'].join(';'),
      }

      const template = `
          <div>
            <button id='timer' style="${[
              style.base,
              state.status === 'playing' ? style.active : '',
            ].join(';')}" onClick={startStop}>${state.duration}</button>
            <button onClick={reset} style="${style.base}">x</button>
          </div>
        `

      const { $root } = renderWithEvents(template, DOMEvents)
      mount(mountToEl, $root)
    }

    render(state)
  }

  const div = document.createElement('div')
  mountTimer(div)
  document.querySelector('.docs-titlebar-buttons').prepend(div)