From 712125889cef56d5f23881c8f2fbfc4bacb4d8e5 Mon Sep 17 00:00:00 2001 From: Matthias Schmidt Date: Sun, 3 Mar 2019 15:41:34 +0100 Subject: [PATCH] Add proper WYSIWYG support for form builder See #2852 --- com.woltlab.wcf/templates/__form.tpl | 25 +- .../templates/__pollOptionsFormField.tpl | 23 + .../__wysiwygAttachmentFormField.tpl | 77 +++ .../templates/__wysiwygFormField.tpl | 3 +- .../templates/__wysiwygPreviewFormButton.tpl | 20 + .../__wysiwygSmileyFormContainer.tpl | 11 + .../templates/__wysiwygSmileyFormField.tpl | 5 + .../__wysiwygTabMenuFormContainer.tpl | 8 + syncTemplates.json | 5 + .../install/files/acp/templates/__form.tpl | 25 +- .../acp/templates/__pollOptionsFormField.tpl | 23 + .../__wysiwygAttachmentFormField.tpl | 77 +++ .../acp/templates/__wysiwygFormField.tpl | 3 +- .../templates/__wysiwygPreviewFormButton.tpl | 20 + .../__wysiwygSmileyFormContainer.tpl | 11 + .../templates/__wysiwygSmileyFormField.tpl | 5 + .../__wysiwygTabMenuFormContainer.tpl | 8 + wcfsetup/install/files/js/WCF.Message.js | 1 - wcfsetup/install/files/js/WCF.Poll.js | 6 +- .../Form/Builder/Field/Dependency/Manager.js | 7 - .../form/builder/FormDocument.class.php | 2 +- .../form/builder/TWysiwygFormNode.class.php | 46 ++ .../WysiwygPreviewFormButton.class.php | 68 +++ .../wysiwyg/WysiwygFormContainer.class.php | 466 ++++++++++++++++++ .../WysiwygPollFormContainer.class.php | 353 +++++++++++++ .../WysiwygSmileyFormContainer.class.php | 70 +++ .../WysiwygTabMenuFormContainer.class.php | 39 ++ .../builder/field/CaptchaFormField.class.php | 4 +- ...lass.php => IObjectTypeFormNode.class.php} | 6 +- ...lass.php => TObjectTypeFormNode.class.php} | 4 +- .../builder/field/acl/AclFormField.class.php | 8 +- .../field/poll/PollOptionsFormField.class.php | 103 ++++ .../builder/field/tag/TagFormField.class.php | 8 +- .../WysiwygAttachmentFormField.class.php | 139 ++++++ .../{ => wysiwyg}/WysiwygFormField.class.php | 90 +++- .../wysiwyg/WysiwygSmileyFormField.class.php | 80 +++ .../install/files/style/ui/attachment.scss | 31 +- .../files/style/ui/tabMenuMessage.scss | 14 + 38 files changed, 1829 insertions(+), 65 deletions(-) create mode 100644 com.woltlab.wcf/templates/__pollOptionsFormField.tpl create mode 100644 com.woltlab.wcf/templates/__wysiwygAttachmentFormField.tpl create mode 100644 com.woltlab.wcf/templates/__wysiwygPreviewFormButton.tpl create mode 100644 com.woltlab.wcf/templates/__wysiwygSmileyFormContainer.tpl create mode 100644 com.woltlab.wcf/templates/__wysiwygSmileyFormField.tpl create mode 100644 com.woltlab.wcf/templates/__wysiwygTabMenuFormContainer.tpl create mode 100644 wcfsetup/install/files/acp/templates/__pollOptionsFormField.tpl create mode 100644 wcfsetup/install/files/acp/templates/__wysiwygAttachmentFormField.tpl create mode 100644 wcfsetup/install/files/acp/templates/__wysiwygPreviewFormButton.tpl create mode 100644 wcfsetup/install/files/acp/templates/__wysiwygSmileyFormContainer.tpl create mode 100644 wcfsetup/install/files/acp/templates/__wysiwygSmileyFormField.tpl create mode 100644 wcfsetup/install/files/acp/templates/__wysiwygTabMenuFormContainer.tpl create mode 100644 wcfsetup/install/files/lib/system/form/builder/TWysiwygFormNode.class.php create mode 100644 wcfsetup/install/files/lib/system/form/builder/button/wysiwyg/WysiwygPreviewFormButton.class.php create mode 100644 wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygFormContainer.class.php create mode 100644 wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygPollFormContainer.class.php create mode 100644 wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygSmileyFormContainer.class.php create mode 100644 wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygTabMenuFormContainer.class.php rename wcfsetup/install/files/lib/system/form/builder/field/{IObjectTypeFormField.class.php => IObjectTypeFormNode.class.php} (89%) rename wcfsetup/install/files/lib/system/form/builder/field/{TObjectTypeFormField.class.php => TObjectTypeFormNode.class.php} (95%) create mode 100644 wcfsetup/install/files/lib/system/form/builder/field/poll/PollOptionsFormField.class.php create mode 100644 wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygAttachmentFormField.class.php rename wcfsetup/install/files/lib/system/form/builder/field/{ => wysiwyg}/WysiwygFormField.class.php (59%) create mode 100644 wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygSmileyFormField.class.php diff --git a/com.woltlab.wcf/templates/__form.tpl b/com.woltlab.wcf/templates/__form.tpl index 08d72dccdf..0d9f06e45a 100644 --- a/com.woltlab.wcf/templates/__form.tpl +++ b/com.woltlab.wcf/templates/__form.tpl @@ -5,12 +5,19 @@ }); -
getClasses()|empty} class="{implode from=$form->getClasses() item='class' glue=' '}{$class}{/implode}"{/if}{* - *}{foreach from=$form->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{* -*}> +{if $form->isAjax()} +
getClasses()|empty} class="{implode from=$form->getClasses() item='class' glue=' '}{$class}{/implode}"{/if}{* + *}{foreach from=$form->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{* + *}> +{else} + getClasses()|empty} class="{implode from=$form->getClasses() item='class' glue=' '}{$class}{/implode}"{/if}{* + *}{foreach from=$form->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{* + *}> +{/if} {foreach from=$form item='child'} {if $child->isAvailable()} {@$child->getHtml()} @@ -26,7 +33,11 @@ {/if} {@SECURITY_TOKEN_INPUT_TAG} - +{if $form->isAjax()} +
+{else} + +{/if} diff --git a/com.woltlab.wcf/templates/__wysiwygAttachmentFormField.tpl b/com.woltlab.wcf/templates/__wysiwygAttachmentFormField.tpl new file mode 100644 index 0000000000..4a12ce6d37 --- /dev/null +++ b/com.woltlab.wcf/templates/__wysiwygAttachmentFormField.tpl @@ -0,0 +1,77 @@ +{include file='__formFieldHeader'} + + +
+ +{js application='wcf' file='WCF.Attachment' bundle='WCF.Combined'} + + + + +{include file='__formFieldFooter'} diff --git a/com.woltlab.wcf/templates/__wysiwygFormField.tpl b/com.woltlab.wcf/templates/__wysiwygFormField.tpl index 4d93d3e258..9336bc7c76 100644 --- a/com.woltlab.wcf/templates/__wysiwygFormField.tpl +++ b/com.woltlab.wcf/templates/__wysiwygFormField.tpl @@ -4,7 +4,8 @@ *}id="{@$field->getPrefixedId()}" {* *}name="{@$field->getPrefixedId()}" {* *}class="wysiwygTextarea" {* - *}data-disable-attachments="true"{* + *}data-disable-attachments="{if $field->supportsAttachments()}false{else}true{/if}"{* + *}data-support-mention="{if $field->supportsMentions()}true{else}false{/if}"{* *}{if $field->getAutosaveId() !== null}{* *} data-autosave="{@$field->getAutosaveId()}"{* *}{if $field->getLastEditTime() !== 0}{* diff --git a/com.woltlab.wcf/templates/__wysiwygPreviewFormButton.tpl b/com.woltlab.wcf/templates/__wysiwygPreviewFormButton.tpl new file mode 100644 index 0000000000..ffdcb291fa --- /dev/null +++ b/com.woltlab.wcf/templates/__wysiwygPreviewFormButton.tpl @@ -0,0 +1,20 @@ + + + diff --git a/com.woltlab.wcf/templates/__wysiwygSmileyFormContainer.tpl b/com.woltlab.wcf/templates/__wysiwygSmileyFormContainer.tpl new file mode 100644 index 0000000000..6f572aa277 --- /dev/null +++ b/com.woltlab.wcf/templates/__wysiwygSmileyFormContainer.tpl @@ -0,0 +1,11 @@ +{include file='__tabTabMenuFormContainer'} + + diff --git a/com.woltlab.wcf/templates/__wysiwygSmileyFormField.tpl b/com.woltlab.wcf/templates/__wysiwygSmileyFormField.tpl new file mode 100644 index 0000000000..422140bc01 --- /dev/null +++ b/com.woltlab.wcf/templates/__wysiwygSmileyFormField.tpl @@ -0,0 +1,5 @@ + diff --git a/com.woltlab.wcf/templates/__wysiwygTabMenuFormContainer.tpl b/com.woltlab.wcf/templates/__wysiwygTabMenuFormContainer.tpl new file mode 100644 index 0000000000..1b5ab16590 --- /dev/null +++ b/com.woltlab.wcf/templates/__wysiwygTabMenuFormContainer.tpl @@ -0,0 +1,8 @@ +{include file='__tabMenuFormContainer'} + +{js application='wcf' file='WCF.Message' bundle='WCF.Combined'} + diff --git a/syncTemplates.json b/syncTemplates.json index 03b7378f65..28fabe23a5 100644 --- a/syncTemplates.json +++ b/syncTemplates.json @@ -28,6 +28,7 @@ "__multipleSelectionFormField", "__nonEmptyFormFieldDependency", "__numericFormField", + "__pollOptionsFormField", "__radioButtonFormField", "__singleSelectionFormField", "__tabFormContainer", @@ -39,8 +40,12 @@ "__userFormField", "__usernameFormField", "__valueFormFieldDependency", + "__wysiwygAttachmentFormField", "__wysiwygCmsToolbar", "__wysiwygFormField", + "__wysiwygPreviewFormButton", + "__wysiwygSmileyFormContainer", + "__wysiwygSmileyFormField", "aclPermissionJavaScript", "articleAdd", "articleAddDialog", diff --git a/wcfsetup/install/files/acp/templates/__form.tpl b/wcfsetup/install/files/acp/templates/__form.tpl index 08d72dccdf..0d9f06e45a 100644 --- a/wcfsetup/install/files/acp/templates/__form.tpl +++ b/wcfsetup/install/files/acp/templates/__form.tpl @@ -5,12 +5,19 @@ }); -
getClasses()|empty} class="{implode from=$form->getClasses() item='class' glue=' '}{$class}{/implode}"{/if}{* - *}{foreach from=$form->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{* -*}> +{if $form->isAjax()} +
getClasses()|empty} class="{implode from=$form->getClasses() item='class' glue=' '}{$class}{/implode}"{/if}{* + *}{foreach from=$form->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{* + *}> +{else} + getClasses()|empty} class="{implode from=$form->getClasses() item='class' glue=' '}{$class}{/implode}"{/if}{* + *}{foreach from=$form->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{* + *}> +{/if} {foreach from=$form item='child'} {if $child->isAvailable()} {@$child->getHtml()} @@ -26,7 +33,11 @@ {/if} {@SECURITY_TOKEN_INPUT_TAG} - +{if $form->isAjax()} +
+{else} + +{/if} diff --git a/wcfsetup/install/files/acp/templates/__wysiwygAttachmentFormField.tpl b/wcfsetup/install/files/acp/templates/__wysiwygAttachmentFormField.tpl new file mode 100644 index 0000000000..4a12ce6d37 --- /dev/null +++ b/wcfsetup/install/files/acp/templates/__wysiwygAttachmentFormField.tpl @@ -0,0 +1,77 @@ +{include file='__formFieldHeader'} + + +
+ +{js application='wcf' file='WCF.Attachment' bundle='WCF.Combined'} + + + + +{include file='__formFieldFooter'} diff --git a/wcfsetup/install/files/acp/templates/__wysiwygFormField.tpl b/wcfsetup/install/files/acp/templates/__wysiwygFormField.tpl index 4d93d3e258..9336bc7c76 100644 --- a/wcfsetup/install/files/acp/templates/__wysiwygFormField.tpl +++ b/wcfsetup/install/files/acp/templates/__wysiwygFormField.tpl @@ -4,7 +4,8 @@ *}id="{@$field->getPrefixedId()}" {* *}name="{@$field->getPrefixedId()}" {* *}class="wysiwygTextarea" {* - *}data-disable-attachments="true"{* + *}data-disable-attachments="{if $field->supportsAttachments()}false{else}true{/if}"{* + *}data-support-mention="{if $field->supportsMentions()}true{else}false{/if}"{* *}{if $field->getAutosaveId() !== null}{* *} data-autosave="{@$field->getAutosaveId()}"{* *}{if $field->getLastEditTime() !== 0}{* diff --git a/wcfsetup/install/files/acp/templates/__wysiwygPreviewFormButton.tpl b/wcfsetup/install/files/acp/templates/__wysiwygPreviewFormButton.tpl new file mode 100644 index 0000000000..ffdcb291fa --- /dev/null +++ b/wcfsetup/install/files/acp/templates/__wysiwygPreviewFormButton.tpl @@ -0,0 +1,20 @@ + + + diff --git a/wcfsetup/install/files/acp/templates/__wysiwygSmileyFormContainer.tpl b/wcfsetup/install/files/acp/templates/__wysiwygSmileyFormContainer.tpl new file mode 100644 index 0000000000..6f572aa277 --- /dev/null +++ b/wcfsetup/install/files/acp/templates/__wysiwygSmileyFormContainer.tpl @@ -0,0 +1,11 @@ +{include file='__tabTabMenuFormContainer'} + + diff --git a/wcfsetup/install/files/acp/templates/__wysiwygSmileyFormField.tpl b/wcfsetup/install/files/acp/templates/__wysiwygSmileyFormField.tpl new file mode 100644 index 0000000000..422140bc01 --- /dev/null +++ b/wcfsetup/install/files/acp/templates/__wysiwygSmileyFormField.tpl @@ -0,0 +1,5 @@ + diff --git a/wcfsetup/install/files/acp/templates/__wysiwygTabMenuFormContainer.tpl b/wcfsetup/install/files/acp/templates/__wysiwygTabMenuFormContainer.tpl new file mode 100644 index 0000000000..b0a8e2d5ba --- /dev/null +++ b/wcfsetup/install/files/acp/templates/__wysiwygTabMenuFormContainer.tpl @@ -0,0 +1,8 @@ +{include file='__tabMenuFormContainer'} + +{js application='wcf' file='WCF.Message' bundle='WCF.Combined'} + diff --git a/wcfsetup/install/files/js/WCF.Message.js b/wcfsetup/install/files/js/WCF.Message.js index f90583296f..a39bfc78d2 100644 --- a/wcfsetup/install/files/js/WCF.Message.js +++ b/wcfsetup/install/files/js/WCF.Message.js @@ -2368,7 +2368,6 @@ $.widget('wcf.messageTabMenu', { if ($name === undefined) { $name = $tab.wcfIdentify(); - console.debug("[wcf.messageTabMenu] Missing name attribute, assuming generic ID '" + $name + "'"); } } diff --git a/wcfsetup/install/files/js/WCF.Poll.js b/wcfsetup/install/files/js/WCF.Poll.js index f04237ec38..e8d8cd1fcc 100644 --- a/wcfsetup/install/files/js/WCF.Poll.js +++ b/wcfsetup/install/files/js/WCF.Poll.js @@ -49,10 +49,12 @@ if (COMPILER_TARGET_DEFAULT) { * @param {int} maxOptions * @param {string} editorId */ - init: function (containerID, optionList, maxOptions, editorId) { + init: function (containerID, optionList, maxOptions, editorId, fieldName) { this._count = 0; this._maxOptions = maxOptions || -1; this._container = $('#' + containerID).children('ol:eq(0)'); + this._fieldName = fieldName || 'pollOptions'; + if (!this._container.length) { console.debug("[WCF.Poll.Management] Invalid container id given, aborting."); return; @@ -223,7 +225,7 @@ if (COMPILER_TARGET_DEFAULT) { var $formSubmit = this._container.parents('form').find('.formSubmit'); for (var $i = 0, $length = $options.length; $i < $length; $i++) { - $('').val($options[$i]).appendTo($formSubmit); + $('').val($options[$i]).appendTo($formSubmit); } } } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.js index cb9fb4887f..84e461dcab 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.js @@ -300,13 +300,6 @@ define(['Dictionary', 'Dom/ChangeListener', 'EventHandler', 'List', 'Dom/Travers if (form === null) { throw new Error("Unknown element with id '" + formId + "'"); } - if (form.tagName !== 'FORM') { - var dialogContent = DomTraverse.parentByClass(form, 'dialogContent'); - - if (dialogContent === null) { - throw new Error("Element with id '" + formId + "' is no form."); - } - } if (_forms.has(form)) { throw new Error("Form with id '" + formId + "' has already been registered."); diff --git a/wcfsetup/install/files/lib/system/form/builder/FormDocument.class.php b/wcfsetup/install/files/lib/system/form/builder/FormDocument.class.php index a96ecadb8b..c137720ab7 100644 --- a/wcfsetup/install/files/lib/system/form/builder/FormDocument.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/FormDocument.class.php @@ -47,7 +47,7 @@ class FormDocument implements IFormDocument { * and `false` otherwise * @var boolean */ - protected $ajax; + protected $ajax = false; /** * buttons registered for this form document diff --git a/wcfsetup/install/files/lib/system/form/builder/TWysiwygFormNode.class.php b/wcfsetup/install/files/lib/system/form/builder/TWysiwygFormNode.class.php new file mode 100644 index 0000000000..a0f4a85da2 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/builder/TWysiwygFormNode.class.php @@ -0,0 +1,46 @@ + + * @package WoltLabSuite\Core\System\Form\Builder\Container\Wysiwyg + * @since 5.2 + */ +trait TWysiwygFormNode { + /** + * id of the related `WysiwygFormField` form field + * @var string + */ + protected $wysiwygId; + + /** + * Returns id of the related `WysiwygFormField` form field. + * + * @return string + * @throws \BadMethodCallException if the id of the related `WysiwygFormField` form field is unknown + */ + public function getWysiwygId() { + if ($this->wysiwygId === null) { + throw new \BadMethodCallException("The id of the related 'WysiwygFormField' form field is unknown."); + } + + return $this->wysiwygId; + } + + /** + * Sets the id of the related `WysiwygFormField` form field and returns this field. + * + * @param string $wysiwygId + * @return static this field + */ + public function wysiwygId($wysiwygId) { + $this->wysiwygId = $wysiwygId; + + return $this; + } +} diff --git a/wcfsetup/install/files/lib/system/form/builder/button/wysiwyg/WysiwygPreviewFormButton.class.php b/wcfsetup/install/files/lib/system/form/builder/button/wysiwyg/WysiwygPreviewFormButton.class.php new file mode 100644 index 0000000000..ce960c089e --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/builder/button/wysiwyg/WysiwygPreviewFormButton.class.php @@ -0,0 +1,68 @@ + + * @package WoltLabSuite\Core\System\Form\Builder\Button\Wysiwyg + * @since 5.2 + */ +class WysiwygPreviewFormButton extends FormButton implements IObjectTypeFormNode { + use TObjectTypeFormNode; + use TWysiwygFormNode; + + /** + * id of the previewed message + * @var integer + */ + protected $objectId = 0; + + /** + * @inheritDoc + */ + protected $templateName = '__wysiwygPreviewFormButton'; + + /** + * Creates a new instance of `WysiwygPreviewFormButton`. + */ + public function __construct() { + $this->label('wcf.global.button.preview'); + } + + /** + * Returns the id of the previewed message. + * + * By default, `0` is returned. + * + * @return integer + */ + public function getObjectId() { + return $this->objectId; + } + + /** + * @inheritDoc + */ + public function getObjectTypeDefinition() { + return 'com.woltlab.wcf.message'; + } + + /** + * Sets the id of the previewed message and returns this button. + * + * @param integer $objectId id of previewed message + * @return WysiwygPreviewFormButton this button + */ + public function objectId($objectId) { + $this->objectId = $objectId; + + return $this; + } +} diff --git a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygFormContainer.class.php new file mode 100644 index 0000000000..70eb0ea400 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygFormContainer.class.php @@ -0,0 +1,466 @@ + + * @package WoltLabSuite\Core\System\Form\Builder\Container\Wysiwyg + * @since 5.2 + */ +class WysiwygFormContainer extends FormContainer { + use TWysiwygFormNode; + + /** + * attachment form field + * @var WysiwygAttachmentFormField + */ + protected $attachmentField; + + /** + * attachment-related data used to create an `AttachmentHandler` object for the attachment + * form field + * @var null|array + */ + protected $attachmentData; + + /** + * name of the relevant message object type + * @var string + */ + protected $messageObjectType; + + /** + * id of the edited object + * @var integer + */ + protected $objectId; + + /** + * pre-select attribute of the tab menu + * @var string + */ + protected $preselect = 'true'; + + /** + * name of the relevant poll object type + * @var string + */ + protected $pollObjectType; + + /** + * poll form container + * @var WysiwygPollFormContainer + */ + protected $pollContainer; + + /** + * settings form container + * @var FormContainer + */ + protected $settingsContainer; + + /** + * setting nodes that will be added to the settings container when it is created + * @var IFormNode[] + */ + protected $settingsNodes = []; + + /** + * form container for smiley categories + * @var WysiwygSmileyFormContainer + */ + protected $smiliesContainer; + + /** + * is `true` if the wysiwyg form field should support mentions, otherwise `false` + * @var boolean + */ + protected $supportMentions = false; + + /** + * is `true` if smilies are supported for this container, otherwise `false` + * @var boolean + */ + protected $supportSmilies = false; + + /** + * actual wysiwyg form field + * @var WysiwygFormNode + */ + protected $wysiwygField; + + /** + * @inheritDoc + */ + public static function create($id) { + // the actual id is used for the form field containing the text + return parent::create($id . 'Container'); + } + + /** + * Adds a node that will be appended to the settings form container when it is built and + * returns this container. + * + * @param IFormNode $settingsNode added settings node + * @return WysiwygFormContainer this form field container + */ + public function addSettingsNode(IFormNode $settingsNode) { + if ($this->settingsContainer !== null) { + // if settings container has already been created, add it directly + $this->settingsContainer->appendChild($settingsNode); + } + else { + $this->settingsNodes[] = $settingsNode; + } + + return $this; + } + + /** + * Adds nodes that will be appended to the settings form container when it is built and + * returns this container. + * + * @param IFormNode[] $settingsNodes added settings nodes + * @return WysiwygFormContainer this form field container + */ + public function addSettingsNodes(array $settingsNodes) { + foreach ($settingsNodes as $settingsNode) { + $this->addSettingsNode($settingsNode); + } + + return $this; + } + + /** + * @inheritDoc + */ + public function loadValuesFromObject(IStorableObject $object) { + $this->objectId = $object->getObjectID(); + + return parent::loadValuesFromObject($object); + } + + /** + * Sets the attachment-related data used to create an `AttachmentHandler` object for the + * attachment form field. If no attachment data is set, attachments are not supported. + * + * By default, no attachment data is set. + * + * @param null|string $objectType name of attachment object type or `null` to unset previous attachment data + * @param integer $parentObjectID id of the parent of the object the attachments belong to or `0` if no such parent exists + * @return WysiwygFormContainer this form container + * @throws \BadMethodCallException if the attachment form field has already been initialized + */ + public function attachmentData($objectType = null, $parentObjectID = 0) { + if ($this->attachmentField !== null) { + throw new \BadMethodCallException("The attachment form field has already been initialized. Use the atatchment form field directly to manipulate attachment data."); + } + + if ($objectType === null) { + $this->attachmentData = null; + } + else { + if (ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.attachment.objectType', $objectType) === null) { + throw new \InvalidArgumentException("Unknown attachment object type '{$objectType}'."); + } + + $this->attachmentData = [ + 'objectType' => $objectType, + 'parentObjectID' => $parentObjectID + ]; + } + + return $this; + } + + /** + * Returns the form field handling attachments. + * + * @return WysiwygAttachmentFormField + * @throws \BadMethodCallException if the form field container has not been populated yet/form has not been built yet + */ + public function getAttachmentField() { + if ($this->attachmentField === null) { + throw new \BadMethodCallException("Wysiwyg form field can only be requested after the form has been built."); + } + + return $this->attachmentField; + } + + /** + * Returns the id of the edited object or `0` if no object is edited. + * + * @return integer + */ + public function getObjectId() { + return $this->objectId; + } + + /** + * Returns the value of the wysiwyg tab menu's `data-preselect` attribute used to determine + * which tab is preselected. + * + * By default, `'true'` is returned which is used to pre-select the first tab. + * + * @return string + */ + public function getPreselect() { + return $this->preselect; + } + + /** + * Returns the wysiwyg form container with all poll-related fields. + * + * @return WysiwygPollFormContainer + * @throws \BadMethodCallException if the form field container has not been populated yet/form has not been built yet + */ + public function getPollContainer() { + if ($this->pollContainer === null) { + throw new \BadMethodCallException("Wysiwyg form field can only be requested after the form has been built."); + } + + return $this->pollContainer; + } + + /** + * Returns the form container for all settings-related fields. + * + * @return FormContainer + * @throws \BadMethodCallException if the form field container has not been populated yet/form has not been built yet + */ + public function getSettingsContainer() { + if ($this->settingsContainer === null) { + throw new \BadMethodCallException("Wysiwyg form field can only be requested after the form has been built."); + } + + return $this->settingsContainer; + } + + /** + * Returns the form container for smiley categories. + * + * @return WysiwygSmileyFormContainer + * @throws \BadMethodCallException if the form field container has not been populated yet/form has not been built yet + */ + public function getSmiliesContainer() { + if ($this->smiliesContainer === null) { + throw new \BadMethodCallException("Smilies form field container can only be requested after the form has been built."); + } + + return $this->smiliesContainer; + } + + /** + * Returns the wysiwyg form field handling the actual text. + * + * @return WysiwygFormField + * @throws \BadMethodCallException if the form field container has not been populated yet/form has not been built yet + */ + public function getWysiwygField() { + if ($this->wysiwygField === null) { + throw new \BadMethodCallException("Wysiwyg form field can only be requested after the form has been built."); + } + + return $this->wysiwygField; + } + + /** + * @inheritDoc + */ + public function id($id) { + $this->wysiwygId(substr($id, 0, -strlen('Container'))); + + return parent::id($id); + } + + /** + * Sets the message object type used by the wysiwyg form field. + * + * @param string $messageObjectType message object type for wysiwyg form field + * @return WysiwygFormContainer this container + * @throws \InvalidArgumentException if the given string is no message object type + */ + public function messageObjectType($messageObjectType) { + if (ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.message', $messageObjectType) === null) { + throw new \InvalidArgumentException("Unknown message object type '{$messageObjectType}'."); + } + + if ($this->wysiwygField !== null) { + $this->wysiwygField->objectType($messageObjectType); + } + else { + $this->messageObjectType = $messageObjectType; + } + + return $this; + } + + /** + * Sets the poll object type used by the poll form field container. + * + * By default, no poll object type is set, thus the poll form field container is not available. + * + * @param string $pollObjectType poll object type for wysiwyg form field + * @return WysiwygFormContainer this container + * @throws \InvalidArgumentException if the given string is no poll object type + */ + public function pollObjectType($pollObjectType = true) { + if (ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.poll', $pollObjectType) === null) { + throw new \InvalidArgumentException("Unknown poll object type '{$pollObjectType}'."); + } + + if ($this->pollContainer !== null) { + $this->pollContainer->objectType($pollObjectType); + } + else { + $this->pollObjectType = $pollObjectType; + } + + return $this; + } + + /** + * @inheritDoc + */ + public function populate() { + parent::populate(); + + $this->wysiwygField = WysiwygFormField::create($this->wysiwygId) + ->objectType($this->messageObjectType) + ->supportAttachments($this->attachmentData !== null) + ->supportMentions($this->supportMentions); + $this->smiliesContainer = WysiwygSmileyFormContainer::create($this->wysiwygId . 'SmiliesTab') + ->wysiwygId($this->getWysiwygId()) + ->label('wcf.message.smilies') + ->available($this->supportSmilies); + $this->attachmentField = WysiwygAttachmentFormField::create($this->wysiwygId . 'Attachments') + ->wysiwygId($this->getWysiwygId()); + $this->settingsContainer = FormContainer::create($this->wysiwygId . 'SettingsContainer') + ->appendChildren($this->settingsNodes); + $this->pollContainer = WysiwygPollFormContainer::create($this->wysiwygId . 'PollContainer') + ->wysiwygId($this->getWysiwygId()); + if ($this->pollObjectType) { + $this->pollContainer->objectType($this->pollObjectType); + } + + $this->appendChildren([ + $this->wysiwygField, + WysiwygTabMenuFormContainer::create($this->wysiwygId . 'Tabs') + ->attribute('data-preselect', $this->getPreselect()) + ->attribute('data-wysiwyg-container-id', $this->wysiwygId) + ->useAnchors(false) + ->appendChildren([ + $this->smiliesContainer, + + TabFormContainer::create($this->wysiwygId . 'AttachmentsTab') + ->addClass('formAttachmentContent') + ->label('wcf.attachment.attachments') + ->appendChild( + FormContainer::create($this->wysiwygId . 'AttachmentsContainer') + ->appendChild($this->attachmentField) + ), + + TabFormContainer::create($this->wysiwygId . 'SettingsTab') + ->label('wcf.message.settings') + ->appendChild($this->settingsContainer) + ->available(MODULE_SMILEY), + + TabFormContainer::create($this->wysiwygId . 'PollTab') + ->label('wcf.poll.management') + ->appendChild($this->pollContainer) + ]) + ]); + + if ($this->attachmentData !== null) { + $this->attachmentField->attachmentHandler( + // the temporary hash may not be empty (at the same time as the + // object id) and it will be changed anyway by the called method + new AttachmentHandler( + $this->attachmentData['objectType'], + $this->getObjectId(), + '.', + $this->attachmentData['parentObjectID'] + ) + ); + } + + $this->getDocument()->addButton( + WysiwygPreviewFormButton::create($this->getWysiwygId() . 'PreviewButton') + ->objectType($this->messageObjectType) + ->wysiwygId($this->getWysiwygId()) + ->objectId($this->getObjectId()) + ); + + EventHandler::getInstance()->fireAction($this, 'populate'); + } + + /** + * Sets the value of the wysiwyg tab menu's `data-preselect` attribute used to determine which + * tab is preselected. + * + * @param string $preselect id of preselected tab, `'true'` for first tab, or non-existing id for no preselected tab + * @return WysiwygFormContainer + */ + public function preselect($preselect = 'true') { + $this->preselect = $preselect; + + return $this; + } + + /** + * Sets if mentions are supported by the editor field and returns this form container. + * + * By default, mentions are not supported. + * + * @param boolean $supportMention + * @return WysiwygFormContainer this form container + * @throws \BadMethodCallException if the wysiwyg form field has already been initialized + */ + public function supportMentions($supportMentions = true) { + if ($this->wysiwygField !== null) { + throw new \BadMethodCallException("The wysiwyg form field has already been initialized. Use the wysiwyg form field directly to manipulate mention support."); + } + + $this->supportMentions = $supportMentions; + + return $this; + } + + /** + * Sets if smilies are supported for this form container and returns this form container. + * + * By default, smilies are not supported. + * + * @param boolean $supportSmilies + * @return WysiwygFormContainer this form container + * @throws \BadMethodCallException if the poll container has already been initialized + */ + public function supportSmilies($supportSmilies = true) { + if ($this->smiliesContainer !== null) { + throw new \BadMethodCallException("The smilies form container has already been initialized. Use the smilies container directly to manipulate poll support."); + } + + $this->supportSmilies = $supportSmilies; + + return $this; + } +} diff --git a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygPollFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygPollFormContainer.class.php new file mode 100644 index 0000000000..e658c460db --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygPollFormContainer.class.php @@ -0,0 +1,353 @@ + + * @package WoltLabSuite\Core\System\Form\Builder\Container\Wysiwyg + * @since 5.2 + */ +class WysiwygPollFormContainer extends FormContainer implements IObjectTypeFormNode { + use TObjectTypeFormNode; + use TWysiwygFormNode; + + /** + * form field to set the end date of the poll + * @var DateFormField + */ + protected $endTimeField; + + /** + * form field to set if votes can be changed + * @var BooleanFormField + */ + protected $isChangeableField; + + /** + * form field to set if the poll results are public + * @var BooleanFormField + */ + protected $isPublicField; + + /** + * form field to set the maximum number of votes per user + * @var IntegerFormField + */ + protected $maxVotesField; + + /** + * form field to set the available poll answers + * @var PollOptionsFormField + */ + protected $optionsField; + + /** + * poll belonging to the edited object + * @var null|Poll + */ + protected $poll; + + /** + * form field to set the question of the poll + * @var TextFormField + */ + protected $questionField; + + /** + * form field to set whether viewing the poll results requires voting + * @var BooleanFormField + */ + protected $resultsRequireVoteField; + + /** + * form field to set whether the poll answers are sorted by votes when viewing the results + * @var BooleanFormField + */ + protected $sortByVotesField; + + const FIELD_NAMES = [ + 'endTime', + 'isChangeable', + 'isPublic', + 'maxVotes', + 'options', + 'question', + 'resultsRequireVote', + 'sortByVotes' + ]; + + /** + * Returns form field to set the end date of the poll. + * + * @return DateFormField + * @throws \BadMethodCallException if the form field has not been populated yet/form has not been built yet + */ + public function getEndTimeField() { + if ($this->endTimeField === null) { + throw new \BadMethodCallException("Poll form field can only be requested after the form has been built."); + } + + return $this->endTimeField; + } + + /** + * Returns the form field to set if votes can be changed. + * + * @return BooleanFormField + * @throws \BadMethodCallException if the form field has not been populated yet/form has not been built yet + */ + public function getIsChangeableField() { + if ($this->isChangeableField === null) { + throw new \BadMethodCallException("Poll form field can only be requested after the form has been built."); + } + + return $this->isChangeableField; + } + + /** + * Returns the form field to set if the poll results are public. + * + * @return BooleanFormField + * @throws \BadMethodCallException if the form field has not been populated yet/form has not been built yet + */ + public function getIsPublicField() { + if ($this->isPublicField === null) { + throw new \BadMethodCallException("Poll form field can only be requested after the form has been built."); + } + + return $this->isPublicField; + } + + /** + * Returns the form field to set the maximum number of votes per user. + * + * @return IntegerFormField + * @throws \BadMethodCallException if the form field has not been populated yet/form has not been built yet + */ + public function getMaxVotesField() { + if ($this->maxVotesField === null) { + throw new \BadMethodCallException("Poll form field can only be requested after the form has been built."); + } + + return $this->maxVotesField; + } + + /** + * @inheritDoc + */ + public function getObjectTypeDefinition() { + return 'com.woltlab.wcf.poll'; + } + + /** + * Returns the form field to set the available poll answers. + * + * @return PollOptionsFormField + * @throws \BadMethodCallException if the form field has not been populated yet/form has not been built yet + */ + public function getOptionsField() { + if ($this->optionsField === null) { + throw new \BadMethodCallException("Poll form field can only be requested after the form has been built."); + } + + return $this->optionsField; + } + + /** + * Returns the form field to set the question of the poll. + * + * @return TextFormField + * @throws \BadMethodCallException if the form field has not been populated yet/form has not been built yet + */ + public function getQuestionField() { + if ($this->questionField === null) { + throw new \BadMethodCallException("Poll form field can only be requested after the form has been built."); + } + + return $this->questionField; + } + + /** + * Returns the form field to set whether viewing the poll results requires voting. + * + * @return BooleanFormField + * @throws \BadMethodCallException if the form field has not been populated yet/form has not been built yet + */ + public function getResultsRequireVoteField() { + if ($this->resultsRequireVoteField === null) { + throw new \BadMethodCallException("Poll form field can only be requested after the form has been built."); + } + + return $this->resultsRequireVoteField; + } + + /** + * Returns the form field to set whether the poll answers are sorted by votes when viewing + * the results. + * + * @return BooleanFormField + * @throws \BadMethodCallException if the form field has not been populated yet/form has not been built yet + */ + public function getSortByVotesField() { + if ($this->sortByVotesField === null) { + throw new \BadMethodCallException("Poll form field can only be requested after the form has been built."); + } + + return $this->sortByVotesField; + } + + /** + * @inheritDoc + */ + public function isAvailable() { + return parent::isAvailable() && $this->objectType !== null; + } + + /** + * @inheritDoc + */ + public function loadValuesFromObject(IStorableObject $object) { + if ($object instanceof IPollContainer && $object->getPollID() !== null) { + $this->poll = new Poll($object->getPollID()); + if (!$this->poll->pollID) { + $this->poll = null; + } + else { + // `isPublic` cannot be changed when editing polls + $this->getIsPublicField()->isAvailable(false); + } + + $this->getQuestionField()->value($this->poll->question); + $this->getOptionsField()->value($this->poll->getOptions()); + $this->getEndTimeField()->value($this->poll->endTime); + $this->getMaxVotesField()->value($this->poll->maxVotes); + $this->getIsChangeableField()->value($this->poll->isChangeable); + $this->getIsPublicField()->value($this->poll->isPublic); + $this->getResultsRequireVoteField()->value($this->poll->resultsRequireVote); + $this->getSortByVotesField()->value($this->poll->sortByVotes); + } + + return parent::loadValuesFromObject($object); + } + + /** + * @inheritDoc + */ + public function populate() { + parent::populate(); + + $id = $this->wysiwygId . 'Poll'; + + // add data handler to group poll data into a sub-array of parameters + $this->getDocument()->getDataHandler()->add(new CustomFormFieldDataProcessor($id, function(IFormDocument $document, array $parameters) use($id) { + if (!$this->isAvailable()) { + return $parameters; + } + + $wysiwygId = $this->getWysiwygId(); + + foreach (self::FIELD_NAMES as $fieldName) { + $parameters[$wysiwygId . '_pollData'][$fieldName] = $parameters['data'][$id . '_' . $fieldName]; + unset($parameters['data'][$id . '_' . $fieldName]); + } + + // this will always add a poll array to the parameters but + // `PollManager::savePoll()` is capable of correctly detecting + // when, based on the given data, nothing has to be done + + return $parameters; + })); + + $this->questionField = TextFormField::create($id . '_question') + ->label('wcf.poll.question') + ->maximumLength(255); + + // if either options or question is given, the other must also be given + $this->optionsField = PollOptionsFormField::create($id . '_options') + ->wysiwygId($this->getWysiwygId()) + ->addValidator(new FormFieldValidator('empty', function(PollOptionsFormField $formField) use ($id) { + /** @var TextFormField $questionFormField */ + $questionFormField = $formField->getDocument()->getNodeById($id . '_question'); + + if (empty($formField->getValue()) && $questionFormField->getValue() !== '') { + $formField->addValidationError(new FormFieldValidationError('empty')); + } + else if (!empty($formField->getValue()) && $questionFormField->getValue() === '') { + $questionFormField->addValidationError(new FormFieldValidationError('empty')); + } + })); + + $this->endTimeField = DateFormField::create($id . '_endTime') + ->label('wcf.poll.endTime') + ->supportTime() + ->addValidator(new FormFieldValidator('futureTime', function(DateFormField $formField) use ($id) { + $endTime = $formField->getSaveValue(); + + if ($endTime && $endTime <= TIME_NOW) { + if ($this->poll === null || $this->poll->endTime >= TIME_NOW) { + $formField->addValidationError(new FormFieldValidationError( + 'invalid', + 'wcf.poll.endTime.error.invalid' + )); + } + } + })); + + $this->maxVotesField = IntegerFormField::create($id . '_maxVotes') + ->label('wcf.poll.maxVotes') + ->minimum(1) + ->maximum(POLL_MAX_OPTIONS) + ->value(1); + + $this->isChangeableField = BooleanFormField::create($id . '_isChangeable') + ->label('wcf.poll.isChangeable'); + + /** @var IPollHandler $pollHandler */ + $pollHandler = null; + if ($this->objectType !== null) { + $pollHandler = $this->getObjectType()->getProcessor(); + } + + $this->isPublicField = BooleanFormField::create($id . '_isPublic') + ->label('wcf.poll.isPublic') + ->available($pollHandler !== null && $pollHandler->canStartPublicPoll()); + + $this->resultsRequireVoteField = BooleanFormField::create($id . '_resultsRequireVote') + ->label('wcf.poll.resultsRequireVote') + ->description('wcf.poll.resultsRequireVote.description'); + + $this->sortByVotesField = BooleanFormField::create($id . '_sortByVotes') + ->label('wcf.poll.sortByVotes'); + + $this->appendChildren([ + $this->getQuestionField(), + $this->getOptionsField(), + $this->getEndTimeField(), + $this->getMaxVotesField(), + $this->getIsChangeableField(), + $this->getIsPublicField(), + $this->getResultsRequireVoteField(), + $this->getSortByVotesField() + ]); + } +} diff --git a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygSmileyFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygSmileyFormContainer.class.php new file mode 100644 index 0000000000..f3a6406f0d --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygSmileyFormContainer.class.php @@ -0,0 +1,70 @@ + + * @package WoltLabSuite\Core\System\Form\Builder\Container\Wysiwyg + * @since 5.2 + */ +class WysiwygSmileyFormContainer extends TabTabMenuFormContainer { + use TWysiwygFormNode; + + /** + * name of container template + * @var string + */ + protected $templateName = '__wysiwygSmileyFormContainer'; + + /** + * Creates a new instance of `WysiwygSmileyFormContainer`. + */ + public function __construct() { + $this->attribute('data-preselect', 'true') + ->attribute('data-collapsible', 'false') + ->useAnchors(false); + } + + /** + * @inheritDoc + */ + public function populate() { + parent::populate(); + + $smileyCategories = SmileyCache::getInstance()->getCategories(); + + foreach ($smileyCategories as $smileyCategory) { + $smileyCategory->loadSmilies(); + if (count($smileyCategory) > 0) { + $this->appendChild( + TabFormContainer::create($this->getId() . '_smileyCategoryTab' . $smileyCategory->categoryID) + ->label(StringUtil::encodeHTML($smileyCategory->getTitle())) + ->removeClass('tabMenuContent') + ->addClass('messageTabMenuContent') + ->appendChild( + FormContainer::create($this->getId() . '_smileyCategoryContainer' . $smileyCategory->categoryID) + ->removeClass('section') + ->appendChild( + WysiwygSmileyFormField::create($this->getId() . '_smileyCategory' . $smileyCategory->categoryID) + ->smilies(SmileyCache::getInstance()->getCategorySmilies($smileyCategory->categoryID ?: null)) + ) + ) + ); + } + } + + if (count($this->children()) > 1) { + $this->addClass('messageTabMenu'); + } + } +} diff --git a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygTabMenuFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygTabMenuFormContainer.class.php new file mode 100644 index 0000000000..71aec9e682 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygTabMenuFormContainer.class.php @@ -0,0 +1,39 @@ + + * @package WoltLabSuite\Core\System\Form\Builder\Container + * @since 5.2 + */ +class WysiwygTabMenuFormContainer extends TabMenuFormContainer { + /** + * @inheritDoc + */ + protected $templateName = '__wysiwygTabMenuFormContainer'; + + /** + * Creates a new instance of `WysiwygTabMenuFormContainer`. + */ + public function __construct() { + $this->removeClass('section') + ->removeClass('tabMenuContainer') + ->addClass('messageTabMenu'); + } + + /** + * @inheritDoc + */ + public function appendChild(IFormChildNode $child) { + $child->removeClass('tabMenuContent') + ->addClass('messageTabMenuContent'); + + return parent::appendChild($child); + } +} diff --git a/wcfsetup/install/files/lib/system/form/builder/field/CaptchaFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/CaptchaFormField.class.php index e8232e1388..735e9fc9b7 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/CaptchaFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/CaptchaFormField.class.php @@ -11,9 +11,9 @@ use wcf\system\captcha\ICaptchaHandler; * @package WoltLabSuite\Core\System\Form\Builder\Field * @since 5.2 */ -class CaptchaFormField extends AbstractFormField implements IObjectTypeFormField { +class CaptchaFormField extends AbstractFormField implements IObjectTypeFormNode { use TDefaultIdFormField; - use TObjectTypeFormField { + use TObjectTypeFormNode { objectType as defaultObjectType; } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/IObjectTypeFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/IObjectTypeFormNode.class.php similarity index 89% rename from wcfsetup/install/files/lib/system/form/builder/field/IObjectTypeFormField.class.php rename to wcfsetup/install/files/lib/system/form/builder/field/IObjectTypeFormNode.class.php index d654592b70..e43b7cdf9c 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/IObjectTypeFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/IObjectTypeFormNode.class.php @@ -4,7 +4,7 @@ use wcf\data\object\type\ObjectType; use wcf\system\exception\InvalidObjectTypeException; /** - * Represents a form field that relies on a specific object type. + * Represents a form node that relies on a specific object type. * * @author Matthias Schmidt * @copyright 2001-2019 WoltLab GmbH @@ -12,7 +12,7 @@ use wcf\system\exception\InvalidObjectTypeException; * @package WoltLabSuite\Core\System\Form\Builder\Field * @since 5.2 */ -interface IObjectTypeFormField { +interface IObjectTypeFormNode { /** * Returns the object type. * @@ -26,7 +26,7 @@ interface IObjectTypeFormField { * Sets the name of the object type and returns this field. * * @param string $objectType object type name - * @return IObjectTypeFormField this field + * @return IObjectTypeFormNode this field * * @throws \BadMethodCallException if object type has already been set * @throws \UnexpectedValueException if object type definition returned by `getObjectTypeDefinition()` is unknown diff --git a/wcfsetup/install/files/lib/system/form/builder/field/TObjectTypeFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/TObjectTypeFormNode.class.php similarity index 95% rename from wcfsetup/install/files/lib/system/form/builder/field/TObjectTypeFormField.class.php rename to wcfsetup/install/files/lib/system/form/builder/field/TObjectTypeFormNode.class.php index 3ba4acf8de..3de1d3837a 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/TObjectTypeFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/TObjectTypeFormNode.class.php @@ -5,7 +5,7 @@ use wcf\data\object\type\ObjectTypeCache; use wcf\system\exception\InvalidObjectTypeException; /** - * Provides default implementations of `IObjectTypeFormField` methods. + * Provides default implementations of `IObjectTypeFormNode` methods. * * @author Matthias Schmidt * @copyright 2001-2019 WoltLab GmbH @@ -13,7 +13,7 @@ use wcf\system\exception\InvalidObjectTypeException; * @package WoltLabSuite\Core\System\Form\Builder\Field * @since 5.2 */ -trait TObjectTypeFormField { +trait TObjectTypeFormNode { /** * object type * @var null|ObjectType diff --git a/wcfsetup/install/files/lib/system/form/builder/field/acl/AclFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/acl/AclFormField.class.php index b4f85fb1d6..02a6e5424d 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/acl/AclFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/acl/AclFormField.class.php @@ -4,8 +4,8 @@ use wcf\data\IStorableObject; use wcf\system\acl\ACLHandler; use wcf\system\form\builder\field\AbstractFormField; use wcf\system\form\builder\field\data\processor\CustomFormFieldDataProcessor; -use wcf\system\form\builder\field\IObjectTypeFormField; -use wcf\system\form\builder\field\TObjectTypeFormField; +use wcf\system\form\builder\field\IObjectTypeFormNode; +use wcf\system\form\builder\field\TObjectTypeFormNode; use wcf\system\form\builder\IFormDocument; /** @@ -17,8 +17,8 @@ use wcf\system\form\builder\IFormDocument; * @package WoltLabSuite\Core\System\Form\Builder\Field\Acl * @since 5.2 */ -class AclFormField extends AbstractFormField implements IObjectTypeFormField { - use TObjectTypeFormField; +class AclFormField extends AbstractFormField implements IObjectTypeFormNode { + use TObjectTypeFormNode; /** * name of/filter for the name(s) of the shown acl option categories diff --git a/wcfsetup/install/files/lib/system/form/builder/field/poll/PollOptionsFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/poll/PollOptionsFormField.class.php new file mode 100644 index 0000000000..2d06caf1a9 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/builder/field/poll/PollOptionsFormField.class.php @@ -0,0 +1,103 @@ + + * @package WoltLabSuite\Core\System\Form\Builder\Field\Poll + * @since 5.2 + */ +class PollOptionsFormField extends AbstractFormField { + use TWysiwygFormNode; + + /** + * @inheritDoc + */ + protected $templateName = '__pollOptionsFormField'; + + /** + * @inheritDoc + */ + protected $value = []; + + /** + * Creates a new instance of `PollOptionsFormField`. + */ + public function __construct() { + $this->label('wcf.poll.options') + ->description('wcf.poll.options.description') + ->addClass('pollOptionContainer'); + } + + /** + * @inheritDoc + */ + public function readValue() { + if ($this->getDocument()->hasRequestData($this->getPrefixedId()) && is_array($this->getDocument()->getRequestData($this->getPrefixedId()))) { + $value = array_slice( + ArrayUtil::trim($this->getDocument()->getRequestData($this->getPrefixedId())), + 0, + POLL_MAX_OPTIONS + ); + + $this->value = []; + foreach ($value as $showOrder => $option) { + list($optionID, $optionValue) = explode('_', $option, 2); + $this->value[$showOrder] = [ + 'optionID' => intval($optionID), + 'optionValue' => StringUtil::trim($optionValue) + ]; + } + } + } + + /** + * @inheritDoc + */ + public function value($value) { + $pollOptions = []; + + foreach ($value as $pollOption) { + if ($pollOption instanceof PollOption) { + $pollOptions[] = [ + 'optionID' => $pollOption->optionID, + 'optionValue' => $pollOption->optionValue + ]; + } + else if (is_array($pollOption) && isset($pollOptions['optionID']) && isset($pollOptions['optionValue'])) { + $pollOptions[] = [ + 'optionID' => $pollOptions['optionID'], + 'optionValue' => $pollOptions['optionValue'] + ]; + } + else { + throw new \InvalidArgumentException("Given value array contains invalid value of type " . gettype($pollOption) . "."); + } + } + + return parent::value($value); + } + + /** + * @inheritDoc + */ + public function validate() { + parent::validate(); + + // ensure maximum length that is already validated via JavaScript + foreach ($this->value as &$value) { + $value = mb_substr($value, 0, 255); + } + unset($value); + } +} diff --git a/wcfsetup/install/files/lib/system/form/builder/field/tag/TagFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/tag/TagFormField.class.php index f5296f769f..e8b6de0d4d 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/tag/TagFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/tag/TagFormField.class.php @@ -4,9 +4,9 @@ use wcf\data\tag\Tag; use wcf\data\IStorableObject; use wcf\system\form\builder\field\AbstractFormField; use wcf\system\form\builder\field\data\processor\CustomFormFieldDataProcessor; -use wcf\system\form\builder\field\IObjectTypeFormField; +use wcf\system\form\builder\field\IObjectTypeFormNode; use wcf\system\form\builder\field\TDefaultIdFormField; -use wcf\system\form\builder\field\TObjectTypeFormField; +use wcf\system\form\builder\field\TObjectTypeFormNode; use wcf\system\form\builder\IFormDocument; use wcf\system\tagging\TagEngine; use wcf\util\ArrayUtil; @@ -24,9 +24,9 @@ use wcf\util\ArrayUtil; * @package WoltLabSuite\Core\System\Form\Builder\Field\Tag * @since 5.2 */ -class TagFormField extends AbstractFormField implements IObjectTypeFormField { +class TagFormField extends AbstractFormField implements IObjectTypeFormNode { use TDefaultIdFormField; - use TObjectTypeFormField; + use TObjectTypeFormNode; /** * @inheritDoc diff --git a/wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygAttachmentFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygAttachmentFormField.class.php new file mode 100644 index 0000000000..8b7eaa0e5f --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygAttachmentFormField.class.php @@ -0,0 +1,139 @@ + + * @package WoltLabSuite\Core\System\Form\Builder\Field\Wysiwyg + * @since 5.2 + */ +class WysiwygAttachmentFormField extends AbstractFormField { + use TWysiwygFormNode; + + /** + * attachment handler + * @var null|AttachmentHandler + */ + protected $attachmentHandler; + + /** + * @inheritDoc + */ + protected $templateName = '__wysiwygAttachmentFormField'; + + /** + * Creates a new instance of `WysiwygAttachmentFormField`. + */ + public function __construct() { + $this->addClass('wide'); + } + + /** + * Sets the attachment handler object for the uploaded attachments. If `null` is given, + * the previously set attachment handler is unset. + * + * For the initial attachment handler set by this method, the temporary hashes will be + * automatically set by either reading them from the session variables if the form handles + * AJAX requests or by creating a new one. If the temporary hashes are read from session, + * the session variable will be unregistered afterwards. + * + * @param null|AttachmentHandler $attachmentHandler + * @return WysiwygAttachmentFormField + */ + public function attachmentHandler(AttachmentHandler $attachmentHandler = null) { + if ($this->attachmentHandler === null && $attachmentHandler !== null) { + $tmpHash = StringUtil::getRandomID(); + if ($this->getDocument()->isAjax()) { + $sessionTmpHash = WCF::getSession()->getVar('__wcfAttachmentTmpHash'); + if ($sessionTmpHash !== null) { + $tmpHash = $sessionTmpHash; + + WCF::getSession()->unregister('__wcfAttachmentTmpHash'); + } + } + + $attachmentHandler->setTmpHashes([$tmpHash]); + } + + $this->attachmentHandler = $attachmentHandler; + + if ($this->attachmentHandler !== null) { + $this->description('wcf.attachment.upload.limits', [ + 'attachmentHandler' => $this->attachmentHandler + ]); + } + else { + $this->description(); + } + + return $this; + } + + /** + * Returns the attachment handler object for the uploaded attachments or `null` if no attachment + * upload is supported. + * + * @return null|AttachmentHandler + */ + public function getAttachmentHandler() { + return $this->attachmentHandler; + } + + /** + * @inheritDoc + */ + public function hasSaveValue() { + return false; + } + + /** + * @inheritDoc + */ + public function isAvailable() { + return parent::isAvailable() && $this->getAttachmentHandler() !== null; + } + + /** + * @inheritDoc + */ + public function populate() { + parent::populate(); + + $this->getDocument()->getDataHandler()->add(new CustomFormFieldDataProcessor($this->getId(), function(IFormDocument $document, array $parameters) { + if ($this->getAttachmentHandler() !== null) { + $parameters[$this->getWysiwygId() . '_attachmentHandler'] = $this->getAttachmentHandler(); + } + + return $parameters; + })); + + return $this; + } + + /** + * @inheritDoc + */ + public function readValue() { + if ($this->getDocument()->hasRequestData($this->getPrefixedId() . '_tmpHash')) { + $tmpHash = $this->getDocument()->getRequestData($this->getPrefixedId() . '_tmpHash'); + if (is_string($tmpHash)) { + $this->getAttachmentHandler()->setTmpHashes([$tmpHash]); + } + else if (is_array($tmpHash)) { + $this->getAttachmentHandler()->setTmpHashes($tmpHash); + } + } + } +} diff --git a/wcfsetup/install/files/lib/system/form/builder/field/WysiwygFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygFormField.class.php similarity index 59% rename from wcfsetup/install/files/lib/system/form/builder/field/WysiwygFormField.class.php rename to wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygFormField.class.php index 1d17b22104..097cb009cc 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/WysiwygFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygFormField.class.php @@ -1,6 +1,13 @@ autosaveId = $autosaveId; @@ -93,7 +113,8 @@ class WysiwygFormField extends AbstractFormField implements IMaximumLengthFormFi * Sets the last time this field has been edited and returns this field. * * @param int $lastEditTime last time field has been edited - * @return WysiwygFormField this field + * + * @return WysiwygFormNode this field */ public function lastEditTime($lastEditTime) { $this->lastEditTime = $lastEditTime; @@ -133,6 +154,59 @@ class WysiwygFormField extends AbstractFormField implements IMaximumLengthFormFi return $this; } + /** + * Sets if the form field supports attachments and returns this field. + * + * @param boolean $supportAttachments + * + * @return WysiwygFormNode + */ + public function supportAttachments($supportAttachments = true) { + $this->supportAttachments = $supportAttachments; + + return $this; + } + + /** + * Sets if the form field supports mentions and returns this field. + * + * @param boolean $supportMentions + * + * @return WysiwygFormNode + */ + public function supportMentions($supportMentions = true) { + $this->supportMentions = $supportMentions; + + return $this; + } + + /** + * Returns `true` if the form field supports attachments and returns `false` otherwise. + * + * Important: If this method returns `true`, it does not necessarily mean that attachment + * support will also work as that is the task of `WysiwygAttachmentFormField`. This method + * is primarily relevant to inform the JavaScript API that the field supports attachments + * so that the relevant editor plugin is loaded. + * + * By default, attachments are not supported. + * + * @return boolean + */ + public function supportsAttachments() { + return $this->supportAttachments; + } + + /** + * Returns `true` if the form field supports mentions and returns `false` otherwise. + * + * By default, mentions are not supported. + * + * @return boolean + */ + public function supportsMentions() { + return $this->supportMentions; + } + /** * @inheritDoc */ diff --git a/wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygSmileyFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygSmileyFormField.class.php new file mode 100644 index 0000000000..582da24052 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygSmileyFormField.class.php @@ -0,0 +1,80 @@ + + * @package WoltLabSuite\Core\System\Form\Builder\Field + * @since 5.2 + */ +class WysiwygSmileyFormField extends AbstractFormField { + /** + * list of available smilies + * @var Smiley[] + */ + protected $smilies = []; + + /** + * @inheritDoc + */ + protected $templateName = '__wysiwygSmileyFormField'; + + /** + * Returns the list of available smilies. + * + * @return Smiley[] + */ + public function getSmilies() { + return $this->smilies; + } + + /** + * @inheritDoc + */ + public function hasSaveValue() { + return false; + } + + /** + * @inheritDoc + */ + public function isAvailable() { + return parent::isAvailable() && !empty($this->smilies); + } + + /** + * @inheritDoc + */ + public function readValue() { + // does nothing + } + + /** + * Sets the list of available smilies. + * + * @param Smiley[] $smilies available smilies + * @return WysiwygSmileyFormField this form field + */ + public function smilies(array $smilies) { + foreach ($smilies as $smiley) { + if (!is_object($smiley)) { + throw new \InvalidArgumentException("Given value array contains invalid value of type " . gettype($smiley) . "."); + } + else if (!($smiley instanceof Smiley)) { + throw new \InvalidArgumentException("Given value array contains invalid object of class " . get_class($smiley) . "."); + } + } + + $this->smilies = $smilies; + + return $this; + } +} diff --git a/wcfsetup/install/files/style/ui/attachment.scss b/wcfsetup/install/files/style/ui/attachment.scss index 1bcdf8b810..85585b73bd 100644 --- a/wcfsetup/install/files/style/ui/attachment.scss +++ b/wcfsetup/install/files/style/ui/attachment.scss @@ -121,7 +121,7 @@ /* attachments tab in editor */ .formAttachmentContent { - > .formAttachmentList { + .formAttachmentList { display: flex; flex-wrap: wrap; margin-left: 0 !important; @@ -138,7 +138,7 @@ } @include screen-md-up { - > .formAttachmentList { + .formAttachmentList { margin-right: -20px; > li { @@ -155,23 +155,24 @@ > dl { margin-top: 0 !important; + } + + > dl > dd > div, + .formAttachmentButtons { + align-items: center; + display: flex; - > dd > div { - align-items: center; - display: flex; + > .button { + flex: 0 0 auto; - > .button { - flex: 0 0 auto; - - &:not(:first-child) { - margin-left: 10px; - } - } - - & + small { - margin-top: 10px !important; + &:not(:first-child) { + margin-left: 10px; } } + + & + small { + margin-top: 10px !important; + } } } diff --git a/wcfsetup/install/files/style/ui/tabMenuMessage.scss b/wcfsetup/install/files/style/ui/tabMenuMessage.scss index 76aec308c1..0021dbe59a 100644 --- a/wcfsetup/install/files/style/ui/tabMenuMessage.scss +++ b/wcfsetup/install/files/style/ui/tabMenuMessage.scss @@ -2,11 +2,21 @@ > .messageTabMenuContent { display: none; + &:not(.messageTabMenu) { + > nav.menu { + display: none; + } + } + &.active { background-color: $wcfContentBackground; display: block; margin-top: 0; } + + > .section:first-child { + margin-top: 0; + } } // prevent double formatting with nested tab menus @@ -25,6 +35,8 @@ > ul { @include inlineList; + border: 0; + > li { outline: 0; @@ -55,6 +67,7 @@ width: 100%; } +.messageTabMenu > nav.tabMenu, .messageTabMenuNavigation { > ul { background-color: $wcfContentBackground; @@ -90,6 +103,7 @@ padding: 10px 20px; @include userSelectNone; + @include wcfFontDefault; @include screen-md-up { > .icon { -- 2.20.1