Skip to content

Commit 56cc203

Browse files
committed
Provide article interactions on the article page
1 parent 595127e commit 56cc203

File tree

10 files changed

+216
-89
lines changed

10 files changed

+216
-89
lines changed

com.woltlab.wcf/templates/article.tpl

Lines changed: 3 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -4,78 +4,7 @@
44

55
{capture assign='contentHeader'}
66
<header class="contentHeader articleContentHeader">
7-
<div class="contentHeaderTitle">
8-
<h1 class="contentTitle" itemprop="name headline">{$articleContent->title}</h1>
9-
<ul class="inlineList contentHeaderMetaData articleMetaData">
10-
<li itemprop="author" itemscope itemtype="http://schema.org/Person">
11-
{icon name='user'}
12-
{if $article->userID}
13-
<a href="{$article->getUserProfile()->getLink()}" class="userLink" data-object-id="{$article->userID}" itemprop="url">
14-
<span itemprop="name">{unsafe:$article->getUserProfile()->getFormattedUsername()}</span>
15-
</a>
16-
{else}
17-
<span itemprop="name">{$article->username}</span>
18-
{/if}
19-
</li>
20-
21-
<li>
22-
{icon name='clock'}
23-
<a href="{$article->getLink()}">{time time=$article->time}</a>
24-
<meta itemprop="datePublished" content="{$article->time|date:'c'}">
25-
</li>
26-
27-
{if $article->hasLabels()}
28-
<li>
29-
{icon name='tags'}
30-
<ul class="labelList">
31-
{foreach from=$article->getLabels() item=label}
32-
<li>{unsafe:$label->render()}</li>
33-
{/foreach}
34-
</ul>
35-
</li>
36-
{/if}
37-
38-
<li>
39-
{icon name='eye'}
40-
{lang}wcf.article.articleViews{/lang}
41-
</li>
42-
43-
{if $article->getDiscussionProvider()->getDiscussionCountPhrase()}
44-
<li itemprop="interactionStatistic" itemscope itemtype="http://schema.org/InteractionCounter">
45-
{icon name='comments'}
46-
{if $article->getDiscussionProvider()->getDiscussionLink()}<a href="{$article->getDiscussionProvider()->getDiscussionLink()}">{else}<span>{/if}
47-
{$article->getDiscussionProvider()->getDiscussionCountPhrase()}
48-
{if $article->getDiscussionProvider()->getDiscussionLink()}</a>{else}</span>{/if}
49-
<meta itemprop="interactionType" content="http://schema.org/CommentAction">
50-
<meta itemprop="userInteractionCount" content="{$article->getDiscussionProvider()->getDiscussionCount()}">
51-
</li>
52-
{/if}
53-
54-
{hascontent}
55-
<li>
56-
{icon name='flag'}
57-
{content}
58-
{if $article->isDeleted}
59-
<span class="badge red">{lang}wcf.message.status.deleted{/lang}</span>
60-
{/if}
61-
{if !$article->isPublished()}
62-
<span class="badge green">{lang}wcf.message.status.disabled{/lang}</span>
63-
{/if}
64-
{event name='contentHeaderMetaDataFlag'}
65-
{/content}
66-
</li>
67-
{/hascontent}
68-
69-
{event name='contentHeaderMetaData'}
70-
</ul>
71-
72-
<div itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
73-
<meta itemprop="name" content="{PAGE_TITLE|phrase}">
74-
<div itemprop="logo" itemscope itemtype="http://schema.org/ImageObject">
75-
<meta itemprop="url" content="{$__wcf->getStyleHandler()->getStyle()->getPageLogo()}">
76-
</div>
77-
</div>
78-
</div>
7+
{include file='articleContentHeaderTitle'}
798

809
{hascontent}
8110
<nav class="contentHeaderNavigation">
@@ -100,10 +29,8 @@
10029
{/capture}
10130

