Add proper WYSIWYG support for form builder
authorMatthias Schmidt <gravatronics@live.com>
Sun, 3 Mar 2019 14:41:34 +0000 (15:41 +0100)
committerMatthias Schmidt <gravatronics@live.com>
Sun, 3 Mar 2019 14:41:34 +0000 (15:41 +0100)
See #2852

41 files changed:
com.woltlab.wcf/templates/__form.tpl
com.woltlab.wcf/templates/__pollOptionsFormField.tpl [new file with mode: 0644]
com.woltlab.wcf/templates/__wysiwygAttachmentFormField.tpl [new file with mode: 0644]
com.woltlab.wcf/templates/__wysiwygFormField.tpl
com.woltlab.wcf/templates/__wysiwygPreviewFormButton.tpl [new file with mode: 0644]
com.woltlab.wcf/templates/__wysiwygSmileyFormContainer.tpl [new file with mode: 0644]
com.woltlab.wcf/templates/__wysiwygSmileyFormField.tpl [new file with mode: 0644]
com.woltlab.wcf/templates/__wysiwygTabMenuFormContainer.tpl [new file with mode: 0644]
syncTemplates.json
wcfsetup/install/files/acp/templates/__form.tpl
wcfsetup/install/files/acp/templates/__pollOptionsFormField.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/__wysiwygAttachmentFormField.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/__wysiwygFormField.tpl
wcfsetup/install/files/acp/templates/__wysiwygPreviewFormButton.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/__wysiwygSmileyFormContainer.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/__wysiwygSmileyFormField.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/__wysiwygTabMenuFormContainer.tpl [new file with mode: 0644]
wcfsetup/install/files/js/WCF.Message.js
wcfsetup/install/files/js/WCF.Poll.js
wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.js
wcfsetup/install/files/lib/system/form/builder/FormDocument.class.php
wcfsetup/install/files/lib/system/form/builder/TWysiwygFormNode.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/form/builder/button/wysiwyg/WysiwygPreviewFormButton.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygFormContainer.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygPollFormContainer.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygSmileyFormContainer.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygTabMenuFormContainer.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/form/builder/field/CaptchaFormField.class.php
wcfsetup/install/files/lib/system/form/builder/field/IObjectTypeFormField.class.php [deleted file]
wcfsetup/install/files/lib/system/form/builder/field/IObjectTypeFormNode.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/form/builder/field/TObjectTypeFormField.class.php [deleted file]
wcfsetup/install/files/lib/system/form/builder/field/TObjectTypeFormNode.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/form/builder/field/WysiwygFormField.class.php [deleted file]
wcfsetup/install/files/lib/system/form/builder/field/acl/AclFormField.class.php
wcfsetup/install/files/lib/system/form/builder/field/poll/PollOptionsFormField.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/form/builder/field/tag/TagFormField.class.php
wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygAttachmentFormField.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygFormField.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygSmileyFormField.class.php [new file with mode: 0644]
wcfsetup/install/files/style/ui/attachment.scss
wcfsetup/install/files/style/ui/tabMenuMessage.scss

index 08d72dccdf455e3765571b7cab3367061b25e497..0d9f06e45aa7ac8da84aebaa9af64c44efb9446b 100644 (file)
@@ -5,12 +5,19 @@
        });
 </script>
 
-<form method="{@$form->getMethod()}" {*
-       *}action="{@$form->getAction()}" {*
-       *}id="{@$form->getId()}"{*
-       *}{if !$form->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()}
+       <section id="{@$form->getId()}"{*
+               *}{if !$form->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}
+       <form method="{@$form->getMethod()}" {*
+               *}action="{@$form->getAction()}" {*
+               *}id="{@$form->getId()}"{*
+               *}{if !$form->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()}
        {/if}
        
        {@SECURITY_TOKEN_INPUT_TAG}
-</form>
+{if $form->isAjax()}
+       </section>
+{else}
+       </form>
+{/if}
 
 <script data-relocate="true">
        {* after all dependencies have been added, check them *}
diff --git a/com.woltlab.wcf/templates/__pollOptionsFormField.tpl b/com.woltlab.wcf/templates/__pollOptionsFormField.tpl
new file mode 100644 (file)
index 0000000..65fe710
--- /dev/null
@@ -0,0 +1,23 @@
+{include file='__formFieldHeader'}
+
+<ol class="sortableList"></ol>
+
+{include file='__formFieldFooter'}
+
+{js application='wcf' file='WCF.Poll' bundle='WCF.Combined'}
+<script data-relocate="true">
+       require(['Dom/Traverse', 'Dom/Util', 'Language'], function(DomTraverse, DomUtil, Language) {
+               Language.addObject({
+                       'wcf.poll.button.addOption': '{lang}wcf.poll.button.addOption{/lang}',
+                       'wcf.poll.button.removeOption': '{lang}wcf.poll.button.removeOption{/lang}'
+               });
+               
+               new WCF.Poll.Management(
+                       DomUtil.identify(DomTraverse.childByTag(elById('{@$field->getPrefixedId()}Container'), 'DD')),
+                       [ {implode from=$field->getValue() item=pollOption}{ optionID: {@$pollOption[optionID]}, optionValue: '{$pollOption[optionValue]|encodeJS}' }{/implode} ],
+                       {@POLL_MAX_OPTIONS},
+                       '{if $field->getDocument()->isAjax()}{@$field->getWysiwygId()}{/if}',
+                       '{@$field->getPrefixedId()}'
+               );
+       });
+</script>
diff --git a/com.woltlab.wcf/templates/__wysiwygAttachmentFormField.tpl b/com.woltlab.wcf/templates/__wysiwygAttachmentFormField.tpl
new file mode 100644 (file)
index 0000000..4a12ce6
--- /dev/null
@@ -0,0 +1,77 @@
+{include file='__formFieldHeader'}
+
+<ul id="{@$field->getPrefixedID()}_attachmentList" {*
+       *}class="formAttachmentList"{*
+       *}{if !$field->getAttachmentHandler()->getAttachmentList()|count} style="display: none"{/if} {*
+       *}data-enable-thumbnails="{if ATTACHMENT_ENABLE_THUMBNAILS}true{else}false{/if}"{*
+*}>
+       {foreach from=$field->getAttachmentHandler()->getAttachmentList() item=$attachment}
+               <li class="box64" {*
+                       *}data-object-id="{@$attachment->attachmentID}" {*
+                       *}data-height="{@$attachment->height}" {*
+                       *}data-width="{@$attachment->width}" {*
+                       *}data-is-image="{@$attachment->isImage}"{*
+               *}>
+                       {if $attachment->tinyThumbnailType}
+                               <img src="{link controller='Attachment' object=$attachment}tiny=1{/link}" alt="" class="attachmentTinyThumbnail">
+                       {else}
+                               <span class="icon icon64 fa-{@$attachment->getIconName()}"></span>
+                       {/if}
+                       
+                       <div>
+                               <div>
+                                       <p><a href="{link controller='Attachment' object=$attachment}{/link}" target="_blank"{if $attachment->isImage} title="{$attachment->filename}" class="jsImageViewer"{/if}>{$attachment->filename}</a></p>
+                                       <small>{@$attachment->filesize|filesize}</small>
+                               </div>
+                               
+                               <ul class="buttonGroup">
+                                       <li><span class="button small jsDeleteButton" data-object-id="{@$attachment->attachmentID}" data-confirm-message="{lang}wcf.attachment.delete.sure{/lang}">{lang}wcf.global.button.delete{/lang}</span></li>
+                                       {if $attachment->isImage}
+                                               {if $attachment->thumbnailType}
+                                                       <li><span class="button small jsButtonAttachmentInsertThumbnail" data-object-id="{@$attachment->attachmentID}" data-url="{link controller='Attachment' object=$attachment}thumbnail=1{/link}">{lang}wcf.attachment.insertThumbnail{/lang}</span></li>
+                                               {/if}
+                                               <li><span class="button small jsButtonAttachmentInsertFull" data-object-id="{@$attachment->attachmentID}" data-url="{link controller='Attachment' object=$attachment}{/link}">{lang}wcf.attachment.insertFull{/lang}</span></li>
+                                       {else}
+                                               <li><span class="button small jsButtonInsertAttachment" data-object-id="{@$attachment->attachmentID}">{lang}wcf.attachment.insert{/lang}</span></li>
+                                       {/if}
+                               </ul>
+                       </div>
+               </li>
+       {/foreach}
+</ul>
+<div id="{@$field->getPrefixedID()}_uploadButton" class="formAttachmentButtons" data-max-size="{@$field->getAttachmentHandler()->getMaxSize()}"></div>
+
+{js application='wcf' file='WCF.Attachment' bundle='WCF.Combined'}
+<script data-relocate="true">
+       $(function() {
+               WCF.Language.addObject({
+                       'wcf.attachment.upload.error.invalidExtension': '{lang}wcf.attachment.upload.error.invalidExtension{/lang}',
+                       'wcf.attachment.upload.error.tooLarge': '{lang}wcf.attachment.upload.error.tooLarge{/lang}',
+                       'wcf.attachment.upload.error.reachedLimit': '{lang}wcf.attachment.upload.error.reachedLimit{/lang}',
+                       'wcf.attachment.upload.error.reachedRemainingLimit': '{lang}wcf.attachment.upload.error.reachedRemainingLimit{/lang}',
+                       'wcf.attachment.upload.error.uploadFailed': '{lang}wcf.attachment.upload.error.uploadFailed{/lang}',
+                       'wcf.attachment.upload.error.uploadPhpLimit': '{lang}wcf.attachment.upload.error.uploadPhpLimit{/lang}',
+                       'wcf.attachment.insert': '{lang}wcf.attachment.insert{/lang}',
+                       'wcf.attachment.insertAll': '{lang}wcf.attachment.insertAll{/lang}',
+                       'wcf.attachment.insertFull': '{lang}wcf.attachment.insertFull{/lang}',
+                       'wcf.attachment.insertThumbnail': '{lang}wcf.attachment.insertThumbnail{/lang}',
+                       'wcf.attachment.delete.sure': '{lang}wcf.attachment.delete.sure{/lang}'
+               });
+               
+               new WCF.Attachment.Upload(
+                       $('#{@$field->getPrefixedID()}_uploadButton'),
+                       $('#{@$field->getPrefixedID()}_attachmentList'),
+                       '{@$field->getAttachmentHandler()->getObjectType()->objectType}',
+                       '{@$field->getAttachmentHandler()->getObjectID()}',
+                       '{$field->getAttachmentHandler()->getTmpHashes()[0]|encodeJS}',
+                       '{@$field->getAttachmentHandler()->getParentObjectID()}',
+                       {@$field->getAttachmentHandler()->getMaxCount()},
+                       '{@$field->getWysiwygId()}'
+               );
+               new WCF.Action.Delete('wcf\\data\\attachment\\AttachmentAction', '.formAttachmentList > li');
+       });
+</script>
+
+<input type="hidden" name="{@$field->getPrefixedID()}_tmpHash" value="{$field->getAttachmentHandler()->getTmpHashes()[0]}">
+
+{include file='__formFieldFooter'}
index 4d93d3e25881364024637abdd9a35bdab943ab6c..9336bc7c7641a143230e10d20a9d5af3ba5bf485 100644 (file)
@@ -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 (file)
index 0000000..ffdcb29
--- /dev/null
@@ -0,0 +1,20 @@
+<button id="{@$button->getPrefixedId()}"{*
+       *}{if !$button->getClasses()|empty} class="{implode from=$button->getClasses() item='class' glue=' '}{$class}{/implode}"{/if}{*
+       *}{foreach from=$button->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{*
+       *}{if $button->getAccessKey()} accesskey="{$button->getAccessKey()}"{/if}{*
+*}>{$button->getLabel()}</button>
+
+<script data-relocate="true">
+       require(['Language'], function(Language) {
+               Language.addObject({
+                       'wcf.global.preview': '{lang}wcf.global.preview{/lang}'
+               });
+               
+               new WCF.Message.DefaultPreview({
+                       messageFieldID: '{@$button->getWysiwygId()}',
+                       previewButtonID: '{@$button->getPrefixedId()}',
+                       messageObjectType: '{@$button->getObjectType()->objectType}',
+                       messageObjectID: '{@$button->getObjectId()}'
+               });
+       });
+</script>
diff --git a/com.woltlab.wcf/templates/__wysiwygSmileyFormContainer.tpl b/com.woltlab.wcf/templates/__wysiwygSmileyFormContainer.tpl
new file mode 100644 (file)
index 0000000..6f572aa
--- /dev/null
@@ -0,0 +1,11 @@
+{include file='__tabTabMenuFormContainer'}
+
+<script data-relocate="true">
+       $(function() {
+               {if $container->children()|count > 1}
+                       new WCF.Message.SmileyCategories('{@$container->getWysiwygId()}');
+               {/if}
+               
+               new WCF.Message.Smilies('{@$container->getWysiwygId()}');
+       });
+</script>
diff --git a/com.woltlab.wcf/templates/__wysiwygSmileyFormField.tpl b/com.woltlab.wcf/templates/__wysiwygSmileyFormField.tpl
new file mode 100644 (file)
index 0000000..422140b
--- /dev/null
@@ -0,0 +1,5 @@
+<ul class="inlineList smileyList">
+       {foreach from=$field->getSmilies() item=smiley}
+               <li><a title="{lang}{$smiley->smileyTitle}{/lang}" class="jsTooltip jsSmiley">{@$smiley->getHtml()}</a></li>
+       {/foreach}
+</ul>
diff --git a/com.woltlab.wcf/templates/__wysiwygTabMenuFormContainer.tpl b/com.woltlab.wcf/templates/__wysiwygTabMenuFormContainer.tpl
new file mode 100644 (file)
index 0000000..1b5ab16
--- /dev/null
@@ -0,0 +1,8 @@
+{include file='__tabMenuFormContainer'}
+
+{js application='wcf' file='WCF.Message' bundle='WCF.Combined'}
+<script data-relocate="true">
+       $(function() {
+               $('.messageTabMenu').messageTabMenu();
+       });
+</script>
index 03b7378f655922bf0e33971b11eec1d661df587c..28fabe23a5600dff697fa0d407b19cff7574e82a 100644 (file)
@@ -28,6 +28,7 @@
     "__multipleSelectionFormField",
     "__nonEmptyFormFieldDependency",
     "__numericFormField",
+    "__pollOptionsFormField",
     "__radioButtonFormField",
     "__singleSelectionFormField",
     "__tabFormContainer",
     "__userFormField",
     "__usernameFormField",
     "__valueFormFieldDependency",
+    "__wysiwygAttachmentFormField",
     "__wysiwygCmsToolbar",
     "__wysiwygFormField",
+    "__wysiwygPreviewFormButton",
+    "__wysiwygSmileyFormContainer",
+    "__wysiwygSmileyFormField",
     "aclPermissionJavaScript",
     "articleAdd",
     "articleAddDialog",
index 08d72dccdf455e3765571b7cab3367061b25e497..0d9f06e45aa7ac8da84aebaa9af64c44efb9446b 100644 (file)
@@ -5,12 +5,19 @@
        });
 </script>
 
