From 45b571cf2908e290f167fcc6b8b54bc7a0d249f7 Mon Sep 17 00:00:00 2001 From: Lex Lim Date: Sun, 10 Jul 2022 22:50:11 +0800 Subject: [PATCH] Init project --- LICENSE | 21 +++ extension.json | 48 +++++++ i18n/en.json | 4 + i18n/zh-hans.json | 4 + includes/Hooks.php | 26 ++++ modules/ext.isekai.offcanvas-toc.js | 198 ++++++++++++++++++++++++++ modules/ext.isekai.offcanvas-toc.less | 141 ++++++++++++++++++ 7 files changed, 442 insertions(+) create mode 100644 LICENSE create mode 100644 extension.json create mode 100644 i18n/en.json create mode 100644 i18n/zh-hans.json create mode 100644 includes/Hooks.php create mode 100644 modules/ext.isekai.offcanvas-toc.js create mode 100644 modules/ext.isekai.offcanvas-toc.less diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f69dd2b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Hyperzlib + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extension.json b/extension.json new file mode 100644 index 0000000..db6ba84 --- /dev/null +++ b/extension.json @@ -0,0 +1,48 @@ +{ + "name": "Isekai Offcanvas Toc", + "namemsg": "isekai-offcanvastoc-name", + "author": "Hyperzlib", + "version": "1.0.0", + "url": "https://www.isekai.cn", + "descriptionmsg": "isekai-offcanvastoc-desc", + "license-name": "MIT", + "type": "other", + "requires": { + + }, + "MessagesDirs": { + "IsekaiOffcanvasToc": [ + "i18n" + ] + }, + "AutoloadNamespaces": { + "Isekai\\OffcanvasToc\\": "includes" + }, + "Hooks": { + "BeforePageDisplay": [ + "Isekai\\OffcanvasToc\\Hooks::onLoad" + ] + }, + "ResourceModules": { + "ext.isekai.offcanvas-toc": { + "scripts": ["ext.isekai.offcanvas-toc.js"], + "styles": ["ext.isekai.offcanvas-toc.less"], + "dependencies": [ + "oojs-ui-core", + "oojs-ui-windows" + ], + "targets": [ + "desktop", + "mobile" + ], + "messages": [ + + ] + } + }, + "ResourceFileModulePaths": { + "localBasePath": "modules", + "remoteExtPath": "IsekaiOffcanvasToc/modules" + }, + "manifest_version": 1 +} \ No newline at end of file diff --git a/i18n/en.json b/i18n/en.json new file mode 100644 index 0000000..1ed6f2c --- /dev/null +++ b/i18n/en.json @@ -0,0 +1,4 @@ +{ + "isekai-offcanvastoc-name": "Isekai Offcanvas TOC", + "isekai-offcanvastoc-desc": "Show Offcanvas TOC on wiki" +} \ No newline at end of file diff --git a/i18n/zh-hans.json b/i18n/zh-hans.json new file mode 100644 index 0000000..fa38785 --- /dev/null +++ b/i18n/zh-hans.json @@ -0,0 +1,4 @@ +{ + "isekai-offcanvastoc-name": "异世界百科 悬浮目录", + "isekai-offcanvastoc-desc": "在页面上显示悬浮目录" +} \ No newline at end of file diff --git a/includes/Hooks.php b/includes/Hooks.php new file mode 100644 index 0000000..e2b29b4 --- /dev/null +++ b/includes/Hooks.php @@ -0,0 +1,26 @@ +enableOOUI(); + $outputPage->addModules('ext.isekai.offcanvas-toc'); + $outputPage->addModules('oojs-ui.styles.icons-layout'); + + $outputPage->addHTML(Html::openElement('div', [ + 'id' => 'isekai-offcanvas-toc', + 'class' => 'toc-offcanvas' + ]) . Html::element('ul') . Html::closeElement('div')); + + $outputPage->addHTML(Html::openElement('button', [ + 'id' => 'iseai-offcanvas-btn', + 'class' => 'toc-offcanvas-btn' + ]) . new \OOUI\IconWidget([ + 'icon' => 'menu' + ]) . Html::closeElement('button')); + + $outputPage->addElement('div', ['id' => 'isekai-offcanvas-cover', 'class' => 'toc-offcanvas-cover']); + } +} \ No newline at end of file diff --git a/modules/ext.isekai.offcanvas-toc.js b/modules/ext.isekai.offcanvas-toc.js new file mode 100644 index 0000000..5745018 --- /dev/null +++ b/modules/ext.isekai.offcanvas-toc.js @@ -0,0 +1,198 @@ +var headingList = []; + +function getScrollOffset() { + if (mw.config.get('skin') === 'timeless' && $(window).width() > 850) { + return 60; + } else { + return 0; + } +} + +function getAnchorOffset() { + if (mw.config.get('skin') === 'timeless' && $(window).width() > 850) { + return 70; + } else { + return 0; + } +} + +function scrollToAnchor(link) { + var target = $(link.replace(/\./g, '\\.')); + if (target.length > 0) { + function doScroll() { + var position = target.offset().top - getScrollOffset(); + + $('html, body').animate({ + scrollTop: position, + }, 500); + } + + if (mw.config.get('skin') === 'minerva') { // 手机端主体,需要检测折叠状态 + var collapseBlock = target.parents('.collapsible-block'); + if (collapseBlock.length > 0 && !collapseBlock.hasClass('open-block')) { + var h1Elem = collapseBlock.prev('.collapsible-heading'); + if (h1Elem.length > 0) { + // 展开目录 + h1Elem.click(); + var tid = setInterval(function() { + // 检测是否已经展开 + if (collapseBlock.hasClass('open-block')) { + doScroll(); + clearInterval(tid); + } + }, 100); + return false; + } + } + doScroll(); + } else { + doScroll(); + } + return false; + } else { + return true; + } +} + +function getScrollbarWidth() { + if (window.innerWidth && document.body.clientWidth) { + return window.innerWidth - document.body.clientWidth; + } else { + return 0; + } +} + +function updateActive() { + var scrollTop = $(window).scrollTop() + getAnchorOffset(); + console.log('scroll top', scrollTop); + $('#isekai-offcanvas-toc ul .list-item').removeClass('active'); + + if (headingList.length > 0) { + var activedId; + for (var i = 0; i < headingList.length; i ++) { + var headItem = headingList[i]; + var headPos = headItem.offset().top; + if (i === 0 && scrollTop < headPos) { // 比第一个head位置靠上,则是简介 + activedId = 'bodyContent'; + break; + } else if (scrollTop < headPos) { // 如果当前滚动条高度低于目前head,则是上一个 + activedId = headingList[i - 1].attr('id'); + break; + } + } + if (!activedId) { + activedId = [headingList.length - 1].attr('id'); + } + $('#isekai-offcanvas-toc ul .list-item[data-id="' + activedId + '"]').addClass('active'); + } +} + +function openOffcanvas() { + let scrollbarWidth = getScrollbarWidth(); + $('body').addClass(['toc-offcanvas-show', 'toc-offcanvas-open']) + .css('margin-right', scrollbarWidth); + $('#iseai-offcanvas-btn').css('margin-right', scrollbarWidth); + + // 滚动到当前项目 + let activedItem = $('#isekai-offcanvas-toc ul .list-item.active'); + if (activedItem.length > 0) { + let targetY = Math.max(activedItem.eq(0).prop('offsetTop') - 50, 0); + $('#isekai-offcanvas-toc').scrollTop(targetY); + } +} + +function closeOffcanvas() { + $('body').removeClass('toc-offcanvas-open'); + setTimeout(function() { + $('body').removeClass('toc-offcanvas-show').css('margin-right', 0); + $('#iseai-offcanvas-btn').css('margin-right', 0); + }, 260); +} + +// 生成目录 +$(function() { + var parserOutput = $('.mw-parser-output'); + var headings = parserOutput.find('h1,h2,h3,h4,h5,h6'); + + var headNum = new Array(6).fill(0); + var menuList = [{ + number: false, + text: '简介', + id: 'bodyContent' + }]; + + headings.each(function() { + var headline = $(this).find('.mw-headline'); + if (headline.length > 0) { + headingList.push(headline); + var text = headline.text(); + var headId = headline.prop('id'); + var indentNum = parseInt(this.tagName.replace(/^H/, '')); + // 计算折叠 + var menuNumStringBuilder = []; + headNum[indentNum - 1] ++; + for (var i = 0; i < indentNum; i ++) { + menuNumStringBuilder.push(headNum[i]); + } + for (var i = indentNum; i < headNum.length; i ++) { + headNum[i] = 0; + } + var menuNum = menuNumStringBuilder.join('.'); + menuList.push({ + number: menuNum, + text: text, + id: headId + }); + } + }); + + // 生成dom + var tocContainer = $('#isekai-offcanvas-toc ul'); + menuList.forEach(function(menuInfo) { + var listItem = document.createElement('a'); + listItem.href = '#' + menuInfo.id; + listItem.dataset.id = menuInfo.id; + listItem.classList.add('list-item'); + + var titleItem = document.createElement('span'); + titleItem.classList.add('title'); + titleItem.innerText = menuInfo.text; + + if (menuInfo.number) { + var numberItem = document.createElement('span'); + numberItem.classList.add('number'); + numberItem.innerText = menuInfo.number; + listItem.appendChild(numberItem); + } + + listItem.appendChild(titleItem); + + tocContainer[0].appendChild(listItem); + }); + + // 事件 + $('#isekai-offcanvas-cover').on('click', function() { + closeOffcanvas(); + }); + + $('#iseai-offcanvas-btn').on('click', function() { + updateActive(); + openOffcanvas(); + }); + + $('#isekai-offcanvas-toc ul .list-item').on('click', function(e) { + // 点击链接 + e.preventDefault(); + var target = $(this).data('id'); + if (target && target != '') { + target = '#' + target; + $('#isekai-offcanvas-toc ul .list-item').removeClass('active'); + $(this).addClass('active'); + scrollToAnchor(target); + if ($(window).width() < 550) { // 手机端,关闭抽屉 + closeOffcanvas(); + } + } + return false; + }); +}); \ No newline at end of file diff --git a/modules/ext.isekai.offcanvas-toc.less b/modules/ext.isekai.offcanvas-toc.less new file mode 100644 index 0000000..9f12cdd --- /dev/null +++ b/modules/ext.isekai.offcanvas-toc.less @@ -0,0 +1,141 @@ +.toc-offcanvas { + position: fixed; + visibility: hidden; + top: 0; + right: 0; + z-index: 102; + margin: 0; + height: 100vh; + min-width: 275px; + max-width: 80%; + box-shadow: 1px 0 8px 0 rgba(0, 0, 0, 0.35); + transform: translate3d(100%, 0, 0); + transition: transform 250ms linear; + will-change: transform; + overflow-y: auto; + background-color: #eaecf0; + scrollbar-width: thin; + + > ul { + list-style: none; + margin: 0; + padding: 0; + + a.list-item { + text-decoration: none; + + &:hover, + &:active, + &:focus, + &:visited { + color: #54595d; + } + } + + .list-item { + display: block; + color: #54595d; + background-color: #fff; + border-top: 1px solid #eaecf0; + max-width: 100%; + padding: 12px 10px 12px 15px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + .title { + margin-inline-start: 10px; + } + + &:active { + background-color: #ccc; + } + + &:first-of-type { + border-top: none; + margin-top: 8px; + } + + &:last-of-type { + margin-bottom: 8px; + } + + &.active { + box-shadow: inset 4px 0 0 0 #3366cc; + } + } + } +} + +.toc-offcanvas-cover { + position: fixed; + visibility: hidden; + pointer-events: none; + top: 0; + left: 0; + right: 0; + opacity: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + z-index: 101; + transition: opacity 250ms linear; + will-change: opacity; +} + +.toc-offcanvas-btn { + position: fixed; + z-index: 50; + right: 18px; + bottom: 6em; + bottom: 20vh; + display: flex; + width: 50px; + height: 50px; + padding: 0; + border: 1px #eee solid; + outline: none; + align-items: center; + justify-content: center; + text-align: center; + border-radius: 50%; + text-shadow: none; + background-color: rgba(255, 255, 255, 0.95); + color: #333; + box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.12); + + &:hover { + background-color: rgba(255, 255, 255, 0.9); + color: #000; + } + + @media screen and (min-width: 768px) { + right: 24px; + } + + @media screen and (min-width: 1340px) { + right: 44px; + } +} + +body.toc-offcanvas-show { + overflow: hidden; + + .toc-offcanvas-cover { + visibility: visible; + pointer-events: auto; + } + + .toc-offcanvas { + visibility: visible; + } +} + +body.toc-offcanvas-open { + .toc-offcanvas-cover { + opacity: 0.5; + } + + .toc-offcanvas { + transform: translate3d(0, 0, 0); + } +}