--- /dev/null
+{include file='__formFieldHeader'}
+
+<ul class="labelList jsOnly" data-object-id="{@$field->getLabelGroup()->groupID}">
+ <li class="dropdown labelChooser" data-group-id="{@$field->getLabelGroup()->groupID}">
+ <div class="dropdownToggle" data-toggle="labelGroup{@$field->getLabelGroup()->groupID}">
+ <span class="badge label">{lang}wcf.label.none{/lang}</span>
+ </div>
+ <div class="dropdownMenu">
+ <ul class="scrollableDropdownMenu">
+ {foreach from=$field->getLabelGroup() item=label}
+ <li data-label-id="{@$label->labelID}">
+ <span><span class="badge label{if $label->getClassNames()} {@$label->getClassNames()}{/if}">{lang}{$label->label}{/lang}</span></span>
+ </li>
+ {/foreach}
+ </ul>
+ </div>
+ </li>
+</ul>
+
+<noscript>
+ <select name="{@$field->getPrefixedId()}[{@$field->getLabelGroup()->groupID}]">
+ {foreach from=$field->getLabelGroup() item=label}
+ <option value="{@$label->labelID}">{lang}{$label->label}{/lang}</option>
+ {/foreach}
+ </select>
+</noscript>
+
+{js application='wcf' file='WCF.Label' bundle='WCF.Combined'}
+<script data-relocate="true">
+ require(['Dom/Util', 'Language', 'WoltLabSuite/Core/Form/Builder/Field/Label'], function(DomUtil, Language, FormBuilderFieldLabel) {
+ Language.addObject({
+ 'wcf.label.none': '{lang}wcf.label.none{/lang}',
+ 'wcf.label.withoutSelection': '{lang}wcf.label.withoutSelection{/lang}'
+ });
+
+ new FormBuilderFieldLabel(
+ '{@$field->getPrefixedId()}',
+ {if $field->getValue()}{@$field->getValue()}{else}null{/if},
+ {
+ forceSelection: {if $field->getLabelGroup()->forceSelection}true{else}false{/if}
+ }
+ );
+ });
+</script>
+
+{include file='__formFieldFooter'}
"__formFieldHeader",
"__iconFormField",
"__itemListFormField",
+ "__labelFormField",
"__mediaSetCategoryDialog",
"__multilineTextFormField",
"__multiPageCondition",
--- /dev/null
+{include file='__formFieldHeader'}
+
+<ul class="labelList jsOnly" data-object-id="{@$field->getLabelGroup()->groupID}">
+ <li class="dropdown labelChooser" data-group-id="{@$field->getLabelGroup()->groupID}">
+ <div class="dropdownToggle" data-toggle="labelGroup{@$field->getLabelGroup()->groupID}">
+ <span class="badge label">{lang}wcf.label.none{/lang}</span>
+ </div>
+ <div class="dropdownMenu">
+ <ul class="scrollableDropdownMenu">
+ {foreach from=$field->getLabelGroup() item=label}
+ <li data-label-id="{@$label->labelID}">
+ <span><span class="badge label{if $label->getClassNames()} {@$label->getClassNames()}{/if}">{lang}{$label->label}{/lang}</span></span>
+ </li>
+ {/foreach}
+ </ul>
+ </div>
+ </li>
+</ul>
+
+<noscript>
+ <select name="{@$field->getPrefixedId()}[{@$field->getLabelGroup()->groupID}]">
+ {foreach from=$field->getLabelGroup() item=label}
+ <option value="{@$label->labelID}">{lang}{$label->label}{/lang}</option>
+ {/foreach}
+ </select>
+</noscript>
+
+{js application='wcf' file='WCF.Label' bundle='WCF.Combined'}
+<script data-relocate="true">
+ require(['Dom/Util', 'Language', 'WoltLabSuite/Core/Form/Builder/Field/Label'], function(DomUtil, Language, FormBuilderFieldLabel) {
+ Language.addObject({
+ 'wcf.label.none': '{lang}wcf.label.none{/lang}',
+ 'wcf.label.withoutSelection': '{lang}wcf.label.withoutSelection{/lang}'
+ });
+
+ new FormBuilderFieldLabel(
+ '{@$field->getPrefixedId()}',
+ {if $field->getValue()}{@$field->getValue()}{else}null{/if},
+ {
+ forceSelection: {if $field->getLabelGroup()->forceSelection}true{else}false{/if}
+ }
+ );
+ });
+</script>
+
+{include file='__formFieldFooter'}
--- /dev/null
+/**
+ * Handles the JavaScript part of the label form field.
+ *
+ * @author Alexander Ebert, Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module WoltLabSuite/Core/Form/Builder/Field/Label
+ * @since 5.2
+ */
+define(['Core', 'Dom/Util', 'Language', 'Ui/SimpleDropdown'], function(Core, DomUtil, Language, UiSimpleDropdown) {
+ "use strict";
+
+ /**
+ * @constructor
+ */
+ function FormBuilderFieldLabel(fielId, labelId, options) {
+ this.init(fielId, labelId, options);
+ };
+ FormBuilderFieldLabel.prototype = {
+ /**
+ * Initializes the label form field.
+ *
+ * @param {string} fieldId id of the relevant form builder field
+ * @param {integer} labelId id of the currently selected label
+ * @param {object} options additional label options
+ */
+ init: function(fieldId, labelId, options) {
+ this._formFieldContainer = elById(fieldId + 'Container');
+ this._labelChooser = elByClass('labelChooser', this._formFieldContainer)[0];
+ this._options = Core.extend({
+ forceSelection: false,
+ showWithoutSelection: false
+ }, options);
+
+ this._input = elCreate('input');
+ this._input.type = 'hidden';
+ this._input.name = fieldId;
+ this._input.value = ~~labelId;
+ this._formFieldContainer.appendChild(this._input);
+
+ var labelChooserId = DomUtil.identify(this._labelChooser);
+
+ // init dropdown
+ var dropdownMenu = UiSimpleDropdown.getDropdownMenu(labelChooserId);
+ if (dropdownMenu === null) {
+ UiSimpleDropdown.init(elByClass('dropdownToggle', this._labelChooser)[0]);
+ dropdownMenu = UiSimpleDropdown.getDropdownMenu(labelChooserId);
+ }
+
+ var additionalOptionList = null;
+ if (this._options.showWithoutSelection || !this._options.forceSelection) {
+ additionalOptionList = elCreate('ul');
+ dropdownMenu.appendChild(additionalOptionList);
+
+ var dropdownDivider = elCreate('li');
+ dropdownDivider.className = 'dropdownDivider';
+ additionalOptionList.appendChild(dropdownDivider);
+ }
+
+ if (this._options.showWithoutSelection) {
+ var listItem = elCreate('li');
+ elData(listItem, 'label-id', -1);
+ this._blockScroll(listItem);
+ additionalOptionList.appendChild(listItem);
+
+ var span = elCreate('span');
+ listItem.appendChild(span);
+
+ var label = elCreate('span');
+ label.className = 'badge label';
+ label.innerHTML = Language.get('wcf.label.withoutSelection');
+ span.appendChild(label);
+ }
+
+ if (!this._options.forceSelection) {
+ var listItem = elCreate('li');
+ elData(listItem, 'label-id', 0);
+ this._blockScroll(listItem);
+ additionalOptionList.appendChild(listItem);
+
+ var span = elCreate('span');
+ listItem.appendChild(span);
+
+ var label = elCreate('span');
+ label.className = 'badge label';
+ label.innerHTML = Language.get('wcf.label.none');
+ span.appendChild(label);
+ }
+
+ elBySelAll('li:not(.dropdownDivider)', dropdownMenu, function(listItem) {
+ listItem.addEventListener('click', this._click.bind(this));
+
+ if (labelId) {
+ if (~~elData(listItem, 'label-id') === labelId) {
+ this._selectLabel(listItem);
+ }
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Blocks page scrolling for the given element.
+ *
+ * @param {HTMLElement} element
+ */
+ _blockScroll: function(element) {
+ element.addEventListener(
+ 'wheel',
+ function(event) {
+ event.preventDefault();
+ },
+ {
+ passive: false
+ }
+ );
+ },
+
+ /**
+ * Select a label after clicking on it.
+ *
+ * @param {Event} event click event in label selection dropdown
+ */
+ _click: function(event) {
+ event.preventDefault();
+
+ this._selectLabel(event.currentTarget, false);
+ },
+
+ /**
+ * Selects the given label.
+ *
+ * @param {HTMLElement} label
+ */
+ _selectLabel: function(label) {
+ // save label
+ var labelId = elData(label, 'label-id');
+ if (!labelId) {
+ labelId = 0;
+ }
+
+ // replace button with currently selected label
+ var displayLabel = elBySel('span > span', label);
+ var button = elBySel('.dropdownToggle > span', this._labelChooser);
+ button.className = displayLabel.className;
+ button.textContent = displayLabel.textContent;
+
+ this._input.value = labelId;
+ }
+ };
+
+ return FormBuilderFieldLabel;
+});
--- /dev/null
+<?php
+namespace wcf\system\form\builder\field\label;
+use wcf\data\IStorableObject;
+use wcf\data\label\group\ViewableLabelGroup;
+use wcf\data\label\Label;
+use wcf\system\form\builder\field\AbstractFormField;
+use wcf\system\form\builder\field\data\processor\CustomFormFieldDataProcessor;
+use wcf\system\form\builder\field\validation\FormFieldValidationError;
+use wcf\system\form\builder\IFormDocument;
+use wcf\system\form\builder\IObjectTypeFormNode;
+use wcf\system\form\builder\TObjectTypeFormNode;
+use wcf\system\label\LabelHandler;
+
+/**
+ * Implementation of a form field to select labels.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\System\Form\Builder\Field
+ * @since 5.2
+ */
+class LabelFormField extends AbstractFormField implements IObjectTypeFormNode {
+ use TObjectTypeFormNode;
+
+ /**
+ * label group whose labels can be selected via this form field
+ * @var ViewableLabelGroup
+ */
+ protected $labelGroup;
+
+ /**
+ * @inheritDoc
+ */
+ protected $templateName = '__labelFormField';
+
+ /**
+ * loaded labels grouped by label object type and object id to avoid loading the same labels
+ * over and over again for the same object and different label groups
+ * @var Label[][]
+ */
+ protected static $loadedLabels = [];
+
+ /**
+ * Returns the label group whose labels can be selected via this form field.
+ *
+ * @return ViewableLabelGroup label group whose labels can be selected
+ * @throws \BadMethodCallException if no label has been set
+ */
+ public function getLabelGroup() {
+ if ($this->labelGroup === null) {
+ throw new \BadMethodCallException("No label group has been set.");
+ }
+
+ return $this->labelGroup;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getObjectTypeDefinition() {
+ return 'com.woltlab.wcf.label.object';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function hasSaveValue() {
+ return false;
+ }
+
+ /**
+ * Sets the label group whose labels can be selected via this form field and returns this
+ * form field.
+ *
+ * If no form field label has been set, the title of the label group will be set as label.
+ *
+ * @param ViewableLabelGroup $labelGroup label group whose labels can be selected
+ * @return static this form field
+ */
+ public function labelGroup(ViewableLabelGroup $labelGroup) {
+ $this->labelGroup = $labelGroup;
+
+ if ($this->label === null) {
+ $this->label($this->getLabelGroup()->getTitle());
+ }
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function loadValueFromObject(IStorableObject $object) {
+ $objectTypeID = $this->getObjectType()->objectTypeID;
+ $objectID = $object->getObjectID();
+
+ if (!isset(static::$loadedLabels[$objectTypeID])) {
+ static::$loadedLabels[$objectTypeID] = [];
+ }
+ if (!isset(static::$loadedLabels[$objectTypeID][$objectID])) {
+ static::$loadedLabels[$objectTypeID][$objectID] = LabelHandler::getInstance()->getAssignedLabels(
+ $objectTypeID,
+ [$objectID]
+ )[$objectID];
+ }
+
+ $labelIDs = $this->getLabelGroup()->getLabelIDs();
+ /** @var Label $label */
+ foreach (static::$loadedLabels[$objectTypeID][$objectID] as $label) {
+ if (in_array($label->labelID, $labelIDs)) {
+ $this->value($label->labelID);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function populate() {
+ parent::populate();
+
+ $this->getDocument()->getDataHandler()->add(new CustomFormFieldDataProcessor('label', function(IFormDocument $document, array $parameters) {
+ $value = $this->getValue();
+
+ // `-1` and `0` are special values that are irrlevent for saving
+ if ($value > 0) {
+ if (!isset($parameters[$this->getObjectProperty()])) {
+ $parameters[$this->getObjectProperty()] = [];
+ }
+
+ $parameters[$this->getObjectProperty()][$this->getLabelGroup()->groupID] = $value;
+ }
+
+ return $parameters;
+ }));
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readValue() {
+ if ($this->getDocument()->hasRequestData($this->getPrefixedId())) {
+ $this->value = intval($this->getDocument()->getRequestData($this->getPrefixedId()));
+ }
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function validate() {
+ if ($this->isRequired()) {
+ if ($this->value <= 0) {
+ $this->addValidationError(new FormFieldValidationError('empty'));
+ }
+ }
+ else if ($this->value > 0 && !in_array($this->value, $this->getLabelGroup()->getLabelIDs())) {
+ $this->addValidationError(new FormFieldValidationError(
+ 'invalidValue',
+ 'wcf.global.form.error.noValidSelection'
+ ));
+ }
+ }
+
+ /**
+ * Returns label group fields based for the given label groups using the given object property.
+ *
+ * The id of each form fields is `{$objectProperty}{$labelGroupID}`.
+ *
+ * @param string $objectType `com.woltlab.wcf.label.object` object type
+ * @param ViewableLabelGroup[] $labelGroups label groups for which label form fields are created
+ * @param string $objectProperty object property of form fields
+ * @return static[]
+ */
+ public static function createFields($objectType, array $labelGroups, $objectProperty = 'labelIDs') {
+ $formFields = [];
+ foreach ($labelGroups as $labelGroup) {
+ $formFields[] = static::create($objectProperty . $labelGroup->groupID)
+ ->objectProperty($objectProperty)
+ ->objectType($objectType)
+ ->required($labelGroup->forceSelection)
+ ->labelGroup($labelGroup);
+ }
+
+ return $formFields;
+ }
+}