10231
{capture assign='contentInteractionButtons'}
103-
{if $article->canEdit()}
104-
<a href="{link controller='ArticleEdit' id=$article->articleID}{/link}" class="contentInteractionButton button small">{icon name='pencil'} <span>{lang}wcf.acp.article.edit{/lang}</span></a>
105-
{/if}
106-
32+
{unsafe:$interactionContextMenu->render()}
33+
10734
{if $article->isMultilingual && $__wcf->user->userID}
10835
<div class="contentInteractionButton dropdown jsOnly">
10936
<button type="button" class="dropdownToggle boxFlag box24 button small">
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<div class="contentHeaderTitle">
2+
<h1 class="contentTitle" itemprop="name headline">{$articleContent->title}</h1>
3+
<ul class="inlineList contentHeaderMetaData articleMetaData">
4+
<li itemprop="author" itemscope itemtype="http://schema.org/Person">
5+
{icon name='user'}
6+
{if $article->userID}
7+
<a href="{$article->getUserProfile()->getLink()}" class="userLink" data-object-id="{$article->userID}" itemprop="url">
8+
<span itemprop="name">{unsafe:$article->getUserProfile()->getFormattedUsername()}</span>
9+
</a>
10+
{else}
11+
<span itemprop="name">{$article->username}</span>
12+
{/if}
13+
</li>
14+
15+
<li>
16+
{icon name='clock'}
17+
<a href="{$article->getLink()}">{time time=$article->time}</a>
18+
<meta itemprop="datePublished" content="{$article->time|date:'c'}">
19+
</li>
20+
21+
{if $article->hasLabels()}
22+
<li>
23+
{icon name='tags'}
24+
<ul class="labelList">
25+
{foreach from=$article->getLabels() item=label}
26+
<li>{unsafe:$label->render()}</li>
27+
{/foreach}
28+
</ul>
29+
</li>
30+
{/if}
31+
32+
<li>
33+
{icon name='eye'}
34+
{lang}wcf.article.articleViews{/lang}
35+
</li>
36+
37+
{if $article->getDiscussionProvider()->getDiscussionCountPhrase()}
38+
<li itemprop="interactionStatistic" itemscope itemtype="http://schema.org/InteractionCounter">
39+
{icon name='comments'}
40+
{if $article->getDiscussionProvider()->getDiscussionLink()}<a href="{$article->getDiscussionProvider()->getDiscussionLink()}">{else}<span>{/if}
41+
{$article->getDiscussionProvider()->getDiscussionCountPhrase()}
42+
{if $article->getDiscussionProvider()->getDiscussionLink()}</a>{else}</span>{/if}
43+
<meta itemprop="interactionType" content="http://schema.org/CommentAction">
44+
<meta itemprop="userInteractionCount" content="{$article->getDiscussionProvider()->getDiscussionCount()}">
45+
</li>
46+
{/if}
47+
48+
{hascontent}
49+
<li>
50+
{icon name='flag'}
51+
{content}
52+
{if $article->isDeleted}
53+
<span class="badge red">{lang}wcf.message.status.deleted{/lang}</span>
54+
{/if}
55+
{if !$article->isPublished()}
56+
<span class="badge green">{lang}wcf.message.status.disabled{/lang}</span>
57+
{/if}
58+
{event name='contentHeaderMetaDataFlag'}
59+
{/content}
60+
</li>
61+
{/hascontent}
62+
63+
{event name='contentHeaderMetaData'}
64+
</ul>
65+
66+
<div itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
67+
<meta itemprop="name" content="{PAGE_TITLE|phrase}">
68+
<div itemprop="logo" itemscope itemtype="http://schema.org/ImageObject">
69+
<meta itemprop="url" content="{$__wcf->getStyleHandler()->getStyle()->getPageLogo()}">
70+
</div>
71+
</div>
72+
</div>

com.woltlab.wcf/templates/shared_standaloneInteractionButton.tpl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
document.getElementById('{unsafe:$containerID|encodeJS}'),
2222
'{unsafe:$providerClassName|encodeJS}',
2323
'{unsafe:$objectID|encodeJS}',
24-
'{unsafe:$redirectUrl|encodeJS}'
24+
'{unsafe:$redirectUrl|encodeJS}',
25+
'{unsafe:$reloadHeaderEndpoint|encodeJS}'
2526
);
2627
});
2728
</script>

ts/WoltLabSuite/Core/Component/Interaction/StandaloneButton.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,33 @@
77
* @since 6.2
88
*/
99

10+
import { getObject } from "WoltLabSuite/Core/Api/GetObject";
1011
import { getContextMenuOptions } from "WoltLabSuite/Core/Api/Interactions/GetContextMenuOptions";
1112
import UiDropdownSimple from "WoltLabSuite/Core/Ui/Dropdown/Simple";
1213

14+
interface HeaderContent {
15+
template: string;
16+
}
17+
1318
export class StandaloneButton {
1419
#container: HTMLElement;
1520
#providerClassName: string;
1621
#objectId: string | number;
1722
#redirectUrl: string;
23+
#reloadHeaderEndpoint: string;
1824

19-
constructor(container: HTMLElement, providerClassName: string, objectId: string | number, redirectUrl: string) {
25+
constructor(
26+
container: HTMLElement,
27+
providerClassName: string,
28+
objectId: string | number,
29+
redirectUrl: string,
30+
reloadHeaderEndpoint: string,
31+
) {
2032
this.#container = container;
2133
this.#providerClassName = providerClassName;
2234
this.#objectId = objectId;
2335
this.#redirectUrl = redirectUrl;
36+
this.#reloadHeaderEndpoint = reloadHeaderEndpoint;
2437

2538
this.#initInteractions();
2639
this.#initEventListeners();
@@ -39,6 +52,24 @@ export class StandaloneButton {
3952
this.#initInteractions();
4053
}
4154

55+
async #refreshHeader(): Promise<void> {
56+
if (!this.#reloadHeaderEndpoint) {
57+
return;
58+
}
59+
60+
const header = document.querySelector(".contentHeaderTitle");
61+
if (!header) {
62+
return;
63+
}
64+
65+
const result = await getObject<HeaderContent>(`${window.WSC_RPC_API_URL}${this.#reloadHeaderEndpoint}`);
66+
if (!result.ok) {
67+
return;
68+
}
69+
70+
header.outerHTML = result.value.template;
71+
}
72+
4273
#getDropdownMenu(): HTMLElement | undefined {
4374
const button = this.#container.querySelector<HTMLButtonElement>(".dropdownToggle");
4475
if (!button) {
@@ -71,10 +102,12 @@ export class StandaloneButton {
71102
#initEventListeners(): void {
72103
this.#container.addEventListener("interaction:invalidate", () => {
73104
void this.#refreshContextMenu();
105+
void this.#refreshHeader();
74106
});
75107

76108
this.#container.addEventListener("interaction:invalidate-all", () => {
77109
void this.#refreshContextMenu();
110+
void this.#refreshHeader();
78111
});
79112

