From: Matthias Schmidt Date: Sun, 24 Mar 2019 12:17:53 +0000 (+0100) Subject: Add rating form field X-Git-Tag: 5.2.0_Alpha_1~185 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=54015e6ae8957612f482037d3115c39d6149f92a;p=GitHub%2FWoltLab%2FWCF.git Add rating form field See #2509 --- diff --git a/com.woltlab.wcf/templates/__ratingFormField.tpl b/com.woltlab.wcf/templates/__ratingFormField.tpl new file mode 100644 index 0000000000..0dc9978b88 --- /dev/null +++ b/com.woltlab.wcf/templates/__ratingFormField.tpl @@ -0,0 +1,30 @@ +{include file='__formFieldHeader'} + + + + + + +{include file='__formFieldFooter'} diff --git a/syncTemplates.json b/syncTemplates.json index 62a00302cd..e82e4f6871 100644 --- a/syncTemplates.json +++ b/syncTemplates.json @@ -29,6 +29,7 @@ "__numericFormField", "__pollOptionsFormField", "__radioButtonFormField", + "__ratingFormField", "__singleSelectionFormField", "__tabFormContainer", "__tabMenuFormContainer", diff --git a/wcfsetup/install/files/acp/templates/__ratingFormField.tpl b/wcfsetup/install/files/acp/templates/__ratingFormField.tpl new file mode 100644 index 0000000000..0dc9978b88 --- /dev/null +++ b/wcfsetup/install/files/acp/templates/__ratingFormField.tpl @@ -0,0 +1,30 @@ +{include file='__formFieldHeader'} + + + + + + +{include file='__formFieldFooter'} diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Rating.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Rating.js new file mode 100644 index 0000000000..441a036038 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Rating.js @@ -0,0 +1,158 @@ +/** + * Handles the JavaScript part of the rating form field. + * + * @author Matthias Schmidt + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Form/Builder/Field/Rating + * @since 5.2 + */ +define(['Dictionary'], function(Dictionary) { + "use strict"; + + /** + * @constructor + */ + function FormBuilderFieldRating(fieldId, value, activeCssClasses, defaultCssClasses) { + this.init(fieldId, value, activeCssClasses, defaultCssClasses); + }; + FormBuilderFieldRating.prototype = { + /** + * Initializes the rating form field. + * + * @param {string} fieldId id of the relevant form builder field + * @param {integer} value current value of the field + * @param {string[]} activeCssClasses CSS classes for the active state of rating elements + * @param {string[]} defaultCssClasses CSS classes for the default state of rating elements + */ + init: function(fieldId, value, activeCssClasses, defaultCssClasses) { + this._field = elBySel('#' + fieldId + 'Container'); + if (this._field === null) { + throw new Error("Unknown field with id '" + fieldId + "'"); + } + + this._input = elCreate('input'); + this._input.name = fieldId; + this._input.type = 'hidden'; + this._input.value = value; + this._field.appendChild(this._input); + + this._activeCssClasses = activeCssClasses; + this._defaultCssClasses = defaultCssClasses; + + this._ratingElements = new Dictionary(); + + var ratingList = elBySel('.ratingList', this._field); + ratingList.addEventListener('mouseleave', this._restoreRating.bind(this)); + + elBySelAll('li', ratingList, function(listItem) { + if (listItem.classList.contains('ratingMetaButton')) { + listItem.addEventListener('click', this._metaButtonClick.bind(this)); + listItem.addEventListener('mouseenter', this._restoreRating.bind(this)); + } + else { + this._ratingElements.set(~~elData(listItem, 'rating'), listItem); + + listItem.addEventListener('click', this._listItemClick.bind(this)); + listItem.addEventListener('mouseenter', this._listItemMouseEnter.bind(this)); + listItem.addEventListener('mouseleave', this._listItemMouseLeave.bind(this)); + } + }.bind(this)); + }, + + /** + * Saves the rating associated with the clicked rating element. + * + * @param {Event} event rating element `click` event + */ + _listItemClick: function(event) { + this._input.value = ~~elData(event.currentTarget, 'rating'); + + if (Environment.platform() !== 'desktop') { + this._restoreRating(); + } + }, + + /** + * Updates the rating UI when hovering over a rating element. + * + * @param {Event} event rating element `mouseenter` event + */ + _listItemMouseEnter: function(event) { + var currentRating = elData(event.currentTarget, 'rating'); + + this._ratingElements.forEach(function(ratingElement, rating) { + var icon = elByClass('icon', ratingElement)[0]; + + this._toggleIcon(icon, rating <= currentRating); + }.bind(this)); + }, + + /** + * Updates the rating UI when leaving a rating element by changing all rating elements + * to their default state. + */ + _listItemMouseLeave: function() { + this._ratingElements.forEach(function(ratingElement) { + var icon = elByClass('icon', ratingElement)[0]; + + this._toggleIcon(icon, false); + }.bind(this)); + }, + + /** + * Handles clicks on meta buttons. + * + * @param {Event} event meta button `click` event + */ + _metaButtonClick: function(event) { + if (elData(event.currentTarget, 'action') === 'removeRating') { + this._input.value = ''; + + this._listItemMouseLeave(); + } + }, + + /** + * Updates the rating UI by changing the rating elements to the stored rating state. + */ + _restoreRating: function() { + this._ratingElements.forEach(function(ratingElement, rating) { + var icon = elByClass('icon', ratingElement)[0]; + + this._toggleIcon(icon, rating <= this._input.value); + }.bind(this)); + }, + + /** + * Toggles the state of the given icon based on the given state parameter. + * + * @param {HTMLElement} icon toggled icon + * @param {boolean} active is `true` if icon will be changed to `active` state, otherwise changed to `default` state + */ + _toggleIcon: function(icon, active) { + active = active || false; + + if (active) { + for (var i = 0; i < this._defaultCssClasses.length; i++) { + icon.classList.remove(this._defaultCssClasses[i]); + } + + for (var i = 0; i < this._activeCssClasses.length; i++) { + icon.classList.add(this._activeCssClasses[i]); + } + } + else { + for (var i = 0; i < this._activeCssClasses.length; i++) { + icon.classList.remove(this._activeCssClasses[i]); + } + + for (var i = 0; i < this._defaultCssClasses.length; i++) { + icon.classList.add(this._defaultCssClasses[i]); + } + } + } + }; + + return FormBuilderFieldRating; +}); diff --git a/wcfsetup/install/files/lib/system/form/builder/field/RatingFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/RatingFormField.class.php new file mode 100644 index 0000000000..f254f8cca5 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/builder/field/RatingFormField.class.php @@ -0,0 +1,207 @@ + + * @package WoltLabSuite\Core\System\Form\Builder\Field + * @since 5.2 + */ +class RatingFormField extends AbstractFormField implements IImmutableFormField, IMaximumFormField, IMinimumFormField, INullableFormField { + use TDefaultIdFormField; + use TImmutableFormField; + use TMaximumFormField { + maximum as protected traitMaximum; + } + use TMinimumFormField { + minimum as protected traitMinimum; + } + use TNullableFormField; + + /** + * CSS classes for the active state of the rating elements + * @var string[] + */ + protected $activeCssClasses = ['fa-star', 'orange']; + + /** + * CSS classes for the default state of the rating elements + * @var string[] + */ + protected $defaultCssClasses = ['fa-star-o']; + + /** + * @inheritDoc + */ + protected $templateName = '__ratingFormField'; + + /** + * Creates a new instance of `RatingFormField`. + */ + public function __construct() { + $this->label('wcf.form.field.rating'); + $this->minimum(1); + $this->maximum(5); + } + + /** + * Sets the CSS classes for the active state of the rating elements. + * + * @param string[] $cssClasses active state CSS classes + * @return static this form field + * @throws \InvalidArgumentException if no or invalid CSS classes are given + */ + public function activeCssClasses(array $cssClasses) { + if (empty($cssClasses)) { + throw new \InvalidArgumentException("No css classes for active state given."); + } + + foreach ($cssClasses as $cssClass) { + static::validateClass($cssClass); + } + + $this->activeCssClasses = $cssClasses; + + return $this; + } + + /** + * Sets the CSS classes for the default state of the rating elements. + * + * @param string[] $cssClasses default state CSS classes + * @return static this form field + * @throws \InvalidArgumentException if no or invalid CSS classes are given + */ + public function defaultCssClasses(array $cssClasses) { + if (empty($cssClasses)) { + throw new \InvalidArgumentException("No css classes for default state given."); + } + + foreach ($cssClasses as $cssClass) { + static::validateClass($cssClass); + } + + $this->defaultCssClasses = $cssClasses; + + return $this; + } + + /** + * Returns the CSS classes for the active state of the rating elements. + * + * @return string[] + */ + public function getActiveCssClasses() { + return $this->activeCssClasses; + } + + /** + * Returns the CSS classes for the default state of the rating elements. + * + * @return string[] + */ + public function getDefaultCssClasses() { + return $this->defaultCssClasses; + } + + /** + * @inheritDoc + */ + public function getValue() { + if ($this->value === null && !$this->isNullable()) { + $this->value = $this->getMinimum(); + } + + return parent::getValue(); + } + + /** + * Returns the sorted list of possible ratings used to generate the form field's html code. + * + * @return integer[] + */ + public function getRatings() { + if (WCF::getLanguage()->get('wcf.global.pageDirection') === 'rtl') { + return range($this->maximum, $this->minimum, -1); + } + + return range($this->minimum, $this->maximum); + } + + /** + * @inheritDoc + */ + public function maximum($maximum = null) { + if ($maximum === null) { + throw new \InvalidArgumentException("Cannot unset maximum value."); + } + + return $this->traitMaximum($maximum); + } + + /** + * @inheritDoc + */ + public function minimum($minimum = null) { + if ($minimum === null) { + throw new \InvalidArgumentException("Cannot unset minimum value."); + } + + return $this->traitMinimum($minimum); + } + + /** + * @inheritDoc + */ + public function readValue() { + if ($this->getDocument()->hasRequestData($this->getPrefixedId())) { + $value = $this->getDocument()->getRequestData($this->getPrefixedId()); + + if ($this->isNullable() && $value === '') { + $this->value = null; + } + else { + $this->value = intval($value); + } + } + + return $this; + } + + /** + * @inheritDoc + */ + protected static function getDefaultId() { + return 'rating'; + } + + /** + * @inheritDoc + */ + public function validate() { + if ($this->getValue() !== null) { + if ($this->getValue() < $this->getMinimum() || $this->getValue() > $this->getMaximum()) { + $this->addValidationError(new FormFieldValidationError( + 'invalid', + 'wcf.global.form.error.noValidSelection' + )); + } + } + } +} diff --git a/wcfsetup/install/files/style/ui/rating.scss b/wcfsetup/install/files/style/ui/rating.scss new file mode 100644 index 0000000000..f4d943aa10 --- /dev/null +++ b/wcfsetup/install/files/style/ui/rating.scss @@ -0,0 +1,7 @@ +.ratingList { + font-size: 0; + + > li { + display: inline-block; + } +} diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 0746260271..3d76b58b0c 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -3874,6 +3874,9 @@ Dateianhänge: {$parentClass} erben.]]> {$segment}{else}(empty){/if} (segment {#$segmentNumber + 1}){/implode}.]]> + + + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index 7e69b33b04..6eb4788ebd 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -3820,6 +3820,9 @@ Attachments: {$segment}{else}(empty){/if} (segment {#$segmentNumber + 1}){/implode}.]]> + + +