bilibili-subtitle-to-text

一次性展示bilibili的cc字幕。适合需要快速阅读字幕的场景。

当前为 2024-07-25 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         bilibili-subtitle-to-text
// @namespace    http://tampermonkey.net/
// @version      0.32
// @description  一次性展示bilibili的cc字幕。适合需要快速阅读字幕的场景。
// @author       You
// @match        https://www.bilibili.com/video/*
// @icon         https://www.bilibili.com/favicon.ico
// @grant        GM_addStyle
// @grant        GM.getValue
// @grant        GM.setValue
// @require      https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/3.6.0/jquery.min.js
// @license      GNU GPLv3
// ==/UserScript==

// us for userscript
GM_addStyle(`
  .us-popup-reader {
  }
  
  .us-popup-reader::-webkit-scrollbar-track {
    -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
    background-color: #F5F5F5;
  }
  
  .us-popup-reader::-webkit-scrollbar {
    width: 6px;
      height: 6px;
    background-color: #F5F5F5;
  }
  
  .us-popup-reader::-webkit-scrollbar-thumb {
    background-color: darkgrey;
  }
  
  .us-lyric-line {
      display: flex;
  }
  .us-lyric-line span {
    margin-left: 0.75rem;
  }
  
  .us-lyric-line-time {
      flex: none;
      overflow: hidden;
      width:66px;
  }
  
  .us-lyric-line-content {
    flex-grow: 1;
    flex-basis: 0;
    word-wrap: break-word;
    flex-wrap: wrap;
  }

  .us-lyric-current {
    font-weight: bold;
    text-decoration: underline;
    text-underline-offset: 2px;
  }
  
  .us-toolbar {
      position: absolute;
      top: 0;
      right: 48px;
      height: 32px;
      line-height: 32px;
      white-space: nowrap;
  }
  
  .us-toolbar a {
      margin-right: 8px;
  }

  .us-setting {}
  .us-setting form { 
    display: table;
    border-spacing: 10px;
  }
  .us-setting p { display: table-row; }
  .us-setting label { display: table-cell; }
  .us-setting input { display: table-cell; }
  .us-input-text {
    padding: 0 5px;
    border-radius: 5px;
  }
  .us-input-submit {
    margin-left: 10px;
    min-width: 3rem;
  }
  .us-fab-div { 
    position: absolute;
    right: 48px;
    bottom: 32px;
    display: flex;
    flex-direction: column;
  }
  .us-fab-button {
    height: 32px;
    weight: 32px;
    background: transparent;
    color: #2C3E50;
  }
  .us-fab-button svg:hover {
    color: #17202A;
    background: lightgrey;
  }
  .us-fab-button svg {
    border-radius: 16px;
    background: #f6f6f6;
  }
`)

// modify origin UI to fit new button
GM_addStyle(`
  .video-toolbar-container .video-toolbar-left .toolbar-left-item-wrap {
      margin-right: 2px!important;
  }
  .video-toolbar-container .video-toolbar-left .toolbar-left-item-wrap .video-toolbar-left-item {
      width: 85px!important;
  }
`)

function fixNumber(n) {
return (n).toLocaleString("en-US", { minimumIntegerDigits: 2, useGrouping: false })
}

function parseTime(t) {
t = parseInt(t)
return `${fixNumber(parseInt(t / 60))}:${fixNumber(t % 60)}`
}

const SettingType = Object.freeze({
Text: Symbol("Text"),
Submit: Symbol("Submit")
})


class Dialog {
constructor() {
  this.dialogDiv = $(`
      <div id="ZulNs-dialog" class="ZulNs-dialog" style="min-width:400px; min-height:280px;">
        <div class="titlebar">Title</div>
        <button name="close">&#x2716</button>
        <div class="us-toolbar"></div>
        <div class="content">
          <div id="content"></div>
          <div class="us-setting">
            <form></form>
          </div>
        </div>
        <div class="us-fab-div"></div>
      </div>
    `)
  this._titleBar = this.dialogDiv.find(".titlebar")
  this.contentPane = this.dialogDiv.find("#content")
  this.contentScrollPane = this.dialogDiv.find(".content")
  this.settingPane = this.dialogDiv.find(".us-setting")
  this.settingForm = this.settingPane.find("form")
  this.settingPane.hide()
  this._toolbar = this.dialogDiv.find(".us-toolbar")
  this.fabDiv = this.dialogDiv.find(".us-fab-div") // floating action button(fab)
}
init() {
  this.dialogDiv.appendTo($("body"))
  this.dialogBox = new DialogBox("ZulNs-dialog", () => { })
}
show() {
  this.dialogBox.showDialog()
}
isShow() {
  return this.dialogDiv.css("display") != "none"
}
setTitle(text) {
  this._titleBar.html(text)
}
addToolbarEntry(text, cb) {
  let link = $(`<a>${text}</a>`)
  link.on("click", cb)
  this._toolbar.append(link)
  return link
}
addSettingEntry(name, type) {
  if (type === SettingType.Submit) {
    let submit = $(`<input type="submit" class="us-input-submit" value="${name}">`)
    this.settingForm.append(submit)
    return submit
  }

  let line = $(`<p><label for="${name}">${name}: </label></p>`)
  let input = null
  if (type === SettingType.Text) {
    input = $(`<input id="${name}" class="us-input-text" type="text">`)
  } else {
    console.error("Unknown SettingType: " + type)
    return
  }
  line.append(input)
  this.settingForm.append(line)
  return line
}
changeFontSize(font) {
  this.dialogDiv.css({ fontSize: `${font}px` })
}
}