80113
this.#container.addEventListener("interaction:remove", () => {

wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/StandaloneButton.js

Lines changed: 20 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ static function (\wcf\event\endpoint\ControllerCollecting $event) {
176176
$event->register(new \wcf\system\endpoint\controller\core\articles\RestoreArticle());
177177
$event->register(new \wcf\system\endpoint\controller\core\articles\PublishArticle());
178178
$event->register(new \wcf\system\endpoint\controller\core\articles\UnpublishArticle());
179+
$event->register(new \wcf\system\endpoint\controller\core\articles\contents\GetArticleContentHeaderTitle());
179180
$event->register(new \wcf\system\endpoint\controller\core\attachments\DeleteAttachment());
180181
$event->register(new \wcf\system\endpoint\controller\core\cronjobs\EnableCronjob());
181182
$event->register(new \wcf\system\endpoint\controller\core\cronjobs\DisableCronjob());

wcfsetup/install/files/lib/page/ArticlePage.class.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
use wcf\data\like\object\LikeObject;
99
use wcf\system\comment\CommentHandler;
1010
use wcf\system\comment\manager\ICommentManager;
11+
use wcf\system\interaction\StandaloneInteractionContextMenuComponent;
12+
use wcf\system\interaction\user\ArticleInteractions;
1113
use wcf\system\MetaTagHandler;
1214
use wcf\system\reaction\ReactionHandler;
15+
use wcf\system\request\LinkHandler;
1316
use wcf\system\WCF;
1417
use wcf\util\StringUtil;
1518

@@ -203,6 +206,13 @@ public function assignVariables()
203206
'previousArticle' => $this->previousArticle,
204207
'nextArticle' => $this->nextArticle,
205208
'articleLikeData' => $this->articleLikeData,
209+
'interactionContextMenu' => StandaloneInteractionContextMenuComponent::forContentInteractionButton(
210+
new ArticleInteractions(),
211+
$this->article,
212+
LinkHandler::getInstance()->getControllerLink(ArticleListPage::class),
213+
WCF::getLanguage()->getDynamicVariable('wcf.acp.article.edit'),
214+
"core/articles/contents/{$this->articleContentID}/content-header-title"
215+
),
206216

207217
// nullified values for backwards-compatibility
208218
'commentCanAdd' => 0,
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace wcf\system\endpoint\controller\core\articles\contents;
4+
5+
use Laminas\Diactoros\Response\JsonResponse;
6+
use Psr\Http\Message\ResponseInterface;
7+
use Psr\Http\Message\ServerRequestInterface;
8+
use wcf\data\article\Article;
9+
use wcf\data\article\content\ViewableArticleContent;
10+
use wcf\system\endpoint\GetRequest;
11+
use wcf\system\endpoint\IController;
12+
use wcf\system\exception\IllegalLinkException;
13+
use wcf\system\exception\PermissionDeniedException;
14+
use wcf\system\WCF;
15+
16+
/**
17+
* API endpoint for the rendering of the article content header title.
18+
*
19+
* @author Marcel Werk
20+
* @copyright 2001-2025 WoltLab GmbH
21+
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
22+
* @since 6.2
23+
*/
24+
#[GetRequest('/core/articles/contents/{id:\d+}/content-header-title')]
25+
final class GetArticleContentHeaderTitle implements IController
26+
{
27+
#[\Override]
28+
public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface
29+
{
30+
$articleContent = ViewableArticleContent::getArticleContent($variables['id']);
31+
if ($articleContent === null) {
32+
throw new IllegalLinkException();
33+
}
34+
35+
$this->assertArticleIsAccessible($articleContent->getArticle()->getDecoratedObject());
36+
37+
$articleContent->getArticle()->getDiscussionProvider()->setArticleContent($articleContent->getDecoratedObject());
38+
39+
return new JsonResponse([
40+
'template' => WCF::getTPL()->render('wcf', 'articleContentHeaderTitle', [
41+
'articleContent' => $articleContent,
42+
'article' => $articleContent->getArticle(),
43+
]),
44+
]);
45+
}
46+
47+
private function assertArticleIsAccessible(Article $article): void
48+
{
49+
if (!$article->canRead()) {
50+
throw new PermissionDeniedException();
51+
}
52+
}
53+
}

0 commit comments

Comments
 (0)