Add GUI support for bbcode package installation plugin
authorMatthias Schmidt <gravatronics@live.com>
Sun, 9 Sep 2018 08:02:58 +0000 (10:02 +0200)
committerMatthias Schmidt <gravatronics@live.com>
Sun, 9 Sep 2018 08:02:58 +0000 (10:02 +0200)
See #2545

wcfsetup/install/files/acp/templates/__bbcodeAttributesFormField.tpl [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Container/Default.js
wcfsetup/install/files/lib/system/form/builder/field/bbcode/BBCodeAttributesFormField.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/package/plugin/BBCodePackageInstallationPlugin.class.php
wcfsetup/install/lang/en.xml

diff --git a/wcfsetup/install/files/acp/templates/__bbcodeAttributesFormField.tpl b/wcfsetup/install/files/acp/templates/__bbcodeAttributesFormField.tpl
new file mode 100644 (file)
index 0000000..e04fc99
--- /dev/null
@@ -0,0 +1,231 @@
+{capture assign='attributeTemplate'}
+       <section class="section">
+               <h2 class="sectionTitle"><span class="icon icon16 fa-times pointer jsDeleteButton jsTooltip" title="{lang}wcf.global.button.delete{/lang}"></span> <span>{lang __literal=true}wcf.acp.bbcode.numberedAttribute{/lang}</span></h2>
+               
+               <dl>
+                       <dt>
+                               <label for="{@$field->getPrefixedId()}[{ldelim}@$attributeNumber}][attributeHtml]">{lang}wcf.acp.bbcode.attribute.attributeHtml{/lang}</label>
+                       </dt>
+                       <dd>
+                               <input type="text" id="{@$field->getPrefixedId()}[{ldelim}@$attributeNumber}][attributeHtml]" name="{@$field->getPrefixedId()}[{ldelim}@$attributeNumber}][attributeHtml]" value="" class="long" maxlength="255">
+                       </dd>
+               </dl>
+               
+               <dl>
+                       <dt>
+                               <label for="{@$field->getPrefixedId()}[{ldelim}@$attributeNumber}][validationPattern]">{lang}wcf.acp.bbcode.attribute.validationPattern{/lang}</label>
+                       </dt>
+                       <dd>
+                               <input type="text" id="{@$field->getPrefixedId()}[{ldelim}@$attributeNumber}][validationPattern]" name="{@$field->getPrefixedId()}[{ldelim}@$attributeNumber}][validationPattern]" value="" class="long" maxlength="255">
+                       </dd>
+               </dl>
+               
+               <dl>
+                       <dt>
+                               <label for="{@$field->getPrefixedId()}_required_{ldelim}@$attributeNumber}">{lang}wcf.acp.bbcode.attribute.required{/lang}</label>
+                       </dt>
+                       <dd>
+                               <ol class="flexibleButtonGroup">
+                                       <li>
+                                               <input type="radio" {*
+                                                       *}id="{@$field->getPrefixedId()}_required_{ldelim}@$attributeNumber}" {*
+                                                       *}name="{@$field->getPrefixedId()}[{ldelim}@$attributeNumber}][required]" {*
+                                                       *}value="1" {*
+                                                       *}data-no-input-id="{@$field->getPrefixedId()}_required_{ldelim}@$attributeNumber}_no"{*
+                                               *}>
+                                               <label for="{@$field->getPrefixedId()}_required_{ldelim}@$attributeNumber}" class="green">
+                                                       <span class="icon icon16 fa-check"></span> {lang}wcf.global.form.boolean.yes{/lang}
+                                               </label>
+                                       </li>
+                                       <li>
+                                               <input type="radio" {*
+                                                       *}id="{@$field->getPrefixedId()}_required_{ldelim}@$attributeNumber}_no" {*
+                                                       *}name="{@$field->getPrefixedId()}[{ldelim}@$attributeNumber}][required]" {*
+                                                       *}value="0" {*
+                                                       *}name="{@$field->getPrefixedId()}[{ldelim}@$attributeNumber}][required]" {*
+                                                       *}checked{*
+                                               *}>
+                                               <label for="{@$field->getPrefixedId()}_required_{ldelim}@$attributeNumber}_no" class="red">
+                                                       <span class="icon icon16 fa-times"></span> {lang}wcf.global.form.boolean.no{/lang}
+                                               </label>
+                                       </li>
+                               </ol>
+                       </dd>
+               </dl>
+               
+               <dl>
+                       <dt>
+                               <label for="{@$field->getPrefixedId()}_useText_{ldelim}@$attributeNumber}">{lang}wcf.acp.bbcode.attribute.useText{/lang}</label>
+                       </dt>
+                       <dd>
+                               <ol class="flexibleButtonGroup">
+                                       <li>
+                                               <input type="radio" {*
+                                                       *}id="{@$field->getPrefixedId()}_useText_{ldelim}@$attributeNumber}" {*
+                                                       *}name="{@$field->getPrefixedId()}[{ldelim}@$attributeNumber}][useText]" {*
+                                                       *}value="1" {*
+                                                       *}data-no-input-id="{@$field->getPrefixedId()}_useText_{ldelim}@$attributeNumber}_no"{*
+                                               *}>
+                                               <label for="{@$field->getPrefixedId()}_useText_{ldelim}@$attributeNumber}" class="green">
+                                                       <span class="icon icon16 fa-check"></span> {lang}wcf.global.form.boolean.yes{/lang}
+                                               </label>
+                                       </li>
+                                       <li>
+                                               <input type="radio" {*
+                                                       *}id="{@$field->getPrefixedId()}_useText_{ldelim}@$attributeNumber}_no" {*
+                                                       *}name="{@$field->getPrefixedId()}[{ldelim}@$attributeNumber}][useText]" {*
+                                                       *}value="0" {*
+                                                       *}name="{@$field->getPrefixedId()}[{ldelim}@$attributeNumber}][useText]" {*
+                                                       *}checked{*
+                                               *}>
+                                               <label for="{@$field->getPrefixedId()}_useText_{ldelim}@$attributeNumber}_no" class="red">
+                                                       <span class="icon icon16 fa-times"></span> {lang}wcf.global.form.boolean.no{/lang}
+                                               </label>
+                                       </li>
+                               </ol>
+                       </dd>
+               </dl>
+               
+               {event name='attributeFields'}
+       </section>
+{/capture}
+
+<script data-relocate="true">
+       require(['Dom/ChangeListener', 'Dom/Traverse', 'Dom/Util', 'WoltLabSuite/Core/Template'], function(DomChangeListener, DomTraverse, DomUtil, Template) {
+               var parentContainer = elById('{@$field->getParent()->getPrefixedId()}Container');
+               
+               var parentTitle = DomTraverse.childBySel(parentContainer, 'h2.sectionTitle');
+               parentTitle.innerHTML = '<span class="icon icon16 fa-plus pointer jsTooltip" id="{@$field->getPrefixedId()}AddAttribute" title="{lang}wcf.global.button.add{/lang}"></span> ' + parentTitle.innerHTML;
+               
+               DomChangeListener.trigger();
+               
+               var addDeleteButtonListeners = function() {
+                       var deleteButtonCallback = function(event) {
+                               elRemove(event.currentTarget.closest('section'));
+                       };
+                       
+                       elBySelAll('.jsDeleteButton', parentContainer, function(deleteButton) {
+                               deleteButton.classList.remove('jsDeleteButton');
+                               deleteButton.addEventListener('click', deleteButtonCallback);
+                       });
+               };
+               addDeleteButtonListeners();
+               
+               var attributeNumber = {if $field->getValue()|empty}0{else}{$field->getValue()|count}{/if};
+               var attributeTemplate = new Template('{@$attributeTemplate|encodeJS}');
+               
+               elById('{@$field->getPrefixedId()}AddAttribute').addEventListener('click', function(event) {
+                       var html = attributeTemplate.fetch({ attributeNumber: attributeNumber++ });
+                       
+                       DomUtil.insertHtml(html, parentContainer, 'append');
+                       
+                       addDeleteButtonListeners();
+                       
+                       DomChangeListener.trigger();
+               });
+       });
+</script>
+
+{foreach from=$field->getValue() key=attributeNumber item=attributeData name=bbcodeAttributes}
+       <section class="section">
+               <h2 class="sectionTitle"><span class="icon icon16 fa-times pointer jsDeleteButton jsTooltip" title="{lang}wcf.global.button.delete{/lang}"></span> <span>{lang}wcf.acp.bbcode.numberedAttribute{/lang}</span></h2>
+               
+               <dl>
+                       <dt>
+                               <label for="{@$field->getPrefixedId()}[{@$attributeNumber}][attributeHtml]">{lang}wcf.acp.bbcode.attribute.attributeHtml{/lang}</label>
+                       </dt>
+                       <dd>
+                               <input type="text" name="{@$field->getPrefixedId()}[{@$attributeNumber}][attributeHtml]" value="{if $attributeData[attributeHtml]|isset}{$attributeData[attributeHtml]}{/if}" class="long" maxlength="255">
+                       </dd>
+               </dl>
+               
+               {assign var='__attributeValidationError' value=null}
+               {foreach from=$field->getValidationErrors() item=validationError}
+                       {if $validationError->getType() === $field->getPrefixedId()|concat:'_validationPattern_':$attributeNumber}
+                               {assign var='__attributeValidationError' value=$validationError}
+                       {/if}
+               {/foreach}
+               <dl{if $__attributeValidationError !== null} class="formError"{/if}>
+                       <dt>
+                               <label for="{@$field->getPrefixedId()}[{@$attributeNumber}][validationPattern]">{lang}wcf.acp.bbcode.attribute.validationPattern{/lang}</label>
+                       </dt>
+                       <dd>
+                               <input type="text" name="{@$field->getPrefixedId()}[{@$attributeNumber}][validationPattern]" value="{if $attributeData[validationPattern]|isset}{$attributeData[validationPattern]}{/if}" class="long" maxlength="255">
+                               {if $__attributeValidationError !== null}
+                                       <small class="innerError">{@$__attributeValidationError->getMessage()}</small>
+                               {/if}
+                       </dd>
+               </dl>
+               
+               <dl>
+                       <dt>
+                               <label for="{@$field->getPrefixedId()}_required_{@$attributeNumber}">{lang}wcf.acp.bbcode.attribute.required{/lang}</label>
+                       </dt>
+                       <dd>
+                               <ol class="flexibleButtonGroup">
+                                       <li>
+                                               <input type="radio" {*
+                                                       *}id="{@$field->getPrefixedId()}_required_{@$attributeNumber}" {*
+                                                       *}name="{@$field->getPrefixedId()}[{@$attributeNumber}][required]" {*
+                                                       *}value="1" {*
+                                                       *}data-no-input-id="{@$field->getPrefixedId()}_required_{@$attributeNumber}_no"{*
+                                                       *}{if !$attributeData[required]|empty} checked{/if}{*
+                                               *}>
+                                               <label for="{@$field->getPrefixedId()}_required_{@$attributeNumber}" class="green">
+                                                       <span class="icon icon16 fa-check"></span> {lang}wcf.global.form.boolean.yes{/lang}
+                                               </label>
+                                       </li>
+                                       <li>
+                                               <input type="radio" {*
+                                                       *}id="{@$field->getPrefixedId()}_required_{@$attributeNumber}_no" {*
+                                                       *}name="{@$field->getPrefixedId()}[{@$attributeNumber}][required]" {*
+                                                       *}value="0" {*
+                                                       *}name="{@$field->getPrefixedId()}[{@$attributeNumber}][required]"{*
+                                                       *}{if $attributeData[required]|empty} checked{/if}{*
+                                               *}>
+                                               <label for="{@$field->getPrefixedId()}_required_{@$attributeNumber}_no" class="red">
+                                                       <span class="icon icon16 fa-times"></span> {lang}wcf.global.form.boolean.no{/lang}
+                                               </label>
+                                       </li>
+                               </ol>
+                       </dd>
+               </dl>
+               
+               <dl>
+                       <dt>
+                               <label for="{@$field->getPrefixedId()}_useText_{@$attributeNumber}">{lang}wcf.acp.bbcode.attribute.useText{/lang}</label>
+                       </dt>
+                       <dd>
+                               <ol class="flexibleButtonGroup">
+                                       <li>
+                                               <input type="radio" {*
+                                                       *}id="{@$field->getPrefixedId()}_useText_{@$attributeNumber}" {*
+                                                       *}name="{@$field->getPrefixedId()}[{@$attributeNumber}][useText]" {*
+                                                       *}value="1" {*
+                                                       *}data-no-input-id="{@$field->getPrefixedId()}_useText_{@$attributeNumber}_no"{*
+                                                       *}{if !$attributeData[useText]|empty} checked{/if}{*
+                                               *}>
+                                               <label for="{@$field->getPrefixedId()}_useText_{@$attributeNumber}" class="green">
+                                                       <span class="icon icon16 fa-check"></span> {lang}wcf.global.form.boolean.yes{/lang}
+                                               </label>
+                                       </li>
+                                       <li>
+                                               <input type="radio" {*
+                                                       *}id="{@$field->getPrefixedId()}_useText_{@$attributeNumber}_no" {*
+                                                       *}name="{@$field->getPrefixedId()}[{@$attributeNumber}][useText]" {*
+                                                       *}value="0" {*
+                                                       *}name="{@$field->getPrefixedId()}[{@$attributeNumber}][useText]"{*
+                                                       *}{if $attributeData[useText]|empty} checked{/if}{*
+                                               *}>
+                                               <label for="{@$field->getPrefixedId()}_useText_{@$attributeNumber}_no" class="red">
+                                                       <span class="icon icon16 fa-times"></span> {lang}wcf.global.form.boolean.no{/lang}
+                                               </label>
+                                       </li>
+                               </ol>
+                               <small>{lang}wcf.acp.bbcode.attribute.useText.description{/lang}</small>
+                       </dd>
+               </dl>
+               
+               {event name='attributeFields'}
+       </section>
+{/foreach}
index c2ca135b5c5e069b3a8586f114b39f640fdb6d90..112a6ee77889c1f2a63905703f49990646e9cf06 100644 (file)
@@ -23,6 +23,10 @@ define(['./Abstract', 'Core', '../Manager'], function(Abstract, Core, Dependency
                 * @see WoltLabSuite/Core/Form/Builder/Field/Dependency/Container/Default#checkContainer
                 */
                checkContainer: function() {
+                       if (elDataBool(this._container, 'ignore-dependencies')) {
+                               return;
+                       }
+                       
                        // only consider containers that have not been hidden by their own dependencies
                        if (DependencyManager.isHiddenByDependencies(this._container)) {
                                return;
diff --git a/wcfsetup/install/files/lib/system/form/builder/field/bbcode/BBCodeAttributesFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/bbcode/BBCodeAttributesFormField.class.php
new file mode 100644 (file)
index 0000000..131899b
--- /dev/null
@@ -0,0 +1,87 @@
+<?php
+namespace wcf\system\form\builder\field\bbcode;
+use wcf\system\form\builder\field\AbstractFormField;
+use wcf\system\form\builder\field\TDefaultIdFormField;
+use wcf\system\form\builder\field\validation\FormFieldValidationError;
+use wcf\system\Regex;
+
+/**
+ * Implementation of a form field for the attributes of a form field.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2018 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Form\Builder\Field\BBCode
+ * @since      3.2
+ */
+class BBCodeAttributesFormField extends AbstractFormField {
+       use TDefaultIdFormField;
+       
+       /**
+        * @inheritDoc
+        */
+       protected $templateName = '__bbcodeAttributesFormField';
+       
+       /**
+        * @inheritDoc
+        */
+       protected $__value = [];
+       
+       /**
+        * @inheritDoc
+        */
+       protected static function getDefaultId() {
+               return 'attributes';
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function readValue() {
+               if ($this->getDocument()->hasRequestData($this->getPrefixedId()) && is_array($this->getDocument()->getRequestData($this->getPrefixedId()))) {
+                       $this->__value = $this->getDocument()->getRequestData($this->getPrefixedId());
+               }
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function validate() {
+               foreach ($this->getValue() as $attributeNumber => $attributeData) {
+                       if (!empty($attributeData['attributeHtml']) && mb_strlen($attributeData['attributeHtml']) > 255) {
+                               $this->addValidationError(
+                                       new FormFieldValidationError(
+                                               $this->getPrefixedId() . '_attributeHtml_' . $attributeNumber,
+                                               'wcf.form.field.text.error.maximumLength',
+                                               [
+                                                       'length' => mb_strlen($attributeData['attributeHtml']),
+                                                       'maximumLength' => 255
+                                               ]
+                                       )
+                               );
+                       }
+                       if (!empty($attributeData['validationPattern'])) {
+                               if (mb_strlen($attributeData['validationPattern']) > 255) {
+                                       $this->addValidationError(
+                                               new FormFieldValidationError(
+                                                       $this->getPrefixedId() . '_validationPattern_' . $attributeNumber,
+                                                       'wcf.form.field.text.error.maximumLength',
+                                                       [
+                                                               'length' => mb_strlen($attributeData['validationPattern']),
+                                                               'maximumLength' => 255
+                                                       ]
+                                               )
+                                       );
+                               }
+                               else if (!Regex::compile($attributeData['validationPattern'])->isValid()) {
+                                       $this->addValidationError(
+                                               new FormFieldValidationError(
+                                                       $this->getPrefixedId() . '_validationPattern_' . $attributeNumber,
+                                                       'wcf.acp.bbcode.attribute.validationPattern.error.invalid'
+                                               )
+                                       );
+                               }
+                       }
+               }
+       }
+}
index c03a3153a4ebfe08efe85803bceae58c9de29e8e..bdb8f2c999206206f40b321e2a7cb1d7ae5e8d3c 100644 (file)
@@ -3,10 +3,28 @@ namespace wcf\system\package\plugin;
 use wcf\data\bbcode\attribute\BBCodeAttributeEditor;
 use wcf\data\bbcode\BBCode;
 use wcf\data\bbcode\BBCodeEditor;
+use wcf\data\bbcode\BBCodeList;
 use wcf\data\package\PackageCache;
+use wcf\system\bbcode\IBBCode;
 use wcf\system\database\util\PreparedStatementConditionBuilder;
-use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
+use wcf\system\devtools\pip\IDevtoolsPipEntryList;
+use wcf\system\devtools\pip\IGuiPackageInstallationPlugin;
+use wcf\system\devtools\pip\TXmlGuiPackageInstallationPlugin;
 use wcf\system\exception\SystemException;
+use wcf\system\form\builder\container\FormContainer;
+use wcf\system\form\builder\field\bbcode\BBCodeAttributesFormField;
+use wcf\system\form\builder\field\BooleanFormField;
+use wcf\system\form\builder\field\ClassNameFormField;
+use wcf\system\form\builder\field\data\VoidFormFieldDataProcessor;
+use wcf\system\form\builder\field\dependency\NonEmptyFormFieldDependency;
+use wcf\system\form\builder\field\dependency\ValueFormFieldDependency;
+use wcf\system\form\builder\field\IconFormField;
+use wcf\system\form\builder\field\RadioButtonFormField;
+use wcf\system\form\builder\field\TextFormField;
+use wcf\system\form\builder\field\validation\FormFieldValidationError;
+use wcf\system\form\builder\field\validation\FormFieldValidator;
+use wcf\system\form\builder\field\validation\FormFieldValidatorUtil;
+use wcf\system\form\builder\IFormDocument;
 use wcf\system\WCF;
 use wcf\util\StringUtil;
 
@@ -18,7 +36,9 @@ use wcf\util\StringUtil;
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @package    WoltLabSuite\Core\Acp\Package\Plugin
  */
-class BBCodePackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
+class BBCodePackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IGuiPackageInstallationPlugin {
+       use TXmlGuiPackageInstallationPlugin;
+       
        /**
         * @inheritDoc
         */
@@ -175,6 +195,10 @@ class BBCodePackageInstallationPlugin extends AbstractXMLPackageInstallationPlug
                
                if (!empty($this->attributes)) {
                        foreach ($this->attributes as $bbcodeID => $bbcodeAttributes) {
+                               if ($bbcodeID != intval($bbcodeID)) {
+                                       $bbcodeID = BBCode::getBBCodeByTag($bbcodeID)->bbcodeID;
+                               }
+                               
                                foreach ($bbcodeAttributes as $attributeNo => $attribute) {
                                        BBCodeAttributeEditor::create([
                                                'bbcodeID' => $bbcodeID,
@@ -199,8 +223,344 @@ class BBCodePackageInstallationPlugin extends AbstractXMLPackageInstallationPlug
        
        /**
         * @inheritDoc
+        * @since       3.1
         */
        public static function getSyncDependencies() {
                return [];
        }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function getElementData(\DOMElement $element, $saveData = false) {
+               $data = [
+                       'attributes' => [],
+                       'bbcodeTag' => $element->getAttribute('name'),
+                       'packageID' => $this->installation->getPackage()->packageID,
+                       'originIsSystem' => 1
+               ];
+               
+               $optionalElements = [
+                       'buttonLabel' => 'buttonlabel',
+                       'className' => 'classname',
+                       'htmlClose' => 'htmlclose',
+                       'htmlOpen' => 'htmlopen',
+                       'isBlockElement' => 'isBlockElement',
+                       'isSourceCode' => 'sourcecode',
+                       'wysiwygIcon' => 'wysiwygicon'
+               ];
+               foreach ($optionalElements as $arrayKey => $elementName) {
+                       $child = $element->getElementsByTagName($elementName)->item(0);
+                       if ($child !== null) {
+                               $data[$arrayKey] = $child->nodeValue;
+                       }
+               }
+               
+               if (!empty($data['wysiwygIcon']) && !empty($data['buttonLabel'])) {
+                       $data['showButton'] = 1;
+               }
+               
+               // attributes
+               $attributes = $element->getElementsByTagName('attributes')->item(0);
+               if ($attributes !== null) {
+                       $optionalAttributeElements = [
+                               'attributeHtml' => 'html',
+                               'required' => 'required',
+                               'useText' => 'usetext',
+                               'validationPattern' => 'validationpattern',
+                       ];
+                       
+                       /** @var \DOMElement $attribute */
+                       foreach ($attributes->childNodes as $attribute) {
+                               $attributeData = ['name' => $attribute->nodeValue];
+                               
+                               foreach ($optionalAttributeElements as $arrayKey => $elementName) {
+                                       $child = $attribute->getElementsByTagName($elementName)->item(0);
+                                       if ($child !== null) {
+                                               $attributeData[$arrayKey] = $child->nodeValue;
+                                       }
+                               }
+                               
+                               $data['attributes'][] = $attributeData;
+                       }
+               }
+               
+               return $data;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function getElementIdentifier(\DOMElement $element) {
+               return $element->getAttribute('name');
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function addFormFields(IFormDocument $form) {
+               /** @var FormContainer $dataContainer */
+               $dataContainer = $form->getNodeById('data');
+               
+               // shared validators
+               $htmlValidator = new FormFieldValidator('format', function(TextFormField $formField) {
+                       $value = $formField->getValue();
+                       if ($value !== null && $value !== '') {
+                               if (in_array(substr($formField->getValue(), 0, 1), ['<', '>'])) {
+                                       $formField->addValidationError(
+                                               new FormFieldValidationError(
+                                                       'leadingBracket',
+                                                       'wcf.acp.pip.bbcode.htmlOpen.error.leadingBracket'
+                                               )
+                                       );
+                               }
+                               else if (in_array(substr($formField->getValue(), -1, 1), ['<', '>'])) {
+                                       $formField->addValidationError(
+                                               new FormFieldValidationError(
+                                                       'trailingBracket',
+                                                       'wcf.acp.pip.bbcode.htmlOpen.error.trailingBracket'
+                                               )
+                                       );
+                               }
+                       }
+               });
+               
+               // add fields
+               $dataContainer->appendChildren([
+                       TextFormField::create('bbcodeTag')
+                               ->objectProperty('name')
+                               ->label('wcf.acp.pip.bbcode.bbcodeTag')
+                               ->description('wcf.acp.pip.bbcode.bbcodeTag.description')
+                               ->required()
+                               ->maximumLength(191)
+                               ->addValidator(FormFieldValidatorUtil::getRegularExpressionValidator(
+                                       '[a-z0-9]+',
+                                       'wcf.acp.pip.bbcode.bbcodeTag'
+                               ))
+                               ->addValidator(new FormFieldValidator('allNone', function(TextFormField $formField) {
+                                       if ($formField->getValue() === 'all' || $formField->getValue() === 'none') {
+                                               $formField->addValidationError(
+                                                       new FormFieldValidationError(
+                                                               'allNone',
+                                                               'wcf.acp.pip.bbcode.bbcodeTag.error.allNone'
+                                                       )
+                                               );
+                                       }
+                               }))
+                               ->addValidator(new FormFieldValidator('uniqueness', function(TextFormField $formField) {
+                                       if (
+                                               $formField->getDocument()->getFormMode() === IFormDocument::FORM_MODE_CREATE ||
+                                               $this->editedEntry->getAttribute('name') !== $formField->getValue()
+                                       ) {
+                                               $providerList = new BBCodeList();
+                                               $providerList->getConditionBuilder()->add('bbcodeTag = ?', [$formField->getValue()]);
+                                               
+                                               if ($providerList->countObjects() > 0) {
+                                                       $formField->addValidationError(
+                                                               new FormFieldValidationError(
+                                                                       'notUnique',
+                                                                       'wcf.acp.pip.bbcode.bbcodeTag.error.notUnique'
+                                                               )
+                                                       );
+                                               }
+                                       }
+                               })),
+                       
+                       TextFormField::create('htmlOpen')
+                               ->objectProperty('htmlopen')
+                               ->label('wcf.acp.pip.bbcode.htmlOpen')
+                               ->description('wcf.acp.pip.bbcode.htmlOpen.description')
+                               ->maximumLength(255)
+                               ->addValidator($htmlValidator),
+                       
+                       TextFormField::create('htmlClose')
+                               ->objectProperty('htmlclose')
+                               ->label('wcf.acp.pip.bbcode.htmlClose')
+                               ->description('wcf.acp.pip.bbcode.htmlClose.description')
+                               ->maximumLength(255)
+                               // note: the language items for the opening tag are reused 
+                               ->addValidator($htmlValidator),
+                       
+                       BooleanFormField::create('isBlockElement')
+                               ->label('wcf.acp.pip.bbcode.isBlockElement')
+                               ->description('wcf.acp.pip.bbcode.isBlockElement.description'),
+                       
+                       BooleanFormField::create('isSourceCode')
+                               ->objectProperty('sourcecode')
+                               ->label('wcf.acp.pip.bbcode.isSourceCode')
+                               ->description('wcf.acp.pip.bbcode.isSourceCode.description'),
+                       
+                       ClassNameFormField::create()
+                               ->objectProperty('classname')
+                               ->implementedInterface(IBBCode::class),
+                       
+                       BooleanFormField::create('showButton')
+                               ->objectProperty('showbutton')
+                               ->label('wcf.acp.pip.bbcode.showButton')
+                               ->description('wcf.acp.pip.bbcode.showButton.description'),
+                       
+                       TextFormField::create('buttonLabel')
+                               ->objectProperty('buttonlabel')
+                               ->label('wcf.acp.pip.bbcode.buttonLabel')
+                               ->description('wcf.acp.pip.bbcode.buttonLabel.description')
+                               ->required()
+                               ->maximumLength(255),
+                       
+                       RadioButtonFormField::create('iconType')
+                               ->label('wcf.acp.pip.bbcode.iconType')
+                               ->options([
+                                       'filePath' => 'wcf.acp.pip.bbcode.iconType.filePath',
+                                       'fontAwesome' => 'wcf.acp.pip.bbcode.iconType.fontAwesome',
+                               ])
+                               ->required()
+                               ->value('fontAwesome'),
+                       
+                       TextFormField::create('iconPath')
+                               ->objectProperty('wysiwygicon')
+                               ->label('wcf.acp.pip.bbcode.iconPath')
+                               ->description('wcf.acp.pip.bbcode.iconPath.description')
+                               ->required()
+                               ->maximumLength(255)
+                               ->addValidator(new FormFieldValidator('fileExists', function(TextFormField $formField) {
+                                       if (!file_exists(WCF_DIR . 'icon/' . $formField->getValue())) {
+                                               $formField->addValidationError(
+                                                       new FormFieldValidationError(
+                                                               'fileDoesNotExist',
+                                                               'wcf.acp.pip.bbcode.iconPath.error.fileDoesNotExist'
+                                                       )
+                                               );
+                                       }
+                               })),
+                       
+                       IconFormField::create('fontAwesomeIcon')
+                               ->objectProperty('wysiwygicon')
+                               ->label('wcf.acp.pip.bbcode.wysiwygIcon')
+                               ->required()
+               ]);
+               
+               $form->appendChild(
+                       FormContainer::create('bbcodeAttributes')
+                               ->attribute('data-ignore-dependencies', 1)
+                               ->label('wcf.acp.pip.bbcode.attributes')
+                               ->appendChild(
+                                       BBCodeAttributesFormField::create()
+                               )
+               );
+               
+               // discard the `iconType` value as it is only used to distinguish the two icon input fields
+               $form->getDataHandler()->add(new VoidFormFieldDataProcessor('iconType'));
+               
+               // add dependencies
+               /** @var BooleanFormField $showButton */
+               $showButton = $dataContainer->getNodeById('showButton');
+               $iconType = $dataContainer->getNodeById('iconType');
+               
+               $dataContainer->getNodeById('buttonLabel')->addDependency(
+                       NonEmptyFormFieldDependency::create('showButton')
+                               ->field($showButton)
+               );
+               $iconType->addDependency(
+                       NonEmptyFormFieldDependency::create('showButton')
+                               ->field($showButton)
+               );
+               $dataContainer->getNodeById('iconPath')->addDependency(
+                       ValueFormFieldDependency::create('iconType')
+                               ->field($iconType)
+                               ->values(['filePath'])
+               );
+               $dataContainer->getNodeById('fontAwesomeIcon')->addDependency(
+                       ValueFormFieldDependency::create('iconType')
+                               ->field($iconType)
+                               ->values(['fontAwesome'])
+               );
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function setEntryListKeys(IDevtoolsPipEntryList $entryList) {
+               $entryList->setKeys([
+                       'bbcodeTag' => 'wcf.acp.pip.bbcode.bbcodeTag',
+                       'htmlOpen' => 'wcf.acp.pip.bbcode.htmlOpen',
+                       'className' => 'wcf.form.field.className'
+               ]);
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function sortDocument(\DOMDocument $document) {
+               $this->sortImportDelete($document);
+               
+               $compareFunction = static::getSortFunction([
+                       [
+                               'isAttribute' => 1,
+                               'name' => 'name'
+                       ]
+               ]);
+               
+               $this->sortChildNodes($document->getElementsByTagName('import'), $compareFunction);
+               $this->sortChildNodes($document->getElementsByTagName('delete'), $compareFunction);
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function writeEntry(\DOMDocument $document, IFormDocument $form) {
+               $data = $form->getData()['data'];
+               
+               $bbcode = $document->createElement($this->tagName);
+               $bbcode->setAttribute('name', $data['name']);
+               
+               $fields = [
+                       'classname' => '',
+                       'htmlclose' => '',
+                       'htmlopen' => '',
+                       'isBlockElement' => 0,
+                       'sourcecode' => 0,
+                       'buttonlabel' => '',
+                       'wysiwygicon' => ''
+               ];
+               
+               foreach ($fields as $field => $defaultValue) {
+                       if (isset($data[$field]) && $data[$field] !== $defaultValue) {
+                               $bbcode->appendChild($document->createElement($field, (string) $data[$field]));
+                       }
+               }
+               
+               if (!empty($data['attributes'])) {
+                       $attributes = $document->createElement('attributes');
+                       $bbcode->appendChild($attributes);
+                       
+                       foreach ($data['attributes'] as $attributeNumber => $attributeData) {
+                               $attribute = $document->createElement('attribute');
+                               $attribute->setAttribute('name', (string) $attributeNumber);
+                               
+                               if (!empty($attributeData['attributeHtml'])) {
+                                       $attribute->appendChild($document->createElement('html', $attributeData['attributeHtml']));
+                               }
+                               if (!empty($attributeData['validationPattern'])) {
+                                       $attribute->appendChild($document->createElement('validationpattern', $attributeData['validationPattern']));
+                               }
+                               if (!empty($attributeData['required'])) {
+                                       $attribute->appendChild($document->createElement('required', $attributeData['required']));
+                               }
+                               if (!empty($attributeData['useText'])) {
+                                       $attribute->appendChild($document->createElement('usetext', $attributeData['useText']));
+                               }
+                               
+                               $attributes->appendChild($attribute);
+                       }
+               }
+               
+               $document->getElementsByTagName('import')->item(0)->appendChild($bbcode);
+               
+               return $bbcode;
+       }
 }
index a462be9ce5dfaed7f64407b8a989d5c2300bdf49..9a52d3def536cd3cbfff0f2424cf1a77535da3ed 100644 (file)
                <item name="wcf.acp.bbcode.add"><![CDATA[Add BBCode]]></item>
                <item name="wcf.acp.bbcode.add.userGroupOptionInfo"><![CDATA[Newly created BBCodes will be accessible for everyone. You can restrict usage by editing the specific user group permissions.]]></item>
                <item name="wcf.acp.bbcode.attribute"><![CDATA[Attribute]]></item>
+               <item name="wcf.acp.bbcode.numberedAttribute"><![CDATA[Attribute {#$attributeNumber}]]></item>
                <item name="wcf.acp.bbcode.attribute.attributeHtml"><![CDATA[Attribute HTML code]]></item>
                <item name="wcf.acp.bbcode.attribute.validationPattern"><![CDATA[Regular Expression for Validation]]></item>
                <item name="wcf.acp.bbcode.attribute.validationPattern.error.invalid"><![CDATA[The regular expression failed to validate.]]></item>
@@ -2142,6 +2143,33 @@ If you have <strong>already bought the licenses for the listed apps</strong>, th
                <item name="wcf.acp.pip.abstractOption.options.optionType.integer.suffix.description"><![CDATA[When setting the option value, the entered suffix (using the language item <code>wcf.acp.option.suffix.{literal}{$suffix}{/literal}</code>) is shown behind the input field.]]></item>
                <item name="wcf.acp.pip.abstractOption.options.optionType.text.minLength"><![CDATA[Minimum Length]]></item>
                <item name="wcf.acp.pip.abstractOption.options.optionType.text.maxLength"><![CDATA[Maximum Length]]></item>
+               <item name="wcf.acp.pip.bbcode.bbcodeTag"><![CDATA[BBCode Identifier]]></item>
+               <item name="wcf.acp.pip.bbcode.bbcodeTag.description"><![CDATA[The textual BBCode identifier is used to identify BBCodes, for example when updating BBCodes. The identifier may only contain lowercase letters and numbers.]]></item>
+               <item name="wcf.acp.pip.bbcode.bbcodeTag.error.allNone"><![CDATA[The identifier may neither be <code>all</code> nor <code>none</code>.]]></item>
+               <item name="wcf.acp.pip.bbcode.bbcodeTag.error.format"><![CDATA[The identifier may only contain lowercase letters and numbers.]]></item>
+               <item name="wcf.acp.pip.bbcode.bbcodeTag.error.notUnique"><![CDATA[The entered identifier is already used by another BBCode.]]></item>
+               <item name="wcf.acp.pip.bbcode.htmlOpen"><![CDATA[Opening HTML tag]]></item>
+               <item name="wcf.acp.pip.bbcode.htmlOpen.description"><![CDATA[Opening tag of the HTML element wrapping the BBCode’s content (without leading or trailing angle brackets).]]></item>
+               <item name="wcf.acp.pip.bbcode.htmlOpen.error.leadingBracket"><![CDATA[The tag may not begin with an angle bracket.]]></item>
+               <item name="wcf.acp.pip.bbcode.htmlOpen.error.trailingBracket"><![CDATA[The tag may not end with an angle bracket.]]></item>
+               <item name="wcf.acp.pip.bbcode.htmlClose"><![CDATA[Closing HTML tag]]></item>
+               <item name="wcf.acp.pip.bbcode.htmlClose.description"><![CDATA[Closing tag of the HTML element wrapping the BBCode’s content (without leading or trailing angle brackets).]]></item>
+               <item name="wcf.acp.pip.bbcode.attributes"><![CDATA[Attributes]]></item>
+               <item name="wcf.acp.pip.bbcode.isBlockElement"><![CDATA[BBCode represents block element]]></item>
+               <item name="wcf.acp.pip.bbcode.isBlockElement.description"><![CDATA[Block elements can contain line breaks and paragraphs, and generally take up the entire available width. This option must not be selected for inline elements.]]></item>
+               <item name="wcf.acp.pip.bbcode.isSourceCode"><![CDATA[Content represents source code]]></item>
+               <item name="wcf.acp.pip.bbcode.isSourceCode.description"><![CDATA[The content of the BBCode will not be parsed.]]></item>
+               <item name="wcf.acp.pip.bbcode.showButton"><![CDATA[Display button in WYSIWYG editor]]></item>
+               <item name="wcf.acp.pip.bbcode.showButton.description"><![CDATA[The BBCode will be easily available by clicking on the associated button in the WYSIWYG editor toolbar.]]></item>
+               <item name="wcf.acp.pip.bbcode.buttonLabel"><![CDATA[Button Label]]></item>
+               <item name="wcf.acp.pip.bbcode.buttonLabel.description"><![CDATA[The label is shown when hovering over the button in the WYSIWYG editor toolbar.]]></item>
+               <item name="wcf.acp.pip.bbcode.iconType"><![CDATA[Button Icon Type]]></item>
+               <item name="wcf.acp.pip.bbcode.iconType.filePath"><![CDATA[Image]]></item>
+               <item name="wcf.acp.pip.bbcode.iconType.fontAwesome"><![CDATA[FontAwesome Icon]]></item>
+               <item name="wcf.acp.pip.bbcode.iconPath"><![CDATA[Path to icon file]]></item>
+               <item name="wcf.acp.pip.bbcode.iconPath.description"><![CDATA[The entered path has to be relative to <code>{'WCF_DIR'|constant}icon/</code>.]]></item>
+               <item name="wcf.acp.pip.bbcode.iconPath.error.fileDoesNotExist"><![CDATA[The entered file does not exist.]]></item>
+               <item name="wcf.acp.pip.bbcode.wysiwygIcon"><![CDATA[Icon]]></item>
        </category>
        
        <category name="wcf.acp.reactionType">
@@ -3469,8 +3497,8 @@ Email: {@$emailAddress} {* this line ends with a space *}
                <item name="wcf.form.field.option.error.nonExistent"><![CDATA[The following options do not exist: {implode from=$options item=option}<code>{$option}</code>{/implode}.]]></item>
                <item name="wcf.form.field.showOrder"><![CDATA[Position]]></item>
                <item name="wcf.form.field.showOrder.firstPosition"><![CDATA[(first position)]]></item>
-               <item name="wcf.form.field.text.error.maximumLength"><![CDATA[The entered text{if $language} for language “{$language}”{/if} contains {#$length} characters but it may not be longer than {#$maximumLength} characters.]]></item>
-               <item name="wcf.form.field.text.error.minimumLength"><![CDATA[The entered text{if $language} for language “{$language}”{/if} only contains {#$length} characters but it may not be shorter than {#$minimumLength} characters.]]></item>
+               <item name="wcf.form.field.text.error.maximumLength"><![CDATA[The entered text{if $language|isset} for language “{$language}”{/if} contains {#$length} characters but it may not be longer than {#$maximumLength} characters.]]></item>
+               <item name="wcf.form.field.text.error.minimumLength"><![CDATA[The entered text{if $language|isset} for language “{$language}”{/if} only contains {#$length} characters but it may not be shorter than {#$minimumLength} characters.]]></item>
                <item name="wcf.form.field.user.error.maxmimumMultiples"><![CDATA[You may not enter more than {#$maximumCount} users but {#$count} user{if $count != 1}s are{else} is{/if} given.]]></item>
                <item name="wcf.form.field.user.error.minimumMultiples"><![CDATA[You have to entered at least {#$minimumCount} users but only {#$count} user{if $count != 1}s are{else} is{/if} given.]]></item>
                <item name="wcf.form.field.user.error.nonExistent"><![CDATA[{if $nonExistentUsernames|isset}The following users do not exist: {implode from=$nonExistentUsernames item=nonExistentUsername}“{$nonExistentUsername}”{/implode}.{else}The entered user does not exists.{/if}]]></item>