diff --git a/IsekaiWidgets.i18n.php b/IsekaiWidgets.i18n.php new file mode 100644 index 0000000..d803955 --- /dev/null +++ b/IsekaiWidgets.i18n.php @@ -0,0 +1,7 @@ + [ + 'htmldetails' => [0, 'htmldetails'], + 'htmlsummary' => [0, 'htmlsummary'], + ], +]; \ No newline at end of file diff --git a/IsekaiWidgets.zip b/IsekaiWidgets.zip deleted file mode 100644 index 25feedd..0000000 Binary files a/IsekaiWidgets.zip and /dev/null differ diff --git a/extension.json b/extension.json index 6a141a3..a188d6e 100644 --- a/extension.json +++ b/extension.json @@ -7,16 +7,23 @@ "descriptionmsg": "isekai-widgets-desc", "license-name": "GPL-2.0-or-later", "type": "parserhook", + "requires": { + "MediaWiki": ">= 1.34.0" + }, "MessagesDirs": { "IsekaiWidgets": [ "i18n" ] }, + "ExtensionMessagesFiles": { + "IsekaiWidgetsMagic": "IsekaiWidgets.i18n.php" + }, "AutoloadNamespaces": { "Isekai\\Widgets\\": "includes" }, "Hooks": { - "ParserFirstCallInit": "Isekai\\Widgets\\Widgets::onParserSetup" + "ParserFirstCallInit": "Isekai\\Widgets\\Widgets::onParserSetup", + "BeforePageDisplay": "Isekai\\Widgets\\Widgets::onLoad" }, "ResourceModules": { "ext.isekai.widgets.global": { @@ -97,11 +104,33 @@ "desktop", "mobile" ] + }, + "ext.isekai.collapse": { + "scripts": [ + "ext.isekai.collapse.js" + ], + "styles": [ + "ext.isekai.collapse.less" + ], + "targets": [ + "desktop", + "mobile" + ] } }, "ResourceFileModulePaths": { "localBasePath": "modules", "remoteExtPath": "IsekaiWidgets/modules" }, - "manifest_version": 1 + "attributes": { + "CodeMirror": { + "TagModes": { + "tilegroup": "text/mediawiki", + "exfont": "text/mediawiki", + "details": "text/mediawiki", + "summary": "text/mediawiki" + } + } + }, + "manifest_version": 2 } \ No newline at end of file diff --git a/i18n/ja.json b/i18n/ja.json index 1a537da..a2b8e81 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -13,5 +13,5 @@ "isekai-discover-loading": "読み込み中...", "isekai-discover-change-btn": "変える", "isekai-discover-readmore-btn": "開く", - "isekai-discover-error-cannotload": "サーバからのページを読み取りに失敗しま。" + "isekai-discover-error-cannotload": "サーバからのページを読み取りに失敗しました。" } \ No newline at end of file diff --git a/includes/ExtraFontWidget.php b/includes/ExtraFontWidget.php index 3116ef0..f6ee07f 100644 --- a/includes/ExtraFontWidget.php +++ b/includes/ExtraFontWidget.php @@ -7,7 +7,7 @@ class ExtraFontWidget { public static function create($text, $params, $parser, $frame){ $existsFonts = $parser->extIsekaiWidgetsCache->get('extraFonts', INF, []); - $content = $text = $frame->expand($text); + $content = $text = $parser->recursiveTagParse($text, $frame); if (!isset($params['name']) || empty($params['name'])) { return '' . wfMessage('isekai-font-error-invalid-params')->parse() . '' . $content; } diff --git a/includes/FontFaceWidget.php b/includes/FontFaceWidget.php index 385c221..4af1334 100644 --- a/includes/FontFaceWidget.php +++ b/includes/FontFaceWidget.php @@ -11,7 +11,7 @@ class FontFaceWidget { * @param \Parser $parser * @param \PPFrame $frame */ - public static function create($text, $params, $parser, $frame){ + public static function create($text, $params, $parser, $frame) { if (!isset($params['src']) || !isset($params['name']) || empty($params['src']) || empty($params['name'])) { return '' . wfMessage('isekai-fontface-error-invalid-params')->parse() . ''; @@ -42,8 +42,8 @@ class FontFaceWidget { $fontUrl = $file->getUrl(); $fontId = substr(Utils::safeBase64Encode(md5($fontName, true)), 0, 8); - $css = ""; + $css = ""; $existsFonts[$fontName] = $fontId; $existsFonts = $parser->extIsekaiWidgetsCache->set('extraFonts', $existsFonts); diff --git a/includes/Html5Widget.php b/includes/Html5Widget.php new file mode 100644 index 0000000..b7712be --- /dev/null +++ b/includes/Html5Widget.php @@ -0,0 +1,35 @@ +getOutput()->addModules('ext.isekai.collapse'); + $allowedAttr = ['class']; + $htmlArgs = array_filter($args, function($k) use($allowedAttr) { + return in_array($k, $allowedAttr); + }, ARRAY_FILTER_USE_KEY); + + $content = ''; + if ($text) { + $content = $parser->recursiveTagParse($text, $frame); + } + + return [Html::rawElement('details', $htmlArgs, $content), "markerType" => 'nowiki']; + } + + public static function createSummary(string $text, array $args, \Parser $parser, \PPFrame $frame) { + $allowedAttr = ['class']; + $htmlArgs = array_filter($args, function($k) use($allowedAttr) { + return in_array($k, $allowedAttr); + }, ARRAY_FILTER_USE_KEY); + + $content = ''; + if ($text) { + $content = $parser->recursiveTagParse($text, $frame); + } + + return [Html::rawElement('summary', $htmlArgs, $content), "markerType" => 'nowiki']; + } +} \ No newline at end of file diff --git a/includes/TileWidget.php b/includes/TileWidget.php index 32a0c67..f4b86f9 100644 --- a/includes/TileWidget.php +++ b/includes/TileWidget.php @@ -130,7 +130,7 @@ class TileWidget { } private function getImagesArgs(array &$element, array &$content){ - /*$service = MediaWikiServices::getInstance(); + $service = MediaWikiServices::getInstance(); $this->images = []; // 提取wikitext图片 preg_match_all('/\[\[(?.+?:.+?)(\|.*?)?\]\]/', $this->content, $matches); @@ -149,13 +149,14 @@ class TileWidget { preg_match_all('/<img .*?src="(?<src>.*?)".*?srcset="(?<srcset>.*?)"[^\>]+>/', $this->content, $matches); if (isset($matches['src']) && !empty($matches['src'])) { $this->images = array_merge($this->images, $matches['src']); - }*/ + } if(!empty($this->images)){ $element['data-effect'] = 'image-set'; foreach($this->images as $image){ $content[] = Html::element('img', [ 'src' => $image, + 'style' => 'display: none' ]); } } diff --git a/includes/Widgets.php b/includes/Widgets.php index 84bc1df..416bd28 100644 --- a/includes/Widgets.php +++ b/includes/Widgets.php @@ -2,20 +2,32 @@ namespace Isekai\Widgets; use MapCacheLRU; +use Parser; class Widgets { + /** + * @param \Parser $parser + */ public static function onParserSetup(&$parser){ - $parser->extIsekaiWidgetsCache = new MapCacheLRU( 100 ); // 100 is arbitrary + $parser->extIsekaiWidgetsCache = new MapCacheLRU( 100 ); // 100 is arbitrary - $parser->setHook('createpage', CreatePageWidget::class . '::create'); - $parser->setHook('discoverbox', DiscoverWidget::class . '::create'); - $parser->setHook('previewcard', PreviewCardWidget::class . '::create'); + $parser->setHook('createpage', [CreatePageWidget::class, 'create']); + $parser->setHook('discoverbox', [DiscoverWidget::class, 'create']); + $parser->setHook('previewcard', [PreviewCardWidget::class, 'create']); - $parser->setHook('tile', TileWidget::class . '::create'); - $parser->setHook('tilegroup', TileGroupWidget::class . '::create'); + $parser->setHook('tile', [TileWidget::class, 'create']); + $parser->setHook('tilegroup', [TileGroupWidget::class, 'create']); - $parser->setHook('fontface', FontFaceWidget::class . '::create'); - $parser->setHook('exfont', ExtraFontWidget::class . '::create'); - return true; + $parser->setHook('fontface', [FontFaceWidget::class, 'create']); + $parser->setHook('exfont', [ExtraFontWidget::class, 'create']); + + $parser->setHook('details', [Html5Widget::class, 'createDetails']); + $parser->setHook('summary', [Html5Widget::class, 'createSummary']); + + return true; + } + + public static function onLoad(\OutputPage $outputPage) { + $outputPage->addModuleStyles("ext.isekai.collapse"); } } \ No newline at end of file diff --git a/modules/ext.isekai.collapse.js b/modules/ext.isekai.collapse.js new file mode 100644 index 0000000..2ecd15f --- /dev/null +++ b/modules/ext.isekai.collapse.js @@ -0,0 +1,33 @@ +(function($) { + $('.isekai-collapse').addClass('animate') + $('.isekai-collapse .isekai-collapse-title').on('click', '', function(e) { + e.preventDefault(); + var titleElem = $(this); + var containerElem = titleElem.parent('.isekai-collapse'); + var contentElem = containerElem.find('.isekai-collapse-content'); + if (containerElem.prop('open')) { // 需要收起 + var collapsedHeight = titleElem.outerHeight(); + var expandedHeight = collapsedHeight + contentElem.outerHeight(); + containerElem.css('height', expandedHeight); + console.log('expandedHeight', expandedHeight); + requestAnimationFrame(function() { + console.log('collapsedHeight', collapsedHeight); + containerElem.addClass('closing').css('height', collapsedHeight); + setTimeout(function() { + containerElem.prop('open', false).removeClass('closing'); //.css('height', 'auto'); + }, 260); + }); + } else { // 需要展开 + containerElem.prop('open', true); + var collapsedHeight = titleElem.outerHeight(); + containerElem.css('height', collapsedHeight); + requestAnimationFrame(function() { + var expandedHeight = collapsedHeight + contentElem.outerHeight(); + containerElem.css('height', expandedHeight); + /*setTimeout(function() { + containerElem.css('height', 'auto'); + }, 260);*/ + }); + } + }); +})(jQuery); diff --git a/modules/ext.isekai.collapse.less b/modules/ext.isekai.collapse.less new file mode 100644 index 0000000..e303396 --- /dev/null +++ b/modules/ext.isekai.collapse.less @@ -0,0 +1,64 @@ +.isekai-collapse { + width: 50%; + background: #fff; + margin-bottom: .5rem; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + border-radius: 5px; + overflow: hidden; + + @media screen and (max-width: 767px) { + width: 100%; + } + + &.animate { + overflow-y: hidden; + will-change: height; + transition: height 250ms ease-in-out; + } + + .isekai-collapse-title { + padding: 1rem; + display: block; + background-color: #f7f7f7; + padding-left: 2.2rem; + position: relative; + cursor: pointer; + color: black; + font-size: 1rem; + + &::before { + content: ''; + border-width: 0.4rem; + border-style: solid; + border-color: transparent transparent transparent #000; + position: absolute; + top: 1.32rem; + left: 1.2rem; + transform: rotate(0); + transform-origin: 0.2rem 50%; + will-change: transform; + transition: transform 250ms ease; + } + + &::-webkit-details-marker { + transform: rotate(90deg); + } + } + + .isekai-collapse-content { + padding: 1em; + } + + &[open] > .isekai-collapse-title:before { + transform: rotate(90deg); + } + + &.closing[open] > .isekai-collapse-title:before { + transform: rotate(0); + } +} + +.isekai-indent > .isekai-collapse { + padding-left: 0; + margin-left: 8px; +} \ No newline at end of file diff --git a/modules/tile/tile.js b/modules/tile/tile.js index dd0fa2b..3d9790b 100644 --- a/modules/tile/tile.js +++ b/modules/tile/tile.js @@ -519,7 +519,7 @@ isekai.tile.init = function () { this._createTile = function(){ function switchImage(el, img_src, i){ - $.setTimeout(function(){ + setTimeout(function(){ el.fadeOut(500, function(){ el.css("background-image", "url(" + img_src + ")"); el.fadeIn(); @@ -582,7 +582,11 @@ isekai.tile.init = function () { element.addClass("image-set"); $.each(element.children("img"), function(){ - that.images.push(this); + var imgElem = document.createElement('img'); + imgElem.src = this.src; + imgElem.srcset = this.srcset; + imgElem.alt = this.alt; + that.images.push(imgElem); $(this).remove(); }); @@ -592,12 +596,14 @@ isekai.tile.init = function () { var rnd_index = rand(0, temp.length - 1); var div = $("<div>").addClass("img -js-img-"+i).css("background-image", "url("+temp[rnd_index].src+")"); element.prepend(div); - temp.splice(rnd_index, 1); + if (temp.length > 1) { + temp.splice(rnd_index, 1); + } } var a = [0, 1, 4, 3, 2]; - $.setInterval(function(){ + setInterval(function(){ var temp = that.images.slice(); var colors = Colors.colors(Colors.PALETTES.ALL), bg; bg = colors[rand(0, colors.length - 1)]; @@ -608,7 +614,9 @@ isekai.tile.init = function () { var rnd_index = rand(0, temp.length - 1); var div = element.find(".-js-img-"+a[i]); switchImage(div, temp[rnd_index].src, i); - temp.splice(rnd_index, 1); + if (temp.length > 1) { + temp.splice(rnd_index, 1); + } } a = a.reverse(); @@ -619,7 +627,7 @@ isekai.tile.init = function () { this._runEffects = function(){ var o = this.options; - if (this.effectInterval === false) this.effectInterval = $.setInterval(function(){ + if (this.effectInterval === false) this.effectInterval = setInterval(function(){ var current, next; current = $(this.slides[this.currentSlide]); @@ -641,7 +649,7 @@ isekai.tile.init = function () { }; this._stopEffects = function(){ - $.clearInterval(this.effectInterval); + clearInterval(this.effectInterval); this.effectInterval = false; };