diff --git a/config/app_local.example.php b/config/app_local.example.php index 03aeef1..637128c 100644 --- a/config/app_local.example.php +++ b/config/app_local.example.php @@ -89,9 +89,4 @@ return [ 'url' => env('EMAIL_TRANSPORT_DEFAULT_URL', null), ], ], - 'Cerebrate' => [ - 'open' => [], - 'dark' => 0, - 'baseurl' => '' - ] ]; diff --git a/config/bootstrap.php b/config/bootstrap.php index 615df3d..ed41346 100644 --- a/config/bootstrap.php +++ b/config/bootstrap.php @@ -87,6 +87,7 @@ try { */ if (file_exists(CONFIG . 'app_local.php')) { Configure::load('app_local', 'default'); + Configure::load('cerebrate', 'default', true); } /* diff --git a/config/cerebrate.php b/config/cerebrate.php new file mode 100644 index 0000000..bfb2cf9 --- /dev/null +++ b/config/cerebrate.php @@ -0,0 +1,9 @@ + [ + 'open' => [], + 'app.baseurl' => 'http://localhost:8000/', + 'app.uuid' => 'cc9b9358-7c4b-4464-9a2c-f0cb089ff974', + 'ui.dark' => 0, + ] +]; diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php index 5d49631..498b0ae 100644 --- a/src/Controller/AppController.php +++ b/src/Controller/AppController.php @@ -124,7 +124,7 @@ class AppController extends Controller $this->set('menu', $this->ACL->getMenu()); $this->set('ajax', $this->request->is('ajax')); $this->request->getParam('prefix'); - $this->set('darkMode', !empty(Configure::read('Cerebrate.dark'))); + $this->set('darkMode', !empty(Configure::read('Cerebrate')['ui.dark'])); $this->set('baseurl', Configure::read('App.fullBaseUrl')); } diff --git a/src/Controller/Component/ACLComponent.php b/src/Controller/Component/ACLComponent.php index a1f06ea..fc8966c 100644 --- a/src/Controller/Component/ACLComponent.php +++ b/src/Controller/Component/ACLComponent.php @@ -811,6 +811,10 @@ class ACLComponent extends Component __('Instance'), 'url' => '/instance/home', 'children' => [ + 'settings' => [ + 'url' => '/instance/settings', + 'label' => __('Settings') + ], 'migration' => [ 'url' => '/instance/migrationIndex', 'label' => __('Database migration') diff --git a/src/Controller/InstanceController.php b/src/Controller/InstanceController.php index df141a9..c5a9556 100644 --- a/src/Controller/InstanceController.php +++ b/src/Controller/InstanceController.php @@ -7,6 +7,7 @@ use Cake\Utility\Hash; use Cake\Utility\Text; use \Cake\Database\Expression\QueryExpression; use Cake\Event\EventInterface; +use Cake\Core\Configure; class InstanceController extends AppController { @@ -101,4 +102,36 @@ class InstanceController extends AppController $this->set('path', ['controller' => 'instance', 'action' => 'rollback']); $this->render('/genericTemplates/confirm'); } + + public function settings() + { + $this->Settings = $this->getTableLocator()->get('Settings'); + $all = $this->Settings->getSettings(true); + $this->set('settingsProvider', $all['settingsProvider']); + $this->set('settings', $all['settings']); + $this->set('settingsFlattened', $all['settingsFlattened']); + $this->set('notices', $all['notices']); + } + + public function saveSetting() + { + if ($this->request->is('post')) { + $data = $this->ParamHandler->harvestParams([ + 'name', + 'value' + ]); + $this->Settings = $this->getTableLocator()->get('Settings'); + $errors = $this->Settings->saveSetting($data['name'], $data['value']); + $message = __('Could not save setting `{0}`', $data['name']); + if (empty($errors)) { + $message = __('Setting `{0}` saved', $data['name']); + $data = $this->Settings->getSetting($data['name']); + } + $this->CRUD->setResponseForController('saveSetting', empty($errors), $message, $data, $errors); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } + } + } } diff --git a/src/Model/Table/SettingsProviderTable.php b/src/Model/Table/SettingsProviderTable.php new file mode 100644 index 0000000..d70b2d2 --- /dev/null +++ b/src/Model/Table/SettingsProviderTable.php @@ -0,0 +1,390 @@ +settingsConfiguration = $this->generateSettingsConfiguration(); + $this->setTable(false); + $this->error_critical = __('Cerebrate will not operate correctly or will be unsecure until these issues are resolved.'); + $this->error_warning = __('Some of the features of Cerebrate cannot be utilised until these issues are resolved.'); + $this->error_info = __('There are some optional tweaks that could be done to improve the looks of your Cerebrate instance.'); + $this->settingValidator = new SettingValidator(); + } + + /** + * Supports up to 3 levels: + * Application -> Network -> Proxy -> Proxy.URL + * page -> [group] -> [panel] -> setting + * Keys of setting configuration are the actual setting name. + * Accepted setting configuration: + * name [required]: The human readable name of the setting. + * type [required]: The type of the setting. + * description [required]: A description of the setting. + * Default severity level is `info` if a `default` value is provided otherwise it becomes `critical` + * default [optional]: The default value of the setting if not specified in the configuration. + * options [optional]: Used to populate the select with options. Keys are values to be saved, values are human readable version of the value. + * Required paramter if `type` == `select`. + * severity [optional]: Severity level of the setting if the configuration is incorrect. + * dependsOn [optional]: If the validation of this setting depends on the validation of the provided setting name + * test [optional]: Could be either a string or an anonymous function to be called in order to warn user if setting is invalid. + * Could be either: `string`, `boolean`, `integer`, `select` + * beforeSave [optional]: Could be either a string or an anonymous function to be called in order to block a setting to be saved. + * afterSave [optional]: Could be either a string or an anonymous function to be called allowing to execute a function after the setting is saved. + * redacted [optional]: Should the setting value be redacted. FIXME: To implement + * cli_only [optional]: Should this setting be modified only via the CLI. + */ + private function generateSettingsConfiguration() + { + return [ + 'Application' => [ + 'General' => [ + 'Essentials' => [ + '_description' => __('Ensentials settings required for the application to run normally.'), + '_icon' => 'user-cog', + 'app.baseurl' => [ + 'name' => __('Base URL'), + 'type' => 'string', + 'description' => __('The base url of the application (in the format https://www.mymispinstance.com or https://myserver.com/misp). Several features depend on this setting being correctly set to function.'), + 'default' => '', + 'severity' => 'critical', + 'test' => 'testBaseURL', + ], + 'app.uuid' => [ + 'name' => 'UUID', + 'type' => 'string', + 'description' => __('The Cerebrate instance UUID. This UUID is used to identify this instance.'), + 'default' => '', + 'severity' => 'critical', + 'test' => 'testUuid', + ], + ], + 'Miscellaneous' => [ + 'sc2.hero' => [ + 'description' => 'The true hero', + 'default' => 'Sarah Kerrigan', + 'name' => 'Hero', + 'options' => [ + 'Jim Raynor' => 'Jim Raynor', + 'Sarah Kerrigan' => 'Sarah Kerrigan', + 'Artanis' => 'Artanis', + 'Zeratul' => 'Zeratul', + ], + 'type' => 'select' + ], + 'sc2.antagonists' => [ + 'description' => 'The bad guys', + 'default' => 'Amon', + 'name' => 'Antagonists', + 'options' => function($settingsProviders) { + return [ + 'Amon' => 'Amon', + 'Sarah Kerrigan' => 'Sarah Kerrigan', + 'Narud' => 'Narud', + ]; + }, + 'severity' => 'warning', + 'type' => 'multi-select' + ], + ], + 'floating-setting' => [ + 'description' => 'floaringSetting', + // 'default' => 'A default value', + 'name' => 'Uncategorized Setting', + // 'severity' => 'critical', + 'severity' => 'warning', + // 'severity' => 'info', + 'type' => 'integer' + ], + ], + 'Network' => [ + 'Proxy' => [ + 'proxy.host' => [ + 'name' => __('Host'), + 'type' => 'string', + 'description' => __('The hostname of an HTTP proxy for outgoing sync requests. Leave empty to not use a proxy.'), + 'test' => 'testHostname', + ], + 'proxy.port' => [ + 'name' => __('Port'), + 'type' => 'integer', + 'description' => __('The TCP port for the HTTP proxy.'), + 'test' => 'testForRangeXY', + ], + 'proxy.user' => [ + 'name' => __('User'), + 'type' => 'string', + 'description' => __('The authentication username for the HTTP proxy.'), + 'default' => 'admin', + 'dependsOn' => 'proxy.host', + ], + 'proxy.password' => [ + 'name' => __('Password'), + 'type' => 'string', + 'description' => __('The authentication password for the HTTP proxy.'), + 'default' => '', + 'dependsOn' => 'proxy.host', + ], + ], + ], + 'UI' => [ + 'General' => [ + 'ui.dark' => [ + 'name' => __('Dark theme'), + 'type' => 'boolean', + 'description' => __('Enable the dark theme of the application'), + 'default' => false, + 'test' => function() { + return 'Fake error'; + }, + ], + ], + ], + ], + 'Security' => [ + 'Development' => [ + 'Debugging' => [ + 'security.debug' => [ + 'name' => __('Debug Level'), + 'type' => 'select', + 'description' => __('The debug level of the instance'), + 'default' => 0, + 'options' => [ + 0 => __('Debug Off'), + 1 => __('Debug On'), + 2 => __('Debug On + SQL Dump'), + ], + 'test' => function($value, $setting, $validator) { + $validator->range('value', [0, 3]); + return testValidator($value, $validator); + }, + ], + ], + ] + ], + 'Features' => [ + ], + ]; + } + + /** + * getSettingsConfiguration Return the setting configuration and merge existing settings into it if provided + * + * @param null|array $settings - Settings to be merged in the provided setting configuration + * @return array + */ + public function getSettingsConfiguration($settings = null) { + $settingConf = $this->settingsConfiguration; + if (!is_null($settings)) { + $settingConf = $this->mergeSettingsIntoSettingConfiguration($settingConf, $settings); + } + return $settingConf; + } + + /** + * mergeSettingsIntoSettingConfiguration Inject the provided settings into the configuration while performing depencency and validation checks + * + * @param array $settingConf the setting configuration to have the setting injected into + * @param array $settings the settings + * @return void + */ + private function mergeSettingsIntoSettingConfiguration(array $settingConf, array $settings, string $path=''): array + { + foreach ($settingConf as $key => $value) { + if ($this->isSettingMetaKey($key)) { + continue; + } + if ($this->isLeaf($value)) { + if (isset($settings[$key])) { + $settingConf[$key]['value'] = $settings[$key]; + } + $settingConf[$key] = $this->evaluateLeaf($settingConf[$key], $settingConf); + $settingConf[$key]['setting-path'] = $path; + $settingConf[$key]['true-name'] = $key; + } else { + $currentPath = empty($path) ? $key : sprintf('%s.%s', $path, $key); + $settingConf[$key] = $this->mergeSettingsIntoSettingConfiguration($value, $settings, $currentPath); + } + } + return $settingConf; + } + + public function flattenSettingsConfiguration(array $settingsProvider, $flattenedSettings=[]): array + { + foreach ($settingsProvider as $key => $value) { + if ($this->isSettingMetaKey($key)) { + continue; + } + if ($this->isLeaf($value)) { + $flattenedSettings[$key] = $value; + } else { + $flattenedSettings = $this->flattenSettingsConfiguration($value, $flattenedSettings); + } + } + return $flattenedSettings; + } + + /** + * getNoticesFromSettingsConfiguration Summarize the validation errors + * + * @param array $settingsProvider the setting configuration having setting value assigned + * @return void + */ + public function getNoticesFromSettingsConfiguration(array $settingsProvider): array + { + $notices = []; + foreach ($settingsProvider as $key => $value) { + if ($this->isSettingMetaKey($key)) { + continue; + } + if ($this->isLeaf($value)) { + if (!empty($value['error'])) { + if (empty($notices[$value['severity']])) { + $notices[$value['severity']] = []; + } + $notices[$value['severity']][] = $value; + } + } else { + $notices = array_merge_recursive($notices, $this->getNoticesFromSettingsConfiguration($value)); + } + } + return $notices; + } + + private function isLeaf($setting) + { + return !empty($setting['name']) && !empty($setting['type']); + } + + private function evaluateLeaf($setting, $settingSection) + { + $skipValidation = false; + if ($setting['type'] == 'select' || $setting['type'] == 'multi-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) { + if ($settingSectionName == $setting['dependsOn']) { + $parentSetting = $settingSectionConfig; + } + } + if (!is_null($parentSetting)) { + $parentSetting = $this->evaluateLeaf($parentSetting, $settingSection); + $skipValidation = $parentSetting['error'] === true || empty($parentSetting['value']); + } + } + $setting['error'] = false; + if (!$skipValidation) { + $validationResult = true; + if (!isset($setting['value'])) { + $validationResult = $this->settingValidator->testEmptyBecomesDefault(null, $setting); + } else if (isset($setting['test'])) { + $setting['value'] = $setting['value'] ?? ''; + $validationResult = $this->evaluateFunctionForSetting($setting['test'], $setting); + } + if ($validationResult !== true) { + $setting['severity'] = $setting['severity'] ?? 'warning'; + if (!in_array($setting['severity'], $this->severities)) { + $setting['severity'] = 'warning'; + } + $setting['errorMessage'] = $validationResult; + } + $setting['error'] = $validationResult !== true ? true : false; + } + return $setting; + } + + /** + * evaluateFunctionForSetting - evaluate the provided function. If function could not be evaluated, its result is defaulted to true + * + * @param mixed $fun + * @param array $setting + * @return mixed + */ + public function evaluateFunctionForSetting($fun, $setting) + { + $functionResult = true; + if (is_callable($fun)) { // Validate with anonymous function + $functionResult = $fun($setting['value'], $setting, new Validator()); + } else if (method_exists($this->settingValidator, $fun)) { // Validate with function defined in settingValidator class + $functionResult = $this->settingValidator->{$fun}($setting['value'], $setting); + } else { + $validator = new Validator(); + if (method_exists($validator, $fun)) { // Validate with cake's validator function + $validator->{$fun}; + $functionResult = $validator->validate($setting['value']); + } + } + return $functionResult; + } + + function isSettingMetaKey($key) + { + return substr($key, 0, 1) == '_'; + } +} + +function testValidator($value, $validator) +{ + $errors = $validator->validate(['value' => $value]); + return !empty($errors) ? implode(', ', $errors['value']) : true; +} + +class SettingValidator +{ + + public function testEmptyBecomesDefault($value, &$setting) + { + if (!empty($value)) { + return true; + } else if (isset($setting['default'])) { + $setting['value'] = $setting['default']; + $setting['severity'] = $setting['severity'] ?? 'info'; + if ($setting['type'] == 'boolean') { + return __('Setting is not set, fallback to default value: {0}', empty($setting['default']) ? 'false' : 'true'); + } else { + return __('Setting is not set, fallback to default value: {0}', $setting['default']); + } + } else { + $setting['severity'] = $setting['severity'] ?? 'critical'; + return __('Cannot be empty. Setting does not have a default value.'); + } + } + + public function testForEmpty($value, &$setting) + { + return !empty($value) ? true : __('Cannot be empty'); + } + + public function testBaseURL($value, &$setting) + { + if (empty($value)) { + return __('Cannot be empty'); + } + if (!empty($value) && !preg_match('/^http(s)?:\/\//i', $value)) { + return __('Invalid URL, please make sure that the protocol is set.'); + } + return true; + } + + public function testUuid($value, &$setting) { + if (empty($value) || !preg_match('/^\{?[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}\}?$/', $value)) { + return __('Invalid UUID.'); + } + return true; + } +} \ No newline at end of file diff --git a/src/Model/Table/SettingsTable.php b/src/Model/Table/SettingsTable.php new file mode 100644 index 0000000..0235cdc --- /dev/null +++ b/src/Model/Table/SettingsTable.php @@ -0,0 +1,97 @@ +setTable(false); + $this->SettingsProvider = TableRegistry::getTableLocator()->get('SettingsProvider'); + } + + public function getSettings($full=false): array + { + $settings = $this->readSettings(); + if (empty($full)) { + return $settings; + } else { + $settingsProvider = $this->SettingsProvider->getSettingsConfiguration($settings); + $settingsFlattened = $this->SettingsProvider->flattenSettingsConfiguration($settingsProvider); + $notices = $this->SettingsProvider->getNoticesFromSettingsConfiguration($settingsProvider, $settings); + return [ + 'settings' => $settings, + 'settingsProvider' => $settingsProvider, + 'settingsFlattened' => $settingsFlattened, + 'notices' => $notices, + ]; + } + } + + public function getSetting($name=false): array + { + $settings = $this->readSettings(); + $settingsProvider = $this->SettingsProvider->getSettingsConfiguration($settings); + $settingsFlattened = $this->SettingsProvider->flattenSettingsConfiguration($settingsProvider); + return $settingsFlattened[$name] ?? []; + } + + public function saveSetting(string $name, string $value): array + { + $errors = []; + $setting = $this->getSetting($name); + $value = $this->normaliseValue($value, $setting); + if ($setting['type'] == 'select') { + if (!in_array($value, array_keys($setting['options']))) { + $errors[] = __('Invalid option provided'); + } + } + if (empty($errors) && !empty($setting['beforeSave'])) { + $setting['value'] = $value ?? ''; + $beforeSaveResult = $this->SettingsProvider->evaluateFunctionForSetting($setting['beforeSave'], $setting); + if ($beforeSaveResult !== true) { + $errors[] = $beforeSaveResult; + } + } + if (empty($errors)) { + $saveResult = $this->saveSettingOnDisk($name, $value); + if ($saveResult) { + if (!empty($setting['afterSave'])) { + $this->SettingsProvider->evaluateFunctionForSetting($setting['afterSave'], $setting); + } + } + } + return $errors; + } + + private function normaliseValue($value, $setting) + { + if ($setting['type'] == 'boolean') { + return (bool) $value; + } + return $value; + } + + private function readSettings() + { + return Configure::read()[$this::$CONFIG_KEY]; + } + + private function saveSettingOnDisk($name, $value) + { + $settings = $this->readSettings(); + $settings[$name] = $value; + Configure::write($this::$CONFIG_KEY, $settings); + Configure::dump($this::$FILENAME, 'default', [$this::$CONFIG_KEY]); + return true; + } +} diff --git a/src/View/Helper/BootstrapHelper.php b/src/View/Helper/BootstrapHelper.php index 136ef03..532f2fd 100644 --- a/src/View/Helper/BootstrapHelper.php +++ b/src/View/Helper/BootstrapHelper.php @@ -81,6 +81,12 @@ class BootstrapHelper extends Helper return $bsButton->button(); } + public function icon($icon, $options=[]) + { + $bsIcon = new BoostrapIcon($icon, $options); + return $bsIcon->icon(); + } + public function badge($options) { $bsBadge = new BoostrapBadge($options); @@ -116,6 +122,17 @@ class BootstrapHelper extends Helper $bsProgressTimeline = new BoostrapProgressTimeline($options, $this); return $bsProgressTimeline->progressTimeline(); } + + public function listGroup($options, $data) + { + $bsListGroup = new BootstrapListGroup($options, $data, $this); + return $bsListGroup->listGroup(); + } + + public function genNode($node, $params=[], $content='') + { + return BootstrapGeneric::genNode($node, $params, $content); + } } class BootstrapGeneric @@ -148,7 +165,7 @@ class BootstrapGeneric } } - protected static function genNode($node, $params=[], $content="") + public static function genNode($node, $params=[], $content="") { return sprintf('<%s %s>%s', $node, BootstrapGeneric::genHTMLParams($params), $content, $node); } @@ -245,7 +262,6 @@ class BootstrapTabs extends BootstrapGeneric $this->bsClasses = [ 'nav' => [], 'nav-item' => $this->options['nav-item-class'], - ]; if (!empty($this->options['justify'])) { @@ -293,7 +309,9 @@ class BootstrapTabs extends BootstrapGeneric } $this->data['navs'][$activeTab]['active'] = true; - $this->options['vertical-size'] = $this->options['vertical-size'] < 0 || $this->options['vertical-size'] > 11 ? 3 : $this->options['vertical-size']; + if (!empty($this->options['vertical-size']) && $this->options['vertical-size'] != 'auto') { + $this->options['vertical-size'] = $this->options['vertical-size'] < 0 || $this->options['vertical-size'] > 11 ? 3 : $this->options['vertical-size']; + } $this->options['header-text-variant'] = $this->options['header-variant'] == 'light' ? 'body' : 'white'; $this->options['header-border-variant'] = $this->options['header-variant'] == 'light' ? '' : $this->options['header-variant']; @@ -346,17 +364,37 @@ class BootstrapTabs extends BootstrapGeneric private function genVerticalTabs() { - $html = $this->openNode('div', ['class' => array_merge(['row', ($this->options['card'] ? 'card flex-row' : '')], ["border-{$this->options['header-border-variant']}"])]); - $html .= $this->openNode('div', ['class' => array_merge(['col-' . $this->options['vertical-size'], ($this->options['card'] ? 'card-header border-right' : '')], ["bg-{$this->options['header-variant']}", "text-{$this->options['header-text-variant']}", "border-{$this->options['header-border-variant']}"])]); + $html = $this->openNode('div', ['class' => array_merge( + [ + 'row', + ($this->options['card'] ? 'card flex-row' : ''), + ($this->options['vertical-size'] == 'auto' ? 'flex-nowrap' : '') + ], + [ + "border-{$this->options['header-border-variant']}" + ] + )]); + $html .= $this->openNode('div', ['class' => array_merge( + [ + ($this->options['vertical-size'] != 'auto' ? 'col-' . $this->options['vertical-size'] : ''), + ($this->options['card'] ? 'card-header border-right' : '') + ], + [ + "bg-{$this->options['header-variant']}", + "text-{$this->options['header-text-variant']}", + "border-{$this->options['header-border-variant']}" + ])]); $html .= $this->genNav(); $html .= $this->closeNode('div'); - $html .= $this->openNode('div', [ - 'class' => array_merge( - ['col-' . (12 - $this->options['vertical-size']), ($this->options['card'] ? 'card-body2' : '')], - $this->options['body-class'] ?? [], - ["bg-{$this->options['body-variant']}", "text-{$this->options['body-text-variant']}"] - ) - ]); + $html .= $this->openNode('div', ['class' => array_merge( + [ + ($this->options['vertical-size'] != 'auto' ? 'col-' . (12 - $this->options['vertical-size']) : ''), + ($this->options['card'] ? 'card-body2' : '') + ], + [ + "bg-{$this->options['body-variant']}", + "text-{$this->options['body-text-variant']}" + ])]); $html .= $this->genContent(); $html .= $this->closeNode('div'); $html .= $this->closeNode('div'); @@ -859,9 +897,10 @@ class BoostrapButton extends BootstrapGeneric { private function genIcon() { - return $this->genNode('span', [ - 'class' => ['mr-1', "fa fa-{$this->options['icon']}"], + $bsIcon = new BoostrapIcon($this->options['icon'], [ + 'class' => ['mr-1'] ]); + return $bsIcon->icon(); } private function genContent() @@ -910,6 +949,40 @@ class BoostrapBadge extends BootstrapGeneric { } } +class BoostrapIcon extends BootstrapGeneric { + private $icon = ''; + private $defaultOptions = [ + 'class' => [], + ]; + + function __construct($icon, $options=[]) { + $this->icon = $icon; + $this->processOptions($options); + } + + private function processOptions($options) + { + $this->options = array_merge($this->defaultOptions, $options); + $this->checkOptionValidity(); + } + + public function icon() + { + return $this->genIcon(); + } + + private function genIcon() + { + $html = $this->genNode('span', [ + 'class' => array_merge( + is_array($this->options['class']) ? $this->options['class'] : [$this->options['class']], + ["fa fa-{$this->icon}"] + ), + ]); + return $html; + } +} + class BoostrapModal extends BootstrapGeneric { private $defaultOptions = [ 'size' => '', @@ -1408,4 +1481,100 @@ class BoostrapProgressTimeline extends BootstrapGeneric { ], $ulIcons . $ulText); return $html; } +} + +class BootstrapListGroup extends BootstrapGeneric +{ + private $defaultOptions = [ + 'hover' => false, + ]; + + private $bsClasses = null; + + function __construct($options, $data, $btHelper) { + $this->data = $data; + $this->processOptions($options); + $this->btHelper = $btHelper; + } + + private function processOptions($options) + { + $this->options = array_merge($this->defaultOptions, $options); + } + + public function listGroup() + { + return $this->genListGroup(); + } + + private function genListGroup() + { + $html = $this->openNode('div', [ + 'class' => ['list-group',], + ]); + foreach ($this->data as $item) { + $html .= $this->genItem($item); + } + $html .= $this->closeNode('div'); + return $html; + } + + private function genItem($item) + { + if (!empty($item['heading'])) { // complex layout with heading, badge and body + $html = $this->genNode('a', [ + 'class' => ['list-group-item', (!empty($this->options['hover']) ? 'list-group-item-action' : ''),], + ], implode('', [ + $this->genHeadingGroup($item), + $this->genBody($item), + ])); + } else { // simple layout with just
  • -like elements + $html = $this->genNode('a', [ + 'class' => ['list-group-item', 'd-flex', 'align-items-center', 'justify-content-between'], + ], implode('', [ + h($item['text']), + $this->genBadge($item) + ])); + } + return $html; + } + + private function genHeadingGroup($item) + { + $html = $this->genNode('div', [ + 'class' => ['d-flex', 'w-100', 'justify-content-between',], + ], implode('', [ + $this->genHeading($item), + $this->genBadge($item) + ])); + return $html; + } + + private function genHeading($item) + { + if (empty($item['heading'])) { + return ''; + } + return $this->genNode('h5', [ + 'class' => ['mb-1'], + ], h($item['heading'])); + } + + private function genBadge($item) + { + if (empty($item['badge'])) { + return ''; + } + return $this->genNode('span', [ + 'class' => ['badge badge-pill', (!empty($item['badge-variant']) ? "badge-{$item['badge-variant']}" : 'badge-primary')], + ], h($item['badge'])); + } + + private function genBody($item) + { + if (!empty($item['bodyHTML'])) { + return $item['bodyHTML']; + } + return !empty($item['body']) ? h($item['body']) : ''; + } } \ No newline at end of file diff --git a/templates/Instance/save_setting.php b/templates/Instance/save_setting.php new file mode 100644 index 0000000..6609749 --- /dev/null +++ b/templates/Instance/save_setting.php @@ -0,0 +1,17 @@ +element('genericElements/Form/genericForm', [ + 'data' => [ + 'description' => __('Authkeys are used for API access. A user can have more than one authkey, so if you would like to use separate keys per tool that queries Cerebrate, add additional keys. Use the comment field to make identifying your keys easier.'), + 'fields' => [ + [ + 'field' => 'name', + ], + [ + 'field' => 'value' + ], + ], + 'submit' => [ + 'action' => $this->request->getParam('action') + ] + ] +]); diff --git a/templates/Instance/settings.php b/templates/Instance/settings.php new file mode 100644 index 0000000..39be8e6 --- /dev/null +++ b/templates/Instance/settings.php @@ -0,0 +1,303 @@ + 'danger', + 'warning' => 'warning', + 'info' => 'info', +]; +$this->set('variantFromSeverity', $variantFromSeverity); +$settingTable = genNavcard($settingsProvider, $this); +?> + + + +
    +
    + element('Settings/search', [ + ]); + ?> +
    + +
    + + $sectionSettings) { + if (!empty($sectionSettings)) { + $cardContent[] = genContentForNav($sectionSettings, $appView); + } else { + $cardContent[] = __('No Settings available yet'); + } + } + array_unshift($cardNavs, __('Settings Diagnostic')); + $notice = $appView->element('Settings/notice', [ + 'variantFromSeverity' => $appView->get('variantFromSeverity'), + ]); + array_unshift($cardContent, $notice); + $tabsOptions0 = [ + // 'vertical' => true, + // 'vertical-size' => 2, + 'card' => false, + 'pills' => false, + 'justify' => 'center', + 'nav-class' => ['settings-tabs'], + 'data' => [ + 'navs' => $cardNavs, + 'content' => $cardContent + ] + ]; + $table0 = $appView->Bootstrap->tabs($tabsOptions0); + return $table0; +} + +function genContentForNav($sectionSettings, $appView) +{ + $groupedContent = []; + $groupedSetting = []; + foreach ($sectionSettings as $sectionName => $subSectionSettings) { + if (!empty($subSectionSettings)) { + $groupedContent[] = genSection($sectionName, $subSectionSettings, $appView); + } else { + $groupedContent[] = ''; + } + if (!isLeaf($subSectionSettings)) { + $groupedSetting[$sectionName] = array_filter( // only show grouped settings + array_keys($subSectionSettings), + function ($settingGroupName) use ($subSectionSettings) { + return !isLeaf($subSectionSettings[$settingGroupName]) && !empty($subSectionSettings[$settingGroupName]); + } + ); + } + } + $contentHtml = implode('', $groupedContent); + $scrollspyNav = $appView->element('Settings/scrollspyNav', [ + 'groupedSetting' => $groupedSetting + ]); + $mainPanelHeight = 'calc(100vh - 42px - 1rem - 56px - 38px - 1rem)'; + $container = '
    '; + $container .= "
    {$scrollspyNav}
    "; + $container .= "
    {$contentHtml}
    "; + $container .= '
    '; + return $container; +} + +function genSection($sectionName, $subSectionSettings, $appView) +{ + $sectionContent = []; + $sectionContent[] = '
    '; + if (isLeaf($subSectionSettings)) { + $panelHTML = $appView->element('Settings/panel', [ + 'sectionName' => $sectionName, + 'panelName' => $sectionName, + 'panelSettings' => $subSectionSettings, + ]); + $sectionContent[] = $panelHTML; + } else { + if (count($subSectionSettings) > 1) { + $sectionContent[] = sprintf('

    %s

    ', getResolvableID($sectionName), h($sectionName)); + } + foreach ($subSectionSettings as $panelName => $panelSettings) { + if (!empty($panelSettings)) { + $panelHTML = $appView->element('Settings/panel', [ + 'sectionName' => $sectionName, + 'panelName' => $panelName, + 'panelSettings' => $panelSettings, + ]); + $sectionContent[] = $panelHTML; + } else { + $sectionContent[] = ''; + } + } + } + $sectionContent[] = '
    '; + return implode('', $sectionContent); +} + +function isLeaf($setting) +{ + return !empty($setting['name']) && !empty($setting['type']); +} + +function getResolvableID($sectionName, $panelName=false) +{ + $id = sprintf('sp-%s', preg_replace('/(\.|\W)/', '_', h($sectionName))); + if (!empty($panelName)) { + $id .= '-' . preg_replace('/(\.|\W)/', '_', h($panelName)); + } + return $id; +} +?> + + + + \ No newline at end of file diff --git a/templates/element/Settings/field.php b/templates/element/Settings/field.php new file mode 100644 index 0000000..4d6c2df --- /dev/null +++ b/templates/element/Settings/field.php @@ -0,0 +1,107 @@ +Bootstrap->genNode( + $setting['type'] == 'textarea' ? 'textarea' : 'input', + [ + 'class' => [ + 'form-control', + 'pr-4', + (!empty($setting['error']) ? 'is-invalid' : ''), + (!empty($setting['error']) ? "border-{$appView->get('variantFromSeverity')[$setting['severity']]}" : ''), + (!empty($setting['error']) ? $appView->get('variantFromSeverity')[$setting['severity']] : ''), + ], + ($setting['type'] == 'textarea' ? '' : 'type') => ($setting['type'] == 'textarea' ? '' : 'text'), + 'id' => $settingId, + 'data-setting-name' => $settingName, + 'value' => isset($setting['value']) ? $setting['value'] : "", + 'placeholder' => $setting['default'] ?? '', + 'aria-describedby' => "{$settingId}Help" + ] + ); + })($settingName, $setting, $this); + + } elseif ($setting['type'] == 'boolean') { + $input = (function ($settingName, $setting, $appView) { + $settingId = str_replace('.', '_', $settingName); + $switch = $appView->Bootstrap->genNode('input', [ + 'class' => [ + 'custom-control-input', + (!empty($setting['error']) ? 'is-invalid' : ''), + (!empty($setting['error']) ? $appView->get('variantFromSeverity')[$setting['severity']] : ''), + ], + 'type' => 'checkbox', + 'value' => !empty($setting['value']) ? 1 : 0, + (!empty($setting['value']) ? 'checked' : '') => !empty($setting['value']) ? 'checked' : '', + 'id' => $settingId, + 'data-setting-name' => $settingName, + ]); + $label = $appView->Bootstrap->genNode('label', [ + 'class' => [ + 'custom-control-label' + ], + 'for' => $settingId, + ], h($setting['description'])); + $container = $appView->Bootstrap->genNode('div', [ + 'class' => [ + 'custom-control', + 'custom-switch', + ], + ], implode('', [$switch, $label])); + return $container; + })($settingName, $setting, $this); + $description = ''; + + } elseif ($setting['type'] == 'integer') { + $input = (function ($settingName, $setting, $appView) { + $settingId = str_replace('.', '_', $settingName); + return $appView->Bootstrap->genNode('input', [ + 'class' => [ + 'form-control', + (!empty($setting['error']) ? 'is-invalid' : ''), + (!empty($setting['error']) ? "border-{$appView->get('variantFromSeverity')[$setting['severity']]}" : ''), + (!empty($setting['error']) ? $appView->get('variantFromSeverity')[$setting['severity']] : ''), + ], + 'type' => 'number', + 'min' => '0', + 'step' => 1, + 'id' => $settingId, + 'data-setting-name' => $settingName, + 'aria-describedby' => "{$settingId}Help" + ]); + })($settingName, $setting, $this); + + } elseif ($setting['type'] == 'select' || $setting['type'] == 'multi-select') { + $input = (function ($settingName, $setting, $appView) { + $settingId = str_replace('.', '_', $settingName); + $setting['value'] = $setting['value'] ?? ''; + $options = []; + if ($setting['type'] == 'select') { + $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'] == $key ? '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']) ? $appView->get('variantFromSeverity')[$setting['severity']] : ''), + ], + ($setting['type'] == 'multi-select' ? 'multiple' : '') => '', + 'id' => $settingId, + 'data-setting-name' => $settingName, + 'placeholder' => $setting['default'] ?? '', + 'aria-describedby' => "{$settingId}Help" + ], $options); + })($settingName, $setting, $this); + } + echo $input; diff --git a/templates/element/Settings/fieldGroup.php b/templates/element/Settings/fieldGroup.php new file mode 100644 index 0000000..ed833ff --- /dev/null +++ b/templates/element/Settings/fieldGroup.php @@ -0,0 +1,64 @@ +Bootstrap->genNode('span', [ + 'class' => [ + 'ml-1', + 'd-inline-block', + 'depends-on-icon' + ], + 'style' => 'min-width: 0.75em;', + 'title' => __('This setting depends on the validity of: {0}', h($setting['dependsOn'])), + ], $this->Bootstrap->genNode('sup', [ + 'class' => $this->FontAwesome->getClass('info'), + ])); + } + $label = $this->Bootstrap->genNode('label', [ + 'class' => ['font-weight-bolder', 'mb-0'], + 'for' => $settingId + ], sprintf('%s', h($settingId), h($settingId), h($setting['name'])) . $dependsOnHtml); + + $description = ''; + if (!empty($setting['description']) && (empty($setting['type']) || $setting['type'] != 'boolean')) { + $description = $this->Bootstrap->genNode('small', [ + 'class' => ['form-text', 'text-muted', 'mt-0'], + 'id' => "{$settingId}Help" + ], h($setting['description'])); + } + $textColor = 'text-warning'; + if (!empty($setting['severity'])) { + $textColor = "text-{$this->get('variantFromSeverity')[$setting['severity']]}"; + } + $validationError = $this->Bootstrap->genNode('div', [ + 'class' => ['d-block', 'invalid-feedback', $textColor], + ], (!empty($setting['error']) ? h($setting['errorMessage']) : '')); + + $input = $this->element('Settings/field', [ + 'setting' => $setting, + 'settingName' => $settingName, + ]); + + $inputGroupSave = $this->Bootstrap->genNode('div', [ + 'class' => ['input-group-append', 'd-none', 'position-relative', 'input-group-actions'], + ], implode('', [ + $this->Bootstrap->genNode('a', [ + 'class' => ['position-absolute', 'fas fa-times', 'p-abs-center-y', 'text-reset text-decoration-none', 'btn-reset-setting'], + 'href' => '#', + ]), + $this->Bootstrap->genNode('button', [ + 'class' => ['btn', 'btn-success', 'btn-save-setting'], + 'type' => 'button', + ], __('save')), + ])); + $inputGroup = $this->Bootstrap->genNode('div', [ + 'class' => ['input-group'], + ], implode('', [$input, $inputGroupSave])); + + $container = $this->Bootstrap->genNode('div', [ + 'class' => ['form-group', 'mb-2'] + ], implode('', [$label, $inputGroup, $description, $validationError])); + + echo $container; +?> \ No newline at end of file diff --git a/templates/element/Settings/notice.php b/templates/element/Settings/notice.php new file mode 100644 index 0000000..9e79877 --- /dev/null +++ b/templates/element/Settings/notice.php @@ -0,0 +1,118 @@ + __('Your Cerebrate instance requires immediate attention.'), + 'warning' => __('Issues found, it is recommended that you resolve them.'), + 'info' => __('There are some optional settings that are incorrect or not set.'), +]; +$headingPerLevel = [ + 'critical' => __('Critical settings'), + 'warning' => __('Warning settings'), + 'info' => __('Info settings'), +]; +$noticeDescriptionPerLevel = [ + 'critical' => __('Cerebrate will not operate correctly or will be unsecure until these issues are resolved.'), + 'warning' => __('Some of the features of Cerebrate cannot be utilised until these issues are resolved.'), + 'info' => __('There are some optional tweaks that could be done to improve the looks of your Cerebrate instance.'), +]; + +$settingNoticeListHeader = []; +$settingNoticeList = []; + +$alertVariant = 'info'; +$skipHeading = false; +$alertBody = ''; +$tableItems = []; +foreach (array_keys($mainNoticeHeading) as $level) { + if(!empty($notices[$level])) { + $variant = $variantFromSeverity[$level]; + if (!$skipHeading) { + $alertBody .= sprintf('
    %s
    ', $mainNoticeHeading[$level]); + $alertVariant = $variant; + $skipHeading = true; + } + $tableItems[] = [ + 'severity' => $headingPerLevel[$level], + 'issues' => count($notices[$level]), + 'badge-variant' => $variant, + 'description' => $noticeDescriptionPerLevel[$level], + ]; + $settingNoticeListHeader[] = [ + 'html' => $this->Bootstrap->badge([ + 'variant' => $variantFromSeverity[$level], + 'text' => $level + ]) + ]; + $settingNoticeList[] = $this->Bootstrap->table([ + 'small' => true, + 'striped' => false, + 'hover' => false, + 'borderless' => true, + 'bordered' => false, + ], [ + 'fields' => [ + ['key' => 'name', 'label' => __('Name'), 'formatter' => function($name, $row) { + $settingID = preg_replace('/(\.|\W)/', '_', h($row['true-name'])); + return sprintf('%s', $settingID, $settingID, h($name)); + }], + ['key' => 'setting-path', 'label' => __('Category'), 'formatter' => function($path, $row) { + return '' . h(str_replace('.', ' ▸ ', $path)) . ''; + }], + ['key' => 'value', 'label' => __('Value'), 'formatter' => function($value, $row) { + $formatedValue = ''; + if (is_null($value)) { + $formatedValue .= '' . __('No value') . ''; + } else if ($value === '') { + $formatedValue .= '' . __('Empty string') . ''; + } else if (is_bool($value)) { + $formatedValue .= '' . ($value ? __('true') : __('false')) . ''; + } else { + $formatedValue .= h($value); + } + $formatedValue .= ''; + return $formatedValue; + }], + ['key' => 'description', 'label' => __('Description')] + ], + 'items' => $notices[$level], + ]); + } +} + +$alertBody = $this->Bootstrap->table([ + 'small' => true, + 'striped' => false, + 'hover' => false, + 'borderless' => true, + 'bordered' => false, + 'tableClass' => 'mb-0' +], [ + 'fields' => [ + ['key' => 'severity', 'label' => __('Severity')], + ['key' => 'issues', 'label' => __('Issues'), 'formatter' => function($count, $row) { + return $this->Bootstrap->badge([ + 'variant' => $row['badge-variant'], + 'text' => $count + ]); + }], + ['key' => 'description', 'label' => __('Description')] + ], + 'items' => $tableItems, +]); + +$settingNotice = $this->Bootstrap->alert([ + 'dismissible' => false, + 'variant' => $alertVariant, + 'html' => $alertBody +]); +$settingNotice = sprintf('
    %s
    ', $settingNotice); +echo $settingNotice; + +$tabsOptions = [ + 'card' => true, + 'pills' => false, + 'data' => [ + 'navs' => $settingNoticeListHeader, + 'content' => $settingNoticeList + ] +]; +echo $this->Bootstrap->tabs($tabsOptions); diff --git a/templates/element/Settings/panel.php b/templates/element/Settings/panel.php new file mode 100644 index 0000000..c49d996 --- /dev/null +++ b/templates/element/Settings/panel.php @@ -0,0 +1,57 @@ +element('Settings/fieldGroup', [ + 'panelName' => $panelName, + 'panelSettings' => $panelSettings, + 'settingName' => $panelName, + 'setting' => $panelSettings, + ]); + $panelHTML = "
    {$singleSetting}
    "; +} else { + $panelID = getResolvableID($sectionName, $panelName); + $panelHTML .= sprintf('

    %s%s

    ', + $panelID, + $panelID, + !empty($panelSettings['_icon']) ? $this->Bootstrap->icon($panelSettings['_icon'], ['class' => 'mr-1']) : '', + h($panelName) + ); + if (!empty($panelSettings['_description'])) { + $panelHTML .= $this->Bootstrap->genNode('div', [ + 'class' => ['mb-1',], + ], h($panelSettings['_description'])); + } + $groupIssueSeverity = false; + foreach ($panelSettings as $singleSettingName => $singleSetting) { + if (substr($singleSettingName, 0, 1) == '_') { + continue; + } + $singleSettingHTML = $this->element('Settings/fieldGroup', [ + 'panelName' => $panelName, + 'panelSettings' => $panelSettings, + 'settingName' => $singleSettingName, + 'setting' => $singleSetting, + ]); + $panelHTML .= sprintf('
    %s
    ', $singleSettingHTML); + if (!empty($singleSetting['error'])) { + $settingVariant = $this->get('variantFromSeverity')[$singleSetting['severity']]; + if ($groupIssueSeverity != 'danger') { + if ($groupIssueSeverity != 'warning') { + $groupIssueSeverity = $settingVariant; + } + } + } + } + $panelHTML = $this->Bootstrap->genNode('div', [ + 'class' => [ + 'shadow', + 'p-2', + 'mb-4', + 'rounded', + 'settings-group', + (!empty($groupIssueSeverity) ? "callout callout-${groupIssueSeverity}" : ''), + ($this->get('darkMode') ? 'bg-dark' : 'bg-light') + ], + ], $panelHTML); +} +echo $panelHTML; \ No newline at end of file diff --git a/templates/element/Settings/scrollspyNav.php b/templates/element/Settings/scrollspyNav.php new file mode 100644 index 0000000..6ce9a6f --- /dev/null +++ b/templates/element/Settings/scrollspyNav.php @@ -0,0 +1,63 @@ + + + + + \ No newline at end of file diff --git a/templates/element/Settings/search.php b/templates/element/Settings/search.php new file mode 100644 index 0000000..4a3acd1 --- /dev/null +++ b/templates/element/Settings/search.php @@ -0,0 +1,121 @@ + + + + + \ No newline at end of file diff --git a/templates/layout/default.php b/templates/layout/default.php index cc8c564..1e2cbde 100644 --- a/templates/layout/default.php +++ b/templates/layout/default.php @@ -41,6 +41,7 @@ $cakeDescription = 'Cerebrate'; Html->script('main.js') ?> Html->script('bootstrap-helper.js') ?> Html->script('api-helper.js') ?> + Html->script('select2.min.js') ?> Html->script('CodeMirror/codemirror.js') ?> Html->script('CodeMirror/mode/javascript/javascript') ?> Html->script('CodeMirror/addon/hint/show-hint') ?> @@ -53,6 +54,8 @@ $cakeDescription = 'Cerebrate'; Html->css('CodeMirror/codemirror-additional') ?> Html->css('CodeMirror/addon/hint/show-hint') ?> Html->css('CodeMirror/addon/lint/lint') ?> + Html->css('select2.min') ?> + Html->css('select2-bootstrap4.min') ?> fetch('meta') ?> fetch('css') ?> fetch('script') ?> @@ -83,5 +86,6 @@ $cakeDescription = 'Cerebrate'; diff --git a/webroot/css/bootstrap-additional.css b/webroot/css/bootstrap-additional.css index 95e2e80..fd47cfa 100644 --- a/webroot/css/bootstrap-additional.css +++ b/webroot/css/bootstrap-additional.css @@ -9,13 +9,13 @@ max-width: 25% !important; } .mh-75 { - max-width: 75% !important; + max-height: 75% !important; } .mh-50 { - max-width: 50% !important; + max-height: 50% !important; } .mh-25 { - max-width: 25% !important; + max-height: 25% !important; } /* Toast */ @@ -107,4 +107,101 @@ div.progress-timeline .progress-line { } div.progress-timeline .progress-line.progress-inactive { opacity: 0.5; +} + +.callout { + border: 1px solid #00000000; + border-left-color: var(--light); + border-left-width: 1px; + border-left-width: .25rem; + border-radius: .25rem; +} +.callout-primary { + border-left-color: var(--primary); +} +.callout-info { + border-left-color: var(--info); +} +.callout-success { + border-left-color: var(--success); +} +.callout-warning { + border-left-color: var(--warning); +} +.callout-danger { + border-left-color: var(--danger); +} +.callout-dark { + border-left-color: var(--dark); +} +.callout-light { + border-left-color: var(--light); +} + +.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") +} +.form-control.is-invalid.warning:focus { + box-shadow: 0 0 0 0.2rem #ffc10740; +} + +.form-control.is-invalid.info { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%2317a2b8' 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='%2317a2b8' stroke='none'/%3e%3c/svg%3e") +} +.form-control.is-invalid.info:focus { + box-shadow: 0 0 0 0.2rem #17a2b840; +} + +.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-select.is-invalid.info { + 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='%2317a2b8' 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='%2317a2b8' 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; +} +.custom-control-input.is-invalid.warning ~ .custom-control-label::before { + border-color: #ffc107; +} +.custom-control-input.is-invalid.warning:checked ~ .custom-control-label::before { + background-color: #ffc107; +} +.custom-control-input.is-invalid.warning:focus:not(:checked) ~ .custom-control-label::before { + border-color: #ffc107; +} +.custom-control-input.is-invalid.warning:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 0.2rem #ffc10740; +} +.custom-control-input.is-invalid.info ~ .custom-control-label { + color: unset; +} +.custom-control-input.is-invalid.info ~ .custom-control-label::before { + border-color: #17a2b8; +} +.custom-control-input.is-invalid.info:checked ~ .custom-control-label::before { + background-color: #17a2b8; +} +.custom-control-input.is-invalid.info:focus:not(:checked) ~ .custom-control-label::before { + border-color: #17a2b8; +} +.custom-control-input.is-invalid.info:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 0.2rem #17a2b840; +} + +.p-abs-center-y { + top: 50%; + transform: translateY(-50%); +} + +.p-abs-center-x { + left: 50%; + transform: translateX(-50%); +} + +.p-abs-center-both { + top: 50%; + left: 50%; + transform: translateX(-50%) translateY(-50%); } \ No newline at end of file diff --git a/webroot/css/select2-bootstrap4.min.css b/webroot/css/select2-bootstrap4.min.css new file mode 100644 index 0000000..6d36591 --- /dev/null +++ b/webroot/css/select2-bootstrap4.min.css @@ -0,0 +1,2 @@ +.select2-container{display:block}.select2-container *:focus{outline:0}.input-group .select2-container--bootstrap4{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.input-group-prepend ~ .select2-container--bootstrap4 .select2-selection{border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.select2-container--bootstrap4:not(:last-child) .select2-selection{border-top-right-radius:0;border-bottom-right-radius:0}.select2-container--bootstrap4 .select2-selection{width:100%;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem;-webkit-transition:border-color 0.15s ease-in-out,-webkit-box-shadow 0.15s ease-in-out;transition:border-color 0.15s ease-in-out,-webkit-box-shadow 0.15s ease-in-out;transition:border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out;transition:border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,-webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.select2-container--bootstrap4 .select2-selection{-webkit-transition:none;transition:none}}.select2-container--bootstrap4.select2-container--focus .select2-selection{border-color:#80bdff;-webkit-box-shadow:0 0 0 .2rem rgba(0,123,255,0.25);box-shadow:0 0 0 .2rem rgba(0,123,255,0.25)}.select2-container--bootstrap4.select2-container--focus.select2-container--open .select2-selection{border-bottom:none;border-bottom-right-radius:0;border-bottom-left-radius:0}.select2-container--bootstrap4.select2-container--open.select2-container--above .select2-selection{border-top-left-radius:0;border-top-right-radius:0}.select2-container--bootstrap4.select2-container--open.select2-container--below .select2-selection{border-bottom-right-radius:0;border-bottom-left-radius:0}.select2-container--bootstrap4.select2-container--disabled .select2-selection,.select2-container--bootstrap4.select2-container--disabled.select2-container--focus .select2-selection{cursor:not-allowed;background-color:#e9ecef;border-color:#ced4da;-webkit-box-shadow:none;box-shadow:none}.select2-container--bootstrap4.select2-container--disabled .select2-search__field,.select2-container--bootstrap4.select2-container--disabled.select2-container--focus .select2-search__field{background-color:transparent}select.is-invalid ~ .select2-container--bootstrap4 .select2-selection,form.was-validated select:invalid ~ .select2-container--bootstrap4 .select2-selection{border-color:#dc3545}select.is-valid ~ .select2-container--bootstrap4 .select2-selection,form.was-validated select:valid ~ .select2-container--bootstrap4 .select2-selection{border-color:#28a745}.select2-container--bootstrap4 .select2-search{width:100%}.select2-container--bootstrap4 .select2-dropdown{border-color:#ced4da;border-radius:0}.select2-container--bootstrap4 .select2-dropdown.select2-dropdown--below{border-top:none;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.select2-container--bootstrap4 .select2-dropdown.select2-dropdown--above{border-top:1px solid #ced4da;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.select2-container--bootstrap4 .select2-dropdown .select2-results__option[aria-selected="true"]{color:#212529;background-color:#f2f2f2}.select2-container--bootstrap4 .select2-results__option--highlighted,.select2-container--bootstrap4 .select2-results__option--highlighted.select2-results__option[aria-selected="true"]{color:#fff;background-color:#007bff}.select2-container--bootstrap4 .select2-results__option[role="group"]{padding:0}.select2-container--bootstrap4 .select2-results__option[role="group"] .select2-results__options--nested .select2-results__option{padding-left:1em}.select2-container--bootstrap4 .select2-results__option{padding:.375rem .75rem}.select2-container--bootstrap4 .select2-results>.select2-results__options{max-height:15em;overflow-y:auto}.select2-container--bootstrap4 .select2-results__group{display:list-item;padding:6px;color:#6c757d}.select2-container--bootstrap4 .select2-selection__clear{float:right;width:.9em;height:.9em;padding-left:.15em;margin-top:.7em;margin-right:.3em;line-height:.75em;color:#f8f9fa;background-color:#c8c8c8;border-radius:100%}.select2-container--bootstrap4 .select2-selection__clear:hover{background-color:#afafaf}.select2-container--bootstrap4 .select2-selection--single{height:calc(1.5em + .75rem + 2px) !important}.select2-container--bootstrap4 .select2-selection--single .select2-selection__placeholder{line-height:calc(1.5em + .75rem);color:#6c757d}.select2-container--bootstrap4 .select2-selection--single .select2-selection__arrow{position:absolute;top:50%;right:3px;width:20px}.select2-container--bootstrap4 .select2-selection--single .select2-selection__arrow b{position:absolute;top:60%;left:50%;width:0;height:0;margin-top:-2px;margin-left:-4px;border-color:#343a40 transparent transparent transparent;border-style:solid;border-width:5px 4px 0}.select2-container--bootstrap4 .select2-selection--single .select2-selection__rendered{padding-left:.75rem;line-height:calc(1.5em + .75rem);color:#495057}.select2-search--dropdown .select2-search__field{padding:.375rem .75rem;border:1px solid #ced4da;border-radius:.25rem}.select2-results__message{color:#6c757d}.select2-container--bootstrap4 .select2-selection--multiple{min-height:calc(1.5em + .75rem + 2px) !important}.select2-container--bootstrap4 .select2-selection--multiple .select2-selection__rendered{-webkit-box-sizing:border-box;box-sizing:border-box;width:100%;padding:0 .375rem;margin:0;list-style:none}.select2-container--bootstrap4 .select2-selection--multiple .select2-selection__choice{float:left;padding:0;padding-right:.75rem;margin-top:calc(.375rem - 2px);margin-right:.375rem;color:#495057;cursor:pointer;border:1px solid #bdc6d0;border-radius:.2rem}.select2-container--bootstrap4 .select2-selection--multiple .select2-search__field{color:#495057}.select2-container--bootstrap4 .select2-selection--multiple .select2-selection__choice+.select2-search{width:0}.select2-container--bootstrap4 .select2-selection--multiple .select2-selection__choice__remove{float:left;padding-right:3px;padding-left:3px;margin-right:1px;margin-left:3px;font-weight:700;color:#bdc6d0}.select2-container--bootstrap4 .select2-selection--multiple .select2-selection__choice__remove:hover{color:#343a40}.select2-container--bootstrap4 .select2-selection--multiple .select2-selection__clear{position:absolute !important;top:0;right:.7em;float:none;margin-right:0}.select2-container--bootstrap4.select2-container--disabled .select2-selection--multiple .select2-selection__choice{padding:0 5px;cursor:not-allowed}.select2-container--bootstrap4.select2-container--disabled .select2-selection--multiple .select2-selection__choice .select2-selection__choice__remove{display:none} + diff --git a/webroot/css/select2.min.css b/webroot/css/select2.min.css new file mode 100644 index 0000000..39a4547 --- /dev/null +++ b/webroot/css/select2.min.css @@ -0,0 +1 @@ +.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{background-color:transparent;border:none;font-size:1em}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline;list-style:none;padding:0}.select2-container .select2-selection--multiple .select2-selection__clear{background-color:transparent;border:none;font-size:1em}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;margin-left:5px;padding:0;max-width:100%;resize:none;height:18px;vertical-align:bottom;font-family:sans-serif;overflow:hidden;word-break:keep-all}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option--selectable{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(50%) !important;clip-path:inset(50%) !important;height:1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important;white-space:nowrap !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;height:26px;margin-right:20px;padding-right:0px}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;padding-bottom:5px;padding-right:5px;position:relative}.select2-container--default .select2-selection--multiple.select2-selection--clearable{padding-right:25px}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;font-weight:bold;height:20px;margin-right:10px;margin-top:5px;position:absolute;right:0;padding:1px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:inline-block;margin-left:5px;margin-top:5px;padding:0;padding-left:20px;position:relative;max-width:100%;overflow:hidden;text-overflow:ellipsis;vertical-align:bottom;white-space:nowrap}.select2-container--default .select2-selection--multiple .select2-selection__choice__display{cursor:default;padding-left:2px;padding-right:5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{background-color:transparent;border:none;border-right:1px solid #aaa;border-top-left-radius:4px;border-bottom-left-radius:4px;color:#999;cursor:pointer;font-size:1em;font-weight:bold;padding:0 4px;position:absolute;left:0;top:0}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:focus{background-color:#f1f1f1;color:#333;outline:none}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__display{padding-left:5px;padding-right:2px}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{border-left:1px solid #aaa;border-right:none;border-top-left-radius:0;border-bottom-left-radius:0;border-top-right-radius:4px;border-bottom-right-radius:4px}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__clear{float:left;margin-left:10px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--group{padding:0}.select2-container--default .select2-results__option--disabled{color:#999}.select2-container--default .select2-results__option--selected{background-color:#ddd}.select2-container--default .select2-results__option--highlighted.select2-results__option--selectable{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;height:26px;margin-right:20px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0;padding-bottom:5px;padding-right:5px}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;display:inline-block;margin-left:5px;margin-top:5px;padding:0}.select2-container--classic .select2-selection--multiple .select2-selection__choice__display{cursor:default;padding-left:2px;padding-right:5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{background-color:transparent;border:none;border-top-left-radius:4px;border-bottom-left-radius:4px;color:#888;cursor:pointer;font-size:1em;font-weight:bold;padding:0 4px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555;outline:none}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__display{padding-left:5px;padding-right:2px}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{border-top-left-radius:0;border-bottom-left-radius:0;border-top-right-radius:4px;border-bottom-right-radius:4px}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option--group{padding:0}.select2-container--classic .select2-results__option--disabled{color:grey}.select2-container--classic .select2-results__option--highlighted.select2-results__option--selectable{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb} diff --git a/webroot/js/api-helper.js b/webroot/js/api-helper.js index 5b47bae..9062c23 100644 --- a/webroot/js/api-helper.js +++ b/webroot/js/api-helper.js @@ -68,6 +68,49 @@ class AJAXApi { } } + provideSuccessFeedback(response, toastOptions, skip=false) { + let alteredToastOptions = Object.assign( + { + 'variant': 'success' + }, + AJAXApi.defaultOptions.successToastOptions, + toastOptions + ) + alteredToastOptions.body = response.message + if (!skip && this.options.provideFeedback) { + UI.toast(alteredToastOptions) + } + } + + provideFailureFeedback(response, toastOptions, skip=false) { + let alteredToastOptions = Object.assign( + { + 'variant': 'danger' + }, + AJAXApi.defaultOptions.errorToastOptions, + toastOptions + ) + if (response.message && response.errors) { + if (Array.isArray(response.errors)) { + alteredToastOptions.title = response.message + alteredToastOptions.body = response.errors.join(', ') + } else if (typeof response.errors === 'string') { + alteredToastOptions.title = response.message + alteredToastOptions.body = response.errors + } else { + alteredToastOptions.title = 'There has been a problem with the operation' + alteredToastOptions.body = response.message + } + } else { + alteredToastOptions.title = 'There has been a problem with the operation' + alteredToastOptions.body = response.message + } + if (!skip && this.options.provideFeedback) { + UI.toast(alteredToastOptions) + console.warn(alteredToastOptions.body) + } + } + /** * Merge newOptions configuration into the current object * @param {Object} The options supported by AJAXApi#defaultOptions @@ -149,6 +192,7 @@ class AJAXApi { /** * @param {string} url - The URL from which to fetch the form * @param {Object} [dataToMerge={}] - Additional data to be integrated or modified in the form + * @param {Object} [options={}] - The options supported by AJAXApi#defaultOptions * @return {Promise} Promise object resolving to the result of the POST operation */ static async quickFetchAndPostForm(url, dataToMerge={}, options={}) { @@ -174,17 +218,10 @@ class AJAXApi { throw new Error(`Network response was not ok. \`${response.statusText}\``) } const dataHtml = await response.text(); - this.provideFeedback({ - variant: 'success', - title: 'URL fetched', - }, false, skipFeedback); + this.provideSuccessFeedback({message: 'URL fetched'}, {}, skipFeedback) toReturn = dataHtml; } catch (error) { - this.provideFeedback({ - variant: 'danger', - title: 'There has been a problem with the operation', - body: error.message - }, true, skipFeedback); + this.provideFailureFeedback(error, {}, skipFeedback) toReturn = Promise.reject(error); } finally { if (!skipRequestHooks) { @@ -211,17 +248,10 @@ class AJAXApi { throw new Error(`Network response was not ok. \`${response.statusText}\``) } const dataJson = await response.json(); - this.provideFeedback({ - variant: 'success', - title: 'URL fetched', - }, false, skipFeedback); + this.provideSuccessFeedback({message: 'JSON fetched'}, {}, skipFeedback) toReturn = dataJson; } catch (error) { - this.provideFeedback({ - variant: 'danger', - title: 'There has been a problem with the operation', - body: error.message - }, true, skipFeedback); + this.provideFailureFeedback(error, {}, skipFeedback) toReturn = Promise.reject(error); } finally { if (!skipRequestHooks) { @@ -256,11 +286,7 @@ class AJAXApi { } toReturn = form[0]; } catch (error) { - this.provideFeedback({ - variant: 'danger', - title: 'There has been a problem with the operation', - body: error.message - }, true, skipFeedback); + this.provideFailureFeedback(error, {}, skipFeedback) toReturn = Promise.reject(error); } finally { if (!skipRequestHooks) { @@ -301,13 +327,10 @@ class AJAXApi { variant: 'success', body: data.message }, false, skipFeedback); + this.provideSuccessFeedback(data, {}, skipFeedback) toReturn = data; } else { - this.provideFeedback({ - variant: 'danger', - title: 'There has been a problem with the operation', - body: data.message - }, true, skipFeedback); + this.provideFailureFeedback(data, {}, skipFeedback) toReturn = Promise.reject(data.errors); } } catch (error) { @@ -326,7 +349,7 @@ class AJAXApi { } /** - * @param {HTMLFormElement} form - The form to be posted + * @param {HTMLFormElement} form - The form to be posted * @param {Object} [dataToMerge={}] - Additional data to be integrated or modified in the form * @param {boolean} [skipRequestHooks=false] - If true, default request hooks will be skipped * @param {boolean} [skipFeedback=false] - Pass this value to the AJAXApi.provideFeedback function @@ -355,17 +378,10 @@ class AJAXApi { try { const data = await response.json() if (data.success) { - this.provideFeedback({ - variant: 'success', - body: data.message - }, false, skipFeedback); + this.provideSuccessFeedback(data, {}, skipFeedback) toReturn = data; } else { - this.provideFeedback({ - variant: 'danger', - title: 'There has been a problem with the operation', - body: data.message - }, true, skipFeedback); + this.provideFailureFeedback(data, {}, skipFeedback) feedbackShown = true this.injectFormValidationFeedback(form, data.errors) toReturn = Promise.reject(data.errors); diff --git a/webroot/js/bootstrap-helper.js b/webroot/js/bootstrap-helper.js index 80af3e8..61bca3f 100644 --- a/webroot/js/bootstrap-helper.js +++ b/webroot/js/bootstrap-helper.js @@ -833,7 +833,7 @@ class OverlayFactory { * @property {string=('primary'|'secondary'|'success'|'danger'|'warning'|'info'|'light'|'dark'|'white'|'transparent')} variant - The variant of the overlay * @property {number} opacity - The opacity of the overlay * @property {boolean} rounded - If the overlay should be rounded - * @property {number} auto - Whether overlay and spinner options should be adapted automatically based on the node + * @property {boolean} auto - Whether overlay and spinner options should be adapted automatically based on the node * @property {string=('primary'|'secondary'|'success'|'danger'|'warning'|'info'|'light'|'dark'|'white'|'transparent')} spinnerVariant - The variant of the spinner * @property {boolean} spinnerSmall - If the spinner inside the overlay should be small * @property {string=('border'|'grow')} spinnerSmall - If the spinner inside the overlay should be small diff --git a/webroot/js/select2.min.js b/webroot/js/select2.min.js new file mode 100644 index 0000000..87c0c56 --- /dev/null +++ b/webroot/js/select2.min.js @@ -0,0 +1,2 @@ +/*! Select2 4.1.0-rc.0 | https://github.com/select2/select2/blob/master/LICENSE.md */ +!function(n){"function"==typeof define&&define.amd?define(["jquery"],n):"object"==typeof module&&module.exports?module.exports=function(e,t){return void 0===t&&(t="undefined"!=typeof window?require("jquery"):require("jquery")(e)),n(t),t}:n(jQuery)}(function(t){var e,n,s,p,r,o,h,f,g,m,y,v,i,a,_,s=(t&&t.fn&&t.fn.select2&&t.fn.select2.amd&&(u=t.fn.select2.amd),u&&u.requirejs||(u?n=u:u={},g={},m={},y={},v={},i=Object.prototype.hasOwnProperty,a=[].slice,_=/\.js$/,h=function(e,t){var n,s,i=c(e),r=i[0],t=t[1];return e=i[1],r&&(n=x(r=l(r,t))),r?e=n&&n.normalize?n.normalize(e,(s=t,function(e){return l(e,s)})):l(e,t):(r=(i=c(e=l(e,t)))[0],e=i[1],r&&(n=x(r))),{f:r?r+"!"+e:e,n:e,pr:r,p:n}},f={require:function(e){return w(e)},exports:function(e){var t=g[e];return void 0!==t?t:g[e]={}},module:function(e){return{id:e,uri:"",exports:g[e],config:(t=e,function(){return y&&y.config&&y.config[t]||{}})};var t}},r=function(e,t,n,s){var i,r,o,a,l,c=[],u=typeof n,d=A(s=s||e);if("undefined"==u||"function"==u){for(t=!t.length&&n.length?["require","exports","module"]:t,a=0;a":">",'"':""","'":"'","/":"/"};return"string"!=typeof e?e:String(e).replace(/[&<>"'\/\\]/g,function(e){return t[e]})},s.__cache={};var n=0;return s.GetUniqueElementId=function(e){var t=e.getAttribute("data-select2-id");return null!=t||(t=e.id?"select2-data-"+e.id:"select2-data-"+(++n).toString()+"-"+s.generateChars(4),e.setAttribute("data-select2-id",t)),t},s.StoreData=function(e,t,n){e=s.GetUniqueElementId(e);s.__cache[e]||(s.__cache[e]={}),s.__cache[e][t]=n},s.GetData=function(e,t){var n=s.GetUniqueElementId(e);return t?s.__cache[n]&&null!=s.__cache[n][t]?s.__cache[n][t]:r(e).data(t):s.__cache[n]},s.RemoveData=function(e){var t=s.GetUniqueElementId(e);null!=s.__cache[t]&&delete s.__cache[t],e.removeAttribute("data-select2-id")},s.copyNonInternalCssClasses=function(e,t){var n=(n=e.getAttribute("class").trim().split(/\s+/)).filter(function(e){return 0===e.indexOf("select2-")}),t=(t=t.getAttribute("class").trim().split(/\s+/)).filter(function(e){return 0!==e.indexOf("select2-")}),t=n.concat(t);e.setAttribute("class",t.join(" "))},s}),u.define("select2/results",["jquery","./utils"],function(d,p){function s(e,t,n){this.$element=e,this.data=n,this.options=t,s.__super__.constructor.call(this)}return p.Extend(s,p.Observable),s.prototype.render=function(){var e=d('
      ');return this.options.get("multiple")&&e.attr("aria-multiselectable","true"),this.$results=e},s.prototype.clear=function(){this.$results.empty()},s.prototype.displayMessage=function(e){var t=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var n=d(''),s=this.options.get("translations").get(e.message);n.append(t(s(e.args))),n[0].className+=" select2-results__message",this.$results.append(n)},s.prototype.hideMessages=function(){this.$results.find(".select2-results__message").remove()},s.prototype.append=function(e){this.hideLoading();var t=[];if(null!=e.results&&0!==e.results.length){e.results=this.sort(e.results);for(var n=0;n",{class:"select2-results__options select2-results__options--nested",role:"none"});i.append(l),o.append(a),o.append(i)}else this.template(e,t);return p.StoreData(t,"data",e),t},s.prototype.bind=function(t,e){var i=this,n=t.id+"-results";this.$results.attr("id",n),t.on("results:all",function(e){i.clear(),i.append(e.data),t.isOpen()&&(i.setClasses(),i.highlightFirstItem())}),t.on("results:append",function(e){i.append(e.data),t.isOpen()&&i.setClasses()}),t.on("query",function(e){i.hideMessages(),i.showLoading(e)}),t.on("select",function(){t.isOpen()&&(i.setClasses(),i.options.get("scrollAfterSelect")&&i.highlightFirstItem())}),t.on("unselect",function(){t.isOpen()&&(i.setClasses(),i.options.get("scrollAfterSelect")&&i.highlightFirstItem())}),t.on("open",function(){i.$results.attr("aria-expanded","true"),i.$results.attr("aria-hidden","false"),i.setClasses(),i.ensureHighlightVisible()}),t.on("close",function(){i.$results.attr("aria-expanded","false"),i.$results.attr("aria-hidden","true"),i.$results.removeAttr("aria-activedescendant")}),t.on("results:toggle",function(){var e=i.getHighlightedResults();0!==e.length&&e.trigger("mouseup")}),t.on("results:select",function(){var e,t=i.getHighlightedResults();0!==t.length&&(e=p.GetData(t[0],"data"),t.hasClass("select2-results__option--selected")?i.trigger("close",{}):i.trigger("select",{data:e}))}),t.on("results:previous",function(){var e,t=i.getHighlightedResults(),n=i.$results.find(".select2-results__option--selectable"),s=n.index(t);s<=0||(e=s-1,0===t.length&&(e=0),(s=n.eq(e)).trigger("mouseenter"),t=i.$results.offset().top,n=s.offset().top,s=i.$results.scrollTop()+(n-t),0===e?i.$results.scrollTop(0):n-t<0&&i.$results.scrollTop(s))}),t.on("results:next",function(){var e,t=i.getHighlightedResults(),n=i.$results.find(".select2-results__option--selectable"),s=n.index(t)+1;s>=n.length||((e=n.eq(s)).trigger("mouseenter"),t=i.$results.offset().top+i.$results.outerHeight(!1),n=e.offset().top+e.outerHeight(!1),e=i.$results.scrollTop()+n-t,0===s?i.$results.scrollTop(0):tthis.$results.outerHeight()||s<0)&&this.$results.scrollTop(n))},s.prototype.template=function(e,t){var n=this.options.get("templateResult"),s=this.options.get("escapeMarkup"),e=n(e,t);null==e?t.style.display="none":"string"==typeof e?t.innerHTML=s(e):d(t).append(e)},s}),u.define("select2/keys",[],function(){return{BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46}}),u.define("select2/selection/base",["jquery","../utils","../keys"],function(n,s,i){function r(e,t){this.$element=e,this.options=t,r.__super__.constructor.call(this)}return s.Extend(r,s.Observable),r.prototype.render=function(){var e=n('');return this._tabindex=0,null!=s.GetData(this.$element[0],"old-tabindex")?this._tabindex=s.GetData(this.$element[0],"old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),e.attr("title",this.$element.attr("title")),e.attr("tabindex",this._tabindex),e.attr("aria-disabled","false"),this.$selection=e},r.prototype.bind=function(e,t){var n=this,s=e.id+"-results";this.container=e,this.$selection.on("focus",function(e){n.trigger("focus",e)}),this.$selection.on("blur",function(e){n._handleBlur(e)}),this.$selection.on("keydown",function(e){n.trigger("keypress",e),e.which===i.SPACE&&e.preventDefault()}),e.on("results:focus",function(e){n.$selection.attr("aria-activedescendant",e.data._resultId)}),e.on("selection:update",function(e){n.update(e.data)}),e.on("open",function(){n.$selection.attr("aria-expanded","true"),n.$selection.attr("aria-owns",s),n._attachCloseHandler(e)}),e.on("close",function(){n.$selection.attr("aria-expanded","false"),n.$selection.removeAttr("aria-activedescendant"),n.$selection.removeAttr("aria-owns"),n.$selection.trigger("focus"),n._detachCloseHandler(e)}),e.on("enable",function(){n.$selection.attr("tabindex",n._tabindex),n.$selection.attr("aria-disabled","false")}),e.on("disable",function(){n.$selection.attr("tabindex","-1"),n.$selection.attr("aria-disabled","true")})},r.prototype._handleBlur=function(e){var t=this;window.setTimeout(function(){document.activeElement==t.$selection[0]||n.contains(t.$selection[0],document.activeElement)||t.trigger("blur",e)},1)},r.prototype._attachCloseHandler=function(e){n(document.body).on("mousedown.select2."+e.id,function(e){var t=n(e.target).closest(".select2");n(".select2.select2-container--open").each(function(){this!=t[0]&&s.GetData(this,"element").select2("close")})})},r.prototype._detachCloseHandler=function(e){n(document.body).off("mousedown.select2."+e.id)},r.prototype.position=function(e,t){t.find(".selection").append(e)},r.prototype.destroy=function(){this._detachCloseHandler(this.container)},r.prototype.update=function(e){throw new Error("The `update` method must be defined in child classes.")},r.prototype.isEnabled=function(){return!this.isDisabled()},r.prototype.isDisabled=function(){return this.options.get("disabled")},r}),u.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(e,t,n,s){function i(){i.__super__.constructor.apply(this,arguments)}return n.Extend(i,t),i.prototype.render=function(){var e=i.__super__.render.call(this);return e[0].classList.add("select2-selection--single"),e.html(''),e},i.prototype.bind=function(t,e){var n=this;i.__super__.bind.apply(this,arguments);var s=t.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",s).attr("role","textbox").attr("aria-readonly","true"),this.$selection.attr("aria-labelledby",s),this.$selection.attr("aria-controls",s),this.$selection.on("mousedown",function(e){1===e.which&&n.trigger("toggle",{originalEvent:e})}),this.$selection.on("focus",function(e){}),this.$selection.on("blur",function(e){}),t.on("focus",function(e){t.isOpen()||n.$selection.trigger("focus")})},i.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},i.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},i.prototype.selectionContainer=function(){return e("")},i.prototype.update=function(e){var t,n;0!==e.length?(n=e[0],t=this.$selection.find(".select2-selection__rendered"),e=this.display(n,t),t.empty().append(e),(n=n.title||n.text)?t.attr("title",n):t.removeAttr("title")):this.clear()},i}),u.define("select2/selection/multiple",["jquery","./base","../utils"],function(i,e,c){function r(e,t){r.__super__.constructor.apply(this,arguments)}return c.Extend(r,e),r.prototype.render=function(){var e=r.__super__.render.call(this);return e[0].classList.add("select2-selection--multiple"),e.html('
        '),e},r.prototype.bind=function(e,t){var n=this;r.__super__.bind.apply(this,arguments);var s=e.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",s),this.$selection.on("click",function(e){n.trigger("toggle",{originalEvent:e})}),this.$selection.on("click",".select2-selection__choice__remove",function(e){var t;n.isDisabled()||(t=i(this).parent(),t=c.GetData(t[0],"data"),n.trigger("unselect",{originalEvent:e,data:t}))}),this.$selection.on("keydown",".select2-selection__choice__remove",function(e){n.isDisabled()||e.stopPropagation()})},r.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},r.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},r.prototype.selectionContainer=function(){return i('
      • ')},r.prototype.update=function(e){if(this.clear(),0!==e.length){for(var t=[],n=this.$selection.find(".select2-selection__rendered").attr("id")+"-choice-",s=0;s')).attr("title",s()),e.attr("aria-label",s()),e.attr("aria-describedby",n),a.StoreData(e[0],"data",t),this.$selection.prepend(e),this.$selection[0].classList.add("select2-selection--clearable"))},e}),u.define("select2/selection/search",["jquery","../utils","../keys"],function(s,a,l){function e(e,t,n){e.call(this,t,n)}return e.prototype.render=function(e){var t=this.options.get("translations").get("search"),n=s('');this.$searchContainer=n,this.$search=n.find("textarea"),this.$search.prop("autocomplete",this.options.get("autocomplete")),this.$search.attr("aria-label",t());e=e.call(this);return this._transferTabIndex(),e.append(this.$searchContainer),e},e.prototype.bind=function(e,t,n){var s=this,i=t.id+"-results",r=t.id+"-container";e.call(this,t,n),s.$search.attr("aria-describedby",r),t.on("open",function(){s.$search.attr("aria-controls",i),s.$search.trigger("focus")}),t.on("close",function(){s.$search.val(""),s.resizeSearch(),s.$search.removeAttr("aria-controls"),s.$search.removeAttr("aria-activedescendant"),s.$search.trigger("focus")}),t.on("enable",function(){s.$search.prop("disabled",!1),s._transferTabIndex()}),t.on("disable",function(){s.$search.prop("disabled",!0)}),t.on("focus",function(e){s.$search.trigger("focus")}),t.on("results:focus",function(e){e.data._resultId?s.$search.attr("aria-activedescendant",e.data._resultId):s.$search.removeAttr("aria-activedescendant")}),this.$selection.on("focusin",".select2-search--inline",function(e){s.trigger("focus",e)}),this.$selection.on("focusout",".select2-search--inline",function(e){s._handleBlur(e)}),this.$selection.on("keydown",".select2-search--inline",function(e){var t;e.stopPropagation(),s.trigger("keypress",e),s._keyUpPrevented=e.isDefaultPrevented(),e.which!==l.BACKSPACE||""!==s.$search.val()||0<(t=s.$selection.find(".select2-selection__choice").last()).length&&(t=a.GetData(t[0],"data"),s.searchRemoveChoice(t),e.preventDefault())}),this.$selection.on("click",".select2-search--inline",function(e){s.$search.val()&&e.stopPropagation()});var t=document.documentMode,o=t&&t<=11;this.$selection.on("input.searchcheck",".select2-search--inline",function(e){o?s.$selection.off("input.search input.searchcheck"):s.$selection.off("keyup.search")}),this.$selection.on("keyup.search input.search",".select2-search--inline",function(e){var t;o&&"input"===e.type?s.$selection.off("input.search input.searchcheck"):(t=e.which)!=l.SHIFT&&t!=l.CTRL&&t!=l.ALT&&t!=l.TAB&&s.handleSearch(e)})},e.prototype._transferTabIndex=function(e){this.$search.attr("tabindex",this.$selection.attr("tabindex")),this.$selection.attr("tabindex","-1")},e.prototype.createPlaceholder=function(e,t){this.$search.attr("placeholder",t.text)},e.prototype.update=function(e,t){var n=this.$search[0]==document.activeElement;this.$search.attr("placeholder",""),e.call(this,t),this.resizeSearch(),n&&this.$search.trigger("focus")},e.prototype.handleSearch=function(){var e;this.resizeSearch(),this._keyUpPrevented||(e=this.$search.val(),this.trigger("query",{term:e})),this._keyUpPrevented=!1},e.prototype.searchRemoveChoice=function(e,t){this.trigger("unselect",{data:t}),this.$search.val(t.text),this.handleSearch()},e.prototype.resizeSearch=function(){this.$search.css("width","25px");var e="100%";""===this.$search.attr("placeholder")&&(e=.75*(this.$search.val().length+1)+"em"),this.$search.css("width",e)},e}),u.define("select2/selection/selectionCss",["../utils"],function(n){function e(){}return e.prototype.render=function(e){var t=e.call(this),e=this.options.get("selectionCssClass")||"";return-1!==e.indexOf(":all:")&&(e=e.replace(":all:",""),n.copyNonInternalCssClasses(t[0],this.$element[0])),t.addClass(e),t},e}),u.define("select2/selection/eventRelay",["jquery"],function(o){function e(){}return e.prototype.bind=function(e,t,n){var s=this,i=["open","opening","close","closing","select","selecting","unselect","unselecting","clear","clearing"],r=["opening","closing","selecting","unselecting","clearing"];e.call(this,t,n),t.on("*",function(e,t){var n;-1!==i.indexOf(e)&&(t=t||{},n=o.Event("select2:"+e,{params:t}),s.$element.trigger(n),-1!==r.indexOf(e)&&(t.prevented=n.isDefaultPrevented()))})},e}),u.define("select2/translation",["jquery","require"],function(t,n){function s(e){this.dict=e||{}}return s.prototype.all=function(){return this.dict},s.prototype.get=function(e){return this.dict[e]},s.prototype.extend=function(e){this.dict=t.extend({},e.all(),this.dict)},s._cache={},s.loadPath=function(e){var t;return e in s._cache||(t=n(e),s._cache[e]=t),new s(s._cache[e])},s}),u.define("select2/diacritics",[],function(){return{"Ⓐ":"A","A":"A","À":"A","Á":"A","Â":"A","Ầ":"A","Ấ":"A","Ẫ":"A","Ẩ":"A","Ã":"A","Ā":"A","Ă":"A","Ằ":"A","Ắ":"A","Ẵ":"A","Ẳ":"A","Ȧ":"A","Ǡ":"A","Ä":"A","Ǟ":"A","Ả":"A","Å":"A","Ǻ":"A","Ǎ":"A","Ȁ":"A","Ȃ":"A","Ạ":"A","Ậ":"A","Ặ":"A","Ḁ":"A","Ą":"A","Ⱥ":"A","Ɐ":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ⓑ":"B","B":"B","Ḃ":"B","Ḅ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ɓ":"B","Ⓒ":"C","C":"C","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","Ç":"C","Ḉ":"C","Ƈ":"C","Ȼ":"C","Ꜿ":"C","Ⓓ":"D","D":"D","Ḋ":"D","Ď":"D","Ḍ":"D","Ḑ":"D","Ḓ":"D","Ḏ":"D","Đ":"D","Ƌ":"D","Ɗ":"D","Ɖ":"D","Ꝺ":"D","DZ":"DZ","DŽ":"DZ","Dz":"Dz","Dž":"Dz","Ⓔ":"E","E":"E","È":"E","É":"E","Ê":"E","Ề":"E","Ế":"E","Ễ":"E","Ể":"E","Ẽ":"E","Ē":"E","Ḕ":"E","Ḗ":"E","Ĕ":"E","Ė":"E","Ë":"E","Ẻ":"E","Ě":"E","Ȅ":"E","Ȇ":"E","Ẹ":"E","Ệ":"E","Ȩ":"E","Ḝ":"E","Ę":"E","Ḙ":"E","Ḛ":"E","Ɛ":"E","Ǝ":"E","Ⓕ":"F","F":"F","Ḟ":"F","Ƒ":"F","Ꝼ":"F","Ⓖ":"G","G":"G","Ǵ":"G","Ĝ":"G","Ḡ":"G","Ğ":"G","Ġ":"G","Ǧ":"G","Ģ":"G","Ǥ":"G","Ɠ":"G","Ꞡ":"G","Ᵹ":"G","Ꝿ":"G","Ⓗ":"H","H":"H","Ĥ":"H","Ḣ":"H","Ḧ":"H","Ȟ":"H","Ḥ":"H","Ḩ":"H","Ḫ":"H","Ħ":"H","Ⱨ":"H","Ⱶ":"H","Ɥ":"H","Ⓘ":"I","I":"I","Ì":"I","Í":"I","Î":"I","Ĩ":"I","Ī":"I","Ĭ":"I","İ":"I","Ï":"I","Ḯ":"I","Ỉ":"I","Ǐ":"I","Ȉ":"I","Ȋ":"I","Ị":"I","Į":"I","Ḭ":"I","Ɨ":"I","Ⓙ":"J","J":"J","Ĵ":"J","Ɉ":"J","Ⓚ":"K","K":"K","Ḱ":"K","Ǩ":"K","Ḳ":"K","Ķ":"K","Ḵ":"K","Ƙ":"K","Ⱪ":"K","Ꝁ":"K","Ꝃ":"K","Ꝅ":"K","Ꞣ":"K","Ⓛ":"L","L":"L","Ŀ":"L","Ĺ":"L","Ľ":"L","Ḷ":"L","Ḹ":"L","Ļ":"L","Ḽ":"L","Ḻ":"L","Ł":"L","Ƚ":"L","Ɫ":"L","Ⱡ":"L","Ꝉ":"L","Ꝇ":"L","Ꞁ":"L","LJ":"LJ","Lj":"Lj","Ⓜ":"M","M":"M","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ɯ":"M","Ⓝ":"N","N":"N","Ǹ":"N","Ń":"N","Ñ":"N","Ṅ":"N","Ň":"N","Ṇ":"N","Ņ":"N","Ṋ":"N","Ṉ":"N","Ƞ":"N","Ɲ":"N","Ꞑ":"N","Ꞥ":"N","NJ":"NJ","Nj":"Nj","Ⓞ":"O","O":"O","Ò":"O","Ó":"O","Ô":"O","Ồ":"O","Ố":"O","Ỗ":"O","Ổ":"O","Õ":"O","Ṍ":"O","Ȭ":"O","Ṏ":"O","Ō":"O","Ṑ":"O","Ṓ":"O","Ŏ":"O","Ȯ":"O","Ȱ":"O","Ö":"O","Ȫ":"O","Ỏ":"O","Ő":"O","Ǒ":"O","Ȍ":"O","Ȏ":"O","Ơ":"O","Ờ":"O","Ớ":"O","Ỡ":"O","Ở":"O","Ợ":"O","Ọ":"O","Ộ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Ɔ":"O","Ɵ":"O","Ꝋ":"O","Ꝍ":"O","Œ":"OE","Ƣ":"OI","Ꝏ":"OO","Ȣ":"OU","Ⓟ":"P","P":"P","Ṕ":"P","Ṗ":"P","Ƥ":"P","Ᵽ":"P","Ꝑ":"P","Ꝓ":"P","Ꝕ":"P","Ⓠ":"Q","Q":"Q","Ꝗ":"Q","Ꝙ":"Q","Ɋ":"Q","Ⓡ":"R","R":"R","Ŕ":"R","Ṙ":"R","Ř":"R","Ȑ":"R","Ȓ":"R","Ṛ":"R","Ṝ":"R","Ŗ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꝛ":"R","Ꞧ":"R","Ꞃ":"R","Ⓢ":"S","S":"S","ẞ":"S","Ś":"S","Ṥ":"S","Ŝ":"S","Ṡ":"S","Š":"S","Ṧ":"S","Ṣ":"S","Ṩ":"S","Ș":"S","Ş":"S","Ȿ":"S","Ꞩ":"S","Ꞅ":"S","Ⓣ":"T","T":"T","Ṫ":"T","Ť":"T","Ṭ":"T","Ț":"T","Ţ":"T","Ṱ":"T","Ṯ":"T","Ŧ":"T","Ƭ":"T","Ʈ":"T","Ⱦ":"T","Ꞇ":"T","Ꜩ":"TZ","Ⓤ":"U","U":"U","Ù":"U","Ú":"U","Û":"U","Ũ":"U","Ṹ":"U","Ū":"U","Ṻ":"U","Ŭ":"U","Ü":"U","Ǜ":"U","Ǘ":"U","Ǖ":"U","Ǚ":"U","Ủ":"U","Ů":"U","Ű":"U","Ǔ":"U","Ȕ":"U","Ȗ":"U","Ư":"U","Ừ":"U","Ứ":"U","Ữ":"U","Ử":"U","Ự":"U","Ụ":"U","Ṳ":"U","Ų":"U","Ṷ":"U","Ṵ":"U","Ʉ":"U","Ⓥ":"V","V":"V","Ṽ":"V","Ṿ":"V","Ʋ":"V","Ꝟ":"V","Ʌ":"V","Ꝡ":"VY","Ⓦ":"W","W":"W","Ẁ":"W","Ẃ":"W","Ŵ":"W","Ẇ":"W","Ẅ":"W","Ẉ":"W","Ⱳ":"W","Ⓧ":"X","X":"X","Ẋ":"X","Ẍ":"X","Ⓨ":"Y","Y":"Y","Ỳ":"Y","Ý":"Y","Ŷ":"Y","Ỹ":"Y","Ȳ":"Y","Ẏ":"Y","Ÿ":"Y","Ỷ":"Y","Ỵ":"Y","Ƴ":"Y","Ɏ":"Y","Ỿ":"Y","Ⓩ":"Z","Z":"Z","Ź":"Z","Ẑ":"Z","Ż":"Z","Ž":"Z","Ẓ":"Z","Ẕ":"Z","Ƶ":"Z","Ȥ":"Z","Ɀ":"Z","Ⱬ":"Z","Ꝣ":"Z","ⓐ":"a","a":"a","ẚ":"a","à":"a","á":"a","â":"a","ầ":"a","ấ":"a","ẫ":"a","ẩ":"a","ã":"a","ā":"a","ă":"a","ằ":"a","ắ":"a","ẵ":"a","ẳ":"a","ȧ":"a","ǡ":"a","ä":"a","ǟ":"a","ả":"a","å":"a","ǻ":"a","ǎ":"a","ȁ":"a","ȃ":"a","ạ":"a","ậ":"a","ặ":"a","ḁ":"a","ą":"a","ⱥ":"a","ɐ":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ⓑ":"b","b":"b","ḃ":"b","ḅ":"b","ḇ":"b","ƀ":"b","ƃ":"b","ɓ":"b","ⓒ":"c","c":"c","ć":"c","ĉ":"c","ċ":"c","č":"c","ç":"c","ḉ":"c","ƈ":"c","ȼ":"c","ꜿ":"c","ↄ":"c","ⓓ":"d","d":"d","ḋ":"d","ď":"d","ḍ":"d","ḑ":"d","ḓ":"d","ḏ":"d","đ":"d","ƌ":"d","ɖ":"d","ɗ":"d","ꝺ":"d","dz":"dz","dž":"dz","ⓔ":"e","e":"e","è":"e","é":"e","ê":"e","ề":"e","ế":"e","ễ":"e","ể":"e","ẽ":"e","ē":"e","ḕ":"e","ḗ":"e","ĕ":"e","ė":"e","ë":"e","ẻ":"e","ě":"e","ȅ":"e","ȇ":"e","ẹ":"e","ệ":"e","ȩ":"e","ḝ":"e","ę":"e","ḙ":"e","ḛ":"e","ɇ":"e","ɛ":"e","ǝ":"e","ⓕ":"f","f":"f","ḟ":"f","ƒ":"f","ꝼ":"f","ⓖ":"g","g":"g","ǵ":"g","ĝ":"g","ḡ":"g","ğ":"g","ġ":"g","ǧ":"g","ģ":"g","ǥ":"g","ɠ":"g","ꞡ":"g","ᵹ":"g","ꝿ":"g","ⓗ":"h","h":"h","ĥ":"h","ḣ":"h","ḧ":"h","ȟ":"h","ḥ":"h","ḩ":"h","ḫ":"h","ẖ":"h","ħ":"h","ⱨ":"h","ⱶ":"h","ɥ":"h","ƕ":"hv","ⓘ":"i","i":"i","ì":"i","í":"i","î":"i","ĩ":"i","ī":"i","ĭ":"i","ï":"i","ḯ":"i","ỉ":"i","ǐ":"i","ȉ":"i","ȋ":"i","ị":"i","į":"i","ḭ":"i","ɨ":"i","ı":"i","ⓙ":"j","j":"j","ĵ":"j","ǰ":"j","ɉ":"j","ⓚ":"k","k":"k","ḱ":"k","ǩ":"k","ḳ":"k","ķ":"k","ḵ":"k","ƙ":"k","ⱪ":"k","ꝁ":"k","ꝃ":"k","ꝅ":"k","ꞣ":"k","ⓛ":"l","l":"l","ŀ":"l","ĺ":"l","ľ":"l","ḷ":"l","ḹ":"l","ļ":"l","ḽ":"l","ḻ":"l","ſ":"l","ł":"l","ƚ":"l","ɫ":"l","ⱡ":"l","ꝉ":"l","ꞁ":"l","ꝇ":"l","lj":"lj","ⓜ":"m","m":"m","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ɯ":"m","ⓝ":"n","n":"n","ǹ":"n","ń":"n","ñ":"n","ṅ":"n","ň":"n","ṇ":"n","ņ":"n","ṋ":"n","ṉ":"n","ƞ":"n","ɲ":"n","ʼn":"n","ꞑ":"n","ꞥ":"n","nj":"nj","ⓞ":"o","o":"o","ò":"o","ó":"o","ô":"o","ồ":"o","ố":"o","ỗ":"o","ổ":"o","õ":"o","ṍ":"o","ȭ":"o","ṏ":"o","ō":"o","ṑ":"o","ṓ":"o","ŏ":"o","ȯ":"o","ȱ":"o","ö":"o","ȫ":"o","ỏ":"o","ő":"o","ǒ":"o","ȍ":"o","ȏ":"o","ơ":"o","ờ":"o","ớ":"o","ỡ":"o","ở":"o","ợ":"o","ọ":"o","ộ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","ɔ":"o","ꝋ":"o","ꝍ":"o","ɵ":"o","œ":"oe","ƣ":"oi","ȣ":"ou","ꝏ":"oo","ⓟ":"p","p":"p","ṕ":"p","ṗ":"p","ƥ":"p","ᵽ":"p","ꝑ":"p","ꝓ":"p","ꝕ":"p","ⓠ":"q","q":"q","ɋ":"q","ꝗ":"q","ꝙ":"q","ⓡ":"r","r":"r","ŕ":"r","ṙ":"r","ř":"r","ȑ":"r","ȓ":"r","ṛ":"r","ṝ":"r","ŗ":"r","ṟ":"r","ɍ":"r","ɽ":"r","ꝛ":"r","ꞧ":"r","ꞃ":"r","ⓢ":"s","s":"s","ß":"s","ś":"s","ṥ":"s","ŝ":"s","ṡ":"s","š":"s","ṧ":"s","ṣ":"s","ṩ":"s","ș":"s","ş":"s","ȿ":"s","ꞩ":"s","ꞅ":"s","ẛ":"s","ⓣ":"t","t":"t","ṫ":"t","ẗ":"t","ť":"t","ṭ":"t","ț":"t","ţ":"t","ṱ":"t","ṯ":"t","ŧ":"t","ƭ":"t","ʈ":"t","ⱦ":"t","ꞇ":"t","ꜩ":"tz","ⓤ":"u","u":"u","ù":"u","ú":"u","û":"u","ũ":"u","ṹ":"u","ū":"u","ṻ":"u","ŭ":"u","ü":"u","ǜ":"u","ǘ":"u","ǖ":"u","ǚ":"u","ủ":"u","ů":"u","ű":"u","ǔ":"u","ȕ":"u","ȗ":"u","ư":"u","ừ":"u","ứ":"u","ữ":"u","ử":"u","ự":"u","ụ":"u","ṳ":"u","ų":"u","ṷ":"u","ṵ":"u","ʉ":"u","ⓥ":"v","v":"v","ṽ":"v","ṿ":"v","ʋ":"v","ꝟ":"v","ʌ":"v","ꝡ":"vy","ⓦ":"w","w":"w","ẁ":"w","ẃ":"w","ŵ":"w","ẇ":"w","ẅ":"w","ẘ":"w","ẉ":"w","ⱳ":"w","ⓧ":"x","x":"x","ẋ":"x","ẍ":"x","ⓨ":"y","y":"y","ỳ":"y","ý":"y","ŷ":"y","ỹ":"y","ȳ":"y","ẏ":"y","ÿ":"y","ỷ":"y","ẙ":"y","ỵ":"y","ƴ":"y","ɏ":"y","ỿ":"y","ⓩ":"z","z":"z","ź":"z","ẑ":"z","ż":"z","ž":"z","ẓ":"z","ẕ":"z","ƶ":"z","ȥ":"z","ɀ":"z","ⱬ":"z","ꝣ":"z","Ά":"Α","Έ":"Ε","Ή":"Η","Ί":"Ι","Ϊ":"Ι","Ό":"Ο","Ύ":"Υ","Ϋ":"Υ","Ώ":"Ω","ά":"α","έ":"ε","ή":"η","ί":"ι","ϊ":"ι","ΐ":"ι","ό":"ο","ύ":"υ","ϋ":"υ","ΰ":"υ","ώ":"ω","ς":"σ","’":"'"}}),u.define("select2/data/base",["../utils"],function(n){function s(e,t){s.__super__.constructor.call(this)}return n.Extend(s,n.Observable),s.prototype.current=function(e){throw new Error("The `current` method must be defined in child classes.")},s.prototype.query=function(e,t){throw new Error("The `query` method must be defined in child classes.")},s.prototype.bind=function(e,t){},s.prototype.destroy=function(){},s.prototype.generateResultId=function(e,t){e=e.id+"-result-";return e+=n.generateChars(4),null!=t.id?e+="-"+t.id.toString():e+="-"+n.generateChars(4),e},s}),u.define("select2/data/select",["./base","../utils","jquery"],function(e,a,l){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return a.Extend(n,e),n.prototype.current=function(e){var t=this;e(Array.prototype.map.call(this.$element[0].querySelectorAll(":checked"),function(e){return t.item(l(e))}))},n.prototype.select=function(i){var e,r=this;if(i.selected=!0,null!=i.element&&"option"===i.element.tagName.toLowerCase())return i.element.selected=!0,void this.$element.trigger("input").trigger("change");this.$element.prop("multiple")?this.current(function(e){var t=[];(i=[i]).push.apply(i,e);for(var n=0;nthis.maximumInputLength?this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:t.term,params:t}}):e.call(this,t,n)},e}),u.define("select2/data/maximumSelectionLength",[],function(){function e(e,t,n){this.maximumSelectionLength=n.get("maximumSelectionLength"),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var s=this;e.call(this,t,n),t.on("select",function(){s._checkIfMaximumSelected()})},e.prototype.query=function(e,t,n){var s=this;this._checkIfMaximumSelected(function(){e.call(s,t,n)})},e.prototype._checkIfMaximumSelected=function(e,t){var n=this;this.current(function(e){e=null!=e?e.length:0;0=n.maximumSelectionLength?n.trigger("results:message",{message:"maximumSelected",args:{maximum:n.maximumSelectionLength}}):t&&t()})},e}),u.define("select2/dropdown",["jquery","./utils"],function(t,e){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return e.Extend(n,e.Observable),n.prototype.render=function(){var e=t('');return e.attr("dir",this.options.get("dir")),this.$dropdown=e},n.prototype.bind=function(){},n.prototype.position=function(e,t){},n.prototype.destroy=function(){this.$dropdown.remove()},n}),u.define("select2/dropdown/search",["jquery"],function(r){function e(){}return e.prototype.render=function(e){var t=e.call(this),n=this.options.get("translations").get("search"),e=r('');return this.$searchContainer=e,this.$search=e.find("input"),this.$search.prop("autocomplete",this.options.get("autocomplete")),this.$search.attr("aria-label",n()),t.prepend(e),t},e.prototype.bind=function(e,t,n){var s=this,i=t.id+"-results";e.call(this,t,n),this.$search.on("keydown",function(e){s.trigger("keypress",e),s._keyUpPrevented=e.isDefaultPrevented()}),this.$search.on("input",function(e){r(this).off("keyup")}),this.$search.on("keyup input",function(e){s.handleSearch(e)}),t.on("open",function(){s.$search.attr("tabindex",0),s.$search.attr("aria-controls",i),s.$search.trigger("focus"),window.setTimeout(function(){s.$search.trigger("focus")},0)}),t.on("close",function(){s.$search.attr("tabindex",-1),s.$search.removeAttr("aria-controls"),s.$search.removeAttr("aria-activedescendant"),s.$search.val(""),s.$search.trigger("blur")}),t.on("focus",function(){t.isOpen()||s.$search.trigger("focus")}),t.on("results:all",function(e){null!=e.query.term&&""!==e.query.term||(s.showSearch(e)?s.$searchContainer[0].classList.remove("select2-search--hide"):s.$searchContainer[0].classList.add("select2-search--hide"))}),t.on("results:focus",function(e){e.data._resultId?s.$search.attr("aria-activedescendant",e.data._resultId):s.$search.removeAttr("aria-activedescendant")})},e.prototype.handleSearch=function(e){var t;this._keyUpPrevented||(t=this.$search.val(),this.trigger("query",{term:t})),this._keyUpPrevented=!1},e.prototype.showSearch=function(e,t){return!0},e}),u.define("select2/dropdown/hidePlaceholder",[],function(){function e(e,t,n,s){this.placeholder=this.normalizePlaceholder(n.get("placeholder")),e.call(this,t,n,s)}return e.prototype.append=function(e,t){t.results=this.removePlaceholder(t.results),e.call(this,t)},e.prototype.normalizePlaceholder=function(e,t){return"string"==typeof t&&(t={id:"",text:t}),t},e.prototype.removePlaceholder=function(e,t){for(var n=t.slice(0),s=t.length-1;0<=s;s--){var i=t[s];this.placeholder.id===i.id&&n.splice(s,1)}return n},e}),u.define("select2/dropdown/infiniteScroll",["jquery"],function(n){function e(e,t,n,s){this.lastParams={},e.call(this,t,n,s),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return e.prototype.append=function(e,t){this.$loadingMore.remove(),this.loading=!1,e.call(this,t),this.showLoadingMore(t)&&(this.$results.append(this.$loadingMore),this.loadMoreIfNeeded())},e.prototype.bind=function(e,t,n){var s=this;e.call(this,t,n),t.on("query",function(e){s.lastParams=e,s.loading=!0}),t.on("query:append",function(e){s.lastParams=e,s.loading=!0}),this.$results.on("scroll",this.loadMoreIfNeeded.bind(this))},e.prototype.loadMoreIfNeeded=function(){var e=n.contains(document.documentElement,this.$loadingMore[0]);!this.loading&&e&&(e=this.$results.offset().top+this.$results.outerHeight(!1),this.$loadingMore.offset().top+this.$loadingMore.outerHeight(!1)<=e+50&&this.loadMore())},e.prototype.loadMore=function(){this.loading=!0;var e=n.extend({},{page:1},this.lastParams);e.page++,this.trigger("query:append",e)},e.prototype.showLoadingMore=function(e,t){return t.pagination&&t.pagination.more},e.prototype.createLoadingMore=function(){var e=n('
      • '),t=this.options.get("translations").get("loadingMore");return e.html(t(this.lastParams)),e},e}),u.define("select2/dropdown/attachBody",["jquery","../utils"],function(u,o){function e(e,t,n){this.$dropdownParent=u(n.get("dropdownParent")||document.body),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var s=this;e.call(this,t,n),t.on("open",function(){s._showDropdown(),s._attachPositioningHandler(t),s._bindContainerResultHandlers(t)}),t.on("close",function(){s._hideDropdown(),s._detachPositioningHandler(t)}),this.$dropdownContainer.on("mousedown",function(e){e.stopPropagation()})},e.prototype.destroy=function(e){e.call(this),this.$dropdownContainer.remove()},e.prototype.position=function(e,t,n){t.attr("class",n.attr("class")),t[0].classList.remove("select2"),t[0].classList.add("select2-container--open"),t.css({position:"absolute",top:-999999}),this.$container=n},e.prototype.render=function(e){var t=u(""),e=e.call(this);return t.append(e),this.$dropdownContainer=t},e.prototype._hideDropdown=function(e){this.$dropdownContainer.detach()},e.prototype._bindContainerResultHandlers=function(e,t){var n;this._containerResultsHandlersBound||(n=this,t.on("results:all",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:append",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:message",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("select",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("unselect",function(){n._positionDropdown(),n._resizeDropdown()}),this._containerResultsHandlersBound=!0)},e.prototype._attachPositioningHandler=function(e,t){var n=this,s="scroll.select2."+t.id,i="resize.select2."+t.id,r="orientationchange.select2."+t.id,t=this.$container.parents().filter(o.hasScroll);t.each(function(){o.StoreData(this,"select2-scroll-position",{x:u(this).scrollLeft(),y:u(this).scrollTop()})}),t.on(s,function(e){var t=o.GetData(this,"select2-scroll-position");u(this).scrollTop(t.y)}),u(window).on(s+" "+i+" "+r,function(e){n._positionDropdown(),n._resizeDropdown()})},e.prototype._detachPositioningHandler=function(e,t){var n="scroll.select2."+t.id,s="resize.select2."+t.id,t="orientationchange.select2."+t.id;this.$container.parents().filter(o.hasScroll).off(n),u(window).off(n+" "+s+" "+t)},e.prototype._positionDropdown=function(){var e=u(window),t=this.$dropdown[0].classList.contains("select2-dropdown--above"),n=this.$dropdown[0].classList.contains("select2-dropdown--below"),s=null,i=this.$container.offset();i.bottom=i.top+this.$container.outerHeight(!1);var r={height:this.$container.outerHeight(!1)};r.top=i.top,r.bottom=i.top+r.height;var o=this.$dropdown.outerHeight(!1),a=e.scrollTop(),l=e.scrollTop()+e.height(),c=ai.bottom+o,a={left:i.left,top:r.bottom},l=this.$dropdownParent;"static"===l.css("position")&&(l=l.offsetParent());i={top:0,left:0};(u.contains(document.body,l[0])||l[0].isConnected)&&(i=l.offset()),a.top-=i.top,a.left-=i.left,t||n||(s="below"),e||!c||t?!c&&e&&t&&(s="below"):s="above",("above"==s||t&&"below"!==s)&&(a.top=r.top-i.top-o),null!=s&&(this.$dropdown[0].classList.remove("select2-dropdown--below"),this.$dropdown[0].classList.remove("select2-dropdown--above"),this.$dropdown[0].classList.add("select2-dropdown--"+s),this.$container[0].classList.remove("select2-container--below"),this.$container[0].classList.remove("select2-container--above"),this.$container[0].classList.add("select2-container--"+s)),this.$dropdownContainer.css(a)},e.prototype._resizeDropdown=function(){var e={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(e.minWidth=e.width,e.position="relative",e.width="auto"),this.$dropdown.css(e)},e.prototype._showDropdown=function(e){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},e}),u.define("select2/dropdown/minimumResultsForSearch",[],function(){function e(e,t,n,s){this.minimumResultsForSearch=n.get("minimumResultsForSearch"),this.minimumResultsForSearch<0&&(this.minimumResultsForSearch=1/0),e.call(this,t,n,s)}return e.prototype.showSearch=function(e,t){return!(function e(t){for(var n=0,s=0;s');return e.attr("dir",this.options.get("dir")),this.$container=e,this.$container[0].classList.add("select2-container--"+this.options.get("theme")),r.StoreData(e[0],"element",this.$element),e},o}),u.define("jquery-mousewheel",["jquery"],function(e){return e}),u.define("jquery.select2",["jquery","jquery-mousewheel","./select2/core","./select2/defaults","./select2/utils"],function(i,e,r,t,o){var a;return null==i.fn.select2&&(a=["open","close","destroy"],i.fn.select2=function(t){if("object"==typeof(t=t||{}))return this.each(function(){var e=i.extend(!0,{},t);new r(i(this),e)}),this;if("string"!=typeof t)throw new Error("Invalid arguments for Select2: "+t);var n,s=Array.prototype.slice.call(arguments,1);return this.each(function(){var e=o.GetData(this,"select2");null==e&&window.console&&console.error&&console.error("The select2('"+t+"') method was called on an element that is not using Select2."),n=e[t].apply(e,s)}),-1