完成页面列表预览组件
parent
fb8e78e630
commit
6e162edd86
@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
namespace Isekai\Widgets;
|
||||||
|
|
||||||
|
use Html;
|
||||||
|
use Title;
|
||||||
|
|
||||||
|
class PreviewPageListWidget {
|
||||||
|
public const CONTAINER_CLASS_NAME = 'isekai-card isekai-preview-page-list-card';
|
||||||
|
|
||||||
|
public static function create($text, $params, \Parser $parser, \PPFrame $frame) {
|
||||||
|
$parser->getOutput()->addModules(['ext.isekai.previewPageList']);
|
||||||
|
|
||||||
|
$loader = $params['loader'] ?? 'unknown';
|
||||||
|
if ($loader === 'unknown' && !empty(trim($text))) {
|
||||||
|
$loader = 'pages';
|
||||||
|
}
|
||||||
|
|
||||||
|
$autoFocus = $params['autofocus'] ?? false;
|
||||||
|
if ($autoFocus === 'false' || $autoFocus === '0') {
|
||||||
|
$autoFocus = false;
|
||||||
|
} else {
|
||||||
|
$autoFocus = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lazyLoad = $params['lazyload'] ?? false;
|
||||||
|
if ($lazyLoad === 'false' || $lazyLoad === '0') {
|
||||||
|
$lazyLoad = false;
|
||||||
|
} else {
|
||||||
|
$lazyLoad = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($loader) {
|
||||||
|
case 'pages':
|
||||||
|
$pageList = explode("\n", trim($text));
|
||||||
|
$pageList = array_map(function ($page) {
|
||||||
|
return trim($page);
|
||||||
|
}, $pageList);
|
||||||
|
|
||||||
|
return Html::openElement('div', [
|
||||||
|
'class' => self::CONTAINER_CLASS_NAME,
|
||||||
|
'data-loader' => $loader,
|
||||||
|
'data-autofocus' => $autoFocus,
|
||||||
|
'data-lazyload' => $lazyLoad,
|
||||||
|
]) . Html::element('script', [
|
||||||
|
'type' => 'application/json',
|
||||||
|
'data-pages' => true,
|
||||||
|
], json_encode($pageList)) . Html::closeElement('div');
|
||||||
|
case 'category':
|
||||||
|
$category = $params['category'] ?? null;
|
||||||
|
if (!$category) {
|
||||||
|
return Html::element('span', [
|
||||||
|
'class' => 'error'
|
||||||
|
], wfMessage('isekai-preview-page-list-error-invalid-category')->parse());
|
||||||
|
}
|
||||||
|
|
||||||
|
$categoryTitle = new Title($category, NS_CATEGORY);
|
||||||
|
$categoryTitleText = $categoryTitle->getPrefixedText();
|
||||||
|
|
||||||
|
return Html::element('div', [
|
||||||
|
'class' => self::CONTAINER_CLASS_NAME,
|
||||||
|
'data-category' => $categoryTitleText,
|
||||||
|
'data-autofocus' => $autoFocus,
|
||||||
|
'data-lazyload' => $lazyLoad,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Html::element('span', [
|
||||||
|
'class' => 'error'
|
||||||
|
], wfMessage('isekai-preview-page-list-error-invalid-loader', $loader)->parse());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,174 @@
|
|||||||
|
const Vue = require("vue");
|
||||||
|
|
||||||
|
class BasePageListLoader {
|
||||||
|
loadPageList() {
|
||||||
|
return Promise.reject(new Error('Not implemented'));
|
||||||
|
}
|
||||||
|
|
||||||
|
parseHTMLString(txt) {
|
||||||
|
try {
|
||||||
|
let parser = new DOMParser();
|
||||||
|
let xmlDoc = parser.parseFromString(txt, "text/html");
|
||||||
|
return xmlDoc;
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e.message);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPageContent(pageInfo) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!pageInfo || !pageInfo.fullTitle) {
|
||||||
|
return reject(new Error('pageInfo.fullTitle is null'));
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = mw.util.getUrl(pageInfo.fullTitle);
|
||||||
|
if (url.indexOf('?') >= 0) {
|
||||||
|
url += '&';
|
||||||
|
} else {
|
||||||
|
url += '?'
|
||||||
|
}
|
||||||
|
url += 'action=render';
|
||||||
|
$.get(url, (str) => {
|
||||||
|
let $dom = $(this.parseHTMLString(str));
|
||||||
|
let $content = $dom.find('.mw-parser-output');
|
||||||
|
if ($content.length > 0) {
|
||||||
|
//删除目录
|
||||||
|
$content.find('.toc').remove();
|
||||||
|
resolve($content.html());
|
||||||
|
}
|
||||||
|
}, 'html');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getPageUrl(pageInfo) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!pageInfo || !pageInfo.fullTitle) {
|
||||||
|
return reject(new Error('pageInfo.fullTitle is null'));
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = mw.util.getUrl(pageInfo.fullTitle);
|
||||||
|
resolve(url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据标题生成页面信息
|
||||||
|
* @param {string} title
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
makePageInfoFromTitle(title) {
|
||||||
|
let pageInfo = {
|
||||||
|
fullTitle: title,
|
||||||
|
title: title,
|
||||||
|
subTitle: null
|
||||||
|
};
|
||||||
|
if (title.indexOf('/') !== 0) {
|
||||||
|
let splitPos = title.lastIndexOf('/');
|
||||||
|
pageInfo.title = title.substring(splitPos + 1);
|
||||||
|
pageInfo.subTitle = title.substring(0, splitPos);
|
||||||
|
}
|
||||||
|
return pageInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PresetPageListLoader extends BasePageListLoader {
|
||||||
|
/**
|
||||||
|
* 从页面列表加载页面
|
||||||
|
* @param {string[]} pageList
|
||||||
|
*/
|
||||||
|
constructor(pageList) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.pageList = pageList.map((pageTitle) => {
|
||||||
|
return this.makePageInfoFromTitle(pageTitle);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载页面列表
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
loadPageList() {
|
||||||
|
return Promise.resolve(this.pageList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CategoryPageListLoader extends BasePageListLoader {
|
||||||
|
/**
|
||||||
|
* 从分类加载页面
|
||||||
|
* @param {string} category
|
||||||
|
*/
|
||||||
|
constructor(category) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.category = category;
|
||||||
|
this.api = new mw.Api();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载页面列表
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
loadPageList() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.api.get({
|
||||||
|
action: 'query',
|
||||||
|
list: 'categorymembers',
|
||||||
|
cmtitle: this.category,
|
||||||
|
cmtype: page,
|
||||||
|
cmnamespace: 0,
|
||||||
|
cmlimit: 200,
|
||||||
|
cmsort: 'sortkey',
|
||||||
|
}).done((data) => {
|
||||||
|
if(data.query && data.query.categorymembers){
|
||||||
|
let pageList = data.query.categorymembers.map((pageInfo) => {
|
||||||
|
return this.makePageInfoFromTitle(pageInfo.title);
|
||||||
|
});
|
||||||
|
resolve(pageList);
|
||||||
|
} else if(data.error){
|
||||||
|
reject(new Error(data.error.info));
|
||||||
|
} else {
|
||||||
|
reject(new Error(mw.message('isekai-discover-error-cannotload').parse()));
|
||||||
|
}
|
||||||
|
}).fail((data) => {
|
||||||
|
reject(new Error(data.error.info));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let previewPageList = document.querySelectorAll('.isekai-preview-page-list-card');
|
||||||
|
if (previewPageList.length > 0) {
|
||||||
|
const App = require("./PreviewPageList.vue");
|
||||||
|
previewPageList.forEach((previewPageListDom) => {
|
||||||
|
try {
|
||||||
|
let props = {};
|
||||||
|
let dataset = previewPageListDom.dataset;
|
||||||
|
|
||||||
|
props.autoFocus = (dataset.autofocus == '1');
|
||||||
|
props.lazyLoad = (dataset.lazyload == '1');
|
||||||
|
|
||||||
|
switch (dataset.loader) {
|
||||||
|
case 'pages':
|
||||||
|
let pagesJsonEl = document.querySelector('script[type="application/json"][data-pages]');
|
||||||
|
let pageList = [];
|
||||||
|
if (pagesJsonEl) {
|
||||||
|
pageList = JSON.parse(pagesJsonEl.innerHTML);
|
||||||
|
}
|
||||||
|
props.pageListLoader = new PresetPageListLoader(pageList);
|
||||||
|
break;
|
||||||
|
case 'category':
|
||||||
|
let category = dataset.category;
|
||||||
|
props.pageListLoader = new CategoryPageListLoader(category);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error('Unknown loader: ' + dataset.loader);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.createMwApp(App, props).mount(previewPageListDom);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,173 @@
|
|||||||
|
@preview-page-list-height: 70vh;
|
||||||
|
|
||||||
|
.isekai-preview-page-list-card {
|
||||||
|
height: @preview-page-list-height;
|
||||||
|
}
|
||||||
|
|
||||||
|
.isekai-preview-page-list-card > .card-header {
|
||||||
|
height: 2.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.isekai-preview-page-list-container {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
width: 100%;
|
||||||
|
height: 99.5%;
|
||||||
|
height: calc(100% - 2px); // fix: overflow because of border
|
||||||
|
margin-top: 1px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
margin: auto;
|
||||||
|
padding: 2rem;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.oo-ui-progressBarWidget {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.isekai-booklet-layout {
|
||||||
|
display: flex;
|
||||||
|
height: @preview-page-list-height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面列表
|
||||||
|
.isekai-booklet-menu {
|
||||||
|
flex: 0 0;
|
||||||
|
width: 280px;
|
||||||
|
flex-basis: 280px;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
height: @preview-page-list-height;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-right: 1px solid rgba(0,0,0,.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.isekai-preview-page-list {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.isekai-list-item {
|
||||||
|
&.active {
|
||||||
|
box-shadow: none;
|
||||||
|
background-color: #36c;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面预览
|
||||||
|
.isekai-booklet-content {
|
||||||
|
flex: 1 1;
|
||||||
|
width: 0;
|
||||||
|
height: @preview-page-list-height;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
flex: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.isekai-preview-container {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
flex: 1 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 12px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提示选择页面
|
||||||
|
.isekai-preview-page-list-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: @preview-page-list-height;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.isekai-booklet-layout {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.animating {
|
||||||
|
.isekai-booklet-menu,
|
||||||
|
.isekai-booklet-content {
|
||||||
|
transition: transform 150ms ease-in-out, opacity 150ms ease-in-out;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.isekai-booklet-menu {
|
||||||
|
display: block;
|
||||||
|
z-index: 3;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.isekai-booklet-content {
|
||||||
|
display: flex;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.focus-menu-transition {
|
||||||
|
.isekai-booklet-menu {
|
||||||
|
transform: scale(100%);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.isekai-booklet-content {
|
||||||
|
transform: scale(80%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.focus-content-transition {
|
||||||
|
.isekai-booklet-menu {
|
||||||
|
transform: scale(120%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.isekai-booklet-content {
|
||||||
|
transform: scale(100%);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.isekai-booklet-menu {
|
||||||
|
width: 100%;
|
||||||
|
flex-basis: 100%;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.isekai-booklet-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.focus-content {
|
||||||
|
.isekai-booklet-menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.isekai-booklet-content {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button-container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue