Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8831324
Add standalone polls embeddable via [poll] BBCode with localized ques…
BurntimeX Oct 23, 2026
f1aa23b
Render embedded standalone polls as an interactive widget with in-pl…
BurntimeX Oct 23, 2026
3885d3f
Prevent leaking restricted poll results through inline vote percentag…
BurntimeX Oct 23, 2026
30213bd
Return the rendered standalone poll widget from the vote endpoint and…
BurntimeX Oct 23, 2026
b475089
Add edit/delete interactions to the standalone poll widget with in-pl…
BurntimeX Oct 23, 2026
ee1eadf
Prevent list, grid and tree views from binding the interactions of em…
BurntimeX Oct 23, 2026
8b96791
Add option to hide poll results from everyone until the poll has ended
BurntimeX Oct 23, 2026
c014f59
Render nothing for [poll] embeds of deleted or unavailable polls inst…
BurntimeX Oct 23, 2026
d887c14
Add a minimum number of required answers to polls
BurntimeX Oct 23, 2026
6221e10
Deprecate the legacy content-bound poll stack in favor of standalone …
BurntimeX Oct 23, 2026
460f4e0
Revert the legacy poll UI to the 6.2 AJAX actions and restrict the RP…
BurntimeX Oct 23, 2026
8d18e7d
Replace simple ACL of standalone polls with ACL options to view and vote
BurntimeX Oct 23, 2026
cf99c6a
Align poll widget styles with style variables, font mixins and visual…
BurntimeX Oct 23, 2026
4877609
Drop redundant vote hint for single-choice polls
BurntimeX Oct 23, 2026
9bb1bad
Escape ACL container ID and input name in CSS selectors
BurntimeX Oct 23, 2026
4c46dea
Fix ACL values submitted by Ajax forms and ignore malformed ACL input
BurntimeX Oct 23, 2026
1e3c616
Adapt poll builders to parameterless afterCreate()/afterUpdate()
BurntimeX Oct 24, 2026
7cbdd7e
Add LegacyPollConverter to migrate container polls to standalone poll…
BurntimeX Oct 25, 2026
b3af637
Mark LegacyPollConverter and PollAclSnapshot as public API for third-…
BurntimeX Oct 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions com.woltlab.wcf/aclOption.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
<option name="canReadArticle">
<objecttype>com.woltlab.wcf.article.category</objecttype>
</option>
<option name="canViewPoll">
<objecttype>com.woltlab.wcf.poll</objecttype>
</option>
<option name="canVotePoll">
<objecttype>com.woltlab.wcf.poll</objecttype>
</option>
</options>
</import>
</data>
10 changes: 10 additions & 0 deletions com.woltlab.wcf/bbcode.xml
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,16 @@
</attribute>
</attributes>
</bbcode>
<bbcode name="poll">
<classname>wcf\system\bbcode\PollBBCode</classname>
<isBlockElement>1</isBlockElement>
<attributes>
<attribute name="0">
<validationpattern><![CDATA[^\d+$]]></validationpattern>
<required>1</required>
</attribute>
</attributes>
</bbcode>
<bbcode name="wsp">
<classname>wcf\system\bbcode\WoltLabSuitePageBBCode</classname>
<attributes>
Expand Down
9 changes: 9 additions & 0 deletions com.woltlab.wcf/objectType.xml
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,10 @@
<name>com.woltlab.wcf.label</name>
<definitionname>com.woltlab.wcf.acl</definitionname>
</type>
<type>
<name>com.woltlab.wcf.poll</name>
<definitionname>com.woltlab.wcf.acl</definitionname>
</type>
<!-- importers -->
<type>
<name>com.woltlab.wcf.user</name>
Expand Down Expand Up @@ -675,6 +679,11 @@
<definitionname>com.woltlab.wcf.message.embeddedObject</definitionname>
<classname>wcf\system\message\embedded\object\MediaMessageEmbeddedObjectHandler</classname>
</type>
<type>
<name>com.woltlab.wcf.poll</name>
<definitionname>com.woltlab.wcf.message.embeddedObject</definitionname>
<classname>wcf\system\message\embedded\object\PollMessageEmbeddedObjectHandler</classname>
</type>
<type>
<name>com.woltlab.wcf.user</name>
<definitionname>com.woltlab.wcf.message.embeddedObject</definitionname>
Expand Down
1 change: 1 addition & 0 deletions com.woltlab.wcf/objectTypeDefinition.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
<definition>
<name>com.woltlab.wcf.searchableObjectType</name>
</definition>
<!-- deprecated since 6.3, use standalone polls that are embedded through the `[poll]` BBCode instead -->
<definition>
<name>com.woltlab.wcf.poll</name>
<interfacename>wcf\system\poll\IPollHandler</interfacename>
Expand Down
1 change: 1 addition & 0 deletions com.woltlab.wcf/option.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1442,6 +1442,7 @@ DESC:wcf.global.sortOrder.descending</selectoptions>
<minvalue>2</minvalue>
<maxvalue>100</maxvalue>
</option>
<!-- deprecated since 6.3, only affects the legacy polls, the widget of standalone polls always uses the full width -->
<option name="poll_full_width">
<categoryname>message.general.poll</categoryname>
<optiontype>boolean</optiontype>
Expand Down
1 change: 1 addition & 0 deletions com.woltlab.wcf/package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
<instruction type="script" run="standalone">acp/update_com.woltlab.wcf_6.3_captchaQuestionL10n.php</instruction>
<instruction type="script" run="standalone">acp/update_com.woltlab.wcf_6.3_userOptionConfiguration.php</instruction>
<instruction type="script" run="standalone">acp/update_com.woltlab.wcf_6.3_mediaL10n.php</instruction>
<instruction type="script" run="standalone">acp/update_com.woltlab.wcf_6.3_pollL10n.php</instruction>
<instruction type="database" run="standalone">acp/database/update_com.woltlab.wcf_6.3_step3.php</instruction>

The following step requires the new files and must run before the `cronjob` PIP, which precedes the `objectType` PIP:
Expand Down
1 change: 1 addition & 0 deletions com.woltlab.wcf/templates/__messageFormPoll.tpl
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{* deprecated since 6.3: use standalone polls that are embedded through the `[poll]` BBCode instead *}
{if $__showPoll|isset && $__showPoll}
<script data-relocate="true">
require(["WoltLabSuite/Core/Ui/Poll/Editor"], (UiPollEditor) => {
Expand Down
1 change: 1 addition & 0 deletions com.woltlab.wcf/templates/__messageFormPollInline.tpl
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{* deprecated since 6.3: use standalone polls that are embedded through the `[poll]` BBCode instead *}
{if $__showPoll|isset && $__showPoll}
<script data-relocate="true">
require(["WoltLabSuite/Core/Ui/Poll/Editor"], (UiPollEditor) => {
Expand Down
1 change: 1 addition & 0 deletions com.woltlab.wcf/templates/poll.tpl
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{* deprecated since 6.3: use standalone polls that are embedded through the `[poll]` BBCode instead *}
{if (!$__wcf->getUser()->isGuest() || $poll->canSeeResult() || $poll->canViewParticipants()) && !$__pollLoadedJavaScript|isset}
{assign var=__pollLoadedJavaScript value=true}
<script data-relocate="true">
Expand Down
1 change: 1 addition & 0 deletions com.woltlab.wcf/templates/pollResult.tpl
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{* deprecated since 6.3: use standalone polls that are embedded through the `[poll]` BBCode instead *}
<ol class="pollResultList">
{foreach from=$poll->getOptions(true) item=option}
<li class="pollResultItem">
Expand Down
1 change: 1 addition & 0 deletions com.woltlab.wcf/templates/pollVote.tpl
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{* deprecated since 6.3: use standalone polls that are embedded through the `[poll]` BBCode instead *}
<dl class="wide pollVoteContainer" data-max-votes="{$poll->maxVotes}">
{foreach from=$poll->getOptions() item=option}
<dt></dt>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
{/if}

var aclList = new AclList(
'#{unsafe:$containerID|encodeJS}',
'#' + CSS.escape('{unsafe:$containerID|encodeJS}'),
{$objectTypeID},
{if $categoryName|isset}'{unsafe:$categoryName|encodeJS}'{else}null{/if},
{if $objectID|isset}{$objectID}{else}0{/if},
Expand Down
141 changes: 141 additions & 0 deletions com.woltlab.wcf/templates/shared_bbcode_poll.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<section class="pollWidget" id="{$elementID}" data-interaction-container aria-labelledby="{$elementID}_question"{*
*} data-poll-id="{$poll->pollID}"{*
*} data-state="{$initialState}"{*
*} data-can-vote="{if $poll->canVote()}true{else}false{/if}"{*
*} data-max-votes="{$poll->maxVotes}"{*
*} data-required-votes="{$poll->getRequiredVotes()}"{*
*} data-question="{$poll->getQuestion()}">
<header class="pollWidget__header">
<div class="pollWidget__eyebrow">
{icon name='chart-simple' type='solid'}
<span class="pollWidget__label">{lang}wcf.poll.widget.label{/lang}</span>
<span class="pollWidget__separator" aria-hidden="true"></span>
<span class="pollWidget__totalVotes">{lang}wcf.poll.widget.votes{/lang}</span>
{unsafe:$interactionButton}
</div>

<h3 class="pollWidget__question" id="{$elementID}_question" tabindex="-1">{$poll->getQuestion()}</h3>

{if $poll->canVote()}
{if $poll->maxVotes > 1}
<div class="pollWidget__hint pollWidget__hint--vote">
{if $poll->getRequiredVotes() == $poll->maxVotes}
{lang}wcf.poll.widget.hint.exactly{/lang}
{elseif $poll->getRequiredVotes() > 1}
{lang}wcf.poll.widget.hint.range{/lang}
{else}
{lang}wcf.poll.widget.hint.multiple{/lang}
{/if}
</div>
{/if}
{elseif !$poll->canSeeResult() && !$poll->resultsRequireEnd}
<div class="pollWidget__hint pollWidget__hint--vote">{lang}wcf.poll.restrictedResult{/lang}</div>
{/if}
{if $poll->isParticipant()}
{if $poll->canSeeResult()}
<div class="pollWidget__hint pollWidget__hint--results">{lang}wcf.poll.widget.hint.voted{/lang}</div>
{else}
{* Participants remain in the vote state while the results are hidden until the end. *}
<div class="pollWidget__hint">{lang}wcf.poll.widget.hint.voted{/lang}</div>
{/if}
{/if}
{if $poll->resultsRequireEnd && !$poll->isFinished()}
<div class="pollWidget__hint">{lang}wcf.poll.widget.hint.resultsAfterEnd{/lang}</div>
{/if}
</header>

<div class="pollWidget__options">
{foreach from=$options item=option}
<label class="pollWidget__option{if $poll->canSeeResult() && $leadingVotes && $option->votes == $leadingVotes} pollWidget__option--leading{/if}"{*
*}{if $poll->canSeeResult()} style="--pollWidget-percent: {$option->getRelativeVotes($poll)}%; --pollWidget-order: {$resultPositions[$option->optionID]}"{/if}>
<input type="{if $poll->maxVotes > 1}checkbox{else}radio{/if}" class="pollWidget__input visuallyHidden" name="{$elementID}" value="{$option->optionID}"{if $option->voted} checked{/if}{if !$poll->canVote()} disabled{/if}>
<span class="pollWidget__indicator" aria-hidden="true"></span>
{if $poll->canSeeResult()}
<span class="pollWidget__bar" aria-hidden="true"></span>
{/if}
<span class="pollWidget__text">{$option->getOptionValue()}</span>
{if $option->voted}
<span class="pollWidget__ownVote" title="{lang}wcf.poll.widget.ownVote{/lang}" role="img" aria-label="{lang}wcf.poll.widget.ownVote{/lang}"></span>
{/if}
{if $poll->canSeeResult()}
<span class="pollWidget__value" aria-hidden="true">
<span class="pollWidget__count">{#$option->votes}</span>
<span class="pollWidget__percent">{#$option->getRelativeVotes($poll)} %</span>
</span>
<span class="pollWidget__resultLabel visuallyHidden">{lang}wcf.poll.widget.optionResult{/lang}</span>
{/if}
</label>
{/foreach}
</div>

<footer class="pollWidget__footer">
{hascontent}
<div class="pollWidget__meta">
{content}
{if $poll->endTime}
<div class="pollWidget__metaItem">
{icon name='clock'}
<span>
{if $poll->isFinished()}
{lang}wcf.poll.widget.ended{/lang}
{else}
{lang}wcf.poll.widget.endTime{/lang}
{/if}
</span>
</div>
{/if}
{if $poll->isPublic}
<div class="pollWidget__metaItem">
{icon name='eye'}
<span>{lang}wcf.poll.isPublic{/lang}</span>
</div>
{/if}

{event name='meta'}
{/content}
</div>
{/hascontent}

{hascontent}
<div class="pollWidget__actions pollWidget__actions--vote">
{content}
{if $poll->canVote()}
<button type="button" class="button buttonPrimary pollWidget__vote" disabled>
{lang}wcf.poll.button.vote{/lang}
</button>
{/if}
{if $poll->canSeeResult()}
<button type="button" class="pollWidget__link pollWidget__showResults">
{lang}wcf.poll.button.showResult{/lang}
</button>
{/if}
{/content}
</div>
{/hascontent}

{hascontent}
<div class="pollWidget__actions pollWidget__actions--results">
{content}
{if $poll->canVote()}
<button type="button" class="button buttonPrimary pollWidget__showVote">
{if $poll->isParticipant()}
{lang}wcf.poll.button.changeVote{/lang}
{else}
{lang}wcf.poll.button.showVote{/lang}
{/if}
</button>
{/if}
{if $poll->canViewParticipants()}
<button type="button" class="pollWidget__link pollWidget__showParticipants">
{lang}wcf.poll.button.showParticipants{/lang}
</button>
{/if}
{/content}
</div>
{/hascontent}

{event name='footer'}
</footer>

{unsafe:$interactionInitialization}
</section>
24 changes: 24 additions & 0 deletions com.woltlab.wcf/templates/shared_pollManagerListItems.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{foreach from=$view->getItems() item='poll'}
<div class="pollManager__item listView__item"
role="option"
aria-selected="false"
tabindex="-1"
data-object-id="{$poll->getObjectID()}"
data-question="{$poll->getQuestion()}"
>
<span class="pollManager__item__indicator" aria-hidden="true">{icon name='check'}</span>

<div class="pollManager__item__content">
<div class="pollManager__item__question">{$poll->getQuestion()}</div>
<div class="pollManager__item__meta">
{lang}wcf.poll.manager.item.meta{/lang}
</div>
</div>

{hascontent}
<div class="pollManager__item__menu">
{content}{unsafe:$view->renderInteractionContextMenuButton($poll)}{/content}
</div>
{/hascontent}
</div>
{/foreach}
24 changes: 24 additions & 0 deletions com.woltlab.wcf/templates/shared_pollOptionListFormField.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<input type="hidden" {*
*}id="{$field->getPrefixedId()}" {*
*}name="{$field->getPrefixedId()}" {*
*}value="{$field->getValue()|json}"{*
*}>
<template id="{$field->getPrefixedId()}_template">
<span class="pollOptionListItem__handle">{icon name='grip-vertical'}</span>
<input type="text" class="long pollOptionListItem__value" maxlength="255">
<button type="button" class="button small pollOptionListItem__remove jsTooltip" title="{lang}wcf.poll.button.removeOption{/lang}">
{icon name='xmark'}
</button>
</template>

<script data-relocate="true">
require(['WoltLabSuite/Core/Component/Poll/OptionList'], ({ setup }) => {
{jsphrase name='wcf.poll.button.addOption'}

setup(
document.getElementById('{unsafe:$field->getPrefixedId()|encodeJS}'),
{unsafe:$availableLanguages|json},
{POLL_MAX_OPTIONS}
);
});
</script>
1 change: 1 addition & 0 deletions com.woltlab.wcf/templates/shared_pollOptionsFormField.tpl
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{* deprecated since 6.3: use `shared_pollOptionListFormField.tpl` instead *}
<ol class="sortableList"></ol>

<script data-relocate="true">
Expand Down
12 changes: 12 additions & 0 deletions com.woltlab.wcf/templates/shared_wysiwyg.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@
{jsphrase name='wcf.editor.restoreDraft'}
{jsphrase name='wcf.editor.restoreDraft.preview'}
{jsphrase name='wcf.editor.restoreDraft.restoreOrDiscard'}
{if MODULE_POLL && $__wcf->session->getPermission('user.poll.canCreatePoll')}
{jsphrase name='wcf.poll.manager'}
{jsphrase name='wcf.poll.manager.selection.none'}
{/if}

{if $__wcf->language->languageCode !== 'en'}
window.CKEDITOR_TRANSLATIONS['{unsafe:$__wcf->getBBCodeHandler()->getEditorLocalization(true)|strtolower|encodeJS}'].dictionary["Single line code"] = '{jslang}wcf.editor.button.code{/jslang}';
Expand Down Expand Up @@ -82,6 +86,7 @@
mark: {if $__wcf->getBBCodeHandler()->isAvailableBBCode('mark')}true{else}false{/if},
media: {if $__wcf->session->getPermission('admin.content.cms.canUseMedia')}true{else}false{/if},
mention: {if $__wcf->session->hasPermission('user.message.canMention')}element.dataset.supportMention === "true"{else}false{/if},
poll: {if MODULE_POLL && $__wcf->session->getPermission('user.poll.canCreatePoll') && $__wcf->getBBCodeHandler()->isAvailableBBCode('poll')}true{else}false{/if},
quoteBlock: true,
strikethrough: true,
submitOnEnter: false,
Expand Down Expand Up @@ -109,6 +114,13 @@
label: '{jslang}wcf.editor.button.media{/jslang}',
});
}
if (features.poll) {
bbcodes.push({
icon: "chart-bar;false",
name: "poll",
label: '{jslang}wcf.editor.button.poll{/jslang}',
});
}
const smileys = [
{foreach from=$__wcf->getSmileyCache()->getEmojis() key=__code item=__smiley}
{
Expand Down
37 changes: 37 additions & 0 deletions com.woltlab.wcf/userGroupOption.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
<category name="user.message.contactForm">
<parent>user.message</parent>
</category>
<category name="user.message.poll">
<parent>user.message</parent>
<options>module_poll</options>
</category>
<category name="user.cms">
<parent>user</parent>
</category>
Expand All @@ -64,6 +68,10 @@
<category name="mod.article">
<parent>mod.cms</parent>
</category>
<category name="mod.poll">
<parent>mod.general</parent>
<options>module_poll</options>
</category>
<category name="admin"/>
<category name="admin.general">
<parent>admin</parent>
Expand Down Expand Up @@ -991,6 +999,35 @@ bmp</defaultvalue>
<moddefaultvalue>1</moddefaultvalue>
<usersonly>1</usersonly>
</option>
<option name="user.poll.canVotePoll">
<categoryname>user.message.poll</categoryname>
<optiontype>boolean</optiontype>
<defaultvalue>0</defaultvalue>
<userdefaultvalue>1</userdefaultvalue>
<usersonly>1</usersonly>
</option>
<option name="user.poll.canCreatePoll">
<categoryname>user.message.poll</categoryname>
<optiontype>boolean</optiontype>
<defaultvalue>0</defaultvalue>
<userdefaultvalue>1</userdefaultvalue>
<usersonly>1</usersonly>
<enableoptions>user.poll.canStartPublicPoll</enableoptions>
</option>
<option name="user.poll.canStartPublicPoll">
<categoryname>user.message.poll</categoryname>
<optiontype>boolean</optiontype>
<defaultvalue>0</defaultvalue>
<userdefaultvalue>1</userdefaultvalue>
<usersonly>1</usersonly>
</option>
<option name="mod.poll.canManagePoll">
<categoryname>mod.poll</categoryname>
<optiontype>boolean</optiontype>
<defaultvalue>0</defaultvalue>
<moddefaultvalue>1</moddefaultvalue>
<usersonly>1</usersonly>
</option>
<option name="mod.article.canEditComment">
<categoryname>mod.article</categoryname>
<optiontype>boolean</optiontype>
Expand Down
Loading
Loading