(async function () {
"use strict"

async function request(url) {
  return await fetch(url, { credentials: "include" })
    .then(res => res.json())
    .then(data => {
      if (data.code != 0) {
        throw new Error(data.message)
      }
      return data
    })
}

async function get_bilibili_video_info(bvid) {
  return (await request(`https://api.bilibili.com/x/web-interface/view?bvid=${bvid}`)).data
}

async function get_bilibili_page_info(aid, cid) {
  return (await request(`https://api.bilibili.com/x/player/v2?aid=${aid}&cid=${cid}`)).data
}

let settings = null
try {
  settings = JSON.parse(await GM.getValue("us-bst-settings"))
} catch (err) {
  console.error(err)
  settings = {
    "font": 14,
    "music_filter_rate": 0.85
  }
}

let dialog = new Dialog()
dialog.changeFontSize(settings.font)
let video = null

dialog.contentPane.addClass("us-popup-reader")
let downloadGenerateLink = dialog.addToolbarEntry("下载字幕", () => { })
let downloadLink = dialog.addToolbarEntry("下载字幕文本", () => { })
let downloadJsonLink = dialog.addToolbarEntry("下载字幕Json", () => { })
downloadGenerateLink.hide()
downloadLink.hide()
downloadJsonLink.hide()

let fontSetting = dialog.addSettingEntry("字号", SettingType.Text)
let musicFilterRateSetting = dialog.addSettingEntry("歌词过滤阈值", SettingType.Text)
dialog.addSettingEntry("确定", SettingType.Submit)
let cancel = dialog.addSettingEntry("取消", SettingType.Submit)
dialog.addToolbarEntry("设置", () => {
  fontSetting.find("input").val(settings.font)
  musicFilterRateSetting.find("input").val(settings.music_filter_rate)
  dialog.settingPane.show()
  dialog.contentPane.hide()
})
cancel.on("click", () => {
  dialog.settingPane.hide()
  dialog.contentPane.show()
})
dialog.settingForm.on("submit", () => {
  settings.font = fontSetting.find("input").val()
  settings.music_filter_rate = musicFilterRateSetting.find("input").val()
  ; (async function () {
    await GM.setValue("us-bst-settings", JSON.stringify(settings))
  })()
  dialog.changeFontSize(settings.font)
  dialog.settingPane.hide()
  dialog.contentPane.show()
  return false
})


let urlParameters = window.location.pathname.split("/")
let bvid = urlParameters.pop()
if (bvid == "") bvid = urlParameters.pop()

// insert link
let subtitleBtn = $(`
    <div class="video-toolbar-right-item">
        <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="video-note-icon video-toolbar-item-icon" viewBox="0 0 16 16">
            <path d="M3.708 7.755c0-1.111.488-1.753 1.319-1.753.681 0 1.138.47 1.186 1.107H7.36V7c-.052-1.186-1.024-2-2.342-2C3.414 5 2.5 6.05 2.5 7.751v.747c0 1.7.905 2.73 2.518 2.73 1.314 0 2.285-.792 2.342-1.939v-.114H6.213c-.048.615-.496 1.05-1.186 1.05-.84 0-1.319-.62-1.319-1.727v-.743zm6.14 0c0-1.111.488-1.753 1.318-1.753.682 0 1.139.47 1.187 1.107H13.5V7c-.053-1.186-1.024-2-2.342-2C9.554 5 8.64 6.05 8.64 7.751v.747c0 1.7.905 2.73 2.518 2.73 1.314 0 2.285-.792 2.342-1.939v-.114h-1.147c-.048.615-.497 1.05-1.187 1.05-.839 0-1.318-.62-1.318-1.727v-.743z"/>
            <path d="M14 3a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h12zM2 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H2z"/>
        </svg>
        字幕
    </div>`)

setTimeout(() => {
  let handler = setInterval(() => {
    let toolbar = $("#arc_toolbar_report .video-toolbar-right .video-tool-more")
    if (toolbar.length != 0) {
      video = document.querySelector("video")
      $(video).on("timeupdate", () => {
        updateCurrentSubtitle()
      })
      $(video).on("seeking", () => {
        updateCurrentSubtitle()
      })
      dialog.init()
      toolbar.css("margin-right", "18px")
      subtitleBtn.insertBefore(toolbar)
      $(".arc_toolbar_report").css("justify-content", "initial")
      clearInterval(handler)
    }
  }, 500)
}, 3000)

let subtitleList = []
let prevTime = 0
let lastSubtitleIdx = 0
function updateCurrentSubtitle() {
  if (subtitleList.length == 0) {
    return
  }
  const time = video.currentTime
  let start = lastSubtitleIdx
  if (time < prevTime) {
    start = 0
  }
  for (; start < subtitleList.length - 1; start++) {
    if (subtitleList[start].time <= time && time < subtitleList[start + 1].time) {
      break
    }
  }
  if (lastSubtitleIdx != start) {
    subtitleList[lastSubtitleIdx].span.removeClass("us-lyric-current")
    subtitleList[start].span.addClass("us-lyric-current")
    lastSubtitleIdx = start
  }
  prevTime = time
}
const backButton = $(`
  <button class="us-fab-button">
    <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-arrow-left-circle" viewBox="0 0 16 16">
      <path fill-rule="evenodd" d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-4.5-.5a.5.5 0 0 1 0 1H5.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L5.707 7.5z"/>
    </svg>
  </button>
`)
dialog.fabDiv.append(backButton)
backButton.on("click", () => {
  if (subtitleList.length == 0) {
    return
  }
  const scrollPane = dialog.contentScrollPane
  const span = subtitleList[lastSubtitleIdx].span
  let top = span.offset().top - scrollPane.offset().top + scrollPane.scrollTop()
  top = Math.max(0, top - 4 * settings["font"] * 1.5)
  scrollPane.animate({
    scrollTop: top
  }, 1000)
})

async function useSubtitle(subtitle, filename_without_ext) {
  dialog.setTitle(subtitle.lan_doc)
  let data = await fetch(subtitle.subtitle_url.replace("http:", "https:")).then(res => res.json())

  dialog.contentPane.html("")
  subtitleList = []
  for (let line of data.body) {
    if (line.music && line.music > settings.music_filter_rate) {
      continue
    }
    let link = $(`<a class="us-lyric-line-time">${parseTime(line.from)}</a>`)
    link.on("click", () => {
      video.currentTime = line.from
    })
    let lineDiv = $("<div class=\"us-lyric-line\" />")
    let span = $(`<span class="us-lyric-line-content">${line.content}</span></div>`)
    lineDiv.append(link)
    lineDiv.append(span)
    dialog.contentPane.append(lineDiv)
    subtitleList.push({ time: line.from, span: span })
  }
  if (subtitleList.length != 0) {
    subtitleList[0].span.addClass("us-lyric-current")
  }
  updateCurrentSubtitle()

  downloadGenerateLink.on("click", () => {
    let subtitleText = ""
    for (let line of data.body) {
      subtitleText += line.content + "\n"
    }
    downloadLink.attr("href", `data:plain/text;charset=utf-8,${encodeURIComponent(subtitleText)}`)
    downloadJsonLink.attr("href", `data:application/json;charset=utf-8,${encodeURIComponent(JSON.stringify(data.body))}`)
    downloadLink.attr("download", `${filename_without_ext}.txt`)
    downloadJsonLink.attr("download", `${filename_without_ext}.json`)
    downloadLink.show()
    downloadJsonLink.show()
    downloadGenerateLink.hide()
  })
  downloadGenerateLink.show()
}

async function getSubtitleList() {
  let cur_page = (new URLSearchParams(window.location.search)).get("p") - 1
  if (!cur_page || cur_page == -1) {
    cur_page = 0
  }
  try {
    let video_info = await get_bilibili_video_info(bvid)
    let page_info = await get_bilibili_page_info(video_info.aid, video_info.pages[cur_page].cid)
    const video_name = video_info.title
    const page_name = video_info.pages[cur_page].part
    let subtitles = page_info.subtitle.subtitles
    if (subtitles.length == 0) {
      dialog.contentPane.html("无字幕")
    } else {
      dialog.contentPane.html("")
      for (let subtitle of subtitles) {
        let link = $("<a>" + subtitle.lan_doc + "</a>")
        link.on("click", () => {
          useSubtitle(subtitle, `${video_name}-p${cur_page}-${page_name}`)
        })
        dialog.contentPane.append(link)
      }
    }
  } catch (e) {
    dialog.contentPane.html(`获取字幕信息失败<br />${e}`)
    console.error(e)
  }
}

// show popup and fetch information
subtitleBtn.on("click", () => {
  // titlebar.html()
  dialog.setTitle("选择字幕")
  dialog.show()
  dialog.contentPane.html("正在加载字幕列表")
  getSubtitleList()
})
})()


