Greasy Fork is available in English.

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