config setting will be used.
+ * @since 4.5.0
+ */
+ public ?int $cacheDuration = null;
+
/**
* @var bool Whether to write out updated YAML changes at the end of the request
*/
@@ -1816,7 +1822,7 @@ private function _loadInternalConfig(): ReadOnlyProjectConfigData
$current = Json::decode(StringHelper::decdec($value));
}
return ProjectConfigHelper::cleanupConfig($data);
- }, null, $this->getCacheDependency());
+ }, $this->cacheDuration, $this->getCacheDependency());
return Craft::createObject(ReadOnlyProjectConfigData::class, [
'data' => $data,
diff --git a/src/services/Sections.php b/src/services/Sections.php
index 30aad29b9f1..b407c9ae499 100644
--- a/src/services/Sections.php
+++ b/src/services/Sections.php
@@ -9,6 +9,7 @@
use Craft;
use craft\base\Element;
+use craft\base\Field;
use craft\base\MemoizableArray;
use craft\db\Query;
use craft\db\Table;
@@ -1150,6 +1151,9 @@ public function handleChangedEntryType(ConfigEvent $event): void
$entryTypeRecord->titleTranslationMethod = $data['titleTranslationMethod'] ?? '';
$entryTypeRecord->titleTranslationKeyFormat = $data['titleTranslationKeyFormat'] ?? null;
$entryTypeRecord->titleFormat = $data['titleFormat'];
+ $entryTypeRecord->slugTranslationMethod = $data['slugTranslationMethod'] ?? Field::TRANSLATION_METHOD_SITE;
+ $entryTypeRecord->slugTranslationKeyFormat = $data['slugTranslationKeyFormat'] ?? null;
+ $entryTypeRecord->showStatusField = $data['showStatusField'] ?? true;
$entryTypeRecord->sortOrder = $data['sortOrder'];
$entryTypeRecord->sectionId = $section->id;
$entryTypeRecord->uid = $entryTypeUid;
@@ -1606,7 +1610,7 @@ private function _populateNewStructure(SectionRecord $sectionRecord): void
*/
private function _createEntryTypeQuery(): Query
{
- return (new Query())
+ $query = (new Query())
->select([
'id',
'sectionId',
@@ -1622,6 +1626,17 @@ private function _createEntryTypeQuery(): Query
])
->from([Table::ENTRYTYPES])
->where(['dateDeleted' => null]);
+
+ // todo: remove after the next breakpoint
+ if (Craft::$app->getDb()->columnExists(Table::ENTRYTYPES, 'slugTranslationMethod')) {
+ $query->addSelect([
+ 'slugTranslationMethod',
+ 'slugTranslationKeyFormat',
+ 'showStatusField',
+ ]);
+ }
+
+ return $query;
}
/**
diff --git a/src/services/Structures.php b/src/services/Structures.php
index 0b99022c125..404b21c482f 100644
--- a/src/services/Structures.php
+++ b/src/services/Structures.php
@@ -31,8 +31,27 @@
*/
class Structures extends Component
{
+ /**
+ * @event MoveElementEvent The event that is triggered before an element is inserted into a structure.
+ *
+ * You may set [[\yii\base\ModelEvent::$isValid]] to `false` to prevent the
+ * element from getting inserted.
+ *
+ * @since 4.5.0
+ */
+ public const EVENT_BEFORE_INSERT_ELEMENT = 'beforeInsertElement';
+
+ /**
+ * @event MoveElementEvent The event that is triggered after an element is inserted into a structure.
+ * @since 4.5.0
+ */
+ public const EVENT_AFTER_INSERT_ELEMENT = 'afterInsertElement';
+
/**
* @event MoveElementEvent The event that is triggered before an element is moved.
+ *
+ * In Craft 4.5 and later, you may set [[\yii\base\ModelEvent::$isValid]] to `false` to prevent the
+ * element from getting moved.
*/
public const EVENT_BEFORE_MOVE_ELEMENT = 'beforeMoveElement';
@@ -41,19 +60,22 @@ class Structures extends Component
*/
public const EVENT_AFTER_MOVE_ELEMENT = 'afterMoveElement';
- /**
- * @since 3.4.21
- */
+ /** @since 3.4.21 */
public const MODE_INSERT = 'insert';
- /**
- * @since 3.4.21
- */
+ /** @since 3.4.21 */
public const MODE_UPDATE = 'update';
- /**
- * @since 3.4.21
- */
+ /** @since 3.4.21 */
public const MODE_AUTO = 'auto';
+ /** @since 4.5.0 */
+ public const ACTION_PREPEND = 'prepend';
+ /** @since 4.5.0 */
+ public const ACTION_APPEND = 'append';
+ /** @since 4.5.0 */
+ public const ACTION_PLACE_BEFORE = 'placeBefore';
+ /** @since 4.5.0 */
+ public const ACTION_PLACE_AFTER = 'placeAfter';
+
/**
* @var int The timeout to pass to [[\yii\mutex\Mutex::acquire()]] when acquiring a lock on the structure.
* @since 3.0.19
@@ -294,7 +316,7 @@ public function prepend(int $structureId, ElementInterface $element, ElementInte
throw new Exception('There was a problem getting the parent element.');
}
- return $this->_doIt($structureId, $element, $parentElementRecord, 'prependTo', $mode);
+ return $this->_doIt($structureId, $element, $parentElementRecord, self::ACTION_PREPEND, $mode);
}
/**
@@ -315,7 +337,7 @@ public function append(int $structureId, ElementInterface $element, ElementInter
throw new Exception('There was a problem getting the parent element.');
}
- return $this->_doIt($structureId, $element, $parentElementRecord, 'appendTo', $mode);
+ return $this->_doIt($structureId, $element, $parentElementRecord, self::ACTION_APPEND, $mode);
}
/**
@@ -330,7 +352,7 @@ public function append(int $structureId, ElementInterface $element, ElementInter
public function prependToRoot(int $structureId, ElementInterface $element, string $mode = self::MODE_AUTO): bool
{
$parentElementRecord = $this->_getRootElementRecord($structureId);
- return $this->_doIt($structureId, $element, $parentElementRecord, 'prependTo', $mode);
+ return $this->_doIt($structureId, $element, $parentElementRecord, self::ACTION_PREPEND, $mode);
}
/**
@@ -345,7 +367,7 @@ public function prependToRoot(int $structureId, ElementInterface $element, strin
public function appendToRoot(int $structureId, ElementInterface $element, string $mode = self::MODE_AUTO): bool
{
$parentElementRecord = $this->_getRootElementRecord($structureId);
- return $this->_doIt($structureId, $element, $parentElementRecord, 'appendTo', $mode);
+ return $this->_doIt($structureId, $element, $parentElementRecord, self::ACTION_APPEND, $mode);
}
/**
@@ -366,7 +388,7 @@ public function moveBefore(int $structureId, ElementInterface $element, ElementI
throw new Exception('There was a problem getting the next element.');
}
- return $this->_doIt($structureId, $element, $nextElementRecord, 'insertBefore', $mode);
+ return $this->_doIt($structureId, $element, $nextElementRecord, self::ACTION_PLACE_BEFORE, $mode);
}
/**
@@ -387,7 +409,7 @@ public function moveAfter(int $structureId, ElementInterface $element, ElementIn
throw new Exception('There was a problem getting the previous element.');
}
- return $this->_doIt($structureId, $element, $prevElementRecord, 'insertAfter', $mode);
+ return $this->_doIt($structureId, $element, $prevElementRecord, self::ACTION_PLACE_AFTER, $mode);
}
/**
@@ -470,8 +492,8 @@ private function _getRootElementRecord(int $structureId): StructureElement
* @param int $structureId
* @param ElementInterface $element
* @param StructureElement $targetElementRecord
- * @param string $action
- * @param string $mode
+ * @param self::ACTION_* $action
+ * @param self::MODE_* $mode
* @return bool Whether it was done
* @throws Throwable if reasons
*/
@@ -505,12 +527,26 @@ private function _doIt(int $structureId, ElementInterface $element, StructureEle
$mode = self::MODE_INSERT;
}
- if ($mode === self::MODE_UPDATE && $this->hasEventHandlers(self::EVENT_BEFORE_MOVE_ELEMENT)) {
- // Fire a 'beforeMoveElement' event
- $this->trigger(self::EVENT_BEFORE_MOVE_ELEMENT, new MoveElementEvent([
- 'structureId' => $structureId,
+ [$beforeEvent, $afterEvent] = match ($mode) {
+ self::MODE_INSERT => [self::EVENT_BEFORE_INSERT_ELEMENT, self::EVENT_AFTER_INSERT_ELEMENT],
+ self::MODE_UPDATE => [self::EVENT_BEFORE_MOVE_ELEMENT, self::EVENT_AFTER_MOVE_ELEMENT],
+ };
+
+ $targetElementId = $targetElementRecord->isRoot() ? null : $targetElementRecord->elementId;
+
+ if ($this->hasEventHandlers($beforeEvent)) {
+ // Fire a 'beforeInsertElement' or 'beforeMoveElement' event
+ $event = new MoveElementEvent([
'element' => $element,
- ]));
+ 'structureId' => $structureId,
+ 'targetElementId' => $targetElementId,
+ 'action' => $action,
+ ]);
+ $this->trigger($beforeEvent, $event);
+ if (!$event->isValid) {
+ $mutex->release($lockName);
+ return false;
+ }
}
// Tell the element about it
@@ -519,9 +555,16 @@ private function _doIt(int $structureId, ElementInterface $element, StructureEle
return false;
}
+ $method = match ($action) {
+ self::ACTION_PREPEND => 'prependTo',
+ self::ACTION_APPEND => 'appendTo',
+ self::ACTION_PLACE_BEFORE => 'insertBefore',
+ self::ACTION_PLACE_AFTER => 'insertAfter',
+ };
+
$transaction = Craft::$app->getDb()->beginTransaction();
try {
- if (!$elementRecord->$action($targetElementRecord)) {
+ if (!$elementRecord->$method($targetElementRecord)) {
$transaction->rollBack();
$mutex->release($lockName);
return false;
@@ -555,11 +598,13 @@ private function _doIt(int $structureId, ElementInterface $element, StructureEle
throw $e;
}
- if ($mode === self::MODE_UPDATE && $this->hasEventHandlers(self::EVENT_AFTER_MOVE_ELEMENT)) {
+ if ($this->hasEventHandlers($afterEvent)) {
// Fire an 'afterMoveElement' event
- $this->trigger(self::EVENT_AFTER_MOVE_ELEMENT, new MoveElementEvent([
- 'structureId' => $structureId,
+ $this->trigger($afterEvent, new MoveElementEvent([
'element' => $element,
+ 'structureId' => $structureId,
+ 'targetElementId' => $targetElementId,
+ 'action' => $action,
]));
}
diff --git a/src/services/TemplateCaches.php b/src/services/TemplateCaches.php
index 3c054d76a49..f8391f03784 100644
--- a/src/services/TemplateCaches.php
+++ b/src/services/TemplateCaches.php
@@ -367,7 +367,7 @@ private function _path(): string
}
$this->_path .= $request->getPathInfo();
- if (Craft::$app->getDb()->getIsMysql()) {
+ if (!Craft::$app->getDb()->getSupportsMb4()) {
$this->_path = StringHelper::encodeMb4($this->_path);
}
diff --git a/src/services/Volumes.php b/src/services/Volumes.php
index 13efc03f142..6f0d7cc718d 100644
--- a/src/services/Volumes.php
+++ b/src/services/Volumes.php
@@ -27,6 +27,7 @@
use craft\records\VolumeFolder as VolumeFolderRecord;
use Throwable;
use yii\base\Component;
+use yii\base\InvalidArgumentException;
use yii\base\InvalidConfigException;
/**
@@ -442,24 +443,14 @@ public function reorderVolumes(array $volumeIds): bool
*
* @param Volume $volume
* @return VolumeFolder
+ * @deprecated in 4.5.0. [[Assets::getRootFolderByVolumeId()]] should be used instead.
*/
public function ensureTopFolder(Volume $volume): VolumeFolder
{
- $assetsService = Craft::$app->getAssets();
- $folder = $assetsService->findFolder([
- 'name' => $volume->name,
- 'volumeId' => $volume->id,
- ]);
-
- if ($folder === null) {
- $folder = new VolumeFolder();
- $folder->volumeId = $volume->id;
- $folder->parentId = null;
- $folder->name = $volume->name;
- $folder->path = '';
- $assetsService->storeFolderRecord($folder);
+ $folder = Craft::$app->getAssets()->getRootFolderByVolumeId($volume->id);
+ if (!$folder) {
+ throw new InvalidArgumentException(sprintf('Invalid volume passed to %s().', __METHOD__));
}
-
return $folder;
}
diff --git a/src/templates/_components/fieldtypes/Assets/input.twig b/src/templates/_components/fieldtypes/Assets/input.twig
index 128dae86d8e..98f379a4612 100644
--- a/src/templates/_components/fieldtypes/Assets/input.twig
+++ b/src/templates/_components/fieldtypes/Assets/input.twig
@@ -61,6 +61,7 @@
fieldId: fieldId,
prevalidate: prevalidate ?? false,
canUpload: canUpload,
+ fsType: fsType,
defaultFieldLayoutId: defaultFieldLayoutId,
modalSettings: {
hideSidebar: hideSidebar ?? false,
diff --git a/src/templates/_components/fieldtypes/PlainText/input.twig b/src/templates/_components/fieldtypes/PlainText/input.twig
index e14e0080203..0e6434d3a89 100644
--- a/src/templates/_components/fieldtypes/PlainText/input.twig
+++ b/src/templates/_components/fieldtypes/PlainText/input.twig
@@ -14,7 +14,7 @@
class: class,
maxlength: field.charLimit,
showCharsLeft: true,
- placeholder: field.placeholder ? field.placeholder|t('site'),
+ placeholder: placeholder,
required: field.required,
rows: field.initialRows,
orientation: orientation ?? null,
diff --git a/src/templates/_components/fieldtypes/Table/settings.twig b/src/templates/_components/fieldtypes/Table/settings.twig
index 99c0a4a6574..9e2025698fd 100644
--- a/src/templates/_components/fieldtypes/Table/settings.twig
+++ b/src/templates/_components/fieldtypes/Table/settings.twig
@@ -3,35 +3,45 @@
{{ columnsField|raw }}
{{ defaultsField|raw }}
-{{ forms.textField({
- label: "Min Rows"|t('app'),
- instructions: "The minimum number of rows the field is allowed to have."|t('app'),
- id: 'minRows',
- name: 'minRows',
- value: field.minRows,
- size: 3,
- errors: field.getErrors('minRows')
+{{ forms.lightswitchField({
+ label: 'Static Rows'|t('app'),
+ instructions: 'Whether the table rows should be restricted to those defined by the “Default Values” setting.'|t('app'),
+ name: 'staticRows',
+ on: field.staticRows,
+ reverseToggle: 'dynamic-row-settings',
}) }}
-{{ forms.textField({
- label: "Max Rows"|t('app'),
- instructions: "The maximum number of rows the field is allowed to have."|t('app'),
- id: 'maxRows',
- name: 'maxRows',
- value: field.maxRows,
- size: 3,
- errors: field.getErrors('maxRows')
-}) }}
+
{% if craft.app.db.isMysql %}
diff --git a/src/templates/_components/utilities/AssetIndexes.twig b/src/templates/_components/utilities/AssetIndexes.twig
index 440822ea0b3..5e7278811f3 100644
--- a/src/templates/_components/utilities/AssetIndexes.twig
+++ b/src/templates/_components/utilities/AssetIndexes.twig
@@ -23,13 +23,15 @@
{{ 'Options'|t('app') }}
- {{ forms.lightswitchField({
- name: 'cacheImages',
- label: 'Cache remote images'|t('app'),
- class: 'volume-selector',
- instructions: 'Whether remotely-stored images should be downloaded and stored locally, to speed up transform generation.'|t('app'),
- on: true,
- }) }}
+ {% if not isEphemeral %}
+ {{ forms.lightswitchField({
+ name: 'cacheImages',
+ label: 'Cache remote images'|t('app'),
+ class: 'volume-selector',
+ instructions: 'Whether remotely-stored images should be downloaded and stored locally, to speed up transform generation.'|t('app'),
+ on: true,
+ }) }}
+ {% endif %}
{{ forms.lightswitchField({
name: 'listEmptyFolders',
diff --git a/src/templates/_elements/sources.twig b/src/templates/_elements/sources.twig
index 689fc61cd52..576eb3208b1 100644
--- a/src/templates/_elements/sources.twig
+++ b/src/templates/_elements/sources.twig
@@ -43,6 +43,7 @@
sites: (source.sites ?? false) ? source.sites|join(',') : false,
'override-status': (source.criteria.status ?? false) ? true : false,
disabled: source.disabled ?? false,
+ 'default-filter': source.defaultFilter ?? false,
}|merge(source.data ?? {}),
html: _self.sourceInnerHtml(source)
}) }}
diff --git a/src/templates/_elements/tableview/elements.twig b/src/templates/_elements/tableview/elements.twig
index bc198736acc..82050f63cb5 100644
--- a/src/templates/_elements/tableview/elements.twig
+++ b/src/templates/_elements/tableview/elements.twig
@@ -44,6 +44,19 @@
{%- include '_elements/element' with {
autoReload: false,
} -%}
+
+ {%- if structure %}
+ {% set textAlternative = 'Level {num}'|t('app', {
+ num: element.level,
+ }) %}
+ {{ tag('span', {
+ class: 'visually-hidden',
+ data: {
+ 'text-alternative': true,
+ },
+ text: textAlternative,
+ }) }}
+ {% endif %}
{% else %}
diff --git a/src/templates/_includes/forms.twig b/src/templates/_includes/forms.twig
index ba7f0e8e153..35d39f1af81 100644
--- a/src/templates/_includes/forms.twig
+++ b/src/templates/_includes/forms.twig
@@ -314,7 +314,9 @@
{% macro autosuggestField(config) %}
- {% set config = config|merge({id: config.id ?? "autosuggest#{random()}"}) %}
+ {% set config = config|merge({
+ id: config.id ?? "autosuggest#{random()}",
+ }) %}
{# Suggest an environment variable / alias? #}
{% if (config.suggestEnvVars ?? false) %}
diff --git a/src/templates/_includes/forms/autosuggest.twig b/src/templates/_includes/forms/autosuggest.twig
index e8fbb1beec7..8c28152ed0b 100644
--- a/src/templates/_includes/forms/autosuggest.twig
+++ b/src/templates/_includes/forms/autosuggest.twig
@@ -30,6 +30,8 @@
@focus="updateFilteredOptions"
@blur="onBlur"
@input="onInputChange"
+ @opened="onOpened"
+ @closed="onClosed"
v-model="inputProps.initialValue"
>
@@ -79,6 +81,26 @@ new Vue({
this.query = (q || '').toLowerCase();
this.updateFilteredOptions();
},
+ onOpened() {
+ let $element = $(this.$el);
+
+ // clicking on autosuggestion value won't work if the element's parent has any sort of tabindex set
+ // (it's not vue-autosuggest issue as on its own it works fine with tabindex)
+ // so we have to temporary remove that tabindex when opening the suggestions list
+ // and then add it back in when closing the list
+ // see https://github.com/craftcms/cms/issues/13553 for more details
+ let $tabindexParent = $element.parents('[tabindex="-1"]');
+ if ($tabindexParent.length > 0) {
+ this.$refs.tabindexParent = $tabindexParent;
+ $tabindexParent.removeAttr('tabindex');
+ }
+ },
+ onClosed() {
+ if (this.$refs.tabindexParent !== undefined) {
+ this.$refs.tabindexParent.attr('tabindex', '-1');
+ this.$refs.tabindexParent = null;
+ }
+ },
updateFilteredOptions() {
if (this.query === '') {
this.filteredOptions = this.suggestions;
diff --git a/src/templates/_includes/forms/booleanMenu.twig b/src/templates/_includes/forms/booleanMenu.twig
index 4e29fc3ca6d..53f0d4ae1de 100644
--- a/src/templates/_includes/forms/booleanMenu.twig
+++ b/src/templates/_includes/forms/booleanMenu.twig
@@ -16,18 +16,14 @@
label: yesLabel ?? 'Yes'|t('app'),
value: '1',
data: {
- data: {
- status: 'green',
- },
+ status: 'green',
},
},
{
label: noLabel ?? 'No'|t('app'),
value: '0',
data: {
- data: {
- status: 'white',
- },
+ status: 'white',
},
},
] %}
diff --git a/src/templates/_includes/forms/copytextbtn.twig b/src/templates/_includes/forms/copytextbtn.twig
index cecb6819bea..c5524202d3a 100644
--- a/src/templates/_includes/forms/copytextbtn.twig
+++ b/src/templates/_includes/forms/copytextbtn.twig
@@ -4,26 +4,39 @@
role: 'button',
title: 'Copy to clipboard'|t('app'),
aria: {
- label: 'Copy to clipboard'|t('app'),
describedby: describedBy ?? false,
},
tabindex: '0',
} %}
-{% tag 'div' with attributes %}
+
{{ input('text', null, value, {
+ class: 'visually-hidden',
readonly: true,
size: value|length,
tabindex: '-1',
+ aria: {
+ hidden: 'true',
+ },
}) }}
-
-{% endtag %}
+ {% tag 'div' with attributes %}
+ {{ tag('span', {
+ class: 'copytextbtn__value',
+ text: value,
+ }) }}
+ {{ tag('span', {
+ class: 'visually-hidden',
+ text: 'Copy to clipboard'|t('app'),
+ }) }}
+
+ {% endtag %}
+
{% js %}
{% block js %}
(() => {
const $btn = $('#{{ id|namespaceInputId|e('js') }}');
- const $input = $btn.children('input');
+ const $input = $btn.prev('input[readonly]');
const copyValue = function() {
$input[0].select();
document.execCommand('copy');
@@ -32,12 +45,9 @@
$input[0].setSelectionRange(0, 0);
$btn.focus();
};
- $btn.on('click', copyValue);
- $btn.on('keydown', ev => {
- if (ev.keyCode === Garnish.SPACE_KEY) {
- copyValue();
- ev.preventDefault();
- }
+ $btn.on('activate', ev => {
+ copyValue();
+ ev.preventDefault();
});
})();
{% endblock %}
diff --git a/src/templates/_includes/forms/editableTable.twig b/src/templates/_includes/forms/editableTable.twig
index 2f0ddcbb56b..c4ad984258f 100644
--- a/src/templates/_includes/forms/editableTable.twig
+++ b/src/templates/_includes/forms/editableTable.twig
@@ -51,6 +51,15 @@
{%- set tableAttributes = tableAttributes|merge(('')|parseAttr, recursive=true) %}
{% endif %}
+{% for col in cols %}
+ {%- switch col.type %}
+ {%- case 'time' %}
+ {%- do view.registerAssetBundle('craft\\web\\assets\\timepicker\\TimepickerAsset') %}
+ {%- case 'template' %}
+ {%- do view.registerAssetBundle("craft\\web\\assets\\vue\\VueAsset") %}
+ {%- endswitch %}
+{% endfor %}
+
{% tag 'table' with tableAttributes %}
{% for col in cols %}
@@ -62,34 +71,30 @@
{% if allowDelete %} {% endif %}
{% if allowReorder %} {% endif %}
{% endif %}
-
-
- {% for col in cols %}
- {%- switch col.type %}
- {%- case 'time' %}
- {%- do view.registerAssetBundle('craft\\web\\assets\\timepicker\\TimepickerAsset') %}
- {%- case 'template' %}
- {%- do view.registerAssetBundle("craft\\web\\assets\\vue\\VueAsset") %}
- {%- endswitch %}
- {% set columnHeadingId = "#{id}-heading-#{loop.index}" %}
-
- {%- if col.headingHtml is defined %}
- {{- col.headingHtml|raw }}
- {%- elseif col.heading ?? false %}
- {{- col.heading }}
- {%- else %}
-
- {%- endif %}
- {%- if col.info is defined -%}
- {{ col.info|md|raw }}
- {%- endif -%}
- |
- {% endfor %}
- {% if (allowDelete or allowReorder) %}
- {{ 'Row actions'|t('app') }} |
- {% endif %}
-
-
+ {% if cols|filter(c => (c.headingHtml ?? c.heading ?? c.info ?? '') is not same as(''))|length %}
+
+
+ {% for col in cols %}
+ {% set columnHeadingId = "#{id}-heading-#{loop.index}" %}
+
+ {%- if col.headingHtml is defined %}
+ {{- col.headingHtml|raw }}
+ {%- elseif col.heading ?? false %}
+ {{- col.heading }}
+ {%- else %}
+
+ {%- endif %}
+ {%- if col.info is defined -%}
+ {{ col.info|md|raw }}
+ {%- endif -%}
+ |
+ {% endfor %}
+ {% if (allowDelete or allowReorder) %}
+ {{ 'Row actions'|t('app') }} |
+ {% endif %}
+
+
+ {% endif %}
{% for rowId, row in rows %}
{% set rowNumber = loop.index %}
diff --git a/src/templates/_includes/forms/selectize.twig b/src/templates/_includes/forms/selectize.twig
index ad1ea16b3aa..0b84c208161 100644
--- a/src/templates/_includes/forms/selectize.twig
+++ b/src/templates/_includes/forms/selectize.twig
@@ -3,21 +3,33 @@
dropdownParent: 'body',
}|merge(selectizeOptions ?? []) %}
+{% set multi = multi ?? false %}
+{% if multi %}
+ {% set selectizeOptions = selectizeOptions|merge({
+ plugins: (selectizeOptions.plugins ?? [])|push('remove_button')
+ }) %}
+{% else %}
+ {% set selectizeOptions = selectizeOptions|merge({
+ plugins: (selectizeOptions.plugins ?? [])|push('select_on_focus')
+ }) %}
+{% endif %}
+
{# Normalize the options #}
{% set options = (options ?? [])|map((o, k) => (o.optgroup is defined or o.value is defined) ? o : {
value: k,
label: o.label is defined ? o.label : o,
}) %}
+{% set options = options|map(o => o|merge({
+ data: (o.data ?? {})|merge(o.data.data ?? {})
+})) %}
{% if includeEnvVars ?? false %}
{% if allowedEnvValues is not defined %}
{% set allowedEnvValues = options|filter(o => o.optgroup is not defined)|map(o => o.value) %}
{% endif %}
- {% set options = options|map(o => o.data.data.hint is defined ? o : o|merge({
+ {% set options = options|map(o => o.data.hint is defined ? o : o|merge({
data: {
- data: {
- hint: o.value,
- }
+ hint: o.value,
},
}, recursive=true)) %}
{% endif %}
@@ -31,89 +43,151 @@
{value: '', label: ' '},
] %}
{% endif %}
- {% set options = options|merge([
- {
- label: addOptionLabel ,
- value: '__add__',
- data: {
- data: {
- addOption: true,
- },
- },
+ {% set options = options|push({
+ label: addOptionLabel ,
+ value: '__add__',
+ data: {
+ addOption: true,
},
- ]) %}
+ }) %}
{% endif %}
{% if includeEnvVars ?? false %}
{% set options = options|merge(craft.cp.getEnvOptions(allowedEnvValues)) %}
{% endif %}
-{% include '_includes/forms/select' with {
+{% include (multi ? '_includes/forms/multiselect.twig' : '_includes/forms/select.twig') with {
class: (class ?? [])|explodeClass|push('selectize')|unique,
+ inputAttributes: {
+ style: {display: 'none'},
+ },
} %}
{% js %}
(() => {
+ const id = {{ id|namespaceInputId|json_encode|raw }};
+
+ const hasData = (data, option) => {
+ return typeof data[option] !== 'undefined' || typeof data[option.toLowerCase()] !== 'undefined';
+ };
+ const getData = (data, option) => {
+ if (typeof data[option] !== 'undefined') {
+ return data[option];
+ }
+ return data[option.toLowerCase()];
+ };
const label = (data, showHint) => {
let label = '';
- if (data.addOption) {
+ if (hasData(data, 'addOption')) {
label += ' ';
}
const status = (() => {
- if (typeof data.status !== 'undefined') {
- return data.status;
+ if (hasData(data, 'status')) {
+ return getData(data, 'status');
}
- if (typeof data.boolean !== 'undefined') {
- return data.boolean ? 'green' : 'white';
+ if (hasData(data, 'boolean')) {
+ return getData(data, 'boolean') ? 'green' : 'white';
}
return null;
})();
if (status) {
label += ``;
}
- label += `${Craft.escapeHtml(data.text)}`;
- if (showHint && typeof data.hint === 'string' && data.hint !== '') {
- const langAttr = data.hintLang ? ` lang="${data.hintLang}"` : '';
- label += `– ${Craft.escapeHtml(data.hint)}`;
+ label += `${Craft.escapeHtml(getData(data, 'text'))}`;
+ if (showHint && hasData(data, 'hint') && getData(data, 'hint') !== '') {
+ const hintLang = getData(data, 'hintLang');
+ const langAttr = hintLang ? ` lang="${hintLang}"` : '';
+ label += `– ${Craft.escapeHtml(getData(data, 'hint'))}`;
}
return `${label} `;
};
- const $select = $("#{{ id|namespaceInputId|e('js') }}");
+ const $select = $(`#${id}`);
const onChange = () => {
const selectize = $select.data('selectize');
- const $item = selectize.$wrapper.find('.item');
+ const $items = selectize.$wrapper.find('.item');
+ const isSelect = $select.is('select');
+
+ for (let i = 0; i < $items.length; i++) {
+ const $item = $items.eq(i);
+
+ if (isSelect) {
+ const boolean = $item.data('boolean');
+ if (typeof boolean !== 'undefined') {
+ $select.data('boolean', !!boolean);
+ } else {
+ $select.removeData('boolean');
+ }
+ }
- const boolean = $item.data('boolean');
- if (typeof boolean !== 'undefined') {
- $select.data('boolean', !!boolean);
- } else {
- $select.removeData('boolean');
+ {% if addOptionFn is defined and addOptionLabel is defined %}
+ if ($item.data('add-option')) {
+ selectize.close();
+ selectize.blur();
+
+ ({{ addOptionFn|raw }})(item => {
+ selectize.addOption(item);
+
+ // Remove the “Create” option and re-place it at the end
+ selectize.removeOption('__add__', true);
+ selectize.addOption({
+ text: {{ addOptionLabel|json_encode|raw }} ,
+ value: '__add__',
+ addOption: true,
+ hint: null,
+ });
+
+ selectize.refreshOptions(false);
+
+ if (isSelect) {
+ selectize.setValue(item.value, true);
+ } else {
+ selectize.addItem(item.value, true);
+ }
+ }, selectize);
+
+ Garnish.requestAnimationFrame(() => {
+ if (isSelect) {
+ selectize.setValue({{ ((options|first).value ?? '')|json_encode|raw }}, true);
+ } else {
+ selectize.removeItem('__add__');
+ }
+ });
+ }
+ {% endif %}
}
+ };
- {% if addOptionFn is defined and addOptionLabel is defined %}
- if ($item.data('add-option')) {
- selectize.setValue('', true);
- ({{ addOptionFn|raw }})(item => {
- selectize.addOption(item);
-
- // Remove the “Create” option and re-place it at the end
- selectize.removeOption('__add__', true);
- selectize.addOption({
- text: {{ addOptionLabel|json_encode|raw }} ,
- value: '__add__',
- addOption: true,
- hint: null,
- });
+ const positionDropdown = () => {
+ const selectize = $select.data('selectize');
- selectize.refreshOptions(false);
- selectize.setValue(item.value);
- }, selectize);
+ // adjust dropdown position - if there's not enough space to display it below the field
+ // display it above the field
+ const bodyHeight = $('body').height();
+ const windowInnerHeight = window.innerHeight;
+ let offsetAdjustment = 0;
+ if (bodyHeight > windowInnerHeight) {
+ offsetAdjustment = bodyHeight - windowInnerHeight;
+ }
+
+ const controlOffset = selectize.settings.dropdownParent === 'body' ? selectize.$control.offset() : selectize.$control.position();
+ const controlHeight = selectize.$control.outerHeight();
+ const dropdownHeight = selectize.$dropdown.outerHeight();
+ const exceededWindowHeight = (controlOffset.top - offsetAdjustment + controlHeight + dropdownHeight) > windowInnerHeight;
+
+ if (exceededWindowHeight) {
+ selectize.$dropdown.css({
+ top: controlOffset.top - dropdownHeight - 4,
+ });
}
- {% endif %}
};
+ {% if not multi %}
+ const selectizeDropdownOpenEvent = new Event("selectizedropdownopen");
+ const selectizeDropdownCloseEvent = new Event("selectizedropdownclose");
+ {% endif %}
+
$select.selectize($.extend({
searchField: ['text', 'hint', 'value', 'keywords'],
render: {
@@ -126,10 +200,10 @@
},
item: data => {
const attrs = ['class="item"'];
- if (typeof data.boolean !== 'undefined') {
- attrs.push(`data-boolean="${data.boolean ? '1' : ''}"`);
+ if (hasData(data, 'boolean')) {
+ attrs.push(`data-boolean="${getData(data, 'boolean') ? '1' : ''}"`);
}
- if (typeof data.addOption !== 'undefined') {
+ if (hasData(data, 'addOption')) {
attrs.push('data-add-option="1"');
}
return `${label(data, false)} `;
@@ -138,40 +212,24 @@
onChange: onChange,
onInitialize: function () {
// Copy all ARIA attributes from initial select to selectize
- var s = this;
- var attrs = {}
- var initialSelect = $(this.$input)[0];
- $(initialSelect.attributes).filter(function (index, attribute) {
- return /^aria-/.test(attribute.name);
- })
- .each(function (index, attribute) {
- attrs[attribute.name] = attribute.value;
- });
- $(this.$control_input).attr(attrs);
+ [...$select[0].attributes]
+ .filter(attr => /^aria-/.test(attr.name))
+ .forEach((attr) => {
+ this.$control_input.attr(attr.name, attr.value);
+ });
},
onDropdownOpen: function() {
- // adjust dropdown position - if there's not enough space to display it below the field
- // display it above the field
- var bodyHeight = $('body').height();
- var windowInnerHeight = window.innerHeight;
- var offsetAdjustment = 0;
- if (bodyHeight > windowInnerHeight) {
- offsetAdjustment = bodyHeight - windowInnerHeight;
- }
-
- var $control = this.$control;
- var controlOffset = this.settings.dropdownParent === 'body' ? $control.offset() : $control.position();
- var controlHeight = $control.outerHeight();
-
- var dropdownHeight = this.$dropdown.outerHeight();
-
- var exceededWindowHeight = (controlOffset.top - offsetAdjustment + controlHeight + dropdownHeight) > windowInnerHeight;
- if (exceededWindowHeight) {
- this.$dropdown.css({
- top: controlOffset.top - dropdownHeight - 4,
- });
- }
- }
+ positionDropdown();
+ {% if not multi %}
+ $select[0].dispatchEvent(selectizeDropdownOpenEvent);
+ {% endif %}
+ },
+ onDropdownClose: function() {
+ {% if not multi %}
+ $select[0].dispatchEvent(selectizeDropdownCloseEvent);
+ {% endif %}
+ },
+ onItemAdd: positionDropdown,
}, {{ selectizeOptions|json_encode|raw }}));
onChange();
diff --git a/src/templates/_layouts/cp.twig b/src/templates/_layouts/cp.twig
index 748b456d0f5..7eabeb2868b 100644
--- a/src/templates/_layouts/cp.twig
+++ b/src/templates/_layouts/cp.twig
@@ -75,6 +75,7 @@
{% set footer = (footer ?? block('footer') ?? '')|trim %}
{% set crumbs = crumbs ?? null %}
{% set tabs = (tabs ?? [])|length > 1 ? tabs : null %}
+{% set errorSummary = errorSummary ?? null %}
{% set mainContentClasses = [
sidebar ? 'has-sidebar',
@@ -117,13 +118,7 @@
{% set userPhoto %}
{% endset %}
@@ -249,6 +244,9 @@
{% endif %}
{% block main %}
+ {% if errorSummary is not empty %}
+ {{ errorSummary is defined ? errorSummary|raw }}
+ {% endif %}
{% if contentNotice or tabs %}
|