// https://github.com/ZulNs/Draggable-Resizable-Dialog/
// On MIT License
GM_addStyle(`
  .ZulNs-dialog {
    // display: none; /* not visible by default */
    font-family: Verdana, sans-serif;
    font-size: 14px;
    font-weight: 400;
    color: #212F3D;
    background: #f6f6f6;
      box-shadow: 0 3px 25px 0 rgba(0,0,0,.3);
      border: 1px solid #e3e5e7; /* change allowed; Border to separate multipe dialog boxes */
    border-radius: 8px;
      margin: 0;
    position: fixed;
      z-index: 9999!important;
      height: 480px;
  }
  .ZulNs-dialog .titlebar {
    height: 32px; /* same as .ZulNs-dialog>button height */
    line-height: 32px; /* same as .ZulNs-dialog>button height */
    vertical-align: middle;
    padding: 0 8px 0 8px; /* change NOT allowed */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    cursor: move;
  }
  .ZulNs-dialog .content {
    position: absolute;
    top: 48px; /* change allowed */
    left: 16px; /* change NOT allowed */
    overflow: auto;
  }
  .ZulNs-dialog .buttonpane:before {
    width: 100%;
    height: 0;
    border-bottom: 1px solid; /* change allowed */
    content: '';
    position: absolute;
    top: -16px; /* change allowed */
  }
  .ZulNs-dialog .buttonpane {
    width: 100%;
    position: absolute;
    bottom: 16px; /* change allowed */
    right: 16px; /* change NOT allowed */
    white-space: nowrap; /* keep buttons on one line */
  }
  .ZulNs-dialog .buttonset {
    float: right;
  }
  .ZulNs-dialog button {
    -webkit-transition: 0.25s;
    transition: 0.25s;
      border: 0;
  }
  .ZulNs-dialog button::-moz-focus-inner {
    border: 0;
  }
  /* .ZulNs-dialog button.hover, */ /* Let's use standard hover */
  .ZulNs-dialog button:hover,
  .ZulNs-dialog button.active
  {
    cursor: pointer;
  }
  .ZulNs-dialog>button {
    width: 32px; /* change NOT allowed */
    height: 32px; /* same as .ZulNs-dialog .titlebar height */
    position: absolute;
    top: 0;
    right: 0;
    padding: 0;
    border: 0;
    font-size: 1.4em;
      background: #f6f6f6;
  }
  /* .ZulNs-dialog>button.hover, */
  .ZulNs-dialog>button:hover
  {
    background: lightgrey;
      border: 0;
  }
  .ZulNs-dialog>button.active {
    border: 0;
  }
  `)