-<form method="{@$form->getMethod()}" {*
-       *}action="{@$form->getAction()}" {*
-       *}id="{@$form->getId()}"{*
-       *}{if !$form->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()}
+       <section id="{@$form->getId()}"{*
+               *}{if !$form->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}
+       <form method="{@$form->getMethod()}" {*
+               *}action="{@$form->getAction()}" {*
+               *}id="{@$form->getId()}"{*
+               *}{if !$form->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()}
        {/if}
        
        {@SECURITY_TOKEN_INPUT_TAG}
-</form>
+{if $form->isAjax()}
+       </section>
+{else}
+       </form>
+{/if}
 
 <script data-relocate="true">
        {* after all dependencies have been added, check them *}
diff --git a/wcfsetup/install/files/acp/templates/__pollOptionsFormField.tpl b/wcfsetup/install/files/acp/templates/__pollOptionsFormField.tpl
new file mode 100644 (file)
index 0000000..65fe710
--- /dev/null
@@ -0,0 +1,23 @@
+{include file='__formFieldHeader'}
+
+<ol class="sortableList"></ol>
+
+{include file='__formFieldFooter'}
+
+{js application='wcf' file='WCF.Poll' bundle='WCF.Combined'}
+<script data-relocate="true">
+       require(['Dom/Traverse', 'Dom/Util', 'Language'], function(DomTraverse, DomUtil, Language) {
+               Language.addObject({
+                       'wcf.poll.button.addOption': '{lang}wcf.poll.button.addOption{/lang}',
+                       'wcf.poll.button.removeOption': '{lang}wcf.poll.button.removeOption{/lang}'
+               });
+               
+               new WCF.Poll.Management(
+                       DomUtil.identify(DomTraverse.childByTag(elById('{@$field->getPrefixedId()}Container'), 'DD')),
+                       [ {implode from=$field->getValue() item=pollOption}{ optionID: {@$pollOption[optionID]}, optionValue: '{$pollOption[optionValue]|encodeJS}' }{/implode} ],
+                       {@POLL_MAX_OPTIONS},
+                       '{if $field->getDocument()->isAjax()}{@$field->getWysiwygId()}{/if}',
+                       '{@$field->getPrefixedId()}'
+               );
+       });
+</script>
diff --git a/wcfsetup/install/files/acp/templates/__wysiwygAttachmentFormField.tpl b/wcfsetup/install/files/acp/templates/__wysiwygAttachmentFormField.tpl
new file mode 100644 (file)
index 0000000..4a12ce6
--- /dev/null
@@ -0,0 +1,77 @@
+{include file='__formFieldHeader'}
+
+<ul id="{@$field->getPrefixedID()}_attachmentList" {*
+       *}class="formAttachmentList"{*
+       *}{if !$field->getAttachmentHandler()->getAttachmentList()|count} style="display: none"{/if} {*
+       *}data-enable-thumbnails="{if ATTACHMENT_ENABLE_THUMBNAILS}true{else}false{/if}"{*
+*}>
+       {foreach from=$field->getAttachmentHandler()->getAttachmentList() item=$attachment}
+               <li class="box64" {*
+                       *}data-object-id="{@$attachment->attachmentID}" {*
+                       *}data-height="{@$attachment->height}" {*
+                       *}data-width="{@$attachment->width}" {*
+                       *}data-is-image="{@$attachment->isImage}"{*
+               *}>
+                       {if $attachment->tinyThumbnailType}
+                               <img src="{link controller='Attachment' object=$attachment}tiny=1{/link}" alt="" class="attachmentTinyThumbnail">
+                       {else}
+                               <span class="icon icon64 fa-{@$attachment->getIconName()}"></span>
+                       {/if}
+                       
+                       <div>
+                               <div>
+                                       <p><a href="{link controller='Attachment' object=$attachment}{/link}" target="_blank"{if $attachment->isImage} title="{$attachment->filename}" class="jsImageViewer"{/if}>{$attachment->filename}</a></p>
+                                       <small>{@$attachment->filesize|filesize}</small>
+                               </div>
+                               
+                               <ul class="buttonGroup">
+                                       <li><span class="button small jsDeleteButton" data-object-id="{@$attachment->attachmentID}" data-confirm-message="{lang}wcf.attachment.delete.sure{/lang}">{lang}wcf.global.button.delete{/lang}</span></li>
+                                       {if $attachment->isImage}
+                                               {if $attachment->thumbnailType}
+                                                       <li><span class="button small jsButtonAttachmentInsertThumbnail" data-object-id="{@$attachment->attachmentID}" data-url="{link controller='Attachment' object=$attachment}thumbnail=1{/link}">{lang}wcf.attachment.insertThumbnail{/lang}</span></li>
+                                               {/if}
+                                               <li><span class="button small jsButtonAttachmentInsertFull" data-object-id="{@$attachment->attachmentID}" data-url="{link controller='Attachment' object=$attachment}{/link}">{lang}wcf.attachment.insertFull{/lang}</span></li>
+                                       {else}
+                                               <li><span class="button small jsButtonInsertAttachment" data-object-id="{@$attachment->attachmentID}">{lang}wcf.attachment.insert{/lang}</span></li>
+                                       {/if}
+                               </ul>
+                       </div>
+               </li>
+       {/foreach}
+</ul>
+<div id="{@$field->getPrefixedID()}_uploadButton" class="formAttachmentButtons" data-max-size="{@$field->getAttachmentHandler()->getMaxSize()}"></div>
+
+{js application='wcf' file='WCF.Attachment' bundle='WCF.Combined'}
+<script data-relocate="true">
+       $(function() {
+               WCF.Language.addObject({
+                       'wcf.attachment.upload.error.invalidExtension': '{lang}wcf.attachment.upload.error.invalidExtension{/lang}',
+                       'wcf.attachment.upload.error.tooLarge': '{lang}wcf.attachment.upload.error.tooLarge{/lang}',
+                       'wcf.attachment.upload.error.reachedLimit': '{lang}wcf.attachment.upload.error.reachedLimit{/lang}',
+                       'wcf.attachment.upload.error.reachedRemainingLimit': '{lang}wcf.attachment.upload.error.reachedRemainingLimit{/lang}',
+                       'wcf.attachment.upload.error.uploadFailed': '{lang}wcf.attachment.upload.error.uploadFailed{/lang}',
+                       'wcf.attachment.upload.error.uploadPhpLimit': '{lang}wcf.attachment.upload.error.uploadPhpLimit{/lang}',
+                       'wcf.attachment.insert': '{lang}wcf.attachment.insert{/lang}',
+                       'wcf.attachment.insertAll': '{lang}wcf.attachment.insertAll{/lang}',
+                       'wcf.attachment.insertFull': '{lang}wcf.attachment.insertFull{/lang}',
+                       'wcf.attachment.insertThumbnail': '{lang}wcf.attachment.insertThumbnail{/lang}',
+                       'wcf.attachment.delete.sure': '{lang}wcf.attachment.delete.sure{/lang}'
+               });
+               
+               new WCF.Attachment.Upload(
+                       $('#{@$field->getPrefixedID()}_uploadButton'),
+                       $('#{@$field->getPrefixedID()}_attachmentList'),
+                       '{@$field->getAttachmentHandler()->getObjectType()->objectType}',
+                       '{@$field->getAttachmentHandler()->getObjectID()}',
+                       '{$field->getAttachmentHandler()->getTmpHashes()[0]|encodeJS}',
+                       '{@$field->getAttachmentHandler()->getParentObjectID()}',
+                       {@$field->getAttachmentHandler()->getMaxCount()},
+                       '{@$field->getWysiwygId()}'
+               );
+               new WCF.Action.Delete('wcf\\data\\attachment\\AttachmentAction', '.formAttachmentList > li');
+       });
+</script>
+
+<input type="hidden" name="{@$field->getPrefixedID()}_tmpHash" value="{$field->getAttachmentHandler()->getTmpHashes()[0]}">
+
+{include file='__formFieldFooter'}
index 4d93d3e25881364024637abdd9a35bdab943ab6c..9336bc7c7641a143230e10d20a9d5af3ba5bf485 100644 (file)
@@ -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 (file)
index 0000000..ffdcb29
--- /dev/null
@@ -0,0 +1,20 @@
+<button id="{@$button->getPrefixedId()}"{*
+       *}{if !$button->getClasses()|empty} class="{implode from=$button->getClasses() item='class' glue=' '}{$class}{/implode}"{/if}{*
+       *}{foreach from=$button->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{*
+       *}{if $button->getAccessKey()} accesskey="{$button->getAccessKey()}"{/if}{*
+*}>{$button->getLabel()}</button>
+
+<script data-relocate="true">
+       require(['Language'], function(Language) {
+               Language.addObject({
+                       'wcf.global.preview': '{lang}wcf.global.preview{/lang}'
+               });
+               
+               new WCF.Message.DefaultPreview({
+                       messageFieldID: '{@$button->getWysiwygId()}',
+                       previewButtonID: '{@$button->getPrefixedId()}',
+                       messageObjectType: '{@$button->getObjectType()->objectType}',
+                       messageObjectID: '{@$button->getObjectId()}'
+               });
+       });
+</script>
diff --git a/wcfsetup/install/files/acp/templates/__wysiwygSmileyFormContainer.tpl b/wcfsetup/install/files/acp/templates/__wysiwygSmileyFormContainer.tpl
new file mode 100644 (file)
index 0000000..6f572aa
--- /dev/null
@@ -0,0 +1,11 @@
+{include file='__tabTabMenuFormContainer'}
+
+<script data-relocate="true">
+       $(function() {
+               {if $container->children()|count > 1}
+                       new WCF.Message.SmileyCategories('{@$container->getWysiwygId()}');
+               {/if}
+               
+               new WCF.Message.Smilies('{@$container->getWysiwygId()}');
+       });
+</script>
diff --git a/wcfsetup/install/files/acp/templates/__wysiwygSmileyFormField.tpl b/wcfsetup/install/files/acp/templates/__wysiwygSmileyFormField.tpl
new file mode 100644 (file)
index 0000000..422140b
--- /dev/null
@@ -0,0 +1,5 @@
+<ul class="inlineList smileyList">
+       {foreach from=$field->getSmilies() item=smiley}
+               <li><a title="{lang}{$smiley->smileyTitle}{/lang}" class="jsTooltip jsSmiley">{@$smiley->getHtml()}</a></li>
+       {/foreach}
+</ul>
diff --git a/wcfsetup/install/files/acp/templates/__wysiwygTabMenuFormContainer.tpl b/wcfsetup/install/files/acp/templates/__wysiwygTabMenuFormContainer.tpl
new file mode 100644 (file)
index 0000000..b0a8e2d
--- /dev/null
@@ -0,0 +1,8 @@
+{include file='__tabMenuFormContainer'}
+
+{js application='wcf' file='WCF.Message' bundle='WCF.Combined'}
+<script data-relocate="true">
+       $(function() {
+               $('#{@$container->getPrefixedId()}').messageTabMenu();
+       });
+</script>
index f90583296f460c98100a6664bd198ea380250cdb..a39bfc78d2a4d6c13dfdf1602eb67ec783afa777 100644 (file)
@@ -2368,7 +2368,6 @@ $.widget('wcf.messageTabMenu', {
                                
                                if ($name === undefined) {
                                        $name = $tab.wcfIdentify();
-                                       console.debug("[wcf.messageTabMenu] Missing name attribute, assuming generic ID '" + $name + "'");
                                }
                        }
                        
index f04237ec3880054fc0bd99627c82a24ab69ab04b..e8d8cd1fcc67095c6132efbb57ed0cb799c8a11d 100644 (file)
@@ -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++) {
-                                               $('<input type="hidden" name="pollOptions[' + $i + ']">').val($options[$i]).appendTo($formSubmit);
+                                               $('<input type="hidden" name="' + this._fieldName + '[' + $i + ']">').val($options[$i]).appendTo($formSubmit);
                                        }
                                }
                        }
