Page Centering

ウェブページを中央配置

// ==UserScript==
// @name         Page Centering
// @namespace    https://greasyfork.org/users/1009-kengo321
// @version      4
// @description  ウェブページを中央配置
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM.getValue
// @grant        GM.setValue
// @match        *://*/*
// @license      MIT
// @noframes
// @run-at       document-start
// ==/UserScript==

;(function() {
  'use strict'

  var createObject = function(prototype, properties) {
    var descriptors = function() {
      return Object.keys(properties).reduce(function(descriptors, key) {
        descriptors[key] = Object.getOwnPropertyDescriptor(properties, key)
        return descriptors
      }, {})
    }
    return Object.defineProperties(Object.create(prototype), descriptors())
  }
  var invoke = function(methodName, args) {
    return function(target) {
      return target[methodName].apply(target, args)
    }
  }

  var EventEmitter = (function() {
    var EventEmitter = function() {
      this._eventNameToListeners = new Map()
    }
    EventEmitter.prototype = {
      on(eventName, listener) {
        var m = this._eventNameToListeners
        var v = m.get(eventName)
        if (v) {
          v.add(listener)
        } else {
          m.set(eventName, new Set([listener]))
        }
        return this
      },
      off(eventName, listener) {
        var v = this._eventNameToListeners.get(eventName)
        if (v) v.delete(listener)
        return this
      },
      emit(eventName) {
        var m = this._eventNameToListeners
        var args = Array.from(arguments).slice(1)
        for (var l of m.get(eventName) || []) l(...args)
      },
    }
    return EventEmitter
  })()

  var PageCentering = (function() {
    var PageCentering = function(obj) {
      this.url = obj.url
      this.maxWidth = obj.maxWidth
      this.matched = false
    }
    PageCentering.prototype = {
      setMatchedIfStarted(url) {
        this.matched = url.startsWith(this.url)
        return this
      },
      center(doc) {
        var s = doc.documentElement.style
        s.maxWidth = this.maxWidth
        s.position = 'relative'
        s.margin = '0px auto'
      },
    }
    Object.assign(PageCentering, {
      clear(doc) {
        var s = doc.documentElement.style
        s.maxWidth = ''
        s.position = ''
        s.margin = ''
      },
      MATCHED_URL_ORDER(o1, o2) {
        if (o1.matched && !o2.matched) return -1
        if (!o1.matched && o2.matched) return 1
        if (o1.url < o2.url) return -1
        if (o1.url > o2.url) return 1
        return 0
      },
      takeLongerURL(o1, o2) {
        if (o1) return o1.url.length >= o2.url.length ? o1 : o2
        return o2
      },
    })
    return PageCentering
  })()

  var Config = (function(_super) {
    var Config = function(get, set) {
      _super.call(this)
      this.get = get
      this.set = set
    }
    Config.prototype = createObject(_super.prototype, {
      async _getPageCenteringObjs() {
        return JSON.parse(await this.get('pageCenterings', '[]'))
      },
      async getPageCenterings() {
        return (await this._getPageCenteringObjs()).map(function(o) {
          return new PageCentering(o)
        })
      },
      async _emitPageCenteringsChanged() {
        this.emit('pageCenteringsChanged', await this.getPageCenterings())
      },
      async setPageCentering(pageCentering) {
        var f = function(o) { return o.url !== pageCentering.url }
        var newObj = {url: pageCentering.url, maxWidth: pageCentering.maxWidth}
        var newObjs = (await this._getPageCenteringObjs()).filter(f).concat(newObj)
        await this.set('pageCenterings', JSON.stringify(newObjs))
        await this._emitPageCenteringsChanged()
      },
      async deletePageCentering(url) {
        var f = function(o) { return o.url !== url }
        var newObjs = (await this._getPageCenteringObjs()).filter(f)
        await this.set('pageCenterings', JSON.stringify(newObjs))
        await this._emitPageCenteringsChanged()
      },
    })
    return Config
  })(EventEmitter)

  var ConfigDialog = (function(_super) {
    var initUrlCell = function(urlCell, pageCentering) {
      urlCell.textContent = pageCentering.url
      if (pageCentering.matched) urlCell.className = 'matched'
    }
    var focusAndSelect = function(elem) {
      elem.focus()
      elem.select()
    }
    var ENTER_KEY = 13
    var ESCAPE_KEY = 27
    var invokeIf = function(key, func) {
      return function(event) {
        if (event.which === key) func()
      }
    }
    var ConfigDialog = function(doc) {
      _super.call(this)
      this.doc = doc
      this.targetURL = this._currentURL
      this._addEventListeners()
      focusAndSelect(this.targetUrlInput)
    }
    ConfigDialog.prototype = createObject(_super.prototype, {
      get _currentURL() {
        return this.doc.defaultView.frameElement.ownerDocument.location.href
      },
      _addEventListeners() {
        var emitPageCenteringChangedIfValid
          = this._emitPageCenteringChangedIfValid.bind(this)
        ;[[this.closeButton, [
            ['click', this._close.bind(this)],
          ]],
          [this.changeButton, [
            ['click', emitPageCenteringChangedIfValid],
          ]],
          [this.targetUrlInput, [
            ['keydown', invokeIf(ENTER_KEY, emitPageCenteringChangedIfValid)],
          ]],
          [this.maxWidthInput, [
            ['keydown', invokeIf(ENTER_KEY, emitPageCenteringChangedIfValid)],
            ['input', this._updateMaxWidthInputValidity.bind(this)],
            ['input', this._updateChangeButtonDisabled.bind(this)],
          ]],
          [this.doc, [
            ['keydown', invokeIf(ESCAPE_KEY, this._close.bind(this))],
          ]],
        ].forEach(function(a) {
          a[1].forEach(function(b) {
            a[0].addEventListener(b[0], b[1])
          })
        })
      },
      get targetUrlInput() {
        return this.doc.getElementById('targetUrlInput')
      },
      get targetURL() {
        return this.targetUrlInput.value
      },
      set targetURL(targetURL) {
        this.targetUrlInput.value = targetURL
      },
      get maxWidthInput() {
        return this.doc.getElementById('maxWidthInput')
      },
      get maxWidth() {
        return this.maxWidthInput.value
      },
      set maxWidth(maxWidth) {
        this.maxWidthInput.value = maxWidth
      },
      get closeButton() {
        return this.doc.getElementById('closeButton')
      },
      get changeButton() {
        return this.doc.getElementById('changeButton')
      },
      _emitPageCenteringChangedIfValid() {
        if (!this._hasValidMaxWidth()) return
        var o = {url: this.targetURL, maxWidth: this.maxWidth}
        this.emit('pageCenteringChanged', new PageCentering(o))
      },
      _sort(pageCenterings) {
        return pageCenterings
          .map(invoke('setMatchedIfStarted', [this._currentURL]))
          .sort(PageCentering.MATCHED_URL_ORDER)
      },
      _initDelCell(delCell, pageCentering) {
        delCell.textContent = '削除'
        delCell.addEventListener('click', function() {
          this.emit('pageCenteringDeleted', pageCentering.url)
        }.bind(this))
      },
      _initSettingCell(settingCell, pageCentering) {
        settingCell.textContent = 'セット'
        settingCell.addEventListener('click', function() {
          this.targetURL = pageCentering.url
          this.maxWidth = pageCentering.maxWidth
          focusAndSelect(this.maxWidthInput)
          this._updateMaxWidthInputValidity()
          this._updateChangeButtonDisabled()
        }.bind(this))
      },
      get dataTable() {
        return this.doc.getElementById('dataTable')
      },
      set pageCenterings(pageCenterings) {
        var t = this.dataTable
        for (var r of Array.from(t.rows)) r.remove()
        for (var c of this._sort(pageCenterings)) {
          var tr = t.insertRow(-1)
          initUrlCell(tr.insertCell(-1), c)
          tr.insertCell(-1).textContent = c.maxWidth
          this._initSettingCell(tr.insertCell(-1), c)
          this._initDelCell(tr.insertCell(-1), c)
        }
      },
      _close() {
        this.doc.defaultView.frameElement.remove()
        this.emit('closed')
      },
      _hasValidMaxWidth() {
        var e = this.doc.createElement('div')
        e.style.maxWidth = this.maxWidthInput.value
        return e.style.maxWidth !== ''
      },
      _updateMaxWidthInputValidity() {
        var MESSAGE = 'CSS の max-width プロパティに有効な値のみを設定できます。'
        if (this._hasValidMaxWidth()) {
          this.maxWidthInput.setCustomValidity('')
        } else {
          this.maxWidthInput.setCustomValidity(MESSAGE)
        }
      },
      _updateChangeButtonDisabled() {
        this.changeButton.disabled = !this._hasValidMaxWidth()
      },
    })
    var frameLoaded = function(frame, back, config) {
      var set = function(target, propName) {
        return function(value) { target[propName] = value }
      }
      var clear = function(updateDialog) {
        return function() {
          back.remove()
          config.off('pageCenteringsChanged', updateDialog)
        }
      }
      return async function() {
        var dialog = new ConfigDialog(frame.contentDocument)
          .on('pageCenteringDeleted', config.deletePageCentering.bind(config))
          .on('pageCenteringChanged', config.setPageCentering.bind(config))
        dialog.pageCenterings = await config.getPageCenterings()
        var updateDialog = set(dialog, 'pageCenterings')
        config.on('pageCenteringsChanged', updateDialog)
        dialog.on('closed', clear(updateDialog))
      }
    }
    ConfigDialog.show = function(doc, config) {
      var MAX_Z_INDEX = 2147483647
      var back = doc.createElement('div')
      back.style.backgroundColor = 'black'
      back.style.opacity = '0.5'
      back.style.zIndex = MAX_Z_INDEX - 1
      back.style.position = 'fixed'
      back.style.top = '0'
      back.style.left = '0'
      back.style.width = '100%'
      back.style.height = '100%'
      doc.body.appendChild(back)
      var f = doc.createElement('iframe')
      f.style.position = 'fixed'
      f.style.top = '0'
      f.style.left = '0'
      f.style.width = '100%'
      f.style.height = '100%'
      f.style.zIndex = MAX_Z_INDEX
      f.srcdoc = ConfigDialog.SRC_DOC
      f.addEventListener('load', frameLoaded(f, back, config))
      doc.body.appendChild(f)
    }
    ConfigDialog.SRC_DOC = `<!doctype html>
<html><head><style>
  html {
    margin: 0 auto;
    max-width: 50em;
    height: 100%;
    line-height: 1.5em;
  }
  body {
    height: 100%;
    margin: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
  }
  p {
    margin: 0;
  }
  #dialog {
    overflow: auto;
    padding: 8px;
    background-color: white;
  }
  #top {
    text-align: right;
  }
  #targetUrlInput {
    width: 100%;
  }
  #maxWidthInput:invalid {
    color: red;
    font-weight: bold;
  }
  #dataTable {
    border-collapse: collapse;
  }
  #dataTable td {
    border: solid thin;
    padding: 0 0.5em;
    line-height: 1.5em;
  }
  #dataTable td:nth-child(1) {
    word-break: break-all;
  }
  #dataTable td:nth-child(2) {
    text-align: right;
    white-space: nowrap;
  }
  #dataTable td:nth-child(3),
  #dataTable td:nth-child(4) {
    text-decoration: underline;
    cursor: pointer;
    white-space: nowrap;
  }
  .matched {
    font-weight: bold;
  }
</style></head><body>
  <div id=dialog>
    <p id=top><input type=button value=閉じる id=closeButton></p>
    <p><label for=targetUrlInput>対象URL(前方一致):</label></p>
    <p><input type=url id=targetUrlInput></p>
    <p><label for=maxWidthInput>最大幅:</label></p>
    <p><input value=1000px id=maxWidthInput></p>
    <p><input type=button value=追加/変更 id=changeButton></p>
    <table id=dataTable></table>
  </div>
</body></html>`
    return ConfigDialog
  })(EventEmitter)

  function addConfigButtonIfScriptPage(config) {
    if (!location.href.startsWith('https://greasyfork.org/ja/scripts/15722-page-centering'))
      return
    const add = () => {
      const e = document.createElement('button')
      e.type = 'button'
      e.textContent = '設定'
      e.addEventListener('click', function() {
        ConfigDialog.show(document, config)
      })
      document.querySelector('#script-info > header > h2').appendChild(e)
    }
    if (['interactive', 'complete'].includes(document.readyState))
      add()
    else
      document.addEventListener('DOMContentLoaded', add)
  }
  var main = async function() {
    const [get, set] = typeof GM_getValue === 'undefined'
                       ? [GM.getValue, GM.setValue]
                       : [GM_getValue, GM_setValue]
    var config = new Config(get, set)
    var updateCentering = function(pageCenterings) {
      var c = pageCenterings
        .map(invoke('setMatchedIfStarted', [document.location.href]))
        .filter(function(c) { return c.matched })
        .reduce(PageCentering.takeLongerURL, null)
      if (c) c.center(document); else PageCentering.clear(document)
    }
    updateCentering(await config.getPageCenterings())
    config.on('pageCenteringsChanged', updateCentering)
    if (typeof GM_registerMenuCommand !== 'undefined') {
      GM_registerMenuCommand('Page Centering 設定', function() {
        ConfigDialog.show(document, config)
      })
    }
    addConfigButtonIfScriptPage(config)
  }

  main()
})()