/* eslint-disable */
function DialogBox(id, callback) {

var _minW = 100, // The exact value get's calculated
  _minH = 1, // The exact value get's calculated
  _resizePixel = 5,
  _hasEventListeners = !!window.addEventListener,
  _parent,
  _dialog,
  _dialogTitle,
  _dialogContent,
  _dialogButtonPane,
  _maxX, _maxY,
  _startX, _startY,
  _startW, _startH,
  _leftPos, _topPos,
  _isDrag = false,
  _isResize = false,
  _isButton = false,
  _isButtonHovered = false, // Let's use standard hover (see css)
  //_isClickEvent = true, // Showing several dialog boxes work better if I do not use this variable
  _resizeMode = "",
  _whichButton,
  _buttons,
  _tabBoundary,
  _callback, // Callback function which transfers the name of the selected button to the caller
  _zIndex, // Initial zIndex of this dialog box 
  _zIndexFlag = false, // Bring this dialog box to front 
  _setCursor, // Forward declaration to get access to this function in the closure
  _whichClick, // Forward declaration to get access to this function in the closure
  _setDialogContent, // Forward declaration to get access to this function in the closure

  _addEvent = function (elm, evt, callback) {
    if (elm == null || typeof (elm) == undefined)
      return
    if (_hasEventListeners)
      elm.addEventListener(evt, callback, false)
    else if (elm.attachEvent)
      elm.attachEvent("on" + evt, callback)
    else
      elm["on" + evt] = callback
  },

  _returnEvent = function (evt) {
    if (evt.stopPropagation)
      evt.stopPropagation()
    if (evt.preventDefault)
      evt.preventDefault()
    else {
      evt.returnValue = false
      return false
    }
  },

  // not used
  /*
  _returnTrueEvent = function(evt) {
      evt.returnValue = true;
      return true;
  },
  */

  // not used
  // Mybe we should be able to destroy a dialog box, too. 
  // In this case we should remove the event listeners from the dialog box but 
  // I do not know how to identfy which event listeners should be removed from the document.
  /*
  _removeEvent = function(elm, evt, callback) {
      if (elm == null || typeof(elm) == undefined)
          return;
      if (window.removeEventListener)
          elm.removeEventListener(evt, callback, false);
      else if (elm.detachEvent)
          elm.detachEvent('on' + evt, callback);
  },
  */

  _adjustFocus = function (evt) {
    evt = evt || window.event
    if (evt.target === _dialogTitle)
      _buttons[_buttons.length - 1].focus()
    else
      _buttons[0].focus()
    return _returnEvent(evt)
  },

  _onFocus = function (evt) {
    evt = evt || window.event
    evt.target.classList.add("focus")
    return _returnEvent(evt)
  },

  _onBlur = function (evt) {
    evt = evt || window.event
    evt.target.classList.remove("focus")
    return _returnEvent(evt)
  },

  _onClick = function (evt) {
    evt = evt || window.event
    //if (_isClickEvent)
    _whichClick(evt.target)
    //else
    //	_isClickEvent = true;
    return _returnEvent(evt)
  },

  _onMouseDown = function (evt) {
    evt = evt || window.event
    _zIndexFlag = true
    // mousedown might happen on any place of the dialog box, therefore 
    // we need to take care that this does not to mess up normal events 
    // on the content of the dialog box, i.e. to copy text
    if (!(evt.target === _dialog || evt.target === _dialogTitle || evt.target === _buttons[0]))
      return
    var rect = _getOffset(_dialog)
    _maxX = Math.max(
      document.documentElement["clientWidth"],
      document.body["scrollWidth"],
      document.documentElement["scrollWidth"],
      document.body["offsetWidth"],
      document.documentElement["offsetWidth"]
    )
    _maxY = Math.max(
      document.documentElement["clientHeight"],
      document.body["scrollHeight"],
      document.documentElement["scrollHeight"],
      document.body["offsetHeight"],
      document.documentElement["offsetHeight"]
    )
    if (rect.right > _maxX)
      _maxX = rect.right
    if (rect.bottom > _maxY)
      _maxY = rect.bottom
    _startX = evt.pageX
    _startY = evt.pageY
    _startW = _dialog.clientWidth
    _startH = _dialog.clientHeight
    _leftPos = rect.left
    _topPos = rect.top
    if (_isButtonHovered) {
      //_whichButton.classList.remove('hover');
      _whichButton.classList.remove("focus")
      _whichButton.classList.add("active")
      _isButtonHovered = false
      _isButton = true
    }
    else if (evt.target === _dialogTitle && _resizeMode == "") {
      _setCursor("move")
      _isDrag = true
    }
    else if (_resizeMode != "") {
      _isResize = true
    }
    var r = _dialog.getBoundingClientRect()
    return _returnEvent(evt)
  },

  _onMouseMove = function (evt) {
    evt = evt || window.event
    // mousemove might run out of the dialog box during drag or resize, therefore we need to 
    // attach the event to the whole document, but we need to take care that this  
    // does not to mess up normal events outside of the dialog box.
    if (!(evt.target === _dialog || evt.target === _dialogTitle || evt.target === _buttons[0]) && !_isDrag && _resizeMode == "")
      return
    if (_isDrag) {
      var dx = _startX - evt.pageX,
        dy = _startY - evt.pageY,
        left = _leftPos - dx,
        top = _topPos - dy,
        scrollL = Math.max(document.body.scrollLeft, document.documentElement.scrollLeft),
        scrollT = Math.max(document.body.scrollTop, document.documentElement.scrollTop)
      if (dx < 0) {
        if (left + _startW > _maxX)
          left = _maxX - _startW
      }
      if (dx > 0) {
        if (left < 0)
          left = 0
      }
      if (dy < 0) {
        if (top + _startH > _maxY)
          top = _maxY - _startH
      }
      if (dy > 0) {
        if (top < 0)
          top = 0
      }
      _dialog.style.left = left + "px"
      _dialog.style.top = top + "px"
      if (evt.clientY > window.innerHeight - 32)
        scrollT += 32
      else if (evt.clientY < 32)
        scrollT -= 32
      if (evt.clientX > window.innerWidth - 32)
        scrollL += 32
      else if (evt.clientX < 32)
        scrollL -= 32
      if (top + _startH == _maxY)
        scrollT = _maxY - window.innerHeight + 20
      else if (top == 0)
        scrollT = 0
      if (left + _startW == _maxX)
        scrollL = _maxX - window.innerWidth + 20
      else if (left == 0)
        scrollL = 0
      if (_startH > window.innerHeight) {
        if (evt.clientY < window.innerHeight / 2)
          scrollT = 0
        else
          scrollT = _maxY - window.innerHeight + 20
      }
      if (_startW > window.innerWidth) {
        if (evt.clientX < window.innerWidth / 2)
          scrollL = 0
        else
          scrollL = _maxX - window.innerWidth + 20
      }
      window.scrollTo(scrollL, scrollT)
    }
    else if (_isResize) {
      var dw, dh, w, h
      if (_resizeMode == "w") {
        dw = _startX - evt.pageX
        if (_leftPos - dw < 0)
          dw = _leftPos
        w = _startW + dw
        if (w < _minW) {
          w = _minW
          dw = w - _startW
        }
        _dialog.style.width = w + "px"
        _dialog.style.left = (_leftPos - dw) + "px"
      }
      else if (_resizeMode == "e") {
        dw = evt.pageX - _startX
        if (_leftPos + _startW + dw > _maxX)
          dw = _maxX - _leftPos - _startW
        w = _startW + dw
        if (w < _minW)
          w = _minW
        _dialog.style.width = w + "px"
      }
      else if (_resizeMode == "n") {
        dh = _startY - evt.pageY
        if (_topPos - dh < 0)
          dh = _topPos
        h = _startH + dh
        if (h < _minH) {
          h = _minH
          dh = h - _startH
        }
        _dialog.style.height = h + "px"
        _dialog.style.top = (_topPos - dh) + "px"
      }
      else if (_resizeMode == "s") {
        dh = evt.pageY - _startY
        if (_topPos + _startH + dh > _maxY)
          dh = _maxY - _topPos - _startH
        h = _startH + dh
        if (h < _minH)
          h = _minH
        _dialog.style.height = h + "px"
      }
      else if (_resizeMode == "nw") {
        dw = _startX - evt.pageX
        dh = _startY - evt.pageY
        if (_leftPos - dw < 0)
          dw = _leftPos
        if (_topPos - dh < 0)
          dh = _topPos
        w = _startW + dw
        h = _startH + dh
        if (w < _minW) {
          w = _minW
          dw = w - _startW
        }
        if (h < _minH) {
          h = _minH
          dh = h - _startH
        }
        _dialog.style.width = w + "px"
        _dialog.style.height = h + "px"
        _dialog.style.left = (_leftPos - dw) + "px"
        _dialog.style.top = (_topPos - dh) + "px"
      }
      else if (_resizeMode == "sw") {
        dw = _startX - evt.pageX
        dh = evt.pageY - _startY
        if (_leftPos - dw < 0)
          dw = _leftPos
        if (_topPos + _startH + dh > _maxY)
          dh = _maxY - _topPos - _startH
        w = _startW + dw
        h = _startH + dh
        if (w < _minW) {
          w = _minW
          dw = w - _startW
        }
        if (h < _minH)
          h = _minH
        _dialog.style.width = w + "px"
        _dialog.style.height = h + "px"
        _dialog.style.left = (_leftPos - dw) + "px"
      }
      else if (_resizeMode == "ne") {
        dw = evt.pageX - _startX
        dh = _startY - evt.pageY
        if (_leftPos + _startW + dw > _maxX)
          dw = _maxX - _leftPos - _startW
        if (_topPos - dh < 0)
          dh = _topPos
        w = _startW + dw
        h = _startH + dh
        if (w < _minW)
          w = _minW
        if (h < _minH) {
          h = _minH
          dh = h - _startH
        }
        _dialog.style.width = w + "px"
        _dialog.style.height = h + "px"
        _dialog.style.top = (_topPos - dh) + "px"
      }
      else if (_resizeMode == "se") {
        dw = evt.pageX - _startX
        dh = evt.pageY - _startY
        if (_leftPos + _startW + dw > _maxX)
          dw = _maxX - _leftPos - _startW
        if (_topPos + _startH + dh > _maxY)
          dh = _maxY - _topPos - _startH
        w = _startW + dw
        h = _startH + dh
        if (w < _minW)
          w = _minW
        if (h < _minH)
          h = _minH
        _dialog.style.width = w + "px"
        _dialog.style.height = h + "px"
      }
      _setDialogContent()
    }
    else if (!_isButton) {
      var cs, rm = ""
      if (evt.target === _dialog || evt.target === _dialogTitle || evt.target === _buttons[0]) {
        var rect = _getOffset(_dialog)
        if (evt.pageY < rect.top + _resizePixel)
          rm = "n"
        else if (evt.pageY > rect.bottom - _resizePixel)
          rm = "s"
        if (evt.pageX < rect.left + _resizePixel)
          rm += "w"
        else if (evt.pageX > rect.right - _resizePixel)
          rm += "e"
      }
      if (rm != "" && _resizeMode != rm) {
        if (rm == "n" || rm == "s")
          cs = "ns-resize"
        else if (rm == "e" || rm == "w")
          cs = "ew-resize"
        else if (rm == "ne" || rm == "sw")
          cs = "nesw-resize"
        else if (rm == "nw" || rm == "se")
          cs = "nwse-resize"
        _setCursor(cs)
        _resizeMode = rm
      }
      else if (rm == "" && _resizeMode != "") {
        _setCursor("")
        _resizeMode = ""
      }
      if (evt.target != _buttons[0] && evt.target.tagName.toLowerCase() == "button" || evt.target === _buttons[0] && rm == "") {
        if (!_isButtonHovered || _isButtonHovered && evt.target != _whichButton) {
          _whichButton = evt.target
          //_whichButton.classList.add('hover');
          _isButtonHovered = true
        }
      }
      else if (_isButtonHovered) {
        //_whichButton.classList.remove('hover');
        _isButtonHovered = false
      }
    }
    return _returnEvent(evt)
  }

_onMouseUp = function (evt) {
  evt = evt || window.event
  if (_zIndexFlag) {
    _dialog.style.zIndex = _zIndex + 1
    _zIndexFlag = false
  } else {
    _dialog.style.zIndex = _zIndex
  }
  // mousemove might run out of the dialog box during drag or resize, therefore we need to 
  // attach the event to the whole document, but we need to take care that this  
  // does not to mess up normal events outside of the dialog box.
  if (!(evt.target === _dialog || evt.target === _dialogTitle || evt.target === _buttons[0]) && !_isDrag && _resizeMode == "")
    return
  //_isClickEvent = false;
  if (_isDrag) {
    _setCursor("")
    _isDrag = false
  }
  else if (_isResize) {
    _setCursor("")
    _isResize = false
    _resizeMode = ""
  }
  else if (_isButton) {
    _whichButton.classList.remove("active")
    _isButton = false
    _whichClick(_whichButton)
  }
  //else
  //_isClickEvent = true;
  return _returnEvent(evt)
},

  _whichClick = function (btn) {
    _dialog.style.display = "none"
    if (_callback)
      _callback(btn.name)
  },

  _getOffset = function (elm) {
    var rect = elm.getBoundingClientRect(),
      offsetX = window.scrollX || document.documentElement.scrollLeft,
      offsetY = window.scrollY || document.documentElement.scrollTop
    return {
      left: rect.left + offsetX,
      top: rect.top + offsetY,
      right: rect.right + offsetX,
      bottom: rect.bottom + offsetY
    }
  },

  _setCursor = function (cur) {
    _dialog.style.cursor = cur
    _dialogTitle.style.cursor = cur
    _buttons[0].style.cursor = cur
  },

  _setDialogContent = function () {
    // Let's try to get rid of some of constants in javascript but use values from css
    var _dialogContentStyle = getComputedStyle(_dialogContent),
      _dialogButtonPaneStyle,
      _dialogButtonPaneStyleBefore
    // if (_buttons.length > 1) {
    //     _dialogButtonPaneStyle = getComputedStyle(_dialogButtonPane)
    //     _dialogButtonPaneStyleBefore = getComputedStyle(_dialogButtonPane, ":before")
    // }

    var w = _dialog.clientWidth
      - parseInt(_dialogContentStyle.left) // .ZulNs-dialog .content { left: 16px; }
      - 16 // right margin?
      ,
      h = _dialog.clientHeight - (
        parseInt(_dialogContentStyle.top) // .ZulNs-dialog .content { top: 48px } 
        + 16 // ?
        // + (_buttons.length > 1 ?
        //     + parseInt(_dialogButtonPaneStyleBefore.borderBottom) // .ZulNs-dialog .buttonpane:before { border-bottom: 1px; }
        //     - parseInt(_dialogButtonPaneStyleBefore.top) // .ZulNs-dialog .buttonpane:before { height: 0; top: -16px; }
        //     + parseInt(_dialogButtonPaneStyle.height) // .ZulNs-dialog .buttonset button { height: 32px; }
        //     + parseInt(_dialogButtonPaneStyle.bottom) // .ZulNs-dialog .buttonpane { bottom: 16px; }
        //     : 0)
      ) // Ensure to get minimal height
    _dialogContent.style.width = w + "px"
    _dialogContent.style.height = h + "px"

    if (_dialogButtonPane) // The buttonpane is optional
      _dialogButtonPane.style.width = w + "px"

    _dialogTitle.style.width = (w - 16) + "px"
  },

  _showDialog = function () {
    _dialog.style.display = "block"
    // if (_buttons[1]) // buttons are optional
    //     _buttons[1].focus();
    // else
    //     _buttons[0].focus();
  },

  _init = function (id, callback) {
    _dialog = document.getElementById(id)
    _callback = callback // Register callback function

    _dialog.style.visibility = "hidden" // We dont want to see anything..
    _dialog.style.display = "block" // but we need to render it to get the size of the dialog box

    _dialogTitle = _dialog.querySelector(".titlebar")
    _dialogContent = _dialog.querySelector(".content")
    _dialogButtonPane = _dialog.querySelector(".buttonpane")
    _buttons = _dialog.querySelectorAll("button")  // Ensure to get minimal width

    // Let's try to get rid of some of constants in javascript but use values from css
    var _dialogStyle = getComputedStyle(_dialog),
      _dialogTitleStyle = getComputedStyle(_dialogTitle),
      _dialogContentStyle = getComputedStyle(_dialogContent),
      _dialogButtonPaneStyle,
      _dialogButtonPaneStyleBefore,
      _dialogButtonStyle
    // if (_buttons.length > 1) {
    //     _dialogButtonPaneStyle = getComputedStyle(_dialogButtonPane)
    //     _dialogButtonPaneStyleBefore = getComputedStyle(_dialogButtonPane, ":before")
    //     _dialogButtonStyle = getComputedStyle(_buttons[1])
    // }

    // Calculate minimal width
    _minW = Math.max(_dialog.clientWidth, _minW,
      // + (_buttons.length > 1 ?
      //     + (_buttons.length - 1) * parseInt(_dialogButtonStyle.width) // .ZulNs-dialog .buttonset button { width: 64px; }
      //     + (_buttons.length - 1 - 1) * 16 // .ZulNs-dialog .buttonset button { margin-left: 16px; } // but not for first-child
      //     + (_buttons.length - 1 - 1) * 16 / 2 // The formula is not correct, however, with fixed value 16 for margin-left: 16px it works
      //     : 0)
      0
    )
    _dialog.style.width = _minW + "px"

    // Calculate minimal height
    _minH = Math.max(_dialog.clientHeight, _minH,
      + parseInt(_dialogContentStyle.top) // .ZulNs-dialog .content { top: 48px } 
      + (2 * parseInt(_dialogStyle.border)) // .ZulNs-dialog { border: 1px }
      + 16 // ?
      + 12 // .p { margin-block-start: 1em; } // default
      + 12 // .ZulNs-dialog { font-size: 12px; } // 1em = 12px
      + 12 // .p { margin-block-end: 1em; } // default
      // + (_buttons.length > 1 ?
      //     + parseInt(_dialogButtonPaneStyleBefore.borderBottom) // .ZulNs-dialog .buttonpane:before { border-bottom: 1px; }
      //     - parseInt(_dialogButtonPaneStyleBefore.top) // .ZulNs-dialog .buttonpane:before { height: 0; top: -16px; }
      //     + parseInt(_dialogButtonPaneStyle.height) // .ZulNs-dialog .buttonset button { height: 32px; }
      //     + parseInt(_dialogButtonPaneStyle.bottom) // .ZulNs-dialog .buttonpane { bottom: 16px; }
      //     : 0)
    )
    _dialog.style.height = _minH + "px"

    _setDialogContent()

    // center the dialog box
    _dialog.style.left = ((window.innerWidth - _dialog.clientWidth) / 2) + "px"
    _dialog.style.top = ((window.innerHeight - _dialog.clientHeight) / 2) + "px"

    _dialog.style.display = "none" // Let's hide it again..
    _dialog.style.visibility = "visible" // and undo visibility = 'hidden'

    _dialogTitle.tabIndex = "0"

    _tabBoundary = document.createElement("div")
    _tabBoundary.tabIndex = "0"
    _dialog.appendChild(_tabBoundary)

    _addEvent(_dialog, "mousedown", _onMouseDown)
    // mousemove might run out of the dialog during resize, therefore we need to 
    // attach the event to the whole document, but we need to take care not to mess 
    // up normal events outside of the dialog.
    _addEvent(document, "mousemove", _onMouseMove)
    // mouseup might happen out of the dialog during resize, therefore we need to 
    // attach the event to the whole document, but we need to take care not to mess 
    // up normal events outside of the dialog.
    _addEvent(document, "mouseup", _onMouseUp)
    for (var i = 0; i < _buttons.length; i++) {
      if (_buttons[i].name == "close") {
        _addEvent(_buttons[i], "click", _onClick)
      }
      _addEvent(_buttons[i], "focus", _onFocus)
      _addEvent(_buttons[i], "blur", _onBlur)
    }
    _addEvent(_dialogTitle, "focus", _adjustFocus)
    _addEvent(_tabBoundary, "focus", _adjustFocus)

    _zIndex = _dialog.style.zIndex
  }

// Execute constructor
_init(id, callback)

// Public interface 
this.showDialog = _showDialog
return this
}
/* eslint-enable */