<span class="articleNavigationArticleContent">
<span class="articleNavigationEntityName">{lang}wcf.article.previousArticle{/lang}</span>
<span class="articleNavigationArticleTitle">
- <a href="{$previousArticle->getLink()}" rel="prev" class="articleNavigationArticleLink">
+ <a href="{$previousArticle->getLink()}" rel="prev" class="articleNavigationArticleLink articleLink" data-object-id="{$previousArticle->getObjectID()}">
{$previousArticle->getTitle()}
</a>
</span>
<span class="articleNavigationArticleContent">
<span class="articleNavigationEntityName">{lang}wcf.article.nextArticle{/lang}</span>
<span class="articleNavigationArticleTitle">
- <a href="{$nextArticle->getLink()}" rel="next" class="articleNavigationArticleLink">
+ <a href="{$nextArticle->getLink()}" rel="next" class="articleNavigationArticleLink articleLink" data-object-id="{$nextArticle->getObjectID()}">
{$nextArticle->getTitle()}
</a>
</span>
--- /dev/null
+<div class="popover__layout">
+ {if $article->getTeaserImage()}
+ <div class="popover__coverPhoto">
+ <img class="popover__coverPhoto__image" src="{$article->getTeaserImage()->getThumbnailLink('medium')}" alt="">
+ </div>
+ {/if}
+
+ <div class="popover__header">
+ {event name='beforeHeader'}
+
+ <div class="popover__avatar">
+ {user object=$article->getUserProfile() type='avatar48' ariaHidden='true' tabindex='-1'}
+ </div>
+ <div class="popover__title">
+ <a href="{$article->getLink()}">{$article->getTitle()}</a>
+ </div>
+ <div class="popover__time">
+ {time time=$article->time}
+ </div>
+
+ {event name='afterHeader'}
+ </div>
+
+ {event name='beforeText'}
+
+ <div class="popover__text htmlContent">
+ {unsafe:$article->getFormattedTeaser()}
+ </div>
+
+ {event name='afterText'}
+</div>
<a href="{$boxArticle->getLink()}" aria-hidden="true" tabindex="-1">{unsafe:$boxArticle->getUserProfile()->getAvatar()->getImageTag(24)}</a>
<div class="sidebarItemTitle">
- <h3><a href="{$boxArticle->getLink()}">{$boxArticle->getTitle()}</a></h3>
+ <h3>{anchor object=$boxArticle class='articleLink' title=$boxArticle->getTitle()}</h3>
<small>
{if $boxSortField == 'time'}
--- /dev/null
+/**
+ * Gets the html code for the rendering of an article popover.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2025 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+
+import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend";
+import { ApiResult, apiResultFromError, apiResultFromValue } from "WoltLabSuite/Core/Api/Result";
+
+type Response = {
+ template: string;
+};
+
+export async function getArticlePopover(articleId: number): Promise<ApiResult<string>> {
+ const url = new URL(`${window.WSC_RPC_API_URL}core/articles/${articleId}/popover`);
+
+ let response: Response;
+ try {
+ response = (await prepareRequest(url).get().fetchAsJson()) as Response;
+ } catch (e) {
+ return apiResultFromError(e);
+ }
+
+ return apiResultFromValue(response.template);
+}
import { whenFirstSeen } from "./LazyLoader";
import { prepareRequest } from "./Ajax/Backend";
import { setup as serviceWorkerSetup } from "./Notification/ServiceWorker";
+import { getArticlePopover } from "./Api/Articles/GetArticlePopover";
interface BootstrapOptions {
backgroundQueue: {
});
}
+function setupArticlePopover(): void {
+ whenFirstSeen(".articleLink", () => {
+ void import("WoltLabSuite/Core/Component/Popover").then(({ setupFor }) => {
+ setupFor({
+ endpoint: async (objectId: number) => {
+ return (await getArticlePopover(objectId)).unwrap();
+ },
+ identifier: "com.woltlab.wcf.article",
+ selector: ".articleLink",
+ });
+ });
+ });
+}
+
declare const COMPILER_TARGET_DEFAULT: boolean;
/**
}
setupUserPopover(options.endpointUserPopover);
+ setupArticlePopover();
if (options.executeCronjobs !== undefined) {
void prepareRequest(options.executeCronjobs)
--- /dev/null
+/**
+ * Gets the html code for the rendering of an article popover.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2025 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "WoltLabSuite/Core/Api/Result"], function (require, exports, Backend_1, Result_1) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ exports.getArticlePopover = getArticlePopover;
+ async function getArticlePopover(articleId) {
+ const url = new URL(`${window.WSC_RPC_API_URL}core/articles/${articleId}/popover`);
+ let response;
+ try {
+ response = (await (0, Backend_1.prepareRequest)(url).get().fetchAsJson());
+ }
+ catch (e) {
+ return (0, Result_1.apiResultFromError)(e);
+ }
+ return (0, Result_1.apiResultFromValue)(response.template);
+ }
+});
* @copyright 2001-2019 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
*/
-define(["require", "exports", "tslib", "./BackgroundQueue", "./Bootstrap", "./Ui/User/Ignore", "./Ui/Page/Header/Menu", "./Ui/Message/UserConsent", "./Ui/Message/Share/Dialog", "./Ui/Message/Share/Providers", "./Ui/Feed/Dialog", "./User", "./Ui/Page/Menu/Main/Frontend", "./LazyLoader", "./Ajax/Backend", "./Notification/ServiceWorker"], function (require, exports, tslib_1, BackgroundQueue, Bootstrap, UiUserIgnore, UiPageHeaderMenu, UiMessageUserConsent, UiMessageShareDialog, Providers_1, UiFeedDialog, User_1, Frontend_1, LazyLoader_1, Backend_1, ServiceWorker_1) {
+define(["require", "exports", "tslib", "./BackgroundQueue", "./Bootstrap", "./Ui/User/Ignore", "./Ui/Page/Header/Menu", "./Ui/Message/UserConsent", "./Ui/Message/Share/Dialog", "./Ui/Message/Share/Providers", "./Ui/Feed/Dialog", "./User", "./Ui/Page/Menu/Main/Frontend", "./LazyLoader", "./Ajax/Backend", "./Notification/ServiceWorker", "./Api/Articles/GetArticlePopover"], function (require, exports, tslib_1, BackgroundQueue, Bootstrap, UiUserIgnore, UiPageHeaderMenu, UiMessageUserConsent, UiMessageShareDialog, Providers_1, UiFeedDialog, User_1, Frontend_1, LazyLoader_1, Backend_1, ServiceWorker_1, GetArticlePopover_1) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.setup = setup;
});
});
}
+ function setupArticlePopover() {
+ (0, LazyLoader_1.whenFirstSeen)(".articleLink", () => {
+ void new Promise((resolve_2, reject_2) => { require(["WoltLabSuite/Core/Component/Popover"], resolve_2, reject_2); }).then(tslib_1.__importStar).then(({ setupFor }) => {
+ setupFor({
+ endpoint: async (objectId) => {
+ return (await (0, GetArticlePopover_1.getArticlePopover)(objectId)).unwrap();
+ },
+ identifier: "com.woltlab.wcf.article",
+ selector: ".articleLink",
+ });
+ });
+ });
+ }
/**
* Bootstraps general modules and frontend exclusive ones.
*/
});
UiPageHeaderMenu.init();
if (options.styleChanger) {
- void new Promise((resolve_2, reject_2) => { require(["./Controller/Style/Changer"], resolve_2, reject_2); }).then(tslib_1.__importStar).then((ControllerStyleChanger) => {
+ void new Promise((resolve_3, reject_3) => { require(["./Controller/Style/Changer"], resolve_3, reject_3); }).then(tslib_1.__importStar).then((ControllerStyleChanger) => {
ControllerStyleChanger.setup();
});
}
setupUserPopover(options.endpointUserPopover);
+ setupArticlePopover();
if (options.executeCronjobs !== undefined) {
void (0, Backend_1.prepareRequest)(options.executeCronjobs)
.get()
}
}
(0, LazyLoader_1.whenFirstSeen)("woltlab-core-reaction-summary", () => {
- void new Promise((resolve_3, reject_3) => { require(["./Ui/Reaction/SummaryDetails"], resolve_3, reject_3); }).then(tslib_1.__importStar).then(({ setup }) => setup());
+ void new Promise((resolve_4, reject_4) => { require(["./Ui/Reaction/SummaryDetails"], resolve_4, reject_4); }).then(tslib_1.__importStar).then(({ setup }) => setup());
});
(0, LazyLoader_1.whenFirstSeen)("woltlab-core-comment", () => {
- void new Promise((resolve_4, reject_4) => { require(["./Component/Comment/woltlab-core-comment"], resolve_4, reject_4); }).then(tslib_1.__importStar);
+ void new Promise((resolve_5, reject_5) => { require(["./Component/Comment/woltlab-core-comment"], resolve_5, reject_5); }).then(tslib_1.__importStar);
});
(0, LazyLoader_1.whenFirstSeen)("woltlab-core-comment-response", () => {
- void new Promise((resolve_5, reject_5) => { require(["./Component/Comment/Response/woltlab-core-comment-response"], resolve_5, reject_5); }).then(tslib_1.__importStar);
+ void new Promise((resolve_6, reject_6) => { require(["./Component/Comment/Response/woltlab-core-comment-response"], resolve_6, reject_6); }).then(tslib_1.__importStar);
});
(0, LazyLoader_1.whenFirstSeen)("woltlab-core-emoji-picker", () => {
- void new Promise((resolve_6, reject_6) => { require(["./Component/EmojiPicker/woltlab-core-emoji-picker"], resolve_6, reject_6); }).then(tslib_1.__importStar);
+ void new Promise((resolve_7, reject_7) => { require(["./Component/EmojiPicker/woltlab-core-emoji-picker"], resolve_7, reject_7); }).then(tslib_1.__importStar);
});
(0, LazyLoader_1.whenFirstSeen)("[data-follow-user]", () => {
- void new Promise((resolve_7, reject_7) => { require(["./Component/User/Follow"], resolve_7, reject_7); }).then(tslib_1.__importStar).then(({ setup }) => setup());
+ void new Promise((resolve_8, reject_8) => { require(["./Component/User/Follow"], resolve_8, reject_8); }).then(tslib_1.__importStar).then(({ setup }) => setup());
});
(0, LazyLoader_1.whenFirstSeen)("[data-ignore-user]", () => {
- void new Promise((resolve_8, reject_8) => { require(["./Component/User/Ignore"], resolve_8, reject_8); }).then(tslib_1.__importStar).then(({ setup }) => setup());
+ void new Promise((resolve_9, reject_9) => { require(["./Component/User/Ignore"], resolve_9, reject_9); }).then(tslib_1.__importStar).then(({ setup }) => setup());
});
}
});
$eventHandler->register(
\wcf\event\endpoint\ControllerCollecting::class,
static function (\wcf\event\endpoint\ControllerCollecting $event) {
+ $event->register(new \wcf\system\endpoint\controller\core\articles\GetArticlePopover);
$event->register(new \wcf\system\endpoint\controller\core\files\DeleteFile);
$event->register(new \wcf\system\endpoint\controller\core\files\GenerateThumbnails);
$event->register(new \wcf\system\endpoint\controller\core\files\PrepareUpload);
use wcf\data\attachment\GroupedAttachmentList;
use wcf\data\DatabaseObject;
use wcf\data\ILinkableObject;
+use wcf\data\IPopoverObject;
use wcf\data\IUserContent;
use wcf\data\object\type\ObjectTypeCache;
use wcf\data\user\UserProfile;
* @property-read int $isDeleted is 1 if the article is in trash bin, otherwise 0
* @property-read int $hasLabels is `1` if labels are assigned to the article
*/
-class Article extends DatabaseObject implements ILinkableObject, IUserContent
+class Article extends DatabaseObject implements ILinkableObject, IPopoverObject, IUserContent
{
/**
* indicates that article is unpublished
return null;
}
+
+ #[\Override]
+ public function getPopoverLinkClass()
+ {
+ return 'articleLink';
+ }
}
--- /dev/null
+<?php
+
+namespace wcf\system\endpoint\controller\core\articles;
+
+use Laminas\Diactoros\Response\JsonResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use wcf\data\article\Article;
+use wcf\data\article\ViewableArticle;
+use wcf\http\Helper;
+use wcf\system\endpoint\GetRequest;
+use wcf\system\endpoint\IController;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\WCF;
+
+/**
+ * API endpoint for the rendering of the article popover.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2025 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ */
+#[GetRequest('/core/articles/{id:\d+}/popover')]
+final class GetArticlePopover implements IController
+{
+ #[\Override]
+ public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface
+ {
+ $article = Helper::fetchObjectFromRequestParameter($variables['id'], Article::class);
+
+ $this->assertArticleIsAccessible($article);
+
+ return new JsonResponse([
+ 'template' => $this->renderPopover($article),
+ ]);
+ }
+
+ private function assertArticleIsAccessible(Article $article): void
+ {
+ if (!$article->canRead()) {
+ throw new PermissionDeniedException();
+ }
+ }
+
+ private function renderPopover(Article $article): string
+ {
+ return WCF::getTPL()->fetch('articlePopover', 'wcf', [
+ 'article' => ViewableArticle::getArticle($article->articleID),
+ ], true);
+ }
+}