一次性展示bilibili的cc字幕。适合需要快速阅读字幕的场景。
// ==UserScript== // @name bilibili-subtitle-to-text // @namespace http://tampermonkey.net/ // @version 0.33 // @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">✖</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", () => { }) let downloadSrtLink = dialog.addToolbarEntry("下载字幕Srt", () => { }) downloadGenerateLink.hide() downloadLink.hide() downloadJsonLink.hide() downloadSrtLink.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 us###btitle(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))}`) downloadSrtLink.attr("href", getSrtLink(data.body)) downloadLink.attr("download", `${filename_without_ext}.txt`) downloadJsonLink.attr("download", `${filename_without_ext}.json`) downloadSrtLink.attr("download", `${filename_without_ext}.srt`) downloadLink.show() downloadJsonLink.show() downloadSrtLink.show() downloadGenerateLink.hide() }) downloadGenerateLink.show() } function getSrtLink(data) { function getSrt(data) { const _ts = this; let r###lt = ``; data.forEach((item, index) => { r###lt += echoSrt(item, index); }); return r###lt; } function echoSrt(data, index) { let str = ``; str += index + 1; str += `\r\n`; str += `${formatTime(data.from)} --> ${formatTime(data.to)}\r\n`; str += `${data.content}\r\n`; str += `\r\n`; return str; } function formatTime(num) { num = num * 1000; let h = (() => { let t = num / 60 / 60 / 1000; return t >= 1 ? ~~t : 0; })(), m = (() => { let t = [num - (h * 60 * 60 * 1000)] / 60 / 1000; return t >= 1 ? ~~t : 0; })(), s = (() => { let t = [num - (h * 60 * 60 * 1000) - (m * 60 * 1000)] / 1000; return t >= 1 ? ~~t : 0; })(), ms = (() => { let t = [num - (h * 60 * 60 * 1000) - (m * 60 * 1000) - (s * 1000)]; return t >= 1 ? ~~t : 0; })(); return `${fillIn(h, 2)}:${fillIn(m, 2)}:${fillIn(s, 2)},${fillIn(ms, 3)}`; } function fillIn(num, len) { num = `` + num; len = len - num.length; if (len) { for (let i = 0; i < len; i++) { num = `0${num}`; }; }; return num; } const srtContent = getSrt(data) // 创建一个Blob对象 var blob = new Blob([srtContent], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); return url } 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", () => { us###btitle(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 */