index cb9fb4887f22e46631b913cfbd6c72d41fdfda1b..84e461dcab2571676eb5c384582979fbf57fb103 100644 (file)
@@ -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.");
index a96ecadb8b5ca859b6f7ad6ab744c789bb038a35..c137720ab7e3979259329652f306925a7e55a7a8 100644 (file)
@@ -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 (file)
index 0000000..a0f4a85
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+namespace wcf\system\form\builder;
+
+/**
+ * Provides methods to get and set the id of the related `WysiwygFormField` form field for wysiwyg-
+ * related form nodes.
+ * 
+ * @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\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 (file)
index 0000000..ce960c0
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+namespace wcf\system\form\builder\button\wysiwyg;
+use wcf\system\form\builder\button\FormButton;
+use wcf\system\form\builder\field\IObjectTypeFormNode;
+use wcf\system\form\builder\field\TObjectTypeFormNode;
+use wcf\system\form\builder\TWysiwygFormNode;
+
+/**
+ * Represents a preview button for a wysiwyg field.
+ * 
+ * @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\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 (file)
index 0000000..70eb0ea
--- /dev/null
@@ -0,0 +1,466 @@
+<?php
+namespace wcf\system\form\builder\container\wysiwyg;
+use wcf\data\IStorableObject;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\system\attachment\AttachmentHandler;
+use wcf\system\event\EventHandler;
+use wcf\system\form\builder\button\wysiwyg\WysiwygPreviewFormButton;
+use wcf\system\form\builder\container\FormContainer;
+use wcf\system\form\builder\container\TabFormContainer;
+use wcf\system\form\builder\field\wysiwyg\WysiwygAttachmentFormField;
+use wcf\system\form\builder\field\wysiwyg\WysiwygFormField;
+use wcf\system\form\builder\IFormNode;
+use wcf\system\form\builder\TWysiwygFormNode;
+
+/**
+ * Represents the whole container with a WYSIWYG editor and the associated tab menu below it with
+ * support for smilies, attchments, settings, and polls.
+ * 
+ * Instead of having to manually set up each individual component, this form container allows to
+ * simply create an instance of this class, set some required data for some components, and the
+ * setup is complete.
+ * 
+ * @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\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 (file)
index 0000000..e658c46
--- /dev/null
@@ -0,0 +1,353 @@
+<?php
+namespace wcf\system\form\builder\container\wysiwyg;
+use wcf\data\IPollContainer;
+use wcf\data\IStorableObject;
+use wcf\data\poll\Poll;
+use wcf\system\form\builder\container\FormContainer;
+use wcf\system\form\builder\field\BooleanFormField;
+use wcf\system\form\builder\field\data\processor\CustomFormFieldDataProcessor;
+use wcf\system\form\builder\field\DateFormField;
+use wcf\system\form\builder\field\IntegerFormField;
+use wcf\system\form\builder\field\IObjectTypeFormNode;
+use wcf\system\form\builder\field\poll\PollOptionsFormField;
+use wcf\system\form\builder\field\TextFormField;
+use wcf\system\form\builder\field\TObjectTypeFormNode;
+use wcf\system\form\builder\field\validation\FormFieldValidationError;
+use wcf\system\form\builder\field\validation\FormFieldValidator;
+use wcf\system\form\builder\IFormDocument;
+use wcf\system\form\builder\TWysiwygFormNode;
+use wcf\system\poll\IPollHandler;
+
+/**
+ * Represents the form container for the poll-related fields below a WYSIWYG editor.
+ * 
+ * @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\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 (file)
index 0000000..f3a6406
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+namespace wcf\system\form\builder\container\wysiwyg;
+use wcf\data\smiley\SmileyCache;
+use wcf\system\form\builder\container\FormContainer;
+use wcf\system\form\builder\container\TabFormContainer;
+use wcf\system\form\builder\container\TabTabMenuFormContainer;
+use wcf\system\form\builder\field\wysiwyg\WysiwygSmileyFormField;
+use wcf\system\form\builder\TWysiwygFormNode;
+use wcf\util\StringUtil;
+
+/**
+ * Represents the tab for the smiley-related fields below a WYSIWYG editor.
+ * 
+ * @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\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 (file)
index 0000000..71aec9e
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+namespace wcf\system\form\builder\container\wysiwyg;
+use wcf\system\form\builder\container\TabMenuFormContainer;
+use wcf\system\form\builder\IFormChildNode;
+
+/**
+ * Represents a container whose children are tabs of a wysiwyg tab menu.
+ * 
+ * @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\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);
+       }
+}
index e8232e13880fb722b0356468c76a74b880e07c73..735e9fc9b737646bd147b1e25a26967012fbfd87 100644 (file)
@@ -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/IObjectTypeFormField.class.php
deleted file mode 100644 (file)
index d654592..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-namespace wcf\system\form\builder\field;
-use wcf\data\object\type\ObjectType;
-use wcf\system\exception\InvalidObjectTypeException;
-
-/**
- * Represents a form field that relies on a specific object type.
- * 
- * @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
- */
-interface IObjectTypeFormField {
-       /**
-        * Returns the object type.
-        * 
-        * @return      ObjectType                      object type
-        *
-        * @throws      \BadMethodCallException         if object type has not been set
-        */
-       public function getObjectType();
-       
-       /**
-        * Sets the name of the object type and returns this field.
-        *
-        * @param       string          $objectType     object type name
-        * @return      IObjectTypeFormField            this field
-        *
-        * @throws      \BadMethodCallException         if object type has already been set
-        * @throws      \UnexpectedValueException       if object type definition returned by `getObjectTypeDefinition()` is unknown
-        * @throws      InvalidObjectTypeException      if given object type name is invalid
-        */
-       public function objectType($objectType);
-       
-       /**
-        * Returns the name of the object type definition the set object type must be of.
-        * 
-        * @return      string          name of object type's definition
-        */
-       public function getObjectTypeDefinition();
-}
diff --git a/wcfsetup/install/files/lib/system/form/builder/field/IObjectTypeFormNode.class.php b/wcfsetup/install/files/lib/system/form/builder/field/IObjectTypeFormNode.class.php
new file mode 100644 (file)
index 0000000..e43b7cd
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+namespace wcf\system\form\builder\field;
+use wcf\data\object\type\ObjectType;
+use wcf\system\exception\InvalidObjectTypeException;
+
+/**
+ * Represents a form node that relies on a specific object type.
+ * 
+ * @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
+ */
+interface IObjectTypeFormNode {
+       /**
+        * Returns the object type.
+        * 
+        * @return      ObjectType                      object type
+        *
+        * @throws      \BadMethodCallException         if object type has not been set
+        */
+       public function getObjectType();
+       
+       /**
+        * Sets the name of the object type and returns this field.
+        *
+        * @param       string          $objectType     object type name
+        * @return      IObjectTypeFormNode             this field
+        *
+        * @throws      \BadMethodCallException         if object type has already been set
+        * @throws      \UnexpectedValueException       if object type definition returned by `getObjectTypeDefinition()` is unknown
+        * @throws      InvalidObjectTypeException      if given object type name is invalid
+        */
+       public function objectType($objectType);
+       
+       /**
+        * Returns the name of the object type definition the set object type must be of.
+        * 
+        * @return      string          name of object type's definition
+        */
+       public function getObjectTypeDefinition();
+}
diff --git a/wcfsetup/install/files/lib/system/form/builder/field/TObjectTypeFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/TObjectTypeFormField.class.php
deleted file mode 100644 (file)
index 3ba4acf..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-<?php
-namespace wcf\system\form\builder\field;
-use wcf\data\object\type\ObjectType;
-use wcf\data\object\type\ObjectTypeCache;
-use wcf\system\exception\InvalidObjectTypeException;
-
-/**
- * Provides default implementations of `IObjectTypeFormField` methods.
- * 
- * @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
- */
-trait TObjectTypeFormField {
-       /**
-        * object type
-        * @var null|ObjectType
-        */
-       protected $objectType;
-       
-       /**
-        * Returns the object type.
-        * 
-        * @return      ObjectType                      object type
-        * 
-        * @throws      \BadMethodCallException         if object type has not been set
-        */
-       public function getObjectType() {
-               if ($this->objectType === null) {
-                       throw new \BadMethodCallException("Object type has not been set.");
-               }
-               
-               return $this->objectType;
-       }
-       
-       /**
-        * Sets the name of the object type and returns this field.
-        * 
-        * @param       string          $objectType     object type name
-        * @return      static                          this field
-        * 
-        * @throws      \BadMethodCallException         if object type has already been set
-        * @throws      \UnexpectedValueException       if object type definition returned by `getObjectTypeDefinition()` is unknown
-        * @throws      InvalidObjectTypeException      if given object type name is invalid
-        */
-       public function objectType($objectType) {
-               if ($this->objectType !== null) {
-                       throw new \BadMethodCallException("Object type has already been set.");
-               }
-               
-               if (ObjectTypeCache::getInstance()->getDefinitionByName($this->getObjectTypeDefinition()) === null) {
-                       throw new \UnexpectedValueException("Unknown definition name '{$this->getObjectTypeDefinition()}'.");
-               }
-               
-               $this->objectType = ObjectTypeCache::getInstance()->getObjectTypeByName($this->getObjectTypeDefinition(), $objectType);
-               if ($this->objectType === null) {
-                       throw new InvalidObjectTypeException($objectType, $this->getObjectTypeDefinition());
-               }
-               
-               return $this;
-       }
-       
-       /**
-        * Returns the name of the object type definition the set object type must be of.
-        *
-        * @return      string          name of object type's definition
-        */
-       abstract public function getObjectTypeDefinition();
-}
diff --git a/wcfsetup/install/files/lib/system/form/builder/field/TObjectTypeFormNode.class.php b/wcfsetup/install/files/lib/system/form/builder/field/TObjectTypeFormNode.class.php
new file mode 100644 (file)
index 0000000..3de1d38
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+namespace wcf\system\form\builder\field;
+use wcf\data\object\type\ObjectType;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\system\exception\InvalidObjectTypeException;
+
+/**
+ * Provides default implementations of `IObjectTypeFormNode` methods.
+ * 
+ * @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
+ */
+trait TObjectTypeFormNode {
+       /**
+        * object type
+        * @var null|ObjectType
+        */
+       protected $objectType;
+       
+       /**
+        * Returns the object type.
+        * 
+        * @return      ObjectType                      object type
+        * 
+        * @throws      \BadMethodCallException         if object type has not been set
+        */
+       public function getObjectType() {
+               if ($this->objectType === null) {
+                       throw new \BadMethodCallException("Object type has not been set.");
+               }
+               
+               return $this->objectType;
+       }
+       
+       /**
+        * Sets the name of the object type and returns this field.
+        * 
+        * @param       string          $objectType     object type name
+        * @return      static                          this field
+        * 
+        * @throws      \BadMethodCallException         if object type has already been set
+        * @throws      \UnexpectedValueException       if object type definition returned by `getObjectTypeDefinition()` is unknown
+        * @throws      InvalidObjectTypeException      if given object type name is invalid
+        */
+       public function objectType($objectType) {
+               if ($this->objectType !== null) {
+                       throw new \BadMethodCallException("Object type has already been set.");
+               }
+               
+               if (ObjectTypeCache::getInstance()->getDefinitionByName($this->getObjectTypeDefinition()) === null) {
+                       throw new \UnexpectedValueException("Unknown definition name '{$this->getObjectTypeDefinition()}'.");
+               }
+               
+               $this->objectType = ObjectTypeCache::getInstance()->getObjectTypeByName($this->getObjectTypeDefinition(), $objectType);
+               if ($this->objectType === null) {
+                       throw new InvalidObjectTypeException($objectType, $this->getObjectTypeDefinition());
+               }
+               
+               return $this;
+       }
+       
+       /**
+        * Returns the name of the object type definition the set object type must be of.
+        *
+        * @return      string          name of object type's definition
+        */
+       abstract public function getObjectTypeDefinition();
+}
diff --git a/wcfsetup/install/files/lib/system/form/builder/field/WysiwygFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/WysiwygFormField.class.php
deleted file mode 100644 (file)
index 1d17b22..0000000
+++ /dev/null
@@ -1,153 +0,0 @@
-<?php
-namespace wcf\system\form\builder\field;
-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\html\input\HtmlInputProcessor;
-use wcf\util\StringUtil;
-
-/**
- * Implementation of a form field for wysiwyg editors.
- * 
- * @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 WysiwygFormField extends AbstractFormField implements IMaximumLengthFormField, IMinimumLengthFormField, IObjectTypeFormField {
-       use TMaximumLengthFormField;
-       use TMinimumLengthFormField;
-       use TObjectTypeFormField;
-       
-       /**
-        * identifier used to autosave the field value; if empty, autosave is disabled
-        * @var string
-        */
-       protected $autosaveId = '';
-       
-       /**
-        * last time the field has been edited; if `0`, the last edit time is unknown
-        * @var int
-        */
-       protected $lastEditTime = 0;
-       
-       /**
-        * input processor containing the wysiwyg text
-        * @var HtmlInputProcessor
-        */
-       protected $htmlInputProcessor;
-       
-       /**
-        * @inheritDoc
-        */
-       protected $templateName = '__wysiwygFormField';
-       
-       /**
-        * Sets the identifier used to autosave the field value and returns this field.
-        * 
-        * @param       string          $autosaveId     identifier used to autosave field value
-        * @return      WysiwygFormField                this field
-        */
-       public function autosaveId($autosaveId) {
-               $this->autosaveId = $autosaveId;
-               
-               return $this;
-       }
-       
-       /**
-        * Returns the identifier used to autosave the field value. If autosave is disabled,
-        * an empty string is returned.
-        * 
-        * @return      string
-        */
-       public function getAutosaveId() {
-               return $this->autosaveId;
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       public function getObjectTypeDefinition() {
-               return 'com.woltlab.wcf.message';
-       }
-       
-       /**
-        * Returns the last time the field has been edited. If no last edit time has
-        * been set, `0` is returned.
-        * 
-        * @return      int
-        */
-       public function getLastEditTime() {
-               return $this->lastEditTime;
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       public function hasSaveValue() {
-               return false;
-       }
-       
-       /**
-        * 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
-        */
-       public function lastEditTime($lastEditTime) {
-               $this->lastEditTime = $lastEditTime;
-               
-               return $this;
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       public function populate() {
-               parent::populate();
-               
-               $this->getDocument()->getDataHandler()->add(new CustomFormFieldDataProcessor('wysiwyg', function(IFormDocument $document, array $parameters) {
-                       if ($this->checkDependencies()) {
-                               $parameters[$this->getObjectProperty() . '_htmlInputProcessor'] = $this->htmlInputProcessor;
-                       }
-                       
-                       return $parameters;
-               }));
-               
-               return $this;
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       public function readValue() {
-               if ($this->getDocument()->hasRequestData($this->getPrefixedId())) {
-                       $value = $this->getDocument()->getRequestData($this->getPrefixedId());
-                       
-                       if (is_string($value)) {
-                               $this->value = StringUtil::trim($value);
-                       }
-               }
-               
-               return $this;
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       public function validate() {
-               if ($this->isRequired() && $this->getValue() === '') {
-                       $this->addValidationError(new FormFieldValidationError('empty'));
-               }
-               else {
-                       $this->validateMinimumLength($this->getValue());
-                       $this->validateMaximumLength($this->getValue());
-               }
-               
-               $this->htmlInputProcessor = new HtmlInputProcessor();
-               $this->htmlInputProcessor->process($this->getValue(), $this->getObjectType()->objectType);
-               
-               parent::validate();
-       }
-}
index b4f85fb1d6302e19751c1b35731b7e1890cffe47..02a6e5424d128bcc9029a93c6b943eee95c507be 100644 (file)
@@ -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 (file)
index 0000000..2d06caf
--- /dev/null
@@ -0,0 +1,103 @@
+<?php
+namespace wcf\system\form\builder\field\poll;
+use wcf\data\poll\option\PollOption;
+use wcf\system\form\builder\field\AbstractFormField;
+use wcf\system\form\builder\TWysiwygFormNode;
+use wcf\util\ArrayUtil;
+use wcf\util\StringUtil;
+
+/**
+ * Represents the form field to manage poll options/answers.
+ * 
+ * This form field should not be used idenpendently but only via `WysiwygPollFormContainer`.
+ * 
+ * @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\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);
+       }
+}
index f5296f769f9a6b3944b6ac6be3975bd8a3f1fbf1..e8b6de0d4d7a5a2323269e302604287039938c8e 100644 (file)
@@ -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 (file)
index 0000000..8b7eaa0
--- /dev/null
@@ -0,0 +1,139 @@
+<?php
+namespace wcf\system\form\builder\field\wysiwyg;
+use wcf\system\attachment\AttachmentHandler;
+use wcf\system\form\builder\field\AbstractFormField;
+use wcf\system\form\builder\field\data\processor\CustomFormFieldDataProcessor;
+use wcf\system\form\builder\IFormDocument;
+use wcf\system\form\builder\TWysiwygFormNode;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Represents the form field to manage attachments for a wysiwyg form container.
+ * 
+ * If no attachment handler has been set, this field is not available.
+ * 
+ * @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\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/wysiwyg/WysiwygFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygFormField.class.php
new file mode 100644 (file)
index 0000000..097cb00
--- /dev/null
@@ -0,0 +1,227 @@
+<?php
+namespace wcf\system\form\builder\field\wysiwyg;
+use wcf\system\form\builder\field\AbstractFormField;
+use wcf\system\form\builder\field\data\processor\CustomFormFieldDataProcessor;
+use wcf\system\form\builder\field\IMaximumLengthFormField;
+use wcf\system\form\builder\field\IMinimumLengthFormField;
+use wcf\system\form\builder\field\IObjectTypeFormNode;
+use wcf\system\form\builder\field\TMaximumLengthFormField;
+use wcf\system\form\builder\field\TMinimumLengthFormField;
+use wcf\system\form\builder\field\TObjectTypeFormNode;
+use wcf\system\form\builder\field\validation\FormFieldValidationError;
+use wcf\system\form\builder\IFormDocument;
+use wcf\system\html\input\HtmlInputProcessor;
+use wcf\util\StringUtil;
+
+/**
+ * Implementation of a form field for wysiwyg editors.
+ * 
+ * @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 WysiwygFormField extends AbstractFormField implements IMaximumLengthFormField, IMinimumLengthFormField, IObjectTypeFormNode {
+       use TMaximumLengthFormField;
+       use TMinimumLengthFormField;
+       use TObjectTypeFormNode;
+       
+       /**
+        * identifier used to autosave the field value; if empty, autosave is disabled
+        * @var string
+        */
+       protected $autosaveId = '';
+       
+       /**
+        * input processor containing the wysiwyg text
+        * @var HtmlInputProcessor
+        */
+       protected $htmlInputProcessor;
+       
+       /**
+        * last time the field has been edited; if `0`, the last edit time is unknown
+        * @var int
+        */
+       protected $lastEditTime = 0;
+       
+       /**
+        * is `true` if this form field should support attachments, otherwise `false`
+        * @var boolean 
+        */
+       protected $supportAttachments = false;
+       
+       /**
+        * is `true` if this form field should support mentions, otherwise `false`
+        * @var boolean
+        */
+       protected $supportMentions = false;
+       
+       /**
+        * @inheritDoc
+        */
+       protected $templateName = '__wysiwygFormField';
+       
+       /**
+        * Sets the identifier used to autosave the field value and returns this field.
+        * 
+        * @param       string          $autosaveId     identifier used to autosave field value
+        *
+        * @return        WysiwygFormNode               this field
+        */
+       public function autosaveId($autosaveId) {
+               $this->autosaveId = $autosaveId;
+               
+               return $this;
+       }
+       
+       /**
+        * Returns the identifier used to autosave the field value. If autosave is disabled,
+        * an empty string is returned.
+        * 
+        * @return      string
+        */
+       public function getAutosaveId() {
+               return $this->autosaveId;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getObjectTypeDefinition() {
+               return 'com.woltlab.wcf.message';
+       }
+       
+       /**
+        * Returns the last time the field has been edited. If no last edit time has
+        * been set, `0` is returned.
+        * 
+        * @return      int
+        */
+       public function getLastEditTime() {
+               return $this->lastEditTime;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function hasSaveValue() {
+               return false;
+       }
+       
+       /**
+        * Sets the last time this field has been edited and returns this field.
+        * 
+        * @param       int     $lastEditTime   last time field has been edited
+        *
+        * @return        WysiwygFormNode       this field
+        */
+       public function lastEditTime($lastEditTime) {
+               $this->lastEditTime = $lastEditTime;
+               
+               return $this;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function populate() {
+               parent::populate();
+               
+               $this->getDocument()->getDataHandler()->add(new CustomFormFieldDataProcessor('wysiwyg', function(IFormDocument $document, array $parameters) {
+                       if ($this->checkDependencies()) {
+                               $parameters[$this->getObjectProperty() . '_htmlInputProcessor'] = $this->htmlInputProcessor;
+                       }
+                       
+                       return $parameters;
+               }));
+               
+               return $this;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function readValue() {
+               if ($this->getDocument()->hasRequestData($this->getPrefixedId())) {
+                       $value = $this->getDocument()->getRequestData($this->getPrefixedId());
+                       
+                       if (is_string($value)) {
+                               $this->value = StringUtil::trim($value);
+                       }
+               }
+               
+               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
+        */
+       public function validate() {
+               if ($this->isRequired() && $this->getValue() === '') {
+                       $this->addValidationError(new FormFieldValidationError('empty'));
+               }
+               else {
+                       $this->validateMinimumLength($this->getValue());
+                       $this->validateMaximumLength($this->getValue());
+               }
+               
+               $this->htmlInputProcessor = new HtmlInputProcessor();
+               $this->htmlInputProcessor->process($this->getValue(), $this->getObjectType()->objectType);
+               
+               parent::validate();
+       }
+}
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 (file)
index 0000000..582da24
--- /dev/null
@@ -0,0 +1,80 @@
+<?php
+namespace wcf\system\form\builder\field\wysiwyg;
+use wcf\data\smiley\Smiley;
+use wcf\system\form\builder\field\AbstractFormField;
+
+/**
+ * Implementation of a form field for the list smilies of a certain category used by a wysiwyg
+ * form container.
+ * 
+ * This is no really a form field in that it does not read any data but only prints data.
+ * 
+ * @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 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;
+       }
+}
index 1bcdf8b810124743afd192ebb8c6cce8a361b7ba..85585b73bd54391b906f272174edb251f20a2c11 100644 (file)
 
 /* attachments tab in editor */
 .formAttachmentContent {
-       .formAttachmentList {
+       .formAttachmentList {
                display: flex;
                flex-wrap: wrap;
                margin-left: 0 !important;
        }
        
        @include screen-md-up {
-               .formAttachmentList {
+               .formAttachmentList {
                        margin-right: -20px;
                        
                        > li {
        
        > 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;
+               }
        }
 }
 
index 76aec308c198e96993fe3743f6e8cd96cba2a4b0..0021dbe59a6851af1121573b37b51857a883e3e6 100644 (file)
@@ -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;
                                padding: 10px 20px;
                                
                                @include userSelectNone;
+                               @include wcfFontDefault;
                                
                                @include screen-md-up {
                                        > .icon {