diff --git a/src/Model/Table/SettingsProviderTable.php b/src/Model/Table/SettingsProviderTable.php index 08ea323..0644883 100644 --- a/src/Model/Table/SettingsProviderTable.php +++ b/src/Model/Table/SettingsProviderTable.php @@ -109,6 +109,11 @@ class SettingsProviderTable extends AppTable private function evaluateLeaf($setting, $settingSection) { $skipValidation = false; + if ($setting['type'] == 'select') { + if (!empty($setting['options']) && is_callable($setting['options'])) { + $setting['options'] = $setting['options']($this); + } + } if (isset($setting['dependsOn'])) { $parentSetting = null; foreach ($settingSection as $settingSectionName => $settingSectionConfig) { @@ -189,19 +194,30 @@ class SettingsProviderTable extends AppTable }, 'type' => 'string' ], - 'to-del2' => [ - 'description' => 'to del', - 'errorMessage' => 'to del', + 'sc2.hero' => [ + 'description' => 'The true hero', 'default' => '', - 'name' => 'To DEL 2', - 'type' => 'string' + 'options' => [ + 'Jim Raynor' => 'Jim Raynor', + 'Sarah Kerrigan' => 'Sarah Kerrigan', + 'Artanis' => 'Artanis', + 'Zeratul' => 'Zeratul', + ], + 'name' => 'Hero', + 'type' => 'select' ], - 'to-del3' => [ - 'description' => 'to del', - 'errorMessage' => 'to del', + 'sc2.antagonist' => [ + 'description' => 'The real bad guy', 'default' => '', - 'name' => 'To DEL 2', - 'type' => 'string' + 'options' => function($settingsProviders) { + return [ + 'Amon' => 'Amon', + 'Sarah Kerrigan' => 'Sarah Kerrigan', + 'Narud' => 'Narud', + ]; + }, + 'name' => 'Antagonist', + 'type' => 'select' ], ], 'floating-setting' => [ diff --git a/templates/Instance/settings.php b/templates/Instance/settings.php index 44327ff..e45efd4 100644 --- a/templates/Instance/settings.php +++ b/templates/Instance/settings.php @@ -243,12 +243,10 @@ function genSingleSetting($settingName, $setting, $appView) $appView->Bootstrap->genNode('a', [ 'class' => ['position-absolute', 'fas fa-times', 'p-abs-center-y', 'text-reset text-decoration-none', 'btn-reset-setting'], 'href' => '#', - 'style' => 'left: -1.25em; z-index: 5;' ]), $appView->Bootstrap->genNode('button', [ 'class' => ['btn', 'btn-success', 'btn-save-setting'], 'type' => 'button', - 'style' => 'z-index: 5;' ], __('save')), ])); $inputGroup = $appView->Bootstrap->genNode('div', [ @@ -324,10 +322,37 @@ function genInputInteger($settingName, $setting, $appView) 'aria-describedby' => "{$settingId}Help" ]); } -function genInputSelect($settingId, $setting, $appView) +function genInputSelect($settingName, $setting, $appView) { + $settingId = str_replace('.', '_', $settingName); + $setting['value'] = $setting['value'] ?? ''; + $options = [ + $appView->Bootstrap->genNode('option', ['value' => '-1', 'data-is-empty-option' => '1'], __('Select an option')) + ]; + foreach ($setting['options'] as $key => $value) { + $options[] = $appView->Bootstrap->genNode('option', [ + 'class' => [], + 'value' => $key, + ($setting['value'] == $value ? 'selected' : '') => $setting['value'] == $value ? 'selected' : '', + ], h($value)); + } + $options = implode('', $options); + return $appView->Bootstrap->genNode('select', [ + 'class' => [ + 'custom-select', + 'pr-4', + (!empty($setting['error']) ? 'is-invalid' : ''), + (!empty($setting['error']) ? "border-{$appView->get('variantFromSeverity')[$setting['severity']]}" : ''), + (!empty($setting['error']) && $setting['severity'] == 'warning' ? 'warning' : ''), + ], + 'type' => 'text', + 'id' => $settingId, + 'data-setting-name' => $settingName, + 'placeholder' => $setting['default'] ?? '', + 'aria-describedby' => "{$settingId}Help" + ], $options); } -function genInputMultiSelect($settingId, $setting, $appView) +function genInputMultiSelect($settingName, $setting, $appView) { } @@ -422,7 +447,7 @@ function isLeaf($setting) $("#search-settings").val(null).trigger('change.select2'); }) - $('.tab-content input').on('input', function() { + $('.tab-content input, .tab-content select').on('input', function() { if ($(this).attr('type') == 'checkbox') { const $input = $(this) const $inputGroup = $(this).closest('.form-group') @@ -435,15 +460,18 @@ function isLeaf($setting) }) $('.tab-content .input-group-actions .btn-save-setting').click(function() { - const $input = $(this).closest('.input-group').find('input') + const $input = $(this).closest('.input-group').find('input, select') const settingName = $input.data('setting-name') const settingValue = $input.val() saveSetting(this, $input, settingName, settingValue) }) $('.tab-content .input-group-actions .btn-reset-setting').click(function() { const $btn = $(this) - const $input = $btn.closest('.input-group').find('input') - const oldValue = settingsFlattened[$input.data('setting-name')].value + const $input = $btn.closest('.input-group').find('input, select') + let oldValue = settingsFlattened[$input.data('setting-name')].value + if ($input.is('select')) { + oldValue = oldValue !== undefined ? oldValue : -1 + } $input.val(oldValue) handleSettingValueChange($input) }) @@ -469,6 +497,72 @@ function isLeaf($setting) }) } + function handleSettingValueChange($input) { + const oldValue = settingsFlattened[$input.data('setting-name')].value + const newValue = ($input.attr('type') == 'checkbox' ? $input.is(':checked') : $input.val()) + if (newValue == oldValue) { + restoreWarnings($input) + } else { + removeWarnings($input) + } + } + + function removeWarnings($input) { + const $inputGroup = $input.closest('.input-group') + const $inputGroupAppend = $inputGroup.find('.input-group-append') + const $saveButton = $inputGroup.find('button.btn-save-setting') + $input.removeClass(['is-invalid', 'border-warning', 'border-danger']) + $inputGroupAppend.removeClass('d-none') + if ($input.is('select') && $input.find('option:selected').data('is-empty-option') == 1) { + $inputGroupAppend.addClass('d-none') // hide save button if empty selection picked + } + $inputGroup.parent().find('.invalid-feedback').removeClass('d-block') + } + + function restoreWarnings($input) { + const $inputGroup = $input.closest('.input-group') + const $inputGroupAppend = $inputGroup.find('.input-group-append') + const $saveButton = $inputGroup.find('button.btn-save-setting') + const setting = settingsFlattened[$input.data('setting-name')] + if (setting.error) { + borderVariant = setting.severity !== undefined ? variantFromSeverity[setting.severity] : 'warning' + $input.addClass(['is-invalid', `border-${borderVariant}`]) + if (setting.severity == 'warning') { + $input.addClass('warning') + } + $inputGroup.parent().find('.invalid-feedback').addClass('d-block').text(setting.errorMessage) + } else { + removeWarnings($input) + } + const $callout = $input.closest('.settings-group') + updateCalloutColors($callout) + $inputGroupAppend.addClass('d-none') + } + + function updateCalloutColors($callout) { + if ($callout.length == 0) { + return + } + const $settings = $callout.find('input') + const settingNames = Array.from($settings).map((i) => { + return $(i).data('setting-name') + }) + const severityMapping = {null: 0, info: 1, warning: 2, critical: 3} + const severityMappingInverted = Object.assign({}, ...Object.entries(severityMapping).map(([k, v]) => ({[v]: k}))) + let highestSeverity = severityMapping[null] + settingNames.forEach(name => { + if (settingsFlattened[name].error) { + highestSeverity = severityMapping[settingsFlattened[name].severity] > highestSeverity ? severityMapping[settingsFlattened[name].severity] : highestSeverity + } + }); + highestSeverity = severityMappingInverted[highestSeverity] + $callout.removeClass(['callout', 'callout-danger', 'callout-warning', 'callout-info']) + if (highestSeverity !== null) { + $callout.addClass(['callout', `callout-${variantFromSeverity[highestSeverity]}`]) + } + } + + function settingMatcher(params, data) { if (params.term == null || params.term.trim() === '') { return data; @@ -519,69 +613,6 @@ function isLeaf($setting) function formatSettingSearchSelection(state) { return state.text } - - function handleSettingValueChange($input) { - const oldValue = settingsFlattened[$input.data('setting-name')].value - const newValue = ($input.attr('type') == 'checkbox' ? $input.is(':checked') : $input.val()) - if (newValue == oldValue) { - restoreWarnings($input) - } else { - removeWarnings($input) - } - } - - function removeWarnings($input) { - const $inputGroup = $input.closest('.input-group') - const $inputGroupAppend = $inputGroup.find('.input-group-append') - const $saveButton = $inputGroup.find('button.btn-save-setting') - $input.removeClass(['is-invalid', 'border-warning', 'border-danger']) - $inputGroupAppend.removeClass('d-none') - $inputGroup.parent().find('.invalid-feedback').removeClass('d-block') - } - - function restoreWarnings($input) { - const $inputGroup = $input.closest('.input-group') - const $inputGroupAppend = $inputGroup.find('.input-group-append') - const $saveButton = $inputGroup.find('button.btn-save-setting') - const setting = settingsFlattened[$input.data('setting-name')] - if (setting.error) { - borderVariant = setting.severity !== undefined ? variantFromSeverity[setting.severity] : 'warning' - $input.addClass(['is-invalid', `border-${borderVariant}`]) - if (setting.severity == 'warning') { - $input.addClass('warning') - } - $inputGroup.parent().find('.invalid-feedback').addClass('d-block').text(setting.errorMessage) - } else { - removeWarnings($input) - } - const $callout = $input.closest('.settings-group') - updateCalloutColors($callout) - $inputGroupAppend.addClass('d-none') - } - - function updateCalloutColors($callout) { - if ($callout.length == 0) { - return - } - const $settings = $callout.find('input') - const settingNames = Array.from($settings).map((i) => { - return $(i).data('setting-name') - }) - const severityMapping = {null: 0, info: 1, warning: 2, critical: 3} - const severityMappingInverted = Object.assign({}, ...Object.entries(severityMapping).map(([k, v]) => ({[v]: k}))) - let highestSeverity = severityMapping[null] - settingNames.forEach(name => { - if (settingsFlattened[name].error) { - highestSeverity = severityMapping[settingsFlattened[name].severity] > highestSeverity ? severityMapping[settingsFlattened[name].severity] : highestSeverity - } - }); - highestSeverity = severityMappingInverted[highestSeverity] - $callout.removeClass(['callout', 'callout-danger', 'callout-warning', 'callout-info']) - if (highestSeverity !== null) { - $callout.addClass(['callout', `callout-${variantFromSeverity[highestSeverity]}`]) - } - } - \ No newline at end of file diff --git a/webroot/css/bootstrap-additional.css b/webroot/css/bootstrap-additional.css index 63c730f..1281dc4 100644 --- a/webroot/css/bootstrap-additional.css +++ b/webroot/css/bootstrap-additional.css @@ -141,6 +141,9 @@ div.progress-timeline .progress-line.progress-inactive { .form-control.is-invalid.warning { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23ffc107' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ffc107' stroke='none'/%3e%3c/svg%3e") } +.custom-select.is-invalid.warning { + background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px, url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23ffc107' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ffc107' stroke='none'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} .custom-control-input.is-invalid.warning ~ .custom-control-label { color: unset;