/bundles/org.openhab.binding.meteoblue/ @9037568
/bundles/org.openhab.binding.meteostick/ @cdjackson
/bundles/org.openhab.binding.miele/ @kgoderis
+/bundles/org.openhab.binding.mielecloud/ @BjoernLange
/bundles/org.openhab.binding.mihome/ @pboos
/bundles/org.openhab.binding.miio/ @marcelrv
/bundles/org.openhab.binding.milight/ @davidgraeff
<artifactId>org.openhab.binding.miele</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.openhab.addons.bundles</groupId>
+ <artifactId>org.openhab.binding.mielecloud</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.mihome</artifactId>
--- /dev/null
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
--- /dev/null
+# Miele Cloud Binding
+
+This binding integrates [Miele@home](https://www.miele.de/brand/smarthome-42801.htm) appliances via a cloud connection.
+A Miele cloud account and a set of developer credentials is required to use the binding.
+The latter can be requested from the [Miele Developer Portal](https://www.miele.com/f/com/en/register_api.aspx).
+
+## Supported Things
+
+Most Miele appliances that directly connect to the cloud via a Wi-Fi module are supported.
+Appliances connecting to the XGW3000 gateway via ZigBee are also supported when registered with the cloud account.
+However they might be better supported by the [gateway-based Miele binding](https://www.openhab.org/addons/bindings/miele/).
+Depending on the age of your appliance the functionality of the binding might be limited.
+Appliances from recent generations will support all functionality.
+
+The following types of appliances are supported:
+
+| Appliance type | Thing type |
+| -------------------------------- | ------------------------ |
+| Coffee Machine | `coffee_system` |
+| Dishwasher | `dishwasher` |
+| Dish Warmer | `dish_warmer` |
+| Freezer | `freezer` |
+| Fridge | `fridge` |
+| Fridge-Freezer Combination | `fridge_freezer` |
+| Hob | `hob` |
+| Hood | `hood` |
+| Microwave Oven | `oven` |
+| Oven | `oven` |
+| Robotic Vacuum Cleaner | `robotic_vacuum_cleaner` |
+| Tumble Dryer | `dryer` |
+| Washer Dryer | `washer_dryer` |
+| Washing Machine | `washing_machine` |
+| Wine Cabinet | `wine_storage` |
+| Wine Cabinet Freezer Combination | `wine_storage` |
+
+## Discovery
+
+Please take the following steps prior to using the binding. Create a Miele cloud account in the Miele@mobile app for [Android](https://play.google.com/store/apps/details?id=de.miele.infocontrol&hl=en_US) or [iOS](https://apps.apple.com/de/app/miele-mobile/id930406907?l=en) (if not already done).
+Afterwards, pair your appliances.
+Once your appliances are set up, register at the [Miele Developer Portal](https://www.miele.com/f/com/en/register_api.aspx).
+You will receive a pair of client ID and client secret which will be used to pair your Miele cloud account to the Miele cloud openHAB binding.
+Keep these credentials to yourself and treat them like a password!
+It may take some time until the registration e-mail arrives.
+
+There is no auto discovery for the Miele cloud account.
+The account is paired using OAuth2 with your Miele login and the developer credentials obtained from the [Miele Developer Portal](https://www.miele.com/f/com/en/register_api.aspx).
+To pair the account go to the binding's configuration UI at `https://<your openHAB address>/mielecloud`.
+For a standard openHABian Pi installation the address is [https://openhabianpi:8443/mielecloud](https://openhabianpi:8443/mielecloud).
+Note that your browser will file a warning that the certificate is self-signed.
+This is fine and you can safely continue.
+It is also possible to use an unsecured connection for pairing but it is strongly recommended to use a secured connection because your credentials will otherwise be transferred without encryption over the local network.
+For more information on this topic, see [Securing access to openHAB](https://www.openhab.org/docs/installation/security.html#encrypted-communication).
+For a detailed walk through the account configuration, see [Account Configuration Example](#account-configuration-example).
+
+Once a Miele account is paired, all supported appliances are automatically discovered as individual things and placed in the inbox.
+They can then be paired with your favorite management UI.
+As an alternative, the binding configuration UI provides a things-file template per paired account that can be used to pair the appliances.
+
+## Thing Configuration
+
+A Miele cloud account needs to be configured to get access to your appliances.
+After that appliances can be configured.
+
+### Account Configuration
+
+The Miele cloud account must be paired via the binding configuration UI before a bridge that relies on it can be configured in openHAB.
+For details on the configuration UI see [Discovery](#discovery) and [Account Configuration Example](#account-configuration-example).
+The account serves as a bridge for the things representing your appliances.
+On success the configuration assistant will directly configure the account without requiring further actions.
+As an alternative, it provides a things-file template.
+
+The account has the following parameters:
+
+| Name | Type | Description |
+| ----------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| email | required | E-mail address identifying this account. This exists only to distinguish accounts. If the address is changed after authorization then the account needs to be authorized again. |
+| locale | optional | The locale to use for full text channels of things from this account. Possible values are `en`, `de`, `da`, `es`, `fr`, `it`, `nl`, `nb`. Default is `en`. |
+
+
+### Appliance Configuration
+
+The binding configuration UI will show a things-file template containing things for all supported appliances from the paired account.
+This can be used as a starting point for a custom things-file.
+
+All Miele cloud appliance things have the following parameters:
+
+| Name | Type | Description |
+| ---------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
+| deviceIdentifier | required | Technical device identifier uniquely identifying the Miele appliance. Use the discovery result or the things-file template to obtain it. |
+
+
+## Channels
+
+The following table lists all available channels.
+See the following chapters for detailed information about which appliance supports which channels.
+Depending on the exact appliance configuration not all channels might be supported, e.g. a hob with four plates will only fill the channels for plates 1-4.
+Channel ID and channel type ID match unless noted.
+
+| Channel Type ID | Item Type | Description | Read only |
+| ----------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------- |
+| remote_control_can_be_started | Switch | Indicates if this device can be started remotely. | Yes |
+| remote_control_can_be_stopped | Switch | Indicates if this device can be stopped remotely. | Yes |
+| remote_control_can_be_paused | Switch | Indicates if this device can be paused remotely. | Yes |
+| remote_control_can_be_switched_on | Switch | Indicates if the device can be switched on remotely. | Yes |
+| remote_control_can_be_switched_off | Switch | Indicates if the device can be switched off remotely. | Yes |
+| remote_control_can_set_program_active | Switch | Indicates if the active program of the device can be set remotely. | Yes |
+| spinning_speed | String | The spinning speed of the active program. | Yes |
+| spinning_speed_raw | Number | The raw spinning speed of the active program. | Yes |
+| program_active | String | The active program of the device. | Yes |
+| program_active_raw | Number | The raw active program of the device. | Yes |
+| dish_warmer_program_active | String | The active program of the device. | No |
+| vacuum_cleaner_program_active | String | The active program of the device. | No |
+| program_phase | String | The phase of the active program. | Yes |
+| program_phase_raw | Number | The raw phase of the active program. | Yes |
+| operation_state | String | The operation state of the device. | Yes |
+| operation_state_raw | Number | The raw operation state of the device. | Yes |
+| program_start | Switch | Starts the currently selected program. | No |
+| program_stop | Switch | Stops the currently selected program. | No |
+| program_start_stop | String | Starts or stops the currently selected program. | No |
+| program_start_stop_pause | String | Starts, stops or pauses the currently selected program. | No |
+| power_state_on_off | String | Switches the device On or Off. | No |
+| finish_state | Switch | Indicates whether the most recent program finished. | Yes |
+| delayed_start_time | Number | The delayed start time of the selected program. | Yes |
+| program_remaining_time | Number | The remaining time of the active program. | Yes |
+| program_elapsed_time | Number | The elapsed time of the active program. | Yes |
+| program_progress | Number | The progress of the active program. | Yes |
+| drying_target | String | The target drying step of the laundry. | Yes |
+| drying_target_raw | Number | The raw target drying step of the laundry. | Yes |
+| pre_heat_finished | Switch | Indicates whether the pre-heating finished. | Yes |
+| temperature_target | Number | The target temperature of the device. | Yes |
+| temperature_current | Number | The currently measured temperature of the device. | Yes |
+| ventilation_power | String | The current ventilation power of the hood. | Yes |
+| ventilation_power_raw | Number | The current raw ventilation power of the hood. | Yes |
+| error_state | Switch | Indication flag which signals an error state for the device. | Yes |
+| info_state | Switch | Indication flag which signals an information of the device. | Yes |
+| fridge_super_cool | Switch | Start the super cooling mode of the fridge. | No |
+| freezer_super_freeze | Switch | Start the super freezing mode of the freezer. | No |
+| super_cool_can_be_controlled | Switch | Indicates if super cooling can be toggled. | Yes |
+| super_freeze_can_be_controlled | Switch | Indicates if super freezing can be toggled | Yes |
+| fridge_temperature_target | Number | The target temperature of the fridge. | Yes |
+| fridge_temperature_current | Number | The currently measured temperature of the fridge. | Yes |
+| freezer_temperature_target | Number | The target temperature of the freezer. | Yes |
+| freezer_temperature_current | Number | The currently measured temperature of the freezer. | Yes |
+| top_temperature_target | Number | The target temperature of the top area. | Yes |
+| top_temperature_current | Number | The currently measured temperature of the top area. | Yes |
+| middle_temperature_target | Number | The target temperature of the middle area. | Yes |
+| middle_temperature_current | Number | The currently measured temperature of the middle area. | Yes |
+| bottom_temperature_target | Number | The target temperature of the bottom area. | Yes |
+| bottom_temperature_current | Number | The currently measured temperature of the bottom area. | Yes |
+| light_switch | Switch | Indicates if the light of the device is enabled. | No |
+| light_can_be_controlled | Switch | Indicates if the light of the device can be controlled. | Yes |
+| plate_power_step | String | The power level of the heating plate. | Yes |
+| plate_power_step_raw | Number | The raw power level of the heating plate. | Yes |
+| door_state | Switch | Indicates if the door of the device is open. | Yes |
+| door_alarm | Switch | Indicates if the door alarm of the device is active. | Yes |
+| battery_level | Number | The battery level of the robotic vacuum cleaner. | Yes |
+
+### Coffee System
+
+- remote_control_can_be_started
+- remote_control_can_be_stopped
+- remote_control_can_be_switched_on
+- remote_control_can_be_switched_off
+- program_active
+- program_active_raw
+- program_phase
+- program_phase_raw
+- operation_state
+- operation_state_raw
+- finish_state
+- power_state_on_off
+- program_remaining_time
+- program_elapsed_time
+- error_state
+- info_state
+- light_switch
+- light_can_be_controlled
+
+### Dish Warmer
+
+- remote_control_can_be_switched_on
+- remote_control_can_be_switched_off
+- dish_warmer_program_active
+- program_active_raw
+- operation_state
+- operation_state_raw
+- power_state_on_off
+- finish_state
+- program_remaining_time
+- program_elapsed_time
+- program_progress
+- temperature_target
+- temperature_current
+- error_state
+- info_state
+- door_state
+
+### Dishwasher
+
+- remote_control_can_be_started
+- remote_control_can_be_stopped
+- remote_control_can_be_switched_on
+- remote_control_can_be_switched_off
+- program_active
+- program_active_raw
+- program_phase
+- program_phase_raw
+- operation_state
+- operation_state_raw
+- program_start_stop
+- finish_state
+- power_state_on_off
+- delayed_start_time
+- program_remaining_time
+- program_elapsed_time
+- program_progress
+- error_state
+- info_state
+- door_state
+
+### Tumble Dryer
+
+- remote_control_can_be_started
+- remote_control_can_be_stopped
+- remote_control_can_be_switched_on
+- remote_control_can_be_switched_off
+- program_active
+- program_active_raw
+- program_phase
+- program_phase_raw
+- operation_state
+- operation_state_raw
+- program_start_stop
+- finish_state
+- power_state_on_off
+- delayed_start_time
+- program_remaining_time
+- program_elapsed_time
+- program_progress
+- drying_target
+- drying_target_raw
+- error_state
+- info_state
+- light_switch
+- light_can_be_controlled
+- door_state
+
+### Freezer
+
+- operation_state
+- operation_state_raw
+- error_state
+- info_state
+- freezer_super_freeze
+- super_freeze_can_be_controlled
+- freezer_temperature_target
+- freezer_temperature_current
+- door_state
+- door_alarm
+
+### Fridge
+
+- operation_state
+- operation_state_raw
+- error_state
+- info_state
+- fridge_super_cool
+- super_cool_can_be_controlled
+- fridge_temperature_target
+- fridge_temperature_current
+- door_state
+- door_alarm
+
+### Fridge Freezer
+
+- operation_state
+- operation_state_raw
+- error_state
+- info_state
+- fridge_super_cool
+- freezer_super_freeze
+- super_cool_can_be_controlled
+- super_freeze_can_be_controlled
+- fridge_temperature_target
+- fridge_temperature_current
+- freezer_temperature_target
+- freezer_temperature_current
+- door_state
+- door_alarm
+
+### Hob
+
+- operation_state
+- operation_state_raw
+- error_state
+- info_state
+- plate_1_power_step to plate_6_power_step with channel type ID plate_power_step
+- plate_1_power_step_raw to plate_6_power_step_raw with channel type ID plate_power_step_raw
+
+### Hood
+
+- remote_control_can_be_started
+- remote_control_can_be_stopped
+- remote_control_can_be_switched_on
+- remote_control_can_be_switched_off
+- program_phase
+- program_phase_raw
+- operation_state
+- operation_state_raw
+- power_state_on_off
+- ventilation_power
+- ventilation_power_raw
+- error_state
+- info_state
+- light_switch
+- light_can_be_controlled
+
+### Oven
+
+- remote_control_can_be_started
+- remote_control_can_be_stopped
+- remote_control_can_be_switched_on
+- remote_control_can_be_switched_off
+- program_active
+- program_active_raw
+- program_phase
+- program_phase_raw
+- operation_state
+- operation_state_raw
+- program_start_stop
+- finish_state
+- power_state_on_off
+- delayed_start_time
+- program_remaining_time
+- program_elapsed_time
+- program_progress
+- pre_heat_finished
+- temperature_target
+- temperature_current
+- error_state
+- info_state
+- light_switch
+- light_can_be_controlled
+- door_state
+
+### Robotic Vacuum Cleaner
+
+- remote_control_can_be_started
+- remote_control_can_be_stopped
+- remote_control_can_be_paused
+- remote_control_can_set_program_active
+- vacuum_cleaner_program_active
+- program_active_raw
+- operation_state
+- operation_state_raw
+- finish_state
+- program_start_stop_pause
+- power_state_on_off
+- error_state
+- info_state
+- battery_level
+
+### Washer Dryer
+
+- remote_control_can_be_started
+- remote_control_can_be_stopped
+- remote_control_can_be_switched_on
+- remote_control_can_be_switched_off
+- spinning_speed
+- spinning_speed_raw
+- program_active
+- program_active_raw
+- program_phase
+- program_phase_raw
+- operation_state
+- operation_state_raw
+- program_start_stop
+- finish_state
+- power_state_on_off
+- delayed_start_time
+- program_remaining_time
+- program_elapsed_time
+- program_progress
+- drying_target
+- drying_target_raw
+- error_state
+- info_state
+- temperature_target
+- light_switch
+- light_can_be_controlled
+- door_state
+
+### Washing Machine
+
+- remote_control_can_be_started
+- remote_control_can_be_stopped
+- remote_control_can_be_switched_on
+- remote_control_can_be_switched_off
+- spinning_speed
+- spinning_speed_raw
+- program_active
+- program_active_raw
+- program_phase
+- program_phase_raw
+- operation_state
+- operation_state_raw
+- program_start_stop
+- finish_state
+- power_state_on_off
+- delayed_start_time
+- program_remaining_time
+- program_elapsed_time
+- program_progress
+- error_state
+- info_state
+- temperature_target
+- light_switch
+- light_can_be_controlled
+- door_state
+
+### Wine Storage
+
+- remote_control_can_be_started
+- remote_control_can_be_stopped
+- remote_control_can_be_switched_on
+- remote_control_can_be_switched_off
+- operation_state
+- operation_state_raw
+- power_state_on_off
+- error_state
+- info_state
+- temperature_target
+- temperature_current
+- top_temperature_target
+- top_temperature_current
+- middle_temperature_target
+- middle_temperature_current
+- bottom_temperature_target
+- bottom_temperature_current
+
+### Note on plate_power_step channels
+
+Hob things have an additional property `plateCount` that indicates the number of plates present on the appliance.
+Only the channels `plate_1_power_step` to `plate_x_power_step` will be populated by the binding where `x` is the value of the `plateCount` property.
+
+The plate numbers do not represent the physical layout of the plates on the appliance, but always start with the `plate_1_power_step` channel.
+This means that a hob with two plates will have `plate_1_power_step` and `plate_2_power_step` populated and all other `plate_x_power_step` channels empty.
+
+The `plate_x_power_step` channels show the current power step of the according plate.
+**Please note that some hobs may use dynamic numbering for plates.**
+Hobs that use dynamic numbering will use the first power step channel that is currently at a power step of zero when the plate is turned on.
+Additionally, when a plate is turned off all other plates with higher numbers will decrease their number by one.
+For example if plate 1, 2 and 3 are active and plate 1 is turned off then plate 2 will become plate 1, plate 3 will become plate 2 and plate 3 will have a power step of zero.
+This behavior is a fixed part of the affected appliances and cannot be changed.
+
+### Note on door_state channel
+
+The `door_state` channel might not always provide a value matching the actual state.
+For example, a washing machine will not provide a valid `door_state` when the appliance is turned off.
+A valid door state can be expected when the appliance is in one of the following raw operation states, compare the `operation_state_raw` channel:
+
+- `3`: Program selected
+- `4`: Program selected, waiting to start
+- `5`: Running
+- `6`: Paused
+
+## Properties
+
+The following chapters list the properties offered by appliances.
+
+### Common Properties
+
+| Property Name | Description |
+| ------------- | ----------------------------------------------------------------------------- |
+| serialNumber | Serial number of the appliance, only present for physical appliances |
+| modelId | Model ID of the appliance |
+| vendor | Always "Miele" |
+
+### Account
+
+| Property Name | Description |
+| ------------- | ----------------------------------------------------------------------------- |
+| connection | Type of connection used by the account, always "INTERNET" |
+| accessToken | The currently used OAuth 2 access token for accessing the Miele 3rd Party API |
+
+### Hob
+
+| Property Name | Description |
+| ------------- | ----------------------------------------------------------------------------- |
+| plateCount | Number of plates offered by the appliance |
+
+## Full Example
+
+### demo.things:
+
+```
+Bridge mielecloud:account:home [ email="me@openhab.org", locale="en" ] {
+ Thing coffee_system 000703261234 "Coffee machine CVA7440" [ deviceIdentifier="000703261234" ]
+ Thing hob 000160102345 "Cooktop KM7677" [ deviceIdentifier="000160102345" ]
+}
+```
+
+### demo.items:
+
+```
+// Coffee system
+Switch coffee_system_remote_control_can_be_started { channel="mielecloud:coffee_system:home:000703261234:remote_control_can_be_started" }
+Switch coffee_system_remote_control_can_be_stopped { channel="mielecloud:coffee_system:home:000703261234:remote_control_can_be_stopped" }
+Switch coffee_system_remote_control_can_be_switched_on { channel="mielecloud:coffee_system:home:000703261234:remote_control_can_be_switched_on" }
+Switch coffee_system_remote_control_can_be_switched_off { channel="mielecloud:coffee_system:home:000703261234:remote_control_can_be_switched_off" }
+String coffee_system_program_active { channel="mielecloud:coffee_system:home:000703261234:program_active" }
+String coffee_system_program_phase { channel="mielecloud:coffee_system:home:000703261234:program_phase" }
+String coffee_system_power_state_on_off { channel="mielecloud:coffee_system:home:000703261234:power_state_on_off" }
+String coffee_system_operation_state { channel="mielecloud:coffee_system:home:000703261234:operation_state" }
+Switch coffee_system_finish_state { channel="mielecloud:coffee_system:home:000703261234:finish_state" }
+Number coffee_system_program_remaining_time { channel="mielecloud:coffee_system:home:000703261234:program_remaining_time" }
+Switch coffee_system_error_state { channel="mielecloud:coffee_system:home:000703261234:error_state" }
+Switch coffee_system_info_state { channel="mielecloud:coffee_system:home:000703261234:info_state" }
+Switch coffee_system_light_switch { channel="mielecloud:coffee_system:home:000703261234:light_switch" }
+Switch coffee_system_light_can_be_controlled { channel="mielecloud:coffee_system:home:000703261234:light_can_be_controlled" }
+
+// Hob
+Switch hob_remote_control_can_be_started { channel="mielecloud:hob:home:000160102345:remote_control_can_be_started" }
+Switch hob_remote_control_can_be_stopped { channel="mielecloud:hob:home:000160102345:remote_control_can_be_stopped" }
+String hob_operation_state { channel="mielecloud:hob:home:000160102345:operation_state" }
+Switch hob_error_state { channel="mielecloud:hob:home:000160102345:error_state" }
+Switch hob_info_state { channel="mielecloud:hob:home:000160102345:info_state" }
+Switch hob_plate_1_is_present { channel="mielecloud:hob:home:000160102345:plate_1_is_present" }
+String hob_plate_1_power_step { channel="mielecloud:hob:home:000160102345:plate_1_power_step" }
+Switch hob_plate_2_is_present { channel="mielecloud:hob:home:000160102345:plate_2_is_present" }
+String hob_plate_2_power_step { channel="mielecloud:hob:home:000160102345:plate_2_power_step" }
+Switch hob_plate_3_is_present { channel="mielecloud:hob:home:000160102345:plate_3_is_present" }
+String hob_plate_3_power_step { channel="mielecloud:hob:home:000160102345:plate_3_power_step" }
+Switch hob_plate_4_is_present { channel="mielecloud:hob:home:000160102345:plate_4_is_present" }
+String hob_plate_4_power_step { channel="mielecloud:hob:home:000160102345:plate_4_power_step" }
+Switch hob_plate_5_is_present { channel="mielecloud:hob:home:000160102345:plate_5_is_present" }
+String hob_plate_5_power_step { channel="mielecloud:hob:home:000160102345:plate_5_power_step" }
+Switch hob_plate_6_is_present { channel="mielecloud:hob:home:000160102345:plate_6_is_present" }
+String hob_plate_6_power_step { channel="mielecloud:hob:home:000160102345:plate_6_power_step" }
+```
+
+### demo.sitemap:
+
+```
+sitemap demo label="Kitchen"
+{
+ Frame {
+ // Coffee system
+ Text item=coffee_system_program_active
+ Text item=coffee_system_program_phase
+ Text item=coffee_system_power_state_on_off
+ Text item=coffee_system_operation_state
+ Switch item=coffee_system_finish_state
+ Default item=coffee_system_program_remaining_time
+ Switch item=coffee_system_error_state
+ Switch item=coffee_system_info_state
+ Switch item=coffee_system_light_switch
+
+ // Hob
+ Text item=hob_operation_state
+ Switch item=hob_error_state
+ Switch item=hob_info_state
+ Text item=hob_plate_1_power_step
+ Text item=hob_plate_2_power_step
+ Text item=hob_plate_3_power_step
+ Text item=hob_plate_4_power_step
+ Text item=hob_plate_5_power_step
+ Text item=hob_plate_6_power_step
+ }
+}
+```
+
+## Account Configuration Example
+
+The configuration UI is accessible at `https://<your openHAB address>/mielecloud`.
+See [Discovery](#discovery) for a detailed description of how to open the configuration UI in a browser.
+
+When first opening the configuration UI no account will be paired.
+
+
+
+We strongly recommend to use a secure connection for pairing, details on this topic can also be found in the [Discovery](#discovery) section.
+Click `Pair Account` to start the pairing process.
+If not already done, go to the [Miele Developer Portal](https://www.miele.com/f/com/en/register_api.aspx), register there and wait for the confirmation e-mail.
+Obtain your client ID and client secret according to the instructions presented there.
+Once you obtained your client ID and client secret continue pairing by filling in your client ID, client secret, bridge ID and an e-mail address that you wish to use for identifying the account.
+You may choose any bridge ID you like as long as you only use letters, numbers, underscores and dashes.
+The e-mail address does not need to match the e-mail address used for your Miele Cloud Account.
+If you need to change the e-mail address later then you will need to authorize the account again.
+
+
+
+A click on `Pair Account` will take you to the Miele cloud service login form where you need to log in with the same account as you used for the Miele@mobile app.
+
+
+
+When this is the first time you pair an account, you will need to allow openHAB to access your account.
+
+When everything worked, you are presented with a page stating that pairing was successful.
+Select the locale which should be used to display localized texts in openHAB channels.
+From here, you have two options:
+Either let the binding automatically configure a bridge instance or copy the presented things-file template to a things-file and return to the overview page.
+
+
+
+Once the bridge instance is `ONLINE`, you can either pair things for all appliances via your favorite management UI or use a things-file.
+The account overview provides a things-file template that is shown when you expand the account.
+This can serve as a starting point for your own things-file.
+
+
+
+## Rule Ideas
+
+Here are some ideas on what could be done with this binding. You have more ideas or even an example? Great! Feel free to contribute!
+
+- Notify yourself of a finished dishwasher, tumble dryer, washer dryer or washing machine, e.g. by changing the lighting
+- Control the supercooler / superfreezer of your freezer, fridge or fridge-freezer combination with a voice assistant
+- Notify yourself when the oven has finished pre-heating
+
+## Acknowledgements
+
+The development of this binding was initiated and sponsored by Miele & Cie. KG.
+
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.openhab.addons.bundles</groupId>
+ <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+ <version>3.1.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>org.openhab.binding.mielecloud</artifactId>
+
+ <name>openHAB Add-ons :: Bundles :: Miele Cloud Binding</name>
+
+</project>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Copyright (c) 2010-2020 Contributors to the openHAB project
+
+ See the NOTICE file(s) distributed with this work for additional
+ information.
+
+ This program and the accompanying materials are made available under the
+ terms of the Eclipse Public License 2.0 which is available at
+ http://www.eclipse.org/legal/epl-2.0
+
+ SPDX-License-Identifier: EPL-2.0
+
+-->
+<features name="org.openhab.binding.mielecloud-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+ <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+ <feature name="openhab-binding-mielecloud" description="Miele Cloud Binding" version="${project.version}">
+ <feature>openhab-runtime-base</feature>
+ <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.mielecloud/${project.version}</bundle>
+ </feature>
+</features>
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link MieleCloudBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Added locale config parameter, added i18n key collection
+ * @author Benjamin Bolte - Add pre-heat finished and plate step channels, door state and door alarm channels, info
+ * state channel and map signal flags from API
+ * @author Björn Lange - Add elapsed time channel, dish warmer thing
+ */
+@NonNullByDefault
+public final class MieleCloudBindingConstants {
+
+ private MieleCloudBindingConstants() {
+ }
+
+ /**
+ * ID of the binding.
+ */
+ public static final String BINDING_ID = "mielecloud";
+
+ /**
+ * Thing type ID of Miele cloud bridges / accounts.
+ */
+ public static final String BRIDGE_TYPE_ID = "account";
+
+ /**
+ * The {@link ThingTypeUID} of Miele cloud bridges / accounts.
+ */
+ public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, BRIDGE_TYPE_ID);
+
+ /**
+ * The {@link ThingTypeUID} of Miele washing machines.
+ */
+ public static final ThingTypeUID THING_TYPE_WASHING_MACHINE = new ThingTypeUID(BINDING_ID, "washing_machine");
+
+ /**
+ * The {@link ThingTypeUID} of Miele washer-dryers.
+ */
+ public static final ThingTypeUID THING_TYPE_WASHER_DRYER = new ThingTypeUID(BINDING_ID, "washer_dryer");
+
+ /**
+ * The {@link ThingTypeUID} of Miele coffee machines.
+ */
+ public static final ThingTypeUID THING_TYPE_COFFEE_SYSTEM = new ThingTypeUID(BINDING_ID, "coffee_system");
+
+ /**
+ * The {@link ThingTypeUID} of Miele fridge-freezers.
+ */
+ public static final ThingTypeUID THING_TYPE_FRIDGE_FREEZER = new ThingTypeUID(BINDING_ID, "fridge_freezer");
+
+ /**
+ * The {@link ThingTypeUID} of Miele fridges.
+ */
+ public static final ThingTypeUID THING_TYPE_FRIDGE = new ThingTypeUID(BINDING_ID, "fridge");
+
+ /**
+ * The {@link ThingTypeUID} of Miele freezers.
+ */
+ public static final ThingTypeUID THING_TYPE_FREEZER = new ThingTypeUID(BINDING_ID, "freezer");
+
+ /**
+ * The {@link ThingTypeUID} of Miele ovens.
+ */
+ public static final ThingTypeUID THING_TYPE_OVEN = new ThingTypeUID(BINDING_ID, "oven");
+
+ /**
+ * The {@link ThingTypeUID} of Miele hobs.
+ */
+ public static final ThingTypeUID THING_TYPE_HOB = new ThingTypeUID(BINDING_ID, "hob");
+
+ /**
+ * The {@link ThingTypeUID} of Miele wine storages.
+ */
+ public static final ThingTypeUID THING_TYPE_WINE_STORAGE = new ThingTypeUID(BINDING_ID, "wine_storage");
+
+ /**
+ * The {@link ThingTypeUID} of Miele dishwashers.
+ */
+ public static final ThingTypeUID THING_TYPE_DISHWASHER = new ThingTypeUID(BINDING_ID, "dishwasher");
+
+ /**
+ * The {@link ThingTypeUID} of Miele dryers.
+ */
+ public static final ThingTypeUID THING_TYPE_DRYER = new ThingTypeUID(BINDING_ID, "dryer");
+
+ /**
+ * The {@link ThingTypeUID} of Miele hoods.
+ */
+ public static final ThingTypeUID THING_TYPE_HOOD = new ThingTypeUID(BINDING_ID, "hood");
+
+ /**
+ * The {@link ThingTypeUID} of Miele dish warmers.
+ */
+ public static final ThingTypeUID THING_TYPE_DISH_WARMER = new ThingTypeUID(BINDING_ID, "dish_warmer");
+
+ /**
+ * The {@link ThingTypeUID} of Miele robotic vacuum cleaners.
+ */
+ public static final ThingTypeUID THING_TYPE_ROBOTIC_VACUUM_CLEANER = new ThingTypeUID(BINDING_ID,
+ "robotic_vacuum_cleaner");
+
+ /**
+ * Name of the property storing the OAuth2 access token.
+ */
+ public static final String PROPERTY_ACCESS_TOKEN = "accessToken";
+
+ /**
+ * Name of the configuration parameter for the e-mail address.
+ */
+ public static final String CONFIG_PARAM_EMAIL = "email";
+
+ /**
+ * Name of the configuration parameter for the device identifier uniquely identifying a Miele device.
+ */
+ public static final String CONFIG_PARAM_DEVICE_IDENTIFIER = "deviceIdentifier";
+
+ /**
+ * Name of the configuration parameter for the locale. The locale is stored as a 2-letter language code.
+ */
+ public static final String CONFIG_PARAM_LOCALE = "locale";
+
+ /**
+ * Name of the property storing the number of plates for hobs.
+ */
+ public static final String PROPERTY_PLATE_COUNT = "plateCount";
+
+ /**
+ * Constants for all channels.
+ */
+ public static final class Channels {
+ private Channels() {
+ }
+
+ public static final String REMOTE_CONTROL_CAN_BE_STARTED = "remote_control_can_be_started";
+ public static final String REMOTE_CONTROL_CAN_BE_STOPPED = "remote_control_can_be_stopped";
+ public static final String REMOTE_CONTROL_CAN_BE_PAUSED = "remote_control_can_be_paused";
+ public static final String REMOTE_CONTROL_CAN_BE_SWITCHED_ON = "remote_control_can_be_switched_on";
+ public static final String REMOTE_CONTROL_CAN_BE_SWITCHED_OFF = "remote_control_can_be_switched_off";
+ public static final String REMOTE_CONTROL_CAN_SET_PROGRAM_ACTIVE = "remote_control_can_set_program_active";
+ public static final String SPINNING_SPEED = "spinning_speed";
+ public static final String SPINNING_SPEED_RAW = "spinning_speed_raw";
+ public static final String PROGRAM_ACTIVE = "program_active";
+ public static final String PROGRAM_ACTIVE_RAW = "program_active_raw";
+ public static final String DISH_WARMER_PROGRAM_ACTIVE = "dish_warmer_program_active";
+ public static final String VACUUM_CLEANER_PROGRAM_ACTIVE = "vacuum_cleaner_program_active";
+ public static final String PROGRAM_PHASE = "program_phase";
+ public static final String PROGRAM_PHASE_RAW = "program_phase_raw";
+ public static final String OPERATION_STATE = "operation_state";
+ public static final String OPERATION_STATE_RAW = "operation_state_raw";
+ public static final String PROGRAM_START_STOP = "program_start_stop";
+ public static final String PROGRAM_START_STOP_PAUSE = "program_start_stop_pause";
+ public static final String POWER_ON_OFF = "power_state_on_off";
+ public static final String FINISH_STATE = "finish_state";
+ public static final String DELAYED_START_TIME = "delayed_start_time";
+ public static final String PROGRAM_REMAINING_TIME = "program_remaining_time";
+ public static final String PROGRAM_ELAPSED_TIME = "program_elapsed_time";
+ public static final String PROGRAM_PROGRESS = "program_progress";
+ public static final String DRYING_TARGET = "drying_target";
+ public static final String DRYING_TARGET_RAW = "drying_target_raw";
+ public static final String PRE_HEAT_FINISHED = "pre_heat_finished";
+ public static final String TEMPERATURE_TARGET = "temperature_target";
+ public static final String TEMPERATURE_CURRENT = "temperature_current";
+ public static final String TEMPERATURE_CORE_TARGET = "temperature_core_target";
+ public static final String TEMPERATURE_CORE_CURRENT = "temperature_core_current";
+ public static final String VENTILATION_POWER = "ventilation_power";
+ public static final String VENTILATION_POWER_RAW = "ventilation_power_raw";
+ public static final String ERROR_STATE = "error_state";
+ public static final String INFO_STATE = "info_state";
+ public static final String FRIDGE_SUPER_COOL = "fridge_super_cool";
+ public static final String FREEZER_SUPER_FREEZE = "freezer_super_freeze";
+ public static final String SUPER_COOL_CAN_BE_CONTROLLED = "super_cool_can_be_controlled";
+ public static final String SUPER_FREEZE_CAN_BE_CONTROLLED = "super_freeze_can_be_controlled";
+ public static final String FRIDGE_TEMPERATURE_TARGET = "fridge_temperature_target";
+ public static final String FRIDGE_TEMPERATURE_CURRENT = "fridge_temperature_current";
+ public static final String FREEZER_TEMPERATURE_TARGET = "freezer_temperature_target";
+ public static final String FREEZER_TEMPERATURE_CURRENT = "freezer_temperature_current";
+ public static final String TOP_TEMPERATURE_TARGET = "top_temperature_target";
+ public static final String TOP_TEMPERATURE_CURRENT = "top_temperature_current";
+ public static final String MIDDLE_TEMPERATURE_TARGET = "middle_temperature_target";
+ public static final String MIDDLE_TEMPERATURE_CURRENT = "middle_temperature_current";
+ public static final String BOTTOM_TEMPERATURE_TARGET = "bottom_temperature_target";
+ public static final String BOTTOM_TEMPERATURE_CURRENT = "bottom_temperature_current";
+ public static final String LIGHT_SWITCH = "light_switch";
+ public static final String LIGHT_CAN_BE_CONTROLLED = "light_can_be_controlled";
+ public static final String PLATE_1_POWER_STEP = "plate_1_power_step";
+ public static final String PLATE_1_POWER_STEP_RAW = "plate_1_power_step_raw";
+ public static final String PLATE_2_POWER_STEP = "plate_2_power_step";
+ public static final String PLATE_2_POWER_STEP_RAW = "plate_2_power_step_raw";
+ public static final String PLATE_3_POWER_STEP = "plate_3_power_step";
+ public static final String PLATE_3_POWER_STEP_RAW = "plate_3_power_step_raw";
+ public static final String PLATE_4_POWER_STEP = "plate_4_power_step";
+ public static final String PLATE_4_POWER_STEP_RAW = "plate_4_power_step_raw";
+ public static final String PLATE_5_POWER_STEP = "plate_5_power_step";
+ public static final String PLATE_5_POWER_STEP_RAW = "plate_5_power_step_raw";
+ public static final String PLATE_6_POWER_STEP = "plate_6_power_step";
+ public static final String PLATE_6_POWER_STEP_RAW = "plate_6_power_step_raw";
+ public static final String DOOR_STATE = "door_state";
+ public static final String DOOR_ALARM = "door_alarm";
+ public static final String BATTERY_LEVEL = "battery_level";
+ }
+
+ /**
+ * Constants for i18n keys.
+ */
+ public static final class I18NKeys {
+ private I18NKeys() {
+ }
+
+ public static final String BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_NOT_CONFIGURED = "@text/mielecloud.bridge.status.access.token.not.configured";
+ public static final String BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED = "@text/mielecloud.bridge.status.account.not.authorized";
+ public static final String BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_REFRESH_FAILED = "@text/mielecloud.bridge.status.access.token.refresh.failed";
+ public static final String BRIDGE_STATUS_DESCRIPTION_INVALID_EMAIL = "@text/mielecloud.bridge.status.invalid.email";
+ public static final String BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR = "@text/mielecloud.bridge.status.transient.http.error";
+
+ public static final String THING_STATUS_DESCRIPTION_WEBSERVICE_MISSING = "@text/mielecloud.thing.status.webservice.missing";
+ public static final String THING_STATUS_DESCRIPTION_REMOVED = "@text/mielecloud.thing.status.removed";
+ public static final String THING_STATUS_DESCRIPTION_RATELIMIT = "@text/mielecloud.thing.status.ratelimit";
+ public static final String THING_STATUS_DESCRIPTION_DISCONNECTED = "@text/mielecloud.thing.status.disconnected";
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.auth;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Indicates an error in the OAuth2 authorization process.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ */
+@NonNullByDefault
+public class OAuthException extends RuntimeException {
+ private static final long serialVersionUID = -1863609233382694104L;
+
+ public OAuthException(final String message) {
+ super(message);
+ }
+
+ public OAuthException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.auth;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Listener that is invoked when an OAuth 2 access token was refreshed.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public interface OAuthTokenRefreshListener {
+ /**
+ * Invoked when a new access token becomes available.
+ *
+ * @param accessToken The new access token.
+ */
+ public void onNewAccessToken(String accessToken);
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.auth;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * An {@link OAuthTokenRefresher} offers convenient access to OAuth 2 authentication related functionality,
+ * especially refreshing the access token.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Allow removing tokens from the storage
+ */
+@NonNullByDefault
+public interface OAuthTokenRefresher {
+ /**
+ * Sets the listener that is called when the access token was refreshed.
+ *
+ * @param listener The listener to register.
+ * @param serviceHandle The service handle identifying the internal OAuth configuration.
+ * @throws OAuthException if the listener needs to be registered at an underlying service which is not available
+ * because the account has not yet been authorized
+ */
+ public void setRefreshListener(OAuthTokenRefreshListener listener, String serviceHandle);
+
+ /**
+ * Unsets a listener.
+ *
+ * @param serviceHandle The service handle identifying the internal OAuth configuration.
+ */
+ public void unsetRefreshListener(String serviceHandle);
+
+ /**
+ * Refreshes the access and refresh tokens for the given service handle. If an {@link OAuthTokenRefreshListener} is
+ * registered for the service handle then it is notified after the refresh has completed.
+ *
+ * This call will succeed if the access token is still valid or a valid refresh token exists, which can be used to
+ * refresh the expired access token. If refreshing fails, an {@link OAuthException} is thrown.
+ *
+ * @param serviceHandle The service handle identifying the internal OAuth configuration.
+ * @throws OAuthException if the token cannot be obtained or refreshed
+ */
+ public void refreshToken(String serviceHandle);
+
+ /**
+ * Gets the currently stored access token from persistent storage.
+ *
+ * @param serviceHandle The service handle identifying the internal OAuth configuration.
+ * @return The currently stored access token or an empty {@link Optional} if there is no stored token.
+ */
+ public Optional<String> getAccessTokenFromStorage(String serviceHandle);
+
+ /**
+ * Removes the tokens from persistent storage.
+ *
+ * Note: Calling this method will force the user to run through the pairing process again in order to obtain a
+ * working bridge.
+ *
+ * @param serviceHandle The service handle identifying the internal OAuth configuration.
+ */
+ public void removeTokensFromStorage(String serviceHandle);
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.auth;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener;
+import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
+import org.openhab.core.auth.client.oauth2.OAuthClientService;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
+import org.openhab.core.auth.client.oauth2.OAuthResponseException;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handles refreshing of OAuth2 tokens managed by the openHAB runtime.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@Component
+@NonNullByDefault
+public final class OpenHabOAuthTokenRefresher implements OAuthTokenRefresher {
+ private final Logger logger = LoggerFactory.getLogger(OpenHabOAuthTokenRefresher.class);
+
+ private final OAuthFactory oauthFactory;
+ private Map<String, @Nullable AccessTokenRefreshListener> listenerByServiceHandle = new HashMap<>();
+
+ @Activate
+ public OpenHabOAuthTokenRefresher(@Reference OAuthFactory oauthFactory) {
+ this.oauthFactory = oauthFactory;
+ }
+
+ @Override
+ public void setRefreshListener(OAuthTokenRefreshListener listener, String serviceHandle) {
+ final AccessTokenRefreshListener refreshListener = tokenResponse -> {
+ final String accessToken = tokenResponse.getAccessToken();
+ if (accessToken == null) {
+ // Fail without exception to ensure that the OAuthClientService notifies all listeners.
+ logger.warn("Ignoring access token response without access token.");
+ } else {
+ listener.onNewAccessToken(accessToken);
+ }
+ };
+
+ OAuthClientService clientService = getOAuthClientService(serviceHandle);
+ clientService.addAccessTokenRefreshListener(refreshListener);
+ listenerByServiceHandle.put(serviceHandle, refreshListener);
+ }
+
+ @Override
+ public void unsetRefreshListener(String serviceHandle) {
+ final AccessTokenRefreshListener refreshListener = listenerByServiceHandle.get(serviceHandle);
+ if (refreshListener != null) {
+ try {
+ OAuthClientService clientService = getOAuthClientService(serviceHandle);
+ clientService.removeAccessTokenRefreshListener(refreshListener);
+ } catch (OAuthException e) {
+ logger.warn("Failed to remove refresh listener: OAuth client service is unavailable. Cause: {}",
+ e.getMessage());
+ }
+ }
+ listenerByServiceHandle.remove(serviceHandle);
+ }
+
+ @Override
+ public void refreshToken(String serviceHandle) {
+ if (listenerByServiceHandle.get(serviceHandle) == null) {
+ logger.warn("Token refreshing was requested but there is no token refresh listener registered!");
+ return;
+ }
+
+ OAuthClientService clientService = getOAuthClientService(serviceHandle);
+ refreshAccessToken(clientService);
+ }
+
+ private OAuthClientService getOAuthClientService(String serviceHandle) {
+ final OAuthClientService clientService = oauthFactory.getOAuthClientService(serviceHandle);
+ if (clientService == null) {
+ throw new OAuthException("OAuth client service is not available.");
+ }
+ return clientService;
+ }
+
+ private void refreshAccessToken(OAuthClientService clientService) {
+ try {
+ final AccessTokenResponse accessTokenResponse = clientService.refreshToken();
+ final String accessToken = accessTokenResponse.getAccessToken();
+ if (accessToken == null) {
+ throw new OAuthException("Access token is not available.");
+ }
+ } catch (org.openhab.core.auth.client.oauth2.OAuthException e) {
+ throw new OAuthException("An error occured during token refresh: " + e.getMessage(), e);
+ } catch (IOException e) {
+ throw new OAuthException("A network error occured during token refresh: " + e.getMessage(), e);
+ } catch (OAuthResponseException e) {
+ throw new OAuthException("Miele cloud service returned an illegal response: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public Optional<String> getAccessTokenFromStorage(String serviceHandle) {
+ try {
+ AccessTokenResponse tokenResponse = getOAuthClientService(serviceHandle).getAccessTokenResponse();
+ if (tokenResponse == null) {
+ return Optional.empty();
+ } else {
+ return Optional.of(tokenResponse.getAccessToken());
+ }
+ } catch (OAuthException | org.openhab.core.auth.client.oauth2.OAuthException | IOException
+ | OAuthResponseException e) {
+ logger.debug("Cannot obtain access token from persistent storage.", e);
+ return Optional.empty();
+ }
+ }
+
+ @Override
+ public void removeTokensFromStorage(String serviceHandle) {
+ oauthFactory.deleteServiceAndAccessToken(serviceHandle);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config;
+
+import java.util.Hashtable;
+import java.util.Map;
+
+import javax.servlet.ServletException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.config.servlet.AccountOverviewServlet;
+import org.openhab.binding.mielecloud.internal.config.servlet.CreateBridgeServlet;
+import org.openhab.binding.mielecloud.internal.config.servlet.FailureServlet;
+import org.openhab.binding.mielecloud.internal.config.servlet.ForwardToLoginServlet;
+import org.openhab.binding.mielecloud.internal.config.servlet.PairAccountServlet;
+import org.openhab.binding.mielecloud.internal.config.servlet.ResourceLoader;
+import org.openhab.binding.mielecloud.internal.config.servlet.ResultServlet;
+import org.openhab.binding.mielecloud.internal.config.servlet.SuccessServlet;
+import org.openhab.binding.mielecloud.internal.webservice.language.CombiningLanguageProvider;
+import org.openhab.binding.mielecloud.internal.webservice.language.JvmLanguageProvider;
+import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider;
+import org.openhab.binding.mielecloud.internal.webservice.language.OpenHabLanguageProvider;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
+import org.openhab.core.common.ThreadPoolManager;
+import org.openhab.core.config.discovery.inbox.Inbox;
+import org.openhab.core.i18n.LocaleProvider;
+import org.openhab.core.thing.ThingRegistry;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.http.HttpContext;
+import org.osgi.service.http.HttpService;
+import org.osgi.service.http.NamespaceException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handles the lifecycle of the Miele Cloud binding's configuration UI.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@Component(service = MieleCloudConfigService.class, immediate = true, configurationPid = "binding.mielecloud.configService")
+@NonNullByDefault
+public final class MieleCloudConfigService {
+ private static final String ROOT_ALIAS = "/mielecloud";
+ private static final String PAIR_ALIAS = ROOT_ALIAS + "/pair";
+ private static final String FORWARD_TO_LOGIN_ALIAS = ROOT_ALIAS + "/forwardToLogin";
+ private static final String RESULT_ALIAS = ROOT_ALIAS + "/result";
+ private static final String SUCCESS_ALIAS = ROOT_ALIAS + "/success";
+ private static final String CREATE_BRIDGE_THING_ALIAS = ROOT_ALIAS + "/createBridgeThing";
+ private static final String FAILURE_ALIAS = ROOT_ALIAS + "/failure";
+ private static final String CSS_ALIAS = ROOT_ALIAS + "/assets/css";
+ private static final String JS_ALIAS = ROOT_ALIAS + "/assets/js";
+ private static final String IMG_ALIAS = ROOT_ALIAS + "/assets/img";
+
+ private static final String WEBSITE_RESOURCE_BASE_PATH = "org/openhab/binding/mielecloud/internal/config";
+ private static final String WEBSITE_CSS_RESOURCE_PATH = WEBSITE_RESOURCE_BASE_PATH + "/assets/css";
+ private static final String WEBSITE_JS_RESOURCE_PATH = WEBSITE_RESOURCE_BASE_PATH + "/assets/js";
+ private static final String WEBSITE_IMG_RESOURCE_PATH = WEBSITE_RESOURCE_BASE_PATH + "/assets/img";
+
+ private final Logger logger = LoggerFactory.getLogger(MieleCloudConfigService.class);
+
+ private HttpService httpService;
+ private OAuthFactory oauthFactory;
+ private Inbox inbox;
+ private ThingRegistry thingRegistry;
+ private LocaleProvider localeProvider;
+
+ /**
+ * For integration test purposes only.
+ */
+ @Nullable
+ private AccountOverviewServlet accountOverviewServlet;
+
+ /**
+ * For integration test purposes only.
+ */
+ @Nullable
+ private ForwardToLoginServlet forwardToLoginServlet;
+
+ /**
+ * For integration test purposes only.
+ */
+ @Nullable
+ private ResultServlet resultServlet;
+
+ /**
+ * For integration test purposes only.
+ */
+ @Nullable
+ private SuccessServlet successServlet;
+
+ /**
+ * For integration test purposes only.
+ */
+ @Nullable
+ private CreateBridgeServlet createBridgeServlet;
+
+ @Activate
+ public MieleCloudConfigService(@Reference HttpService httpService, @Reference OAuthFactory oauthFactory,
+ @Reference Inbox inbox, @Reference ThingRegistry thingRegistry, @Reference LocaleProvider localeProvider) {
+ this.httpService = httpService;
+ this.oauthFactory = oauthFactory;
+ this.inbox = inbox;
+ this.thingRegistry = thingRegistry;
+ this.localeProvider = localeProvider;
+ }
+
+ @Nullable
+ public AccountOverviewServlet getAccountOverviewServlet() {
+ return accountOverviewServlet;
+ }
+
+ @Nullable
+ public ForwardToLoginServlet getForwardToLoginServlet() {
+ return forwardToLoginServlet;
+ }
+
+ @Nullable
+ public ResultServlet getResultServlet() {
+ return resultServlet;
+ }
+
+ @Nullable
+ public SuccessServlet getSuccessServlet() {
+ return successServlet;
+ }
+
+ @Nullable
+ public CreateBridgeServlet getCreateBridgeServlet() {
+ return createBridgeServlet;
+ }
+
+ @Activate
+ protected void activate(ComponentContext componentContext, Map<String, Object> properties) {
+ registerWebsite(componentContext.getBundleContext());
+ }
+
+ private void registerWebsite(BundleContext bundleContext) {
+ ResourceLoader resourceLoader = new ResourceLoader(WEBSITE_RESOURCE_BASE_PATH, bundleContext);
+ OAuthAuthorizationHandler authorizationHandler = new OAuthAuthorizationHandlerImpl(oauthFactory,
+ ThreadPoolManager.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON));
+
+ try {
+ HttpContext httpContext = httpService.createDefaultHttpContext();
+ httpService.registerServlet(ROOT_ALIAS,
+ accountOverviewServlet = new AccountOverviewServlet(resourceLoader, thingRegistry, inbox),
+ new Hashtable<>(), httpContext);
+ httpService.registerServlet(PAIR_ALIAS, new PairAccountServlet(resourceLoader), new Hashtable<>(),
+ httpContext);
+ httpService.registerServlet(FORWARD_TO_LOGIN_ALIAS,
+ forwardToLoginServlet = new ForwardToLoginServlet(authorizationHandler), new Hashtable<>(),
+ httpContext);
+ httpService.registerServlet(RESULT_ALIAS, resultServlet = new ResultServlet(authorizationHandler),
+ new Hashtable<>(), httpContext);
+ httpService.registerServlet(SUCCESS_ALIAS,
+ successServlet = new SuccessServlet(resourceLoader, createLanguageProvider()), new Hashtable<>(),
+ httpContext);
+ httpService.registerServlet(CREATE_BRIDGE_THING_ALIAS,
+ createBridgeServlet = new CreateBridgeServlet(inbox, thingRegistry), new Hashtable<>(),
+ httpContext);
+ httpService.registerServlet(FAILURE_ALIAS, new FailureServlet(resourceLoader), new Hashtable<>(),
+ httpContext);
+ httpService.registerResources(CSS_ALIAS, WEBSITE_CSS_RESOURCE_PATH, httpContext);
+ httpService.registerResources(JS_ALIAS, WEBSITE_JS_RESOURCE_PATH, httpContext);
+ httpService.registerResources(IMG_ALIAS, WEBSITE_IMG_RESOURCE_PATH, httpContext);
+ logger.debug("Registered Miele Cloud binding website at /mielecloud");
+ } catch (NamespaceException | ServletException e) {
+ logger.warn(
+ "Failed to register Miele Cloud binding website. Miele Cloud binding website will not be available.",
+ e);
+ unregisterWebsite();
+ }
+ }
+
+ private LanguageProvider createLanguageProvider() {
+ return new CombiningLanguageProvider(new OpenHabLanguageProvider(localeProvider), new JvmLanguageProvider());
+ }
+
+ @Deactivate
+ protected void deactivate() {
+ unregisterWebsite();
+ }
+
+ private void unregisterWebsite() {
+ unregisterWebResource(ROOT_ALIAS);
+ unregisterWebResource(PAIR_ALIAS);
+ unregisterWebResource(FORWARD_TO_LOGIN_ALIAS);
+ unregisterWebResource(RESULT_ALIAS);
+ unregisterWebResource(SUCCESS_ALIAS);
+ unregisterWebResource(CREATE_BRIDGE_THING_ALIAS);
+ unregisterWebResource(CSS_ALIAS);
+ unregisterWebResource(JS_ALIAS);
+ unregisterWebResource(IMG_ALIAS);
+ forwardToLoginServlet = null;
+ resultServlet = null;
+ createBridgeServlet = null;
+ logger.debug("Unregistered Miele Cloud binding website at /mielecloud");
+ }
+
+ private void unregisterWebResource(String alias) {
+ try {
+ httpService.unregister(alias);
+ } catch (IllegalArgumentException e) {
+ logger.warn("Failed to unregister Miele Cloud binding website alias {}", alias, e);
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.config.exception.NoOngoingAuthorizationException;
+import org.openhab.binding.mielecloud.internal.config.exception.OngoingAuthorizationException;
+import org.openhab.core.thing.ThingUID;
+
+/**
+ * Handles OAuth 2 authorization processes.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public interface OAuthAuthorizationHandler {
+ /**
+ * Begins the authorization process after the user provided client ID, client secret and a bridge ID.
+ *
+ * @param clientId Client ID.
+ * @param clientSecret Client secret.
+ * @param bridgeUid The UID of the bridge to authorize.
+ * @param email E-mail address identifying the account to authorize.
+ * @throws OngoingAuthorizationException if there already is an ongoing authorization.
+ */
+ void beginAuthorization(String clientId, String clientSecret, ThingUID bridgeUid, String email);
+
+ /**
+ * Creates the authorization URL for the ongoing authorization.
+ *
+ * @param redirectUri The URI to which the user is redirected after a successful login. This should point to our own
+ * service.
+ * @return The authorization URL to which the user is redirected for the log in.
+ * @throws NoOngoingAuthorizationException if there is no ongoing authorization.
+ * @throws OAuthException if the authorization URL cannot be determined. In this case the ongoing authorization is
+ * cancelled.
+ */
+ String getAuthorizationUrl(String redirectUri);
+
+ /**
+ * Gets the UID of the bridge that is currently being authorized.
+ */
+ ThingUID getBridgeUid();
+
+ /**
+ * Gets the e-mail address associated with the account that is currently being authorized.
+ */
+ String getEmail();
+
+ /**
+ * Completes the authorization by extracting the authorization code from the given redirection URL, fetching the
+ * access token response and persisting it. After this method succeeded the access token can be read from the
+ * persistent storage.
+ *
+ * @param redirectUrlWithParameters The URL the remote service redirected the user to. This is the URL our servlet
+ * was called with.
+ * @throws NoOngoingAuthorizationException if there is no ongoing authorization.
+ * @throws OAuthException if the authorization failed. In this case the ongoing authorization is cancelled.
+ */
+ void completeAuthorization(String redirectUrlWithParameters);
+
+ /**
+ * Gets the access token from persistent storage.
+ *
+ * @param email E-mail address for which the access token is requested.
+ * @return The access token.
+ * @throws OAuthException if the access token cannot be obtained.
+ */
+ String getAccessToken(String email);
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.auth.OAuthException;
+import org.openhab.binding.mielecloud.internal.config.exception.NoOngoingAuthorizationException;
+import org.openhab.binding.mielecloud.internal.config.exception.OngoingAuthorizationException;
+import org.openhab.binding.mielecloud.internal.webservice.DefaultMieleWebservice;
+import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
+import org.openhab.core.auth.client.oauth2.OAuthClientService;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
+import org.openhab.core.auth.client.oauth2.OAuthResponseException;
+import org.openhab.core.thing.ThingUID;
+
+/**
+ * {@link OAuthAuthorizationHandler} implementation handling the OAuth 2 authorization via openHAB services.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class OAuthAuthorizationHandlerImpl implements OAuthAuthorizationHandler {
+ private static final String TOKEN_URL = DefaultMieleWebservice.THIRD_PARTY_ENDPOINTS_BASENAME + "/token";
+ private static final String AUTHORIZATION_URL = DefaultMieleWebservice.THIRD_PARTY_ENDPOINTS_BASENAME + "/login";
+
+ private static final long AUTHORIZATION_TIMEOUT_IN_MINUTES = 5;
+
+ private final OAuthFactory oauthFactory;
+ private final ScheduledExecutorService scheduler;
+
+ @Nullable
+ private OAuthClientService oauthClientService;
+ @Nullable
+ private ThingUID bridgeUid;
+ @Nullable
+ private String email;
+ @Nullable
+ private String redirectUri;
+ @Nullable
+ private ScheduledFuture<?> timer;
+ @Nullable
+ private LocalDateTime timerExpiryTimestamp;
+
+ /**
+ * Creates a new {@link OAuthAuthorizationHandlerImpl}.
+ *
+ * @param oauthFactory Factory for accessing the {@link OAuthClientService}.
+ * @param scheduler System-wide scheduler.
+ */
+ public OAuthAuthorizationHandlerImpl(OAuthFactory oauthFactory, ScheduledExecutorService scheduler) {
+ this.oauthFactory = oauthFactory;
+ this.scheduler = scheduler;
+ }
+
+ @Override
+ public synchronized void beginAuthorization(String clientId, String clientSecret, ThingUID bridgeUid,
+ String email) {
+ if (this.oauthClientService != null) {
+ throw new OngoingAuthorizationException("There is already an ongoing authorization!", timerExpiryTimestamp);
+ }
+
+ this.oauthClientService = oauthFactory.createOAuthClientService(email, TOKEN_URL, AUTHORIZATION_URL, clientId,
+ clientSecret, null, false);
+ this.bridgeUid = bridgeUid;
+ this.email = email;
+ redirectUri = null;
+ timer = null;
+ timerExpiryTimestamp = null;
+ }
+
+ @Override
+ public synchronized String getAuthorizationUrl(String redirectUri) {
+ final OAuthClientService oauthClientService = this.oauthClientService;
+ if (oauthClientService == null) {
+ throw new NoOngoingAuthorizationException("There is no ongoing authorization!");
+ }
+
+ this.redirectUri = redirectUri;
+ try {
+ timer = scheduler.schedule(this::cancelAuthorization, AUTHORIZATION_TIMEOUT_IN_MINUTES, TimeUnit.MINUTES);
+ timerExpiryTimestamp = LocalDateTime.now().plusMinutes(AUTHORIZATION_TIMEOUT_IN_MINUTES);
+ return oauthClientService.getAuthorizationUrl(redirectUri, null, null);
+ } catch (org.openhab.core.auth.client.oauth2.OAuthException e) {
+ abortTimer();
+ cancelAuthorization();
+ throw new OAuthException("Failed to determine authorization URL: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public ThingUID getBridgeUid() {
+ final ThingUID bridgeUid = this.bridgeUid;
+ if (bridgeUid == null) {
+ throw new NoOngoingAuthorizationException("There is no ongoing authorization.");
+ }
+ return bridgeUid;
+ }
+
+ @Override
+ public String getEmail() {
+ final String email = this.email;
+ if (email == null) {
+ throw new NoOngoingAuthorizationException("There is no ongoing authorization.");
+ }
+ return email;
+ }
+
+ @Override
+ public synchronized void completeAuthorization(String redirectUrlWithParameters) {
+ abortTimer();
+
+ final OAuthClientService oauthClientService = this.oauthClientService;
+ if (oauthClientService == null) {
+ throw new NoOngoingAuthorizationException("There is no ongoing authorization.");
+ }
+
+ try {
+ String authorizationCode = oauthClientService.extractAuthCodeFromAuthResponse(redirectUrlWithParameters);
+
+ // Although this method is called "get" it actually fetches and stores the token response as a side effect.
+ oauthClientService.getAccessTokenResponseByAuthorizationCode(authorizationCode, redirectUri);
+ } catch (IOException e) {
+ throw new OAuthException("Network error while retrieving token response: " + e.getMessage(), e);
+ } catch (OAuthResponseException e) {
+ throw new OAuthException("Failed to retrieve token response: " + e.getMessage(), e);
+ } catch (org.openhab.core.auth.client.oauth2.OAuthException e) {
+ throw new OAuthException("Error while processing Miele service response: " + e.getMessage(), e);
+ } finally {
+ this.oauthClientService = null;
+ this.bridgeUid = null;
+ this.email = null;
+ this.redirectUri = null;
+ }
+ }
+
+ /**
+ * Aborts the timer.
+ *
+ * Note: All calls to this method must be {@code synchronized} to ensure thread-safety. Also note that
+ * {@link #cancelAuthorization()} is {@code synchronized} so the execution of this method and
+ * {@link #cancelAuthorization()} cannot overlap. Therefore, this method is an atomic operation from the timer's
+ * perspective.
+ */
+ private void abortTimer() {
+ final ScheduledFuture<?> timer = this.timer;
+ if (timer == null) {
+ return;
+ }
+
+ if (!timer.isDone()) {
+ timer.cancel(false);
+ }
+ this.timer = null;
+ timerExpiryTimestamp = null;
+ }
+
+ private synchronized void cancelAuthorization() {
+ oauthClientService = null;
+ bridgeUid = null;
+ email = null;
+ redirectUri = null;
+ final ScheduledFuture<?> timer = this.timer;
+ if (timer != null) {
+ timer.cancel(false);
+ this.timer = null;
+ timerExpiryTimestamp = null;
+ }
+ }
+
+ @Override
+ public String getAccessToken(String email) {
+ OAuthClientService clientService = oauthFactory.getOAuthClientService(email);
+ if (clientService == null) {
+ throw new OAuthException("There is no access token registered for '" + email + "'");
+ }
+
+ try {
+ AccessTokenResponse response = clientService.getAccessTokenResponse();
+ if (response == null) {
+ throw new OAuthException(
+ "There is no access token in the persistent storage or it already expired and could not be refreshed");
+ } else {
+ return response.getAccessToken();
+ }
+ } catch (org.openhab.core.auth.client.oauth2.OAuthException e) {
+ throw new OAuthException("Failed to read access token from persistent storage: " + e.getMessage(), e);
+ } catch (IOException e) {
+ throw new OAuthException(
+ "Network error during token refresh or error while reading from persistent storage: "
+ + e.getMessage(),
+ e);
+ } catch (OAuthResponseException e) {
+ throw new OAuthException("Failed to retrieve token response: " + e.getMessage(), e);
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+
+/**
+ * Generator for templates which can be copy-pasted into .things files by the user.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class ThingsTemplateGenerator {
+ /**
+ * Creates a template for the bridge.
+ *
+ * @param bridgeId Id of the bridge (last part of the thing UID).
+ * @param locale Locale for accessing the Miele cloud service.
+ * @return The template.
+ */
+ public String createBridgeConfigurationTemplate(String bridgeId, String email, String locale) {
+ var builder = new StringBuilder();
+ builder.append("Bridge ");
+ builder.append(MieleCloudBindingConstants.THING_TYPE_BRIDGE.getAsString());
+ builder.append(":");
+ builder.append(bridgeId);
+ builder.append(" [ email=\"");
+ builder.append(email);
+ builder.append("\", locale=\"");
+ builder.append(locale);
+ builder.append("\" ]");
+ return builder.toString();
+ }
+
+ /**
+ * Creates a complete template containing the bridge and all paired devices.
+ *
+ * @param bridge The bridge which is used to pair the things.
+ * @param pairedThings The paired things.
+ * @param discoveryResults The discovery results which can be paired.
+ * @return The template.
+ */
+ public String createBridgeAndThingConfigurationTemplate(Bridge bridge, List<Thing> pairedThings,
+ List<DiscoveryResult> discoveryResults) {
+ StringBuilder result = new StringBuilder();
+ result.append(createBridgeConfigurationTemplate(bridge.getUID().getId(),
+ bridge.getConfiguration().get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL).toString(),
+ getLocale(bridge)));
+ result.append(" {\n");
+
+ for (Thing thing : pairedThings) {
+ result.append(" ").append(createThingConfigurationTemplate(thing)).append("\n");
+ }
+
+ for (DiscoveryResult discoveryResult : discoveryResults) {
+ result.append(" ").append(createThingConfigurationTemplate(discoveryResult)).append("\n");
+ }
+
+ result.append("}");
+ return result.toString();
+ }
+
+ private String getLocale(Bridge bridge) {
+ var locale = bridge.getConfiguration().get(MieleCloudBindingConstants.CONFIG_PARAM_LOCALE);
+ if (locale instanceof String) {
+ return (String) locale;
+ } else {
+ return "en";
+ }
+ }
+
+ private String createThingConfigurationTemplate(Thing thing) {
+ StringBuilder result = new StringBuilder();
+ result.append("Thing ").append(thing.getThingTypeUID().getId()).append(" ").append(thing.getUID().getId())
+ .append(" ");
+
+ final String label = thing.getLabel();
+ if (label != null) {
+ result.append("\"").append(label).append("\" ");
+ }
+
+ result.append("[ ");
+ result.append("deviceIdentifier=\"");
+ result.append(
+ thing.getConfiguration().get(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER).toString());
+ result.append("\"");
+ result.append(" ]");
+ return result.toString();
+ }
+
+ private String createThingConfigurationTemplate(DiscoveryResult discoveryResult) {
+ return "Thing " + discoveryResult.getThingTypeUID().getId() + " " + discoveryResult.getThingUID().getId()
+ + " \"" + discoveryResult.getLabel() + "\" [ deviceIdentifier=\""
+ + getProperty(discoveryResult, MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER) + "\" ]";
+ }
+
+ private String getProperty(DiscoveryResult discoveryResult, String propertyName) {
+ var value = discoveryResult.getProperties().get(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER);
+ if (value == null) {
+ return "";
+ } else {
+ return value.toString();
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Exception thrown when a bridge cannot be created in the configuration flow.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class BridgeCreationFailedException extends RuntimeException {
+ private static final long serialVersionUID = -6150154333256723312L;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Exception thrown when reconfiguring an existing bridge fails.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class BridgeReconfigurationFailedException extends RuntimeException {
+ private static final long serialVersionUID = -6341258448724364940L;
+
+ public BridgeReconfigurationFailedException(String message) {
+ super(message);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Exception thrown when no authorization is ongoing.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class NoOngoingAuthorizationException extends RuntimeException {
+ private static final long serialVersionUID = 3074275827393542416L;
+
+ public NoOngoingAuthorizationException(String message) {
+ super(message);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.exception;
+
+import java.time.LocalDateTime;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Exception thrown when there already is an ongoing authorization process.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class OngoingAuthorizationException extends RuntimeException {
+ private static final long serialVersionUID = -6742384930140134244L;
+
+ @Nullable
+ private final LocalDateTime ongoingAuthorizationExpiryTimestamp;
+
+ /**
+ * Creates a new {@link OngoingAuthorizationException}.
+ *
+ * @param message Exception message.
+ * @param ongoingAuthorizationExpiryTimestamp Timestamp when the ongoing authorization will expire.
+ */
+ public OngoingAuthorizationException(String message, @Nullable LocalDateTime ongoingAuthorizationExpiryTimestamp) {
+ super(message);
+ this.ongoingAuthorizationExpiryTimestamp = ongoingAuthorizationExpiryTimestamp;
+ }
+
+ /**
+ * Gets the timestamp representing when the ongoing authorization will expire.
+ */
+ @Nullable
+ public LocalDateTime getOngoingAuthorizationExpiryTimestamp() {
+ return ongoingAuthorizationExpiryTimestamp;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Base class for servlets that have no visible frontend and just serve the purpose of redirecting the user to another
+ * website.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public abstract class AbstractRedirectionServlet extends HttpServlet {
+ private static final long serialVersionUID = 4280026301732437523L;
+
+ private final Logger logger = LoggerFactory.getLogger(AbstractRedirectionServlet.class);
+
+ @Override
+ protected void doGet(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response)
+ throws ServletException, IOException {
+ if (response == null) {
+ logger.warn("Ignoring received request without response.");
+ return;
+ }
+ if (request == null) {
+ logger.warn("Ignoring illegal request.");
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+ return;
+ }
+
+ response.sendRedirect(getRedirectionDestination(request));
+ }
+
+ /**
+ * Gets the redirection destination. This can be a relative or absolute path or a link to another website.
+ *
+ * @param request The original request sent by the browser.
+ * @return The redirection destination.
+ */
+ protected abstract String getRedirectionDestination(HttpServletRequest request);
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Base class for servlets that show a visible frontend in the browser.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public abstract class AbstractShowPageServlet extends HttpServlet {
+ private static final long serialVersionUID = 3820684716753275768L;
+
+ private static final String CONTENT_TYPE = "text/html;charset=UTF-8";
+
+ private final Logger logger = LoggerFactory.getLogger(AbstractShowPageServlet.class);
+
+ private final ResourceLoader resourceLoader;
+
+ protected ResourceLoader getResourceLoader() {
+ return resourceLoader;
+ }
+
+ /**
+ * Creates a new {@link AbstractShowPageServlet}.
+ *
+ * @param resourceLoader Loader for resource files.
+ */
+ public AbstractShowPageServlet(ResourceLoader resourceLoader) {
+ this.resourceLoader = resourceLoader;
+ }
+
+ @Override
+ protected void doGet(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response)
+ throws ServletException, IOException {
+ if (response == null) {
+ logger.warn("Ignoring received request without response.");
+ return;
+ }
+ if (request == null) {
+ logger.warn("Ignoring illegal request.");
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+ return;
+ }
+
+ try {
+ String html = handleGetRequest(request, response);
+ response.setContentType(CONTENT_TYPE);
+ response.getWriter().write(html);
+ response.getWriter().close();
+ } catch (MieleHttpException e) {
+ response.sendError(e.getHttpErrorCode());
+ } catch (IOException e) {
+ logger.warn("Failed to load resources.", e);
+ response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ /**
+ * Handles a GET request.
+ *
+ * @param request The request.
+ * @param response The response.
+ * @return A rendered HTML body to be displayed in the browser. The body will be framed by the binding's frontend
+ * layout.
+ * @throws MieleHttpException if an error occurs that should be handled by sending a default error response.
+ * @throws IOException if an error occurs while loading resources.
+ */
+ protected abstract String handleGetRequest(HttpServletRequest request, HttpServletResponse response)
+ throws MieleHttpException, IOException;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.config.ThingsTemplateGenerator;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.inbox.Inbox;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingRegistry;
+import org.openhab.core.thing.ThingStatus;
+
+/**
+ * Servlet showing the account overview page.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class AccountOverviewServlet extends AbstractShowPageServlet {
+ private static final long serialVersionUID = -4551210904923220429L;
+ private static final String ACCOUNTS_SKELETON = "index.html";
+
+ private static final String BRIDGES_TITLE_PLACEHOLDER = "<!-- BRIDGES TITLE -->";
+ private static final String BRIDGES_PLACEHOLDER = "<!-- BRIDGES -->";
+ private static final String NO_SSL_WARNING_PLACEHOLDER = "<!-- NO SSL WARNING -->";
+
+ private final ThingRegistry thingRegistry;
+ private final Inbox inbox;
+ private final ThingsTemplateGenerator templateGenerator;
+
+ /**
+ * Creates a new {@link AccountOverviewServlet}.
+ *
+ * @param resourceLoader Loader to use for resources.
+ * @param thingRegistry openHAB thing registry.
+ * @param inbox openHAB inbox for discovery results.
+ */
+ public AccountOverviewServlet(ResourceLoader resourceLoader, ThingRegistry thingRegistry, Inbox inbox) {
+ super(resourceLoader);
+ this.thingRegistry = thingRegistry;
+ this.inbox = inbox;
+ this.templateGenerator = new ThingsTemplateGenerator();
+ }
+
+ @Override
+ protected String handleGetRequest(HttpServletRequest request, HttpServletResponse response)
+ throws MieleHttpException, IOException {
+ String skeleton = getResourceLoader().loadResourceAsString(ACCOUNTS_SKELETON);
+ skeleton = renderBridges(skeleton);
+ skeleton = renderSslWarning(request, skeleton);
+ return skeleton;
+ }
+
+ private String renderBridges(String skeleton) {
+ List<Thing> bridges = thingRegistry.stream().filter(this::isMieleCloudBridge).collect(Collectors.toList());
+ if (bridges.isEmpty()) {
+ return renderNoBridges(skeleton);
+ } else {
+ return renderBridgesIntoSkeleton(skeleton, bridges);
+ }
+ }
+
+ private String renderNoBridges(String skeleton) {
+ return skeleton.replace(BRIDGES_TITLE_PLACEHOLDER, "There is no account paired at the moment.")
+ .replace(BRIDGES_PLACEHOLDER, "");
+ }
+
+ private String renderBridgesIntoSkeleton(String skeleton, List<Thing> bridges) {
+ StringBuilder builder = new StringBuilder();
+
+ int index = 0;
+ Iterator<Thing> bridgeIterator = bridges.iterator();
+ while (bridgeIterator.hasNext()) {
+ builder.append(renderBridge(bridgeIterator.next(), index));
+ index++;
+ }
+
+ return skeleton.replace(BRIDGES_TITLE_PLACEHOLDER, "The following bridges are paired")
+ .replace(BRIDGES_PLACEHOLDER, builder.toString());
+ }
+
+ private String renderBridge(Thing bridge, int index) {
+ StringBuilder builder = new StringBuilder();
+ builder.append(" <li>\n");
+
+ String thingUid = bridge.getUID().getAsString();
+ String thingId = bridge.getUID().getId();
+ builder.append(" ");
+ builder.append(thingUid.substring(0, thingUid.length() - thingId.length()));
+ builder.append(" ");
+ builder.append(thingId);
+ builder.append(" ");
+ builder.append(bridge.getConfiguration().get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL).toString());
+ builder.append("\n");
+
+ builder.append(" <span class=\"status ");
+ final ThingStatus status = bridge.getStatus();
+ if (status == ThingStatus.ONLINE) {
+ builder.append("online");
+ } else {
+ builder.append("offline");
+ }
+ builder.append("\">");
+ builder.append(status.toString());
+ builder.append("</span>\n");
+
+ builder.append(" <input class=\"trigger\" id=\"mielecloud-account-");
+ builder.append(thingId);
+ builder.append("\" type=\"checkbox\" name=\"things-file\" />\n");
+
+ builder.append(" <label for=\"mielecloud-account-");
+ builder.append(thingId);
+ builder.append("\">< ></label>\n");
+
+ builder.append(" <div class=\"things\">\n");
+ builder.append(
+ " <span class=\"legend\">You can use this things-file template to pair all available devices:</span>\n");
+ builder.append(" <div class=\"code-container\">\n");
+ builder.append(
+ " <a href=\"#\" onclick=\"copyCodeToClipboard(event, this);\" class=\"btn btn-outline-info btn-sm copy\">Copy</a>\n");
+ builder.append(" <textarea readonly>");
+ builder.append(generateConfigurationTemplate((Bridge) bridge));
+ builder.append("</textarea>\n");
+ builder.append(" </div>\n");
+ builder.append(" </div>\n");
+ builder.append(" </li>");
+
+ return builder.toString();
+ }
+
+ private String generateConfigurationTemplate(Bridge bridge) {
+ List<Thing> pairedThings = thingRegistry.stream().filter(thing -> isConnectedVia(thing, bridge))
+ .collect(Collectors.toList());
+ List<DiscoveryResult> discoveryResults = inbox.stream()
+ .filter(discoveryResult -> willConnectVia(discoveryResult, bridge)).collect(Collectors.toList());
+
+ return templateGenerator.createBridgeAndThingConfigurationTemplate(bridge, pairedThings, discoveryResults);
+ }
+
+ private boolean isConnectedVia(Thing thing, Bridge bridge) {
+ return bridge.getUID().equals(thing.getBridgeUID());
+ }
+
+ private boolean willConnectVia(DiscoveryResult discoveryResult, Bridge bridge) {
+ return bridge.getUID().equals(discoveryResult.getBridgeUID());
+ }
+
+ private boolean isMieleCloudBridge(Thing thing) {
+ return MieleCloudBindingConstants.THING_TYPE_BRIDGE.equals(thing.getThingTypeUID());
+ }
+
+ private String renderSslWarning(HttpServletRequest request, String skeleton) {
+ if (!request.isSecure()) {
+ return skeleton.replace(NO_SSL_WARNING_PLACEHOLDER, "<div class=\"alert alert-danger\" role=\"alert\">\n"
+ + " Warning: We strongly advice to proceed only with SSL enabled for a secure data exchange.\n"
+ + " See <a href=\"https://www.openhab.org/docs/installation/security.html\">Securing access to openHAB</a> for details.\n"
+ + " </div>");
+ } else {
+ return skeleton.replace(NO_SSL_WARNING_PLACEHOLDER, "");
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import java.util.concurrent.TimeUnit;
+import java.util.function.BooleanSupplier;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.config.exception.BridgeCreationFailedException;
+import org.openhab.binding.mielecloud.internal.config.exception.BridgeReconfigurationFailedException;
+import org.openhab.binding.mielecloud.internal.handler.MieleBridgeHandler;
+import org.openhab.binding.mielecloud.internal.util.EmailValidator;
+import org.openhab.binding.mielecloud.internal.util.LocaleValidator;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.inbox.Inbox;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingRegistry;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Servlet that automatically creates a bridge and then redirects the browser to the account overview page.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class CreateBridgeServlet extends AbstractRedirectionServlet {
+ private static final String MIELE_CLOUD_BRIDGE_NAME = "Cloud Connector";
+ private static final String MIELE_CLOUD_BRIDGE_LABEL = "Miele@home Account";
+
+ private static final String LOCALE_PARAMETER_NAME = "locale";
+ public static final String BRIDGE_UID_PARAMETER_NAME = "bridgeUid";
+ public static final String EMAIL_PARAMETER_NAME = "email";
+
+ private static final long serialVersionUID = -2912042079128722887L;
+
+ private static final String DEFAULT_LOCALE = "en";
+
+ private static final long ONLINE_WAIT_TIMEOUT_IN_MILLISECONDS = 5000;
+ private static final long DISCOVERY_COMPLETION_TIMEOUT_IN_MILLISECONDS = 5000;
+ private static final long CHECK_INTERVAL_IN_MILLISECONDS = 100;
+
+ private final Logger logger = LoggerFactory.getLogger(CreateBridgeServlet.class);
+
+ private final Inbox inbox;
+ private final ThingRegistry thingRegistry;
+
+ /**
+ * Creates a new {@link CreateBridgeServlet}.
+ *
+ * @param inbox openHAB inbox for discovery results.
+ * @param thingRegistry openHAB thing registry.
+ */
+ public CreateBridgeServlet(Inbox inbox, ThingRegistry thingRegistry) {
+ this.inbox = inbox;
+ this.thingRegistry = thingRegistry;
+ }
+
+ @Override
+ protected String getRedirectionDestination(HttpServletRequest request) {
+ String bridgeUidString = request.getParameter(BRIDGE_UID_PARAMETER_NAME);
+ if (bridgeUidString == null || bridgeUidString.isEmpty()) {
+ logger.warn("Cannot create bridge: Bridge UID is missing.");
+ return "/mielecloud/failure?" + FailureServlet.MISSING_BRIDGE_UID_PARAMETER_NAME + "=true";
+ }
+
+ String email = request.getParameter(EMAIL_PARAMETER_NAME);
+ if (email == null || email.isEmpty()) {
+ logger.warn("Cannot create bridge: E-mail address is missing.");
+ return "/mielecloud/failure?" + FailureServlet.MISSING_EMAIL_PARAMETER_NAME + "=true";
+ }
+
+ ThingUID bridgeUid = null;
+ try {
+ bridgeUid = new ThingUID(bridgeUidString);
+ } catch (IllegalArgumentException e) {
+ logger.warn("Cannot create bridge: Bridge UID '{}' is malformed.", bridgeUid);
+ return "/mielecloud/failure?" + FailureServlet.MALFORMED_BRIDGE_UID_PARAMETER_NAME + "=true";
+ }
+
+ if (!EmailValidator.isValid(email)) {
+ logger.warn("Cannot create bridge: E-mail address '{}' is malformed.", email);
+ return "/mielecloud/failure?" + FailureServlet.MALFORMED_EMAIL_PARAMETER_NAME + "=true";
+ }
+
+ String locale = getValidLocale(request.getParameter(LOCALE_PARAMETER_NAME));
+
+ logger.debug("Auto configuring Miele account using locale '{}' (requested locale was '{}')", locale,
+ request.getParameter(LOCALE_PARAMETER_NAME));
+ try {
+ Thing bridge = pairOrReconfigureBridge(locale, bridgeUid, email);
+ waitForBridgeToComeOnline(bridge);
+ return "/mielecloud";
+ } catch (BridgeReconfigurationFailedException e) {
+ logger.warn("{}", e.getMessage());
+ return "/mielecloud/success?" + SuccessServlet.BRIDGE_RECONFIGURATION_FAILED_PARAMETER_NAME + "=true&"
+ + SuccessServlet.BRIDGE_UID_PARAMETER_NAME + "=" + bridgeUidString + "&"
+ + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + email;
+ } catch (BridgeCreationFailedException e) {
+ logger.warn("Thing creation failed because there was no binding available that supports the thing.");
+ return "/mielecloud/success?" + SuccessServlet.BRIDGE_CREATION_FAILED_PARAMETER_NAME + "=true&"
+ + SuccessServlet.BRIDGE_UID_PARAMETER_NAME + "=" + bridgeUidString + "&"
+ + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + email;
+ }
+ }
+
+ private Thing pairOrReconfigureBridge(String locale, ThingUID bridgeUid, String email) {
+ DiscoveryResult result = DiscoveryResultBuilder.create(bridgeUid)
+ .withRepresentationProperty(Thing.PROPERTY_MODEL_ID).withLabel(MIELE_CLOUD_BRIDGE_LABEL)
+ .withProperty(Thing.PROPERTY_MODEL_ID, MIELE_CLOUD_BRIDGE_NAME)
+ .withProperty(MieleCloudBindingConstants.CONFIG_PARAM_LOCALE, locale)
+ .withProperty(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL, email).build();
+ if (inbox.add(result)) {
+ return pairBridge(bridgeUid);
+ } else {
+ return reconfigureBridge(bridgeUid, locale, email);
+ }
+ }
+
+ private Thing pairBridge(ThingUID thingUid) {
+ Thing thing = inbox.approve(thingUid, MIELE_CLOUD_BRIDGE_LABEL, null);
+ if (thing == null) {
+ throw new BridgeCreationFailedException();
+ }
+
+ logger.debug("Successfully created bridge {}", thingUid);
+ return thing;
+ }
+
+ private Thing reconfigureBridge(ThingUID thingUid, String locale, String email) {
+ logger.debug("Thing already exists. Modifying configuration.");
+ Thing thing = thingRegistry.get(thingUid);
+ if (thing == null) {
+ throw new BridgeReconfigurationFailedException(
+ "Cannot modify non existing bridge: Could neither add bridge via inbox nor find existing bridge.");
+ }
+
+ ThingHandler handler = thing.getHandler();
+ if (handler == null) {
+ throw new BridgeReconfigurationFailedException("Bridge exists but has no handler.");
+ }
+ if (!(handler instanceof MieleBridgeHandler)) {
+ throw new BridgeReconfigurationFailedException("Bridge handler is of wrong type, expected '"
+ + MieleBridgeHandler.class.getSimpleName() + "' but got '" + handler.getClass().getName() + "'.");
+ }
+
+ MieleBridgeHandler bridgeHandler = (MieleBridgeHandler) handler;
+ bridgeHandler.disposeWebservice();
+ bridgeHandler.initializeWebservice();
+
+ return thing;
+ }
+
+ private String getValidLocale(@Nullable String localeParameterValue) {
+ if (localeParameterValue == null || localeParameterValue.isEmpty()
+ || !LocaleValidator.isValidLanguage(localeParameterValue)) {
+ return DEFAULT_LOCALE;
+ } else {
+ return localeParameterValue;
+ }
+ }
+
+ private void waitForBridgeToComeOnline(Thing bridge) {
+ try {
+ waitForConditionWithTimeout(() -> bridge.getStatus() == ThingStatus.ONLINE,
+ ONLINE_WAIT_TIMEOUT_IN_MILLISECONDS);
+ waitForConditionWithTimeout(new DiscoveryResultCountDoesNotChangeCondition(),
+ DISCOVERY_COMPLETION_TIMEOUT_IN_MILLISECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ private void waitForConditionWithTimeout(BooleanSupplier condition, long timeoutInMilliseconds)
+ throws InterruptedException {
+ long remainingWaitTime = timeoutInMilliseconds;
+ while (!condition.getAsBoolean() && remainingWaitTime > 0) {
+ TimeUnit.MILLISECONDS.sleep(CHECK_INTERVAL_IN_MILLISECONDS);
+ remainingWaitTime -= CHECK_INTERVAL_IN_MILLISECONDS;
+ }
+ }
+
+ private class DiscoveryResultCountDoesNotChangeCondition implements BooleanSupplier {
+ private long previousDiscoveryResultCount = 0;
+
+ @Override
+ public boolean getAsBoolean() {
+ var discoveryResultCount = countOwnDiscoveryResults();
+ var discoveryResultCountUnchanged = previousDiscoveryResultCount == discoveryResultCount;
+ previousDiscoveryResultCount = discoveryResultCount;
+ return discoveryResultCountUnchanged;
+ }
+
+ private long countOwnDiscoveryResults() {
+ return inbox.stream().map(DiscoveryResult::getBindingId)
+ .filter(MieleCloudBindingConstants.BINDING_ID::equals).count();
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Servlet showing a failure page.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class FailureServlet extends AbstractShowPageServlet {
+ private static final long serialVersionUID = -5195984256535664942L;
+
+ public static final String OAUTH2_ERROR_PARAMETER_NAME = "oauth2Error";
+ public static final String ILLEGAL_RESPONSE_PARAMETER_NAME = "illegalResponse";
+ public static final String NO_ONGOING_AUTHORIZATION_PARAMETER_NAME = "noOngoingAuthorization";
+ public static final String FAILED_TO_COMPLETE_AUTHORIZATION_PARAMETER_NAME = "failedToCompleteAuthorization";
+ public static final String MISSING_BRIDGE_UID_PARAMETER_NAME = "missingBridgeUid";
+ public static final String MISSING_EMAIL_PARAMETER_NAME = "missingEmail";
+ public static final String MALFORMED_BRIDGE_UID_PARAMETER_NAME = "malformedBridgeUid";
+ public static final String MALFORMED_EMAIL_PARAMETER_NAME = "malformedEmail";
+ public static final String MISSING_REQUEST_URL_PARAMETER_NAME = "missingRequestUrl";
+
+ public static final String OAUTH2_ERROR_ACCESS_DENIED = "access_denied";
+ public static final String OAUTH2_ERROR_INVALID_REQUEST = "invalid_request";
+ public static final String OAUTH2_ERROR_UNAUTHORIZED_CLIENT = "unauthorized_client";
+ public static final String OAUTH2_ERROR_UNSUPPORTED_RESPONSE_TYPE = "unsupported_response_type";
+ public static final String OAUTH2_ERROR_INVALID_SCOPE = "invalid_scope";
+ public static final String OAUTH2_ERROR_SERVER_ERROR = "server_error";
+ public static final String OAUTH2_ERROR_TEMPORARY_UNAVAILABLE = "temporarily_unavailable";
+
+ private static final String ERROR_MESSAGE_TEXT_PLACEHOLDER = "<!-- ERROR MESSAGE TEXT -->";
+
+ /**
+ * Creates a new {@link FailureServlet}.
+ *
+ * @param resourceLoader Loader to use for resources.
+ */
+ public FailureServlet(ResourceLoader resourceLoader) {
+ super(resourceLoader);
+ }
+
+ @Override
+ protected String handleGetRequest(HttpServletRequest request, HttpServletResponse response)
+ throws MieleHttpException, IOException {
+ return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
+ getErrorMessage(request));
+ }
+
+ private String getErrorMessage(HttpServletRequest request) {
+ String oauth2Error = request.getParameter(OAUTH2_ERROR_PARAMETER_NAME);
+ if (oauth2Error != null) {
+ return getOAuth2ErrorMessage(oauth2Error);
+ } else if (ServletUtil.isParameterEnabled(request, ILLEGAL_RESPONSE_PARAMETER_NAME)) {
+ return "Miele cloud service returned an illegal response.";
+ } else if (ServletUtil.isParameterEnabled(request, NO_ONGOING_AUTHORIZATION_PARAMETER_NAME)) {
+ return "There is no ongoing authorization. Please start an authorization first.";
+ } else if (ServletUtil.isParameterEnabled(request, FAILED_TO_COMPLETE_AUTHORIZATION_PARAMETER_NAME)) {
+ return "Completing the final authorization request failed. Please try the config flow again.";
+ } else if (ServletUtil.isParameterEnabled(request, MISSING_BRIDGE_UID_PARAMETER_NAME)) {
+ return "Missing bridge UID.";
+ } else if (ServletUtil.isParameterEnabled(request, MISSING_EMAIL_PARAMETER_NAME)) {
+ return "Missing e-mail address.";
+ } else if (ServletUtil.isParameterEnabled(request, MALFORMED_BRIDGE_UID_PARAMETER_NAME)) {
+ return "Malformed bridge UID.";
+ } else if (ServletUtil.isParameterEnabled(request, MALFORMED_EMAIL_PARAMETER_NAME)) {
+ return "Malformed e-mail address.";
+ } else if (ServletUtil.isParameterEnabled(request, MISSING_REQUEST_URL_PARAMETER_NAME)) {
+ return "Missing request URL. Please try the config flow again.";
+ } else {
+ return "Unknown error.";
+ }
+ }
+
+ private String getOAuth2ErrorMessage(String oauth2Error) {
+ return "OAuth2 authentication with Miele cloud service failed: " + getOAuth2ErrorDetailMessage(oauth2Error);
+ }
+
+ private String getOAuth2ErrorDetailMessage(String oauth2Error) {
+ switch (oauth2Error) {
+ case OAUTH2_ERROR_ACCESS_DENIED:
+ return "Access denied.";
+ case OAUTH2_ERROR_INVALID_REQUEST:
+ return "Malformed request.";
+ case OAUTH2_ERROR_UNAUTHORIZED_CLIENT:
+ return "Account not authorized to request authorization code.";
+ case OAUTH2_ERROR_UNSUPPORTED_RESPONSE_TYPE:
+ return "Obtaining an authorization code is not supported.";
+ case OAUTH2_ERROR_INVALID_SCOPE:
+ return "Invalid scope.";
+ case OAUTH2_ERROR_SERVER_ERROR:
+ return "Unexpected server error.";
+ case OAUTH2_ERROR_TEMPORARY_UNAVAILABLE:
+ return "Authorization server temporarily unavailable.";
+ default:
+ return "Unknown error code \"" + oauth2Error + "\".";
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.auth.OAuthException;
+import org.openhab.binding.mielecloud.internal.config.OAuthAuthorizationHandler;
+import org.openhab.binding.mielecloud.internal.config.exception.NoOngoingAuthorizationException;
+import org.openhab.binding.mielecloud.internal.config.exception.OngoingAuthorizationException;
+import org.openhab.binding.mielecloud.internal.util.EmailValidator;
+import org.openhab.core.thing.ThingUID;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Servlet gathers and processes required information to perform an authorization with the Miele cloud service
+ * and create a bridge afterwards. Required parameters are the client ID, client secret, an ID for the bridge and an
+ * e-mail address. If the given parameters are valid, the browser is redirected to the Miele service login. Otherwise,
+ * the browser is redirected to the previous page with an according error message.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class ForwardToLoginServlet extends AbstractRedirectionServlet {
+ private static final long serialVersionUID = -9094642228439994183L;
+
+ public static final String CLIENT_ID_PARAMETER_NAME = "clientId";
+ public static final String CLIENT_SECRET_PARAMETER_NAME = "clientSecret";
+ public static final String BRIDGE_ID_PARAMETER_NAME = "bridgeId";
+ public static final String EMAIL_PARAMETER_NAME = "email";
+
+ private final Logger logger = LoggerFactory.getLogger(ForwardToLoginServlet.class);
+
+ private final OAuthAuthorizationHandler authorizationHandler;
+
+ /**
+ * Creates a new {@link ForwardToLoginServlet}.
+ *
+ * @param authorizationHandler Handler implementing the OAuth authorization process.
+ */
+ public ForwardToLoginServlet(OAuthAuthorizationHandler authorizationHandler) {
+ this.authorizationHandler = authorizationHandler;
+ }
+
+ @Override
+ protected String getRedirectionDestination(HttpServletRequest request) {
+ String clientId = request.getParameter(CLIENT_ID_PARAMETER_NAME);
+ String clientSecret = request.getParameter(CLIENT_SECRET_PARAMETER_NAME);
+ String bridgeId = request.getParameter(BRIDGE_ID_PARAMETER_NAME);
+ String email = request.getParameter(EMAIL_PARAMETER_NAME);
+
+ if (clientId == null || clientId.isEmpty()) {
+ logger.warn("Request is missing client ID.");
+ return getErrorRedirectionUrl(PairAccountServlet.MISSING_CLIENT_ID_PARAMETER_NAME);
+ }
+ if (clientSecret == null || clientSecret.isEmpty()) {
+ logger.warn("Request is missing client secret.");
+ return getErrorRedirectionUrl(PairAccountServlet.MISSING_CLIENT_SECRET_PARAMETER_NAME);
+ }
+ if (bridgeId == null || bridgeId.isEmpty()) {
+ logger.warn("Request is missing bridge ID.");
+ return getErrorRedirectionUrl(PairAccountServlet.MISSING_BRIDGE_ID_PARAMETER_NAME);
+ }
+ if (email == null || email.isEmpty()) {
+ logger.warn("Request is missing e-mail address.");
+ return getErrorRedirectionUrl(PairAccountServlet.MISSING_EMAIL_PARAMETER_NAME);
+ }
+
+ ThingUID bridgeUid = null;
+ try {
+ bridgeUid = new ThingUID(MieleCloudBindingConstants.THING_TYPE_BRIDGE, bridgeId);
+ } catch (IllegalArgumentException e) {
+ logger.warn("Passed bridge ID '{}' is invalid.", bridgeId);
+ return getErrorRedirectionUrl(PairAccountServlet.MALFORMED_BRIDGE_ID_PARAMETER_NAME);
+ }
+
+ if (!EmailValidator.isValid(email)) {
+ logger.warn("Passed e-mail address '{}' is invalid.", email);
+ return getErrorRedirectionUrl(PairAccountServlet.MALFORMED_EMAIL_PARAMETER_NAME);
+ }
+
+ try {
+ authorizationHandler.beginAuthorization(clientId, clientSecret, bridgeUid, email);
+ } catch (OngoingAuthorizationException e) {
+ logger.warn("Cannot begin new authorization process while another one is still running.");
+ return getErrorRedirectUrlWithExpiryTime(e.getOngoingAuthorizationExpiryTimestamp());
+ }
+
+ StringBuffer requestUrl = request.getRequestURL();
+ if (requestUrl == null) {
+ return getErrorRedirectionUrl(PairAccountServlet.MISSING_REQUEST_URL_PARAMETER_NAME);
+ }
+
+ try {
+ return authorizationHandler.getAuthorizationUrl(deriveRedirectUri(requestUrl.toString()));
+ } catch (NoOngoingAuthorizationException e) {
+ logger.warn(
+ "Failed to create authorization URL: There was no ongoing authorization although we just started one.");
+ return getErrorRedirectionUrl(PairAccountServlet.NO_ONGOING_AUTHORIZATION_IN_STEP2_PARAMETER_NAME);
+ } catch (OAuthException e) {
+ logger.warn("Failed to create authorization URL.", e);
+ return getErrorRedirectionUrl(PairAccountServlet.FAILED_TO_DERIVE_REDIRECT_URL_PARAMETER_NAME);
+ }
+ }
+
+ private String getErrorRedirectUrlWithExpiryTime(@Nullable LocalDateTime ongoingAuthorizationExpiryTimestamp) {
+ if (ongoingAuthorizationExpiryTimestamp == null) {
+ return getErrorRedirectionUrl(
+ PairAccountServlet.ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME,
+ PairAccountServlet.ONGOING_AUTHORIZATION_UNKNOWN_EXPIRY_TIME);
+ }
+
+ long minutesUntilExpiry = ChronoUnit.MINUTES.between(LocalDateTime.now(), ongoingAuthorizationExpiryTimestamp)
+ + 1;
+ return getErrorRedirectionUrl(
+ PairAccountServlet.ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME,
+ Long.toString(minutesUntilExpiry));
+ }
+
+ private String getErrorRedirectionUrl(String errorCode) {
+ return getErrorRedirectionUrl(errorCode, "true");
+ }
+
+ private String getErrorRedirectionUrl(String errorCode, String parameterValue) {
+ return "/mielecloud/pair?" + errorCode + "=" + parameterValue;
+ }
+
+ private String deriveRedirectUri(String requestUrl) {
+ return requestUrl + "/../result";
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Exception wrapping a HTTP error code for further processing.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class MieleHttpException extends Exception {
+ private static final long serialVersionUID = 1825214275413952809L;
+
+ private final int httpErrorCode;
+
+ public MieleHttpException(int httpErrorCode) {
+ this.httpErrorCode = httpErrorCode;
+ }
+
+ public int getHttpErrorCode() {
+ return httpErrorCode;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import java.io.IOException;
+import java.util.Optional;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Servlet showing the pair account page.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class PairAccountServlet extends AbstractShowPageServlet {
+ private static final long serialVersionUID = 6565378471951635420L;
+
+ public static final String CLIENT_ID_PARAMETER_NAME = "clientId";
+ public static final String CLIENT_SECRET_PARAMETER_NAME = "clientSecret";
+
+ public static final String MISSING_CLIENT_ID_PARAMETER_NAME = "missingClientId";
+ public static final String MISSING_CLIENT_SECRET_PARAMETER_NAME = "missingClientSecret";
+ public static final String MISSING_BRIDGE_ID_PARAMETER_NAME = "missingBridgeId";
+ public static final String MISSING_EMAIL_PARAMETER_NAME = "missingEmail";
+ public static final String MALFORMED_BRIDGE_ID_PARAMETER_NAME = "malformedBridgeId";
+ public static final String MALFORMED_EMAIL_PARAMETER_NAME = "malformedEmail";
+ public static final String FAILED_TO_DERIVE_REDIRECT_URL_PARAMETER_NAME = "failedToDeriveRedirectUrl";
+ public static final String ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME = "ongoingAuthorizationInStep1ExpiresInMinutes";
+ public static final String ONGOING_AUTHORIZATION_UNKNOWN_EXPIRY_TIME = "unknown";
+ public static final String NO_ONGOING_AUTHORIZATION_IN_STEP2_PARAMETER_NAME = "noOngoingAuthorizationInStep2";
+ public static final String MISSING_REQUEST_URL_PARAMETER_NAME = "missingRequestUrl";
+
+ private static final String PAIR_ACCOUNT_SKELETON = "pairing.html";
+
+ private static final String CLIENT_ID_PLACEHOLDER = "<!-- CLIENT ID -->";
+ private static final String CLIENT_SECRET_PLACEHOLDER = "<!-- CLIENT SECRET -->";
+ private static final String ERROR_MESSAGE_PLACEHOLDER = "<!-- ERROR MESSAGE -->";
+
+ /**
+ * Creates a new {@link PairAccountServlet}.
+ *
+ * @param resourceLoader Loader for resources.
+ */
+ public PairAccountServlet(ResourceLoader resourceLoader) {
+ super(resourceLoader);
+ }
+
+ @Override
+ protected String handleGetRequest(HttpServletRequest request, HttpServletResponse response)
+ throws MieleHttpException, IOException {
+ String skeleton = getResourceLoader().loadResourceAsString(PAIR_ACCOUNT_SKELETON);
+ skeleton = renderClientIdAndClientSecret(request, skeleton);
+ skeleton = renderErrorMessage(request, skeleton);
+ return skeleton;
+ }
+
+ private String renderClientIdAndClientSecret(HttpServletRequest request, String skeleton) {
+ String prefilledClientId = Optional.ofNullable(request.getParameter(CLIENT_ID_PARAMETER_NAME)).orElse("");
+ String prefilledClientSecret = Optional.ofNullable(request.getParameter(CLIENT_SECRET_PARAMETER_NAME))
+ .orElse("");
+ return skeleton.replace(CLIENT_ID_PLACEHOLDER, prefilledClientId).replace(CLIENT_SECRET_PLACEHOLDER,
+ prefilledClientSecret);
+ }
+
+ private String renderErrorMessage(HttpServletRequest request, String skeleton) {
+ if (ServletUtil.isParameterEnabled(request, MISSING_CLIENT_ID_PARAMETER_NAME)) {
+ return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+ "<div class=\"alert alert-danger\" role=\"alert\">Missing client ID.</div>");
+ } else if (ServletUtil.isParameterEnabled(request, MISSING_CLIENT_SECRET_PARAMETER_NAME)) {
+ return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+
+ "<div class=\"alert alert-danger\" role=\"alert\">Missing client secret.</div>");
+ } else if (ServletUtil.isParameterEnabled(request, MISSING_BRIDGE_ID_PARAMETER_NAME)) {
+ return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+ "<div class=\"alert alert-danger\" role=\"alert\">Missing bridge ID.</div>");
+ } else if (ServletUtil.isParameterEnabled(request, MISSING_EMAIL_PARAMETER_NAME)) {
+ return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+ "<div class=\"alert alert-danger\" role=\"alert\">Missing e-mail address.</div>");
+ } else if (ServletUtil.isParameterEnabled(request, MALFORMED_BRIDGE_ID_PARAMETER_NAME)) {
+ return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+ "<div class=\"alert alert-danger\" role=\"alert\">Malformed bridge ID. A bridge ID may only contain letters, numbers, '-' and '_'!</div>");
+ } else if (ServletUtil.isParameterEnabled(request, MALFORMED_EMAIL_PARAMETER_NAME)) {
+ return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+ "<div class=\"alert alert-danger\" role=\"alert\">Malformed e-mail address.</div>");
+ } else if (ServletUtil.isParameterEnabled(request, FAILED_TO_DERIVE_REDIRECT_URL_PARAMETER_NAME)) {
+ return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+ "<div class=\"alert alert-danger\" role=\"alert\">Failed to derive redirect URL.</div>");
+ } else if (ServletUtil.isParameterPresent(request,
+ ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME)) {
+ String minutesUntilExpiry = request
+ .getParameter(ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME);
+ if (ONGOING_AUTHORIZATION_UNKNOWN_EXPIRY_TIME.equals(minutesUntilExpiry)) {
+ return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+ "<div class=\"alert alert-danger\" role=\"alert\">There is an authorization ongoing at the moment. Please complete that authorization prior to starting a new one or try again later.</div>");
+ } else {
+ return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+ "<div class=\"alert alert-danger\" role=\"alert\">There is an authorization ongoing at the moment. Please complete that authorization prior to starting a new one or try again in "
+ + minutesUntilExpiry + " minutes.</div>");
+ }
+ } else if (ServletUtil.isParameterEnabled(request, NO_ONGOING_AUTHORIZATION_IN_STEP2_PARAMETER_NAME)) {
+ return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+ "<div class=\"alert alert-danger\" role=\"alert\">Failed to start auhtorization process. Are you trying to perform multiple authorizations at the same time?</div>");
+ } else if (ServletUtil.isParameterEnabled(request, MISSING_REQUEST_URL_PARAMETER_NAME)) {
+ return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER,
+ "<div class=\"alert alert-danger\" role=\"alert\">Missing request URL. Please try again.</div>");
+ } else {
+ return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER, "");
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Scanner;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.osgi.framework.BundleContext;
+
+/**
+ * Provides access to resource files for servlets.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class ResourceLoader {
+ private static final String BEGINNING_OF_INPUT = "\\A";
+
+ private final String basePath;
+ private final BundleContext bundleContext;
+
+ /**
+ * Creates a new {@link ResourceLoader}.
+ *
+ * @param basePath The base path to use for loading. A trailing {@code "/"} is removed.
+ * @param bundleContext {@link BundleContext} to load from.
+ */
+ public ResourceLoader(String basePath, BundleContext bundleContext) {
+ this.basePath = removeTrailingSlashes(basePath);
+ this.bundleContext = bundleContext;
+ }
+
+ private String removeTrailingSlashes(String value) {
+ String ret = value;
+ while (ret.endsWith("/")) {
+ ret = ret.substring(0, ret.length() - 1);
+ }
+ return ret;
+ }
+
+ /**
+ * Opens a resource relative to the base path.
+ *
+ * @param filename The filename of the resource to load.
+ * @return A stream reading from the resource file.
+ * @throws FileNotFoundException If the requested resource file cannot be found.
+ * @throws IOException If an error occurs while opening a stream to the resource.
+ */
+ public InputStream openResource(String filename) throws IOException {
+ URL url = bundleContext.getBundle().getEntry(basePath + "/" + filename);
+ if (url == null) {
+ throw new FileNotFoundException("Cannot find '" + filename + "' relative to '" + basePath + "'");
+ }
+
+ return url.openStream();
+ }
+
+ /**
+ * Loads the contents of a resource file as UTF-8 encoded {@link String}.
+ *
+ * @param filename The filename of the resource to load.
+ * @return The contents of the file.
+ * @throws FileNotFoundException If the requested resource file cannot be found.
+ * @throws IOException If an error occurs while opening a stream to the resource or reading from it.
+ */
+ public String loadResourceAsString(String filename) throws IOException {
+ try (Scanner scanner = new Scanner(openResource(filename), StandardCharsets.UTF_8.name())) {
+ return scanner.useDelimiter(BEGINNING_OF_INPUT).next();
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.auth.OAuthException;
+import org.openhab.binding.mielecloud.internal.config.OAuthAuthorizationHandler;
+import org.openhab.binding.mielecloud.internal.config.exception.NoOngoingAuthorizationException;
+import org.openhab.core.thing.ThingUID;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Servlet processing the response by the Miele service after a login. This servlet is called as a result of a
+ * completed login to the Miele service and assumes that the OAuth 2 parameters are passed. Depending on the parameters
+ * and whether the token response can be fetched either the browser is redirected to the success or the failure page.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class ResultServlet extends AbstractRedirectionServlet {
+ private static final long serialVersionUID = 2157912755568949550L;
+
+ public static final String CODE_PARAMETER_NAME = "code";
+ public static final String STATE_PARAMETER_NAME = "state";
+ public static final String ERROR_PARAMETER_NAME = "error";
+
+ private final Logger logger = LoggerFactory.getLogger(ResultServlet.class);
+
+ private final OAuthAuthorizationHandler authorizationHandler;
+
+ /**
+ * Creates a new {@link ResultServlet}.
+ *
+ * @param authorizationHandler Handler implementing the OAuth authorization.
+ */
+ public ResultServlet(OAuthAuthorizationHandler authorizationHandler) {
+ this.authorizationHandler = authorizationHandler;
+ }
+
+ @Override
+ protected String getRedirectionDestination(HttpServletRequest request) {
+ String error = request.getParameter(ERROR_PARAMETER_NAME);
+ if (error != null) {
+ logger.warn("Received error response: {}", error);
+ return "/mielecloud/failure?" + FailureServlet.OAUTH2_ERROR_PARAMETER_NAME + "=" + error;
+ }
+
+ String code = request.getParameter(CODE_PARAMETER_NAME);
+ if (code == null) {
+ logger.warn("Code is null");
+ return "/mielecloud/failure?" + FailureServlet.ILLEGAL_RESPONSE_PARAMETER_NAME + "=true";
+ }
+ String state = request.getParameter(STATE_PARAMETER_NAME);
+ if (state == null) {
+ logger.warn("State is null");
+ return "/mielecloud/failure?" + FailureServlet.ILLEGAL_RESPONSE_PARAMETER_NAME + "=true";
+ }
+
+ try {
+ ThingUID bridgeId = authorizationHandler.getBridgeUid();
+ String email = authorizationHandler.getEmail();
+
+ StringBuffer requestUrl = request.getRequestURL();
+ if (requestUrl == null) {
+ return "/mielecloud/failure?" + FailureServlet.MISSING_REQUEST_URL_PARAMETER_NAME + "=true";
+ }
+
+ try {
+ authorizationHandler.completeAuthorization(requestUrl.toString() + "?" + request.getQueryString());
+ } catch (OAuthException e) {
+ logger.warn("Failed to complete authorization.", e);
+ return "/mielecloud/failure?" + FailureServlet.FAILED_TO_COMPLETE_AUTHORIZATION_PARAMETER_NAME
+ + "=true";
+ }
+
+ return "/mielecloud/success?" + SuccessServlet.BRIDGE_UID_PARAMETER_NAME + "=" + bridgeId.getAsString()
+ + "&" + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + email;
+ } catch (NoOngoingAuthorizationException e) {
+ logger.warn("Failed to complete authorization: There is no ongoing authorization or it timed out");
+ return "/mielecloud/failure?" + FailureServlet.NO_ONGOING_AUTHORIZATION_PARAMETER_NAME + "=true";
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Utility class for common servlet tasks.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class ServletUtil {
+ private ServletUtil() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Gets the value of a request parameter or returns a default if the parameter is not present.
+ */
+ public static String getParameterValueOrDefault(HttpServletRequest request, String parameterName,
+ String defaultValue) {
+ String parameterValue = request.getParameter(parameterName);
+ if (parameterValue == null) {
+ return defaultValue;
+ } else {
+ return parameterValue;
+ }
+ }
+
+ /**
+ * Checks whether a request parameter is enabled.
+ */
+ public static boolean isParameterEnabled(HttpServletRequest request, String parameterName) {
+ return "true".equalsIgnoreCase(getParameterValueOrDefault(request, parameterName, "false"));
+ }
+
+ /**
+ * Checks whether a parameter is present in a request.
+ */
+ public static boolean isParameterPresent(HttpServletRequest request, String parameterName) {
+ String parameterValue = request.getParameter(parameterName);
+ return parameterValue != null && !parameterValue.trim().isEmpty();
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import java.io.IOException;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.config.ThingsTemplateGenerator;
+import org.openhab.binding.mielecloud.internal.util.EmailValidator;
+import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider;
+import org.openhab.core.thing.ThingUID;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Servlet showing the success page.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class SuccessServlet extends AbstractShowPageServlet {
+ private static final long serialVersionUID = 7013060161686096950L;
+
+ public static final String BRIDGE_UID_PARAMETER_NAME = "bridgeUid";
+ public static final String EMAIL_PARAMETER_NAME = "email";
+
+ public static final String BRIDGE_CREATION_FAILED_PARAMETER_NAME = "bridgeCreationFailed";
+ public static final String BRIDGE_RECONFIGURATION_FAILED_PARAMETER_NAME = "bridgeReconfigurationFailed";
+
+ private static final String ERROR_MESSAGE_TEXT_PLACEHOLDER = "<!-- ERROR MESSAGE TEXT -->";
+ private static final String BRIDGE_UID_PLACEHOLDER = "<!-- BRIDGE UID -->";
+ private static final String EMAIL_PLACEHOLDER = "<!-- EMAIL -->";
+ private static final String THINGS_TEMPLATE_CODE_PLACEHOLDER = "<!-- THINGS TEMPLATE CODE -->";
+
+ private static final String LOCALE_OPTIONS_PLACEHOLDER = "<!-- LOCALE OPTIONS -->";
+
+ private static final String DEFAULT_LANGUAGE = "en";
+ private static final Set<String> SUPPORTED_LANGUAGES = Set.of("da", "nl", "en", "fr", "de", "it", "nb", "es");
+
+ private final Logger logger = LoggerFactory.getLogger(SuccessServlet.class);
+
+ private final LanguageProvider languageProvider;
+ private final ThingsTemplateGenerator templateGenerator;
+
+ /**
+ * Creates a new {@link SuccessServlet}.
+ *
+ * @param resourceLoader Loader for resources.
+ * @param languageProvider Provider for the language to use as default selection.
+ */
+ public SuccessServlet(ResourceLoader resourceLoader, LanguageProvider languageProvider) {
+ super(resourceLoader);
+ this.languageProvider = languageProvider;
+ this.templateGenerator = new ThingsTemplateGenerator();
+ }
+
+ @Override
+ protected String handleGetRequest(HttpServletRequest request, HttpServletResponse response)
+ throws MieleHttpException, IOException {
+ String bridgeUidString = request.getParameter(BRIDGE_UID_PARAMETER_NAME);
+ if (bridgeUidString == null || bridgeUidString.isEmpty()) {
+ logger.warn("Success page is missing bridge UID.");
+ return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
+ "Missing bridge UID.");
+ }
+
+ String email = request.getParameter(EMAIL_PARAMETER_NAME);
+ if (email == null || email.isEmpty()) {
+ logger.warn("Success page is missing e-mail address.");
+ return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
+ "Missing e-mail address.");
+ }
+
+ ThingUID bridgeUid = null;
+ try {
+ bridgeUid = new ThingUID(bridgeUidString);
+ } catch (IllegalArgumentException e) {
+ logger.warn("Success page received malformed bridge UID '{}'.", bridgeUidString);
+ return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
+ "Malformed bridge UID.");
+ }
+
+ if (!EmailValidator.isValid(email)) {
+ logger.warn("Success page received malformed e-mail address '{}'.", email);
+ return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
+ "Malformed e-mail address.");
+ }
+
+ String skeleton = getResourceLoader().loadResourceAsString("success.html");
+ skeleton = renderErrorMessage(request, skeleton);
+ skeleton = renderBridgeUid(skeleton, bridgeUid);
+ skeleton = renderEmail(skeleton, email);
+ skeleton = renderLocaleSelection(skeleton);
+ skeleton = renderBridgeConfigurationTemplate(skeleton, bridgeUid, email);
+ return skeleton;
+ }
+
+ private String renderErrorMessage(HttpServletRequest request, String skeleton) {
+ if (ServletUtil.isParameterEnabled(request, BRIDGE_CREATION_FAILED_PARAMETER_NAME)) {
+ return skeleton.replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
+ "<div class=\"alert alert-danger\" role=\"alert\">Could not auto configure the bridge. Failed to approve the bridge from the inbox. Please try the configuration flow again.</div>");
+ } else if (ServletUtil.isParameterEnabled(request, BRIDGE_RECONFIGURATION_FAILED_PARAMETER_NAME)) {
+ return skeleton.replace(ERROR_MESSAGE_TEXT_PLACEHOLDER,
+ "<div class=\"alert alert-danger\" role=\"alert\">Could not auto reconfigure the bridge. Bridge thing or thing handler is not available. Please try the configuration flow again.</div>");
+ } else {
+ return skeleton.replace(ERROR_MESSAGE_TEXT_PLACEHOLDER, "");
+ }
+ }
+
+ private String renderBridgeUid(String skeleton, ThingUID bridgeUid) {
+ return skeleton.replace(BRIDGE_UID_PLACEHOLDER, bridgeUid.getAsString());
+ }
+
+ private String renderEmail(String skeleton, String email) {
+ return skeleton.replace(EMAIL_PLACEHOLDER, email);
+ }
+
+ private String renderLocaleSelection(String skeleton) {
+ String preSelectedLanguage = languageProvider.getLanguage().filter(SUPPORTED_LANGUAGES::contains)
+ .orElse(DEFAULT_LANGUAGE);
+
+ return skeleton.replace(LOCALE_OPTIONS_PLACEHOLDER,
+ SUPPORTED_LANGUAGES.stream().map(Language::fromCode).filter(Optional::isPresent).map(Optional::get)
+ .sorted()
+ .map(language -> createOptionTag(language, preSelectedLanguage.equals(language.getCode())))
+ .collect(Collectors.joining("\n")));
+ }
+
+ private String createOptionTag(Language language, boolean selected) {
+ String firstPart = " <option value=\"" + language.getCode() + "\"";
+ String secondPart = ">" + language.format() + "</option>";
+ if (selected) {
+ return firstPart + " selected=\"selected\"" + secondPart;
+ } else {
+ return firstPart + secondPart;
+ }
+ }
+
+ private String renderBridgeConfigurationTemplate(String skeleton, ThingUID bridgeUid, String email) {
+ String bridgeTemplate = templateGenerator.createBridgeConfigurationTemplate(bridgeUid.getId(), email,
+ languageProvider.getLanguage().orElse("en"));
+ return skeleton.replace(THINGS_TEMPLATE_CODE_PLACEHOLDER, bridgeTemplate);
+ }
+
+ /**
+ * A language representation for user display.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+ private static final class Language implements Comparable<Language> {
+ private final String code;
+ private final String name;
+
+ private Language(String code, String name) {
+ this.code = code;
+ this.name = name;
+ }
+
+ /**
+ * Gets the 2-letter language code for accessing the Miele Cloud service.
+ */
+ public String getCode() {
+ return code;
+ }
+
+ /**
+ * Formats the language for displaying.
+ */
+ public String format() {
+ return name + " - " + code;
+ }
+
+ @Override
+ public int compareTo(Language other) {
+ return name.toUpperCase().compareTo(other.name.toUpperCase());
+ }
+
+ /**
+ * Constructs a {@link Language} from a 2-letter language code.
+ *
+ * @param code 2-letter language code.
+ * @return An {@link Optional} wrapping the {@link Language} or an empty {@link Optional} if there is no
+ * representation for the given language code.
+ */
+ public static Optional<Language> fromCode(String code) {
+ Locale locale = new Locale(code);
+ String name = locale.getDisplayLanguage(locale);
+ if (name.isEmpty()) {
+ return Optional.empty();
+ } else {
+ return Optional.of(new Language(code, name));
+ }
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.discovery;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.*;
+import static org.openhab.binding.mielecloud.internal.handler.MieleHandlerFactory.SUPPORTED_THING_TYPES;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.handler.MieleBridgeHandler;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Discovery service for things linked to a Miele cloud account.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Do not directly listen to webservice events
+ */
+@NonNullByDefault
+public class ThingDiscoveryService extends AbstractDiscoveryService implements DiscoveryService, ThingHandlerService {
+ private static final int BACKGROUND_DISCOVERY_TIMEOUT_IN_SECONDS = 5;
+
+ @Nullable
+ private MieleBridgeHandler bridgeHandler;
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ private boolean discoveringDevices = false;
+
+ /**
+ * Creates a new {@link ThingDiscoveryService}.
+ */
+ public ThingDiscoveryService() {
+ super(SUPPORTED_THING_TYPES, BACKGROUND_DISCOVERY_TIMEOUT_IN_SECONDS);
+ }
+
+ @Nullable
+ private ThingUID getBridgeUid() {
+ var bridgeHandler = this.bridgeHandler;
+ if (bridgeHandler == null) {
+ return null;
+ } else {
+ return bridgeHandler.getThing().getUID();
+ }
+ }
+
+ @Override
+ protected void startScan() {
+ }
+
+ @Override
+ public void activate() {
+ startBackgroundDiscovery();
+ }
+
+ @Override
+ public void deactivate() {
+ stopBackgroundDiscovery();
+ removeOlderResults(System.currentTimeMillis(), getBridgeUid());
+ }
+
+ /**
+ * Invoked when a device state update is received from the Miele cloud.
+ */
+ public void onDeviceStateUpdated(DeviceState deviceState) {
+ if (!discoveringDevices) {
+ return;
+ }
+
+ Optional<ThingTypeUID> thingTypeUid = getThingTypeUID(deviceState);
+ if (thingTypeUid.isPresent()) {
+ createDiscoveryResult(deviceState, thingTypeUid.get());
+ } else {
+ logger.debug("Unsupported Miele device type: {}", deviceState.getType().orElse("<Empty>"));
+ }
+ }
+
+ private void createDiscoveryResult(DeviceState deviceState, ThingTypeUID thingTypeUid) {
+ MieleBridgeHandler bridgeHandler = this.bridgeHandler;
+ if (bridgeHandler == null) {
+ return;
+ }
+
+ ThingUID thingUid = new ThingUID(thingTypeUid, bridgeHandler.getThing().getUID(),
+ deviceState.getDeviceIdentifier());
+
+ DiscoveryResultBuilder discoveryResultBuilder = DiscoveryResultBuilder.create(thingUid)
+ .withBridge(bridgeHandler.getThing().getUID()).withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER)
+ .withLabel(getLabel(deviceState));
+
+ ThingInformationExtractor.extractProperties(thingTypeUid, deviceState).entrySet()
+ .forEach(entry -> discoveryResultBuilder.withProperty(entry.getKey(), entry.getValue()));
+
+ DiscoveryResult result = discoveryResultBuilder.build();
+
+ thingDiscovered(result);
+ }
+
+ private Optional<ThingTypeUID> getThingTypeUID(DeviceState deviceState) {
+ switch (deviceState.getRawType()) {
+ case COFFEE_SYSTEM:
+ return Optional.of(THING_TYPE_COFFEE_SYSTEM);
+ case TUMBLE_DRYER:
+ return Optional.of(THING_TYPE_DRYER);
+ case WASHING_MACHINE:
+ return Optional.of(THING_TYPE_WASHING_MACHINE);
+ case WASHER_DRYER:
+ return Optional.of(THING_TYPE_WASHER_DRYER);
+ case FREEZER:
+ return Optional.of(THING_TYPE_FREEZER);
+ case FRIDGE:
+ return Optional.of(THING_TYPE_FRIDGE);
+ case FRIDGE_FREEZER_COMBINATION:
+ return Optional.of(THING_TYPE_FRIDGE_FREEZER);
+ case HOB_INDUCTION:
+ case HOB_HIGHLIGHT:
+ return Optional.of(THING_TYPE_HOB);
+ case DISHWASHER:
+ return Optional.of(THING_TYPE_DISHWASHER);
+ case OVEN:
+ case OVEN_MICROWAVE:
+ case STEAM_OVEN:
+ case STEAM_OVEN_COMBINATION:
+ case STEAM_OVEN_MICROWAVE_COMBINATION:
+ case DIALOGOVEN:
+ return Optional.of(THING_TYPE_OVEN);
+ case WINE_CABINET:
+ case WINE_STORAGE_CONDITIONING_UNIT:
+ case WINE_CONDITIONING_UNIT:
+ case WINE_CABINET_FREEZER_COMBINATION:
+ return Optional.of(THING_TYPE_WINE_STORAGE);
+ case HOOD:
+ return Optional.of(THING_TYPE_HOOD);
+ case DISH_WARMER:
+ return Optional.of(THING_TYPE_DISH_WARMER);
+ case VACUUM_CLEANER:
+ return Optional.of(THING_TYPE_ROBOTIC_VACUUM_CLEANER);
+
+ default:
+ if (deviceState.getRawType() != DeviceType.UNKNOWN) {
+ logger.warn("Found no matching thing type for device type {}", deviceState.getRawType());
+ }
+ return Optional.empty();
+ }
+ }
+
+ @Override
+ protected void startBackgroundDiscovery() {
+ logger.debug("Starting background discovery");
+
+ removeOlderResults(System.currentTimeMillis(), getBridgeUid());
+ discoveringDevices = true;
+ }
+
+ @Override
+ protected void stopBackgroundDiscovery() {
+ logger.debug("Stopping background discovery");
+ discoveringDevices = false;
+ }
+
+ /**
+ * Invoked when a device is removed from the Miele cloud.
+ */
+ public void onDeviceRemoved(String deviceIdentifier) {
+ removeOlderResults(System.currentTimeMillis(), getBridgeUid());
+ }
+
+ private String getLabel(DeviceState deviceState) {
+ Optional<String> deviceName = deviceState.getDeviceName();
+ if (deviceName.isPresent()) {
+ return deviceName.get();
+ }
+
+ return ThingInformationExtractor.getDeviceAndTechType(deviceState).orElse("Miele Device");
+ }
+
+ @Override
+ public void setThingHandler(ThingHandler handler) {
+ if (handler instanceof MieleBridgeHandler) {
+ var bridgeHandler = (MieleBridgeHandler) handler;
+ bridgeHandler.setDiscoveryService(this);
+ this.bridgeHandler = bridgeHandler;
+ }
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return bridgeHandler;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.discovery;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * Helper class extracting information related to things from {@link DeviceState}s received from the Miele cloud.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public final class ThingInformationExtractor {
+ private ThingInformationExtractor() {
+ throw new IllegalStateException(getClass().getName() + " cannot be instantiated");
+ }
+
+ /**
+ * Extracts thing properties from a {@link DeviceState}.
+ *
+ * The returned properties always contain {@link Thing#PROPERTY_SERIAL_NUMBER} and {@link Thing#PROPERTY_MODEL_ID}.
+ * More might be present depending on the type of device.
+ *
+ * @param thingTypeUid {@link ThingTypeUID} of the thing to extract properties for.
+ * @param deviceState {@link DeviceState} received from the Miele cloud.
+ * @return A {@link Map} holding the properties as key-value pairs.
+ */
+ public static Map<String, String> extractProperties(ThingTypeUID thingTypeUid, DeviceState deviceState) {
+ var propertyMap = new HashMap<String, String>();
+ propertyMap.put(Thing.PROPERTY_SERIAL_NUMBER, getSerialNumber(deviceState));
+ propertyMap.put(Thing.PROPERTY_MODEL_ID, getModelId(deviceState));
+ propertyMap.put(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER, deviceState.getDeviceIdentifier());
+
+ if (MieleCloudBindingConstants.THING_TYPE_HOB.equals(thingTypeUid)) {
+ deviceState.getPlateStepCount().ifPresent(plateCount -> propertyMap
+ .put(MieleCloudBindingConstants.PROPERTY_PLATE_COUNT, plateCount.toString()));
+ }
+
+ return propertyMap;
+ }
+
+ private static String getSerialNumber(DeviceState deviceState) {
+ return deviceState.getFabNumber().orElse(deviceState.getDeviceIdentifier());
+ }
+
+ private static String getModelId(DeviceState deviceState) {
+ return getDeviceAndTechType(deviceState).orElse("Unknown");
+ }
+
+ /**
+ * Formats device type and tech type from the given {@link DeviceState} for the purpose of displaying then to the
+ * user.
+ *
+ * If either of device or tech type is missing then it will be omitted. If both are missing then an empty
+ * {@link Optional} will be returned.
+ *
+ * @param deviceState {@link DeviceState} obtained from the Miele cloud.
+ * @return An {@link Optional} holding the formatted value or an empty {@link Optional} if neither device type nor
+ * tech type were present.
+ */
+ static Optional<String> getDeviceAndTechType(DeviceState deviceState) {
+ Optional<String> deviceType = deviceState.getType();
+ Optional<String> techType = deviceState.getTechType();
+ if (deviceType.isPresent() && techType.isPresent()) {
+ return Optional.of(deviceType.get() + " " + techType.get());
+ }
+ if (!deviceType.isPresent() && techType.isPresent()) {
+ return techType;
+ }
+ if (deviceType.isPresent() && !techType.isPresent()) {
+ return deviceType;
+ }
+ return Optional.empty();
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+import static org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus.*;
+import static org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus.*;
+import static org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction.*;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.I18NKeys;
+import org.openhab.binding.mielecloud.internal.discovery.ThingInformationExtractor;
+import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
+import org.openhab.binding.mielecloud.internal.webservice.ActionStateFetcher;
+import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
+import org.openhab.binding.mielecloud.internal.webservice.UnavailableMieleWebservice;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.TransitionState;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.BridgeHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Abstract base class for all Miele thing handlers.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Add channel state wrappers
+ */
+@NonNullByDefault
+public abstract class AbstractMieleThingHandler extends BaseThingHandler {
+ protected final ActionStateFetcher actionFetcher;
+ protected DeviceState latestDeviceState = new DeviceState(getDeviceId(), null);
+ protected TransitionState latestTransitionState = new TransitionState(null, latestDeviceState);
+ protected ActionsState latestActionsState = new ActionsState(getDeviceId(), null);
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ /**
+ * Creates a new {@link AbstractMieleThingHandler}.
+ *
+ * @param thing The thing to handle.
+ */
+ public AbstractMieleThingHandler(Thing thing) {
+ super(thing);
+ this.actionFetcher = new ActionStateFetcher(this::getWebservice, scheduler);
+ }
+
+ private Optional<MieleBridgeHandler> getMieleBridgeHandler() {
+ Bridge bridge = getBridge();
+ if (bridge == null) {
+ return Optional.empty();
+ }
+
+ BridgeHandler handler = bridge.getHandler();
+ if (handler == null || !(handler instanceof MieleBridgeHandler)) {
+ return Optional.empty();
+ }
+
+ return Optional.of((MieleBridgeHandler) handler);
+ }
+
+ protected MieleWebservice getWebservice() {
+ return getMieleBridgeHandler().map(MieleBridgeHandler::getWebservice)
+ .orElse(UnavailableMieleWebservice.INSTANCE);
+ }
+
+ @Override
+ public void initialize() {
+ getWebservice().dispatchDeviceState(getDeviceId());
+
+ // If no device state update was received so far, set the device to OFFLINE.
+ if (getThing().getStatus() == ThingStatus.INITIALIZING) {
+ updateStatus(ThingStatus.OFFLINE);
+ }
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (RefreshType.REFRESH.equals(command)) {
+ updateDeviceState(new DeviceChannelState(latestDeviceState));
+ updateTransitionState(new TransitionChannelState(latestTransitionState));
+ updateActionState(new ActionsChannelState(latestActionsState));
+ }
+
+ switch (channelUID.getId()) {
+ case PROGRAM_START_STOP:
+ if (PROGRAM_STARTED.matches(command.toString())) {
+ triggerProcessAction(START);
+ } else if (PROGRAM_STOPPED.matches(command.toString())) {
+ triggerProcessAction(STOP);
+ }
+ break;
+
+ case PROGRAM_START_STOP_PAUSE:
+ if (PROGRAM_STARTED.matches(command.toString())) {
+ triggerProcessAction(START);
+ } else if (PROGRAM_STOPPED.matches(command.toString())) {
+ triggerProcessAction(STOP);
+ } else if (PROGRAM_PAUSED.matches(command.toString())) {
+ triggerProcessAction(PAUSE);
+ }
+ break;
+
+ case LIGHT_SWITCH:
+ if (command instanceof OnOffType) {
+ triggerLight(OnOffType.ON.equals(command));
+ }
+ break;
+
+ case POWER_ON_OFF:
+ if (POWER_ON.matches(command.toString()) || POWER_OFF.matches(command.toString())) {
+ triggerPowerState(OnOffType.ON.equals(OnOffType.from(command.toString())));
+ }
+ break;
+ }
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ /**
+ * Invoked when an update of the available actions for the device managed by this handler is received from the Miele
+ * cloud.
+ */
+ public final void onProcessActionUpdated(ActionsState actionState) {
+ latestActionsState = actionState;
+ updateActionState(new ActionsChannelState(latestActionsState));
+ }
+
+ /**
+ * Invoked when the device managed by this handler was removed from the Miele cloud.
+ */
+ public final void onDeviceRemoved() {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, I18NKeys.THING_STATUS_DESCRIPTION_REMOVED);
+ }
+
+ /**
+ * Invoked when a device state update for the device managed by this handler is received from the Miele cloud.
+ */
+ public final void onDeviceStateUpdated(DeviceState deviceState) {
+ actionFetcher.onDeviceStateUpdated(deviceState);
+
+ latestTransitionState = new TransitionState(latestTransitionState, deviceState);
+ latestDeviceState = deviceState;
+
+ updateThingProperties(deviceState);
+ updateDeviceState(new DeviceChannelState(latestDeviceState));
+ updateTransitionState(new TransitionChannelState(latestTransitionState));
+ updateThingStatus(latestDeviceState);
+ }
+
+ protected void triggerProcessAction(final ProcessAction processAction) {
+ performPutAction(() -> getWebservice().putProcessAction(getDeviceId(), processAction),
+ t -> logger.warn("Failed to perform '{}' operation for device '{}'.", processAction, getDeviceId(), t));
+ }
+
+ protected void triggerLight(final boolean on) {
+ performPutAction(() -> getWebservice().putLight(getDeviceId(), on),
+ t -> logger.warn("Failed to set light state to '{}' for device '{}'.", on, getDeviceId(), t));
+ }
+
+ protected void triggerPowerState(final boolean on) {
+ performPutAction(() -> getWebservice().putPowerState(getDeviceId(), on),
+ t -> logger.warn("Failed to set the power state to '{}' for device '{}'.", on, getDeviceId(), t));
+ }
+
+ protected void triggerProgram(final long programId) {
+ performPutAction(() -> getWebservice().putProgram(getDeviceId(), programId), t -> logger
+ .warn("Failed to activate program with ID '{}' for device '{}'.", programId, getDeviceId(), t));
+ }
+
+ private void performPutAction(Runnable action, Consumer<Exception> onError) {
+ scheduler.execute(() -> {
+ try {
+ action.run();
+ } catch (TooManyRequestsException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ I18NKeys.THING_STATUS_DESCRIPTION_RATELIMIT);
+ onError.accept(e);
+ } catch (Exception e) {
+ onError.accept(e);
+ }
+ });
+ }
+
+ protected final String getDeviceId() {
+ return getConfig().get(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER).toString();
+ }
+
+ /**
+ * Creates a {@link ChannelUID} from the given name.
+ *
+ * @param name channel name
+ * @return {@link ChannelUID}
+ */
+ protected ChannelUID channel(String name) {
+ return new ChannelUID(getThing().getUID(), name);
+ }
+
+ /**
+ * Updates the thing status depending on whether the managed device is connected and reachable.
+ */
+ private void updateThingStatus(DeviceState deviceState) {
+ if (deviceState.isInState(StateType.NOT_CONNECTED)) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ I18NKeys.THING_STATUS_DESCRIPTION_DISCONNECTED);
+ } else {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ }
+
+ /**
+ * Determines the status of the currently selected program.
+ */
+ protected ProgramStatus getProgramStatus(StateType rawStatus) {
+ if (rawStatus.equals(StateType.RUNNING)) {
+ return PROGRAM_STARTED;
+ }
+ return PROGRAM_STOPPED;
+ }
+
+ /**
+ * Determines the power status of the managed device.
+ */
+ protected PowerStatus getPowerStatus(StateType rawStatus) {
+ if (rawStatus.equals(StateType.OFF) || rawStatus.equals(StateType.NOT_CONNECTED)) {
+ return POWER_OFF;
+ }
+ return POWER_ON;
+ }
+
+ /**
+ * Updates the thing properties. This is necessary if properties have not been set during discovery.
+ */
+ private void updateThingProperties(DeviceState deviceState) {
+ var properties = editProperties();
+ properties.putAll(ThingInformationExtractor.extractProperties(getThing().getThingTypeUID(), deviceState));
+ updateProperties(properties);
+ }
+
+ /**
+ * Updates the device state channels.
+ *
+ * @param device The {@link DeviceChannelState} information to update the device channel states with.
+ */
+ protected abstract void updateDeviceState(DeviceChannelState device);
+
+ /**
+ * Updates the transition state channels.
+ *
+ * @param transition The {@link TransitionChannelState} information to update the transition channel states with.
+ */
+ protected abstract void updateTransitionState(TransitionChannelState transition);
+
+ /**
+ * Updates the device action state channels.
+ *
+ * @param action The {@link ActionsChannelState} information to update the action channel states with.
+ */
+ protected abstract void updateActionState(ActionsChannelState actions);
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.Thing;
+
+/**
+ * ThingHandler implementation for the Miele coffee devices.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Switch from polling to SSE, add channel state wrappers
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API
+ * @author Björn Lange - Add elapsed time channel
+ */
+@NonNullByDefault
+public class CoffeeSystemThingHandler extends AbstractMieleThingHandler {
+ /**
+ * Creates a new {@link CoffeeSystemThingHandler}.
+ *
+ * @param thing The thing to handle.
+ */
+ public CoffeeSystemThingHandler(Thing thing) {
+ super(thing);
+
+ updateState(channel(REMOTE_CONTROL_CAN_BE_STARTED), OnOffType.OFF);
+ updateState(channel(REMOTE_CONTROL_CAN_BE_STOPPED), OnOffType.OFF);
+ }
+
+ @Override
+ protected void updateDeviceState(DeviceChannelState device) {
+ updateState(channel(PROGRAM_ACTIVE), device.getProgramActive());
+ updateState(channel(PROGRAM_ACTIVE_RAW), device.getProgramActiveRaw());
+ updateState(channel(PROGRAM_PHASE), device.getProgramPhase());
+ updateState(channel(PROGRAM_PHASE_RAW), device.getProgramPhaseRaw());
+ updateState(channel(OPERATION_STATE), device.getOperationState());
+ updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
+ updateState(channel(PROGRAM_ELAPSED_TIME), device.getProgramElapsedTime());
+ updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
+ updateState(channel(ERROR_STATE), device.getErrorState());
+ updateState(channel(INFO_STATE), device.getInfoState());
+ updateState(channel(LIGHT_SWITCH), device.getLightSwitch());
+ }
+
+ @Override
+ protected void updateTransitionState(TransitionChannelState transition) {
+ updateState(channel(PROGRAM_REMAINING_TIME), transition.getProgramRemainingTime());
+ if (transition.hasFinishedChanged()) {
+ updateState(channel(FINISH_STATE), transition.getFinishState());
+ }
+ }
+
+ @Override
+ protected void updateActionState(ActionsChannelState actions) {
+ updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_ON), actions.getRemoteControlCanBeSwitchedOn());
+ updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF), actions.getRemoteControlCanBeSwitchedOff());
+ updateState(channel(LIGHT_CAN_BE_CONTROLLED), actions.getLightCanBeControlled());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+
+/**
+ * ThingHandler implementation for the Miele cooling devices.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Add channel state wrappers
+ * @author Benjamin Bolte - Add door state and door alarm, add info state channel and map signal flags from API
+ */
+@NonNullByDefault
+public class CoolingDeviceThingHandler extends AbstractMieleThingHandler {
+ /**
+ * Creates a new {@link CoolingDeviceThingHandler}.
+ *
+ * @param thing The thing to handle.
+ */
+ public CoolingDeviceThingHandler(Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ super.handleCommand(channelUID, command);
+
+ if (!OnOffType.ON.equals(command) && !OnOffType.OFF.equals(command)) {
+ return;
+ }
+
+ switch (channelUID.getId()) {
+ case FRIDGE_SUPER_COOL:
+ triggerProcessAction(OnOffType.ON.equals(command) ? ProcessAction.START_SUPERCOOLING
+ : ProcessAction.STOP_SUPERCOOLING);
+ break;
+
+ case FREEZER_SUPER_FREEZE:
+ triggerProcessAction(OnOffType.ON.equals(command) ? ProcessAction.START_SUPERFREEZING
+ : ProcessAction.STOP_SUPERFREEZING);
+ break;
+ }
+ }
+
+ @Override
+ protected void updateDeviceState(DeviceChannelState device) {
+ updateState(channel(OPERATION_STATE), device.getOperationState());
+ updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
+ updateState(channel(FRIDGE_SUPER_COOL), device.getFridgeSuperCool());
+ updateState(channel(FREEZER_SUPER_FREEZE), device.getFreezerSuperFreeze());
+ updateState(channel(FRIDGE_TEMPERATURE_TARGET), device.getFridgeTemperatureTarget());
+ updateState(channel(FREEZER_TEMPERATURE_TARGET), device.getFreezerTemperatureTarget());
+ updateState(channel(FRIDGE_TEMPERATURE_CURRENT), device.getFridgeTemperatureCurrent());
+ updateState(channel(FREEZER_TEMPERATURE_CURRENT), device.getFreezerTemperatureCurrent());
+ updateState(channel(ERROR_STATE), device.getErrorState());
+ updateState(channel(INFO_STATE), device.getInfoState());
+ updateState(channel(DOOR_STATE), device.getDoorState());
+ updateState(channel(DOOR_ALARM), device.getDoorAlarm());
+ }
+
+ @Override
+ protected void updateTransitionState(TransitionChannelState transition) {
+ }
+
+ @Override
+ protected void updateActionState(ActionsChannelState actions) {
+ updateState(channel(SUPER_COOL_CAN_BE_CONTROLLED), actions.getSuperCoolCanBeControlled());
+ updateState(channel(SUPER_FREEZE_CAN_BE_CONTROLLED), actions.getSuperFreezeCanBeControlled());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * ThingHandler implementation for Miele dish warmers.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class DishWarmerDeviceThingHandler extends AbstractMieleThingHandler {
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ /**
+ * Creates a new {@link DishWarmerDeviceThingHandler}.
+ *
+ * @param thing The thing to handle.
+ */
+ public DishWarmerDeviceThingHandler(Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ super.handleCommand(channelUID, command);
+
+ if (DISH_WARMER_PROGRAM_ACTIVE.equals(channelUID.getId()) && command instanceof StringType) {
+ try {
+ triggerProgram(Long.parseLong(command.toString()));
+ } catch (NumberFormatException e) {
+ logger.warn("Failed to activate program: '{}' is not a valid program ID", command.toString());
+ }
+ }
+ }
+
+ @Override
+ protected void updateDeviceState(DeviceChannelState device) {
+ updateState(channel(DISH_WARMER_PROGRAM_ACTIVE), device.getProgramActiveId());
+ updateState(channel(PROGRAM_ACTIVE_RAW), device.getProgramActiveRaw());
+ updateState(channel(OPERATION_STATE), device.getOperationState());
+ updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
+ updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
+ updateState(channel(PROGRAM_ELAPSED_TIME), device.getProgramElapsedTime());
+ updateState(channel(TEMPERATURE_TARGET), device.getTemperatureTarget());
+ updateState(channel(TEMPERATURE_CURRENT), device.getTemperatureCurrent());
+ updateState(channel(ERROR_STATE), device.getErrorState());
+ updateState(channel(INFO_STATE), device.getInfoState());
+ updateState(channel(DOOR_STATE), device.getDoorState());
+ }
+
+ @Override
+ protected void updateTransitionState(TransitionChannelState transition) {
+ updateState(channel(PROGRAM_REMAINING_TIME), transition.getProgramRemainingTime());
+ updateState(channel(PROGRAM_PROGRESS), transition.getProgramProgress());
+ if (transition.hasFinishedChanged()) {
+ updateState(channel(FINISH_STATE), transition.getFinishState());
+ }
+ }
+
+ @Override
+ protected void updateActionState(ActionsChannelState actions) {
+ updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_ON), actions.getRemoteControlCanBeSwitchedOn());
+ updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF), actions.getRemoteControlCanBeSwitchedOff());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
+import org.openhab.core.thing.Thing;
+
+/**
+ * ThingHandler implementation for the Miele dishwasher devices.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Add channel state wrappers
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API
+ * @author Björn Lange - Add elapsed time channel
+ */
+@NonNullByDefault
+public class DishwasherDeviceThingHandler extends AbstractMieleThingHandler {
+ /**
+ * Creates a new {@link DishwasherDeviceThingHandler}.
+ *
+ * @param thing The thing to handle.
+ */
+ public DishwasherDeviceThingHandler(Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ protected void updateDeviceState(DeviceChannelState device) {
+ updateState(channel(PROGRAM_ACTIVE), device.getProgramActive());
+ updateState(channel(PROGRAM_ACTIVE_RAW), device.getProgramActiveRaw());
+ updateState(channel(PROGRAM_PHASE), device.getProgramPhase());
+ updateState(channel(PROGRAM_PHASE_RAW), device.getProgramPhaseRaw());
+ updateState(channel(OPERATION_STATE), device.getOperationState());
+ updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
+ updateState(channel(PROGRAM_START_STOP), device.getProgramStartStop());
+ updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
+ updateState(channel(DELAYED_START_TIME), device.getDelayedStartTime());
+ updateState(channel(PROGRAM_ELAPSED_TIME), device.getProgramElapsedTime());
+ updateState(channel(ERROR_STATE), device.getErrorState());
+ updateState(channel(INFO_STATE), device.getInfoState());
+ updateState(channel(DOOR_STATE), device.getDoorState());
+ }
+
+ @Override
+ protected void updateTransitionState(TransitionChannelState transition) {
+ updateState(channel(PROGRAM_REMAINING_TIME), transition.getProgramRemainingTime());
+ updateState(channel(PROGRAM_PROGRESS), transition.getProgramProgress());
+ if (transition.hasFinishedChanged()) {
+ updateState(channel(FINISH_STATE), transition.getFinishState());
+ }
+ }
+
+ @Override
+ protected void updateActionState(ActionsChannelState actions) {
+ updateState(channel(REMOTE_CONTROL_CAN_BE_STARTED), actions.getRemoteControlCanBeStarted());
+ updateState(channel(REMOTE_CONTROL_CAN_BE_STOPPED), actions.getRemoteControlCanBeStopped());
+ updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_ON), actions.getRemoteControlCanBeSwitchedOn());
+ updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF), actions.getRemoteControlCanBeSwitchedOff());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
+import org.openhab.core.thing.Thing;
+
+/**
+ * ThingHandler implementation for the Miele dryer and washingDryer devices.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Add channel state wrappers
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API
+ * @author Björn Lange - Add elapsed time channel
+ */
+@NonNullByDefault
+public class DryerDeviceThingHandler extends AbstractMieleThingHandler {
+ /**
+ * Creates a new {@link DryerDeviceThingHandler}.
+ *
+ * @param thing The thing to handle.
+ */
+ public DryerDeviceThingHandler(Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ protected void updateDeviceState(DeviceChannelState device) {
+ updateState(channel(PROGRAM_ACTIVE), device.getProgramActive());
+ updateState(channel(PROGRAM_ACTIVE_RAW), device.getProgramActiveRaw());
+ updateState(channel(PROGRAM_PHASE), device.getProgramPhase());
+ updateState(channel(PROGRAM_PHASE_RAW), device.getProgramPhaseRaw());
+ updateState(channel(OPERATION_STATE), device.getOperationState());
+ updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
+ updateState(channel(PROGRAM_START_STOP), device.getProgramStartStop());
+ updateState(channel(DELAYED_START_TIME), device.getDelayedStartTime());
+ updateState(channel(PROGRAM_ELAPSED_TIME), device.getProgramElapsedTime());
+ updateState(channel(DRYING_TARGET), device.getDryingTarget());
+ updateState(channel(DRYING_TARGET_RAW), device.getDryingTargetRaw());
+ updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
+ updateState(channel(ERROR_STATE), device.getErrorState());
+ updateState(channel(INFO_STATE), device.getInfoState());
+ updateState(channel(LIGHT_SWITCH), device.getLightSwitch());
+ updateState(channel(DOOR_STATE), device.getDoorState());
+ }
+
+ @Override
+ protected void updateTransitionState(TransitionChannelState transition) {
+ updateState(channel(PROGRAM_REMAINING_TIME), transition.getProgramRemainingTime());
+ updateState(channel(PROGRAM_PROGRESS), transition.getProgramProgress());
+ if (transition.hasFinishedChanged()) {
+ updateState(channel(FINISH_STATE), transition.getFinishState());
+ }
+ }
+
+ @Override
+ protected void updateActionState(ActionsChannelState actions) {
+ updateState(channel(REMOTE_CONTROL_CAN_BE_STARTED), actions.getRemoteControlCanBeStarted());
+ updateState(channel(REMOTE_CONTROL_CAN_BE_STOPPED), actions.getRemoteControlCanBeStopped());
+ updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_ON), actions.getRemoteControlCanBeSwitchedOn());
+ updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF), actions.getRemoteControlCanBeSwitchedOff());
+ updateState(channel(LIGHT_CAN_BE_CONTROLLED), actions.getLightCanBeControlled());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
+import org.openhab.core.thing.Thing;
+
+/**
+ * ThingHandler implementation for the Miele hob devices.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Add channel state wrappers
+ * @author Benjamin Bolte - Add plate step, add info state channel and map signal flags from API
+ */
+@NonNullByDefault
+public class HobDeviceThingHandler extends AbstractMieleThingHandler {
+ /**
+ * Creates a new {@link HobDeviceThingHandler}.
+ *
+ * @param thing The thing to handle.
+ */
+ public HobDeviceThingHandler(Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ protected void updateDeviceState(DeviceChannelState device) {
+ updateState(channel(OPERATION_STATE), device.getOperationState());
+ updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
+ updateState(channel(ERROR_STATE), device.getErrorState());
+ updateState(channel(INFO_STATE), device.getInfoState());
+ updateState(channel(PLATE_1_POWER_STEP), device.getPlateStep(0));
+ updateState(channel(PLATE_1_POWER_STEP_RAW), device.getPlateStepRaw(0));
+ updateState(channel(PLATE_2_POWER_STEP), device.getPlateStep(1));
+ updateState(channel(PLATE_2_POWER_STEP_RAW), device.getPlateStepRaw(1));
+ updateState(channel(PLATE_3_POWER_STEP), device.getPlateStep(2));
+ updateState(channel(PLATE_3_POWER_STEP_RAW), device.getPlateStepRaw(2));
+ updateState(channel(PLATE_4_POWER_STEP), device.getPlateStep(3));
+ updateState(channel(PLATE_4_POWER_STEP_RAW), device.getPlateStepRaw(3));
+ updateState(channel(PLATE_5_POWER_STEP), device.getPlateStep(4));
+ updateState(channel(PLATE_5_POWER_STEP_RAW), device.getPlateStepRaw(4));
+ updateState(channel(PLATE_6_POWER_STEP), device.getPlateStep(5));
+ updateState(channel(PLATE_6_POWER_STEP_RAW), device.getPlateStepRaw(5));
+ }
+
+ @Override
+ protected void updateTransitionState(TransitionChannelState transition) {
+ // No state transition required
+ }
+
+ @Override
+ protected void updateActionState(ActionsChannelState actions) {
+ // The Hob device has no trigger actions
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.Thing;
+
+/**
+ * ThingHandler implementation for the Miele Hood devices.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Add channel state wrappers
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API
+ */
+@NonNullByDefault
+public class HoodDeviceThingHandler extends AbstractMieleThingHandler {
+ /**
+ * Creates a new {@link HoodDeviceThingHandler}.
+ *
+ * @param thing The thing to handle.
+ */
+ public HoodDeviceThingHandler(Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ public void initialize() {
+ super.initialize();
+ updateState(channel(REMOTE_CONTROL_CAN_BE_STARTED), OnOffType.OFF);
+ updateState(channel(REMOTE_CONTROL_CAN_BE_STOPPED), OnOffType.OFF);
+ }
+
+ @Override
+ protected void updateDeviceState(DeviceChannelState device) {
+ updateState(channel(PROGRAM_PHASE), device.getProgramPhase());
+ updateState(channel(PROGRAM_PHASE_RAW), device.getProgramPhaseRaw());
+ updateState(channel(OPERATION_STATE), device.getOperationState());
+ updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
+ updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
+ updateState(channel(VENTILATION_POWER), device.getVentilationPower());
+ updateState(channel(VENTILATION_POWER_RAW), device.getVentilationPowerRaw());
+ updateState(channel(ERROR_STATE), device.getErrorState());
+ updateState(channel(INFO_STATE), device.getInfoState());
+ updateState(channel(LIGHT_SWITCH), device.getLightSwitch());
+ }
+
+ @Override
+ protected void updateTransitionState(TransitionChannelState transition) {
+ }
+
+ @Override
+ protected void updateActionState(ActionsChannelState actions) {
+ updateState(channel(LIGHT_CAN_BE_CONTROLLED), actions.getLightCanBeControlled());
+ updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_ON), actions.getRemoteControlCanBeSwitchedOn());
+ updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF), actions.getRemoteControlCanBeSwitchedOff());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.I18NKeys;
+import org.openhab.binding.mielecloud.internal.auth.OAuthException;
+import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefreshListener;
+import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.discovery.ThingDiscoveryService;
+import org.openhab.binding.mielecloud.internal.util.EmailValidator;
+import org.openhab.binding.mielecloud.internal.util.LocaleValidator;
+import org.openhab.binding.mielecloud.internal.webservice.ConnectionError;
+import org.openhab.binding.mielecloud.internal.webservice.ConnectionStatusListener;
+import org.openhab.binding.mielecloud.internal.webservice.DeviceStateListener;
+import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
+import org.openhab.binding.mielecloud.internal.webservice.UnavailableMieleWebservice;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceInitializationException;
+import org.openhab.binding.mielecloud.internal.webservice.language.CombiningLanguageProvider;
+import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * BridgeHandler implementation for the Miele cloud account.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Introduced CombiningLanguageProvider field and interactions, added LanguageProvider super
+ * interface, switched from polling to SSE, added support for multiple bridges
+ */
+@NonNullByDefault
+public class MieleBridgeHandler extends BaseBridgeHandler
+ implements OAuthTokenRefreshListener, LanguageProvider, ConnectionStatusListener, DeviceStateListener {
+ private static final int NUMBER_OF_SSE_RECONNECTION_ATTEMPTS_BEFORE_STATUS_IS_UPDATED = 6;
+
+ private final Supplier<MieleWebservice> webserviceFactory;
+
+ private final OAuthTokenRefresher tokenRefresher;
+ private final CombiningLanguageProvider languageProvider;
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ private @Nullable CompletableFuture<@Nullable Void> logoutFuture;
+ private @Nullable MieleWebservice webService;
+ private @Nullable ThingDiscoveryService discoveryService;
+
+ /**
+ * Creates a new {@link MieleBridgeHandler}.
+ *
+ * @param bridge The bridge to handle.
+ * @param webserviceFactory Factory for creating {@link MieleWebservice} instances.
+ * @param tokenRefresher Token refresher.
+ * @param languageProvider Language provider.
+ */
+ public MieleBridgeHandler(Bridge bridge, Function<ScheduledExecutorService, MieleWebservice> webserviceFactory,
+ OAuthTokenRefresher tokenRefresher, CombiningLanguageProvider languageProvider) {
+ super(bridge);
+ this.webserviceFactory = () -> webserviceFactory.apply(scheduler);
+ this.tokenRefresher = tokenRefresher;
+ this.languageProvider = languageProvider;
+ }
+
+ public void setDiscoveryService(@Nullable ThingDiscoveryService discoveryService) {
+ this.discoveryService = discoveryService;
+ }
+
+ /**
+ * Gets the current webservice instance for communication with the Miele service.
+ *
+ * This function may return an {@link UnavailableMieleWebservice} in case no webservice is available at the moment.
+ */
+ public MieleWebservice getWebservice() {
+ MieleWebservice webservice = webService;
+ if (webservice != null) {
+ return webservice;
+ } else {
+ return UnavailableMieleWebservice.INSTANCE;
+ }
+ }
+
+ private String getOAuthServiceHandle() {
+ return getConfig().get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL).toString();
+ }
+
+ @Override
+ public void initialize() {
+ // It is required to set a status in this method as stated in the Javadoc of ThingHandler.initialize
+ updateStatus(ThingStatus.UNKNOWN);
+
+ initializeWebservice();
+ }
+
+ public void initializeWebservice() {
+ if (!EmailValidator.isValid(getConfig().get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL).toString())) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ I18NKeys.BRIDGE_STATUS_DESCRIPTION_INVALID_EMAIL);
+ // When the e-mail configuration is changed a new initialization will be triggered. Therefore, we can leave
+ // the bridge in this state.
+ return;
+ }
+
+ try {
+ webService = webserviceFactory.get();
+ } catch (MieleWebserviceInitializationException e) {
+ logger.warn("Failed to initialize webservice.", e);
+ updateStatus(ThingStatus.OFFLINE);
+ return;
+ }
+
+ try {
+ tokenRefresher.setRefreshListener(this, getOAuthServiceHandle());
+ } catch (OAuthException e) {
+ logger.debug("Could not initialize Miele Cloud bridge.", e);
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED);
+ // When the authorization takes place a new initialization will be triggered. Therefore, we can leave the
+ // bridge in this state.
+ return;
+ }
+ languageProvider.setPrioritizedLanguageProvider(this);
+ tryInitializeWebservice();
+
+ MieleWebservice webservice = getWebservice();
+ webservice.addConnectionStatusListener(this);
+ webservice.addDeviceStateListener(this);
+ if (webservice.hasAccessToken()) {
+ webservice.connectSse();
+ }
+ }
+
+ @Override
+ public void handleRemoval() {
+ performLogout();
+ tokenRefresher.removeTokensFromStorage(getOAuthServiceHandle());
+ super.handleRemoval();
+ }
+
+ @Override
+ public void dispose() {
+ logger.debug("Disposing {}", this.getClass().getName());
+ disposeWebservice();
+ }
+
+ public void disposeWebservice() {
+ getWebservice().removeConnectionStatusListener(this);
+ getWebservice().removeDeviceStateListener(this);
+ getWebservice().disconnectSse();
+ languageProvider.unsetPrioritizedLanguageProvider();
+ tokenRefresher.unsetRefreshListener(getOAuthServiceHandle());
+
+ stopWebservice();
+ }
+
+ private void stopWebservice() {
+ final MieleWebservice webService = this.webService;
+ this.webService = null;
+ if (webService == null) {
+ return;
+ }
+
+ scheduler.submit(() -> {
+ CompletableFuture<@Nullable Void> logoutFuture = this.logoutFuture;
+ if (logoutFuture != null) {
+ try {
+ logoutFuture.get();
+ } catch (InterruptedException e) {
+ logger.warn("Interrupted while waiting for logout!");
+ } catch (ExecutionException e) {
+ logger.warn("Failed to wait for logout.", e);
+ }
+ }
+
+ try {
+ webService.close();
+ } catch (Exception e) {
+ logger.warn("Failed to close webservice.", e);
+ }
+ });
+ }
+
+ @Override
+ public void onNewAccessToken(String accessToken) {
+ logger.debug("Setting new access token for webservice access.");
+ updateProperty(MieleCloudBindingConstants.PROPERTY_ACCESS_TOKEN, accessToken);
+
+ // Without this the retry would fail causing the thing to go OFFLINE
+ getWebservice().setAccessToken(accessToken);
+
+ // If there was no access token during initialization then the SSE connection was not established.
+ getWebservice().connectSse();
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ }
+
+ private void performLogout() {
+ logoutFuture = new CompletableFuture<>();
+ scheduler.execute(() -> {
+ try {
+ getWebservice().logout();
+ } catch (Exception e) {
+ logger.warn("Failed to logout from Miele cloud.", e);
+ }
+ Optional.ofNullable(logoutFuture).map(future -> future.complete(null));
+ });
+ }
+
+ private void tryInitializeWebservice() {
+ Optional<String> accessToken = tokenRefresher.getAccessTokenFromStorage(getOAuthServiceHandle());
+ if (!accessToken.isPresent()) {
+ logger.debug("No OAuth2 access token available. Retrying later.");
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
+ I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_NOT_CONFIGURED);
+ return;
+ }
+ getWebservice().setAccessToken(accessToken.get());
+ updateProperty(MieleCloudBindingConstants.PROPERTY_ACCESS_TOKEN, accessToken.get());
+ }
+
+ @Override
+ public void onConnectionAlive() {
+ updateStatus(ThingStatus.ONLINE);
+ }
+
+ @Override
+ public void onConnectionError(ConnectionError connectionError, int failedReconnectionAttempts) {
+ if (connectionError == ConnectionError.AUTHORIZATION_FAILED) {
+ tryToRefreshAccessToken();
+ return;
+ }
+
+ if (failedReconnectionAttempts <= NUMBER_OF_SSE_RECONNECTION_ATTEMPTS_BEFORE_STATUS_IS_UPDATED
+ && getThing().getStatus() != ThingStatus.UNKNOWN) {
+ return;
+ }
+
+ if (getThing().getStatus() == ThingStatus.UNKNOWN && connectionError == ConnectionError.REQUEST_INTERRUPTED
+ && failedReconnectionAttempts <= NUMBER_OF_SSE_RECONNECTION_ATTEMPTS_BEFORE_STATUS_IS_UPDATED) {
+ return;
+ }
+
+ switch (connectionError) {
+ case AUTHORIZATION_FAILED:
+ // Handled above.
+ break;
+
+ case REQUEST_EXECUTION_FAILED:
+ case SERVICE_UNAVAILABLE:
+ case RESPONSE_MALFORMED:
+ case TIMEOUT:
+ case TOO_MANY_RERQUESTS:
+ case SSE_STREAM_ENDED:
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+ break;
+
+ case SERVER_ERROR:
+ case REQUEST_INTERRUPTED:
+ case OTHER_HTTP_ERROR:
+ default:
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
+ I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
+ break;
+ }
+ }
+
+ private void tryToRefreshAccessToken() {
+ try {
+ tokenRefresher.refreshToken(getOAuthServiceHandle());
+ getWebservice().connectSse();
+ } catch (OAuthException e) {
+ logger.debug("Failed to refresh OAuth token!", e);
+ getWebservice().disconnectSse();
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_REFRESH_FAILED);
+ }
+ }
+
+ @Override
+ public Optional<String> getLanguage() {
+ Object languageObject = thing.getConfiguration().get(MieleCloudBindingConstants.CONFIG_PARAM_LOCALE);
+ if (languageObject instanceof String) {
+ String language = (String) languageObject;
+ if (language.isEmpty() || !LocaleValidator.isValidLanguage(language)) {
+ return Optional.empty();
+ } else {
+ return Optional.of(language);
+ }
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ @Override
+ public void onDeviceStateUpdated(DeviceState deviceState) {
+ ThingDiscoveryService discoveryService = this.discoveryService;
+ if (discoveryService != null) {
+ discoveryService.onDeviceStateUpdated(deviceState);
+ }
+
+ invokeOnThingHandlers(deviceState.getDeviceIdentifier(), handler -> handler.onDeviceStateUpdated(deviceState));
+ }
+
+ @Override
+ public void onProcessActionUpdated(ActionsState actionState) {
+ invokeOnThingHandlers(actionState.getDeviceIdentifier(),
+ handler -> handler.onProcessActionUpdated(actionState));
+ }
+
+ @Override
+ public void onDeviceRemoved(String deviceIdentifier) {
+ ThingDiscoveryService discoveryService = this.discoveryService;
+ if (discoveryService != null) {
+ discoveryService.onDeviceRemoved(deviceIdentifier);
+ }
+
+ invokeOnThingHandlers(deviceIdentifier, handler -> handler.onDeviceRemoved());
+ }
+
+ private void invokeOnThingHandlers(String deviceIdentifier, Consumer<AbstractMieleThingHandler> action) {
+ getThing().getThings().stream().map(Thing::getHandler)
+ .filter(handler -> handler instanceof AbstractMieleThingHandler)
+ .map(handler -> (AbstractMieleThingHandler) handler)
+ .filter(handler -> deviceIdentifier.equals(handler.getDeviceId())).forEach(action);
+ }
+
+ @Override
+ public Collection<Class<? extends ThingHandlerService>> getServices() {
+ return Collections.singleton(ThingDiscoveryService.class);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.*;
+
+import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.Function;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.webservice.DefaultMieleWebserviceFactory;
+import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
+import org.openhab.binding.mielecloud.internal.webservice.MieleWebserviceConfiguration;
+import org.openhab.binding.mielecloud.internal.webservice.MieleWebserviceFactory;
+import org.openhab.binding.mielecloud.internal.webservice.language.CombiningLanguageProvider;
+import org.openhab.binding.mielecloud.internal.webservice.language.OpenHabLanguageProvider;
+import org.openhab.core.i18n.LocaleProvider;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * Factory producing the {@link ThingHandler}s for all things supported by this binding.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Added language provider, added support for multiple bridges
+ */
+@NonNullByDefault
+@Component(service = ThingHandlerFactory.class, configurationPid = "binding.mielecloud")
+public class MieleHandlerFactory extends BaseThingHandlerFactory {
+ public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_BRIDGE, THING_TYPE_WASHING_MACHINE,
+ THING_TYPE_WASHER_DRYER, THING_TYPE_COFFEE_SYSTEM, THING_TYPE_FRIDGE_FREEZER, THING_TYPE_FRIDGE,
+ THING_TYPE_FREEZER, THING_TYPE_OVEN, THING_TYPE_WINE_STORAGE, THING_TYPE_HOB, THING_TYPE_DRYER,
+ THING_TYPE_DISHWASHER, THING_TYPE_HOOD, THING_TYPE_DISH_WARMER, THING_TYPE_ROBOTIC_VACUUM_CLEANER);
+
+ private final HttpClientFactory httpClientFactory;
+ private final OAuthTokenRefresher tokenRefresher;
+ private final LocaleProvider localeProvider;
+
+ private final MieleWebserviceFactory webserviceFactory = new DefaultMieleWebserviceFactory();
+
+ @Activate
+ public MieleHandlerFactory(@Reference HttpClientFactory httpClientFactory,
+ @Reference OAuthTokenRefresher tokenRefresher, @Reference LocaleProvider localeProvider) {
+ this.httpClientFactory = httpClientFactory;
+ this.tokenRefresher = tokenRefresher;
+ this.localeProvider = localeProvider;
+ }
+
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES.contains(thingTypeUID);
+ }
+
+ @Override
+ @Nullable
+ protected ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+ if (thingTypeUID.equals(THING_TYPE_BRIDGE)) {
+ return createBridgeHandler(thing);
+ } else if (thingTypeUID.equals(THING_TYPE_WASHING_MACHINE) || thingTypeUID.equals(THING_TYPE_WASHER_DRYER)) {
+ return new WashingDeviceThingHandler(thing);
+ } else if (thingTypeUID.equals(THING_TYPE_COFFEE_SYSTEM)) {
+ return new CoffeeSystemThingHandler(thing);
+ } else if (thingTypeUID.equals(THING_TYPE_FRIDGE_FREEZER) || thingTypeUID.equals(THING_TYPE_FRIDGE)
+ || thingTypeUID.equals(THING_TYPE_FREEZER)) {
+ return new CoolingDeviceThingHandler(thing);
+ } else if (thingTypeUID.equals(THING_TYPE_WINE_STORAGE)) {
+ return new WineStorageDeviceThingHandler(thing);
+ } else if (thingTypeUID.equals(THING_TYPE_OVEN)) {
+ return new OvenDeviceThingHandler(thing);
+ } else if (thingTypeUID.equals(THING_TYPE_HOB)) {
+ return new HobDeviceThingHandler(thing);
+ } else if (thingTypeUID.equals(THING_TYPE_DISHWASHER)) {
+ return new DishwasherDeviceThingHandler(thing);
+ } else if (thingTypeUID.equals(THING_TYPE_DRYER)) {
+ return new DryerDeviceThingHandler(thing);
+ } else if (thingTypeUID.equals(THING_TYPE_HOOD)) {
+ return new HoodDeviceThingHandler(thing);
+ } else if (thingTypeUID.equals(THING_TYPE_DISH_WARMER)) {
+ return new DishWarmerDeviceThingHandler(thing);
+ } else if (thingTypeUID.equals(THING_TYPE_ROBOTIC_VACUUM_CLEANER)) {
+ return new RoboticVacuumCleanerDeviceThingHandler(thing);
+ }
+
+ return null;
+ }
+
+ private ThingHandler createBridgeHandler(Thing thing) {
+ CombiningLanguageProvider languageProvider = getLanguageProvider();
+ Function<ScheduledExecutorService, MieleWebservice> webserviceFactoryFunction = scheduler -> webserviceFactory
+ .create(MieleWebserviceConfiguration.builder().withHttpClientFactory(httpClientFactory)
+ .withLanguageProvider(languageProvider).withTokenRefresher(tokenRefresher)
+ .withServiceHandle(thing.getUID().getAsString()).withScheduler(scheduler).build());
+
+ return new MieleBridgeHandler((Bridge) thing, webserviceFactoryFunction, tokenRefresher, languageProvider);
+ }
+
+ private CombiningLanguageProvider getLanguageProvider() {
+ return new CombiningLanguageProvider(null, new OpenHabLanguageProvider(localeProvider));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
+import org.openhab.core.thing.Thing;
+
+/**
+ * ThingHandler implementation for the Miele oven devices.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Add channel state wrappers
+ * @author Benjamin Bolte - Add pre-heat finished channel, add info state channel and map signal flags from API
+ * @author Björn Lange - Add elapsed time channel
+ */
+@NonNullByDefault
+public class OvenDeviceThingHandler extends AbstractMieleThingHandler {
+ /**
+ * Creates a new {@link OvenDeviceThingHandler}.
+ *
+ * @param thing The thing to handle.
+ */
+ public OvenDeviceThingHandler(Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ protected void updateDeviceState(DeviceChannelState device) {
+ updateState(channel(PROGRAM_ACTIVE), device.getProgramActive());
+ updateState(channel(PROGRAM_ACTIVE_RAW), device.getProgramActiveRaw());
+ updateState(channel(PROGRAM_PHASE), device.getProgramPhase());
+ updateState(channel(PROGRAM_PHASE_RAW), device.getProgramPhaseRaw());
+ updateState(channel(OPERATION_STATE), device.getOperationState());
+ updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
+ updateState(channel(PROGRAM_START_STOP), device.getProgramStartStop());
+ updateState(channel(DELAYED_START_TIME), device.getDelayedStartTime());
+ updateState(channel(PROGRAM_ELAPSED_TIME), device.getProgramElapsedTime());
+ updateState(channel(PRE_HEAT_FINISHED), device.hasPreHeatFinished());
+ updateState(channel(TEMPERATURE_TARGET), device.getTemperatureTarget());
+ updateState(channel(TEMPERATURE_CURRENT), device.getTemperatureCurrent());
+ updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
+ updateState(channel(ERROR_STATE), device.getErrorState());
+ updateState(channel(INFO_STATE), device.getInfoState());
+ updateState(channel(LIGHT_SWITCH), device.getLightSwitch());
+ updateState(channel(DOOR_STATE), device.getDoorState());
+ }
+
+ @Override
+ protected void updateTransitionState(TransitionChannelState transition) {
+ updateState(channel(PROGRAM_REMAINING_TIME), transition.getProgramRemainingTime());
+ updateState(channel(PROGRAM_PROGRESS), transition.getProgramProgress());
+ if (transition.hasFinishedChanged()) {
+ updateState(channel(FINISH_STATE), transition.getFinishState());
+ }
+ }
+
+ @Override
+ protected void updateActionState(ActionsChannelState actions) {
+ updateState(channel(REMOTE_CONTROL_CAN_BE_STARTED), actions.getRemoteControlCanBeStarted());
+ updateState(channel(REMOTE_CONTROL_CAN_BE_STOPPED), actions.getRemoteControlCanBeStopped());
+ updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_ON), actions.getRemoteControlCanBeSwitchedOn());
+ updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF), actions.getRemoteControlCanBeSwitchedOff());
+ updateState(channel(LIGHT_CAN_BE_CONTROLLED), actions.getLightCanBeControlled());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * ThingHandler implementation for Miele robotic vacuum cleaners.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class RoboticVacuumCleanerDeviceThingHandler extends AbstractMieleThingHandler {
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ /**
+ * Creates a new {@link RoboticVacuumCleanerDeviceThingHandler}.
+ *
+ * @param thing The thing to handle.
+ */
+ public RoboticVacuumCleanerDeviceThingHandler(Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ super.handleCommand(channelUID, command);
+
+ if (VACUUM_CLEANER_PROGRAM_ACTIVE.equals(channelUID.getId()) && command instanceof StringType) {
+ try {
+ triggerProgram(Long.parseLong(command.toString()));
+ } catch (NumberFormatException e) {
+ logger.warn("Failed to activate program: '{}' is not a valid program ID", command.toString());
+ }
+ }
+ }
+
+ @Override
+ protected void updateDeviceState(DeviceChannelState device) {
+ updateState(channel(VACUUM_CLEANER_PROGRAM_ACTIVE), device.getProgramActiveId());
+ updateState(channel(PROGRAM_ACTIVE_RAW), device.getProgramActiveRaw());
+ updateState(channel(OPERATION_STATE), device.getOperationState());
+ updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
+ updateState(channel(PROGRAM_START_STOP_PAUSE), device.getProgramStartStopPause());
+ updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
+ updateState(channel(ERROR_STATE), device.getErrorState());
+ updateState(channel(INFO_STATE), device.getInfoState());
+ updateState(channel(BATTERY_LEVEL), device.getBatteryLevel());
+ }
+
+ @Override
+ protected void updateTransitionState(TransitionChannelState transition) {
+ if (transition.hasFinishedChanged()) {
+ updateState(channel(FINISH_STATE), transition.getFinishState());
+ }
+ }
+
+ @Override
+ protected void updateActionState(ActionsChannelState actions) {
+ updateState(channel(REMOTE_CONTROL_CAN_BE_STARTED), actions.getRemoteControlCanBeStarted());
+ updateState(channel(REMOTE_CONTROL_CAN_BE_STOPPED), actions.getRemoteControlCanBeStopped());
+ updateState(channel(REMOTE_CONTROL_CAN_BE_PAUSED), actions.getRemoteControlCanBePaused());
+ updateState(channel(REMOTE_CONTROL_CAN_SET_PROGRAM_ACTIVE), actions.getRemoteControlCanSetProgramActive());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
+import org.openhab.core.thing.Thing;
+
+/**
+ * ThingHandler implementation for the Miele washing devices.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Add channel state wrappers
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API
+ * @author Björn Lange - Add elapsed time channel
+ */
+@NonNullByDefault
+public class WashingDeviceThingHandler extends AbstractMieleThingHandler {
+ /**
+ * Creates a new {@link WashingDeviceThingHandler}.
+ *
+ * @param thing The thing to handle.
+ */
+ public WashingDeviceThingHandler(Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ protected void updateDeviceState(DeviceChannelState device) {
+ updateState(channel(SPINNING_SPEED), device.getSpinningSpeed());
+ updateState(channel(SPINNING_SPEED_RAW), device.getSpinningSpeedRaw());
+ updateState(channel(PROGRAM_ACTIVE), device.getProgramActive());
+ updateState(channel(PROGRAM_ACTIVE_RAW), device.getProgramActiveRaw());
+ updateState(channel(PROGRAM_PHASE), device.getProgramPhase());
+ updateState(channel(PROGRAM_PHASE_RAW), device.getProgramPhaseRaw());
+ updateState(channel(OPERATION_STATE), device.getOperationState());
+ updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
+ updateState(channel(PROGRAM_START_STOP), device.getProgramStartStop());
+ updateState(channel(DELAYED_START_TIME), device.getDelayedStartTime());
+ updateState(channel(PROGRAM_ELAPSED_TIME), device.getProgramElapsedTime());
+ updateState(channel(TEMPERATURE_TARGET), device.getTemperatureTarget());
+ updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
+ updateState(channel(ERROR_STATE), device.getErrorState());
+ updateState(channel(INFO_STATE), device.getInfoState());
+ updateState(channel(LIGHT_SWITCH), device.getLightSwitch());
+ updateState(channel(DOOR_STATE), device.getDoorState());
+ }
+
+ @Override
+ protected void updateTransitionState(TransitionChannelState transition) {
+ updateState(channel(PROGRAM_REMAINING_TIME), transition.getProgramRemainingTime());
+ updateState(channel(PROGRAM_PROGRESS), transition.getProgramProgress());
+ if (transition.hasFinishedChanged()) {
+ updateState(channel(FINISH_STATE), transition.getFinishState());
+ }
+ }
+
+ @Override
+ protected void updateActionState(ActionsChannelState actions) {
+ updateState(channel(REMOTE_CONTROL_CAN_BE_STARTED), actions.getRemoteControlCanBeStarted());
+ updateState(channel(REMOTE_CONTROL_CAN_BE_STOPPED), actions.getRemoteControlCanBeStopped());
+ updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_ON), actions.getRemoteControlCanBeSwitchedOn());
+ updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF), actions.getRemoteControlCanBeSwitchedOff());
+ updateState(channel(LIGHT_CAN_BE_CONTROLLED), actions.getLightCanBeControlled());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
+import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.Thing;
+
+/**
+ * ThingHandler implementation for the Miele wine storage devices.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Add channel state wrappers
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API
+ */
+@NonNullByDefault
+public class WineStorageDeviceThingHandler extends AbstractMieleThingHandler {
+ /**
+ * Creates a new {@link WineStorageDeviceThingHandler}.
+ *
+ * @param thing The thing to handle.
+ */
+ public WineStorageDeviceThingHandler(Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ public void initialize() {
+ super.initialize();
+ updateState(channel(REMOTE_CONTROL_CAN_BE_STARTED), OnOffType.OFF);
+ updateState(channel(REMOTE_CONTROL_CAN_BE_STOPPED), OnOffType.OFF);
+ }
+
+ @Override
+ protected void updateDeviceState(DeviceChannelState device) {
+ updateState(channel(OPERATION_STATE), device.getOperationState());
+ updateState(channel(OPERATION_STATE_RAW), device.getOperationStateRaw());
+ updateState(channel(TEMPERATURE_TARGET), device.getWineTemperatureTarget());
+ updateState(channel(TEMPERATURE_CURRENT), device.getWineTemperatureCurrent());
+ updateState(channel(TOP_TEMPERATURE_TARGET), device.getWineTopTemperatureTarget());
+ updateState(channel(TOP_TEMPERATURE_CURRENT), device.getWineTopTemperatureCurrent());
+ updateState(channel(MIDDLE_TEMPERATURE_TARGET), device.getWineMiddleTemperatureTarget());
+ updateState(channel(MIDDLE_TEMPERATURE_CURRENT), device.getWineMiddleTemperatureCurrent());
+ updateState(channel(BOTTOM_TEMPERATURE_TARGET), device.getWineBottomTemperatureTarget());
+ updateState(channel(BOTTOM_TEMPERATURE_CURRENT), device.getWineBottomTemperatureCurrent());
+ updateState(channel(POWER_ON_OFF), device.getPowerOnOff());
+ updateState(channel(ERROR_STATE), device.getErrorState());
+ updateState(channel(INFO_STATE), device.getInfoState());
+ }
+
+ @Override
+ protected void updateTransitionState(TransitionChannelState transition) {
+ }
+
+ @Override
+ protected void updateActionState(ActionsChannelState actions) {
+ updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_ON), actions.getRemoteControlCanBeSwitchedOn());
+ updateState(channel(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF), actions.getRemoteControlCanBeSwitchedOff());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler.channel;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.types.State;
+
+/**
+ * Wrapper for {@link ActionsState} handling the type conversion to {@link State} for directly filling channels.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class ActionsChannelState {
+ private final ActionsState actions;
+
+ public ActionsChannelState(ActionsState actions) {
+ this.actions = actions;
+ }
+
+ public State getRemoteControlCanBeSwitchedOn() {
+ return OnOffType.from(actions.canBeSwitchedOn());
+ }
+
+ public State getRemoteControlCanBeSwitchedOff() {
+ return OnOffType.from(actions.canBeSwitchedOff());
+ }
+
+ public State getLightCanBeControlled() {
+ return OnOffType.from(actions.canControlLight());
+ }
+
+ public State getSuperCoolCanBeControlled() {
+ return OnOffType.from(actions.canContolSupercooling());
+ }
+
+ public State getSuperFreezeCanBeControlled() {
+ return OnOffType.from(actions.canControlSuperfreezing());
+ }
+
+ public State getRemoteControlCanBeStarted() {
+ return OnOffType.from(actions.canBeStarted());
+ }
+
+ public State getRemoteControlCanBeStopped() {
+ return OnOffType.from(actions.canBeStopped());
+ }
+
+ public State getRemoteControlCanBePaused() {
+ return OnOffType.from(actions.canBePaused());
+ }
+
+ public State getRemoteControlCanSetProgramActive() {
+ return OnOffType.from(actions.canSetActiveProgramId());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler.channel;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * Utility class handling type conversions from Java types to channel types.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class ChannelTypeUtil {
+ private ChannelTypeUtil() {
+ throw new IllegalStateException("ChannelTypeUtil cannot be instantiated.");
+ }
+
+ /**
+ * Converts an {@link Optional} of {@link String} to {@link State}.
+ */
+ public static State stringToState(Optional<String> value) {
+ return value.filter(v -> !v.isEmpty()).filter(v -> !v.equals("null")).map(v -> (State) new StringType(v))
+ .orElse(UnDefType.UNDEF);
+ }
+
+ /**
+ * Converts an {@link Optional} of {@link Boolean} to {@link State}.
+ */
+ public static State booleanToState(Optional<Boolean> value) {
+ return value.map(v -> (State) OnOffType.from(v)).orElse(UnDefType.UNDEF);
+ }
+
+ /**
+ * Converts an {@link Optional} of {@link Integer} to {@link State}.
+ */
+ public static State intToState(Optional<Integer> value) {
+ return value.map(v -> (State) new DecimalType(v)).orElse(UnDefType.UNDEF);
+ }
+
+ /**
+ * Converts an {@link Optional} of {@link Long} to {@link State}.
+ */
+ public static State longToState(Optional<Long> value) {
+ return value.map(v -> (State) new DecimalType(v)).orElse(UnDefType.UNDEF);
+ }
+
+ /**
+ * Converts an {@link Optional} of {@link Integer} to {@link State} representing a temperature.
+ */
+ public static State intToTemperatureState(Optional<Integer> value) {
+ // The Miele 3rd Party API always provides temperatures in °C (even if the device uses another unit).
+ return value.map(v -> (State) new QuantityType<>(v, SIUnits.CELSIUS)).orElse(UnDefType.UNDEF);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler.channel;
+
+import static org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus.*;
+import static org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus.*;
+
+import java.util.Arrays;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.webservice.api.CoolingDeviceTemperatureState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.WineStorageDeviceTemperatureState;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.types.State;
+
+/**
+ * Wrapper for {@link DeviceState} handling the type conversion to {@link State} for directly filling channels.
+ *
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add pre-heat finished, plate step, door state, door alarm and info state channel and map
+ * signal flags from API
+ * @author Björn Lange - Add elapsed time channel, dish warmer and robotic vacuum cleaner thing
+ */
+@NonNullByDefault
+public final class DeviceChannelState {
+ private final DeviceState device;
+ private final CoolingDeviceTemperatureState coolingTemperature;
+ private final WineStorageDeviceTemperatureState wineTemperature;
+
+ public DeviceChannelState(DeviceState device) {
+ this.device = device;
+ this.coolingTemperature = new CoolingDeviceTemperatureState(device);
+ this.wineTemperature = new WineStorageDeviceTemperatureState(device);
+ }
+
+ public State getLightSwitch() {
+ return ChannelTypeUtil.booleanToState(device.getLightState());
+ }
+
+ public State getDoorState() {
+ return ChannelTypeUtil.booleanToState(device.getDoorState());
+ }
+
+ public State getDoorAlarm() {
+ return ChannelTypeUtil.booleanToState(device.getDoorAlarm());
+ }
+
+ public State getErrorState() {
+ return OnOffType.from(device.hasError());
+ }
+
+ public State getInfoState() {
+ return OnOffType.from(device.hasInfo());
+ }
+
+ public State getPowerOnOff() {
+ return new StringType(getPowerStatus().getState());
+ }
+
+ public State getProgramElapsedTime() {
+ return ChannelTypeUtil.intToState(device.getElapsedTime());
+ }
+
+ public State getOperationState() {
+ return ChannelTypeUtil.stringToState(device.getStatus());
+ }
+
+ public State getOperationStateRaw() {
+ return ChannelTypeUtil.intToState(device.getStatusRaw());
+ }
+
+ public State getProgramPhase() {
+ return ChannelTypeUtil.stringToState(device.getProgramPhase());
+ }
+
+ public State getProgramPhaseRaw() {
+ return ChannelTypeUtil.intToState(device.getProgramPhaseRaw());
+ }
+
+ public State getProgramActive() {
+ return ChannelTypeUtil.stringToState(device.getSelectedProgram());
+ }
+
+ public State getProgramActiveRaw() {
+ return ChannelTypeUtil.longToState(device.getSelectedProgramId());
+ }
+
+ public State getProgramActiveId() {
+ return ChannelTypeUtil.stringToState(device.getSelectedProgramId().map(Object::toString));
+ }
+
+ public State getFridgeSuperCool() {
+ return ChannelTypeUtil.booleanToState(isInState(StateType.SUPERCOOLING, StateType.SUPERCOOLING_SUPERFREEZING));
+ }
+
+ public State getFreezerSuperFreeze() {
+ return ChannelTypeUtil.booleanToState(isInState(StateType.SUPERFREEZING, StateType.SUPERCOOLING_SUPERFREEZING));
+ }
+
+ public State getFridgeTemperatureTarget() {
+ return ChannelTypeUtil.intToTemperatureState(coolingTemperature.getFridgeTargetTemperature());
+ }
+
+ public State getFreezerTemperatureTarget() {
+ return ChannelTypeUtil.intToTemperatureState(coolingTemperature.getFreezerTargetTemperature());
+ }
+
+ public State getFridgeTemperatureCurrent() {
+ return ChannelTypeUtil.intToTemperatureState(coolingTemperature.getFridgeTemperature());
+ }
+
+ public State getFreezerTemperatureCurrent() {
+ return ChannelTypeUtil.intToTemperatureState(coolingTemperature.getFreezerTemperature());
+ }
+
+ public State getProgramStartStop() {
+ return new StringType(getProgramStartStopStatus().getState());
+ }
+
+ public State getProgramStartStopPause() {
+ return new StringType(getProgramStartStopPauseStatus().getState());
+ }
+
+ public State getDelayedStartTime() {
+ return ChannelTypeUtil.intToState(device.getStartTime());
+ }
+
+ public State getDryingTarget() {
+ return ChannelTypeUtil.stringToState(device.getDryingTarget());
+ }
+
+ public State getDryingTargetRaw() {
+ return ChannelTypeUtil.intToState(device.getDryingTargetRaw());
+ }
+
+ public State hasPreHeatFinished() {
+ return ChannelTypeUtil.booleanToState(device.hasPreHeatFinished());
+ }
+
+ public State getTemperatureTarget() {
+ return ChannelTypeUtil.intToTemperatureState(device.getTargetTemperature(0));
+ }
+
+ public State getVentilationPower() {
+ return ChannelTypeUtil.stringToState(device.getVentilationStep());
+ }
+
+ public State getVentilationPowerRaw() {
+ return ChannelTypeUtil.intToState(device.getVentilationStepRaw());
+ }
+
+ public State getPlateStep(int index) {
+ return ChannelTypeUtil.stringToState(device.getPlateStep(index));
+ }
+
+ public State getPlateStepRaw(int index) {
+ return ChannelTypeUtil.intToState(device.getPlateStepRaw(index));
+ }
+
+ public State getTemperatureCurrent() {
+ return ChannelTypeUtil.intToTemperatureState(device.getTemperature(0));
+ }
+
+ public State getSpinningSpeed() {
+ return ChannelTypeUtil.stringToState(device.getSpinningSpeed());
+ }
+
+ public State getSpinningSpeedRaw() {
+ return ChannelTypeUtil.intToState(device.getSpinningSpeedRaw());
+ }
+
+ public State getBatteryLevel() {
+ return ChannelTypeUtil.intToState(device.getBatteryLevel());
+ }
+
+ public State getWineTemperatureTarget() {
+ return ChannelTypeUtil.intToState(wineTemperature.getTargetTemperature());
+ }
+
+ public State getWineTemperatureCurrent() {
+ return ChannelTypeUtil.intToTemperatureState(wineTemperature.getTemperature());
+ }
+
+ public State getWineTopTemperatureTarget() {
+ return ChannelTypeUtil.intToTemperatureState(wineTemperature.getTopTargetTemperature());
+ }
+
+ public State getWineTopTemperatureCurrent() {
+ return ChannelTypeUtil.intToTemperatureState(wineTemperature.getTopTemperature());
+ }
+
+ public State getWineMiddleTemperatureTarget() {
+ return ChannelTypeUtil.intToTemperatureState(wineTemperature.getMiddleTargetTemperature());
+ }
+
+ public State getWineMiddleTemperatureCurrent() {
+ return ChannelTypeUtil.intToTemperatureState(wineTemperature.getMiddleTemperature());
+ }
+
+ public State getWineBottomTemperatureTarget() {
+ return ChannelTypeUtil.intToTemperatureState(wineTemperature.getBottomTargetTemperature());
+ }
+
+ public State getWineBottomTemperatureCurrent() {
+ return ChannelTypeUtil.intToTemperatureState(wineTemperature.getBottomTemperature());
+ }
+
+ /**
+ * Determines the status of the currently selected program.
+ */
+ private PowerStatus getPowerStatus() {
+ if (device.isInState(StateType.OFF) || device.isInState(StateType.NOT_CONNECTED)) {
+ return POWER_OFF;
+ } else {
+ return POWER_ON;
+ }
+ }
+
+ /**
+ * Determines the status of the currently selected program respecting the possibilities started and stopped.
+ */
+ protected ProgramStatus getProgramStartStopStatus() {
+ if (device.isInState(StateType.RUNNING)) {
+ return PROGRAM_STARTED;
+ } else {
+ return PROGRAM_STOPPED;
+ }
+ }
+
+ /**
+ * Determines the status of the currently selected program respecting the possibilities started, stopped and paused.
+ */
+ protected ProgramStatus getProgramStartStopPauseStatus() {
+ if (device.isInState(StateType.RUNNING)) {
+ return PROGRAM_STARTED;
+ } else if (device.isInState(StateType.PAUSE)) {
+ return PROGRAM_PAUSED;
+ } else {
+ return PROGRAM_STOPPED;
+ }
+ }
+
+ /**
+ * Gets whether the device is in one of the given states.
+ *
+ * @param stateType The states to check.
+ * @return An empty {@link Optional} if the raw status is unknown, otherwise an {@link Optional} with a value
+ * indicating whether the device is in one of the given states.
+ */
+ private Optional<Boolean> isInState(StateType... stateType) {
+ return device.getStateType().map(it -> Arrays.asList(stateType).contains(it));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler.channel;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.webservice.api.TransitionState;
+import org.openhab.core.types.State;
+
+/**
+ * Wrapper for {@link TransitionState} handling the type conversion to {@link State} for directly filling channels.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class TransitionChannelState {
+ private final TransitionState transition;
+
+ public TransitionChannelState(TransitionState transition) {
+ this.transition = transition;
+ }
+
+ public boolean hasFinishedChanged() {
+ return transition.hasFinishedChanged();
+ }
+
+ public State getFinishState() {
+ return ChannelTypeUtil.booleanToState(transition.isFinished());
+ }
+
+ public State getProgramRemainingTime() {
+ return ChannelTypeUtil.intToState(transition.getRemainingTime());
+ }
+
+ public State getProgramProgress() {
+ return ChannelTypeUtil.intToState(transition.getProgress());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.util;
+
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Utility for validating e-mail addresses.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class EmailValidator {
+ private static final Pattern EMAIL_PATTERN = Pattern.compile("^[\\w-_\\.+]*[\\w-_\\.]\\@([\\w]+\\.)+[\\w]+[\\w]$");
+
+ private EmailValidator() {
+ throw new UnsupportedOperationException();
+ }
+
+ public static boolean isValid(String emailAddress) {
+ return EMAIL_PATTERN.matcher(emailAddress).matches();
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.util;
+
+import java.util.Locale;
+import java.util.MissingResourceException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Utility for validating locales.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class LocaleValidator {
+ private LocaleValidator() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Checks whether the given string is a valid two letter language code.
+ *
+ * @param language The string to check.
+ * @return Whether it is a valid language.
+ */
+ public static boolean isValidLanguage(String language) {
+ try {
+ String iso3Language = new Locale(language).getISO3Language();
+ return iso3Language != null && !iso3Language.isEmpty();
+ } catch (MissingResourceException e) {
+ return false;
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import java.util.Optional;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.Supplier;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link ActionStateFetcher} fetches the updated actions state for a device from the {@link MieleWebservice} if
+ * the state of that device changed.
+ *
+ * Note that an instance of this class is required for each device.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Make calls to webservice asynchronous
+ */
+@NonNullByDefault
+public class ActionStateFetcher {
+ private Optional<DeviceState> lastDeviceState = Optional.empty();
+ private final Supplier<MieleWebservice> webserviceSupplier;
+ private final ScheduledExecutorService scheduler;
+
+ private final Logger logger = LoggerFactory.getLogger(ActionStateFetcher.class);
+
+ /**
+ * Creates a new {@link ActionStateFetcher}.
+ *
+ * @param webserviceSupplier Getter function for access to the {@link MieleWebservice}.
+ * @param scheduler System-wide scheduler.
+ */
+ public ActionStateFetcher(Supplier<MieleWebservice> webserviceSupplier, ScheduledExecutorService scheduler) {
+ this.webserviceSupplier = webserviceSupplier;
+ this.scheduler = scheduler;
+ }
+
+ /**
+ * Invoked when the state of a device was updated.
+ */
+ public void onDeviceStateUpdated(DeviceState deviceState) {
+ if (hasDeviceStatusChanged(deviceState)) {
+ scheduler.submit(() -> fetchActions(deviceState));
+ }
+ lastDeviceState = Optional.of(deviceState);
+ }
+
+ private boolean hasDeviceStatusChanged(DeviceState newDeviceState) {
+ return lastDeviceState.map(DeviceState::getStateType)
+ .map(rawStatus -> !newDeviceState.getStateType().equals(rawStatus)).orElse(true);
+ }
+
+ private void fetchActions(DeviceState deviceState) {
+ try {
+ webserviceSupplier.get().fetchActions(deviceState.getDeviceIdentifier());
+ } catch (MieleWebserviceException e) {
+ logger.warn("Failed to fetch action state for device {}: {} - {}", deviceState.getDeviceIdentifier(),
+ e.getConnectionError(), e.getMessage());
+ } catch (AuthorizationFailedException | TooManyRequestsException e) {
+ logger.warn("Failed to fetch action state for device {}: {}", deviceState.getDeviceIdentifier(),
+ e.getMessage());
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link ConnectionError} enumeration represents the error state of a connection to the Miele cloud.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public enum ConnectionError {
+ SERVER_ERROR,
+ SERVICE_UNAVAILABLE,
+ OTHER_HTTP_ERROR,
+ REQUEST_INTERRUPTED,
+ TIMEOUT,
+ REQUEST_EXECUTION_FAILED,
+ RESPONSE_MALFORMED,
+ AUTHORIZATION_FAILED,
+ TOO_MANY_RERQUESTS,
+ SSE_STREAM_ENDED,
+ UNKNOWN,
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Listener for the connection status.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public interface ConnectionStatusListener {
+ /**
+ * Called regularly while the connection is up and running.
+ */
+ void onConnectionAlive();
+
+ /**
+ * Called when a connection error is encountered.
+ *
+ * @param connectionError The error.
+ * @param failedReconnectAttempts The number of failed attempts to reconnect.
+ */
+ void onConnectionError(ConnectionError connectionError, int failedReconnectAttempts);
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Actions;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceCollection;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Light;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.MieleSyntaxException;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
+import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceInitializationException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceTransientException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
+import org.openhab.binding.mielecloud.internal.webservice.request.RequestFactory;
+import org.openhab.binding.mielecloud.internal.webservice.request.RequestFactoryImpl;
+import org.openhab.binding.mielecloud.internal.webservice.retry.AuthorizationFailedRetryStrategy;
+import org.openhab.binding.mielecloud.internal.webservice.retry.NTimesRetryStrategy;
+import org.openhab.binding.mielecloud.internal.webservice.retry.RetryStrategy;
+import org.openhab.binding.mielecloud.internal.webservice.retry.RetryStrategyCombiner;
+import org.openhab.binding.mielecloud.internal.webservice.sse.ServerSentEvent;
+import org.openhab.binding.mielecloud.internal.webservice.sse.SseConnection;
+import org.openhab.binding.mielecloud.internal.webservice.sse.SseListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * Default implementation of the {@link MieleWebservice}.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public final class DefaultMieleWebservice implements MieleWebservice, SseListener {
+ private static final String SERVER_ADDRESS = "https://api.mcs3.miele.com";
+ public static final String THIRD_PARTY_ENDPOINTS_BASENAME = SERVER_ADDRESS + "/thirdparty";
+ private static final String ENDPOINT_DEVICES = SERVER_ADDRESS + "/v1/devices/";
+ private static final String ENDPOINT_ACTIONS = ENDPOINT_DEVICES + "%s" + "/actions";
+ private static final String ENDPOINT_LOGOUT = THIRD_PARTY_ENDPOINTS_BASENAME + "/logout";
+ private static final String ENDPOINT_ALL_SSE_EVENTS = ENDPOINT_DEVICES + "all/events";
+
+ private static final String SSE_EVENT_TYPE_DEVICES = "devices";
+
+ private static final Gson GSON = new Gson();
+
+ private final Logger logger = LoggerFactory.getLogger(DefaultMieleWebservice.class);
+
+ private Optional<String> accessToken = Optional.empty();
+ private final RequestFactory requestFactory;
+
+ private final DeviceStateDispatcher deviceStateDispatcher;
+ private final List<ConnectionStatusListener> connectionStatusListeners = new ArrayList<>();
+
+ private final RetryStrategy retryStrategy;
+
+ private final SseConnection sseConnection;
+
+ /**
+ * Creates a new {@link DefaultMieleWebservice} with default retry configuration which is to retry failed operations
+ * once on a transient error. In case an authorization error occurs, a new access token is requested and a retry of
+ * the failed request is executed.
+ *
+ * @param configuration The configuration holding all parameters for constructing the instance.
+ * @throws MieleWebserviceInitializationException if initializing the HTTP client fails.
+ */
+ public DefaultMieleWebservice(MieleWebserviceConfiguration configuration) {
+ this(new RequestFactoryImpl(configuration.getHttpClientFactory(), configuration.getLanguageProvider()),
+ new RetryStrategyCombiner(new NTimesRetryStrategy(1),
+ new AuthorizationFailedRetryStrategy(configuration.getTokenRefresher(),
+ configuration.getServiceHandle())),
+ new DeviceStateDispatcher(), configuration.getScheduler());
+ }
+
+ /**
+ * This constructor only exists for testing.
+ */
+ DefaultMieleWebservice(RequestFactory requestFactory, RetryStrategy retryStrategy,
+ DeviceStateDispatcher deviceStateDispatcher, ScheduledExecutorService scheduler) {
+ this.requestFactory = requestFactory;
+ this.retryStrategy = retryStrategy;
+ this.deviceStateDispatcher = deviceStateDispatcher;
+ this.sseConnection = new SseConnection(ENDPOINT_ALL_SSE_EVENTS, this::createSseRequest, scheduler);
+ this.sseConnection.addSseListener(this);
+ }
+
+ @Override
+ public void setAccessToken(String accessToken) {
+ this.accessToken = Optional.of(accessToken);
+ }
+
+ @Override
+ public boolean hasAccessToken() {
+ return accessToken.isPresent();
+ }
+
+ @Override
+ public synchronized void connectSse() {
+ sseConnection.connect();
+ }
+
+ @Override
+ public synchronized void disconnectSse() {
+ sseConnection.disconnect();
+ }
+
+ @Nullable
+ private Request createSseRequest(String endpoint) {
+ Optional<String> accessToken = this.accessToken;
+ if (!accessToken.isPresent()) {
+ logger.warn("No access token present.");
+ return null;
+ }
+
+ return requestFactory.createSseRequest(endpoint, accessToken.get());
+ }
+
+ @Override
+ public void onServerSentEvent(ServerSentEvent event) {
+ fireConnectionAlive();
+
+ if (!SSE_EVENT_TYPE_DEVICES.equals(event.getEvent())) {
+ return;
+ }
+
+ try {
+ deviceStateDispatcher.dispatchDeviceStateUpdates(DeviceCollection.fromJson(event.getData()));
+ } catch (MieleSyntaxException e) {
+ logger.warn("SSE payload is not valid Json: {}", event.getData());
+ }
+ }
+
+ private void fireConnectionAlive() {
+ connectionStatusListeners.forEach(ConnectionStatusListener::onConnectionAlive);
+ }
+
+ @Override
+ public void onConnectionError(ConnectionError connectionError, int failedReconnectAttempts) {
+ connectionStatusListeners.forEach(l -> l.onConnectionError(connectionError, failedReconnectAttempts));
+ }
+
+ @Override
+ public void fetchActions(String deviceId) {
+ Actions actions = retryStrategy.performRetryableOperation(() -> getActions(deviceId),
+ e -> logger.warn("Cannot poll action state: {}. Retrying...", e.getMessage()));
+ if (actions != null) {
+ deviceStateDispatcher.dispatchActionStateUpdates(deviceId, actions);
+ } else {
+ logger.warn("Cannot poll action state. Response is missing actions.");
+ }
+ }
+
+ @Override
+ public void putProcessAction(String deviceId, ProcessAction processAction) {
+ if (processAction.equals(ProcessAction.UNKNOWN)) {
+ throw new IllegalArgumentException("Process action must not be UNKNOWN.");
+ }
+
+ String formattedProcessAction = GSON.toJson(processAction, ProcessAction.class);
+ formattedProcessAction = formattedProcessAction.substring(1, formattedProcessAction.length() - 1);
+ String json = "{\"processAction\":" + formattedProcessAction + "}";
+
+ logger.debug("Activate process action {} of Miele device {}", processAction.toString(), deviceId);
+ putActions(deviceId, json);
+ }
+
+ @Override
+ public void putLight(String deviceId, boolean enabled) {
+ Light light = enabled ? Light.ENABLE : Light.DISABLE;
+ String json = "{\"light\":" + light.format() + "}";
+
+ logger.debug("Set light of Miele device {} to {}", deviceId, enabled);
+ putActions(deviceId, json);
+ }
+
+ @Override
+ public void putPowerState(String deviceId, boolean enabled) {
+ String action = enabled ? "powerOn" : "powerOff";
+ String json = "{\"" + action + "\":true}";
+
+ logger.debug("Set power state of Miele device {} to {}", deviceId, action);
+ putActions(deviceId, json);
+ }
+
+ @Override
+ public void putProgram(String deviceId, long programId) {
+ String json = "{\"programId\":" + programId + "}";
+
+ logger.debug("Activate program with ID {} of Miele device {}", programId, deviceId);
+ putActions(deviceId, json);
+ }
+
+ @Override
+ public void logout() {
+ Optional<String> accessToken = this.accessToken;
+ if (!accessToken.isPresent()) {
+ logger.debug("No access token present.");
+ return;
+ }
+
+ try {
+ logger.debug("Invalidating Miele webservice access token.");
+ Request request = requestFactory.createPostRequest(ENDPOINT_LOGOUT, accessToken.get());
+ this.accessToken = Optional.empty();
+ sendRequest(request);
+ } catch (MieleWebserviceTransientException e) {
+ throw new MieleWebserviceException("Transient error occurred during logout.", e, e.getConnectionError());
+ }
+ }
+
+ /**
+ * Sends the given request and wraps the possible exceptions in Miele exception types.
+ *
+ * @param request The {@link Request} to send.
+ * @return The obtained {@link ContentResponse}.
+ * @throws MieleWebserviceException if an irrecoverable error occurred.
+ * @throws MieleWebserviceTransientException if a recoverable error occurred.
+ */
+ private ContentResponse sendRequest(Request request) {
+ try {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Send {} request to Miele webservice on uri {}",
+ Optional.ofNullable(request).map(Request::getMethod).orElse("null"),
+ Optional.ofNullable(request).map(Request::getURI).map(URI::toString).orElse("null"));
+ }
+
+ ContentResponse response = request.send();
+ logger.debug("Received response with status code {}", response.getStatus());
+ return response;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new MieleWebserviceException("Interrupted.", e, ConnectionError.REQUEST_INTERRUPTED);
+ } catch (TimeoutException e) {
+ throw new MieleWebserviceTransientException("Request timed out.", e, ConnectionError.TIMEOUT);
+ } catch (ExecutionException e) {
+ throw new MieleWebserviceException("Request execution failed.", e,
+ ConnectionError.REQUEST_EXECUTION_FAILED);
+ }
+ }
+
+ /**
+ * Gets all available device actions.
+ *
+ * @param deviceId The unique device ID.
+ *
+ * @throws MieleWebserviceException if an error occurs during webservice requests or content parsing.
+ * @throws MieleWebserviceTransientException if an error occurs during webservice requests or content parsing that
+ * is recoverable by retrying the operation.
+ * @throws AuthorizationFailedException if the authorization against the webservice failed.
+ * @throws TooManyRequestsException if too many requests have been made against the webservice recently.
+ */
+ private Actions getActions(String deviceId) {
+ Optional<String> accessToken = this.accessToken;
+ if (!accessToken.isPresent()) {
+ throw new MieleWebserviceException("Missing access token.", ConnectionError.AUTHORIZATION_FAILED);
+ }
+
+ try {
+ logger.debug("Fetch action state description for Miele device {}", deviceId);
+ Request request = requestFactory.createGetRequest(String.format(ENDPOINT_ACTIONS, deviceId),
+ accessToken.get());
+ ContentResponse response = sendRequest(request);
+ HttpUtil.checkHttpSuccess(response);
+ Actions actions = GSON.fromJson(response.getContentAsString(), Actions.class);
+ if (actions == null) {
+ throw new MieleWebserviceTransientException("Failed to parse response message.",
+ ConnectionError.RESPONSE_MALFORMED);
+ }
+ return actions;
+ } catch (JsonSyntaxException e) {
+ throw new MieleWebserviceTransientException("Failed to parse response message.", e,
+ ConnectionError.RESPONSE_MALFORMED);
+ }
+ }
+
+ /**
+ * Performs a PUT request to the actions endpoint for the specified device.
+ *
+ * @param deviceId The ID of the device to PUT for.
+ * @param json The Json body to send with the request.
+ * @throws MieleWebserviceException if an error occurs during webservice requests or content parsing.
+ * @throws MieleWebserviceTransientException if an error occurs during webservice requests or content parsing that
+ * is recoverable by retrying the operation.
+ * @throws AuthorizationFailedException if the authorization against the webservice failed.
+ * @throws TooManyRequestsException if too many requests have been made against the webservice recently.
+ */
+ private void putActions(String deviceId, String json) {
+ retryStrategy.performRetryableOperation(() -> {
+ Optional<String> accessToken = this.accessToken;
+ if (!accessToken.isPresent()) {
+ throw new MieleWebserviceException("Missing access token.", ConnectionError.AUTHORIZATION_FAILED);
+ }
+
+ Request request = requestFactory.createPutRequest(String.format(ENDPOINT_ACTIONS, deviceId),
+ accessToken.get(), json);
+ ContentResponse response = sendRequest(request);
+ HttpUtil.checkHttpSuccess(response);
+ }, e -> {
+ logger.warn("Failed to perform PUT request: {}. Retrying...", e.getMessage());
+ });
+ }
+
+ @Override
+ public void dispatchDeviceState(String deviceIdentifier) {
+ deviceStateDispatcher.dispatchDeviceState(deviceIdentifier);
+ }
+
+ @Override
+ public void addDeviceStateListener(DeviceStateListener listener) {
+ deviceStateDispatcher.addListener(listener);
+ }
+
+ @Override
+ public void removeDeviceStateListener(DeviceStateListener listener) {
+ deviceStateDispatcher.removeListener(listener);
+ }
+
+ @Override
+ public void addConnectionStatusListener(ConnectionStatusListener listener) {
+ connectionStatusListeners.add(listener);
+ }
+
+ @Override
+ public void removeConnectionStatusListener(ConnectionStatusListener listener) {
+ connectionStatusListeners.remove(listener);
+ }
+
+ @Override
+ public void close() throws Exception {
+ requestFactory.close();
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Factory creating {@link DefaultMieleWebservice} instances.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class DefaultMieleWebserviceFactory implements MieleWebserviceFactory {
+ @Override
+ public MieleWebservice create(MieleWebserviceConfiguration configuration) {
+ return new DefaultMieleWebservice(configuration);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Device;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceCollection;
+
+/**
+ * A cache for {@link Device} objects associated with unique identifiers.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+class DeviceCache {
+ private final Map<String, Device> entries = new HashMap<>();
+
+ public void replaceAllDevices(DeviceCollection deviceCollection) {
+ clear();
+ deviceCollection.getDeviceIdentifiers().stream().forEach(i -> entries.put(i, deviceCollection.getDevice(i)));
+ }
+
+ public void clear() {
+ entries.clear();
+ }
+
+ public Set<String> getDeviceIds() {
+ return entries.keySet();
+ }
+
+ public Optional<Device> getDevice(String deviceIdentifier) {
+ return Optional.ofNullable(entries.get(deviceIdentifier));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Actions;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceCollection;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handles event dispatching to {@link DeviceStateListener}s.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class DeviceStateDispatcher {
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ private final List<DeviceStateListener> listeners = new CopyOnWriteArrayList<>();
+ private Set<String> previousDeviceIdentifiers = new HashSet<>();
+ private final DeviceCache cache = new DeviceCache();
+
+ /**
+ * Adds a listener. The listener will be immediately invoked with the current status of all known devices.
+ *
+ * @param listener The listener to add.
+ */
+ public void addListener(DeviceStateListener listener) {
+ if (listeners.contains(listener)) {
+ logger.warn("Listener '{}' was registered multiple times.", listener);
+ }
+ listeners.add(listener);
+
+ cache.getDeviceIds().forEach(deviceIdentifier -> cache.getDevice(deviceIdentifier)
+ .ifPresent(device -> listener.onDeviceStateUpdated(new DeviceState(deviceIdentifier, device))));
+ }
+
+ /**
+ * Removes a listener.
+ */
+ public void removeListener(DeviceStateListener listener) {
+ listeners.remove(listener);
+ }
+
+ /**
+ * Clears the internal device state cache.
+ */
+ public void clearCache() {
+ cache.clear();
+ }
+
+ /**
+ * Dispatches device status updates to all registered {@link DeviceStateListener}. This includes device removal.
+ *
+ * @param devices {@link DeviceCollection} which contains the state information to dispatch.
+ */
+ public void dispatchDeviceStateUpdates(DeviceCollection devices) {
+ cache.replaceAllDevices(devices);
+ dispatchDevicesRemoved(devices);
+ cache.getDeviceIds().forEach(this::dispatchDeviceState);
+ }
+
+ /**
+ * Dispatches the cached state of the device identified by the given device identifier.
+ */
+ public void dispatchDeviceState(String deviceIdentifier) {
+ cache.getDevice(deviceIdentifier).ifPresent(device -> listeners
+ .forEach(listener -> listener.onDeviceStateUpdated(new DeviceState(deviceIdentifier, device))));
+ }
+
+ /**
+ * Dispatches device action updates to all registered {@link DeviceStateListener}.
+ *
+ * @param deviceId ID of the device to dispatch the {@link Actions} for.
+ * @param actions {@link Actions} to dispatch.
+ */
+ public void dispatchActionStateUpdates(String deviceId, Actions actions) {
+ listeners.forEach(listener -> listener.onProcessActionUpdated(new ActionsState(deviceId, actions)));
+ }
+
+ private void dispatchDevicesRemoved(DeviceCollection devices) {
+ Set<String> presentDeviceIdentifiers = devices.getDeviceIdentifiers();
+ Set<String> removedDeviceIdentifiers = previousDeviceIdentifiers;
+ removedDeviceIdentifiers.removeAll(presentDeviceIdentifiers);
+
+ previousDeviceIdentifiers = devices.getDeviceIdentifiers();
+
+ removedDeviceIdentifiers
+ .forEach(deviceIdentifier -> listeners.forEach(listener -> listener.onDeviceRemoved(deviceIdentifier)));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+
+/**
+ * Listener for the device states.
+ *
+ * @author Björn Lange and Roland Edelhoff - Initial contribution
+ */
+@NonNullByDefault
+public interface DeviceStateListener {
+ /**
+ * Invoked when new status information is available for a device.
+ *
+ * @param deviceState The device state information.
+ */
+ void onDeviceStateUpdated(DeviceState deviceState);
+
+ /**
+ * Invoked when a new process action is available for a device.
+ *
+ * @param ActionsState The action state information.
+ */
+ void onProcessActionUpdated(ActionsState actionState);
+
+ /**
+ * Invoked when a device got removed from the Miele cloud and no information is available about it.
+ *
+ * @param deviceIdentifier The identifier of the removed device.
+ */
+ void onDeviceRemoved(String deviceIdentifier);
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Response;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ErrorMessage;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.MieleSyntaxException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceTransientException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
+
+/**
+ * Holds utility functions for working with HTTP.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class HttpUtil {
+ private static final String RETRY_AFTER_HEADER_FIELD_NAME = "Retry-After";
+
+ private HttpUtil() {
+ throw new IllegalStateException("This class must not be instantiated");
+ }
+
+ /**
+ * Checks whether the HTTP status given in {@code response} is a success state. In case an error state is obtained,
+ * exceptions are thrown.
+ *
+ * @param response The response to check.
+ * @throws MieleWebserviceTransientException if the status indicates a transient HTTP error.
+ * @throws MieleWebserviceException if the status indicates another HTTP error.
+ * @throws AuthorizationFailedException if the status indicates an authorization failure.
+ * @throws TooManyRequestsException if the status indicates that too many requests have been made against the remote
+ * endpoint.
+ */
+ public static void checkHttpSuccess(Response response) {
+ if (isHttpSuccessStatus(response.getStatus())) {
+ return;
+ }
+
+ String exceptionMessage = getHttpErrorMessageFromCloudResponse(response);
+
+ switch (response.getStatus()) {
+ case 401:
+ throw new AuthorizationFailedException(exceptionMessage);
+ case 429:
+ String retryAfter = null;
+ if (response.getHeaders().containsKey(RETRY_AFTER_HEADER_FIELD_NAME)) {
+ retryAfter = response.getHeaders().get(RETRY_AFTER_HEADER_FIELD_NAME);
+ }
+ throw new TooManyRequestsException(exceptionMessage, retryAfter);
+ case 500:
+ throw new MieleWebserviceTransientException(exceptionMessage, ConnectionError.SERVER_ERROR);
+ case 503:
+ throw new MieleWebserviceTransientException(exceptionMessage, ConnectionError.SERVICE_UNAVAILABLE);
+ default:
+ throw new MieleWebserviceException(exceptionMessage, ConnectionError.OTHER_HTTP_ERROR);
+ }
+ }
+
+ /**
+ * Gets whether {@code httpStatus} is a HTTP error code from the 200 range (success).
+ */
+ private static boolean isHttpSuccessStatus(int httpStatus) {
+ return httpStatus / 100 == 2;
+ }
+
+ private static String getHttpErrorMessageFromCloudResponse(Response response) {
+ String exceptionMessage = "HTTP error " + response.getStatus() + ": " + response.getReason();
+
+ if (response instanceof ContentResponse) {
+ try {
+ ErrorMessage errorMessage = ErrorMessage.fromJson(((ContentResponse) response).getContentAsString());
+ exceptionMessage += "\nCloud returned message: " + errorMessage.getMessage();
+ } catch (MieleSyntaxException e) {
+ exceptionMessage += "\nCloud returned invalid message.";
+ }
+ }
+ return exceptionMessage;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
+import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
+
+/**
+ * The {@link MieleWebservice} serves as an interface to the Miele REST API and wraps all calls to it.
+ *
+ * @author Björn Lange and Roland Edelhoff - Initial contribution
+ */
+@NonNullByDefault
+public interface MieleWebservice extends AutoCloseable {
+ /**
+ * Sets the OAuth2 access token to use.
+ */
+ void setAccessToken(String accessToken);
+
+ /**
+ * Returns whether an access token is available.
+ */
+ boolean hasAccessToken();
+
+ /**
+ * Connects to the Miele webservice SSE endpoint and starts receiving events.
+ */
+ void connectSse();
+
+ /**
+ * Disconnects a running connection from the Miele SSE endpoint.
+ */
+ void disconnectSse();
+
+ /**
+ * Fetches the available actions for the device with the given {@code deviceId}.
+ *
+ * @param deviceId The unique ID of the device to fetch the available actions for.
+ * @throws MieleWebserviceException if an error occurs during webservice requests or content parsing.
+ * @throws AuthorizationFailedException if the authorization against the webservice failed.
+ * @throws TooManyRequestsException if too many requests have been made against the webservice recently.
+ */
+ void fetchActions(String deviceId);
+
+ /**
+ * Performs a PUT operation with the given {@code processAction}.
+ *
+ * @param deviceId ID of the device to trigger the action for.
+ * @param processAction The action to perform.
+ * @throws MieleWebserviceException if an error occurs during webservice requests or content parsing.
+ * @throws AuthorizationFailedException if the authorization against the webservice failed.
+ * @throws TooManyRequestsException if too many requests have been made against the webservice recently.
+ */
+ void putProcessAction(String deviceId, ProcessAction processAction);
+
+ /**
+ * Performs a PUT operation enabling or disabling the device's light.
+ *
+ * @param deviceId ID of the device to trigger the action for.
+ * @param enabled {@code true} to enable or {@code false} to disable the light.
+ * @throws MieleWebserviceException if an error occurs during webservice requests or content parsing.
+ * @throws AuthorizationFailedException if the authorization against the webservice failed.
+ * @throws TooManyRequestsException if too many requests have been made against the webservice recently.
+ */
+ void putLight(String deviceId, boolean enabled);
+
+ /**
+ * Performs a PUT operation switching the device on or off.
+ *
+ * @param deviceId ID of the device to trigger the action for.
+ * @param enabled {@code true} to switch on or {@code false} to switch off the device.
+ * @throws MieleWebserviceException if an error occurs during webservice requests or content parsing.
+ * @throws AuthorizationFailedException if the authorization against the webservice failed.
+ * @throws TooManyRequestsException if too many requests have been made against the webservice recently.
+ */
+ void putPowerState(String deviceId, boolean enabled);
+
+ /**
+ * Performs a PUT operation setting the active program.
+ *
+ * @param deviceId ID of the device to trigger the action for.
+ * @param program The program to activate.
+ * @throws MieleWebserviceException if an error occurs during webservice requests or content parsing.
+ * @throws AuthorizationFailedException if the authorization against the webservice failed.
+ * @throws TooManyRequestsException if too many requests have been made against the webservice recently.
+ */
+ void putProgram(String deviceId, long programId);
+
+ /**
+ * Performs a logout and invalidates the current OAuth2 token. This operation is assumed to work on the first try
+ * and is never retried. HTTP errors are ignored.
+ *
+ * @throws MieleWebserviceException if the request operation fails.
+ */
+ void logout();
+
+ /**
+ * Dispatches the cached state of the device identified by the given device identifier.
+ */
+ void dispatchDeviceState(String deviceIdentifier);
+
+ /**
+ * Adds a {@link DeviceStateListener}.
+ *
+ * @param listener The listener to add.
+ */
+ void addDeviceStateListener(DeviceStateListener listener);
+
+ /**
+ * Removes a {@link DeviceStateListener}.
+ *
+ * @param listener The listener to remove.
+ */
+ void removeDeviceStateListener(DeviceStateListener listener);
+
+ /**
+ * Adds a {@link ConnectionStatusListener}.
+ *
+ * @param listener The listener to add.
+ */
+ void addConnectionStatusListener(ConnectionStatusListener listener);
+
+ /**
+ * Removes a {@link ConnectionStatusListener}.
+ *
+ * @param listener The listener to remove.
+ */
+ void removeConnectionStatusListener(ConnectionStatusListener listener);
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import java.util.concurrent.ScheduledExecutorService;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider;
+import org.openhab.core.io.net.http.HttpClientFactory;
+
+/**
+ * Represents a webservice configuration.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public final class MieleWebserviceConfiguration {
+ private final HttpClientFactory httpClientFactory;
+ private final LanguageProvider languageProvider;
+ private final OAuthTokenRefresher tokenRefresher;
+ private final String serviceHandle;
+ private final ScheduledExecutorService scheduler;
+
+ private MieleWebserviceConfiguration(MieleWebserviceConfigurationBuilder builder) {
+ this.httpClientFactory = getOrThrow(builder.httpClientFactory, "httpClientFactory");
+ this.languageProvider = getOrThrow(builder.languageProvider, "languageProvider");
+ this.tokenRefresher = getOrThrow(builder.tokenRefresher, "tokenRefresher");
+ this.serviceHandle = getOrThrow(builder.serviceHandle, "serviceHandle");
+ this.scheduler = getOrThrow(builder.scheduler, "scheduler");
+ }
+
+ private static <T> T getOrThrow(@Nullable T object, String objectName) {
+ if (object == null) {
+ throw new IllegalArgumentException(objectName + " must not be null");
+ }
+ return object;
+ }
+
+ /**
+ * Gets the factory to use for HttpClient construction.
+ */
+ public HttpClientFactory getHttpClientFactory() {
+ return httpClientFactory;
+ }
+
+ /**
+ * Gets the provider for the language to use when making requests to the API.
+ */
+ public LanguageProvider getLanguageProvider() {
+ return languageProvider;
+ }
+
+ /**
+ * Gets the refresher for OAuth tokens.
+ */
+ public OAuthTokenRefresher getTokenRefresher() {
+ return tokenRefresher;
+ }
+
+ /**
+ * Gets the handle referring to the OAuth tokens in the framework's persistent storage.
+ */
+ public String getServiceHandle() {
+ return serviceHandle;
+ }
+
+ /**
+ * Gets the system wide scheduler.
+ */
+ public ScheduledExecutorService getScheduler() {
+ return scheduler;
+ }
+
+ public static MieleWebserviceConfigurationBuilder builder() {
+ return new MieleWebserviceConfigurationBuilder();
+ }
+
+ public static final class MieleWebserviceConfigurationBuilder {
+ @Nullable
+ private HttpClientFactory httpClientFactory;
+ @Nullable
+ private LanguageProvider languageProvider;
+ @Nullable
+ private OAuthTokenRefresher tokenRefresher;
+ @Nullable
+ private String serviceHandle;
+ @Nullable
+ private ScheduledExecutorService scheduler;
+
+ private MieleWebserviceConfigurationBuilder() {
+ }
+
+ public MieleWebserviceConfigurationBuilder withHttpClientFactory(HttpClientFactory httpClientFactory) {
+ this.httpClientFactory = httpClientFactory;
+ return this;
+ }
+
+ public MieleWebserviceConfigurationBuilder withLanguageProvider(LanguageProvider languageProvider) {
+ this.languageProvider = languageProvider;
+ return this;
+ }
+
+ public MieleWebserviceConfigurationBuilder withTokenRefresher(OAuthTokenRefresher tokenRefresher) {
+ this.tokenRefresher = tokenRefresher;
+ return this;
+ }
+
+ public MieleWebserviceConfigurationBuilder withServiceHandle(String serviceHandle) {
+ this.serviceHandle = serviceHandle;
+ return this;
+ }
+
+ public MieleWebserviceConfigurationBuilder withScheduler(ScheduledExecutorService scheduler) {
+ this.scheduler = scheduler;
+ return this;
+ }
+
+ public MieleWebserviceConfiguration build() {
+ return new MieleWebserviceConfiguration(this);
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Factory for creating {@link MieleWebservice} instances.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public interface MieleWebserviceFactory {
+ /**
+ * Creates a new {@link MieleWebservice}.
+ *
+ * @param configuration The configuration holding all required parameters to construct the instance.
+ * @return A new {@link MieleWebservice}.
+ */
+ public MieleWebservice create(MieleWebserviceConfiguration configuration);
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implementation of {@link MieleWebservice} that serves as a replacement when no webservice is available.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class UnavailableMieleWebservice implements MieleWebservice {
+ public static final UnavailableMieleWebservice INSTANCE = new UnavailableMieleWebservice();
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ private UnavailableMieleWebservice() {
+ }
+
+ @Override
+ public void setAccessToken(String accessToken) {
+ logger.warn("Cannot set access token: The Miele cloud service is not available.");
+ }
+
+ @Override
+ public boolean hasAccessToken() {
+ logger.warn("There is no access token: The Miele cloud service is not available.");
+ return false;
+ }
+
+ @Override
+ public void connectSse() {
+ logger.warn("Cannot connect to SSE stream: The Miele cloud service is not available.");
+ }
+
+ @Override
+ public void disconnectSse() {
+ logger.warn("Cannot disconnect from SSE stream: The Miele cloud service is not available.");
+ }
+
+ @Override
+ public void fetchActions(String deviceId) {
+ logger.warn("Cannot fetch actions for device '{}': The Miele cloud service is not available.", deviceId);
+ }
+
+ @Override
+ public void putProcessAction(String deviceId, ProcessAction processAction) {
+ logger.warn("Cannot perform '{}' operation for device '{}': The Miele cloud service is not available.",
+ processAction, deviceId);
+ }
+
+ @Override
+ public void putLight(String deviceId, boolean enabled) {
+ logger.warn("Cannot set light state to '{}' for device '{}': The Miele cloud service is not available.",
+ enabled ? "ON" : "OFF", deviceId);
+ }
+
+ @Override
+ public void putPowerState(String deviceId, boolean enabled) {
+ logger.warn("Cannot set power state to '{}' for device '{}': The Miele cloud service is not available.",
+ enabled ? "ON" : "OFF", deviceId);
+ }
+
+ @Override
+ public void putProgram(String deviceId, long programId) {
+ logger.warn("Cannot activate program with ID '{}' for device '{}': The Miele cloud service is not available.",
+ programId, deviceId);
+ }
+
+ @Override
+ public void logout() {
+ logger.warn("Cannot logout: The Miele cloud service is not available.");
+ }
+
+ @Override
+ public void dispatchDeviceState(String deviceIdentifier) {
+ logger.warn("Cannot re-emit device state for device '{}': The Miele cloud service is not available.",
+ deviceIdentifier);
+ }
+
+ @Override
+ public void addDeviceStateListener(DeviceStateListener listener) {
+ logger.warn("Cannot add listener for all devices: The Miele cloud service is not available.");
+ }
+
+ @Override
+ public void removeDeviceStateListener(DeviceStateListener listener) {
+ logger.warn("Cannot remove listener: The Miele cloud service is not available.");
+ }
+
+ @Override
+ public void addConnectionStatusListener(ConnectionStatusListener listener) {
+ logger.warn("Cannot add connection error listener: The Miele cloud service is not available.");
+ }
+
+ @Override
+ public void removeConnectionStatusListener(ConnectionStatusListener listener) {
+ logger.warn("Cannot remove listener: The Miele cloud service is not available.");
+ }
+
+ @Override
+ public void close() throws Exception {
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Actions;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Light;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
+
+/**
+ * Provides convenient access to the list of actions that can be performed with a device.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ */
+@NonNullByDefault
+public class ActionsState {
+
+ private final String deviceIdentifier;
+ private final Optional<Actions> actions;
+
+ public ActionsState(String deviceIdentifier, @Nullable Actions actions) {
+ this.deviceIdentifier = deviceIdentifier;
+ this.actions = Optional.ofNullable(actions);
+ }
+
+ /**
+ * Gets the unique identifier of the device to which this state refers.
+ */
+ public String getDeviceIdentifier() {
+ return deviceIdentifier;
+ }
+
+ /**
+ * Gets whether the device can be started.
+ */
+ public boolean canBeStarted() {
+ return actions.map(Actions::getProcessAction).map(a -> a.contains(ProcessAction.START)).orElse(false);
+ }
+
+ /**
+ * Gets whether the device can be stopped.
+ */
+ public boolean canBeStopped() {
+ return actions.map(Actions::getProcessAction).map(a -> a.contains(ProcessAction.STOP)).orElse(false);
+ }
+
+ /**
+ * Gets whether the device can be paused.
+ */
+ public boolean canBePaused() {
+ return actions.map(Actions::getProcessAction).map(a -> a.contains(ProcessAction.PAUSE)).orElse(false);
+ }
+
+ /**
+ * Gets whether supercooling can be controlled.
+ */
+ public boolean canContolSupercooling() {
+ return canStartSupercooling() || canStopSupercooling();
+ }
+
+ /**
+ * Gets whether supercooling can be started.
+ */
+ public boolean canStartSupercooling() {
+ return actions.map(Actions::getProcessAction).map(a -> a.contains(ProcessAction.START_SUPERCOOLING))
+ .orElse(false);
+ }
+
+ /**
+ * Gets whether supercooling can be stopped.
+ */
+ public boolean canStopSupercooling() {
+ return actions.map(Actions::getProcessAction).map(a -> a.contains(ProcessAction.STOP_SUPERCOOLING))
+ .orElse(false);
+ }
+
+ /**
+ * Gets whether superfreezing can be controlled.
+ */
+ public boolean canControlSuperfreezing() {
+ return canStartSuperfreezing() || canStopSuperfreezing();
+ }
+
+ /**
+ * Gets whether superfreezing can be started.
+ */
+ public boolean canStartSuperfreezing() {
+ return actions.map(Actions::getProcessAction).map(a -> a.contains(ProcessAction.START_SUPERFREEZING))
+ .orElse(false);
+ }
+
+ /**
+ * Gets whether superfreezing can be stopped.
+ */
+ public boolean canStopSuperfreezing() {
+ return actions.map(Actions::getProcessAction).map(a -> a.contains(ProcessAction.STOP_SUPERFREEZING))
+ .orElse(false);
+ }
+
+ /**
+ * Gets whether light can be enabled.
+ */
+ public boolean canEnableLight() {
+ return actions.map(Actions::getLight).map(a -> a.contains(Light.ENABLE)).orElse(false);
+ }
+
+ /**
+ * Gets whether light can be disabled.
+ */
+ public boolean canDisableLight() {
+ return actions.map(Actions::getLight).map(a -> a.contains(Light.DISABLE)).orElse(false);
+ }
+
+ /**
+ * Gets whether the device can be switched on.
+ */
+ public boolean canBeSwitchedOn() {
+ return actions.flatMap(Actions::getPowerOn).map(Boolean.TRUE::equals).orElse(false);
+ }
+
+ /**
+ * Gets whether the device can be switched off.
+ */
+ public boolean canBeSwitchedOff() {
+ return actions.flatMap(Actions::getPowerOff).map(Boolean.TRUE::equals).orElse(false);
+ }
+
+ /**
+ * Gets whether the light can be controlled.
+ */
+ public boolean canControlLight() {
+ return canEnableLight() || canDisableLight();
+ }
+
+ /**
+ * Gets whether the active program can be set.
+ */
+ public boolean canSetActiveProgramId() {
+ return !actions.map(Actions::getProgramId).map(List::isEmpty).orElse(true);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(actions, deviceIdentifier);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ ActionsState other = (ActionsState) obj;
+ return Objects.equals(actions, other.actions) && Objects.equals(deviceIdentifier, other.deviceIdentifier);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Provides easy access to temperature values mapped for cooling devices.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class CoolingDeviceTemperatureState {
+ private final DeviceState deviceState;
+
+ public CoolingDeviceTemperatureState(DeviceState deviceState) {
+ this.deviceState = deviceState;
+ }
+
+ /**
+ * Gets the current temperature of the fridge part of the device.
+ *
+ * @return The current temperature of the fridge part of the device.
+ */
+ public Optional<Integer> getFridgeTemperature() {
+ switch (deviceState.getRawType()) {
+ case FRIDGE:
+ return deviceState.getTemperature(0);
+
+ case FRIDGE_FREEZER_COMBINATION:
+ return deviceState.getTemperature(0);
+
+ default:
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * Gets the target temperature of the fridge part of the device.
+ *
+ * @return The target temperature of the fridge part of the device.
+ */
+ public Optional<Integer> getFridgeTargetTemperature() {
+ switch (deviceState.getRawType()) {
+ case FRIDGE:
+ return deviceState.getTargetTemperature(0);
+
+ case FRIDGE_FREEZER_COMBINATION:
+ return deviceState.getTargetTemperature(0);
+
+ default:
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * Gets the current temperature of the freezer part of the device.
+ *
+ * @return The current temperature of the freezer part of the device.
+ */
+ public Optional<Integer> getFreezerTemperature() {
+ switch (deviceState.getRawType()) {
+ case FREEZER:
+ return deviceState.getTemperature(0);
+
+ case FRIDGE_FREEZER_COMBINATION:
+ return deviceState.getTemperature(1);
+
+ default:
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * Gets the target temperature of the freezer part of the device.
+ *
+ * @return The target temperature of the freezer part of the device.
+ */
+ public Optional<Integer> getFreezerTargetTemperature() {
+ switch (deviceState.getRawType()) {
+ case FREEZER:
+ return deviceState.getTargetTemperature(0);
+
+ case FRIDGE_FREEZER_COMBINATION:
+ return deviceState.getTargetTemperature(1);
+
+ default:
+ return Optional.empty();
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Device;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceIdentLabel;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DryingStep;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Ident;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Light;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.PlateStep;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProgramId;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProgramPhase;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.RemoteEnable;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.SpinningSpeed;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.State;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Status;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Temperature;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Type;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.VentilationStep;
+
+/**
+ * This immutable class provides methods to extract the device state information in a comfortable way.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Introduced null handling
+ * @author Benjamin Bolte - Add pre-heat finished, plate step, door state, door alarm, info state channel and map signal
+ * flags from API
+ * @author Björn Lange - Add elapsed time channel, dish warmer and robotic vacuum cleaner things
+ */
+@NonNullByDefault
+public class DeviceState {
+
+ private final String deviceIdentifier;
+
+ private final Optional<Device> device;
+
+ public DeviceState(String deviceIdentifier, @Nullable Device device) {
+ this.deviceIdentifier = deviceIdentifier;
+ this.device = Optional.ofNullable(device);
+ }
+
+ /**
+ * Gets the unique identifier for this device.
+ *
+ * @return The unique identifier for this device.
+ */
+ public String getDeviceIdentifier() {
+ return deviceIdentifier;
+ }
+
+ /**
+ * Gets the main operation status of the device.
+ *
+ * @return The main operation status of the device.
+ */
+ public Optional<String> getStatus() {
+ return device.flatMap(Device::getState).flatMap(State::getStatus).flatMap(Status::getValueLocalized);
+ }
+
+ /**
+ * Gets the raw main operation status of the device.
+ *
+ * @return The raw main operation status of the device.
+ */
+ public Optional<Integer> getStatusRaw() {
+ return device.flatMap(Device::getState).flatMap(State::getStatus).flatMap(Status::getValueRaw);
+ }
+
+ /**
+ * Gets the raw operation status of the device parsed to a {@link StateType}.
+ *
+ * @return The raw operation status of the device parsed to a {@link StateType}.
+ */
+ public Optional<StateType> getStateType() {
+ return device.flatMap(Device::getState).flatMap(State::getStatus).flatMap(Status::getValueRaw)
+ .flatMap(StateType::fromCode);
+ }
+
+ /**
+ * Gets the currently selected program type of the device.
+ *
+ * @return The currently selected program type of the device.
+ */
+ public Optional<String> getSelectedProgram() {
+ if (deviceIsInOffState()) {
+ return Optional.empty();
+ }
+ return device.flatMap(Device::getState).flatMap(State::getProgramId).flatMap(ProgramId::getValueLocalized);
+ }
+
+ /**
+ * Gets the selected program ID.
+ *
+ * @return The selected program ID.
+ */
+ public Optional<Long> getSelectedProgramId() {
+ if (deviceIsInOffState()) {
+ return Optional.empty();
+ }
+ return device.flatMap(Device::getState).flatMap(State::getProgramId).flatMap(ProgramId::getValueRaw);
+ }
+
+ /**
+ * Gets the currently active phase of the active program.
+ *
+ * @return The currently active phase of the active program.
+ */
+ public Optional<String> getProgramPhase() {
+ if (deviceIsInOffState()) {
+ return Optional.empty();
+ }
+ return device.flatMap(Device::getState).flatMap(State::getProgramPhase)
+ .flatMap(ProgramPhase::getValueLocalized);
+ }
+
+ /**
+ * Gets the currently active raw phase of the active program.
+ *
+ * @return The currently active raw phase of the active program.
+ */
+ public Optional<Integer> getProgramPhaseRaw() {
+ if (deviceIsInOffState()) {
+ return Optional.empty();
+ }
+ return device.flatMap(Device::getState).flatMap(State::getProgramPhase).flatMap(ProgramPhase::getValueRaw);
+ }
+
+ /**
+ * Gets the currently selected drying step.
+ *
+ * @return The currently selected drying step.
+ */
+ public Optional<String> getDryingTarget() {
+ if (deviceIsInOffState()) {
+ return Optional.empty();
+ }
+ return device.flatMap(Device::getState).flatMap(State::getDryingStep).flatMap(DryingStep::getValueLocalized);
+ }
+
+ /**
+ * Gets the currently selected raw drying step.
+ *
+ * @return The currently selected raw drying step.
+ */
+ public Optional<Integer> getDryingTargetRaw() {
+ if (deviceIsInOffState()) {
+ return Optional.empty();
+ }
+ return device.flatMap(Device::getState).flatMap(State::getDryingStep).flatMap(DryingStep::getValueRaw);
+ }
+
+ /**
+ * Calculates if pre-heating the oven has finished.
+ *
+ * @return Whether pre-heating the oven has finished.
+ */
+ public Optional<Boolean> hasPreHeatFinished() {
+ if (deviceIsInOffState()) {
+ return Optional.empty();
+ }
+
+ Optional<Integer> targetTemperature = getTargetTemperature(0);
+ Optional<Integer> currentTemperature = getTemperature(0);
+
+ if (!targetTemperature.isPresent() || !currentTemperature.isPresent()) {
+ return Optional.empty();
+ }
+
+ return Optional.of(isInState(StateType.RUNNING) && currentTemperature.get() >= targetTemperature.get());
+ }
+
+ /**
+ * Gets the target temperature with the given index.
+ *
+ * @return The target temperature with the given index.
+ */
+ public Optional<Integer> getTargetTemperature(int index) {
+ if (deviceIsInOffState()) {
+ return Optional.empty();
+ }
+ return device.flatMap(Device::getState).map(State::getTargetTemperature).flatMap(l -> getOrNull(l, index))
+ .flatMap(Temperature::getValueLocalized);
+ }
+
+ /**
+ * Gets the current temperature of the device for the given index.
+ *
+ * @param index The index of the device zone for which the temperature shall be obtained.
+ * @return The target temperature if available.
+ */
+ public Optional<Integer> getTemperature(int index) {
+ if (deviceIsInOffState()) {
+ return Optional.empty();
+ }
+
+ return device.flatMap(Device::getState).map(State::getTemperature).flatMap(l -> getOrNull(l, index))
+ .flatMap(Temperature::getValueLocalized);
+ }
+
+ /**
+ * Gets the remaining time of the active program.
+ *
+ * @return The remaining time in seconds.
+ */
+ public Optional<Integer> getRemainingTime() {
+ if (deviceIsInOffState()) {
+ return Optional.empty();
+ }
+ return device.flatMap(Device::getState).flatMap(State::getRemainingTime).flatMap(this::toSeconds);
+ }
+
+ /**
+ * Gets the elapsed time of the active program.
+ *
+ * @return The elapsed time in seconds.
+ */
+ public Optional<Integer> getElapsedTime() {
+ if (deviceIsInOffState()) {
+ return Optional.empty();
+ }
+ return device.flatMap(Device::getState).flatMap(State::getElapsedTime).flatMap(this::toSeconds);
+ }
+
+ /**
+ * Gets the relative start time of the active program.
+ *
+ * @return The delayed start time in seconds.
+ */
+ public Optional<Integer> getStartTime() {
+ if (deviceIsInOffState()) {
+ return Optional.empty();
+ }
+ return device.flatMap(Device::getState).flatMap(State::getStartTime).flatMap(this::toSeconds);
+ }
+
+ /**
+ * Gets the "fullRemoteControl" state information of the device. If this flag is true ALL remote control actions
+ * of the device can be triggered.
+ *
+ * @return Whether the device can be remote controlled.
+ */
+ public Optional<Boolean> isRemoteControlEnabled() {
+ return device.flatMap(Device::getState).flatMap(State::getRemoteEnable)
+ .flatMap(RemoteEnable::getFullRemoteControl);
+ }
+
+ /**
+ * Calculates the program process.
+ *
+ * @return The progress of the active program in percent.
+ */
+ public Optional<Integer> getProgress() {
+ if (deviceIsInOffState()) {
+ return Optional.empty();
+ }
+
+ Optional<Double> elapsedTime = device.flatMap(Device::getState).flatMap(State::getElapsedTime)
+ .flatMap(this::toSeconds).map(Integer::doubleValue);
+ Optional<Double> remainingTime = device.flatMap(Device::getState).flatMap(State::getRemainingTime)
+ .flatMap(this::toSeconds).map(Integer::doubleValue);
+
+ if (elapsedTime.isPresent() && remainingTime.isPresent()
+ && (elapsedTime.get() != 0 || remainingTime.get() != 0)) {
+ return Optional.of((int) ((elapsedTime.get() / (elapsedTime.get() + remainingTime.get())) * 100.0));
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ private Optional<Integer> toSeconds(List<Integer> time) {
+ if (time.size() != 2) {
+ return Optional.empty();
+ }
+ return Optional.of((time.get(0) * 60 + time.get(1)) * 60);
+ }
+
+ /**
+ * Gets the spinning speed.
+ *
+ * @return The spinning speed.
+ */
+ public Optional<String> getSpinningSpeed() {
+ if (deviceIsInOffState()) {
+ return Optional.empty();
+ }
+ return device.flatMap(Device::getState).flatMap(State::getSpinningSpeed).flatMap(SpinningSpeed::getValueRaw)
+ .map(String::valueOf);
+ }
+
+ /**
+ * Gets the raw spinning speed.
+ *
+ * @return The raw spinning speed.
+ */
+ public Optional<Integer> getSpinningSpeedRaw() {
+ if (deviceIsInOffState()) {
+ return Optional.empty();
+ }
+ return device.flatMap(Device::getState).flatMap(State::getSpinningSpeed).flatMap(SpinningSpeed::getValueRaw);
+ }
+
+ /**
+ * Gets the ventilation step.
+ *
+ * @return The ventilation step.
+ */
+ public Optional<String> getVentilationStep() {
+ if (deviceIsInOffState()) {
+ return Optional.empty();
+ }
+ return device.flatMap(Device::getState).flatMap(State::getVentilationStep)
+ .flatMap(VentilationStep::getValueLocalized).map(Object::toString);
+ }
+
+ /**
+ * Gets the raw ventilation step.
+ *
+ * @return The raw ventilation step.
+ */
+ public Optional<Integer> getVentilationStepRaw() {
+ if (deviceIsInOffState()) {
+ return Optional.empty();
+ }
+ return device.flatMap(Device::getState).flatMap(State::getVentilationStep)
+ .flatMap(VentilationStep::getValueRaw);
+ }
+
+ /**
+ * Gets the plate power step of the device for the given index.
+ *
+ * @param index The index of the device plate for which the power step shall be obtained.
+ * @return The plate power step if available.
+ */
+ public Optional<String> getPlateStep(int index) {
+ if (deviceIsInOffState()) {
+ return Optional.empty();
+ }
+ return device.flatMap(Device::getState).map(State::getPlateStep).flatMap(l -> getOrNull(l, index))
+ .flatMap(PlateStep::getValueLocalized);
+ }
+
+ /**
+ * Gets the raw plate power step of the device for the given index.
+ *
+ * @param index The index of the device plate for which the power step shall be obtained.
+ * @return The raw plate power step if available.
+ */
+ public Optional<Integer> getPlateStepRaw(int index) {
+ if (deviceIsInOffState()) {
+ return Optional.empty();
+ }
+ return device.flatMap(Device::getState).map(State::getPlateStep).flatMap(l -> getOrNull(l, index))
+ .flatMap(PlateStep::getValueRaw);
+ }
+
+ /**
+ * Gets the number of available plate steps.
+ *
+ * @return The number of available plate steps.
+ */
+ public Optional<Integer> getPlateStepCount() {
+ return device.flatMap(Device::getState).map(State::getPlateStep).map(List::size);
+ }
+
+ /**
+ * Indicates if the device has an error that requires a user action.
+ *
+ * @return Whether the device has an error that requires a user action.
+ */
+ public boolean hasError() {
+ return isInState(StateType.FAILURE)
+ || device.flatMap(Device::getState).flatMap(State::getSignalFailure).orElse(false);
+ }
+
+ /**
+ * Indicates if the device has a user information.
+ *
+ * @return Whether the device has a user information.
+ */
+ public boolean hasInfo() {
+ if (deviceIsInOffState()) {
+ return false;
+ }
+ return device.flatMap(Device::getState).flatMap(State::getSignalInfo).orElse(false);
+ }
+
+ /**
+ * Gets the state of the light attached to the device.
+ *
+ * @return An {@link Optional} with value {@code true} if the light is turned on, {@code false} if the light is
+ * turned off or an empty {@link Optional} if light is not supported or no state is available.
+ */
+ public Optional<Boolean> getLightState() {
+ if (deviceIsInOffState()) {
+ return Optional.empty();
+ }
+
+ Optional<Light> light = device.flatMap(Device::getState).map(State::getLight);
+ if (light.isPresent()) {
+ if (light.get().equals(Light.ENABLE)) {
+ return Optional.of(true);
+ } else if (light.get().equals(Light.DISABLE)) {
+ return Optional.of(false);
+ }
+ }
+
+ return Optional.empty();
+ }
+
+ /**
+ * Gets the state of the door attached to the device.
+ *
+ * @return Whether the device door is open.
+ */
+ public Optional<Boolean> getDoorState() {
+ if (deviceIsInOffState()) {
+ return Optional.empty();
+ }
+
+ return device.flatMap(Device::getState).flatMap(State::getSignalDoor);
+ }
+
+ /**
+ * Gets the state of the device's door alarm.
+ *
+ * @return Whether the device door alarm was triggered.
+ */
+ public Optional<Boolean> getDoorAlarm() {
+ if (deviceIsInOffState()) {
+ return Optional.empty();
+ }
+
+ Optional<Boolean> doorState = getDoorState();
+ Optional<Boolean> failure = device.flatMap(Device::getState).flatMap(State::getSignalFailure);
+
+ if (!doorState.isPresent() || !failure.isPresent()) {
+ return Optional.empty();
+ }
+
+ return Optional.of(doorState.get() && failure.get());
+ }
+
+ /**
+ * Gets the battery level.
+ *
+ * @return The battery level.
+ */
+ public Optional<Integer> getBatteryLevel() {
+ if (deviceIsInOffState()) {
+ return Optional.empty();
+ }
+
+ return device.flatMap(Device::getState).flatMap(State::getBatteryLevel);
+ }
+
+ /**
+ * Gets the device type.
+ *
+ * @return The device type as human readable value.
+ */
+ public Optional<String> getType() {
+ return device.flatMap(Device::getIdent).flatMap(Ident::getType).flatMap(Type::getValueLocalized)
+ .filter(type -> !type.isEmpty());
+ }
+
+ /**
+ * Gets the raw device type.
+ *
+ * @return The raw device type.
+ */
+ public DeviceType getRawType() {
+ return device.flatMap(Device::getIdent).flatMap(Ident::getType).map(Type::getValueRaw)
+ .orElse(DeviceType.UNKNOWN);
+ }
+
+ /**
+ * Gets the user-defined name of the device.
+ *
+ * @return The user-defined name of the device.
+ */
+ public Optional<String> getDeviceName() {
+ return device.flatMap(Device::getIdent).flatMap(Ident::getDeviceName).filter(name -> !name.isEmpty());
+ }
+
+ /**
+ * Gets the fabrication (=serial) number of the device.
+ *
+ * @return The serial number of the device.
+ */
+ public Optional<String> getFabNumber() {
+ return device.flatMap(Device::getIdent).flatMap(Ident::getDeviceIdentLabel)
+ .flatMap(DeviceIdentLabel::getFabNumber).filter(fabNumber -> !fabNumber.isEmpty());
+ }
+
+ /**
+ * Gets the tech type of the device.
+ *
+ * @return The tech type of the device.
+ */
+ public Optional<String> getTechType() {
+ return device.flatMap(Device::getIdent).flatMap(Ident::getDeviceIdentLabel)
+ .flatMap(DeviceIdentLabel::getTechType).filter(techType -> !techType.isEmpty());
+ }
+
+ private <T> Optional<T> getOrNull(List<T> list, int index) {
+ if (index < 0 || index >= list.size()) {
+ return Optional.empty();
+ }
+
+ return Optional.ofNullable(list.get(index));
+ }
+
+ private boolean deviceIsInOffState() {
+ return getStateType().map(StateType.OFF::equals).orElse(true);
+ }
+
+ public boolean isInState(StateType stateType) {
+ return getStateType().map(stateType::equals).orElse(false);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(device, deviceIdentifier);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ DeviceState other = (DeviceState) obj;
+ return Objects.equals(device, other.device) && Objects.equals(deviceIdentifier, other.deviceIdentifier);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Represents the power status of the device, i.e. whether it is powered on, off or in standby.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public enum PowerStatus {
+ POWER_ON("on"),
+ POWER_OFF("off"),
+ STANDBY("standby");
+
+ /**
+ * Corresponding state of the ChannelTypeDefinition
+ */
+ private String state;
+
+ PowerStatus(String value) {
+ this.state = value;
+ }
+
+ /**
+ * Checks whether the given value is the raw state represented by this enum instance.
+ */
+ public boolean matches(String passedValue) {
+ return state.equalsIgnoreCase(passedValue);
+ }
+
+ /**
+ * Gets the raw state.
+ */
+ public String getState() {
+ return state;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Represents the status of a program.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public enum ProgramStatus {
+ PROGRAM_STARTED("start"),
+ PROGRAM_STOPPED("stop"),
+ PROGRAM_PAUSED("pause");
+
+ /**
+ * Corresponding state of the ChannelTypeDefinition
+ */
+ private String state;
+
+ ProgramStatus(String value) {
+ this.state = value;
+ }
+
+ /**
+ * Checks whether the given value is the raw state represented by this enum instance.
+ */
+ public boolean matches(String passedValue) {
+ return state.equalsIgnoreCase(passedValue);
+ }
+
+ /**
+ * Gets the raw state.
+ */
+ public String getState() {
+ return state;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+
+/**
+ * This immutable class provides methods to extract the state information related to state transitions in a comfortable
+ * way.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class TransitionState {
+ private final boolean remainingTimeWasSetInCurrentProgram;
+ private final Optional<DeviceState> previousState;
+ private final DeviceState nextState;
+
+ /**
+ * Creates a new {@link TransitionState}.
+ *
+ * Note: {@code previousState} <b>must not</b> be saved in a field in this class as this will create a linked list
+ * and cause memory issues. The constructor only serves the purpose of unpacking state that must be carried on.
+ *
+ * @param previousTransitionState The previous transition state if it exists.
+ * @param nextState The device state which the device is transitioning to.
+ */
+ public TransitionState(@Nullable TransitionState previousTransitionState, DeviceState nextState) {
+ this.remainingTimeWasSetInCurrentProgram = wasRemainingTimeSetInCurrentProgram(previousTransitionState,
+ nextState);
+ this.previousState = Optional.ofNullable(previousTransitionState).map(it -> it.nextState);
+ this.nextState = nextState;
+ }
+
+ /**
+ * Gets whether the finish state changed due to the transition form the previous to the current state.
+ *
+ * @return Whether the finish state changed due to the transition form the previous to the current state.
+ */
+ public boolean hasFinishedChanged() {
+ return previousState.map(this::hasFinishedChangedFromPreviousState).orElse(true);
+ }
+
+ private boolean hasFinishedChangedFromPreviousState(DeviceState previous) {
+ if (previous.getStateType().equals(nextState.getStateType())) {
+ return false;
+ }
+
+ if (isInRunningState(previous) && nextState.isInState(StateType.FAILURE)) {
+ return false;
+ }
+
+ if (isInRunningState(previous) != isInRunningState(nextState)) {
+ return true;
+ }
+
+ if (nextState.isInState(StateType.OFF)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Gets whether a program finished.
+ *
+ * @return Whether a program finished.
+ */
+ public Optional<Boolean> isFinished() {
+ return previousState.flatMap(this::hasFinishedFromPreviousState);
+ }
+
+ private Optional<Boolean> hasFinishedFromPreviousState(DeviceState prevState) {
+ if (!prevState.getStateType().isPresent()) {
+ return Optional.empty();
+ }
+
+ if (nextState.isInState(StateType.OFF)) {
+ return Optional.of(false);
+ }
+
+ if (nextState.isInState(StateType.FAILURE)) {
+ return Optional.of(false);
+ }
+
+ return Optional.of(!isInRunningState(nextState));
+ }
+
+ /**
+ * Gets the remaining time of the active program.
+ *
+ * Note: Tracking changes in the remaining time is a workaround for the Miele API not properly distinguishing
+ * between "there is no remaining time set" and "the remaining time is zero". If the remaining time is zero when a
+ * program is started then we assume that no timer was set / program with remaining time is active. This may be
+ * changed later by the user which is detected by the remaining time changing from 0 to some larger value.
+ *
+ * @return The remaining time in seconds.
+ */
+ public Optional<Integer> getRemainingTime() {
+ if (!remainingTimeWasSetInCurrentProgram && isInRunningState(nextState)) {
+ return nextState.getRemainingTime().filter(it -> it != 0);
+ } else {
+ return nextState.getRemainingTime();
+ }
+ }
+
+ /**
+ * Gets the program progress.
+ *
+ * @return The progress of the active program in percent.
+ */
+ public Optional<Integer> getProgress() {
+ if (getRemainingTime().isPresent()) {
+ return nextState.getProgress();
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ private static boolean wasRemainingTimeSetInCurrentProgram(@Nullable TransitionState previousTransitionState,
+ DeviceState nextState) {
+ if (previousTransitionState != null && isInRunningState(previousTransitionState.nextState)) {
+ return previousTransitionState.remainingTimeWasSetInCurrentProgram
+ || previousTransitionState.getRemainingTime().isPresent();
+ } else {
+ return false;
+ }
+ }
+
+ private static boolean isInRunningState(DeviceState device) {
+ return device.isInState(StateType.RUNNING) || device.isInState(StateType.PAUSE);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
+
+/**
+ * Provides easy access to temperature values mapped for wine storage devices.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class WineStorageDeviceTemperatureState {
+ private static final Set<DeviceType> ALL_WINE_STORAGES = Set.of(DeviceType.WINE_CABINET,
+ DeviceType.WINE_CABINET_FREEZER_COMBINATION, DeviceType.WINE_CONDITIONING_UNIT,
+ DeviceType.WINE_STORAGE_CONDITIONING_UNIT);
+
+ private final DeviceState deviceState;
+ private final List<Integer> effectiveTemperatures;
+ private final List<Integer> effectiveTargetTemperatures;
+
+ /**
+ * Creates a new {@link WineStorageDeviceTemperatureState}.
+ *
+ * @param deviceState Device state to query extended state information from.
+ */
+ public WineStorageDeviceTemperatureState(DeviceState deviceState) {
+ this.deviceState = deviceState;
+ effectiveTemperatures = getEffectiveTemperatures();
+ effectiveTargetTemperatures = getEffectiveTargetTemperatures();
+ }
+
+ private List<Integer> getEffectiveTemperatures() {
+ return Arrays
+ .asList(deviceState.getTemperature(0), deviceState.getTemperature(1), deviceState.getTemperature(2))
+ .stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
+ }
+
+ private List<Integer> getEffectiveTargetTemperatures() {
+ return Arrays
+ .asList(deviceState.getTargetTemperature(0), deviceState.getTargetTemperature(1),
+ deviceState.getTargetTemperature(2))
+ .stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
+ }
+
+ /**
+ * Gets the current main temperature of the wine storage.
+ *
+ * @return The current main temperature of the wine storage.
+ */
+ public Optional<Integer> getTemperature() {
+ if (!ALL_WINE_STORAGES.contains(deviceState.getRawType())) {
+ return Optional.empty();
+ }
+
+ return getTemperatureFromList(effectiveTemperatures);
+ }
+
+ /**
+ * Gets the target main temperature of the wine storage.
+ *
+ * @return The target main temperature of the wine storage.
+ */
+ public Optional<Integer> getTargetTemperature() {
+ if (!ALL_WINE_STORAGES.contains(deviceState.getRawType())) {
+ return Optional.empty();
+ }
+
+ return getTemperatureFromList(effectiveTargetTemperatures);
+ }
+
+ private Optional<Integer> getTemperatureFromList(List<Integer> temperatures) {
+ if (temperatures.isEmpty()) {
+ return Optional.empty();
+ }
+
+ if (temperatures.size() > 1) {
+ return Optional.empty();
+ }
+
+ return Optional.of(temperatures.get(0));
+ }
+
+ /**
+ * Gets the current top temperature of the wine storage.
+ *
+ * @return The current top temperature of the wine storage.
+ */
+ public Optional<Integer> getTopTemperature() {
+ if (!ALL_WINE_STORAGES.contains(deviceState.getRawType())) {
+ return Optional.empty();
+ }
+
+ return getTopTemperatureFromList(effectiveTemperatures);
+ }
+
+ /**
+ * Gets the target top temperature of the wine storage.
+ *
+ * @return The target top temperature of the wine storage.
+ */
+ public Optional<Integer> getTopTargetTemperature() {
+ if (!ALL_WINE_STORAGES.contains(deviceState.getRawType())) {
+ return Optional.empty();
+ }
+
+ return getTopTemperatureFromList(effectiveTargetTemperatures);
+ }
+
+ private Optional<Integer> getTopTemperatureFromList(List<Integer> temperatures) {
+ if (temperatures.size() <= 1) {
+ return Optional.empty();
+ }
+
+ return Optional.of(temperatures.get(0));
+ }
+
+ /**
+ * Gets the current middle temperature of the wine storage.
+ *
+ * @return The current middle temperature of the wine storage.
+ */
+ public Optional<Integer> getMiddleTemperature() {
+ if (!ALL_WINE_STORAGES.contains(deviceState.getRawType())) {
+ return Optional.empty();
+ }
+
+ return getMiddleTemperatureFromList(effectiveTemperatures);
+ }
+
+ /**
+ * Gets the target middle temperature of the wine storage.
+ *
+ * @return The target middle temperature of the wine storage.
+ */
+ public Optional<Integer> getMiddleTargetTemperature() {
+ if (!ALL_WINE_STORAGES.contains(deviceState.getRawType())) {
+ return Optional.empty();
+ }
+
+ return getMiddleTemperatureFromList(effectiveTargetTemperatures);
+ }
+
+ private Optional<Integer> getMiddleTemperatureFromList(List<Integer> temperatures) {
+ if (temperatures.size() != 3) {
+ return Optional.empty();
+ }
+
+ return Optional.of(temperatures.get(1));
+ }
+
+ /**
+ * Gets the current bottom temperature of the wine storage.
+ *
+ * @return The current bottom temperature of the wine storage.
+ */
+ public Optional<Integer> getBottomTemperature() {
+ if (!ALL_WINE_STORAGES.contains(deviceState.getRawType())) {
+ return Optional.empty();
+ }
+
+ return getBottomTemperatureFromList(effectiveTemperatures);
+ }
+
+ /**
+ * Gets the target bottom temperature of the wine storage.
+ *
+ * @return The target bottom temperature of the wine storage.
+ */
+ public Optional<Integer> getBottomTargetTemperature() {
+ if (!ALL_WINE_STORAGES.contains(deviceState.getRawType())) {
+ return Optional.empty();
+ }
+
+ return getBottomTemperatureFromList(effectiveTargetTemperatures);
+ }
+
+ private Optional<Integer> getBottomTemperatureFromList(List<Integer> temperatures) {
+ if (temperatures.size() == 3) {
+ return Optional.of(temperatures.get(2));
+ }
+
+ if (temperatures.size() == 2) {
+ return Optional.of(temperatures.get(1));
+ }
+
+ return Optional.empty();
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Immutable POJO representing the device actions queried from the Miele REST API.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ */
+@NonNullByDefault
+public class Actions {
+ @SerializedName("processAction")
+ @Nullable
+ private final List<ProcessAction> processAction = null;
+ @SerializedName("light")
+ @Nullable
+ private final List<Integer> light = null;
+ @SerializedName("startTime")
+ @Nullable
+ private final List<List<Integer>> startTime = null;
+ @SerializedName("programId")
+ @Nullable
+ private final List<Integer> programId = null;
+ @SerializedName("deviceName")
+ @Nullable
+ private String deviceName;
+ @SerializedName("powerOff")
+ @Nullable
+ private Boolean powerOff;
+ @SerializedName("powerOn")
+ @Nullable
+ private Boolean powerOn;
+
+ public List<ProcessAction> getProcessAction() {
+ if (processAction == null) {
+ return Collections.emptyList();
+ }
+
+ return Collections.unmodifiableList(processAction);
+ }
+
+ public List<Light> getLight() {
+ final List<Integer> lightRefCopy = light;
+ if (lightRefCopy == null) {
+ return Collections.emptyList();
+ }
+
+ return Collections.unmodifiableList(lightRefCopy.stream().map(Light::fromId).collect(Collectors.toList()));
+ }
+
+ /**
+ * Gets the start time encoded as {@link List} of {@link List} of {@link Integer} values.
+ * The first list entry defines the lower time constraint for setting the delayed start time. The second list
+ * entry defines the upper time constraint. The time constraints are defined as a list of integers with the full
+ * hour as first and minutes as second element.
+ *
+ * @return The possible start time interval encoded as described above.
+ */
+ public Optional<List<List<Integer>>> getStartTime() {
+ if (startTime == null) {
+ return Optional.empty();
+ }
+
+ return Optional.of(Collections.unmodifiableList(startTime));
+ }
+
+ public List<Integer> getProgramId() {
+ if (programId == null) {
+ return Collections.emptyList();
+ }
+
+ return Collections.unmodifiableList(programId);
+ }
+
+ public Optional<String> getDeviceName() {
+ return Optional.ofNullable(deviceName);
+ }
+
+ public Optional<Boolean> getPowerOn() {
+ return Optional.ofNullable(powerOn);
+ }
+
+ public Optional<Boolean> getPowerOff() {
+ return Optional.ofNullable(powerOff);
+ }
+
+ @Override
+ public String toString() {
+ return "ActionState [processAction=" + processAction + ", light=" + light + ", startTime=" + startTime
+ + ", programId=" + programId + ", deviceName=" + deviceName + ", powerOff=" + powerOff + ", powerOn="
+ + powerOn + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(deviceName, light, powerOn, powerOff, processAction, startTime, programId);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ Actions other = (Actions) obj;
+ return Objects.equals(deviceName, other.deviceName) && Objects.equals(light, other.light)
+ && Objects.equals(powerOn, other.powerOn) && Objects.equals(powerOff, other.powerOff)
+ && Objects.equals(processAction, other.processAction) && Objects.equals(startTime, other.startTime)
+ && Objects.equals(programId, other.programId);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Immutable POJO representing a device queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class Device {
+ @Nullable
+ private Ident ident;
+ @Nullable
+ private State state;
+
+ public Optional<Ident> getIdent() {
+ return Optional.ofNullable(ident);
+ }
+
+ public Optional<State> getState() {
+ return Optional.ofNullable(state);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(ident, state);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ Device other = (Device) obj;
+ return Objects.equals(ident, other.ident) && Objects.equals(state, other.state);
+ }
+
+ @Override
+ public String toString() {
+ return "Device [ident=" + ident + ", state=" + state + "]";
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * Immutable POJO representing a collection of devices queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class DeviceCollection {
+ private static final java.lang.reflect.Type STRING_DEVICE_MAP_TYPE = new TypeToken<Map<String, Device>>() {
+ }.getType();
+
+ private final Map<String, Device> devices;
+
+ DeviceCollection(Map<String, Device> devices) {
+ this.devices = devices;
+ }
+
+ /**
+ * Creates a new {@link DeviceCollection} from the given Json text.
+ *
+ * @param json The Json text.
+ * @return The created {@link DeviceCollection}.
+ * @throws MieleSyntaxException if parsing the data from {@code json} fails.
+ */
+ public static DeviceCollection fromJson(String json) {
+ try {
+ Map<String, Device> devices = new Gson().fromJson(json, STRING_DEVICE_MAP_TYPE);
+ if (devices == null) {
+ throw new MieleSyntaxException("Failed to parse Json.");
+ }
+ return new DeviceCollection(devices);
+ } catch (JsonSyntaxException e) {
+ throw new MieleSyntaxException("Failed to parse Json.", e);
+ }
+ }
+
+ public Set<String> getDeviceIdentifiers() {
+ return devices.keySet();
+ }
+
+ public Device getDevice(String identifier) {
+ Device device = devices.get(identifier);
+ if (device == null) {
+ throw new IllegalArgumentException("There is no device for identifier " + identifier);
+ }
+ return device;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(devices);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ DeviceCollection other = (DeviceCollection) obj;
+ return Objects.equals(devices, other.devices);
+ }
+
+ @Override
+ public String toString() {
+ return "DeviceCollection [devices=" + devices + "]";
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Immutable POJO representing the full device identification queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class DeviceIdentLabel {
+ @Nullable
+ private String fabNumber;
+ @Nullable
+ private String fabIndex;
+ @Nullable
+ private String techType;
+ @Nullable
+ private String matNumber;
+ @Nullable
+ private final List<String> swids = null;
+
+ public Optional<String> getFabNumber() {
+ return Optional.ofNullable(fabNumber);
+ }
+
+ public Optional<String> getFabIndex() {
+ return Optional.ofNullable(fabIndex);
+ }
+
+ public Optional<String> getTechType() {
+ return Optional.ofNullable(techType);
+ }
+
+ public Optional<String> getMatNumber() {
+ return Optional.ofNullable(matNumber);
+ }
+
+ public List<String> getSwids() {
+ if (swids == null) {
+ return Collections.emptyList();
+ }
+
+ return Collections.unmodifiableList(swids);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(fabIndex, fabNumber, matNumber, swids, techType);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ DeviceIdentLabel other = (DeviceIdentLabel) obj;
+ return Objects.equals(fabIndex, other.fabIndex) && Objects.equals(fabNumber, other.fabNumber)
+ && Objects.equals(matNumber, other.matNumber) && Objects.equals(swids, other.swids)
+ && Objects.equals(techType, other.techType);
+ }
+
+ @Override
+ public String toString() {
+ return "DeviceIdentLabel [fabNumber=" + fabNumber + ", fabIndex=" + fabIndex + ", techType=" + techType
+ + ", matNumber=" + matNumber + ", swids=" + swids + "]";
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Represents the Miele device type.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public enum DeviceType {
+ /**
+ * {@link DeviceType} for unknown devices.
+ */
+ UNKNOWN,
+
+ @SerializedName("1")
+ WASHING_MACHINE,
+
+ @SerializedName("2")
+ TUMBLE_DRYER,
+
+ @SerializedName("7")
+ DISHWASHER,
+
+ @SerializedName("8")
+ DISHWASHER_SEMI_PROF,
+
+ @SerializedName("12")
+ OVEN,
+
+ @SerializedName("13")
+ OVEN_MICROWAVE,
+
+ @SerializedName("14")
+ HOB_HIGHLIGHT,
+
+ @SerializedName("15")
+ STEAM_OVEN,
+
+ @SerializedName("16")
+ MICROWAVE,
+
+ @SerializedName("17")
+ COFFEE_SYSTEM,
+
+ @SerializedName("18")
+ HOOD,
+
+ @SerializedName("19")
+ FRIDGE,
+
+ @SerializedName("20")
+ FREEZER,
+
+ @SerializedName("21")
+ FRIDGE_FREEZER_COMBINATION,
+
+ /**
+ * Might also be AUTOMATIC ROBOTIC VACUUM CLEANER.
+ */
+ @SerializedName("23")
+ VACUUM_CLEANER,
+
+ @SerializedName("24")
+ WASHER_DRYER,
+
+ @SerializedName("25")
+ DISH_WARMER,
+
+ @SerializedName("27")
+ HOB_INDUCTION,
+
+ @SerializedName("28")
+ HOB_GAS,
+
+ @SerializedName("31")
+ STEAM_OVEN_COMBINATION,
+
+ @SerializedName("32")
+ WINE_CABINET,
+
+ @SerializedName("33")
+ WINE_CONDITIONING_UNIT,
+
+ @SerializedName("34")
+ WINE_STORAGE_CONDITIONING_UNIT,
+
+ @SerializedName("39")
+ DOUBLE_OVEN,
+
+ @SerializedName("40")
+ DOUBLE_STEAM_OVEN,
+
+ @SerializedName("41")
+ DOUBLE_STEAM_OVEN_COMBINATION,
+
+ @SerializedName("42")
+ DOUBLE_MICROWAVE,
+
+ @SerializedName("43")
+ DOUBLE_MICROWAVE_OVEN,
+
+ @SerializedName("45")
+ STEAM_OVEN_MICROWAVE_COMBINATION,
+
+ @SerializedName("48")
+ VACUUM_DRAWER,
+
+ @SerializedName("67")
+ DIALOGOVEN,
+
+ @SerializedName("68")
+ WINE_CABINET_FREEZER_COMBINATION,
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Immutable POJO representing the current drying step, queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class DryingStep {
+ @SerializedName("value_raw")
+ @Nullable
+ private Integer valueRaw;
+ @SerializedName("value_localized")
+ @Nullable
+ private String valueLocalized;
+ @SerializedName("key_localized")
+ @Nullable
+ private String keyLocalized;
+
+ public Optional<Integer> getValueRaw() {
+ return Optional.ofNullable(valueRaw);
+ }
+
+ public Optional<String> getValueLocalized() {
+ return Optional.ofNullable(valueLocalized);
+ }
+
+ public Optional<String> getKeyLocalized() {
+ return Optional.ofNullable(keyLocalized);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(keyLocalized, valueLocalized, valueRaw);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ DryingStep other = (DryingStep) obj;
+ return Objects.equals(keyLocalized, other.keyLocalized) && Objects.equals(valueLocalized, other.valueLocalized)
+ && Objects.equals(valueRaw, other.valueRaw);
+ }
+
+ @Override
+ public String toString() {
+ return "DryingStep [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", keyLocalized="
+ + keyLocalized + "]";
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * Immutable POJO representing an error message. Queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class ErrorMessage {
+ @Nullable
+ private String message;
+
+ /**
+ * Creates a new {@link ErrorMessage} from the given Json text.
+ *
+ * @param json The Json text.
+ * @return The created {@link ErrorMessage}.
+ * @throws MieleSyntaxException if parsing the data from {@code json} fails.
+ */
+ public static ErrorMessage fromJson(String json) {
+ try {
+ ErrorMessage errorMessage = new Gson().fromJson(json, ErrorMessage.class);
+ if (errorMessage == null) {
+ throw new MieleSyntaxException("Failed to parse Json.");
+ }
+ return errorMessage;
+ } catch (JsonSyntaxException e) {
+ throw new MieleSyntaxException("Failed to parse Json.", e);
+ }
+ }
+
+ public Optional<String> getMessage() {
+ return Optional.ofNullable(message);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(message);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ ErrorMessage other = (ErrorMessage) obj;
+ return Objects.equals(message, other.message);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Immutable POJO representing the device identification queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class Ident {
+ @Nullable
+ private Type type;
+ @Nullable
+ private String deviceName;
+ @Nullable
+ private DeviceIdentLabel deviceIdentLabel;
+ @Nullable
+ private XkmIdentLabel xkmIdentLabel;
+
+ public Optional<Type> getType() {
+ return Optional.ofNullable(type);
+ }
+
+ public Optional<String> getDeviceName() {
+ return Optional.ofNullable(deviceName);
+ }
+
+ public Optional<DeviceIdentLabel> getDeviceIdentLabel() {
+ return Optional.ofNullable(deviceIdentLabel);
+ }
+
+ public Optional<XkmIdentLabel> getXkmIdentLabel() {
+ return Optional.ofNullable(xkmIdentLabel);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(deviceIdentLabel, deviceName, type, xkmIdentLabel);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ Ident other = (Ident) obj;
+ return Objects.equals(deviceIdentLabel, other.deviceIdentLabel) && Objects.equals(deviceName, other.deviceName)
+ && Objects.equals(type, other.type) && Objects.equals(xkmIdentLabel, other.xkmIdentLabel);
+ }
+
+ @Override
+ public String toString() {
+ return "Ident [type=" + type + ", deviceName=" + deviceName + ", deviceIdentLabel=" + deviceIdentLabel
+ + ", xkmIdentLabel=" + xkmIdentLabel + "]";
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Represents the state of a light on a Miele device.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ * @author Björn Lange - Added NOT_SUPPORTED entry
+ */
+@NonNullByDefault
+public enum Light {
+ /**
+ * {Light} for unknown states.
+ */
+ UNKNOWN(),
+
+ ENABLE(1),
+
+ DISABLE(2),
+
+ NOT_SUPPORTED(0, 255);
+
+ private List<Integer> ids;
+
+ Light(int... ids) {
+ this.ids = Collections.unmodifiableList(Arrays.stream(ids).boxed().collect(Collectors.toList()));
+ }
+
+ /**
+ * Gets the {@link Light} state matching the given ID.
+ *
+ * @param id The ID.
+ * @return The matching {@link Light} or {@code UNKNOWN} if no ID matches.
+ */
+ public static Light fromId(@Nullable Integer id) {
+ for (Light light : Light.values()) {
+ if (light.ids.contains(id)) {
+ return light;
+ }
+ }
+
+ return Light.UNKNOWN;
+ }
+
+ /**
+ * Formats this instance for interaction with the Miele webservice.
+ */
+ public String format() {
+ if (ids.isEmpty()) {
+ return "";
+ } else {
+ return Integer.toString(ids.get(0));
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * {@link RuntimeException} thrown when the syntax of a message received from the Miele REST API does not match and
+ * cannot be interpreted as the expected syntax (e.g. by ignoring entries).
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class MieleSyntaxException extends RuntimeException {
+ private static final long serialVersionUID = 8253804935427566729L;
+
+ public MieleSyntaxException(String message) {
+ super(message);
+ }
+
+ public MieleSyntaxException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Immutable POJO representing a plate power state. Queried from the Miele REST API.
+ *
+ * @author Benjamin Bolte - Initial contribution
+ */
+@NonNullByDefault
+public class PlateStep {
+ @SerializedName("value_raw")
+ @Nullable
+ private Integer valueRaw;
+ @SerializedName("value_localized")
+ @Nullable
+ private String valueLocalized;
+ @SerializedName("key_localized")
+ @Nullable
+ private String keyLocalized;
+
+ public Optional<Integer> getValueRaw() {
+ return Optional.ofNullable(valueRaw);
+ }
+
+ public Optional<String> getValueLocalized() {
+ return Optional.ofNullable(valueLocalized);
+ }
+
+ public Optional<String> getKeyLocalized() {
+ return Optional.ofNullable(keyLocalized);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(keyLocalized, valueLocalized, valueRaw);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ PlateStep other = (PlateStep) obj;
+ return Objects.equals(keyLocalized, other.keyLocalized) && Objects.equals(valueLocalized, other.valueLocalized)
+ && Objects.equals(valueRaw, other.valueRaw);
+ }
+
+ @Override
+ public String toString() {
+ return "PlateStep [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", key_localized="
+ + keyLocalized + "]";
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Represents a process action.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ */
+@NonNullByDefault
+public enum ProcessAction {
+ /**
+ * {@StateType} for unknown states.
+ */
+ UNKNOWN,
+
+ @SerializedName("1")
+ START,
+
+ @SerializedName("2")
+ STOP,
+
+ @SerializedName("3")
+ PAUSE,
+
+ @SerializedName("4")
+ START_SUPERFREEZING,
+
+ @SerializedName("5")
+ STOP_SUPERFREEZING,
+
+ @SerializedName("6")
+ START_SUPERCOOLING,
+
+ @SerializedName("7")
+ STOP_SUPERCOOLING,
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Immutable POJO representing the program type that is currently running. Queried from the Miele REST API.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ */
+@NonNullByDefault
+public class ProgramId {
+ @SerializedName("value_raw")
+ @Nullable
+ private Long valueRaw;
+ @SerializedName("value_localized")
+ @Nullable
+ private String valueLocalized;
+ @SerializedName("key_localized")
+ @Nullable
+ private String keyLocalized;
+
+ public Optional<Long> getValueRaw() {
+ return Optional.ofNullable(valueRaw);
+ }
+
+ public Optional<String> getValueLocalized() {
+ return Optional.ofNullable(valueLocalized);
+ }
+
+ public Optional<String> getKeyLocalized() {
+ return Optional.ofNullable(keyLocalized);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(keyLocalized, valueLocalized, valueRaw);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ ProgramId other = (ProgramId) obj;
+ return Objects.equals(keyLocalized, other.keyLocalized) && Objects.equals(valueLocalized, other.valueLocalized)
+ && Objects.equals(valueRaw, other.valueRaw);
+ }
+
+ @Override
+ public String toString() {
+ return "ProgramType [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", keyLocalized="
+ + keyLocalized + "]";
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Immutable POJO representing the current program's phase. Queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class ProgramPhase {
+ @SerializedName("value_raw")
+ @Nullable
+ private Integer valueRaw;
+ @SerializedName("value_localized")
+ @Nullable
+ private String valueLocalized;
+ @SerializedName("key_localized")
+ @Nullable
+ private String keyLocalized;
+
+ public Optional<Integer> getValueRaw() {
+ return Optional.ofNullable(valueRaw);
+ }
+
+ public Optional<String> getValueLocalized() {
+ return Optional.ofNullable(valueLocalized);
+ }
+
+ public Optional<String> getKeyLocalized() {
+ return Optional.ofNullable(keyLocalized);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(keyLocalized, valueLocalized, valueRaw);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ ProgramPhase other = (ProgramPhase) obj;
+ return Objects.equals(keyLocalized, other.keyLocalized) && Objects.equals(valueLocalized, other.valueLocalized)
+ && Objects.equals(valueRaw, other.valueRaw);
+ }
+
+ @Override
+ public String toString() {
+ return "ProgramPhase [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", keyLocalized="
+ + keyLocalized + "]";
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Immutable POJO representing the type of program currently running. Queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class ProgramType {
+ @SerializedName("value_raw")
+ @Nullable
+ private Integer valueRaw;
+ @SerializedName("value_localized")
+ @Nullable
+ private String valueLocalized;
+ @SerializedName("key_localized")
+ @Nullable
+ private String keyLocalized;
+
+ public Optional<Integer> getValueRaw() {
+ return Optional.ofNullable(valueRaw);
+ }
+
+ public Optional<String> getValueLocalized() {
+ return Optional.ofNullable(valueLocalized);
+ }
+
+ public Optional<String> getKeyLocalized() {
+ return Optional.ofNullable(keyLocalized);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(keyLocalized, valueLocalized, valueRaw);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ ProgramType other = (ProgramType) obj;
+ return Objects.equals(keyLocalized, other.keyLocalized) && Objects.equals(valueLocalized, other.valueLocalized)
+ && Objects.equals(valueRaw, other.valueRaw);
+ }
+
+ @Override
+ public String toString() {
+ return "ProgramType [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", keyLocalized="
+ + keyLocalized + "]";
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Immutable POJO representing the remote control capabilities of a device. Queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class RemoteEnable {
+ @Nullable
+ private Boolean fullRemoteControl;
+ @Nullable
+ private Boolean smartGrid;
+
+ public Optional<Boolean> getFullRemoteControl() {
+ return Optional.ofNullable(fullRemoteControl);
+ }
+
+ public Optional<Boolean> getSmartGrid() {
+ return Optional.ofNullable(smartGrid);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(fullRemoteControl, smartGrid);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ RemoteEnable other = (RemoteEnable) obj;
+ return Objects.equals(fullRemoteControl, other.fullRemoteControl) && Objects.equals(smartGrid, other.smartGrid);
+ }
+
+ @Override
+ public String toString() {
+ return "RemoteEnable [fullRemoteControl=" + fullRemoteControl + ", smartGrid=" + smartGrid + "]";
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Immutable POJO representing the current spinning speed, queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class SpinningSpeed {
+ @SerializedName("value_raw")
+ @Nullable
+ private Integer valueRaw;
+ @SerializedName("value_localized")
+ @Nullable
+ private String valueLocalized;
+ @SerializedName("unit")
+ @Nullable
+ private String unit;
+
+ public Optional<Integer> getValueRaw() {
+ return Optional.ofNullable(valueRaw);
+ }
+
+ public Optional<String> getValueLocalized() {
+ return Optional.ofNullable(valueLocalized);
+ }
+
+ public Optional<String> getUnit() {
+ return Optional.ofNullable(unit);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(unit, valueLocalized, valueRaw);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ SpinningSpeed other = (SpinningSpeed) obj;
+ return Objects.equals(unit, other.unit) && Objects.equals(valueLocalized, other.valueLocalized)
+ && Objects.equals(valueRaw, other.valueRaw);
+ }
+
+ @Override
+ public String toString() {
+ return "SpinningSpeed [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", unit=" + unit + "]";
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Immutable POJO representing the state of a device. Queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add plate step
+ * @author Björn Lange - Add elapsed time channel
+ */
+@NonNullByDefault
+public class State {
+ @Nullable
+ private Status status;
+ /**
+ * Currently used by Miele webservice.
+ */
+ @Nullable
+ private ProgramId ProgramID;
+ /**
+ * Planned to be used in the future.
+ */
+ @Nullable
+ private ProgramId programId;
+ @Nullable
+ private ProgramType programType;
+ @Nullable
+ private ProgramPhase programPhase;
+ @Nullable
+ private final List<Integer> remainingTime = null;
+ @Nullable
+ private final List<Integer> startTime = null;
+ @Nullable
+ private final List<Temperature> targetTemperature = null;
+ @Nullable
+ private final List<Temperature> temperature = null;
+ @Nullable
+ private Boolean signalInfo;
+ @Nullable
+ private Boolean signalFailure;
+ @Nullable
+ private Boolean signalDoor;
+ @Nullable
+ private RemoteEnable remoteEnable;
+ @Nullable
+ private Integer light;
+ @Nullable
+ private final List<Integer> elapsedTime = null;
+ @Nullable
+ private SpinningSpeed spinningSpeed;
+ @Nullable
+ private DryingStep dryingStep;
+ @Nullable
+ private VentilationStep ventilationStep;
+ @Nullable
+ private final List<PlateStep> plateStep = null;
+ @Nullable
+ private Integer batteryLevel;
+
+ public Optional<Status> getStatus() {
+ return Optional.ofNullable(status);
+ }
+
+ public Optional<ProgramId> getProgramId() {
+ // There is a typo for the program ID in the Miele Cloud API, which will be corrected in the future.
+ // For the sake of robustness, we currently support both upper and lower case.
+ return Optional.ofNullable(programId != null ? programId : ProgramID);
+ }
+
+ public Optional<ProgramType> getProgramType() {
+ return Optional.ofNullable(programType);
+ }
+
+ public Optional<ProgramPhase> getProgramPhase() {
+ return Optional.ofNullable(programPhase);
+ }
+
+ /**
+ * Gets the remaining time encoded as {@link List} of {@link Integer} values.
+ *
+ * @return The remaining time encoded as {@link List} of {@link Integer} values.
+ */
+ public Optional<List<Integer>> getRemainingTime() {
+ if (remainingTime == null) {
+ return Optional.empty();
+ }
+
+ return Optional.ofNullable(Collections.unmodifiableList(remainingTime));
+ }
+
+ /**
+ * Gets the start time encoded as {@link List} of {@link Integer} values.
+ *
+ * @return The start time encoded as {@link List} of {@link Integer} values.
+ */
+ public Optional<List<Integer>> getStartTime() {
+ if (startTime == null) {
+ return Optional.empty();
+ }
+
+ return Optional.ofNullable(Collections.unmodifiableList(startTime));
+ }
+
+ public List<Temperature> getTargetTemperature() {
+ if (targetTemperature == null) {
+ return Collections.emptyList();
+ }
+
+ return Collections.unmodifiableList(targetTemperature);
+ }
+
+ public List<Temperature> getTemperature() {
+ if (temperature == null) {
+ return Collections.emptyList();
+ }
+
+ return Collections.unmodifiableList(temperature);
+ }
+
+ public Optional<Boolean> getSignalInfo() {
+ return Optional.ofNullable(signalInfo);
+ }
+
+ public Optional<Boolean> getSignalFailure() {
+ return Optional.ofNullable(signalFailure);
+ }
+
+ public Optional<Boolean> getSignalDoor() {
+ return Optional.ofNullable(signalDoor);
+ }
+
+ public Optional<RemoteEnable> getRemoteEnable() {
+ return Optional.ofNullable(remoteEnable);
+ }
+
+ public Light getLight() {
+ return Light.fromId(light);
+ }
+
+ /**
+ * Gets the elapsed time encoded as {@link List} of {@link Integer} values.
+ *
+ * @return The elapsed time encoded as {@link List} of {@link Integer} values.
+ */
+ public Optional<List<Integer>> getElapsedTime() {
+ if (elapsedTime == null) {
+ return Optional.empty();
+ }
+
+ return Optional.ofNullable(Collections.unmodifiableList(elapsedTime));
+ }
+
+ public Optional<SpinningSpeed> getSpinningSpeed() {
+ return Optional.ofNullable(spinningSpeed);
+ }
+
+ public Optional<DryingStep> getDryingStep() {
+ return Optional.ofNullable(dryingStep);
+ }
+
+ public Optional<VentilationStep> getVentilationStep() {
+ return Optional.ofNullable(ventilationStep);
+ }
+
+ public List<PlateStep> getPlateStep() {
+ if (plateStep == null) {
+ return Collections.emptyList();
+ }
+
+ return Collections.unmodifiableList(plateStep);
+ }
+
+ public Optional<Integer> getBatteryLevel() {
+ return Optional.ofNullable(batteryLevel);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(dryingStep, elapsedTime, light, programPhase, ProgramID, programId, programType,
+ remainingTime, remoteEnable, signalDoor, signalFailure, signalInfo, startTime, status,
+ targetTemperature, temperature, ventilationStep, plateStep, batteryLevel);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ State other = (State) obj;
+ return Objects.equals(dryingStep, other.dryingStep) && Objects.equals(elapsedTime, other.elapsedTime)
+ && Objects.equals(light, other.light) && Objects.equals(programPhase, other.programPhase)
+ && Objects.equals(ProgramID, other.ProgramID) && Objects.equals(programId, other.programId)
+ && Objects.equals(programType, other.programType) && Objects.equals(remainingTime, other.remainingTime)
+ && Objects.equals(remoteEnable, other.remoteEnable) && Objects.equals(signalDoor, other.signalDoor)
+ && Objects.equals(signalFailure, other.signalFailure) && Objects.equals(signalInfo, other.signalInfo)
+ && Objects.equals(startTime, other.startTime) && Objects.equals(status, other.status)
+ && Objects.equals(targetTemperature, other.targetTemperature)
+ && Objects.equals(temperature, other.temperature)
+ && Objects.equals(ventilationStep, other.ventilationStep) && Objects.equals(plateStep, other.plateStep)
+ && Objects.equals(batteryLevel, other.batteryLevel);
+ }
+
+ @Override
+ public String toString() {
+ return "State [status=" + status + ", programId=" + getProgramId() + ", programType=" + programType
+ + ", programPhase=" + programPhase + ", remainingTime=" + remainingTime + ", startTime=" + startTime
+ + ", targetTemperature=" + targetTemperature + ", temperature=" + temperature + ", signalInfo="
+ + signalInfo + ", signalFailure=" + signalFailure + ", signalDoor=" + signalDoor + ", remoteEnable="
+ + remoteEnable + ", light=" + light + ", elapsedTime=" + elapsedTime + ", dryingStep=" + dryingStep
+ + ", ventilationStep=" + ventilationStep + ", plateStep=" + plateStep + ", batteryLevel=" + batteryLevel
+ + "]";
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Represents the Miele device state.
+ *
+ * @author Roland Edelhoff - Initial contribution
+ */
+@NonNullByDefault
+public enum StateType {
+ OFF(1),
+ ON(2),
+ PROGRAMMED(3),
+ PROGRAMMED_WAITING_TO_START(4),
+ RUNNING(5),
+ PAUSE(6),
+ END_PROGRAMMED(7),
+ FAILURE(8),
+ PROGRAMME_INTERRUPTED(9),
+ IDLE(10),
+ RINSE_HOLD(11),
+ SERVICE(12),
+ SUPERFREEZING(13),
+ SUPERCOOLING(14),
+ SUPERHEATING(15),
+ SUPERCOOLING_SUPERFREEZING(146),
+ NOT_CONNECTED(255);
+
+ private static final Map<Integer, StateType> STATE_TYPE_BY_CODE;
+
+ static {
+ Map<Integer, StateType> stateTypeByCode = new HashMap<>();
+ for (StateType stateType : values()) {
+ stateTypeByCode.put(stateType.code, stateType);
+ }
+ STATE_TYPE_BY_CODE = Collections.unmodifiableMap(stateTypeByCode);
+ }
+
+ private final int code;
+
+ private StateType(int code) {
+ this.code = code;
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+ public static Optional<StateType> fromCode(int code) {
+ return Optional.ofNullable(STATE_TYPE_BY_CODE.get(code));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Immutable POJO representing the actual status of a device. Queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class Status {
+ @SerializedName("value_raw")
+ @Nullable
+ private Integer valueRaw;
+ @SerializedName("value_localized")
+ @Nullable
+ private String valueLocalized;
+ @SerializedName("key_localized")
+ @Nullable
+ private String keyLocalized;
+
+ public Optional<Integer> getValueRaw() {
+ return Optional.ofNullable(valueRaw);
+ }
+
+ public Optional<String> getValueLocalized() {
+ return Optional.ofNullable(valueLocalized);
+ }
+
+ public Optional<String> getKeyLocalized() {
+ return Optional.ofNullable(keyLocalized);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(keyLocalized, valueLocalized, valueRaw);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ Status other = (Status) obj;
+ return Objects.equals(keyLocalized, other.keyLocalized) && Objects.equals(valueLocalized, other.valueLocalized)
+ && Objects.equals(valueRaw, other.valueRaw);
+ }
+
+ @Override
+ public String toString() {
+ return "Status [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", keyLocalized=" + keyLocalized
+ + "]";
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Immutable POJO representing a temperature value. Queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class Temperature {
+ @SerializedName("value_raw")
+ @Nullable
+ private Integer valueRaw;
+ @SerializedName("value_localized")
+ @Nullable
+ private Double valueLocalized;
+ @SerializedName("unit")
+ @Nullable
+ private String unit;
+
+ public Optional<Integer> getValueRaw() {
+ return Optional.ofNullable(valueRaw);
+ }
+
+ public Optional<Integer> getValueLocalized() {
+ return Optional.ofNullable(valueLocalized).map(Double::intValue);
+ }
+
+ public Optional<String> getUnit() {
+ return Optional.ofNullable(unit);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(unit, valueLocalized, valueRaw);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ Temperature other = (Temperature) obj;
+ return Objects.equals(unit, other.unit) && Objects.equals(valueLocalized, other.valueLocalized)
+ && Objects.equals(valueRaw, other.valueRaw);
+ }
+
+ @Override
+ public String toString() {
+ return "Temperature [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", unit=" + unit + "]";
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Immutable POJO representing the type of a device. Queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class Type {
+ @SerializedName("key_localized")
+ @Nullable
+ private String keyLocalized;
+ @SerializedName("value_raw")
+ @Nullable
+ private DeviceType valueRaw;
+ @SerializedName("value_localized")
+ @Nullable
+ private String valueLocalized;
+
+ public Optional<String> getKeyLocalized() {
+ return Optional.ofNullable(keyLocalized);
+ }
+
+ public DeviceType getValueRaw() {
+ return Optional.ofNullable(valueRaw).orElse(DeviceType.UNKNOWN);
+ }
+
+ public Optional<String> getValueLocalized() {
+ return Optional.ofNullable(valueLocalized);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(keyLocalized, valueLocalized, valueRaw);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ Type other = (Type) obj;
+ return Objects.equals(keyLocalized, other.keyLocalized) && Objects.equals(valueLocalized, other.valueLocalized)
+ && valueRaw == other.valueRaw;
+ }
+
+ @Override
+ public String toString() {
+ return "Type [keyLocalized=" + keyLocalized + ", valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized
+ + "]";
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Immutable POJO representing the current ventilation step. Queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class VentilationStep {
+ @SerializedName("value_raw")
+ @Nullable
+ private Integer valueRaw;
+ @SerializedName("value_localized")
+ @Nullable
+ private String valueLocalized;
+ @SerializedName("key_localized")
+ @Nullable
+ private String keyLocalized;
+
+ public Optional<Integer> getValueRaw() {
+ return Optional.ofNullable(valueRaw);
+ }
+
+ public Optional<String> getValueLocalized() {
+ return Optional.ofNullable(valueLocalized);
+ }
+
+ public Optional<String> getKeyLocalized() {
+ return Optional.ofNullable(keyLocalized);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(keyLocalized, valueLocalized, valueRaw);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ VentilationStep other = (VentilationStep) obj;
+ return Objects.equals(keyLocalized, other.keyLocalized) && Objects.equals(valueLocalized, other.valueLocalized)
+ && Objects.equals(valueRaw, other.valueRaw);
+ }
+
+ @Override
+ public String toString() {
+ return "VentilationStep [valueRaw=" + valueRaw + ", valueLocalized=" + valueLocalized + ", keyLocalized="
+ + keyLocalized + "]";
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Immutable POJO representing the XKM (Miele communication module) identification. Queried from the Miele REST API.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class XkmIdentLabel {
+ @Nullable
+ private String techType;
+ @Nullable
+ private String releaseVersion;
+
+ public Optional<String> getTechType() {
+ return Optional.ofNullable(techType);
+ }
+
+ public Optional<String> getReleaseVersion() {
+ return Optional.ofNullable(releaseVersion);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(releaseVersion, techType);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ XkmIdentLabel other = (XkmIdentLabel) obj;
+ return Objects.equals(releaseVersion, other.releaseVersion) && Objects.equals(techType, other.techType);
+ }
+
+ @Override
+ public String toString() {
+ return "XkmIdentLabel [techType=" + techType + ", releaseVersion=" + releaseVersion + "]";
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * This {@link RuntimeException} is thrown if an error occurred due to authorization failure.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class AuthorizationFailedException extends RuntimeException {
+ private static final long serialVersionUID = 963609531804668970L;
+
+ public AuthorizationFailedException(final String message) {
+ super(message);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Used as a notification to close SSE connections.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class MieleWebserviceDisconnectSseException extends RuntimeException {
+ private static final long serialVersionUID = 607435177026345387L;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.webservice.ConnectionError;
+
+/**
+ * {@link RuntimeException} thrown if the Miele service is not available or unable to handle requests.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class MieleWebserviceException extends RuntimeException {
+
+ private static final long serialVersionUID = 6268725866086530042L;
+
+ private final ConnectionError connectionError;
+
+ public MieleWebserviceException(final String message, final ConnectionError connectionError) {
+ super(message);
+ this.connectionError = connectionError;
+ }
+
+ public MieleWebserviceException(final String message, @Nullable final Throwable cause,
+ final ConnectionError connectionError) {
+ super(message, cause);
+ this.connectionError = connectionError;
+ }
+
+ public ConnectionError getConnectionError() {
+ return connectionError;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Exception thrown when the Miele webservice fails to initialize.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class MieleWebserviceInitializationException extends RuntimeException {
+ private static final long serialVersionUID = -3778846331483843234L;
+
+ public MieleWebserviceInitializationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.exception;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.webservice.ConnectionError;
+
+/**
+ * {@link RuntimeException} thrown if a transient error occurred which the binding can recover from by retrying.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class MieleWebserviceTransientException extends RuntimeException {
+ private static final long serialVersionUID = -1863609233382694104L;
+
+ private final ConnectionError connectionError;
+
+ public MieleWebserviceTransientException(final String message, final ConnectionError connectionError) {
+ super(message);
+ this.connectionError = connectionError;
+ }
+
+ public MieleWebserviceTransientException(final String message, final Throwable cause,
+ final ConnectionError connectionError) {
+ super(message, cause);
+ this.connectionError = connectionError;
+ }
+
+ public ConnectionError getConnectionError() {
+ return connectionError;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.exception;
+
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.Locale;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@link RuntimeException} indicating that too many requests have been made against the cloud service.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class TooManyRequestsException extends RuntimeException {
+ private static final long serialVersionUID = 3393292912418862566L;
+
+ @Nullable
+ private final String retryAfter;
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ public TooManyRequestsException(String message, @Nullable String retryAfter) {
+ super(message);
+ this.retryAfter = retryAfter;
+ }
+
+ /**
+ * Gets whether a hint on when to retry the operation is available.
+ *
+ * @return Whether a hint on when to retry the operation is available.
+ */
+ public boolean hasRetryAfterHint() {
+ return retryAfter != null;
+ }
+
+ /**
+ * Gets the number of seconds until the operation may be retried.
+ *
+ * @return The number of seconds until the operation may be retried. This will return -1 if no Retry-After header
+ * was present or parsing the data from the header fails.
+ */
+ public long getSecondsUntilRetry() {
+ String retryAfter = this.retryAfter;
+ if (retryAfter == null) {
+ logger.debug("Received no Retry-After header.");
+ return -1;
+ }
+
+ logger.debug("Received Retry-After header: {}", retryAfter);
+ try {
+ long seconds = Long.parseLong(retryAfter);
+ logger.debug("Interpreted Retry-After header value: {} seconds", seconds);
+ return seconds;
+ } catch (NumberFormatException e) {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("ccc, d MMM yyyy HH:mm:ss z", Locale.US);
+
+ try {
+ LocalDateTime dateTime = LocalDateTime.parse(retryAfter, formatter);
+ logger.debug("Interpreted Retry-After header value: {}", dateTime);
+
+ Duration duration = Duration.between(LocalDateTime.now(), dateTime);
+
+ long seconds = Math.max(0, duration.toMillis() / 1000);
+ logger.debug("Interpreted Retry-After header value: {} seconds.", seconds);
+ return seconds;
+ } catch (DateTimeParseException dateTimeParseException) {
+ logger.warn("Unable to parse Retry-After header: {}", retryAfter);
+ return -1;
+ }
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.language;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * {@link LanguageProvider} combining two {@link LanguageProvider}s, a prioritized and a fallback provider.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class CombiningLanguageProvider implements LanguageProvider {
+ private @Nullable LanguageProvider prioritizedLanguageProvider;
+ private @Nullable LanguageProvider fallbackLanguageProvider;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param prioritizedLanguageProvider Primary {@link LanguageProvider} to use. May be {@code null}, in that case the
+ * {@code fallbackLanguageProvider} will be used.
+ * @param fallbackLanguageProvider {@link LanguageProvider} to fall back to if the
+ * {@code prioritizedLanguageProvider} is {@code null} or provides no language. May be
+ * {@code null}, in case the fallback is used and returns no language then no language will be returned.
+ */
+ public CombiningLanguageProvider(@Nullable LanguageProvider prioritizedLanguageProvider,
+ @Nullable LanguageProvider fallbackLanguageProvider) {
+ this.prioritizedLanguageProvider = prioritizedLanguageProvider;
+ this.fallbackLanguageProvider = fallbackLanguageProvider;
+ }
+
+ public void setPrioritizedLanguageProvider(LanguageProvider prioritizedLanguageProvider) {
+ this.prioritizedLanguageProvider = prioritizedLanguageProvider;
+ }
+
+ public void unsetPrioritizedLanguageProvider() {
+ this.prioritizedLanguageProvider = null;
+ }
+
+ public void setFallbackLanguageProvider(LanguageProvider fallbackLanguageProvider) {
+ this.fallbackLanguageProvider = fallbackLanguageProvider;
+ }
+
+ public void unsetFallbackLanguageProvider() {
+ this.fallbackLanguageProvider = null;
+ }
+
+ @Override
+ public Optional<String> getLanguage() {
+ Optional<String> prioritizedLanguage = Optional.ofNullable(prioritizedLanguageProvider)
+ .flatMap(LanguageProvider::getLanguage);
+ if (prioritizedLanguage.isPresent()) {
+ return prioritizedLanguage;
+ } else {
+ return Optional.ofNullable(fallbackLanguageProvider).flatMap(LanguageProvider::getLanguage);
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.language;
+
+import java.util.Locale;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * {@link LanguageProvider} returning the default JVM language.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class JvmLanguageProvider implements LanguageProvider {
+ @Override
+ public Optional<String> getLanguage() {
+ return Optional.ofNullable(Locale.getDefault()).map(Locale::getLanguage).filter(l -> !l.isEmpty());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.language;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Interface for providing language code information.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public interface LanguageProvider {
+ /**
+ * Gets a language represented as 2-letter language code.
+ *
+ * @return The language represented as 2-letter language code.
+ */
+ Optional<String> getLanguage();
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.language;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.i18n.LocaleProvider;
+
+/**
+ * Language provider relying on the openHAB runtime to provide a locale which is converted to a language.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class OpenHabLanguageProvider implements LanguageProvider {
+ private final LocaleProvider localeProvider;
+
+ public OpenHabLanguageProvider(LocaleProvider localeProvider) {
+ this.localeProvider = localeProvider;
+ }
+
+ @Override
+ public Optional<String> getLanguage() {
+ return Optional.of(localeProvider.getLocale().getLanguage());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.request;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.api.Request;
+
+/**
+ * Factory for {@link Request} objects.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public interface RequestFactory extends AutoCloseable {
+ /**
+ * Creates a GET {@link Request} for the given URL decorated with all required headers to interact with the Miele
+ * cloud.
+ *
+ * @param url The URL to GET.
+ * @param accessToken The OAuth2 access token for bearer authentication.
+ * @return The {@link Request}.
+ */
+ Request createGetRequest(String url, String accessToken);
+
+ /**
+ * Creates a PUT {@link Request} for the given URL decorated with all required headers to interact with the Miele
+ * cloud.
+ *
+ * @param url The URL to PUT.
+ * @param accessToken The OAuth2 access token for bearer authentication.
+ * @param jsonContent Json content to send in the body of the request.
+ * @return The {@link Request}.
+ */
+ Request createPutRequest(String url, String accessToken, String jsonContent);
+
+ /**
+ * Creates a POST {@link Request} for the given URL decorated with all required headers to interact with the Miele
+ * cloud.
+ *
+ * @param url The URL to POST.
+ * @param accessToken The OAuth2 access token for bearer authentication.
+ * @return The {@link Request}.
+ */
+ Request createPostRequest(String url, String accessToken);
+
+ /**
+ * Creates a GET request prepared for HTTP event stream data (also referred to as Server Sent Events, SSE).
+ *
+ * @param url The URL to subscribe to.
+ * @param accessToken The OAuth2 access token for bearer authentication.
+ * @return The {@link Request}.
+ */
+ Request createSseRequest(String url, String accessToken);
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.request;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceInitializationException;
+import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider;
+import org.openhab.core.io.net.http.HttpClientFactory;
+
+/**
+ * Default implementation of {@link RequestFactory}.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class RequestFactoryImpl implements RequestFactory {
+ private static final long REQUEST_TIMEOUT = 5;
+ private static final long EXTENDED_REQUEST_TIMEOUT = 10;
+ private static final TimeUnit REQUEST_TIMEOUT_UNIT = TimeUnit.SECONDS;
+
+ private final HttpClient httpClient;
+ private final LanguageProvider languageProvider;
+
+ /**
+ * Creates a new {@link RequestFactoryImpl}.
+ *
+ * @param httpClientFactory Factory for obtaining a {@link HttpClient}.
+ * @param languageProvider Provider for the language to use for new requests.
+ * @throws MieleWebserviceInitializationException if creating and starting a new {@link HttpClient} fails.
+ */
+ public RequestFactoryImpl(HttpClientFactory httpClientFactory, LanguageProvider languageProvider) {
+ this.httpClient = httpClientFactory.createHttpClient("mielecloud");
+ try {
+ this.httpClient.start();
+ } catch (Exception e) {
+ throw new MieleWebserviceInitializationException("Failed to start HttpClient", e);
+ }
+ this.languageProvider = languageProvider;
+ }
+
+ private Request createRequestWithDefaultHeaders(String url, String accessToken) {
+ return httpClient.newRequest(url).header("Content-type", "application/json").header("Authorization",
+ "Bearer " + accessToken);
+ }
+
+ private Request decorateWithLanguageParameter(Request request) {
+ Optional<String> language = languageProvider.getLanguage();
+ if (language.isPresent() && !language.get().isEmpty()) {
+ return request.param("language", language.get());
+ } else {
+ return request;
+ }
+ }
+
+ private Request decorateWithAcceptLanguageHeader(Request request) {
+ Optional<String> language = languageProvider.getLanguage();
+ if (language.isPresent() && !language.get().isEmpty()) {
+ return request.header("Accept-Language", language.get());
+ } else {
+ return request;
+ }
+ }
+
+ private Request createDefaultHttpRequest(String url, String accessToken, long timeout) {
+ return decorateWithLanguageParameter(createRequestWithDefaultHeaders(url, accessToken)).header("Accept", "*/*")
+ .timeout(timeout, REQUEST_TIMEOUT_UNIT);
+ }
+
+ @Override
+ public Request createGetRequest(String url, String accessToken) {
+ return createDefaultHttpRequest(url, accessToken, REQUEST_TIMEOUT).method(HttpMethod.GET);
+ }
+
+ @Override
+ public Request createPutRequest(String url, String accessToken, String jsonContent) {
+ return createDefaultHttpRequest(url, accessToken, EXTENDED_REQUEST_TIMEOUT).method(HttpMethod.PUT)
+ .content(new StringContentProvider("application/json", jsonContent, StandardCharsets.UTF_8));
+ }
+
+ @Override
+ public Request createPostRequest(String url, String accessToken) {
+ return createDefaultHttpRequest(url, accessToken, REQUEST_TIMEOUT).method(HttpMethod.POST);
+ }
+
+ @Override
+ public Request createSseRequest(String url, String accessToken) {
+ return decorateWithAcceptLanguageHeader(createRequestWithDefaultHeaders(url, accessToken)).header("Accept",
+ "text/event-stream");
+ }
+
+ @Override
+ public void close() throws Exception {
+ httpClient.stop();
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.retry;
+
+import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.auth.OAuthException;
+import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.webservice.ConnectionError;
+import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link AuthorizationFailedRetryStrategy} retries an operation after refreshing the access token in case of an
+ * authorization failure.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class AuthorizationFailedRetryStrategy implements RetryStrategy {
+ /**
+ * Message of exception thrown by the Jetty client in case of unmatching header fields and body content. E.g.
+ * application/json header with HTML body content. Mostly thrown when an invalid 401 response is received.
+ */
+ public static final String JETTY_401_HEADER_BODY_MISMATCH_EXCEPTION_MESSAGE = "org.eclipse.jetty.client.HttpResponseException: HTTP protocol violation: Authentication challenge without WWW-Authenticate header";
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ private final OAuthTokenRefresher tokenRefresher;
+ private final String serviceHandle;
+
+ public AuthorizationFailedRetryStrategy(OAuthTokenRefresher tokenRefresher, String serviceHandle) {
+ this.tokenRefresher = tokenRefresher;
+ this.serviceHandle = serviceHandle;
+ }
+
+ private void refreshToken() {
+ try {
+ logger.debug("Refreshing Miele OAuth access token.");
+ tokenRefresher.refreshToken(serviceHandle);
+ logger.debug("Miele OAuth access token has successfully been refreshed.");
+ } catch (OAuthException e) {
+ throw new MieleWebserviceException("Failed to refresh access token.", e,
+ ConnectionError.AUTHORIZATION_FAILED);
+ }
+ }
+
+ @Override
+ public <@Nullable T> T performRetryableOperation(Supplier<T> operation, Consumer<Exception> onException) {
+ try {
+ return operation.get();
+ } catch (AuthorizationFailedException e) {
+ onException.accept(e);
+ refreshToken();
+ } catch (MieleWebserviceException e) {
+ // Workaround for HTML response from cloud in case of a 401 HTTP error.
+ var cause = e.getCause();
+ if (cause == null || !(cause instanceof ExecutionException)) {
+ throw e;
+ }
+
+ if (!JETTY_401_HEADER_BODY_MISMATCH_EXCEPTION_MESSAGE.equals(cause.getMessage())) {
+ throw e;
+ }
+
+ onException.accept(e);
+ refreshToken();
+ }
+
+ try {
+ return operation.get();
+ } catch (AuthorizationFailedException e) {
+ throw new MieleWebserviceException("Request failed after access token renewal.", e,
+ ConnectionError.AUTHORIZATION_FAILED);
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.retry;
+
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.webservice.ConnectionError;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceTransientException;
+
+/**
+ * {@link RetryStrategy} retrying a failing operation for a number of times.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class NTimesRetryStrategy implements RetryStrategy {
+ private final int numberOfRetries;
+
+ /**
+ * Creates a new {@link NTimesRetryStrategy}.
+ *
+ * @param numberOfRetries The number of retries to make.
+ * @throws IllegalArgumentException if {@code numberOfRetries} is smaller than zero.
+ */
+ public NTimesRetryStrategy(int numberOfRetries) {
+ if (numberOfRetries < 0) {
+ throw new IllegalArgumentException("Number of retries must not be negative.");
+ }
+
+ this.numberOfRetries = numberOfRetries;
+ }
+
+ @Override
+ public <@Nullable T> T performRetryableOperation(Supplier<T> operation, Consumer<Exception> onException) {
+ boolean obtainedReturnValue = false;
+ T returnValue = null;
+ MieleWebserviceTransientException lastException = null;
+ for (int i = 0; !obtainedReturnValue && i < numberOfRetries + 1; i++) {
+ try {
+ returnValue = operation.get();
+ obtainedReturnValue = true;
+ } catch (MieleWebserviceTransientException e) {
+ lastException = e;
+ if (i < numberOfRetries) {
+ onException.accept(e);
+ }
+ }
+ }
+
+ if (!obtainedReturnValue) {
+ throw new MieleWebserviceException(
+ "Unable to perform operation. Operation failed " + (numberOfRetries + 1) + " times.", lastException,
+ lastException == null ? ConnectionError.UNKNOWN : lastException.getConnectionError());
+ } else {
+ return returnValue;
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.retry;
+
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Interface for strategies implementing the retry behavior of requests against the Miele cloud.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public interface RetryStrategy {
+ /**
+ * Performs an operation which may be retried several times.
+ *
+ * If retrying fails or a critical error occurred, this method may throw {@link Exception}s of any type.
+ *
+ * @param operation The operation to perform. To signal that an error can be resolved by retrying this operation it
+ * should throw an {@link Exception}. Whether the operation is retried is up to the {@link RetryStrategy}
+ * implementation.
+ * @param onException Handler to invoke when an {@link Exception} is handled by retrying the {@code operation}. This
+ * handler should at least log a message. It must not throw any exception.
+ * @return The object returned by {@code operation} if it completed successfully.
+ */
+ <@Nullable T> T performRetryableOperation(Supplier<T> operation, Consumer<Exception> onException);
+
+ /**
+ * Performs an operation which may be retried several times.
+ *
+ * If retrying fails or a critical error occurred, this method may throw {@link Exception}s of any type.
+ *
+ * @param operation The operation to perform. To signal that an error can be resolved by retrying this operation it
+ * should throw an {@link Exception}. Whether the operation is retried is up to the {@link RetryStrategy}
+ * implementation
+ * @param onException Handler to invoke when an {@link Exception} is handled by retrying the {@code operation}. This
+ * handler should at least log a message. It may not throw any exception.
+ */
+ default void performRetryableOperation(Runnable operation, Consumer<Exception> onException) {
+ performRetryableOperation(new Supplier<@Nullable Void>() {
+ @Override
+ public @Nullable Void get() {
+ operation.run();
+ return null;
+ }
+ }, onException);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.retry;
+
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * {@link RetryStrategy} implementation wrapping the consecutive execution of two retry strategies.
+ *
+ * @author Björn Lange and Roland Edelhoff - Initial contribution
+ */
+@NonNullByDefault
+public class RetryStrategyCombiner implements RetryStrategy {
+ private final RetryStrategy first;
+ private final RetryStrategy second;
+
+ /**
+ * Creates a new {@link RetryStrategy} combining the given ones.
+ *
+ * @param first First strategy to execute.
+ * @param second Strategy to execute in each execution of {@code first}.
+ */
+ public RetryStrategyCombiner(RetryStrategy first, RetryStrategy second) {
+ this.first = first;
+ this.second = second;
+ }
+
+ @Override
+ public <@Nullable T> T performRetryableOperation(Supplier<T> operation, Consumer<Exception> onException) {
+ return first.performRetryableOperation(() -> second.performRetryableOperation(operation, onException),
+ onException);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.sse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * A strategy computing the wait time between multiple connection attempts.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+interface BackoffStrategy {
+ /**
+ * Gets the minimal number of seconds to wait until retrying an operation. This is the lower bound of the value
+ * returned by {@link #getSecondsUntilRetry(int)}.
+ *
+ * @return The minimal number of seconds to wait until retrying an operation. Always larger or equal to zero, always
+ * smaller than {@link #getMaximumSecondsUntilRetry()}.
+ */
+ long getMinimumSecondsUntilRetry();
+
+ /**
+ * Gets the maximal number of seconds to wait until retrying an operation. This is the upper bound of the value
+ * returned by {@link #getSecondsUntilRetry(int)}.
+ *
+ * @return The maximal number of seconds to wait until retrying an operation. Always larger or equal to zero, always
+ * larger than {@link #getMinimumSecondsUntilRetry()}.
+ */
+ long getMaximumSecondsUntilRetry();
+
+ /**
+ * Gets the number of seconds until a retryable operation is performed. The value returned by this method is within
+ * the interval defined by {@link #getMinimumSecondsUntilRetry()} and {@link #getMaximumSecondsUntilRetry()}.
+ *
+ * @param failedConnectionAttempts The number of failed attempts.
+ * @return The number of seconds to wait before making the next attempt.
+ */
+ long getSecondsUntilRetry(int failedAttempts);
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.sse;
+
+import java.util.Random;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implements the exponential backoff with jitter backoff strategy.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+class ExponentialBackoffWithJitter implements BackoffStrategy {
+ private static final long INITIAL_RECONNECT_ATTEMPT_WAIT_TIME_IN_SECONDS = 5;
+ private static final long MAXIMUM_RECONNECT_ATTEMPT_WAIT_TIME_IN_SECONDS = 3600;
+
+ private final long minimumWaitTimeInSeconds;
+ private final long maximumWaitTimeInSeconds;
+ private final long retryIntervalInSeconds;
+ private final Random random;
+
+ private final Logger logger = LoggerFactory.getLogger(ExponentialBackoffWithJitter.class);
+
+ /**
+ * Creates a new {@link ExponentialBackoffWithJitter}.
+ */
+ public ExponentialBackoffWithJitter() {
+ this(INITIAL_RECONNECT_ATTEMPT_WAIT_TIME_IN_SECONDS, MAXIMUM_RECONNECT_ATTEMPT_WAIT_TIME_IN_SECONDS,
+ INITIAL_RECONNECT_ATTEMPT_WAIT_TIME_IN_SECONDS);
+ }
+
+ ExponentialBackoffWithJitter(long minimumWaitTimeInSeconds, long maximumWaitTimeInSeconds,
+ long retryIntervalInSeconds) {
+ this(minimumWaitTimeInSeconds, maximumWaitTimeInSeconds, retryIntervalInSeconds, new Random());
+ }
+
+ ExponentialBackoffWithJitter(long minimumWaitTimeInSeconds, long maximumWaitTimeInSeconds,
+ long retryIntervalInSeconds, Random random) {
+ if (minimumWaitTimeInSeconds < 0) {
+ throw new IllegalArgumentException("minimumWaitTimeInSeconds must not be smaller than zero");
+ }
+ if (maximumWaitTimeInSeconds < 0) {
+ throw new IllegalArgumentException("maximumWaitTimeInSeconds must not be smaller than zero");
+ }
+ if (retryIntervalInSeconds < 0) {
+ throw new IllegalArgumentException("retryIntervalInSeconds must not be smaller than zero");
+ }
+ if (maximumWaitTimeInSeconds < minimumWaitTimeInSeconds) {
+ throw new IllegalArgumentException(
+ "maximumWaitTimeInSeconds must not be smaller than minimumWaitTimeInSeconds");
+ }
+ if (maximumWaitTimeInSeconds < retryIntervalInSeconds) {
+ throw new IllegalArgumentException(
+ "maximumWaitTimeInSeconds must not be smaller than retryIntervalInSeconds");
+ }
+
+ this.minimumWaitTimeInSeconds = minimumWaitTimeInSeconds;
+ this.maximumWaitTimeInSeconds = maximumWaitTimeInSeconds;
+ this.retryIntervalInSeconds = retryIntervalInSeconds;
+ this.random = random;
+ }
+
+ @Override
+ public long getMinimumSecondsUntilRetry() {
+ return minimumWaitTimeInSeconds;
+ }
+
+ @Override
+ public long getMaximumSecondsUntilRetry() {
+ return maximumWaitTimeInSeconds;
+ }
+
+ @Override
+ public long getSecondsUntilRetry(int failedAttempts) {
+ if (failedAttempts < 0) {
+ logger.warn("The number of failed attempts must not be smaller than zero, was {}.", failedAttempts);
+ }
+
+ return minimumWaitTimeInSeconds
+ + getRandomLongWithUpperLimit(Math.min(maximumWaitTimeInSeconds - minimumWaitTimeInSeconds,
+ retryIntervalInSeconds * (long) Math.pow(2, Math.max(0, failedAttempts))));
+ }
+
+ private long getRandomLongWithUpperLimit(long upperLimit) {
+ return Math.abs(random.nextLong()) % (upperLimit + 1);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.sse;
+
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * An event emitted by an SSE connection.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class ServerSentEvent {
+ private final String event;
+ private final String data;
+
+ ServerSentEvent(String event, String data) {
+ this.event = event;
+ this.data = data;
+ }
+
+ public String getEvent() {
+ return event;
+ }
+
+ public String getData() {
+ return data;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(event, data);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ ServerSentEvent other = (ServerSentEvent) obj;
+ return Objects.equals(event, other.event) && Objects.equals(data, other.data);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.sse;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.client.util.InputStreamResponseListener;
+import org.openhab.binding.mielecloud.internal.webservice.ConnectionError;
+import org.openhab.binding.mielecloud.internal.webservice.HttpUtil;
+import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceDisconnectSseException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceTransientException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
+import org.openhab.binding.mielecloud.internal.webservice.retry.AuthorizationFailedRetryStrategy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An active or inactive SSE connection emitting a stream of events.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class SseConnection {
+ private static final long CONNECTION_TIMEOUT = 30;
+ private static final TimeUnit CONNECTION_TIMEOUT_UNIT = TimeUnit.SECONDS;
+
+ private final Logger logger = LoggerFactory.getLogger(SseConnection.class);
+
+ private final String endpoint;
+ private final SseRequestFactory requestFactory;
+ private final ScheduledExecutorService scheduler;
+ private final BackoffStrategy backoffStrategy;
+
+ private final List<SseListener> listeners = new ArrayList<>();
+
+ private boolean active = false;
+
+ private int failedConnectionAttempts = 0;
+
+ @Nullable
+ private Request sseRequest;
+
+ /**
+ * Creates a new {@link SseConnection} to the given endpoint.
+ *
+ * Note: It is required to call {@link #connect()} in order to open the connection and start receiving events.
+ *
+ * @param endpoint The endpoint to connect to.
+ * @param requestFactory Factory for creating requests.
+ * @param scheduler Scheduler to run scheduled and concurrent tasks on.
+ */
+ public SseConnection(String endpoint, SseRequestFactory requestFactory, ScheduledExecutorService scheduler) {
+ this(endpoint, requestFactory, scheduler, new ExponentialBackoffWithJitter());
+ }
+
+ /**
+ * Creates a new {@link SseConnection} to the given endpoint.
+ *
+ * Note: It is required to call {@link #connect()} in order to open the connection and start receiving events.
+ *
+ * @param endpoint The endpoint to connect to.
+ * @param requestFactory Factory for creating requests.
+ * @param scheduler Scheduler to run scheduled and concurrent tasks on.
+ * @param backoffStrategy Strategy for deriving the wait time between connection attempts.
+ */
+ SseConnection(String endpoint, SseRequestFactory requestFactory, ScheduledExecutorService scheduler,
+ BackoffStrategy backoffStrategy) {
+ this.endpoint = endpoint;
+ this.requestFactory = requestFactory;
+ this.scheduler = scheduler;
+ this.backoffStrategy = backoffStrategy;
+ }
+
+ public synchronized void connect() {
+ active = true;
+ connectInternal();
+ }
+
+ private synchronized void connectInternal() {
+ if (!active) {
+ return;
+ }
+
+ Request runningRequest = this.sseRequest;
+ if (runningRequest != null) {
+ return;
+ }
+
+ logger.debug("Opening SSE connection...");
+ Request sseRequest = createRequest();
+ if (sseRequest == null) {
+ logger.warn("Could not create SSE request, not opening SSE connection.");
+ return;
+ }
+
+ final InputStreamResponseListener stream = new InputStreamResponseListener();
+ SseStreamParser eventStreamParser = new SseStreamParser(stream.getInputStream(), this::onServerSentEvent,
+ this::onSseStreamClosed);
+
+ sseRequest = sseRequest
+ .onResponseHeaders(
+ response -> scheduler.schedule(eventStreamParser::parseAndDispatchEvents, 0, TimeUnit.SECONDS))
+ .onComplete(result -> onConnectionComplete(result));
+ sseRequest.send(stream);
+ this.sseRequest = sseRequest;
+ }
+
+ @Nullable
+ private Request createRequest() {
+ Request sseRequest = requestFactory.createSseRequest(endpoint);
+ if (sseRequest == null) {
+ return null;
+ }
+
+ return sseRequest.timeout(0, TimeUnit.SECONDS).idleTimeout(CONNECTION_TIMEOUT, CONNECTION_TIMEOUT_UNIT);
+ }
+
+ private synchronized void onSseStreamClosed(@Nullable Throwable exception) {
+ if (exception != null && AuthorizationFailedRetryStrategy.JETTY_401_HEADER_BODY_MISMATCH_EXCEPTION_MESSAGE
+ .equals(exception.getMessage())) {
+ onConnectionError(ConnectionError.AUTHORIZATION_FAILED);
+ } else if (exception instanceof TimeoutException) {
+ onConnectionError(ConnectionError.TIMEOUT);
+ } else {
+ onConnectionError(ConnectionError.SSE_STREAM_ENDED);
+ }
+ }
+
+ private synchronized void onConnectionComplete(@Nullable Result result) {
+ sseRequest = null;
+
+ if (result == null) {
+ logger.warn("SSE stream was closed but there was no result delivered.");
+ onConnectionError(ConnectionError.SSE_STREAM_ENDED);
+ return;
+ }
+
+ Response response = result.getResponse();
+ if (response == null) {
+ logger.warn("SSE stream was closed without response.");
+ onConnectionError(ConnectionError.SSE_STREAM_ENDED);
+ return;
+ }
+
+ onConnectionClosed(response);
+ }
+
+ private void onConnectionClosed(Response response) {
+ try {
+ HttpUtil.checkHttpSuccess(response);
+ onConnectionError(ConnectionError.SSE_STREAM_ENDED);
+ } catch (AuthorizationFailedException e) {
+ onConnectionError(ConnectionError.AUTHORIZATION_FAILED);
+ } catch (TooManyRequestsException e) {
+ long secondsUntilRetry = e.getSecondsUntilRetry();
+ if (secondsUntilRetry < 0) {
+ onConnectionError(ConnectionError.TOO_MANY_RERQUESTS);
+ } else {
+ onConnectionError(ConnectionError.TOO_MANY_RERQUESTS, secondsUntilRetry);
+ }
+ } catch (MieleWebserviceTransientException e) {
+ onConnectionError(e.getConnectionError(), 0);
+ } catch (MieleWebserviceException e) {
+ onConnectionError(e.getConnectionError());
+ }
+ }
+
+ private void onConnectionError(ConnectionError connectionError) {
+ onConnectionError(connectionError, backoffStrategy.getSecondsUntilRetry(failedConnectionAttempts));
+ }
+
+ private synchronized void onConnectionError(ConnectionError connectionError, long secondsUntilRetry) {
+ if (!active) {
+ return;
+ }
+
+ if (connectionError != ConnectionError.AUTHORIZATION_FAILED) {
+ scheduleReconnect(secondsUntilRetry);
+ }
+
+ fireConnectionError(connectionError);
+ failedConnectionAttempts++;
+ }
+
+ private void scheduleReconnect(long secondsUntilRetry) {
+ long retryInSeconds = Math.max(backoffStrategy.getMinimumSecondsUntilRetry(),
+ Math.min(secondsUntilRetry, backoffStrategy.getMaximumSecondsUntilRetry()));
+ scheduler.schedule(this::connectInternal, retryInSeconds, TimeUnit.SECONDS);
+ logger.debug("Scheduled reconnect attempt for Miele webservice to take place in {} seconds", retryInSeconds);
+ }
+
+ public synchronized void disconnect() {
+ active = false;
+
+ Request runningRequest = sseRequest;
+ if (runningRequest == null) {
+ logger.debug("SSE connection is not established, skipping SSE disconnect.");
+ return;
+ }
+
+ logger.debug("Disconnecting SSE");
+ runningRequest.abort(new MieleWebserviceDisconnectSseException());
+ sseRequest = null;
+ logger.debug("Disconnected");
+ }
+
+ private void onServerSentEvent(ServerSentEvent event) {
+ failedConnectionAttempts = 0;
+ listeners.forEach(l -> l.onServerSentEvent(event));
+ }
+
+ private void fireConnectionError(ConnectionError connectionError) {
+ listeners.forEach(l -> l.onConnectionError(connectionError, failedConnectionAttempts));
+ }
+
+ public void addSseListener(SseListener listener) {
+ listeners.add(listener);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.sse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.webservice.ConnectionError;
+
+/**
+ * Listens to events received via a SSE connection and errors concerning that connection.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public interface SseListener {
+ /**
+ * Called when an event is received via a SSE connection.
+ *
+ * @param event The received event.
+ */
+ void onServerSentEvent(ServerSentEvent event);
+
+ /**
+ * Called when an error occurs that is related to the connection and cannot be handled automatically.
+ *
+ * @param connectionError The connection error.
+ * @param failedReconnectAttempts The number of attempts that were made to reconnect to the event stream.
+ */
+ void onConnectionError(ConnectionError connectionError, int failedReconnectAttempts);
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.sse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.Request;
+
+/**
+ * Factory that produces configured {@link Request} instances for usage with SSE.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+@FunctionalInterface
+public interface SseRequestFactory {
+ /**
+ * Produces a {@link Request} which is decorated with all required headers.
+ *
+ * @param endpoint The endpoint to connect to.
+ * @return The created {@link Request} or {@code null} if no request can be created due to lacking request
+ * information. If this method returns {@code null} then all connection attempts will be cancelled.
+ */
+ @Nullable
+ Request createSseRequest(String endpoint);
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.sse;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceDisconnectSseException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Parses events from the SSE event stream and emits them via the given dispatcher.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+class SseStreamParser {
+ private static final String SSE_KEY_EVENT = "event:";
+ private static final String SSE_KEY_DATA = "data:";
+
+ private final Logger logger = LoggerFactory.getLogger(SseStreamParser.class);
+
+ private final BufferedReader reader;
+ private final Consumer<ServerSentEvent> onServerSentEventCallback;
+ private final Consumer<@Nullable Throwable> onStreamClosedCallback;
+
+ private @Nullable String event;
+
+ SseStreamParser(InputStream inputStream, Consumer<ServerSentEvent> onServerSentEventCallback,
+ Consumer<@Nullable Throwable> onStreamClosedCallback) {
+ this.reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
+ this.onServerSentEventCallback = onServerSentEventCallback;
+ this.onStreamClosedCallback = onStreamClosedCallback;
+ }
+
+ void parseAndDispatchEvents() {
+ try {
+ String line = null;
+ while ((line = reader.readLine()) != null) {
+ onLineReceived(line);
+ }
+
+ silentlyCloseReader();
+ logger.debug("SSE stream ended. Closing stream.");
+ onStreamClosedCallback.accept(null);
+ } catch (IOException exception) {
+ silentlyCloseReader();
+
+ if (!(exception.getCause() instanceof MieleWebserviceDisconnectSseException)) {
+ logger.warn("SSE connection failed unexpectedly: {}", exception.getMessage());
+ onStreamClosedCallback.accept(exception.getCause());
+ }
+ }
+ logger.debug("SSE stream closed.");
+ }
+
+ private void silentlyCloseReader() {
+ try {
+ reader.close();
+ } catch (IOException e) {
+ logger.warn("Failed to clean up SSE connection resources!", e);
+ }
+ }
+
+ private void onLineReceived(String line) {
+ if (line.isEmpty()) {
+ return;
+ }
+
+ if (line.startsWith(SSE_KEY_EVENT)) {
+ event = line.substring(SSE_KEY_EVENT.length()).trim();
+ } else if (line.startsWith(SSE_KEY_DATA)) {
+ String event = this.event;
+ String data = line.substring(SSE_KEY_DATA.length()).trim();
+
+ if (event == null) {
+ logger.warn("Received data payload without prior event payload.");
+ } else {
+ onServerSentEventCallback.accept(new ServerSentEvent(event, data));
+ }
+ } else {
+ logger.warn("Unable to parse line from SSE stream: {}", line);
+ }
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="mielecloud" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+ <name>@text/binding.mielecloud.name</name>
+ <description>@text/binding.mielecloud.description</description>
+</binding:binding>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+ xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
+ xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0">
+
+ <config-description uri="thing-type:mielecloud:device">
+ <parameter name="deviceIdentifier" type="text" required="true">
+ <label>@text/thing-type.config.mielecloud.device.deviceIdentifier.label</label>
+ <description>@text/thing-type.config.mielecloud.device.deviceIdentifier.description</description>
+ </parameter>
+ </config-description>
+
+</config-description:config-descriptions>
--- /dev/null
+# Binding related texts
+binding.mielecloud.name=Miele@home Cloud Binding
+binding.mielecloud.description=This is the cloud-based Miele@home binding.
+
+# Thing related texts
+thing-type.mielecloud.account.label=Miele@home Account
+thing-type.mielecloud.account.description=The Miele@home Account is used to access linked Miele Conn@ct smart home devices.
+
+thing-type.config.mielecloud.account.locale.label=E-mail
+thing-type.config.mielecloud.account.locale.description=E-mail address associated with the Miele Cloud account.
+
+thing-type.config.mielecloud.account.locale.label=Locale
+thing-type.config.mielecloud.account.locale.description=Locale to be used for API calls.
+
+thing-type.config.mielecloud.device.deviceIdentifier.label=Device identifier
+thing-type.config.mielecloud.device.deviceIdentifier.description=Technical device identifier used to identify the Miele device.
+
+thing-type.mielecloud.coffee_system.label=Coffee System
+thing-type.mielecloud.coffee_system.description=The generic thing type for all Miele coffee systems.
+
+thing-type.mielecloud.dishwasher.label=Dishwasher
+thing-type.mielecloud.dishwasher.description=The generic thing type for all Miele dish washing devices.
+
+thing-type.mielecloud.dish_warmer.label=Dish Warmer
+thing-type.mielecloud.dish_warmer.description=The generic thing type for all Miele dish warmer devices.
+
+thing-type.mielecloud.dryer.label=Tumble Dryer
+thing-type.mielecloud.dryer.description=The generic thing type for all Miele drying devices.
+
+thing-type.mielecloud.freezer.label=Freezer
+thing-type.mielecloud.freezer.description=The generic thing type for all Miele freezer devices.
+
+thing-type.mielecloud.fridge.label=Fridge
+thing-type.mielecloud.fridge.description=The generic thing type for all Miele fridge devices.
+
+thing-type.mielecloud.fridge_freezer.label=Fridge Freezer
+thing-type.mielecloud.fridge_freezer.description=The generic thing type for all Miele fridge freezer devices.
+
+thing-type.mielecloud.hob.label=Hob
+thing-type.mielecloud.hob.description=The generic thing type for all Miele hob devices.
+
+thing-type.mielecloud.hood.label=Hood
+thing-type.mielecloud.hood.description=The generic thing type for all Miele hood devices.
+
+thing-type.mielecloud.oven.label=Oven
+thing-type.mielecloud.oven.description=The generic thing type for all Miele oven devices. Includes also Steam Ovens and Dialog Oven.
+
+thing-type.mielecloud.robotic_vacuum_cleaner.label=Robotic Vacuum Cleaner
+thing-type.mielecloud.robotic_vacuum_cleaner.description=The generic thing type for all Miele robotic vacuum cleaner devices.
+
+thing-type.mielecloud.washer_dryer.label=Washer Dryer
+thing-type.mielecloud.washer_dryer.description=The generic thing type for all Miele washer dryer devices.
+
+thing-type.mielecloud.washing_machine.label=Washing Machine
+thing-type.mielecloud.washing_machine.description=The generic thing type for all Miele washing devices.
+
+thing-type.mielecloud.wine_storage.label=Wine Storage
+thing-type.mielecloud.wine_storage.description=The generic thing type for all Miele wine storage devices.
+
+# Channel related texts
+channel-type.mielecloud.remote_control_can_be_started.label=Can Be Started
+channel-type.mielecloud.remote_control_can_be_started.description=Indicates if this device can be started remotely.
+
+channel-type.mielecloud.remote_control_can_be_stopped.label=Can Be Stopped
+channel-type.mielecloud.remote_control_can_be_stopped.description=Indicates if this device can be stopped remotely.
+
+channel-type.mielecloud.remote_control_can_be_paused.label=Can Be Paused
+channel-type.mielecloud.remote_control_can_be_paused.description=Indicates if this device can be paused remotely.
+
+channel-type.mielecloud.remote_control_can_be_switched_on.label=Can Be Switched On
+channel-type.mielecloud.remote_control_can_be_switched_on.description=Indicates if the device can be switched on remotely.
+
+channel-type.mielecloud.remote_control_can_be_switched_off.label=Can Be Switched Off
+channel-type.mielecloud.remote_control_can_be_switched_off.description=Indicates if the device can be switched off remotely.
+
+channel-type.mielecloud.remote_control_can_set_program_active.label=Can Set Active Program
+channel-type.mielecloud.remote_control_can_set_program_active.description=Indicates if the active program of the device can be set remotely.
+
+channel-type.mielecloud.spinning_speed.label=Spinning Speed
+channel-type.mielecloud.spinning_speed.description=The spinning speed of the active program.
+
+channel-type.mielecloud.spinning_speed_raw.label=Raw Spinning Speed
+channel-type.mielecloud.spinning_speed_raw.description=The raw spinning speed of the active program.
+
+channel-type.mielecloud.program_active.label=Active Program
+channel-type.mielecloud.program_active.description=The active program of the device.
+
+channel-type.mielecloud.program_active_raw.label=Raw Active Program
+channel-type.mielecloud.program_active_raw.description=The raw active program of the device.
+
+channel-type.mielecloud.dish_warmer_program_active.label=Active Program
+channel-type.mielecloud.dish_warmer_program_active.description=The active program of the device.
+channel-option.mielecloud.dish_warmer_program_active.warming_cups_glasses=Warming cups/glasses
+channel-option.mielecloud.dish_warmer_program_active.warming_dishes_plates=Warming dishes/plates
+channel-option.mielecloud.dish_warmer_program_active.keeping_food_warm=Keeping food warm
+channel-option.mielecloud.dish_warmer_program_active.low_temperature_cooking=Low temperature cooking
+
+channel-type.mielecloud.vacuum_cleaner_program_active.label=Active Program
+channel-type.mielecloud.vacuum_cleaner_program_active.description=The active program of the device.
+channel-option.mielecloud.vacuum_cleaner_program_active.auto=Auto
+channel-option.mielecloud.vacuum_cleaner_program_active.spot=Spot
+channel-option.mielecloud.vacuum_cleaner_program_active.turbo=Turbo
+channel-option.mielecloud.vacuum_cleaner_program_active.silent=Silent
+
+channel-type.mielecloud.program_phase.label=Program Phase
+channel-type.mielecloud.program_phase.description=The phase of the active program.
+
+channel-type.mielecloud.program_phase_raw.label=Raw Program Phase
+channel-type.mielecloud.program_phase_raw.description=The raw phase of the active program.
+
+channel-type.mielecloud.operation_state.label=Operation State
+channel-type.mielecloud.operation_state.description=The operation state of the device.
+
+channel-type.mielecloud.operation_state_raw.label=Raw Operation State
+channel-type.mielecloud.operation_state_raw.description=The raw operation state of the device.
+
+channel-type.mielecloud.program_start.label=Start
+channel-type.mielecloud.program_start.description=Starts the currently selected program.
+
+channel-type.mielecloud.program_stop.label=Stop
+channel-type.mielecloud.program_stop.description=Stops the currently selected program.
+
+channel-type.mielecloud.program_start_stop.label=Start Stop
+channel-type.mielecloud.program_start_stop.description=Starts or stops the currently selected program.
+channel-option.mielecloud.program_start_stop.start=Start
+channel-option.mielecloud.program_start_stop.stop=Stop
+
+channel-type.mielecloud.program_start_stop_pause.label=Start Stop Pause
+channel-type.mielecloud.program_start_stop_pause.description=Starts, stops or pauses the currently selected program.
+channel-option.mielecloud.program_start_stop_pause.start=Start
+channel-option.mielecloud.program_start_stop_pause.stop=Stop
+channel-option.mielecloud.program_start_stop_pause.pause=Pause
+
+channel-type.mielecloud.power_state_on_off.label=Power
+channel-type.mielecloud.power_state_on_off.description=Switches the device On or Off.
+channel-option.mielecloud.power_state_on_off.on=On
+channel-option.mielecloud.power_state_on_off.off=Off
+
+channel-type.mielecloud.finish_state.label=Finished
+channel-type.mielecloud.finish_state.description=Indicates whether the most recent program finished.
+
+channel-type.mielecloud.delayed_start_time.label=Delayed Start Time
+channel-type.mielecloud.delayed_start_time.description=The delayed start time of the selected program.
+
+channel-type.mielecloud.program_remaining_time.label=Program Remaining Time
+channel-type.mielecloud.program_remaining_time.description=The remaining time of the active program.
+
+channel-type.mielecloud.program_elapsed_time.label=Program Elapsed Time
+channel-type.mielecloud.program_elapsed_time.description=The elapsed time of the active program.
+
+channel-type.mielecloud.program_progress.label=Program Progress
+channel-type.mielecloud.program_progress.description=The progress of the active program.
+
+channel-type.mielecloud.drying_target.label=Drying Target
+channel-type.mielecloud.drying_target.description=The target drying step of the laundry.
+
+channel-type.mielecloud.drying_target_raw.label=Raw Drying Target
+channel-type.mielecloud.drying_target_raw.description=The raw target drying step of the laundry.
+
+channel-type.mielecloud.pre_heat_finished.label=Pre-heat Finished
+channel-type.mielecloud.pre_heat_finished.description=Indicates whether the pre-heating finished.
+
+channel-type.mielecloud.temperature_target.label=Target Temperature
+channel-type.mielecloud.temperature_target.description=The target temperature of the device.
+
+channel-type.mielecloud.temperature_current.label=Current Temperature
+channel-type.mielecloud.temperature_current.description=The currently measured temperature of the device.
+
+channel-type.mielecloud.ventilation_power.label=Ventilation Power
+channel-type.mielecloud.ventilation_power.description=The current ventilation power of the hood.
+
+channel-type.mielecloud.ventilation_power_raw.label=Raw Ventilation Power
+channel-type.mielecloud.ventilation_power_raw.description=The current raw ventilation power of the hood.
+
+channel-type.mielecloud.error_state.label=Error
+channel-type.mielecloud.error_state.description=Indication flag which signals an error state for the device.
+
+channel-type.mielecloud.info_state.label=Info
+channel-type.mielecloud.info_state.description=Indication flag which signals an information of the device.
+
+channel-type.mielecloud.fridge_super_cool.label=Supercool
+channel-type.mielecloud.fridge_super_cool.description=Start the super cooling mode of the fridge.
+
+channel-type.mielecloud.freezer_super_freeze.label=Superfreeze
+channel-type.mielecloud.freezer_super_freeze.description=Start the super freezing mode of the freezer.
+
+channel-type.mielecloud.super_cool_can_be_controlled.label=Can Control Supercool
+channel-type.mielecloud.super_cool_can_be_controlled.description=Indicates if super cooling can be toggled.
+
+channel-type.mielecloud.super_freeze_can_be_controlled.label=Can Control Superfreeze
+channel-type.mielecloud.super_freeze_can_be_controlled.description=Indicates if super freezing can be toggled
+
+channel-type.mielecloud.fridge_temperature_target.label=Fridge Target Temperature
+channel-type.mielecloud.fridge_temperature_target.description=The target temperature of the fridge.
+
+channel-type.mielecloud.fridge_temperature_current.label=Current Fridge Temperature
+channel-type.mielecloud.fridge_temperature_current.description=The currently measured temperature of the fridge.
+
+channel-type.mielecloud.freezer_temperature_target.label=Freezer Target Temperature
+channel-type.mielecloud.freezer_temperature_target.description=The target temperature of the freezer.
+
+channel-type.mielecloud.freezer_temperature_current.label=Current Freezer Temperature
+channel-type.mielecloud.freezer_temperature_current.description=The currently measured temperature of the freezer.
+
+channel-type.mielecloud.top_temperature_target.label=Top Target Temperature
+channel-type.mielecloud.top_temperature_target.description=The target temperature of the top area.
+
+channel-type.mielecloud.top_temperature_current.label=Current Top Temperature
+channel-type.mielecloud.top_temperature_current.description=The currently measured temperature of the top area.
+
+channel-type.mielecloud.middle_temperature_target.label=Middle Target Temperature
+channel-type.mielecloud.middle_temperature_target.description=The target temperature of the middle area.
+
+channel-type.mielecloud.middle_temperature_current.label=Current Middle Temperature
+channel-type.mielecloud.middle_temperature_current.description=The currently measured temperature of the middle area.
+
+channel-type.mielecloud.bottom_temperature_target.label=Bottom Target Temperature
+channel-type.mielecloud.bottom_temperature_target.description=The target temperature of the bottom area.
+
+channel-type.mielecloud.bottom_temperature_current.label=Current Bottom Temperature
+channel-type.mielecloud.bottom_temperature_current.description=The currently measured temperature of the bottom area.
+
+channel-type.mielecloud.light_switch.label=Light Enabled
+channel-type.mielecloud.light_switch.description=Indicates if the light of the device is enabled.
+
+channel-type.mielecloud.light_can_be_controlled.label=Can Control Light
+channel-type.mielecloud.light_can_be_controlled.description=Indicates if the light of the device can be controlled.
+
+channel-type.mielecloud.plate_power_step.label=Plate Power Step
+channel-type.mielecloud.plate_power_step.description=The power level of the heating plate.
+
+channel-type.mielecloud.plate_power_step_raw.label=Raw Plate Power Step
+channel-type.mielecloud.plate_power_step_raw.description=The raw power level of the heating plate.
+
+channel-type.mielecloud.door_state.label=Door Signal
+channel-type.mielecloud.door_state.description=Indicates if the door of the device is open.
+
+channel-type.mielecloud.door_alarm.label=Door Alarm
+channel-type.mielecloud.door_alarm.description=Indicates if the door alarm of the device is active.
+
+channel-type.mielecloud.battery_level.label=Battery Level
+channel-type.mielecloud.battery_level.description=The battery level of the robotic vacuum cleaner.
+
+# Error message texts
+mielecloud.bridge.status.access.token.not.configured=The OAuth2 access token is not configured.
+mielecloud.bridge.status.account.not.authorized=The account has not been authorized. Please consult the documentation on how to do that.
+mielecloud.bridge.status.access.token.refresh.failed=Failed to refresh the OAuth2 access token. Please authorize the account again.
+mielecloud.bridge.status.invalid.email=The configured e-mail address has an invalid format.
+mielecloud.bridge.status.transient.http.error=An unexpected HTTP error occurred. Check the logs if this error persists.
+mielecloud.thing.status.webservice.missing=The Miele webservice cannot be accessed over the bridge. Check the bridge configuration.
+mielecloud.thing.status.removed=This Miele device has been removed from the Miele@home account.
+mielecloud.thing.status.ratelimit=The rate limit of the Miele cloud has been exceeded.
+mielecloud.thing.status.disconnected=This Miele device is not connected to the internet.
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <!-- Miele Cloud Connector Bridge -->
+ <bridge-type id="account">
+ <label>@text/thing-type.mielecloud.account.label</label>
+ <description>@text/thing-type.mielecloud.account.description</description>
+ <category>WebService</category>
+
+ <properties>
+ <property name="vendor">Miele</property>
+ <property name="modelId">Cloud Connector</property>
+ <property name="connection">INTERNET</property>
+ <!-- accessToken property is set on creation. -->
+ </properties>
+
+ <config-description>
+ <parameter name="email" type="text" required="true">
+ <context>email</context>
+ <label>@text/thing-type.config.mielecloud.account.email.label</label>
+ <description>@text/thing-type.config.mielecloud.account.email.description</description>
+ </parameter>
+ <parameter name="locale" type="text">
+ <label>@text/thing-type.config.mielecloud.account.locale.label</label>
+ <description>@text/thing-type.config.mielecloud.account.locale.description</description>
+ </parameter>
+ </config-description>
+ </bridge-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <channel-type id="remote_control_can_be_started">
+ <item-type>Switch</item-type>
+ <label>@text/channel-type.mielecloud.remote_control_can_be_started.label</label>
+ <description>@text/channel-type.mielecloud.remote_control_can_be_started.description</description>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="remote_control_can_be_stopped">
+ <item-type>Switch</item-type>
+ <label>@text/channel-type.mielecloud.remote_control_can_be_stopped.label</label>
+ <description>@text/channel-type.mielecloud.remote_control_can_be_stopped.description</description>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="remote_control_can_be_paused">
+ <item-type>Switch</item-type>
+ <label>@text/channel-type.mielecloud.remote_control_can_be_paused.label</label>
+ <description>@text/channel-type.mielecloud.remote_control_can_be_paused.description</description>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="remote_control_can_be_switched_on">
+ <item-type>Switch</item-type>
+ <label>@text/channel-type.mielecloud.remote_control_can_be_switched_on.label</label>
+ <description>@text/channel-type.mielecloud.remote_control_can_be_switched_on.description</description>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="remote_control_can_be_switched_off">
+ <item-type>Switch</item-type>
+ <label>@text/channel-type.mielecloud.remote_control_can_be_switched_off.label</label>
+ <description>@text/channel-type.mielecloud.remote_control_can_be_switched_off.description</description>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="remote_control_can_set_program_active">
+ <item-type>Switch</item-type>
+ <label>@text/channel-type.mielecloud.remote_control_can_set_program_active.label</label>
+ <description>@text/channel-type.mielecloud.remote_control_can_set_program_active.description</description>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="spinning_speed">
+ <item-type>String</item-type>
+ <label>@text/channel-type.mielecloud.spinning_speed.label</label>
+ <description>@text/channel-type.mielecloud.spinning_speed.description</description>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="spinning_speed_raw">
+ <item-type>Number</item-type>
+ <label>@text/channel-type.mielecloud.spinning_speed_raw.label</label>
+ <description>@text/channel-type.mielecloud.spinning_speed_raw.description</description>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="program_active">
+ <item-type>String</item-type>
+ <label>@text/channel-type.mielecloud.program_active.label</label>
+ <description>@text/channel-type.mielecloud.program_active.description</description>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="program_active_raw">
+ <item-type>Number</item-type>
+ <label>@text/channel-type.mielecloud.program_active_raw.label</label>
+ <description>@text/channel-type.mielecloud.program_active_raw.description</description>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="dish_warmer_program_active">
+ <item-type>String</item-type>
+ <label>@text/channel-type.mielecloud.dish_warmer_program_active.label</label>
+ <description>@text/channel-type.mielecloud.dish_warmer_program_active.description</description>
+ <state>
+ <options>
+ <option value="1">@text/channel-option.mielecloud.dish_warmer_program_active.warming_cups_glasses</option>
+ <option value="2">@text/channel-option.mielecloud.dish_warmer_program_active.warming_dishes_plates</option>
+ <option value="3">@text/channel-option.mielecloud.dish_warmer_program_active.keeping_food_warm</option>
+ <option value="4">@text/channel-option.mielecloud.dish_warmer_program_active.low_temperature_cooking</option>
+ </options>
+ </state>
+ </channel-type>
+
+ <channel-type id="vacuum_cleaner_program_active">
+ <item-type>String</item-type>
+ <label>@text/channel-type.mielecloud.vacuum_cleaner_program_active.label</label>
+ <description>@text/channel-type.mielecloud.vacuum_cleaner_program_active.description</description>
+ <state>
+ <options>
+ <option value="1">@text/channel-option.mielecloud.vacuum_cleaner_program_active.auto</option>
+ <option value="2">@text/channel-option.mielecloud.vacuum_cleaner_program_active.spot</option>
+ <option value="3">@text/channel-option.mielecloud.vacuum_cleaner_program_active.turbo</option>
+ <option value="4">@text/channel-option.mielecloud.vacuum_cleaner_program_active.silent</option>
+ </options>
+ </state>
+ </channel-type>
+
+ <channel-type id="program_phase">
+ <item-type>String</item-type>
+ <label>@text/channel-type.mielecloud.program_phase.label</label>
+ <description>@text/channel-type.mielecloud.program_phase.description</description>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="program_phase_raw">
+ <item-type>Number</item-type>
+ <label>@text/channel-type.mielecloud.program_phase_raw.label</label>
+ <description>@text/channel-type.mielecloud.program_phase_raw.description</description>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="operation_state">
+ <item-type>String</item-type>
+ <label>@text/channel-type.mielecloud.operation_state.label</label>
+ <description>@text/channel-type.mielecloud.operation_state.description</description>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="operation_state_raw">
+ <item-type>Number</item-type>
+ <label>@text/channel-type.mielecloud.operation_state_raw.label</label>
+ <description>@text/channel-type.mielecloud.operation_state_raw.description</description>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="program_start">
+ <item-type>Switch</item-type>
+ <label>@text/channel-type.mielecloud.program_start.label</label>
+ <description>@text/channel-type.mielecloud.program_start.description</description>
+ </channel-type>
+
+ <channel-type id="program_stop">
+ <item-type>Switch</item-type>
+ <label>@text/channel-type.mielecloud.program_stop.label</label>
+ <description>@text/channel-type.mielecloud.program_stop.description</description>
+ </channel-type>
+
+ <channel-type id="program_start_stop">
+ <item-type>String</item-type>
+ <label>@text/channel-type.mielecloud.program_start_stop.label</label>
+ <description>@text/channel-type.mielecloud.program_start_stop.description</description>
+ <state>
+ <options>
+ <option value="start">@text/channel-option.mielecloud.program_start_stop.start</option>
+ <option value="stop">@text/channel-option.mielecloud.program_start_stop.stop</option>
+ </options>
+ </state>
+ </channel-type>
+
+ <channel-type id="program_start_stop_pause">
+ <item-type>String</item-type>
+ <label>@text/channel-type.mielecloud.program_start_stop_pause.label</label>
+ <description>@text/channel-type.mielecloud.program_start_stop_pause.description</description>
+ <state>
+ <options>
+ <option value="start">@text/channel-option.mielecloud.program_start_stop_pause.start</option>
+ <option value="stop">@text/channel-option.mielecloud.program_start_stop_pause.stop</option>
+ <option value="pause">@text/channel-option.mielecloud.program_start_stop_pause.pause</option>
+ </options>
+ </state>
+ </channel-type>
+
+ <channel-type id="power_state_on_off">
+ <item-type>String</item-type>
+ <label>@text/channel-type.mielecloud.power_state_on_off.label</label>
+ <description>@text/channel-type.mielecloud.power_state_on_off.description</description>
+ <state>
+ <options>
+ <option value="on">@text/channel-option.mielecloud.power_state_on_off.on</option>
+ <option value="off">@text/channel-option.mielecloud.power_state_on_off.off</option>
+ </options>
+ </state>
+ </channel-type>
+
+ <channel-type id="finish_state">
+ <item-type>Switch</item-type>
+ <label>@text/channel-type.mielecloud.finish_state.label</label>
+ <description>@text/channel-type.mielecloud.finish_state.description</description>
+ <category>Alarm</category>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="delayed_start_time">
+ <item-type>Number</item-type>
+ <label>@text/channel-type.mielecloud.delayed_start_time.label</label>
+ <description>@text/channel-type.mielecloud.delayed_start_time.description</description>
+ <category>Number</category>
+ <state pattern="%d sec" readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="program_remaining_time">
+ <item-type>Number</item-type>
+ <label>@text/channel-type.mielecloud.program_remaining_time.label</label>
+ <description>@text/channel-type.mielecloud.program_remaining_time.description</description>
+ <category>Number</category>
+ <state pattern="%d sec" readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="program_elapsed_time">
+ <item-type>Number</item-type>
+ <label>@text/channel-type.mielecloud.program_elapsed_time.label</label>
+ <description>@text/channel-type.mielecloud.program_elapsed_time.description</description>
+ <category>Number</category>
+ <state pattern="%d sec" readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="program_progress">
+ <item-type>Number</item-type>
+ <label>@text/channel-type.mielecloud.program_progress.label</label>
+ <description>@text/channel-type.mielecloud.program_progress.description</description>
+ <category>Number</category>
+ <state min="0" max="100" step="1" pattern="%d %%" readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="drying_target">
+ <item-type>String</item-type>
+ <label>@text/channel-type.mielecloud.drying_target.label</label>
+ <description>@text/channel-type.mielecloud.drying_target.description</description>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="drying_target_raw">
+ <item-type>Number</item-type>
+ <label>@text/channel-type.mielecloud.drying_target_raw.label</label>
+ <description>@text/channel-type.mielecloud.drying_target_raw.description</description>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="pre_heat_finished">
+ <item-type>Switch</item-type>
+ <label>@text/channel-type.mielecloud.pre_heat_finished.label</label>
+ <description>@text/channel-type.mielecloud.pre_heat_finished.description</description>
+ <category>Alarm</category>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="temperature_target">
+ <item-type>Number:Temperature</item-type>
+ <label>@text/channel-type.mielecloud.temperature_target.label</label>
+ <description>@text/channel-type.mielecloud.temperature_target.description</description>
+ <category>Number:Temperature</category>
+ <state pattern="%.1f %unit%" readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="temperature_current">
+ <item-type>Number:Temperature</item-type>
+ <label>@text/channel-type.mielecloud.temperature_current.label</label>
+ <description>@text/channel-type.mielecloud.temperature_current.description</description>
+ <category>Number:Temperature</category>
+ <state pattern="%.1f %unit%" readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="ventilation_power">
+ <item-type>String</item-type>
+ <label>@text/channel-type.mielecloud.ventilation_power.label</label>
+ <description>@text/channel-type.mielecloud.ventilation_power.description</description>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="ventilation_power_raw">
+ <item-type>Number</item-type>
+ <label>@text/channel-type.mielecloud.ventilation_power_raw.label</label>
+ <description>@text/channel-type.mielecloud.ventilation_power_raw.description</description>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="error_state">
+ <item-type>Switch</item-type>
+ <label>@text/channel-type.mielecloud.error_state.label</label>
+ <description>@text/channel-type.mielecloud.error_state.description</description>
+ <category>Alarm</category>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="info_state">
+ <item-type>Switch</item-type>
+ <label>@text/channel-type.mielecloud.info_state.label</label>
+ <description>@text/channel-type.mielecloud.info_state.description</description>
+ <category>Alarm</category>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="fridge_super_cool">
+ <item-type>Switch</item-type>
+ <label>@text/channel-type.mielecloud.fridge_super_cool.label</label>
+ <description>@text/channel-type.mielecloud.fridge_super_cool.description</description>
+ <category>Switch</category>
+ </channel-type>
+
+ <channel-type id="freezer_super_freeze">
+ <item-type>Switch</item-type>
+ <label>@text/channel-type.mielecloud.freezer_super_freeze.label</label>
+ <description>@text/channel-type.mielecloud.freezer_super_freeze.description</description>
+ <category>Switch</category>
+ </channel-type>
+
+ <channel-type id="super_cool_can_be_controlled">
+ <item-type>Switch</item-type>
+ <label>@text/channel-type.mielecloud.super_cool_can_be_controlled.label</label>
+ <description>@text/channel-type.mielecloud.super_cool_can_be_controlled.description</description>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="super_freeze_can_be_controlled">
+ <item-type>Switch</item-type>
+ <label>@text/channel-type.mielecloud.super_freeze_can_be_controlled.label</label>
+ <description>@text/channel-type.mielecloud.super_freeze_can_be_controlled.description</description>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="fridge_temperature_target">
+ <item-type>Number:Temperature</item-type>
+ <label>@text/channel-type.mielecloud.fridge_temperature_target.label</label>
+ <description>@text/channel-type.mielecloud.fridge_temperature_target.description</description>
+ <category>Number:Temperature</category>
+ <state pattern="%.1f %unit%" readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="fridge_temperature_current">
+ <item-type>Number:Temperature</item-type>
+ <label>@text/channel-type.mielecloud.fridge_temperature_current.label</label>
+ <description>@text/channel-type.mielecloud.fridge_temperature_current.description</description>
+ <category>Number:Temperature</category>
+ <state pattern="%.1f %unit%" readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="freezer_temperature_target">
+ <item-type>Number:Temperature</item-type>
+ <label>@text/channel-type.mielecloud.freezer_temperature_target.label</label>
+ <description>@text/channel-type.mielecloud.freezer_temperature_target.description</description>
+ <category>Number:Temperature</category>
+ <state pattern="%.1f %unit%" readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="freezer_temperature_current">
+ <item-type>Number:Temperature</item-type>
+ <label>@text/channel-type.mielecloud.freezer_temperature_current.label</label>
+ <description>@text/channel-type.mielecloud.freezer_temperature_current.description</description>
+ <category>Number:Temperature</category>
+ <state pattern="%.1f %unit%" readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="top_temperature_target">
+ <item-type>Number:Temperature</item-type>
+ <label>@text/channel-type.mielecloud.top_temperature_target.label</label>
+ <description>@text/channel-type.mielecloud.top_temperature_target.description</description>
+ <category>Number:Temperature</category>
+ <state pattern="%.1f %unit%" readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="top_temperature_current">
+ <item-type>Number:Temperature</item-type>
+ <label>@text/channel-type.mielecloud.top_temperature_current.label</label>
+ <description>@text/channel-type.mielecloud.top_temperature_current.description</description>
+ <category>Number:Temperature</category>
+ <state pattern="%.1f %unit%" readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="middle_temperature_target">
+ <item-type>Number:Temperature</item-type>
+ <label>@text/channel-type.mielecloud.middle_temperature_target.label</label>
+ <description>@text/channel-type.mielecloud.middle_temperature_target.description</description>
+ <category>Number:Temperature</category>
+ <state pattern="%.1f %unit%" readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="middle_temperature_current">
+ <item-type>Number:Temperature</item-type>
+ <label>@text/channel-type.mielecloud.middle_temperature_current.label</label>
+ <description>@text/channel-type.mielecloud.middle_temperature_current.description</description>
+ <category>Number:Temperature</category>
+ <state pattern="%.1f %unit%" readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="bottom_temperature_target">
+ <item-type>Number:Temperature</item-type>
+ <label>@text/channel-type.mielecloud.bottom_temperature_target.label</label>
+ <description>@text/channel-type.mielecloud.bottom_temperature_target.description</description>
+ <category>Number:Temperature</category>
+ <state pattern="%.1f %unit%" readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="bottom_temperature_current">
+ <item-type>Number:Temperature</item-type>
+ <label>@text/channel-type.mielecloud.bottom_temperature_current.label</label>
+ <description>@text/channel-type.mielecloud.bottom_temperature_current.description</description>
+ <category>Number:Temperature</category>
+ <state pattern="%.1f %unit%" readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="light_switch">
+ <item-type>Switch</item-type>
+ <label>@text/channel-type.mielecloud.light_switch.label</label>
+ <description>@text/channel-type.mielecloud.light_switch.description</description>
+ <category>Switch</category>
+ </channel-type>
+
+ <channel-type id="light_can_be_controlled">
+ <item-type>Switch</item-type>
+ <label>@text/channel-type.mielecloud.light_can_be_controlled.label</label>
+ <description>@text/channel-type.mielecloud.light_can_be_controlled.description</description>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="plate_power_step">
+ <item-type>String</item-type>
+ <label>@text/channel-type.mielecloud.plate_power_step.label</label>
+ <description>@text/channel-type.mielecloud.plate_power_step.description</description>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="plate_power_step_raw">
+ <item-type>Number</item-type>
+ <label>@text/channel-type.mielecloud.plate_power_step_raw.label</label>
+ <description>@text/channel-type.mielecloud.plate_power_step_raw.description</description>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="door_state">
+ <item-type>Switch</item-type>
+ <label>@text/channel-type.mielecloud.door_state.label</label>
+ <description>@text/channel-type.mielecloud.door_state.description</description>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="door_alarm">
+ <item-type>Switch</item-type>
+ <label>@text/channel-type.mielecloud.door_alarm.label</label>
+ <description>@text/channel-type.mielecloud.door_alarm.description</description>
+ <category>Alarm</category>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <channel-type id="battery_level">
+ <item-type>Number</item-type>
+ <label>@text/channel-type.mielecloud.battery_level.label</label>
+ <description>@text/channel-type.mielecloud.battery_level.description</description>
+ <category>Battery</category>
+ <state readOnly="true"/>
+ </channel-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <thing-type id="coffee_system">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="account"/>
+ </supported-bridge-type-refs>
+
+ <label>@text/thing-type.mielecloud.coffee_system.label</label>
+ <description>@text/thing-type.mielecloud.coffee_system.description</description>
+ <category>WhiteGood</category>
+
+ <channels>
+ <channel id="remote_control_can_be_started" typeId="remote_control_can_be_started"/>
+ <channel id="remote_control_can_be_stopped" typeId="remote_control_can_be_stopped"/>
+ <channel id="remote_control_can_be_switched_on" typeId="remote_control_can_be_switched_on"/>
+ <channel id="remote_control_can_be_switched_off" typeId="remote_control_can_be_switched_off"/>
+ <channel id="program_active" typeId="program_active"/>
+ <channel id="program_active_raw" typeId="program_active_raw"/>
+ <channel id="program_phase" typeId="program_phase"/>
+ <channel id="program_phase_raw" typeId="program_phase_raw"/>
+ <channel id="operation_state" typeId="operation_state"/>
+ <channel id="operation_state_raw" typeId="operation_state_raw"/>
+ <channel id="finish_state" typeId="finish_state"/>
+ <channel id="power_state_on_off" typeId="power_state_on_off"/>
+ <channel id="program_remaining_time" typeId="program_remaining_time"/>
+ <channel id="program_elapsed_time" typeId="program_elapsed_time"/>
+ <channel id="error_state" typeId="error_state"/>
+ <channel id="info_state" typeId="info_state"/>
+ <channel id="light_switch" typeId="light_switch"/>
+ <channel id="light_can_be_controlled" typeId="light_can_be_controlled"/>
+ </channels>
+
+ <properties>
+ <property name="vendor">Miele</property>
+ </properties>
+
+ <config-description-ref uri="thing-type:mielecloud:device"/>
+ </thing-type>
+
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <thing-type id="dish_warmer">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="account"/>
+ </supported-bridge-type-refs>
+
+ <label>@text/thing-type.mielecloud.dish_warmer.label</label>
+ <description>@text/thing-type.mielecloud.dish_warmer.description</description>
+ <category>WhiteGood</category>
+
+ <channels>
+ <channel id="remote_control_can_be_switched_on" typeId="remote_control_can_be_switched_on"/>
+ <channel id="remote_control_can_be_switched_off" typeId="remote_control_can_be_switched_off"/>
+ <channel id="dish_warmer_program_active" typeId="dish_warmer_program_active"/>
+ <channel id="program_active_raw" typeId="program_active_raw"/>
+ <channel id="operation_state" typeId="operation_state"/>
+ <channel id="operation_state_raw" typeId="operation_state_raw"/>
+ <channel id="power_state_on_off" typeId="power_state_on_off"/>
+ <channel id="finish_state" typeId="finish_state"/>
+ <channel id="program_remaining_time" typeId="program_remaining_time"/>
+ <channel id="program_elapsed_time" typeId="program_elapsed_time"/>
+ <channel id="program_progress" typeId="program_progress"/>
+ <channel id="temperature_target" typeId="temperature_target"/>
+ <channel id="temperature_current" typeId="temperature_current"/>
+ <channel id="error_state" typeId="error_state"/>
+ <channel id="info_state" typeId="info_state"/>
+ <channel id="door_state" typeId="door_state"/>
+ </channels>
+
+ <properties>
+ <property name="vendor">Miele</property>
+ </properties>
+
+ <config-description-ref uri="thing-type:mielecloud:device"/>
+ </thing-type>
+
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <thing-type id="dishwasher">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="account"/>
+ </supported-bridge-type-refs>
+
+ <label>@text/thing-type.mielecloud.dishwasher.label</label>
+ <description>@text/thing-type.mielecloud.dishwasher.description</description>
+ <category>WhiteGood</category>
+
+ <channels>
+ <channel id="remote_control_can_be_started" typeId="remote_control_can_be_started"/>
+ <channel id="remote_control_can_be_stopped" typeId="remote_control_can_be_stopped"/>
+ <channel id="remote_control_can_be_switched_on" typeId="remote_control_can_be_switched_on"/>
+ <channel id="remote_control_can_be_switched_off" typeId="remote_control_can_be_switched_off"/>
+ <channel id="program_active" typeId="program_active"/>
+ <channel id="program_active_raw" typeId="program_active_raw"/>
+ <channel id="program_phase" typeId="program_phase"/>
+ <channel id="program_phase_raw" typeId="program_phase_raw"/>
+ <channel id="operation_state" typeId="operation_state"/>
+ <channel id="operation_state_raw" typeId="operation_state_raw"/>
+ <channel id="program_start_stop" typeId="program_start_stop"/>
+ <channel id="finish_state" typeId="finish_state"/>
+ <channel id="power_state_on_off" typeId="power_state_on_off"/>
+ <channel id="delayed_start_time" typeId="delayed_start_time"/>
+ <channel id="program_remaining_time" typeId="program_remaining_time"/>
+ <channel id="program_elapsed_time" typeId="program_elapsed_time"/>
+ <channel id="program_progress" typeId="program_progress"/>
+ <channel id="error_state" typeId="error_state"/>
+ <channel id="info_state" typeId="info_state"/>
+ <channel id="door_state" typeId="door_state"/>
+ </channels>
+
+ <properties>
+ <property name="vendor">Miele</property>
+ </properties>
+
+ <config-description-ref uri="thing-type:mielecloud:device"/>
+ </thing-type>
+
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <thing-type id="dryer">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="account"/>
+ </supported-bridge-type-refs>
+
+ <label>@text/thing-type.mielecloud.dryer.label</label>
+ <description>@text/thing-type.mielecloud.dryer.description</description>
+ <category>WhiteGood</category>
+
+ <channels>
+ <channel id="remote_control_can_be_started" typeId="remote_control_can_be_started"/>
+ <channel id="remote_control_can_be_stopped" typeId="remote_control_can_be_stopped"/>
+ <channel id="remote_control_can_be_switched_on" typeId="remote_control_can_be_switched_on"/>
+ <channel id="remote_control_can_be_switched_off" typeId="remote_control_can_be_switched_off"/>
+ <channel id="program_active" typeId="program_active"/>
+ <channel id="program_active_raw" typeId="program_active_raw"/>
+ <channel id="program_phase" typeId="program_phase"/>
+ <channel id="program_phase_raw" typeId="program_phase_raw"/>
+ <channel id="operation_state" typeId="operation_state"/>
+ <channel id="operation_state_raw" typeId="operation_state_raw"/>
+ <channel id="program_start_stop" typeId="program_start_stop"/>
+ <channel id="finish_state" typeId="finish_state"/>
+ <channel id="power_state_on_off" typeId="power_state_on_off"/>
+ <channel id="delayed_start_time" typeId="delayed_start_time"/>
+ <channel id="program_remaining_time" typeId="program_remaining_time"/>
+ <channel id="program_elapsed_time" typeId="program_elapsed_time"/>
+ <channel id="program_progress" typeId="program_progress"/>
+ <channel id="drying_target" typeId="drying_target"/>
+ <channel id="drying_target_raw" typeId="drying_target_raw"/>
+ <channel id="error_state" typeId="error_state"/>
+ <channel id="info_state" typeId="info_state"/>
+ <channel id="light_switch" typeId="light_switch"/>
+ <channel id="light_can_be_controlled" typeId="light_can_be_controlled"/>
+ <channel id="door_state" typeId="door_state"/>
+ </channels>
+
+ <properties>
+ <property name="vendor">Miele</property>
+ </properties>
+
+ <config-description-ref uri="thing-type:mielecloud:device"/>
+ </thing-type>
+
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <thing-type id="freezer">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="account"/>
+ </supported-bridge-type-refs>
+
+ <label>@text/thing-type.mielecloud.freezer.label</label>
+ <description>@text/thing-type.mielecloud.freezer.description</description>
+ <category>WhiteGood</category>
+
+ <channels>
+ <channel id="operation_state" typeId="operation_state"/>
+ <channel id="operation_state_raw" typeId="operation_state_raw"/>
+ <channel id="error_state" typeId="error_state"/>
+ <channel id="info_state" typeId="info_state"/>
+ <channel id="freezer_super_freeze" typeId="freezer_super_freeze"/>
+ <channel id="super_freeze_can_be_controlled" typeId="super_freeze_can_be_controlled"/>
+ <channel id="freezer_temperature_target" typeId="freezer_temperature_target"/>
+ <channel id="freezer_temperature_current" typeId="freezer_temperature_current"/>
+ <channel id="door_state" typeId="door_state"/>
+ <channel id="door_alarm" typeId="door_alarm"/>
+ </channels>
+
+ <properties>
+ <property name="vendor">Miele</property>
+ </properties>
+
+ <config-description-ref uri="thing-type:mielecloud:device"/>
+ </thing-type>
+
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <thing-type id="fridge">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="account"/>
+ </supported-bridge-type-refs>
+
+ <label>@text/thing-type.mielecloud.fridge.label</label>
+ <description>@text/thing-type.mielecloud.fridge.description</description>
+ <category>WhiteGood</category>
+
+ <channels>
+ <channel id="operation_state" typeId="operation_state"/>
+ <channel id="operation_state_raw" typeId="operation_state_raw"/>
+ <channel id="error_state" typeId="error_state"/>
+ <channel id="info_state" typeId="info_state"/>
+ <channel id="fridge_super_cool" typeId="fridge_super_cool"/>
+ <channel id="super_cool_can_be_controlled" typeId="super_cool_can_be_controlled"/>
+ <channel id="fridge_temperature_target" typeId="fridge_temperature_target"/>
+ <channel id="fridge_temperature_current" typeId="fridge_temperature_current"/>
+ <channel id="door_state" typeId="door_state"/>
+ <channel id="door_alarm" typeId="door_alarm"/>
+ </channels>
+
+ <properties>
+ <property name="vendor">Miele</property>
+ </properties>
+
+ <config-description-ref uri="thing-type:mielecloud:device"/>
+ </thing-type>
+
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <thing-type id="fridge_freezer">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="account"/>
+ </supported-bridge-type-refs>
+
+ <label>@text/thing-type.mielecloud.fridge_freezer.label</label>
+ <description>@text/thing-type.mielecloud.fridge_freezer.description</description>
+ <category>WhiteGood</category>
+
+ <channels>
+ <channel id="operation_state" typeId="operation_state"/>
+ <channel id="operation_state_raw" typeId="operation_state_raw"/>
+ <channel id="error_state" typeId="error_state"/>
+ <channel id="info_state" typeId="info_state"/>
+ <channel id="fridge_super_cool" typeId="fridge_super_cool"/>
+ <channel id="freezer_super_freeze" typeId="freezer_super_freeze"/>
+ <channel id="super_cool_can_be_controlled" typeId="super_cool_can_be_controlled"/>
+ <channel id="super_freeze_can_be_controlled" typeId="super_freeze_can_be_controlled"/>
+ <channel id="fridge_temperature_target" typeId="fridge_temperature_target"/>
+ <channel id="fridge_temperature_current" typeId="fridge_temperature_current"/>
+ <channel id="freezer_temperature_target" typeId="freezer_temperature_target"/>
+ <channel id="freezer_temperature_current" typeId="freezer_temperature_current"/>
+ <channel id="door_state" typeId="door_state"/>
+ <channel id="door_alarm" typeId="door_alarm"/>
+ </channels>
+
+ <properties>
+ <property name="vendor">Miele</property>
+ </properties>
+
+ <config-description-ref uri="thing-type:mielecloud:device"/>
+ </thing-type>
+
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <thing-type id="hob">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="account"/>
+ </supported-bridge-type-refs>
+
+ <label>@text/thing-type.mielecloud.hob.label</label>
+ <description>@text/thing-type.mielecloud.hob.description</description>
+ <category>WhiteGood</category>
+
+ <channels>
+ <channel id="operation_state" typeId="operation_state"/>
+ <channel id="operation_state_raw" typeId="operation_state_raw"/>
+ <channel id="error_state" typeId="error_state"/>
+ <channel id="info_state" typeId="info_state"/>
+ <channel id="plate_1_power_step" typeId="plate_power_step"/>
+ <channel id="plate_1_power_step_raw" typeId="plate_power_step_raw"/>
+ <channel id="plate_2_power_step" typeId="plate_power_step"/>
+ <channel id="plate_2_power_step_raw" typeId="plate_power_step_raw"/>
+ <channel id="plate_3_power_step" typeId="plate_power_step"/>
+ <channel id="plate_3_power_step_raw" typeId="plate_power_step_raw"/>
+ <channel id="plate_4_power_step" typeId="plate_power_step"/>
+ <channel id="plate_4_power_step_raw" typeId="plate_power_step_raw"/>
+ <channel id="plate_5_power_step" typeId="plate_power_step"/>
+ <channel id="plate_5_power_step_raw" typeId="plate_power_step_raw"/>
+ <channel id="plate_6_power_step" typeId="plate_power_step"/>
+ <channel id="plate_6_power_step_raw" typeId="plate_power_step_raw"/>
+ </channels>
+
+ <properties>
+ <property name="vendor">Miele</property>
+ </properties>
+
+ <config-description-ref uri="thing-type:mielecloud:device"/>
+ </thing-type>
+
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <thing-type id="hood">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="account"/>
+ </supported-bridge-type-refs>
+
+ <label>@text/thing-type.mielecloud.hood.label</label>
+ <description>@text/thing-type.mielecloud.hood.description</description>
+ <category>WhiteGood</category>
+
+ <channels>
+ <channel id="remote_control_can_be_started" typeId="remote_control_can_be_started"/>
+ <channel id="remote_control_can_be_stopped" typeId="remote_control_can_be_stopped"/>
+ <channel id="remote_control_can_be_switched_on" typeId="remote_control_can_be_switched_on"/>
+ <channel id="remote_control_can_be_switched_off" typeId="remote_control_can_be_switched_off"/>
+ <channel id="program_phase" typeId="program_phase"/>
+ <channel id="program_phase_raw" typeId="program_phase_raw"/>
+ <channel id="operation_state" typeId="operation_state"/>
+ <channel id="operation_state_raw" typeId="operation_state_raw"/>
+ <channel id="power_state_on_off" typeId="power_state_on_off"/>
+ <channel id="ventilation_power" typeId="ventilation_power"/>
+ <channel id="ventilation_power_raw" typeId="ventilation_power_raw"/>
+ <channel id="error_state" typeId="error_state"/>
+ <channel id="info_state" typeId="info_state"/>
+ <channel id="light_switch" typeId="light_switch"/>
+ <channel id="light_can_be_controlled" typeId="light_can_be_controlled"/>
+ </channels>
+
+ <properties>
+ <property name="vendor">Miele</property>
+ </properties>
+
+ <config-description-ref uri="thing-type:mielecloud:device"/>
+ </thing-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <thing-type id="oven">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="account"/>
+ </supported-bridge-type-refs>
+
+ <label>@text/thing-type.mielecloud.oven.label</label>
+ <description>@text/thing-type.mielecloud.oven.description</description>
+ <category>WhiteGood</category>
+
+ <channels>
+ <channel id="remote_control_can_be_started" typeId="remote_control_can_be_started"/>
+ <channel id="remote_control_can_be_stopped" typeId="remote_control_can_be_stopped"/>
+ <channel id="remote_control_can_be_switched_on" typeId="remote_control_can_be_switched_on"/>
+ <channel id="remote_control_can_be_switched_off" typeId="remote_control_can_be_switched_off"/>
+ <channel id="program_active" typeId="program_active"/>
+ <channel id="program_active_raw" typeId="program_active_raw"/>
+ <channel id="program_phase" typeId="program_phase"/>
+ <channel id="program_phase_raw" typeId="program_phase_raw"/>
+ <channel id="operation_state" typeId="operation_state"/>
+ <channel id="operation_state_raw" typeId="operation_state_raw"/>
+ <channel id="program_start_stop" typeId="program_start_stop"/>
+ <channel id="finish_state" typeId="finish_state"/>
+ <channel id="power_state_on_off" typeId="power_state_on_off"/>
+ <channel id="delayed_start_time" typeId="delayed_start_time"/>
+ <channel id="program_remaining_time" typeId="program_remaining_time"/>
+ <channel id="program_elapsed_time" typeId="program_elapsed_time"/>
+ <channel id="program_progress" typeId="program_progress"/>
+ <channel id="pre_heat_finished" typeId="pre_heat_finished"/>
+ <channel id="temperature_target" typeId="temperature_target"/>
+ <channel id="temperature_current" typeId="temperature_current"/>
+ <channel id="error_state" typeId="error_state"/>
+ <channel id="info_state" typeId="info_state"/>
+ <channel id="light_switch" typeId="light_switch"/>
+ <channel id="light_can_be_controlled" typeId="light_can_be_controlled"/>
+ <channel id="door_state" typeId="door_state"/>
+ </channels>
+
+ <properties>
+ <property name="vendor">Miele</property>
+ </properties>
+
+ <config-description-ref uri="thing-type:mielecloud:device"/>
+ </thing-type>
+
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <thing-type id="robotic_vacuum_cleaner">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="account"/>
+ </supported-bridge-type-refs>
+
+ <label>@text/thing-type.mielecloud.robotic_vacuum_cleaner.label</label>
+ <description>@text/thing-type.mielecloud.robotic_vacuum_cleaner.description</description>
+ <category>WhiteGood</category>
+
+ <channels>
+ <channel id="remote_control_can_be_started" typeId="remote_control_can_be_started"/>
+ <channel id="remote_control_can_be_stopped" typeId="remote_control_can_be_stopped"/>
+ <channel id="remote_control_can_be_paused" typeId="remote_control_can_be_paused"/>
+ <channel id="remote_control_can_set_program_active" typeId="remote_control_can_set_program_active"/>
+ <channel id="vacuum_cleaner_program_active" typeId="vacuum_cleaner_program_active"/>
+ <channel id="program_active_raw" typeId="program_active_raw"/>
+ <channel id="operation_state" typeId="operation_state"/>
+ <channel id="operation_state_raw" typeId="operation_state_raw"/>
+ <channel id="finish_state" typeId="finish_state"/>
+ <channel id="program_start_stop_pause" typeId="program_start_stop_pause"/>
+ <channel id="power_state_on_off" typeId="power_state_on_off"/>
+ <channel id="error_state" typeId="error_state"/>
+ <channel id="info_state" typeId="info_state"/>
+ <channel id="battery_level" typeId="battery_level"/>
+ </channels>
+
+ <properties>
+ <property name="vendor">Miele</property>
+ </properties>
+
+ <config-description-ref uri="thing-type:mielecloud:device"/>
+ </thing-type>
+
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <thing-type id="washer_dryer">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="account"/>
+ </supported-bridge-type-refs>
+
+ <label>@text/thing-type.mielecloud.washer_dryer.label</label>
+ <description>@text/thing-type.mielecloud.washer_dryer.description</description>
+ <category>WhiteGood</category>
+
+ <channels>
+ <channel id="remote_control_can_be_started" typeId="remote_control_can_be_started"/>
+ <channel id="remote_control_can_be_stopped" typeId="remote_control_can_be_stopped"/>
+ <channel id="remote_control_can_be_switched_on" typeId="remote_control_can_be_switched_on"/>
+ <channel id="remote_control_can_be_switched_off" typeId="remote_control_can_be_switched_off"/>
+ <channel id="spinning_speed" typeId="spinning_speed"/>
+ <channel id="spinning_speed_raw" typeId="spinning_speed_raw"/>
+ <channel id="program_active" typeId="program_active"/>
+ <channel id="program_active_raw" typeId="program_active_raw"/>
+ <channel id="program_phase" typeId="program_phase"/>
+ <channel id="program_phase_raw" typeId="program_phase_raw"/>
+ <channel id="operation_state" typeId="operation_state"/>
+ <channel id="operation_state_raw" typeId="operation_state_raw"/>
+ <channel id="program_start_stop" typeId="program_start_stop"/>
+ <channel id="finish_state" typeId="finish_state"/>
+ <channel id="power_state_on_off" typeId="power_state_on_off"/>
+ <channel id="delayed_start_time" typeId="delayed_start_time"/>
+ <channel id="program_remaining_time" typeId="program_remaining_time"/>
+ <channel id="program_elapsed_time" typeId="program_elapsed_time"/>
+ <channel id="program_progress" typeId="program_progress"/>
+ <channel id="drying_target" typeId="drying_target"/>
+ <channel id="drying_target_raw" typeId="drying_target_raw"/>
+ <channel id="error_state" typeId="error_state"/>
+ <channel id="info_state" typeId="info_state"/>
+ <channel id="temperature_target" typeId="temperature_target"/>
+ <channel id="light_switch" typeId="light_switch"/>
+ <channel id="light_can_be_controlled" typeId="light_can_be_controlled"/>
+ <channel id="door_state" typeId="door_state"/>
+ </channels>
+
+ <properties>
+ <property name="vendor">Miele</property>
+ </properties>
+
+ <config-description-ref uri="thing-type:mielecloud:device"/>
+ </thing-type>
+
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <thing-type id="washing_machine">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="account"/>
+ </supported-bridge-type-refs>
+
+ <label>@text/thing-type.mielecloud.washing_machine.label</label>
+ <description>@text/thing-type.mielecloud.washing_machine.description</description>
+ <category>WhiteGood</category>
+
+ <channels>
+ <channel id="remote_control_can_be_started" typeId="remote_control_can_be_started"/>
+ <channel id="remote_control_can_be_stopped" typeId="remote_control_can_be_stopped"/>
+ <channel id="remote_control_can_be_switched_on" typeId="remote_control_can_be_switched_on"/>
+ <channel id="remote_control_can_be_switched_off" typeId="remote_control_can_be_switched_off"/>
+ <channel id="spinning_speed" typeId="spinning_speed"/>
+ <channel id="spinning_speed_raw" typeId="spinning_speed_raw"/>
+ <channel id="program_active" typeId="program_active"/>
+ <channel id="program_active_raw" typeId="program_active_raw"/>
+ <channel id="program_phase" typeId="program_phase"/>
+ <channel id="program_phase_raw" typeId="program_phase_raw"/>
+ <channel id="operation_state" typeId="operation_state"/>
+ <channel id="operation_state_raw" typeId="operation_state_raw"/>
+ <channel id="program_start_stop" typeId="program_start_stop"/>
+ <channel id="finish_state" typeId="finish_state"/>
+ <channel id="power_state_on_off" typeId="power_state_on_off"/>
+ <channel id="delayed_start_time" typeId="delayed_start_time"/>
+ <channel id="program_remaining_time" typeId="program_remaining_time"/>
+ <channel id="program_elapsed_time" typeId="program_elapsed_time"/>
+ <channel id="program_progress" typeId="program_progress"/>
+ <channel id="error_state" typeId="error_state"/>
+ <channel id="info_state" typeId="info_state"/>
+ <channel id="temperature_target" typeId="temperature_target"/>
+ <channel id="light_switch" typeId="light_switch"/>
+ <channel id="light_can_be_controlled" typeId="light_can_be_controlled"/>
+ <channel id="door_state" typeId="door_state"/>
+ </channels>
+
+ <properties>
+ <property name="vendor">Miele</property>
+ </properties>
+
+ <config-description-ref uri="thing-type:mielecloud:device"/>
+ </thing-type>
+
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mielecloud"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <thing-type id="wine_storage">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="account"/>
+ </supported-bridge-type-refs>
+
+ <label>@text/thing-type.mielecloud.wine_storage.label</label>
+ <description>@text/thing-type.mielecloud.wine_storage.description</description>
+ <category>WhiteGood</category>
+
+ <channels>
+ <channel id="remote_control_can_be_started" typeId="remote_control_can_be_started"/>
+ <channel id="remote_control_can_be_stopped" typeId="remote_control_can_be_stopped"/>
+ <channel id="remote_control_can_be_switched_on" typeId="remote_control_can_be_switched_on"/>
+ <channel id="remote_control_can_be_switched_off" typeId="remote_control_can_be_switched_off"/>
+ <channel id="operation_state" typeId="operation_state"/>
+ <channel id="operation_state_raw" typeId="operation_state_raw"/>
+ <channel id="power_state_on_off" typeId="power_state_on_off"/>
+ <channel id="error_state" typeId="error_state"/>
+ <channel id="info_state" typeId="info_state"/>
+ <channel id="temperature_target" typeId="temperature_target"/>
+ <channel id="temperature_current" typeId="temperature_current"/>
+ <channel id="top_temperature_target" typeId="top_temperature_target"/>
+ <channel id="top_temperature_current" typeId="top_temperature_current"/>
+ <channel id="middle_temperature_target" typeId="middle_temperature_target"/>
+ <channel id="middle_temperature_current" typeId="middle_temperature_current"/>
+ <channel id="bottom_temperature_target" typeId="bottom_temperature_target"/>
+ <channel id="bottom_temperature_current" typeId="bottom_temperature_current"/>
+ </channels>
+
+ <properties>
+ <property name="vendor">Miele</property>
+ </properties>
+
+ <config-description-ref uri="thing-type:mielecloud:device"/>
+ </thing-type>
+
+</thing:thing-descriptions>
--- /dev/null
+/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */
+/**\r
+ * 1. Change the default font family in all browsers (opinionated).\r
+ * 2. Correct the line height in all browsers.\r
+ * 3. Prevent adjustments of font size after orientation changes in\r
+ * IE on Windows Phone and in iOS.\r
+ */
+/* Document\r
+ ========================================================================== */
+/* line 13, src/assets/scss/vendors/_normalize.scss */
+html {
+ font-family: sans-serif;
+ /* 1 */
+ line-height: 1.15;
+ /* 2 */
+ -ms-text-size-adjust: 100%;
+ /* 3 */
+ -webkit-text-size-adjust: 100%;
+ /* 3 */
+}
+
+/* Sections\r
+ ========================================================================== */
+/**\r
+ * Remove the margin in all browsers (opinionated).\r
+ */
+/* line 27, src/assets/scss/vendors/_normalize.scss */
+body {
+ margin: 0;
+}
+
+/**\r
+ * Add the correct display in IE 9-.\r
+ */
+/* line 35, src/assets/scss/vendors/_normalize.scss */
+article,
+aside,
+footer,
+header,
+nav,
+section {
+ display: block;
+}
+
+/**\r
+ * Correct the font size and margin on `h1` elements within `section` and\r
+ * `article` contexts in Chrome, Firefox, and Safari.\r
+ */
+/* line 49, src/assets/scss/vendors/_normalize.scss */
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/* Grouping content\r
+ ========================================================================== */
+/**\r
+ * Add the correct display in IE 9-.\r
+ * 1. Add the correct display in IE.\r
+ */
+/* line 62, src/assets/scss/vendors/_normalize.scss */
+figcaption,
+figure,
+main {
+ /* 1 */
+ display: block;
+}
+
+/**\r
+ * Add the correct margin in IE 8.\r
+ */
+/* line 72, src/assets/scss/vendors/_normalize.scss */
+figure {
+ margin: 1em 40px;
+}
+
+/**\r
+ * 1. Add the correct box sizing in Firefox.\r
+ * 2. Show the overflow in Edge and IE.\r
+ */
+/* line 81, src/assets/scss/vendors/_normalize.scss */
+hr {
+ -webkit-box-sizing: content-box;
+ box-sizing: content-box;
+ /* 1 */
+ height: 0;
+ /* 1 */
+ overflow: visible;
+ /* 2 */
+}
+
+/**\r
+ * 1. Correct the inheritance and scaling of font size in all browsers.\r
+ * 2. Correct the odd `em` font sizing in all browsers.\r
+ */
+/* line 92, src/assets/scss/vendors/_normalize.scss */
+pre {
+ font-family: monospace, monospace;
+ /* 1 */
+ font-size: 1em;
+ /* 2 */
+}
+
+/* Text-level semantics\r
+ ========================================================================== */
+/**\r
+ * 1. Remove the gray background on active links in IE 10.\r
+ * 2. Remove gaps in links underline in iOS 8+ and Safari 8+.\r
+ */
+/* line 105, src/assets/scss/vendors/_normalize.scss */
+a {
+ background-color: transparent;
+ /* 1 */
+ -webkit-text-decoration-skip: objects;
+ /* 2 */
+}
+
+/**\r
+ * Remove the outline on focused links when they are also active or hovered\r
+ * in all browsers (opinionated).\r
+ */
+/* line 115, src/assets/scss/vendors/_normalize.scss */
+a:active,
+a:hover {
+ outline-width: 0;
+}
+
+/**\r
+ * 1. Remove the bottom border in Firefox 39-.\r
+ * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\r
+ */
+/* line 125, src/assets/scss/vendors/_normalize.scss */
+abbr[title] {
+ border-bottom: none;
+ /* 1 */
+ text-decoration: underline;
+ /* 2 */
+ -webkit-text-decoration: underline dotted;
+ text-decoration: underline dotted;
+ /* 2 */
+}
+
+/**\r
+ * Prevent the duplicate application of `bolder` by the next rule in Safari 6.\r
+ */
+/* line 135, src/assets/scss/vendors/_normalize.scss */
+b,
+strong {
+ font-weight: inherit;
+}
+
+/**\r
+ * Add the correct font weight in Chrome, Edge, and Safari.\r
+ */
+/* line 144, src/assets/scss/vendors/_normalize.scss */
+b,
+strong {
+ font-weight: bolder;
+}
+
+/**\r
+ * 1. Correct the inheritance and scaling of font size in all browsers.\r
+ * 2. Correct the odd `em` font sizing in all browsers.\r
+ */
+/* line 154, src/assets/scss/vendors/_normalize.scss */
+code,
+kbd,
+samp {
+ font-family: monospace, monospace;
+ /* 1 */
+ font-size: 1em;
+ /* 2 */
+}
+
+/**\r
+ * Add the correct font style in Android 4.3-.\r
+ */
+/* line 165, src/assets/scss/vendors/_normalize.scss */
+dfn {
+ font-style: italic;
+}
+
+/**\r
+ * Add the correct background and color in IE 9-.\r
+ */
+/* line 173, src/assets/scss/vendors/_normalize.scss */
+mark {
+ background-color: #ff0;
+ color: #000;
+}
+
+/**\r
+ * Add the correct font size in all browsers.\r
+ */
+/* line 182, src/assets/scss/vendors/_normalize.scss */
+small {
+ font-size: 80%;
+}
+
+/**\r
+ * Prevent `sub` and `sup` elements from affecting the line height in\r
+ * all browsers.\r
+ */
+/* line 191, src/assets/scss/vendors/_normalize.scss */
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+/* line 199, src/assets/scss/vendors/_normalize.scss */
+sub {
+ bottom: -0.25em;
+}
+
+/* line 203, src/assets/scss/vendors/_normalize.scss */
+sup {
+ top: -0.5em;
+}
+
+/* Embedded content\r
+ ========================================================================== */
+/**\r
+ * Add the correct display in IE 9-.\r
+ */
+/* line 214, src/assets/scss/vendors/_normalize.scss */
+audio,
+video {
+ display: inline-block;
+}
+
+/**\r
+ * Add the correct display in iOS 4-7.\r
+ */
+/* line 223, src/assets/scss/vendors/_normalize.scss */
+audio:not([controls]) {
+ display: none;
+ height: 0;
+}
+
+/**\r
+ * Remove the border on images inside links in IE 10-.\r
+ */
+/* line 232, src/assets/scss/vendors/_normalize.scss */
+img {
+ border-style: none;
+}
+
+/**\r
+ * Hide the overflow in IE.\r
+ */
+/* line 240, src/assets/scss/vendors/_normalize.scss */
+svg:not(:root) {
+ overflow: hidden;
+}
+
+/* Forms\r
+ ========================================================================== */
+/**\r
+ * 1. Change the font styles in all browsers (opinionated).\r
+ * 2. Remove the margin in Firefox and Safari.\r
+ */
+/* line 252, src/assets/scss/vendors/_normalize.scss */
+button,
+input,
+optgroup,
+select,
+textarea {
+ font-family: sans-serif;
+ /* 1 */
+ font-size: 100%;
+ /* 1 */
+ line-height: 1.15;
+ /* 1 */
+ margin: 0;
+ /* 2 */
+}
+
+/**\r
+ * Show the overflow in IE.\r
+ * 1. Show the overflow in Edge.\r
+ */
+/* line 268, src/assets/scss/vendors/_normalize.scss */
+button,
+input {
+ /* 1 */
+ overflow: visible;
+}
+
+/**\r
+ * Remove the inheritance of text transform in Edge, Firefox, and IE.\r
+ * 1. Remove the inheritance of text transform in Firefox.\r
+ */
+/* line 278, src/assets/scss/vendors/_normalize.scss */
+button,
+select {
+ /* 1 */
+ text-transform: none;
+}
+
+/**\r
+ * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`\r
+ * controls in Android 4.\r
+ * 2. Correct the inability to style clickable types in iOS and Safari.\r
+ */
+/* line 289, src/assets/scss/vendors/_normalize.scss */
+button,
+html [type="button"],
+[type="reset"],
+[type="submit"] {
+ -webkit-appearance: button;
+ /* 2 */
+}
+
+/**\r
+ * Remove the inner border and padding in Firefox.\r
+ */
+/* line 300, src/assets/scss/vendors/_normalize.scss */
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner {
+ border-style: none;
+ padding: 0;
+}
+
+/**\r
+ * Restore the focus styles unset by the previous rule.\r
+ */
+/* line 312, src/assets/scss/vendors/_normalize.scss */
+button:-moz-focusring,
+[type="button"]:-moz-focusring,
+[type="reset"]:-moz-focusring,
+[type="submit"]:-moz-focusring {
+ outline: 1px dotted ButtonText;
+}
+
+/**\r
+ * Change the border, margin, and padding in all browsers (opinionated).\r
+ */
+/* line 323, src/assets/scss/vendors/_normalize.scss */
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em;
+}
+
+/**\r
+ * 1. Correct the text wrapping in Edge and IE.\r
+ * 2. Correct the color inheritance from `fieldset` elements in IE.\r
+ * 3. Remove the padding so developers are not caught out when they zero out\r
+ * `fieldset` elements in all browsers.\r
+ */
+/* line 336, src/assets/scss/vendors/_normalize.scss */
+legend {
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ /* 1 */
+ color: inherit;
+ /* 2 */
+ display: table;
+ /* 1 */
+ max-width: 100%;
+ /* 1 */
+ padding: 0;
+ /* 3 */
+ white-space: normal;
+ /* 1 */
+}
+
+/**\r
+ * 1. Add the correct display in IE 9-.\r
+ * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.\r
+ */
+/* line 350, src/assets/scss/vendors/_normalize.scss */
+progress {
+ display: inline-block;
+ /* 1 */
+ vertical-align: baseline;
+ /* 2 */
+}
+
+/**\r
+ * Remove the default vertical scrollbar in IE.\r
+ */
+/* line 359, src/assets/scss/vendors/_normalize.scss */
+textarea {
+ overflow: auto;
+}
+
+/**\r
+ * 1. Add the correct box sizing in IE 10-.\r
+ * 2. Remove the padding in IE 10-.\r
+ */
+/* line 368, src/assets/scss/vendors/_normalize.scss */
+[type="checkbox"],
+[type="radio"] {
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ /* 1 */
+ padding: 0;
+ /* 2 */
+}
+
+/**\r
+ * Correct the cursor style of increment and decrement buttons in Chrome.\r
+ */
+/* line 378, src/assets/scss/vendors/_normalize.scss */
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/**\r
+ * 1. Correct the odd appearance in Chrome and Safari.\r
+ * 2. Correct the outline style in Safari.\r
+ */
+/* line 388, src/assets/scss/vendors/_normalize.scss */
+[type="search"] {
+ -webkit-appearance: textfield;
+ /* 1 */
+ outline-offset: -2px;
+ /* 2 */
+}
+
+/**\r
+ * Remove the inner padding and cancel buttons in Chrome and Safari on macOS.\r
+ */
+/* line 397, src/assets/scss/vendors/_normalize.scss */
+[type="search"]::-webkit-search-cancel-button,
+[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**\r
+ * 1. Correct the inability to style clickable types in iOS and Safari.\r
+ * 2. Change font properties to `inherit` in Safari.\r
+ */
+/* line 407, src/assets/scss/vendors/_normalize.scss */
+::-webkit-file-upload-button {
+ -webkit-appearance: button;
+ /* 1 */
+ font: inherit;
+ /* 2 */
+}
+
+/* Interactive\r
+ ========================================================================== */
+/*\r
+ * Add the correct display in IE 9-.\r
+ * 1. Add the correct display in Edge, IE, and Firefox.\r
+ */
+/* line 420, src/assets/scss/vendors/_normalize.scss */
+details,
+menu {
+ display: block;
+}
+
+/*\r
+ * Add the correct display in all browsers.\r
+ */
+/* line 429, src/assets/scss/vendors/_normalize.scss */
+summary {
+ display: list-item;
+}
+
+/* Scripting\r
+ ========================================================================== */
+/**\r
+ * Add the correct display in IE 9-.\r
+ */
+/* line 440, src/assets/scss/vendors/_normalize.scss */
+canvas {
+ display: inline-block;
+}
+
+/**\r
+ * Add the correct display in IE.\r
+ */
+/* line 448, src/assets/scss/vendors/_normalize.scss */
+template {
+ display: none;
+}
+
+/* Hidden\r
+ ========================================================================== */
+/**\r
+ * Add the correct display in IE 10-.\r
+ */
+/* line 459, src/assets/scss/vendors/_normalize.scss */
+[hidden] {
+ display: none;
+}
+
+/*!
+ * Bootstrap v4.5.2 (https://getbootstrap.com/)
+ * Copyright 2011-2020 The Bootstrap Authors
+ * Copyright 2011-2020 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ */
+/* line 2, node_modules/bootstrap/scss/_root.scss */
+:root {
+ --blue: #6fa7fd;
+ --indigo: #6610f2;
+ --purple: #6f42c1;
+ --pink: #e83e8c;
+ --red: #e54a19;
+ --orange: #fd7e14;
+ --yellow: #ffc107;
+ --green: #28a745;
+ --teal: #20c997;
+ --cyan: #17a2b8;
+ --white: #ffffff;
+ --gray: #6c757d;
+ --gray-dark: #343a40;
+ --primary: #464746;
+ --secondary: #f0ebe3;
+ --success: #f0ebe3;
+ --info: #464746;
+ --warning: #464746;
+ --danger: #e54a19;
+ --light: #f7f7f7;
+ --dark: #343a40;
+ --breakpoint-xs: 0;
+ --breakpoint-sm: 576px;
+ --breakpoint-md: 768px;
+ --breakpoint-lg: 1024px;
+ --breakpoint-xl: 1280px;
+ --breakpoint-xxl: 1440px;
+ --font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+}
+
+/* line 19, node_modules/bootstrap/scss/_reboot.scss */
+*,
+*::before,
+*::after {
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+/* line 25, node_modules/bootstrap/scss/_reboot.scss */
+html {
+ font-family: sans-serif;
+ line-height: 1.15;
+ -webkit-text-size-adjust: 100%;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+
+/* line 35, node_modules/bootstrap/scss/_reboot.scss */
+article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
+ display: block;
+}
+
+/* line 46, node_modules/bootstrap/scss/_reboot.scss */
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ font-size: 1rem;
+ font-weight: 300;
+ line-height: 1.5;
+ color: #464746;
+ text-align: left;
+ background-color: #ffffff;
+}
+
+/* line 66, node_modules/bootstrap/scss/_reboot.scss */
+[tabindex="-1"]:focus:not(:focus-visible) {
+ outline: 0 !important;
+}
+
+/* line 76, node_modules/bootstrap/scss/_reboot.scss */
+hr {
+ -webkit-box-sizing: content-box;
+ box-sizing: content-box;
+ height: 0;
+ overflow: visible;
+}
+
+/* line 92, node_modules/bootstrap/scss/_reboot.scss */
+h1, h2, h3, h4, h5, h6 {
+ margin-top: 0;
+ margin-bottom: 0.5rem;
+}
+
+/* line 101, node_modules/bootstrap/scss/_reboot.scss */
+p {
+ margin-top: 0;
+ margin-bottom: 1rem;
+}
+
+/* line 114, node_modules/bootstrap/scss/_reboot.scss */
+abbr[title],
+abbr[data-original-title] {
+ text-decoration: underline;
+ -webkit-text-decoration: underline dotted;
+ text-decoration: underline dotted;
+ cursor: help;
+ border-bottom: 0;
+ -webkit-text-decoration-skip-ink: none;
+ text-decoration-skip-ink: none;
+}
+
+/* line 123, node_modules/bootstrap/scss/_reboot.scss */
+address {
+ margin-bottom: 1rem;
+ font-style: normal;
+ line-height: inherit;
+}
+
+/* line 129, node_modules/bootstrap/scss/_reboot.scss */
+ol,
+ul,
+dl {
+ margin-top: 0;
+ margin-bottom: 1rem;
+}
+
+/* line 136, node_modules/bootstrap/scss/_reboot.scss */
+ol ol,
+ul ul,
+ol ul,
+ul ol {
+ margin-bottom: 0;
+}
+
+/* line 143, node_modules/bootstrap/scss/_reboot.scss */
+dt {
+ font-weight: 700;
+}
+
+/* line 147, node_modules/bootstrap/scss/_reboot.scss */
+dd {
+ margin-bottom: .5rem;
+ margin-left: 0;
+}
+
+/* line 152, node_modules/bootstrap/scss/_reboot.scss */
+blockquote {
+ margin: 0 0 1rem;
+}
+
+/* line 156, node_modules/bootstrap/scss/_reboot.scss */
+b,
+strong {
+ font-weight: 800;
+}
+
+/* line 161, node_modules/bootstrap/scss/_reboot.scss */
+small {
+ font-size: 80%;
+}
+
+/* line 170, node_modules/bootstrap/scss/_reboot.scss */
+sub,
+sup {
+ position: relative;
+ font-size: 75%;
+ line-height: 0;
+ vertical-align: baseline;
+}
+
+/* line 178, node_modules/bootstrap/scss/_reboot.scss */
+sub {
+ bottom: -.25em;
+}
+
+/* line 179, node_modules/bootstrap/scss/_reboot.scss */
+sup {
+ top: -.5em;
+}
+
+/* line 186, node_modules/bootstrap/scss/_reboot.scss */
+a {
+ color: #464746;
+ text-decoration: none;
+ background-color: transparent;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+a:hover {
+ color: #202020;
+ text-decoration: underline;
+}
+
+/* line 202, node_modules/bootstrap/scss/_reboot.scss */
+a:not([href]):not([class]) {
+ color: inherit;
+ text-decoration: none;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+a:not([href]):not([class]):hover {
+ color: inherit;
+ text-decoration: none;
+}
+
+/* line 217, node_modules/bootstrap/scss/_reboot.scss */
+pre,
+code,
+kbd,
+samp {
+ font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+ font-size: 1em;
+}
+
+/* line 225, node_modules/bootstrap/scss/_reboot.scss */
+pre {
+ margin-top: 0;
+ margin-bottom: 1rem;
+ overflow: auto;
+ -ms-overflow-style: scrollbar;
+}
+
+/* line 242, node_modules/bootstrap/scss/_reboot.scss */
+figure {
+ margin: 0 0 1rem;
+}
+
+/* line 252, node_modules/bootstrap/scss/_reboot.scss */
+img {
+ vertical-align: middle;
+ border-style: none;
+}
+
+/* line 257, node_modules/bootstrap/scss/_reboot.scss */
+svg {
+ overflow: hidden;
+ vertical-align: middle;
+}
+
+/* line 269, node_modules/bootstrap/scss/_reboot.scss */
+table {
+ border-collapse: collapse;
+}
+
+/* line 273, node_modules/bootstrap/scss/_reboot.scss */
+caption {
+ padding-top: 0.75rem;
+ padding-bottom: 0.75rem;
+ color: #6c757d;
+ text-align: left;
+ caption-side: bottom;
+}
+
+/* line 281, node_modules/bootstrap/scss/_reboot.scss */
+th {
+ text-align: inherit;
+}
+
+/* line 292, node_modules/bootstrap/scss/_reboot.scss */
+label {
+ display: inline-block;
+ margin-bottom: 0.5rem;
+}
+
+/* line 301, node_modules/bootstrap/scss/_reboot.scss */
+button {
+ border-radius: 0;
+}
+
+/* line 310, node_modules/bootstrap/scss/_reboot.scss */
+button:focus {
+ outline: 1px dotted;
+ outline: 5px auto -webkit-focus-ring-color;
+}
+
+/* line 315, node_modules/bootstrap/scss/_reboot.scss */
+input,
+button,
+select,
+optgroup,
+textarea {
+ margin: 0;
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+}
+
+/* line 326, node_modules/bootstrap/scss/_reboot.scss */
+button,
+input {
+ overflow: visible;
+}
+
+/* line 331, node_modules/bootstrap/scss/_reboot.scss */
+button,
+select {
+ text-transform: none;
+}
+
+/* line 339, node_modules/bootstrap/scss/_reboot.scss */
+[role="button"] {
+ cursor: pointer;
+}
+
+/* line 346, node_modules/bootstrap/scss/_reboot.scss */
+select {
+ word-wrap: normal;
+}
+
+/* line 354, node_modules/bootstrap/scss/_reboot.scss */
+button,
+[type="button"],
+[type="reset"],
+[type="submit"] {
+ -webkit-appearance: button;
+}
+
+/* line 367, node_modules/bootstrap/scss/_reboot.scss */
+button:not(:disabled),
+[type="button"]:not(:disabled),
+[type="reset"]:not(:disabled),
+[type="submit"]:not(:disabled) {
+ cursor: pointer;
+}
+
+/* line 374, node_modules/bootstrap/scss/_reboot.scss */
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner {
+ padding: 0;
+ border-style: none;
+}
+
+/* line 382, node_modules/bootstrap/scss/_reboot.scss */
+input[type="radio"],
+input[type="checkbox"] {
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ padding: 0;
+}
+
+/* line 389, node_modules/bootstrap/scss/_reboot.scss */
+textarea {
+ overflow: auto;
+ resize: vertical;
+}
+
+/* line 395, node_modules/bootstrap/scss/_reboot.scss */
+fieldset {
+ min-width: 0;
+ padding: 0;
+ margin: 0;
+ border: 0;
+}
+
+/* line 410, node_modules/bootstrap/scss/_reboot.scss */
+legend {
+ display: block;
+ width: 100%;
+ max-width: 100%;
+ padding: 0;
+ margin-bottom: .5rem;
+ font-size: 1.5rem;
+ line-height: inherit;
+ color: inherit;
+ white-space: normal;
+}
+
+/* line 422, node_modules/bootstrap/scss/_reboot.scss */
+progress {
+ vertical-align: baseline;
+}
+
+/* line 427, node_modules/bootstrap/scss/_reboot.scss */
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/* line 432, node_modules/bootstrap/scss/_reboot.scss */
+[type="search"] {
+ outline-offset: -2px;
+ -webkit-appearance: none;
+}
+
+/* line 445, node_modules/bootstrap/scss/_reboot.scss */
+[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/* line 454, node_modules/bootstrap/scss/_reboot.scss */
+::-webkit-file-upload-button {
+ font: inherit;
+ -webkit-appearance: button;
+}
+
+/* line 463, node_modules/bootstrap/scss/_reboot.scss */
+output {
+ display: inline-block;
+}
+
+/* line 467, node_modules/bootstrap/scss/_reboot.scss */
+summary {
+ display: list-item;
+ cursor: pointer;
+}
+
+/* line 472, node_modules/bootstrap/scss/_reboot.scss */
+template {
+ display: none;
+}
+
+/* line 478, node_modules/bootstrap/scss/_reboot.scss */
+[hidden] {
+ display: none !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/_type.scss */
+h1, h2, h3, h4, h5, h6,
+.h1, .h2, .h3, .h4, .h5, .h6 {
+ margin-bottom: 0.5rem;
+ font-weight: 500;
+ line-height: 1.2;
+}
+
+/* line 16, node_modules/bootstrap/scss/_type.scss */
+h1, .h1 {
+ font-size: 4rem;
+}
+
+/* line 17, node_modules/bootstrap/scss/_type.scss */
+h2, .h2 {
+ font-size: 2.4375rem;
+}
+
+/* line 18, node_modules/bootstrap/scss/_type.scss */
+h3, .h3 {
+ font-size: 1.5rem;
+}
+
+/* line 19, node_modules/bootstrap/scss/_type.scss */
+h4, .h4 {
+ font-size: 0.9375rem;
+}
+
+/* line 20, node_modules/bootstrap/scss/_type.scss */
+h5, .h5 {
+ font-size: 0.75rem;
+}
+
+/* line 21, node_modules/bootstrap/scss/_type.scss */
+h6, .h6 {
+ font-size: 0.6875rem;
+}
+
+/* line 23, node_modules/bootstrap/scss/_type.scss */
+.lead {
+ font-size: 1.25rem;
+ font-weight: 300;
+}
+
+/* line 29, node_modules/bootstrap/scss/_type.scss */
+.display-1 {
+ font-size: 6rem;
+ font-weight: 300;
+ line-height: 1.2;
+}
+
+/* line 34, node_modules/bootstrap/scss/_type.scss */
+.display-2 {
+ font-size: 5.5rem;
+ font-weight: 300;
+ line-height: 1.2;
+}
+
+/* line 39, node_modules/bootstrap/scss/_type.scss */
+.display-3 {
+ font-size: 4.5rem;
+ font-weight: 300;
+ line-height: 1.2;
+}
+
+/* line 44, node_modules/bootstrap/scss/_type.scss */
+.display-4 {
+ font-size: 3.5rem;
+ font-weight: 300;
+ line-height: 1.2;
+}
+
+/* line 55, node_modules/bootstrap/scss/_type.scss */
+hr {
+ margin-top: 1rem;
+ margin-bottom: 1rem;
+ border: 0;
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
+}
+
+/* line 67, node_modules/bootstrap/scss/_type.scss */
+small,
+.small {
+ font-size: 80%;
+ font-weight: 400;
+}
+
+/* line 73, node_modules/bootstrap/scss/_type.scss */
+mark,
+.mark {
+ padding: 0.2em;
+ background-color: #fcf8e3;
+}
+
+/* line 84, node_modules/bootstrap/scss/_type.scss */
+.list-unstyled {
+ padding-left: 0;
+ list-style: none;
+}
+
+/* line 89, node_modules/bootstrap/scss/_type.scss */
+.list-inline {
+ padding-left: 0;
+ list-style: none;
+}
+
+/* line 92, node_modules/bootstrap/scss/_type.scss */
+.list-inline-item {
+ display: inline-block;
+}
+
+/* line 95, node_modules/bootstrap/scss/_type.scss */
+.list-inline-item:not(:last-child) {
+ margin-right: 0.5rem;
+}
+
+/* line 106, node_modules/bootstrap/scss/_type.scss */
+.initialism {
+ font-size: 90%;
+ text-transform: uppercase;
+}
+
+/* line 112, node_modules/bootstrap/scss/_type.scss */
+.blockquote {
+ margin-bottom: 1rem;
+ font-size: 1.25rem;
+}
+
+/* line 117, node_modules/bootstrap/scss/_type.scss */
+.blockquote-footer {
+ display: block;
+ font-size: 80%;
+ color: #6c757d;
+}
+
+/* line 122, node_modules/bootstrap/scss/_type.scss */
+.blockquote-footer::before {
+ content: "\2014\00A0";
+}
+
+/* line 8, node_modules/bootstrap/scss/_images.scss */
+.img-fluid {
+ max-width: 100%;
+ height: auto;
+}
+
+/* line 14, node_modules/bootstrap/scss/_images.scss */
+.img-thumbnail {
+ padding: 0.25rem;
+ background-color: #ffffff;
+ border: 1px solid #dee2e6;
+ border-radius: 0.25rem;
+ max-width: 100%;
+ height: auto;
+}
+
+/* line 29, node_modules/bootstrap/scss/_images.scss */
+.figure {
+ display: inline-block;
+}
+
+/* line 34, node_modules/bootstrap/scss/_images.scss */
+.figure-img {
+ margin-bottom: 0.5rem;
+ line-height: 1;
+}
+
+/* line 39, node_modules/bootstrap/scss/_images.scss */
+.figure-caption {
+ font-size: 90%;
+ color: #6c757d;
+}
+
+/* line 2, node_modules/bootstrap/scss/_code.scss */
+code {
+ font-size: 87.5%;
+ color: #e83e8c;
+ word-wrap: break-word;
+}
+
+/* line 8, node_modules/bootstrap/scss/_code.scss */
+a > code {
+ color: inherit;
+}
+
+/* line 14, node_modules/bootstrap/scss/_code.scss */
+kbd {
+ padding: 0.2rem 0.4rem;
+ font-size: 87.5%;
+ color: #ffffff;
+ background-color: #464746;
+ border-radius: 0.2rem;
+}
+
+/* line 22, node_modules/bootstrap/scss/_code.scss */
+kbd kbd {
+ padding: 0;
+ font-size: 100%;
+ font-weight: 700;
+}
+
+/* line 31, node_modules/bootstrap/scss/_code.scss */
+pre {
+ display: block;
+ font-size: 87.5%;
+ color: #464746;
+}
+
+/* line 37, node_modules/bootstrap/scss/_code.scss */
+pre code {
+ font-size: inherit;
+ color: inherit;
+ word-break: normal;
+}
+
+/* line 45, node_modules/bootstrap/scss/_code.scss */
+.pre-scrollable {
+ max-height: 340px;
+ overflow-y: scroll;
+}
+
+/* line 7, node_modules/bootstrap/scss/_grid.scss */
+.container,
+.container-fluid,
+.container-sm,
+.container-md,
+.container-lg,
+.container-xl,
+.container-xxl {
+ width: 100%;
+ padding-right: 15px;
+ padding-left: 15px;
+ margin-right: auto;
+ margin-left: auto;
+}
+
+@media (min-width: 576px) {
+ /* line 20, node_modules/bootstrap/scss/_grid.scss */
+ .container, .container-sm {
+ max-width: 100%;
+ }
+}
+
+@media (min-width: 768px) {
+ /* line 20, node_modules/bootstrap/scss/_grid.scss */
+ .container, .container-sm, .container-md {
+ max-width: 100%;
+ }
+}
+
+@media (min-width: 1024px) {
+ /* line 20, node_modules/bootstrap/scss/_grid.scss */
+ .container, .container-sm, .container-md, .container-lg {
+ max-width: 100%;
+ }
+}
+
+@media (min-width: 1280px) {
+ /* line 20, node_modules/bootstrap/scss/_grid.scss */
+ .container, .container-sm, .container-md, .container-lg, .container-xl {
+ max-width: 1150px;
+ }
+}
+
+@media (min-width: 1440px) {
+ /* line 20, node_modules/bootstrap/scss/_grid.scss */
+ .container, .container-sm, .container-md, .container-lg, .container-xl, .container-xxl {
+ max-width: 1310px;
+ }
+}
+
+/* line 49, node_modules/bootstrap/scss/_grid.scss */
+.row {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+ margin-right: -15px;
+ margin-left: -15px;
+}
+
+/* line 55, node_modules/bootstrap/scss/_grid.scss */
+.no-gutters {
+ margin-right: 0;
+ margin-left: 0;
+}
+
+/* line 59, node_modules/bootstrap/scss/_grid.scss */
+.no-gutters > .col,
+.no-gutters > [class*="col-"] {
+ padding-right: 0;
+ padding-left: 0;
+}
+
+/* line 8, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col,
+.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm,
+.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md,
+.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg,
+.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl,
+.col-xl-auto, .col-xxl-1, .col-xxl-2, .col-xxl-3, .col-xxl-4, .col-xxl-5, .col-xxl-6, .col-xxl-7, .col-xxl-8, .col-xxl-9, .col-xxl-10, .col-xxl-11, .col-xxl-12, .col-xxl,
+.col-xxl-auto {
+ position: relative;
+ width: 100%;
+ padding-right: 15px;
+ padding-left: 15px;
+}
+
+/* line 34, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col {
+ -ms-flex-preferred-size: 0;
+ flex-basis: 0;
+ -webkit-box-flex: 1;
+ -ms-flex-positive: 1;
+ flex-grow: 1;
+ max-width: 100%;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+.row-cols-1 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 100%;
+ flex: 0 0 100%;
+ max-width: 100%;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+.row-cols-2 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 50%;
+ flex: 0 0 50%;
+ max-width: 50%;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+.row-cols-3 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 33.33333%;
+ flex: 0 0 33.33333%;
+ max-width: 33.33333%;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+.row-cols-4 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 25%;
+ flex: 0 0 25%;
+ max-width: 25%;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+.row-cols-5 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 20%;
+ flex: 0 0 20%;
+ max-width: 20%;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+.row-cols-6 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 16.66667%;
+ flex: 0 0 16.66667%;
+ max-width: 16.66667%;
+}
+
+/* line 48, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-auto {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 auto;
+ flex: 0 0 auto;
+ width: auto;
+ max-width: 100%;
+}
+
+/* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-1 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 8.33333%;
+ flex: 0 0 8.33333%;
+ max-width: 8.33333%;
+}
+
+/* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-2 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 16.66667%;
+ flex: 0 0 16.66667%;
+ max-width: 16.66667%;
+}
+
+/* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-3 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 25%;
+ flex: 0 0 25%;
+ max-width: 25%;
+}
+
+/* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-4 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 33.33333%;
+ flex: 0 0 33.33333%;
+ max-width: 33.33333%;
+}
+
+/* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-5 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 41.66667%;
+ flex: 0 0 41.66667%;
+ max-width: 41.66667%;
+}
+
+/* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-6 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 50%;
+ flex: 0 0 50%;
+ max-width: 50%;
+}
+
+/* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-7 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 58.33333%;
+ flex: 0 0 58.33333%;
+ max-width: 58.33333%;
+}
+
+/* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-8 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 66.66667%;
+ flex: 0 0 66.66667%;
+ max-width: 66.66667%;
+}
+
+/* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-9 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 75%;
+ flex: 0 0 75%;
+ max-width: 75%;
+}
+
+/* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-10 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 83.33333%;
+ flex: 0 0 83.33333%;
+ max-width: 83.33333%;
+}
+
+/* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-11 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 91.66667%;
+ flex: 0 0 91.66667%;
+ max-width: 91.66667%;
+}
+
+/* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.col-12 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 100%;
+ flex: 0 0 100%;
+ max-width: 100%;
+}
+
+/* line 60, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-first {
+ -webkit-box-ordinal-group: 0;
+ -ms-flex-order: -1;
+ order: -1;
+}
+
+/* line 62, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-last {
+ -webkit-box-ordinal-group: 14;
+ -ms-flex-order: 13;
+ order: 13;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-0 {
+ -webkit-box-ordinal-group: 1;
+ -ms-flex-order: 0;
+ order: 0;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-1 {
+ -webkit-box-ordinal-group: 2;
+ -ms-flex-order: 1;
+ order: 1;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-2 {
+ -webkit-box-ordinal-group: 3;
+ -ms-flex-order: 2;
+ order: 2;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-3 {
+ -webkit-box-ordinal-group: 4;
+ -ms-flex-order: 3;
+ order: 3;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-4 {
+ -webkit-box-ordinal-group: 5;
+ -ms-flex-order: 4;
+ order: 4;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-5 {
+ -webkit-box-ordinal-group: 6;
+ -ms-flex-order: 5;
+ order: 5;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-6 {
+ -webkit-box-ordinal-group: 7;
+ -ms-flex-order: 6;
+ order: 6;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-7 {
+ -webkit-box-ordinal-group: 8;
+ -ms-flex-order: 7;
+ order: 7;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-8 {
+ -webkit-box-ordinal-group: 9;
+ -ms-flex-order: 8;
+ order: 8;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-9 {
+ -webkit-box-ordinal-group: 10;
+ -ms-flex-order: 9;
+ order: 9;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-10 {
+ -webkit-box-ordinal-group: 11;
+ -ms-flex-order: 10;
+ order: 10;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-11 {
+ -webkit-box-ordinal-group: 12;
+ -ms-flex-order: 11;
+ order: 11;
+}
+
+/* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.order-12 {
+ -webkit-box-ordinal-group: 13;
+ -ms-flex-order: 12;
+ order: 12;
+}
+
+/* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.offset-1 {
+ margin-left: 8.33333%;
+}
+
+/* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.offset-2 {
+ margin-left: 16.66667%;
+}
+
+/* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.offset-3 {
+ margin-left: 25%;
+}
+
+/* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.offset-4 {
+ margin-left: 33.33333%;
+}
+
+/* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.offset-5 {
+ margin-left: 41.66667%;
+}
+
+/* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.offset-6 {
+ margin-left: 50%;
+}
+
+/* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.offset-7 {
+ margin-left: 58.33333%;
+}
+
+/* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.offset-8 {
+ margin-left: 66.66667%;
+}
+
+/* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.offset-9 {
+ margin-left: 75%;
+}
+
+/* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.offset-10 {
+ margin-left: 83.33333%;
+}
+
+/* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+.offset-11 {
+ margin-left: 91.66667%;
+}
+
+@media (min-width: 576px) {
+ /* line 34, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-sm {
+ -ms-flex-preferred-size: 0;
+ flex-basis: 0;
+ -webkit-box-flex: 1;
+ -ms-flex-positive: 1;
+ flex-grow: 1;
+ max-width: 100%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-sm-1 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 100%;
+ flex: 0 0 100%;
+ max-width: 100%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-sm-2 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 50%;
+ flex: 0 0 50%;
+ max-width: 50%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-sm-3 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 33.33333%;
+ flex: 0 0 33.33333%;
+ max-width: 33.33333%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-sm-4 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 25%;
+ flex: 0 0 25%;
+ max-width: 25%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-sm-5 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 20%;
+ flex: 0 0 20%;
+ max-width: 20%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-sm-6 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 16.66667%;
+ flex: 0 0 16.66667%;
+ max-width: 16.66667%;
+ }
+ /* line 48, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-sm-auto {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 auto;
+ flex: 0 0 auto;
+ width: auto;
+ max-width: 100%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-sm-1 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 8.33333%;
+ flex: 0 0 8.33333%;
+ max-width: 8.33333%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-sm-2 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 16.66667%;
+ flex: 0 0 16.66667%;
+ max-width: 16.66667%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-sm-3 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 25%;
+ flex: 0 0 25%;
+ max-width: 25%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-sm-4 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 33.33333%;
+ flex: 0 0 33.33333%;
+ max-width: 33.33333%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-sm-5 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 41.66667%;
+ flex: 0 0 41.66667%;
+ max-width: 41.66667%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-sm-6 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 50%;
+ flex: 0 0 50%;
+ max-width: 50%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-sm-7 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 58.33333%;
+ flex: 0 0 58.33333%;
+ max-width: 58.33333%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-sm-8 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 66.66667%;
+ flex: 0 0 66.66667%;
+ max-width: 66.66667%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-sm-9 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 75%;
+ flex: 0 0 75%;
+ max-width: 75%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-sm-10 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 83.33333%;
+ flex: 0 0 83.33333%;
+ max-width: 83.33333%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-sm-11 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 91.66667%;
+ flex: 0 0 91.66667%;
+ max-width: 91.66667%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-sm-12 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 100%;
+ flex: 0 0 100%;
+ max-width: 100%;
+ }
+ /* line 60, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-sm-first {
+ -webkit-box-ordinal-group: 0;
+ -ms-flex-order: -1;
+ order: -1;
+ }
+ /* line 62, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-sm-last {
+ -webkit-box-ordinal-group: 14;
+ -ms-flex-order: 13;
+ order: 13;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-sm-0 {
+ -webkit-box-ordinal-group: 1;
+ -ms-flex-order: 0;
+ order: 0;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-sm-1 {
+ -webkit-box-ordinal-group: 2;
+ -ms-flex-order: 1;
+ order: 1;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-sm-2 {
+ -webkit-box-ordinal-group: 3;
+ -ms-flex-order: 2;
+ order: 2;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-sm-3 {
+ -webkit-box-ordinal-group: 4;
+ -ms-flex-order: 3;
+ order: 3;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-sm-4 {
+ -webkit-box-ordinal-group: 5;
+ -ms-flex-order: 4;
+ order: 4;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-sm-5 {
+ -webkit-box-ordinal-group: 6;
+ -ms-flex-order: 5;
+ order: 5;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-sm-6 {
+ -webkit-box-ordinal-group: 7;
+ -ms-flex-order: 6;
+ order: 6;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-sm-7 {
+ -webkit-box-ordinal-group: 8;
+ -ms-flex-order: 7;
+ order: 7;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-sm-8 {
+ -webkit-box-ordinal-group: 9;
+ -ms-flex-order: 8;
+ order: 8;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-sm-9 {
+ -webkit-box-ordinal-group: 10;
+ -ms-flex-order: 9;
+ order: 9;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-sm-10 {
+ -webkit-box-ordinal-group: 11;
+ -ms-flex-order: 10;
+ order: 10;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-sm-11 {
+ -webkit-box-ordinal-group: 12;
+ -ms-flex-order: 11;
+ order: 11;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-sm-12 {
+ -webkit-box-ordinal-group: 13;
+ -ms-flex-order: 12;
+ order: 12;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-sm-0 {
+ margin-left: 0;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-sm-1 {
+ margin-left: 8.33333%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-sm-2 {
+ margin-left: 16.66667%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-sm-3 {
+ margin-left: 25%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-sm-4 {
+ margin-left: 33.33333%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-sm-5 {
+ margin-left: 41.66667%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-sm-6 {
+ margin-left: 50%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-sm-7 {
+ margin-left: 58.33333%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-sm-8 {
+ margin-left: 66.66667%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-sm-9 {
+ margin-left: 75%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-sm-10 {
+ margin-left: 83.33333%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-sm-11 {
+ margin-left: 91.66667%;
+ }
+}
+
+@media (min-width: 768px) {
+ /* line 34, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-md {
+ -ms-flex-preferred-size: 0;
+ flex-basis: 0;
+ -webkit-box-flex: 1;
+ -ms-flex-positive: 1;
+ flex-grow: 1;
+ max-width: 100%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-md-1 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 100%;
+ flex: 0 0 100%;
+ max-width: 100%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-md-2 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 50%;
+ flex: 0 0 50%;
+ max-width: 50%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-md-3 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 33.33333%;
+ flex: 0 0 33.33333%;
+ max-width: 33.33333%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-md-4 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 25%;
+ flex: 0 0 25%;
+ max-width: 25%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-md-5 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 20%;
+ flex: 0 0 20%;
+ max-width: 20%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-md-6 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 16.66667%;
+ flex: 0 0 16.66667%;
+ max-width: 16.66667%;
+ }
+ /* line 48, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-md-auto {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 auto;
+ flex: 0 0 auto;
+ width: auto;
+ max-width: 100%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-md-1 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 8.33333%;
+ flex: 0 0 8.33333%;
+ max-width: 8.33333%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-md-2 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 16.66667%;
+ flex: 0 0 16.66667%;
+ max-width: 16.66667%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-md-3 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 25%;
+ flex: 0 0 25%;
+ max-width: 25%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-md-4 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 33.33333%;
+ flex: 0 0 33.33333%;
+ max-width: 33.33333%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-md-5 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 41.66667%;
+ flex: 0 0 41.66667%;
+ max-width: 41.66667%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-md-6 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 50%;
+ flex: 0 0 50%;
+ max-width: 50%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-md-7 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 58.33333%;
+ flex: 0 0 58.33333%;
+ max-width: 58.33333%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-md-8 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 66.66667%;
+ flex: 0 0 66.66667%;
+ max-width: 66.66667%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-md-9 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 75%;
+ flex: 0 0 75%;
+ max-width: 75%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-md-10 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 83.33333%;
+ flex: 0 0 83.33333%;
+ max-width: 83.33333%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-md-11 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 91.66667%;
+ flex: 0 0 91.66667%;
+ max-width: 91.66667%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-md-12 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 100%;
+ flex: 0 0 100%;
+ max-width: 100%;
+ }
+ /* line 60, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-md-first {
+ -webkit-box-ordinal-group: 0;
+ -ms-flex-order: -1;
+ order: -1;
+ }
+ /* line 62, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-md-last {
+ -webkit-box-ordinal-group: 14;
+ -ms-flex-order: 13;
+ order: 13;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-md-0 {
+ -webkit-box-ordinal-group: 1;
+ -ms-flex-order: 0;
+ order: 0;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-md-1 {
+ -webkit-box-ordinal-group: 2;
+ -ms-flex-order: 1;
+ order: 1;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-md-2 {
+ -webkit-box-ordinal-group: 3;
+ -ms-flex-order: 2;
+ order: 2;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-md-3 {
+ -webkit-box-ordinal-group: 4;
+ -ms-flex-order: 3;
+ order: 3;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-md-4 {
+ -webkit-box-ordinal-group: 5;
+ -ms-flex-order: 4;
+ order: 4;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-md-5 {
+ -webkit-box-ordinal-group: 6;
+ -ms-flex-order: 5;
+ order: 5;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-md-6 {
+ -webkit-box-ordinal-group: 7;
+ -ms-flex-order: 6;
+ order: 6;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-md-7 {
+ -webkit-box-ordinal-group: 8;
+ -ms-flex-order: 7;
+ order: 7;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-md-8 {
+ -webkit-box-ordinal-group: 9;
+ -ms-flex-order: 8;
+ order: 8;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-md-9 {
+ -webkit-box-ordinal-group: 10;
+ -ms-flex-order: 9;
+ order: 9;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-md-10 {
+ -webkit-box-ordinal-group: 11;
+ -ms-flex-order: 10;
+ order: 10;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-md-11 {
+ -webkit-box-ordinal-group: 12;
+ -ms-flex-order: 11;
+ order: 11;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-md-12 {
+ -webkit-box-ordinal-group: 13;
+ -ms-flex-order: 12;
+ order: 12;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-md-0 {
+ margin-left: 0;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-md-1 {
+ margin-left: 8.33333%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-md-2 {
+ margin-left: 16.66667%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-md-3 {
+ margin-left: 25%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-md-4 {
+ margin-left: 33.33333%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-md-5 {
+ margin-left: 41.66667%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-md-6 {
+ margin-left: 50%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-md-7 {
+ margin-left: 58.33333%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-md-8 {
+ margin-left: 66.66667%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-md-9 {
+ margin-left: 75%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-md-10 {
+ margin-left: 83.33333%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-md-11 {
+ margin-left: 91.66667%;
+ }
+}
+
+@media (min-width: 1024px) {
+ /* line 34, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-lg {
+ -ms-flex-preferred-size: 0;
+ flex-basis: 0;
+ -webkit-box-flex: 1;
+ -ms-flex-positive: 1;
+ flex-grow: 1;
+ max-width: 100%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-lg-1 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 100%;
+ flex: 0 0 100%;
+ max-width: 100%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-lg-2 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 50%;
+ flex: 0 0 50%;
+ max-width: 50%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-lg-3 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 33.33333%;
+ flex: 0 0 33.33333%;
+ max-width: 33.33333%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-lg-4 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 25%;
+ flex: 0 0 25%;
+ max-width: 25%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-lg-5 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 20%;
+ flex: 0 0 20%;
+ max-width: 20%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-lg-6 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 16.66667%;
+ flex: 0 0 16.66667%;
+ max-width: 16.66667%;
+ }
+ /* line 48, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-lg-auto {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 auto;
+ flex: 0 0 auto;
+ width: auto;
+ max-width: 100%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-lg-1 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 8.33333%;
+ flex: 0 0 8.33333%;
+ max-width: 8.33333%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-lg-2 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 16.66667%;
+ flex: 0 0 16.66667%;
+ max-width: 16.66667%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-lg-3 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 25%;
+ flex: 0 0 25%;
+ max-width: 25%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-lg-4 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 33.33333%;
+ flex: 0 0 33.33333%;
+ max-width: 33.33333%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-lg-5 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 41.66667%;
+ flex: 0 0 41.66667%;
+ max-width: 41.66667%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-lg-6 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 50%;
+ flex: 0 0 50%;
+ max-width: 50%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-lg-7 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 58.33333%;
+ flex: 0 0 58.33333%;
+ max-width: 58.33333%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-lg-8 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 66.66667%;
+ flex: 0 0 66.66667%;
+ max-width: 66.66667%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-lg-9 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 75%;
+ flex: 0 0 75%;
+ max-width: 75%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-lg-10 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 83.33333%;
+ flex: 0 0 83.33333%;
+ max-width: 83.33333%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-lg-11 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 91.66667%;
+ flex: 0 0 91.66667%;
+ max-width: 91.66667%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-lg-12 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 100%;
+ flex: 0 0 100%;
+ max-width: 100%;
+ }
+ /* line 60, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-lg-first {
+ -webkit-box-ordinal-group: 0;
+ -ms-flex-order: -1;
+ order: -1;
+ }
+ /* line 62, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-lg-last {
+ -webkit-box-ordinal-group: 14;
+ -ms-flex-order: 13;
+ order: 13;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-lg-0 {
+ -webkit-box-ordinal-group: 1;
+ -ms-flex-order: 0;
+ order: 0;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-lg-1 {
+ -webkit-box-ordinal-group: 2;
+ -ms-flex-order: 1;
+ order: 1;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-lg-2 {
+ -webkit-box-ordinal-group: 3;
+ -ms-flex-order: 2;
+ order: 2;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-lg-3 {
+ -webkit-box-ordinal-group: 4;
+ -ms-flex-order: 3;
+ order: 3;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-lg-4 {
+ -webkit-box-ordinal-group: 5;
+ -ms-flex-order: 4;
+ order: 4;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-lg-5 {
+ -webkit-box-ordinal-group: 6;
+ -ms-flex-order: 5;
+ order: 5;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-lg-6 {
+ -webkit-box-ordinal-group: 7;
+ -ms-flex-order: 6;
+ order: 6;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-lg-7 {
+ -webkit-box-ordinal-group: 8;
+ -ms-flex-order: 7;
+ order: 7;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-lg-8 {
+ -webkit-box-ordinal-group: 9;
+ -ms-flex-order: 8;
+ order: 8;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-lg-9 {
+ -webkit-box-ordinal-group: 10;
+ -ms-flex-order: 9;
+ order: 9;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-lg-10 {
+ -webkit-box-ordinal-group: 11;
+ -ms-flex-order: 10;
+ order: 10;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-lg-11 {
+ -webkit-box-ordinal-group: 12;
+ -ms-flex-order: 11;
+ order: 11;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-lg-12 {
+ -webkit-box-ordinal-group: 13;
+ -ms-flex-order: 12;
+ order: 12;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-lg-0 {
+ margin-left: 0;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-lg-1 {
+ margin-left: 8.33333%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-lg-2 {
+ margin-left: 16.66667%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-lg-3 {
+ margin-left: 25%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-lg-4 {
+ margin-left: 33.33333%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-lg-5 {
+ margin-left: 41.66667%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-lg-6 {
+ margin-left: 50%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-lg-7 {
+ margin-left: 58.33333%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-lg-8 {
+ margin-left: 66.66667%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-lg-9 {
+ margin-left: 75%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-lg-10 {
+ margin-left: 83.33333%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-lg-11 {
+ margin-left: 91.66667%;
+ }
+}
+
+@media (min-width: 1280px) {
+ /* line 34, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xl {
+ -ms-flex-preferred-size: 0;
+ flex-basis: 0;
+ -webkit-box-flex: 1;
+ -ms-flex-positive: 1;
+ flex-grow: 1;
+ max-width: 100%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-xl-1 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 100%;
+ flex: 0 0 100%;
+ max-width: 100%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-xl-2 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 50%;
+ flex: 0 0 50%;
+ max-width: 50%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-xl-3 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 33.33333%;
+ flex: 0 0 33.33333%;
+ max-width: 33.33333%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-xl-4 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 25%;
+ flex: 0 0 25%;
+ max-width: 25%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-xl-5 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 20%;
+ flex: 0 0 20%;
+ max-width: 20%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-xl-6 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 16.66667%;
+ flex: 0 0 16.66667%;
+ max-width: 16.66667%;
+ }
+ /* line 48, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xl-auto {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 auto;
+ flex: 0 0 auto;
+ width: auto;
+ max-width: 100%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xl-1 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 8.33333%;
+ flex: 0 0 8.33333%;
+ max-width: 8.33333%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xl-2 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 16.66667%;
+ flex: 0 0 16.66667%;
+ max-width: 16.66667%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xl-3 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 25%;
+ flex: 0 0 25%;
+ max-width: 25%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xl-4 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 33.33333%;
+ flex: 0 0 33.33333%;
+ max-width: 33.33333%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xl-5 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 41.66667%;
+ flex: 0 0 41.66667%;
+ max-width: 41.66667%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xl-6 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 50%;
+ flex: 0 0 50%;
+ max-width: 50%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xl-7 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 58.33333%;
+ flex: 0 0 58.33333%;
+ max-width: 58.33333%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xl-8 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 66.66667%;
+ flex: 0 0 66.66667%;
+ max-width: 66.66667%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xl-9 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 75%;
+ flex: 0 0 75%;
+ max-width: 75%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xl-10 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 83.33333%;
+ flex: 0 0 83.33333%;
+ max-width: 83.33333%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xl-11 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 91.66667%;
+ flex: 0 0 91.66667%;
+ max-width: 91.66667%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xl-12 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 100%;
+ flex: 0 0 100%;
+ max-width: 100%;
+ }
+ /* line 60, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xl-first {
+ -webkit-box-ordinal-group: 0;
+ -ms-flex-order: -1;
+ order: -1;
+ }
+ /* line 62, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xl-last {
+ -webkit-box-ordinal-group: 14;
+ -ms-flex-order: 13;
+ order: 13;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xl-0 {
+ -webkit-box-ordinal-group: 1;
+ -ms-flex-order: 0;
+ order: 0;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xl-1 {
+ -webkit-box-ordinal-group: 2;
+ -ms-flex-order: 1;
+ order: 1;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xl-2 {
+ -webkit-box-ordinal-group: 3;
+ -ms-flex-order: 2;
+ order: 2;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xl-3 {
+ -webkit-box-ordinal-group: 4;
+ -ms-flex-order: 3;
+ order: 3;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xl-4 {
+ -webkit-box-ordinal-group: 5;
+ -ms-flex-order: 4;
+ order: 4;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xl-5 {
+ -webkit-box-ordinal-group: 6;
+ -ms-flex-order: 5;
+ order: 5;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xl-6 {
+ -webkit-box-ordinal-group: 7;
+ -ms-flex-order: 6;
+ order: 6;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xl-7 {
+ -webkit-box-ordinal-group: 8;
+ -ms-flex-order: 7;
+ order: 7;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xl-8 {
+ -webkit-box-ordinal-group: 9;
+ -ms-flex-order: 8;
+ order: 8;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xl-9 {
+ -webkit-box-ordinal-group: 10;
+ -ms-flex-order: 9;
+ order: 9;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xl-10 {
+ -webkit-box-ordinal-group: 11;
+ -ms-flex-order: 10;
+ order: 10;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xl-11 {
+ -webkit-box-ordinal-group: 12;
+ -ms-flex-order: 11;
+ order: 11;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xl-12 {
+ -webkit-box-ordinal-group: 13;
+ -ms-flex-order: 12;
+ order: 12;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-xl-0 {
+ margin-left: 0;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-xl-1 {
+ margin-left: 8.33333%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-xl-2 {
+ margin-left: 16.66667%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-xl-3 {
+ margin-left: 25%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-xl-4 {
+ margin-left: 33.33333%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-xl-5 {
+ margin-left: 41.66667%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-xl-6 {
+ margin-left: 50%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-xl-7 {
+ margin-left: 58.33333%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-xl-8 {
+ margin-left: 66.66667%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-xl-9 {
+ margin-left: 75%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-xl-10 {
+ margin-left: 83.33333%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-xl-11 {
+ margin-left: 91.66667%;
+ }
+}
+
+@media (min-width: 1440px) {
+ /* line 34, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xxl {
+ -ms-flex-preferred-size: 0;
+ flex-basis: 0;
+ -webkit-box-flex: 1;
+ -ms-flex-positive: 1;
+ flex-grow: 1;
+ max-width: 100%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-xxl-1 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 100%;
+ flex: 0 0 100%;
+ max-width: 100%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-xxl-2 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 50%;
+ flex: 0 0 50%;
+ max-width: 50%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-xxl-3 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 33.33333%;
+ flex: 0 0 33.33333%;
+ max-width: 33.33333%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-xxl-4 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 25%;
+ flex: 0 0 25%;
+ max-width: 25%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-xxl-5 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 20%;
+ flex: 0 0 20%;
+ max-width: 20%;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid.scss */
+ .row-cols-xxl-6 > * {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 16.66667%;
+ flex: 0 0 16.66667%;
+ max-width: 16.66667%;
+ }
+ /* line 48, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xxl-auto {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 auto;
+ flex: 0 0 auto;
+ width: auto;
+ max-width: 100%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xxl-1 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 8.33333%;
+ flex: 0 0 8.33333%;
+ max-width: 8.33333%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xxl-2 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 16.66667%;
+ flex: 0 0 16.66667%;
+ max-width: 16.66667%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xxl-3 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 25%;
+ flex: 0 0 25%;
+ max-width: 25%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xxl-4 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 33.33333%;
+ flex: 0 0 33.33333%;
+ max-width: 33.33333%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xxl-5 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 41.66667%;
+ flex: 0 0 41.66667%;
+ max-width: 41.66667%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xxl-6 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 50%;
+ flex: 0 0 50%;
+ max-width: 50%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xxl-7 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 58.33333%;
+ flex: 0 0 58.33333%;
+ max-width: 58.33333%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xxl-8 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 66.66667%;
+ flex: 0 0 66.66667%;
+ max-width: 66.66667%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xxl-9 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 75%;
+ flex: 0 0 75%;
+ max-width: 75%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xxl-10 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 83.33333%;
+ flex: 0 0 83.33333%;
+ max-width: 83.33333%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xxl-11 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 91.66667%;
+ flex: 0 0 91.66667%;
+ max-width: 91.66667%;
+ }
+ /* line 54, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .col-xxl-12 {
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 100%;
+ flex: 0 0 100%;
+ max-width: 100%;
+ }
+ /* line 60, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xxl-first {
+ -webkit-box-ordinal-group: 0;
+ -ms-flex-order: -1;
+ order: -1;
+ }
+ /* line 62, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xxl-last {
+ -webkit-box-ordinal-group: 14;
+ -ms-flex-order: 13;
+ order: 13;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xxl-0 {
+ -webkit-box-ordinal-group: 1;
+ -ms-flex-order: 0;
+ order: 0;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xxl-1 {
+ -webkit-box-ordinal-group: 2;
+ -ms-flex-order: 1;
+ order: 1;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xxl-2 {
+ -webkit-box-ordinal-group: 3;
+ -ms-flex-order: 2;
+ order: 2;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xxl-3 {
+ -webkit-box-ordinal-group: 4;
+ -ms-flex-order: 3;
+ order: 3;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xxl-4 {
+ -webkit-box-ordinal-group: 5;
+ -ms-flex-order: 4;
+ order: 4;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xxl-5 {
+ -webkit-box-ordinal-group: 6;
+ -ms-flex-order: 5;
+ order: 5;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xxl-6 {
+ -webkit-box-ordinal-group: 7;
+ -ms-flex-order: 6;
+ order: 6;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xxl-7 {
+ -webkit-box-ordinal-group: 8;
+ -ms-flex-order: 7;
+ order: 7;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xxl-8 {
+ -webkit-box-ordinal-group: 9;
+ -ms-flex-order: 8;
+ order: 8;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xxl-9 {
+ -webkit-box-ordinal-group: 10;
+ -ms-flex-order: 9;
+ order: 9;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xxl-10 {
+ -webkit-box-ordinal-group: 11;
+ -ms-flex-order: 10;
+ order: 10;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xxl-11 {
+ -webkit-box-ordinal-group: 12;
+ -ms-flex-order: 11;
+ order: 11;
+ }
+ /* line 65, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .order-xxl-12 {
+ -webkit-box-ordinal-group: 13;
+ -ms-flex-order: 12;
+ order: 12;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-xxl-0 {
+ margin-left: 0;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-xxl-1 {
+ margin-left: 8.33333%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-xxl-2 {
+ margin-left: 16.66667%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-xxl-3 {
+ margin-left: 25%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-xxl-4 {
+ margin-left: 33.33333%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-xxl-5 {
+ margin-left: 41.66667%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-xxl-6 {
+ margin-left: 50%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-xxl-7 {
+ margin-left: 58.33333%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-xxl-8 {
+ margin-left: 66.66667%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-xxl-9 {
+ margin-left: 75%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-xxl-10 {
+ margin-left: 83.33333%;
+ }
+ /* line 72, node_modules/bootstrap/scss/mixins/_grid-framework.scss */
+ .offset-xxl-11 {
+ margin-left: 91.66667%;
+ }
+}
+
+/* line 5, node_modules/bootstrap/scss/_tables.scss */
+.table {
+ width: 100%;
+ margin-bottom: 1rem;
+ color: #464746;
+}
+
+/* line 11, node_modules/bootstrap/scss/_tables.scss */
+.table th,
+.table td {
+ padding: 0.75rem;
+ vertical-align: top;
+ border-top: 1px solid #dee2e6;
+}
+
+/* line 18, node_modules/bootstrap/scss/_tables.scss */
+.table thead th {
+ vertical-align: bottom;
+ border-bottom: 2px solid #dee2e6;
+}
+
+/* line 23, node_modules/bootstrap/scss/_tables.scss */
+.table tbody + tbody {
+ border-top: 2px solid #dee2e6;
+}
+
+/* line 34, node_modules/bootstrap/scss/_tables.scss */
+.table-sm th,
+.table-sm td {
+ padding: 0.3rem;
+}
+
+/* line 45, node_modules/bootstrap/scss/_tables.scss */
+.table-bordered {
+ border: 1px solid #dee2e6;
+}
+
+/* line 48, node_modules/bootstrap/scss/_tables.scss */
+.table-bordered th,
+.table-bordered td {
+ border: 1px solid #dee2e6;
+}
+
+/* line 54, node_modules/bootstrap/scss/_tables.scss */
+.table-bordered thead th,
+.table-bordered thead td {
+ border-bottom-width: 2px;
+}
+
+/* line 62, node_modules/bootstrap/scss/_tables.scss */
+.table-borderless th,
+.table-borderless td,
+.table-borderless thead th,
+.table-borderless tbody + tbody {
+ border: 0;
+}
+
+/* line 75, node_modules/bootstrap/scss/_tables.scss */
+.table-striped tbody tr:nth-of-type(odd) {
+ background-color: rgba(0, 0, 0, 0.05);
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.table-hover tbody tr:hover {
+ color: #464746;
+ background-color: rgba(0, 0, 0, 0.075);
+}
+
+/* line 7, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-primary,
+.table-primary > th,
+.table-primary > td {
+ background-color: #cbcbcb;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-primary th,
+.table-primary td,
+.table-primary thead th,
+.table-primary tbody + tbody {
+ border-color: #9f9f9f;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.table-hover .table-primary:hover {
+ background-color: #bebebe;
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-hover .table-primary:hover > td,
+.table-hover .table-primary:hover > th {
+ background-color: #bebebe;
+}
+
+/* line 7, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-secondary,
+.table-secondary > th,
+.table-secondary > td {
+ background-color: #fbf9f7;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-secondary th,
+.table-secondary td,
+.table-secondary thead th,
+.table-secondary tbody + tbody {
+ border-color: #f7f5f0;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.table-hover .table-secondary:hover {
+ background-color: #f3ece6;
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-hover .table-secondary:hover > td,
+.table-hover .table-secondary:hover > th {
+ background-color: #f3ece6;
+}
+
+/* line 7, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-success,
+.table-success > th,
+.table-success > td {
+ background-color: #fbf9f7;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-success th,
+.table-success td,
+.table-success thead th,
+.table-success tbody + tbody {
+ border-color: #f7f5f0;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.table-hover .table-success:hover {
+ background-color: #f3ece6;
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-hover .table-success:hover > td,
+.table-hover .table-success:hover > th {
+ background-color: #f3ece6;
+}
+
+/* line 7, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-info,
+.table-info > th,
+.table-info > td {
+ background-color: #cbcbcb;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-info th,
+.table-info td,
+.table-info thead th,
+.table-info tbody + tbody {
+ border-color: #9f9f9f;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.table-hover .table-info:hover {
+ background-color: #bebebe;
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-hover .table-info:hover > td,
+.table-hover .table-info:hover > th {
+ background-color: #bebebe;
+}
+
+/* line 7, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-warning,
+.table-warning > th,
+.table-warning > td {
+ background-color: #cbcbcb;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-warning th,
+.table-warning td,
+.table-warning thead th,
+.table-warning tbody + tbody {
+ border-color: #9f9f9f;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.table-hover .table-warning:hover {
+ background-color: #bebebe;
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-hover .table-warning:hover > td,
+.table-hover .table-warning:hover > th {
+ background-color: #bebebe;
+}
+
+/* line 7, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-danger,
+.table-danger > th,
+.table-danger > td {
+ background-color: #f8ccbf;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-danger th,
+.table-danger td,
+.table-danger thead th,
+.table-danger tbody + tbody {
+ border-color: #f1a187;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.table-hover .table-danger:hover {
+ background-color: #f5baa8;
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-hover .table-danger:hover > td,
+.table-hover .table-danger:hover > th {
+ background-color: #f5baa8;
+}
+
+/* line 7, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-light,
+.table-light > th,
+.table-light > td {
+ background-color: #fdfdfd;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-light th,
+.table-light td,
+.table-light thead th,
+.table-light tbody + tbody {
+ border-color: #fbfbfb;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.table-hover .table-light:hover {
+ background-color: #f0f0f0;
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-hover .table-light:hover > td,
+.table-hover .table-light:hover > th {
+ background-color: #f0f0f0;
+}
+
+/* line 7, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-dark,
+.table-dark > th,
+.table-dark > td {
+ background-color: #c6c8ca;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-dark th,
+.table-dark td,
+.table-dark thead th,
+.table-dark tbody + tbody {
+ border-color: #95999c;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.table-hover .table-dark:hover {
+ background-color: #b9bbbe;
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-hover .table-dark:hover > td,
+.table-hover .table-dark:hover > th {
+ background-color: #b9bbbe;
+}
+
+/* line 7, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-active,
+.table-active > th,
+.table-active > td {
+ background-color: rgba(0, 0, 0, 0.075);
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.table-hover .table-active:hover {
+ background-color: rgba(0, 0, 0, 0.075);
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_table-row.scss */
+.table-hover .table-active:hover > td,
+.table-hover .table-active:hover > th {
+ background-color: rgba(0, 0, 0, 0.075);
+}
+
+/* line 114, node_modules/bootstrap/scss/_tables.scss */
+.table .thead-dark th {
+ color: #ffffff;
+ background-color: #343a40;
+ border-color: #454d55;
+}
+
+/* line 122, node_modules/bootstrap/scss/_tables.scss */
+.table .thead-light th {
+ color: #495057;
+ background-color: #eaebea;
+ border-color: #dee2e6;
+}
+
+/* line 130, node_modules/bootstrap/scss/_tables.scss */
+.table-dark {
+ color: #ffffff;
+ background-color: #343a40;
+}
+
+/* line 134, node_modules/bootstrap/scss/_tables.scss */
+.table-dark th,
+.table-dark td,
+.table-dark thead th {
+ border-color: #454d55;
+}
+
+/* line 140, node_modules/bootstrap/scss/_tables.scss */
+.table-dark.table-bordered {
+ border: 0;
+}
+
+/* line 145, node_modules/bootstrap/scss/_tables.scss */
+.table-dark.table-striped tbody tr:nth-of-type(odd) {
+ background-color: rgba(255, 255, 255, 0.05);
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.table-dark.table-hover tbody tr:hover {
+ color: #ffffff;
+ background-color: rgba(255, 255, 255, 0.075);
+}
+
+@media (max-width: 575.98px) {
+ /* line 171, node_modules/bootstrap/scss/_tables.scss */
+ .table-responsive-sm {
+ display: block;
+ width: 100%;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+ /* line 179, node_modules/bootstrap/scss/_tables.scss */
+ .table-responsive-sm > .table-bordered {
+ border: 0;
+ }
+}
+
+@media (max-width: 767.98px) {
+ /* line 171, node_modules/bootstrap/scss/_tables.scss */
+ .table-responsive-md {
+ display: block;
+ width: 100%;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+ /* line 179, node_modules/bootstrap/scss/_tables.scss */
+ .table-responsive-md > .table-bordered {
+ border: 0;
+ }
+}
+
+@media (max-width: 1023.98px) {
+ /* line 171, node_modules/bootstrap/scss/_tables.scss */
+ .table-responsive-lg {
+ display: block;
+ width: 100%;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+ /* line 179, node_modules/bootstrap/scss/_tables.scss */
+ .table-responsive-lg > .table-bordered {
+ border: 0;
+ }
+}
+
+@media (max-width: 1279.98px) {
+ /* line 171, node_modules/bootstrap/scss/_tables.scss */
+ .table-responsive-xl {
+ display: block;
+ width: 100%;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+ /* line 179, node_modules/bootstrap/scss/_tables.scss */
+ .table-responsive-xl > .table-bordered {
+ border: 0;
+ }
+}
+
+@media (max-width: 1439.98px) {
+ /* line 171, node_modules/bootstrap/scss/_tables.scss */
+ .table-responsive-xxl {
+ display: block;
+ width: 100%;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+ /* line 179, node_modules/bootstrap/scss/_tables.scss */
+ .table-responsive-xxl > .table-bordered {
+ border: 0;
+ }
+}
+
+/* line 171, node_modules/bootstrap/scss/_tables.scss */
+.table-responsive {
+ display: block;
+ width: 100%;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+}
+
+/* line 179, node_modules/bootstrap/scss/_tables.scss */
+.table-responsive > .table-bordered {
+ border: 0;
+}
+
+/* line 7, node_modules/bootstrap/scss/_forms.scss */
+.form-control {
+ display: block;
+ width: 100%;
+ height: calc(1.5em + 0.75rem + 2px);
+ padding: 0.375rem 0.75rem;
+ font-size: 1rem;
+ font-weight: 300;
+ line-height: 1.5;
+ color: #495057;
+ background-color: #ffffff;
+ background-clip: padding-box;
+ border: 1px solid #ced4da;
+ border-radius: 0.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) {
+ /* line 7, node_modules/bootstrap/scss/_forms.scss */
+ .form-control {
+ -webkit-transition: none;
+ transition: none;
+ }
+}
+
+/* line 28, node_modules/bootstrap/scss/_forms.scss */
+.form-control::-ms-expand {
+ background-color: transparent;
+ border: 0;
+}
+
+/* line 34, node_modules/bootstrap/scss/_forms.scss */
+.form-control:-moz-focusring {
+ color: transparent;
+ text-shadow: 0 0 0 #495057;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_forms.scss */
+.form-control:focus {
+ color: #495057;
+ background-color: #ffffff;
+ border-color: #858785;
+ outline: 0;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+ box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+}
+
+/* line 43, node_modules/bootstrap/scss/_forms.scss */
+.form-control::-webkit-input-placeholder {
+ color: #6c757d;
+ opacity: 1;
+}
+.form-control::-moz-placeholder {
+ color: #6c757d;
+ opacity: 1;
+}
+.form-control:-ms-input-placeholder {
+ color: #6c757d;
+ opacity: 1;
+}
+.form-control::-ms-input-placeholder {
+ color: #6c757d;
+ opacity: 1;
+}
+.form-control::placeholder {
+ color: #6c757d;
+ opacity: 1;
+}
+
+/* line 54, node_modules/bootstrap/scss/_forms.scss */
+.form-control:disabled, .form-control[readonly] {
+ background-color: #eaebea;
+ opacity: 1;
+}
+
+/* line 66, node_modules/bootstrap/scss/_forms.scss */
+input[type="date"].form-control,
+input[type="time"].form-control,
+input[type="datetime-local"].form-control,
+input[type="month"].form-control {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+}
+
+/* line 72, node_modules/bootstrap/scss/_forms.scss */
+select.form-control:focus::-ms-value {
+ color: #495057;
+ background-color: #ffffff;
+}
+
+/* line 84, node_modules/bootstrap/scss/_forms.scss */
+.form-control-file,
+.form-control-range {
+ display: block;
+ width: 100%;
+}
+
+/* line 97, node_modules/bootstrap/scss/_forms.scss */
+.col-form-label {
+ padding-top: calc(0.375rem + 1px);
+ padding-bottom: calc(0.375rem + 1px);
+ margin-bottom: 0;
+ font-size: inherit;
+ line-height: 1.5;
+}
+
+/* line 105, node_modules/bootstrap/scss/_forms.scss */
+.col-form-label-lg {
+ padding-top: calc(0.5rem + 1px);
+ padding-bottom: calc(0.5rem + 1px);
+ font-size: 1.25rem;
+ line-height: 1.5;
+}
+
+/* line 112, node_modules/bootstrap/scss/_forms.scss */
+.col-form-label-sm {
+ padding-top: calc(0.25rem + 1px);
+ padding-bottom: calc(0.25rem + 1px);
+ font-size: 0.875rem;
+ line-height: 1.5;
+}
+
+/* line 125, node_modules/bootstrap/scss/_forms.scss */
+.form-control-plaintext {
+ display: block;
+ width: 100%;
+ padding: 0.375rem 0;
+ margin-bottom: 0;
+ font-size: 1rem;
+ line-height: 1.5;
+ color: #464746;
+ background-color: transparent;
+ border: solid transparent;
+ border-width: 1px 0;
+}
+
+/* line 137, node_modules/bootstrap/scss/_forms.scss */
+.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg {
+ padding-right: 0;
+ padding-left: 0;
+}
+
+/* line 152, node_modules/bootstrap/scss/_forms.scss */
+.form-control-sm {
+ height: calc(1.5em + 0.5rem + 2px);
+ padding: 0.25rem 0.5rem;
+ font-size: 0.875rem;
+ line-height: 1.5;
+ border-radius: 0.2rem;
+}
+
+/* line 160, node_modules/bootstrap/scss/_forms.scss */
+.form-control-lg {
+ height: calc(1.5em + 1rem + 2px);
+ padding: 0.5rem 1rem;
+ font-size: 1.25rem;
+ line-height: 1.5;
+ border-radius: 0.3rem;
+}
+
+/* line 170, node_modules/bootstrap/scss/_forms.scss */
+select.form-control[size], select.form-control[multiple] {
+ height: auto;
+}
+
+/* line 176, node_modules/bootstrap/scss/_forms.scss */
+textarea.form-control {
+ height: auto;
+}
+
+/* line 185, node_modules/bootstrap/scss/_forms.scss */
+.form-group {
+ margin-bottom: 1rem;
+}
+
+/* line 189, node_modules/bootstrap/scss/_forms.scss */
+.form-text {
+ display: block;
+ margin-top: 0.25rem;
+}
+
+/* line 199, node_modules/bootstrap/scss/_forms.scss */
+.form-row {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+ margin-right: -5px;
+ margin-left: -5px;
+}
+
+/* line 205, node_modules/bootstrap/scss/_forms.scss */
+.form-row > .col,
+.form-row > [class*="col-"] {
+ padding-right: 5px;
+ padding-left: 5px;
+}
+
+/* line 217, node_modules/bootstrap/scss/_forms.scss */
+.form-check {
+ position: relative;
+ display: block;
+ padding-left: 1.25rem;
+}
+
+/* line 223, node_modules/bootstrap/scss/_forms.scss */
+.form-check-input {
+ position: absolute;
+ margin-top: 0.3rem;
+ margin-left: -1.25rem;
+}
+
+/* line 229, node_modules/bootstrap/scss/_forms.scss */
+.form-check-input[disabled] ~ .form-check-label,
+.form-check-input:disabled ~ .form-check-label {
+ color: #6c757d;
+}
+
+/* line 235, node_modules/bootstrap/scss/_forms.scss */
+.form-check-label {
+ margin-bottom: 0;
+}
+
+/* line 239, node_modules/bootstrap/scss/_forms.scss */
+.form-check-inline {
+ display: -webkit-inline-box;
+ display: -ms-inline-flexbox;
+ display: inline-flex;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ padding-left: 0;
+ margin-right: 0.75rem;
+}
+
+/* line 246, node_modules/bootstrap/scss/_forms.scss */
+.form-check-inline .form-check-input {
+ position: static;
+ margin-top: 0;
+ margin-right: 0.3125rem;
+ margin-left: 0;
+}
+
+/* line 45, node_modules/bootstrap/scss/mixins/_forms.scss */
+.valid-feedback {
+ display: none;
+ width: 100%;
+ margin-top: 0.25rem;
+ font-size: 80%;
+ color: #f0ebe3;
+}
+
+/* line 53, node_modules/bootstrap/scss/mixins/_forms.scss */
+.valid-tooltip {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: 5;
+ display: none;
+ max-width: 100%;
+ padding: 0.25rem 0.5rem;
+ margin-top: .1rem;
+ font-size: 0.875rem;
+ line-height: 1.5;
+ color: #464746;
+ background-color: rgba(240, 235, 227, 0.9);
+ border-radius: 0.25rem;
+}
+
+/* line 70, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated :valid ~ .valid-feedback,
+.was-validated :valid ~ .valid-tooltip,
+.is-valid ~ .valid-feedback,
+.is-valid ~ .valid-tooltip {
+ display: block;
+}
+
+/* line 33, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .form-control:valid, .form-control.is-valid {
+ border-color: #f0ebe3;
+ padding-right: calc(1.5em + 0.75rem);
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23f0ebe3' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
+ background-repeat: no-repeat;
+ background-position: right calc(0.375em + 0.1875rem) center;
+ background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
+}
+
+/* line 88, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .form-control:valid:focus, .form-control.is-valid:focus {
+ border-color: #f0ebe3;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.25);
+ box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.25);
+}
+
+/* line 33, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated textarea.form-control:valid, textarea.form-control.is-valid {
+ padding-right: calc(1.5em + 0.75rem);
+ background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);
+}
+
+/* line 33, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-select:valid, .custom-select.is-valid {
+ border-color: #f0ebe3;
+ padding-right: calc(0.75em + 2.3125rem);
+ 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='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23f0ebe3' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #ffffff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
+}
+
+/* line 114, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-select:valid:focus, .custom-select.is-valid:focus {
+ border-color: #f0ebe3;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.25);
+ box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.25);
+}
+
+/* line 123, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label {
+ color: #f0ebe3;
+}
+
+/* line 127, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .form-check-input:valid ~ .valid-feedback,
+.was-validated .form-check-input:valid ~ .valid-tooltip, .form-check-input.is-valid ~ .valid-feedback,
+.form-check-input.is-valid ~ .valid-tooltip {
+ display: block;
+}
+
+/* line 136, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-control-input:valid ~ .custom-control-label, .custom-control-input.is-valid ~ .custom-control-label {
+ color: #f0ebe3;
+}
+
+/* line 139, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-control-input:valid ~ .custom-control-label::before, .custom-control-input.is-valid ~ .custom-control-label::before {
+ border-color: #f0ebe3;
+}
+
+/* line 145, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before, .custom-control-input.is-valid:checked ~ .custom-control-label::before {
+ border-color: white;
+ background-color: white;
+}
+
+/* line 152, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before, .custom-control-input.is-valid:focus ~ .custom-control-label::before {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.25);
+ box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.25);
+}
+
+/* line 156, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before {
+ border-color: #f0ebe3;
+}
+
+/* line 166, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-file-input:valid ~ .custom-file-label, .custom-file-input.is-valid ~ .custom-file-label {
+ border-color: #f0ebe3;
+}
+
+/* line 171, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-file-input:valid:focus ~ .custom-file-label, .custom-file-input.is-valid:focus ~ .custom-file-label {
+ border-color: #f0ebe3;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.25);
+ box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.25);
+}
+
+/* line 45, node_modules/bootstrap/scss/mixins/_forms.scss */
+.invalid-feedback {
+ display: none;
+ width: 100%;
+ margin-top: 0.25rem;
+ font-size: 80%;
+ color: #e54a19;
+}
+
+/* line 53, node_modules/bootstrap/scss/mixins/_forms.scss */
+.invalid-tooltip {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: 5;
+ display: none;
+ max-width: 100%;
+ padding: 0.25rem 0.5rem;
+ margin-top: .1rem;
+ font-size: 0.875rem;
+ line-height: 1.5;
+ color: #ffffff;
+ background-color: rgba(229, 74, 25, 0.9);
+ border-radius: 0.25rem;
+}
+
+/* line 70, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated :invalid ~ .invalid-feedback,
+.was-validated :invalid ~ .invalid-tooltip,
+.is-invalid ~ .invalid-feedback,
+.is-invalid ~ .invalid-tooltip {
+ display: block;
+}
+
+/* line 33, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .form-control:invalid, .form-control.is-invalid {
+ border-color: #e54a19;
+ padding-right: calc(1.5em + 0.75rem);
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23e54a19' 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='%23e54a19' stroke='none'/%3e%3c/svg%3e");
+ background-repeat: no-repeat;
+ background-position: right calc(0.375em + 0.1875rem) center;
+ background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
+}
+
+/* line 88, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus {
+ border-color: #e54a19;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.25);
+ box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.25);
+}
+
+/* line 33, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid {
+ padding-right: calc(1.5em + 0.75rem);
+ background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);
+}
+
+/* line 33, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-select:invalid, .custom-select.is-invalid {
+ border-color: #e54a19;
+ padding-right: calc(0.75em + 2.3125rem);
+ 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='%23e54a19' 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='%23e54a19' stroke='none'/%3e%3c/svg%3e") #ffffff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
+}
+
+/* line 114, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-select:invalid:focus, .custom-select.is-invalid:focus {
+ border-color: #e54a19;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.25);
+ box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.25);
+}
+
+/* line 123, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label {
+ color: #e54a19;
+}
+
+/* line 127, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .form-check-input:invalid ~ .invalid-feedback,
+.was-validated .form-check-input:invalid ~ .invalid-tooltip, .form-check-input.is-invalid ~ .invalid-feedback,
+.form-check-input.is-invalid ~ .invalid-tooltip {
+ display: block;
+}
+
+/* line 136, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-control-input:invalid ~ .custom-control-label, .custom-control-input.is-invalid ~ .custom-control-label {
+ color: #e54a19;
+}
+
+/* line 139, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-control-input:invalid ~ .custom-control-label::before, .custom-control-input.is-invalid ~ .custom-control-label::before {
+ border-color: #e54a19;
+}
+
+/* line 145, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before, .custom-control-input.is-invalid:checked ~ .custom-control-label::before {
+ border-color: #eb6e46;
+ background-color: #eb6e46;
+}
+
+/* line 152, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before, .custom-control-input.is-invalid:focus ~ .custom-control-label::before {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.25);
+ box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.25);
+}
+
+/* line 156, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before {
+ border-color: #e54a19;
+}
+
+/* line 166, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label {
+ border-color: #e54a19;
+}
+
+/* line 171, node_modules/bootstrap/scss/mixins/_forms.scss */
+.was-validated .custom-file-input:invalid:focus ~ .custom-file-label, .custom-file-input.is-invalid:focus ~ .custom-file-label {
+ border-color: #e54a19;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.25);
+ box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.25);
+}
+
+/* line 275, node_modules/bootstrap/scss/_forms.scss */
+.form-inline {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-orient: horizontal;
+ -webkit-box-direction: normal;
+ -ms-flex-flow: row wrap;
+ flex-flow: row wrap;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+}
+
+/* line 283, node_modules/bootstrap/scss/_forms.scss */
+.form-inline .form-check {
+ width: 100%;
+}
+
+@media (min-width: 576px) {
+ /* line 289, node_modules/bootstrap/scss/_forms.scss */
+ .form-inline label {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-box-pack: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+ margin-bottom: 0;
+ }
+ /* line 297, node_modules/bootstrap/scss/_forms.scss */
+ .form-inline .form-group {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-flex: 0;
+ -ms-flex: 0 0 auto;
+ flex: 0 0 auto;
+ -webkit-box-orient: horizontal;
+ -webkit-box-direction: normal;
+ -ms-flex-flow: row wrap;
+ flex-flow: row wrap;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ margin-bottom: 0;
+ }
+ /* line 306, node_modules/bootstrap/scss/_forms.scss */
+ .form-inline .form-control {
+ display: inline-block;
+ width: auto;
+ vertical-align: middle;
+ }
+ /* line 313, node_modules/bootstrap/scss/_forms.scss */
+ .form-inline .form-control-plaintext {
+ display: inline-block;
+ }
+ /* line 317, node_modules/bootstrap/scss/_forms.scss */
+ .form-inline .input-group,
+ .form-inline .custom-select {
+ width: auto;
+ }
+ /* line 324, node_modules/bootstrap/scss/_forms.scss */
+ .form-inline .form-check {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-box-pack: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+ width: auto;
+ padding-left: 0;
+ }
+ /* line 331, node_modules/bootstrap/scss/_forms.scss */
+ .form-inline .form-check-input {
+ position: relative;
+ -ms-flex-negative: 0;
+ flex-shrink: 0;
+ margin-top: 0;
+ margin-right: 0.25rem;
+ margin-left: 0;
+ }
+ /* line 339, node_modules/bootstrap/scss/_forms.scss */
+ .form-inline .custom-control {
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-box-pack: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+ }
+ /* line 343, node_modules/bootstrap/scss/_forms.scss */
+ .form-inline .custom-control-label {
+ margin-bottom: 0;
+ }
+}
+
+/* line 7, node_modules/bootstrap/scss/_buttons.scss */
+.btn {
+ display: inline-block;
+ font-weight: 400;
+ color: #464746;
+ text-align: center;
+ vertical-align: middle;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ background-color: transparent;
+ border: 1px solid transparent;
+ padding: 0.375rem 0.75rem;
+ font-size: 1rem;
+ line-height: 1.5;
+ border-radius: 0.25rem;
+ -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, 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) {
+ /* line 7, node_modules/bootstrap/scss/_buttons.scss */
+ .btn {
+ -webkit-transition: none;
+ transition: none;
+ }
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn:hover {
+ color: #464746;
+ text-decoration: none;
+}
+
+/* line 27, node_modules/bootstrap/scss/_buttons.scss */
+.btn:focus, .btn.focus {
+ outline: 0;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+ box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+}
+
+/* line 34, node_modules/bootstrap/scss/_buttons.scss */
+.btn.disabled, .btn:disabled {
+ opacity: 0.65;
+}
+
+/* line 40, node_modules/bootstrap/scss/_buttons.scss */
+.btn:not(:disabled):not(.disabled) {
+ cursor: pointer;
+}
+
+/* line 55, node_modules/bootstrap/scss/_buttons.scss */
+a.btn.disabled,
+fieldset:disabled a.btn {
+ pointer-events: none;
+}
+
+/* line 66, node_modules/bootstrap/scss/_buttons.scss */
+.btn-primary {
+ color: #ffffff;
+ background-color: #464746;
+ border-color: #464746;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-primary:hover {
+ color: #ffffff;
+ background-color: #333433;
+ border-color: #2d2d2d;
+}
+
+/* line 18, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-primary:focus, .btn-primary.focus {
+ color: #ffffff;
+ background-color: #333433;
+ border-color: #2d2d2d;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(98, 99, 98, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(98, 99, 98, 0.5);
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-primary.disabled, .btn-primary:disabled {
+ color: #ffffff;
+ background-color: #464746;
+ border-color: #464746;
+}
+
+/* line 43, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active,
+.show > .btn-primary.dropdown-toggle {
+ color: #ffffff;
+ background-color: #2d2d2d;
+ border-color: #262726;
+}
+
+/* line 53, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-primary:not(:disabled):not(.disabled):active:focus, .btn-primary:not(:disabled):not(.disabled).active:focus,
+.show > .btn-primary.dropdown-toggle:focus {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(98, 99, 98, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(98, 99, 98, 0.5);
+}
+
+/* line 66, node_modules/bootstrap/scss/_buttons.scss */
+.btn-secondary {
+ color: #464746;
+ background-color: #f0ebe3;
+ border-color: #f0ebe3;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-secondary:hover {
+ color: #464746;
+ background-color: #e3d9ca;
+ border-color: #ded3c2;
+}
+
+/* line 18, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-secondary:focus, .btn-secondary.focus {
+ color: #464746;
+ background-color: #e3d9ca;
+ border-color: #ded3c2;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(215, 210, 203, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(215, 210, 203, 0.5);
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-secondary.disabled, .btn-secondary:disabled {
+ color: #464746;
+ background-color: #f0ebe3;
+ border-color: #f0ebe3;
+}
+
+/* line 43, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active,
+.show > .btn-secondary.dropdown-toggle {
+ color: #464746;
+ background-color: #ded3c2;
+ border-color: #dacdb9;
+}
+
+/* line 53, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus,
+.show > .btn-secondary.dropdown-toggle:focus {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(215, 210, 203, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(215, 210, 203, 0.5);
+}
+
+/* line 66, node_modules/bootstrap/scss/_buttons.scss */
+.btn-success {
+ color: #464746;
+ background-color: #f0ebe3;
+ border-color: #f0ebe3;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-success:hover {
+ color: #464746;
+ background-color: #e3d9ca;
+ border-color: #ded3c2;
+}
+
+/* line 18, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-success:focus, .btn-success.focus {
+ color: #464746;
+ background-color: #e3d9ca;
+ border-color: #ded3c2;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(215, 210, 203, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(215, 210, 203, 0.5);
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-success.disabled, .btn-success:disabled {
+ color: #464746;
+ background-color: #f0ebe3;
+ border-color: #f0ebe3;
+}
+
+/* line 43, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-success:not(:disabled):not(.disabled):active, .btn-success:not(:disabled):not(.disabled).active,
+.show > .btn-success.dropdown-toggle {
+ color: #464746;
+ background-color: #ded3c2;
+ border-color: #dacdb9;
+}
+
+/* line 53, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-success:not(:disabled):not(.disabled):active:focus, .btn-success:not(:disabled):not(.disabled).active:focus,
+.show > .btn-success.dropdown-toggle:focus {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(215, 210, 203, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(215, 210, 203, 0.5);
+}
+
+/* line 66, node_modules/bootstrap/scss/_buttons.scss */
+.btn-info {
+ color: #ffffff;
+ background-color: #464746;
+ border-color: #464746;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-info:hover {
+ color: #ffffff;
+ background-color: #333433;
+ border-color: #2d2d2d;
+}
+
+/* line 18, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-info:focus, .btn-info.focus {
+ color: #ffffff;
+ background-color: #333433;
+ border-color: #2d2d2d;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(98, 99, 98, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(98, 99, 98, 0.5);
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-info.disabled, .btn-info:disabled {
+ color: #ffffff;
+ background-color: #464746;
+ border-color: #464746;
+}
+
+/* line 43, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-info:not(:disabled):not(.disabled):active, .btn-info:not(:disabled):not(.disabled).active,
+.show > .btn-info.dropdown-toggle {
+ color: #ffffff;
+ background-color: #2d2d2d;
+ border-color: #262726;
+}
+
+/* line 53, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-info:not(:disabled):not(.disabled):active:focus, .btn-info:not(:disabled):not(.disabled).active:focus,
+.show > .btn-info.dropdown-toggle:focus {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(98, 99, 98, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(98, 99, 98, 0.5);
+}
+
+/* line 66, node_modules/bootstrap/scss/_buttons.scss */
+.btn-warning {
+ color: #ffffff;
+ background-color: #464746;
+ border-color: #464746;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-warning:hover {
+ color: #ffffff;
+ background-color: #333433;
+ border-color: #2d2d2d;
+}
+
+/* line 18, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-warning:focus, .btn-warning.focus {
+ color: #ffffff;
+ background-color: #333433;
+ border-color: #2d2d2d;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(98, 99, 98, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(98, 99, 98, 0.5);
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-warning.disabled, .btn-warning:disabled {
+ color: #ffffff;
+ background-color: #464746;
+ border-color: #464746;
+}
+
+/* line 43, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-warning:not(:disabled):not(.disabled):active, .btn-warning:not(:disabled):not(.disabled).active,
+.show > .btn-warning.dropdown-toggle {
+ color: #ffffff;
+ background-color: #2d2d2d;
+ border-color: #262726;
+}
+
+/* line 53, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-warning:not(:disabled):not(.disabled):active:focus, .btn-warning:not(:disabled):not(.disabled).active:focus,
+.show > .btn-warning.dropdown-toggle:focus {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(98, 99, 98, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(98, 99, 98, 0.5);
+}
+
+/* line 66, node_modules/bootstrap/scss/_buttons.scss */
+.btn-danger {
+ color: #ffffff;
+ background-color: #e54a19;
+ border-color: #e54a19;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-danger:hover {
+ color: #ffffff;
+ background-color: #c33f15;
+ border-color: #b73b14;
+}
+
+/* line 18, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-danger:focus, .btn-danger.focus {
+ color: #ffffff;
+ background-color: #c33f15;
+ border-color: #b73b14;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(233, 101, 60, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(233, 101, 60, 0.5);
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-danger.disabled, .btn-danger:disabled {
+ color: #ffffff;
+ background-color: #e54a19;
+ border-color: #e54a19;
+}
+
+/* line 43, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-danger:not(:disabled):not(.disabled):active, .btn-danger:not(:disabled):not(.disabled).active,
+.show > .btn-danger.dropdown-toggle {
+ color: #ffffff;
+ background-color: #b73b14;
+ border-color: #ac3713;
+}
+
+/* line 53, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-danger:not(:disabled):not(.disabled):active:focus, .btn-danger:not(:disabled):not(.disabled).active:focus,
+.show > .btn-danger.dropdown-toggle:focus {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(233, 101, 60, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(233, 101, 60, 0.5);
+}
+
+/* line 66, node_modules/bootstrap/scss/_buttons.scss */
+.btn-light {
+ color: #464746;
+ background-color: #f7f7f7;
+ border-color: #f7f7f7;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-light:hover {
+ color: #464746;
+ background-color: #e4e4e4;
+ border-color: #dedede;
+}
+
+/* line 18, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-light:focus, .btn-light.focus {
+ color: #464746;
+ background-color: #e4e4e4;
+ border-color: #dedede;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(220, 221, 220, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(220, 221, 220, 0.5);
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-light.disabled, .btn-light:disabled {
+ color: #464746;
+ background-color: #f7f7f7;
+ border-color: #f7f7f7;
+}
+
+/* line 43, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-light:not(:disabled):not(.disabled):active, .btn-light:not(:disabled):not(.disabled).active,
+.show > .btn-light.dropdown-toggle {
+ color: #464746;
+ background-color: #dedede;
+ border-color: #d7d7d7;
+}
+
+/* line 53, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-light:not(:disabled):not(.disabled):active:focus, .btn-light:not(:disabled):not(.disabled).active:focus,
+.show > .btn-light.dropdown-toggle:focus {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(220, 221, 220, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(220, 221, 220, 0.5);
+}
+
+/* line 66, node_modules/bootstrap/scss/_buttons.scss */
+.btn-dark {
+ color: #ffffff;
+ background-color: #343a40;
+ border-color: #343a40;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-dark:hover {
+ color: #ffffff;
+ background-color: #23272b;
+ border-color: #1d2124;
+}
+
+/* line 18, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-dark:focus, .btn-dark.focus {
+ color: #ffffff;
+ background-color: #23272b;
+ border-color: #1d2124;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);
+}
+
+/* line 32, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-dark.disabled, .btn-dark:disabled {
+ color: #ffffff;
+ background-color: #343a40;
+ border-color: #343a40;
+}
+
+/* line 43, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active,
+.show > .btn-dark.dropdown-toggle {
+ color: #ffffff;
+ background-color: #1d2124;
+ border-color: #171a1d;
+}
+
+/* line 53, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-dark:not(:disabled):not(.disabled):active:focus, .btn-dark:not(:disabled):not(.disabled).active:focus,
+.show > .btn-dark.dropdown-toggle:focus {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);
+}
+
+/* line 72, node_modules/bootstrap/scss/_buttons.scss */
+.btn-outline-primary {
+ color: #464746;
+ border-color: #464746;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-outline-primary:hover {
+ color: #ffffff;
+ background-color: #464746;
+ border-color: #464746;
+}
+
+/* line 74, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-primary:focus, .btn-outline-primary.focus {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+}
+
+/* line 79, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-primary.disabled, .btn-outline-primary:disabled {
+ color: #464746;
+ background-color: transparent;
+}
+
+/* line 85, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active,
+.show > .btn-outline-primary.dropdown-toggle {
+ color: #ffffff;
+ background-color: #464746;
+ border-color: #464746;
+}
+
+/* line 92, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus,
+.show > .btn-outline-primary.dropdown-toggle:focus {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+}
+
+/* line 72, node_modules/bootstrap/scss/_buttons.scss */
+.btn-outline-secondary {
+ color: #f0ebe3;
+ border-color: #f0ebe3;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-outline-secondary:hover {
+ color: #464746;
+ background-color: #f0ebe3;
+ border-color: #f0ebe3;
+}
+
+/* line 74, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-secondary:focus, .btn-outline-secondary.focus {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.5);
+}
+
+/* line 79, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-secondary.disabled, .btn-outline-secondary:disabled {
+ color: #f0ebe3;
+ background-color: transparent;
+}
+
+/* line 85, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active,
+.show > .btn-outline-secondary.dropdown-toggle {
+ color: #464746;
+ background-color: #f0ebe3;
+ border-color: #f0ebe3;
+}
+
+/* line 92, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus,
+.show > .btn-outline-secondary.dropdown-toggle:focus {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.5);
+}
+
+/* line 72, node_modules/bootstrap/scss/_buttons.scss */
+.btn-outline-success {
+ color: #f0ebe3;
+ border-color: #f0ebe3;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-outline-success:hover {
+ color: #464746;
+ background-color: #f0ebe3;
+ border-color: #f0ebe3;
+}
+
+/* line 74, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-success:focus, .btn-outline-success.focus {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.5);
+}
+
+/* line 79, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-success.disabled, .btn-outline-success:disabled {
+ color: #f0ebe3;
+ background-color: transparent;
+}
+
+/* line 85, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-success:not(:disabled):not(.disabled):active, .btn-outline-success:not(:disabled):not(.disabled).active,
+.show > .btn-outline-success.dropdown-toggle {
+ color: #464746;
+ background-color: #f0ebe3;
+ border-color: #f0ebe3;
+}
+
+/* line 92, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-success:not(:disabled):not(.disabled):active:focus, .btn-outline-success:not(:disabled):not(.disabled).active:focus,
+.show > .btn-outline-success.dropdown-toggle:focus {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.5);
+}
+
+/* line 72, node_modules/bootstrap/scss/_buttons.scss */
+.btn-outline-info {
+ color: #464746;
+ border-color: #464746;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-outline-info:hover {
+ color: #ffffff;
+ background-color: #464746;
+ border-color: #464746;
+}
+
+/* line 74, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-info:focus, .btn-outline-info.focus {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+}
+
+/* line 79, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-info.disabled, .btn-outline-info:disabled {
+ color: #464746;
+ background-color: transparent;
+}
+
+/* line 85, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active,
+.show > .btn-outline-info.dropdown-toggle {
+ color: #ffffff;
+ background-color: #464746;
+ border-color: #464746;
+}
+
+/* line 92, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus,
+.show > .btn-outline-info.dropdown-toggle:focus {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+}
+
+/* line 72, node_modules/bootstrap/scss/_buttons.scss */
+.btn-outline-warning {
+ color: #464746;
+ border-color: #464746;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-outline-warning:hover {
+ color: #ffffff;
+ background-color: #464746;
+ border-color: #464746;
+}
+
+/* line 74, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-warning:focus, .btn-outline-warning.focus {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+}
+
+/* line 79, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-warning.disabled, .btn-outline-warning:disabled {
+ color: #464746;
+ background-color: transparent;
+}
+
+/* line 85, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-warning:not(:disabled):not(.disabled):active, .btn-outline-warning:not(:disabled):not(.disabled).active,
+.show > .btn-outline-warning.dropdown-toggle {
+ color: #ffffff;
+ background-color: #464746;
+ border-color: #464746;
+}
+
+/* line 92, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-warning:not(:disabled):not(.disabled):active:focus, .btn-outline-warning:not(:disabled):not(.disabled).active:focus,
+.show > .btn-outline-warning.dropdown-toggle:focus {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+}
+
+/* line 72, node_modules/bootstrap/scss/_buttons.scss */
+.btn-outline-danger {
+ color: #e54a19;
+ border-color: #e54a19;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-outline-danger:hover {
+ color: #ffffff;
+ background-color: #e54a19;
+ border-color: #e54a19;
+}
+
+/* line 74, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-danger:focus, .btn-outline-danger.focus {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.5);
+}
+
+/* line 79, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-danger.disabled, .btn-outline-danger:disabled {
+ color: #e54a19;
+ background-color: transparent;
+}
+
+/* line 85, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-danger:not(:disabled):not(.disabled):active, .btn-outline-danger:not(:disabled):not(.disabled).active,
+.show > .btn-outline-danger.dropdown-toggle {
+ color: #ffffff;
+ background-color: #e54a19;
+ border-color: #e54a19;
+}
+
+/* line 92, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-danger:not(:disabled):not(.disabled):active:focus, .btn-outline-danger:not(:disabled):not(.disabled).active:focus,
+.show > .btn-outline-danger.dropdown-toggle:focus {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.5);
+}
+
+/* line 72, node_modules/bootstrap/scss/_buttons.scss */
+.btn-outline-light {
+ color: #f7f7f7;
+ border-color: #f7f7f7;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-outline-light:hover {
+ color: #464746;
+ background-color: #f7f7f7;
+ border-color: #f7f7f7;
+}
+
+/* line 74, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-light:focus, .btn-outline-light.focus {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(247, 247, 247, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(247, 247, 247, 0.5);
+}
+
+/* line 79, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-light.disabled, .btn-outline-light:disabled {
+ color: #f7f7f7;
+ background-color: transparent;
+}
+
+/* line 85, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-light:not(:disabled):not(.disabled):active, .btn-outline-light:not(:disabled):not(.disabled).active,
+.show > .btn-outline-light.dropdown-toggle {
+ color: #464746;
+ background-color: #f7f7f7;
+ border-color: #f7f7f7;
+}
+
+/* line 92, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-light:not(:disabled):not(.disabled):active:focus, .btn-outline-light:not(:disabled):not(.disabled).active:focus,
+.show > .btn-outline-light.dropdown-toggle:focus {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(247, 247, 247, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(247, 247, 247, 0.5);
+}
+
+/* line 72, node_modules/bootstrap/scss/_buttons.scss */
+.btn-outline-dark {
+ color: #343a40;
+ border-color: #343a40;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-outline-dark:hover {
+ color: #ffffff;
+ background-color: #343a40;
+ border-color: #343a40;
+}
+
+/* line 74, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-dark:focus, .btn-outline-dark.focus {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);
+}
+
+/* line 79, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-dark.disabled, .btn-outline-dark:disabled {
+ color: #343a40;
+ background-color: transparent;
+}
+
+/* line 85, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-dark:not(:disabled):not(.disabled):active, .btn-outline-dark:not(:disabled):not(.disabled).active,
+.show > .btn-outline-dark.dropdown-toggle {
+ color: #ffffff;
+ background-color: #343a40;
+ border-color: #343a40;
+}
+
+/* line 92, node_modules/bootstrap/scss/mixins/_buttons.scss */
+.btn-outline-dark:not(:disabled):not(.disabled):active:focus, .btn-outline-dark:not(:disabled):not(.disabled).active:focus,
+.show > .btn-outline-dark.dropdown-toggle:focus {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);
+}
+
+/* line 83, node_modules/bootstrap/scss/_buttons.scss */
+.btn-link {
+ font-weight: 400;
+ color: #464746;
+ text-decoration: none;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-link:hover {
+ color: #202020;
+ text-decoration: underline;
+}
+
+/* line 93, node_modules/bootstrap/scss/_buttons.scss */
+.btn-link:focus, .btn-link.focus {
+ text-decoration: underline;
+}
+
+/* line 98, node_modules/bootstrap/scss/_buttons.scss */
+.btn-link:disabled, .btn-link.disabled {
+ color: #6c757d;
+ pointer-events: none;
+}
+
+/* line 112, node_modules/bootstrap/scss/_buttons.scss */
+.btn-lg, .btn-group-lg > .btn {
+ padding: 0.5rem 1rem;
+ font-size: 1.25rem;
+ line-height: 1.5;
+ border-radius: 0.3rem;
+}
+
+/* line 116, node_modules/bootstrap/scss/_buttons.scss */
+.btn-sm, .btn-group-sm > .btn {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.875rem;
+ line-height: 1.5;
+ border-radius: 0.2rem;
+}
+
+/* line 125, node_modules/bootstrap/scss/_buttons.scss */
+.btn-block {
+ display: block;
+ width: 100%;
+}
+
+/* line 130, node_modules/bootstrap/scss/_buttons.scss */
+.btn-block + .btn-block {
+ margin-top: 0.5rem;
+}
+
+/* line 139, node_modules/bootstrap/scss/_buttons.scss */
+input[type="submit"].btn-block,
+input[type="reset"].btn-block,
+input[type="button"].btn-block {
+ width: 100%;
+}
+
+/* line 1, node_modules/bootstrap/scss/_transitions.scss */
+.fade {
+ -webkit-transition: opacity 0.15s linear;
+ transition: opacity 0.15s linear;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ /* line 1, node_modules/bootstrap/scss/_transitions.scss */
+ .fade {
+ -webkit-transition: none;
+ transition: none;
+ }
+}
+
+/* line 4, node_modules/bootstrap/scss/_transitions.scss */
+.fade:not(.show) {
+ opacity: 0;
+}
+
+/* line 10, node_modules/bootstrap/scss/_transitions.scss */
+.collapse:not(.show) {
+ display: none;
+}
+
+/* line 15, node_modules/bootstrap/scss/_transitions.scss */
+.collapsing {
+ position: relative;
+ height: 0;
+ overflow: hidden;
+ -webkit-transition: height 0.35s ease;
+ transition: height 0.35s ease;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ /* line 15, node_modules/bootstrap/scss/_transitions.scss */
+ .collapsing {
+ -webkit-transition: none;
+ transition: none;
+ }
+}
+
+/* line 2, node_modules/bootstrap/scss/_dropdown.scss */
+.dropup,
+.dropright,
+.dropdown,
+.dropleft {
+ position: relative;
+}
+
+/* line 9, node_modules/bootstrap/scss/_dropdown.scss */
+.dropdown-toggle {
+ white-space: nowrap;
+}
+
+/* line 30, node_modules/bootstrap/scss/mixins/_caret.scss */
+.dropdown-toggle::after {
+ display: inline-block;
+ margin-left: 0.255em;
+ vertical-align: 0.255em;
+ content: "";
+ border-top: 0.3em solid;
+ border-right: 0.3em solid transparent;
+ border-bottom: 0;
+ border-left: 0.3em solid transparent;
+}
+
+/* line 58, node_modules/bootstrap/scss/mixins/_caret.scss */
+.dropdown-toggle:empty::after {
+ margin-left: 0;
+}
+
+/* line 17, node_modules/bootstrap/scss/_dropdown.scss */
+.dropdown-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: 1000;
+ display: none;
+ float: left;
+ min-width: 10rem;
+ padding: 0.5rem 0;
+ margin: 0.125rem 0 0;
+ font-size: 1rem;
+ color: #464746;
+ text-align: left;
+ list-style: none;
+ background-color: #ffffff;
+ background-clip: padding-box;
+ border: 1px solid rgba(0, 0, 0, 0.15);
+ border-radius: 0.25rem;
+}
+
+/* line 42, node_modules/bootstrap/scss/_dropdown.scss */
+.dropdown-menu-left {
+ right: auto;
+ left: 0;
+}
+
+/* line 47, node_modules/bootstrap/scss/_dropdown.scss */
+.dropdown-menu-right {
+ right: 0;
+ left: auto;
+}
+
+@media (min-width: 576px) {
+ /* line 42, node_modules/bootstrap/scss/_dropdown.scss */
+ .dropdown-menu-sm-left {
+ right: auto;
+ left: 0;
+ }
+ /* line 47, node_modules/bootstrap/scss/_dropdown.scss */
+ .dropdown-menu-sm-right {
+ right: 0;
+ left: auto;
+ }
+}
+
+@media (min-width: 768px) {
+ /* line 42, node_modules/bootstrap/scss/_dropdown.scss */
+ .dropdown-menu-md-left {
+ right: auto;
+ left: 0;
+ }
+ /* line 47, node_modules/bootstrap/scss/_dropdown.scss */
+ .dropdown-menu-md-right {
+ right: 0;
+ left: auto;
+ }
+}
+
+@media (min-width: 1024px) {
+ /* line 42, node_modules/bootstrap/scss/_dropdown.scss */
+ .dropdown-menu-lg-left {
+ right: auto;
+ left: 0;
+ }
+ /* line 47, node_modules/bootstrap/scss/_dropdown.scss */
+ .dropdown-menu-lg-right {
+ right: 0;
+ left: auto;
+ }
+}
+
+@media (min-width: 1280px) {
+ /* line 42, node_modules/bootstrap/scss/_dropdown.scss */
+ .dropdown-menu-xl-left {
+ right: auto;
+ left: 0;
+ }
+ /* line 47, node_modules/bootstrap/scss/_dropdown.scss */
+ .dropdown-menu-xl-right {
+ right: 0;
+ left: auto;
+ }
+}
+
+@media (min-width: 1440px) {
+ /* line 42, node_modules/bootstrap/scss/_dropdown.scss */
+ .dropdown-menu-xxl-left {
+ right: auto;
+ left: 0;
+ }
+ /* line 47, node_modules/bootstrap/scss/_dropdown.scss */
+ .dropdown-menu-xxl-right {
+ right: 0;
+ left: auto;
+ }
+}
+
+/* line 57, node_modules/bootstrap/scss/_dropdown.scss */
+.dropup .dropdown-menu {
+ top: auto;
+ bottom: 100%;
+ margin-top: 0;
+ margin-bottom: 0.125rem;
+}
+
+/* line 30, node_modules/bootstrap/scss/mixins/_caret.scss */
+.dropup .dropdown-toggle::after {
+ display: inline-block;
+ margin-left: 0.255em;
+ vertical-align: 0.255em;
+ content: "";
+ border-top: 0;
+ border-right: 0.3em solid transparent;
+ border-bottom: 0.3em solid;
+ border-left: 0.3em solid transparent;
+}
+
+/* line 58, node_modules/bootstrap/scss/mixins/_caret.scss */
+.dropup .dropdown-toggle:empty::after {
+ margin-left: 0;
+}
+
+/* line 70, node_modules/bootstrap/scss/_dropdown.scss */
+.dropright .dropdown-menu {
+ top: 0;
+ right: auto;
+ left: 100%;
+ margin-top: 0;
+ margin-left: 0.125rem;
+}
+
+/* line 30, node_modules/bootstrap/scss/mixins/_caret.scss */
+.dropright .dropdown-toggle::after {
+ display: inline-block;
+ margin-left: 0.255em;
+ vertical-align: 0.255em;
+ content: "";
+ border-top: 0.3em solid transparent;
+ border-right: 0;
+ border-bottom: 0.3em solid transparent;
+ border-left: 0.3em solid;
+}
+
+/* line 58, node_modules/bootstrap/scss/mixins/_caret.scss */
+.dropright .dropdown-toggle:empty::after {
+ margin-left: 0;
+}
+
+/* line 80, node_modules/bootstrap/scss/_dropdown.scss */
+.dropright .dropdown-toggle::after {
+ vertical-align: 0;
+}
+
+/* line 87, node_modules/bootstrap/scss/_dropdown.scss */
+.dropleft .dropdown-menu {
+ top: 0;
+ right: 100%;
+ left: auto;
+ margin-top: 0;
+ margin-right: 0.125rem;
+}
+
+/* line 30, node_modules/bootstrap/scss/mixins/_caret.scss */
+.dropleft .dropdown-toggle::after {
+ display: inline-block;
+ margin-left: 0.255em;
+ vertical-align: 0.255em;
+ content: "";
+}
+
+/* line 45, node_modules/bootstrap/scss/mixins/_caret.scss */
+.dropleft .dropdown-toggle::after {
+ display: none;
+}
+
+/* line 49, node_modules/bootstrap/scss/mixins/_caret.scss */
+.dropleft .dropdown-toggle::before {
+ display: inline-block;
+ margin-right: 0.255em;
+ vertical-align: 0.255em;
+ content: "";
+ border-top: 0.3em solid transparent;
+ border-right: 0.3em solid;
+ border-bottom: 0.3em solid transparent;
+}
+
+/* line 58, node_modules/bootstrap/scss/mixins/_caret.scss */
+.dropleft .dropdown-toggle:empty::after {
+ margin-left: 0;
+}
+
+/* line 97, node_modules/bootstrap/scss/_dropdown.scss */
+.dropleft .dropdown-toggle::before {
+ vertical-align: 0;
+}
+
+/* line 106, node_modules/bootstrap/scss/_dropdown.scss */
+.dropdown-menu[x-placement^="top"], .dropdown-menu[x-placement^="right"], .dropdown-menu[x-placement^="bottom"], .dropdown-menu[x-placement^="left"] {
+ right: auto;
+ bottom: auto;
+}
+
+/* line 116, node_modules/bootstrap/scss/_dropdown.scss */
+.dropdown-divider {
+ height: 0;
+ margin: 0.5rem 0;
+ overflow: hidden;
+ border-top: 1px solid #eaebea;
+}
+
+/* line 123, node_modules/bootstrap/scss/_dropdown.scss */
+.dropdown-item {
+ display: block;
+ width: 100%;
+ padding: 0.25rem 1.5rem;
+ clear: both;
+ font-weight: 400;
+ color: #464746;
+ text-align: inherit;
+ white-space: nowrap;
+ background-color: transparent;
+ border: 0;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.dropdown-item:hover, .dropdown-item:focus {
+ color: #393a39;
+ text-decoration: none;
+ background-color: #f7f7f7;
+}
+
+/* line 154, node_modules/bootstrap/scss/_dropdown.scss */
+.dropdown-item.active, .dropdown-item:active {
+ color: #ffffff;
+ text-decoration: none;
+ background-color: #464746;
+}
+
+/* line 161, node_modules/bootstrap/scss/_dropdown.scss */
+.dropdown-item.disabled, .dropdown-item:disabled {
+ color: #6c757d;
+ pointer-events: none;
+ background-color: transparent;
+}
+
+/* line 173, node_modules/bootstrap/scss/_dropdown.scss */
+.dropdown-menu.show {
+ display: block;
+}
+
+/* line 178, node_modules/bootstrap/scss/_dropdown.scss */
+.dropdown-header {
+ display: block;
+ padding: 0.5rem 1.5rem;
+ margin-bottom: 0;
+ font-size: 0.875rem;
+ color: #6c757d;
+ white-space: nowrap;
+}
+
+/* line 188, node_modules/bootstrap/scss/_dropdown.scss */
+.dropdown-item-text {
+ display: block;
+ padding: 0.25rem 1.5rem;
+ color: #464746;
+}
+
+/* line 4, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group,
+.btn-group-vertical {
+ position: relative;
+ display: -webkit-inline-box;
+ display: -ms-inline-flexbox;
+ display: inline-flex;
+ vertical-align: middle;
+}
+
+/* line 10, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group > .btn,
+.btn-group-vertical > .btn {
+ position: relative;
+ -webkit-box-flex: 1;
+ -ms-flex: 1 1 auto;
+ flex: 1 1 auto;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.btn-group > .btn:hover,
+.btn-group-vertical > .btn:hover {
+ z-index: 1;
+}
+
+/* line 19, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active,
+.btn-group-vertical > .btn:focus,
+.btn-group-vertical > .btn:active,
+.btn-group-vertical > .btn.active {
+ z-index: 1;
+}
+
+/* line 28, node_modules/bootstrap/scss/_button-group.scss */
+.btn-toolbar {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+ -webkit-box-pack: start;
+ -ms-flex-pack: start;
+ justify-content: flex-start;
+}
+
+/* line 33, node_modules/bootstrap/scss/_button-group.scss */
+.btn-toolbar .input-group {
+ width: auto;
+}
+
+/* line 40, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group > .btn:not(:first-child),
+.btn-group > .btn-group:not(:first-child) {
+ margin-left: -1px;
+}
+
+/* line 46, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group > .btn:not(:last-child):not(.dropdown-toggle),
+.btn-group > .btn-group:not(:last-child) > .btn {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+/* line 51, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group > .btn:not(:first-child),
+.btn-group > .btn-group:not(:first-child) > .btn {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+/* line 69, node_modules/bootstrap/scss/_button-group.scss */
+.dropdown-toggle-split {
+ padding-right: 0.5625rem;
+ padding-left: 0.5625rem;
+}
+
+/* line 73, node_modules/bootstrap/scss/_button-group.scss */
+.dropdown-toggle-split::after,
+.dropup .dropdown-toggle-split::after,
+.dropright .dropdown-toggle-split::after {
+ margin-left: 0;
+}
+
+/* line 79, node_modules/bootstrap/scss/_button-group.scss */
+.dropleft .dropdown-toggle-split::before {
+ margin-right: 0;
+}
+
+/* line 84, node_modules/bootstrap/scss/_button-group.scss */
+.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split {
+ padding-right: 0.375rem;
+ padding-left: 0.375rem;
+}
+
+/* line 89, node_modules/bootstrap/scss/_button-group.scss */
+.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split {
+ padding-right: 0.75rem;
+ padding-left: 0.75rem;
+}
+
+/* line 111, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group-vertical {
+ -webkit-box-orient: vertical;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ -webkit-box-align: start;
+ -ms-flex-align: start;
+ align-items: flex-start;
+ -webkit-box-pack: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+}
+
+/* line 116, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group-vertical > .btn,
+.btn-group-vertical > .btn-group {
+ width: 100%;
+}
+
+/* line 121, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group-vertical > .btn:not(:first-child),
+.btn-group-vertical > .btn-group:not(:first-child) {
+ margin-top: -1px;
+}
+
+/* line 127, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),
+.btn-group-vertical > .btn-group:not(:last-child) > .btn {
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+/* line 132, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group-vertical > .btn:not(:first-child),
+.btn-group-vertical > .btn-group:not(:first-child) > .btn {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+}
+
+/* line 152, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group-toggle > .btn,
+.btn-group-toggle > .btn-group > .btn {
+ margin-bottom: 0;
+}
+
+/* line 156, node_modules/bootstrap/scss/_button-group.scss */
+.btn-group-toggle > .btn input[type="radio"],
+.btn-group-toggle > .btn input[type="checkbox"],
+.btn-group-toggle > .btn-group > .btn input[type="radio"],
+.btn-group-toggle > .btn-group > .btn input[type="checkbox"] {
+ position: absolute;
+ clip: rect(0, 0, 0, 0);
+ pointer-events: none;
+}
+
+/* line 7, node_modules/bootstrap/scss/_input-group.scss */
+.input-group {
+ position: relative;
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+ -webkit-box-align: stretch;
+ -ms-flex-align: stretch;
+ align-items: stretch;
+ width: 100%;
+}
+
+/* line 14, node_modules/bootstrap/scss/_input-group.scss */
+.input-group > .form-control,
+.input-group > .form-control-plaintext,
+.input-group > .custom-select,
+.input-group > .custom-file {
+ position: relative;
+ -webkit-box-flex: 1;
+ -ms-flex: 1 1 auto;
+ flex: 1 1 auto;
+ width: 1%;
+ min-width: 0;
+ margin-bottom: 0;
+}
+
+/* line 24, node_modules/bootstrap/scss/_input-group.scss */
+.input-group > .form-control + .form-control,
+.input-group > .form-control + .custom-select,
+.input-group > .form-control + .custom-file,
+.input-group > .form-control-plaintext + .form-control,
+.input-group > .form-control-plaintext + .custom-select,
+.input-group > .form-control-plaintext + .custom-file,
+.input-group > .custom-select + .form-control,
+.input-group > .custom-select + .custom-select,
+.input-group > .custom-select + .custom-file,
+.input-group > .custom-file + .form-control,
+.input-group > .custom-file + .custom-select,
+.input-group > .custom-file + .custom-file {
+ margin-left: -1px;
+}
+
+/* line 32, node_modules/bootstrap/scss/_input-group.scss */
+.input-group > .form-control:focus,
+.input-group > .custom-select:focus,
+.input-group > .custom-file .custom-file-input:focus ~ .custom-file-label {
+ z-index: 3;
+}
+
+/* line 39, node_modules/bootstrap/scss/_input-group.scss */
+.input-group > .custom-file .custom-file-input:focus {
+ z-index: 4;
+}
+
+/* line 45, node_modules/bootstrap/scss/_input-group.scss */
+.input-group > .form-control:not(:last-child),
+.input-group > .custom-select:not(:last-child) {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+/* line 46, node_modules/bootstrap/scss/_input-group.scss */
+.input-group > .form-control:not(:first-child),
+.input-group > .custom-select:not(:first-child) {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+/* line 51, node_modules/bootstrap/scss/_input-group.scss */
+.input-group > .custom-file {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+}
+
+/* line 55, node_modules/bootstrap/scss/_input-group.scss */
+.input-group > .custom-file:not(:last-child) .custom-file-label,
+.input-group > .custom-file:not(:last-child) .custom-file-label::after {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+/* line 57, node_modules/bootstrap/scss/_input-group.scss */
+.input-group > .custom-file:not(:first-child) .custom-file-label {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+/* line 68, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-prepend,
+.input-group-append {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+}
+
+/* line 75, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-prepend .btn,
+.input-group-append .btn {
+ position: relative;
+ z-index: 2;
+}
+
+/* line 79, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-prepend .btn:focus,
+.input-group-append .btn:focus {
+ z-index: 3;
+}
+
+/* line 84, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-prepend .btn + .btn,
+.input-group-prepend .btn + .input-group-text,
+.input-group-prepend .input-group-text + .input-group-text,
+.input-group-prepend .input-group-text + .btn,
+.input-group-append .btn + .btn,
+.input-group-append .btn + .input-group-text,
+.input-group-append .input-group-text + .input-group-text,
+.input-group-append .input-group-text + .btn {
+ margin-left: -1px;
+}
+
+/* line 92, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-prepend {
+ margin-right: -1px;
+}
+
+/* line 93, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-append {
+ margin-left: -1px;
+}
+
+/* line 101, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-text {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ padding: 0.375rem 0.75rem;
+ margin-bottom: 0;
+ font-size: 1rem;
+ font-weight: 400;
+ line-height: 1.5;
+ color: #495057;
+ text-align: center;
+ white-space: nowrap;
+ background-color: #eaebea;
+ border: 1px solid #ced4da;
+ border-radius: 0.25rem;
+}
+
+/* line 117, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-text input[type="radio"],
+.input-group-text input[type="checkbox"] {
+ margin-top: 0;
+}
+
+/* line 129, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-lg > .form-control:not(textarea),
+.input-group-lg > .custom-select {
+ height: calc(1.5em + 1rem + 2px);
+}
+
+/* line 134, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-lg > .form-control,
+.input-group-lg > .custom-select,
+.input-group-lg > .input-group-prepend > .input-group-text,
+.input-group-lg > .input-group-append > .input-group-text,
+.input-group-lg > .input-group-prepend > .btn,
+.input-group-lg > .input-group-append > .btn {
+ padding: 0.5rem 1rem;
+ font-size: 1.25rem;
+ line-height: 1.5;
+ border-radius: 0.3rem;
+}
+
+/* line 146, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-sm > .form-control:not(textarea),
+.input-group-sm > .custom-select {
+ height: calc(1.5em + 0.5rem + 2px);
+}
+
+/* line 151, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-sm > .form-control,
+.input-group-sm > .custom-select,
+.input-group-sm > .input-group-prepend > .input-group-text,
+.input-group-sm > .input-group-append > .input-group-text,
+.input-group-sm > .input-group-prepend > .btn,
+.input-group-sm > .input-group-append > .btn {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.875rem;
+ line-height: 1.5;
+ border-radius: 0.2rem;
+}
+
+/* line 163, node_modules/bootstrap/scss/_input-group.scss */
+.input-group-lg > .custom-select,
+.input-group-sm > .custom-select {
+ padding-right: 1.75rem;
+}
+
+/* line 176, node_modules/bootstrap/scss/_input-group.scss */
+.input-group > .input-group-prepend > .btn,
+.input-group > .input-group-prepend > .input-group-text,
+.input-group > .input-group-append:not(:last-child) > .btn,
+.input-group > .input-group-append:not(:last-child) > .input-group-text,
+.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle),
+.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+/* line 185, node_modules/bootstrap/scss/_input-group.scss */
+.input-group > .input-group-append > .btn,
+.input-group > .input-group-append > .input-group-text,
+.input-group > .input-group-prepend:not(:first-child) > .btn,
+.input-group > .input-group-prepend:not(:first-child) > .input-group-text,
+.input-group > .input-group-prepend:first-child > .btn:not(:first-child),
+.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+/* line 10, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control {
+ position: relative;
+ z-index: 1;
+ display: block;
+ min-height: 1.5rem;
+ padding-left: 1.5rem;
+}
+
+/* line 18, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control-inline {
+ display: -webkit-inline-box;
+ display: -ms-inline-flexbox;
+ display: inline-flex;
+ margin-right: 1rem;
+}
+
+/* line 23, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control-input {
+ position: absolute;
+ left: 0;
+ z-index: -1;
+ width: 1rem;
+ height: 1.25rem;
+ opacity: 0;
+}
+
+/* line 31, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control-input:checked ~ .custom-control-label::before {
+ color: #ffffff;
+ border-color: #464746;
+ background-color: #464746;
+}
+
+/* line 38, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control-input:focus ~ .custom-control-label::before {
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+ box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+}
+
+/* line 47, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control-input:focus:not(:checked) ~ .custom-control-label::before {
+ border-color: #858785;
+}
+
+/* line 51, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control-input:not(:disabled):active ~ .custom-control-label::before {
+ color: #ffffff;
+ background-color: #9fa09f;
+ border-color: #9fa09f;
+}
+
+/* line 61, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control-input[disabled] ~ .custom-control-label, .custom-control-input:disabled ~ .custom-control-label {
+ color: #6c757d;
+}
+
+/* line 64, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control-input[disabled] ~ .custom-control-label::before, .custom-control-input:disabled ~ .custom-control-label::before {
+ background-color: #eaebea;
+}
+
+/* line 75, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control-label {
+ position: relative;
+ margin-bottom: 0;
+ vertical-align: top;
+}
+
+/* line 83, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control-label::before {
+ position: absolute;
+ top: 0.25rem;
+ left: -1.5rem;
+ display: block;
+ width: 1rem;
+ height: 1rem;
+ pointer-events: none;
+ content: "";
+ background-color: #ffffff;
+ border: #adb5bd solid 1px;
+}
+
+/* line 98, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control-label::after {
+ position: absolute;
+ top: 0.25rem;
+ left: -1.5rem;
+ display: block;
+ width: 1rem;
+ height: 1rem;
+ content: "";
+ background: no-repeat 50% / 50% 50%;
+}
+
+/* line 116, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-checkbox .custom-control-label::before {
+ border-radius: 0.25rem;
+}
+
+/* line 121, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e");
+}
+
+/* line 127, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before {
+ border-color: #464746;
+ background-color: #464746;
+}
+
+/* line 132, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23ffffff' d='M0 2h4'/%3e%3c/svg%3e");
+}
+
+/* line 138, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before {
+ background-color: rgba(70, 71, 70, 0.5);
+}
+
+/* line 141, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before {
+ background-color: rgba(70, 71, 70, 0.5);
+}
+
+/* line 152, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-radio .custom-control-label::before {
+ border-radius: 50%;
+}
+
+/* line 158, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-radio .custom-control-input:checked ~ .custom-control-label::after {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23ffffff'/%3e%3c/svg%3e");
+}
+
+/* line 164, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before {
+ background-color: rgba(70, 71, 70, 0.5);
+}
+
+/* line 175, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-switch {
+ padding-left: 2.25rem;
+}
+
+/* line 179, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-switch .custom-control-label::before {
+ left: -2.25rem;
+ width: 1.75rem;
+ pointer-events: all;
+ border-radius: 0.5rem;
+}
+
+/* line 187, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-switch .custom-control-label::after {
+ top: calc(0.25rem + 2px);
+ left: calc(-2.25rem + 2px);
+ width: calc(1rem - 4px);
+ height: calc(1rem - 4px);
+ background-color: #adb5bd;
+ border-radius: 0.5rem;
+ -webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+ transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+ transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ /* line 187, node_modules/bootstrap/scss/_custom-forms.scss */
+ .custom-switch .custom-control-label::after {
+ -webkit-transition: none;
+ transition: none;
+ }
+}
+
+/* line 200, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-switch .custom-control-input:checked ~ .custom-control-label::after {
+ background-color: #ffffff;
+ -webkit-transform: translateX(0.75rem);
+ transform: translateX(0.75rem);
+}
+
+/* line 207, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before {
+ background-color: rgba(70, 71, 70, 0.5);
+}
+
+/* line 220, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-select {
+ display: inline-block;
+ width: 100%;
+ height: calc(1.5em + 0.75rem + 2px);
+ padding: 0.375rem 1.75rem 0.375rem 0.75rem;
+ font-size: 1rem;
+ font-weight: 300;
+ line-height: 1.5;
+ color: #495057;
+ vertical-align: middle;
+ background: #ffffff 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;
+ border: 1px solid #ced4da;
+ border-radius: 0.25rem;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+}
+
+/* line 237, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-select:focus {
+ border-color: #858785;
+ outline: 0;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+ box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+}
+
+/* line 247, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-select:focus::-ms-value {
+ color: #495057;
+ background-color: #ffffff;
+}
+
+/* line 258, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-select[multiple], .custom-select[size]:not([size="1"]) {
+ height: auto;
+ padding-right: 0.75rem;
+ background-image: none;
+}
+
+/* line 265, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-select:disabled {
+ color: #6c757d;
+ background-color: #eaebea;
+}
+
+/* line 271, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-select::-ms-expand {
+ display: none;
+}
+
+/* line 276, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-select:-moz-focusring {
+ color: transparent;
+ text-shadow: 0 0 0 #495057;
+}
+
+/* line 282, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-select-sm {
+ height: calc(1.5em + 0.5rem + 2px);
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+ padding-left: 0.5rem;
+ font-size: 0.875rem;
+}
+
+/* line 290, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-select-lg {
+ height: calc(1.5em + 1rem + 2px);
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ padding-left: 1rem;
+ font-size: 1.25rem;
+}
+
+/* line 303, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-file {
+ position: relative;
+ display: inline-block;
+ width: 100%;
+ height: calc(1.5em + 0.75rem + 2px);
+ margin-bottom: 0;
+}
+
+/* line 311, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-file-input {
+ position: relative;
+ z-index: 2;
+ width: 100%;
+ height: calc(1.5em + 0.75rem + 2px);
+ margin: 0;
+ opacity: 0;
+}
+
+/* line 319, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-file-input:focus ~ .custom-file-label {
+ border-color: #858785;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+ box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+}
+
+/* line 325, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-file-input[disabled] ~ .custom-file-label,
+.custom-file-input:disabled ~ .custom-file-label {
+ background-color: #eaebea;
+}
+
+/* line 331, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-file-input:lang(en) ~ .custom-file-label::after {
+ content: "Browse";
+}
+
+/* line 336, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-file-input ~ .custom-file-label[data-browse]::after {
+ content: attr(data-browse);
+}
+
+/* line 341, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-file-label {
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: 0;
+ z-index: 1;
+ height: calc(1.5em + 0.75rem + 2px);
+ padding: 0.375rem 0.75rem;
+ font-weight: 300;
+ line-height: 1.5;
+ color: #495057;
+ background-color: #ffffff;
+ border: 1px solid #ced4da;
+ border-radius: 0.25rem;
+}
+
+/* line 358, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-file-label::after {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 3;
+ display: block;
+ height: calc(1.5em + 0.75rem);
+ padding: 0.375rem 0.75rem;
+ line-height: 1.5;
+ color: #495057;
+ content: "Browse";
+ background-color: #eaebea;
+ border-left: inherit;
+ border-radius: 0 0.25rem 0.25rem 0;
+}
+
+/* line 382, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range {
+ width: 100%;
+ height: 1.4rem;
+ padding: 0;
+ background-color: transparent;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+}
+
+/* line 389, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range:focus {
+ outline: none;
+}
+
+/* line 394, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range:focus::-webkit-slider-thumb {
+ -webkit-box-shadow: 0 0 0 1px #ffffff, 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+ box-shadow: 0 0 0 1px #ffffff, 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+}
+
+/* line 395, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range:focus::-moz-range-thumb {
+ box-shadow: 0 0 0 1px #ffffff, 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+}
+
+/* line 396, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range:focus::-ms-thumb {
+ box-shadow: 0 0 0 1px #ffffff, 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+}
+
+/* line 399, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range::-moz-focus-outer {
+ border: 0;
+}
+
+/* line 403, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range::-webkit-slider-thumb {
+ width: 1rem;
+ height: 1rem;
+ margin-top: -0.25rem;
+ background-color: #464746;
+ border: 0;
+ border-radius: 1rem;
+ -webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+ transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+ transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+ -webkit-appearance: none;
+ appearance: none;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ /* line 403, node_modules/bootstrap/scss/_custom-forms.scss */
+ .custom-range::-webkit-slider-thumb {
+ -webkit-transition: none;
+ transition: none;
+ }
+}
+
+/* line 414, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range::-webkit-slider-thumb:active {
+ background-color: #9fa09f;
+}
+
+/* line 419, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range::-webkit-slider-runnable-track {
+ width: 100%;
+ height: 0.5rem;
+ color: transparent;
+ cursor: pointer;
+ background-color: #dee2e6;
+ border-color: transparent;
+ border-radius: 1rem;
+}
+
+/* line 430, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range::-moz-range-thumb {
+ width: 1rem;
+ height: 1rem;
+ background-color: #464746;
+ border: 0;
+ border-radius: 1rem;
+ -moz-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ -moz-appearance: none;
+ appearance: none;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ /* line 430, node_modules/bootstrap/scss/_custom-forms.scss */
+ .custom-range::-moz-range-thumb {
+ -moz-transition: none;
+ transition: none;
+ }
+}
+
+/* line 440, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range::-moz-range-thumb:active {
+ background-color: #9fa09f;
+}
+
+/* line 445, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range::-moz-range-track {
+ width: 100%;
+ height: 0.5rem;
+ color: transparent;
+ cursor: pointer;
+ background-color: #dee2e6;
+ border-color: transparent;
+ border-radius: 1rem;
+}
+
+/* line 456, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range::-ms-thumb {
+ width: 1rem;
+ height: 1rem;
+ margin-top: 0;
+ margin-right: 0.2rem;
+ margin-left: 0.2rem;
+ background-color: #464746;
+ border: 0;
+ border-radius: 1rem;
+ -ms-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ appearance: none;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ /* line 456, node_modules/bootstrap/scss/_custom-forms.scss */
+ .custom-range::-ms-thumb {
+ -ms-transition: none;
+ transition: none;
+ }
+}
+
+/* line 469, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range::-ms-thumb:active {
+ background-color: #9fa09f;
+}
+
+/* line 474, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range::-ms-track {
+ width: 100%;
+ height: 0.5rem;
+ color: transparent;
+ cursor: pointer;
+ background-color: transparent;
+ border-color: transparent;
+ border-width: 0.5rem;
+}
+
+/* line 485, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range::-ms-fill-lower {
+ background-color: #dee2e6;
+ border-radius: 1rem;
+}
+
+/* line 490, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range::-ms-fill-upper {
+ margin-right: 15px;
+ background-color: #dee2e6;
+ border-radius: 1rem;
+}
+
+/* line 497, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range:disabled::-webkit-slider-thumb {
+ background-color: #adb5bd;
+}
+
+/* line 501, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range:disabled::-webkit-slider-runnable-track {
+ cursor: default;
+}
+
+/* line 505, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range:disabled::-moz-range-thumb {
+ background-color: #adb5bd;
+}
+
+/* line 509, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range:disabled::-moz-range-track {
+ cursor: default;
+}
+
+/* line 513, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-range:disabled::-ms-thumb {
+ background-color: #adb5bd;
+}
+
+/* line 519, node_modules/bootstrap/scss/_custom-forms.scss */
+.custom-control-label::before,
+.custom-file-label,
+.custom-select {
+ -webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+ transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+ transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ transition: background-color 0.15s ease-in-out, 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) {
+ /* line 519, node_modules/bootstrap/scss/_custom-forms.scss */
+ .custom-control-label::before,
+ .custom-file-label,
+ .custom-select {
+ -webkit-transition: none;
+ transition: none;
+ }
+}
+
+/* line 6, node_modules/bootstrap/scss/_nav.scss */
+.nav {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+ padding-left: 0;
+ margin-bottom: 0;
+ list-style: none;
+}
+
+/* line 14, node_modules/bootstrap/scss/_nav.scss */
+.nav-link {
+ display: block;
+ padding: 0.5rem 1rem;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.nav-link:hover, .nav-link:focus {
+ text-decoration: none;
+}
+
+/* line 24, node_modules/bootstrap/scss/_nav.scss */
+.nav-link.disabled {
+ color: #6c757d;
+ pointer-events: none;
+ cursor: default;
+}
+
+/* line 35, node_modules/bootstrap/scss/_nav.scss */
+.nav-tabs {
+ border-bottom: 1px solid #dee2e6;
+}
+
+/* line 38, node_modules/bootstrap/scss/_nav.scss */
+.nav-tabs .nav-item {
+ margin-bottom: -1px;
+}
+
+/* line 42, node_modules/bootstrap/scss/_nav.scss */
+.nav-tabs .nav-link {
+ border: 1px solid transparent;
+ border-top-left-radius: 0.25rem;
+ border-top-right-radius: 0.25rem;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {
+ border-color: #eaebea #eaebea #dee2e6;
+}
+
+/* line 50, node_modules/bootstrap/scss/_nav.scss */
+.nav-tabs .nav-link.disabled {
+ color: #6c757d;
+ background-color: transparent;
+ border-color: transparent;
+}
+
+/* line 57, node_modules/bootstrap/scss/_nav.scss */
+.nav-tabs .nav-link.active,
+.nav-tabs .nav-item.show .nav-link {
+ color: #495057;
+ background-color: #ffffff;
+ border-color: #dee2e6 #dee2e6 #ffffff;
+}
+
+/* line 64, node_modules/bootstrap/scss/_nav.scss */
+.nav-tabs .dropdown-menu {
+ margin-top: -1px;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+}
+
+/* line 78, node_modules/bootstrap/scss/_nav.scss */
+.nav-pills .nav-link {
+ border-radius: 0.25rem;
+}
+
+/* line 82, node_modules/bootstrap/scss/_nav.scss */
+.nav-pills .nav-link.active,
+.nav-pills .show > .nav-link {
+ color: #ffffff;
+ background-color: #464746;
+}
+
+/* line 95, node_modules/bootstrap/scss/_nav.scss */
+.nav-fill > .nav-link,
+.nav-fill .nav-item {
+ -webkit-box-flex: 1;
+ -ms-flex: 1 1 auto;
+ flex: 1 1 auto;
+ text-align: center;
+}
+
+/* line 103, node_modules/bootstrap/scss/_nav.scss */
+.nav-justified > .nav-link,
+.nav-justified .nav-item {
+ -ms-flex-preferred-size: 0;
+ flex-basis: 0;
+ -webkit-box-flex: 1;
+ -ms-flex-positive: 1;
+ flex-grow: 1;
+ text-align: center;
+}
+
+/* line 117, node_modules/bootstrap/scss/_nav.scss */
+.tab-content > .tab-pane {
+ display: none;
+}
+
+/* line 120, node_modules/bootstrap/scss/_nav.scss */
+.tab-content > .active {
+ display: block;
+}
+
+/* line 18, node_modules/bootstrap/scss/_navbar.scss */
+.navbar {
+ position: relative;
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-box-pack: justify;
+ -ms-flex-pack: justify;
+ justify-content: space-between;
+ padding: 0.5rem 1rem;
+}
+
+/* line 28, node_modules/bootstrap/scss/_navbar.scss */
+.navbar .container,
+.navbar .container-fluid, .navbar .container-sm, .navbar .container-md, .navbar .container-lg, .navbar .container-xl, .navbar .container-xxl {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-box-pack: justify;
+ -ms-flex-pack: justify;
+ justify-content: space-between;
+}
+
+/* line 52, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-brand {
+ display: inline-block;
+ padding-top: 0.3125rem;
+ padding-bottom: 0.3125rem;
+ margin-right: 1rem;
+ font-size: 1.25rem;
+ line-height: inherit;
+ white-space: nowrap;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.navbar-brand:hover, .navbar-brand:focus {
+ text-decoration: none;
+}
+
+/* line 71, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-nav {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-orient: vertical;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ padding-left: 0;
+ margin-bottom: 0;
+ list-style: none;
+}
+
+/* line 78, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-nav .nav-link {
+ padding-right: 0;
+ padding-left: 0;
+}
+
+/* line 83, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-nav .dropdown-menu {
+ position: static;
+ float: none;
+}
+
+/* line 94, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-text {
+ display: inline-block;
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+}
+
+/* line 109, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-collapse {
+ -ms-flex-preferred-size: 100%;
+ flex-basis: 100%;
+ -webkit-box-flex: 1;
+ -ms-flex-positive: 1;
+ flex-grow: 1;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+}
+
+/* line 118, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-toggler {
+ padding: 0.25rem 0.75rem;
+ font-size: 1.25rem;
+ line-height: 1;
+ background-color: transparent;
+ border: 1px solid transparent;
+ border-radius: 0.25rem;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.navbar-toggler:hover, .navbar-toggler:focus {
+ text-decoration: none;
+}
+
+/* line 133, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-toggler-icon {
+ display: inline-block;
+ width: 1.5em;
+ height: 1.5em;
+ vertical-align: middle;
+ content: "";
+ background: no-repeat center center;
+ background-size: 100% 100%;
+}
+
+@media (max-width: 575.98px) {
+ /* line 152, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-sm > .container,
+ .navbar-expand-sm > .container-fluid, .navbar-expand-sm > .container-sm, .navbar-expand-sm > .container-md, .navbar-expand-sm > .container-lg, .navbar-expand-sm > .container-xl, .navbar-expand-sm > .container-xxl {
+ padding-right: 0;
+ padding-left: 0;
+ }
+}
+
+@media (min-width: 576px) {
+ /* line 150, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-sm {
+ -webkit-box-orient: horizontal;
+ -webkit-box-direction: normal;
+ -ms-flex-flow: row nowrap;
+ flex-flow: row nowrap;
+ -webkit-box-pack: start;
+ -ms-flex-pack: start;
+ justify-content: flex-start;
+ }
+ /* line 173, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-sm .navbar-nav {
+ -webkit-box-orient: horizontal;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ }
+ /* line 176, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-sm .navbar-nav .dropdown-menu {
+ position: absolute;
+ }
+ /* line 180, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-sm .navbar-nav .nav-link {
+ padding-right: 0.5rem;
+ padding-left: 0.5rem;
+ }
+ /* line 187, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-sm > .container,
+ .navbar-expand-sm > .container-fluid, .navbar-expand-sm > .container-sm, .navbar-expand-sm > .container-md, .navbar-expand-sm > .container-lg, .navbar-expand-sm > .container-xl, .navbar-expand-sm > .container-xxl {
+ -ms-flex-wrap: nowrap;
+ flex-wrap: nowrap;
+ }
+ /* line 202, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-sm .navbar-collapse {
+ display: -webkit-box !important;
+ display: -ms-flexbox !important;
+ display: flex !important;
+ -ms-flex-preferred-size: auto;
+ flex-basis: auto;
+ }
+ /* line 209, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-sm .navbar-toggler {
+ display: none;
+ }
+}
+
+@media (max-width: 767.98px) {
+ /* line 152, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-md > .container,
+ .navbar-expand-md > .container-fluid, .navbar-expand-md > .container-sm, .navbar-expand-md > .container-md, .navbar-expand-md > .container-lg, .navbar-expand-md > .container-xl, .navbar-expand-md > .container-xxl {
+ padding-right: 0;
+ padding-left: 0;
+ }
+}
+
+@media (min-width: 768px) {
+ /* line 150, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-md {
+ -webkit-box-orient: horizontal;
+ -webkit-box-direction: normal;
+ -ms-flex-flow: row nowrap;
+ flex-flow: row nowrap;
+ -webkit-box-pack: start;
+ -ms-flex-pack: start;
+ justify-content: flex-start;
+ }
+ /* line 173, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-md .navbar-nav {
+ -webkit-box-orient: horizontal;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ }
+ /* line 176, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-md .navbar-nav .dropdown-menu {
+ position: absolute;
+ }
+ /* line 180, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-md .navbar-nav .nav-link {
+ padding-right: 0.5rem;
+ padding-left: 0.5rem;
+ }
+ /* line 187, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-md > .container,
+ .navbar-expand-md > .container-fluid, .navbar-expand-md > .container-sm, .navbar-expand-md > .container-md, .navbar-expand-md > .container-lg, .navbar-expand-md > .container-xl, .navbar-expand-md > .container-xxl {
+ -ms-flex-wrap: nowrap;
+ flex-wrap: nowrap;
+ }
+ /* line 202, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-md .navbar-collapse {
+ display: -webkit-box !important;
+ display: -ms-flexbox !important;
+ display: flex !important;
+ -ms-flex-preferred-size: auto;
+ flex-basis: auto;
+ }
+ /* line 209, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-md .navbar-toggler {
+ display: none;
+ }
+}
+
+@media (max-width: 1023.98px) {
+ /* line 152, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-lg > .container,
+ .navbar-expand-lg > .container-fluid, .navbar-expand-lg > .container-sm, .navbar-expand-lg > .container-md, .navbar-expand-lg > .container-lg, .navbar-expand-lg > .container-xl, .navbar-expand-lg > .container-xxl {
+ padding-right: 0;
+ padding-left: 0;
+ }
+}
+
+@media (min-width: 1024px) {
+ /* line 150, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-lg {
+ -webkit-box-orient: horizontal;
+ -webkit-box-direction: normal;
+ -ms-flex-flow: row nowrap;
+ flex-flow: row nowrap;
+ -webkit-box-pack: start;
+ -ms-flex-pack: start;
+ justify-content: flex-start;
+ }
+ /* line 173, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-lg .navbar-nav {
+ -webkit-box-orient: horizontal;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ }
+ /* line 176, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-lg .navbar-nav .dropdown-menu {
+ position: absolute;
+ }
+ /* line 180, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-lg .navbar-nav .nav-link {
+ padding-right: 0.5rem;
+ padding-left: 0.5rem;
+ }
+ /* line 187, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-lg > .container,
+ .navbar-expand-lg > .container-fluid, .navbar-expand-lg > .container-sm, .navbar-expand-lg > .container-md, .navbar-expand-lg > .container-lg, .navbar-expand-lg > .container-xl, .navbar-expand-lg > .container-xxl {
+ -ms-flex-wrap: nowrap;
+ flex-wrap: nowrap;
+ }
+ /* line 202, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-lg .navbar-collapse {
+ display: -webkit-box !important;
+ display: -ms-flexbox !important;
+ display: flex !important;
+ -ms-flex-preferred-size: auto;
+ flex-basis: auto;
+ }
+ /* line 209, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-lg .navbar-toggler {
+ display: none;
+ }
+}
+
+@media (max-width: 1279.98px) {
+ /* line 152, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-xl > .container,
+ .navbar-expand-xl > .container-fluid, .navbar-expand-xl > .container-sm, .navbar-expand-xl > .container-md, .navbar-expand-xl > .container-lg, .navbar-expand-xl > .container-xl, .navbar-expand-xl > .container-xxl {
+ padding-right: 0;
+ padding-left: 0;
+ }
+}
+
+@media (min-width: 1280px) {
+ /* line 150, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-xl {
+ -webkit-box-orient: horizontal;
+ -webkit-box-direction: normal;
+ -ms-flex-flow: row nowrap;
+ flex-flow: row nowrap;
+ -webkit-box-pack: start;
+ -ms-flex-pack: start;
+ justify-content: flex-start;
+ }
+ /* line 173, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-xl .navbar-nav {
+ -webkit-box-orient: horizontal;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ }
+ /* line 176, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-xl .navbar-nav .dropdown-menu {
+ position: absolute;
+ }
+ /* line 180, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-xl .navbar-nav .nav-link {
+ padding-right: 0.5rem;
+ padding-left: 0.5rem;
+ }
+ /* line 187, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-xl > .container,
+ .navbar-expand-xl > .container-fluid, .navbar-expand-xl > .container-sm, .navbar-expand-xl > .container-md, .navbar-expand-xl > .container-lg, .navbar-expand-xl > .container-xl, .navbar-expand-xl > .container-xxl {
+ -ms-flex-wrap: nowrap;
+ flex-wrap: nowrap;
+ }
+ /* line 202, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-xl .navbar-collapse {
+ display: -webkit-box !important;
+ display: -ms-flexbox !important;
+ display: flex !important;
+ -ms-flex-preferred-size: auto;
+ flex-basis: auto;
+ }
+ /* line 209, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-xl .navbar-toggler {
+ display: none;
+ }
+}
+
+@media (max-width: 1439.98px) {
+ /* line 152, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-xxl > .container,
+ .navbar-expand-xxl > .container-fluid, .navbar-expand-xxl > .container-sm, .navbar-expand-xxl > .container-md, .navbar-expand-xxl > .container-lg, .navbar-expand-xxl > .container-xl, .navbar-expand-xxl > .container-xxl {
+ padding-right: 0;
+ padding-left: 0;
+ }
+}
+
+@media (min-width: 1440px) {
+ /* line 150, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-xxl {
+ -webkit-box-orient: horizontal;
+ -webkit-box-direction: normal;
+ -ms-flex-flow: row nowrap;
+ flex-flow: row nowrap;
+ -webkit-box-pack: start;
+ -ms-flex-pack: start;
+ justify-content: flex-start;
+ }
+ /* line 173, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-xxl .navbar-nav {
+ -webkit-box-orient: horizontal;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ }
+ /* line 176, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-xxl .navbar-nav .dropdown-menu {
+ position: absolute;
+ }
+ /* line 180, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-xxl .navbar-nav .nav-link {
+ padding-right: 0.5rem;
+ padding-left: 0.5rem;
+ }
+ /* line 187, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-xxl > .container,
+ .navbar-expand-xxl > .container-fluid, .navbar-expand-xxl > .container-sm, .navbar-expand-xxl > .container-md, .navbar-expand-xxl > .container-lg, .navbar-expand-xxl > .container-xl, .navbar-expand-xxl > .container-xxl {
+ -ms-flex-wrap: nowrap;
+ flex-wrap: nowrap;
+ }
+ /* line 202, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-xxl .navbar-collapse {
+ display: -webkit-box !important;
+ display: -ms-flexbox !important;
+ display: flex !important;
+ -ms-flex-preferred-size: auto;
+ flex-basis: auto;
+ }
+ /* line 209, node_modules/bootstrap/scss/_navbar.scss */
+ .navbar-expand-xxl .navbar-toggler {
+ display: none;
+ }
+}
+
+/* line 150, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-expand {
+ -webkit-box-orient: horizontal;
+ -webkit-box-direction: normal;
+ -ms-flex-flow: row nowrap;
+ flex-flow: row nowrap;
+ -webkit-box-pack: start;
+ -ms-flex-pack: start;
+ justify-content: flex-start;
+}
+
+/* line 152, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-expand > .container,
+.navbar-expand > .container-fluid, .navbar-expand > .container-sm, .navbar-expand > .container-md, .navbar-expand > .container-lg, .navbar-expand > .container-xl, .navbar-expand > .container-xxl {
+ padding-right: 0;
+ padding-left: 0;
+}
+
+/* line 173, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-expand .navbar-nav {
+ -webkit-box-orient: horizontal;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: row;
+ flex-direction: row;
+}
+
+/* line 176, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-expand .navbar-nav .dropdown-menu {
+ position: absolute;
+}
+
+/* line 180, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-expand .navbar-nav .nav-link {
+ padding-right: 0.5rem;
+ padding-left: 0.5rem;
+}
+
+/* line 187, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-expand > .container,
+.navbar-expand > .container-fluid, .navbar-expand > .container-sm, .navbar-expand > .container-md, .navbar-expand > .container-lg, .navbar-expand > .container-xl, .navbar-expand > .container-xxl {
+ -ms-flex-wrap: nowrap;
+ flex-wrap: nowrap;
+}
+
+/* line 202, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-expand .navbar-collapse {
+ display: -webkit-box !important;
+ display: -ms-flexbox !important;
+ display: flex !important;
+ -ms-flex-preferred-size: auto;
+ flex-basis: auto;
+}
+
+/* line 209, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-expand .navbar-toggler {
+ display: none;
+}
+
+/* line 224, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-light .navbar-brand {
+ color: rgba(0, 0, 0, 0.9);
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus {
+ color: rgba(0, 0, 0, 0.9);
+}
+
+/* line 233, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-light .navbar-nav .nav-link {
+ color: rgba(0, 0, 0, 0.5);
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus {
+ color: rgba(0, 0, 0, 0.7);
+}
+
+/* line 240, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-light .navbar-nav .nav-link.disabled {
+ color: rgba(0, 0, 0, 0.3);
+}
+
+/* line 245, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-light .navbar-nav .show > .nav-link,
+.navbar-light .navbar-nav .active > .nav-link,
+.navbar-light .navbar-nav .nav-link.show,
+.navbar-light .navbar-nav .nav-link.active {
+ color: rgba(0, 0, 0, 0.9);
+}
+
+/* line 253, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-light .navbar-toggler {
+ color: rgba(0, 0, 0, 0.5);
+ border-color: rgba(0, 0, 0, 0.1);
+}
+
+/* line 258, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-light .navbar-toggler-icon {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
+}
+
+/* line 262, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-light .navbar-text {
+ color: rgba(0, 0, 0, 0.5);
+}
+
+/* line 264, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-light .navbar-text a {
+ color: rgba(0, 0, 0, 0.9);
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.navbar-light .navbar-text a:hover, .navbar-light .navbar-text a:focus {
+ color: rgba(0, 0, 0, 0.9);
+}
+
+/* line 276, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-dark .navbar-brand {
+ color: #ffffff;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus {
+ color: #ffffff;
+}
+
+/* line 285, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-dark .navbar-nav .nav-link {
+ color: rgba(255, 255, 255, 0.5);
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus {
+ color: rgba(255, 255, 255, 0.75);
+}
+
+/* line 292, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-dark .navbar-nav .nav-link.disabled {
+ color: rgba(255, 255, 255, 0.25);
+}
+
+/* line 297, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-dark .navbar-nav .show > .nav-link,
+.navbar-dark .navbar-nav .active > .nav-link,
+.navbar-dark .navbar-nav .nav-link.show,
+.navbar-dark .navbar-nav .nav-link.active {
+ color: #ffffff;
+}
+
+/* line 305, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-dark .navbar-toggler {
+ color: rgba(255, 255, 255, 0.5);
+ border-color: rgba(255, 255, 255, 0.1);
+}
+
+/* line 310, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-dark .navbar-toggler-icon {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
+}
+
+/* line 314, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-dark .navbar-text {
+ color: rgba(255, 255, 255, 0.5);
+}
+
+/* line 316, node_modules/bootstrap/scss/_navbar.scss */
+.navbar-dark .navbar-text a {
+ color: #ffffff;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.navbar-dark .navbar-text a:hover, .navbar-dark .navbar-text a:focus {
+ color: #ffffff;
+}
+
+/* line 5, node_modules/bootstrap/scss/_card.scss */
+.card {
+ position: relative;
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-orient: vertical;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ min-width: 0;
+ word-wrap: break-word;
+ background-color: #ffffff;
+ background-clip: border-box;
+ border: 1px solid rgba(0, 0, 0, 0.125);
+ border-radius: 0.25rem;
+}
+
+/* line 17, node_modules/bootstrap/scss/_card.scss */
+.card > hr {
+ margin-right: 0;
+ margin-left: 0;
+}
+
+/* line 22, node_modules/bootstrap/scss/_card.scss */
+.card > .list-group {
+ border-top: inherit;
+ border-bottom: inherit;
+}
+
+/* line 26, node_modules/bootstrap/scss/_card.scss */
+.card > .list-group:first-child {
+ border-top-width: 0;
+ border-top-left-radius: calc(0.25rem - 1px);
+ border-top-right-radius: calc(0.25rem - 1px);
+}
+
+/* line 31, node_modules/bootstrap/scss/_card.scss */
+.card > .list-group:last-child {
+ border-bottom-width: 0;
+ border-bottom-right-radius: calc(0.25rem - 1px);
+ border-bottom-left-radius: calc(0.25rem - 1px);
+}
+
+/* line 39, node_modules/bootstrap/scss/_card.scss */
+.card > .card-header + .list-group,
+.card > .list-group + .card-footer {
+ border-top: 0;
+}
+
+/* line 45, node_modules/bootstrap/scss/_card.scss */
+.card-body {
+ -webkit-box-flex: 1;
+ -ms-flex: 1 1 auto;
+ flex: 1 1 auto;
+ min-height: 1px;
+ padding: 1.25rem;
+}
+
+/* line 56, node_modules/bootstrap/scss/_card.scss */
+.card-title {
+ margin-bottom: 0.75rem;
+}
+
+/* line 60, node_modules/bootstrap/scss/_card.scss */
+.card-subtitle {
+ margin-top: -0.375rem;
+ margin-bottom: 0;
+}
+
+/* line 65, node_modules/bootstrap/scss/_card.scss */
+.card-text:last-child {
+ margin-bottom: 0;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.card-link:hover {
+ text-decoration: none;
+}
+
+/* line 74, node_modules/bootstrap/scss/_card.scss */
+.card-link + .card-link {
+ margin-left: 1.25rem;
+}
+
+/* line 83, node_modules/bootstrap/scss/_card.scss */
+.card-header {
+ padding: 0.75rem 1.25rem;
+ margin-bottom: 0;
+ background-color: rgba(0, 0, 0, 0.03);
+ border-bottom: 1px solid rgba(0, 0, 0, 0.125);
+}
+
+/* line 90, node_modules/bootstrap/scss/_card.scss */
+.card-header:first-child {
+ border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0;
+}
+
+/* line 95, node_modules/bootstrap/scss/_card.scss */
+.card-footer {
+ padding: 0.75rem 1.25rem;
+ background-color: rgba(0, 0, 0, 0.03);
+ border-top: 1px solid rgba(0, 0, 0, 0.125);
+}
+
+/* line 101, node_modules/bootstrap/scss/_card.scss */
+.card-footer:last-child {
+ border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px);
+}
+
+/* line 111, node_modules/bootstrap/scss/_card.scss */
+.card-header-tabs {
+ margin-right: -0.625rem;
+ margin-bottom: -0.75rem;
+ margin-left: -0.625rem;
+ border-bottom: 0;
+}
+
+/* line 118, node_modules/bootstrap/scss/_card.scss */
+.card-header-pills {
+ margin-right: -0.625rem;
+ margin-left: -0.625rem;
+}
+
+/* line 124, node_modules/bootstrap/scss/_card.scss */
+.card-img-overlay {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ padding: 1.25rem;
+ border-radius: calc(0.25rem - 1px);
+}
+
+/* line 134, node_modules/bootstrap/scss/_card.scss */
+.card-img,
+.card-img-top,
+.card-img-bottom {
+ -ms-flex-negative: 0;
+ flex-shrink: 0;
+ width: 100%;
+}
+
+/* line 141, node_modules/bootstrap/scss/_card.scss */
+.card-img,
+.card-img-top {
+ border-top-left-radius: calc(0.25rem - 1px);
+ border-top-right-radius: calc(0.25rem - 1px);
+}
+
+/* line 146, node_modules/bootstrap/scss/_card.scss */
+.card-img,
+.card-img-bottom {
+ border-bottom-right-radius: calc(0.25rem - 1px);
+ border-bottom-left-radius: calc(0.25rem - 1px);
+}
+
+/* line 155, node_modules/bootstrap/scss/_card.scss */
+.card-deck .card {
+ margin-bottom: 15px;
+}
+
+@media (min-width: 576px) {
+ /* line 154, node_modules/bootstrap/scss/_card.scss */
+ .card-deck {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-orient: horizontal;
+ -webkit-box-direction: normal;
+ -ms-flex-flow: row wrap;
+ flex-flow: row wrap;
+ margin-right: -15px;
+ margin-left: -15px;
+ }
+ /* line 165, node_modules/bootstrap/scss/_card.scss */
+ .card-deck .card {
+ -webkit-box-flex: 1;
+ -ms-flex: 1 0 0%;
+ flex: 1 0 0%;
+ margin-right: 15px;
+ margin-bottom: 0;
+ margin-left: 15px;
+ }
+}
+
+/* line 183, node_modules/bootstrap/scss/_card.scss */
+.card-group > .card {
+ margin-bottom: 15px;
+}
+
+@media (min-width: 576px) {
+ /* line 180, node_modules/bootstrap/scss/_card.scss */
+ .card-group {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-orient: horizontal;
+ -webkit-box-direction: normal;
+ -ms-flex-flow: row wrap;
+ flex-flow: row wrap;
+ }
+ /* line 192, node_modules/bootstrap/scss/_card.scss */
+ .card-group > .card {
+ -webkit-box-flex: 1;
+ -ms-flex: 1 0 0%;
+ flex: 1 0 0%;
+ margin-bottom: 0;
+ }
+ /* line 197, node_modules/bootstrap/scss/_card.scss */
+ .card-group > .card + .card {
+ margin-left: 0;
+ border-left: 0;
+ }
+ /* line 204, node_modules/bootstrap/scss/_card.scss */
+ .card-group > .card:not(:last-child) {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+ /* line 207, node_modules/bootstrap/scss/_card.scss */
+ .card-group > .card:not(:last-child) .card-img-top,
+ .card-group > .card:not(:last-child) .card-header {
+ border-top-right-radius: 0;
+ }
+ /* line 212, node_modules/bootstrap/scss/_card.scss */
+ .card-group > .card:not(:last-child) .card-img-bottom,
+ .card-group > .card:not(:last-child) .card-footer {
+ border-bottom-right-radius: 0;
+ }
+ /* line 219, node_modules/bootstrap/scss/_card.scss */
+ .card-group > .card:not(:first-child) {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+ /* line 222, node_modules/bootstrap/scss/_card.scss */
+ .card-group > .card:not(:first-child) .card-img-top,
+ .card-group > .card:not(:first-child) .card-header {
+ border-top-left-radius: 0;
+ }
+ /* line 227, node_modules/bootstrap/scss/_card.scss */
+ .card-group > .card:not(:first-child) .card-img-bottom,
+ .card-group > .card:not(:first-child) .card-footer {
+ border-bottom-left-radius: 0;
+ }
+}
+
+/* line 244, node_modules/bootstrap/scss/_card.scss */
+.card-columns .card {
+ margin-bottom: 0.75rem;
+}
+
+@media (min-width: 576px) {
+ /* line 243, node_modules/bootstrap/scss/_card.scss */
+ .card-columns {
+ -webkit-column-count: 3;
+ -moz-column-count: 3;
+ column-count: 3;
+ -webkit-column-gap: 1.25rem;
+ -moz-column-gap: 1.25rem;
+ column-gap: 1.25rem;
+ orphans: 1;
+ widows: 1;
+ }
+ /* line 254, node_modules/bootstrap/scss/_card.scss */
+ .card-columns .card {
+ display: inline-block;
+ width: 100%;
+ }
+}
+
+/* line 266, node_modules/bootstrap/scss/_card.scss */
+.accordion {
+ overflow-anchor: none;
+}
+
+/* line 269, node_modules/bootstrap/scss/_card.scss */
+.accordion > .card {
+ overflow: hidden;
+}
+
+/* line 272, node_modules/bootstrap/scss/_card.scss */
+.accordion > .card:not(:last-of-type) {
+ border-bottom: 0;
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+/* line 277, node_modules/bootstrap/scss/_card.scss */
+.accordion > .card:not(:first-of-type) {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+}
+
+/* line 281, node_modules/bootstrap/scss/_card.scss */
+.accordion > .card > .card-header {
+ border-radius: 0;
+ margin-bottom: -1px;
+}
+
+/* line 1, node_modules/bootstrap/scss/_breadcrumb.scss */
+.breadcrumb {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+ padding: 0.75rem 1rem;
+ margin-bottom: 1rem;
+ list-style: none;
+ background-color: #eaebea;
+ border-radius: 0.25rem;
+}
+
+/* line 12, node_modules/bootstrap/scss/_breadcrumb.scss */
+.breadcrumb-item {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+}
+
+/* line 16, node_modules/bootstrap/scss/_breadcrumb.scss */
+.breadcrumb-item + .breadcrumb-item {
+ padding-left: 0.5rem;
+}
+
+/* line 19, node_modules/bootstrap/scss/_breadcrumb.scss */
+.breadcrumb-item + .breadcrumb-item::before {
+ display: inline-block;
+ padding-right: 0.5rem;
+ color: #6c757d;
+ content: "/";
+}
+
+/* line 33, node_modules/bootstrap/scss/_breadcrumb.scss */
+.breadcrumb-item + .breadcrumb-item:hover::before {
+ text-decoration: underline;
+}
+
+/* line 37, node_modules/bootstrap/scss/_breadcrumb.scss */
+.breadcrumb-item + .breadcrumb-item:hover::before {
+ text-decoration: none;
+}
+
+/* line 41, node_modules/bootstrap/scss/_breadcrumb.scss */
+.breadcrumb-item.active {
+ color: #6c757d;
+}
+
+/* line 1, node_modules/bootstrap/scss/_pagination.scss */
+.pagination {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ padding-left: 0;
+ list-style: none;
+ border-radius: 0.25rem;
+}
+
+/* line 7, node_modules/bootstrap/scss/_pagination.scss */
+.page-link {
+ position: relative;
+ display: block;
+ padding: 0.5rem 0.75rem;
+ margin-left: -1px;
+ line-height: 1.25;
+ color: #464746;
+ background-color: #ffffff;
+ border: 1px solid #dee2e6;
+}
+
+/* line 18, node_modules/bootstrap/scss/_pagination.scss */
+.page-link:hover {
+ z-index: 2;
+ color: #202020;
+ text-decoration: none;
+ background-color: #eaebea;
+ border-color: #dee2e6;
+}
+
+/* line 26, node_modules/bootstrap/scss/_pagination.scss */
+.page-link:focus {
+ z-index: 3;
+ outline: 0;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+ box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.25);
+}
+
+/* line 35, node_modules/bootstrap/scss/_pagination.scss */
+.page-item:first-child .page-link {
+ margin-left: 0;
+ border-top-left-radius: 0.25rem;
+ border-bottom-left-radius: 0.25rem;
+}
+
+/* line 41, node_modules/bootstrap/scss/_pagination.scss */
+.page-item:last-child .page-link {
+ border-top-right-radius: 0.25rem;
+ border-bottom-right-radius: 0.25rem;
+}
+
+/* line 46, node_modules/bootstrap/scss/_pagination.scss */
+.page-item.active .page-link {
+ z-index: 3;
+ color: #ffffff;
+ background-color: #464746;
+ border-color: #464746;
+}
+
+/* line 53, node_modules/bootstrap/scss/_pagination.scss */
+.page-item.disabled .page-link {
+ color: #6c757d;
+ pointer-events: none;
+ cursor: auto;
+ background-color: #ffffff;
+ border-color: #dee2e6;
+}
+
+/* line 4, node_modules/bootstrap/scss/mixins/_pagination.scss */
+.pagination-lg .page-link {
+ padding: 0.75rem 1.5rem;
+ font-size: 1.25rem;
+ line-height: 1.5;
+}
+
+/* line 12, node_modules/bootstrap/scss/mixins/_pagination.scss */
+.pagination-lg .page-item:first-child .page-link {
+ border-top-left-radius: 0.3rem;
+ border-bottom-left-radius: 0.3rem;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_pagination.scss */
+.pagination-lg .page-item:last-child .page-link {
+ border-top-right-radius: 0.3rem;
+ border-bottom-right-radius: 0.3rem;
+}
+
+/* line 4, node_modules/bootstrap/scss/mixins/_pagination.scss */
+.pagination-sm .page-link {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.875rem;
+ line-height: 1.5;
+}
+
+/* line 12, node_modules/bootstrap/scss/mixins/_pagination.scss */
+.pagination-sm .page-item:first-child .page-link {
+ border-top-left-radius: 0.2rem;
+ border-bottom-left-radius: 0.2rem;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_pagination.scss */
+.pagination-sm .page-item:last-child .page-link {
+ border-top-right-radius: 0.2rem;
+ border-bottom-right-radius: 0.2rem;
+}
+
+/* line 6, node_modules/bootstrap/scss/_badge.scss */
+.badge {
+ display: inline-block;
+ padding: 0.25em 0.4em;
+ font-size: 75%;
+ font-weight: 700;
+ line-height: 1;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: baseline;
+ border-radius: 0.25rem;
+ -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, 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) {
+ /* line 6, node_modules/bootstrap/scss/_badge.scss */
+ .badge {
+ -webkit-transition: none;
+ transition: none;
+ }
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.badge:hover, a.badge:focus {
+ text-decoration: none;
+}
+
+/* line 25, node_modules/bootstrap/scss/_badge.scss */
+.badge:empty {
+ display: none;
+}
+
+/* line 31, node_modules/bootstrap/scss/_badge.scss */
+.btn .badge {
+ position: relative;
+ top: -1px;
+}
+
+/* line 40, node_modules/bootstrap/scss/_badge.scss */
+.badge-pill {
+ padding-right: 0.6em;
+ padding-left: 0.6em;
+ border-radius: 10rem;
+}
+
+/* line 51, node_modules/bootstrap/scss/_badge.scss */
+.badge-primary {
+ color: #ffffff;
+ background-color: #464746;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.badge-primary:hover, a.badge-primary:focus {
+ color: #ffffff;
+ background-color: #2d2d2d;
+}
+
+/* line 11, node_modules/bootstrap/scss/mixins/_badge.scss */
+a.badge-primary:focus, a.badge-primary.focus {
+ outline: 0;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+}
+
+/* line 51, node_modules/bootstrap/scss/_badge.scss */
+.badge-secondary {
+ color: #464746;
+ background-color: #f0ebe3;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.badge-secondary:hover, a.badge-secondary:focus {
+ color: #464746;
+ background-color: #ded3c2;
+}
+
+/* line 11, node_modules/bootstrap/scss/mixins/_badge.scss */
+a.badge-secondary:focus, a.badge-secondary.focus {
+ outline: 0;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.5);
+}
+
+/* line 51, node_modules/bootstrap/scss/_badge.scss */
+.badge-success {
+ color: #464746;
+ background-color: #f0ebe3;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.badge-success:hover, a.badge-success:focus {
+ color: #464746;
+ background-color: #ded3c2;
+}
+
+/* line 11, node_modules/bootstrap/scss/mixins/_badge.scss */
+a.badge-success:focus, a.badge-success.focus {
+ outline: 0;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(240, 235, 227, 0.5);
+}
+
+/* line 51, node_modules/bootstrap/scss/_badge.scss */
+.badge-info {
+ color: #ffffff;
+ background-color: #464746;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.badge-info:hover, a.badge-info:focus {
+ color: #ffffff;
+ background-color: #2d2d2d;
+}
+
+/* line 11, node_modules/bootstrap/scss/mixins/_badge.scss */
+a.badge-info:focus, a.badge-info.focus {
+ outline: 0;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+}
+
+/* line 51, node_modules/bootstrap/scss/_badge.scss */
+.badge-warning {
+ color: #ffffff;
+ background-color: #464746;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.badge-warning:hover, a.badge-warning:focus {
+ color: #ffffff;
+ background-color: #2d2d2d;
+}
+
+/* line 11, node_modules/bootstrap/scss/mixins/_badge.scss */
+a.badge-warning:focus, a.badge-warning.focus {
+ outline: 0;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(70, 71, 70, 0.5);
+}
+
+/* line 51, node_modules/bootstrap/scss/_badge.scss */
+.badge-danger {
+ color: #ffffff;
+ background-color: #e54a19;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.badge-danger:hover, a.badge-danger:focus {
+ color: #ffffff;
+ background-color: #b73b14;
+}
+
+/* line 11, node_modules/bootstrap/scss/mixins/_badge.scss */
+a.badge-danger:focus, a.badge-danger.focus {
+ outline: 0;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(229, 74, 25, 0.5);
+}
+
+/* line 51, node_modules/bootstrap/scss/_badge.scss */
+.badge-light {
+ color: #464746;
+ background-color: #f7f7f7;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.badge-light:hover, a.badge-light:focus {
+ color: #464746;
+ background-color: #dedede;
+}
+
+/* line 11, node_modules/bootstrap/scss/mixins/_badge.scss */
+a.badge-light:focus, a.badge-light.focus {
+ outline: 0;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(247, 247, 247, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(247, 247, 247, 0.5);
+}
+
+/* line 51, node_modules/bootstrap/scss/_badge.scss */
+.badge-dark {
+ color: #ffffff;
+ background-color: #343a40;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.badge-dark:hover, a.badge-dark:focus {
+ color: #ffffff;
+ background-color: #1d2124;
+}
+
+/* line 11, node_modules/bootstrap/scss/mixins/_badge.scss */
+a.badge-dark:focus, a.badge-dark.focus {
+ outline: 0;
+ -webkit-box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);
+}
+
+/* line 1, node_modules/bootstrap/scss/_jumbotron.scss */
+.jumbotron {
+ padding: 2rem 1rem;
+ margin-bottom: 2rem;
+ background-color: #eaebea;
+ border-radius: 0.3rem;
+}
+
+@media (min-width: 576px) {
+ /* line 1, node_modules/bootstrap/scss/_jumbotron.scss */
+ .jumbotron {
+ padding: 4rem 2rem;
+ }
+}
+
+/* line 13, node_modules/bootstrap/scss/_jumbotron.scss */
+.jumbotron-fluid {
+ padding-right: 0;
+ padding-left: 0;
+ border-radius: 0;
+}
+
+/* line 5, node_modules/bootstrap/scss/_alert.scss */
+.alert {
+ position: relative;
+ padding: 0.75rem 1.25rem;
+ margin-bottom: 1rem;
+ border: 1px solid transparent;
+ border-radius: 0.25rem;
+}
+
+/* line 14, node_modules/bootstrap/scss/_alert.scss */
+.alert-heading {
+ color: inherit;
+}
+
+/* line 20, node_modules/bootstrap/scss/_alert.scss */
+.alert-link {
+ font-weight: 700;
+}
+
+/* line 29, node_modules/bootstrap/scss/_alert.scss */
+.alert-dismissible {
+ padding-right: 4rem;
+}
+
+/* line 33, node_modules/bootstrap/scss/_alert.scss */
+.alert-dismissible .close {
+ position: absolute;
+ top: 0;
+ right: 0;
+ padding: 0.75rem 1.25rem;
+ color: inherit;
+}
+
+/* line 48, node_modules/bootstrap/scss/_alert.scss */
+.alert-primary {
+ color: #242524;
+ background-color: #dadada;
+ border-color: #cbcbcb;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-primary hr {
+ border-top-color: #bebebe;
+}
+
+/* line 10, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-primary .alert-link {
+ color: #0b0b0b;
+}
+
+/* line 48, node_modules/bootstrap/scss/_alert.scss */
+.alert-secondary {
+ color: #7d7a76;
+ background-color: #fcfbf9;
+ border-color: #fbf9f7;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-secondary hr {
+ border-top-color: #f3ece6;
+}
+
+/* line 10, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-secondary .alert-link {
+ color: #63605d;
+}
+
+/* line 48, node_modules/bootstrap/scss/_alert.scss */
+.alert-success {
+ color: #7d7a76;
+ background-color: #fcfbf9;
+ border-color: #fbf9f7;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-success hr {
+ border-top-color: #f3ece6;
+}
+
+/* line 10, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-success .alert-link {
+ color: #63605d;
+}
+
+/* line 48, node_modules/bootstrap/scss/_alert.scss */
+.alert-info {
+ color: #242524;
+ background-color: #dadada;
+ border-color: #cbcbcb;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-info hr {
+ border-top-color: #bebebe;
+}
+
+/* line 10, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-info .alert-link {
+ color: #0b0b0b;
+}
+
+/* line 48, node_modules/bootstrap/scss/_alert.scss */
+.alert-warning {
+ color: #242524;
+ background-color: #dadada;
+ border-color: #cbcbcb;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-warning hr {
+ border-top-color: #bebebe;
+}
+
+/* line 10, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-warning .alert-link {
+ color: #0b0b0b;
+}
+
+/* line 48, node_modules/bootstrap/scss/_alert.scss */
+.alert-danger {
+ color: #77260d;
+ background-color: #fadbd1;
+ border-color: #f8ccbf;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-danger hr {
+ border-top-color: #f5baa8;
+}
+
+/* line 10, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-danger .alert-link {
+ color: #491708;
+}
+
+/* line 48, node_modules/bootstrap/scss/_alert.scss */
+.alert-light {
+ color: gray;
+ background-color: #fdfdfd;
+ border-color: #fdfdfd;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-light hr {
+ border-top-color: #f0f0f0;
+}
+
+/* line 10, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-light .alert-link {
+ color: #676767;
+}
+
+/* line 48, node_modules/bootstrap/scss/_alert.scss */
+.alert-dark {
+ color: #1b1e21;
+ background-color: #d6d8d9;
+ border-color: #c6c8ca;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-dark hr {
+ border-top-color: #b9bbbe;
+}
+
+/* line 10, node_modules/bootstrap/scss/mixins/_alert.scss */
+.alert-dark .alert-link {
+ color: #040505;
+}
+
+@-webkit-keyframes progress-bar-stripes {
+ from {
+ background-position: 1rem 0;
+ }
+ to {
+ background-position: 0 0;
+ }
+}
+
+@keyframes progress-bar-stripes {
+ from {
+ background-position: 1rem 0;
+ }
+ to {
+ background-position: 0 0;
+ }
+}
+
+/* line 9, node_modules/bootstrap/scss/_progress.scss */
+.progress {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ height: 1rem;
+ overflow: hidden;
+ line-height: 0;
+ font-size: 0.75rem;
+ background-color: #eaebea;
+ border-radius: 0.25rem;
+}
+
+/* line 20, node_modules/bootstrap/scss/_progress.scss */
+.progress-bar {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-orient: vertical;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ -webkit-box-pack: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+ overflow: hidden;
+ color: #ffffff;
+ text-align: center;
+ white-space: nowrap;
+ background-color: #464746;
+ -webkit-transition: width 0.6s ease;
+ transition: width 0.6s ease;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ /* line 20, node_modules/bootstrap/scss/_progress.scss */
+ .progress-bar {
+ -webkit-transition: none;
+ transition: none;
+ }
+}
+
+/* line 32, node_modules/bootstrap/scss/_progress.scss */
+.progress-bar-striped {
+ background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-size: 1rem 1rem;
+}
+
+/* line 38, node_modules/bootstrap/scss/_progress.scss */
+.progress-bar-animated {
+ -webkit-animation: progress-bar-stripes 1s linear infinite;
+ animation: progress-bar-stripes 1s linear infinite;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ /* line 38, node_modules/bootstrap/scss/_progress.scss */
+ .progress-bar-animated {
+ -webkit-animation: none;
+ animation: none;
+ }
+}
+
+/* line 1, node_modules/bootstrap/scss/_media.scss */
+.media {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-align: start;
+ -ms-flex-align: start;
+ align-items: flex-start;
+}
+
+/* line 6, node_modules/bootstrap/scss/_media.scss */
+.media-body {
+ -webkit-box-flex: 1;
+ -ms-flex: 1;
+ flex: 1;
+}
+
+/* line 5, node_modules/bootstrap/scss/_list-group.scss */
+.list-group {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-orient: vertical;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ padding-left: 0;
+ margin-bottom: 0;
+ border-radius: 0.25rem;
+}
+
+/* line 21, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-item-action {
+ width: 100%;
+ color: #495057;
+ text-align: inherit;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.list-group-item-action:hover, .list-group-item-action:focus {
+ z-index: 1;
+ color: #495057;
+ text-decoration: none;
+ background-color: #f7f7f7;
+}
+
+/* line 34, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-item-action:active {
+ color: #464746;
+ background-color: #eaebea;
+}
+
+/* line 45, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-item {
+ position: relative;
+ display: block;
+ padding: 0.75rem 1.25rem;
+ background-color: #ffffff;
+ border: 1px solid rgba(0, 0, 0, 0.125);
+}
+
+/* line 54, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-item:first-child {
+ border-top-left-radius: inherit;
+ border-top-right-radius: inherit;
+}
+
+/* line 58, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-item:last-child {
+ border-bottom-right-radius: inherit;
+ border-bottom-left-radius: inherit;
+}
+
+/* line 62, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-item.disabled, .list-group-item:disabled {
+ color: #6c757d;
+ pointer-events: none;
+ background-color: #ffffff;
+}
+
+/* line 70, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-item.active {
+ z-index: 2;
+ color: #ffffff;
+ background-color: #464746;
+ border-color: #464746;
+}
+
+/* line 77, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-item + .list-group-item {
+ border-top-width: 0;
+}
+
+/* line 80, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-item + .list-group-item.active {
+ margin-top: -1px;
+ border-top-width: 1px;
+}
+
+/* line 96, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-horizontal {
+ -webkit-box-orient: horizontal;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: row;
+ flex-direction: row;
+}
+
+/* line 100, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-horizontal > .list-group-item:first-child {
+ border-bottom-left-radius: 0.25rem;
+ border-top-right-radius: 0;
+}
+
+/* line 105, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-horizontal > .list-group-item:last-child {
+ border-top-right-radius: 0.25rem;
+ border-bottom-left-radius: 0;
+}
+
+/* line 110, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-horizontal > .list-group-item.active {
+ margin-top: 0;
+}
+
+/* line 114, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-horizontal > .list-group-item + .list-group-item {
+ border-top-width: 1px;
+ border-left-width: 0;
+}
+
+/* line 118, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-horizontal > .list-group-item + .list-group-item.active {
+ margin-left: -1px;
+ border-left-width: 1px;
+}
+
+@media (min-width: 576px) {
+ /* line 96, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-sm {
+ -webkit-box-orient: horizontal;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ }
+ /* line 100, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-sm > .list-group-item:first-child {
+ border-bottom-left-radius: 0.25rem;
+ border-top-right-radius: 0;
+ }
+ /* line 105, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-sm > .list-group-item:last-child {
+ border-top-right-radius: 0.25rem;
+ border-bottom-left-radius: 0;
+ }
+ /* line 110, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-sm > .list-group-item.active {
+ margin-top: 0;
+ }
+ /* line 114, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-sm > .list-group-item + .list-group-item {
+ border-top-width: 1px;
+ border-left-width: 0;
+ }
+ /* line 118, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-sm > .list-group-item + .list-group-item.active {
+ margin-left: -1px;
+ border-left-width: 1px;
+ }
+}
+
+@media (min-width: 768px) {
+ /* line 96, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-md {
+ -webkit-box-orient: horizontal;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ }
+ /* line 100, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-md > .list-group-item:first-child {
+ border-bottom-left-radius: 0.25rem;
+ border-top-right-radius: 0;
+ }
+ /* line 105, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-md > .list-group-item:last-child {
+ border-top-right-radius: 0.25rem;
+ border-bottom-left-radius: 0;
+ }
+ /* line 110, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-md > .list-group-item.active {
+ margin-top: 0;
+ }
+ /* line 114, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-md > .list-group-item + .list-group-item {
+ border-top-width: 1px;
+ border-left-width: 0;
+ }
+ /* line 118, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-md > .list-group-item + .list-group-item.active {
+ margin-left: -1px;
+ border-left-width: 1px;
+ }
+}
+
+@media (min-width: 1024px) {
+ /* line 96, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-lg {
+ -webkit-box-orient: horizontal;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ }
+ /* line 100, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-lg > .list-group-item:first-child {
+ border-bottom-left-radius: 0.25rem;
+ border-top-right-radius: 0;
+ }
+ /* line 105, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-lg > .list-group-item:last-child {
+ border-top-right-radius: 0.25rem;
+ border-bottom-left-radius: 0;
+ }
+ /* line 110, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-lg > .list-group-item.active {
+ margin-top: 0;
+ }
+ /* line 114, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-lg > .list-group-item + .list-group-item {
+ border-top-width: 1px;
+ border-left-width: 0;
+ }
+ /* line 118, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-lg > .list-group-item + .list-group-item.active {
+ margin-left: -1px;
+ border-left-width: 1px;
+ }
+}
+
+@media (min-width: 1280px) {
+ /* line 96, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-xl {
+ -webkit-box-orient: horizontal;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ }
+ /* line 100, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-xl > .list-group-item:first-child {
+ border-bottom-left-radius: 0.25rem;
+ border-top-right-radius: 0;
+ }
+ /* line 105, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-xl > .list-group-item:last-child {
+ border-top-right-radius: 0.25rem;
+ border-bottom-left-radius: 0;
+ }
+ /* line 110, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-xl > .list-group-item.active {
+ margin-top: 0;
+ }
+ /* line 114, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-xl > .list-group-item + .list-group-item {
+ border-top-width: 1px;
+ border-left-width: 0;
+ }
+ /* line 118, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-xl > .list-group-item + .list-group-item.active {
+ margin-left: -1px;
+ border-left-width: 1px;
+ }
+}
+
+@media (min-width: 1440px) {
+ /* line 96, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-xxl {
+ -webkit-box-orient: horizontal;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ }
+ /* line 100, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-xxl > .list-group-item:first-child {
+ border-bottom-left-radius: 0.25rem;
+ border-top-right-radius: 0;
+ }
+ /* line 105, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-xxl > .list-group-item:last-child {
+ border-top-right-radius: 0.25rem;
+ border-bottom-left-radius: 0;
+ }
+ /* line 110, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-xxl > .list-group-item.active {
+ margin-top: 0;
+ }
+ /* line 114, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-xxl > .list-group-item + .list-group-item {
+ border-top-width: 1px;
+ border-left-width: 0;
+ }
+ /* line 118, node_modules/bootstrap/scss/_list-group.scss */
+ .list-group-horizontal-xxl > .list-group-item + .list-group-item.active {
+ margin-left: -1px;
+ border-left-width: 1px;
+ }
+}
+
+/* line 134, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-flush {
+ border-radius: 0;
+}
+
+/* line 137, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-flush > .list-group-item {
+ border-width: 0 0 1px;
+}
+
+/* line 140, node_modules/bootstrap/scss/_list-group.scss */
+.list-group-flush > .list-group-item:last-child {
+ border-bottom-width: 0;
+}
+
+/* line 4, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-primary {
+ color: #242524;
+ background-color: #cbcbcb;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus {
+ color: #242524;
+ background-color: #bebebe;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-primary.list-group-item-action.active {
+ color: #ffffff;
+ background-color: #242524;
+ border-color: #242524;
+}
+
+/* line 4, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-secondary {
+ color: #7d7a76;
+ background-color: #fbf9f7;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus {
+ color: #7d7a76;
+ background-color: #f3ece6;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-secondary.list-group-item-action.active {
+ color: #ffffff;
+ background-color: #7d7a76;
+ border-color: #7d7a76;
+}
+
+/* line 4, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-success {
+ color: #7d7a76;
+ background-color: #fbf9f7;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus {
+ color: #7d7a76;
+ background-color: #f3ece6;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-success.list-group-item-action.active {
+ color: #ffffff;
+ background-color: #7d7a76;
+ border-color: #7d7a76;
+}
+
+/* line 4, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-info {
+ color: #242524;
+ background-color: #cbcbcb;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus {
+ color: #242524;
+ background-color: #bebebe;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-info.list-group-item-action.active {
+ color: #ffffff;
+ background-color: #242524;
+ border-color: #242524;
+}
+
+/* line 4, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-warning {
+ color: #242524;
+ background-color: #cbcbcb;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus {
+ color: #242524;
+ background-color: #bebebe;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-warning.list-group-item-action.active {
+ color: #ffffff;
+ background-color: #242524;
+ border-color: #242524;
+}
+
+/* line 4, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-danger {
+ color: #77260d;
+ background-color: #f8ccbf;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus {
+ color: #77260d;
+ background-color: #f5baa8;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-danger.list-group-item-action.active {
+ color: #ffffff;
+ background-color: #77260d;
+ border-color: #77260d;
+}
+
+/* line 4, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-light {
+ color: gray;
+ background-color: #fdfdfd;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus {
+ color: gray;
+ background-color: #f0f0f0;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-light.list-group-item-action.active {
+ color: #ffffff;
+ background-color: gray;
+ border-color: gray;
+}
+
+/* line 4, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-dark {
+ color: #1b1e21;
+ background-color: #c6c8ca;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus {
+ color: #1b1e21;
+ background-color: #b9bbbe;
+}
+
+/* line 14, node_modules/bootstrap/scss/mixins/_list-group.scss */
+.list-group-item-dark.list-group-item-action.active {
+ color: #ffffff;
+ background-color: #1b1e21;
+ border-color: #1b1e21;
+}
+
+/* line 1, node_modules/bootstrap/scss/_close.scss */
+.close {
+ float: right;
+ font-size: 1.5rem;
+ font-weight: 700;
+ line-height: 1;
+ color: #000000;
+ text-shadow: 0 1px 0 #ffffff;
+ opacity: .5;
+}
+
+/* line 13, node_modules/bootstrap/scss/mixins/_hover.scss */
+.close:hover {
+ color: #000000;
+ text-decoration: none;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.close:not(:disabled):not(.disabled):hover, .close:not(:disabled):not(.disabled):focus {
+ opacity: .75;
+}
+
+/* line 29, node_modules/bootstrap/scss/_close.scss */
+button.close {
+ padding: 0;
+ background-color: transparent;
+ border: 0;
+}
+
+/* line 38, node_modules/bootstrap/scss/_close.scss */
+a.close.disabled {
+ pointer-events: none;
+}
+
+/* line 1, node_modules/bootstrap/scss/_toasts.scss */
+.toast {
+ -ms-flex-preferred-size: 350px;
+ flex-basis: 350px;
+ max-width: 350px;
+ font-size: 0.875rem;
+ background-color: rgba(255, 255, 255, 0.85);
+ background-clip: padding-box;
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ -webkit-box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1);
+ box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1);
+ opacity: 0;
+ border-radius: 0.25rem;
+}
+
+/* line 15, node_modules/bootstrap/scss/_toasts.scss */
+.toast:not(:last-child) {
+ margin-bottom: 0.75rem;
+}
+
+/* line 19, node_modules/bootstrap/scss/_toasts.scss */
+.toast.showing {
+ opacity: 1;
+}
+
+/* line 23, node_modules/bootstrap/scss/_toasts.scss */
+.toast.show {
+ display: block;
+ opacity: 1;
+}
+
+/* line 28, node_modules/bootstrap/scss/_toasts.scss */
+.toast.hide {
+ display: none;
+}
+
+/* line 33, node_modules/bootstrap/scss/_toasts.scss */
+.toast-header {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ padding: 0.25rem 0.75rem;
+ color: #6c757d;
+ background-color: rgba(255, 255, 255, 0.85);
+ background-clip: padding-box;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
+ border-top-left-radius: calc(0.25rem - 1px);
+ border-top-right-radius: calc(0.25rem - 1px);
+}
+
+/* line 44, node_modules/bootstrap/scss/_toasts.scss */
+.toast-body {
+ padding: 0.75rem;
+}
+
+/* line 7, node_modules/bootstrap/scss/_modal.scss */
+.modal-open {
+ overflow: hidden;
+}
+
+/* line 11, node_modules/bootstrap/scss/_modal.scss */
+.modal-open .modal {
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+/* line 18, node_modules/bootstrap/scss/_modal.scss */
+.modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 1050;
+ display: none;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ outline: 0;
+}
+
+/* line 36, node_modules/bootstrap/scss/_modal.scss */
+.modal-dialog {
+ position: relative;
+ width: auto;
+ margin: 0.5rem;
+ pointer-events: none;
+}
+
+/* line 44, node_modules/bootstrap/scss/_modal.scss */
+.modal.fade .modal-dialog {
+ -webkit-transition: -webkit-transform 0.3s ease-out;
+ transition: -webkit-transform 0.3s ease-out;
+ transition: transform 0.3s ease-out;
+ transition: transform 0.3s ease-out, -webkit-transform 0.3s ease-out;
+ -webkit-transform: translate(0, -50px);
+ transform: translate(0, -50px);
+}
+
+@media (prefers-reduced-motion: reduce) {
+ /* line 44, node_modules/bootstrap/scss/_modal.scss */
+ .modal.fade .modal-dialog {
+ -webkit-transition: none;
+ transition: none;
+ }
+}
+
+/* line 48, node_modules/bootstrap/scss/_modal.scss */
+.modal.show .modal-dialog {
+ -webkit-transform: none;
+ transform: none;
+}
+
+/* line 53, node_modules/bootstrap/scss/_modal.scss */
+.modal.modal-static .modal-dialog {
+ -webkit-transform: scale(1.02);
+ transform: scale(1.02);
+}
+
+/* line 58, node_modules/bootstrap/scss/_modal.scss */
+.modal-dialog-scrollable {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ max-height: calc(100% - 1rem);
+}
+
+/* line 62, node_modules/bootstrap/scss/_modal.scss */
+.modal-dialog-scrollable .modal-content {
+ max-height: calc(100vh - 1rem);
+ overflow: hidden;
+}
+
+/* line 67, node_modules/bootstrap/scss/_modal.scss */
+.modal-dialog-scrollable .modal-header,
+.modal-dialog-scrollable .modal-footer {
+ -ms-flex-negative: 0;
+ flex-shrink: 0;
+}
+
+/* line 72, node_modules/bootstrap/scss/_modal.scss */
+.modal-dialog-scrollable .modal-body {
+ overflow-y: auto;
+}
+
+/* line 77, node_modules/bootstrap/scss/_modal.scss */
+.modal-dialog-centered {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ min-height: calc(100% - 1rem);
+}
+
+/* line 83, node_modules/bootstrap/scss/_modal.scss */
+.modal-dialog-centered::before {
+ display: block;
+ height: calc(100vh - 1rem);
+ height: -webkit-min-content;
+ height: -moz-min-content;
+ height: min-content;
+ content: "";
+}
+
+/* line 91, node_modules/bootstrap/scss/_modal.scss */
+.modal-dialog-centered.modal-dialog-scrollable {
+ -webkit-box-orient: vertical;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ -webkit-box-pack: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+ height: 100%;
+}
+
+/* line 96, node_modules/bootstrap/scss/_modal.scss */
+.modal-dialog-centered.modal-dialog-scrollable .modal-content {
+ max-height: none;
+}
+
+/* line 100, node_modules/bootstrap/scss/_modal.scss */
+.modal-dialog-centered.modal-dialog-scrollable::before {
+ content: none;
+}
+
+/* line 107, node_modules/bootstrap/scss/_modal.scss */
+.modal-content {
+ position: relative;
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-orient: vertical;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ width: 100%;
+ pointer-events: auto;
+ background-color: #ffffff;
+ background-clip: padding-box;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ border-radius: 0.3rem;
+ outline: 0;
+}
+
+/* line 125, node_modules/bootstrap/scss/_modal.scss */
+.modal-backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 1040;
+ width: 100vw;
+ height: 100vh;
+ background-color: #000000;
+}
+
+/* line 135, node_modules/bootstrap/scss/_modal.scss */
+.modal-backdrop.fade {
+ opacity: 0;
+}
+
+/* line 136, node_modules/bootstrap/scss/_modal.scss */
+.modal-backdrop.show {
+ opacity: 0.5;
+}
+
+/* line 141, node_modules/bootstrap/scss/_modal.scss */
+.modal-header {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-align: start;
+ -ms-flex-align: start;
+ align-items: flex-start;
+ -webkit-box-pack: justify;
+ -ms-flex-pack: justify;
+ justify-content: space-between;
+ padding: 1rem 1rem;
+ border-bottom: 1px solid #dee2e6;
+ border-top-left-radius: calc(0.3rem - 1px);
+ border-top-right-radius: calc(0.3rem - 1px);
+}
+
+/* line 149, node_modules/bootstrap/scss/_modal.scss */
+.modal-header .close {
+ padding: 1rem 1rem;
+ margin: -1rem -1rem -1rem auto;
+}
+
+/* line 157, node_modules/bootstrap/scss/_modal.scss */
+.modal-title {
+ margin-bottom: 0;
+ line-height: 1.5;
+}
+
+/* line 164, node_modules/bootstrap/scss/_modal.scss */
+.modal-body {
+ position: relative;
+ -webkit-box-flex: 1;
+ -ms-flex: 1 1 auto;
+ flex: 1 1 auto;
+ padding: 1rem;
+}
+
+/* line 173, node_modules/bootstrap/scss/_modal.scss */
+.modal-footer {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-box-pack: end;
+ -ms-flex-pack: end;
+ justify-content: flex-end;
+ padding: 0.75rem;
+ border-top: 1px solid #dee2e6;
+ border-bottom-right-radius: calc(0.3rem - 1px);
+ border-bottom-left-radius: calc(0.3rem - 1px);
+}
+
+/* line 185, node_modules/bootstrap/scss/_modal.scss */
+.modal-footer > * {
+ margin: 0.25rem;
+}
+
+/* line 191, node_modules/bootstrap/scss/_modal.scss */
+.modal-scrollbar-measure {
+ position: absolute;
+ top: -9999px;
+ width: 50px;
+ height: 50px;
+ overflow: scroll;
+}
+
+@media (min-width: 576px) {
+ /* line 202, node_modules/bootstrap/scss/_modal.scss */
+ .modal-dialog {
+ max-width: 500px;
+ margin: 1.75rem auto;
+ }
+ /* line 207, node_modules/bootstrap/scss/_modal.scss */
+ .modal-dialog-scrollable {
+ max-height: calc(100% - 3.5rem);
+ }
+ /* line 210, node_modules/bootstrap/scss/_modal.scss */
+ .modal-dialog-scrollable .modal-content {
+ max-height: calc(100vh - 3.5rem);
+ }
+ /* line 215, node_modules/bootstrap/scss/_modal.scss */
+ .modal-dialog-centered {
+ min-height: calc(100% - 3.5rem);
+ }
+ /* line 218, node_modules/bootstrap/scss/_modal.scss */
+ .modal-dialog-centered::before {
+ height: calc(100vh - 3.5rem);
+ height: -webkit-min-content;
+ height: -moz-min-content;
+ height: min-content;
+ }
+ /* line 228, node_modules/bootstrap/scss/_modal.scss */
+ .modal-sm {
+ max-width: 300px;
+ }
+}
+
+@media (min-width: 1024px) {
+ /* line 232, node_modules/bootstrap/scss/_modal.scss */
+ .modal-lg,
+ .modal-xl {
+ max-width: 800px;
+ }
+}
+
+@media (min-width: 1280px) {
+ /* line 239, node_modules/bootstrap/scss/_modal.scss */
+ .modal-xl {
+ max-width: 1140px;
+ }
+}
+
+/* line 2, node_modules/bootstrap/scss/_tooltip.scss */
+.tooltip {
+ position: absolute;
+ z-index: 1070;
+ display: block;
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ font-style: normal;
+ font-weight: 400;
+ line-height: 1.5;
+ text-align: left;
+ text-align: start;
+ text-decoration: none;
+ text-shadow: none;
+ text-transform: none;
+ letter-spacing: normal;
+ word-break: normal;
+ word-spacing: normal;
+ white-space: normal;
+ line-break: auto;
+ font-size: 0.875rem;
+ word-wrap: break-word;
+ opacity: 0;
+}
+
+/* line 15, node_modules/bootstrap/scss/_tooltip.scss */
+.tooltip.show {
+ opacity: 0.9;
+}
+
+/* line 17, node_modules/bootstrap/scss/_tooltip.scss */
+.tooltip .arrow {
+ position: absolute;
+ display: block;
+ width: 0.8rem;
+ height: 0.4rem;
+}
+
+/* line 23, node_modules/bootstrap/scss/_tooltip.scss */
+.tooltip .arrow::before {
+ position: absolute;
+ content: "";
+ border-color: transparent;
+ border-style: solid;
+}
+
+/* line 32, node_modules/bootstrap/scss/_tooltip.scss */
+.bs-tooltip-top, .bs-tooltip-auto[x-placement^="top"] {
+ padding: 0.4rem 0;
+}
+
+/* line 35, node_modules/bootstrap/scss/_tooltip.scss */
+.bs-tooltip-top .arrow, .bs-tooltip-auto[x-placement^="top"] .arrow {
+ bottom: 0;
+}
+
+/* line 38, node_modules/bootstrap/scss/_tooltip.scss */
+.bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^="top"] .arrow::before {
+ top: 0;
+ border-width: 0.4rem 0.4rem 0;
+ border-top-color: #000000;
+}
+
+/* line 46, node_modules/bootstrap/scss/_tooltip.scss */
+.bs-tooltip-right, .bs-tooltip-auto[x-placement^="right"] {
+ padding: 0 0.4rem;
+}
+
+/* line 49, node_modules/bootstrap/scss/_tooltip.scss */
+.bs-tooltip-right .arrow, .bs-tooltip-auto[x-placement^="right"] .arrow {
+ left: 0;
+ width: 0.4rem;
+ height: 0.8rem;
+}
+
+/* line 54, node_modules/bootstrap/scss/_tooltip.scss */
+.bs-tooltip-right .arrow::before, .bs-tooltip-auto[x-placement^="right"] .arrow::before {
+ right: 0;
+ border-width: 0.4rem 0.4rem 0.4rem 0;
+ border-right-color: #000000;
+}
+
+/* line 62, node_modules/bootstrap/scss/_tooltip.scss */
+.bs-tooltip-bottom, .bs-tooltip-auto[x-placement^="bottom"] {
+ padding: 0.4rem 0;
+}
+
+/* line 65, node_modules/bootstrap/scss/_tooltip.scss */
+.bs-tooltip-bottom .arrow, .bs-tooltip-auto[x-placement^="bottom"] .arrow {
+ top: 0;
+}
+
+/* line 68, node_modules/bootstrap/scss/_tooltip.scss */
+.bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^="bottom"] .arrow::before {
+ bottom: 0;
+ border-width: 0 0.4rem 0.4rem;
+ border-bottom-color: #000000;
+}
+
+/* line 76, node_modules/bootstrap/scss/_tooltip.scss */
+.bs-tooltip-left, .bs-tooltip-auto[x-placement^="left"] {
+ padding: 0 0.4rem;
+}
+
+/* line 79, node_modules/bootstrap/scss/_tooltip.scss */
+.bs-tooltip-left .arrow, .bs-tooltip-auto[x-placement^="left"] .arrow {
+ right: 0;
+ width: 0.4rem;
+ height: 0.8rem;
+}
+
+/* line 84, node_modules/bootstrap/scss/_tooltip.scss */
+.bs-tooltip-left .arrow::before, .bs-tooltip-auto[x-placement^="left"] .arrow::before {
+ left: 0;
+ border-width: 0.4rem 0 0.4rem 0.4rem;
+ border-left-color: #000000;
+}
+
+/* line 108, node_modules/bootstrap/scss/_tooltip.scss */
+.tooltip-inner {
+ max-width: 200px;
+ padding: 0.25rem 0.5rem;
+ color: #ffffff;
+ text-align: center;
+ background-color: #000000;
+ border-radius: 0.25rem;
+}
+
+/* line 1, node_modules/bootstrap/scss/_popover.scss */
+.popover {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 1060;
+ display: block;
+ max-width: 276px;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ font-style: normal;
+ font-weight: 400;
+ line-height: 1.5;
+ text-align: left;
+ text-align: start;
+ text-decoration: none;
+ text-shadow: none;
+ text-transform: none;
+ letter-spacing: normal;
+ word-break: normal;
+ word-spacing: normal;
+ white-space: normal;
+ line-break: auto;
+ font-size: 0.875rem;
+ word-wrap: break-word;
+ background-color: #ffffff;
+ background-clip: padding-box;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ border-radius: 0.3rem;
+}
+
+/* line 20, node_modules/bootstrap/scss/_popover.scss */
+.popover .arrow {
+ position: absolute;
+ display: block;
+ width: 1rem;
+ height: 0.5rem;
+ margin: 0 0.3rem;
+}
+
+/* line 27, node_modules/bootstrap/scss/_popover.scss */
+.popover .arrow::before, .popover .arrow::after {
+ position: absolute;
+ display: block;
+ content: "";
+ border-color: transparent;
+ border-style: solid;
+}
+
+/* line 38, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-top, .bs-popover-auto[x-placement^="top"] {
+ margin-bottom: 0.5rem;
+}
+
+/* line 41, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-top > .arrow, .bs-popover-auto[x-placement^="top"] > .arrow {
+ bottom: calc(-0.5rem - 1px);
+}
+
+/* line 44, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-top > .arrow::before, .bs-popover-auto[x-placement^="top"] > .arrow::before {
+ bottom: 0;
+ border-width: 0.5rem 0.5rem 0;
+ border-top-color: rgba(0, 0, 0, 0.25);
+}
+
+/* line 50, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-top > .arrow::after, .bs-popover-auto[x-placement^="top"] > .arrow::after {
+ bottom: 1px;
+ border-width: 0.5rem 0.5rem 0;
+ border-top-color: #ffffff;
+}
+
+/* line 58, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-right, .bs-popover-auto[x-placement^="right"] {
+ margin-left: 0.5rem;
+}
+
+/* line 61, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-right > .arrow, .bs-popover-auto[x-placement^="right"] > .arrow {
+ left: calc(-0.5rem - 1px);
+ width: 0.5rem;
+ height: 1rem;
+ margin: 0.3rem 0;
+}
+
+/* line 67, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-right > .arrow::before, .bs-popover-auto[x-placement^="right"] > .arrow::before {
+ left: 0;
+ border-width: 0.5rem 0.5rem 0.5rem 0;
+ border-right-color: rgba(0, 0, 0, 0.25);
+}
+
+/* line 73, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-right > .arrow::after, .bs-popover-auto[x-placement^="right"] > .arrow::after {
+ left: 1px;
+ border-width: 0.5rem 0.5rem 0.5rem 0;
+ border-right-color: #ffffff;
+}
+
+/* line 81, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-bottom, .bs-popover-auto[x-placement^="bottom"] {
+ margin-top: 0.5rem;
+}
+
+/* line 84, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-bottom > .arrow, .bs-popover-auto[x-placement^="bottom"] > .arrow {
+ top: calc(-0.5rem - 1px);
+}
+
+/* line 87, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-bottom > .arrow::before, .bs-popover-auto[x-placement^="bottom"] > .arrow::before {
+ top: 0;
+ border-width: 0 0.5rem 0.5rem 0.5rem;
+ border-bottom-color: rgba(0, 0, 0, 0.25);
+}
+
+/* line 93, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-bottom > .arrow::after, .bs-popover-auto[x-placement^="bottom"] > .arrow::after {
+ top: 1px;
+ border-width: 0 0.5rem 0.5rem 0.5rem;
+ border-bottom-color: #ffffff;
+}
+
+/* line 101, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^="bottom"] .popover-header::before {
+ position: absolute;
+ top: 0;
+ left: 50%;
+ display: block;
+ width: 1rem;
+ margin-left: -0.5rem;
+ content: "";
+ border-bottom: 1px solid #f7f7f7;
+}
+
+/* line 113, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-left, .bs-popover-auto[x-placement^="left"] {
+ margin-right: 0.5rem;
+}
+
+/* line 116, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-left > .arrow, .bs-popover-auto[x-placement^="left"] > .arrow {
+ right: calc(-0.5rem - 1px);
+ width: 0.5rem;
+ height: 1rem;
+ margin: 0.3rem 0;
+}
+
+/* line 122, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-left > .arrow::before, .bs-popover-auto[x-placement^="left"] > .arrow::before {
+ right: 0;
+ border-width: 0.5rem 0 0.5rem 0.5rem;
+ border-left-color: rgba(0, 0, 0, 0.25);
+}
+
+/* line 128, node_modules/bootstrap/scss/_popover.scss */
+.bs-popover-left > .arrow::after, .bs-popover-auto[x-placement^="left"] > .arrow::after {
+ right: 1px;
+ border-width: 0.5rem 0 0.5rem 0.5rem;
+ border-left-color: #ffffff;
+}
+
+/* line 153, node_modules/bootstrap/scss/_popover.scss */
+.popover-header {
+ padding: 0.5rem 0.75rem;
+ margin-bottom: 0;
+ font-size: 1rem;
+ background-color: #f7f7f7;
+ border-bottom: 1px solid #ebebeb;
+ border-top-left-radius: calc(0.3rem - 1px);
+ border-top-right-radius: calc(0.3rem - 1px);
+}
+
+/* line 162, node_modules/bootstrap/scss/_popover.scss */
+.popover-header:empty {
+ display: none;
+}
+
+/* line 167, node_modules/bootstrap/scss/_popover.scss */
+.popover-body {
+ padding: 0.5rem 0.75rem;
+ color: #464746;
+}
+
+/* line 14, node_modules/bootstrap/scss/_carousel.scss */
+.carousel {
+ position: relative;
+}
+
+/* line 18, node_modules/bootstrap/scss/_carousel.scss */
+.carousel.pointer-event {
+ -ms-touch-action: pan-y;
+ touch-action: pan-y;
+}
+
+/* line 22, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-inner {
+ position: relative;
+ width: 100%;
+ overflow: hidden;
+}
+
+/* line 2, node_modules/bootstrap/scss/mixins/_clearfix.scss */
+.carousel-inner::after {
+ display: block;
+ clear: both;
+ content: "";
+}
+
+/* line 29, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-item {
+ position: relative;
+ display: none;
+ float: left;
+ width: 100%;
+ margin-right: -100%;
+ -webkit-backface-visibility: hidden;
+ backface-visibility: hidden;
+ -webkit-transition: -webkit-transform 0.6s ease-in-out;
+ transition: -webkit-transform 0.6s ease-in-out;
+ transition: transform 0.6s ease-in-out;
+ transition: transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ /* line 29, node_modules/bootstrap/scss/_carousel.scss */
+ .carousel-item {
+ -webkit-transition: none;
+ transition: none;
+ }
+}
+
+/* line 39, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-item.active,
+.carousel-item-next,
+.carousel-item-prev {
+ display: block;
+}
+
+/* line 45, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-item-next:not(.carousel-item-left),
+.active.carousel-item-right {
+ -webkit-transform: translateX(100%);
+ transform: translateX(100%);
+}
+
+/* line 50, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-item-prev:not(.carousel-item-right),
+.active.carousel-item-left {
+ -webkit-transform: translateX(-100%);
+ transform: translateX(-100%);
+}
+
+/* line 61, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-fade .carousel-item {
+ opacity: 0;
+ -webkit-transition-property: opacity;
+ transition-property: opacity;
+ -webkit-transform: none;
+ transform: none;
+}
+
+/* line 67, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-fade .carousel-item.active,
+.carousel-fade .carousel-item-next.carousel-item-left,
+.carousel-fade .carousel-item-prev.carousel-item-right {
+ z-index: 1;
+ opacity: 1;
+}
+
+/* line 74, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-fade .active.carousel-item-left,
+.carousel-fade .active.carousel-item-right {
+ z-index: 0;
+ opacity: 0;
+ -webkit-transition: opacity 0s 0.6s;
+ transition: opacity 0s 0.6s;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ /* line 74, node_modules/bootstrap/scss/_carousel.scss */
+ .carousel-fade .active.carousel-item-left,
+ .carousel-fade .active.carousel-item-right {
+ -webkit-transition: none;
+ transition: none;
+ }
+}
+
+/* line 87, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-control-prev,
+.carousel-control-next {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ z-index: 1;
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-box-pack: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+ width: 15%;
+ color: #ffffff;
+ text-align: center;
+ opacity: 0.5;
+ -webkit-transition: opacity 0.15s ease;
+ transition: opacity 0.15s ease;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ /* line 87, node_modules/bootstrap/scss/_carousel.scss */
+ .carousel-control-prev,
+ .carousel-control-next {
+ -webkit-transition: none;
+ transition: none;
+ }
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+.carousel-control-prev:hover, .carousel-control-prev:focus,
+.carousel-control-next:hover,
+.carousel-control-next:focus {
+ color: #ffffff;
+ text-decoration: none;
+ outline: 0;
+ opacity: 0.9;
+}
+
+/* line 111, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-control-prev {
+ left: 0;
+}
+
+/* line 117, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-control-next {
+ right: 0;
+}
+
+/* line 125, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-control-prev-icon,
+.carousel-control-next-icon {
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ background: no-repeat 50% / 100% 100%;
+}
+
+/* line 132, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-control-prev-icon {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23ffffff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e");
+}
+
+/* line 135, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-control-next-icon {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23ffffff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e");
+}
+
+/* line 145, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-indicators {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 15;
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-pack: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+ padding-left: 0;
+ margin-right: 15%;
+ margin-left: 15%;
+ list-style: none;
+}
+
+/* line 159, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-indicators li {
+ -webkit-box-sizing: content-box;
+ box-sizing: content-box;
+ -webkit-box-flex: 0;
+ -ms-flex: 0 1 auto;
+ flex: 0 1 auto;
+ width: 30px;
+ height: 3px;
+ margin-right: 3px;
+ margin-left: 3px;
+ text-indent: -999px;
+ cursor: pointer;
+ background-color: #ffffff;
+ background-clip: padding-box;
+ border-top: 10px solid transparent;
+ border-bottom: 10px solid transparent;
+ opacity: .5;
+ -webkit-transition: opacity 0.6s ease;
+ transition: opacity 0.6s ease;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ /* line 159, node_modules/bootstrap/scss/_carousel.scss */
+ .carousel-indicators li {
+ -webkit-transition: none;
+ transition: none;
+ }
+}
+
+/* line 177, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-indicators .active {
+ opacity: 1;
+}
+
+/* line 187, node_modules/bootstrap/scss/_carousel.scss */
+.carousel-caption {
+ position: absolute;
+ right: 15%;
+ bottom: 20px;
+ left: 15%;
+ z-index: 10;
+ padding-top: 20px;
+ padding-bottom: 20px;
+ color: #ffffff;
+ text-align: center;
+}
+
+@-webkit-keyframes spinner-border {
+ to {
+ -webkit-transform: rotate(360deg);
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes spinner-border {
+ to {
+ -webkit-transform: rotate(360deg);
+ transform: rotate(360deg);
+ }
+}
+
+/* line 9, node_modules/bootstrap/scss/_spinners.scss */
+.spinner-border {
+ display: inline-block;
+ width: 2rem;
+ height: 2rem;
+ vertical-align: text-bottom;
+ border: 0.25em solid currentColor;
+ border-right-color: transparent;
+ border-radius: 50%;
+ -webkit-animation: spinner-border .75s linear infinite;
+ animation: spinner-border .75s linear infinite;
+}
+
+/* line 21, node_modules/bootstrap/scss/_spinners.scss */
+.spinner-border-sm {
+ width: 1rem;
+ height: 1rem;
+ border-width: 0.2em;
+}
+
+@-webkit-keyframes spinner-grow {
+ 0% {
+ -webkit-transform: scale(0);
+ transform: scale(0);
+ }
+ 50% {
+ opacity: 1;
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+@keyframes spinner-grow {
+ 0% {
+ -webkit-transform: scale(0);
+ transform: scale(0);
+ }
+ 50% {
+ opacity: 1;
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+/* line 41, node_modules/bootstrap/scss/_spinners.scss */
+.spinner-grow {
+ display: inline-block;
+ width: 2rem;
+ height: 2rem;
+ vertical-align: text-bottom;
+ background-color: currentColor;
+ border-radius: 50%;
+ opacity: 0;
+ -webkit-animation: spinner-grow .75s linear infinite;
+ animation: spinner-grow .75s linear infinite;
+}
+
+/* line 53, node_modules/bootstrap/scss/_spinners.scss */
+.spinner-grow-sm {
+ width: 1rem;
+ height: 1rem;
+}
+
+/* line 3, node_modules/bootstrap/scss/utilities/_align.scss */
+.align-baseline {
+ vertical-align: baseline !important;
+}
+
+/* line 4, node_modules/bootstrap/scss/utilities/_align.scss */
+.align-top {
+ vertical-align: top !important;
+}
+
+/* line 5, node_modules/bootstrap/scss/utilities/_align.scss */
+.align-middle {
+ vertical-align: middle !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/utilities/_align.scss */
+.align-bottom {
+ vertical-align: bottom !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_align.scss */
+.align-text-bottom {
+ vertical-align: text-bottom !important;
+}
+
+/* line 8, node_modules/bootstrap/scss/utilities/_align.scss */
+.align-text-top {
+ vertical-align: text-top !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_background-variant.scss */
+.bg-primary {
+ background-color: #464746 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.bg-primary:hover, a.bg-primary:focus,
+button.bg-primary:hover,
+button.bg-primary:focus {
+ background-color: #2d2d2d !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_background-variant.scss */
+.bg-secondary {
+ background-color: #f0ebe3 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.bg-secondary:hover, a.bg-secondary:focus,
+button.bg-secondary:hover,
+button.bg-secondary:focus {
+ background-color: #ded3c2 !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_background-variant.scss */
+.bg-success {
+ background-color: #f0ebe3 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.bg-success:hover, a.bg-success:focus,
+button.bg-success:hover,
+button.bg-success:focus {
+ background-color: #ded3c2 !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_background-variant.scss */
+.bg-info {
+ background-color: #464746 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.bg-info:hover, a.bg-info:focus,
+button.bg-info:hover,
+button.bg-info:focus {
+ background-color: #2d2d2d !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_background-variant.scss */
+.bg-warning {
+ background-color: #464746 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.bg-warning:hover, a.bg-warning:focus,
+button.bg-warning:hover,
+button.bg-warning:focus {
+ background-color: #2d2d2d !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_background-variant.scss */
+.bg-danger {
+ background-color: #e54a19 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.bg-danger:hover, a.bg-danger:focus,
+button.bg-danger:hover,
+button.bg-danger:focus {
+ background-color: #b73b14 !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_background-variant.scss */
+.bg-light {
+ background-color: #f7f7f7 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.bg-light:hover, a.bg-light:focus,
+button.bg-light:hover,
+button.bg-light:focus {
+ background-color: #dedede !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_background-variant.scss */
+.bg-dark {
+ background-color: #343a40 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.bg-dark:hover, a.bg-dark:focus,
+button.bg-dark:hover,
+button.bg-dark:focus {
+ background-color: #1d2124 !important;
+}
+
+/* line 13, node_modules/bootstrap/scss/utilities/_background.scss */
+.bg-white {
+ background-color: #ffffff !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/utilities/_background.scss */
+.bg-transparent {
+ background-color: transparent !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border {
+ border: 1px solid #dee2e6 !important;
+}
+
+/* line 8, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-top {
+ border-top: 1px solid #dee2e6 !important;
+}
+
+/* line 9, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-right {
+ border-right: 1px solid #dee2e6 !important;
+}
+
+/* line 10, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-bottom {
+ border-bottom: 1px solid #dee2e6 !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-left {
+ border-left: 1px solid #dee2e6 !important;
+}
+
+/* line 13, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-0 {
+ border: 0 !important;
+}
+
+/* line 14, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-top-0 {
+ border-top: 0 !important;
+}
+
+/* line 15, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-right-0 {
+ border-right: 0 !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-bottom-0 {
+ border-bottom: 0 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-left-0 {
+ border-left: 0 !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-primary {
+ border-color: #464746 !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-secondary {
+ border-color: #f0ebe3 !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-success {
+ border-color: #f0ebe3 !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-info {
+ border-color: #464746 !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-warning {
+ border-color: #464746 !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-danger {
+ border-color: #e54a19 !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-light {
+ border-color: #f7f7f7 !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-dark {
+ border-color: #343a40 !important;
+}
+
+/* line 25, node_modules/bootstrap/scss/utilities/_borders.scss */
+.border-white {
+ border-color: #ffffff !important;
+}
+
+/* line 33, node_modules/bootstrap/scss/utilities/_borders.scss */
+.rounded-sm {
+ border-radius: 0.2rem !important;
+}
+
+/* line 37, node_modules/bootstrap/scss/utilities/_borders.scss */
+.rounded {
+ border-radius: 0.25rem !important;
+}
+
+/* line 41, node_modules/bootstrap/scss/utilities/_borders.scss */
+.rounded-top {
+ border-top-left-radius: 0.25rem !important;
+ border-top-right-radius: 0.25rem !important;
+}
+
+/* line 46, node_modules/bootstrap/scss/utilities/_borders.scss */
+.rounded-right {
+ border-top-right-radius: 0.25rem !important;
+ border-bottom-right-radius: 0.25rem !important;
+}
+
+/* line 51, node_modules/bootstrap/scss/utilities/_borders.scss */
+.rounded-bottom {
+ border-bottom-right-radius: 0.25rem !important;
+ border-bottom-left-radius: 0.25rem !important;
+}
+
+/* line 56, node_modules/bootstrap/scss/utilities/_borders.scss */
+.rounded-left {
+ border-top-left-radius: 0.25rem !important;
+ border-bottom-left-radius: 0.25rem !important;
+}
+
+/* line 61, node_modules/bootstrap/scss/utilities/_borders.scss */
+.rounded-lg {
+ border-radius: 0.3rem !important;
+}
+
+/* line 65, node_modules/bootstrap/scss/utilities/_borders.scss */
+.rounded-circle {
+ border-radius: 50% !important;
+}
+
+/* line 69, node_modules/bootstrap/scss/utilities/_borders.scss */
+.rounded-pill {
+ border-radius: 50rem !important;
+}
+
+/* line 73, node_modules/bootstrap/scss/utilities/_borders.scss */
+.rounded-0 {
+ border-radius: 0 !important;
+}
+
+/* line 2, node_modules/bootstrap/scss/mixins/_clearfix.scss */
+.clearfix::after {
+ display: block;
+ clear: both;
+ content: "";
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+.d-none {
+ display: none !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+.d-inline {
+ display: inline !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+.d-inline-block {
+ display: inline-block !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+.d-block {
+ display: block !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+.d-table {
+ display: table !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+.d-table-row {
+ display: table-row !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+.d-table-cell {
+ display: table-cell !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+.d-flex {
+ display: -webkit-box !important;
+ display: -ms-flexbox !important;
+ display: flex !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+.d-inline-flex {
+ display: -webkit-inline-box !important;
+ display: -ms-inline-flexbox !important;
+ display: inline-flex !important;
+}
+
+@media (min-width: 576px) {
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-sm-none {
+ display: none !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-sm-inline {
+ display: inline !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-sm-inline-block {
+ display: inline-block !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-sm-block {
+ display: block !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-sm-table {
+ display: table !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-sm-table-row {
+ display: table-row !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-sm-table-cell {
+ display: table-cell !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-sm-flex {
+ display: -webkit-box !important;
+ display: -ms-flexbox !important;
+ display: flex !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-sm-inline-flex {
+ display: -webkit-inline-box !important;
+ display: -ms-inline-flexbox !important;
+ display: inline-flex !important;
+ }
+}
+
+@media (min-width: 768px) {
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-md-none {
+ display: none !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-md-inline {
+ display: inline !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-md-inline-block {
+ display: inline-block !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-md-block {
+ display: block !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-md-table {
+ display: table !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-md-table-row {
+ display: table-row !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-md-table-cell {
+ display: table-cell !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-md-flex {
+ display: -webkit-box !important;
+ display: -ms-flexbox !important;
+ display: flex !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-md-inline-flex {
+ display: -webkit-inline-box !important;
+ display: -ms-inline-flexbox !important;
+ display: inline-flex !important;
+ }
+}
+
+@media (min-width: 1024px) {
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-lg-none {
+ display: none !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-lg-inline {
+ display: inline !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-lg-inline-block {
+ display: inline-block !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-lg-block {
+ display: block !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-lg-table {
+ display: table !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-lg-table-row {
+ display: table-row !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-lg-table-cell {
+ display: table-cell !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-lg-flex {
+ display: -webkit-box !important;
+ display: -ms-flexbox !important;
+ display: flex !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-lg-inline-flex {
+ display: -webkit-inline-box !important;
+ display: -ms-inline-flexbox !important;
+ display: inline-flex !important;
+ }
+}
+
+@media (min-width: 1280px) {
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-xl-none {
+ display: none !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-xl-inline {
+ display: inline !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-xl-inline-block {
+ display: inline-block !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-xl-block {
+ display: block !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-xl-table {
+ display: table !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-xl-table-row {
+ display: table-row !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-xl-table-cell {
+ display: table-cell !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-xl-flex {
+ display: -webkit-box !important;
+ display: -ms-flexbox !important;
+ display: flex !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-xl-inline-flex {
+ display: -webkit-inline-box !important;
+ display: -ms-inline-flexbox !important;
+ display: inline-flex !important;
+ }
+}
+
+@media (min-width: 1440px) {
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-xxl-none {
+ display: none !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-xxl-inline {
+ display: inline !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-xxl-inline-block {
+ display: inline-block !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-xxl-block {
+ display: block !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-xxl-table {
+ display: table !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-xxl-table-row {
+ display: table-row !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-xxl-table-cell {
+ display: table-cell !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-xxl-flex {
+ display: -webkit-box !important;
+ display: -ms-flexbox !important;
+ display: flex !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-xxl-inline-flex {
+ display: -webkit-inline-box !important;
+ display: -ms-inline-flexbox !important;
+ display: inline-flex !important;
+ }
+}
+
+@media print {
+ /* line 24, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-print-none {
+ display: none !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-print-inline {
+ display: inline !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-print-inline-block {
+ display: inline-block !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-print-block {
+ display: block !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-print-table {
+ display: table !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-print-table-row {
+ display: table-row !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-print-table-cell {
+ display: table-cell !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-print-flex {
+ display: -webkit-box !important;
+ display: -ms-flexbox !important;
+ display: flex !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_display.scss */
+ .d-print-inline-flex {
+ display: -webkit-inline-box !important;
+ display: -ms-inline-flexbox !important;
+ display: inline-flex !important;
+ }
+}
+
+/* line 3, node_modules/bootstrap/scss/utilities/_embed.scss */
+.embed-responsive {
+ position: relative;
+ display: block;
+ width: 100%;
+ padding: 0;
+ overflow: hidden;
+}
+
+/* line 10, node_modules/bootstrap/scss/utilities/_embed.scss */
+.embed-responsive::before {
+ display: block;
+ content: "";
+}
+
+/* line 15, node_modules/bootstrap/scss/utilities/_embed.scss */
+.embed-responsive .embed-responsive-item,
+.embed-responsive iframe,
+.embed-responsive embed,
+.embed-responsive object,
+.embed-responsive video {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border: 0;
+}
+
+/* line 35, node_modules/bootstrap/scss/utilities/_embed.scss */
+.embed-responsive-21by9::before {
+ padding-top: 42.85714%;
+}
+
+/* line 35, node_modules/bootstrap/scss/utilities/_embed.scss */
+.embed-responsive-16by9::before {
+ padding-top: 56.25%;
+}
+
+/* line 35, node_modules/bootstrap/scss/utilities/_embed.scss */
+.embed-responsive-4by3::before {
+ padding-top: 75%;
+}
+
+/* line 35, node_modules/bootstrap/scss/utilities/_embed.scss */
+.embed-responsive-1by1::before {
+ padding-top: 100%;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_flex.scss */
+.flex-row {
+ -webkit-box-orient: horizontal !important;
+ -webkit-box-direction: normal !important;
+ -ms-flex-direction: row !important;
+ flex-direction: row !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_flex.scss */
+.flex-column {
+ -webkit-box-orient: vertical !important;
+ -webkit-box-direction: normal !important;
+ -ms-flex-direction: column !important;
+ flex-direction: column !important;
+}
+
+/* line 13, node_modules/bootstrap/scss/utilities/_flex.scss */
+.flex-row-reverse {
+ -webkit-box-orient: horizontal !important;
+ -webkit-box-direction: reverse !important;
+ -ms-flex-direction: row-reverse !important;
+ flex-direction: row-reverse !important;
+}
+
+/* line 14, node_modules/bootstrap/scss/utilities/_flex.scss */
+.flex-column-reverse {
+ -webkit-box-orient: vertical !important;
+ -webkit-box-direction: reverse !important;
+ -ms-flex-direction: column-reverse !important;
+ flex-direction: column-reverse !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_flex.scss */
+.flex-wrap {
+ -ms-flex-wrap: wrap !important;
+ flex-wrap: wrap !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/utilities/_flex.scss */
+.flex-nowrap {
+ -ms-flex-wrap: nowrap !important;
+ flex-wrap: nowrap !important;
+}
+
+/* line 18, node_modules/bootstrap/scss/utilities/_flex.scss */
+.flex-wrap-reverse {
+ -ms-flex-wrap: wrap-reverse !important;
+ flex-wrap: wrap-reverse !important;
+}
+
+/* line 19, node_modules/bootstrap/scss/utilities/_flex.scss */
+.flex-fill {
+ -webkit-box-flex: 1 !important;
+ -ms-flex: 1 1 auto !important;
+ flex: 1 1 auto !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_flex.scss */
+.flex-grow-0 {
+ -webkit-box-flex: 0 !important;
+ -ms-flex-positive: 0 !important;
+ flex-grow: 0 !important;
+}
+
+/* line 21, node_modules/bootstrap/scss/utilities/_flex.scss */
+.flex-grow-1 {
+ -webkit-box-flex: 1 !important;
+ -ms-flex-positive: 1 !important;
+ flex-grow: 1 !important;
+}
+
+/* line 22, node_modules/bootstrap/scss/utilities/_flex.scss */
+.flex-shrink-0 {
+ -ms-flex-negative: 0 !important;
+ flex-shrink: 0 !important;
+}
+
+/* line 23, node_modules/bootstrap/scss/utilities/_flex.scss */
+.flex-shrink-1 {
+ -ms-flex-negative: 1 !important;
+ flex-shrink: 1 !important;
+}
+
+/* line 25, node_modules/bootstrap/scss/utilities/_flex.scss */
+.justify-content-start {
+ -webkit-box-pack: start !important;
+ -ms-flex-pack: start !important;
+ justify-content: flex-start !important;
+}
+
+/* line 26, node_modules/bootstrap/scss/utilities/_flex.scss */
+.justify-content-end {
+ -webkit-box-pack: end !important;
+ -ms-flex-pack: end !important;
+ justify-content: flex-end !important;
+}
+
+/* line 27, node_modules/bootstrap/scss/utilities/_flex.scss */
+.justify-content-center {
+ -webkit-box-pack: center !important;
+ -ms-flex-pack: center !important;
+ justify-content: center !important;
+}
+
+/* line 28, node_modules/bootstrap/scss/utilities/_flex.scss */
+.justify-content-between {
+ -webkit-box-pack: justify !important;
+ -ms-flex-pack: justify !important;
+ justify-content: space-between !important;
+}
+
+/* line 29, node_modules/bootstrap/scss/utilities/_flex.scss */
+.justify-content-around {
+ -ms-flex-pack: distribute !important;
+ justify-content: space-around !important;
+}
+
+/* line 31, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-items-start {
+ -webkit-box-align: start !important;
+ -ms-flex-align: start !important;
+ align-items: flex-start !important;
+}
+
+/* line 32, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-items-end {
+ -webkit-box-align: end !important;
+ -ms-flex-align: end !important;
+ align-items: flex-end !important;
+}
+
+/* line 33, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-items-center {
+ -webkit-box-align: center !important;
+ -ms-flex-align: center !important;
+ align-items: center !important;
+}
+
+/* line 34, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-items-baseline {
+ -webkit-box-align: baseline !important;
+ -ms-flex-align: baseline !important;
+ align-items: baseline !important;
+}
+
+/* line 35, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-items-stretch {
+ -webkit-box-align: stretch !important;
+ -ms-flex-align: stretch !important;
+ align-items: stretch !important;
+}
+
+/* line 37, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-content-start {
+ -ms-flex-line-pack: start !important;
+ align-content: flex-start !important;
+}
+
+/* line 38, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-content-end {
+ -ms-flex-line-pack: end !important;
+ align-content: flex-end !important;
+}
+
+/* line 39, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-content-center {
+ -ms-flex-line-pack: center !important;
+ align-content: center !important;
+}
+
+/* line 40, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-content-between {
+ -ms-flex-line-pack: justify !important;
+ align-content: space-between !important;
+}
+
+/* line 41, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-content-around {
+ -ms-flex-line-pack: distribute !important;
+ align-content: space-around !important;
+}
+
+/* line 42, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-content-stretch {
+ -ms-flex-line-pack: stretch !important;
+ align-content: stretch !important;
+}
+
+/* line 44, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-self-auto {
+ -ms-flex-item-align: auto !important;
+ align-self: auto !important;
+}
+
+/* line 45, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-self-start {
+ -ms-flex-item-align: start !important;
+ align-self: flex-start !important;
+}
+
+/* line 46, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-self-end {
+ -ms-flex-item-align: end !important;
+ align-self: flex-end !important;
+}
+
+/* line 47, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-self-center {
+ -ms-flex-item-align: center !important;
+ align-self: center !important;
+}
+
+/* line 48, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-self-baseline {
+ -ms-flex-item-align: baseline !important;
+ align-self: baseline !important;
+}
+
+/* line 49, node_modules/bootstrap/scss/utilities/_flex.scss */
+.align-self-stretch {
+ -ms-flex-item-align: stretch !important;
+ align-self: stretch !important;
+}
+
+@media (min-width: 576px) {
+ /* line 11, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-sm-row {
+ -webkit-box-orient: horizontal !important;
+ -webkit-box-direction: normal !important;
+ -ms-flex-direction: row !important;
+ flex-direction: row !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-sm-column {
+ -webkit-box-orient: vertical !important;
+ -webkit-box-direction: normal !important;
+ -ms-flex-direction: column !important;
+ flex-direction: column !important;
+ }
+ /* line 13, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-sm-row-reverse {
+ -webkit-box-orient: horizontal !important;
+ -webkit-box-direction: reverse !important;
+ -ms-flex-direction: row-reverse !important;
+ flex-direction: row-reverse !important;
+ }
+ /* line 14, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-sm-column-reverse {
+ -webkit-box-orient: vertical !important;
+ -webkit-box-direction: reverse !important;
+ -ms-flex-direction: column-reverse !important;
+ flex-direction: column-reverse !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-sm-wrap {
+ -ms-flex-wrap: wrap !important;
+ flex-wrap: wrap !important;
+ }
+ /* line 17, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-sm-nowrap {
+ -ms-flex-wrap: nowrap !important;
+ flex-wrap: nowrap !important;
+ }
+ /* line 18, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-sm-wrap-reverse {
+ -ms-flex-wrap: wrap-reverse !important;
+ flex-wrap: wrap-reverse !important;
+ }
+ /* line 19, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-sm-fill {
+ -webkit-box-flex: 1 !important;
+ -ms-flex: 1 1 auto !important;
+ flex: 1 1 auto !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-sm-grow-0 {
+ -webkit-box-flex: 0 !important;
+ -ms-flex-positive: 0 !important;
+ flex-grow: 0 !important;
+ }
+ /* line 21, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-sm-grow-1 {
+ -webkit-box-flex: 1 !important;
+ -ms-flex-positive: 1 !important;
+ flex-grow: 1 !important;
+ }
+ /* line 22, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-sm-shrink-0 {
+ -ms-flex-negative: 0 !important;
+ flex-shrink: 0 !important;
+ }
+ /* line 23, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-sm-shrink-1 {
+ -ms-flex-negative: 1 !important;
+ flex-shrink: 1 !important;
+ }
+ /* line 25, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-sm-start {
+ -webkit-box-pack: start !important;
+ -ms-flex-pack: start !important;
+ justify-content: flex-start !important;
+ }
+ /* line 26, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-sm-end {
+ -webkit-box-pack: end !important;
+ -ms-flex-pack: end !important;
+ justify-content: flex-end !important;
+ }
+ /* line 27, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-sm-center {
+ -webkit-box-pack: center !important;
+ -ms-flex-pack: center !important;
+ justify-content: center !important;
+ }
+ /* line 28, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-sm-between {
+ -webkit-box-pack: justify !important;
+ -ms-flex-pack: justify !important;
+ justify-content: space-between !important;
+ }
+ /* line 29, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-sm-around {
+ -ms-flex-pack: distribute !important;
+ justify-content: space-around !important;
+ }
+ /* line 31, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-sm-start {
+ -webkit-box-align: start !important;
+ -ms-flex-align: start !important;
+ align-items: flex-start !important;
+ }
+ /* line 32, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-sm-end {
+ -webkit-box-align: end !important;
+ -ms-flex-align: end !important;
+ align-items: flex-end !important;
+ }
+ /* line 33, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-sm-center {
+ -webkit-box-align: center !important;
+ -ms-flex-align: center !important;
+ align-items: center !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-sm-baseline {
+ -webkit-box-align: baseline !important;
+ -ms-flex-align: baseline !important;
+ align-items: baseline !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-sm-stretch {
+ -webkit-box-align: stretch !important;
+ -ms-flex-align: stretch !important;
+ align-items: stretch !important;
+ }
+ /* line 37, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-sm-start {
+ -ms-flex-line-pack: start !important;
+ align-content: flex-start !important;
+ }
+ /* line 38, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-sm-end {
+ -ms-flex-line-pack: end !important;
+ align-content: flex-end !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-sm-center {
+ -ms-flex-line-pack: center !important;
+ align-content: center !important;
+ }
+ /* line 40, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-sm-between {
+ -ms-flex-line-pack: justify !important;
+ align-content: space-between !important;
+ }
+ /* line 41, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-sm-around {
+ -ms-flex-line-pack: distribute !important;
+ align-content: space-around !important;
+ }
+ /* line 42, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-sm-stretch {
+ -ms-flex-line-pack: stretch !important;
+ align-content: stretch !important;
+ }
+ /* line 44, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-sm-auto {
+ -ms-flex-item-align: auto !important;
+ align-self: auto !important;
+ }
+ /* line 45, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-sm-start {
+ -ms-flex-item-align: start !important;
+ align-self: flex-start !important;
+ }
+ /* line 46, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-sm-end {
+ -ms-flex-item-align: end !important;
+ align-self: flex-end !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-sm-center {
+ -ms-flex-item-align: center !important;
+ align-self: center !important;
+ }
+ /* line 48, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-sm-baseline {
+ -ms-flex-item-align: baseline !important;
+ align-self: baseline !important;
+ }
+ /* line 49, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-sm-stretch {
+ -ms-flex-item-align: stretch !important;
+ align-self: stretch !important;
+ }
+}
+
+@media (min-width: 768px) {
+ /* line 11, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-md-row {
+ -webkit-box-orient: horizontal !important;
+ -webkit-box-direction: normal !important;
+ -ms-flex-direction: row !important;
+ flex-direction: row !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-md-column {
+ -webkit-box-orient: vertical !important;
+ -webkit-box-direction: normal !important;
+ -ms-flex-direction: column !important;
+ flex-direction: column !important;
+ }
+ /* line 13, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-md-row-reverse {
+ -webkit-box-orient: horizontal !important;
+ -webkit-box-direction: reverse !important;
+ -ms-flex-direction: row-reverse !important;
+ flex-direction: row-reverse !important;
+ }
+ /* line 14, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-md-column-reverse {
+ -webkit-box-orient: vertical !important;
+ -webkit-box-direction: reverse !important;
+ -ms-flex-direction: column-reverse !important;
+ flex-direction: column-reverse !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-md-wrap {
+ -ms-flex-wrap: wrap !important;
+ flex-wrap: wrap !important;
+ }
+ /* line 17, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-md-nowrap {
+ -ms-flex-wrap: nowrap !important;
+ flex-wrap: nowrap !important;
+ }
+ /* line 18, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-md-wrap-reverse {
+ -ms-flex-wrap: wrap-reverse !important;
+ flex-wrap: wrap-reverse !important;
+ }
+ /* line 19, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-md-fill {
+ -webkit-box-flex: 1 !important;
+ -ms-flex: 1 1 auto !important;
+ flex: 1 1 auto !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-md-grow-0 {
+ -webkit-box-flex: 0 !important;
+ -ms-flex-positive: 0 !important;
+ flex-grow: 0 !important;
+ }
+ /* line 21, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-md-grow-1 {
+ -webkit-box-flex: 1 !important;
+ -ms-flex-positive: 1 !important;
+ flex-grow: 1 !important;
+ }
+ /* line 22, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-md-shrink-0 {
+ -ms-flex-negative: 0 !important;
+ flex-shrink: 0 !important;
+ }
+ /* line 23, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-md-shrink-1 {
+ -ms-flex-negative: 1 !important;
+ flex-shrink: 1 !important;
+ }
+ /* line 25, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-md-start {
+ -webkit-box-pack: start !important;
+ -ms-flex-pack: start !important;
+ justify-content: flex-start !important;
+ }
+ /* line 26, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-md-end {
+ -webkit-box-pack: end !important;
+ -ms-flex-pack: end !important;
+ justify-content: flex-end !important;
+ }
+ /* line 27, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-md-center {
+ -webkit-box-pack: center !important;
+ -ms-flex-pack: center !important;
+ justify-content: center !important;
+ }
+ /* line 28, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-md-between {
+ -webkit-box-pack: justify !important;
+ -ms-flex-pack: justify !important;
+ justify-content: space-between !important;
+ }
+ /* line 29, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-md-around {
+ -ms-flex-pack: distribute !important;
+ justify-content: space-around !important;
+ }
+ /* line 31, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-md-start {
+ -webkit-box-align: start !important;
+ -ms-flex-align: start !important;
+ align-items: flex-start !important;
+ }
+ /* line 32, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-md-end {
+ -webkit-box-align: end !important;
+ -ms-flex-align: end !important;
+ align-items: flex-end !important;
+ }
+ /* line 33, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-md-center {
+ -webkit-box-align: center !important;
+ -ms-flex-align: center !important;
+ align-items: center !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-md-baseline {
+ -webkit-box-align: baseline !important;
+ -ms-flex-align: baseline !important;
+ align-items: baseline !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-md-stretch {
+ -webkit-box-align: stretch !important;
+ -ms-flex-align: stretch !important;
+ align-items: stretch !important;
+ }
+ /* line 37, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-md-start {
+ -ms-flex-line-pack: start !important;
+ align-content: flex-start !important;
+ }
+ /* line 38, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-md-end {
+ -ms-flex-line-pack: end !important;
+ align-content: flex-end !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-md-center {
+ -ms-flex-line-pack: center !important;
+ align-content: center !important;
+ }
+ /* line 40, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-md-between {
+ -ms-flex-line-pack: justify !important;
+ align-content: space-between !important;
+ }
+ /* line 41, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-md-around {
+ -ms-flex-line-pack: distribute !important;
+ align-content: space-around !important;
+ }
+ /* line 42, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-md-stretch {
+ -ms-flex-line-pack: stretch !important;
+ align-content: stretch !important;
+ }
+ /* line 44, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-md-auto {
+ -ms-flex-item-align: auto !important;
+ align-self: auto !important;
+ }
+ /* line 45, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-md-start {
+ -ms-flex-item-align: start !important;
+ align-self: flex-start !important;
+ }
+ /* line 46, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-md-end {
+ -ms-flex-item-align: end !important;
+ align-self: flex-end !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-md-center {
+ -ms-flex-item-align: center !important;
+ align-self: center !important;
+ }
+ /* line 48, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-md-baseline {
+ -ms-flex-item-align: baseline !important;
+ align-self: baseline !important;
+ }
+ /* line 49, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-md-stretch {
+ -ms-flex-item-align: stretch !important;
+ align-self: stretch !important;
+ }
+}
+
+@media (min-width: 1024px) {
+ /* line 11, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-lg-row {
+ -webkit-box-orient: horizontal !important;
+ -webkit-box-direction: normal !important;
+ -ms-flex-direction: row !important;
+ flex-direction: row !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-lg-column {
+ -webkit-box-orient: vertical !important;
+ -webkit-box-direction: normal !important;
+ -ms-flex-direction: column !important;
+ flex-direction: column !important;
+ }
+ /* line 13, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-lg-row-reverse {
+ -webkit-box-orient: horizontal !important;
+ -webkit-box-direction: reverse !important;
+ -ms-flex-direction: row-reverse !important;
+ flex-direction: row-reverse !important;
+ }
+ /* line 14, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-lg-column-reverse {
+ -webkit-box-orient: vertical !important;
+ -webkit-box-direction: reverse !important;
+ -ms-flex-direction: column-reverse !important;
+ flex-direction: column-reverse !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-lg-wrap {
+ -ms-flex-wrap: wrap !important;
+ flex-wrap: wrap !important;
+ }
+ /* line 17, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-lg-nowrap {
+ -ms-flex-wrap: nowrap !important;
+ flex-wrap: nowrap !important;
+ }
+ /* line 18, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-lg-wrap-reverse {
+ -ms-flex-wrap: wrap-reverse !important;
+ flex-wrap: wrap-reverse !important;
+ }
+ /* line 19, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-lg-fill {
+ -webkit-box-flex: 1 !important;
+ -ms-flex: 1 1 auto !important;
+ flex: 1 1 auto !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-lg-grow-0 {
+ -webkit-box-flex: 0 !important;
+ -ms-flex-positive: 0 !important;
+ flex-grow: 0 !important;
+ }
+ /* line 21, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-lg-grow-1 {
+ -webkit-box-flex: 1 !important;
+ -ms-flex-positive: 1 !important;
+ flex-grow: 1 !important;
+ }
+ /* line 22, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-lg-shrink-0 {
+ -ms-flex-negative: 0 !important;
+ flex-shrink: 0 !important;
+ }
+ /* line 23, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-lg-shrink-1 {
+ -ms-flex-negative: 1 !important;
+ flex-shrink: 1 !important;
+ }
+ /* line 25, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-lg-start {
+ -webkit-box-pack: start !important;
+ -ms-flex-pack: start !important;
+ justify-content: flex-start !important;
+ }
+ /* line 26, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-lg-end {
+ -webkit-box-pack: end !important;
+ -ms-flex-pack: end !important;
+ justify-content: flex-end !important;
+ }
+ /* line 27, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-lg-center {
+ -webkit-box-pack: center !important;
+ -ms-flex-pack: center !important;
+ justify-content: center !important;
+ }
+ /* line 28, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-lg-between {
+ -webkit-box-pack: justify !important;
+ -ms-flex-pack: justify !important;
+ justify-content: space-between !important;
+ }
+ /* line 29, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-lg-around {
+ -ms-flex-pack: distribute !important;
+ justify-content: space-around !important;
+ }
+ /* line 31, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-lg-start {
+ -webkit-box-align: start !important;
+ -ms-flex-align: start !important;
+ align-items: flex-start !important;
+ }
+ /* line 32, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-lg-end {
+ -webkit-box-align: end !important;
+ -ms-flex-align: end !important;
+ align-items: flex-end !important;
+ }
+ /* line 33, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-lg-center {
+ -webkit-box-align: center !important;
+ -ms-flex-align: center !important;
+ align-items: center !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-lg-baseline {
+ -webkit-box-align: baseline !important;
+ -ms-flex-align: baseline !important;
+ align-items: baseline !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-lg-stretch {
+ -webkit-box-align: stretch !important;
+ -ms-flex-align: stretch !important;
+ align-items: stretch !important;
+ }
+ /* line 37, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-lg-start {
+ -ms-flex-line-pack: start !important;
+ align-content: flex-start !important;
+ }
+ /* line 38, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-lg-end {
+ -ms-flex-line-pack: end !important;
+ align-content: flex-end !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-lg-center {
+ -ms-flex-line-pack: center !important;
+ align-content: center !important;
+ }
+ /* line 40, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-lg-between {
+ -ms-flex-line-pack: justify !important;
+ align-content: space-between !important;
+ }
+ /* line 41, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-lg-around {
+ -ms-flex-line-pack: distribute !important;
+ align-content: space-around !important;
+ }
+ /* line 42, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-lg-stretch {
+ -ms-flex-line-pack: stretch !important;
+ align-content: stretch !important;
+ }
+ /* line 44, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-lg-auto {
+ -ms-flex-item-align: auto !important;
+ align-self: auto !important;
+ }
+ /* line 45, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-lg-start {
+ -ms-flex-item-align: start !important;
+ align-self: flex-start !important;
+ }
+ /* line 46, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-lg-end {
+ -ms-flex-item-align: end !important;
+ align-self: flex-end !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-lg-center {
+ -ms-flex-item-align: center !important;
+ align-self: center !important;
+ }
+ /* line 48, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-lg-baseline {
+ -ms-flex-item-align: baseline !important;
+ align-self: baseline !important;
+ }
+ /* line 49, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-lg-stretch {
+ -ms-flex-item-align: stretch !important;
+ align-self: stretch !important;
+ }
+}
+
+@media (min-width: 1280px) {
+ /* line 11, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-xl-row {
+ -webkit-box-orient: horizontal !important;
+ -webkit-box-direction: normal !important;
+ -ms-flex-direction: row !important;
+ flex-direction: row !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-xl-column {
+ -webkit-box-orient: vertical !important;
+ -webkit-box-direction: normal !important;
+ -ms-flex-direction: column !important;
+ flex-direction: column !important;
+ }
+ /* line 13, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-xl-row-reverse {
+ -webkit-box-orient: horizontal !important;
+ -webkit-box-direction: reverse !important;
+ -ms-flex-direction: row-reverse !important;
+ flex-direction: row-reverse !important;
+ }
+ /* line 14, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-xl-column-reverse {
+ -webkit-box-orient: vertical !important;
+ -webkit-box-direction: reverse !important;
+ -ms-flex-direction: column-reverse !important;
+ flex-direction: column-reverse !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-xl-wrap {
+ -ms-flex-wrap: wrap !important;
+ flex-wrap: wrap !important;
+ }
+ /* line 17, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-xl-nowrap {
+ -ms-flex-wrap: nowrap !important;
+ flex-wrap: nowrap !important;
+ }
+ /* line 18, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-xl-wrap-reverse {
+ -ms-flex-wrap: wrap-reverse !important;
+ flex-wrap: wrap-reverse !important;
+ }
+ /* line 19, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-xl-fill {
+ -webkit-box-flex: 1 !important;
+ -ms-flex: 1 1 auto !important;
+ flex: 1 1 auto !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-xl-grow-0 {
+ -webkit-box-flex: 0 !important;
+ -ms-flex-positive: 0 !important;
+ flex-grow: 0 !important;
+ }
+ /* line 21, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-xl-grow-1 {
+ -webkit-box-flex: 1 !important;
+ -ms-flex-positive: 1 !important;
+ flex-grow: 1 !important;
+ }
+ /* line 22, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-xl-shrink-0 {
+ -ms-flex-negative: 0 !important;
+ flex-shrink: 0 !important;
+ }
+ /* line 23, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-xl-shrink-1 {
+ -ms-flex-negative: 1 !important;
+ flex-shrink: 1 !important;
+ }
+ /* line 25, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-xl-start {
+ -webkit-box-pack: start !important;
+ -ms-flex-pack: start !important;
+ justify-content: flex-start !important;
+ }
+ /* line 26, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-xl-end {
+ -webkit-box-pack: end !important;
+ -ms-flex-pack: end !important;
+ justify-content: flex-end !important;
+ }
+ /* line 27, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-xl-center {
+ -webkit-box-pack: center !important;
+ -ms-flex-pack: center !important;
+ justify-content: center !important;
+ }
+ /* line 28, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-xl-between {
+ -webkit-box-pack: justify !important;
+ -ms-flex-pack: justify !important;
+ justify-content: space-between !important;
+ }
+ /* line 29, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-xl-around {
+ -ms-flex-pack: distribute !important;
+ justify-content: space-around !important;
+ }
+ /* line 31, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-xl-start {
+ -webkit-box-align: start !important;
+ -ms-flex-align: start !important;
+ align-items: flex-start !important;
+ }
+ /* line 32, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-xl-end {
+ -webkit-box-align: end !important;
+ -ms-flex-align: end !important;
+ align-items: flex-end !important;
+ }
+ /* line 33, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-xl-center {
+ -webkit-box-align: center !important;
+ -ms-flex-align: center !important;
+ align-items: center !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-xl-baseline {
+ -webkit-box-align: baseline !important;
+ -ms-flex-align: baseline !important;
+ align-items: baseline !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-xl-stretch {
+ -webkit-box-align: stretch !important;
+ -ms-flex-align: stretch !important;
+ align-items: stretch !important;
+ }
+ /* line 37, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-xl-start {
+ -ms-flex-line-pack: start !important;
+ align-content: flex-start !important;
+ }
+ /* line 38, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-xl-end {
+ -ms-flex-line-pack: end !important;
+ align-content: flex-end !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-xl-center {
+ -ms-flex-line-pack: center !important;
+ align-content: center !important;
+ }
+ /* line 40, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-xl-between {
+ -ms-flex-line-pack: justify !important;
+ align-content: space-between !important;
+ }
+ /* line 41, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-xl-around {
+ -ms-flex-line-pack: distribute !important;
+ align-content: space-around !important;
+ }
+ /* line 42, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-xl-stretch {
+ -ms-flex-line-pack: stretch !important;
+ align-content: stretch !important;
+ }
+ /* line 44, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-xl-auto {
+ -ms-flex-item-align: auto !important;
+ align-self: auto !important;
+ }
+ /* line 45, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-xl-start {
+ -ms-flex-item-align: start !important;
+ align-self: flex-start !important;
+ }
+ /* line 46, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-xl-end {
+ -ms-flex-item-align: end !important;
+ align-self: flex-end !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-xl-center {
+ -ms-flex-item-align: center !important;
+ align-self: center !important;
+ }
+ /* line 48, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-xl-baseline {
+ -ms-flex-item-align: baseline !important;
+ align-self: baseline !important;
+ }
+ /* line 49, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-xl-stretch {
+ -ms-flex-item-align: stretch !important;
+ align-self: stretch !important;
+ }
+}
+
+@media (min-width: 1440px) {
+ /* line 11, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-xxl-row {
+ -webkit-box-orient: horizontal !important;
+ -webkit-box-direction: normal !important;
+ -ms-flex-direction: row !important;
+ flex-direction: row !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-xxl-column {
+ -webkit-box-orient: vertical !important;
+ -webkit-box-direction: normal !important;
+ -ms-flex-direction: column !important;
+ flex-direction: column !important;
+ }
+ /* line 13, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-xxl-row-reverse {
+ -webkit-box-orient: horizontal !important;
+ -webkit-box-direction: reverse !important;
+ -ms-flex-direction: row-reverse !important;
+ flex-direction: row-reverse !important;
+ }
+ /* line 14, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-xxl-column-reverse {
+ -webkit-box-orient: vertical !important;
+ -webkit-box-direction: reverse !important;
+ -ms-flex-direction: column-reverse !important;
+ flex-direction: column-reverse !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-xxl-wrap {
+ -ms-flex-wrap: wrap !important;
+ flex-wrap: wrap !important;
+ }
+ /* line 17, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-xxl-nowrap {
+ -ms-flex-wrap: nowrap !important;
+ flex-wrap: nowrap !important;
+ }
+ /* line 18, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-xxl-wrap-reverse {
+ -ms-flex-wrap: wrap-reverse !important;
+ flex-wrap: wrap-reverse !important;
+ }
+ /* line 19, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-xxl-fill {
+ -webkit-box-flex: 1 !important;
+ -ms-flex: 1 1 auto !important;
+ flex: 1 1 auto !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-xxl-grow-0 {
+ -webkit-box-flex: 0 !important;
+ -ms-flex-positive: 0 !important;
+ flex-grow: 0 !important;
+ }
+ /* line 21, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-xxl-grow-1 {
+ -webkit-box-flex: 1 !important;
+ -ms-flex-positive: 1 !important;
+ flex-grow: 1 !important;
+ }
+ /* line 22, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-xxl-shrink-0 {
+ -ms-flex-negative: 0 !important;
+ flex-shrink: 0 !important;
+ }
+ /* line 23, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .flex-xxl-shrink-1 {
+ -ms-flex-negative: 1 !important;
+ flex-shrink: 1 !important;
+ }
+ /* line 25, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-xxl-start {
+ -webkit-box-pack: start !important;
+ -ms-flex-pack: start !important;
+ justify-content: flex-start !important;
+ }
+ /* line 26, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-xxl-end {
+ -webkit-box-pack: end !important;
+ -ms-flex-pack: end !important;
+ justify-content: flex-end !important;
+ }
+ /* line 27, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-xxl-center {
+ -webkit-box-pack: center !important;
+ -ms-flex-pack: center !important;
+ justify-content: center !important;
+ }
+ /* line 28, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-xxl-between {
+ -webkit-box-pack: justify !important;
+ -ms-flex-pack: justify !important;
+ justify-content: space-between !important;
+ }
+ /* line 29, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .justify-content-xxl-around {
+ -ms-flex-pack: distribute !important;
+ justify-content: space-around !important;
+ }
+ /* line 31, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-xxl-start {
+ -webkit-box-align: start !important;
+ -ms-flex-align: start !important;
+ align-items: flex-start !important;
+ }
+ /* line 32, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-xxl-end {
+ -webkit-box-align: end !important;
+ -ms-flex-align: end !important;
+ align-items: flex-end !important;
+ }
+ /* line 33, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-xxl-center {
+ -webkit-box-align: center !important;
+ -ms-flex-align: center !important;
+ align-items: center !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-xxl-baseline {
+ -webkit-box-align: baseline !important;
+ -ms-flex-align: baseline !important;
+ align-items: baseline !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-items-xxl-stretch {
+ -webkit-box-align: stretch !important;
+ -ms-flex-align: stretch !important;
+ align-items: stretch !important;
+ }
+ /* line 37, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-xxl-start {
+ -ms-flex-line-pack: start !important;
+ align-content: flex-start !important;
+ }
+ /* line 38, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-xxl-end {
+ -ms-flex-line-pack: end !important;
+ align-content: flex-end !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-xxl-center {
+ -ms-flex-line-pack: center !important;
+ align-content: center !important;
+ }
+ /* line 40, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-xxl-between {
+ -ms-flex-line-pack: justify !important;
+ align-content: space-between !important;
+ }
+ /* line 41, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-xxl-around {
+ -ms-flex-line-pack: distribute !important;
+ align-content: space-around !important;
+ }
+ /* line 42, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-content-xxl-stretch {
+ -ms-flex-line-pack: stretch !important;
+ align-content: stretch !important;
+ }
+ /* line 44, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-xxl-auto {
+ -ms-flex-item-align: auto !important;
+ align-self: auto !important;
+ }
+ /* line 45, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-xxl-start {
+ -ms-flex-item-align: start !important;
+ align-self: flex-start !important;
+ }
+ /* line 46, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-xxl-end {
+ -ms-flex-item-align: end !important;
+ align-self: flex-end !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-xxl-center {
+ -ms-flex-item-align: center !important;
+ align-self: center !important;
+ }
+ /* line 48, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-xxl-baseline {
+ -ms-flex-item-align: baseline !important;
+ align-self: baseline !important;
+ }
+ /* line 49, node_modules/bootstrap/scss/utilities/_flex.scss */
+ .align-self-xxl-stretch {
+ -ms-flex-item-align: stretch !important;
+ align-self: stretch !important;
+ }
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_float.scss */
+.float-left {
+ float: left !important;
+}
+
+/* line 8, node_modules/bootstrap/scss/utilities/_float.scss */
+.float-right {
+ float: right !important;
+}
+
+/* line 9, node_modules/bootstrap/scss/utilities/_float.scss */
+.float-none {
+ float: none !important;
+}
+
+@media (min-width: 576px) {
+ /* line 7, node_modules/bootstrap/scss/utilities/_float.scss */
+ .float-sm-left {
+ float: left !important;
+ }
+ /* line 8, node_modules/bootstrap/scss/utilities/_float.scss */
+ .float-sm-right {
+ float: right !important;
+ }
+ /* line 9, node_modules/bootstrap/scss/utilities/_float.scss */
+ .float-sm-none {
+ float: none !important;
+ }
+}
+
+@media (min-width: 768px) {
+ /* line 7, node_modules/bootstrap/scss/utilities/_float.scss */
+ .float-md-left {
+ float: left !important;
+ }
+ /* line 8, node_modules/bootstrap/scss/utilities/_float.scss */
+ .float-md-right {
+ float: right !important;
+ }
+ /* line 9, node_modules/bootstrap/scss/utilities/_float.scss */
+ .float-md-none {
+ float: none !important;
+ }
+}
+
+@media (min-width: 1024px) {
+ /* line 7, node_modules/bootstrap/scss/utilities/_float.scss */
+ .float-lg-left {
+ float: left !important;
+ }
+ /* line 8, node_modules/bootstrap/scss/utilities/_float.scss */
+ .float-lg-right {
+ float: right !important;
+ }
+ /* line 9, node_modules/bootstrap/scss/utilities/_float.scss */
+ .float-lg-none {
+ float: none !important;
+ }
+}
+
+@media (min-width: 1280px) {
+ /* line 7, node_modules/bootstrap/scss/utilities/_float.scss */
+ .float-xl-left {
+ float: left !important;
+ }
+ /* line 8, node_modules/bootstrap/scss/utilities/_float.scss */
+ .float-xl-right {
+ float: right !important;
+ }
+ /* line 9, node_modules/bootstrap/scss/utilities/_float.scss */
+ .float-xl-none {
+ float: none !important;
+ }
+}
+
+@media (min-width: 1440px) {
+ /* line 7, node_modules/bootstrap/scss/utilities/_float.scss */
+ .float-xxl-left {
+ float: left !important;
+ }
+ /* line 8, node_modules/bootstrap/scss/utilities/_float.scss */
+ .float-xxl-right {
+ float: right !important;
+ }
+ /* line 9, node_modules/bootstrap/scss/utilities/_float.scss */
+ .float-xxl-none {
+ float: none !important;
+ }
+}
+
+/* line 4, node_modules/bootstrap/scss/utilities/_interactions.scss */
+.user-select-all {
+ -webkit-user-select: all !important;
+ -moz-user-select: all !important;
+ -ms-user-select: all !important;
+ user-select: all !important;
+}
+
+/* line 4, node_modules/bootstrap/scss/utilities/_interactions.scss */
+.user-select-auto {
+ -webkit-user-select: auto !important;
+ -moz-user-select: auto !important;
+ -ms-user-select: auto !important;
+ user-select: auto !important;
+}
+
+/* line 4, node_modules/bootstrap/scss/utilities/_interactions.scss */
+.user-select-none {
+ -webkit-user-select: none !important;
+ -moz-user-select: none !important;
+ -ms-user-select: none !important;
+ user-select: none !important;
+}
+
+/* line 4, node_modules/bootstrap/scss/utilities/_overflow.scss */
+.overflow-auto {
+ overflow: auto !important;
+}
+
+/* line 4, node_modules/bootstrap/scss/utilities/_overflow.scss */
+.overflow-hidden {
+ overflow: hidden !important;
+}
+
+/* line 5, node_modules/bootstrap/scss/utilities/_position.scss */
+.position-static {
+ position: static !important;
+}
+
+/* line 5, node_modules/bootstrap/scss/utilities/_position.scss */
+.position-relative {
+ position: relative !important;
+}
+
+/* line 5, node_modules/bootstrap/scss/utilities/_position.scss */
+.position-absolute {
+ position: absolute !important;
+}
+
+/* line 5, node_modules/bootstrap/scss/utilities/_position.scss */
+.position-fixed {
+ position: fixed !important;
+}
+
+/* line 5, node_modules/bootstrap/scss/utilities/_position.scss */
+.position-sticky {
+ position: sticky !important;
+}
+
+/* line 10, node_modules/bootstrap/scss/utilities/_position.scss */
+.fixed-top {
+ position: fixed;
+ top: 0;
+ right: 0;
+ left: 0;
+ z-index: 1030;
+}
+
+/* line 18, node_modules/bootstrap/scss/utilities/_position.scss */
+.fixed-bottom {
+ position: fixed;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1030;
+}
+
+@supports (position: sticky) {
+ /* line 26, node_modules/bootstrap/scss/utilities/_position.scss */
+ .sticky-top {
+ position: sticky;
+ top: 0;
+ z-index: 1020;
+ }
+}
+
+/* line 5, node_modules/bootstrap/scss/utilities/_screenreaders.scss */
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+/* line 25, node_modules/bootstrap/scss/mixins/_screen-reader.scss */
+.sr-only-focusable:active, .sr-only-focusable:focus {
+ position: static;
+ width: auto;
+ height: auto;
+ overflow: visible;
+ clip: auto;
+ white-space: normal;
+}
+
+/* line 3, node_modules/bootstrap/scss/utilities/_shadows.scss */
+.shadow-sm {
+ -webkit-box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;
+ box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;
+}
+
+/* line 4, node_modules/bootstrap/scss/utilities/_shadows.scss */
+.shadow {
+ -webkit-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
+}
+
+/* line 5, node_modules/bootstrap/scss/utilities/_shadows.scss */
+.shadow-lg {
+ -webkit-box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;
+ box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/utilities/_shadows.scss */
+.shadow-none {
+ -webkit-box-shadow: none !important;
+ box-shadow: none !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.w-25 {
+ width: 25% !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.w-50 {
+ width: 50% !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.w-75 {
+ width: 75% !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.w-100 {
+ width: 100% !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.w-auto {
+ width: auto !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.h-25 {
+ height: 25% !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.h-50 {
+ height: 50% !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.h-75 {
+ height: 75% !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.h-100 {
+ height: 100% !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.h-auto {
+ height: auto !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.mw-100 {
+ max-width: 100% !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.mh-100 {
+ max-height: 100% !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.min-vw-100 {
+ min-width: 100vw !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.min-vh-100 {
+ min-height: 100vh !important;
+}
+
+/* line 19, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.vw-100 {
+ width: 100vw !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_sizing.scss */
+.vh-100 {
+ height: 100vh !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.m-0 {
+ margin: 0 !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mt-0,
+.my-0 {
+ margin-top: 0 !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mr-0,
+.mx-0 {
+ margin-right: 0 !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mb-0,
+.my-0 {
+ margin-bottom: 0 !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.ml-0,
+.mx-0 {
+ margin-left: 0 !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.m-1 {
+ margin: 0.25rem !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mt-1,
+.my-1 {
+ margin-top: 0.25rem !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mr-1,
+.mx-1 {
+ margin-right: 0.25rem !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mb-1,
+.my-1 {
+ margin-bottom: 0.25rem !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.ml-1,
+.mx-1 {
+ margin-left: 0.25rem !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.m-2 {
+ margin: 0.5rem !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mt-2,
+.my-2 {
+ margin-top: 0.5rem !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mr-2,
+.mx-2 {
+ margin-right: 0.5rem !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mb-2,
+.my-2 {
+ margin-bottom: 0.5rem !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.ml-2,
+.mx-2 {
+ margin-left: 0.5rem !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.m-3 {
+ margin: 1rem !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mt-3,
+.my-3 {
+ margin-top: 1rem !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mr-3,
+.mx-3 {
+ margin-right: 1rem !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mb-3,
+.my-3 {
+ margin-bottom: 1rem !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.ml-3,
+.mx-3 {
+ margin-left: 1rem !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.m-4 {
+ margin: 1.5rem !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mt-4,
+.my-4 {
+ margin-top: 1.5rem !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mr-4,
+.mx-4 {
+ margin-right: 1.5rem !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mb-4,
+.my-4 {
+ margin-bottom: 1.5rem !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.ml-4,
+.mx-4 {
+ margin-left: 1.5rem !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.m-5 {
+ margin: 3rem !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mt-5,
+.my-5 {
+ margin-top: 3rem !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mr-5,
+.mx-5 {
+ margin-right: 3rem !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mb-5,
+.my-5 {
+ margin-bottom: 3rem !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.ml-5,
+.mx-5 {
+ margin-left: 3rem !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.p-0 {
+ padding: 0 !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pt-0,
+.py-0 {
+ padding-top: 0 !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pr-0,
+.px-0 {
+ padding-right: 0 !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pb-0,
+.py-0 {
+ padding-bottom: 0 !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pl-0,
+.px-0 {
+ padding-left: 0 !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.p-1 {
+ padding: 0.25rem !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pt-1,
+.py-1 {
+ padding-top: 0.25rem !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pr-1,
+.px-1 {
+ padding-right: 0.25rem !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pb-1,
+.py-1 {
+ padding-bottom: 0.25rem !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pl-1,
+.px-1 {
+ padding-left: 0.25rem !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.p-2 {
+ padding: 0.5rem !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pt-2,
+.py-2 {
+ padding-top: 0.5rem !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pr-2,
+.px-2 {
+ padding-right: 0.5rem !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pb-2,
+.py-2 {
+ padding-bottom: 0.5rem !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pl-2,
+.px-2 {
+ padding-left: 0.5rem !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.p-3 {
+ padding: 1rem !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pt-3,
+.py-3 {
+ padding-top: 1rem !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pr-3,
+.px-3 {
+ padding-right: 1rem !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pb-3,
+.py-3 {
+ padding-bottom: 1rem !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pl-3,
+.px-3 {
+ padding-left: 1rem !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.p-4 {
+ padding: 1.5rem !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pt-4,
+.py-4 {
+ padding-top: 1.5rem !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pr-4,
+.px-4 {
+ padding-right: 1.5rem !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pb-4,
+.py-4 {
+ padding-bottom: 1.5rem !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pl-4,
+.px-4 {
+ padding-left: 1.5rem !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.p-5 {
+ padding: 3rem !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pt-5,
+.py-5 {
+ padding-top: 3rem !important;
+}
+
+/* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pr-5,
+.px-5 {
+ padding-right: 3rem !important;
+}
+
+/* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pb-5,
+.py-5 {
+ padding-bottom: 3rem !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.pl-5,
+.px-5 {
+ padding-left: 3rem !important;
+}
+
+/* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.m-n1 {
+ margin: -0.25rem !important;
+}
+
+/* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mt-n1,
+.my-n1 {
+ margin-top: -0.25rem !important;
+}
+
+/* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mr-n1,
+.mx-n1 {
+ margin-right: -0.25rem !important;
+}
+
+/* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mb-n1,
+.my-n1 {
+ margin-bottom: -0.25rem !important;
+}
+
+/* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.ml-n1,
+.mx-n1 {
+ margin-left: -0.25rem !important;
+}
+
+/* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.m-n2 {
+ margin: -0.5rem !important;
+}
+
+/* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mt-n2,
+.my-n2 {
+ margin-top: -0.5rem !important;
+}
+
+/* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mr-n2,
+.mx-n2 {
+ margin-right: -0.5rem !important;
+}
+
+/* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mb-n2,
+.my-n2 {
+ margin-bottom: -0.5rem !important;
+}
+
+/* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.ml-n2,
+.mx-n2 {
+ margin-left: -0.5rem !important;
+}
+
+/* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.m-n3 {
+ margin: -1rem !important;
+}
+
+/* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mt-n3,
+.my-n3 {
+ margin-top: -1rem !important;
+}
+
+/* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mr-n3,
+.mx-n3 {
+ margin-right: -1rem !important;
+}
+
+/* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mb-n3,
+.my-n3 {
+ margin-bottom: -1rem !important;
+}
+
+/* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.ml-n3,
+.mx-n3 {
+ margin-left: -1rem !important;
+}
+
+/* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.m-n4 {
+ margin: -1.5rem !important;
+}
+
+/* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mt-n4,
+.my-n4 {
+ margin-top: -1.5rem !important;
+}
+
+/* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mr-n4,
+.mx-n4 {
+ margin-right: -1.5rem !important;
+}
+
+/* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mb-n4,
+.my-n4 {
+ margin-bottom: -1.5rem !important;
+}
+
+/* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.ml-n4,
+.mx-n4 {
+ margin-left: -1.5rem !important;
+}
+
+/* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.m-n5 {
+ margin: -3rem !important;
+}
+
+/* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mt-n5,
+.my-n5 {
+ margin-top: -3rem !important;
+}
+
+/* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mr-n5,
+.mx-n5 {
+ margin-right: -3rem !important;
+}
+
+/* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mb-n5,
+.my-n5 {
+ margin-bottom: -3rem !important;
+}
+
+/* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.ml-n5,
+.mx-n5 {
+ margin-left: -3rem !important;
+}
+
+/* line 55, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.m-auto {
+ margin: auto !important;
+}
+
+/* line 56, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mt-auto,
+.my-auto {
+ margin-top: auto !important;
+}
+
+/* line 60, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mr-auto,
+.mx-auto {
+ margin-right: auto !important;
+}
+
+/* line 64, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.mb-auto,
+.my-auto {
+ margin-bottom: auto !important;
+}
+
+/* line 68, node_modules/bootstrap/scss/utilities/_spacing.scss */
+.ml-auto,
+.mx-auto {
+ margin-left: auto !important;
+}
+
+@media (min-width: 576px) {
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-sm-0 {
+ margin: 0 !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-sm-0,
+ .my-sm-0 {
+ margin-top: 0 !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-sm-0,
+ .mx-sm-0 {
+ margin-right: 0 !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-sm-0,
+ .my-sm-0 {
+ margin-bottom: 0 !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-sm-0,
+ .mx-sm-0 {
+ margin-left: 0 !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-sm-1 {
+ margin: 0.25rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-sm-1,
+ .my-sm-1 {
+ margin-top: 0.25rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-sm-1,
+ .mx-sm-1 {
+ margin-right: 0.25rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-sm-1,
+ .my-sm-1 {
+ margin-bottom: 0.25rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-sm-1,
+ .mx-sm-1 {
+ margin-left: 0.25rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-sm-2 {
+ margin: 0.5rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-sm-2,
+ .my-sm-2 {
+ margin-top: 0.5rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-sm-2,
+ .mx-sm-2 {
+ margin-right: 0.5rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-sm-2,
+ .my-sm-2 {
+ margin-bottom: 0.5rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-sm-2,
+ .mx-sm-2 {
+ margin-left: 0.5rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-sm-3 {
+ margin: 1rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-sm-3,
+ .my-sm-3 {
+ margin-top: 1rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-sm-3,
+ .mx-sm-3 {
+ margin-right: 1rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-sm-3,
+ .my-sm-3 {
+ margin-bottom: 1rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-sm-3,
+ .mx-sm-3 {
+ margin-left: 1rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-sm-4 {
+ margin: 1.5rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-sm-4,
+ .my-sm-4 {
+ margin-top: 1.5rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-sm-4,
+ .mx-sm-4 {
+ margin-right: 1.5rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-sm-4,
+ .my-sm-4 {
+ margin-bottom: 1.5rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-sm-4,
+ .mx-sm-4 {
+ margin-left: 1.5rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-sm-5 {
+ margin: 3rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-sm-5,
+ .my-sm-5 {
+ margin-top: 3rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-sm-5,
+ .mx-sm-5 {
+ margin-right: 3rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-sm-5,
+ .my-sm-5 {
+ margin-bottom: 3rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-sm-5,
+ .mx-sm-5 {
+ margin-left: 3rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-sm-0 {
+ padding: 0 !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-sm-0,
+ .py-sm-0 {
+ padding-top: 0 !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-sm-0,
+ .px-sm-0 {
+ padding-right: 0 !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-sm-0,
+ .py-sm-0 {
+ padding-bottom: 0 !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-sm-0,
+ .px-sm-0 {
+ padding-left: 0 !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-sm-1 {
+ padding: 0.25rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-sm-1,
+ .py-sm-1 {
+ padding-top: 0.25rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-sm-1,
+ .px-sm-1 {
+ padding-right: 0.25rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-sm-1,
+ .py-sm-1 {
+ padding-bottom: 0.25rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-sm-1,
+ .px-sm-1 {
+ padding-left: 0.25rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-sm-2 {
+ padding: 0.5rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-sm-2,
+ .py-sm-2 {
+ padding-top: 0.5rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-sm-2,
+ .px-sm-2 {
+ padding-right: 0.5rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-sm-2,
+ .py-sm-2 {
+ padding-bottom: 0.5rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-sm-2,
+ .px-sm-2 {
+ padding-left: 0.5rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-sm-3 {
+ padding: 1rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-sm-3,
+ .py-sm-3 {
+ padding-top: 1rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-sm-3,
+ .px-sm-3 {
+ padding-right: 1rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-sm-3,
+ .py-sm-3 {
+ padding-bottom: 1rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-sm-3,
+ .px-sm-3 {
+ padding-left: 1rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-sm-4 {
+ padding: 1.5rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-sm-4,
+ .py-sm-4 {
+ padding-top: 1.5rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-sm-4,
+ .px-sm-4 {
+ padding-right: 1.5rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-sm-4,
+ .py-sm-4 {
+ padding-bottom: 1.5rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-sm-4,
+ .px-sm-4 {
+ padding-left: 1.5rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-sm-5 {
+ padding: 3rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-sm-5,
+ .py-sm-5 {
+ padding-top: 3rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-sm-5,
+ .px-sm-5 {
+ padding-right: 3rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-sm-5,
+ .py-sm-5 {
+ padding-bottom: 3rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-sm-5,
+ .px-sm-5 {
+ padding-left: 3rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-sm-n1 {
+ margin: -0.25rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-sm-n1,
+ .my-sm-n1 {
+ margin-top: -0.25rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-sm-n1,
+ .mx-sm-n1 {
+ margin-right: -0.25rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-sm-n1,
+ .my-sm-n1 {
+ margin-bottom: -0.25rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-sm-n1,
+ .mx-sm-n1 {
+ margin-left: -0.25rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-sm-n2 {
+ margin: -0.5rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-sm-n2,
+ .my-sm-n2 {
+ margin-top: -0.5rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-sm-n2,
+ .mx-sm-n2 {
+ margin-right: -0.5rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-sm-n2,
+ .my-sm-n2 {
+ margin-bottom: -0.5rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-sm-n2,
+ .mx-sm-n2 {
+ margin-left: -0.5rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-sm-n3 {
+ margin: -1rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-sm-n3,
+ .my-sm-n3 {
+ margin-top: -1rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-sm-n3,
+ .mx-sm-n3 {
+ margin-right: -1rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-sm-n3,
+ .my-sm-n3 {
+ margin-bottom: -1rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-sm-n3,
+ .mx-sm-n3 {
+ margin-left: -1rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-sm-n4 {
+ margin: -1.5rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-sm-n4,
+ .my-sm-n4 {
+ margin-top: -1.5rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-sm-n4,
+ .mx-sm-n4 {
+ margin-right: -1.5rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-sm-n4,
+ .my-sm-n4 {
+ margin-bottom: -1.5rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-sm-n4,
+ .mx-sm-n4 {
+ margin-left: -1.5rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-sm-n5 {
+ margin: -3rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-sm-n5,
+ .my-sm-n5 {
+ margin-top: -3rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-sm-n5,
+ .mx-sm-n5 {
+ margin-right: -3rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-sm-n5,
+ .my-sm-n5 {
+ margin-bottom: -3rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-sm-n5,
+ .mx-sm-n5 {
+ margin-left: -3rem !important;
+ }
+ /* line 55, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-sm-auto {
+ margin: auto !important;
+ }
+ /* line 56, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-sm-auto,
+ .my-sm-auto {
+ margin-top: auto !important;
+ }
+ /* line 60, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-sm-auto,
+ .mx-sm-auto {
+ margin-right: auto !important;
+ }
+ /* line 64, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-sm-auto,
+ .my-sm-auto {
+ margin-bottom: auto !important;
+ }
+ /* line 68, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-sm-auto,
+ .mx-sm-auto {
+ margin-left: auto !important;
+ }
+}
+
+@media (min-width: 768px) {
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-md-0 {
+ margin: 0 !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-md-0,
+ .my-md-0 {
+ margin-top: 0 !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-md-0,
+ .mx-md-0 {
+ margin-right: 0 !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-md-0,
+ .my-md-0 {
+ margin-bottom: 0 !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-md-0,
+ .mx-md-0 {
+ margin-left: 0 !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-md-1 {
+ margin: 0.25rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-md-1,
+ .my-md-1 {
+ margin-top: 0.25rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-md-1,
+ .mx-md-1 {
+ margin-right: 0.25rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-md-1,
+ .my-md-1 {
+ margin-bottom: 0.25rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-md-1,
+ .mx-md-1 {
+ margin-left: 0.25rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-md-2 {
+ margin: 0.5rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-md-2,
+ .my-md-2 {
+ margin-top: 0.5rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-md-2,
+ .mx-md-2 {
+ margin-right: 0.5rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-md-2,
+ .my-md-2 {
+ margin-bottom: 0.5rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-md-2,
+ .mx-md-2 {
+ margin-left: 0.5rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-md-3 {
+ margin: 1rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-md-3,
+ .my-md-3 {
+ margin-top: 1rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-md-3,
+ .mx-md-3 {
+ margin-right: 1rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-md-3,
+ .my-md-3 {
+ margin-bottom: 1rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-md-3,
+ .mx-md-3 {
+ margin-left: 1rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-md-4 {
+ margin: 1.5rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-md-4,
+ .my-md-4 {
+ margin-top: 1.5rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-md-4,
+ .mx-md-4 {
+ margin-right: 1.5rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-md-4,
+ .my-md-4 {
+ margin-bottom: 1.5rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-md-4,
+ .mx-md-4 {
+ margin-left: 1.5rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-md-5 {
+ margin: 3rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-md-5,
+ .my-md-5 {
+ margin-top: 3rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-md-5,
+ .mx-md-5 {
+ margin-right: 3rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-md-5,
+ .my-md-5 {
+ margin-bottom: 3rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-md-5,
+ .mx-md-5 {
+ margin-left: 3rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-md-0 {
+ padding: 0 !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-md-0,
+ .py-md-0 {
+ padding-top: 0 !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-md-0,
+ .px-md-0 {
+ padding-right: 0 !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-md-0,
+ .py-md-0 {
+ padding-bottom: 0 !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-md-0,
+ .px-md-0 {
+ padding-left: 0 !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-md-1 {
+ padding: 0.25rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-md-1,
+ .py-md-1 {
+ padding-top: 0.25rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-md-1,
+ .px-md-1 {
+ padding-right: 0.25rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-md-1,
+ .py-md-1 {
+ padding-bottom: 0.25rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-md-1,
+ .px-md-1 {
+ padding-left: 0.25rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-md-2 {
+ padding: 0.5rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-md-2,
+ .py-md-2 {
+ padding-top: 0.5rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-md-2,
+ .px-md-2 {
+ padding-right: 0.5rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-md-2,
+ .py-md-2 {
+ padding-bottom: 0.5rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-md-2,
+ .px-md-2 {
+ padding-left: 0.5rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-md-3 {
+ padding: 1rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-md-3,
+ .py-md-3 {
+ padding-top: 1rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-md-3,
+ .px-md-3 {
+ padding-right: 1rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-md-3,
+ .py-md-3 {
+ padding-bottom: 1rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-md-3,
+ .px-md-3 {
+ padding-left: 1rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-md-4 {
+ padding: 1.5rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-md-4,
+ .py-md-4 {
+ padding-top: 1.5rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-md-4,
+ .px-md-4 {
+ padding-right: 1.5rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-md-4,
+ .py-md-4 {
+ padding-bottom: 1.5rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-md-4,
+ .px-md-4 {
+ padding-left: 1.5rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-md-5 {
+ padding: 3rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-md-5,
+ .py-md-5 {
+ padding-top: 3rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-md-5,
+ .px-md-5 {
+ padding-right: 3rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-md-5,
+ .py-md-5 {
+ padding-bottom: 3rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-md-5,
+ .px-md-5 {
+ padding-left: 3rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-md-n1 {
+ margin: -0.25rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-md-n1,
+ .my-md-n1 {
+ margin-top: -0.25rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-md-n1,
+ .mx-md-n1 {
+ margin-right: -0.25rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-md-n1,
+ .my-md-n1 {
+ margin-bottom: -0.25rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-md-n1,
+ .mx-md-n1 {
+ margin-left: -0.25rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-md-n2 {
+ margin: -0.5rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-md-n2,
+ .my-md-n2 {
+ margin-top: -0.5rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-md-n2,
+ .mx-md-n2 {
+ margin-right: -0.5rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-md-n2,
+ .my-md-n2 {
+ margin-bottom: -0.5rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-md-n2,
+ .mx-md-n2 {
+ margin-left: -0.5rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-md-n3 {
+ margin: -1rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-md-n3,
+ .my-md-n3 {
+ margin-top: -1rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-md-n3,
+ .mx-md-n3 {
+ margin-right: -1rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-md-n3,
+ .my-md-n3 {
+ margin-bottom: -1rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-md-n3,
+ .mx-md-n3 {
+ margin-left: -1rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-md-n4 {
+ margin: -1.5rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-md-n4,
+ .my-md-n4 {
+ margin-top: -1.5rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-md-n4,
+ .mx-md-n4 {
+ margin-right: -1.5rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-md-n4,
+ .my-md-n4 {
+ margin-bottom: -1.5rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-md-n4,
+ .mx-md-n4 {
+ margin-left: -1.5rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-md-n5 {
+ margin: -3rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-md-n5,
+ .my-md-n5 {
+ margin-top: -3rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-md-n5,
+ .mx-md-n5 {
+ margin-right: -3rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-md-n5,
+ .my-md-n5 {
+ margin-bottom: -3rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-md-n5,
+ .mx-md-n5 {
+ margin-left: -3rem !important;
+ }
+ /* line 55, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-md-auto {
+ margin: auto !important;
+ }
+ /* line 56, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-md-auto,
+ .my-md-auto {
+ margin-top: auto !important;
+ }
+ /* line 60, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-md-auto,
+ .mx-md-auto {
+ margin-right: auto !important;
+ }
+ /* line 64, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-md-auto,
+ .my-md-auto {
+ margin-bottom: auto !important;
+ }
+ /* line 68, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-md-auto,
+ .mx-md-auto {
+ margin-left: auto !important;
+ }
+}
+
+@media (min-width: 1024px) {
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-lg-0 {
+ margin: 0 !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-lg-0,
+ .my-lg-0 {
+ margin-top: 0 !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-lg-0,
+ .mx-lg-0 {
+ margin-right: 0 !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-lg-0,
+ .my-lg-0 {
+ margin-bottom: 0 !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-lg-0,
+ .mx-lg-0 {
+ margin-left: 0 !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-lg-1 {
+ margin: 0.25rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-lg-1,
+ .my-lg-1 {
+ margin-top: 0.25rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-lg-1,
+ .mx-lg-1 {
+ margin-right: 0.25rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-lg-1,
+ .my-lg-1 {
+ margin-bottom: 0.25rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-lg-1,
+ .mx-lg-1 {
+ margin-left: 0.25rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-lg-2 {
+ margin: 0.5rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-lg-2,
+ .my-lg-2 {
+ margin-top: 0.5rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-lg-2,
+ .mx-lg-2 {
+ margin-right: 0.5rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-lg-2,
+ .my-lg-2 {
+ margin-bottom: 0.5rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-lg-2,
+ .mx-lg-2 {
+ margin-left: 0.5rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-lg-3 {
+ margin: 1rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-lg-3,
+ .my-lg-3 {
+ margin-top: 1rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-lg-3,
+ .mx-lg-3 {
+ margin-right: 1rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-lg-3,
+ .my-lg-3 {
+ margin-bottom: 1rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-lg-3,
+ .mx-lg-3 {
+ margin-left: 1rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-lg-4 {
+ margin: 1.5rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-lg-4,
+ .my-lg-4 {
+ margin-top: 1.5rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-lg-4,
+ .mx-lg-4 {
+ margin-right: 1.5rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-lg-4,
+ .my-lg-4 {
+ margin-bottom: 1.5rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-lg-4,
+ .mx-lg-4 {
+ margin-left: 1.5rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-lg-5 {
+ margin: 3rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-lg-5,
+ .my-lg-5 {
+ margin-top: 3rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-lg-5,
+ .mx-lg-5 {
+ margin-right: 3rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-lg-5,
+ .my-lg-5 {
+ margin-bottom: 3rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-lg-5,
+ .mx-lg-5 {
+ margin-left: 3rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-lg-0 {
+ padding: 0 !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-lg-0,
+ .py-lg-0 {
+ padding-top: 0 !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-lg-0,
+ .px-lg-0 {
+ padding-right: 0 !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-lg-0,
+ .py-lg-0 {
+ padding-bottom: 0 !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-lg-0,
+ .px-lg-0 {
+ padding-left: 0 !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-lg-1 {
+ padding: 0.25rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-lg-1,
+ .py-lg-1 {
+ padding-top: 0.25rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-lg-1,
+ .px-lg-1 {
+ padding-right: 0.25rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-lg-1,
+ .py-lg-1 {
+ padding-bottom: 0.25rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-lg-1,
+ .px-lg-1 {
+ padding-left: 0.25rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-lg-2 {
+ padding: 0.5rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-lg-2,
+ .py-lg-2 {
+ padding-top: 0.5rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-lg-2,
+ .px-lg-2 {
+ padding-right: 0.5rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-lg-2,
+ .py-lg-2 {
+ padding-bottom: 0.5rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-lg-2,
+ .px-lg-2 {
+ padding-left: 0.5rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-lg-3 {
+ padding: 1rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-lg-3,
+ .py-lg-3 {
+ padding-top: 1rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-lg-3,
+ .px-lg-3 {
+ padding-right: 1rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-lg-3,
+ .py-lg-3 {
+ padding-bottom: 1rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-lg-3,
+ .px-lg-3 {
+ padding-left: 1rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-lg-4 {
+ padding: 1.5rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-lg-4,
+ .py-lg-4 {
+ padding-top: 1.5rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-lg-4,
+ .px-lg-4 {
+ padding-right: 1.5rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-lg-4,
+ .py-lg-4 {
+ padding-bottom: 1.5rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-lg-4,
+ .px-lg-4 {
+ padding-left: 1.5rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-lg-5 {
+ padding: 3rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-lg-5,
+ .py-lg-5 {
+ padding-top: 3rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-lg-5,
+ .px-lg-5 {
+ padding-right: 3rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-lg-5,
+ .py-lg-5 {
+ padding-bottom: 3rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-lg-5,
+ .px-lg-5 {
+ padding-left: 3rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-lg-n1 {
+ margin: -0.25rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-lg-n1,
+ .my-lg-n1 {
+ margin-top: -0.25rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-lg-n1,
+ .mx-lg-n1 {
+ margin-right: -0.25rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-lg-n1,
+ .my-lg-n1 {
+ margin-bottom: -0.25rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-lg-n1,
+ .mx-lg-n1 {
+ margin-left: -0.25rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-lg-n2 {
+ margin: -0.5rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-lg-n2,
+ .my-lg-n2 {
+ margin-top: -0.5rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-lg-n2,
+ .mx-lg-n2 {
+ margin-right: -0.5rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-lg-n2,
+ .my-lg-n2 {
+ margin-bottom: -0.5rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-lg-n2,
+ .mx-lg-n2 {
+ margin-left: -0.5rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-lg-n3 {
+ margin: -1rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-lg-n3,
+ .my-lg-n3 {
+ margin-top: -1rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-lg-n3,
+ .mx-lg-n3 {
+ margin-right: -1rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-lg-n3,
+ .my-lg-n3 {
+ margin-bottom: -1rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-lg-n3,
+ .mx-lg-n3 {
+ margin-left: -1rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-lg-n4 {
+ margin: -1.5rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-lg-n4,
+ .my-lg-n4 {
+ margin-top: -1.5rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-lg-n4,
+ .mx-lg-n4 {
+ margin-right: -1.5rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-lg-n4,
+ .my-lg-n4 {
+ margin-bottom: -1.5rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-lg-n4,
+ .mx-lg-n4 {
+ margin-left: -1.5rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-lg-n5 {
+ margin: -3rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-lg-n5,
+ .my-lg-n5 {
+ margin-top: -3rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-lg-n5,
+ .mx-lg-n5 {
+ margin-right: -3rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-lg-n5,
+ .my-lg-n5 {
+ margin-bottom: -3rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-lg-n5,
+ .mx-lg-n5 {
+ margin-left: -3rem !important;
+ }
+ /* line 55, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-lg-auto {
+ margin: auto !important;
+ }
+ /* line 56, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-lg-auto,
+ .my-lg-auto {
+ margin-top: auto !important;
+ }
+ /* line 60, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-lg-auto,
+ .mx-lg-auto {
+ margin-right: auto !important;
+ }
+ /* line 64, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-lg-auto,
+ .my-lg-auto {
+ margin-bottom: auto !important;
+ }
+ /* line 68, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-lg-auto,
+ .mx-lg-auto {
+ margin-left: auto !important;
+ }
+}
+
+@media (min-width: 1280px) {
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-xl-0 {
+ margin: 0 !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-xl-0,
+ .my-xl-0 {
+ margin-top: 0 !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-xl-0,
+ .mx-xl-0 {
+ margin-right: 0 !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-xl-0,
+ .my-xl-0 {
+ margin-bottom: 0 !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-xl-0,
+ .mx-xl-0 {
+ margin-left: 0 !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-xl-1 {
+ margin: 0.25rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-xl-1,
+ .my-xl-1 {
+ margin-top: 0.25rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-xl-1,
+ .mx-xl-1 {
+ margin-right: 0.25rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-xl-1,
+ .my-xl-1 {
+ margin-bottom: 0.25rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-xl-1,
+ .mx-xl-1 {
+ margin-left: 0.25rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-xl-2 {
+ margin: 0.5rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-xl-2,
+ .my-xl-2 {
+ margin-top: 0.5rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-xl-2,
+ .mx-xl-2 {
+ margin-right: 0.5rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-xl-2,
+ .my-xl-2 {
+ margin-bottom: 0.5rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-xl-2,
+ .mx-xl-2 {
+ margin-left: 0.5rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-xl-3 {
+ margin: 1rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-xl-3,
+ .my-xl-3 {
+ margin-top: 1rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-xl-3,
+ .mx-xl-3 {
+ margin-right: 1rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-xl-3,
+ .my-xl-3 {
+ margin-bottom: 1rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-xl-3,
+ .mx-xl-3 {
+ margin-left: 1rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-xl-4 {
+ margin: 1.5rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-xl-4,
+ .my-xl-4 {
+ margin-top: 1.5rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-xl-4,
+ .mx-xl-4 {
+ margin-right: 1.5rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-xl-4,
+ .my-xl-4 {
+ margin-bottom: 1.5rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-xl-4,
+ .mx-xl-4 {
+ margin-left: 1.5rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-xl-5 {
+ margin: 3rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-xl-5,
+ .my-xl-5 {
+ margin-top: 3rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-xl-5,
+ .mx-xl-5 {
+ margin-right: 3rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-xl-5,
+ .my-xl-5 {
+ margin-bottom: 3rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-xl-5,
+ .mx-xl-5 {
+ margin-left: 3rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-xl-0 {
+ padding: 0 !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-xl-0,
+ .py-xl-0 {
+ padding-top: 0 !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-xl-0,
+ .px-xl-0 {
+ padding-right: 0 !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-xl-0,
+ .py-xl-0 {
+ padding-bottom: 0 !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-xl-0,
+ .px-xl-0 {
+ padding-left: 0 !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-xl-1 {
+ padding: 0.25rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-xl-1,
+ .py-xl-1 {
+ padding-top: 0.25rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-xl-1,
+ .px-xl-1 {
+ padding-right: 0.25rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-xl-1,
+ .py-xl-1 {
+ padding-bottom: 0.25rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-xl-1,
+ .px-xl-1 {
+ padding-left: 0.25rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-xl-2 {
+ padding: 0.5rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-xl-2,
+ .py-xl-2 {
+ padding-top: 0.5rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-xl-2,
+ .px-xl-2 {
+ padding-right: 0.5rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-xl-2,
+ .py-xl-2 {
+ padding-bottom: 0.5rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-xl-2,
+ .px-xl-2 {
+ padding-left: 0.5rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-xl-3 {
+ padding: 1rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-xl-3,
+ .py-xl-3 {
+ padding-top: 1rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-xl-3,
+ .px-xl-3 {
+ padding-right: 1rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-xl-3,
+ .py-xl-3 {
+ padding-bottom: 1rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-xl-3,
+ .px-xl-3 {
+ padding-left: 1rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-xl-4 {
+ padding: 1.5rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-xl-4,
+ .py-xl-4 {
+ padding-top: 1.5rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-xl-4,
+ .px-xl-4 {
+ padding-right: 1.5rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-xl-4,
+ .py-xl-4 {
+ padding-bottom: 1.5rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-xl-4,
+ .px-xl-4 {
+ padding-left: 1.5rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-xl-5 {
+ padding: 3rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-xl-5,
+ .py-xl-5 {
+ padding-top: 3rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-xl-5,
+ .px-xl-5 {
+ padding-right: 3rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-xl-5,
+ .py-xl-5 {
+ padding-bottom: 3rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-xl-5,
+ .px-xl-5 {
+ padding-left: 3rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-xl-n1 {
+ margin: -0.25rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-xl-n1,
+ .my-xl-n1 {
+ margin-top: -0.25rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-xl-n1,
+ .mx-xl-n1 {
+ margin-right: -0.25rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-xl-n1,
+ .my-xl-n1 {
+ margin-bottom: -0.25rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-xl-n1,
+ .mx-xl-n1 {
+ margin-left: -0.25rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-xl-n2 {
+ margin: -0.5rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-xl-n2,
+ .my-xl-n2 {
+ margin-top: -0.5rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-xl-n2,
+ .mx-xl-n2 {
+ margin-right: -0.5rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-xl-n2,
+ .my-xl-n2 {
+ margin-bottom: -0.5rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-xl-n2,
+ .mx-xl-n2 {
+ margin-left: -0.5rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-xl-n3 {
+ margin: -1rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-xl-n3,
+ .my-xl-n3 {
+ margin-top: -1rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-xl-n3,
+ .mx-xl-n3 {
+ margin-right: -1rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-xl-n3,
+ .my-xl-n3 {
+ margin-bottom: -1rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-xl-n3,
+ .mx-xl-n3 {
+ margin-left: -1rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-xl-n4 {
+ margin: -1.5rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-xl-n4,
+ .my-xl-n4 {
+ margin-top: -1.5rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-xl-n4,
+ .mx-xl-n4 {
+ margin-right: -1.5rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-xl-n4,
+ .my-xl-n4 {
+ margin-bottom: -1.5rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-xl-n4,
+ .mx-xl-n4 {
+ margin-left: -1.5rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-xl-n5 {
+ margin: -3rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-xl-n5,
+ .my-xl-n5 {
+ margin-top: -3rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-xl-n5,
+ .mx-xl-n5 {
+ margin-right: -3rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-xl-n5,
+ .my-xl-n5 {
+ margin-bottom: -3rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-xl-n5,
+ .mx-xl-n5 {
+ margin-left: -3rem !important;
+ }
+ /* line 55, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-xl-auto {
+ margin: auto !important;
+ }
+ /* line 56, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-xl-auto,
+ .my-xl-auto {
+ margin-top: auto !important;
+ }
+ /* line 60, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-xl-auto,
+ .mx-xl-auto {
+ margin-right: auto !important;
+ }
+ /* line 64, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-xl-auto,
+ .my-xl-auto {
+ margin-bottom: auto !important;
+ }
+ /* line 68, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-xl-auto,
+ .mx-xl-auto {
+ margin-left: auto !important;
+ }
+}
+
+@media (min-width: 1440px) {
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-xxl-0 {
+ margin: 0 !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-xxl-0,
+ .my-xxl-0 {
+ margin-top: 0 !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-xxl-0,
+ .mx-xxl-0 {
+ margin-right: 0 !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-xxl-0,
+ .my-xxl-0 {
+ margin-bottom: 0 !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-xxl-0,
+ .mx-xxl-0 {
+ margin-left: 0 !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-xxl-1 {
+ margin: 0.25rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-xxl-1,
+ .my-xxl-1 {
+ margin-top: 0.25rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-xxl-1,
+ .mx-xxl-1 {
+ margin-right: 0.25rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-xxl-1,
+ .my-xxl-1 {
+ margin-bottom: 0.25rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-xxl-1,
+ .mx-xxl-1 {
+ margin-left: 0.25rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-xxl-2 {
+ margin: 0.5rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-xxl-2,
+ .my-xxl-2 {
+ margin-top: 0.5rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-xxl-2,
+ .mx-xxl-2 {
+ margin-right: 0.5rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-xxl-2,
+ .my-xxl-2 {
+ margin-bottom: 0.5rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-xxl-2,
+ .mx-xxl-2 {
+ margin-left: 0.5rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-xxl-3 {
+ margin: 1rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-xxl-3,
+ .my-xxl-3 {
+ margin-top: 1rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-xxl-3,
+ .mx-xxl-3 {
+ margin-right: 1rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-xxl-3,
+ .my-xxl-3 {
+ margin-bottom: 1rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-xxl-3,
+ .mx-xxl-3 {
+ margin-left: 1rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-xxl-4 {
+ margin: 1.5rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-xxl-4,
+ .my-xxl-4 {
+ margin-top: 1.5rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-xxl-4,
+ .mx-xxl-4 {
+ margin-right: 1.5rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-xxl-4,
+ .my-xxl-4 {
+ margin-bottom: 1.5rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-xxl-4,
+ .mx-xxl-4 {
+ margin-left: 1.5rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-xxl-5 {
+ margin: 3rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-xxl-5,
+ .my-xxl-5 {
+ margin-top: 3rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-xxl-5,
+ .mx-xxl-5 {
+ margin-right: 3rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-xxl-5,
+ .my-xxl-5 {
+ margin-bottom: 3rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-xxl-5,
+ .mx-xxl-5 {
+ margin-left: 3rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-xxl-0 {
+ padding: 0 !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-xxl-0,
+ .py-xxl-0 {
+ padding-top: 0 !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-xxl-0,
+ .px-xxl-0 {
+ padding-right: 0 !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-xxl-0,
+ .py-xxl-0 {
+ padding-bottom: 0 !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-xxl-0,
+ .px-xxl-0 {
+ padding-left: 0 !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-xxl-1 {
+ padding: 0.25rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-xxl-1,
+ .py-xxl-1 {
+ padding-top: 0.25rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-xxl-1,
+ .px-xxl-1 {
+ padding-right: 0.25rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-xxl-1,
+ .py-xxl-1 {
+ padding-bottom: 0.25rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-xxl-1,
+ .px-xxl-1 {
+ padding-left: 0.25rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-xxl-2 {
+ padding: 0.5rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-xxl-2,
+ .py-xxl-2 {
+ padding-top: 0.5rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-xxl-2,
+ .px-xxl-2 {
+ padding-right: 0.5rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-xxl-2,
+ .py-xxl-2 {
+ padding-bottom: 0.5rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-xxl-2,
+ .px-xxl-2 {
+ padding-left: 0.5rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-xxl-3 {
+ padding: 1rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-xxl-3,
+ .py-xxl-3 {
+ padding-top: 1rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-xxl-3,
+ .px-xxl-3 {
+ padding-right: 1rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-xxl-3,
+ .py-xxl-3 {
+ padding-bottom: 1rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-xxl-3,
+ .px-xxl-3 {
+ padding-left: 1rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-xxl-4 {
+ padding: 1.5rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-xxl-4,
+ .py-xxl-4 {
+ padding-top: 1.5rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-xxl-4,
+ .px-xxl-4 {
+ padding-right: 1.5rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-xxl-4,
+ .py-xxl-4 {
+ padding-bottom: 1.5rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-xxl-4,
+ .px-xxl-4 {
+ padding-left: 1.5rem !important;
+ }
+ /* line 11, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .p-xxl-5 {
+ padding: 3rem !important;
+ }
+ /* line 12, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pt-xxl-5,
+ .py-xxl-5 {
+ padding-top: 3rem !important;
+ }
+ /* line 16, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pr-xxl-5,
+ .px-xxl-5 {
+ padding-right: 3rem !important;
+ }
+ /* line 20, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pb-xxl-5,
+ .py-xxl-5 {
+ padding-bottom: 3rem !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .pl-xxl-5,
+ .px-xxl-5 {
+ padding-left: 3rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-xxl-n1 {
+ margin: -0.25rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-xxl-n1,
+ .my-xxl-n1 {
+ margin-top: -0.25rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-xxl-n1,
+ .mx-xxl-n1 {
+ margin-right: -0.25rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-xxl-n1,
+ .my-xxl-n1 {
+ margin-bottom: -0.25rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-xxl-n1,
+ .mx-xxl-n1 {
+ margin-left: -0.25rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-xxl-n2 {
+ margin: -0.5rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-xxl-n2,
+ .my-xxl-n2 {
+ margin-top: -0.5rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-xxl-n2,
+ .mx-xxl-n2 {
+ margin-right: -0.5rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-xxl-n2,
+ .my-xxl-n2 {
+ margin-bottom: -0.5rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-xxl-n2,
+ .mx-xxl-n2 {
+ margin-left: -0.5rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-xxl-n3 {
+ margin: -1rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-xxl-n3,
+ .my-xxl-n3 {
+ margin-top: -1rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-xxl-n3,
+ .mx-xxl-n3 {
+ margin-right: -1rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-xxl-n3,
+ .my-xxl-n3 {
+ margin-bottom: -1rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-xxl-n3,
+ .mx-xxl-n3 {
+ margin-left: -1rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-xxl-n4 {
+ margin: -1.5rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-xxl-n4,
+ .my-xxl-n4 {
+ margin-top: -1.5rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-xxl-n4,
+ .mx-xxl-n4 {
+ margin-right: -1.5rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-xxl-n4,
+ .my-xxl-n4 {
+ margin-bottom: -1.5rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-xxl-n4,
+ .mx-xxl-n4 {
+ margin-left: -1.5rem !important;
+ }
+ /* line 34, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-xxl-n5 {
+ margin: -3rem !important;
+ }
+ /* line 35, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-xxl-n5,
+ .my-xxl-n5 {
+ margin-top: -3rem !important;
+ }
+ /* line 39, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-xxl-n5,
+ .mx-xxl-n5 {
+ margin-right: -3rem !important;
+ }
+ /* line 43, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-xxl-n5,
+ .my-xxl-n5 {
+ margin-bottom: -3rem !important;
+ }
+ /* line 47, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-xxl-n5,
+ .mx-xxl-n5 {
+ margin-left: -3rem !important;
+ }
+ /* line 55, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .m-xxl-auto {
+ margin: auto !important;
+ }
+ /* line 56, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mt-xxl-auto,
+ .my-xxl-auto {
+ margin-top: auto !important;
+ }
+ /* line 60, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mr-xxl-auto,
+ .mx-xxl-auto {
+ margin-right: auto !important;
+ }
+ /* line 64, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .mb-xxl-auto,
+ .my-xxl-auto {
+ margin-bottom: auto !important;
+ }
+ /* line 68, node_modules/bootstrap/scss/utilities/_spacing.scss */
+ .ml-xxl-auto,
+ .mx-xxl-auto {
+ margin-left: auto !important;
+ }
+}
+
+/* line 6, node_modules/bootstrap/scss/utilities/_stretched-link.scss */
+.stretched-link::after {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1;
+ pointer-events: auto;
+ content: "";
+ background-color: rgba(0, 0, 0, 0);
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-monospace {
+ font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-justify {
+ text-align: justify !important;
+}
+
+/* line 12, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-wrap {
+ white-space: normal !important;
+}
+
+/* line 13, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-nowrap {
+ white-space: nowrap !important;
+}
+
+/* line 14, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-truncate {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* line 22, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-left {
+ text-align: left !important;
+}
+
+/* line 23, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-right {
+ text-align: right !important;
+}
+
+/* line 24, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-center {
+ text-align: center !important;
+}
+
+@media (min-width: 576px) {
+ /* line 22, node_modules/bootstrap/scss/utilities/_text.scss */
+ .text-sm-left {
+ text-align: left !important;
+ }
+ /* line 23, node_modules/bootstrap/scss/utilities/_text.scss */
+ .text-sm-right {
+ text-align: right !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_text.scss */
+ .text-sm-center {
+ text-align: center !important;
+ }
+}
+
+@media (min-width: 768px) {
+ /* line 22, node_modules/bootstrap/scss/utilities/_text.scss */
+ .text-md-left {
+ text-align: left !important;
+ }
+ /* line 23, node_modules/bootstrap/scss/utilities/_text.scss */
+ .text-md-right {
+ text-align: right !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_text.scss */
+ .text-md-center {
+ text-align: center !important;
+ }
+}
+
+@media (min-width: 1024px) {
+ /* line 22, node_modules/bootstrap/scss/utilities/_text.scss */
+ .text-lg-left {
+ text-align: left !important;
+ }
+ /* line 23, node_modules/bootstrap/scss/utilities/_text.scss */
+ .text-lg-right {
+ text-align: right !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_text.scss */
+ .text-lg-center {
+ text-align: center !important;
+ }
+}
+
+@media (min-width: 1280px) {
+ /* line 22, node_modules/bootstrap/scss/utilities/_text.scss */
+ .text-xl-left {
+ text-align: left !important;
+ }
+ /* line 23, node_modules/bootstrap/scss/utilities/_text.scss */
+ .text-xl-right {
+ text-align: right !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_text.scss */
+ .text-xl-center {
+ text-align: center !important;
+ }
+}
+
+@media (min-width: 1440px) {
+ /* line 22, node_modules/bootstrap/scss/utilities/_text.scss */
+ .text-xxl-left {
+ text-align: left !important;
+ }
+ /* line 23, node_modules/bootstrap/scss/utilities/_text.scss */
+ .text-xxl-right {
+ text-align: right !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/utilities/_text.scss */
+ .text-xxl-center {
+ text-align: center !important;
+ }
+}
+
+/* line 30, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-lowercase {
+ text-transform: lowercase !important;
+}
+
+/* line 31, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-uppercase {
+ text-transform: uppercase !important;
+}
+
+/* line 32, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-capitalize {
+ text-transform: capitalize !important;
+}
+
+/* line 36, node_modules/bootstrap/scss/utilities/_text.scss */
+.font-weight-light {
+ font-weight: 300 !important;
+}
+
+/* line 37, node_modules/bootstrap/scss/utilities/_text.scss */
+.font-weight-lighter {
+ font-weight: lighter !important;
+}
+
+/* line 38, node_modules/bootstrap/scss/utilities/_text.scss */
+.font-weight-normal {
+ font-weight: 400 !important;
+}
+
+/* line 39, node_modules/bootstrap/scss/utilities/_text.scss */
+.font-weight-bold {
+ font-weight: 700 !important;
+}
+
+/* line 40, node_modules/bootstrap/scss/utilities/_text.scss */
+.font-weight-bolder {
+ font-weight: 800 !important;
+}
+
+/* line 41, node_modules/bootstrap/scss/utilities/_text.scss */
+.font-italic {
+ font-style: italic !important;
+}
+
+/* line 45, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-white {
+ color: #ffffff !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_text-emphasis.scss */
+.text-primary {
+ color: #464746 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.text-primary:hover, a.text-primary:focus {
+ color: #202020 !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_text-emphasis.scss */
+.text-secondary {
+ color: #f0ebe3 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.text-secondary:hover, a.text-secondary:focus {
+ color: #d5c7b1 !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_text-emphasis.scss */
+.text-success {
+ color: #f0ebe3 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.text-success:hover, a.text-success:focus {
+ color: #d5c7b1 !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_text-emphasis.scss */
+.text-info {
+ color: #464746 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.text-info:hover, a.text-info:focus {
+ color: #202020 !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_text-emphasis.scss */
+.text-warning {
+ color: #464746 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.text-warning:hover, a.text-warning:focus {
+ color: #202020 !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_text-emphasis.scss */
+.text-danger {
+ color: #e54a19 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.text-danger:hover, a.text-danger:focus {
+ color: #a03411 !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_text-emphasis.scss */
+.text-light {
+ color: #f7f7f7 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.text-light:hover, a.text-light:focus {
+ color: #d1d1d1 !important;
+}
+
+/* line 6, node_modules/bootstrap/scss/mixins/_text-emphasis.scss */
+.text-dark {
+ color: #343a40 !important;
+}
+
+/* line 17, node_modules/bootstrap/scss/mixins/_hover.scss */
+a.text-dark:hover, a.text-dark:focus {
+ color: #121416 !important;
+}
+
+/* line 51, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-body {
+ color: #464746 !important;
+}
+
+/* line 52, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-muted {
+ color: #6c757d !important;
+}
+
+/* line 54, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-black-50 {
+ color: rgba(0, 0, 0, 0.5) !important;
+}
+
+/* line 55, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-white-50 {
+ color: rgba(255, 255, 255, 0.5) !important;
+}
+
+/* line 59, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-hide {
+ font: 0/0 a;
+ color: transparent;
+ text-shadow: none;
+ background-color: transparent;
+ border: 0;
+}
+
+/* line 63, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-decoration-none {
+ text-decoration: none !important;
+}
+
+/* line 65, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-break {
+ word-break: break-word !important;
+ overflow-wrap: break-word !important;
+}
+
+/* line 72, node_modules/bootstrap/scss/utilities/_text.scss */
+.text-reset {
+ color: inherit !important;
+}
+
+/* line 7, node_modules/bootstrap/scss/utilities/_visibility.scss */
+.visible {
+ visibility: visible !important;
+}
+
+/* line 11, node_modules/bootstrap/scss/utilities/_visibility.scss */
+.invisible {
+ visibility: hidden !important;
+}
+
+@media print {
+ /* line 13, node_modules/bootstrap/scss/_print.scss */
+ *,
+ *::before,
+ *::after {
+ text-shadow: none !important;
+ -webkit-box-shadow: none !important;
+ box-shadow: none !important;
+ }
+ /* line 24, node_modules/bootstrap/scss/_print.scss */
+ a:not(.btn) {
+ text-decoration: underline;
+ }
+ /* line 34, node_modules/bootstrap/scss/_print.scss */
+ abbr[title]::after {
+ content: " (" attr(title) ")";
+ }
+ /* line 49, node_modules/bootstrap/scss/_print.scss */
+ pre {
+ white-space: pre-wrap !important;
+ }
+ /* line 52, node_modules/bootstrap/scss/_print.scss */
+ pre,
+ blockquote {
+ border: 1px solid #adb5bd;
+ page-break-inside: avoid;
+ }
+ /* line 63, node_modules/bootstrap/scss/_print.scss */
+ thead {
+ display: table-header-group;
+ }
+ /* line 67, node_modules/bootstrap/scss/_print.scss */
+ tr,
+ img {
+ page-break-inside: avoid;
+ }
+ /* line 72, node_modules/bootstrap/scss/_print.scss */
+ p,
+ h2,
+ h3 {
+ orphans: 3;
+ widows: 3;
+ }
+ /* line 79, node_modules/bootstrap/scss/_print.scss */
+ h2,
+ h3 {
+ page-break-after: avoid;
+ }
+ @page {
+ size: a3;
+ }
+ /* line 92, node_modules/bootstrap/scss/_print.scss */
+ body {
+ min-width: 1024px !important;
+ }
+ /* line 95, node_modules/bootstrap/scss/_print.scss */
+ .container {
+ min-width: 1024px !important;
+ }
+ /* line 100, node_modules/bootstrap/scss/_print.scss */
+ .navbar {
+ display: none;
+ }
+ /* line 103, node_modules/bootstrap/scss/_print.scss */
+ .badge {
+ border: 1px solid #000000;
+ }
+ /* line 107, node_modules/bootstrap/scss/_print.scss */
+ .table {
+ border-collapse: collapse !important;
+ }
+ /* line 110, node_modules/bootstrap/scss/_print.scss */
+ .table td,
+ .table th {
+ background-color: #ffffff !important;
+ }
+ /* line 117, node_modules/bootstrap/scss/_print.scss */
+ .table-bordered th,
+ .table-bordered td {
+ border: 1px solid #dee2e6 !important;
+ }
+ /* line 123, node_modules/bootstrap/scss/_print.scss */
+ .table-dark {
+ color: inherit;
+ }
+ /* line 126, node_modules/bootstrap/scss/_print.scss */
+ .table-dark th,
+ .table-dark td,
+ .table-dark thead th,
+ .table-dark tbody + tbody {
+ border-color: #dee2e6;
+ }
+ /* line 134, node_modules/bootstrap/scss/_print.scss */
+ .table .thead-dark th {
+ color: inherit;
+ border-color: #dee2e6;
+ }
+}
+
+/**
+ * Set up a decent box model on the root element
+ */
+/* line 8, src/assets/scss/base/_base.scss */
+html {
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+/**
+ * Make all elements from the DOM inherit from the parent box-sizing
+ * Since `*` has a specificity of 0, it does not override the `html` value
+ * making all elements inheriting from the root box-sizing value
+ * See: https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/
+ */
+/* line 18, src/assets/scss/base/_base.scss */
+*,
+*::before,
+*::after {
+ -webkit-box-sizing: inherit;
+ box-sizing: inherit;
+}
+
+/**
+ * Basic styles for links
+ */
+/* line 27, src/assets/scss/base/_base.scss */
+a {
+ color: #e54a19;
+ text-decoration: none;
+}
+
+/**
+ * Basic typography style for copy text
+ A solution for this problem is percentage. Usually default font-size of the browser is 16px.
+ Setting font-size: 100% will make 1rem = 16px. But it will make calculations a little difficult.
+ A better way is to set font-size: 62.5%. Because 62.5% of 16px is 10px. Which makes 1rem = 10px.
+ CALCULATION: Element font size in rem x 16px;
+ */
+/* line 16, src/assets/scss/base/_typography.scss */
+body {
+ font-size: 0.875rem;
+ font-weight: 300;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+ letter-spacing: 0.4px;
+ line-height: 1.5rem;
+ color: #464746;
+}
+
+@media (min-width: 1024px) {
+ /* line 16, src/assets/scss/base/_typography.scss */
+ body {
+ font-size: 1rem;
+ }
+}
+
+/* line 30, src/assets/scss/base/_typography.scss */
+h1, h2, h3, h4, h5, h6,
+.h1, .h2, .h3, .h4, .h5, .h6 {
+ margin-bottom: 28px;
+}
+
+@media (min-width: 1024px) {
+ /* line 30, src/assets/scss/base/_typography.scss */
+ h1, h2, h3, h4, h5, h6,
+ .h1, .h2, .h3, .h4, .h5, .h6 {
+ margin-bottom: 36px;
+ }
+}
+
+/* line 49, src/assets/scss/base/_typography.scss */
+ol,
+ul,
+p,
+blockquote,
+.preamble {
+ margin-bottom: 28px;
+}
+
+@media (min-width: 1024px) {
+ /* line 49, src/assets/scss/base/_typography.scss */
+ ol,
+ ul,
+ p,
+ blockquote,
+ .preamble {
+ margin-bottom: 36px;
+ }
+}
+
+/* line 63, src/assets/scss/base/_typography.scss */
+h1,
+h2,
+h3,
+h4,
+.h1,
+.h2,
+.h3,
+.h4 {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+}
+
+/* line 74, src/assets/scss/base/_typography.scss */
+h5,
+h6,
+.h5,
+.h6 {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+}
+
+/* line 81, src/assets/scss/base/_typography.scss */
+h1, .h1 {
+ color: #111111;
+ font-size: 2.25rem;
+ font-weight: 400;
+ line-height: 2.5rem;
+ letter-spacing: 0.13px;
+ text-transform: uppercase;
+}
+
+@media (min-width: 1024px) {
+ /* line 81, src/assets/scss/base/_typography.scss */
+ h1, .h1 {
+ font-size: 4rem;
+ letter-spacing: 0.22px;
+ line-height: 4.5rem;
+ }
+}
+
+/* line 97, src/assets/scss/base/_typography.scss */
+h2, .h2 {
+ color: #111111;
+ font-size: 1.5rem;
+ font-weight: 400;
+ letter-spacing: 0.08px;
+ line-height: 2.25rem;
+ text-transform: initial;
+}
+
+@media (min-width: 1024px) {
+ /* line 97, src/assets/scss/base/_typography.scss */
+ h2, .h2 {
+ font-size: 2.4375rem;
+ letter-spacing: 0.14px;
+ line-height: 3rem;
+ }
+}
+
+/* line 113, src/assets/scss/base/_typography.scss */
+h3, .h3 {
+ color: #111111;
+ font-size: 1.0625rem;
+ font-weight: 400;
+ letter-spacing: 0.06px;
+ line-height: 1.5rem;
+ text-transform: initial;
+}
+
+@media (min-width: 1024px) {
+ /* line 113, src/assets/scss/base/_typography.scss */
+ h3, .h3 {
+ font-size: 1.5rem;
+ letter-spacing: 0.08px;
+ line-height: 2.25rem;
+ }
+}
+
+/* line 129, src/assets/scss/base/_typography.scss */
+h4, .h4 {
+ color: #111111;
+ font-size: 0.875rem;
+ font-weight: 400;
+ letter-spacing: 0.05px;
+ line-height: 1.5rem;
+ text-transform: uppercase;
+}
+
+@media (min-width: 1024px) {
+ /* line 129, src/assets/scss/base/_typography.scss */
+ h4, .h4 {
+ font-size: 0.9375rem;
+ line-height: 1.5rem;
+ }
+}
+
+/* line 144, src/assets/scss/base/_typography.scss */
+h5, .h5 {
+ color: #111111;
+ font-size: 0.75rem;
+ font-weight: 700;
+ letter-spacing: normal;
+ line-height: 1.5rem;
+ text-transform: initial;
+}
+
+@media (min-width: 1024px) {
+ /* line 144, src/assets/scss/base/_typography.scss */
+ h5, .h5 {
+ font-size: 0.75rem;
+ line-height: 1.5rem;
+ }
+}
+
+/* line 159, src/assets/scss/base/_typography.scss */
+h6, .h6 {
+ color: #111111;
+ font-size: 0.6875rem;
+ font-weight: 700;
+ letter-spacing: normal;
+ line-height: 1.5rem;
+ text-transform: initial;
+}
+
+@media (min-width: 1024px) {
+ /* line 159, src/assets/scss/base/_typography.scss */
+ h6, .h6 {
+ font-size: 0.6875rem;
+ line-height: 1.5rem;
+ }
+}
+
+/**
+ * Clear inner floats
+ */
+/* line 8, src/assets/scss/base/_helpers.scss */
+.clearfix::after {
+ clear: both;
+ content: '';
+ display: table;
+}
+
+/**
+ * Hide text while making it readable for screen readers
+ * 1. Needed in WebKit-based browsers because of an implementation bug;
+ * See: https://code.google.com/p/chromium/issues/detail?id=457146
+ */
+/* line 19, src/assets/scss/base/_helpers.scss */
+.hide-text {
+ overflow: hidden;
+ padding: 0;
+ /* 1 */
+ text-indent: 101%;
+ white-space: nowrap;
+}
+
+/**
+ * Hide element while making it readable for screen readers
+ * Shamelessly borrowed from HTML5Boilerplate:
+ * https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css#L119-L133
+ */
+/* line 31, src/assets/scss/base/_helpers.scss */
+.visually-hidden {
+ border: 0;
+ clip: rect(0 0 0 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+}
+
+/* line 4, src/assets/scss/layout/_header.scss */
+.logo-container {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-orient: horizontal;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-box-pack: justify;
+ -ms-flex-pack: justify;
+ justify-content: space-between;
+ padding: 20px 0;
+ margin: 10px 0 40px;
+ border-bottom: 1px solid #eaebea;
+}
+
+/* line 12, src/assets/scss/layout/_header.scss */
+.logo-container img {
+ display: block;
+ max-width: 50%;
+ height: 100%;
+}
+
+/* line 17, src/assets/scss/layout/_header.scss */
+.logo-container img:last-child {
+ max-width: 35%;
+}
+
+/* line 4, src/assets/scss/layout/_footer.scss */
+.footer {
+ border-top: 1px solid #f7f7f7;
+}
+
+/* line 1, src/assets/scss/components/_alert.scss */
+.alert {
+ border-radius: 0;
+ margin-top: 10px;
+ background: none;
+ border-width: 2px;
+ padding: 15px;
+ line-height: 1.4;
+}
+
+/* line 9, src/assets/scss/components/_alert.scss */
+.alert-danger {
+ color: #e54a19;
+}
+
+/* line 5, src/assets/scss/components/_button.scss */
+.btn {
+ border-radius: 0;
+}
+
+/* line 8, src/assets/scss/components/_button.scss */
+.btn.btn-lg, .btn-group-lg > .btn {
+ font-size: 21px;
+}
+
+/* line 2, src/assets/scss/components/_form.scss */
+form .form-group {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ margin-bottom: 8px;
+}
+
+/* line 6, src/assets/scss/components/_form.scss */
+form .form-group label {
+ width: 180px;
+ height: calc(1.5em + 0.75rem + 2px);
+ line-height: 38px;
+}
+
+/* line 12, src/assets/scss/components/_form.scss */
+form .form-group input,
+form .form-group textarea,
+form .form-group select {
+ border-radius: 0;
+}
+
+/* line 19, src/assets/scss/components/_form.scss */
+form button {
+ margin-top: 30px;
+}
+
+/* line 1, src/assets/scss/components/_statusbar.scss */
+.statusbar {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-orient: horizontal;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ list-style-type: none;
+ margin: 0 0 50px;
+ padding: 0;
+}
+
+/* line 8, src/assets/scss/components/_statusbar.scss */
+.statusbar li {
+ position: relative;
+ width: 33.33%;
+ padding: 5px 10px;
+ background: #eaebea;
+ color: #989998;
+ text-align: center;
+ text-transform: uppercase;
+ font-size: 18px;
+}
+
+/* line 18, src/assets/scss/components/_statusbar.scss */
+.statusbar li:not(:first-of-type)::before {
+ content: '';
+ border: 17px solid transparent;
+ width: 0;
+ height: 0;
+ border-top-color: #eaebea;
+ border-bottom-color: #eaebea;
+ border-right-width: 0;
+ line-height: 0;
+ position: absolute;
+ left: -17px;
+ top: 0;
+}
+
+/* line 32, src/assets/scss/components/_statusbar.scss */
+.statusbar li:not(:last-of-type) {
+ margin-right: 23px;
+}
+
+/* line 35, src/assets/scss/components/_statusbar.scss */
+.statusbar li:not(:last-of-type)::after {
+ content: '';
+ border: 17px solid transparent;
+ width: 0;
+ height: 0;
+ border-left-color: #eaebea;
+ border-right-width: 0;
+ line-height: 0;
+ position: absolute;
+ right: -17px;
+ top: 0;
+}
+
+/* line 49, src/assets/scss/components/_statusbar.scss */
+.statusbar li.active {
+ background: #e54a19;
+ color: #ffffff;
+}
+
+/* line 53, src/assets/scss/components/_statusbar.scss */
+.statusbar li.active::before {
+ border-top-color: #e54a19;
+ border-bottom-color: #e54a19;
+}
+
+/* line 58, src/assets/scss/components/_statusbar.scss */
+.statusbar li.active::after {
+ border-left-color: #e54a19;
+}
+
+/* line 5, src/assets/scss/pages/_home.scss */
+.container {
+ max-width: 800px;
+}
+
+/* line 9, src/assets/scss/pages/_home.scss */
+h2 {
+ font-size: 40px;
+ line-height: 1.2;
+}
+
+/* line 13, src/assets/scss/pages/_home.scss */
+h3 {
+ font-size: 26px;
+}
+
+/* line 16, src/assets/scss/pages/_home.scss */
+h2,
+h3 {
+ color: #464746;
+}
+
+/* line 21, src/assets/scss/pages/_home.scss */
+a {
+ color: #6fa7fd;
+}
+
+/* line 25, src/assets/scss/pages/_home.scss */
+p {
+ margin-bottom: 23px;
+}
+
+/* line 29, src/assets/scss/pages/_home.scss */
+.accounts {
+ list-style-type: none;
+ margin: 0 0 60px;
+ padding: 0;
+}
+
+/* line 34, src/assets/scss/pages/_home.scss */
+.accounts li {
+ margin-bottom: 30px;
+ color: #e54a19;
+ font-size: 21px;
+}
+
+/* line 39, src/assets/scss/pages/_home.scss */
+.accounts li .status {
+ background: #eaebea;
+ color: #111111;
+ padding: 3px 5px;
+ margin-left: 10px;
+ font-size: 16px;
+}
+
+/* line 46, src/assets/scss/pages/_home.scss */
+.accounts li .status.online {
+ background: #e54a19;
+ color: #ffffff;
+}
+
+/* line 51, src/assets/scss/pages/_home.scss */
+.accounts li label {
+ position: relative;
+ background: #eaebea;
+ color: #eaebea;
+ border-color: #464746;
+ font-size: 16px;
+ padding: 4px 5px;
+ margin: 0;
+ line-height: 1;
+ cursor: pointer;
+}
+
+/* line 62, src/assets/scss/pages/_home.scss */
+.accounts li label::before, .accounts li label::after {
+ content: '';
+ position: absolute;
+ border: 2px solid transparent;
+ border-top-color: inherit;
+ border-left-color: inherit;
+ height: 10px;
+ width: 10px;
+ top: 50%;
+ margin-top: -5px;
+}
+
+/* line 74, src/assets/scss/pages/_home.scss */
+.accounts li label::before {
+ -webkit-transform: rotate(-45deg);
+ transform: rotate(-45deg);
+ left: 8px;
+}
+
+/* line 78, src/assets/scss/pages/_home.scss */
+.accounts li label::after {
+ -webkit-transform: rotate(135deg);
+ transform: rotate(135deg);
+ right: 8px;
+}
+
+/* line 83, src/assets/scss/pages/_home.scss */
+.accounts li .trigger {
+ display: none;
+}
+
+/* line 86, src/assets/scss/pages/_home.scss */
+.accounts li .trigger:checked + label {
+ background: #464746;
+ color: #464746;
+ border-color: #ffffff;
+}
+
+/* line 91, src/assets/scss/pages/_home.scss */
+.accounts li .trigger:checked + label + .things {
+ display: block;
+}
+
+/* line 96, src/assets/scss/pages/_home.scss */
+.accounts li .things {
+ display: none;
+}
+
+/* line 102, src/assets/scss/pages/_home.scss */
+.things {
+ margin-top: 25px;
+}
+
+/* line 105, src/assets/scss/pages/_home.scss */
+.things .legend {
+ display: block;
+ color: #111111;
+ margin: 5px 0 10px;
+ font-size: 16px;
+}
+
+/* line 111, src/assets/scss/pages/_home.scss */
+.things .code-container {
+ position: relative;
+}
+
+/* line 114, src/assets/scss/pages/_home.scss */
+.things .code-container textarea {
+ background: #eaebea;
+ color: #464746;
+ border: none;
+ padding: 25px 20px;
+ width: 100%;
+ height: 200px;
+ font-family: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
+ font-size: .8rem;
+ white-space: pre;
+}
+
+/* line 126, src/assets/scss/pages/_home.scss */
+.things .code-container .copy {
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ border: 0;
+ color: #989998;
+}
+
+/* line 133, src/assets/scss/pages/_home.scss */
+.things .code-container .copy:hover, .things .code-container .copy:focus, .things .code-container .copy:active {
+ color: #fff;
+}
+
+/* line 143, src/assets/scss/pages/_home.scss */
+.accounts + .alert-danger::after {
+ content: '';
+ position: absolute;
+ left: 65px;
+ bottom: -8px;
+ height: 16px;
+ width: 16px;
+ background: white;
+ border: inherit;
+ border-top-color: transparent;
+ border-left-color: transparent;
+ -webkit-transform: rotate(45deg);
+ transform: rotate(45deg);
+}
+
+/* line 158, src/assets/scss/pages/_home.scss */
+.controls {
+ margin-top: 25px;
+}
+
+/*# sourceMappingURL=../../../scss */
\ No newline at end of file
--- /dev/null
+/* line 5, src/assets/scss/rtl.scss */
+.jumbotron {
+ direction: ltr;
+ text-align: left;
+ margin: 0 2em 0 1em;
+ padding-right: 1em;
+ margin: 0 !important;
+}
+
+/*# sourceMappingURL=../../../scss */
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 169.18 36.997" height="139.83" width="639.422"><path d="M70.658 31.467h-2.855c.01.426.142.751.394.978.253.226.579.34.98.34.437 0 .82-.068 1.147-.203l.13.529c-.387.173-.846.26-1.374.26-.613 0-1.1-.182-1.465-.545-.363-.363-.544-.847-.544-1.448 0-.603.174-1.107.523-1.513.349-.407.816-.61 1.404-.61.543 0 .959.18 1.249.542.29.362.435.806.435 1.329 0 .149-.008.263-.024.34zm-2.847-.545h2.14c0-.339-.087-.612-.263-.82-.174-.207-.424-.31-.746-.31-.307 0-.56.103-.761.31-.201.208-.324.481-.37.82zm3.782 2.36v-2.856c0-.436-.01-.797-.032-1.082h.658l.058.643c.292-.488.72-.732 1.285-.732.26 0 .492.07.694.211.202.141.355.336.46.586.323-.531.773-.797 1.351-.797.39 0 .712.148.964.443.252.296.378.728.378 1.298v2.285h-.74v-2.195c0-.392-.075-.691-.225-.9-.15-.209-.366-.313-.645-.313a.89.89 0 00-.644.278.957.957 0 00-.284.714v2.416h-.74v-2.326c0-.334-.075-.598-.224-.792a.73.73 0 00-.614-.29c-.252 0-.474.106-.668.317a1.06 1.06 0 00-.292.74v2.351zm7.004 1.61v-4.254c0-.529-.01-.96-.033-1.294h.676l.04.684h.016c.328-.516.817-.773 1.464-.773.494 0 .905.185 1.232.558.329.373.493.852.493 1.435 0 .648-.176 1.163-.53 1.546a1.73 1.73 0 01-1.325.577 1.66 1.66 0 01-.737-.163 1.23 1.23 0 01-.523-.464h-.017v2.147h-.756zm.756-3.872v.634c0 .318.112.584.332.799.222.215.487.324.8.324.379 0 .68-.136.902-.406.222-.269.333-.633.333-1.09 0-.413-.11-.753-.33-1.022a1.074 1.074 0 00-.873-.402 1.11 1.11 0 00-.643.2 1.067 1.067 0 00-.407.492 1.37 1.37 0 00-.114.471zm7.769.252c0 .423-.089.793-.265 1.11a1.811 1.811 0 01-.74.732c-.317.171-.66.257-1.029.257-.575 0-1.049-.189-1.424-.564-.374-.376-.56-.863-.56-1.462 0-.637.193-1.145.58-1.523.386-.38.873-.567 1.461-.567.586 0 1.062.187 1.428.562.366.376.549.861.549 1.455zm-3.246.049c0 .43.115.786.346 1.067.23.28.52.421.866.421.355 0 .652-.14.89-.423.237-.281.355-.645.355-1.09 0-.41-.109-.759-.327-1.047-.218-.29-.516-.433-.894-.433-.382 0-.683.142-.904.428-.22.286-.332.645-.332 1.077zm3.644-1.976h.773c.515 1.908.792 2.987.83 3.237h.016c.03-.212.366-1.291 1.008-3.237h.643c.583 1.824.909 2.903.976 3.237h.017c.075-.407.17-.806.284-1.196l.594-2.041h.748l-1.301 3.936h-.7c-.447-1.377-.699-2.16-.756-2.35a10.208 10.208 0 01-.195-.797h-.017c-.09.409-.207.826-.353 1.252l-.656 1.895h-.699l-1.212-3.936zm9.843 2.122h-2.856c.011.426.143.751.395.978.252.226.579.34.98.34.437 0 .82-.068 1.147-.203l.13.529c-.388.173-.846.26-1.374.26-.613 0-1.101-.181-1.465-.545-.363-.363-.544-.846-.544-1.448 0-.603.174-1.107.523-1.513.348-.407.816-.61 1.404-.61.543 0 .959.18 1.249.542.29.362.435.806.435 1.329 0 .149-.008.263-.024.34zm-2.847-.545h2.14c0-.339-.088-.612-.263-.82-.174-.207-.424-.31-.747-.31-.306 0-.56.103-.76.31-.201.208-.324.481-.37.82zm3.782 2.36v-2.685c0-.51-.011-.927-.032-1.252h.667l.048.789c.1-.277.254-.493.46-.648a1.097 1.097 0 01.874-.215v.716a1.235 1.235 0 00-.26-.025.924.924 0 00-.705.322c-.197.214-.295.519-.295.915v2.082h-.757zm3.522 0h-.756v-3.937h.756zm.106-5.134c0 .136-.045.25-.134.342a.47.47 0 01-.354.139.46.46 0 01-.346-.139.472.472 0 01-.134-.342c0-.132.046-.245.137-.336a.473.473 0 01.351-.136.465.465 0 01.48.472zm1.107 5.133v-2.855c0-.436-.012-.797-.033-1.081h.667l.065.667c.124-.229.308-.412.549-.55a1.57 1.57 0 01.793-.207c.412 0 .755.145 1.03.436.273.292.41.719.41 1.28v2.31h-.757v-2.229c0-.371-.08-.66-.24-.865-.16-.204-.392-.306-.695-.306-.271 0-.511.102-.72.305a.997.997 0 00-.313.744v2.351zm8.167-2.871v2.285c0 .858-.187 1.454-.558 1.79-.371.336-.877.504-1.517.504-.57 0-1.028-.114-1.375-.34l.188-.579c.35.217.75.325 1.203.325.42 0 .744-.116.97-.35.227-.232.34-.574.34-1.023l-.016-.432c-.286.45-.719.675-1.302.675-.496 0-.908-.184-1.238-.552-.33-.367-.494-.823-.494-1.368 0-.631.181-1.139.544-1.52.361-.38.792-.57 1.294-.57.596 0 1.027.227 1.294.683l.032-.593h.667c-.022.235-.032.59-.032 1.065zm-.757 1.155v-.643c0-.293-.099-.546-.297-.76-.198-.214-.457-.322-.777-.322a1.08 1.08 0 00-.866.403c-.225.269-.338.622-.338 1.061 0 .404.109.735.324.995.215.258.506.388.872.388.29 0 .543-.104.759-.312.215-.209.323-.48.323-.81zm3.904-2.953l.74-.22v.953h1.05v.577h-1.05v2.05c0 .273.045.47.134.59.088.121.227.182.42.182.176 0 .32-.016.43-.048l.033.569c-.187.07-.412.106-.675.106-.72 0-1.082-.45-1.082-1.35v-2.1h-.618v-.576h.618zm2.628 4.67V27.49h.756v2.48h.017c.132-.222.318-.397.555-.524.237-.127.48-.191.73-.191.406 0 .745.145 1.016.436.272.292.408.719.408 1.28v2.31h-.757v-2.229c0-.371-.08-.66-.24-.865-.16-.204-.392-.306-.696-.306-.257 0-.493.096-.709.29a.964.964 0 00-.324.751v2.36zm7.955-1.815h-2.855c.01.426.143.751.395.978.252.226.578.34.98.34.436 0 .819-.068 1.146-.203l.13.529c-.387.173-.845.26-1.374.26-.613 0-1.1-.181-1.464-.545-.364-.363-.545-.846-.545-1.448 0-.603.174-1.107.523-1.513.349-.407.816-.61 1.405-.61.542 0 .958.18 1.248.542.29.362.435.806.435 1.329 0 .149-.008.263-.024.34zm-2.847-.545h2.14c0-.339-.087-.612-.262-.82-.175-.207-.424-.31-.747-.31-.307 0-.56.103-.76.31-.202.208-.324.481-.371.82zm5.28 2.172l.178-.578c.32.196.65.293.992.293.244 0 .433-.051.566-.154a.498.498 0 00.199-.415.535.535 0 00-.164-.405c-.11-.105-.302-.202-.576-.295-.722-.246-1.082-.618-1.082-1.114 0-.33.128-.609.385-.834.256-.225.596-.337 1.022-.337.397 0 .732.081 1.009.244l-.187.545a1.68 1.68 0 00-.846-.236c-.201 0-.36.049-.476.147a.48.48 0 00-.175.381c0 .14.05.255.149.35.099.095.304.198.615.31.38.135.653.295.818.48.165.186.248.42.248.7 0 .357-.135.646-.408.865-.272.22-.644.33-1.113.33-.43 0-.816-.092-1.155-.277zm3.618.187v-2.855c0-.436-.01-.797-.032-1.081h.659l.057.642c.292-.488.721-.732 1.285-.732.26 0 .493.07.694.211.202.141.356.336.461.586.323-.531.772-.797 1.35-.797.39 0 .713.148.964.443.253.296.378.728.378 1.298v2.285h-.739v-2.195c0-.392-.075-.691-.225-.9-.15-.209-.367-.313-.646-.313a.888.888 0 00-.643.278.957.957 0 00-.284.714v2.416h-.74v-2.326c0-.334-.075-.598-.224-.792a.73.73 0 00-.614-.29c-.253 0-.475.106-.668.317a1.058 1.058 0 00-.292.74v2.351zm9.883-2.383v1.44c0 .39.023.705.066.943h-.692l-.089-.504c-.296.396-.71.594-1.244.594-.375 0-.674-.111-.9-.332a1.078 1.078 0 01-.337-.799c0-.477.207-.842.622-1.094.416-.252 1.022-.378 1.818-.378v-.074c0-.279-.076-.497-.229-.653-.153-.155-.38-.233-.682-.233-.4 0-.759.1-1.073.301l-.171-.496c.38-.239.832-.358 1.359-.358.511 0 .9.14 1.161.423.261.281.391.688.391 1.22zm-.74 1.04v-.65c-.634 0-1.074.077-1.322.228-.246.152-.37.36-.37.627 0 .209.063.37.186.488a.676.676 0 00.49.178c.287 0 .528-.09.724-.266.195-.179.293-.379.293-.604zm1.92 1.343v-2.684c0-.51-.01-.927-.032-1.252h.667l.048.789c.1-.277.254-.493.46-.648a1.098 1.098 0 01.875-.215v.716a1.247 1.247 0 00-.26-.025.925.925 0 00-.706.322c-.196.214-.295.519-.295.915v2.082h-.757zm3.116-4.669l.74-.22v.953h1.05v.577h-1.05v2.05c0 .273.045.47.133.59.088.121.228.182.42.182.176 0 .32-.016.43-.048l.034.569c-.188.07-.412.106-.675.106-.722 0-1.082-.45-1.082-1.35v-2.1h-.619v-.576h.619zm4.4 4.67V27.49h.758v2.48h.015a1.41 1.41 0 01.556-.524 1.53 1.53 0 01.73-.191c.406 0 .745.145 1.016.436.272.292.407.719.407 1.28v2.31h-.756v-2.229c0-.371-.08-.66-.24-.865-.16-.204-.392-.306-.696-.306a1.04 1.04 0 00-.71.29.966.966 0 00-.322.751v2.36zm8.387-2.01c0 .423-.088.793-.265 1.11a1.811 1.811 0 01-.74.732c-.318.171-.66.257-1.028.257-.576 0-1.05-.189-1.425-.564-.374-.376-.56-.863-.56-1.462 0-.637.193-1.145.58-1.523.386-.379.873-.567 1.461-.567.586 0 1.062.187 1.427.562.367.376.55.861.55 1.455zm-3.246.049c0 .43.115.786.346 1.067.23.28.52.421.867.421.355 0 .651-.14.889-.423.237-.281.355-.645.355-1.09 0-.41-.11-.759-.327-1.047-.218-.29-.516-.433-.893-.433-.383 0-.683.142-.905.428-.22.286-.332.645-.332 1.077zm4.165 1.96v-2.855c0-.436-.011-.797-.032-1.081h.658l.058.642c.292-.488.72-.732 1.284-.732.261 0 .493.07.694.211.203.141.356.336.462.586.322-.531.772-.797 1.35-.797.39 0 .712.148.964.443.252.296.378.728.378 1.298v2.285h-.74v-2.195c0-.392-.075-.691-.225-.9-.151-.209-.366-.313-.646-.313a.891.891 0 00-.644.278.958.958 0 00-.282.714v2.416h-.741v-2.326c0-.334-.075-.598-.224-.792a.731.731 0 00-.614-.29c-.252 0-.474.106-.668.317a1.06 1.06 0 00-.292.74v2.351zm10.29-1.814H166.3c.01.426.142.751.394.978.253.226.58.34.98.34.436 0 .82-.068 1.147-.203l.13.529c-.387.173-.845.26-1.374.26-.613 0-1.1-.181-1.464-.545-.363-.363-.545-.846-.545-1.448 0-.603.175-1.107.523-1.513.349-.407.817-.61 1.404-.61.543 0 .959.18 1.249.542.29.362.435.806.435 1.329 0 .149-.008.263-.025.34zm-2.847-.545h2.139c0-.339-.087-.612-.262-.82-.175-.207-.424-.31-.747-.31-.306 0-.56.103-.76.31-.2.208-.324.481-.37.82z" clip-rule="evenodd" fill="#989898" fill-rule="evenodd"/><g clip-rule="evenodd" fill-rule="evenodd"><path d="M1.914 26.411L16.39 11.925l2.108-2.108 2.108 2.108L31.292 22.61l-.016.054-.21.621-.24.606-.269.592-.296.577-.269.467-11.494-11.494L3.452 29.09c-.579-.853-1.117-1.732-1.538-2.679z" fill="#e64a19"/><path d="M18.498 0c10.19 0 18.498 8.31 18.498 18.498 0 10.189-8.309 18.499-18.498 18.499-5.565 0-10.569-2.482-13.964-6.393l.653-.656.475-.475.475-.475.476-.476.02-.02c2.852 3.379 7.114 5.531 11.865 5.531 8.557 0 15.535-6.978 15.535-15.535S27.055 2.963 18.498 2.963 2.963 9.941 2.963 18.498c0 1.153.128 2.276.368 3.358l-1.03 1.033-1.376 1.377A18.378 18.378 0 010 18.498C0 8.31 8.31 0 18.498 0z" fill="#474747"/></g><path d="M156.694 16.156v6.789h4.443l.264-.002.255-.005.244-.008.234-.012.225-.015.215-.018.206-.021.195-.025.186-.027.175-.03.166-.034.155-.036.145-.038.133-.04.125-.044.11-.043.223-.104.21-.116.197-.127.187-.14.173-.152.163-.166.152-.179.142-.193.13-.204.11-.205.093-.21.076-.213.06-.217.042-.223.024-.228.01-.233-.013-.288-.035-.276-.06-.265-.082-.255-.106-.248-.13-.24-.156-.232-.18-.225-.2-.211-.22-.195-.24-.178-.258-.161-.28-.145-.3-.129-.322-.111-.346-.094-.117-.026-.134-.025-.146-.024-.16-.022-.174-.021-.185-.019-.198-.017-.21-.016-.225-.013-.235-.012-.247-.01-.26-.008-.272-.006-.283-.005-.295-.003h-3.399zm0-8.45v5.518h3.103l.252-.001.241-.004.233-.008.223-.01.214-.014.205-.016.197-.019.185-.022.178-.024.168-.028.158-.03.147-.032.14-.035.128-.036.118-.04.107-.04.213-.093.198-.104.185-.114.173-.126.161-.138.152-.149.14-.162.13-.175.117-.183.101-.186.084-.192.07-.196.054-.2.04-.209.023-.214.008-.22-.014-.295-.038-.271-.064-.25-.086-.23-.111-.215-.135-.197-.16-.186-.19-.175-.096-.073-.11-.073-.12-.069-.128-.065-.14-.06-.148-.058-.157-.051-.168-.047-.18-.042-.187-.037-.198-.031-.209-.026-.217-.02-.227-.015-.238-.009-.249-.003h-4.276zm5.377-2.74l.26.026.253.03.245.035.236.04.229.044.22.05.214.054.204.06.198.065.19.071.185.077.34.164.325.188.307.21.29.229.269.252.25.273.231.292.209.309.1.164.09.164.086.166.08.169.072.17.067.173.06.175.054.177.047.18.041.18.035.183.028.185.022.187.016.187.009.19.004.196-.012.357-.033.354-.057.345-.079.34-.102.33-.124.323-.146.314-.168.307-.193.296-.213.28-.234.266-.255.25-.275.235-.143.107.266.117.332.163.31.17.289.177.266.184.246.193.224.2.21.21.197.22.185.231.172.24.16.25.148.261.137.272.12.28.105.285.088.292.073.297.055.303.04.308.024.313.008.319-.008.321-.021.316-.038.31-.053.307-.067.3-.083.293-.097.288-.113.282-.128.275-.143.269-.157.261-.173.256-.186.248-.2.241-.216.234-.23.229-.244.219-.255.203-.265.19-.275.177-.284.161-.294.147-.304.134-.312.119-.323.104-.33.09-.34.076-.348.063-.357.047-.365.034-.374.02-.379.008h-8.378V4.91h6.833l.298.002.293.007.285.01.276.017.268.02zm-47.325-.053h2.926v8.422h9.308V4.913h2.917v20.986h-2.917v-9.678h-9.308v9.678h-2.926zm23.606 13.236h7.125l-3.551-7.538zm8.435 2.86h-9.753l-2.316 4.883h-3.24l9.917-20.982h1.07l9.776 20.982h-3.143l-.837-1.755z" clip-rule="evenodd" fill="#474747" fill-rule="evenodd"/><path d="M50.546 9.667l.437.01.43.03.42.05.413.07.404.091.396.112.386.132.377.152.367.172.358.193.348.213.339.232.327.252.318.273.308.29.299.313.257.299.24.304.224.312.208.32.191.327.175.333.158.34.142.349.125.354.108.36.091.367.075.373.058.38.041.385.025.39.008.398-.009.398-.026.393-.044.387-.061.382-.08.375-.097.37-.115.364-.132.357-.15.351-.168.344-.184.337-.203.331-.22.324-.236.316-.253.31-.272.303-.286.292-.298.274-.309.256-.319.238-.33.219-.339.2-.35.18-.36.163-.37.143-.378.124-.388.104-.397.086-.407.065-.415.047-.425.03-.432.009-.436-.01-.426-.028-.417-.048-.408-.065-.399-.086-.39-.104-.379-.124-.37-.143-.36-.162-.35-.181-.34-.2-.328-.22-.32-.237-.307-.256-.298-.275-.285-.291-.27-.304-.255-.309-.236-.316-.22-.324-.202-.33-.184-.338-.167-.345-.15-.35-.132-.357-.114-.364-.097-.37-.079-.376-.06-.382-.045-.387-.026-.392-.008-.398.008-.396.025-.389.04-.384.06-.378.073-.372.092-.366.108-.36.125-.352.141-.347.159-.34.174-.334.192-.325.208-.32.224-.311.24-.305.257-.298.298-.313.307-.293.318-.274.328-.253.338-.235.349-.213.358-.194.368-.173.377-.154.387-.132.395-.112.405-.091.413-.072.422-.05.43-.03.439-.01zm0 2.736l-.269.006-.262.019-.256.03-.251.044-.246.056-.24.068-.237.08-.23.093-.228.105-.221.117-.219.13-.213.144-.21.156-.206.17-.203.182-.197.196-.19.204-.177.21-.164.213-.151.218-.14.224-.126.228-.115.232-.102.239-.09.243-.08.248-.065.254-.055.26-.043.265-.03.271-.018.278-.006.286.002.182.009.182.013.18.02.178.025.177.03.175.036.173.042.173.046.17.053.17.058.167.063.166.07.165.074.164.08.162.085.159.186.312.2.29.214.27.23.252.245.233.26.215.276.197.297.181.147.08.152.078.154.07.157.067.157.061.16.055.16.05.164.046.164.04.168.034.168.03.17.023.173.019.176.013.177.008.179.003.18-.003.18-.008.178-.013.174-.019.172-.024.17-.03.17-.034.166-.04.163-.044.162-.05.16-.057.158-.06.156-.066.156-.071.153-.077.146-.08.291-.18.274-.198.258-.215.243-.233.228-.252.214-.27.2-.291.186-.312.083-.157.08-.163.074-.163.069-.165.062-.167.058-.168.053-.17.046-.17.041-.172.036-.174.03-.176.026-.176.019-.179.014-.18.008-.182.003-.181-.006-.285-.019-.278-.03-.272-.043-.265-.055-.26-.067-.254-.079-.248-.09-.243-.104-.238-.114-.233-.128-.228-.139-.223-.152-.22-.164-.213-.177-.209L54.23 14l-.198-.196-.203-.182-.206-.17-.21-.156-.214-.143-.218-.13-.222-.118-.226-.105-.231-.093-.236-.08-.24-.068-.245-.056-.251-.043-.256-.031-.261-.019-.267-.006zm43.86 9.28l-.212.371-.221.353-.23.334-.241.315-.25.298-.26.278-.27.259-.276.24-.284.227-.294.21-.302.196-.311.181-.32.167-.327.15-.334.136-.174.063-.174.059-.177.054-.18.05-.181.047-.184.042-.187.038-.19.035-.192.03-.194.026-.197.022-.199.019-.202.013-.203.01-.206.007-.211.002-.458-.01-.448-.03-.437-.047-.426-.067-.414-.087-.403-.106-.39-.127-.379-.145-.366-.166-.354-.186-.341-.206-.328-.224-.316-.245-.301-.264-.288-.283-.273-.3-.257-.31-.239-.314-.223-.32-.206-.325-.191-.331-.174-.337-.158-.342-.141-.346-.125-.353-.108-.357-.092-.363-.074-.367-.058-.372-.042-.376-.024-.381-.008-.384.006-.364.022-.359.034-.354.05-.35.063-.345.077-.34.092-.336.106-.33.12-.326.134-.32.148-.316.163-.309.175-.305.19-.3.203-.293.218-.29.288-.351.304-.332.317-.31.33-.288.342-.265.356-.242.368-.22.381-.196.393-.174.405-.15.416-.127.429-.103.438-.08.45-.057.46-.034.47-.011.481.011.47.035.46.058.45.083.437.105.426.13.416.153.403.178.39.2.379.225.365.248.353.271.34.294.327.317.314.339.3.361.203.27.191.278.179.286.165.295.15.302.14.31.125.32.113.326.098.333.086.341.073.348.06.357.047.362.034.37.02.378.019.837H82.036l.01.132.033.282.045.276.055.267.067.26.077.252.088.245.099.238.11.23.12.223.132.218.143.21.153.204.165.198.177.192.184.183.189.17.193.158.198.146.203.133.208.123.213.11.218.099.223.087.228.075.234.064.24.053.246.04.252.03.258.018.264.006.26-.006.258-.017.255-.029.253-.039.25-.051.25-.062.247-.074.244-.085.24-.096.233-.103.221-.11.212-.116.202-.123.192-.13.182-.135.172-.141.167-.155.174-.18.181-.206.188-.233.193-.26.2-.287.203-.312.435-.702 2.337 1.25-.404.776zm-2.914-7.046l-.147-.23-.155-.208-.169-.196-.183-.189-.196-.18-.21-.17-.225-.161-.239-.152-.253-.143-.271-.134-.137-.061-.138-.059-.14-.054-.14-.05-.141-.046-.142-.042-.142-.038-.144-.034-.144-.03-.145-.026-.145-.022-.148-.018-.147-.014-.148-.01-.15-.006-.148-.002-.25.005-.24.015-.238.025-.231.035-.227.044-.222.056-.217.064-.213.074-.21.084-.202.094-.2.104-.196.114-.192.124-.188.134-.184.145-.18.155-.123.116-.12.124-.118.132-.113.14-.11.147-.107.155-.104.163-.1.171-.097.18-.092.187-.09.195-.085.204-.082.212-.041.115h9.663l-.038-.122-.113-.31-.122-.288-.131-.269-.14-.25zm-22.555-4.97l.438.01.429.03.42.05.413.07.404.091.396.112.386.132.377.152.367.172.358.193.349.213.338.232.328.252.317.273.308.29.299.313.257.299.24.304.225.312.207.32.191.327.175.333.159.34.141.349.125.354.108.36.091.367.075.373.058.38.042.385.024.39.009.398-.01.398-.026.393-.044.387-.061.382-.08.375-.097.37-.114.364-.133.357-.15.351-.168.344-.184.337-.203.331-.22.324-.236.316-.253.31-.271.303-.287.292-.298.274-.309.256-.318.238-.33.219-.34.2-.35.18-.359.163-.37.143-.379.124-.388.104-.397.086-.407.065-.415.047-.424.03-.433.009-.436-.01-.426-.028-.417-.048-.408-.065-.399-.086-.39-.104-.379-.124-.37-.143-.36-.162-.35-.181-.34-.2-.328-.22-.32-.237-.307-.256-.007-.007v7.453h-2.82V17.718h.001l.006-.27.025-.39.041-.384.058-.378.075-.372.09-.366.11-.36.124-.352.141-.347.159-.34.175-.334.191-.325.208-.32.224-.311.24-.305.257-.298.298-.313.308-.293.317-.274.328-.253.338-.235.349-.213.358-.194.368-.173.377-.154.387-.132.395-.112.405-.091.413-.072.422-.05.43-.03.44-.01zM63.7 18.055v-.304l.003-.165.018-.278.03-.27.043-.266.055-.26.066-.254.078-.248.091-.243.102-.239.115-.232.127-.228.14-.224.15-.218.164-.214.177-.209.19-.204.198-.196.202-.182.206-.17.21-.156.214-.143.218-.13.221-.118.228-.105.23-.093.237-.08.24-.068.246-.056.251-.043.257-.031.262-.019.268-.006.267.006.261.019.256.03.25.044.246.056.24.068.236.08.231.093.226.105.222.117.218.131.215.143.21.156.206.17.202.182.198.196.19.204.176.21.165.213.151.219.14.223.127.228.115.233.103.238.09.243.08.248.066.255.055.26.043.264.03.272.019.278.006.285-.003.181-.008.182-.014.18-.02.179-.024.176-.03.176-.037.174-.04.172-.047.17-.053.17-.057.168-.063.167-.069.165-.074.163-.08.163-.083.157-.186.312-.2.29-.213.271-.229.252-.243.233-.258.215-.274.197-.29.181-.147.08-.153.077-.155.07-.156.067-.159.06-.16.056-.162.05-.163.046-.166.04-.17.033-.17.03-.172.024-.174.019-.178.013-.179.008-.18.003-.18-.003-.177-.008-.176-.013-.172-.019-.17-.024-.17-.03-.167-.034-.164-.04-.163-.044-.162-.05-.159-.056-.157-.061-.156-.066-.155-.071-.152-.077-.147-.08-.296-.182-.277-.197-.26-.215-.245-.233-.23-.252-.214-.27-.2-.29-.186-.312-.084-.159-.08-.162-.075-.164-.07-.165-.063-.166-.058-.167-.053-.17-.046-.17-.042-.173-.036-.173-.03-.175-.025-.177-.02-.179-.013-.18-.008-.18zm40.82-8.365c-1.223 0-2.852.294-3.9.899-1.06.612-1.828 1.45-2.283 2.487-.447 1.025-.665 2.558-.665 4.69v8.13h2.765v-7.567c0-1.7.072-2.84.214-3.394.214-.908.611-1.522 1.217-1.885.628-.376 1.371-.688 2.651-.688 1.28 0 2.009.314 2.637.69.606.363 1.003.978 1.217 1.886.142.552.213 1.694.213 3.393v7.567h2.766v-8.13c0-2.131-.218-3.665-.666-4.69-.455-1.037-1.222-1.874-2.282-2.487-1.048-.605-2.661-.901-3.884-.901z" clip-rule="evenodd" fill="#e64a19" fill-rule="evenodd"/></svg>
\ No newline at end of file
--- /dev/null
+/*!
+ * jQuery JavaScript Library v3.4.1
+ * https://jquery.com/
+ *
+ * Includes Sizzle.js
+ * https://sizzlejs.com/
+ *
+ * Copyright JS Foundation and other contributors
+ * Released under the MIT license
+ * https://jquery.org/license
+ *
+ * Date: 2019-05-01T21:04Z
+ */
+( function( global, factory ) {
+
+ "use strict";
+
+ if ( typeof module === "object" && typeof module.exports === "object" ) {
+
+ // For CommonJS and CommonJS-like environments where a proper `window`
+ // is present, execute the factory and get jQuery.
+ // For environments that do not have a `window` with a `document`
+ // (such as Node.js), expose a factory as module.exports.
+ // This accentuates the need for the creation of a real `window`.
+ // e.g. var jQuery = require("jquery")(window);
+ // See ticket #14549 for more info.
+ module.exports = global.document ?
+ factory( global, true ) :
+ function( w ) {
+ if ( !w.document ) {
+ throw new Error( "jQuery requires a window with a document" );
+ }
+ return factory( w );
+ };
+ } else {
+ factory( global );
+ }
+
+// Pass this if window is not defined yet
+} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
+
+// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1
+// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode
+// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common
+// enough that all such attempts are guarded in a try block.
+"use strict";
+
+var arr = [];
+
+var document = window.document;
+
+var getProto = Object.getPrototypeOf;
+
+var slice = arr.slice;
+
+var concat = arr.concat;
+
+var push = arr.push;
+
+var indexOf = arr.indexOf;
+
+var class2type = {};
+
+var toString = class2type.toString;
+
+var hasOwn = class2type.hasOwnProperty;
+
+var fnToString = hasOwn.toString;
+
+var ObjectFunctionString = fnToString.call( Object );
+
+var support = {};
+
+var isFunction = function isFunction( obj ) {
+
+ // Support: Chrome <=57, Firefox <=52
+ // In some browsers, typeof returns "function" for HTML <object> elements
+ // (i.e., `typeof document.createElement( "object" ) === "function"`).
+ // We don't want to classify *any* DOM node as a function.
+ return typeof obj === "function" && typeof obj.nodeType !== "number";
+ };
+
+
+var isWindow = function isWindow( obj ) {
+ return obj != null && obj === obj.window;
+ };
+
+
+
+
+ var preservedScriptAttributes = {
+ type: true,
+ src: true,
+ nonce: true,
+ noModule: true
+ };
+
+ function DOMEval( code, node, doc ) {
+ doc = doc || document;
+
+ var i, val,
+ script = doc.createElement( "script" );
+
+ script.text = code;
+ if ( node ) {
+ for ( i in preservedScriptAttributes ) {
+
+ // Support: Firefox 64+, Edge 18+
+ // Some browsers don't support the "nonce" property on scripts.
+ // On the other hand, just using `getAttribute` is not enough as
+ // the `nonce` attribute is reset to an empty string whenever it
+ // becomes browsing-context connected.
+ // See https://github.com/whatwg/html/issues/2369
+ // See https://html.spec.whatwg.org/#nonce-attributes
+ // The `node.getAttribute` check was added for the sake of
+ // `jQuery.globalEval` so that it can fake a nonce-containing node
+ // via an object.
+ val = node[ i ] || node.getAttribute && node.getAttribute( i );
+ if ( val ) {
+ script.setAttribute( i, val );
+ }
+ }
+ }
+ doc.head.appendChild( script ).parentNode.removeChild( script );
+ }
+
+
+function toType( obj ) {
+ if ( obj == null ) {
+ return obj + "";
+ }
+
+ // Support: Android <=2.3 only (functionish RegExp)
+ return typeof obj === "object" || typeof obj === "function" ?
+ class2type[ toString.call( obj ) ] || "object" :
+ typeof obj;
+}
+/* global Symbol */
+// Defining this global in .eslintrc.json would create a danger of using the global
+// unguarded in another place, it seems safer to define global only for this module
+
+
+
+var
+ version = "3.4.1",
+
+ // Define a local copy of jQuery
+ jQuery = function( selector, context ) {
+
+ // The jQuery object is actually just the init constructor 'enhanced'
+ // Need init if jQuery is called (just allow error to be thrown if not included)
+ return new jQuery.fn.init( selector, context );
+ },
+
+ // Support: Android <=4.0 only
+ // Make sure we trim BOM and NBSP
+ rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;
+
+jQuery.fn = jQuery.prototype = {
+
+ // The current version of jQuery being used
+ jquery: version,
+
+ constructor: jQuery,
+
+ // The default length of a jQuery object is 0
+ length: 0,
+
+ toArray: function() {
+ return slice.call( this );
+ },
+
+ // Get the Nth element in the matched element set OR
+ // Get the whole matched element set as a clean array
+ get: function( num ) {
+
+ // Return all the elements in a clean array
+ if ( num == null ) {
+ return slice.call( this );
+ }
+
+ // Return just the one element from the set
+ return num < 0 ? this[ num + this.length ] : this[ num ];
+ },
+
+ // Take an array of elements and push it onto the stack
+ // (returning the new matched element set)
+ pushStack: function( elems ) {
+
+ // Build a new jQuery matched element set
+ var ret = jQuery.merge( this.constructor(), elems );
+
+ // Add the old object onto the stack (as a reference)
+ ret.prevObject = this;
+
+ // Return the newly-formed element set
+ return ret;
+ },
+
+ // Execute a callback for every element in the matched set.
+ each: function( callback ) {
+ return jQuery.each( this, callback );
+ },
+
+ map: function( callback ) {
+ return this.pushStack( jQuery.map( this, function( elem, i ) {
+ return callback.call( elem, i, elem );
+ } ) );
+ },
+
+ slice: function() {
+ return this.pushStack( slice.apply( this, arguments ) );
+ },
+
+ first: function() {
+ return this.eq( 0 );
+ },
+
+ last: function() {
+ return this.eq( -1 );
+ },
+
+ eq: function( i ) {
+ var len = this.length,
+ j = +i + ( i < 0 ? len : 0 );
+ return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] );
+ },
+
+ end: function() {
+ return this.prevObject || this.constructor();
+ },
+
+ // For internal use only.
+ // Behaves like an Array's method, not like a jQuery method.
+ push: push,
+ sort: arr.sort,
+ splice: arr.splice
+};
+
+jQuery.extend = jQuery.fn.extend = function() {
+ var options, name, src, copy, copyIsArray, clone,
+ target = arguments[ 0 ] || {},
+ i = 1,
+ length = arguments.length,
+ deep = false;
+
+ // Handle a deep copy situation
+ if ( typeof target === "boolean" ) {
+ deep = target;
+
+ // Skip the boolean and the target
+ target = arguments[ i ] || {};
+ i++;
+ }
+
+ // Handle case when target is a string or something (possible in deep copy)
+ if ( typeof target !== "object" && !isFunction( target ) ) {
+ target = {};
+ }
+
+ // Extend jQuery itself if only one argument is passed
+ if ( i === length ) {
+ target = this;
+ i--;
+ }
+
+ for ( ; i < length; i++ ) {
+
+ // Only deal with non-null/undefined values
+ if ( ( options = arguments[ i ] ) != null ) {
+
+ // Extend the base object
+ for ( name in options ) {
+ copy = options[ name ];
+
+ // Prevent Object.prototype pollution
+ // Prevent never-ending loop
+ if ( name === "__proto__" || target === copy ) {
+ continue;
+ }
+
+ // Recurse if we're merging plain objects or arrays
+ if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
+ ( copyIsArray = Array.isArray( copy ) ) ) ) {
+ src = target[ name ];
+
+ // Ensure proper type for the source value
+ if ( copyIsArray && !Array.isArray( src ) ) {
+ clone = [];
+ } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) {
+ clone = {};
+ } else {
+ clone = src;
+ }
+ copyIsArray = false;
+
+ // Never move original objects, clone them
+ target[ name ] = jQuery.extend( deep, clone, copy );
+
+ // Don't bring in undefined values
+ } else if ( copy !== undefined ) {
+ target[ name ] = copy;
+ }
+ }
+ }
+ }
+
+ // Return the modified object
+ return target;
+};
+
+jQuery.extend( {
+
+ // Unique for each copy of jQuery on the page
+ expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ),
+
+ // Assume jQuery is ready without the ready module
+ isReady: true,
+
+ error: function( msg ) {
+ throw new Error( msg );
+ },
+
+ noop: function() {},
+
+ isPlainObject: function( obj ) {
+ var proto, Ctor;
+
+ // Detect obvious negatives
+ // Use toString instead of jQuery.type to catch host objects
+ if ( !obj || toString.call( obj ) !== "[object Object]" ) {
+ return false;
+ }
+
+ proto = getProto( obj );
+
+ // Objects with no prototype (e.g., `Object.create( null )`) are plain
+ if ( !proto ) {
+ return true;
+ }
+
+ // Objects with prototype are plain iff they were constructed by a global Object function
+ Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor;
+ return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString;
+ },
+
+ isEmptyObject: function( obj ) {
+ var name;
+
+ for ( name in obj ) {
+ return false;
+ }
+ return true;
+ },
+
+ // Evaluates a script in a global context
+ globalEval: function( code, options ) {
+ DOMEval( code, { nonce: options && options.nonce } );
+ },
+
+ each: function( obj, callback ) {
+ var length, i = 0;
+
+ if ( isArrayLike( obj ) ) {
+ length = obj.length;
+ for ( ; i < length; i++ ) {
+ if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
+ break;
+ }
+ }
+ } else {
+ for ( i in obj ) {
+ if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
+ break;
+ }
+ }
+ }
+
+ return obj;
+ },
+
+ // Support: Android <=4.0 only
+ trim: function( text ) {
+ return text == null ?
+ "" :
+ ( text + "" ).replace( rtrim, "" );
+ },
+
+ // results is for internal usage only
+ makeArray: function( arr, results ) {
+ var ret = results || [];
+
+ if ( arr != null ) {
+ if ( isArrayLike( Object( arr ) ) ) {
+ jQuery.merge( ret,
+ typeof arr === "string" ?
+ [ arr ] : arr
+ );
+ } else {
+ push.call( ret, arr );
+ }
+ }
+
+ return ret;
+ },
+
+ inArray: function( elem, arr, i ) {
+ return arr == null ? -1 : indexOf.call( arr, elem, i );
+ },
+
+ // Support: Android <=4.0 only, PhantomJS 1 only
+ // push.apply(_, arraylike) throws on ancient WebKit
+ merge: function( first, second ) {
+ var len = +second.length,
+ j = 0,
+ i = first.length;
+
+ for ( ; j < len; j++ ) {
+ first[ i++ ] = second[ j ];
+ }
+
+ first.length = i;
+
+ return first;
+ },
+
+ grep: function( elems, callback, invert ) {
+ var callbackInverse,
+ matches = [],
+ i = 0,
+ length = elems.length,
+ callbackExpect = !invert;
+
+ // Go through the array, only saving the items
+ // that pass the validator function
+ for ( ; i < length; i++ ) {
+ callbackInverse = !callback( elems[ i ], i );
+ if ( callbackInverse !== callbackExpect ) {
+ matches.push( elems[ i ] );
+ }
+ }
+
+ return matches;
+ },
+
+ // arg is for internal usage only
+ map: function( elems, callback, arg ) {
+ var length, value,
+ i = 0,
+ ret = [];
+
+ // Go through the array, translating each of the items to their new values
+ if ( isArrayLike( elems ) ) {
+ length = elems.length;
+ for ( ; i < length; i++ ) {
+ value = callback( elems[ i ], i, arg );
+
+ if ( value != null ) {
+ ret.push( value );
+ }
+ }
+
+ // Go through every key on the object,
+ } else {
+ for ( i in elems ) {
+ value = callback( elems[ i ], i, arg );
+
+ if ( value != null ) {
+ ret.push( value );
+ }
+ }
+ }
+
+ // Flatten any nested arrays
+ return concat.apply( [], ret );
+ },
+
+ // A global GUID counter for objects
+ guid: 1,
+
+ // jQuery.support is not used in Core but other projects attach their
+ // properties to it so it needs to exist.
+ support: support
+} );
+
+if ( typeof Symbol === "function" ) {
+ jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ];
+}
+
+// Populate the class2type map
+jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ),
+function( i, name ) {
+ class2type[ "[object " + name + "]" ] = name.toLowerCase();
+} );
+
+function isArrayLike( obj ) {
+
+ // Support: real iOS 8.2 only (not reproducible in simulator)
+ // `in` check used to prevent JIT error (gh-2145)
+ // hasOwn isn't used here due to false negatives
+ // regarding Nodelist length in IE
+ var length = !!obj && "length" in obj && obj.length,
+ type = toType( obj );
+
+ if ( isFunction( obj ) || isWindow( obj ) ) {
+ return false;
+ }
+
+ return type === "array" || length === 0 ||
+ typeof length === "number" && length > 0 && ( length - 1 ) in obj;
+}
+var Sizzle =
+/*!
+ * Sizzle CSS Selector Engine v2.3.4
+ * https://sizzlejs.com/
+ *
+ * Copyright JS Foundation and other contributors
+ * Released under the MIT license
+ * https://js.foundation/
+ *
+ * Date: 2019-04-08
+ */
+(function( window ) {
+
+var i,
+ support,
+ Expr,
+ getText,
+ isXML,
+ tokenize,
+ compile,
+ select,
+ outermostContext,
+ sortInput,
+ hasDuplicate,
+
+ // Local document vars
+ setDocument,
+ document,
+ docElem,
+ documentIsHTML,
+ rbuggyQSA,
+ rbuggyMatches,
+ matches,
+ contains,
+
+ // Instance-specific data
+ expando = "sizzle" + 1 * new Date(),
+ preferredDoc = window.document,
+ dirruns = 0,
+ done = 0,
+ classCache = createCache(),
+ tokenCache = createCache(),
+ compilerCache = createCache(),
+ nonnativeSelectorCache = createCache(),
+ sortOrder = function( a, b ) {
+ if ( a === b ) {
+ hasDuplicate = true;
+ }
+ return 0;
+ },
+
+ // Instance methods
+ hasOwn = ({}).hasOwnProperty,
+ arr = [],
+ pop = arr.pop,
+ push_native = arr.push,
+ push = arr.push,
+ slice = arr.slice,
+ // Use a stripped-down indexOf as it's faster than native
+ // https://jsperf.com/thor-indexof-vs-for/5
+ indexOf = function( list, elem ) {
+ var i = 0,
+ len = list.length;
+ for ( ; i < len; i++ ) {
+ if ( list[i] === elem ) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",
+
+ // Regular expressions
+
+ // http://www.w3.org/TR/css3-selectors/#whitespace
+ whitespace = "[\\x20\\t\\r\\n\\f]",
+
+ // http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
+ identifier = "(?:\\\\.|[\\w-]|[^\0-\\xa0])+",
+
+ // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors
+ attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace +
+ // Operator (capture 2)
+ "*([*^$|!~]?=)" + whitespace +
+ // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]"
+ "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace +
+ "*\\]",
+
+ pseudos = ":(" + identifier + ")(?:\\((" +
+ // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:
+ // 1. quoted (capture 3; capture 4 or capture 5)
+ "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" +
+ // 2. simple (capture 6)
+ "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" +
+ // 3. anything else (capture 2)
+ ".*" +
+ ")\\)|)",
+
+ // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
+ rwhitespace = new RegExp( whitespace + "+", "g" ),
+ rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ),
+
+ rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
+ rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ),
+ rdescend = new RegExp( whitespace + "|>" ),
+
+ rpseudo = new RegExp( pseudos ),
+ ridentifier = new RegExp( "^" + identifier + "$" ),
+
+ matchExpr = {
+ "ID": new RegExp( "^#(" + identifier + ")" ),
+ "CLASS": new RegExp( "^\\.(" + identifier + ")" ),
+ "TAG": new RegExp( "^(" + identifier + "|[*])" ),
+ "ATTR": new RegExp( "^" + attributes ),
+ "PSEUDO": new RegExp( "^" + pseudos ),
+ "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace +
+ "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace +
+ "*(\\d+)|))" + whitespace + "*\\)|)", "i" ),
+ "bool": new RegExp( "^(?:" + booleans + ")$", "i" ),
+ // For use in libraries implementing .is()
+ // We use this for POS matching in `select`
+ "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" +
+ whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" )
+ },
+
+ rhtml = /HTML$/i,
+ rinputs = /^(?:input|select|textarea|button)$/i,
+ rheader = /^h\d$/i,
+
+ rnative = /^[^{]+\{\s*\[native \w/,
+
+ // Easily-parseable/retrievable ID or TAG or CLASS selectors
+ rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,
+
+ rsibling = /[+~]/,
+
+ // CSS escapes
+ // http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
+ runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ),
+ funescape = function( _, escaped, escapedWhitespace ) {
+ var high = "0x" + escaped - 0x10000;
+ // NaN means non-codepoint
+ // Support: Firefox<24
+ // Workaround erroneous numeric interpretation of +"0x"
+ return high !== high || escapedWhitespace ?
+ escaped :
+ high < 0 ?
+ // BMP codepoint
+ String.fromCharCode( high + 0x10000 ) :
+ // Supplemental Plane codepoint (surrogate pair)
+ String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
+ },
+
+ // CSS string/identifier serialization
+ // https://drafts.csswg.org/cssom/#common-serializing-idioms
+ rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,
+ fcssescape = function( ch, asCodePoint ) {
+ if ( asCodePoint ) {
+
+ // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER
+ if ( ch === "\0" ) {
+ return "\uFFFD";
+ }
+
+ // Control characters and (dependent upon position) numbers get escaped as code points
+ return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " ";
+ }
+
+ // Other potentially-special ASCII characters get backslash-escaped
+ return "\\" + ch;
+ },
+
+ // Used for iframes
+ // See setDocument()
+ // Removing the function wrapper causes a "Permission Denied"
+ // error in IE
+ unloadHandler = function() {
+ setDocument();
+ },
+
+ inDisabledFieldset = addCombinator(
+ function( elem ) {
+ return elem.disabled === true && elem.nodeName.toLowerCase() === "fieldset";
+ },
+ { dir: "parentNode", next: "legend" }
+ );
+
+// Optimize for push.apply( _, NodeList )
+try {
+ push.apply(
+ (arr = slice.call( preferredDoc.childNodes )),
+ preferredDoc.childNodes
+ );
+ // Support: Android<4.0
+ // Detect silently failing push.apply
+ arr[ preferredDoc.childNodes.length ].nodeType;
+} catch ( e ) {
+ push = { apply: arr.length ?
+
+ // Leverage slice if possible
+ function( target, els ) {
+ push_native.apply( target, slice.call(els) );
+ } :
+
+ // Support: IE<9
+ // Otherwise append directly
+ function( target, els ) {
+ var j = target.length,
+ i = 0;
+ // Can't trust NodeList.length
+ while ( (target[j++] = els[i++]) ) {}
+ target.length = j - 1;
+ }
+ };
+}
+
+function Sizzle( selector, context, results, seed ) {
+ var m, i, elem, nid, match, groups, newSelector,
+ newContext = context && context.ownerDocument,
+
+ // nodeType defaults to 9, since context defaults to document
+ nodeType = context ? context.nodeType : 9;
+
+ results = results || [];
+
+ // Return early from calls with invalid selector or context
+ if ( typeof selector !== "string" || !selector ||
+ nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) {
+
+ return results;
+ }
+
+ // Try to shortcut find operations (as opposed to filters) in HTML documents
+ if ( !seed ) {
+
+ if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {
+ setDocument( context );
+ }
+ context = context || document;
+
+ if ( documentIsHTML ) {
+
+ // If the selector is sufficiently simple, try using a "get*By*" DOM method
+ // (excepting DocumentFragment context, where the methods don't exist)
+ if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) {
+
+ // ID selector
+ if ( (m = match[1]) ) {
+
+ // Document context
+ if ( nodeType === 9 ) {
+ if ( (elem = context.getElementById( m )) ) {
+
+ // Support: IE, Opera, Webkit
+ // TODO: identify versions
+ // getElementById can match elements by name instead of ID
+ if ( elem.id === m ) {
+ results.push( elem );
+ return results;
+ }
+ } else {
+ return results;
+ }
+
+ // Element context
+ } else {
+
+ // Support: IE, Opera, Webkit
+ // TODO: identify versions
+ // getElementById can match elements by name instead of ID
+ if ( newContext && (elem = newContext.getElementById( m )) &&
+ contains( context, elem ) &&
+ elem.id === m ) {
+
+ results.push( elem );
+ return results;
+ }
+ }
+
+ // Type selector
+ } else if ( match[2] ) {
+ push.apply( results, context.getElementsByTagName( selector ) );
+ return results;
+
+ // Class selector
+ } else if ( (m = match[3]) && support.getElementsByClassName &&
+ context.getElementsByClassName ) {
+
+ push.apply( results, context.getElementsByClassName( m ) );
+ return results;
+ }
+ }
+
+ // Take advantage of querySelectorAll
+ if ( support.qsa &&
+ !nonnativeSelectorCache[ selector + " " ] &&
+ (!rbuggyQSA || !rbuggyQSA.test( selector )) &&
+
+ // Support: IE 8 only
+ // Exclude object elements
+ (nodeType !== 1 || context.nodeName.toLowerCase() !== "object") ) {
+
+ newSelector = selector;
+ newContext = context;
+
+ // qSA considers elements outside a scoping root when evaluating child or
+ // descendant combinators, which is not what we want.
+ // In such cases, we work around the behavior by prefixing every selector in the
+ // list with an ID selector referencing the scope context.
+ // Thanks to Andrew Dupont for this technique.
+ if ( nodeType === 1 && rdescend.test( selector ) ) {
+
+ // Capture the context ID, setting it first if necessary
+ if ( (nid = context.getAttribute( "id" )) ) {
+ nid = nid.replace( rcssescape, fcssescape );
+ } else {
+ context.setAttribute( "id", (nid = expando) );
+ }
+
+ // Prefix every selector in the list
+ groups = tokenize( selector );
+ i = groups.length;
+ while ( i-- ) {
+ groups[i] = "#" + nid + " " + toSelector( groups[i] );
+ }
+ newSelector = groups.join( "," );
+
+ // Expand context for sibling selectors
+ newContext = rsibling.test( selector ) && testContext( context.parentNode ) ||
+ context;
+ }
+
+ try {
+ push.apply( results,
+ newContext.querySelectorAll( newSelector )
+ );
+ return results;
+ } catch ( qsaError ) {
+ nonnativeSelectorCache( selector, true );
+ } finally {
+ if ( nid === expando ) {
+ context.removeAttribute( "id" );
+ }
+ }
+ }
+ }
+ }
+
+ // All others
+ return select( selector.replace( rtrim, "$1" ), context, results, seed );
+}
+
+/**
+ * Create key-value caches of limited size
+ * @returns {function(string, object)} Returns the Object data after storing it on itself with
+ * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)
+ * deleting the oldest entry
+ */
+function createCache() {
+ var keys = [];
+
+ function cache( key, value ) {
+ // Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
+ if ( keys.push( key + " " ) > Expr.cacheLength ) {
+ // Only keep the most recent entries
+ delete cache[ keys.shift() ];
+ }
+ return (cache[ key + " " ] = value);
+ }
+ return cache;
+}
+
+/**
+ * Mark a function for special use by Sizzle
+ * @param {Function} fn The function to mark
+ */
+function markFunction( fn ) {
+ fn[ expando ] = true;
+ return fn;
+}
+
+/**
+ * Support testing using an element
+ * @param {Function} fn Passed the created element and returns a boolean result
+ */
+function assert( fn ) {
+ var el = document.createElement("fieldset");
+
+ try {
+ return !!fn( el );
+ } catch (e) {
+ return false;
+ } finally {
+ // Remove from its parent by default
+ if ( el.parentNode ) {
+ el.parentNode.removeChild( el );
+ }
+ // release memory in IE
+ el = null;
+ }
+}
+
+/**
+ * Adds the same handler for all of the specified attrs
+ * @param {String} attrs Pipe-separated list of attributes
+ * @param {Function} handler The method that will be applied
+ */
+function addHandle( attrs, handler ) {
+ var arr = attrs.split("|"),
+ i = arr.length;
+
+ while ( i-- ) {
+ Expr.attrHandle[ arr[i] ] = handler;
+ }
+}
+
+/**
+ * Checks document order of two siblings
+ * @param {Element} a
+ * @param {Element} b
+ * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b
+ */
+function siblingCheck( a, b ) {
+ var cur = b && a,
+ diff = cur && a.nodeType === 1 && b.nodeType === 1 &&
+ a.sourceIndex - b.sourceIndex;
+
+ // Use IE sourceIndex if available on both nodes
+ if ( diff ) {
+ return diff;
+ }
+
+ // Check if b follows a
+ if ( cur ) {
+ while ( (cur = cur.nextSibling) ) {
+ if ( cur === b ) {
+ return -1;
+ }
+ }
+ }
+
+ return a ? 1 : -1;
+}
+
+/**
+ * Returns a function to use in pseudos for input types
+ * @param {String} type
+ */
+function createInputPseudo( type ) {
+ return function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return name === "input" && elem.type === type;
+ };
+}
+
+/**
+ * Returns a function to use in pseudos for buttons
+ * @param {String} type
+ */
+function createButtonPseudo( type ) {
+ return function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return (name === "input" || name === "button") && elem.type === type;
+ };
+}
+
+/**
+ * Returns a function to use in pseudos for :enabled/:disabled
+ * @param {Boolean} disabled true for :disabled; false for :enabled
+ */
+function createDisabledPseudo( disabled ) {
+
+ // Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable
+ return function( elem ) {
+
+ // Only certain elements can match :enabled or :disabled
+ // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled
+ // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled
+ if ( "form" in elem ) {
+
+ // Check for inherited disabledness on relevant non-disabled elements:
+ // * listed form-associated elements in a disabled fieldset
+ // https://html.spec.whatwg.org/multipage/forms.html#category-listed
+ // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled
+ // * option elements in a disabled optgroup
+ // https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled
+ // All such elements have a "form" property.
+ if ( elem.parentNode && elem.disabled === false ) {
+
+ // Option elements defer to a parent optgroup if present
+ if ( "label" in elem ) {
+ if ( "label" in elem.parentNode ) {
+ return elem.parentNode.disabled === disabled;
+ } else {
+ return elem.disabled === disabled;
+ }
+ }
+
+ // Support: IE 6 - 11
+ // Use the isDisabled shortcut property to check for disabled fieldset ancestors
+ return elem.isDisabled === disabled ||
+
+ // Where there is no isDisabled, check manually
+ /* jshint -W018 */
+ elem.isDisabled !== !disabled &&
+ inDisabledFieldset( elem ) === disabled;
+ }
+
+ return elem.disabled === disabled;
+
+ // Try to winnow out elements that can't be disabled before trusting the disabled property.
+ // Some victims get caught in our net (label, legend, menu, track), but it shouldn't
+ // even exist on them, let alone have a boolean value.
+ } else if ( "label" in elem ) {
+ return elem.disabled === disabled;
+ }
+
+ // Remaining elements are neither :enabled nor :disabled
+ return false;
+ };
+}
+
+/**
+ * Returns a function to use in pseudos for positionals
+ * @param {Function} fn
+ */
+function createPositionalPseudo( fn ) {
+ return markFunction(function( argument ) {
+ argument = +argument;
+ return markFunction(function( seed, matches ) {
+ var j,
+ matchIndexes = fn( [], seed.length, argument ),
+ i = matchIndexes.length;
+
+ // Match elements found at the specified indexes
+ while ( i-- ) {
+ if ( seed[ (j = matchIndexes[i]) ] ) {
+ seed[j] = !(matches[j] = seed[j]);
+ }
+ }
+ });
+ });
+}
+
+/**
+ * Checks a node for validity as a Sizzle context
+ * @param {Element|Object=} context
+ * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value
+ */
+function testContext( context ) {
+ return context && typeof context.getElementsByTagName !== "undefined" && context;
+}
+
+// Expose support vars for convenience
+support = Sizzle.support = {};
+
+/**
+ * Detects XML nodes
+ * @param {Element|Object} elem An element or a document
+ * @returns {Boolean} True iff elem is a non-HTML XML node
+ */
+isXML = Sizzle.isXML = function( elem ) {
+ var namespace = elem.namespaceURI,
+ docElem = (elem.ownerDocument || elem).documentElement;
+
+ // Support: IE <=8
+ // Assume HTML when documentElement doesn't yet exist, such as inside loading iframes
+ // https://bugs.jquery.com/ticket/4833
+ return !rhtml.test( namespace || docElem && docElem.nodeName || "HTML" );
+};
+
+/**
+ * Sets document-related variables once based on the current document
+ * @param {Element|Object} [doc] An element or document object to use to set the document
+ * @returns {Object} Returns the current document
+ */
+setDocument = Sizzle.setDocument = function( node ) {
+ var hasCompare, subWindow,
+ doc = node ? node.ownerDocument || node : preferredDoc;
+
+ // Return early if doc is invalid or already selected
+ if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {
+ return document;
+ }
+
+ // Update global variables
+ document = doc;
+ docElem = document.documentElement;
+ documentIsHTML = !isXML( document );
+
+ // Support: IE 9-11, Edge
+ // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936)
+ if ( preferredDoc !== document &&
+ (subWindow = document.defaultView) && subWindow.top !== subWindow ) {
+
+ // Support: IE 11, Edge
+ if ( subWindow.addEventListener ) {
+ subWindow.addEventListener( "unload", unloadHandler, false );
+
+ // Support: IE 9 - 10 only
+ } else if ( subWindow.attachEvent ) {
+ subWindow.attachEvent( "onunload", unloadHandler );
+ }
+ }
+
+ /* Attributes
+ ---------------------------------------------------------------------- */
+
+ // Support: IE<8
+ // Verify that getAttribute really returns attributes and not properties
+ // (excepting IE8 booleans)
+ support.attributes = assert(function( el ) {
+ el.className = "i";
+ return !el.getAttribute("className");
+ });
+
+ /* getElement(s)By*
+ ---------------------------------------------------------------------- */
+
+ // Check if getElementsByTagName("*") returns only elements
+ support.getElementsByTagName = assert(function( el ) {
+ el.appendChild( document.createComment("") );
+ return !el.getElementsByTagName("*").length;
+ });
+
+ // Support: IE<9
+ support.getElementsByClassName = rnative.test( document.getElementsByClassName );
+
+ // Support: IE<10
+ // Check if getElementById returns elements by name
+ // The broken getElementById methods don't pick up programmatically-set names,
+ // so use a roundabout getElementsByName test
+ support.getById = assert(function( el ) {
+ docElem.appendChild( el ).id = expando;
+ return !document.getElementsByName || !document.getElementsByName( expando ).length;
+ });
+
+ // ID filter and find
+ if ( support.getById ) {
+ Expr.filter["ID"] = function( id ) {
+ var attrId = id.replace( runescape, funescape );
+ return function( elem ) {
+ return elem.getAttribute("id") === attrId;
+ };
+ };
+ Expr.find["ID"] = function( id, context ) {
+ if ( typeof context.getElementById !== "undefined" && documentIsHTML ) {
+ var elem = context.getElementById( id );
+ return elem ? [ elem ] : [];
+ }
+ };
+ } else {
+ Expr.filter["ID"] = function( id ) {
+ var attrId = id.replace( runescape, funescape );
+ return function( elem ) {
+ var node = typeof elem.getAttributeNode !== "undefined" &&
+ elem.getAttributeNode("id");
+ return node && node.value === attrId;
+ };
+ };
+
+ // Support: IE 6 - 7 only
+ // getElementById is not reliable as a find shortcut
+ Expr.find["ID"] = function( id, context ) {
+ if ( typeof context.getElementById !== "undefined" && documentIsHTML ) {
+ var node, i, elems,
+ elem = context.getElementById( id );
+
+ if ( elem ) {
+
+ // Verify the id attribute
+ node = elem.getAttributeNode("id");
+ if ( node && node.value === id ) {
+ return [ elem ];
+ }
+
+ // Fall back on getElementsByName
+ elems = context.getElementsByName( id );
+ i = 0;
+ while ( (elem = elems[i++]) ) {
+ node = elem.getAttributeNode("id");
+ if ( node && node.value === id ) {
+ return [ elem ];
+ }
+ }
+ }
+
+ return [];
+ }
+ };
+ }
+
+ // Tag
+ Expr.find["TAG"] = support.getElementsByTagName ?
+ function( tag, context ) {
+ if ( typeof context.getElementsByTagName !== "undefined" ) {
+ return context.getElementsByTagName( tag );
+
+ // DocumentFragment nodes don't have gEBTN
+ } else if ( support.qsa ) {
+ return context.querySelectorAll( tag );
+ }
+ } :
+
+ function( tag, context ) {
+ var elem,
+ tmp = [],
+ i = 0,
+ // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too
+ results = context.getElementsByTagName( tag );
+
+ // Filter out possible comments
+ if ( tag === "*" ) {
+ while ( (elem = results[i++]) ) {
+ if ( elem.nodeType === 1 ) {
+ tmp.push( elem );
+ }
+ }
+
+ return tmp;
+ }
+ return results;
+ };
+
+ // Class
+ Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) {
+ if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) {
+ return context.getElementsByClassName( className );
+ }
+ };
+
+ /* QSA/matchesSelector
+ ---------------------------------------------------------------------- */
+
+ // QSA and matchesSelector support
+
+ // matchesSelector(:active) reports false when true (IE9/Opera 11.5)
+ rbuggyMatches = [];
+
+ // qSa(:focus) reports false when true (Chrome 21)
+ // We allow this because of a bug in IE8/9 that throws an error
+ // whenever `document.activeElement` is accessed on an iframe
+ // So, we allow :focus to pass through QSA all the time to avoid the IE error
+ // See https://bugs.jquery.com/ticket/13378
+ rbuggyQSA = [];
+
+ if ( (support.qsa = rnative.test( document.querySelectorAll )) ) {
+ // Build QSA regex
+ // Regex strategy adopted from Diego Perini
+ assert(function( el ) {
+ // Select is set to empty string on purpose
+ // This is to test IE's treatment of not explicitly
+ // setting a boolean content attribute,
+ // since its presence should be enough
+ // https://bugs.jquery.com/ticket/12359
+ docElem.appendChild( el ).innerHTML = "<a id='" + expando + "'></a>" +
+ "<select id='" + expando + "-\r\\' msallowcapture=''>" +
+ "<option selected=''></option></select>";
+
+ // Support: IE8, Opera 11-12.16
+ // Nothing should be selected when empty strings follow ^= or $= or *=
+ // The test attribute must be unknown in Opera but "safe" for WinRT
+ // https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section
+ if ( el.querySelectorAll("[msallowcapture^='']").length ) {
+ rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" );
+ }
+
+ // Support: IE8
+ // Boolean attributes and "value" are not treated correctly
+ if ( !el.querySelectorAll("[selected]").length ) {
+ rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" );
+ }
+
+ // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+
+ if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) {
+ rbuggyQSA.push("~=");
+ }
+
+ // Webkit/Opera - :checked should return selected option elements
+ // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+ // IE8 throws error here and will not see later tests
+ if ( !el.querySelectorAll(":checked").length ) {
+ rbuggyQSA.push(":checked");
+ }
+
+ // Support: Safari 8+, iOS 8+
+ // https://bugs.webkit.org/show_bug.cgi?id=136851
+ // In-page `selector#id sibling-combinator selector` fails
+ if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) {
+ rbuggyQSA.push(".#.+[+~]");
+ }
+ });
+
+ assert(function( el ) {
+ el.innerHTML = "<a href='' disabled='disabled'></a>" +
+ "<select disabled='disabled'><option/></select>";
+
+ // Support: Windows 8 Native Apps
+ // The type and name attributes are restricted during .innerHTML assignment
+ var input = document.createElement("input");
+ input.setAttribute( "type", "hidden" );
+ el.appendChild( input ).setAttribute( "name", "D" );
+
+ // Support: IE8
+ // Enforce case-sensitivity of name attribute
+ if ( el.querySelectorAll("[name=d]").length ) {
+ rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" );
+ }
+
+ // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
+ // IE8 throws error here and will not see later tests
+ if ( el.querySelectorAll(":enabled").length !== 2 ) {
+ rbuggyQSA.push( ":enabled", ":disabled" );
+ }
+
+ // Support: IE9-11+
+ // IE's :disabled selector does not pick up the children of disabled fieldsets
+ docElem.appendChild( el ).disabled = true;
+ if ( el.querySelectorAll(":disabled").length !== 2 ) {
+ rbuggyQSA.push( ":enabled", ":disabled" );
+ }
+
+ // Opera 10-11 does not throw on post-comma invalid pseudos
+ el.querySelectorAll("*,:x");
+ rbuggyQSA.push(",.*:");
+ });
+ }
+
+ if ( (support.matchesSelector = rnative.test( (matches = docElem.matches ||
+ docElem.webkitMatchesSelector ||
+ docElem.mozMatchesSelector ||
+ docElem.oMatchesSelector ||
+ docElem.msMatchesSelector) )) ) {
+
+ assert(function( el ) {
+ // Check to see if it's possible to do matchesSelector
+ // on a disconnected node (IE 9)
+ support.disconnectedMatch = matches.call( el, "*" );
+
+ // This should fail with an exception
+ // Gecko does not error, returns false instead
+ matches.call( el, "[s!='']:x" );
+ rbuggyMatches.push( "!=", pseudos );
+ });
+ }
+
+ rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") );
+ rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") );
+
+ /* Contains
+ ---------------------------------------------------------------------- */
+ hasCompare = rnative.test( docElem.compareDocumentPosition );
+
+ // Element contains another
+ // Purposefully self-exclusive
+ // As in, an element does not contain itself
+ contains = hasCompare || rnative.test( docElem.contains ) ?
+ function( a, b ) {
+ var adown = a.nodeType === 9 ? a.documentElement : a,
+ bup = b && b.parentNode;
+ return a === bup || !!( bup && bup.nodeType === 1 && (
+ adown.contains ?
+ adown.contains( bup ) :
+ a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
+ ));
+ } :
+ function( a, b ) {
+ if ( b ) {
+ while ( (b = b.parentNode) ) {
+ if ( b === a ) {
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+
+ /* Sorting
+ ---------------------------------------------------------------------- */
+
+ // Document order sorting
+ sortOrder = hasCompare ?
+ function( a, b ) {
+
+ // Flag for duplicate removal
+ if ( a === b ) {
+ hasDuplicate = true;
+ return 0;
+ }
+
+ // Sort on method existence if only one input has compareDocumentPosition
+ var compare = !a.compareDocumentPosition - !b.compareDocumentPosition;
+ if ( compare ) {
+ return compare;
+ }
+
+ // Calculate position if both inputs belong to the same document
+ compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ?
+ a.compareDocumentPosition( b ) :
+
+ // Otherwise we know they are disconnected
+ 1;
+
+ // Disconnected nodes
+ if ( compare & 1 ||
+ (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) {
+
+ // Choose the first element that is related to our preferred document
+ if ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) {
+ return -1;
+ }
+ if ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) {
+ return 1;
+ }
+
+ // Maintain original order
+ return sortInput ?
+ ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
+ 0;
+ }
+
+ return compare & 4 ? -1 : 1;
+ } :
+ function( a, b ) {
+ // Exit early if the nodes are identical
+ if ( a === b ) {
+ hasDuplicate = true;
+ return 0;
+ }
+
+ var cur,
+ i = 0,
+ aup = a.parentNode,
+ bup = b.parentNode,
+ ap = [ a ],
+ bp = [ b ];
+
+ // Parentless nodes are either documents or disconnected
+ if ( !aup || !bup ) {
+ return a === document ? -1 :
+ b === document ? 1 :
+ aup ? -1 :
+ bup ? 1 :
+ sortInput ?
+ ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
+ 0;
+
+ // If the nodes are siblings, we can do a quick check
+ } else if ( aup === bup ) {
+ return siblingCheck( a, b );
+ }
+
+ // Otherwise we need full lists of their ancestors for comparison
+ cur = a;
+ while ( (cur = cur.parentNode) ) {
+ ap.unshift( cur );
+ }
+ cur = b;
+ while ( (cur = cur.parentNode) ) {
+ bp.unshift( cur );
+ }
+
+ // Walk down the tree looking for a discrepancy
+ while ( ap[i] === bp[i] ) {
+ i++;
+ }
+
+ return i ?
+ // Do a sibling check if the nodes have a common ancestor
+ siblingCheck( ap[i], bp[i] ) :
+
+ // Otherwise nodes in our document sort first
+ ap[i] === preferredDoc ? -1 :
+ bp[i] === preferredDoc ? 1 :
+ 0;
+ };
+
+ return document;
+};
+
+Sizzle.matches = function( expr, elements ) {
+ return Sizzle( expr, null, null, elements );
+};
+
+Sizzle.matchesSelector = function( elem, expr ) {
+ // Set document vars if needed
+ if ( ( elem.ownerDocument || elem ) !== document ) {
+ setDocument( elem );
+ }
+
+ if ( support.matchesSelector && documentIsHTML &&
+ !nonnativeSelectorCache[ expr + " " ] &&
+ ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&
+ ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) {
+
+ try {
+ var ret = matches.call( elem, expr );
+
+ // IE 9's matchesSelector returns false on disconnected nodes
+ if ( ret || support.disconnectedMatch ||
+ // As well, disconnected nodes are said to be in a document
+ // fragment in IE 9
+ elem.document && elem.document.nodeType !== 11 ) {
+ return ret;
+ }
+ } catch (e) {
+ nonnativeSelectorCache( expr, true );
+ }
+ }
+
+ return Sizzle( expr, document, null, [ elem ] ).length > 0;
+};
+
+Sizzle.contains = function( context, elem ) {
+ // Set document vars if needed
+ if ( ( context.ownerDocument || context ) !== document ) {
+ setDocument( context );
+ }
+ return contains( context, elem );
+};
+
+Sizzle.attr = function( elem, name ) {
+ // Set document vars if needed
+ if ( ( elem.ownerDocument || elem ) !== document ) {
+ setDocument( elem );
+ }
+
+ var fn = Expr.attrHandle[ name.toLowerCase() ],
+ // Don't get fooled by Object.prototype properties (jQuery #13807)
+ val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?
+ fn( elem, name, !documentIsHTML ) :
+ undefined;
+
+ return val !== undefined ?
+ val :
+ support.attributes || !documentIsHTML ?
+ elem.getAttribute( name ) :
+ (val = elem.getAttributeNode(name)) && val.specified ?
+ val.value :
+ null;
+};
+
+Sizzle.escape = function( sel ) {
+ return (sel + "").replace( rcssescape, fcssescape );
+};
+
+Sizzle.error = function( msg ) {
+ throw new Error( "Syntax error, unrecognized expression: " + msg );
+};
+
+/**
+ * Document sorting and removing duplicates
+ * @param {ArrayLike} results
+ */
+Sizzle.uniqueSort = function( results ) {
+ var elem,
+ duplicates = [],
+ j = 0,
+ i = 0;
+
+ // Unless we *know* we can detect duplicates, assume their presence
+ hasDuplicate = !support.detectDuplicates;
+ sortInput = !support.sortStable && results.slice( 0 );
+ results.sort( sortOrder );
+
+ if ( hasDuplicate ) {
+ while ( (elem = results[i++]) ) {
+ if ( elem === results[ i ] ) {
+ j = duplicates.push( i );
+ }
+ }
+ while ( j-- ) {
+ results.splice( duplicates[ j ], 1 );
+ }
+ }
+
+ // Clear input after sorting to release objects
+ // See https://github.com/jquery/sizzle/pull/225
+ sortInput = null;
+
+ return results;
+};
+
+/**
+ * Utility function for retrieving the text value of an array of DOM nodes
+ * @param {Array|Element} elem
+ */
+getText = Sizzle.getText = function( elem ) {
+ var node,
+ ret = "",
+ i = 0,
+ nodeType = elem.nodeType;
+
+ if ( !nodeType ) {
+ // If no nodeType, this is expected to be an array
+ while ( (node = elem[i++]) ) {
+ // Do not traverse comment nodes
+ ret += getText( node );
+ }
+ } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
+ // Use textContent for elements
+ // innerText usage removed for consistency of new lines (jQuery #11153)
+ if ( typeof elem.textContent === "string" ) {
+ return elem.textContent;
+ } else {
+ // Traverse its children
+ for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+ ret += getText( elem );
+ }
+ }
+ } else if ( nodeType === 3 || nodeType === 4 ) {
+ return elem.nodeValue;
+ }
+ // Do not include comment or processing instruction nodes
+
+ return ret;
+};
+
+Expr = Sizzle.selectors = {
+
+ // Can be adjusted by the user
+ cacheLength: 50,
+
+ createPseudo: markFunction,
+
+ match: matchExpr,
+
+ attrHandle: {},
+
+ find: {},
+
+ relative: {
+ ">": { dir: "parentNode", first: true },
+ " ": { dir: "parentNode" },
+ "+": { dir: "previousSibling", first: true },
+ "~": { dir: "previousSibling" }
+ },
+
+ preFilter: {
+ "ATTR": function( match ) {
+ match[1] = match[1].replace( runescape, funescape );
+
+ // Move the given value to match[3] whether quoted or unquoted
+ match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape );
+
+ if ( match[2] === "~=" ) {
+ match[3] = " " + match[3] + " ";
+ }
+
+ return match.slice( 0, 4 );
+ },
+
+ "CHILD": function( match ) {
+ /* matches from matchExpr["CHILD"]
+ 1 type (only|nth|...)
+ 2 what (child|of-type)
+ 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...)
+ 4 xn-component of xn+y argument ([+-]?\d*n|)
+ 5 sign of xn-component
+ 6 x of xn-component
+ 7 sign of y-component
+ 8 y of y-component
+ */
+ match[1] = match[1].toLowerCase();
+
+ if ( match[1].slice( 0, 3 ) === "nth" ) {
+ // nth-* requires argument
+ if ( !match[3] ) {
+ Sizzle.error( match[0] );
+ }
+
+ // numeric x and y parameters for Expr.filter.CHILD
+ // remember that false/true cast respectively to 0/1
+ match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) );
+ match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" );
+
+ // other types prohibit arguments
+ } else if ( match[3] ) {
+ Sizzle.error( match[0] );
+ }
+
+ return match;
+ },
+
+ "PSEUDO": function( match ) {
+ var excess,
+ unquoted = !match[6] && match[2];
+
+ if ( matchExpr["CHILD"].test( match[0] ) ) {
+ return null;
+ }
+
+ // Accept quoted arguments as-is
+ if ( match[3] ) {
+ match[2] = match[4] || match[5] || "";
+
+ // Strip excess characters from unquoted arguments
+ } else if ( unquoted && rpseudo.test( unquoted ) &&
+ // Get excess from tokenize (recursively)
+ (excess = tokenize( unquoted, true )) &&
+ // advance to the next closing parenthesis
+ (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) {
+
+ // excess is a negative index
+ match[0] = match[0].slice( 0, excess );
+ match[2] = unquoted.slice( 0, excess );
+ }
+
+ // Return only captures needed by the pseudo filter method (type and argument)
+ return match.slice( 0, 3 );
+ }
+ },
+
+ filter: {
+
+ "TAG": function( nodeNameSelector ) {
+ var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();
+ return nodeNameSelector === "*" ?
+ function() { return true; } :
+ function( elem ) {
+ return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
+ };
+ },
+
+ "CLASS": function( className ) {
+ var pattern = classCache[ className + " " ];
+
+ return pattern ||
+ (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) &&
+ classCache( className, function( elem ) {
+ return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" );
+ });
+ },
+
+ "ATTR": function( name, operator, check ) {
+ return function( elem ) {
+ var result = Sizzle.attr( elem, name );
+
+ if ( result == null ) {
+ return operator === "!=";
+ }
+ if ( !operator ) {
+ return true;
+ }
+
+ result += "";
+
+ return operator === "=" ? result === check :
+ operator === "!=" ? result !== check :
+ operator === "^=" ? check && result.indexOf( check ) === 0 :
+ operator === "*=" ? check && result.indexOf( check ) > -1 :
+ operator === "$=" ? check && result.slice( -check.length ) === check :
+ operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 :
+ operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" :
+ false;
+ };
+ },
+
+ "CHILD": function( type, what, argument, first, last ) {
+ var simple = type.slice( 0, 3 ) !== "nth",
+ forward = type.slice( -4 ) !== "last",
+ ofType = what === "of-type";
+
+ return first === 1 && last === 0 ?
+
+ // Shortcut for :nth-*(n)
+ function( elem ) {
+ return !!elem.parentNode;
+ } :
+
+ function( elem, context, xml ) {
+ var cache, uniqueCache, outerCache, node, nodeIndex, start,
+ dir = simple !== forward ? "nextSibling" : "previousSibling",
+ parent = elem.parentNode,
+ name = ofType && elem.nodeName.toLowerCase(),
+ useCache = !xml && !ofType,
+ diff = false;
+
+ if ( parent ) {
+
+ // :(first|last|only)-(child|of-type)
+ if ( simple ) {
+ while ( dir ) {
+ node = elem;
+ while ( (node = node[ dir ]) ) {
+ if ( ofType ?
+ node.nodeName.toLowerCase() === name :
+ node.nodeType === 1 ) {
+
+ return false;
+ }
+ }
+ // Reverse direction for :only-* (if we haven't yet done so)
+ start = dir = type === "only" && !start && "nextSibling";
+ }
+ return true;
+ }
+
+ start = [ forward ? parent.firstChild : parent.lastChild ];
+
+ // non-xml :nth-child(...) stores cache data on `parent`
+ if ( forward && useCache ) {
+
+ // Seek `elem` from a previously-cached index
+
+ // ...in a gzip-friendly way
+ node = parent;
+ outerCache = node[ expando ] || (node[ expando ] = {});
+
+ // Support: IE <9 only
+ // Defend against cloned attroperties (jQuery gh-1709)
+ uniqueCache = outerCache[ node.uniqueID ] ||
+ (outerCache[ node.uniqueID ] = {});
+
+ cache = uniqueCache[ type ] || [];
+ nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];
+ diff = nodeIndex && cache[ 2 ];
+ node = nodeIndex && parent.childNodes[ nodeIndex ];
+
+ while ( (node = ++nodeIndex && node && node[ dir ] ||
+
+ // Fallback to seeking `elem` from the start
+ (diff = nodeIndex = 0) || start.pop()) ) {
+
+ // When found, cache indexes on `parent` and break
+ if ( node.nodeType === 1 && ++diff && node === elem ) {
+ uniqueCache[ type ] = [ dirruns, nodeIndex, diff ];
+ break;
+ }
+ }
+
+ } else {
+ // Use previously-cached element index if available
+ if ( useCache ) {
+ // ...in a gzip-friendly way
+ node = elem;
+ outerCache = node[ expando ] || (node[ expando ] = {});
+
+ // Support: IE <9 only
+ // Defend against cloned attroperties (jQuery gh-1709)
+ uniqueCache = outerCache[ node.uniqueID ] ||
+ (outerCache[ node.uniqueID ] = {});
+
+ cache = uniqueCache[ type ] || [];
+ nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];
+ diff = nodeIndex;
+ }
+
+ // xml :nth-child(...)
+ // or :nth-last-child(...) or :nth(-last)?-of-type(...)
+ if ( diff === false ) {
+ // Use the same loop as above to seek `elem` from the start
+ while ( (node = ++nodeIndex && node && node[ dir ] ||
+ (diff = nodeIndex = 0) || start.pop()) ) {
+
+ if ( ( ofType ?
+ node.nodeName.toLowerCase() === name :
+ node.nodeType === 1 ) &&
+ ++diff ) {
+
+ // Cache the index of each encountered element
+ if ( useCache ) {
+ outerCache = node[ expando ] || (node[ expando ] = {});
+
+ // Support: IE <9 only
+ // Defend against cloned attroperties (jQuery gh-1709)
+ uniqueCache = outerCache[ node.uniqueID ] ||
+ (outerCache[ node.uniqueID ] = {});
+
+ uniqueCache[ type ] = [ dirruns, diff ];
+ }
+
+ if ( node === elem ) {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // Incorporate the offset, then check against cycle size
+ diff -= last;
+ return diff === first || ( diff % first === 0 && diff / first >= 0 );
+ }
+ };
+ },
+
+ "PSEUDO": function( pseudo, argument ) {
+ // pseudo-class names are case-insensitive
+ // http://www.w3.org/TR/selectors/#pseudo-classes
+ // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
+ // Remember that setFilters inherits from pseudos
+ var args,
+ fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||
+ Sizzle.error( "unsupported pseudo: " + pseudo );
+
+ // The user may use createPseudo to indicate that
+ // arguments are needed to create the filter function
+ // just as Sizzle does
+ if ( fn[ expando ] ) {
+ return fn( argument );
+ }
+
+ // But maintain support for old signatures
+ if ( fn.length > 1 ) {
+ args = [ pseudo, pseudo, "", argument ];
+ return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?
+ markFunction(function( seed, matches ) {
+ var idx,
+ matched = fn( seed, argument ),
+ i = matched.length;
+ while ( i-- ) {
+ idx = indexOf( seed, matched[i] );
+ seed[ idx ] = !( matches[ idx ] = matched[i] );
+ }
+ }) :
+ function( elem ) {
+ return fn( elem, 0, args );
+ };
+ }
+
+ return fn;
+ }
+ },
+
+ pseudos: {
+ // Potentially complex pseudos
+ "not": markFunction(function( selector ) {
+ // Trim the selector passed to compile
+ // to avoid treating leading and trailing
+ // spaces as combinators
+ var input = [],
+ results = [],
+ matcher = compile( selector.replace( rtrim, "$1" ) );
+
+ return matcher[ expando ] ?
+ markFunction(function( seed, matches, context, xml ) {
+ var elem,
+ unmatched = matcher( seed, null, xml, [] ),
+ i = seed.length;
+
+ // Match elements unmatched by `matcher`
+ while ( i-- ) {
+ if ( (elem = unmatched[i]) ) {
+ seed[i] = !(matches[i] = elem);
+ }
+ }
+ }) :
+ function( elem, context, xml ) {
+ input[0] = elem;
+ matcher( input, null, xml, results );
+ // Don't keep the element (issue #299)
+ input[0] = null;
+ return !results.pop();
+ };
+ }),
+
+ "has": markFunction(function( selector ) {
+ return function( elem ) {
+ return Sizzle( selector, elem ).length > 0;
+ };
+ }),
+
+ "contains": markFunction(function( text ) {
+ text = text.replace( runescape, funescape );
+ return function( elem ) {
+ return ( elem.textContent || getText( elem ) ).indexOf( text ) > -1;
+ };
+ }),
+
+ // "Whether an element is represented by a :lang() selector
+ // is based solely on the element's language value
+ // being equal to the identifier C,
+ // or beginning with the identifier C immediately followed by "-".
+ // The matching of C against the element's language value is performed case-insensitively.
+ // The identifier C does not have to be a valid language name."
+ // http://www.w3.org/TR/selectors/#lang-pseudo
+ "lang": markFunction( function( lang ) {
+ // lang value must be a valid identifier
+ if ( !ridentifier.test(lang || "") ) {
+ Sizzle.error( "unsupported lang: " + lang );
+ }
+ lang = lang.replace( runescape, funescape ).toLowerCase();
+ return function( elem ) {
+ var elemLang;
+ do {
+ if ( (elemLang = documentIsHTML ?
+ elem.lang :
+ elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) {
+
+ elemLang = elemLang.toLowerCase();
+ return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0;
+ }
+ } while ( (elem = elem.parentNode) && elem.nodeType === 1 );
+ return false;
+ };
+ }),
+
+ // Miscellaneous
+ "target": function( elem ) {
+ var hash = window.location && window.location.hash;
+ return hash && hash.slice( 1 ) === elem.id;
+ },
+
+ "root": function( elem ) {
+ return elem === docElem;
+ },
+
+ "focus": function( elem ) {
+ return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);
+ },
+
+ // Boolean properties
+ "enabled": createDisabledPseudo( false ),
+ "disabled": createDisabledPseudo( true ),
+
+ "checked": function( elem ) {
+ // In CSS3, :checked should return both checked and selected elements
+ // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+ var nodeName = elem.nodeName.toLowerCase();
+ return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected);
+ },
+
+ "selected": function( elem ) {
+ // Accessing this property makes selected-by-default
+ // options in Safari work properly
+ if ( elem.parentNode ) {
+ elem.parentNode.selectedIndex;
+ }
+
+ return elem.selected === true;
+ },
+
+ // Contents
+ "empty": function( elem ) {
+ // http://www.w3.org/TR/selectors/#empty-pseudo
+ // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),
+ // but not by others (comment: 8; processing instruction: 7; etc.)
+ // nodeType < 6 works because attributes (2) do not appear as children
+ for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+ if ( elem.nodeType < 6 ) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ "parent": function( elem ) {
+ return !Expr.pseudos["empty"]( elem );
+ },
+
+ // Element/input types
+ "header": function( elem ) {
+ return rheader.test( elem.nodeName );
+ },
+
+ "input": function( elem ) {
+ return rinputs.test( elem.nodeName );
+ },
+
+ "button": function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return name === "input" && elem.type === "button" || name === "button";
+ },
+
+ "text": function( elem ) {
+ var attr;
+ return elem.nodeName.toLowerCase() === "input" &&
+ elem.type === "text" &&
+
+ // Support: IE<8
+ // New HTML5 attribute values (e.g., "search") appear with elem.type === "text"
+ ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" );
+ },
+
+ // Position-in-collection
+ "first": createPositionalPseudo(function() {
+ return [ 0 ];
+ }),
+
+ "last": createPositionalPseudo(function( matchIndexes, length ) {
+ return [ length - 1 ];
+ }),
+
+ "eq": createPositionalPseudo(function( matchIndexes, length, argument ) {
+ return [ argument < 0 ? argument + length : argument ];
+ }),
+
+ "even": createPositionalPseudo(function( matchIndexes, length ) {
+ var i = 0;
+ for ( ; i < length; i += 2 ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ }),
+
+ "odd": createPositionalPseudo(function( matchIndexes, length ) {
+ var i = 1;
+ for ( ; i < length; i += 2 ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ }),
+
+ "lt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+ var i = argument < 0 ?
+ argument + length :
+ argument > length ?
+ length :
+ argument;
+ for ( ; --i >= 0; ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ }),
+
+ "gt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+ var i = argument < 0 ? argument + length : argument;
+ for ( ; ++i < length; ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ })
+ }
+};
+
+Expr.pseudos["nth"] = Expr.pseudos["eq"];
+
+// Add button/input type pseudos
+for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {
+ Expr.pseudos[ i ] = createInputPseudo( i );
+}
+for ( i in { submit: true, reset: true } ) {
+ Expr.pseudos[ i ] = createButtonPseudo( i );
+}
+
+// Easy API for creating new setFilters
+function setFilters() {}
+setFilters.prototype = Expr.filters = Expr.pseudos;
+Expr.setFilters = new setFilters();
+
+tokenize = Sizzle.tokenize = function( selector, parseOnly ) {
+ var matched, match, tokens, type,
+ soFar, groups, preFilters,
+ cached = tokenCache[ selector + " " ];
+
+ if ( cached ) {
+ return parseOnly ? 0 : cached.slice( 0 );
+ }
+
+ soFar = selector;
+ groups = [];
+ preFilters = Expr.preFilter;
+
+ while ( soFar ) {
+
+ // Comma and first run
+ if ( !matched || (match = rcomma.exec( soFar )) ) {
+ if ( match ) {
+ // Don't consume trailing commas as valid
+ soFar = soFar.slice( match[0].length ) || soFar;
+ }
+ groups.push( (tokens = []) );
+ }
+
+ matched = false;
+
+ // Combinators
+ if ( (match = rcombinators.exec( soFar )) ) {
+ matched = match.shift();
+ tokens.push({
+ value: matched,
+ // Cast descendant combinators to space
+ type: match[0].replace( rtrim, " " )
+ });
+ soFar = soFar.slice( matched.length );
+ }
+
+ // Filters
+ for ( type in Expr.filter ) {
+ if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||
+ (match = preFilters[ type ]( match ))) ) {
+ matched = match.shift();
+ tokens.push({
+ value: matched,
+ type: type,
+ matches: match
+ });
+ soFar = soFar.slice( matched.length );
+ }
+ }
+
+ if ( !matched ) {
+ break;
+ }
+ }
+
+ // Return the length of the invalid excess
+ // if we're just parsing
+ // Otherwise, throw an error or return tokens
+ return parseOnly ?
+ soFar.length :
+ soFar ?
+ Sizzle.error( selector ) :
+ // Cache the tokens
+ tokenCache( selector, groups ).slice( 0 );
+};
+
+function toSelector( tokens ) {
+ var i = 0,
+ len = tokens.length,
+ selector = "";
+ for ( ; i < len; i++ ) {
+ selector += tokens[i].value;
+ }
+ return selector;
+}
+
+function addCombinator( matcher, combinator, base ) {
+ var dir = combinator.dir,
+ skip = combinator.next,
+ key = skip || dir,
+ checkNonElements = base && key === "parentNode",
+ doneName = done++;
+
+ return combinator.first ?
+ // Check against closest ancestor/preceding element
+ function( elem, context, xml ) {
+ while ( (elem = elem[ dir ]) ) {
+ if ( elem.nodeType === 1 || checkNonElements ) {
+ return matcher( elem, context, xml );
+ }
+ }
+ return false;
+ } :
+
+ // Check against all ancestor/preceding elements
+ function( elem, context, xml ) {
+ var oldCache, uniqueCache, outerCache,
+ newCache = [ dirruns, doneName ];
+
+ // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching
+ if ( xml ) {
+ while ( (elem = elem[ dir ]) ) {
+ if ( elem.nodeType === 1 || checkNonElements ) {
+ if ( matcher( elem, context, xml ) ) {
+ return true;
+ }
+ }
+ }
+ } else {
+ while ( (elem = elem[ dir ]) ) {
+ if ( elem.nodeType === 1 || checkNonElements ) {
+ outerCache = elem[ expando ] || (elem[ expando ] = {});
+
+ // Support: IE <9 only
+ // Defend against cloned attroperties (jQuery gh-1709)
+ uniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {});
+
+ if ( skip && skip === elem.nodeName.toLowerCase() ) {
+ elem = elem[ dir ] || elem;
+ } else if ( (oldCache = uniqueCache[ key ]) &&
+ oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) {
+
+ // Assign to newCache so results back-propagate to previous elements
+ return (newCache[ 2 ] = oldCache[ 2 ]);
+ } else {
+ // Reuse newcache so results back-propagate to previous elements
+ uniqueCache[ key ] = newCache;
+
+ // A match means we're done; a fail means we have to keep checking
+ if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ };
+}
+
+function elementMatcher( matchers ) {
+ return matchers.length > 1 ?
+ function( elem, context, xml ) {
+ var i = matchers.length;
+ while ( i-- ) {
+ if ( !matchers[i]( elem, context, xml ) ) {
+ return false;
+ }
+ }
+ return true;
+ } :
+ matchers[0];
+}
+
+function multipleContexts( selector, contexts, results ) {
+ var i = 0,
+ len = contexts.length;
+ for ( ; i < len; i++ ) {
+ Sizzle( selector, contexts[i], results );
+ }
+ return results;
+}
+
+function condense( unmatched, map, filter, context, xml ) {
+ var elem,
+ newUnmatched = [],
+ i = 0,
+ len = unmatched.length,
+ mapped = map != null;
+
+ for ( ; i < len; i++ ) {
+ if ( (elem = unmatched[i]) ) {
+ if ( !filter || filter( elem, context, xml ) ) {
+ newUnmatched.push( elem );
+ if ( mapped ) {
+ map.push( i );
+ }
+ }
+ }
+ }
+
+ return newUnmatched;
+}
+
+function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {
+ if ( postFilter && !postFilter[ expando ] ) {
+ postFilter = setMatcher( postFilter );
+ }
+ if ( postFinder && !postFinder[ expando ] ) {
+ postFinder = setMatcher( postFinder, postSelector );
+ }
+ return markFunction(function( seed, results, context, xml ) {
+ var temp, i, elem,
+ preMap = [],
+ postMap = [],
+ preexisting = results.length,
+
+ // Get initial elements from seed or context
+ elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ),
+
+ // Prefilter to get matcher input, preserving a map for seed-results synchronization
+ matcherIn = preFilter && ( seed || !selector ) ?
+ condense( elems, preMap, preFilter, context, xml ) :
+ elems,
+
+ matcherOut = matcher ?
+ // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,
+ postFinder || ( seed ? preFilter : preexisting || postFilter ) ?
+
+ // ...intermediate processing is necessary
+ [] :
+
+ // ...otherwise use results directly
+ results :
+ matcherIn;
+
+ // Find primary matches
+ if ( matcher ) {
+ matcher( matcherIn, matcherOut, context, xml );
+ }
+
+ // Apply postFilter
+ if ( postFilter ) {
+ temp = condense( matcherOut, postMap );
+ postFilter( temp, [], context, xml );
+
+ // Un-match failing elements by moving them back to matcherIn
+ i = temp.length;
+ while ( i-- ) {
+ if ( (elem = temp[i]) ) {
+ matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);
+ }
+ }
+ }
+
+ if ( seed ) {
+ if ( postFinder || preFilter ) {
+ if ( postFinder ) {
+ // Get the final matcherOut by condensing this intermediate into postFinder contexts
+ temp = [];
+ i = matcherOut.length;
+ while ( i-- ) {
+ if ( (elem = matcherOut[i]) ) {
+ // Restore matcherIn since elem is not yet a final match
+ temp.push( (matcherIn[i] = elem) );
+ }
+ }
+ postFinder( null, (matcherOut = []), temp, xml );
+ }
+
+ // Move matched elements from seed to results to keep them synchronized
+ i = matcherOut.length;
+ while ( i-- ) {
+ if ( (elem = matcherOut[i]) &&
+ (temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) {
+
+ seed[temp] = !(results[temp] = elem);
+ }
+ }
+ }
+
+ // Add elements to results, through postFinder if defined
+ } else {
+ matcherOut = condense(
+ matcherOut === results ?
+ matcherOut.splice( preexisting, matcherOut.length ) :
+ matcherOut
+ );
+ if ( postFinder ) {
+ postFinder( null, results, matcherOut, xml );
+ } else {
+ push.apply( results, matcherOut );
+ }
+ }
+ });
+}
+
+function matcherFromTokens( tokens ) {
+ var checkContext, matcher, j,
+ len = tokens.length,
+ leadingRelative = Expr.relative[ tokens[0].type ],
+ implicitRelative = leadingRelative || Expr.relative[" "],
+ i = leadingRelative ? 1 : 0,
+
+ // The foundational matcher ensures that elements are reachable from top-level context(s)
+ matchContext = addCombinator( function( elem ) {
+ return elem === checkContext;
+ }, implicitRelative, true ),
+ matchAnyContext = addCombinator( function( elem ) {
+ return indexOf( checkContext, elem ) > -1;
+ }, implicitRelative, true ),
+ matchers = [ function( elem, context, xml ) {
+ var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
+ (checkContext = context).nodeType ?
+ matchContext( elem, context, xml ) :
+ matchAnyContext( elem, context, xml ) );
+ // Avoid hanging onto element (issue #299)
+ checkContext = null;
+ return ret;
+ } ];
+
+ for ( ; i < len; i++ ) {
+ if ( (matcher = Expr.relative[ tokens[i].type ]) ) {
+ matchers = [ addCombinator(elementMatcher( matchers ), matcher) ];
+ } else {
+ matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );
+
+ // Return special upon seeing a positional matcher
+ if ( matcher[ expando ] ) {
+ // Find the next relative operator (if any) for proper handling
+ j = ++i;
+ for ( ; j < len; j++ ) {
+ if ( Expr.relative[ tokens[j].type ] ) {
+ break;
+ }
+ }
+ return setMatcher(
+ i > 1 && elementMatcher( matchers ),
+ i > 1 && toSelector(
+ // If the preceding token was a descendant combinator, insert an implicit any-element `*`
+ tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" })
+ ).replace( rtrim, "$1" ),
+ matcher,
+ i < j && matcherFromTokens( tokens.slice( i, j ) ),
+ j < len && matcherFromTokens( (tokens = tokens.slice( j )) ),
+ j < len && toSelector( tokens )
+ );
+ }
+ matchers.push( matcher );
+ }
+ }
+
+ return elementMatcher( matchers );
+}
+
+function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
+ var bySet = setMatchers.length > 0,
+ byElement = elementMatchers.length > 0,
+ superMatcher = function( seed, context, xml, results, outermost ) {
+ var elem, j, matcher,
+ matchedCount = 0,
+ i = "0",
+ unmatched = seed && [],
+ setMatched = [],
+ contextBackup = outermostContext,
+ // We must always have either seed elements or outermost context
+ elems = seed || byElement && Expr.find["TAG"]( "*", outermost ),
+ // Use integer dirruns iff this is the outermost matcher
+ dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1),
+ len = elems.length;
+
+ if ( outermost ) {
+ outermostContext = context === document || context || outermost;
+ }
+
+ // Add elements passing elementMatchers directly to results
+ // Support: IE<9, Safari
+ // Tolerate NodeList properties (IE: "length"; Safari: <number>) matching elements by id
+ for ( ; i !== len && (elem = elems[i]) != null; i++ ) {
+ if ( byElement && elem ) {
+ j = 0;
+ if ( !context && elem.ownerDocument !== document ) {
+ setDocument( elem );
+ xml = !documentIsHTML;
+ }
+ while ( (matcher = elementMatchers[j++]) ) {
+ if ( matcher( elem, context || document, xml) ) {
+ results.push( elem );
+ break;
+ }
+ }
+ if ( outermost ) {
+ dirruns = dirrunsUnique;
+ }
+ }
+
+ // Track unmatched elements for set filters
+ if ( bySet ) {
+ // They will have gone through all possible matchers
+ if ( (elem = !matcher && elem) ) {
+ matchedCount--;
+ }
+
+ // Lengthen the array for every element, matched or not
+ if ( seed ) {
+ unmatched.push( elem );
+ }
+ }
+ }
+
+ // `i` is now the count of elements visited above, and adding it to `matchedCount`
+ // makes the latter nonnegative.
+ matchedCount += i;
+
+ // Apply set filters to unmatched elements
+ // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount`
+ // equals `i`), unless we didn't visit _any_ elements in the above loop because we have
+ // no element matchers and no seed.
+ // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that
+ // case, which will result in a "00" `matchedCount` that differs from `i` but is also
+ // numerically zero.
+ if ( bySet && i !== matchedCount ) {
+ j = 0;
+ while ( (matcher = setMatchers[j++]) ) {
+ matcher( unmatched, setMatched, context, xml );
+ }
+
+ if ( seed ) {
+ // Reintegrate element matches to eliminate the need for sorting
+ if ( matchedCount > 0 ) {
+ while ( i-- ) {
+ if ( !(unmatched[i] || setMatched[i]) ) {
+ setMatched[i] = pop.call( results );
+ }
+ }
+ }
+
+ // Discard index placeholder values to get only actual matches
+ setMatched = condense( setMatched );
+ }
+
+ // Add matches to results
+ push.apply( results, setMatched );
+
+ // Seedless set matches succeeding multiple successful matchers stipulate sorting
+ if ( outermost && !seed && setMatched.length > 0 &&
+ ( matchedCount + setMatchers.length ) > 1 ) {
+
+ Sizzle.uniqueSort( results );
+ }
+ }
+
+ // Override manipulation of globals by nested matchers
+ if ( outermost ) {
+ dirruns = dirrunsUnique;
+ outermostContext = contextBackup;
+ }
+
+ return unmatched;
+ };
+
+ return bySet ?
+ markFunction( superMatcher ) :
+ superMatcher;
+}
+
+compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {
+ var i,
+ setMatchers = [],
+ elementMatchers = [],
+ cached = compilerCache[ selector + " " ];
+
+ if ( !cached ) {
+ // Generate a function of recursive functions that can be used to check each element
+ if ( !match ) {
+ match = tokenize( selector );
+ }
+ i = match.length;
+ while ( i-- ) {
+ cached = matcherFromTokens( match[i] );
+ if ( cached[ expando ] ) {
+ setMatchers.push( cached );
+ } else {
+ elementMatchers.push( cached );
+ }
+ }
+
+ // Cache the compiled function
+ cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );
+
+ // Save selector and tokenization
+ cached.selector = selector;
+ }
+ return cached;
+};
+
+/**
+ * A low-level selection function that works with Sizzle's compiled
+ * selector functions
+ * @param {String|Function} selector A selector or a pre-compiled
+ * selector function built with Sizzle.compile
+ * @param {Element} context
+ * @param {Array} [results]
+ * @param {Array} [seed] A set of elements to match against
+ */
+select = Sizzle.select = function( selector, context, results, seed ) {
+ var i, tokens, token, type, find,
+ compiled = typeof selector === "function" && selector,
+ match = !seed && tokenize( (selector = compiled.selector || selector) );
+
+ results = results || [];
+
+ // Try to minimize operations if there is only one selector in the list and no seed
+ // (the latter of which guarantees us context)
+ if ( match.length === 1 ) {
+
+ // Reduce context if the leading compound selector is an ID
+ tokens = match[0] = match[0].slice( 0 );
+ if ( tokens.length > 2 && (token = tokens[0]).type === "ID" &&
+ context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[1].type ] ) {
+
+ context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];
+ if ( !context ) {
+ return results;
+
+ // Precompiled matchers will still verify ancestry, so step up a level
+ } else if ( compiled ) {
+ context = context.parentNode;
+ }
+
+ selector = selector.slice( tokens.shift().value.length );
+ }
+
+ // Fetch a seed set for right-to-left matching
+ i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length;
+ while ( i-- ) {
+ token = tokens[i];
+
+ // Abort if we hit a combinator
+ if ( Expr.relative[ (type = token.type) ] ) {
+ break;
+ }
+ if ( (find = Expr.find[ type ]) ) {
+ // Search, expanding context for leading sibling combinators
+ if ( (seed = find(
+ token.matches[0].replace( runescape, funescape ),
+ rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context
+ )) ) {
+
+ // If seed is empty or no tokens remain, we can return early
+ tokens.splice( i, 1 );
+ selector = seed.length && toSelector( tokens );
+ if ( !selector ) {
+ push.apply( results, seed );
+ return results;
+ }
+
+ break;
+ }
+ }
+ }
+ }
+
+ // Compile and execute a filtering function if one is not provided
+ // Provide `match` to avoid retokenization if we modified the selector above
+ ( compiled || compile( selector, match ) )(
+ seed,
+ context,
+ !documentIsHTML,
+ results,
+ !context || rsibling.test( selector ) && testContext( context.parentNode ) || context
+ );
+ return results;
+};
+
+// One-time assignments
+
+// Sort stability
+support.sortStable = expando.split("").sort( sortOrder ).join("") === expando;
+
+// Support: Chrome 14-35+
+// Always assume duplicates if they aren't passed to the comparison function
+support.detectDuplicates = !!hasDuplicate;
+
+// Initialize against the default document
+setDocument();
+
+// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)
+// Detached nodes confoundingly follow *each other*
+support.sortDetached = assert(function( el ) {
+ // Should return 1, but returns 4 (following)
+ return el.compareDocumentPosition( document.createElement("fieldset") ) & 1;
+});
+
+// Support: IE<8
+// Prevent attribute/property "interpolation"
+// https://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
+if ( !assert(function( el ) {
+ el.innerHTML = "<a href='#'></a>";
+ return el.firstChild.getAttribute("href") === "#" ;
+}) ) {
+ addHandle( "type|href|height|width", function( elem, name, isXML ) {
+ if ( !isXML ) {
+ return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 );
+ }
+ });
+}
+
+// Support: IE<9
+// Use defaultValue in place of getAttribute("value")
+if ( !support.attributes || !assert(function( el ) {
+ el.innerHTML = "<input/>";
+ el.firstChild.setAttribute( "value", "" );
+ return el.firstChild.getAttribute( "value" ) === "";
+}) ) {
+ addHandle( "value", function( elem, name, isXML ) {
+ if ( !isXML && elem.nodeName.toLowerCase() === "input" ) {
+ return elem.defaultValue;
+ }
+ });
+}
+
+// Support: IE<9
+// Use getAttributeNode to fetch booleans when getAttribute lies
+if ( !assert(function( el ) {
+ return el.getAttribute("disabled") == null;
+}) ) {
+ addHandle( booleans, function( elem, name, isXML ) {
+ var val;
+ if ( !isXML ) {
+ return elem[ name ] === true ? name.toLowerCase() :
+ (val = elem.getAttributeNode( name )) && val.specified ?
+ val.value :
+ null;
+ }
+ });
+}
+
+return Sizzle;
+
+})( window );
+
+
+
+jQuery.find = Sizzle;
+jQuery.expr = Sizzle.selectors;
+
+// Deprecated
+jQuery.expr[ ":" ] = jQuery.expr.pseudos;
+jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort;
+jQuery.text = Sizzle.getText;
+jQuery.isXMLDoc = Sizzle.isXML;
+jQuery.contains = Sizzle.contains;
+jQuery.escapeSelector = Sizzle.escape;
+
+
+
+
+var dir = function( elem, dir, until ) {
+ var matched = [],
+ truncate = until !== undefined;
+
+ while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) {
+ if ( elem.nodeType === 1 ) {
+ if ( truncate && jQuery( elem ).is( until ) ) {
+ break;
+ }
+ matched.push( elem );
+ }
+ }
+ return matched;
+};
+
+
+var siblings = function( n, elem ) {
+ var matched = [];
+
+ for ( ; n; n = n.nextSibling ) {
+ if ( n.nodeType === 1 && n !== elem ) {
+ matched.push( n );
+ }
+ }
+
+ return matched;
+};
+
+
+var rneedsContext = jQuery.expr.match.needsContext;
+
+
+
+function nodeName( elem, name ) {
+
+ return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
+
+};
+var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i );
+
+
+
+// Implement the identical functionality for filter and not
+function winnow( elements, qualifier, not ) {
+ if ( isFunction( qualifier ) ) {
+ return jQuery.grep( elements, function( elem, i ) {
+ return !!qualifier.call( elem, i, elem ) !== not;
+ } );
+ }
+
+ // Single element
+ if ( qualifier.nodeType ) {
+ return jQuery.grep( elements, function( elem ) {
+ return ( elem === qualifier ) !== not;
+ } );
+ }
+
+ // Arraylike of elements (jQuery, arguments, Array)
+ if ( typeof qualifier !== "string" ) {
+ return jQuery.grep( elements, function( elem ) {
+ return ( indexOf.call( qualifier, elem ) > -1 ) !== not;
+ } );
+ }
+
+ // Filtered directly for both simple and complex selectors
+ return jQuery.filter( qualifier, elements, not );
+}
+
+jQuery.filter = function( expr, elems, not ) {
+ var elem = elems[ 0 ];
+
+ if ( not ) {
+ expr = ":not(" + expr + ")";
+ }
+
+ if ( elems.length === 1 && elem.nodeType === 1 ) {
+ return jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [];
+ }
+
+ return jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {
+ return elem.nodeType === 1;
+ } ) );
+};
+
+jQuery.fn.extend( {
+ find: function( selector ) {
+ var i, ret,
+ len = this.length,
+ self = this;
+
+ if ( typeof selector !== "string" ) {
+ return this.pushStack( jQuery( selector ).filter( function() {
+ for ( i = 0; i < len; i++ ) {
+ if ( jQuery.contains( self[ i ], this ) ) {
+ return true;
+ }
+ }
+ } ) );
+ }
+
+ ret = this.pushStack( [] );
+
+ for ( i = 0; i < len; i++ ) {
+ jQuery.find( selector, self[ i ], ret );
+ }
+
+ return len > 1 ? jQuery.uniqueSort( ret ) : ret;
+ },
+ filter: function( selector ) {
+ return this.pushStack( winnow( this, selector || [], false ) );
+ },
+ not: function( selector ) {
+ return this.pushStack( winnow( this, selector || [], true ) );
+ },
+ is: function( selector ) {
+ return !!winnow(
+ this,
+
+ // If this is a positional/relative selector, check membership in the returned set
+ // so $("p:first").is("p:last") won't return true for a doc with two "p".
+ typeof selector === "string" && rneedsContext.test( selector ) ?
+ jQuery( selector ) :
+ selector || [],
+ false
+ ).length;
+ }
+} );
+
+
+// Initialize a jQuery object
+
+
+// A central reference to the root jQuery(document)
+var rootjQuery,
+
+ // A simple way to check for HTML strings
+ // Prioritize #id over <tag> to avoid XSS via location.hash (#9521)
+ // Strict HTML recognition (#11290: must start with <)
+ // Shortcut simple #id case for speed
+ rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,
+
+ init = jQuery.fn.init = function( selector, context, root ) {
+ var match, elem;
+
+ // HANDLE: $(""), $(null), $(undefined), $(false)
+ if ( !selector ) {
+ return this;
+ }
+
+ // Method init() accepts an alternate rootjQuery
+ // so migrate can support jQuery.sub (gh-2101)
+ root = root || rootjQuery;
+
+ // Handle HTML strings
+ if ( typeof selector === "string" ) {
+ if ( selector[ 0 ] === "<" &&
+ selector[ selector.length - 1 ] === ">" &&
+ selector.length >= 3 ) {
+
+ // Assume that strings that start and end with <> are HTML and skip the regex check
+ match = [ null, selector, null ];
+
+ } else {
+ match = rquickExpr.exec( selector );
+ }
+
+ // Match html or make sure no context is specified for #id
+ if ( match && ( match[ 1 ] || !context ) ) {
+
+ // HANDLE: $(html) -> $(array)
+ if ( match[ 1 ] ) {
+ context = context instanceof jQuery ? context[ 0 ] : context;
+
+ // Option to run scripts is true for back-compat
+ // Intentionally let the error be thrown if parseHTML is not present
+ jQuery.merge( this, jQuery.parseHTML(
+ match[ 1 ],
+ context && context.nodeType ? context.ownerDocument || context : document,
+ true
+ ) );
+
+ // HANDLE: $(html, props)
+ if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) {
+ for ( match in context ) {
+
+ // Properties of context are called as methods if possible
+ if ( isFunction( this[ match ] ) ) {
+ this[ match ]( context[ match ] );
+
+ // ...and otherwise set as attributes
+ } else {
+ this.attr( match, context[ match ] );
+ }
+ }
+ }
+
+ return this;
+
+ // HANDLE: $(#id)
+ } else {
+ elem = document.getElementById( match[ 2 ] );
+
+ if ( elem ) {
+
+ // Inject the element directly into the jQuery object
+ this[ 0 ] = elem;
+ this.length = 1;
+ }
+ return this;
+ }
+
+ // HANDLE: $(expr, $(...))
+ } else if ( !context || context.jquery ) {
+ return ( context || root ).find( selector );
+
+ // HANDLE: $(expr, context)
+ // (which is just equivalent to: $(context).find(expr)
+ } else {
+ return this.constructor( context ).find( selector );
+ }
+
+ // HANDLE: $(DOMElement)
+ } else if ( selector.nodeType ) {
+ this[ 0 ] = selector;
+ this.length = 1;
+ return this;
+
+ // HANDLE: $(function)
+ // Shortcut for document ready
+ } else if ( isFunction( selector ) ) {
+ return root.ready !== undefined ?
+ root.ready( selector ) :
+
+ // Execute immediately if ready is not present
+ selector( jQuery );
+ }
+
+ return jQuery.makeArray( selector, this );
+ };
+
+// Give the init function the jQuery prototype for later instantiation
+init.prototype = jQuery.fn;
+
+// Initialize central reference
+rootjQuery = jQuery( document );
+
+
+var rparentsprev = /^(?:parents|prev(?:Until|All))/,
+
+ // Methods guaranteed to produce a unique set when starting from a unique set
+ guaranteedUnique = {
+ children: true,
+ contents: true,
+ next: true,
+ prev: true
+ };
+
+jQuery.fn.extend( {
+ has: function( target ) {
+ var targets = jQuery( target, this ),
+ l = targets.length;
+
+ return this.filter( function() {
+ var i = 0;
+ for ( ; i < l; i++ ) {
+ if ( jQuery.contains( this, targets[ i ] ) ) {
+ return true;
+ }
+ }
+ } );
+ },
+
+ closest: function( selectors, context ) {
+ var cur,
+ i = 0,
+ l = this.length,
+ matched = [],
+ targets = typeof selectors !== "string" && jQuery( selectors );
+
+ // Positional selectors never match, since there's no _selection_ context
+ if ( !rneedsContext.test( selectors ) ) {
+ for ( ; i < l; i++ ) {
+ for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) {
+
+ // Always skip document fragments
+ if ( cur.nodeType < 11 && ( targets ?
+ targets.index( cur ) > -1 :
+
+ // Don't pass non-elements to Sizzle
+ cur.nodeType === 1 &&
+ jQuery.find.matchesSelector( cur, selectors ) ) ) {
+
+ matched.push( cur );
+ break;
+ }
+ }
+ }
+ }
+
+ return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched );
+ },
+
+ // Determine the position of an element within the set
+ index: function( elem ) {
+
+ // No argument, return index in parent
+ if ( !elem ) {
+ return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1;
+ }
+
+ // Index in selector
+ if ( typeof elem === "string" ) {
+ return indexOf.call( jQuery( elem ), this[ 0 ] );
+ }
+
+ // Locate the position of the desired element
+ return indexOf.call( this,
+
+ // If it receives a jQuery object, the first element is used
+ elem.jquery ? elem[ 0 ] : elem
+ );
+ },
+
+ add: function( selector, context ) {
+ return this.pushStack(
+ jQuery.uniqueSort(
+ jQuery.merge( this.get(), jQuery( selector, context ) )
+ )
+ );
+ },
+
+ addBack: function( selector ) {
+ return this.add( selector == null ?
+ this.prevObject : this.prevObject.filter( selector )
+ );
+ }
+} );
+
+function sibling( cur, dir ) {
+ while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {}
+ return cur;
+}
+
+jQuery.each( {
+ parent: function( elem ) {
+ var parent = elem.parentNode;
+ return parent && parent.nodeType !== 11 ? parent : null;
+ },
+ parents: function( elem ) {
+ return dir( elem, "parentNode" );
+ },
+ parentsUntil: function( elem, i, until ) {
+ return dir( elem, "parentNode", until );
+ },
+ next: function( elem ) {
+ return sibling( elem, "nextSibling" );
+ },
+ prev: function( elem ) {
+ return sibling( elem, "previousSibling" );
+ },
+ nextAll: function( elem ) {
+ return dir( elem, "nextSibling" );
+ },
+ prevAll: function( elem ) {
+ return dir( elem, "previousSibling" );
+ },
+ nextUntil: function( elem, i, until ) {
+ return dir( elem, "nextSibling", until );
+ },
+ prevUntil: function( elem, i, until ) {
+ return dir( elem, "previousSibling", until );
+ },
+ siblings: function( elem ) {
+ return siblings( ( elem.parentNode || {} ).firstChild, elem );
+ },
+ children: function( elem ) {
+ return siblings( elem.firstChild );
+ },
+ contents: function( elem ) {
+ if ( typeof elem.contentDocument !== "undefined" ) {
+ return elem.contentDocument;
+ }
+
+ // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only
+ // Treat the template element as a regular one in browsers that
+ // don't support it.
+ if ( nodeName( elem, "template" ) ) {
+ elem = elem.content || elem;
+ }
+
+ return jQuery.merge( [], elem.childNodes );
+ }
+}, function( name, fn ) {
+ jQuery.fn[ name ] = function( until, selector ) {
+ var matched = jQuery.map( this, fn, until );
+
+ if ( name.slice( -5 ) !== "Until" ) {
+ selector = until;
+ }
+
+ if ( selector && typeof selector === "string" ) {
+ matched = jQuery.filter( selector, matched );
+ }
+
+ if ( this.length > 1 ) {
+
+ // Remove duplicates
+ if ( !guaranteedUnique[ name ] ) {
+ jQuery.uniqueSort( matched );
+ }
+
+ // Reverse order for parents* and prev-derivatives
+ if ( rparentsprev.test( name ) ) {
+ matched.reverse();
+ }
+ }
+
+ return this.pushStack( matched );
+ };
+} );
+var rnothtmlwhite = ( /[^\x20\t\r\n\f]+/g );
+
+
+
+// Convert String-formatted options into Object-formatted ones
+function createOptions( options ) {
+ var object = {};
+ jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) {
+ object[ flag ] = true;
+ } );
+ return object;
+}
+
+/*
+ * Create a callback list using the following parameters:
+ *
+ * options: an optional list of space-separated options that will change how
+ * the callback list behaves or a more traditional option object
+ *
+ * By default a callback list will act like an event callback list and can be
+ * "fired" multiple times.
+ *
+ * Possible options:
+ *
+ * once: will ensure the callback list can only be fired once (like a Deferred)
+ *
+ * memory: will keep track of previous values and will call any callback added
+ * after the list has been fired right away with the latest "memorized"
+ * values (like a Deferred)
+ *
+ * unique: will ensure a callback can only be added once (no duplicate in the list)
+ *
+ * stopOnFalse: interrupt callings when a callback returns false
+ *
+ */
+jQuery.Callbacks = function( options ) {
+
+ // Convert options from String-formatted to Object-formatted if needed
+ // (we check in cache first)
+ options = typeof options === "string" ?
+ createOptions( options ) :
+ jQuery.extend( {}, options );
+
+ var // Flag to know if list is currently firing
+ firing,
+
+ // Last fire value for non-forgettable lists
+ memory,
+
+ // Flag to know if list was already fired
+ fired,
+
+ // Flag to prevent firing
+ locked,
+
+ // Actual callback list
+ list = [],
+
+ // Queue of execution data for repeatable lists
+ queue = [],
+
+ // Index of currently firing callback (modified by add/remove as needed)
+ firingIndex = -1,
+
+ // Fire callbacks
+ fire = function() {
+
+ // Enforce single-firing
+ locked = locked || options.once;
+
+ // Execute callbacks for all pending executions,
+ // respecting firingIndex overrides and runtime changes
+ fired = firing = true;
+ for ( ; queue.length; firingIndex = -1 ) {
+ memory = queue.shift();
+ while ( ++firingIndex < list.length ) {
+
+ // Run callback and check for early termination
+ if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false &&
+ options.stopOnFalse ) {
+
+ // Jump to end and forget the data so .add doesn't re-fire
+ firingIndex = list.length;
+ memory = false;
+ }
+ }
+ }
+
+ // Forget the data if we're done with it
+ if ( !options.memory ) {
+ memory = false;
+ }
+
+ firing = false;
+
+ // Clean up if we're done firing for good
+ if ( locked ) {
+
+ // Keep an empty list if we have data for future add calls
+ if ( memory ) {
+ list = [];
+
+ // Otherwise, this object is spent
+ } else {
+ list = "";
+ }
+ }
+ },
+
+ // Actual Callbacks object
+ self = {
+
+ // Add a callback or a collection of callbacks to the list
+ add: function() {
+ if ( list ) {
+
+ // If we have memory from a past run, we should fire after adding
+ if ( memory && !firing ) {
+ firingIndex = list.length - 1;
+ queue.push( memory );
+ }
+
+ ( function add( args ) {
+ jQuery.each( args, function( _, arg ) {
+ if ( isFunction( arg ) ) {
+ if ( !options.unique || !self.has( arg ) ) {
+ list.push( arg );
+ }
+ } else if ( arg && arg.length && toType( arg ) !== "string" ) {
+
+ // Inspect recursively
+ add( arg );
+ }
+ } );
+ } )( arguments );
+
+ if ( memory && !firing ) {
+ fire();
+ }
+ }
+ return this;
+ },
+
+ // Remove a callback from the list
+ remove: function() {
+ jQuery.each( arguments, function( _, arg ) {
+ var index;
+ while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
+ list.splice( index, 1 );
+
+ // Handle firing indexes
+ if ( index <= firingIndex ) {
+ firingIndex--;
+ }
+ }
+ } );
+ return this;
+ },
+
+ // Check if a given callback is in the list.
+ // If no argument is given, return whether or not list has callbacks attached.
+ has: function( fn ) {
+ return fn ?
+ jQuery.inArray( fn, list ) > -1 :
+ list.length > 0;
+ },
+
+ // Remove all callbacks from the list
+ empty: function() {
+ if ( list ) {
+ list = [];
+ }
+ return this;
+ },
+
+ // Disable .fire and .add
+ // Abort any current/pending executions
+ // Clear all callbacks and values
+ disable: function() {
+ locked = queue = [];
+ list = memory = "";
+ return this;
+ },
+ disabled: function() {
+ return !list;
+ },
+
+ // Disable .fire
+ // Also disable .add unless we have memory (since it would have no effect)
+ // Abort any pending executions
+ lock: function() {
+ locked = queue = [];
+ if ( !memory && !firing ) {
+ list = memory = "";
+ }
+ return this;
+ },
+ locked: function() {
+ return !!locked;
+ },
+
+ // Call all callbacks with the given context and arguments
+ fireWith: function( context, args ) {
+ if ( !locked ) {
+ args = args || [];
+ args = [ context, args.slice ? args.slice() : args ];
+ queue.push( args );
+ if ( !firing ) {
+ fire();
+ }
+ }
+ return this;
+ },
+
+ // Call all the callbacks with the given arguments
+ fire: function() {
+ self.fireWith( this, arguments );
+ return this;
+ },
+
+ // To know if the callbacks have already been called at least once
+ fired: function() {
+ return !!fired;
+ }
+ };
+
+ return self;
+};
+
+
+function Identity( v ) {
+ return v;
+}
+function Thrower( ex ) {
+ throw ex;
+}
+
+function adoptValue( value, resolve, reject, noValue ) {
+ var method;
+
+ try {
+
+ // Check for promise aspect first to privilege synchronous behavior
+ if ( value && isFunction( ( method = value.promise ) ) ) {
+ method.call( value ).done( resolve ).fail( reject );
+
+ // Other thenables
+ } else if ( value && isFunction( ( method = value.then ) ) ) {
+ method.call( value, resolve, reject );
+
+ // Other non-thenables
+ } else {
+
+ // Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer:
+ // * false: [ value ].slice( 0 ) => resolve( value )
+ // * true: [ value ].slice( 1 ) => resolve()
+ resolve.apply( undefined, [ value ].slice( noValue ) );
+ }
+
+ // For Promises/A+, convert exceptions into rejections
+ // Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in
+ // Deferred#then to conditionally suppress rejection.
+ } catch ( value ) {
+
+ // Support: Android 4.0 only
+ // Strict mode functions invoked without .call/.apply get global-object context
+ reject.apply( undefined, [ value ] );
+ }
+}
+
+jQuery.extend( {
+
+ Deferred: function( func ) {
+ var tuples = [
+
+ // action, add listener, callbacks,
+ // ... .then handlers, argument index, [final state]
+ [ "notify", "progress", jQuery.Callbacks( "memory" ),
+ jQuery.Callbacks( "memory" ), 2 ],
+ [ "resolve", "done", jQuery.Callbacks( "once memory" ),
+ jQuery.Callbacks( "once memory" ), 0, "resolved" ],
+ [ "reject", "fail", jQuery.Callbacks( "once memory" ),
+ jQuery.Callbacks( "once memory" ), 1, "rejected" ]
+ ],
+ state = "pending",
+ promise = {
+ state: function() {
+ return state;
+ },
+ always: function() {
+ deferred.done( arguments ).fail( arguments );
+ return this;
+ },
+ "catch": function( fn ) {
+ return promise.then( null, fn );
+ },
+
+ // Keep pipe for back-compat
+ pipe: function( /* fnDone, fnFail, fnProgress */ ) {
+ var fns = arguments;
+
+ return jQuery.Deferred( function( newDefer ) {
+ jQuery.each( tuples, function( i, tuple ) {
+
+ // Map tuples (progress, done, fail) to arguments (done, fail, progress)
+ var fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ];
+
+ // deferred.progress(function() { bind to newDefer or newDefer.notify })
+ // deferred.done(function() { bind to newDefer or newDefer.resolve })
+ // deferred.fail(function() { bind to newDefer or newDefer.reject })
+ deferred[ tuple[ 1 ] ]( function() {
+ var returned = fn && fn.apply( this, arguments );
+ if ( returned && isFunction( returned.promise ) ) {
+ returned.promise()
+ .progress( newDefer.notify )
+ .done( newDefer.resolve )
+ .fail( newDefer.reject );
+ } else {
+ newDefer[ tuple[ 0 ] + "With" ](
+ this,
+ fn ? [ returned ] : arguments
+ );
+ }
+ } );
+ } );
+ fns = null;
+ } ).promise();
+ },
+ then: function( onFulfilled, onRejected, onProgress ) {
+ var maxDepth = 0;
+ function resolve( depth, deferred, handler, special ) {
+ return function() {
+ var that = this,
+ args = arguments,
+ mightThrow = function() {
+ var returned, then;
+
+ // Support: Promises/A+ section 2.3.3.3.3
+ // https://promisesaplus.com/#point-59
+ // Ignore double-resolution attempts
+ if ( depth < maxDepth ) {
+ return;
+ }
+
+ returned = handler.apply( that, args );
+
+ // Support: Promises/A+ section 2.3.1
+ // https://promisesaplus.com/#point-48
+ if ( returned === deferred.promise() ) {
+ throw new TypeError( "Thenable self-resolution" );
+ }
+
+ // Support: Promises/A+ sections 2.3.3.1, 3.5
+ // https://promisesaplus.com/#point-54
+ // https://promisesaplus.com/#point-75
+ // Retrieve `then` only once
+ then = returned &&
+
+ // Support: Promises/A+ section 2.3.4
+ // https://promisesaplus.com/#point-64
+ // Only check objects and functions for thenability
+ ( typeof returned === "object" ||
+ typeof returned === "function" ) &&
+ returned.then;
+
+ // Handle a returned thenable
+ if ( isFunction( then ) ) {
+
+ // Special processors (notify) just wait for resolution
+ if ( special ) {
+ then.call(
+ returned,
+ resolve( maxDepth, deferred, Identity, special ),
+ resolve( maxDepth, deferred, Thrower, special )
+ );
+
+ // Normal processors (resolve) also hook into progress
+ } else {
+
+ // ...and disregard older resolution values
+ maxDepth++;
+
+ then.call(
+ returned,
+ resolve( maxDepth, deferred, Identity, special ),
+ resolve( maxDepth, deferred, Thrower, special ),
+ resolve( maxDepth, deferred, Identity,
+ deferred.notifyWith )
+ );
+ }
+
+ // Handle all other returned values
+ } else {
+
+ // Only substitute handlers pass on context
+ // and multiple values (non-spec behavior)
+ if ( handler !== Identity ) {
+ that = undefined;
+ args = [ returned ];
+ }
+
+ // Process the value(s)
+ // Default process is resolve
+ ( special || deferred.resolveWith )( that, args );
+ }
+ },
+
+ // Only normal processors (resolve) catch and reject exceptions
+ process = special ?
+ mightThrow :
+ function() {
+ try {
+ mightThrow();
+ } catch ( e ) {
+
+ if ( jQuery.Deferred.exceptionHook ) {
+ jQuery.Deferred.exceptionHook( e,
+ process.stackTrace );
+ }
+
+ // Support: Promises/A+ section 2.3.3.3.4.1
+ // https://promisesaplus.com/#point-61
+ // Ignore post-resolution exceptions
+ if ( depth + 1 >= maxDepth ) {
+
+ // Only substitute handlers pass on context
+ // and multiple values (non-spec behavior)
+ if ( handler !== Thrower ) {
+ that = undefined;
+ args = [ e ];
+ }
+
+ deferred.rejectWith( that, args );
+ }
+ }
+ };
+
+ // Support: Promises/A+ section 2.3.3.3.1
+ // https://promisesaplus.com/#point-57
+ // Re-resolve promises immediately to dodge false rejection from
+ // subsequent errors
+ if ( depth ) {
+ process();
+ } else {
+
+ // Call an optional hook to record the stack, in case of exception
+ // since it's otherwise lost when execution goes async
+ if ( jQuery.Deferred.getStackHook ) {
+ process.stackTrace = jQuery.Deferred.getStackHook();
+ }
+ window.setTimeout( process );
+ }
+ };
+ }
+
+ return jQuery.Deferred( function( newDefer ) {
+
+ // progress_handlers.add( ... )
+ tuples[ 0 ][ 3 ].add(
+ resolve(
+ 0,
+ newDefer,
+ isFunction( onProgress ) ?
+ onProgress :
+ Identity,
+ newDefer.notifyWith
+ )
+ );
+
+ // fulfilled_handlers.add( ... )
+ tuples[ 1 ][ 3 ].add(
+ resolve(
+ 0,
+ newDefer,
+ isFunction( onFulfilled ) ?
+ onFulfilled :
+ Identity
+ )
+ );
+
+ // rejected_handlers.add( ... )
+ tuples[ 2 ][ 3 ].add(
+ resolve(
+ 0,
+ newDefer,
+ isFunction( onRejected ) ?
+ onRejected :
+ Thrower
+ )
+ );
+ } ).promise();
+ },
+
+ // Get a promise for this deferred
+ // If obj is provided, the promise aspect is added to the object
+ promise: function( obj ) {
+ return obj != null ? jQuery.extend( obj, promise ) : promise;
+ }
+ },
+ deferred = {};
+
+ // Add list-specific methods
+ jQuery.each( tuples, function( i, tuple ) {
+ var list = tuple[ 2 ],
+ stateString = tuple[ 5 ];
+
+ // promise.progress = list.add
+ // promise.done = list.add
+ // promise.fail = list.add
+ promise[ tuple[ 1 ] ] = list.add;
+
+ // Handle state
+ if ( stateString ) {
+ list.add(
+ function() {
+
+ // state = "resolved" (i.e., fulfilled)
+ // state = "rejected"
+ state = stateString;
+ },
+
+ // rejected_callbacks.disable
+ // fulfilled_callbacks.disable
+ tuples[ 3 - i ][ 2 ].disable,
+
+ // rejected_handlers.disable
+ // fulfilled_handlers.disable
+ tuples[ 3 - i ][ 3 ].disable,
+
+ // progress_callbacks.lock
+ tuples[ 0 ][ 2 ].lock,
+
+ // progress_handlers.lock
+ tuples[ 0 ][ 3 ].lock
+ );
+ }
+
+ // progress_handlers.fire
+ // fulfilled_handlers.fire
+ // rejected_handlers.fire
+ list.add( tuple[ 3 ].fire );
+
+ // deferred.notify = function() { deferred.notifyWith(...) }
+ // deferred.resolve = function() { deferred.resolveWith(...) }
+ // deferred.reject = function() { deferred.rejectWith(...) }
+ deferred[ tuple[ 0 ] ] = function() {
+ deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments );
+ return this;
+ };
+
+ // deferred.notifyWith = list.fireWith
+ // deferred.resolveWith = list.fireWith
+ // deferred.rejectWith = list.fireWith
+ deferred[ tuple[ 0 ] + "With" ] = list.fireWith;
+ } );
+
+ // Make the deferred a promise
+ promise.promise( deferred );
+
+ // Call given func if any
+ if ( func ) {
+ func.call( deferred, deferred );
+ }
+
+ // All done!
+ return deferred;
+ },
+
+ // Deferred helper
+ when: function( singleValue ) {
+ var
+
+ // count of uncompleted subordinates
+ remaining = arguments.length,
+
+ // count of unprocessed arguments
+ i = remaining,
+
+ // subordinate fulfillment data
+ resolveContexts = Array( i ),
+ resolveValues = slice.call( arguments ),
+
+ // the master Deferred
+ master = jQuery.Deferred(),
+
+ // subordinate callback factory
+ updateFunc = function( i ) {
+ return function( value ) {
+ resolveContexts[ i ] = this;
+ resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;
+ if ( !( --remaining ) ) {
+ master.resolveWith( resolveContexts, resolveValues );
+ }
+ };
+ };
+
+ // Single- and empty arguments are adopted like Promise.resolve
+ if ( remaining <= 1 ) {
+ adoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject,
+ !remaining );
+
+ // Use .then() to unwrap secondary thenables (cf. gh-3000)
+ if ( master.state() === "pending" ||
+ isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) {
+
+ return master.then();
+ }
+ }
+
+ // Multiple arguments are aggregated like Promise.all array elements
+ while ( i-- ) {
+ adoptValue( resolveValues[ i ], updateFunc( i ), master.reject );
+ }
+
+ return master.promise();
+ }
+} );
+
+
+// These usually indicate a programmer mistake during development,
+// warn about them ASAP rather than swallowing them by default.
+var rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;
+
+jQuery.Deferred.exceptionHook = function( error, stack ) {
+
+ // Support: IE 8 - 9 only
+ // Console exists when dev tools are open, which can happen at any time
+ if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) {
+ window.console.warn( "jQuery.Deferred exception: " + error.message, error.stack, stack );
+ }
+};
+
+
+
+
+jQuery.readyException = function( error ) {
+ window.setTimeout( function() {
+ throw error;
+ } );
+};
+
+
+
+
+// The deferred used on DOM ready
+var readyList = jQuery.Deferred();
+
+jQuery.fn.ready = function( fn ) {
+
+ readyList
+ .then( fn )
+
+ // Wrap jQuery.readyException in a function so that the lookup
+ // happens at the time of error handling instead of callback
+ // registration.
+ .catch( function( error ) {
+ jQuery.readyException( error );
+ } );
+
+ return this;
+};
+
+jQuery.extend( {
+
+ // Is the DOM ready to be used? Set to true once it occurs.
+ isReady: false,
+
+ // A counter to track how many items to wait for before
+ // the ready event fires. See #6781
+ readyWait: 1,
+
+ // Handle when the DOM is ready
+ ready: function( wait ) {
+
+ // Abort if there are pending holds or we're already ready
+ if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {
+ return;
+ }
+
+ // Remember that the DOM is ready
+ jQuery.isReady = true;
+
+ // If a normal DOM Ready event fired, decrement, and wait if need be
+ if ( wait !== true && --jQuery.readyWait > 0 ) {
+ return;
+ }
+
+ // If there are functions bound, to execute
+ readyList.resolveWith( document, [ jQuery ] );
+ }
+} );
+
+jQuery.ready.then = readyList.then;
+
+// The ready event handler and self cleanup method
+function completed() {
+ document.removeEventListener( "DOMContentLoaded", completed );
+ window.removeEventListener( "load", completed );
+ jQuery.ready();
+}
+
+// Catch cases where $(document).ready() is called
+// after the browser event has already occurred.
+// Support: IE <=9 - 10 only
+// Older IE sometimes signals "interactive" too soon
+if ( document.readyState === "complete" ||
+ ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) {
+
+ // Handle it asynchronously to allow scripts the opportunity to delay ready
+ window.setTimeout( jQuery.ready );
+
+} else {
+
+ // Use the handy event callback
+ document.addEventListener( "DOMContentLoaded", completed );
+
+ // A fallback to window.onload, that will always work
+ window.addEventListener( "load", completed );
+}
+
+
+
+
+// Multifunctional method to get and set values of a collection
+// The value/s can optionally be executed if it's a function
+var access = function( elems, fn, key, value, chainable, emptyGet, raw ) {
+ var i = 0,
+ len = elems.length,
+ bulk = key == null;
+
+ // Sets many values
+ if ( toType( key ) === "object" ) {
+ chainable = true;
+ for ( i in key ) {
+ access( elems, fn, i, key[ i ], true, emptyGet, raw );
+ }
+
+ // Sets one value
+ } else if ( value !== undefined ) {
+ chainable = true;
+
+ if ( !isFunction( value ) ) {
+ raw = true;
+ }
+
+ if ( bulk ) {
+
+ // Bulk operations run against the entire set
+ if ( raw ) {
+ fn.call( elems, value );
+ fn = null;
+
+ // ...except when executing function values
+ } else {
+ bulk = fn;
+ fn = function( elem, key, value ) {
+ return bulk.call( jQuery( elem ), value );
+ };
+ }
+ }
+
+ if ( fn ) {
+ for ( ; i < len; i++ ) {
+ fn(
+ elems[ i ], key, raw ?
+ value :
+ value.call( elems[ i ], i, fn( elems[ i ], key ) )
+ );
+ }
+ }
+ }
+
+ if ( chainable ) {
+ return elems;
+ }
+
+ // Gets
+ if ( bulk ) {
+ return fn.call( elems );
+ }
+
+ return len ? fn( elems[ 0 ], key ) : emptyGet;
+};
+
+
+// Matches dashed string for camelizing
+var rmsPrefix = /^-ms-/,
+ rdashAlpha = /-([a-z])/g;
+
+// Used by camelCase as callback to replace()
+function fcamelCase( all, letter ) {
+ return letter.toUpperCase();
+}
+
+// Convert dashed to camelCase; used by the css and data modules
+// Support: IE <=9 - 11, Edge 12 - 15
+// Microsoft forgot to hump their vendor prefix (#9572)
+function camelCase( string ) {
+ return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
+}
+var acceptData = function( owner ) {
+
+ // Accepts only:
+ // - Node
+ // - Node.ELEMENT_NODE
+ // - Node.DOCUMENT_NODE
+ // - Object
+ // - Any
+ return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType );
+};
+
+
+
+
+function Data() {
+ this.expando = jQuery.expando + Data.uid++;
+}
+
+Data.uid = 1;
+
+Data.prototype = {
+
+ cache: function( owner ) {
+
+ // Check if the owner object already has a cache
+ var value = owner[ this.expando ];
+
+ // If not, create one
+ if ( !value ) {
+ value = {};
+
+ // We can accept data for non-element nodes in modern browsers,
+ // but we should not, see #8335.
+ // Always return an empty object.
+ if ( acceptData( owner ) ) {
+
+ // If it is a node unlikely to be stringify-ed or looped over
+ // use plain assignment
+ if ( owner.nodeType ) {
+ owner[ this.expando ] = value;
+
+ // Otherwise secure it in a non-enumerable property
+ // configurable must be true to allow the property to be
+ // deleted when data is removed
+ } else {
+ Object.defineProperty( owner, this.expando, {
+ value: value,
+ configurable: true
+ } );
+ }
+ }
+ }
+
+ return value;
+ },
+ set: function( owner, data, value ) {
+ var prop,
+ cache = this.cache( owner );
+
+ // Handle: [ owner, key, value ] args
+ // Always use camelCase key (gh-2257)
+ if ( typeof data === "string" ) {
+ cache[ camelCase( data ) ] = value;
+
+ // Handle: [ owner, { properties } ] args
+ } else {
+
+ // Copy the properties one-by-one to the cache object
+ for ( prop in data ) {
+ cache[ camelCase( prop ) ] = data[ prop ];
+ }
+ }
+ return cache;
+ },
+ get: function( owner, key ) {
+ return key === undefined ?
+ this.cache( owner ) :
+
+ // Always use camelCase key (gh-2257)
+ owner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ];
+ },
+ access: function( owner, key, value ) {
+
+ // In cases where either:
+ //
+ // 1. No key was specified
+ // 2. A string key was specified, but no value provided
+ //
+ // Take the "read" path and allow the get method to determine
+ // which value to return, respectively either:
+ //
+ // 1. The entire cache object
+ // 2. The data stored at the key
+ //
+ if ( key === undefined ||
+ ( ( key && typeof key === "string" ) && value === undefined ) ) {
+
+ return this.get( owner, key );
+ }
+
+ // When the key is not a string, or both a key and value
+ // are specified, set or extend (existing objects) with either:
+ //
+ // 1. An object of properties
+ // 2. A key and value
+ //
+ this.set( owner, key, value );
+
+ // Since the "set" path can have two possible entry points
+ // return the expected data based on which path was taken[*]
+ return value !== undefined ? value : key;
+ },
+ remove: function( owner, key ) {
+ var i,
+ cache = owner[ this.expando ];
+
+ if ( cache === undefined ) {
+ return;
+ }
+
+ if ( key !== undefined ) {
+
+ // Support array or space separated string of keys
+ if ( Array.isArray( key ) ) {
+
+ // If key is an array of keys...
+ // We always set camelCase keys, so remove that.
+ key = key.map( camelCase );
+ } else {
+ key = camelCase( key );
+
+ // If a key with the spaces exists, use it.
+ // Otherwise, create an array by matching non-whitespace
+ key = key in cache ?
+ [ key ] :
+ ( key.match( rnothtmlwhite ) || [] );
+ }
+
+ i = key.length;
+
+ while ( i-- ) {
+ delete cache[ key[ i ] ];
+ }
+ }
+
+ // Remove the expando if there's no more data
+ if ( key === undefined || jQuery.isEmptyObject( cache ) ) {
+
+ // Support: Chrome <=35 - 45
+ // Webkit & Blink performance suffers when deleting properties
+ // from DOM nodes, so set to undefined instead
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted)
+ if ( owner.nodeType ) {
+ owner[ this.expando ] = undefined;
+ } else {
+ delete owner[ this.expando ];
+ }
+ }
+ },
+ hasData: function( owner ) {
+ var cache = owner[ this.expando ];
+ return cache !== undefined && !jQuery.isEmptyObject( cache );
+ }
+};
+var dataPriv = new Data();
+
+var dataUser = new Data();
+
+
+
+// Implementation Summary
+//
+// 1. Enforce API surface and semantic compatibility with 1.9.x branch
+// 2. Improve the module's maintainability by reducing the storage
+// paths to a single mechanism.
+// 3. Use the same single mechanism to support "private" and "user" data.
+// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData)
+// 5. Avoid exposing implementation details on user objects (eg. expando properties)
+// 6. Provide a clear path for implementation upgrade to WeakMap in 2014
+
+var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,
+ rmultiDash = /[A-Z]/g;
+
+function getData( data ) {
+ if ( data === "true" ) {
+ return true;
+ }
+
+ if ( data === "false" ) {
+ return false;
+ }
+
+ if ( data === "null" ) {
+ return null;
+ }
+
+ // Only convert to a number if it doesn't change the string
+ if ( data === +data + "" ) {
+ return +data;
+ }
+
+ if ( rbrace.test( data ) ) {
+ return JSON.parse( data );
+ }
+
+ return data;
+}
+
+function dataAttr( elem, key, data ) {
+ var name;
+
+ // If nothing was found internally, try to fetch any
+ // data from the HTML5 data-* attribute
+ if ( data === undefined && elem.nodeType === 1 ) {
+ name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase();
+ data = elem.getAttribute( name );
+
+ if ( typeof data === "string" ) {
+ try {
+ data = getData( data );
+ } catch ( e ) {}
+
+ // Make sure we set the data so it isn't changed later
+ dataUser.set( elem, key, data );
+ } else {
+ data = undefined;
+ }
+ }
+ return data;
+}
+
+jQuery.extend( {
+ hasData: function( elem ) {
+ return dataUser.hasData( elem ) || dataPriv.hasData( elem );
+ },
+
+ data: function( elem, name, data ) {
+ return dataUser.access( elem, name, data );
+ },
+
+ removeData: function( elem, name ) {
+ dataUser.remove( elem, name );
+ },
+
+ // TODO: Now that all calls to _data and _removeData have been replaced
+ // with direct calls to dataPriv methods, these can be deprecated.
+ _data: function( elem, name, data ) {
+ return dataPriv.access( elem, name, data );
+ },
+
+ _removeData: function( elem, name ) {
+ dataPriv.remove( elem, name );
+ }
+} );
+
+jQuery.fn.extend( {
+ data: function( key, value ) {
+ var i, name, data,
+ elem = this[ 0 ],
+ attrs = elem && elem.attributes;
+
+ // Gets all values
+ if ( key === undefined ) {
+ if ( this.length ) {
+ data = dataUser.get( elem );
+
+ if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) {
+ i = attrs.length;
+ while ( i-- ) {
+
+ // Support: IE 11 only
+ // The attrs elements can be null (#14894)
+ if ( attrs[ i ] ) {
+ name = attrs[ i ].name;
+ if ( name.indexOf( "data-" ) === 0 ) {
+ name = camelCase( name.slice( 5 ) );
+ dataAttr( elem, name, data[ name ] );
+ }
+ }
+ }
+ dataPriv.set( elem, "hasDataAttrs", true );
+ }
+ }
+
+ return data;
+ }
+
+ // Sets multiple values
+ if ( typeof key === "object" ) {
+ return this.each( function() {
+ dataUser.set( this, key );
+ } );
+ }
+
+ return access( this, function( value ) {
+ var data;
+
+ // The calling jQuery object (element matches) is not empty
+ // (and therefore has an element appears at this[ 0 ]) and the
+ // `value` parameter was not undefined. An empty jQuery object
+ // will result in `undefined` for elem = this[ 0 ] which will
+ // throw an exception if an attempt to read a data cache is made.
+ if ( elem && value === undefined ) {
+
+ // Attempt to get data from the cache
+ // The key will always be camelCased in Data
+ data = dataUser.get( elem, key );
+ if ( data !== undefined ) {
+ return data;
+ }
+
+ // Attempt to "discover" the data in
+ // HTML5 custom data-* attrs
+ data = dataAttr( elem, key );
+ if ( data !== undefined ) {
+ return data;
+ }
+
+ // We tried really hard, but the data doesn't exist.
+ return;
+ }
+
+ // Set the data...
+ this.each( function() {
+
+ // We always store the camelCased key
+ dataUser.set( this, key, value );
+ } );
+ }, null, value, arguments.length > 1, null, true );
+ },
+
+ removeData: function( key ) {
+ return this.each( function() {
+ dataUser.remove( this, key );
+ } );
+ }
+} );
+
+
+jQuery.extend( {
+ queue: function( elem, type, data ) {
+ var queue;
+
+ if ( elem ) {
+ type = ( type || "fx" ) + "queue";
+ queue = dataPriv.get( elem, type );
+
+ // Speed up dequeue by getting out quickly if this is just a lookup
+ if ( data ) {
+ if ( !queue || Array.isArray( data ) ) {
+ queue = dataPriv.access( elem, type, jQuery.makeArray( data ) );
+ } else {
+ queue.push( data );
+ }
+ }
+ return queue || [];
+ }
+ },
+
+ dequeue: function( elem, type ) {
+ type = type || "fx";
+
+ var queue = jQuery.queue( elem, type ),
+ startLength = queue.length,
+ fn = queue.shift(),
+ hooks = jQuery._queueHooks( elem, type ),
+ next = function() {
+ jQuery.dequeue( elem, type );
+ };
+
+ // If the fx queue is dequeued, always remove the progress sentinel
+ if ( fn === "inprogress" ) {
+ fn = queue.shift();
+ startLength--;
+ }
+
+ if ( fn ) {
+
+ // Add a progress sentinel to prevent the fx queue from being
+ // automatically dequeued
+ if ( type === "fx" ) {
+ queue.unshift( "inprogress" );
+ }
+
+ // Clear up the last queue stop function
+ delete hooks.stop;
+ fn.call( elem, next, hooks );
+ }
+
+ if ( !startLength && hooks ) {
+ hooks.empty.fire();
+ }
+ },
+
+ // Not public - generate a queueHooks object, or return the current one
+ _queueHooks: function( elem, type ) {
+ var key = type + "queueHooks";
+ return dataPriv.get( elem, key ) || dataPriv.access( elem, key, {
+ empty: jQuery.Callbacks( "once memory" ).add( function() {
+ dataPriv.remove( elem, [ type + "queue", key ] );
+ } )
+ } );
+ }
+} );
+
+jQuery.fn.extend( {
+ queue: function( type, data ) {
+ var setter = 2;
+
+ if ( typeof type !== "string" ) {
+ data = type;
+ type = "fx";
+ setter--;
+ }
+
+ if ( arguments.length < setter ) {
+ return jQuery.queue( this[ 0 ], type );
+ }
+
+ return data === undefined ?
+ this :
+ this.each( function() {
+ var queue = jQuery.queue( this, type, data );
+
+ // Ensure a hooks for this queue
+ jQuery._queueHooks( this, type );
+
+ if ( type === "fx" && queue[ 0 ] !== "inprogress" ) {
+ jQuery.dequeue( this, type );
+ }
+ } );
+ },
+ dequeue: function( type ) {
+ return this.each( function() {
+ jQuery.dequeue( this, type );
+ } );
+ },
+ clearQueue: function( type ) {
+ return this.queue( type || "fx", [] );
+ },
+
+ // Get a promise resolved when queues of a certain type
+ // are emptied (fx is the type by default)
+ promise: function( type, obj ) {
+ var tmp,
+ count = 1,
+ defer = jQuery.Deferred(),
+ elements = this,
+ i = this.length,
+ resolve = function() {
+ if ( !( --count ) ) {
+ defer.resolveWith( elements, [ elements ] );
+ }
+ };
+
+ if ( typeof type !== "string" ) {
+ obj = type;
+ type = undefined;
+ }
+ type = type || "fx";
+
+ while ( i-- ) {
+ tmp = dataPriv.get( elements[ i ], type + "queueHooks" );
+ if ( tmp && tmp.empty ) {
+ count++;
+ tmp.empty.add( resolve );
+ }
+ }
+ resolve();
+ return defer.promise( obj );
+ }
+} );
+var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source;
+
+var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" );
+
+
+var cssExpand = [ "Top", "Right", "Bottom", "Left" ];
+
+var documentElement = document.documentElement;
+
+
+
+ var isAttached = function( elem ) {
+ return jQuery.contains( elem.ownerDocument, elem );
+ },
+ composed = { composed: true };
+
+ // Support: IE 9 - 11+, Edge 12 - 18+, iOS 10.0 - 10.2 only
+ // Check attachment across shadow DOM boundaries when possible (gh-3504)
+ // Support: iOS 10.0-10.2 only
+ // Early iOS 10 versions support `attachShadow` but not `getRootNode`,
+ // leading to errors. We need to check for `getRootNode`.
+ if ( documentElement.getRootNode ) {
+ isAttached = function( elem ) {
+ return jQuery.contains( elem.ownerDocument, elem ) ||
+ elem.getRootNode( composed ) === elem.ownerDocument;
+ };
+ }
+var isHiddenWithinTree = function( elem, el ) {
+
+ // isHiddenWithinTree might be called from jQuery#filter function;
+ // in that case, element will be second argument
+ elem = el || elem;
+
+ // Inline style trumps all
+ return elem.style.display === "none" ||
+ elem.style.display === "" &&
+
+ // Otherwise, check computed style
+ // Support: Firefox <=43 - 45
+ // Disconnected elements can have computed display: none, so first confirm that elem is
+ // in the document.
+ isAttached( elem ) &&
+
+ jQuery.css( elem, "display" ) === "none";
+ };
+
+var swap = function( elem, options, callback, args ) {
+ var ret, name,
+ old = {};
+
+ // Remember the old values, and insert the new ones
+ for ( name in options ) {
+ old[ name ] = elem.style[ name ];
+ elem.style[ name ] = options[ name ];
+ }
+
+ ret = callback.apply( elem, args || [] );
+
+ // Revert the old values
+ for ( name in options ) {
+ elem.style[ name ] = old[ name ];
+ }
+
+ return ret;
+};
+
+
+
+
+function adjustCSS( elem, prop, valueParts, tween ) {
+ var adjusted, scale,
+ maxIterations = 20,
+ currentValue = tween ?
+ function() {
+ return tween.cur();
+ } :
+ function() {
+ return jQuery.css( elem, prop, "" );
+ },
+ initial = currentValue(),
+ unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ),
+
+ // Starting value computation is required for potential unit mismatches
+ initialInUnit = elem.nodeType &&
+ ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) &&
+ rcssNum.exec( jQuery.css( elem, prop ) );
+
+ if ( initialInUnit && initialInUnit[ 3 ] !== unit ) {
+
+ // Support: Firefox <=54
+ // Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144)
+ initial = initial / 2;
+
+ // Trust units reported by jQuery.css
+ unit = unit || initialInUnit[ 3 ];
+
+ // Iteratively approximate from a nonzero starting point
+ initialInUnit = +initial || 1;
+
+ while ( maxIterations-- ) {
+
+ // Evaluate and update our best guess (doubling guesses that zero out).
+ // Finish if the scale equals or crosses 1 (making the old*new product non-positive).
+ jQuery.style( elem, prop, initialInUnit + unit );
+ if ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) {
+ maxIterations = 0;
+ }
+ initialInUnit = initialInUnit / scale;
+
+ }
+
+ initialInUnit = initialInUnit * 2;
+ jQuery.style( elem, prop, initialInUnit + unit );
+
+ // Make sure we update the tween properties later on
+ valueParts = valueParts || [];
+ }
+
+ if ( valueParts ) {
+ initialInUnit = +initialInUnit || +initial || 0;
+
+ // Apply relative offset (+=/-=) if specified
+ adjusted = valueParts[ 1 ] ?
+ initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] :
+ +valueParts[ 2 ];
+ if ( tween ) {
+ tween.unit = unit;
+ tween.start = initialInUnit;
+ tween.end = adjusted;
+ }
+ }
+ return adjusted;
+}
+
+
+var defaultDisplayMap = {};
+
+function getDefaultDisplay( elem ) {
+ var temp,
+ doc = elem.ownerDocument,
+ nodeName = elem.nodeName,
+ display = defaultDisplayMap[ nodeName ];
+
+ if ( display ) {
+ return display;
+ }
+
+ temp = doc.body.appendChild( doc.createElement( nodeName ) );
+ display = jQuery.css( temp, "display" );
+
+ temp.parentNode.removeChild( temp );
+
+ if ( display === "none" ) {
+ display = "block";
+ }
+ defaultDisplayMap[ nodeName ] = display;
+
+ return display;
+}
+
+function showHide( elements, show ) {
+ var display, elem,
+ values = [],
+ index = 0,
+ length = elements.length;
+
+ // Determine new display value for elements that need to change
+ for ( ; index < length; index++ ) {
+ elem = elements[ index ];
+ if ( !elem.style ) {
+ continue;
+ }
+
+ display = elem.style.display;
+ if ( show ) {
+
+ // Since we force visibility upon cascade-hidden elements, an immediate (and slow)
+ // check is required in this first loop unless we have a nonempty display value (either
+ // inline or about-to-be-restored)
+ if ( display === "none" ) {
+ values[ index ] = dataPriv.get( elem, "display" ) || null;
+ if ( !values[ index ] ) {
+ elem.style.display = "";
+ }
+ }
+ if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) {
+ values[ index ] = getDefaultDisplay( elem );
+ }
+ } else {
+ if ( display !== "none" ) {
+ values[ index ] = "none";
+
+ // Remember what we're overwriting
+ dataPriv.set( elem, "display", display );
+ }
+ }
+ }
+
+ // Set the display of the elements in a second loop to avoid constant reflow
+ for ( index = 0; index < length; index++ ) {
+ if ( values[ index ] != null ) {
+ elements[ index ].style.display = values[ index ];
+ }
+ }
+
+ return elements;
+}
+
+jQuery.fn.extend( {
+ show: function() {
+ return showHide( this, true );
+ },
+ hide: function() {
+ return showHide( this );
+ },
+ toggle: function( state ) {
+ if ( typeof state === "boolean" ) {
+ return state ? this.show() : this.hide();
+ }
+
+ return this.each( function() {
+ if ( isHiddenWithinTree( this ) ) {
+ jQuery( this ).show();
+ } else {
+ jQuery( this ).hide();
+ }
+ } );
+ }
+} );
+var rcheckableType = ( /^(?:checkbox|radio)$/i );
+
+var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]*)/i );
+
+var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i );
+
+
+
+// We have to close these tags to support XHTML (#13200)
+var wrapMap = {
+
+ // Support: IE <=9 only
+ option: [ 1, "<select multiple='multiple'>", "</select>" ],
+
+ // XHTML parsers do not magically insert elements in the
+ // same way that tag soup parsers do. So we cannot shorten
+ // this by omitting <tbody> or other required elements.
+ thead: [ 1, "<table>", "</table>" ],
+ col: [ 2, "<table><colgroup>", "</colgroup></table>" ],
+ tr: [ 2, "<table><tbody>", "</tbody></table>" ],
+ td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
+
+ _default: [ 0, "", "" ]
+};
+
+// Support: IE <=9 only
+wrapMap.optgroup = wrapMap.option;
+
+wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
+wrapMap.th = wrapMap.td;
+
+
+function getAll( context, tag ) {
+
+ // Support: IE <=9 - 11 only
+ // Use typeof to avoid zero-argument method invocation on host objects (#15151)
+ var ret;
+
+ if ( typeof context.getElementsByTagName !== "undefined" ) {
+ ret = context.getElementsByTagName( tag || "*" );
+
+ } else if ( typeof context.querySelectorAll !== "undefined" ) {
+ ret = context.querySelectorAll( tag || "*" );
+
+ } else {
+ ret = [];
+ }
+
+ if ( tag === undefined || tag && nodeName( context, tag ) ) {
+ return jQuery.merge( [ context ], ret );
+ }
+
+ return ret;
+}
+
+
+// Mark scripts as having already been evaluated
+function setGlobalEval( elems, refElements ) {
+ var i = 0,
+ l = elems.length;
+
+ for ( ; i < l; i++ ) {
+ dataPriv.set(
+ elems[ i ],
+ "globalEval",
+ !refElements || dataPriv.get( refElements[ i ], "globalEval" )
+ );
+ }
+}
+
+
+var rhtml = /<|&#?\w+;/;
+
+function buildFragment( elems, context, scripts, selection, ignored ) {
+ var elem, tmp, tag, wrap, attached, j,
+ fragment = context.createDocumentFragment(),
+ nodes = [],
+ i = 0,
+ l = elems.length;
+
+ for ( ; i < l; i++ ) {
+ elem = elems[ i ];
+
+ if ( elem || elem === 0 ) {
+
+ // Add nodes directly
+ if ( toType( elem ) === "object" ) {
+
+ // Support: Android <=4.0 only, PhantomJS 1 only
+ // push.apply(_, arraylike) throws on ancient WebKit
+ jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );
+
+ // Convert non-html into a text node
+ } else if ( !rhtml.test( elem ) ) {
+ nodes.push( context.createTextNode( elem ) );
+
+ // Convert html into DOM nodes
+ } else {
+ tmp = tmp || fragment.appendChild( context.createElement( "div" ) );
+
+ // Deserialize a standard representation
+ tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase();
+ wrap = wrapMap[ tag ] || wrapMap._default;
+ tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ];
+
+ // Descend through wrappers to the right content
+ j = wrap[ 0 ];
+ while ( j-- ) {
+ tmp = tmp.lastChild;
+ }
+
+ // Support: Android <=4.0 only, PhantomJS 1 only
+ // push.apply(_, arraylike) throws on ancient WebKit
+ jQuery.merge( nodes, tmp.childNodes );
+
+ // Remember the top-level container
+ tmp = fragment.firstChild;
+
+ // Ensure the created nodes are orphaned (#12392)
+ tmp.textContent = "";
+ }
+ }
+ }
+
+ // Remove wrapper from fragment
+ fragment.textContent = "";
+
+ i = 0;
+ while ( ( elem = nodes[ i++ ] ) ) {
+
+ // Skip elements already in the context collection (trac-4087)
+ if ( selection && jQuery.inArray( elem, selection ) > -1 ) {
+ if ( ignored ) {
+ ignored.push( elem );
+ }
+ continue;
+ }
+
+ attached = isAttached( elem );
+
+ // Append to fragment
+ tmp = getAll( fragment.appendChild( elem ), "script" );
+
+ // Preserve script evaluation history
+ if ( attached ) {
+ setGlobalEval( tmp );
+ }
+
+ // Capture executables
+ if ( scripts ) {
+ j = 0;
+ while ( ( elem = tmp[ j++ ] ) ) {
+ if ( rscriptType.test( elem.type || "" ) ) {
+ scripts.push( elem );
+ }
+ }
+ }
+ }
+
+ return fragment;
+}
+
+
+( function() {
+ var fragment = document.createDocumentFragment(),
+ div = fragment.appendChild( document.createElement( "div" ) ),
+ input = document.createElement( "input" );
+
+ // Support: Android 4.0 - 4.3 only
+ // Check state lost if the name is set (#11217)
+ // Support: Windows Web Apps (WWA)
+ // `name` and `type` must use .setAttribute for WWA (#14901)
+ input.setAttribute( "type", "radio" );
+ input.setAttribute( "checked", "checked" );
+ input.setAttribute( "name", "t" );
+
+ div.appendChild( input );
+
+ // Support: Android <=4.1 only
+ // Older WebKit doesn't clone checked state correctly in fragments
+ support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked;
+
+ // Support: IE <=11 only
+ // Make sure textarea (and checkbox) defaultValue is properly cloned
+ div.innerHTML = "<textarea>x</textarea>";
+ support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue;
+} )();
+
+
+var
+ rkeyEvent = /^key/,
+ rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/,
+ rtypenamespace = /^([^.]*)(?:\.(.+)|)/;
+
+function returnTrue() {
+ return true;
+}
+
+function returnFalse() {
+ return false;
+}
+
+// Support: IE <=9 - 11+
+// focus() and blur() are asynchronous, except when they are no-op.
+// So expect focus to be synchronous when the element is already active,
+// and blur to be synchronous when the element is not already active.
+// (focus and blur are always synchronous in other supported browsers,
+// this just defines when we can count on it).
+function expectSync( elem, type ) {
+ return ( elem === safeActiveElement() ) === ( type === "focus" );
+}
+
+// Support: IE <=9 only
+// Accessing document.activeElement can throw unexpectedly
+// https://bugs.jquery.com/ticket/13393
+function safeActiveElement() {
+ try {
+ return document.activeElement;
+ } catch ( err ) { }
+}
+
+function on( elem, types, selector, data, fn, one ) {
+ var origFn, type;
+
+ // Types can be a map of types/handlers
+ if ( typeof types === "object" ) {
+
+ // ( types-Object, selector, data )
+ if ( typeof selector !== "string" ) {
+
+ // ( types-Object, data )
+ data = data || selector;
+ selector = undefined;
+ }
+ for ( type in types ) {
+ on( elem, type, selector, data, types[ type ], one );
+ }
+ return elem;
+ }
+
+ if ( data == null && fn == null ) {
+
+ // ( types, fn )
+ fn = selector;
+ data = selector = undefined;
+ } else if ( fn == null ) {
+ if ( typeof selector === "string" ) {
+
+ // ( types, selector, fn )
+ fn = data;
+ data = undefined;
+ } else {
+
+ // ( types, data, fn )
+ fn = data;
+ data = selector;
+ selector = undefined;
+ }
+ }
+ if ( fn === false ) {
+ fn = returnFalse;
+ } else if ( !fn ) {
+ return elem;
+ }
+
+ if ( one === 1 ) {
+ origFn = fn;
+ fn = function( event ) {
+
+ // Can use an empty set, since event contains the info
+ jQuery().off( event );
+ return origFn.apply( this, arguments );
+ };
+
+ // Use same guid so caller can remove using origFn
+ fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
+ }
+ return elem.each( function() {
+ jQuery.event.add( this, types, fn, data, selector );
+ } );
+}
+
+/*
+ * Helper functions for managing events -- not part of the public interface.
+ * Props to Dean Edwards' addEvent library for many of the ideas.
+ */
+jQuery.event = {
+
+ global: {},
+
+ add: function( elem, types, handler, data, selector ) {
+
+ var handleObjIn, eventHandle, tmp,
+ events, t, handleObj,
+ special, handlers, type, namespaces, origType,
+ elemData = dataPriv.get( elem );
+
+ // Don't attach events to noData or text/comment nodes (but allow plain objects)
+ if ( !elemData ) {
+ return;
+ }
+
+ // Caller can pass in an object of custom data in lieu of the handler
+ if ( handler.handler ) {
+ handleObjIn = handler;
+ handler = handleObjIn.handler;
+ selector = handleObjIn.selector;
+ }
+
+ // Ensure that invalid selectors throw exceptions at attach time
+ // Evaluate against documentElement in case elem is a non-element node (e.g., document)
+ if ( selector ) {
+ jQuery.find.matchesSelector( documentElement, selector );
+ }
+
+ // Make sure that the handler has a unique ID, used to find/remove it later
+ if ( !handler.guid ) {
+ handler.guid = jQuery.guid++;
+ }
+
+ // Init the element's event structure and main handler, if this is the first
+ if ( !( events = elemData.events ) ) {
+ events = elemData.events = {};
+ }
+ if ( !( eventHandle = elemData.handle ) ) {
+ eventHandle = elemData.handle = function( e ) {
+
+ // Discard the second event of a jQuery.event.trigger() and
+ // when an event is called after a page has unloaded
+ return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ?
+ jQuery.event.dispatch.apply( elem, arguments ) : undefined;
+ };
+ }
+
+ // Handle multiple events separated by a space
+ types = ( types || "" ).match( rnothtmlwhite ) || [ "" ];
+ t = types.length;
+ while ( t-- ) {
+ tmp = rtypenamespace.exec( types[ t ] ) || [];
+ type = origType = tmp[ 1 ];
+ namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort();
+
+ // There *must* be a type, no attaching namespace-only handlers
+ if ( !type ) {
+ continue;
+ }
+
+ // If event changes its type, use the special event handlers for the changed type
+ special = jQuery.event.special[ type ] || {};
+
+ // If selector defined, determine special event api type, otherwise given type
+ type = ( selector ? special.delegateType : special.bindType ) || type;
+
+ // Update special based on newly reset type
+ special = jQuery.event.special[ type ] || {};
+
+ // handleObj is passed to all event handlers
+ handleObj = jQuery.extend( {
+ type: type,
+ origType: origType,
+ data: data,
+ handler: handler,
+ guid: handler.guid,
+ selector: selector,
+ needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
+ namespace: namespaces.join( "." )
+ }, handleObjIn );
+
+ // Init the event handler queue if we're the first
+ if ( !( handlers = events[ type ] ) ) {
+ handlers = events[ type ] = [];
+ handlers.delegateCount = 0;
+
+ // Only use addEventListener if the special events handler returns false
+ if ( !special.setup ||
+ special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
+
+ if ( elem.addEventListener ) {
+ elem.addEventListener( type, eventHandle );
+ }
+ }
+ }
+
+ if ( special.add ) {
+ special.add.call( elem, handleObj );
+
+ if ( !handleObj.handler.guid ) {
+ handleObj.handler.guid = handler.guid;
+ }
+ }
+
+ // Add to the element's handler list, delegates in front
+ if ( selector ) {
+ handlers.splice( handlers.delegateCount++, 0, handleObj );
+ } else {
+ handlers.push( handleObj );
+ }
+
+ // Keep track of which events have ever been used, for event optimization
+ jQuery.event.global[ type ] = true;
+ }
+
+ },
+
+ // Detach an event or set of events from an element
+ remove: function( elem, types, handler, selector, mappedTypes ) {
+
+ var j, origCount, tmp,
+ events, t, handleObj,
+ special, handlers, type, namespaces, origType,
+ elemData = dataPriv.hasData( elem ) && dataPriv.get( elem );
+
+ if ( !elemData || !( events = elemData.events ) ) {
+ return;
+ }
+
+ // Once for each type.namespace in types; type may be omitted
+ types = ( types || "" ).match( rnothtmlwhite ) || [ "" ];
+ t = types.length;
+ while ( t-- ) {
+ tmp = rtypenamespace.exec( types[ t ] ) || [];
+ type = origType = tmp[ 1 ];
+ namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort();
+
+ // Unbind all events (on this namespace, if provided) for the element
+ if ( !type ) {
+ for ( type in events ) {
+ jQuery.event.remove( elem, type + types[ t ], handler, selector, true );
+ }
+ continue;
+ }
+
+ special = jQuery.event.special[ type ] || {};
+ type = ( selector ? special.delegateType : special.bindType ) || type;
+ handlers = events[ type ] || [];
+ tmp = tmp[ 2 ] &&
+ new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" );
+
+ // Remove matching events
+ origCount = j = handlers.length;
+ while ( j-- ) {
+ handleObj = handlers[ j ];
+
+ if ( ( mappedTypes || origType === handleObj.origType ) &&
+ ( !handler || handler.guid === handleObj.guid ) &&
+ ( !tmp || tmp.test( handleObj.namespace ) ) &&
+ ( !selector || selector === handleObj.selector ||
+ selector === "**" && handleObj.selector ) ) {
+ handlers.splice( j, 1 );
+
+ if ( handleObj.selector ) {
+ handlers.delegateCount--;
+ }
+ if ( special.remove ) {
+ special.remove.call( elem, handleObj );
+ }
+ }
+ }
+
+ // Remove generic event handler if we removed something and no more handlers exist
+ // (avoids potential for endless recursion during removal of special event handlers)
+ if ( origCount && !handlers.length ) {
+ if ( !special.teardown ||
+ special.teardown.call( elem, namespaces, elemData.handle ) === false ) {
+
+ jQuery.removeEvent( elem, type, elemData.handle );
+ }
+
+ delete events[ type ];
+ }
+ }
+
+ // Remove data and the expando if it's no longer used
+ if ( jQuery.isEmptyObject( events ) ) {
+ dataPriv.remove( elem, "handle events" );
+ }
+ },
+
+ dispatch: function( nativeEvent ) {
+
+ // Make a writable jQuery.Event from the native event object
+ var event = jQuery.event.fix( nativeEvent );
+
+ var i, j, ret, matched, handleObj, handlerQueue,
+ args = new Array( arguments.length ),
+ handlers = ( dataPriv.get( this, "events" ) || {} )[ event.type ] || [],
+ special = jQuery.event.special[ event.type ] || {};
+
+ // Use the fix-ed jQuery.Event rather than the (read-only) native event
+ args[ 0 ] = event;
+
+ for ( i = 1; i < arguments.length; i++ ) {
+ args[ i ] = arguments[ i ];
+ }
+
+ event.delegateTarget = this;
+
+ // Call the preDispatch hook for the mapped type, and let it bail if desired
+ if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {
+ return;
+ }
+
+ // Determine handlers
+ handlerQueue = jQuery.event.handlers.call( this, event, handlers );
+
+ // Run delegates first; they may want to stop propagation beneath us
+ i = 0;
+ while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) {
+ event.currentTarget = matched.elem;
+
+ j = 0;
+ while ( ( handleObj = matched.handlers[ j++ ] ) &&
+ !event.isImmediatePropagationStopped() ) {
+
+ // If the event is namespaced, then each handler is only invoked if it is
+ // specially universal or its namespaces are a superset of the event's.
+ if ( !event.rnamespace || handleObj.namespace === false ||
+ event.rnamespace.test( handleObj.namespace ) ) {
+
+ event.handleObj = handleObj;
+ event.data = handleObj.data;
+
+ ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle ||
+ handleObj.handler ).apply( matched.elem, args );
+
+ if ( ret !== undefined ) {
+ if ( ( event.result = ret ) === false ) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+ }
+ }
+ }
+
+ // Call the postDispatch hook for the mapped type
+ if ( special.postDispatch ) {
+ special.postDispatch.call( this, event );
+ }
+
+ return event.result;
+ },
+
+ handlers: function( event, handlers ) {
+ var i, handleObj, sel, matchedHandlers, matchedSelectors,
+ handlerQueue = [],
+ delegateCount = handlers.delegateCount,
+ cur = event.target;
+
+ // Find delegate handlers
+ if ( delegateCount &&
+
+ // Support: IE <=9
+ // Black-hole SVG <use> instance trees (trac-13180)
+ cur.nodeType &&
+
+ // Support: Firefox <=42
+ // Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861)
+ // https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click
+ // Support: IE 11 only
+ // ...but not arrow key "clicks" of radio inputs, which can have `button` -1 (gh-2343)
+ !( event.type === "click" && event.button >= 1 ) ) {
+
+ for ( ; cur !== this; cur = cur.parentNode || this ) {
+
+ // Don't check non-elements (#13208)
+ // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)
+ if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) {
+ matchedHandlers = [];
+ matchedSelectors = {};
+ for ( i = 0; i < delegateCount; i++ ) {
+ handleObj = handlers[ i ];
+
+ // Don't conflict with Object.prototype properties (#13203)
+ sel = handleObj.selector + " ";
+
+ if ( matchedSelectors[ sel ] === undefined ) {
+ matchedSelectors[ sel ] = handleObj.needsContext ?
+ jQuery( sel, this ).index( cur ) > -1 :
+ jQuery.find( sel, this, null, [ cur ] ).length;
+ }
+ if ( matchedSelectors[ sel ] ) {
+ matchedHandlers.push( handleObj );
+ }
+ }
+ if ( matchedHandlers.length ) {
+ handlerQueue.push( { elem: cur, handlers: matchedHandlers } );
+ }
+ }
+ }
+ }
+
+ // Add the remaining (directly-bound) handlers
+ cur = this;
+ if ( delegateCount < handlers.length ) {
+ handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } );
+ }
+
+ return handlerQueue;
+ },
+
+ addProp: function( name, hook ) {
+ Object.defineProperty( jQuery.Event.prototype, name, {
+ enumerable: true,
+ configurable: true,
+
+ get: isFunction( hook ) ?
+ function() {
+ if ( this.originalEvent ) {
+ return hook( this.originalEvent );
+ }
+ } :
+ function() {
+ if ( this.originalEvent ) {
+ return this.originalEvent[ name ];
+ }
+ },
+
+ set: function( value ) {
+ Object.defineProperty( this, name, {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: value
+ } );
+ }
+ } );
+ },
+
+ fix: function( originalEvent ) {
+ return originalEvent[ jQuery.expando ] ?
+ originalEvent :
+ new jQuery.Event( originalEvent );
+ },
+
+ special: {
+ load: {
+
+ // Prevent triggered image.load events from bubbling to window.load
+ noBubble: true
+ },
+ click: {
+
+ // Utilize native event to ensure correct state for checkable inputs
+ setup: function( data ) {
+
+ // For mutual compressibility with _default, replace `this` access with a local var.
+ // `|| data` is dead code meant only to preserve the variable through minification.
+ var el = this || data;
+
+ // Claim the first handler
+ if ( rcheckableType.test( el.type ) &&
+ el.click && nodeName( el, "input" ) ) {
+
+ // dataPriv.set( el, "click", ... )
+ leverageNative( el, "click", returnTrue );
+ }
+
+ // Return false to allow normal processing in the caller
+ return false;
+ },
+ trigger: function( data ) {
+
+ // For mutual compressibility with _default, replace `this` access with a local var.
+ // `|| data` is dead code meant only to preserve the variable through minification.
+ var el = this || data;
+
+ // Force setup before triggering a click
+ if ( rcheckableType.test( el.type ) &&
+ el.click && nodeName( el, "input" ) ) {
+
+ leverageNative( el, "click" );
+ }
+
+ // Return non-false to allow normal event-path propagation
+ return true;
+ },
+
+ // For cross-browser consistency, suppress native .click() on links
+ // Also prevent it if we're currently inside a leveraged native-event stack
+ _default: function( event ) {
+ var target = event.target;
+ return rcheckableType.test( target.type ) &&
+ target.click && nodeName( target, "input" ) &&
+ dataPriv.get( target, "click" ) ||
+ nodeName( target, "a" );
+ }
+ },
+
+ beforeunload: {
+ postDispatch: function( event ) {
+
+ // Support: Firefox 20+
+ // Firefox doesn't alert if the returnValue field is not set.
+ if ( event.result !== undefined && event.originalEvent ) {
+ event.originalEvent.returnValue = event.result;
+ }
+ }
+ }
+ }
+};
+
+// Ensure the presence of an event listener that handles manually-triggered
+// synthetic events by interrupting progress until reinvoked in response to
+// *native* events that it fires directly, ensuring that state changes have
+// already occurred before other listeners are invoked.
+function leverageNative( el, type, expectSync ) {
+
+ // Missing expectSync indicates a trigger call, which must force setup through jQuery.event.add
+ if ( !expectSync ) {
+ if ( dataPriv.get( el, type ) === undefined ) {
+ jQuery.event.add( el, type, returnTrue );
+ }
+ return;
+ }
+
+ // Register the controller as a special universal handler for all event namespaces
+ dataPriv.set( el, type, false );
+ jQuery.event.add( el, type, {
+ namespace: false,
+ handler: function( event ) {
+ var notAsync, result,
+ saved = dataPriv.get( this, type );
+
+ if ( ( event.isTrigger & 1 ) && this[ type ] ) {
+
+ // Interrupt processing of the outer synthetic .trigger()ed event
+ // Saved data should be false in such cases, but might be a leftover capture object
+ // from an async native handler (gh-4350)
+ if ( !saved.length ) {
+
+ // Store arguments for use when handling the inner native event
+ // There will always be at least one argument (an event object), so this array
+ // will not be confused with a leftover capture object.
+ saved = slice.call( arguments );
+ dataPriv.set( this, type, saved );
+
+ // Trigger the native event and capture its result
+ // Support: IE <=9 - 11+
+ // focus() and blur() are asynchronous
+ notAsync = expectSync( this, type );
+ this[ type ]();
+ result = dataPriv.get( this, type );
+ if ( saved !== result || notAsync ) {
+ dataPriv.set( this, type, false );
+ } else {
+ result = {};
+ }
+ if ( saved !== result ) {
+
+ // Cancel the outer synthetic event
+ event.stopImmediatePropagation();
+ event.preventDefault();
+ return result.value;
+ }
+
+ // If this is an inner synthetic event for an event with a bubbling surrogate
+ // (focus or blur), assume that the surrogate already propagated from triggering the
+ // native event and prevent that from happening again here.
+ // This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the
+ // bubbling surrogate propagates *after* the non-bubbling base), but that seems
+ // less bad than duplication.
+ } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) {
+ event.stopPropagation();
+ }
+
+ // If this is a native event triggered above, everything is now in order
+ // Fire an inner synthetic event with the original arguments
+ } else if ( saved.length ) {
+
+ // ...and capture the result
+ dataPriv.set( this, type, {
+ value: jQuery.event.trigger(
+
+ // Support: IE <=9 - 11+
+ // Extend with the prototype to reset the above stopImmediatePropagation()
+ jQuery.extend( saved[ 0 ], jQuery.Event.prototype ),
+ saved.slice( 1 ),
+ this
+ )
+ } );
+
+ // Abort handling of the native event
+ event.stopImmediatePropagation();
+ }
+ }
+ } );
+}
+
+jQuery.removeEvent = function( elem, type, handle ) {
+
+ // This "if" is needed for plain objects
+ if ( elem.removeEventListener ) {
+ elem.removeEventListener( type, handle );
+ }
+};
+
+jQuery.Event = function( src, props ) {
+
+ // Allow instantiation without the 'new' keyword
+ if ( !( this instanceof jQuery.Event ) ) {
+ return new jQuery.Event( src, props );
+ }
+
+ // Event object
+ if ( src && src.type ) {
+ this.originalEvent = src;
+ this.type = src.type;
+
+ // Events bubbling up the document may have been marked as prevented
+ // by a handler lower down the tree; reflect the correct value.
+ this.isDefaultPrevented = src.defaultPrevented ||
+ src.defaultPrevented === undefined &&
+
+ // Support: Android <=2.3 only
+ src.returnValue === false ?
+ returnTrue :
+ returnFalse;
+
+ // Create target properties
+ // Support: Safari <=6 - 7 only
+ // Target should not be a text node (#504, #13143)
+ this.target = ( src.target && src.target.nodeType === 3 ) ?
+ src.target.parentNode :
+ src.target;
+
+ this.currentTarget = src.currentTarget;
+ this.relatedTarget = src.relatedTarget;
+
+ // Event type
+ } else {
+ this.type = src;
+ }
+
+ // Put explicitly provided properties onto the event object
+ if ( props ) {
+ jQuery.extend( this, props );
+ }
+
+ // Create a timestamp if incoming event doesn't have one
+ this.timeStamp = src && src.timeStamp || Date.now();
+
+ // Mark it as fixed
+ this[ jQuery.expando ] = true;
+};
+
+// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
+// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
+jQuery.Event.prototype = {
+ constructor: jQuery.Event,
+ isDefaultPrevented: returnFalse,
+ isPropagationStopped: returnFalse,
+ isImmediatePropagationStopped: returnFalse,
+ isSimulated: false,
+
+ preventDefault: function() {
+ var e = this.originalEvent;
+
+ this.isDefaultPrevented = returnTrue;
+
+ if ( e && !this.isSimulated ) {
+ e.preventDefault();
+ }
+ },
+ stopPropagation: function() {
+ var e = this.originalEvent;
+
+ this.isPropagationStopped = returnTrue;
+
+ if ( e && !this.isSimulated ) {
+ e.stopPropagation();
+ }
+ },
+ stopImmediatePropagation: function() {
+ var e = this.originalEvent;
+
+ this.isImmediatePropagationStopped = returnTrue;
+
+ if ( e && !this.isSimulated ) {
+ e.stopImmediatePropagation();
+ }
+
+ this.stopPropagation();
+ }
+};
+
+// Includes all common event props including KeyEvent and MouseEvent specific props
+jQuery.each( {
+ altKey: true,
+ bubbles: true,
+ cancelable: true,
+ changedTouches: true,
+ ctrlKey: true,
+ detail: true,
+ eventPhase: true,
+ metaKey: true,
+ pageX: true,
+ pageY: true,
+ shiftKey: true,
+ view: true,
+ "char": true,
+ code: true,
+ charCode: true,
+ key: true,
+ keyCode: true,
+ button: true,
+ buttons: true,
+ clientX: true,
+ clientY: true,
+ offsetX: true,
+ offsetY: true,
+ pointerId: true,
+ pointerType: true,
+ screenX: true,
+ screenY: true,
+ targetTouches: true,
+ toElement: true,
+ touches: true,
+
+ which: function( event ) {
+ var button = event.button;
+
+ // Add which for key events
+ if ( event.which == null && rkeyEvent.test( event.type ) ) {
+ return event.charCode != null ? event.charCode : event.keyCode;
+ }
+
+ // Add which for click: 1 === left; 2 === middle; 3 === right
+ if ( !event.which && button !== undefined && rmouseEvent.test( event.type ) ) {
+ if ( button & 1 ) {
+ return 1;
+ }
+
+ if ( button & 2 ) {
+ return 3;
+ }
+
+ if ( button & 4 ) {
+ return 2;
+ }
+
+ return 0;
+ }
+
+ return event.which;
+ }
+}, jQuery.event.addProp );
+
+jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) {
+ jQuery.event.special[ type ] = {
+
+ // Utilize native event if possible so blur/focus sequence is correct
+ setup: function() {
+
+ // Claim the first handler
+ // dataPriv.set( this, "focus", ... )
+ // dataPriv.set( this, "blur", ... )
+ leverageNative( this, type, expectSync );
+
+ // Return false to allow normal processing in the caller
+ return false;
+ },
+ trigger: function() {
+
+ // Force setup before trigger
+ leverageNative( this, type );
+
+ // Return non-false to allow normal event-path propagation
+ return true;
+ },
+
+ delegateType: delegateType
+ };
+} );
+
+// Create mouseenter/leave events using mouseover/out and event-time checks
+// so that event delegation works in jQuery.
+// Do the same for pointerenter/pointerleave and pointerover/pointerout
+//
+// Support: Safari 7 only
+// Safari sends mouseenter too often; see:
+// https://bugs.chromium.org/p/chromium/issues/detail?id=470258
+// for the description of the bug (it existed in older Chrome versions as well).
+jQuery.each( {
+ mouseenter: "mouseover",
+ mouseleave: "mouseout",
+ pointerenter: "pointerover",
+ pointerleave: "pointerout"
+}, function( orig, fix ) {
+ jQuery.event.special[ orig ] = {
+ delegateType: fix,
+ bindType: fix,
+
+ handle: function( event ) {
+ var ret,
+ target = this,
+ related = event.relatedTarget,
+ handleObj = event.handleObj;
+
+ // For mouseenter/leave call the handler if related is outside the target.
+ // NB: No relatedTarget if the mouse left/entered the browser window
+ if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) {
+ event.type = handleObj.origType;
+ ret = handleObj.handler.apply( this, arguments );
+ event.type = fix;
+ }
+ return ret;
+ }
+ };
+} );
+
+jQuery.fn.extend( {
+
+ on: function( types, selector, data, fn ) {
+ return on( this, types, selector, data, fn );
+ },
+ one: function( types, selector, data, fn ) {
+ return on( this, types, selector, data, fn, 1 );
+ },
+ off: function( types, selector, fn ) {
+ var handleObj, type;
+ if ( types && types.preventDefault && types.handleObj ) {
+
+ // ( event ) dispatched jQuery.Event
+ handleObj = types.handleObj;
+ jQuery( types.delegateTarget ).off(
+ handleObj.namespace ?
+ handleObj.origType + "." + handleObj.namespace :
+ handleObj.origType,
+ handleObj.selector,
+ handleObj.handler
+ );
+ return this;
+ }
+ if ( typeof types === "object" ) {
+
+ // ( types-object [, selector] )
+ for ( type in types ) {
+ this.off( type, selector, types[ type ] );
+ }
+ return this;
+ }
+ if ( selector === false || typeof selector === "function" ) {
+
+ // ( types [, fn] )
+ fn = selector;
+ selector = undefined;
+ }
+ if ( fn === false ) {
+ fn = returnFalse;
+ }
+ return this.each( function() {
+ jQuery.event.remove( this, types, fn, selector );
+ } );
+ }
+} );
+
+
+var
+
+ /* eslint-disable max-len */
+
+ // See https://github.com/eslint/eslint/issues/3229
+ rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,
+
+ /* eslint-enable */
+
+ // Support: IE <=10 - 11, Edge 12 - 13 only
+ // In IE/Edge using regex groups here causes severe slowdowns.
+ // See https://connect.microsoft.com/IE/feedback/details/1736512/
+ rnoInnerhtml = /<script|<style|<link/i,
+
+ // checked="checked" or checked
+ rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
+ rcleanScript = /^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;
+
+// Prefer a tbody over its parent table for containing new rows
+function manipulationTarget( elem, content ) {
+ if ( nodeName( elem, "table" ) &&
+ nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) {
+
+ return jQuery( elem ).children( "tbody" )[ 0 ] || elem;
+ }
+
+ return elem;
+}
+
+// Replace/restore the type attribute of script elements for safe DOM manipulation
+function disableScript( elem ) {
+ elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type;
+ return elem;
+}
+function restoreScript( elem ) {
+ if ( ( elem.type || "" ).slice( 0, 5 ) === "true/" ) {
+ elem.type = elem.type.slice( 5 );
+ } else {
+ elem.removeAttribute( "type" );
+ }
+
+ return elem;
+}
+
+function cloneCopyEvent( src, dest ) {
+ var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events;
+
+ if ( dest.nodeType !== 1 ) {
+ return;
+ }
+
+ // 1. Copy private data: events, handlers, etc.
+ if ( dataPriv.hasData( src ) ) {
+ pdataOld = dataPriv.access( src );
+ pdataCur = dataPriv.set( dest, pdataOld );
+ events = pdataOld.events;
+
+ if ( events ) {
+ delete pdataCur.handle;
+ pdataCur.events = {};
+
+ for ( type in events ) {
+ for ( i = 0, l = events[ type ].length; i < l; i++ ) {
+ jQuery.event.add( dest, type, events[ type ][ i ] );
+ }
+ }
+ }
+ }
+
+ // 2. Copy user data
+ if ( dataUser.hasData( src ) ) {
+ udataOld = dataUser.access( src );
+ udataCur = jQuery.extend( {}, udataOld );
+
+ dataUser.set( dest, udataCur );
+ }
+}
+
+// Fix IE bugs, see support tests
+function fixInput( src, dest ) {
+ var nodeName = dest.nodeName.toLowerCase();
+
+ // Fails to persist the checked state of a cloned checkbox or radio button.
+ if ( nodeName === "input" && rcheckableType.test( src.type ) ) {
+ dest.checked = src.checked;
+
+ // Fails to return the selected option to the default selected state when cloning options
+ } else if ( nodeName === "input" || nodeName === "textarea" ) {
+ dest.defaultValue = src.defaultValue;
+ }
+}
+
+function domManip( collection, args, callback, ignored ) {
+
+ // Flatten any nested arrays
+ args = concat.apply( [], args );
+
+ var fragment, first, scripts, hasScripts, node, doc,
+ i = 0,
+ l = collection.length,
+ iNoClone = l - 1,
+ value = args[ 0 ],
+ valueIsFunction = isFunction( value );
+
+ // We can't cloneNode fragments that contain checked, in WebKit
+ if ( valueIsFunction ||
+ ( l > 1 && typeof value === "string" &&
+ !support.checkClone && rchecked.test( value ) ) ) {
+ return collection.each( function( index ) {
+ var self = collection.eq( index );
+ if ( valueIsFunction ) {
+ args[ 0 ] = value.call( this, index, self.html() );
+ }
+ domManip( self, args, callback, ignored );
+ } );
+ }
+
+ if ( l ) {
+ fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored );
+ first = fragment.firstChild;
+
+ if ( fragment.childNodes.length === 1 ) {
+ fragment = first;
+ }
+
+ // Require either new content or an interest in ignored elements to invoke the callback
+ if ( first || ignored ) {
+ scripts = jQuery.map( getAll( fragment, "script" ), disableScript );
+ hasScripts = scripts.length;
+
+ // Use the original fragment for the last item
+ // instead of the first because it can end up
+ // being emptied incorrectly in certain situations (#8070).
+ for ( ; i < l; i++ ) {
+ node = fragment;
+
+ if ( i !== iNoClone ) {
+ node = jQuery.clone( node, true, true );
+
+ // Keep references to cloned scripts for later restoration
+ if ( hasScripts ) {
+
+ // Support: Android <=4.0 only, PhantomJS 1 only
+ // push.apply(_, arraylike) throws on ancient WebKit
+ jQuery.merge( scripts, getAll( node, "script" ) );
+ }
+ }
+
+ callback.call( collection[ i ], node, i );
+ }
+
+ if ( hasScripts ) {
+ doc = scripts[ scripts.length - 1 ].ownerDocument;
+
+ // Reenable scripts
+ jQuery.map( scripts, restoreScript );
+
+ // Evaluate executable scripts on first document insertion
+ for ( i = 0; i < hasScripts; i++ ) {
+ node = scripts[ i ];
+ if ( rscriptType.test( node.type || "" ) &&
+ !dataPriv.access( node, "globalEval" ) &&
+ jQuery.contains( doc, node ) ) {
+
+ if ( node.src && ( node.type || "" ).toLowerCase() !== "module" ) {
+
+ // Optional AJAX dependency, but won't run scripts if not present
+ if ( jQuery._evalUrl && !node.noModule ) {
+ jQuery._evalUrl( node.src, {
+ nonce: node.nonce || node.getAttribute( "nonce" )
+ } );
+ }
+ } else {
+ DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc );
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return collection;
+}
+
+function remove( elem, selector, keepData ) {
+ var node,
+ nodes = selector ? jQuery.filter( selector, elem ) : elem,
+ i = 0;
+
+ for ( ; ( node = nodes[ i ] ) != null; i++ ) {
+ if ( !keepData && node.nodeType === 1 ) {
+ jQuery.cleanData( getAll( node ) );
+ }
+
+ if ( node.parentNode ) {
+ if ( keepData && isAttached( node ) ) {
+ setGlobalEval( getAll( node, "script" ) );
+ }
+ node.parentNode.removeChild( node );
+ }
+ }
+
+ return elem;
+}
+
+jQuery.extend( {
+ htmlPrefilter: function( html ) {
+ return html.replace( rxhtmlTag, "<$1></$2>" );
+ },
+
+ clone: function( elem, dataAndEvents, deepDataAndEvents ) {
+ var i, l, srcElements, destElements,
+ clone = elem.cloneNode( true ),
+ inPage = isAttached( elem );
+
+ // Fix IE cloning issues
+ if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) &&
+ !jQuery.isXMLDoc( elem ) ) {
+
+ // We eschew Sizzle here for performance reasons: https://jsperf.com/getall-vs-sizzle/2
+ destElements = getAll( clone );
+ srcElements = getAll( elem );
+
+ for ( i = 0, l = srcElements.length; i < l; i++ ) {
+ fixInput( srcElements[ i ], destElements[ i ] );
+ }
+ }
+
+ // Copy the events from the original to the clone
+ if ( dataAndEvents ) {
+ if ( deepDataAndEvents ) {
+ srcElements = srcElements || getAll( elem );
+ destElements = destElements || getAll( clone );
+
+ for ( i = 0, l = srcElements.length; i < l; i++ ) {
+ cloneCopyEvent( srcElements[ i ], destElements[ i ] );
+ }
+ } else {
+ cloneCopyEvent( elem, clone );
+ }
+ }
+
+ // Preserve script evaluation history
+ destElements = getAll( clone, "script" );
+ if ( destElements.length > 0 ) {
+ setGlobalEval( destElements, !inPage && getAll( elem, "script" ) );
+ }
+
+ // Return the cloned set
+ return clone;
+ },
+
+ cleanData: function( elems ) {
+ var data, elem, type,
+ special = jQuery.event.special,
+ i = 0;
+
+ for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) {
+ if ( acceptData( elem ) ) {
+ if ( ( data = elem[ dataPriv.expando ] ) ) {
+ if ( data.events ) {
+ for ( type in data.events ) {
+ if ( special[ type ] ) {
+ jQuery.event.remove( elem, type );
+
+ // This is a shortcut to avoid jQuery.event.remove's overhead
+ } else {
+ jQuery.removeEvent( elem, type, data.handle );
+ }
+ }
+ }
+
+ // Support: Chrome <=35 - 45+
+ // Assign undefined instead of using delete, see Data#remove
+ elem[ dataPriv.expando ] = undefined;
+ }
+ if ( elem[ dataUser.expando ] ) {
+
+ // Support: Chrome <=35 - 45+
+ // Assign undefined instead of using delete, see Data#remove
+ elem[ dataUser.expando ] = undefined;
+ }
+ }
+ }
+ }
+} );
+
+jQuery.fn.extend( {
+ detach: function( selector ) {
+ return remove( this, selector, true );
+ },
+
+ remove: function( selector ) {
+ return remove( this, selector );
+ },
+
+ text: function( value ) {
+ return access( this, function( value ) {
+ return value === undefined ?
+ jQuery.text( this ) :
+ this.empty().each( function() {
+ if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
+ this.textContent = value;
+ }
+ } );
+ }, null, value, arguments.length );
+ },
+
+ append: function() {
+ return domManip( this, arguments, function( elem ) {
+ if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
+ var target = manipulationTarget( this, elem );
+ target.appendChild( elem );
+ }
+ } );
+ },
+
+ prepend: function() {
+ return domManip( this, arguments, function( elem ) {
+ if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
+ var target = manipulationTarget( this, elem );
+ target.insertBefore( elem, target.firstChild );
+ }
+ } );
+ },
+
+ before: function() {
+ return domManip( this, arguments, function( elem ) {
+ if ( this.parentNode ) {
+ this.parentNode.insertBefore( elem, this );
+ }
+ } );
+ },
+
+ after: function() {
+ return domManip( this, arguments, function( elem ) {
+ if ( this.parentNode ) {
+ this.parentNode.insertBefore( elem, this.nextSibling );
+ }
+ } );
+ },
+
+ empty: function() {
+ var elem,
+ i = 0;
+
+ for ( ; ( elem = this[ i ] ) != null; i++ ) {
+ if ( elem.nodeType === 1 ) {
+
+ // Prevent memory leaks
+ jQuery.cleanData( getAll( elem, false ) );
+
+ // Remove any remaining nodes
+ elem.textContent = "";
+ }
+ }
+
+ return this;
+ },
+
+ clone: function( dataAndEvents, deepDataAndEvents ) {
+ dataAndEvents = dataAndEvents == null ? false : dataAndEvents;
+ deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;
+
+ return this.map( function() {
+ return jQuery.clone( this, dataAndEvents, deepDataAndEvents );
+ } );
+ },
+
+ html: function( value ) {
+ return access( this, function( value ) {
+ var elem = this[ 0 ] || {},
+ i = 0,
+ l = this.length;
+
+ if ( value === undefined && elem.nodeType === 1 ) {
+ return elem.innerHTML;
+ }
+
+ // See if we can take a shortcut and just use innerHTML
+ if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
+ !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) {
+
+ value = jQuery.htmlPrefilter( value );
+
+ try {
+ for ( ; i < l; i++ ) {
+ elem = this[ i ] || {};
+
+ // Remove element nodes and prevent memory leaks
+ if ( elem.nodeType === 1 ) {
+ jQuery.cleanData( getAll( elem, false ) );
+ elem.innerHTML = value;
+ }
+ }
+
+ elem = 0;
+
+ // If using innerHTML throws an exception, use the fallback method
+ } catch ( e ) {}
+ }
+
+ if ( elem ) {
+ this.empty().append( value );
+ }
+ }, null, value, arguments.length );
+ },
+
+ replaceWith: function() {
+ var ignored = [];
+
+ // Make the changes, replacing each non-ignored context element with the new content
+ return domManip( this, arguments, function( elem ) {
+ var parent = this.parentNode;
+
+ if ( jQuery.inArray( this, ignored ) < 0 ) {
+ jQuery.cleanData( getAll( this ) );
+ if ( parent ) {
+ parent.replaceChild( elem, this );
+ }
+ }
+
+ // Force callback invocation
+ }, ignored );
+ }
+} );
+
+jQuery.each( {
+ appendTo: "append",
+ prependTo: "prepend",
+ insertBefore: "before",
+ insertAfter: "after",
+ replaceAll: "replaceWith"
+}, function( name, original ) {
+ jQuery.fn[ name ] = function( selector ) {
+ var elems,
+ ret = [],
+ insert = jQuery( selector ),
+ last = insert.length - 1,
+ i = 0;
+
+ for ( ; i <= last; i++ ) {
+ elems = i === last ? this : this.clone( true );
+ jQuery( insert[ i ] )[ original ]( elems );
+
+ // Support: Android <=4.0 only, PhantomJS 1 only
+ // .get() because push.apply(_, arraylike) throws on ancient WebKit
+ push.apply( ret, elems.get() );
+ }
+
+ return this.pushStack( ret );
+ };
+} );
+var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" );
+
+var getStyles = function( elem ) {
+
+ // Support: IE <=11 only, Firefox <=30 (#15098, #14150)
+ // IE throws on elements created in popups
+ // FF meanwhile throws on frame elements through "defaultView.getComputedStyle"
+ var view = elem.ownerDocument.defaultView;
+
+ if ( !view || !view.opener ) {
+ view = window;
+ }
+
+ return view.getComputedStyle( elem );
+ };
+
+var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" );
+
+
+
+( function() {
+
+ // Executing both pixelPosition & boxSizingReliable tests require only one layout
+ // so they're executed at the same time to save the second computation.
+ function computeStyleTests() {
+
+ // This is a singleton, we need to execute it only once
+ if ( !div ) {
+ return;
+ }
+
+ container.style.cssText = "position:absolute;left:-11111px;width:60px;" +
+ "margin-top:1px;padding:0;border:0";
+ div.style.cssText =
+ "position:relative;display:block;box-sizing:border-box;overflow:scroll;" +
+ "margin:auto;border:1px;padding:1px;" +
+ "width:60%;top:1%";
+ documentElement.appendChild( container ).appendChild( div );
+
+ var divStyle = window.getComputedStyle( div );
+ pixelPositionVal = divStyle.top !== "1%";
+
+ // Support: Android 4.0 - 4.3 only, Firefox <=3 - 44
+ reliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12;
+
+ // Support: Android 4.0 - 4.3 only, Safari <=9.1 - 10.1, iOS <=7.0 - 9.3
+ // Some styles come back with percentage values, even though they shouldn't
+ div.style.right = "60%";
+ pixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36;
+
+ // Support: IE 9 - 11 only
+ // Detect misreporting of content dimensions for box-sizing:border-box elements
+ boxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36;
+
+ // Support: IE 9 only
+ // Detect overflow:scroll screwiness (gh-3699)
+ // Support: Chrome <=64
+ // Don't get tricked when zoom affects offsetWidth (gh-4029)
+ div.style.position = "absolute";
+ scrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12;
+
+ documentElement.removeChild( container );
+
+ // Nullify the div so it wouldn't be stored in the memory and
+ // it will also be a sign that checks already performed
+ div = null;
+ }
+
+ function roundPixelMeasures( measure ) {
+ return Math.round( parseFloat( measure ) );
+ }
+
+ var pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal,
+ reliableMarginLeftVal,
+ container = document.createElement( "div" ),
+ div = document.createElement( "div" );
+
+ // Finish early in limited (non-browser) environments
+ if ( !div.style ) {
+ return;
+ }
+
+ // Support: IE <=9 - 11 only
+ // Style of cloned element affects source element cloned (#8908)
+ div.style.backgroundClip = "content-box";
+ div.cloneNode( true ).style.backgroundClip = "";
+ support.clearCloneStyle = div.style.backgroundClip === "content-box";
+
+ jQuery.extend( support, {
+ boxSizingReliable: function() {
+ computeStyleTests();
+ return boxSizingReliableVal;
+ },
+ pixelBoxStyles: function() {
+ computeStyleTests();
+ return pixelBoxStylesVal;
+ },
+ pixelPosition: function() {
+ computeStyleTests();
+ return pixelPositionVal;
+ },
+ reliableMarginLeft: function() {
+ computeStyleTests();
+ return reliableMarginLeftVal;
+ },
+ scrollboxSize: function() {
+ computeStyleTests();
+ return scrollboxSizeVal;
+ }
+ } );
+} )();
+
+
+function curCSS( elem, name, computed ) {
+ var width, minWidth, maxWidth, ret,
+
+ // Support: Firefox 51+
+ // Retrieving style before computed somehow
+ // fixes an issue with getting wrong values
+ // on detached elements
+ style = elem.style;
+
+ computed = computed || getStyles( elem );
+
+ // getPropertyValue is needed for:
+ // .css('filter') (IE 9 only, #12537)
+ // .css('--customProperty) (#3144)
+ if ( computed ) {
+ ret = computed.getPropertyValue( name ) || computed[ name ];
+
+ if ( ret === "" && !isAttached( elem ) ) {
+ ret = jQuery.style( elem, name );
+ }
+
+ // A tribute to the "awesome hack by Dean Edwards"
+ // Android Browser returns percentage for some values,
+ // but width seems to be reliably pixels.
+ // This is against the CSSOM draft spec:
+ // https://drafts.csswg.org/cssom/#resolved-values
+ if ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) {
+
+ // Remember the original values
+ width = style.width;
+ minWidth = style.minWidth;
+ maxWidth = style.maxWidth;
+
+ // Put in the new values to get a computed value out
+ style.minWidth = style.maxWidth = style.width = ret;
+ ret = computed.width;
+
+ // Revert the changed values
+ style.width = width;
+ style.minWidth = minWidth;
+ style.maxWidth = maxWidth;
+ }
+ }
+
+ return ret !== undefined ?
+
+ // Support: IE <=9 - 11 only
+ // IE returns zIndex value as an integer.
+ ret + "" :
+ ret;
+}
+
+
+function addGetHookIf( conditionFn, hookFn ) {
+
+ // Define the hook, we'll check on the first run if it's really needed.
+ return {
+ get: function() {
+ if ( conditionFn() ) {
+
+ // Hook not needed (or it's not possible to use it due
+ // to missing dependency), remove it.
+ delete this.get;
+ return;
+ }
+
+ // Hook needed; redefine it so that the support test is not executed again.
+ return ( this.get = hookFn ).apply( this, arguments );
+ }
+ };
+}
+
+
+var cssPrefixes = [ "Webkit", "Moz", "ms" ],
+ emptyStyle = document.createElement( "div" ).style,
+ vendorProps = {};
+
+// Return a vendor-prefixed property or undefined
+function vendorPropName( name ) {
+
+ // Check for vendor prefixed names
+ var capName = name[ 0 ].toUpperCase() + name.slice( 1 ),
+ i = cssPrefixes.length;
+
+ while ( i-- ) {
+ name = cssPrefixes[ i ] + capName;
+ if ( name in emptyStyle ) {
+ return name;
+ }
+ }
+}
+
+// Return a potentially-mapped jQuery.cssProps or vendor prefixed property
+function finalPropName( name ) {
+ var final = jQuery.cssProps[ name ] || vendorProps[ name ];
+
+ if ( final ) {
+ return final;
+ }
+ if ( name in emptyStyle ) {
+ return name;
+ }
+ return vendorProps[ name ] = vendorPropName( name ) || name;
+}
+
+
+var
+
+ // Swappable if display is none or starts with table
+ // except "table", "table-cell", or "table-caption"
+ // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display
+ rdisplayswap = /^(none|table(?!-c[ea]).+)/,
+ rcustomProp = /^--/,
+ cssShow = { position: "absolute", visibility: "hidden", display: "block" },
+ cssNormalTransform = {
+ letterSpacing: "0",
+ fontWeight: "400"
+ };
+
+function setPositiveNumber( elem, value, subtract ) {
+
+ // Any relative (+/-) values have already been
+ // normalized at this point
+ var matches = rcssNum.exec( value );
+ return matches ?
+
+ // Guard against undefined "subtract", e.g., when used as in cssHooks
+ Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) :
+ value;
+}
+
+function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) {
+ var i = dimension === "width" ? 1 : 0,
+ extra = 0,
+ delta = 0;
+
+ // Adjustment may not be necessary
+ if ( box === ( isBorderBox ? "border" : "content" ) ) {
+ return 0;
+ }
+
+ for ( ; i < 4; i += 2 ) {
+
+ // Both box models exclude margin
+ if ( box === "margin" ) {
+ delta += jQuery.css( elem, box + cssExpand[ i ], true, styles );
+ }
+
+ // If we get here with a content-box, we're seeking "padding" or "border" or "margin"
+ if ( !isBorderBox ) {
+
+ // Add padding
+ delta += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
+
+ // For "border" or "margin", add border
+ if ( box !== "padding" ) {
+ delta += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
+
+ // But still keep track of it otherwise
+ } else {
+ extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
+ }
+
+ // If we get here with a border-box (content + padding + border), we're seeking "content" or
+ // "padding" or "margin"
+ } else {
+
+ // For "content", subtract padding
+ if ( box === "content" ) {
+ delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
+ }
+
+ // For "content" or "padding", subtract border
+ if ( box !== "margin" ) {
+ delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
+ }
+ }
+ }
+
+ // Account for positive content-box scroll gutter when requested by providing computedVal
+ if ( !isBorderBox && computedVal >= 0 ) {
+
+ // offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border
+ // Assuming integer scroll gutter, subtract the rest and round down
+ delta += Math.max( 0, Math.ceil(
+ elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] -
+ computedVal -
+ delta -
+ extra -
+ 0.5
+
+ // If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter
+ // Use an explicit zero to avoid NaN (gh-3964)
+ ) ) || 0;
+ }
+
+ return delta;
+}
+
+function getWidthOrHeight( elem, dimension, extra ) {
+
+ // Start with computed style
+ var styles = getStyles( elem ),
+
+ // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322).
+ // Fake content-box until we know it's needed to know the true value.
+ boxSizingNeeded = !support.boxSizingReliable() || extra,
+ isBorderBox = boxSizingNeeded &&
+ jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
+ valueIsBorderBox = isBorderBox,
+
+ val = curCSS( elem, dimension, styles ),
+ offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 );
+
+ // Support: Firefox <=54
+ // Return a confounding non-pixel value or feign ignorance, as appropriate.
+ if ( rnumnonpx.test( val ) ) {
+ if ( !extra ) {
+ return val;
+ }
+ val = "auto";
+ }
+
+
+ // Fall back to offsetWidth/offsetHeight when value is "auto"
+ // This happens for inline elements with no explicit setting (gh-3571)
+ // Support: Android <=4.1 - 4.3 only
+ // Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602)
+ // Support: IE 9-11 only
+ // Also use offsetWidth/offsetHeight for when box sizing is unreliable
+ // We use getClientRects() to check for hidden/disconnected.
+ // In those cases, the computed value can be trusted to be border-box
+ if ( ( !support.boxSizingReliable() && isBorderBox ||
+ val === "auto" ||
+ !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) &&
+ elem.getClientRects().length ) {
+
+ isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box";
+
+ // Where available, offsetWidth/offsetHeight approximate border box dimensions.
+ // Where not available (e.g., SVG), assume unreliable box-sizing and interpret the
+ // retrieved value as a content box dimension.
+ valueIsBorderBox = offsetProp in elem;
+ if ( valueIsBorderBox ) {
+ val = elem[ offsetProp ];
+ }
+ }
+
+ // Normalize "" and auto
+ val = parseFloat( val ) || 0;
+
+ // Adjust for the element's box model
+ return ( val +
+ boxModelAdjustment(
+ elem,
+ dimension,
+ extra || ( isBorderBox ? "border" : "content" ),
+ valueIsBorderBox,
+ styles,
+
+ // Provide the current computed size to request scroll gutter calculation (gh-3589)
+ val
+ )
+ ) + "px";
+}
+
+jQuery.extend( {
+
+ // Add in style property hooks for overriding the default
+ // behavior of getting and setting a style property
+ cssHooks: {
+ opacity: {
+ get: function( elem, computed ) {
+ if ( computed ) {
+
+ // We should always get a number back from opacity
+ var ret = curCSS( elem, "opacity" );
+ return ret === "" ? "1" : ret;
+ }
+ }
+ }
+ },
+
+ // Don't automatically add "px" to these possibly-unitless properties
+ cssNumber: {
+ "animationIterationCount": true,
+ "columnCount": true,
+ "fillOpacity": true,
+ "flexGrow": true,
+ "flexShrink": true,
+ "fontWeight": true,
+ "gridArea": true,
+ "gridColumn": true,
+ "gridColumnEnd": true,
+ "gridColumnStart": true,
+ "gridRow": true,
+ "gridRowEnd": true,
+ "gridRowStart": true,
+ "lineHeight": true,
+ "opacity": true,
+ "order": true,
+ "orphans": true,
+ "widows": true,
+ "zIndex": true,
+ "zoom": true
+ },
+
+ // Add in properties whose names you wish to fix before
+ // setting or getting the value
+ cssProps: {},
+
+ // Get and set the style property on a DOM Node
+ style: function( elem, name, value, extra ) {
+
+ // Don't set styles on text and comment nodes
+ if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
+ return;
+ }
+
+ // Make sure that we're working with the right name
+ var ret, type, hooks,
+ origName = camelCase( name ),
+ isCustomProp = rcustomProp.test( name ),
+ style = elem.style;
+
+ // Make sure that we're working with the right name. We don't
+ // want to query the value if it is a CSS custom property
+ // since they are user-defined.
+ if ( !isCustomProp ) {
+ name = finalPropName( origName );
+ }
+
+ // Gets hook for the prefixed version, then unprefixed version
+ hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
+
+ // Check if we're setting a value
+ if ( value !== undefined ) {
+ type = typeof value;
+
+ // Convert "+=" or "-=" to relative numbers (#7345)
+ if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) {
+ value = adjustCSS( elem, name, ret );
+
+ // Fixes bug #9237
+ type = "number";
+ }
+
+ // Make sure that null and NaN values aren't set (#7116)
+ if ( value == null || value !== value ) {
+ return;
+ }
+
+ // If a number was passed in, add the unit (except for certain CSS properties)
+ // The isCustomProp check can be removed in jQuery 4.0 when we only auto-append
+ // "px" to a few hardcoded values.
+ if ( type === "number" && !isCustomProp ) {
+ value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" );
+ }
+
+ // background-* props affect original clone's values
+ if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) {
+ style[ name ] = "inherit";
+ }
+
+ // If a hook was provided, use that value, otherwise just set the specified value
+ if ( !hooks || !( "set" in hooks ) ||
+ ( value = hooks.set( elem, value, extra ) ) !== undefined ) {
+
+ if ( isCustomProp ) {
+ style.setProperty( name, value );
+ } else {
+ style[ name ] = value;
+ }
+ }
+
+ } else {
+
+ // If a hook was provided get the non-computed value from there
+ if ( hooks && "get" in hooks &&
+ ( ret = hooks.get( elem, false, extra ) ) !== undefined ) {
+
+ return ret;
+ }
+
+ // Otherwise just get the value from the style object
+ return style[ name ];
+ }
+ },
+
+ css: function( elem, name, extra, styles ) {
+ var val, num, hooks,
+ origName = camelCase( name ),
+ isCustomProp = rcustomProp.test( name );
+
+ // Make sure that we're working with the right name. We don't
+ // want to modify the value if it is a CSS custom property
+ // since they are user-defined.
+ if ( !isCustomProp ) {
+ name = finalPropName( origName );
+ }
+
+ // Try prefixed name followed by the unprefixed name
+ hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
+
+ // If a hook was provided get the computed value from there
+ if ( hooks && "get" in hooks ) {
+ val = hooks.get( elem, true, extra );
+ }
+
+ // Otherwise, if a way to get the computed value exists, use that
+ if ( val === undefined ) {
+ val = curCSS( elem, name, styles );
+ }
+
+ // Convert "normal" to computed value
+ if ( val === "normal" && name in cssNormalTransform ) {
+ val = cssNormalTransform[ name ];
+ }
+
+ // Make numeric if forced or a qualifier was provided and val looks numeric
+ if ( extra === "" || extra ) {
+ num = parseFloat( val );
+ return extra === true || isFinite( num ) ? num || 0 : val;
+ }
+
+ return val;
+ }
+} );
+
+jQuery.each( [ "height", "width" ], function( i, dimension ) {
+ jQuery.cssHooks[ dimension ] = {
+ get: function( elem, computed, extra ) {
+ if ( computed ) {
+
+ // Certain elements can have dimension info if we invisibly show them
+ // but it must have a current display style that would benefit
+ return rdisplayswap.test( jQuery.css( elem, "display" ) ) &&
+
+ // Support: Safari 8+
+ // Table columns in Safari have non-zero offsetWidth & zero
+ // getBoundingClientRect().width unless display is changed.
+ // Support: IE <=11 only
+ // Running getBoundingClientRect on a disconnected node
+ // in IE throws an error.
+ ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ?
+ swap( elem, cssShow, function() {
+ return getWidthOrHeight( elem, dimension, extra );
+ } ) :
+ getWidthOrHeight( elem, dimension, extra );
+ }
+ },
+
+ set: function( elem, value, extra ) {
+ var matches,
+ styles = getStyles( elem ),
+
+ // Only read styles.position if the test has a chance to fail
+ // to avoid forcing a reflow.
+ scrollboxSizeBuggy = !support.scrollboxSize() &&
+ styles.position === "absolute",
+
+ // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991)
+ boxSizingNeeded = scrollboxSizeBuggy || extra,
+ isBorderBox = boxSizingNeeded &&
+ jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
+ subtract = extra ?
+ boxModelAdjustment(
+ elem,
+ dimension,
+ extra,
+ isBorderBox,
+ styles
+ ) :
+ 0;
+
+ // Account for unreliable border-box dimensions by comparing offset* to computed and
+ // faking a content-box to get border and padding (gh-3699)
+ if ( isBorderBox && scrollboxSizeBuggy ) {
+ subtract -= Math.ceil(
+ elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] -
+ parseFloat( styles[ dimension ] ) -
+ boxModelAdjustment( elem, dimension, "border", false, styles ) -
+ 0.5
+ );
+ }
+
+ // Convert to pixels if value adjustment is needed
+ if ( subtract && ( matches = rcssNum.exec( value ) ) &&
+ ( matches[ 3 ] || "px" ) !== "px" ) {
+
+ elem.style[ dimension ] = value;
+ value = jQuery.css( elem, dimension );
+ }
+
+ return setPositiveNumber( elem, value, subtract );
+ }
+ };
+} );
+
+jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft,
+ function( elem, computed ) {
+ if ( computed ) {
+ return ( parseFloat( curCSS( elem, "marginLeft" ) ) ||
+ elem.getBoundingClientRect().left -
+ swap( elem, { marginLeft: 0 }, function() {
+ return elem.getBoundingClientRect().left;
+ } )
+ ) + "px";
+ }
+ }
+);
+
+// These hooks are used by animate to expand properties
+jQuery.each( {
+ margin: "",
+ padding: "",
+ border: "Width"
+}, function( prefix, suffix ) {
+ jQuery.cssHooks[ prefix + suffix ] = {
+ expand: function( value ) {
+ var i = 0,
+ expanded = {},
+
+ // Assumes a single number if not a string
+ parts = typeof value === "string" ? value.split( " " ) : [ value ];
+
+ for ( ; i < 4; i++ ) {
+ expanded[ prefix + cssExpand[ i ] + suffix ] =
+ parts[ i ] || parts[ i - 2 ] || parts[ 0 ];
+ }
+
+ return expanded;
+ }
+ };
+
+ if ( prefix !== "margin" ) {
+ jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;
+ }
+} );
+
+jQuery.fn.extend( {
+ css: function( name, value ) {
+ return access( this, function( elem, name, value ) {
+ var styles, len,
+ map = {},
+ i = 0;
+
+ if ( Array.isArray( name ) ) {
+ styles = getStyles( elem );
+ len = name.length;
+
+ for ( ; i < len; i++ ) {
+ map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );
+ }
+
+ return map;
+ }
+
+ return value !== undefined ?
+ jQuery.style( elem, name, value ) :
+ jQuery.css( elem, name );
+ }, name, value, arguments.length > 1 );
+ }
+} );
+
+
+function Tween( elem, options, prop, end, easing ) {
+ return new Tween.prototype.init( elem, options, prop, end, easing );
+}
+jQuery.Tween = Tween;
+
+Tween.prototype = {
+ constructor: Tween,
+ init: function( elem, options, prop, end, easing, unit ) {
+ this.elem = elem;
+ this.prop = prop;
+ this.easing = easing || jQuery.easing._default;
+ this.options = options;
+ this.start = this.now = this.cur();
+ this.end = end;
+ this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" );
+ },
+ cur: function() {
+ var hooks = Tween.propHooks[ this.prop ];
+
+ return hooks && hooks.get ?
+ hooks.get( this ) :
+ Tween.propHooks._default.get( this );
+ },
+ run: function( percent ) {
+ var eased,
+ hooks = Tween.propHooks[ this.prop ];
+
+ if ( this.options.duration ) {
+ this.pos = eased = jQuery.easing[ this.easing ](
+ percent, this.options.duration * percent, 0, 1, this.options.duration
+ );
+ } else {
+ this.pos = eased = percent;
+ }
+ this.now = ( this.end - this.start ) * eased + this.start;
+
+ if ( this.options.step ) {
+ this.options.step.call( this.elem, this.now, this );
+ }
+
+ if ( hooks && hooks.set ) {
+ hooks.set( this );
+ } else {
+ Tween.propHooks._default.set( this );
+ }
+ return this;
+ }
+};
+
+Tween.prototype.init.prototype = Tween.prototype;
+
+Tween.propHooks = {
+ _default: {
+ get: function( tween ) {
+ var result;
+
+ // Use a property on the element directly when it is not a DOM element,
+ // or when there is no matching style property that exists.
+ if ( tween.elem.nodeType !== 1 ||
+ tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) {
+ return tween.elem[ tween.prop ];
+ }
+
+ // Passing an empty string as a 3rd parameter to .css will automatically
+ // attempt a parseFloat and fallback to a string if the parse fails.
+ // Simple values such as "10px" are parsed to Float;
+ // complex values such as "rotate(1rad)" are returned as-is.
+ result = jQuery.css( tween.elem, tween.prop, "" );
+
+ // Empty strings, null, undefined and "auto" are converted to 0.
+ return !result || result === "auto" ? 0 : result;
+ },
+ set: function( tween ) {
+
+ // Use step hook for back compat.
+ // Use cssHook if its there.
+ // Use .style if available and use plain properties where available.
+ if ( jQuery.fx.step[ tween.prop ] ) {
+ jQuery.fx.step[ tween.prop ]( tween );
+ } else if ( tween.elem.nodeType === 1 && (
+ jQuery.cssHooks[ tween.prop ] ||
+ tween.elem.style[ finalPropName( tween.prop ) ] != null ) ) {
+ jQuery.style( tween.elem, tween.prop, tween.now + tween.unit );
+ } else {
+ tween.elem[ tween.prop ] = tween.now;
+ }
+ }
+ }
+};
+
+// Support: IE <=9 only
+// Panic based approach to setting things on disconnected nodes
+Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {
+ set: function( tween ) {
+ if ( tween.elem.nodeType && tween.elem.parentNode ) {
+ tween.elem[ tween.prop ] = tween.now;
+ }
+ }
+};
+
+jQuery.easing = {
+ linear: function( p ) {
+ return p;
+ },
+ swing: function( p ) {
+ return 0.5 - Math.cos( p * Math.PI ) / 2;
+ },
+ _default: "swing"
+};
+
+jQuery.fx = Tween.prototype.init;
+
+// Back compat <1.8 extension point
+jQuery.fx.step = {};
+
+
+
+
+var
+ fxNow, inProgress,
+ rfxtypes = /^(?:toggle|show|hide)$/,
+ rrun = /queueHooks$/;
+
+function schedule() {
+ if ( inProgress ) {
+ if ( document.hidden === false && window.requestAnimationFrame ) {
+ window.requestAnimationFrame( schedule );
+ } else {
+ window.setTimeout( schedule, jQuery.fx.interval );
+ }
+
+ jQuery.fx.tick();
+ }
+}
+
+// Animations created synchronously will run synchronously
+function createFxNow() {
+ window.setTimeout( function() {
+ fxNow = undefined;
+ } );
+ return ( fxNow = Date.now() );
+}
+
+// Generate parameters to create a standard animation
+function genFx( type, includeWidth ) {
+ var which,
+ i = 0,
+ attrs = { height: type };
+
+ // If we include width, step value is 1 to do all cssExpand values,
+ // otherwise step value is 2 to skip over Left and Right
+ includeWidth = includeWidth ? 1 : 0;
+ for ( ; i < 4; i += 2 - includeWidth ) {
+ which = cssExpand[ i ];
+ attrs[ "margin" + which ] = attrs[ "padding" + which ] = type;
+ }
+
+ if ( includeWidth ) {
+ attrs.opacity = attrs.width = type;
+ }
+
+ return attrs;
+}
+
+function createTween( value, prop, animation ) {
+ var tween,
+ collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ),
+ index = 0,
+ length = collection.length;
+ for ( ; index < length; index++ ) {
+ if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) {
+
+ // We're done with this property
+ return tween;
+ }
+ }
+}
+
+function defaultPrefilter( elem, props, opts ) {
+ var prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display,
+ isBox = "width" in props || "height" in props,
+ anim = this,
+ orig = {},
+ style = elem.style,
+ hidden = elem.nodeType && isHiddenWithinTree( elem ),
+ dataShow = dataPriv.get( elem, "fxshow" );
+
+ // Queue-skipping animations hijack the fx hooks
+ if ( !opts.queue ) {
+ hooks = jQuery._queueHooks( elem, "fx" );
+ if ( hooks.unqueued == null ) {
+ hooks.unqueued = 0;
+ oldfire = hooks.empty.fire;
+ hooks.empty.fire = function() {
+ if ( !hooks.unqueued ) {
+ oldfire();
+ }
+ };
+ }
+ hooks.unqueued++;
+
+ anim.always( function() {
+
+ // Ensure the complete handler is called before this completes
+ anim.always( function() {
+ hooks.unqueued--;
+ if ( !jQuery.queue( elem, "fx" ).length ) {
+ hooks.empty.fire();
+ }
+ } );
+ } );
+ }
+
+ // Detect show/hide animations
+ for ( prop in props ) {
+ value = props[ prop ];
+ if ( rfxtypes.test( value ) ) {
+ delete props[ prop ];
+ toggle = toggle || value === "toggle";
+ if ( value === ( hidden ? "hide" : "show" ) ) {
+
+ // Pretend to be hidden if this is a "show" and
+ // there is still data from a stopped show/hide
+ if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) {
+ hidden = true;
+
+ // Ignore all other no-op show/hide data
+ } else {
+ continue;
+ }
+ }
+ orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop );
+ }
+ }
+
+ // Bail out if this is a no-op like .hide().hide()
+ propTween = !jQuery.isEmptyObject( props );
+ if ( !propTween && jQuery.isEmptyObject( orig ) ) {
+ return;
+ }
+
+ // Restrict "overflow" and "display" styles during box animations
+ if ( isBox && elem.nodeType === 1 ) {
+
+ // Support: IE <=9 - 11, Edge 12 - 15
+ // Record all 3 overflow attributes because IE does not infer the shorthand
+ // from identically-valued overflowX and overflowY and Edge just mirrors
+ // the overflowX value there.
+ opts.overflow = [ style.overflow, style.overflowX, style.overflowY ];
+
+ // Identify a display type, preferring old show/hide data over the CSS cascade
+ restoreDisplay = dataShow && dataShow.display;
+ if ( restoreDisplay == null ) {
+ restoreDisplay = dataPriv.get( elem, "display" );
+ }
+ display = jQuery.css( elem, "display" );
+ if ( display === "none" ) {
+ if ( restoreDisplay ) {
+ display = restoreDisplay;
+ } else {
+
+ // Get nonempty value(s) by temporarily forcing visibility
+ showHide( [ elem ], true );
+ restoreDisplay = elem.style.display || restoreDisplay;
+ display = jQuery.css( elem, "display" );
+ showHide( [ elem ] );
+ }
+ }
+
+ // Animate inline elements as inline-block
+ if ( display === "inline" || display === "inline-block" && restoreDisplay != null ) {
+ if ( jQuery.css( elem, "float" ) === "none" ) {
+
+ // Restore the original display value at the end of pure show/hide animations
+ if ( !propTween ) {
+ anim.done( function() {
+ style.display = restoreDisplay;
+ } );
+ if ( restoreDisplay == null ) {
+ display = style.display;
+ restoreDisplay = display === "none" ? "" : display;
+ }
+ }
+ style.display = "inline-block";
+ }
+ }
+ }
+
+ if ( opts.overflow ) {
+ style.overflow = "hidden";
+ anim.always( function() {
+ style.overflow = opts.overflow[ 0 ];
+ style.overflowX = opts.overflow[ 1 ];
+ style.overflowY = opts.overflow[ 2 ];
+ } );
+ }
+
+ // Implement show/hide animations
+ propTween = false;
+ for ( prop in orig ) {
+
+ // General show/hide setup for this element animation
+ if ( !propTween ) {
+ if ( dataShow ) {
+ if ( "hidden" in dataShow ) {
+ hidden = dataShow.hidden;
+ }
+ } else {
+ dataShow = dataPriv.access( elem, "fxshow", { display: restoreDisplay } );
+ }
+
+ // Store hidden/visible for toggle so `.stop().toggle()` "reverses"
+ if ( toggle ) {
+ dataShow.hidden = !hidden;
+ }
+
+ // Show elements before animating them
+ if ( hidden ) {
+ showHide( [ elem ], true );
+ }
+
+ /* eslint-disable no-loop-func */
+
+ anim.done( function() {
+
+ /* eslint-enable no-loop-func */
+
+ // The final step of a "hide" animation is actually hiding the element
+ if ( !hidden ) {
+ showHide( [ elem ] );
+ }
+ dataPriv.remove( elem, "fxshow" );
+ for ( prop in orig ) {
+ jQuery.style( elem, prop, orig[ prop ] );
+ }
+ } );
+ }
+
+ // Per-property setup
+ propTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim );
+ if ( !( prop in dataShow ) ) {
+ dataShow[ prop ] = propTween.start;
+ if ( hidden ) {
+ propTween.end = propTween.start;
+ propTween.start = 0;
+ }
+ }
+ }
+}
+
+function propFilter( props, specialEasing ) {
+ var index, name, easing, value, hooks;
+
+ // camelCase, specialEasing and expand cssHook pass
+ for ( index in props ) {
+ name = camelCase( index );
+ easing = specialEasing[ name ];
+ value = props[ index ];
+ if ( Array.isArray( value ) ) {
+ easing = value[ 1 ];
+ value = props[ index ] = value[ 0 ];
+ }
+
+ if ( index !== name ) {
+ props[ name ] = value;
+ delete props[ index ];
+ }
+
+ hooks = jQuery.cssHooks[ name ];
+ if ( hooks && "expand" in hooks ) {
+ value = hooks.expand( value );
+ delete props[ name ];
+
+ // Not quite $.extend, this won't overwrite existing keys.
+ // Reusing 'index' because we have the correct "name"
+ for ( index in value ) {
+ if ( !( index in props ) ) {
+ props[ index ] = value[ index ];
+ specialEasing[ index ] = easing;
+ }
+ }
+ } else {
+ specialEasing[ name ] = easing;
+ }
+ }
+}
+
+function Animation( elem, properties, options ) {
+ var result,
+ stopped,
+ index = 0,
+ length = Animation.prefilters.length,
+ deferred = jQuery.Deferred().always( function() {
+
+ // Don't match elem in the :animated selector
+ delete tick.elem;
+ } ),
+ tick = function() {
+ if ( stopped ) {
+ return false;
+ }
+ var currentTime = fxNow || createFxNow(),
+ remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),
+
+ // Support: Android 2.3 only
+ // Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497)
+ temp = remaining / animation.duration || 0,
+ percent = 1 - temp,
+ index = 0,
+ length = animation.tweens.length;
+
+ for ( ; index < length; index++ ) {
+ animation.tweens[ index ].run( percent );
+ }
+
+ deferred.notifyWith( elem, [ animation, percent, remaining ] );
+
+ // If there's more to do, yield
+ if ( percent < 1 && length ) {
+ return remaining;
+ }
+
+ // If this was an empty animation, synthesize a final progress notification
+ if ( !length ) {
+ deferred.notifyWith( elem, [ animation, 1, 0 ] );
+ }
+
+ // Resolve the animation and report its conclusion
+ deferred.resolveWith( elem, [ animation ] );
+ return false;
+ },
+ animation = deferred.promise( {
+ elem: elem,
+ props: jQuery.extend( {}, properties ),
+ opts: jQuery.extend( true, {
+ specialEasing: {},
+ easing: jQuery.easing._default
+ }, options ),
+ originalProperties: properties,
+ originalOptions: options,
+ startTime: fxNow || createFxNow(),
+ duration: options.duration,
+ tweens: [],
+ createTween: function( prop, end ) {
+ var tween = jQuery.Tween( elem, animation.opts, prop, end,
+ animation.opts.specialEasing[ prop ] || animation.opts.easing );
+ animation.tweens.push( tween );
+ return tween;
+ },
+ stop: function( gotoEnd ) {
+ var index = 0,
+
+ // If we are going to the end, we want to run all the tweens
+ // otherwise we skip this part
+ length = gotoEnd ? animation.tweens.length : 0;
+ if ( stopped ) {
+ return this;
+ }
+ stopped = true;
+ for ( ; index < length; index++ ) {
+ animation.tweens[ index ].run( 1 );
+ }
+
+ // Resolve when we played the last frame; otherwise, reject
+ if ( gotoEnd ) {
+ deferred.notifyWith( elem, [ animation, 1, 0 ] );
+ deferred.resolveWith( elem, [ animation, gotoEnd ] );
+ } else {
+ deferred.rejectWith( elem, [ animation, gotoEnd ] );
+ }
+ return this;
+ }
+ } ),
+ props = animation.props;
+
+ propFilter( props, animation.opts.specialEasing );
+
+ for ( ; index < length; index++ ) {
+ result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts );
+ if ( result ) {
+ if ( isFunction( result.stop ) ) {
+ jQuery._queueHooks( animation.elem, animation.opts.queue ).stop =
+ result.stop.bind( result );
+ }
+ return result;
+ }
+ }
+
+ jQuery.map( props, createTween, animation );
+
+ if ( isFunction( animation.opts.start ) ) {
+ animation.opts.start.call( elem, animation );
+ }
+
+ // Attach callbacks from options
+ animation
+ .progress( animation.opts.progress )
+ .done( animation.opts.done, animation.opts.complete )
+ .fail( animation.opts.fail )
+ .always( animation.opts.always );
+
+ jQuery.fx.timer(
+ jQuery.extend( tick, {
+ elem: elem,
+ anim: animation,
+ queue: animation.opts.queue
+ } )
+ );
+
+ return animation;
+}
+
+jQuery.Animation = jQuery.extend( Animation, {
+
+ tweeners: {
+ "*": [ function( prop, value ) {
+ var tween = this.createTween( prop, value );
+ adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween );
+ return tween;
+ } ]
+ },
+
+ tweener: function( props, callback ) {
+ if ( isFunction( props ) ) {
+ callback = props;
+ props = [ "*" ];
+ } else {
+ props = props.match( rnothtmlwhite );
+ }
+
+ var prop,
+ index = 0,
+ length = props.length;
+
+ for ( ; index < length; index++ ) {
+ prop = props[ index ];
+ Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || [];
+ Animation.tweeners[ prop ].unshift( callback );
+ }
+ },
+
+ prefilters: [ defaultPrefilter ],
+
+ prefilter: function( callback, prepend ) {
+ if ( prepend ) {
+ Animation.prefilters.unshift( callback );
+ } else {
+ Animation.prefilters.push( callback );
+ }
+ }
+} );
+
+jQuery.speed = function( speed, easing, fn ) {
+ var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
+ complete: fn || !fn && easing ||
+ isFunction( speed ) && speed,
+ duration: speed,
+ easing: fn && easing || easing && !isFunction( easing ) && easing
+ };
+
+ // Go to the end state if fx are off
+ if ( jQuery.fx.off ) {
+ opt.duration = 0;
+
+ } else {
+ if ( typeof opt.duration !== "number" ) {
+ if ( opt.duration in jQuery.fx.speeds ) {
+ opt.duration = jQuery.fx.speeds[ opt.duration ];
+
+ } else {
+ opt.duration = jQuery.fx.speeds._default;
+ }
+ }
+ }
+
+ // Normalize opt.queue - true/undefined/null -> "fx"
+ if ( opt.queue == null || opt.queue === true ) {
+ opt.queue = "fx";
+ }
+
+ // Queueing
+ opt.old = opt.complete;
+
+ opt.complete = function() {
+ if ( isFunction( opt.old ) ) {
+ opt.old.call( this );
+ }
+
+ if ( opt.queue ) {
+ jQuery.dequeue( this, opt.queue );
+ }
+ };
+
+ return opt;
+};
+
+jQuery.fn.extend( {
+ fadeTo: function( speed, to, easing, callback ) {
+
+ // Show any hidden elements after setting opacity to 0
+ return this.filter( isHiddenWithinTree ).css( "opacity", 0 ).show()
+
+ // Animate to the value specified
+ .end().animate( { opacity: to }, speed, easing, callback );
+ },
+ animate: function( prop, speed, easing, callback ) {
+ var empty = jQuery.isEmptyObject( prop ),
+ optall = jQuery.speed( speed, easing, callback ),
+ doAnimation = function() {
+
+ // Operate on a copy of prop so per-property easing won't be lost
+ var anim = Animation( this, jQuery.extend( {}, prop ), optall );
+
+ // Empty animations, or finishing resolves immediately
+ if ( empty || dataPriv.get( this, "finish" ) ) {
+ anim.stop( true );
+ }
+ };
+ doAnimation.finish = doAnimation;
+
+ return empty || optall.queue === false ?
+ this.each( doAnimation ) :
+ this.queue( optall.queue, doAnimation );
+ },
+ stop: function( type, clearQueue, gotoEnd ) {
+ var stopQueue = function( hooks ) {
+ var stop = hooks.stop;
+ delete hooks.stop;
+ stop( gotoEnd );
+ };
+
+ if ( typeof type !== "string" ) {
+ gotoEnd = clearQueue;
+ clearQueue = type;
+ type = undefined;
+ }
+ if ( clearQueue && type !== false ) {
+ this.queue( type || "fx", [] );
+ }
+
+ return this.each( function() {
+ var dequeue = true,
+ index = type != null && type + "queueHooks",
+ timers = jQuery.timers,
+ data = dataPriv.get( this );
+
+ if ( index ) {
+ if ( data[ index ] && data[ index ].stop ) {
+ stopQueue( data[ index ] );
+ }
+ } else {
+ for ( index in data ) {
+ if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {
+ stopQueue( data[ index ] );
+ }
+ }
+ }
+
+ for ( index = timers.length; index--; ) {
+ if ( timers[ index ].elem === this &&
+ ( type == null || timers[ index ].queue === type ) ) {
+
+ timers[ index ].anim.stop( gotoEnd );
+ dequeue = false;
+ timers.splice( index, 1 );
+ }
+ }
+
+ // Start the next in the queue if the last step wasn't forced.
+ // Timers currently will call their complete callbacks, which
+ // will dequeue but only if they were gotoEnd.
+ if ( dequeue || !gotoEnd ) {
+ jQuery.dequeue( this, type );
+ }
+ } );
+ },
+ finish: function( type ) {
+ if ( type !== false ) {
+ type = type || "fx";
+ }
+ return this.each( function() {
+ var index,
+ data = dataPriv.get( this ),
+ queue = data[ type + "queue" ],
+ hooks = data[ type + "queueHooks" ],
+ timers = jQuery.timers,
+ length = queue ? queue.length : 0;
+
+ // Enable finishing flag on private data
+ data.finish = true;
+
+ // Empty the queue first
+ jQuery.queue( this, type, [] );
+
+ if ( hooks && hooks.stop ) {
+ hooks.stop.call( this, true );
+ }
+
+ // Look for any active animations, and finish them
+ for ( index = timers.length; index--; ) {
+ if ( timers[ index ].elem === this && timers[ index ].queue === type ) {
+ timers[ index ].anim.stop( true );
+ timers.splice( index, 1 );
+ }
+ }
+
+ // Look for any animations in the old queue and finish them
+ for ( index = 0; index < length; index++ ) {
+ if ( queue[ index ] && queue[ index ].finish ) {
+ queue[ index ].finish.call( this );
+ }
+ }
+
+ // Turn off finishing flag
+ delete data.finish;
+ } );
+ }
+} );
+
+jQuery.each( [ "toggle", "show", "hide" ], function( i, name ) {
+ var cssFn = jQuery.fn[ name ];
+ jQuery.fn[ name ] = function( speed, easing, callback ) {
+ return speed == null || typeof speed === "boolean" ?
+ cssFn.apply( this, arguments ) :
+ this.animate( genFx( name, true ), speed, easing, callback );
+ };
+} );
+
+// Generate shortcuts for custom animations
+jQuery.each( {
+ slideDown: genFx( "show" ),
+ slideUp: genFx( "hide" ),
+ slideToggle: genFx( "toggle" ),
+ fadeIn: { opacity: "show" },
+ fadeOut: { opacity: "hide" },
+ fadeToggle: { opacity: "toggle" }
+}, function( name, props ) {
+ jQuery.fn[ name ] = function( speed, easing, callback ) {
+ return this.animate( props, speed, easing, callback );
+ };
+} );
+
+jQuery.timers = [];
+jQuery.fx.tick = function() {
+ var timer,
+ i = 0,
+ timers = jQuery.timers;
+
+ fxNow = Date.now();
+
+ for ( ; i < timers.length; i++ ) {
+ timer = timers[ i ];
+
+ // Run the timer and safely remove it when done (allowing for external removal)
+ if ( !timer() && timers[ i ] === timer ) {
+ timers.splice( i--, 1 );
+ }
+ }
+
+ if ( !timers.length ) {
+ jQuery.fx.stop();
+ }
+ fxNow = undefined;
+};
+
+jQuery.fx.timer = function( timer ) {
+ jQuery.timers.push( timer );
+ jQuery.fx.start();
+};
+
+jQuery.fx.interval = 13;
+jQuery.fx.start = function() {
+ if ( inProgress ) {
+ return;
+ }
+
+ inProgress = true;
+ schedule();
+};
+
+jQuery.fx.stop = function() {
+ inProgress = null;
+};
+
+jQuery.fx.speeds = {
+ slow: 600,
+ fast: 200,
+
+ // Default speed
+ _default: 400
+};
+
+
+// Based off of the plugin by Clint Helfers, with permission.
+// https://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/
+jQuery.fn.delay = function( time, type ) {
+ time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;
+ type = type || "fx";
+
+ return this.queue( type, function( next, hooks ) {
+ var timeout = window.setTimeout( next, time );
+ hooks.stop = function() {
+ window.clearTimeout( timeout );
+ };
+ } );
+};
+
+
+( function() {
+ var input = document.createElement( "input" ),
+ select = document.createElement( "select" ),
+ opt = select.appendChild( document.createElement( "option" ) );
+
+ input.type = "checkbox";
+
+ // Support: Android <=4.3 only
+ // Default value for a checkbox should be "on"
+ support.checkOn = input.value !== "";
+
+ // Support: IE <=11 only
+ // Must access selectedIndex to make default options select
+ support.optSelected = opt.selected;
+
+ // Support: IE <=11 only
+ // An input loses its value after becoming a radio
+ input = document.createElement( "input" );
+ input.value = "t";
+ input.type = "radio";
+ support.radioValue = input.value === "t";
+} )();
+
+
+var boolHook,
+ attrHandle = jQuery.expr.attrHandle;
+
+jQuery.fn.extend( {
+ attr: function( name, value ) {
+ return access( this, jQuery.attr, name, value, arguments.length > 1 );
+ },
+
+ removeAttr: function( name ) {
+ return this.each( function() {
+ jQuery.removeAttr( this, name );
+ } );
+ }
+} );
+
+jQuery.extend( {
+ attr: function( elem, name, value ) {
+ var ret, hooks,
+ nType = elem.nodeType;
+
+ // Don't get/set attributes on text, comment and attribute nodes
+ if ( nType === 3 || nType === 8 || nType === 2 ) {
+ return;
+ }
+
+ // Fallback to prop when attributes are not supported
+ if ( typeof elem.getAttribute === "undefined" ) {
+ return jQuery.prop( elem, name, value );
+ }
+
+ // Attribute hooks are determined by the lowercase version
+ // Grab necessary hook if one is defined
+ if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {
+ hooks = jQuery.attrHooks[ name.toLowerCase() ] ||
+ ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined );
+ }
+
+ if ( value !== undefined ) {
+ if ( value === null ) {
+ jQuery.removeAttr( elem, name );
+ return;
+ }
+
+ if ( hooks && "set" in hooks &&
+ ( ret = hooks.set( elem, value, name ) ) !== undefined ) {
+ return ret;
+ }
+
+ elem.setAttribute( name, value + "" );
+ return value;
+ }
+
+ if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {
+ return ret;
+ }
+
+ ret = jQuery.find.attr( elem, name );
+
+ // Non-existent attributes return null, we normalize to undefined
+ return ret == null ? undefined : ret;
+ },
+
+ attrHooks: {
+ type: {
+ set: function( elem, value ) {
+ if ( !support.radioValue && value === "radio" &&
+ nodeName( elem, "input" ) ) {
+ var val = elem.value;
+ elem.setAttribute( "type", value );
+ if ( val ) {
+ elem.value = val;
+ }
+ return value;
+ }
+ }
+ }
+ },
+
+ removeAttr: function( elem, value ) {
+ var name,
+ i = 0,
+
+ // Attribute names can contain non-HTML whitespace characters
+ // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
+ attrNames = value && value.match( rnothtmlwhite );
+
+ if ( attrNames && elem.nodeType === 1 ) {
+ while ( ( name = attrNames[ i++ ] ) ) {
+ elem.removeAttribute( name );
+ }
+ }
+ }
+} );
+
+// Hooks for boolean attributes
+boolHook = {
+ set: function( elem, value, name ) {
+ if ( value === false ) {
+
+ // Remove boolean attributes when set to false
+ jQuery.removeAttr( elem, name );
+ } else {
+ elem.setAttribute( name, name );
+ }
+ return name;
+ }
+};
+
+jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) {
+ var getter = attrHandle[ name ] || jQuery.find.attr;
+
+ attrHandle[ name ] = function( elem, name, isXML ) {
+ var ret, handle,
+ lowercaseName = name.toLowerCase();
+
+ if ( !isXML ) {
+
+ // Avoid an infinite loop by temporarily removing this function from the getter
+ handle = attrHandle[ lowercaseName ];
+ attrHandle[ lowercaseName ] = ret;
+ ret = getter( elem, name, isXML ) != null ?
+ lowercaseName :
+ null;
+ attrHandle[ lowercaseName ] = handle;
+ }
+ return ret;
+ };
+} );
+
+
+
+
+var rfocusable = /^(?:input|select|textarea|button)$/i,
+ rclickable = /^(?:a|area)$/i;
+
+jQuery.fn.extend( {
+ prop: function( name, value ) {
+ return access( this, jQuery.prop, name, value, arguments.length > 1 );
+ },
+
+ removeProp: function( name ) {
+ return this.each( function() {
+ delete this[ jQuery.propFix[ name ] || name ];
+ } );
+ }
+} );
+
+jQuery.extend( {
+ prop: function( elem, name, value ) {
+ var ret, hooks,
+ nType = elem.nodeType;
+
+ // Don't get/set properties on text, comment and attribute nodes
+ if ( nType === 3 || nType === 8 || nType === 2 ) {
+ return;
+ }
+
+ if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {
+
+ // Fix name and attach hooks
+ name = jQuery.propFix[ name ] || name;
+ hooks = jQuery.propHooks[ name ];
+ }
+
+ if ( value !== undefined ) {
+ if ( hooks && "set" in hooks &&
+ ( ret = hooks.set( elem, value, name ) ) !== undefined ) {
+ return ret;
+ }
+
+ return ( elem[ name ] = value );
+ }
+
+ if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {
+ return ret;
+ }
+
+ return elem[ name ];
+ },
+
+ propHooks: {
+ tabIndex: {
+ get: function( elem ) {
+
+ // Support: IE <=9 - 11 only
+ // elem.tabIndex doesn't always return the
+ // correct value when it hasn't been explicitly set
+ // https://web.archive.org/web/20141116233347/http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
+ // Use proper attribute retrieval(#12072)
+ var tabindex = jQuery.find.attr( elem, "tabindex" );
+
+ if ( tabindex ) {
+ return parseInt( tabindex, 10 );
+ }
+
+ if (
+ rfocusable.test( elem.nodeName ) ||
+ rclickable.test( elem.nodeName ) &&
+ elem.href
+ ) {
+ return 0;
+ }
+
+ return -1;
+ }
+ }
+ },
+
+ propFix: {
+ "for": "htmlFor",
+ "class": "className"
+ }
+} );
+
+// Support: IE <=11 only
+// Accessing the selectedIndex property
+// forces the browser to respect setting selected
+// on the option
+// The getter ensures a default option is selected
+// when in an optgroup
+// eslint rule "no-unused-expressions" is disabled for this code
+// since it considers such accessions noop
+if ( !support.optSelected ) {
+ jQuery.propHooks.selected = {
+ get: function( elem ) {
+
+ /* eslint no-unused-expressions: "off" */
+
+ var parent = elem.parentNode;
+ if ( parent && parent.parentNode ) {
+ parent.parentNode.selectedIndex;
+ }
+ return null;
+ },
+ set: function( elem ) {
+
+ /* eslint no-unused-expressions: "off" */
+
+ var parent = elem.parentNode;
+ if ( parent ) {
+ parent.selectedIndex;
+
+ if ( parent.parentNode ) {
+ parent.parentNode.selectedIndex;
+ }
+ }
+ }
+ };
+}
+
+jQuery.each( [
+ "tabIndex",
+ "readOnly",
+ "maxLength",
+ "cellSpacing",
+ "cellPadding",
+ "rowSpan",
+ "colSpan",
+ "useMap",
+ "frameBorder",
+ "contentEditable"
+], function() {
+ jQuery.propFix[ this.toLowerCase() ] = this;
+} );
+
+
+
+
+ // Strip and collapse whitespace according to HTML spec
+ // https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace
+ function stripAndCollapse( value ) {
+ var tokens = value.match( rnothtmlwhite ) || [];
+ return tokens.join( " " );
+ }
+
+
+function getClass( elem ) {
+ return elem.getAttribute && elem.getAttribute( "class" ) || "";
+}
+
+function classesToArray( value ) {
+ if ( Array.isArray( value ) ) {
+ return value;
+ }
+ if ( typeof value === "string" ) {
+ return value.match( rnothtmlwhite ) || [];
+ }
+ return [];
+}
+
+jQuery.fn.extend( {
+ addClass: function( value ) {
+ var classes, elem, cur, curValue, clazz, j, finalValue,
+ i = 0;
+
+ if ( isFunction( value ) ) {
+ return this.each( function( j ) {
+ jQuery( this ).addClass( value.call( this, j, getClass( this ) ) );
+ } );
+ }
+
+ classes = classesToArray( value );
+
+ if ( classes.length ) {
+ while ( ( elem = this[ i++ ] ) ) {
+ curValue = getClass( elem );
+ cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " );
+
+ if ( cur ) {
+ j = 0;
+ while ( ( clazz = classes[ j++ ] ) ) {
+ if ( cur.indexOf( " " + clazz + " " ) < 0 ) {
+ cur += clazz + " ";
+ }
+ }
+
+ // Only assign if different to avoid unneeded rendering.
+ finalValue = stripAndCollapse( cur );
+ if ( curValue !== finalValue ) {
+ elem.setAttribute( "class", finalValue );
+ }
+ }
+ }
+ }
+
+ return this;
+ },
+
+ removeClass: function( value ) {
+ var classes, elem, cur, curValue, clazz, j, finalValue,
+ i = 0;
+
+ if ( isFunction( value ) ) {
+ return this.each( function( j ) {
+ jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) );
+ } );
+ }
+
+ if ( !arguments.length ) {
+ return this.attr( "class", "" );
+ }
+
+ classes = classesToArray( value );
+
+ if ( classes.length ) {
+ while ( ( elem = this[ i++ ] ) ) {
+ curValue = getClass( elem );
+
+ // This expression is here for better compressibility (see addClass)
+ cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " );
+
+ if ( cur ) {
+ j = 0;
+ while ( ( clazz = classes[ j++ ] ) ) {
+
+ // Remove *all* instances
+ while ( cur.indexOf( " " + clazz + " " ) > -1 ) {
+ cur = cur.replace( " " + clazz + " ", " " );
+ }
+ }
+
+ // Only assign if different to avoid unneeded rendering.
+ finalValue = stripAndCollapse( cur );
+ if ( curValue !== finalValue ) {
+ elem.setAttribute( "class", finalValue );
+ }
+ }
+ }
+ }
+
+ return this;
+ },
+
+ toggleClass: function( value, stateVal ) {
+ var type = typeof value,
+ isValidValue = type === "string" || Array.isArray( value );
+
+ if ( typeof stateVal === "boolean" && isValidValue ) {
+ return stateVal ? this.addClass( value ) : this.removeClass( value );
+ }
+
+ if ( isFunction( value ) ) {
+ return this.each( function( i ) {
+ jQuery( this ).toggleClass(
+ value.call( this, i, getClass( this ), stateVal ),
+ stateVal
+ );
+ } );
+ }
+
+ return this.each( function() {
+ var className, i, self, classNames;
+
+ if ( isValidValue ) {
+
+ // Toggle individual class names
+ i = 0;
+ self = jQuery( this );
+ classNames = classesToArray( value );
+
+ while ( ( className = classNames[ i++ ] ) ) {
+
+ // Check each className given, space separated list
+ if ( self.hasClass( className ) ) {
+ self.removeClass( className );
+ } else {
+ self.addClass( className );
+ }
+ }
+
+ // Toggle whole class name
+ } else if ( value === undefined || type === "boolean" ) {
+ className = getClass( this );
+ if ( className ) {
+
+ // Store className if set
+ dataPriv.set( this, "__className__", className );
+ }
+
+ // If the element has a class name or if we're passed `false`,
+ // then remove the whole classname (if there was one, the above saved it).
+ // Otherwise bring back whatever was previously saved (if anything),
+ // falling back to the empty string if nothing was stored.
+ if ( this.setAttribute ) {
+ this.setAttribute( "class",
+ className || value === false ?
+ "" :
+ dataPriv.get( this, "__className__" ) || ""
+ );
+ }
+ }
+ } );
+ },
+
+ hasClass: function( selector ) {
+ var className, elem,
+ i = 0;
+
+ className = " " + selector + " ";
+ while ( ( elem = this[ i++ ] ) ) {
+ if ( elem.nodeType === 1 &&
+ ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+} );
+
+
+
+
+var rreturn = /\r/g;
+
+jQuery.fn.extend( {
+ val: function( value ) {
+ var hooks, ret, valueIsFunction,
+ elem = this[ 0 ];
+
+ if ( !arguments.length ) {
+ if ( elem ) {
+ hooks = jQuery.valHooks[ elem.type ] ||
+ jQuery.valHooks[ elem.nodeName.toLowerCase() ];
+
+ if ( hooks &&
+ "get" in hooks &&
+ ( ret = hooks.get( elem, "value" ) ) !== undefined
+ ) {
+ return ret;
+ }
+
+ ret = elem.value;
+
+ // Handle most common string cases
+ if ( typeof ret === "string" ) {
+ return ret.replace( rreturn, "" );
+ }
+
+ // Handle cases where value is null/undef or number
+ return ret == null ? "" : ret;
+ }
+
+ return;
+ }
+
+ valueIsFunction = isFunction( value );
+
+ return this.each( function( i ) {
+ var val;
+
+ if ( this.nodeType !== 1 ) {
+ return;
+ }
+
+ if ( valueIsFunction ) {
+ val = value.call( this, i, jQuery( this ).val() );
+ } else {
+ val = value;
+ }
+
+ // Treat null/undefined as ""; convert numbers to string
+ if ( val == null ) {
+ val = "";
+
+ } else if ( typeof val === "number" ) {
+ val += "";
+
+ } else if ( Array.isArray( val ) ) {
+ val = jQuery.map( val, function( value ) {
+ return value == null ? "" : value + "";
+ } );
+ }
+
+ hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];
+
+ // If set returns undefined, fall back to normal setting
+ if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) {
+ this.value = val;
+ }
+ } );
+ }
+} );
+
+jQuery.extend( {
+ valHooks: {
+ option: {
+ get: function( elem ) {
+
+ var val = jQuery.find.attr( elem, "value" );
+ return val != null ?
+ val :
+
+ // Support: IE <=10 - 11 only
+ // option.text throws exceptions (#14686, #14858)
+ // Strip and collapse whitespace
+ // https://html.spec.whatwg.org/#strip-and-collapse-whitespace
+ stripAndCollapse( jQuery.text( elem ) );
+ }
+ },
+ select: {
+ get: function( elem ) {
+ var value, option, i,
+ options = elem.options,
+ index = elem.selectedIndex,
+ one = elem.type === "select-one",
+ values = one ? null : [],
+ max = one ? index + 1 : options.length;
+
+ if ( index < 0 ) {
+ i = max;
+
+ } else {
+ i = one ? index : 0;
+ }
+
+ // Loop through all the selected options
+ for ( ; i < max; i++ ) {
+ option = options[ i ];
+
+ // Support: IE <=9 only
+ // IE8-9 doesn't update selected after form reset (#2551)
+ if ( ( option.selected || i === index ) &&
+
+ // Don't return options that are disabled or in a disabled optgroup
+ !option.disabled &&
+ ( !option.parentNode.disabled ||
+ !nodeName( option.parentNode, "optgroup" ) ) ) {
+
+ // Get the specific value for the option
+ value = jQuery( option ).val();
+
+ // We don't need an array for one selects
+ if ( one ) {
+ return value;
+ }
+
+ // Multi-Selects return an array
+ values.push( value );
+ }
+ }
+
+ return values;
+ },
+
+ set: function( elem, value ) {
+ var optionSet, option,
+ options = elem.options,
+ values = jQuery.makeArray( value ),
+ i = options.length;
+
+ while ( i-- ) {
+ option = options[ i ];
+
+ /* eslint-disable no-cond-assign */
+
+ if ( option.selected =
+ jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1
+ ) {
+ optionSet = true;
+ }
+
+ /* eslint-enable no-cond-assign */
+ }
+
+ // Force browsers to behave consistently when non-matching value is set
+ if ( !optionSet ) {
+ elem.selectedIndex = -1;
+ }
+ return values;
+ }
+ }
+ }
+} );
+
+// Radios and checkboxes getter/setter
+jQuery.each( [ "radio", "checkbox" ], function() {
+ jQuery.valHooks[ this ] = {
+ set: function( elem, value ) {
+ if ( Array.isArray( value ) ) {
+ return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 );
+ }
+ }
+ };
+ if ( !support.checkOn ) {
+ jQuery.valHooks[ this ].get = function( elem ) {
+ return elem.getAttribute( "value" ) === null ? "on" : elem.value;
+ };
+ }
+} );
+
+
+
+
+// Return jQuery for attributes-only inclusion
+
+
+support.focusin = "onfocusin" in window;
+
+
+var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/,
+ stopPropagationCallback = function( e ) {
+ e.stopPropagation();
+ };
+
+jQuery.extend( jQuery.event, {
+
+ trigger: function( event, data, elem, onlyHandlers ) {
+
+ var i, cur, tmp, bubbleType, ontype, handle, special, lastElement,
+ eventPath = [ elem || document ],
+ type = hasOwn.call( event, "type" ) ? event.type : event,
+ namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : [];
+
+ cur = lastElement = tmp = elem = elem || document;
+
+ // Don't do events on text and comment nodes
+ if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
+ return;
+ }
+
+ // focus/blur morphs to focusin/out; ensure we're not firing them right now
+ if ( rfocusMorph.test( type + jQuery.event.triggered ) ) {
+ return;
+ }
+
+ if ( type.indexOf( "." ) > -1 ) {
+
+ // Namespaced trigger; create a regexp to match event type in handle()
+ namespaces = type.split( "." );
+ type = namespaces.shift();
+ namespaces.sort();
+ }
+ ontype = type.indexOf( ":" ) < 0 && "on" + type;
+
+ // Caller can pass in a jQuery.Event object, Object, or just an event type string
+ event = event[ jQuery.expando ] ?
+ event :
+ new jQuery.Event( type, typeof event === "object" && event );
+
+ // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true)
+ event.isTrigger = onlyHandlers ? 2 : 3;
+ event.namespace = namespaces.join( "." );
+ event.rnamespace = event.namespace ?
+ new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) :
+ null;
+
+ // Clean up the event in case it is being reused
+ event.result = undefined;
+ if ( !event.target ) {
+ event.target = elem;
+ }
+
+ // Clone any incoming data and prepend the event, creating the handler arg list
+ data = data == null ?
+ [ event ] :
+ jQuery.makeArray( data, [ event ] );
+
+ // Allow special events to draw outside the lines
+ special = jQuery.event.special[ type ] || {};
+ if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {
+ return;
+ }
+
+ // Determine event propagation path in advance, per W3C events spec (#9951)
+ // Bubble up to document, then to window; watch for a global ownerDocument var (#9724)
+ if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) {
+
+ bubbleType = special.delegateType || type;
+ if ( !rfocusMorph.test( bubbleType + type ) ) {
+ cur = cur.parentNode;
+ }
+ for ( ; cur; cur = cur.parentNode ) {
+ eventPath.push( cur );
+ tmp = cur;
+ }
+
+ // Only add window if we got to document (e.g., not plain obj or detached DOM)
+ if ( tmp === ( elem.ownerDocument || document ) ) {
+ eventPath.push( tmp.defaultView || tmp.parentWindow || window );
+ }
+ }
+
+ // Fire handlers on the event path
+ i = 0;
+ while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) {
+ lastElement = cur;
+ event.type = i > 1 ?
+ bubbleType :
+ special.bindType || type;
+
+ // jQuery handler
+ handle = ( dataPriv.get( cur, "events" ) || {} )[ event.type ] &&
+ dataPriv.get( cur, "handle" );
+ if ( handle ) {
+ handle.apply( cur, data );
+ }
+
+ // Native handler
+ handle = ontype && cur[ ontype ];
+ if ( handle && handle.apply && acceptData( cur ) ) {
+ event.result = handle.apply( cur, data );
+ if ( event.result === false ) {
+ event.preventDefault();
+ }
+ }
+ }
+ event.type = type;
+
+ // If nobody prevented the default action, do it now
+ if ( !onlyHandlers && !event.isDefaultPrevented() ) {
+
+ if ( ( !special._default ||
+ special._default.apply( eventPath.pop(), data ) === false ) &&
+ acceptData( elem ) ) {
+
+ // Call a native DOM method on the target with the same name as the event.
+ // Don't do default actions on window, that's where global variables be (#6170)
+ if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) {
+
+ // Don't re-trigger an onFOO event when we call its FOO() method
+ tmp = elem[ ontype ];
+
+ if ( tmp ) {
+ elem[ ontype ] = null;
+ }
+
+ // Prevent re-triggering of the same event, since we already bubbled it above
+ jQuery.event.triggered = type;
+
+ if ( event.isPropagationStopped() ) {
+ lastElement.addEventListener( type, stopPropagationCallback );
+ }
+
+ elem[ type ]();
+
+ if ( event.isPropagationStopped() ) {
+ lastElement.removeEventListener( type, stopPropagationCallback );
+ }
+
+ jQuery.event.triggered = undefined;
+
+ if ( tmp ) {
+ elem[ ontype ] = tmp;
+ }
+ }
+ }
+ }
+
+ return event.result;
+ },
+
+ // Piggyback on a donor event to simulate a different one
+ // Used only for `focus(in | out)` events
+ simulate: function( type, elem, event ) {
+ var e = jQuery.extend(
+ new jQuery.Event(),
+ event,
+ {
+ type: type,
+ isSimulated: true
+ }
+ );
+
+ jQuery.event.trigger( e, null, elem );
+ }
+
+} );
+
+jQuery.fn.extend( {
+
+ trigger: function( type, data ) {
+ return this.each( function() {
+ jQuery.event.trigger( type, data, this );
+ } );
+ },
+ triggerHandler: function( type, data ) {
+ var elem = this[ 0 ];
+ if ( elem ) {
+ return jQuery.event.trigger( type, data, elem, true );
+ }
+ }
+} );
+
+
+// Support: Firefox <=44
+// Firefox doesn't have focus(in | out) events
+// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787
+//
+// Support: Chrome <=48 - 49, Safari <=9.0 - 9.1
+// focus(in | out) events fire after focus & blur events,
+// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order
+// Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857
+if ( !support.focusin ) {
+ jQuery.each( { focus: "focusin", blur: "focusout" }, function( orig, fix ) {
+
+ // Attach a single capturing handler on the document while someone wants focusin/focusout
+ var handler = function( event ) {
+ jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) );
+ };
+
+ jQuery.event.special[ fix ] = {
+ setup: function() {
+ var doc = this.ownerDocument || this,
+ attaches = dataPriv.access( doc, fix );
+
+ if ( !attaches ) {
+ doc.addEventListener( orig, handler, true );
+ }
+ dataPriv.access( doc, fix, ( attaches || 0 ) + 1 );
+ },
+ teardown: function() {
+ var doc = this.ownerDocument || this,
+ attaches = dataPriv.access( doc, fix ) - 1;
+
+ if ( !attaches ) {
+ doc.removeEventListener( orig, handler, true );
+ dataPriv.remove( doc, fix );
+
+ } else {
+ dataPriv.access( doc, fix, attaches );
+ }
+ }
+ };
+ } );
+}
+var location = window.location;
+
+var nonce = Date.now();
+
+var rquery = ( /\?/ );
+
+
+
+// Cross-browser xml parsing
+jQuery.parseXML = function( data ) {
+ var xml;
+ if ( !data || typeof data !== "string" ) {
+ return null;
+ }
+
+ // Support: IE 9 - 11 only
+ // IE throws on parseFromString with invalid input.
+ try {
+ xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" );
+ } catch ( e ) {
+ xml = undefined;
+ }
+
+ if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) {
+ jQuery.error( "Invalid XML: " + data );
+ }
+ return xml;
+};
+
+
+var
+ rbracket = /\[\]$/,
+ rCRLF = /\r?\n/g,
+ rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i,
+ rsubmittable = /^(?:input|select|textarea|keygen)/i;
+
+function buildParams( prefix, obj, traditional, add ) {
+ var name;
+
+ if ( Array.isArray( obj ) ) {
+
+ // Serialize array item.
+ jQuery.each( obj, function( i, v ) {
+ if ( traditional || rbracket.test( prefix ) ) {
+
+ // Treat each array item as a scalar.
+ add( prefix, v );
+
+ } else {
+
+ // Item is non-scalar (array or object), encode its numeric index.
+ buildParams(
+ prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]",
+ v,
+ traditional,
+ add
+ );
+ }
+ } );
+
+ } else if ( !traditional && toType( obj ) === "object" ) {
+
+ // Serialize object item.
+ for ( name in obj ) {
+ buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add );
+ }
+
+ } else {
+
+ // Serialize scalar item.
+ add( prefix, obj );
+ }
+}
+
+// Serialize an array of form elements or a set of
+// key/values into a query string
+jQuery.param = function( a, traditional ) {
+ var prefix,
+ s = [],
+ add = function( key, valueOrFunction ) {
+
+ // If value is a function, invoke it and use its return value
+ var value = isFunction( valueOrFunction ) ?
+ valueOrFunction() :
+ valueOrFunction;
+
+ s[ s.length ] = encodeURIComponent( key ) + "=" +
+ encodeURIComponent( value == null ? "" : value );
+ };
+
+ if ( a == null ) {
+ return "";
+ }
+
+ // If an array was passed in, assume that it is an array of form elements.
+ if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {
+
+ // Serialize the form elements
+ jQuery.each( a, function() {
+ add( this.name, this.value );
+ } );
+
+ } else {
+
+ // If traditional, encode the "old" way (the way 1.3.2 or older
+ // did it), otherwise encode params recursively.
+ for ( prefix in a ) {
+ buildParams( prefix, a[ prefix ], traditional, add );
+ }
+ }
+
+ // Return the resulting serialization
+ return s.join( "&" );
+};
+
+jQuery.fn.extend( {
+ serialize: function() {
+ return jQuery.param( this.serializeArray() );
+ },
+ serializeArray: function() {
+ return this.map( function() {
+
+ // Can add propHook for "elements" to filter or add form elements
+ var elements = jQuery.prop( this, "elements" );
+ return elements ? jQuery.makeArray( elements ) : this;
+ } )
+ .filter( function() {
+ var type = this.type;
+
+ // Use .is( ":disabled" ) so that fieldset[disabled] works
+ return this.name && !jQuery( this ).is( ":disabled" ) &&
+ rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&
+ ( this.checked || !rcheckableType.test( type ) );
+ } )
+ .map( function( i, elem ) {
+ var val = jQuery( this ).val();
+
+ if ( val == null ) {
+ return null;
+ }
+
+ if ( Array.isArray( val ) ) {
+ return jQuery.map( val, function( val ) {
+ return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+ } );
+ }
+
+ return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+ } ).get();
+ }
+} );
+
+
+var
+ r20 = /%20/g,
+ rhash = /#.*$/,
+ rantiCache = /([?&])_=[^&]*/,
+ rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg,
+
+ // #7653, #8125, #8152: local protocol detection
+ rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/,
+ rnoContent = /^(?:GET|HEAD)$/,
+ rprotocol = /^\/\//,
+
+ /* Prefilters
+ * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)
+ * 2) These are called:
+ * - BEFORE asking for a transport
+ * - AFTER param serialization (s.data is a string if s.processData is true)
+ * 3) key is the dataType
+ * 4) the catchall symbol "*" can be used
+ * 5) execution will start with transport dataType and THEN continue down to "*" if needed
+ */
+ prefilters = {},
+
+ /* Transports bindings
+ * 1) key is the dataType
+ * 2) the catchall symbol "*" can be used
+ * 3) selection will start with transport dataType and THEN go to "*" if needed
+ */
+ transports = {},
+
+ // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression
+ allTypes = "*/".concat( "*" ),
+
+ // Anchor tag for parsing the document origin
+ originAnchor = document.createElement( "a" );
+ originAnchor.href = location.href;
+
+// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport
+function addToPrefiltersOrTransports( structure ) {
+
+ // dataTypeExpression is optional and defaults to "*"
+ return function( dataTypeExpression, func ) {
+
+ if ( typeof dataTypeExpression !== "string" ) {
+ func = dataTypeExpression;
+ dataTypeExpression = "*";
+ }
+
+ var dataType,
+ i = 0,
+ dataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || [];
+
+ if ( isFunction( func ) ) {
+
+ // For each dataType in the dataTypeExpression
+ while ( ( dataType = dataTypes[ i++ ] ) ) {
+
+ // Prepend if requested
+ if ( dataType[ 0 ] === "+" ) {
+ dataType = dataType.slice( 1 ) || "*";
+ ( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func );
+
+ // Otherwise append
+ } else {
+ ( structure[ dataType ] = structure[ dataType ] || [] ).push( func );
+ }
+ }
+ }
+ };
+}
+
+// Base inspection function for prefilters and transports
+function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {
+
+ var inspected = {},
+ seekingTransport = ( structure === transports );
+
+ function inspect( dataType ) {
+ var selected;
+ inspected[ dataType ] = true;
+ jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {
+ var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );
+ if ( typeof dataTypeOrTransport === "string" &&
+ !seekingTransport && !inspected[ dataTypeOrTransport ] ) {
+
+ options.dataTypes.unshift( dataTypeOrTransport );
+ inspect( dataTypeOrTransport );
+ return false;
+ } else if ( seekingTransport ) {
+ return !( selected = dataTypeOrTransport );
+ }
+ } );
+ return selected;
+ }
+
+ return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" );
+}
+
+// A special extend for ajax options
+// that takes "flat" options (not to be deep extended)
+// Fixes #9887
+function ajaxExtend( target, src ) {
+ var key, deep,
+ flatOptions = jQuery.ajaxSettings.flatOptions || {};
+
+ for ( key in src ) {
+ if ( src[ key ] !== undefined ) {
+ ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ];
+ }
+ }
+ if ( deep ) {
+ jQuery.extend( true, target, deep );
+ }
+
+ return target;
+}
+
+/* Handles responses to an ajax request:
+ * - finds the right dataType (mediates between content-type and expected dataType)
+ * - returns the corresponding response
+ */
+function ajaxHandleResponses( s, jqXHR, responses ) {
+
+ var ct, type, finalDataType, firstDataType,
+ contents = s.contents,
+ dataTypes = s.dataTypes;
+
+ // Remove auto dataType and get content-type in the process
+ while ( dataTypes[ 0 ] === "*" ) {
+ dataTypes.shift();
+ if ( ct === undefined ) {
+ ct = s.mimeType || jqXHR.getResponseHeader( "Content-Type" );
+ }
+ }
+
+ // Check if we're dealing with a known content-type
+ if ( ct ) {
+ for ( type in contents ) {
+ if ( contents[ type ] && contents[ type ].test( ct ) ) {
+ dataTypes.unshift( type );
+ break;
+ }
+ }
+ }
+
+ // Check to see if we have a response for the expected dataType
+ if ( dataTypes[ 0 ] in responses ) {
+ finalDataType = dataTypes[ 0 ];
+ } else {
+
+ // Try convertible dataTypes
+ for ( type in responses ) {
+ if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[ 0 ] ] ) {
+ finalDataType = type;
+ break;
+ }
+ if ( !firstDataType ) {
+ firstDataType = type;
+ }
+ }
+
+ // Or just use first one
+ finalDataType = finalDataType || firstDataType;
+ }
+
+ // If we found a dataType
+ // We add the dataType to the list if needed
+ // and return the corresponding response
+ if ( finalDataType ) {
+ if ( finalDataType !== dataTypes[ 0 ] ) {
+ dataTypes.unshift( finalDataType );
+ }
+ return responses[ finalDataType ];
+ }
+}
+
+/* Chain conversions given the request and the original response
+ * Also sets the responseXXX fields on the jqXHR instance
+ */
+function ajaxConvert( s, response, jqXHR, isSuccess ) {
+ var conv2, current, conv, tmp, prev,
+ converters = {},
+
+ // Work with a copy of dataTypes in case we need to modify it for conversion
+ dataTypes = s.dataTypes.slice();
+
+ // Create converters map with lowercased keys
+ if ( dataTypes[ 1 ] ) {
+ for ( conv in s.converters ) {
+ converters[ conv.toLowerCase() ] = s.converters[ conv ];
+ }
+ }
+
+ current = dataTypes.shift();
+
+ // Convert to each sequential dataType
+ while ( current ) {
+
+ if ( s.responseFields[ current ] ) {
+ jqXHR[ s.responseFields[ current ] ] = response;
+ }
+
+ // Apply the dataFilter if provided
+ if ( !prev && isSuccess && s.dataFilter ) {
+ response = s.dataFilter( response, s.dataType );
+ }
+
+ prev = current;
+ current = dataTypes.shift();
+
+ if ( current ) {
+
+ // There's only work to do if current dataType is non-auto
+ if ( current === "*" ) {
+
+ current = prev;
+
+ // Convert response if prev dataType is non-auto and differs from current
+ } else if ( prev !== "*" && prev !== current ) {
+
+ // Seek a direct converter
+ conv = converters[ prev + " " + current ] || converters[ "* " + current ];
+
+ // If none found, seek a pair
+ if ( !conv ) {
+ for ( conv2 in converters ) {
+
+ // If conv2 outputs current
+ tmp = conv2.split( " " );
+ if ( tmp[ 1 ] === current ) {
+
+ // If prev can be converted to accepted input
+ conv = converters[ prev + " " + tmp[ 0 ] ] ||
+ converters[ "* " + tmp[ 0 ] ];
+ if ( conv ) {
+
+ // Condense equivalence converters
+ if ( conv === true ) {
+ conv = converters[ conv2 ];
+
+ // Otherwise, insert the intermediate dataType
+ } else if ( converters[ conv2 ] !== true ) {
+ current = tmp[ 0 ];
+ dataTypes.unshift( tmp[ 1 ] );
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ // Apply converter (if not an equivalence)
+ if ( conv !== true ) {
+
+ // Unless errors are allowed to bubble, catch and return them
+ if ( conv && s.throws ) {
+ response = conv( response );
+ } else {
+ try {
+ response = conv( response );
+ } catch ( e ) {
+ return {
+ state: "parsererror",
+ error: conv ? e : "No conversion from " + prev + " to " + current
+ };
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return { state: "success", data: response };
+}
+
+jQuery.extend( {
+
+ // Counter for holding the number of active queries
+ active: 0,
+
+ // Last-Modified header cache for next request
+ lastModified: {},
+ etag: {},
+
+ ajaxSettings: {
+ url: location.href,
+ type: "GET",
+ isLocal: rlocalProtocol.test( location.protocol ),
+ global: true,
+ processData: true,
+ async: true,
+ contentType: "application/x-www-form-urlencoded; charset=UTF-8",
+
+ /*
+ timeout: 0,
+ data: null,
+ dataType: null,
+ username: null,
+ password: null,
+ cache: null,
+ throws: false,
+ traditional: false,
+ headers: {},
+ */
+
+ accepts: {
+ "*": allTypes,
+ text: "text/plain",
+ html: "text/html",
+ xml: "application/xml, text/xml",
+ json: "application/json, text/javascript"
+ },
+
+ contents: {
+ xml: /\bxml\b/,
+ html: /\bhtml/,
+ json: /\bjson\b/
+ },
+
+ responseFields: {
+ xml: "responseXML",
+ text: "responseText",
+ json: "responseJSON"
+ },
+
+ // Data converters
+ // Keys separate source (or catchall "*") and destination types with a single space
+ converters: {
+
+ // Convert anything to text
+ "* text": String,
+
+ // Text to html (true = no transformation)
+ "text html": true,
+
+ // Evaluate text as a json expression
+ "text json": JSON.parse,
+
+ // Parse text as xml
+ "text xml": jQuery.parseXML
+ },
+
+ // For options that shouldn't be deep extended:
+ // you can add your own custom options here if
+ // and when you create one that shouldn't be
+ // deep extended (see ajaxExtend)
+ flatOptions: {
+ url: true,
+ context: true
+ }
+ },
+
+ // Creates a full fledged settings object into target
+ // with both ajaxSettings and settings fields.
+ // If target is omitted, writes into ajaxSettings.
+ ajaxSetup: function( target, settings ) {
+ return settings ?
+
+ // Building a settings object
+ ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) :
+
+ // Extending ajaxSettings
+ ajaxExtend( jQuery.ajaxSettings, target );
+ },
+
+ ajaxPrefilter: addToPrefiltersOrTransports( prefilters ),
+ ajaxTransport: addToPrefiltersOrTransports( transports ),
+
+ // Main method
+ ajax: function( url, options ) {
+
+ // If url is an object, simulate pre-1.5 signature
+ if ( typeof url === "object" ) {
+ options = url;
+ url = undefined;
+ }
+
+ // Force options to be an object
+ options = options || {};
+
+ var transport,
+
+ // URL without anti-cache param
+ cacheURL,
+
+ // Response headers
+ responseHeadersString,
+ responseHeaders,
+
+ // timeout handle
+ timeoutTimer,
+
+ // Url cleanup var
+ urlAnchor,
+
+ // Request state (becomes false upon send and true upon completion)
+ completed,
+
+ // To know if global events are to be dispatched
+ fireGlobals,
+
+ // Loop variable
+ i,
+
+ // uncached part of the url
+ uncached,
+
+ // Create the final options object
+ s = jQuery.ajaxSetup( {}, options ),
+
+ // Callbacks context
+ callbackContext = s.context || s,
+
+ // Context for global events is callbackContext if it is a DOM node or jQuery collection
+ globalEventContext = s.context &&
+ ( callbackContext.nodeType || callbackContext.jquery ) ?
+ jQuery( callbackContext ) :
+ jQuery.event,
+
+ // Deferreds
+ deferred = jQuery.Deferred(),
+ completeDeferred = jQuery.Callbacks( "once memory" ),
+
+ // Status-dependent callbacks
+ statusCode = s.statusCode || {},
+
+ // Headers (they are sent all at once)
+ requestHeaders = {},
+ requestHeadersNames = {},
+
+ // Default abort message
+ strAbort = "canceled",
+
+ // Fake xhr
+ jqXHR = {
+ readyState: 0,
+
+ // Builds headers hashtable if needed
+ getResponseHeader: function( key ) {
+ var match;
+ if ( completed ) {
+ if ( !responseHeaders ) {
+ responseHeaders = {};
+ while ( ( match = rheaders.exec( responseHeadersString ) ) ) {
+ responseHeaders[ match[ 1 ].toLowerCase() + " " ] =
+ ( responseHeaders[ match[ 1 ].toLowerCase() + " " ] || [] )
+ .concat( match[ 2 ] );
+ }
+ }
+ match = responseHeaders[ key.toLowerCase() + " " ];
+ }
+ return match == null ? null : match.join( ", " );
+ },
+
+ // Raw string
+ getAllResponseHeaders: function() {
+ return completed ? responseHeadersString : null;
+ },
+
+ // Caches the header
+ setRequestHeader: function( name, value ) {
+ if ( completed == null ) {
+ name = requestHeadersNames[ name.toLowerCase() ] =
+ requestHeadersNames[ name.toLowerCase() ] || name;
+ requestHeaders[ name ] = value;
+ }
+ return this;
+ },
+
+ // Overrides response content-type header
+ overrideMimeType: function( type ) {
+ if ( completed == null ) {
+ s.mimeType = type;
+ }
+ return this;
+ },
+
+ // Status-dependent callbacks
+ statusCode: function( map ) {
+ var code;
+ if ( map ) {
+ if ( completed ) {
+
+ // Execute the appropriate callbacks
+ jqXHR.always( map[ jqXHR.status ] );
+ } else {
+
+ // Lazy-add the new callbacks in a way that preserves old ones
+ for ( code in map ) {
+ statusCode[ code ] = [ statusCode[ code ], map[ code ] ];
+ }
+ }
+ }
+ return this;
+ },
+
+ // Cancel the request
+ abort: function( statusText ) {
+ var finalText = statusText || strAbort;
+ if ( transport ) {
+ transport.abort( finalText );
+ }
+ done( 0, finalText );
+ return this;
+ }
+ };
+
+ // Attach deferreds
+ deferred.promise( jqXHR );
+
+ // Add protocol if not provided (prefilters might expect it)
+ // Handle falsy url in the settings object (#10093: consistency with old signature)
+ // We also use the url parameter if available
+ s.url = ( ( url || s.url || location.href ) + "" )
+ .replace( rprotocol, location.protocol + "//" );
+
+ // Alias method option to type as per ticket #12004
+ s.type = options.method || options.type || s.method || s.type;
+
+ // Extract dataTypes list
+ s.dataTypes = ( s.dataType || "*" ).toLowerCase().match( rnothtmlwhite ) || [ "" ];
+
+ // A cross-domain request is in order when the origin doesn't match the current origin.
+ if ( s.crossDomain == null ) {
+ urlAnchor = document.createElement( "a" );
+
+ // Support: IE <=8 - 11, Edge 12 - 15
+ // IE throws exception on accessing the href property if url is malformed,
+ // e.g. http://example.com:80x/
+ try {
+ urlAnchor.href = s.url;
+
+ // Support: IE <=8 - 11 only
+ // Anchor's host property isn't correctly set when s.url is relative
+ urlAnchor.href = urlAnchor.href;
+ s.crossDomain = originAnchor.protocol + "//" + originAnchor.host !==
+ urlAnchor.protocol + "//" + urlAnchor.host;
+ } catch ( e ) {
+
+ // If there is an error parsing the URL, assume it is crossDomain,
+ // it can be rejected by the transport if it is invalid
+ s.crossDomain = true;
+ }
+ }
+
+ // Convert data if not already a string
+ if ( s.data && s.processData && typeof s.data !== "string" ) {
+ s.data = jQuery.param( s.data, s.traditional );
+ }
+
+ // Apply prefilters
+ inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
+
+ // If request was aborted inside a prefilter, stop there
+ if ( completed ) {
+ return jqXHR;
+ }
+
+ // We can fire global events as of now if asked to
+ // Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118)
+ fireGlobals = jQuery.event && s.global;
+
+ // Watch for a new set of requests
+ if ( fireGlobals && jQuery.active++ === 0 ) {
+ jQuery.event.trigger( "ajaxStart" );
+ }
+
+ // Uppercase the type
+ s.type = s.type.toUpperCase();
+
+ // Determine if request has content
+ s.hasContent = !rnoContent.test( s.type );
+
+ // Save the URL in case we're toying with the If-Modified-Since
+ // and/or If-None-Match header later on
+ // Remove hash to simplify url manipulation
+ cacheURL = s.url.replace( rhash, "" );
+
+ // More options handling for requests with no content
+ if ( !s.hasContent ) {
+
+ // Remember the hash so we can put it back
+ uncached = s.url.slice( cacheURL.length );
+
+ // If data is available and should be processed, append data to url
+ if ( s.data && ( s.processData || typeof s.data === "string" ) ) {
+ cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data;
+
+ // #9682: remove data so that it's not used in an eventual retry
+ delete s.data;
+ }
+
+ // Add or update anti-cache param if needed
+ if ( s.cache === false ) {
+ cacheURL = cacheURL.replace( rantiCache, "$1" );
+ uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce++ ) + uncached;
+ }
+
+ // Put hash and anti-cache on the URL that will be requested (gh-1732)
+ s.url = cacheURL + uncached;
+
+ // Change '%20' to '+' if this is encoded form body content (gh-2658)
+ } else if ( s.data && s.processData &&
+ ( s.contentType || "" ).indexOf( "application/x-www-form-urlencoded" ) === 0 ) {
+ s.data = s.data.replace( r20, "+" );
+ }
+
+ // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+ if ( s.ifModified ) {
+ if ( jQuery.lastModified[ cacheURL ] ) {
+ jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] );
+ }
+ if ( jQuery.etag[ cacheURL ] ) {
+ jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] );
+ }
+ }
+
+ // Set the correct header, if data is being sent
+ if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {
+ jqXHR.setRequestHeader( "Content-Type", s.contentType );
+ }
+
+ // Set the Accepts header for the server, depending on the dataType
+ jqXHR.setRequestHeader(
+ "Accept",
+ s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ?
+ s.accepts[ s.dataTypes[ 0 ] ] +
+ ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) :
+ s.accepts[ "*" ]
+ );
+
+ // Check for headers option
+ for ( i in s.headers ) {
+ jqXHR.setRequestHeader( i, s.headers[ i ] );
+ }
+
+ // Allow custom headers/mimetypes and early abort
+ if ( s.beforeSend &&
+ ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) {
+
+ // Abort if not done already and return
+ return jqXHR.abort();
+ }
+
+ // Aborting is no longer a cancellation
+ strAbort = "abort";
+
+ // Install callbacks on deferreds
+ completeDeferred.add( s.complete );
+ jqXHR.done( s.success );
+ jqXHR.fail( s.error );
+
+ // Get transport
+ transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );
+
+ // If no transport, we auto-abort
+ if ( !transport ) {
+ done( -1, "No Transport" );
+ } else {
+ jqXHR.readyState = 1;
+
+ // Send global event
+ if ( fireGlobals ) {
+ globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] );
+ }
+
+ // If request was aborted inside ajaxSend, stop there
+ if ( completed ) {
+ return jqXHR;
+ }
+
+ // Timeout
+ if ( s.async && s.timeout > 0 ) {
+ timeoutTimer = window.setTimeout( function() {
+ jqXHR.abort( "timeout" );
+ }, s.timeout );
+ }
+
+ try {
+ completed = false;
+ transport.send( requestHeaders, done );
+ } catch ( e ) {
+
+ // Rethrow post-completion exceptions
+ if ( completed ) {
+ throw e;
+ }
+
+ // Propagate others as results
+ done( -1, e );
+ }
+ }
+
+ // Callback for when everything is done
+ function done( status, nativeStatusText, responses, headers ) {
+ var isSuccess, success, error, response, modified,
+ statusText = nativeStatusText;
+
+ // Ignore repeat invocations
+ if ( completed ) {
+ return;
+ }
+
+ completed = true;
+
+ // Clear timeout if it exists
+ if ( timeoutTimer ) {
+ window.clearTimeout( timeoutTimer );
+ }
+
+ // Dereference transport for early garbage collection
+ // (no matter how long the jqXHR object will be used)
+ transport = undefined;
+
+ // Cache response headers
+ responseHeadersString = headers || "";
+
+ // Set readyState
+ jqXHR.readyState = status > 0 ? 4 : 0;
+
+ // Determine if successful
+ isSuccess = status >= 200 && status < 300 || status === 304;
+
+ // Get response data
+ if ( responses ) {
+ response = ajaxHandleResponses( s, jqXHR, responses );
+ }
+
+ // Convert no matter what (that way responseXXX fields are always set)
+ response = ajaxConvert( s, response, jqXHR, isSuccess );
+
+ // If successful, handle type chaining
+ if ( isSuccess ) {
+
+ // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+ if ( s.ifModified ) {
+ modified = jqXHR.getResponseHeader( "Last-Modified" );
+ if ( modified ) {
+ jQuery.lastModified[ cacheURL ] = modified;
+ }
+ modified = jqXHR.getResponseHeader( "etag" );
+ if ( modified ) {
+ jQuery.etag[ cacheURL ] = modified;
+ }
+ }
+
+ // if no content
+ if ( status === 204 || s.type === "HEAD" ) {
+ statusText = "nocontent";
+
+ // if not modified
+ } else if ( status === 304 ) {
+ statusText = "notmodified";
+
+ // If we have data, let's convert it
+ } else {
+ statusText = response.state;
+ success = response.data;
+ error = response.error;
+ isSuccess = !error;
+ }
+ } else {
+
+ // Extract error from statusText and normalize for non-aborts
+ error = statusText;
+ if ( status || !statusText ) {
+ statusText = "error";
+ if ( status < 0 ) {
+ status = 0;
+ }
+ }
+ }
+
+ // Set data for the fake xhr object
+ jqXHR.status = status;
+ jqXHR.statusText = ( nativeStatusText || statusText ) + "";
+
+ // Success/Error
+ if ( isSuccess ) {
+ deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
+ } else {
+ deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
+ }
+
+ // Status-dependent callbacks
+ jqXHR.statusCode( statusCode );
+ statusCode = undefined;
+
+ if ( fireGlobals ) {
+ globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError",
+ [ jqXHR, s, isSuccess ? success : error ] );
+ }
+
+ // Complete
+ completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );
+
+ if ( fireGlobals ) {
+ globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] );
+
+ // Handle the global AJAX counter
+ if ( !( --jQuery.active ) ) {
+ jQuery.event.trigger( "ajaxStop" );
+ }
+ }
+ }
+
+ return jqXHR;
+ },
+
+ getJSON: function( url, data, callback ) {
+ return jQuery.get( url, data, callback, "json" );
+ },
+
+ getScript: function( url, callback ) {
+ return jQuery.get( url, undefined, callback, "script" );
+ }
+} );
+
+jQuery.each( [ "get", "post" ], function( i, method ) {
+ jQuery[ method ] = function( url, data, callback, type ) {
+
+ // Shift arguments if data argument was omitted
+ if ( isFunction( data ) ) {
+ type = type || callback;
+ callback = data;
+ data = undefined;
+ }
+
+ // The url can be an options object (which then must have .url)
+ return jQuery.ajax( jQuery.extend( {
+ url: url,
+ type: method,
+ dataType: type,
+ data: data,
+ success: callback
+ }, jQuery.isPlainObject( url ) && url ) );
+ };
+} );
+
+
+jQuery._evalUrl = function( url, options ) {
+ return jQuery.ajax( {
+ url: url,
+
+ // Make this explicit, since user can override this through ajaxSetup (#11264)
+ type: "GET",
+ dataType: "script",
+ cache: true,
+ async: false,
+ global: false,
+
+ // Only evaluate the response if it is successful (gh-4126)
+ // dataFilter is not invoked for failure responses, so using it instead
+ // of the default converter is kludgy but it works.
+ converters: {
+ "text script": function() {}
+ },
+ dataFilter: function( response ) {
+ jQuery.globalEval( response, options );
+ }
+ } );
+};
+
+
+jQuery.fn.extend( {
+ wrapAll: function( html ) {
+ var wrap;
+
+ if ( this[ 0 ] ) {
+ if ( isFunction( html ) ) {
+ html = html.call( this[ 0 ] );
+ }
+
+ // The elements to wrap the target around
+ wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true );
+
+ if ( this[ 0 ].parentNode ) {
+ wrap.insertBefore( this[ 0 ] );
+ }
+
+ wrap.map( function() {
+ var elem = this;
+
+ while ( elem.firstElementChild ) {
+ elem = elem.firstElementChild;
+ }
+
+ return elem;
+ } ).append( this );
+ }
+
+ return this;
+ },
+
+ wrapInner: function( html ) {
+ if ( isFunction( html ) ) {
+ return this.each( function( i ) {
+ jQuery( this ).wrapInner( html.call( this, i ) );
+ } );
+ }
+
+ return this.each( function() {
+ var self = jQuery( this ),
+ contents = self.contents();
+
+ if ( contents.length ) {
+ contents.wrapAll( html );
+
+ } else {
+ self.append( html );
+ }
+ } );
+ },
+
+ wrap: function( html ) {
+ var htmlIsFunction = isFunction( html );
+
+ return this.each( function( i ) {
+ jQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html );
+ } );
+ },
+
+ unwrap: function( selector ) {
+ this.parent( selector ).not( "body" ).each( function() {
+ jQuery( this ).replaceWith( this.childNodes );
+ } );
+ return this;
+ }
+} );
+
+
+jQuery.expr.pseudos.hidden = function( elem ) {
+ return !jQuery.expr.pseudos.visible( elem );
+};
+jQuery.expr.pseudos.visible = function( elem ) {
+ return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );
+};
+
+
+
+
+jQuery.ajaxSettings.xhr = function() {
+ try {
+ return new window.XMLHttpRequest();
+ } catch ( e ) {}
+};
+
+var xhrSuccessStatus = {
+
+ // File protocol always yields status code 0, assume 200
+ 0: 200,
+
+ // Support: IE <=9 only
+ // #1450: sometimes IE returns 1223 when it should be 204
+ 1223: 204
+ },
+ xhrSupported = jQuery.ajaxSettings.xhr();
+
+support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported );
+support.ajax = xhrSupported = !!xhrSupported;
+
+jQuery.ajaxTransport( function( options ) {
+ var callback, errorCallback;
+
+ // Cross domain only allowed if supported through XMLHttpRequest
+ if ( support.cors || xhrSupported && !options.crossDomain ) {
+ return {
+ send: function( headers, complete ) {
+ var i,
+ xhr = options.xhr();
+
+ xhr.open(
+ options.type,
+ options.url,
+ options.async,
+ options.username,
+ options.password
+ );
+
+ // Apply custom fields if provided
+ if ( options.xhrFields ) {
+ for ( i in options.xhrFields ) {
+ xhr[ i ] = options.xhrFields[ i ];
+ }
+ }
+
+ // Override mime type if needed
+ if ( options.mimeType && xhr.overrideMimeType ) {
+ xhr.overrideMimeType( options.mimeType );
+ }
+
+ // X-Requested-With header
+ // For cross-domain requests, seeing as conditions for a preflight are
+ // akin to a jigsaw puzzle, we simply never set it to be sure.
+ // (it can always be set on a per-request basis or even using ajaxSetup)
+ // For same-domain requests, won't change header if already provided.
+ if ( !options.crossDomain && !headers[ "X-Requested-With" ] ) {
+ headers[ "X-Requested-With" ] = "XMLHttpRequest";
+ }
+
+ // Set headers
+ for ( i in headers ) {
+ xhr.setRequestHeader( i, headers[ i ] );
+ }
+
+ // Callback
+ callback = function( type ) {
+ return function() {
+ if ( callback ) {
+ callback = errorCallback = xhr.onload =
+ xhr.onerror = xhr.onabort = xhr.ontimeout =
+ xhr.onreadystatechange = null;
+
+ if ( type === "abort" ) {
+ xhr.abort();
+ } else if ( type === "error" ) {
+
+ // Support: IE <=9 only
+ // On a manual native abort, IE9 throws
+ // errors on any property access that is not readyState
+ if ( typeof xhr.status !== "number" ) {
+ complete( 0, "error" );
+ } else {
+ complete(
+
+ // File: protocol always yields status 0; see #8605, #14207
+ xhr.status,
+ xhr.statusText
+ );
+ }
+ } else {
+ complete(
+ xhrSuccessStatus[ xhr.status ] || xhr.status,
+ xhr.statusText,
+
+ // Support: IE <=9 only
+ // IE9 has no XHR2 but throws on binary (trac-11426)
+ // For XHR2 non-text, let the caller handle it (gh-2498)
+ ( xhr.responseType || "text" ) !== "text" ||
+ typeof xhr.responseText !== "string" ?
+ { binary: xhr.response } :
+ { text: xhr.responseText },
+ xhr.getAllResponseHeaders()
+ );
+ }
+ }
+ };
+ };
+
+ // Listen to events
+ xhr.onload = callback();
+ errorCallback = xhr.onerror = xhr.ontimeout = callback( "error" );
+
+ // Support: IE 9 only
+ // Use onreadystatechange to replace onabort
+ // to handle uncaught aborts
+ if ( xhr.onabort !== undefined ) {
+ xhr.onabort = errorCallback;
+ } else {
+ xhr.onreadystatechange = function() {
+
+ // Check readyState before timeout as it changes
+ if ( xhr.readyState === 4 ) {
+
+ // Allow onerror to be called first,
+ // but that will not handle a native abort
+ // Also, save errorCallback to a variable
+ // as xhr.onerror cannot be accessed
+ window.setTimeout( function() {
+ if ( callback ) {
+ errorCallback();
+ }
+ } );
+ }
+ };
+ }
+
+ // Create the abort callback
+ callback = callback( "abort" );
+
+ try {
+
+ // Do send the request (this may raise an exception)
+ xhr.send( options.hasContent && options.data || null );
+ } catch ( e ) {
+
+ // #14683: Only rethrow if this hasn't been notified as an error yet
+ if ( callback ) {
+ throw e;
+ }
+ }
+ },
+
+ abort: function() {
+ if ( callback ) {
+ callback();
+ }
+ }
+ };
+ }
+} );
+
+
+
+
+// Prevent auto-execution of scripts when no explicit dataType was provided (See gh-2432)
+jQuery.ajaxPrefilter( function( s ) {
+ if ( s.crossDomain ) {
+ s.contents.script = false;
+ }
+} );
+
+// Install script dataType
+jQuery.ajaxSetup( {
+ accepts: {
+ script: "text/javascript, application/javascript, " +
+ "application/ecmascript, application/x-ecmascript"
+ },
+ contents: {
+ script: /\b(?:java|ecma)script\b/
+ },
+ converters: {
+ "text script": function( text ) {
+ jQuery.globalEval( text );
+ return text;
+ }
+ }
+} );
+
+// Handle cache's special case and crossDomain
+jQuery.ajaxPrefilter( "script", function( s ) {
+ if ( s.cache === undefined ) {
+ s.cache = false;
+ }
+ if ( s.crossDomain ) {
+ s.type = "GET";
+ }
+} );
+
+// Bind script tag hack transport
+jQuery.ajaxTransport( "script", function( s ) {
+
+ // This transport only deals with cross domain or forced-by-attrs requests
+ if ( s.crossDomain || s.scriptAttrs ) {
+ var script, callback;
+ return {
+ send: function( _, complete ) {
+ script = jQuery( "<script>" )
+ .attr( s.scriptAttrs || {} )
+ .prop( { charset: s.scriptCharset, src: s.url } )
+ .on( "load error", callback = function( evt ) {
+ script.remove();
+ callback = null;
+ if ( evt ) {
+ complete( evt.type === "error" ? 404 : 200, evt.type );
+ }
+ } );
+
+ // Use native DOM manipulation to avoid our domManip AJAX trickery
+ document.head.appendChild( script[ 0 ] );
+ },
+ abort: function() {
+ if ( callback ) {
+ callback();
+ }
+ }
+ };
+ }
+} );
+
+
+
+
+var oldCallbacks = [],
+ rjsonp = /(=)\?(?=&|$)|\?\?/;
+
+// Default jsonp settings
+jQuery.ajaxSetup( {
+ jsonp: "callback",
+ jsonpCallback: function() {
+ var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( nonce++ ) );
+ this[ callback ] = true;
+ return callback;
+ }
+} );
+
+// Detect, normalize options and install callbacks for jsonp requests
+jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
+
+ var callbackName, overwritten, responseContainer,
+ jsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ?
+ "url" :
+ typeof s.data === "string" &&
+ ( s.contentType || "" )
+ .indexOf( "application/x-www-form-urlencoded" ) === 0 &&
+ rjsonp.test( s.data ) && "data"
+ );
+
+ // Handle iff the expected data type is "jsonp" or we have a parameter to set
+ if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) {
+
+ // Get callback name, remembering preexisting value associated with it
+ callbackName = s.jsonpCallback = isFunction( s.jsonpCallback ) ?
+ s.jsonpCallback() :
+ s.jsonpCallback;
+
+ // Insert callback into url or form data
+ if ( jsonProp ) {
+ s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName );
+ } else if ( s.jsonp !== false ) {
+ s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName;
+ }
+
+ // Use data converter to retrieve json after script execution
+ s.converters[ "script json" ] = function() {
+ if ( !responseContainer ) {
+ jQuery.error( callbackName + " was not called" );
+ }
+ return responseContainer[ 0 ];
+ };
+
+ // Force json dataType
+ s.dataTypes[ 0 ] = "json";
+
+ // Install callback
+ overwritten = window[ callbackName ];
+ window[ callbackName ] = function() {
+ responseContainer = arguments;
+ };
+
+ // Clean-up function (fires after converters)
+ jqXHR.always( function() {
+
+ // If previous value didn't exist - remove it
+ if ( overwritten === undefined ) {
+ jQuery( window ).removeProp( callbackName );
+
+ // Otherwise restore preexisting value
+ } else {
+ window[ callbackName ] = overwritten;
+ }
+
+ // Save back as free
+ if ( s[ callbackName ] ) {
+
+ // Make sure that re-using the options doesn't screw things around
+ s.jsonpCallback = originalSettings.jsonpCallback;
+
+ // Save the callback name for future use
+ oldCallbacks.push( callbackName );
+ }
+
+ // Call if it was a function and we have a response
+ if ( responseContainer && isFunction( overwritten ) ) {
+ overwritten( responseContainer[ 0 ] );
+ }
+
+ responseContainer = overwritten = undefined;
+ } );
+
+ // Delegate to script
+ return "script";
+ }
+} );
+
+
+
+
+// Support: Safari 8 only
+// In Safari 8 documents created via document.implementation.createHTMLDocument
+// collapse sibling forms: the second one becomes a child of the first one.
+// Because of that, this security measure has to be disabled in Safari 8.
+// https://bugs.webkit.org/show_bug.cgi?id=137337
+support.createHTMLDocument = ( function() {
+ var body = document.implementation.createHTMLDocument( "" ).body;
+ body.innerHTML = "<form></form><form></form>";
+ return body.childNodes.length === 2;
+} )();
+
+
+// Argument "data" should be string of html
+// context (optional): If specified, the fragment will be created in this context,
+// defaults to document
+// keepScripts (optional): If true, will include scripts passed in the html string
+jQuery.parseHTML = function( data, context, keepScripts ) {
+ if ( typeof data !== "string" ) {
+ return [];
+ }
+ if ( typeof context === "boolean" ) {
+ keepScripts = context;
+ context = false;
+ }
+
+ var base, parsed, scripts;
+
+ if ( !context ) {
+
+ // Stop scripts or inline event handlers from being executed immediately
+ // by using document.implementation
+ if ( support.createHTMLDocument ) {
+ context = document.implementation.createHTMLDocument( "" );
+
+ // Set the base href for the created document
+ // so any parsed elements with URLs
+ // are based on the document's URL (gh-2965)
+ base = context.createElement( "base" );
+ base.href = document.location.href;
+ context.head.appendChild( base );
+ } else {
+ context = document;
+ }
+ }
+
+ parsed = rsingleTag.exec( data );
+ scripts = !keepScripts && [];
+
+ // Single tag
+ if ( parsed ) {
+ return [ context.createElement( parsed[ 1 ] ) ];
+ }
+
+ parsed = buildFragment( [ data ], context, scripts );
+
+ if ( scripts && scripts.length ) {
+ jQuery( scripts ).remove();
+ }
+
+ return jQuery.merge( [], parsed.childNodes );
+};
+
+
+/**
+ * Load a url into a page
+ */
+jQuery.fn.load = function( url, params, callback ) {
+ var selector, type, response,
+ self = this,
+ off = url.indexOf( " " );
+
+ if ( off > -1 ) {
+ selector = stripAndCollapse( url.slice( off ) );
+ url = url.slice( 0, off );
+ }
+
+ // If it's a function
+ if ( isFunction( params ) ) {
+
+ // We assume that it's the callback
+ callback = params;
+ params = undefined;
+
+ // Otherwise, build a param string
+ } else if ( params && typeof params === "object" ) {
+ type = "POST";
+ }
+
+ // If we have elements to modify, make the request
+ if ( self.length > 0 ) {
+ jQuery.ajax( {
+ url: url,
+
+ // If "type" variable is undefined, then "GET" method will be used.
+ // Make value of this field explicit since
+ // user can override it through ajaxSetup method
+ type: type || "GET",
+ dataType: "html",
+ data: params
+ } ).done( function( responseText ) {
+
+ // Save response for use in complete callback
+ response = arguments;
+
+ self.html( selector ?
+
+ // If a selector was specified, locate the right elements in a dummy div
+ // Exclude scripts to avoid IE 'Permission Denied' errors
+ jQuery( "<div>" ).append( jQuery.parseHTML( responseText ) ).find( selector ) :
+
+ // Otherwise use the full result
+ responseText );
+
+ // If the request succeeds, this function gets "data", "status", "jqXHR"
+ // but they are ignored because response was set above.
+ // If it fails, this function gets "jqXHR", "status", "error"
+ } ).always( callback && function( jqXHR, status ) {
+ self.each( function() {
+ callback.apply( this, response || [ jqXHR.responseText, status, jqXHR ] );
+ } );
+ } );
+ }
+
+ return this;
+};
+
+
+
+
+// Attach a bunch of functions for handling common AJAX events
+jQuery.each( [
+ "ajaxStart",
+ "ajaxStop",
+ "ajaxComplete",
+ "ajaxError",
+ "ajaxSuccess",
+ "ajaxSend"
+], function( i, type ) {
+ jQuery.fn[ type ] = function( fn ) {
+ return this.on( type, fn );
+ };
+} );
+
+
+
+
+jQuery.expr.pseudos.animated = function( elem ) {
+ return jQuery.grep( jQuery.timers, function( fn ) {
+ return elem === fn.elem;
+ } ).length;
+};
+
+
+
+
+jQuery.offset = {
+ setOffset: function( elem, options, i ) {
+ var curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition,
+ position = jQuery.css( elem, "position" ),
+ curElem = jQuery( elem ),
+ props = {};
+
+ // Set position first, in-case top/left are set even on static elem
+ if ( position === "static" ) {
+ elem.style.position = "relative";
+ }
+
+ curOffset = curElem.offset();
+ curCSSTop = jQuery.css( elem, "top" );
+ curCSSLeft = jQuery.css( elem, "left" );
+ calculatePosition = ( position === "absolute" || position === "fixed" ) &&
+ ( curCSSTop + curCSSLeft ).indexOf( "auto" ) > -1;
+
+ // Need to be able to calculate position if either
+ // top or left is auto and position is either absolute or fixed
+ if ( calculatePosition ) {
+ curPosition = curElem.position();
+ curTop = curPosition.top;
+ curLeft = curPosition.left;
+
+ } else {
+ curTop = parseFloat( curCSSTop ) || 0;
+ curLeft = parseFloat( curCSSLeft ) || 0;
+ }
+
+ if ( isFunction( options ) ) {
+
+ // Use jQuery.extend here to allow modification of coordinates argument (gh-1848)
+ options = options.call( elem, i, jQuery.extend( {}, curOffset ) );
+ }
+
+ if ( options.top != null ) {
+ props.top = ( options.top - curOffset.top ) + curTop;
+ }
+ if ( options.left != null ) {
+ props.left = ( options.left - curOffset.left ) + curLeft;
+ }
+
+ if ( "using" in options ) {
+ options.using.call( elem, props );
+
+ } else {
+ curElem.css( props );
+ }
+ }
+};
+
+jQuery.fn.extend( {
+
+ // offset() relates an element's border box to the document origin
+ offset: function( options ) {
+
+ // Preserve chaining for setter
+ if ( arguments.length ) {
+ return options === undefined ?
+ this :
+ this.each( function( i ) {
+ jQuery.offset.setOffset( this, options, i );
+ } );
+ }
+
+ var rect, win,
+ elem = this[ 0 ];
+
+ if ( !elem ) {
+ return;
+ }
+
+ // Return zeros for disconnected and hidden (display: none) elements (gh-2310)
+ // Support: IE <=11 only
+ // Running getBoundingClientRect on a
+ // disconnected node in IE throws an error
+ if ( !elem.getClientRects().length ) {
+ return { top: 0, left: 0 };
+ }
+
+ // Get document-relative position by adding viewport scroll to viewport-relative gBCR
+ rect = elem.getBoundingClientRect();
+ win = elem.ownerDocument.defaultView;
+ return {
+ top: rect.top + win.pageYOffset,
+ left: rect.left + win.pageXOffset
+ };
+ },
+
+ // position() relates an element's margin box to its offset parent's padding box
+ // This corresponds to the behavior of CSS absolute positioning
+ position: function() {
+ if ( !this[ 0 ] ) {
+ return;
+ }
+
+ var offsetParent, offset, doc,
+ elem = this[ 0 ],
+ parentOffset = { top: 0, left: 0 };
+
+ // position:fixed elements are offset from the viewport, which itself always has zero offset
+ if ( jQuery.css( elem, "position" ) === "fixed" ) {
+
+ // Assume position:fixed implies availability of getBoundingClientRect
+ offset = elem.getBoundingClientRect();
+
+ } else {
+ offset = this.offset();
+
+ // Account for the *real* offset parent, which can be the document or its root element
+ // when a statically positioned element is identified
+ doc = elem.ownerDocument;
+ offsetParent = elem.offsetParent || doc.documentElement;
+ while ( offsetParent &&
+ ( offsetParent === doc.body || offsetParent === doc.documentElement ) &&
+ jQuery.css( offsetParent, "position" ) === "static" ) {
+
+ offsetParent = offsetParent.parentNode;
+ }
+ if ( offsetParent && offsetParent !== elem && offsetParent.nodeType === 1 ) {
+
+ // Incorporate borders into its offset, since they are outside its content origin
+ parentOffset = jQuery( offsetParent ).offset();
+ parentOffset.top += jQuery.css( offsetParent, "borderTopWidth", true );
+ parentOffset.left += jQuery.css( offsetParent, "borderLeftWidth", true );
+ }
+ }
+
+ // Subtract parent offsets and element margins
+ return {
+ top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ),
+ left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true )
+ };
+ },
+
+ // This method will return documentElement in the following cases:
+ // 1) For the element inside the iframe without offsetParent, this method will return
+ // documentElement of the parent window
+ // 2) For the hidden or detached element
+ // 3) For body or html element, i.e. in case of the html node - it will return itself
+ //
+ // but those exceptions were never presented as a real life use-cases
+ // and might be considered as more preferable results.
+ //
+ // This logic, however, is not guaranteed and can change at any point in the future
+ offsetParent: function() {
+ return this.map( function() {
+ var offsetParent = this.offsetParent;
+
+ while ( offsetParent && jQuery.css( offsetParent, "position" ) === "static" ) {
+ offsetParent = offsetParent.offsetParent;
+ }
+
+ return offsetParent || documentElement;
+ } );
+ }
+} );
+
+// Create scrollLeft and scrollTop methods
+jQuery.each( { scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, function( method, prop ) {
+ var top = "pageYOffset" === prop;
+
+ jQuery.fn[ method ] = function( val ) {
+ return access( this, function( elem, method, val ) {
+
+ // Coalesce documents and windows
+ var win;
+ if ( isWindow( elem ) ) {
+ win = elem;
+ } else if ( elem.nodeType === 9 ) {
+ win = elem.defaultView;
+ }
+
+ if ( val === undefined ) {
+ return win ? win[ prop ] : elem[ method ];
+ }
+
+ if ( win ) {
+ win.scrollTo(
+ !top ? val : win.pageXOffset,
+ top ? val : win.pageYOffset
+ );
+
+ } else {
+ elem[ method ] = val;
+ }
+ }, method, val, arguments.length );
+ };
+} );
+
+// Support: Safari <=7 - 9.1, Chrome <=37 - 49
+// Add the top/left cssHooks using jQuery.fn.position
+// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084
+// Blink bug: https://bugs.chromium.org/p/chromium/issues/detail?id=589347
+// getComputedStyle returns percent when specified for top/left/bottom/right;
+// rather than make the css module depend on the offset module, just check for it here
+jQuery.each( [ "top", "left" ], function( i, prop ) {
+ jQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition,
+ function( elem, computed ) {
+ if ( computed ) {
+ computed = curCSS( elem, prop );
+
+ // If curCSS returns percentage, fallback to offset
+ return rnumnonpx.test( computed ) ?
+ jQuery( elem ).position()[ prop ] + "px" :
+ computed;
+ }
+ }
+ );
+} );
+
+
+// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods
+jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
+ jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name },
+ function( defaultExtra, funcName ) {
+
+ // Margin is only for outerHeight, outerWidth
+ jQuery.fn[ funcName ] = function( margin, value ) {
+ var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ),
+ extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );
+
+ return access( this, function( elem, type, value ) {
+ var doc;
+
+ if ( isWindow( elem ) ) {
+
+ // $( window ).outerWidth/Height return w/h including scrollbars (gh-1729)
+ return funcName.indexOf( "outer" ) === 0 ?
+ elem[ "inner" + name ] :
+ elem.document.documentElement[ "client" + name ];
+ }
+
+ // Get document width or height
+ if ( elem.nodeType === 9 ) {
+ doc = elem.documentElement;
+
+ // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height],
+ // whichever is greatest
+ return Math.max(
+ elem.body[ "scroll" + name ], doc[ "scroll" + name ],
+ elem.body[ "offset" + name ], doc[ "offset" + name ],
+ doc[ "client" + name ]
+ );
+ }
+
+ return value === undefined ?
+
+ // Get width or height on the element, requesting but not forcing parseFloat
+ jQuery.css( elem, type, extra ) :
+
+ // Set width or height on the element
+ jQuery.style( elem, type, value, extra );
+ }, type, chainable ? margin : undefined, chainable );
+ };
+ } );
+} );
+
+
+jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " +
+ "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
+ "change select submit keydown keypress keyup contextmenu" ).split( " " ),
+ function( i, name ) {
+
+ // Handle event binding
+ jQuery.fn[ name ] = function( data, fn ) {
+ return arguments.length > 0 ?
+ this.on( name, null, data, fn ) :
+ this.trigger( name );
+ };
+} );
+
+jQuery.fn.extend( {
+ hover: function( fnOver, fnOut ) {
+ return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
+ }
+} );
+
+
+
+
+jQuery.fn.extend( {
+
+ bind: function( types, data, fn ) {
+ return this.on( types, null, data, fn );
+ },
+ unbind: function( types, fn ) {
+ return this.off( types, null, fn );
+ },
+
+ delegate: function( selector, types, data, fn ) {
+ return this.on( types, selector, data, fn );
+ },
+ undelegate: function( selector, types, fn ) {
+
+ // ( namespace ) or ( selector, types [, fn] )
+ return arguments.length === 1 ?
+ this.off( selector, "**" ) :
+ this.off( types, selector || "**", fn );
+ }
+} );
+
+// Bind a function to a context, optionally partially applying any
+// arguments.
+// jQuery.proxy is deprecated to promote standards (specifically Function#bind)
+// However, it is not slated for removal any time soon
+jQuery.proxy = function( fn, context ) {
+ var tmp, args, proxy;
+
+ if ( typeof context === "string" ) {
+ tmp = fn[ context ];
+ context = fn;
+ fn = tmp;
+ }
+
+ // Quick check to determine if target is callable, in the spec
+ // this throws a TypeError, but we will just return undefined.
+ if ( !isFunction( fn ) ) {
+ return undefined;
+ }
+
+ // Simulated bind
+ args = slice.call( arguments, 2 );
+ proxy = function() {
+ return fn.apply( context || this, args.concat( slice.call( arguments ) ) );
+ };
+
+ // Set the guid of unique handler to the same of original handler, so it can be removed
+ proxy.guid = fn.guid = fn.guid || jQuery.guid++;
+
+ return proxy;
+};
+
+jQuery.holdReady = function( hold ) {
+ if ( hold ) {
+ jQuery.readyWait++;
+ } else {
+ jQuery.ready( true );
+ }
+};
+jQuery.isArray = Array.isArray;
+jQuery.parseJSON = JSON.parse;
+jQuery.nodeName = nodeName;
+jQuery.isFunction = isFunction;
+jQuery.isWindow = isWindow;
+jQuery.camelCase = camelCase;
+jQuery.type = toType;
+
+jQuery.now = Date.now;
+
+jQuery.isNumeric = function( obj ) {
+
+ // As of jQuery 3.0, isNumeric is limited to
+ // strings and numbers (primitives or objects)
+ // that can be coerced to finite numbers (gh-2662)
+ var type = jQuery.type( obj );
+ return ( type === "number" || type === "string" ) &&
+
+ // parseFloat NaNs numeric-cast false positives ("")
+ // ...but misinterprets leading-number strings, particularly hex literals ("0x...")
+ // subtraction forces infinities to NaN
+ !isNaN( obj - parseFloat( obj ) );
+};
+
+
+
+
+// Register as a named AMD module, since jQuery can be concatenated with other
+// files that may use define, but not via a proper concatenation script that
+// understands anonymous AMD modules. A named AMD is safest and most robust
+// way to register. Lowercase jquery is used because AMD module names are
+// derived from file names, and jQuery is normally delivered in a lowercase
+// file name. Do this after creating the global so that if an AMD module wants
+// to call noConflict to hide this version of jQuery, it will work.
+
+// Note that for maximum portability, libraries that are not jQuery should
+// declare themselves as anonymous modules, and avoid setting a global if an
+// AMD loader is present. jQuery is a special case. For more information, see
+// https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon
+
+if ( typeof define === "function" && define.amd ) {
+ define( "jquery", [], function() {
+ return jQuery;
+ } );
+}
+
+
+
+
+var
+
+ // Map over jQuery in case of overwrite
+ _jQuery = window.jQuery,
+
+ // Map over the $ in case of overwrite
+ _$ = window.$;
+
+jQuery.noConflict = function( deep ) {
+ if ( window.$ === jQuery ) {
+ window.$ = _$;
+ }
+
+ if ( deep && window.jQuery === jQuery ) {
+ window.jQuery = _jQuery;
+ }
+
+ return jQuery;
+};
+
+// Expose jQuery and $ identifiers, even in AMD
+// (#7102#comment:10, https://github.com/jquery/jquery/pull/557)
+// and CommonJS for browser emulators (#13566)
+if ( !noGlobal ) {
+ window.jQuery = window.$ = jQuery;
+}
+
+
+
+
+return jQuery;
+} );
+/**!
+ * @fileOverview Kickass library to create and place poppers near their reference elements.
+ * @version 1.15.0
+ * @license
+ * Copyright (c) 2016 Federico Zivolo and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global.Popper = factory());
+}(this, (function () { 'use strict';
+
+var isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
+
+var longerTimeoutBrowsers = ['Edge', 'Trident', 'Firefox'];
+var timeoutDuration = 0;
+for (var i = 0; i < longerTimeoutBrowsers.length; i += 1) {
+ if (isBrowser && navigator.userAgent.indexOf(longerTimeoutBrowsers[i]) >= 0) {
+ timeoutDuration = 1;
+ break;
+ }
+}
+
+function microtaskDebounce(fn) {
+ var called = false;
+ return function () {
+ if (called) {
+ return;
+ }
+ called = true;
+ window.Promise.resolve().then(function () {
+ called = false;
+ fn();
+ });
+ };
+}
+
+function taskDebounce(fn) {
+ var scheduled = false;
+ return function () {
+ if (!scheduled) {
+ scheduled = true;
+ setTimeout(function () {
+ scheduled = false;
+ fn();
+ }, timeoutDuration);
+ }
+ };
+}
+
+var supportsMicroTasks = isBrowser && window.Promise;
+
+/**
+* Create a debounced version of a method, that's asynchronously deferred
+* but called in the minimum time possible.
+*
+* @method
+* @memberof Popper.Utils
+* @argument {Function} fn
+* @returns {Function}
+*/
+var debounce = supportsMicroTasks ? microtaskDebounce : taskDebounce;
+
+/**
+ * Check if the given variable is a function
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Any} functionToCheck - variable to check
+ * @returns {Boolean} answer to: is a function?
+ */
+function isFunction(functionToCheck) {
+ var getType = {};
+ return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
+}
+
+/**
+ * Get CSS computed property of the given element
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Eement} element
+ * @argument {String} property
+ */
+function getStyleComputedProperty(element, property) {
+ if (element.nodeType !== 1) {
+ return [];
+ }
+ // NOTE: 1 DOM access here
+ var window = element.ownerDocument.defaultView;
+ var css = window.getComputedStyle(element, null);
+ return property ? css[property] : css;
+}
+
+/**
+ * Returns the parentNode or the host of the element
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Element} element
+ * @returns {Element} parent
+ */
+function getParentNode(element) {
+ if (element.nodeName === 'HTML') {
+ return element;
+ }
+ return element.parentNode || element.host;
+}
+
+/**
+ * Returns the scrolling parent of the given element
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Element} element
+ * @returns {Element} scroll parent
+ */
+function getScrollParent(element) {
+ // Return body, `getScroll` will take care to get the correct `scrollTop` from it
+ if (!element) {
+ return document.body;
+ }
+
+ switch (element.nodeName) {
+ case 'HTML':
+ case 'BODY':
+ return element.ownerDocument.body;
+ case '#document':
+ return element.body;
+ }
+
+ // Firefox want us to check `-x` and `-y` variations as well
+
+ var _getStyleComputedProp = getStyleComputedProperty(element),
+ overflow = _getStyleComputedProp.overflow,
+ overflowX = _getStyleComputedProp.overflowX,
+ overflowY = _getStyleComputedProp.overflowY;
+
+ if (/(auto|scroll|overlay)/.test(overflow + overflowY + overflowX)) {
+ return element;
+ }
+
+ return getScrollParent(getParentNode(element));
+}
+
+var isIE11 = isBrowser && !!(window.MSInputMethodContext && document.documentMode);
+var isIE10 = isBrowser && /MSIE 10/.test(navigator.userAgent);
+
+/**
+ * Determines if the browser is Internet Explorer
+ * @method
+ * @memberof Popper.Utils
+ * @param {Number} version to check
+ * @returns {Boolean} isIE
+ */
+function isIE(version) {
+ if (version === 11) {
+ return isIE11;
+ }
+ if (version === 10) {
+ return isIE10;
+ }
+ return isIE11 || isIE10;
+}
+
+/**
+ * Returns the offset parent of the given element
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Element} element
+ * @returns {Element} offset parent
+ */
+function getOffsetParent(element) {
+ if (!element) {
+ return document.documentElement;
+ }
+
+ var noOffsetParent = isIE(10) ? document.body : null;
+
+ // NOTE: 1 DOM access here
+ var offsetParent = element.offsetParent || null;
+ // Skip hidden elements which don't have an offsetParent
+ while (offsetParent === noOffsetParent && element.nextElementSibling) {
+ offsetParent = (element = element.nextElementSibling).offsetParent;
+ }
+
+ var nodeName = offsetParent && offsetParent.nodeName;
+
+ if (!nodeName || nodeName === 'BODY' || nodeName === 'HTML') {
+ return element ? element.ownerDocument.documentElement : document.documentElement;
+ }
+
+ // .offsetParent will return the closest TH, TD or TABLE in case
+ // no offsetParent is present, I hate this job...
+ if (['TH', 'TD', 'TABLE'].indexOf(offsetParent.nodeName) !== -1 && getStyleComputedProperty(offsetParent, 'position') === 'static') {
+ return getOffsetParent(offsetParent);
+ }
+
+ return offsetParent;
+}
+
+function isOffsetContainer(element) {
+ var nodeName = element.nodeName;
+
+ if (nodeName === 'BODY') {
+ return false;
+ }
+ return nodeName === 'HTML' || getOffsetParent(element.firstElementChild) === element;
+}
+
+/**
+ * Finds the root node (document, shadowDOM root) of the given element
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Element} node
+ * @returns {Element} root node
+ */
+function getRoot(node) {
+ if (node.parentNode !== null) {
+ return getRoot(node.parentNode);
+ }
+
+ return node;
+}
+
+/**
+ * Finds the offset parent common to the two provided nodes
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Element} element1
+ * @argument {Element} element2
+ * @returns {Element} common offset parent
+ */
+function findCommonOffsetParent(element1, element2) {
+ // This check is needed to avoid errors in case one of the elements isn't defined for any reason
+ if (!element1 || !element1.nodeType || !element2 || !element2.nodeType) {
+ return document.documentElement;
+ }
+
+ // Here we make sure to give as "start" the element that comes first in the DOM
+ var order = element1.compareDocumentPosition(element2) & Node.DOCUMENT_POSITION_FOLLOWING;
+ var start = order ? element1 : element2;
+ var end = order ? element2 : element1;
+
+ // Get common ancestor container
+ var range = document.createRange();
+ range.setStart(start, 0);
+ range.setEnd(end, 0);
+ var commonAncestorContainer = range.commonAncestorContainer;
+
+ // Both nodes are inside #document
+
+ if (element1 !== commonAncestorContainer && element2 !== commonAncestorContainer || start.contains(end)) {
+ if (isOffsetContainer(commonAncestorContainer)) {
+ return commonAncestorContainer;
+ }
+
+ return getOffsetParent(commonAncestorContainer);
+ }
+
+ // one of the nodes is inside shadowDOM, find which one
+ var element1root = getRoot(element1);
+ if (element1root.host) {
+ return findCommonOffsetParent(element1root.host, element2);
+ } else {
+ return findCommonOffsetParent(element1, getRoot(element2).host);
+ }
+}
+
+/**
+ * Gets the scroll value of the given element in the given side (top and left)
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Element} element
+ * @argument {String} side `top` or `left`
+ * @returns {number} amount of scrolled pixels
+ */
+function getScroll(element) {
+ var side = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'top';
+
+ var upperSide = side === 'top' ? 'scrollTop' : 'scrollLeft';
+ var nodeName = element.nodeName;
+
+ if (nodeName === 'BODY' || nodeName === 'HTML') {
+ var html = element.ownerDocument.documentElement;
+ var scrollingElement = element.ownerDocument.scrollingElement || html;
+ return scrollingElement[upperSide];
+ }
+
+ return element[upperSide];
+}
+
+/*
+ * Sum or subtract the element scroll values (left and top) from a given rect object
+ * @method
+ * @memberof Popper.Utils
+ * @param {Object} rect - Rect object you want to change
+ * @param {HTMLElement} element - The element from the function reads the scroll values
+ * @param {Boolean} subtract - set to true if you want to subtract the scroll values
+ * @return {Object} rect - The modifier rect object
+ */
+function includeScroll(rect, element) {
+ var subtract = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+
+ var scrollTop = getScroll(element, 'top');
+ var scrollLeft = getScroll(element, 'left');
+ var modifier = subtract ? -1 : 1;
+ rect.top += scrollTop * modifier;
+ rect.bottom += scrollTop * modifier;
+ rect.left += scrollLeft * modifier;
+ rect.right += scrollLeft * modifier;
+ return rect;
+}
+
+/*
+ * Helper to detect borders of a given element
+ * @method
+ * @memberof Popper.Utils
+ * @param {CSSStyleDeclaration} styles
+ * Result of `getStyleComputedProperty` on the given element
+ * @param {String} axis - `x` or `y`
+ * @return {number} borders - The borders size of the given axis
+ */
+
+function getBordersSize(styles, axis) {
+ var sideA = axis === 'x' ? 'Left' : 'Top';
+ var sideB = sideA === 'Left' ? 'Right' : 'Bottom';
+
+ return parseFloat(styles['border' + sideA + 'Width'], 10) + parseFloat(styles['border' + sideB + 'Width'], 10);
+}
+
+function getSize(axis, body, html, computedStyle) {
+ return Math.max(body['offset' + axis], body['scroll' + axis], html['client' + axis], html['offset' + axis], html['scroll' + axis], isIE(10) ? parseInt(html['offset' + axis]) + parseInt(computedStyle['margin' + (axis === 'Height' ? 'Top' : 'Left')]) + parseInt(computedStyle['margin' + (axis === 'Height' ? 'Bottom' : 'Right')]) : 0);
+}
+
+function getWindowSizes(document) {
+ var body = document.body;
+ var html = document.documentElement;
+ var computedStyle = isIE(10) && getComputedStyle(html);
+
+ return {
+ height: getSize('Height', body, html, computedStyle),
+ width: getSize('Width', body, html, computedStyle)
+ };
+}
+
+var classCallCheck = function (instance, Constructor) {
+ if (!(instance instanceof Constructor)) {
+ throw new TypeError("Cannot call a class as a function");
+ }
+};
+
+var createClass = function () {
+ function defineProperties(target, props) {
+ for (var i = 0; i < props.length; i++) {
+ var descriptor = props[i];
+ descriptor.enumerable = descriptor.enumerable || false;
+ descriptor.configurable = true;
+ if ("value" in descriptor) descriptor.writable = true;
+ Object.defineProperty(target, descriptor.key, descriptor);
+ }
+ }
+
+ return function (Constructor, protoProps, staticProps) {
+ if (protoProps) defineProperties(Constructor.prototype, protoProps);
+ if (staticProps) defineProperties(Constructor, staticProps);
+ return Constructor;
+ };
+}();
+
+
+
+
+
+var defineProperty = function (obj, key, value) {
+ if (key in obj) {
+ Object.defineProperty(obj, key, {
+ value: value,
+ enumerable: true,
+ configurable: true,
+ writable: true
+ });
+ } else {
+ obj[key] = value;
+ }
+
+ return obj;
+};
+
+var _extends = Object.assign || function (target) {
+ for (var i = 1; i < arguments.length; i++) {
+ var source = arguments[i];
+
+ for (var key in source) {
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
+ target[key] = source[key];
+ }
+ }
+ }
+
+ return target;
+};
+
+/**
+ * Given element offsets, generate an output similar to getBoundingClientRect
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Object} offsets
+ * @returns {Object} ClientRect like output
+ */
+function getClientRect(offsets) {
+ return _extends({}, offsets, {
+ right: offsets.left + offsets.width,
+ bottom: offsets.top + offsets.height
+ });
+}
+
+/**
+ * Get bounding client rect of given element
+ * @method
+ * @memberof Popper.Utils
+ * @param {HTMLElement} element
+ * @return {Object} client rect
+ */
+function getBoundingClientRect(element) {
+ var rect = {};
+
+ // IE10 10 FIX: Please, don't ask, the element isn't
+ // considered in DOM in some circumstances...
+ // This isn't reproducible in IE10 compatibility mode of IE11
+ try {
+ if (isIE(10)) {
+ rect = element.getBoundingClientRect();
+ var scrollTop = getScroll(element, 'top');
+ var scrollLeft = getScroll(element, 'left');
+ rect.top += scrollTop;
+ rect.left += scrollLeft;
+ rect.bottom += scrollTop;
+ rect.right += scrollLeft;
+ } else {
+ rect = element.getBoundingClientRect();
+ }
+ } catch (e) {}
+
+ var result = {
+ left: rect.left,
+ top: rect.top,
+ width: rect.right - rect.left,
+ height: rect.bottom - rect.top
+ };
+
+ // subtract scrollbar size from sizes
+ var sizes = element.nodeName === 'HTML' ? getWindowSizes(element.ownerDocument) : {};
+ var width = sizes.width || element.clientWidth || result.right - result.left;
+ var height = sizes.height || element.clientHeight || result.bottom - result.top;
+
+ var horizScrollbar = element.offsetWidth - width;
+ var vertScrollbar = element.offsetHeight - height;
+
+ // if an hypothetical scrollbar is detected, we must be sure it's not a `border`
+ // we make this check conditional for performance reasons
+ if (horizScrollbar || vertScrollbar) {
+ var styles = getStyleComputedProperty(element);
+ horizScrollbar -= getBordersSize(styles, 'x');
+ vertScrollbar -= getBordersSize(styles, 'y');
+
+ result.width -= horizScrollbar;
+ result.height -= vertScrollbar;
+ }
+
+ return getClientRect(result);
+}
+
+function getOffsetRectRelativeToArbitraryNode(children, parent) {
+ var fixedPosition = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+
+ var isIE10 = isIE(10);
+ var isHTML = parent.nodeName === 'HTML';
+ var childrenRect = getBoundingClientRect(children);
+ var parentRect = getBoundingClientRect(parent);
+ var scrollParent = getScrollParent(children);
+
+ var styles = getStyleComputedProperty(parent);
+ var borderTopWidth = parseFloat(styles.borderTopWidth, 10);
+ var borderLeftWidth = parseFloat(styles.borderLeftWidth, 10);
+
+ // In cases where the parent is fixed, we must ignore negative scroll in offset calc
+ if (fixedPosition && isHTML) {
+ parentRect.top = Math.max(parentRect.top, 0);
+ parentRect.left = Math.max(parentRect.left, 0);
+ }
+ var offsets = getClientRect({
+ top: childrenRect.top - parentRect.top - borderTopWidth,
+ left: childrenRect.left - parentRect.left - borderLeftWidth,
+ width: childrenRect.width,
+ height: childrenRect.height
+ });
+ offsets.marginTop = 0;
+ offsets.marginLeft = 0;
+
+ // Subtract margins of documentElement in case it's being used as parent
+ // we do this only on HTML because it's the only element that behaves
+ // differently when margins are applied to it. The margins are included in
+ // the box of the documentElement, in the other cases not.
+ if (!isIE10 && isHTML) {
+ var marginTop = parseFloat(styles.marginTop, 10);
+ var marginLeft = parseFloat(styles.marginLeft, 10);
+
+ offsets.top -= borderTopWidth - marginTop;
+ offsets.bottom -= borderTopWidth - marginTop;
+ offsets.left -= borderLeftWidth - marginLeft;
+ offsets.right -= borderLeftWidth - marginLeft;
+
+ // Attach marginTop and marginLeft because in some circumstances we may need them
+ offsets.marginTop = marginTop;
+ offsets.marginLeft = marginLeft;
+ }
+
+ if (isIE10 && !fixedPosition ? parent.contains(scrollParent) : parent === scrollParent && scrollParent.nodeName !== 'BODY') {
+ offsets = includeScroll(offsets, parent);
+ }
+
+ return offsets;
+}
+
+function getViewportOffsetRectRelativeToArtbitraryNode(element) {
+ var excludeScroll = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+
+ var html = element.ownerDocument.documentElement;
+ var relativeOffset = getOffsetRectRelativeToArbitraryNode(element, html);
+ var width = Math.max(html.clientWidth, window.innerWidth || 0);
+ var height = Math.max(html.clientHeight, window.innerHeight || 0);
+
+ var scrollTop = !excludeScroll ? getScroll(html) : 0;
+ var scrollLeft = !excludeScroll ? getScroll(html, 'left') : 0;
+
+ var offset = {
+ top: scrollTop - relativeOffset.top + relativeOffset.marginTop,
+ left: scrollLeft - relativeOffset.left + relativeOffset.marginLeft,
+ width: width,
+ height: height
+ };
+
+ return getClientRect(offset);
+}
+
+/**
+ * Check if the given element is fixed or is inside a fixed parent
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Element} element
+ * @argument {Element} customContainer
+ * @returns {Boolean} answer to "isFixed?"
+ */
+function isFixed(element) {
+ var nodeName = element.nodeName;
+ if (nodeName === 'BODY' || nodeName === 'HTML') {
+ return false;
+ }
+ if (getStyleComputedProperty(element, 'position') === 'fixed') {
+ return true;
+ }
+ var parentNode = getParentNode(element);
+ if (!parentNode) {
+ return false;
+ }
+ return isFixed(parentNode);
+}
+
+/**
+ * Finds the first parent of an element that has a transformed property defined
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Element} element
+ * @returns {Element} first transformed parent or documentElement
+ */
+
+function getFixedPositionOffsetParent(element) {
+ // This check is needed to avoid errors in case one of the elements isn't defined for any reason
+ if (!element || !element.parentElement || isIE()) {
+ return document.documentElement;
+ }
+ var el = element.parentElement;
+ while (el && getStyleComputedProperty(el, 'transform') === 'none') {
+ el = el.parentElement;
+ }
+ return el || document.documentElement;
+}
+
+/**
+ * Computed the boundaries limits and return them
+ * @method
+ * @memberof Popper.Utils
+ * @param {HTMLElement} popper
+ * @param {HTMLElement} reference
+ * @param {number} padding
+ * @param {HTMLElement} boundariesElement - Element used to define the boundaries
+ * @param {Boolean} fixedPosition - Is in fixed position mode
+ * @returns {Object} Coordinates of the boundaries
+ */
+function getBoundaries(popper, reference, padding, boundariesElement) {
+ var fixedPosition = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
+
+ // NOTE: 1 DOM access here
+
+ var boundaries = { top: 0, left: 0 };
+ var offsetParent = fixedPosition ? getFixedPositionOffsetParent(popper) : findCommonOffsetParent(popper, reference);
+
+ // Handle viewport case
+ if (boundariesElement === 'viewport') {
+ boundaries = getViewportOffsetRectRelativeToArtbitraryNode(offsetParent, fixedPosition);
+ } else {
+ // Handle other cases based on DOM element used as boundaries
+ var boundariesNode = void 0;
+ if (boundariesElement === 'scrollParent') {
+ boundariesNode = getScrollParent(getParentNode(reference));
+ if (boundariesNode.nodeName === 'BODY') {
+ boundariesNode = popper.ownerDocument.documentElement;
+ }
+ } else if (boundariesElement === 'window') {
+ boundariesNode = popper.ownerDocument.documentElement;
+ } else {
+ boundariesNode = boundariesElement;
+ }
+
+ var offsets = getOffsetRectRelativeToArbitraryNode(boundariesNode, offsetParent, fixedPosition);
+
+ // In case of HTML, we need a different computation
+ if (boundariesNode.nodeName === 'HTML' && !isFixed(offsetParent)) {
+ var _getWindowSizes = getWindowSizes(popper.ownerDocument),
+ height = _getWindowSizes.height,
+ width = _getWindowSizes.width;
+
+ boundaries.top += offsets.top - offsets.marginTop;
+ boundaries.bottom = height + offsets.top;
+ boundaries.left += offsets.left - offsets.marginLeft;
+ boundaries.right = width + offsets.left;
+ } else {
+ // for all the other DOM elements, this one is good
+ boundaries = offsets;
+ }
+ }
+
+ // Add paddings
+ padding = padding || 0;
+ var isPaddingNumber = typeof padding === 'number';
+ boundaries.left += isPaddingNumber ? padding : padding.left || 0;
+ boundaries.top += isPaddingNumber ? padding : padding.top || 0;
+ boundaries.right -= isPaddingNumber ? padding : padding.right || 0;
+ boundaries.bottom -= isPaddingNumber ? padding : padding.bottom || 0;
+
+ return boundaries;
+}
+
+function getArea(_ref) {
+ var width = _ref.width,
+ height = _ref.height;
+
+ return width * height;
+}
+
+/**
+ * Utility used to transform the `auto` placement to the placement with more
+ * available space.
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Object} data - The data object generated by update method
+ * @argument {Object} options - Modifiers configuration and options
+ * @returns {Object} The data object, properly modified
+ */
+function computeAutoPlacement(placement, refRect, popper, reference, boundariesElement) {
+ var padding = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : 0;
+
+ if (placement.indexOf('auto') === -1) {
+ return placement;
+ }
+
+ var boundaries = getBoundaries(popper, reference, padding, boundariesElement);
+
+ var rects = {
+ top: {
+ width: boundaries.width,
+ height: refRect.top - boundaries.top
+ },
+ right: {
+ width: boundaries.right - refRect.right,
+ height: boundaries.height
+ },
+ bottom: {
+ width: boundaries.width,
+ height: boundaries.bottom - refRect.bottom
+ },
+ left: {
+ width: refRect.left - boundaries.left,
+ height: boundaries.height
+ }
+ };
+
+ var sortedAreas = Object.keys(rects).map(function (key) {
+ return _extends({
+ key: key
+ }, rects[key], {
+ area: getArea(rects[key])
+ });
+ }).sort(function (a, b) {
+ return b.area - a.area;
+ });
+
+ var filteredAreas = sortedAreas.filter(function (_ref2) {
+ var width = _ref2.width,
+ height = _ref2.height;
+ return width >= popper.clientWidth && height >= popper.clientHeight;
+ });
+
+ var computedPlacement = filteredAreas.length > 0 ? filteredAreas[0].key : sortedAreas[0].key;
+
+ var variation = placement.split('-')[1];
+
+ return computedPlacement + (variation ? '-' + variation : '');
+}
+
+/**
+ * Get offsets to the reference element
+ * @method
+ * @memberof Popper.Utils
+ * @param {Object} state
+ * @param {Element} popper - the popper element
+ * @param {Element} reference - the reference element (the popper will be relative to this)
+ * @param {Element} fixedPosition - is in fixed position mode
+ * @returns {Object} An object containing the offsets which will be applied to the popper
+ */
+function getReferenceOffsets(state, popper, reference) {
+ var fixedPosition = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;
+
+ var commonOffsetParent = fixedPosition ? getFixedPositionOffsetParent(popper) : findCommonOffsetParent(popper, reference);
+ return getOffsetRectRelativeToArbitraryNode(reference, commonOffsetParent, fixedPosition);
+}
+
+/**
+ * Get the outer sizes of the given element (offset size + margins)
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Element} element
+ * @returns {Object} object containing width and height properties
+ */
+function getOuterSizes(element) {
+ var window = element.ownerDocument.defaultView;
+ var styles = window.getComputedStyle(element);
+ var x = parseFloat(styles.marginTop || 0) + parseFloat(styles.marginBottom || 0);
+ var y = parseFloat(styles.marginLeft || 0) + parseFloat(styles.marginRight || 0);
+ var result = {
+ width: element.offsetWidth + y,
+ height: element.offsetHeight + x
+ };
+ return result;
+}
+
+/**
+ * Get the opposite placement of the given one
+ * @method
+ * @memberof Popper.Utils
+ * @argument {String} placement
+ * @returns {String} flipped placement
+ */
+function getOppositePlacement(placement) {
+ var hash = { left: 'right', right: 'left', bottom: 'top', top: 'bottom' };
+ return placement.replace(/left|right|bottom|top/g, function (matched) {
+ return hash[matched];
+ });
+}
+
+/**
+ * Get offsets to the popper
+ * @method
+ * @memberof Popper.Utils
+ * @param {Object} position - CSS position the Popper will get applied
+ * @param {HTMLElement} popper - the popper element
+ * @param {Object} referenceOffsets - the reference offsets (the popper will be relative to this)
+ * @param {String} placement - one of the valid placement options
+ * @returns {Object} popperOffsets - An object containing the offsets which will be applied to the popper
+ */
+function getPopperOffsets(popper, referenceOffsets, placement) {
+ placement = placement.split('-')[0];
+
+ // Get popper node sizes
+ var popperRect = getOuterSizes(popper);
+
+ // Add position, width and height to our offsets object
+ var popperOffsets = {
+ width: popperRect.width,
+ height: popperRect.height
+ };
+
+ // depending by the popper placement we have to compute its offsets slightly differently
+ var isHoriz = ['right', 'left'].indexOf(placement) !== -1;
+ var mainSide = isHoriz ? 'top' : 'left';
+ var secondarySide = isHoriz ? 'left' : 'top';
+ var measurement = isHoriz ? 'height' : 'width';
+ var secondaryMeasurement = !isHoriz ? 'height' : 'width';
+
+ popperOffsets[mainSide] = referenceOffsets[mainSide] + referenceOffsets[measurement] / 2 - popperRect[measurement] / 2;
+ if (placement === secondarySide) {
+ popperOffsets[secondarySide] = referenceOffsets[secondarySide] - popperRect[secondaryMeasurement];
+ } else {
+ popperOffsets[secondarySide] = referenceOffsets[getOppositePlacement(secondarySide)];
+ }
+
+ return popperOffsets;
+}
+
+/**
+ * Mimics the `find` method of Array
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Array} arr
+ * @argument prop
+ * @argument value
+ * @returns index or -1
+ */
+function find(arr, check) {
+ // use native find if supported
+ if (Array.prototype.find) {
+ return arr.find(check);
+ }
+
+ // use `filter` to obtain the same behavior of `find`
+ return arr.filter(check)[0];
+}
+
+/**
+ * Return the index of the matching object
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Array} arr
+ * @argument prop
+ * @argument value
+ * @returns index or -1
+ */
+function findIndex(arr, prop, value) {
+ // use native findIndex if supported
+ if (Array.prototype.findIndex) {
+ return arr.findIndex(function (cur) {
+ return cur[prop] === value;
+ });
+ }
+
+ // use `find` + `indexOf` if `findIndex` isn't supported
+ var match = find(arr, function (obj) {
+ return obj[prop] === value;
+ });
+ return arr.indexOf(match);
+}
+
+/**
+ * Loop trough the list of modifiers and run them in order,
+ * each of them will then edit the data object.
+ * @method
+ * @memberof Popper.Utils
+ * @param {dataObject} data
+ * @param {Array} modifiers
+ * @param {String} ends - Optional modifier name used as stopper
+ * @returns {dataObject}
+ */
+function runModifiers(modifiers, data, ends) {
+ var modifiersToRun = ends === undefined ? modifiers : modifiers.slice(0, findIndex(modifiers, 'name', ends));
+
+ modifiersToRun.forEach(function (modifier) {
+ if (modifier['function']) {
+ // eslint-disable-line dot-notation
+ console.warn('`modifier.function` is deprecated, use `modifier.fn`!');
+ }
+ var fn = modifier['function'] || modifier.fn; // eslint-disable-line dot-notation
+ if (modifier.enabled && isFunction(fn)) {
+ // Add properties to offsets to make them a complete clientRect object
+ // we do this before each modifier to make sure the previous one doesn't
+ // mess with these values
+ data.offsets.popper = getClientRect(data.offsets.popper);
+ data.offsets.reference = getClientRect(data.offsets.reference);
+
+ data = fn(data, modifier);
+ }
+ });
+
+ return data;
+}
+
+/**
+ * Updates the position of the popper, computing the new offsets and applying
+ * the new style.<br />
+ * Prefer `scheduleUpdate` over `update` because of performance reasons.
+ * @method
+ * @memberof Popper
+ */
+function update() {
+ // if popper is destroyed, don't perform any further update
+ if (this.state.isDestroyed) {
+ return;
+ }
+
+ var data = {
+ instance: this,
+ styles: {},
+ arrowStyles: {},
+ attributes: {},
+ flipped: false,
+ offsets: {}
+ };
+
+ // compute reference element offsets
+ data.offsets.reference = getReferenceOffsets(this.state, this.popper, this.reference, this.options.positionFixed);
+
+ // compute auto placement, store placement inside the data object,
+ // modifiers will be able to edit `placement` if needed
+ // and refer to originalPlacement to know the original value
+ data.placement = computeAutoPlacement(this.options.placement, data.offsets.reference, this.popper, this.reference, this.options.modifiers.flip.boundariesElement, this.options.modifiers.flip.padding);
+
+ // store the computed placement inside `originalPlacement`
+ data.originalPlacement = data.placement;
+
+ data.positionFixed = this.options.positionFixed;
+
+ // compute the popper offsets
+ data.offsets.popper = getPopperOffsets(this.popper, data.offsets.reference, data.placement);
+
+ data.offsets.popper.position = this.options.positionFixed ? 'fixed' : 'absolute';
+
+ // run the modifiers
+ data = runModifiers(this.modifiers, data);
+
+ // the first `update` will call `onCreate` callback
+ // the other ones will call `onUpdate` callback
+ if (!this.state.isCreated) {
+ this.state.isCreated = true;
+ this.options.onCreate(data);
+ } else {
+ this.options.onUpdate(data);
+ }
+}
+
+/**
+ * Helper used to know if the given modifier is enabled.
+ * @method
+ * @memberof Popper.Utils
+ * @returns {Boolean}
+ */
+function isModifierEnabled(modifiers, modifierName) {
+ return modifiers.some(function (_ref) {
+ var name = _ref.name,
+ enabled = _ref.enabled;
+ return enabled && name === modifierName;
+ });
+}
+
+/**
+ * Get the prefixed supported property name
+ * @method
+ * @memberof Popper.Utils
+ * @argument {String} property (camelCase)
+ * @returns {String} prefixed property (camelCase or PascalCase, depending on the vendor prefix)
+ */
+function getSupportedPropertyName(property) {
+ var prefixes = [false, 'ms', 'Webkit', 'Moz', 'O'];
+ var upperProp = property.charAt(0).toUpperCase() + property.slice(1);
+
+ for (var i = 0; i < prefixes.length; i++) {
+ var prefix = prefixes[i];
+ var toCheck = prefix ? '' + prefix + upperProp : property;
+ if (typeof document.body.style[toCheck] !== 'undefined') {
+ return toCheck;
+ }
+ }
+ return null;
+}
+
+/**
+ * Destroys the popper.
+ * @method
+ * @memberof Popper
+ */
+function destroy() {
+ this.state.isDestroyed = true;
+
+ // touch DOM only if `applyStyle` modifier is enabled
+ if (isModifierEnabled(this.modifiers, 'applyStyle')) {
+ this.popper.removeAttribute('x-placement');
+ this.popper.style.position = '';
+ this.popper.style.top = '';
+ this.popper.style.left = '';
+ this.popper.style.right = '';
+ this.popper.style.bottom = '';
+ this.popper.style.willChange = '';
+ this.popper.style[getSupportedPropertyName('transform')] = '';
+ }
+
+ this.disableEventListeners();
+
+ // remove the popper if user explicity asked for the deletion on destroy
+ // do not use `remove` because IE11 doesn't support it
+ if (this.options.removeOnDestroy) {
+ this.popper.parentNode.removeChild(this.popper);
+ }
+ return this;
+}
+
+/**
+ * Get the window associated with the element
+ * @argument {Element} element
+ * @returns {Window}
+ */
+function getWindow(element) {
+ var ownerDocument = element.ownerDocument;
+ return ownerDocument ? ownerDocument.defaultView : window;
+}
+
+function attachToScrollParents(scrollParent, event, callback, scrollParents) {
+ var isBody = scrollParent.nodeName === 'BODY';
+ var target = isBody ? scrollParent.ownerDocument.defaultView : scrollParent;
+ target.addEventListener(event, callback, { passive: true });
+
+ if (!isBody) {
+ attachToScrollParents(getScrollParent(target.parentNode), event, callback, scrollParents);
+ }
+ scrollParents.push(target);
+}
+
+/**
+ * Setup needed event listeners used to update the popper position
+ * @method
+ * @memberof Popper.Utils
+ * @private
+ */
+function setupEventListeners(reference, options, state, updateBound) {
+ // Resize event listener on window
+ state.updateBound = updateBound;
+ getWindow(reference).addEventListener('resize', state.updateBound, { passive: true });
+
+ // Scroll event listener on scroll parents
+ var scrollElement = getScrollParent(reference);
+ attachToScrollParents(scrollElement, 'scroll', state.updateBound, state.scrollParents);
+ state.scrollElement = scrollElement;
+ state.eventsEnabled = true;
+
+ return state;
+}
+
+/**
+ * It will add resize/scroll events and start recalculating
+ * position of the popper element when they are triggered.
+ * @method
+ * @memberof Popper
+ */
+function enableEventListeners() {
+ if (!this.state.eventsEnabled) {
+ this.state = setupEventListeners(this.reference, this.options, this.state, this.scheduleUpdate);
+ }
+}
+
+/**
+ * Remove event listeners used to update the popper position
+ * @method
+ * @memberof Popper.Utils
+ * @private
+ */
+function removeEventListeners(reference, state) {
+ // Remove resize event listener on window
+ getWindow(reference).removeEventListener('resize', state.updateBound);
+
+ // Remove scroll event listener on scroll parents
+ state.scrollParents.forEach(function (target) {
+ target.removeEventListener('scroll', state.updateBound);
+ });
+
+ // Reset state
+ state.updateBound = null;
+ state.scrollParents = [];
+ state.scrollElement = null;
+ state.eventsEnabled = false;
+ return state;
+}
+
+/**
+ * It will remove resize/scroll events and won't recalculate popper position
+ * when they are triggered. It also won't trigger `onUpdate` callback anymore,
+ * unless you call `update` method manually.
+ * @method
+ * @memberof Popper
+ */
+function disableEventListeners() {
+ if (this.state.eventsEnabled) {
+ cancelAnimationFrame(this.scheduleUpdate);
+ this.state = removeEventListeners(this.reference, this.state);
+ }
+}
+
+/**
+ * Tells if a given input is a number
+ * @method
+ * @memberof Popper.Utils
+ * @param {*} input to check
+ * @return {Boolean}
+ */
+function isNumeric(n) {
+ return n !== '' && !isNaN(parseFloat(n)) && isFinite(n);
+}
+
+/**
+ * Set the style to the given popper
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Element} element - Element to apply the style to
+ * @argument {Object} styles
+ * Object with a list of properties and values which will be applied to the element
+ */
+function setStyles(element, styles) {
+ Object.keys(styles).forEach(function (prop) {
+ var unit = '';
+ // add unit if the value is numeric and is one of the following
+ if (['width', 'height', 'top', 'right', 'bottom', 'left'].indexOf(prop) !== -1 && isNumeric(styles[prop])) {
+ unit = 'px';
+ }
+ element.style[prop] = styles[prop] + unit;
+ });
+}
+
+/**
+ * Set the attributes to the given popper
+ * @method
+ * @memberof Popper.Utils
+ * @argument {Element} element - Element to apply the attributes to
+ * @argument {Object} styles
+ * Object with a list of properties and values which will be applied to the element
+ */
+function setAttributes(element, attributes) {
+ Object.keys(attributes).forEach(function (prop) {
+ var value = attributes[prop];
+ if (value !== false) {
+ element.setAttribute(prop, attributes[prop]);
+ } else {
+ element.removeAttribute(prop);
+ }
+ });
+}
+
+/**
+ * @function
+ * @memberof Modifiers
+ * @argument {Object} data - The data object generated by `update` method
+ * @argument {Object} data.styles - List of style properties - values to apply to popper element
+ * @argument {Object} data.attributes - List of attribute properties - values to apply to popper element
+ * @argument {Object} options - Modifiers configuration and options
+ * @returns {Object} The same data object
+ */
+function applyStyle(data) {
+ // any property present in `data.styles` will be applied to the popper,
+ // in this way we can make the 3rd party modifiers add custom styles to it
+ // Be aware, modifiers could override the properties defined in the previous
+ // lines of this modifier!
+ setStyles(data.instance.popper, data.styles);
+
+ // any property present in `data.attributes` will be applied to the popper,
+ // they will be set as HTML attributes of the element
+ setAttributes(data.instance.popper, data.attributes);
+
+ // if arrowElement is defined and arrowStyles has some properties
+ if (data.arrowElement && Object.keys(data.arrowStyles).length) {
+ setStyles(data.arrowElement, data.arrowStyles);
+ }
+
+ return data;
+}
+
+/**
+ * Set the x-placement attribute before everything else because it could be used
+ * to add margins to the popper margins needs to be calculated to get the
+ * correct popper offsets.
+ * @method
+ * @memberof Popper.modifiers
+ * @param {HTMLElement} reference - The reference element used to position the popper
+ * @param {HTMLElement} popper - The HTML element used as popper
+ * @param {Object} options - Popper.js options
+ */
+function applyStyleOnLoad(reference, popper, options, modifierOptions, state) {
+ // compute reference element offsets
+ var referenceOffsets = getReferenceOffsets(state, popper, reference, options.positionFixed);
+
+ // compute auto placement, store placement inside the data object,
+ // modifiers will be able to edit `placement` if needed
+ // and refer to originalPlacement to know the original value
+ var placement = computeAutoPlacement(options.placement, referenceOffsets, popper, reference, options.modifiers.flip.boundariesElement, options.modifiers.flip.padding);
+
+ popper.setAttribute('x-placement', placement);
+
+ // Apply `position` to popper before anything else because
+ // without the position applied we can't guarantee correct computations
+ setStyles(popper, { position: options.positionFixed ? 'fixed' : 'absolute' });
+
+ return options;
+}
+
+/**
+ * @function
+ * @memberof Popper.Utils
+ * @argument {Object} data - The data object generated by `update` method
+ * @argument {Boolean} shouldRound - If the offsets should be rounded at all
+ * @returns {Object} The popper's position offsets rounded
+ *
+ * The tale of pixel-perfect positioning. It's still not 100% perfect, but as
+ * good as it can be within reason.
+ * Discussion here: https://github.com/FezVrasta/popper.js/pull/715
+ *
+ * Low DPI screens cause a popper to be blurry if not using full pixels (Safari
+ * as well on High DPI screens).
+ *
+ * Firefox prefers no rounding for positioning and does not have blurriness on
+ * high DPI screens.
+ *
+ * Only horizontal placement and left/right values need to be considered.
+ */
+function getRoundedOffsets(data, shouldRound) {
+ var _data$offsets = data.offsets,
+ popper = _data$offsets.popper,
+ reference = _data$offsets.reference;
+ var round = Math.round,
+ floor = Math.floor;
+
+ var noRound = function noRound(v) {
+ return v;
+ };
+
+ var referenceWidth = round(reference.width);
+ var popperWidth = round(popper.width);
+
+ var isVertical = ['left', 'right'].indexOf(data.placement) !== -1;
+ var isVariation = data.placement.indexOf('-') !== -1;
+ var sameWidthParity = referenceWidth % 2 === popperWidth % 2;
+ var bothOddWidth = referenceWidth % 2 === 1 && popperWidth % 2 === 1;
+
+ var horizontalToInteger = !shouldRound ? noRound : isVertical || isVariation || sameWidthParity ? round : floor;
+ var verticalToInteger = !shouldRound ? noRound : round;
+
+ return {
+ left: horizontalToInteger(bothOddWidth && !isVariation && shouldRound ? popper.left - 1 : popper.left),
+ top: verticalToInteger(popper.top),
+ bottom: verticalToInteger(popper.bottom),
+ right: horizontalToInteger(popper.right)
+ };
+}
+
+var isFirefox = isBrowser && /Firefox/i.test(navigator.userAgent);
+
+/**
+ * @function
+ * @memberof Modifiers
+ * @argument {Object} data - The data object generated by `update` method
+ * @argument {Object} options - Modifiers configuration and options
+ * @returns {Object} The data object, properly modified
+ */
+function computeStyle(data, options) {
+ var x = options.x,
+ y = options.y;
+ var popper = data.offsets.popper;
+
+ // Remove this legacy support in Popper.js v2
+
+ var legacyGpuAccelerationOption = find(data.instance.modifiers, function (modifier) {
+ return modifier.name === 'applyStyle';
+ }).gpuAcceleration;
+ if (legacyGpuAccelerationOption !== undefined) {
+ console.warn('WARNING: `gpuAcceleration` option moved to `computeStyle` modifier and will not be supported in future versions of Popper.js!');
+ }
+ var gpuAcceleration = legacyGpuAccelerationOption !== undefined ? legacyGpuAccelerationOption : options.gpuAcceleration;
+
+ var offsetParent = getOffsetParent(data.instance.popper);
+ var offsetParentRect = getBoundingClientRect(offsetParent);
+
+ // Styles
+ var styles = {
+ position: popper.position
+ };
+
+ var offsets = getRoundedOffsets(data, window.devicePixelRatio < 2 || !isFirefox);
+
+ var sideA = x === 'bottom' ? 'top' : 'bottom';
+ var sideB = y === 'right' ? 'left' : 'right';
+
+ // if gpuAcceleration is set to `true` and transform is supported,
+ // we use `translate3d` to apply the position to the popper we
+ // automatically use the supported prefixed version if needed
+ var prefixedProperty = getSupportedPropertyName('transform');
+
+ // now, let's make a step back and look at this code closely (wtf?)
+ // If the content of the popper grows once it's been positioned, it
+ // may happen that the popper gets misplaced because of the new content
+ // overflowing its reference element
+ // To avoid this problem, we provide two options (x and y), which allow
+ // the consumer to define the offset origin.
+ // If we position a popper on top of a reference element, we can set
+ // `x` to `top` to make the popper grow towards its top instead of
+ // its bottom.
+ var left = void 0,
+ top = void 0;
+ if (sideA === 'bottom') {
+ // when offsetParent is <html> the positioning is relative to the bottom of the screen (excluding the scrollbar)
+ // and not the bottom of the html element
+ if (offsetParent.nodeName === 'HTML') {
+ top = -offsetParent.clientHeight + offsets.bottom;
+ } else {
+ top = -offsetParentRect.height + offsets.bottom;
+ }
+ } else {
+ top = offsets.top;
+ }
+ if (sideB === 'right') {
+ if (offsetParent.nodeName === 'HTML') {
+ left = -offsetParent.clientWidth + offsets.right;
+ } else {
+ left = -offsetParentRect.width + offsets.right;
+ }
+ } else {
+ left = offsets.left;
+ }
+ if (gpuAcceleration && prefixedProperty) {
+ styles[prefixedProperty] = 'translate3d(' + left + 'px, ' + top + 'px, 0)';
+ styles[sideA] = 0;
+ styles[sideB] = 0;
+ styles.willChange = 'transform';
+ } else {
+ // othwerise, we use the standard `top`, `left`, `bottom` and `right` properties
+ var invertTop = sideA === 'bottom' ? -1 : 1;
+ var invertLeft = sideB === 'right' ? -1 : 1;
+ styles[sideA] = top * invertTop;
+ styles[sideB] = left * invertLeft;
+ styles.willChange = sideA + ', ' + sideB;
+ }
+
+ // Attributes
+ var attributes = {
+ 'x-placement': data.placement
+ };
+
+ // Update `data` attributes, styles and arrowStyles
+ data.attributes = _extends({}, attributes, data.attributes);
+ data.styles = _extends({}, styles, data.styles);
+ data.arrowStyles = _extends({}, data.offsets.arrow, data.arrowStyles);
+
+ return data;
+}
+
+/**
+ * Helper used to know if the given modifier depends from another one.<br />
+ * It checks if the needed modifier is listed and enabled.
+ * @method
+ * @memberof Popper.Utils
+ * @param {Array} modifiers - list of modifiers
+ * @param {String} requestingName - name of requesting modifier
+ * @param {String} requestedName - name of requested modifier
+ * @returns {Boolean}
+ */
+function isModifierRequired(modifiers, requestingName, requestedName) {
+ var requesting = find(modifiers, function (_ref) {
+ var name = _ref.name;
+ return name === requestingName;
+ });
+
+ var isRequired = !!requesting && modifiers.some(function (modifier) {
+ return modifier.name === requestedName && modifier.enabled && modifier.order < requesting.order;
+ });
+
+ if (!isRequired) {
+ var _requesting = '`' + requestingName + '`';
+ var requested = '`' + requestedName + '`';
+ console.warn(requested + ' modifier is required by ' + _requesting + ' modifier in order to work, be sure to include it before ' + _requesting + '!');
+ }
+ return isRequired;
+}
+
+/**
+ * @function
+ * @memberof Modifiers
+ * @argument {Object} data - The data object generated by update method
+ * @argument {Object} options - Modifiers configuration and options
+ * @returns {Object} The data object, properly modified
+ */
+function arrow(data, options) {
+ var _data$offsets$arrow;
+
+ // arrow depends on keepTogether in order to work
+ if (!isModifierRequired(data.instance.modifiers, 'arrow', 'keepTogether')) {
+ return data;
+ }
+
+ var arrowElement = options.element;
+
+ // if arrowElement is a string, suppose it's a CSS selector
+ if (typeof arrowElement === 'string') {
+ arrowElement = data.instance.popper.querySelector(arrowElement);
+
+ // if arrowElement is not found, don't run the modifier
+ if (!arrowElement) {
+ return data;
+ }
+ } else {
+ // if the arrowElement isn't a query selector we must check that the
+ // provided DOM node is child of its popper node
+ if (!data.instance.popper.contains(arrowElement)) {
+ console.warn('WARNING: `arrow.element` must be child of its popper element!');
+ return data;
+ }
+ }
+
+ var placement = data.placement.split('-')[0];
+ var _data$offsets = data.offsets,
+ popper = _data$offsets.popper,
+ reference = _data$offsets.reference;
+
+ var isVertical = ['left', 'right'].indexOf(placement) !== -1;
+
+ var len = isVertical ? 'height' : 'width';
+ var sideCapitalized = isVertical ? 'Top' : 'Left';
+ var side = sideCapitalized.toLowerCase();
+ var altSide = isVertical ? 'left' : 'top';
+ var opSide = isVertical ? 'bottom' : 'right';
+ var arrowElementSize = getOuterSizes(arrowElement)[len];
+
+ //
+ // extends keepTogether behavior making sure the popper and its
+ // reference have enough pixels in conjunction
+ //
+
+ // top/left side
+ if (reference[opSide] - arrowElementSize < popper[side]) {
+ data.offsets.popper[side] -= popper[side] - (reference[opSide] - arrowElementSize);
+ }
+ // bottom/right side
+ if (reference[side] + arrowElementSize > popper[opSide]) {
+ data.offsets.popper[side] += reference[side] + arrowElementSize - popper[opSide];
+ }
+ data.offsets.popper = getClientRect(data.offsets.popper);
+
+ // compute center of the popper
+ var center = reference[side] + reference[len] / 2 - arrowElementSize / 2;
+
+ // Compute the sideValue using the updated popper offsets
+ // take popper margin in account because we don't have this info available
+ var css = getStyleComputedProperty(data.instance.popper);
+ var popperMarginSide = parseFloat(css['margin' + sideCapitalized], 10);
+ var popperBorderSide = parseFloat(css['border' + sideCapitalized + 'Width'], 10);
+ var sideValue = center - data.offsets.popper[side] - popperMarginSide - popperBorderSide;
+
+ // prevent arrowElement from being placed not contiguously to its popper
+ sideValue = Math.max(Math.min(popper[len] - arrowElementSize, sideValue), 0);
+
+ data.arrowElement = arrowElement;
+ data.offsets.arrow = (_data$offsets$arrow = {}, defineProperty(_data$offsets$arrow, side, Math.round(sideValue)), defineProperty(_data$offsets$arrow, altSide, ''), _data$offsets$arrow);
+
+ return data;
+}
+
+/**
+ * Get the opposite placement variation of the given one
+ * @method
+ * @memberof Popper.Utils
+ * @argument {String} placement variation
+ * @returns {String} flipped placement variation
+ */
+function getOppositeVariation(variation) {
+ if (variation === 'end') {
+ return 'start';
+ } else if (variation === 'start') {
+ return 'end';
+ }
+ return variation;
+}
+
+/**
+ * List of accepted placements to use as values of the `placement` option.<br />
+ * Valid placements are:
+ * - `auto`
+ * - `top`
+ * - `right`
+ * - `bottom`
+ * - `left`
+ *
+ * Each placement can have a variation from this list:
+ * - `-start`
+ * - `-end`
+ *
+ * Variations are interpreted easily if you think of them as the left to right
+ * written languages. Horizontally (`top` and `bottom`), `start` is left and `end`
+ * is right.<br />
+ * Vertically (`left` and `right`), `start` is top and `end` is bottom.
+ *
+ * Some valid examples are:
+ * - `top-end` (on top of reference, right aligned)
+ * - `right-start` (on right of reference, top aligned)
+ * - `bottom` (on bottom, centered)
+ * - `auto-end` (on the side with more space available, alignment depends by placement)
+ *
+ * @static
+ * @type {Array}
+ * @enum {String}
+ * @readonly
+ * @method placements
+ * @memberof Popper
+ */
+var placements = ['auto-start', 'auto', 'auto-end', 'top-start', 'top', 'top-end', 'right-start', 'right', 'right-end', 'bottom-end', 'bottom', 'bottom-start', 'left-end', 'left', 'left-start'];
+
+// Get rid of `auto` `auto-start` and `auto-end`
+var validPlacements = placements.slice(3);
+
+/**
+ * Given an initial placement, returns all the subsequent placements
+ * clockwise (or counter-clockwise).
+ *
+ * @method
+ * @memberof Popper.Utils
+ * @argument {String} placement - A valid placement (it accepts variations)
+ * @argument {Boolean} counter - Set to true to walk the placements counterclockwise
+ * @returns {Array} placements including their variations
+ */
+function clockwise(placement) {
+ var counter = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+
+ var index = validPlacements.indexOf(placement);
+ var arr = validPlacements.slice(index + 1).concat(validPlacements.slice(0, index));
+ return counter ? arr.reverse() : arr;
+}
+
+var BEHAVIORS = {
+ FLIP: 'flip',
+ CLOCKWISE: 'clockwise',
+ COUNTERCLOCKWISE: 'counterclockwise'
+};
+
+/**
+ * @function
+ * @memberof Modifiers
+ * @argument {Object} data - The data object generated by update method
+ * @argument {Object} options - Modifiers configuration and options
+ * @returns {Object} The data object, properly modified
+ */
+function flip(data, options) {
+ // if `inner` modifier is enabled, we can't use the `flip` modifier
+ if (isModifierEnabled(data.instance.modifiers, 'inner')) {
+ return data;
+ }
+
+ if (data.flipped && data.placement === data.originalPlacement) {
+ // seems like flip is trying to loop, probably there's not enough space on any of the flippable sides
+ return data;
+ }
+
+ var boundaries = getBoundaries(data.instance.popper, data.instance.reference, options.padding, options.boundariesElement, data.positionFixed);
+
+ var placement = data.placement.split('-')[0];
+ var placementOpposite = getOppositePlacement(placement);
+ var variation = data.placement.split('-')[1] || '';
+
+ var flipOrder = [];
+
+ switch (options.behavior) {
+ case BEHAVIORS.FLIP:
+ flipOrder = [placement, placementOpposite];
+ break;
+ case BEHAVIORS.CLOCKWISE:
+ flipOrder = clockwise(placement);
+ break;
+ case BEHAVIORS.COUNTERCLOCKWISE:
+ flipOrder = clockwise(placement, true);
+ break;
+ default:
+ flipOrder = options.behavior;
+ }
+
+ flipOrder.forEach(function (step, index) {
+ if (placement !== step || flipOrder.length === index + 1) {
+ return data;
+ }
+
+ placement = data.placement.split('-')[0];
+ placementOpposite = getOppositePlacement(placement);
+
+ var popperOffsets = data.offsets.popper;
+ var refOffsets = data.offsets.reference;
+
+ // using floor because the reference offsets may contain decimals we are not going to consider here
+ var floor = Math.floor;
+ var overlapsRef = placement === 'left' && floor(popperOffsets.right) > floor(refOffsets.left) || placement === 'right' && floor(popperOffsets.left) < floor(refOffsets.right) || placement === 'top' && floor(popperOffsets.bottom) > floor(refOffsets.top) || placement === 'bottom' && floor(popperOffsets.top) < floor(refOffsets.bottom);
+
+ var overflowsLeft = floor(popperOffsets.left) < floor(boundaries.left);
+ var overflowsRight = floor(popperOffsets.right) > floor(boundaries.right);
+ var overflowsTop = floor(popperOffsets.top) < floor(boundaries.top);
+ var overflowsBottom = floor(popperOffsets.bottom) > floor(boundaries.bottom);
+
+ var overflowsBoundaries = placement === 'left' && overflowsLeft || placement === 'right' && overflowsRight || placement === 'top' && overflowsTop || placement === 'bottom' && overflowsBottom;
+
+ // flip the variation if required
+ var isVertical = ['top', 'bottom'].indexOf(placement) !== -1;
+
+ // flips variation if reference element overflows boundaries
+ var flippedVariationByRef = !!options.flipVariations && (isVertical && variation === 'start' && overflowsLeft || isVertical && variation === 'end' && overflowsRight || !isVertical && variation === 'start' && overflowsTop || !isVertical && variation === 'end' && overflowsBottom);
+
+ // flips variation if popper content overflows boundaries
+ var flippedVariationByContent = !!options.flipVariationsByContent && (isVertical && variation === 'start' && overflowsRight || isVertical && variation === 'end' && overflowsLeft || !isVertical && variation === 'start' && overflowsBottom || !isVertical && variation === 'end' && overflowsTop);
+
+ var flippedVariation = flippedVariationByRef || flippedVariationByContent;
+
+ if (overlapsRef || overflowsBoundaries || flippedVariation) {
+ // this boolean to detect any flip loop
+ data.flipped = true;
+
+ if (overlapsRef || overflowsBoundaries) {
+ placement = flipOrder[index + 1];
+ }
+
+ if (flippedVariation) {
+ variation = getOppositeVariation(variation);
+ }
+
+ data.placement = placement + (variation ? '-' + variation : '');
+
+ // this object contains `position`, we want to preserve it along with
+ // any additional property we may add in the future
+ data.offsets.popper = _extends({}, data.offsets.popper, getPopperOffsets(data.instance.popper, data.offsets.reference, data.placement));
+
+ data = runModifiers(data.instance.modifiers, data, 'flip');
+ }
+ });
+ return data;
+}
+
+/**
+ * @function
+ * @memberof Modifiers
+ * @argument {Object} data - The data object generated by update method
+ * @argument {Object} options - Modifiers configuration and options
+ * @returns {Object} The data object, properly modified
+ */
+function keepTogether(data) {
+ var _data$offsets = data.offsets,
+ popper = _data$offsets.popper,
+ reference = _data$offsets.reference;
+
+ var placement = data.placement.split('-')[0];
+ var floor = Math.floor;
+ var isVertical = ['top', 'bottom'].indexOf(placement) !== -1;
+ var side = isVertical ? 'right' : 'bottom';
+ var opSide = isVertical ? 'left' : 'top';
+ var measurement = isVertical ? 'width' : 'height';
+
+ if (popper[side] < floor(reference[opSide])) {
+ data.offsets.popper[opSide] = floor(reference[opSide]) - popper[measurement];
+ }
+ if (popper[opSide] > floor(reference[side])) {
+ data.offsets.popper[opSide] = floor(reference[side]);
+ }
+
+ return data;
+}
+
+/**
+ * Converts a string containing value + unit into a px value number
+ * @function
+ * @memberof {modifiers~offset}
+ * @private
+ * @argument {String} str - Value + unit string
+ * @argument {String} measurement - `height` or `width`
+ * @argument {Object} popperOffsets
+ * @argument {Object} referenceOffsets
+ * @returns {Number|String}
+ * Value in pixels, or original string if no values were extracted
+ */
+function toValue(str, measurement, popperOffsets, referenceOffsets) {
+ // separate value from unit
+ var split = str.match(/((?:\-|\+)?\d*\.?\d*)(.*)/);
+ var value = +split[1];
+ var unit = split[2];
+
+ // If it's not a number it's an operator, I guess
+ if (!value) {
+ return str;
+ }
+
+ if (unit.indexOf('%') === 0) {
+ var element = void 0;
+ switch (unit) {
+ case '%p':
+ element = popperOffsets;
+ break;
+ case '%':
+ case '%r':
+ default:
+ element = referenceOffsets;
+ }
+
+ var rect = getClientRect(element);
+ return rect[measurement] / 100 * value;
+ } else if (unit === 'vh' || unit === 'vw') {
+ // if is a vh or vw, we calculate the size based on the viewport
+ var size = void 0;
+ if (unit === 'vh') {
+ size = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
+ } else {
+ size = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
+ }
+ return size / 100 * value;
+ } else {
+ // if is an explicit pixel unit, we get rid of the unit and keep the value
+ // if is an implicit unit, it's px, and we return just the value
+ return value;
+ }
+}
+
+/**
+ * Parse an `offset` string to extrapolate `x` and `y` numeric offsets.
+ * @function
+ * @memberof {modifiers~offset}
+ * @private
+ * @argument {String} offset
+ * @argument {Object} popperOffsets
+ * @argument {Object} referenceOffsets
+ * @argument {String} basePlacement
+ * @returns {Array} a two cells array with x and y offsets in numbers
+ */
+function parseOffset(offset, popperOffsets, referenceOffsets, basePlacement) {
+ var offsets = [0, 0];
+
+ // Use height if placement is left or right and index is 0 otherwise use width
+ // in this way the first offset will use an axis and the second one
+ // will use the other one
+ var useHeight = ['right', 'left'].indexOf(basePlacement) !== -1;
+
+ // Split the offset string to obtain a list of values and operands
+ // The regex addresses values with the plus or minus sign in front (+10, -20, etc)
+ var fragments = offset.split(/(\+|\-)/).map(function (frag) {
+ return frag.trim();
+ });
+
+ // Detect if the offset string contains a pair of values or a single one
+ // they could be separated by comma or space
+ var divider = fragments.indexOf(find(fragments, function (frag) {
+ return frag.search(/,|\s/) !== -1;
+ }));
+
+ if (fragments[divider] && fragments[divider].indexOf(',') === -1) {
+ console.warn('Offsets separated by white space(s) are deprecated, use a comma (,) instead.');
+ }
+
+ // If divider is found, we divide the list of values and operands to divide
+ // them by ofset X and Y.
+ var splitRegex = /\s*,\s*|\s+/;
+ var ops = divider !== -1 ? [fragments.slice(0, divider).concat([fragments[divider].split(splitRegex)[0]]), [fragments[divider].split(splitRegex)[1]].concat(fragments.slice(divider + 1))] : [fragments];
+
+ // Convert the values with units to absolute pixels to allow our computations
+ ops = ops.map(function (op, index) {
+ // Most of the units rely on the orientation of the popper
+ var measurement = (index === 1 ? !useHeight : useHeight) ? 'height' : 'width';
+ var mergeWithPrevious = false;
+ return op
+ // This aggregates any `+` or `-` sign that aren't considered operators
+ // e.g.: 10 + +5 => [10, +, +5]
+ .reduce(function (a, b) {
+ if (a[a.length - 1] === '' && ['+', '-'].indexOf(b) !== -1) {
+ a[a.length - 1] = b;
+ mergeWithPrevious = true;
+ return a;
+ } else if (mergeWithPrevious) {
+ a[a.length - 1] += b;
+ mergeWithPrevious = false;
+ return a;
+ } else {
+ return a.concat(b);
+ }
+ }, [])
+ // Here we convert the string values into number values (in px)
+ .map(function (str) {
+ return toValue(str, measurement, popperOffsets, referenceOffsets);
+ });
+ });
+
+ // Loop trough the offsets arrays and execute the operations
+ ops.forEach(function (op, index) {
+ op.forEach(function (frag, index2) {
+ if (isNumeric(frag)) {
+ offsets[index] += frag * (op[index2 - 1] === '-' ? -1 : 1);
+ }
+ });
+ });
+ return offsets;
+}
+
+/**
+ * @function
+ * @memberof Modifiers
+ * @argument {Object} data - The data object generated by update method
+ * @argument {Object} options - Modifiers configuration and options
+ * @argument {Number|String} options.offset=0
+ * The offset value as described in the modifier description
+ * @returns {Object} The data object, properly modified
+ */
+function offset(data, _ref) {
+ var offset = _ref.offset;
+ var placement = data.placement,
+ _data$offsets = data.offsets,
+ popper = _data$offsets.popper,
+ reference = _data$offsets.reference;
+
+ var basePlacement = placement.split('-')[0];
+
+ var offsets = void 0;
+ if (isNumeric(+offset)) {
+ offsets = [+offset, 0];
+ } else {
+ offsets = parseOffset(offset, popper, reference, basePlacement);
+ }
+
+ if (basePlacement === 'left') {
+ popper.top += offsets[0];
+ popper.left -= offsets[1];
+ } else if (basePlacement === 'right') {
+ popper.top += offsets[0];
+ popper.left += offsets[1];
+ } else if (basePlacement === 'top') {
+ popper.left += offsets[0];
+ popper.top -= offsets[1];
+ } else if (basePlacement === 'bottom') {
+ popper.left += offsets[0];
+ popper.top += offsets[1];
+ }
+
+ data.popper = popper;
+ return data;
+}
+
+/**
+ * @function
+ * @memberof Modifiers
+ * @argument {Object} data - The data object generated by `update` method
+ * @argument {Object} options - Modifiers configuration and options
+ * @returns {Object} The data object, properly modified
+ */
+function preventOverflow(data, options) {
+ var boundariesElement = options.boundariesElement || getOffsetParent(data.instance.popper);
+
+ // If offsetParent is the reference element, we really want to
+ // go one step up and use the next offsetParent as reference to
+ // avoid to make this modifier completely useless and look like broken
+ if (data.instance.reference === boundariesElement) {
+ boundariesElement = getOffsetParent(boundariesElement);
+ }
+
+ // NOTE: DOM access here
+ // resets the popper's position so that the document size can be calculated excluding
+ // the size of the popper element itself
+ var transformProp = getSupportedPropertyName('transform');
+ var popperStyles = data.instance.popper.style; // assignment to help minification
+ var top = popperStyles.top,
+ left = popperStyles.left,
+ transform = popperStyles[transformProp];
+
+ popperStyles.top = '';
+ popperStyles.left = '';
+ popperStyles[transformProp] = '';
+
+ var boundaries = getBoundaries(data.instance.popper, data.instance.reference, options.padding, boundariesElement, data.positionFixed);
+
+ // NOTE: DOM access here
+ // restores the original style properties after the offsets have been computed
+ popperStyles.top = top;
+ popperStyles.left = left;
+ popperStyles[transformProp] = transform;
+
+ options.boundaries = boundaries;
+
+ var order = options.priority;
+ var popper = data.offsets.popper;
+
+ var check = {
+ primary: function primary(placement) {
+ var value = popper[placement];
+ if (popper[placement] < boundaries[placement] && !options.escapeWithReference) {
+ value = Math.max(popper[placement], boundaries[placement]);
+ }
+ return defineProperty({}, placement, value);
+ },
+ secondary: function secondary(placement) {
+ var mainSide = placement === 'right' ? 'left' : 'top';
+ var value = popper[mainSide];
+ if (popper[placement] > boundaries[placement] && !options.escapeWithReference) {
+ value = Math.min(popper[mainSide], boundaries[placement] - (placement === 'right' ? popper.width : popper.height));
+ }
+ return defineProperty({}, mainSide, value);
+ }
+ };
+
+ order.forEach(function (placement) {
+ var side = ['left', 'top'].indexOf(placement) !== -1 ? 'primary' : 'secondary';
+ popper = _extends({}, popper, check[side](placement));
+ });
+
+ data.offsets.popper = popper;
+
+ return data;
+}
+
+/**
+ * @function
+ * @memberof Modifiers
+ * @argument {Object} data - The data object generated by `update` method
+ * @argument {Object} options - Modifiers configuration and options
+ * @returns {Object} The data object, properly modified
+ */
+function shift(data) {
+ var placement = data.placement;
+ var basePlacement = placement.split('-')[0];
+ var shiftvariation = placement.split('-')[1];
+
+ // if shift shiftvariation is specified, run the modifier
+ if (shiftvariation) {
+ var _data$offsets = data.offsets,
+ reference = _data$offsets.reference,
+ popper = _data$offsets.popper;
+
+ var isVertical = ['bottom', 'top'].indexOf(basePlacement) !== -1;
+ var side = isVertical ? 'left' : 'top';
+ var measurement = isVertical ? 'width' : 'height';
+
+ var shiftOffsets = {
+ start: defineProperty({}, side, reference[side]),
+ end: defineProperty({}, side, reference[side] + reference[measurement] - popper[measurement])
+ };
+
+ data.offsets.popper = _extends({}, popper, shiftOffsets[shiftvariation]);
+ }
+
+ return data;
+}
+
+/**
+ * @function
+ * @memberof Modifiers
+ * @argument {Object} data - The data object generated by update method
+ * @argument {Object} options - Modifiers configuration and options
+ * @returns {Object} The data object, properly modified
+ */
+function hide(data) {
+ if (!isModifierRequired(data.instance.modifiers, 'hide', 'preventOverflow')) {
+ return data;
+ }
+
+ var refRect = data.offsets.reference;
+ var bound = find(data.instance.modifiers, function (modifier) {
+ return modifier.name === 'preventOverflow';
+ }).boundaries;
+
+ if (refRect.bottom < bound.top || refRect.left > bound.right || refRect.top > bound.bottom || refRect.right < bound.left) {
+ // Avoid unnecessary DOM access if visibility hasn't changed
+ if (data.hide === true) {
+ return data;
+ }
+
+ data.hide = true;
+ data.attributes['x-out-of-boundaries'] = '';
+ } else {
+ // Avoid unnecessary DOM access if visibility hasn't changed
+ if (data.hide === false) {
+ return data;
+ }
+
+ data.hide = false;
+ data.attributes['x-out-of-boundaries'] = false;
+ }
+
+ return data;
+}
+
+/**
+ * @function
+ * @memberof Modifiers
+ * @argument {Object} data - The data object generated by `update` method
+ * @argument {Object} options - Modifiers configuration and options
+ * @returns {Object} The data object, properly modified
+ */
+function inner(data) {
+ var placement = data.placement;
+ var basePlacement = placement.split('-')[0];
+ var _data$offsets = data.offsets,
+ popper = _data$offsets.popper,
+ reference = _data$offsets.reference;
+
+ var isHoriz = ['left', 'right'].indexOf(basePlacement) !== -1;
+
+ var subtractLength = ['top', 'left'].indexOf(basePlacement) === -1;
+
+ popper[isHoriz ? 'left' : 'top'] = reference[basePlacement] - (subtractLength ? popper[isHoriz ? 'width' : 'height'] : 0);
+
+ data.placement = getOppositePlacement(placement);
+ data.offsets.popper = getClientRect(popper);
+
+ return data;
+}
+
+/**
+ * Modifier function, each modifier can have a function of this type assigned
+ * to its `fn` property.<br />
+ * These functions will be called on each update, this means that you must
+ * make sure they are performant enough to avoid performance bottlenecks.
+ *
+ * @function ModifierFn
+ * @argument {dataObject} data - The data object generated by `update` method
+ * @argument {Object} options - Modifiers configuration and options
+ * @returns {dataObject} The data object, properly modified
+ */
+
+/**
+ * Modifiers are plugins used to alter the behavior of your poppers.<br />
+ * Popper.js uses a set of 9 modifiers to provide all the basic functionalities
+ * needed by the library.
+ *
+ * Usually you don't want to override the `order`, `fn` and `onLoad` props.
+ * All the other properties are configurations that could be tweaked.
+ * @namespace modifiers
+ */
+var modifiers = {
+ /**
+ * Modifier used to shift the popper on the start or end of its reference
+ * element.<br />
+ * It will read the variation of the `placement` property.<br />
+ * It can be one either `-end` or `-start`.
+ * @memberof modifiers
+ * @inner
+ */
+ shift: {
+ /** @prop {number} order=100 - Index used to define the order of execution */
+ order: 100,
+ /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */
+ enabled: true,
+ /** @prop {ModifierFn} */
+ fn: shift
+ },
+
+ /**
+ * The `offset` modifier can shift your popper on both its axis.
+ *
+ * It accepts the following units:
+ * - `px` or unit-less, interpreted as pixels
+ * - `%` or `%r`, percentage relative to the length of the reference element
+ * - `%p`, percentage relative to the length of the popper element
+ * - `vw`, CSS viewport width unit
+ * - `vh`, CSS viewport height unit
+ *
+ * For length is intended the main axis relative to the placement of the popper.<br />
+ * This means that if the placement is `top` or `bottom`, the length will be the
+ * `width`. In case of `left` or `right`, it will be the `height`.
+ *
+ * You can provide a single value (as `Number` or `String`), or a pair of values
+ * as `String` divided by a comma or one (or more) white spaces.<br />
+ * The latter is a deprecated method because it leads to confusion and will be
+ * removed in v2.<br />
+ * Additionally, it accepts additions and subtractions between different units.
+ * Note that multiplications and divisions aren't supported.
+ *
+ * Valid examples are:
+ * ```
+ * 10
+ * '10%'
+ * '10, 10'
+ * '10%, 10'
+ * '10 + 10%'
+ * '10 - 5vh + 3%'
+ * '-10px + 5vh, 5px - 6%'
+ * ```
+ * > **NB**: If you desire to apply offsets to your poppers in a way that may make them overlap
+ * > with their reference element, unfortunately, you will have to disable the `flip` modifier.
+ * > You can read more on this at this [issue](https://github.com/FezVrasta/popper.js/issues/373).
+ *
+ * @memberof modifiers
+ * @inner
+ */
+ offset: {
+ /** @prop {number} order=200 - Index used to define the order of execution */
+ order: 200,
+ /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */
+ enabled: true,
+ /** @prop {ModifierFn} */
+ fn: offset,
+ /** @prop {Number|String} offset=0
+ * The offset value as described in the modifier description
+ */
+ offset: 0
+ },
+
+ /**
+ * Modifier used to prevent the popper from being positioned outside the boundary.
+ *
+ * A scenario exists where the reference itself is not within the boundaries.<br />
+ * We can say it has "escaped the boundaries" — or just "escaped".<br />
+ * In this case we need to decide whether the popper should either:
+ *
+ * - detach from the reference and remain "trapped" in the boundaries, or
+ * - if it should ignore the boundary and "escape with its reference"
+ *
+ * When `escapeWithReference` is set to`true` and reference is completely
+ * outside its boundaries, the popper will overflow (or completely leave)
+ * the boundaries in order to remain attached to the edge of the reference.
+ *
+ * @memberof modifiers
+ * @inner
+ */
+ preventOverflow: {
+ /** @prop {number} order=300 - Index used to define the order of execution */
+ order: 300,
+ /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */
+ enabled: true,
+ /** @prop {ModifierFn} */
+ fn: preventOverflow,
+ /**
+ * @prop {Array} [priority=['left','right','top','bottom']]
+ * Popper will try to prevent overflow following these priorities by default,
+ * then, it could overflow on the left and on top of the `boundariesElement`
+ */
+ priority: ['left', 'right', 'top', 'bottom'],
+ /**
+ * @prop {number} padding=5
+ * Amount of pixel used to define a minimum distance between the boundaries
+ * and the popper. This makes sure the popper always has a little padding
+ * between the edges of its container
+ */
+ padding: 5,
+ /**
+ * @prop {String|HTMLElement} boundariesElement='scrollParent'
+ * Boundaries used by the modifier. Can be `scrollParent`, `window`,
+ * `viewport` or any DOM element.
+ */
+ boundariesElement: 'scrollParent'
+ },
+
+ /**
+ * Modifier used to make sure the reference and its popper stay near each other
+ * without leaving any gap between the two. Especially useful when the arrow is
+ * enabled and you want to ensure that it points to its reference element.
+ * It cares only about the first axis. You can still have poppers with margin
+ * between the popper and its reference element.
+ * @memberof modifiers
+ * @inner
+ */
+ keepTogether: {
+ /** @prop {number} order=400 - Index used to define the order of execution */
+ order: 400,
+ /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */
+ enabled: true,
+ /** @prop {ModifierFn} */
+ fn: keepTogether
+ },
+
+ /**
+ * This modifier is used to move the `arrowElement` of the popper to make
+ * sure it is positioned between the reference element and its popper element.
+ * It will read the outer size of the `arrowElement` node to detect how many
+ * pixels of conjunction are needed.
+ *
+ * It has no effect if no `arrowElement` is provided.
+ * @memberof modifiers
+ * @inner
+ */
+ arrow: {
+ /** @prop {number} order=500 - Index used to define the order of execution */
+ order: 500,
+ /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */
+ enabled: true,
+ /** @prop {ModifierFn} */
+ fn: arrow,
+ /** @prop {String|HTMLElement} element='[x-arrow]' - Selector or node used as arrow */
+ element: '[x-arrow]'
+ },
+
+ /**
+ * Modifier used to flip the popper's placement when it starts to overlap its
+ * reference element.
+ *
+ * Requires the `preventOverflow` modifier before it in order to work.
+ *
+ * **NOTE:** this modifier will interrupt the current update cycle and will
+ * restart it if it detects the need to flip the placement.
+ * @memberof modifiers
+ * @inner
+ */
+ flip: {
+ /** @prop {number} order=600 - Index used to define the order of execution */
+ order: 600,
+ /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */
+ enabled: true,
+ /** @prop {ModifierFn} */
+ fn: flip,
+ /**
+ * @prop {String|Array} behavior='flip'
+ * The behavior used to change the popper's placement. It can be one of
+ * `flip`, `clockwise`, `counterclockwise` or an array with a list of valid
+ * placements (with optional variations)
+ */
+ behavior: 'flip',
+ /**
+ * @prop {number} padding=5
+ * The popper will flip if it hits the edges of the `boundariesElement`
+ */
+ padding: 5,
+ /**
+ * @prop {String|HTMLElement} boundariesElement='viewport'
+ * The element which will define the boundaries of the popper position.
+ * The popper will never be placed outside of the defined boundaries
+ * (except if `keepTogether` is enabled)
+ */
+ boundariesElement: 'viewport',
+ /**
+ * @prop {Boolean} flipVariations=false
+ * The popper will switch placement variation between `-start` and `-end` when
+ * the reference element overlaps its boundaries.
+ *
+ * The original placement should have a set variation.
+ */
+ flipVariations: false,
+ /**
+ * @prop {Boolean} flipVariationsByContent=false
+ * The popper will switch placement variation between `-start` and `-end` when
+ * the popper element overlaps its reference boundaries.
+ *
+ * The original placement should have a set variation.
+ */
+ flipVariationsByContent: false
+ },
+
+ /**
+ * Modifier used to make the popper flow toward the inner of the reference element.
+ * By default, when this modifier is disabled, the popper will be placed outside
+ * the reference element.
+ * @memberof modifiers
+ * @inner
+ */
+ inner: {
+ /** @prop {number} order=700 - Index used to define the order of execution */
+ order: 700,
+ /** @prop {Boolean} enabled=false - Whether the modifier is enabled or not */
+ enabled: false,
+ /** @prop {ModifierFn} */
+ fn: inner
+ },
+
+ /**
+ * Modifier used to hide the popper when its reference element is outside of the
+ * popper boundaries. It will set a `x-out-of-boundaries` attribute which can
+ * be used to hide with a CSS selector the popper when its reference is
+ * out of boundaries.
+ *
+ * Requires the `preventOverflow` modifier before it in order to work.
+ * @memberof modifiers
+ * @inner
+ */
+ hide: {
+ /** @prop {number} order=800 - Index used to define the order of execution */
+ order: 800,
+ /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */
+ enabled: true,
+ /** @prop {ModifierFn} */
+ fn: hide
+ },
+
+ /**
+ * Computes the style that will be applied to the popper element to gets
+ * properly positioned.
+ *
+ * Note that this modifier will not touch the DOM, it just prepares the styles
+ * so that `applyStyle` modifier can apply it. This separation is useful
+ * in case you need to replace `applyStyle` with a custom implementation.
+ *
+ * This modifier has `850` as `order` value to maintain backward compatibility
+ * with previous versions of Popper.js. Expect the modifiers ordering method
+ * to change in future major versions of the library.
+ *
+ * @memberof modifiers
+ * @inner
+ */
+ computeStyle: {
+ /** @prop {number} order=850 - Index used to define the order of execution */
+ order: 850,
+ /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */
+ enabled: true,
+ /** @prop {ModifierFn} */
+ fn: computeStyle,
+ /**
+ * @prop {Boolean} gpuAcceleration=true
+ * If true, it uses the CSS 3D transformation to position the popper.
+ * Otherwise, it will use the `top` and `left` properties
+ */
+ gpuAcceleration: true,
+ /**
+ * @prop {string} [x='bottom']
+ * Where to anchor the X axis (`bottom` or `top`). AKA X offset origin.
+ * Change this if your popper should grow in a direction different from `bottom`
+ */
+ x: 'bottom',
+ /**
+ * @prop {string} [x='left']
+ * Where to anchor the Y axis (`left` or `right`). AKA Y offset origin.
+ * Change this if your popper should grow in a direction different from `right`
+ */
+ y: 'right'
+ },
+
+ /**
+ * Applies the computed styles to the popper element.
+ *
+ * All the DOM manipulations are limited to this modifier. This is useful in case
+ * you want to integrate Popper.js inside a framework or view library and you
+ * want to delegate all the DOM manipulations to it.
+ *
+ * Note that if you disable this modifier, you must make sure the popper element
+ * has its position set to `absolute` before Popper.js can do its work!
+ *
+ * Just disable this modifier and define your own to achieve the desired effect.
+ *
+ * @memberof modifiers
+ * @inner
+ */
+ applyStyle: {
+ /** @prop {number} order=900 - Index used to define the order of execution */
+ order: 900,
+ /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */
+ enabled: true,
+ /** @prop {ModifierFn} */
+ fn: applyStyle,
+ /** @prop {Function} */
+ onLoad: applyStyleOnLoad,
+ /**
+ * @deprecated since version 1.10.0, the property moved to `computeStyle` modifier
+ * @prop {Boolean} gpuAcceleration=true
+ * If true, it uses the CSS 3D transformation to position the popper.
+ * Otherwise, it will use the `top` and `left` properties
+ */
+ gpuAcceleration: undefined
+ }
+};
+
+/**
+ * The `dataObject` is an object containing all the information used by Popper.js.
+ * This object is passed to modifiers and to the `onCreate` and `onUpdate` callbacks.
+ * @name dataObject
+ * @property {Object} data.instance The Popper.js instance
+ * @property {String} data.placement Placement applied to popper
+ * @property {String} data.originalPlacement Placement originally defined on init
+ * @property {Boolean} data.flipped True if popper has been flipped by flip modifier
+ * @property {Boolean} data.hide True if the reference element is out of boundaries, useful to know when to hide the popper
+ * @property {HTMLElement} data.arrowElement Node used as arrow by arrow modifier
+ * @property {Object} data.styles Any CSS property defined here will be applied to the popper. It expects the JavaScript nomenclature (eg. `marginBottom`)
+ * @property {Object} data.arrowStyles Any CSS property defined here will be applied to the popper arrow. It expects the JavaScript nomenclature (eg. `marginBottom`)
+ * @property {Object} data.boundaries Offsets of the popper boundaries
+ * @property {Object} data.offsets The measurements of popper, reference and arrow elements
+ * @property {Object} data.offsets.popper `top`, `left`, `width`, `height` values
+ * @property {Object} data.offsets.reference `top`, `left`, `width`, `height` values
+ * @property {Object} data.offsets.arrow] `top` and `left` offsets, only one of them will be different from 0
+ */
+
+/**
+ * Default options provided to Popper.js constructor.<br />
+ * These can be overridden using the `options` argument of Popper.js.<br />
+ * To override an option, simply pass an object with the same
+ * structure of the `options` object, as the 3rd argument. For example:
+ * ```
+ * new Popper(ref, pop, {
+ * modifiers: {
+ * preventOverflow: { enabled: false }
+ * }
+ * })
+ * ```
+ * @type {Object}
+ * @static
+ * @memberof Popper
+ */
+var Defaults = {
+ /**
+ * Popper's placement.
+ * @prop {Popper.placements} placement='bottom'
+ */
+ placement: 'bottom',
+
+ /**
+ * Set this to true if you want popper to position it self in 'fixed' mode
+ * @prop {Boolean} positionFixed=false
+ */
+ positionFixed: false,
+
+ /**
+ * Whether events (resize, scroll) are initially enabled.
+ * @prop {Boolean} eventsEnabled=true
+ */
+ eventsEnabled: true,
+
+ /**
+ * Set to true if you want to automatically remove the popper when
+ * you call the `destroy` method.
+ * @prop {Boolean} removeOnDestroy=false
+ */
+ removeOnDestroy: false,
+
+ /**
+ * Callback called when the popper is created.<br />
+ * By default, it is set to no-op.<br />
+ * Access Popper.js instance with `data.instance`.
+ * @prop {onCreate}
+ */
+ onCreate: function onCreate() {},
+
+ /**
+ * Callback called when the popper is updated. This callback is not called
+ * on the initialization/creation of the popper, but only on subsequent
+ * updates.<br />
+ * By default, it is set to no-op.<br />
+ * Access Popper.js instance with `data.instance`.
+ * @prop {onUpdate}
+ */
+ onUpdate: function onUpdate() {},
+
+ /**
+ * List of modifiers used to modify the offsets before they are applied to the popper.
+ * They provide most of the functionalities of Popper.js.
+ * @prop {modifiers}
+ */
+ modifiers: modifiers
+};
+
+/**
+ * @callback onCreate
+ * @param {dataObject} data
+ */
+
+/**
+ * @callback onUpdate
+ * @param {dataObject} data
+ */
+
+// Utils
+// Methods
+var Popper = function () {
+ /**
+ * Creates a new Popper.js instance.
+ * @class Popper
+ * @param {Element|referenceObject} reference - The reference element used to position the popper
+ * @param {Element} popper - The HTML / XML element used as the popper
+ * @param {Object} options - Your custom options to override the ones defined in [Defaults](#defaults)
+ * @return {Object} instance - The generated Popper.js instance
+ */
+ function Popper(reference, popper) {
+ var _this = this;
+
+ var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
+ classCallCheck(this, Popper);
+
+ this.scheduleUpdate = function () {
+ return requestAnimationFrame(_this.update);
+ };
+
+ // make update() debounced, so that it only runs at most once-per-tick
+ this.update = debounce(this.update.bind(this));
+
+ // with {} we create a new object with the options inside it
+ this.options = _extends({}, Popper.Defaults, options);
+
+ // init state
+ this.state = {
+ isDestroyed: false,
+ isCreated: false,
+ scrollParents: []
+ };
+
+ // get reference and popper elements (allow jQuery wrappers)
+ this.reference = reference && reference.jquery ? reference[0] : reference;
+ this.popper = popper && popper.jquery ? popper[0] : popper;
+
+ // Deep merge modifiers options
+ this.options.modifiers = {};
+ Object.keys(_extends({}, Popper.Defaults.modifiers, options.modifiers)).forEach(function (name) {
+ _this.options.modifiers[name] = _extends({}, Popper.Defaults.modifiers[name] || {}, options.modifiers ? options.modifiers[name] : {});
+ });
+
+ // Refactoring modifiers' list (Object => Array)
+ this.modifiers = Object.keys(this.options.modifiers).map(function (name) {
+ return _extends({
+ name: name
+ }, _this.options.modifiers[name]);
+ })
+ // sort the modifiers by order
+ .sort(function (a, b) {
+ return a.order - b.order;
+ });
+
+ // modifiers have the ability to execute arbitrary code when Popper.js get inited
+ // such code is executed in the same order of its modifier
+ // they could add new properties to their options configuration
+ // BE AWARE: don't add options to `options.modifiers.name` but to `modifierOptions`!
+ this.modifiers.forEach(function (modifierOptions) {
+ if (modifierOptions.enabled && isFunction(modifierOptions.onLoad)) {
+ modifierOptions.onLoad(_this.reference, _this.popper, _this.options, modifierOptions, _this.state);
+ }
+ });
+
+ // fire the first update to position the popper in the right place
+ this.update();
+
+ var eventsEnabled = this.options.eventsEnabled;
+ if (eventsEnabled) {
+ // setup event listeners, they will take care of update the position in specific situations
+ this.enableEventListeners();
+ }
+
+ this.state.eventsEnabled = eventsEnabled;
+ }
+
+ // We can't use class properties because they don't get listed in the
+ // class prototype and break stuff like Sinon stubs
+
+
+ createClass(Popper, [{
+ key: 'update',
+ value: function update$$1() {
+ return update.call(this);
+ }
+ }, {
+ key: 'destroy',
+ value: function destroy$$1() {
+ return destroy.call(this);
+ }
+ }, {
+ key: 'enableEventListeners',
+ value: function enableEventListeners$$1() {
+ return enableEventListeners.call(this);
+ }
+ }, {
+ key: 'disableEventListeners',
+ value: function disableEventListeners$$1() {
+ return disableEventListeners.call(this);
+ }
+
+ /**
+ * Schedules an update. It will run on the next UI update available.
+ * @method scheduleUpdate
+ * @memberof Popper
+ */
+
+
+ /**
+ * Collection of utilities useful when writing custom modifiers.
+ * Starting from version 1.7, this method is available only if you
+ * include `popper-utils.js` before `popper.js`.
+ *
+ * **DEPRECATION**: This way to access PopperUtils is deprecated
+ * and will be removed in v2! Use the PopperUtils module directly instead.
+ * Due to the high instability of the methods contained in Utils, we can't
+ * guarantee them to follow semver. Use them at your own risk!
+ * @static
+ * @private
+ * @type {Object}
+ * @deprecated since version 1.8
+ * @member Utils
+ * @memberof Popper
+ */
+
+ }]);
+ return Popper;
+}();
+
+/**
+ * The `referenceObject` is an object that provides an interface compatible with Popper.js
+ * and lets you use it as replacement of a real DOM node.<br />
+ * You can use this method to position a popper relatively to a set of coordinates
+ * in case you don't have a DOM node to use as reference.
+ *
+ * ```
+ * new Popper(referenceObject, popperNode);
+ * ```
+ *
+ * NB: This feature isn't supported in Internet Explorer 10.
+ * @name referenceObject
+ * @property {Function} data.getBoundingClientRect
+ * A function that returns a set of coordinates compatible with the native `getBoundingClientRect` method.
+ * @property {number} data.clientWidth
+ * An ES6 getter that will return the width of the virtual reference element.
+ * @property {number} data.clientHeight
+ * An ES6 getter that will return the height of the virtual reference element.
+ */
+
+
+Popper.Utils = (typeof window !== 'undefined' ? window : global).PopperUtils;
+Popper.placements = placements;
+Popper.Defaults = Defaults;
+
+return Popper;
+
+})));
+//# sourceMappingURL=popper.js.map
+/*!
+ * Bootstrap v4.3.1 (https://getbootstrap.com/)
+ * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+ (function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('jquery'), require('popper.js')) :
+ typeof define === 'function' && define.amd ? define(['exports', 'jquery', 'popper.js'], factory) :
+ (global = global || self, factory(global.bootstrap = {}, global.jQuery, global.Popper));
+}(this, function (exports, $, Popper) { 'use strict';
+
+ $ = $ && $.hasOwnProperty('default') ? $['default'] : $;
+ Popper = Popper && Popper.hasOwnProperty('default') ? Popper['default'] : Popper;
+
+ function _defineProperties(target, props) {
+ for (var i = 0; i < props.length; i++) {
+ var descriptor = props[i];
+ descriptor.enumerable = descriptor.enumerable || false;
+ descriptor.configurable = true;
+ if ("value" in descriptor) descriptor.writable = true;
+ Object.defineProperty(target, descriptor.key, descriptor);
+ }
+ }
+
+ function _createClass(Constructor, protoProps, staticProps) {
+ if (protoProps) _defineProperties(Constructor.prototype, protoProps);
+ if (staticProps) _defineProperties(Constructor, staticProps);
+ return Constructor;
+ }
+
+ function _defineProperty(obj, key, value) {
+ if (key in obj) {
+ Object.defineProperty(obj, key, {
+ value: value,
+ enumerable: true,
+ configurable: true,
+ writable: true
+ });
+ } else {
+ obj[key] = value;
+ }
+
+ return obj;
+ }
+
+ function _objectSpread(target) {
+ for (var i = 1; i < arguments.length; i++) {
+ var source = arguments[i] != null ? arguments[i] : {};
+ var ownKeys = Object.keys(source);
+
+ if (typeof Object.getOwnPropertySymbols === 'function') {
+ ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) {
+ return Object.getOwnPropertyDescriptor(source, sym).enumerable;
+ }));
+ }
+
+ ownKeys.forEach(function (key) {
+ _defineProperty(target, key, source[key]);
+ });
+ }
+
+ return target;
+ }
+
+ function _inheritsLoose(subClass, superClass) {
+ subClass.prototype = Object.create(superClass.prototype);
+ subClass.prototype.constructor = subClass;
+ subClass.__proto__ = superClass;
+ }
+
+ /**
+ * --------------------------------------------------------------------------
+ * Bootstrap (v4.3.1): util.js
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * --------------------------------------------------------------------------
+ */
+ /**
+ * ------------------------------------------------------------------------
+ * Private TransitionEnd Helpers
+ * ------------------------------------------------------------------------
+ */
+
+ var TRANSITION_END = 'transitionend';
+ var MAX_UID = 1000000;
+ var MILLISECONDS_MULTIPLIER = 1000; // Shoutout AngusCroll (https://goo.gl/pxwQGp)
+
+ function toType(obj) {
+ return {}.toString.call(obj).match(/\s([a-z]+)/i)[1].toLowerCase();
+ }
+
+ function getSpecialTransitionEndEvent() {
+ return {
+ bindType: TRANSITION_END,
+ delegateType: TRANSITION_END,
+ handle: function handle(event) {
+ if ($(event.target).is(this)) {
+ return event.handleObj.handler.apply(this, arguments); // eslint-disable-line prefer-rest-params
+ }
+
+ return undefined; // eslint-disable-line no-undefined
+ }
+ };
+ }
+
+ function transitionEndEmulator(duration) {
+ var _this = this;
+
+ var called = false;
+ $(this).one(Util.TRANSITION_END, function () {
+ called = true;
+ });
+ setTimeout(function () {
+ if (!called) {
+ Util.triggerTransitionEnd(_this);
+ }
+ }, duration);
+ return this;
+ }
+
+ function setTransitionEndSupport() {
+ $.fn.emulateTransitionEnd = transitionEndEmulator;
+ $.event.special[Util.TRANSITION_END] = getSpecialTransitionEndEvent();
+ }
+ /**
+ * --------------------------------------------------------------------------
+ * Public Util Api
+ * --------------------------------------------------------------------------
+ */
+
+
+ var Util = {
+ TRANSITION_END: 'bsTransitionEnd',
+ getUID: function getUID(prefix) {
+ do {
+ // eslint-disable-next-line no-bitwise
+ prefix += ~~(Math.random() * MAX_UID); // "~~" acts like a faster Math.floor() here
+ } while (document.getElementById(prefix));
+
+ return prefix;
+ },
+ getSelectorFromElement: function getSelectorFromElement(element) {
+ var selector = element.getAttribute('data-target');
+
+ if (!selector || selector === '#') {
+ var hrefAttr = element.getAttribute('href');
+ selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : '';
+ }
+
+ try {
+ return document.querySelector(selector) ? selector : null;
+ } catch (err) {
+ return null;
+ }
+ },
+ getTransitionDurationFromElement: function getTransitionDurationFromElement(element) {
+ if (!element) {
+ return 0;
+ } // Get transition-duration of the element
+
+
+ var transitionDuration = $(element).css('transition-duration');
+ var transitionDelay = $(element).css('transition-delay');
+ var floatTransitionDuration = parseFloat(transitionDuration);
+ var floatTransitionDelay = parseFloat(transitionDelay); // Return 0 if element or transition duration is not found
+
+ if (!floatTransitionDuration && !floatTransitionDelay) {
+ return 0;
+ } // If multiple durations are defined, take the first
+
+
+ transitionDuration = transitionDuration.split(',')[0];
+ transitionDelay = transitionDelay.split(',')[0];
+ return (parseFloat(transitionDuration) + parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER;
+ },
+ reflow: function reflow(element) {
+ return element.offsetHeight;
+ },
+ triggerTransitionEnd: function triggerTransitionEnd(element) {
+ $(element).trigger(TRANSITION_END);
+ },
+ // TODO: Remove in v5
+ supportsTransitionEnd: function supportsTransitionEnd() {
+ return Boolean(TRANSITION_END);
+ },
+ isElement: function isElement(obj) {
+ return (obj[0] || obj).nodeType;
+ },
+ typeCheckConfig: function typeCheckConfig(componentName, config, configTypes) {
+ for (var property in configTypes) {
+ if (Object.prototype.hasOwnProperty.call(configTypes, property)) {
+ var expectedTypes = configTypes[property];
+ var value = config[property];
+ var valueType = value && Util.isElement(value) ? 'element' : toType(value);
+
+ if (!new RegExp(expectedTypes).test(valueType)) {
+ throw new Error(componentName.toUpperCase() + ": " + ("Option \"" + property + "\" provided type \"" + valueType + "\" ") + ("but expected type \"" + expectedTypes + "\"."));
+ }
+ }
+ }
+ },
+ findShadowRoot: function findShadowRoot(element) {
+ if (!document.documentElement.attachShadow) {
+ return null;
+ } // Can find the shadow root otherwise it'll return the document
+
+
+ if (typeof element.getRootNode === 'function') {
+ var root = element.getRootNode();
+ return root instanceof ShadowRoot ? root : null;
+ }
+
+ if (element instanceof ShadowRoot) {
+ return element;
+ } // when we don't find a shadow root
+
+
+ if (!element.parentNode) {
+ return null;
+ }
+
+ return Util.findShadowRoot(element.parentNode);
+ }
+ };
+ setTransitionEndSupport();
+
+ /**
+ * ------------------------------------------------------------------------
+ * Constants
+ * ------------------------------------------------------------------------
+ */
+
+ var NAME = 'alert';
+ var VERSION = '4.3.1';
+ var DATA_KEY = 'bs.alert';
+ var EVENT_KEY = "." + DATA_KEY;
+ var DATA_API_KEY = '.data-api';
+ var JQUERY_NO_CONFLICT = $.fn[NAME];
+ var Selector = {
+ DISMISS: '[data-dismiss="alert"]'
+ };
+ var Event = {
+ CLOSE: "close" + EVENT_KEY,
+ CLOSED: "closed" + EVENT_KEY,
+ CLICK_DATA_API: "click" + EVENT_KEY + DATA_API_KEY
+ };
+ var ClassName = {
+ ALERT: 'alert',
+ FADE: 'fade',
+ SHOW: 'show'
+ /**
+ * ------------------------------------------------------------------------
+ * Class Definition
+ * ------------------------------------------------------------------------
+ */
+
+ };
+
+ var Alert =
+ /*#__PURE__*/
+ function () {
+ function Alert(element) {
+ this._element = element;
+ } // Getters
+
+
+ var _proto = Alert.prototype;
+
+ // Public
+ _proto.close = function close(element) {
+ var rootElement = this._element;
+
+ if (element) {
+ rootElement = this._getRootElement(element);
+ }
+
+ var customEvent = this._triggerCloseEvent(rootElement);
+
+ if (customEvent.isDefaultPrevented()) {
+ return;
+ }
+
+ this._removeElement(rootElement);
+ };
+
+ _proto.dispose = function dispose() {
+ $.removeData(this._element, DATA_KEY);
+ this._element = null;
+ } // Private
+ ;
+
+ _proto._getRootElement = function _getRootElement(element) {
+ var selector = Util.getSelectorFromElement(element);
+ var parent = false;
+
+ if (selector) {
+ parent = document.querySelector(selector);
+ }
+
+ if (!parent) {
+ parent = $(element).closest("." + ClassName.ALERT)[0];
+ }
+
+ return parent;
+ };
+
+ _proto._triggerCloseEvent = function _triggerCloseEvent(element) {
+ var closeEvent = $.Event(Event.CLOSE);
+ $(element).trigger(closeEvent);
+ return closeEvent;
+ };
+
+ _proto._removeElement = function _removeElement(element) {
+ var _this = this;
+
+ $(element).removeClass(ClassName.SHOW);
+
+ if (!$(element).hasClass(ClassName.FADE)) {
+ this._destroyElement(element);
+
+ return;
+ }
+
+ var transitionDuration = Util.getTransitionDurationFromElement(element);
+ $(element).one(Util.TRANSITION_END, function (event) {
+ return _this._destroyElement(element, event);
+ }).emulateTransitionEnd(transitionDuration);
+ };
+
+ _proto._destroyElement = function _destroyElement(element) {
+ $(element).detach().trigger(Event.CLOSED).remove();
+ } // Static
+ ;
+
+ Alert._jQueryInterface = function _jQueryInterface(config) {
+ return this.each(function () {
+ var $element = $(this);
+ var data = $element.data(DATA_KEY);
+
+ if (!data) {
+ data = new Alert(this);
+ $element.data(DATA_KEY, data);
+ }
+
+ if (config === 'close') {
+ data[config](this);
+ }
+ });
+ };
+
+ Alert._handleDismiss = function _handleDismiss(alertInstance) {
+ return function (event) {
+ if (event) {
+ event.preventDefault();
+ }
+
+ alertInstance.close(this);
+ };
+ };
+
+ _createClass(Alert, null, [{
+ key: "VERSION",
+ get: function get() {
+ return VERSION;
+ }
+ }]);
+
+ return Alert;
+ }();
+ /**
+ * ------------------------------------------------------------------------
+ * Data Api implementation
+ * ------------------------------------------------------------------------
+ */
+
+
+ $(document).on(Event.CLICK_DATA_API, Selector.DISMISS, Alert._handleDismiss(new Alert()));
+ /**
+ * ------------------------------------------------------------------------
+ * jQuery
+ * ------------------------------------------------------------------------
+ */
+
+ $.fn[NAME] = Alert._jQueryInterface;
+ $.fn[NAME].Constructor = Alert;
+
+ $.fn[NAME].noConflict = function () {
+ $.fn[NAME] = JQUERY_NO_CONFLICT;
+ return Alert._jQueryInterface;
+ };
+
+ /**
+ * ------------------------------------------------------------------------
+ * Constants
+ * ------------------------------------------------------------------------
+ */
+
+ var NAME$1 = 'button';
+ var VERSION$1 = '4.3.1';
+ var DATA_KEY$1 = 'bs.button';
+ var EVENT_KEY$1 = "." + DATA_KEY$1;
+ var DATA_API_KEY$1 = '.data-api';
+ var JQUERY_NO_CONFLICT$1 = $.fn[NAME$1];
+ var ClassName$1 = {
+ ACTIVE: 'active',
+ BUTTON: 'btn',
+ FOCUS: 'focus'
+ };
+ var Selector$1 = {
+ DATA_TOGGLE_CARROT: '[data-toggle^="button"]',
+ DATA_TOGGLE: '[data-toggle="buttons"]',
+ INPUT: 'input:not([type="hidden"])',
+ ACTIVE: '.active',
+ BUTTON: '.btn'
+ };
+ var Event$1 = {
+ CLICK_DATA_API: "click" + EVENT_KEY$1 + DATA_API_KEY$1,
+ FOCUS_BLUR_DATA_API: "focus" + EVENT_KEY$1 + DATA_API_KEY$1 + " " + ("blur" + EVENT_KEY$1 + DATA_API_KEY$1)
+ /**
+ * ------------------------------------------------------------------------
+ * Class Definition
+ * ------------------------------------------------------------------------
+ */
+
+ };
+
+ var Button =
+ /*#__PURE__*/
+ function () {
+ function Button(element) {
+ this._element = element;
+ } // Getters
+
+
+ var _proto = Button.prototype;
+
+ // Public
+ _proto.toggle = function toggle() {
+ var triggerChangeEvent = true;
+ var addAriaPressed = true;
+ var rootElement = $(this._element).closest(Selector$1.DATA_TOGGLE)[0];
+
+ if (rootElement) {
+ var input = this._element.querySelector(Selector$1.INPUT);
+
+ if (input) {
+ if (input.type === 'radio') {
+ if (input.checked && this._element.classList.contains(ClassName$1.ACTIVE)) {
+ triggerChangeEvent = false;
+ } else {
+ var activeElement = rootElement.querySelector(Selector$1.ACTIVE);
+
+ if (activeElement) {
+ $(activeElement).removeClass(ClassName$1.ACTIVE);
+ }
+ }
+ }
+
+ if (triggerChangeEvent) {
+ if (input.hasAttribute('disabled') || rootElement.hasAttribute('disabled') || input.classList.contains('disabled') || rootElement.classList.contains('disabled')) {
+ return;
+ }
+
+ input.checked = !this._element.classList.contains(ClassName$1.ACTIVE);
+ $(input).trigger('change');
+ }
+
+ input.focus();
+ addAriaPressed = false;
+ }
+ }
+
+ if (addAriaPressed) {
+ this._element.setAttribute('aria-pressed', !this._element.classList.contains(ClassName$1.ACTIVE));
+ }
+
+ if (triggerChangeEvent) {
+ $(this._element).toggleClass(ClassName$1.ACTIVE);
+ }
+ };
+
+ _proto.dispose = function dispose() {
+ $.removeData(this._element, DATA_KEY$1);
+ this._element = null;
+ } // Static
+ ;
+
+ Button._jQueryInterface = function _jQueryInterface(config) {
+ return this.each(function () {
+ var data = $(this).data(DATA_KEY$1);
+
+ if (!data) {
+ data = new Button(this);
+ $(this).data(DATA_KEY$1, data);
+ }
+
+ if (config === 'toggle') {
+ data[config]();
+ }
+ });
+ };
+
+ _createClass(Button, null, [{
+ key: "VERSION",
+ get: function get() {
+ return VERSION$1;
+ }
+ }]);
+
+ return Button;
+ }();
+ /**
+ * ------------------------------------------------------------------------
+ * Data Api implementation
+ * ------------------------------------------------------------------------
+ */
+
+
+ $(document).on(Event$1.CLICK_DATA_API, Selector$1.DATA_TOGGLE_CARROT, function (event) {
+ event.preventDefault();
+ var button = event.target;
+
+ if (!$(button).hasClass(ClassName$1.BUTTON)) {
+ button = $(button).closest(Selector$1.BUTTON);
+ }
+
+ Button._jQueryInterface.call($(button), 'toggle');
+ }).on(Event$1.FOCUS_BLUR_DATA_API, Selector$1.DATA_TOGGLE_CARROT, function (event) {
+ var button = $(event.target).closest(Selector$1.BUTTON)[0];
+ $(button).toggleClass(ClassName$1.FOCUS, /^focus(in)?$/.test(event.type));
+ });
+ /**
+ * ------------------------------------------------------------------------
+ * jQuery
+ * ------------------------------------------------------------------------
+ */
+
+ $.fn[NAME$1] = Button._jQueryInterface;
+ $.fn[NAME$1].Constructor = Button;
+
+ $.fn[NAME$1].noConflict = function () {
+ $.fn[NAME$1] = JQUERY_NO_CONFLICT$1;
+ return Button._jQueryInterface;
+ };
+
+ /**
+ * ------------------------------------------------------------------------
+ * Constants
+ * ------------------------------------------------------------------------
+ */
+
+ var NAME$2 = 'carousel';
+ var VERSION$2 = '4.3.1';
+ var DATA_KEY$2 = 'bs.carousel';
+ var EVENT_KEY$2 = "." + DATA_KEY$2;
+ var DATA_API_KEY$2 = '.data-api';
+ var JQUERY_NO_CONFLICT$2 = $.fn[NAME$2];
+ var ARROW_LEFT_KEYCODE = 37; // KeyboardEvent.which value for left arrow key
+
+ var ARROW_RIGHT_KEYCODE = 39; // KeyboardEvent.which value for right arrow key
+
+ var TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch
+
+ var SWIPE_THRESHOLD = 40;
+ var Default = {
+ interval: 5000,
+ keyboard: true,
+ slide: false,
+ pause: 'hover',
+ wrap: true,
+ touch: true
+ };
+ var DefaultType = {
+ interval: '(number|boolean)',
+ keyboard: 'boolean',
+ slide: '(boolean|string)',
+ pause: '(string|boolean)',
+ wrap: 'boolean',
+ touch: 'boolean'
+ };
+ var Direction = {
+ NEXT: 'next',
+ PREV: 'prev',
+ LEFT: 'left',
+ RIGHT: 'right'
+ };
+ var Event$2 = {
+ SLIDE: "slide" + EVENT_KEY$2,
+ SLID: "slid" + EVENT_KEY$2,
+ KEYDOWN: "keydown" + EVENT_KEY$2,
+ MOUSEENTER: "mouseenter" + EVENT_KEY$2,
+ MOUSELEAVE: "mouseleave" + EVENT_KEY$2,
+ TOUCHSTART: "touchstart" + EVENT_KEY$2,
+ TOUCHMOVE: "touchmove" + EVENT_KEY$2,
+ TOUCHEND: "touchend" + EVENT_KEY$2,
+ POINTERDOWN: "pointerdown" + EVENT_KEY$2,
+ POINTERUP: "pointerup" + EVENT_KEY$2,
+ DRAG_START: "dragstart" + EVENT_KEY$2,
+ LOAD_DATA_API: "load" + EVENT_KEY$2 + DATA_API_KEY$2,
+ CLICK_DATA_API: "click" + EVENT_KEY$2 + DATA_API_KEY$2
+ };
+ var ClassName$2 = {
+ CAROUSEL: 'carousel',
+ ACTIVE: 'active',
+ SLIDE: 'slide',
+ RIGHT: 'carousel-item-right',
+ LEFT: 'carousel-item-left',
+ NEXT: 'carousel-item-next',
+ PREV: 'carousel-item-prev',
+ ITEM: 'carousel-item',
+ POINTER_EVENT: 'pointer-event'
+ };
+ var Selector$2 = {
+ ACTIVE: '.active',
+ ACTIVE_ITEM: '.active.carousel-item',
+ ITEM: '.carousel-item',
+ ITEM_IMG: '.carousel-item img',
+ NEXT_PREV: '.carousel-item-next, .carousel-item-prev',
+ INDICATORS: '.carousel-indicators',
+ DATA_SLIDE: '[data-slide], [data-slide-to]',
+ DATA_RIDE: '[data-ride="carousel"]'
+ };
+ var PointerType = {
+ TOUCH: 'touch',
+ PEN: 'pen'
+ /**
+ * ------------------------------------------------------------------------
+ * Class Definition
+ * ------------------------------------------------------------------------
+ */
+
+ };
+
+ var Carousel =
+ /*#__PURE__*/
+ function () {
+ function Carousel(element, config) {
+ this._items = null;
+ this._interval = null;
+ this._activeElement = null;
+ this._isPaused = false;
+ this._isSliding = false;
+ this.touchTimeout = null;
+ this.touchStartX = 0;
+ this.touchDeltaX = 0;
+ this._config = this._getConfig(config);
+ this._element = element;
+ this._indicatorsElement = this._element.querySelector(Selector$2.INDICATORS);
+ this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0;
+ this._pointerEvent = Boolean(window.PointerEvent || window.MSPointerEvent);
+
+ this._addEventListeners();
+ } // Getters
+
+
+ var _proto = Carousel.prototype;
+
+ // Public
+ _proto.next = function next() {
+ if (!this._isSliding) {
+ this._slide(Direction.NEXT);
+ }
+ };
+
+ _proto.nextWhenVisible = function nextWhenVisible() {
+ // Don't call next when the page isn't visible
+ // or the carousel or its parent isn't visible
+ if (!document.hidden && $(this._element).is(':visible') && $(this._element).css('visibility') !== 'hidden') {
+ this.next();
+ }
+ };
+
+ _proto.prev = function prev() {
+ if (!this._isSliding) {
+ this._slide(Direction.PREV);
+ }
+ };
+
+ _proto.pause = function pause(event) {
+ if (!event) {
+ this._isPaused = true;
+ }
+
+ if (this._element.querySelector(Selector$2.NEXT_PREV)) {
+ Util.triggerTransitionEnd(this._element);
+ this.cycle(true);
+ }
+
+ clearInterval(this._interval);
+ this._interval = null;
+ };
+
+ _proto.cycle = function cycle(event) {
+ if (!event) {
+ this._isPaused = false;
+ }
+
+ if (this._interval) {
+ clearInterval(this._interval);
+ this._interval = null;
+ }
+
+ if (this._config.interval && !this._isPaused) {
+ this._interval = setInterval((document.visibilityState ? this.nextWhenVisible : this.next).bind(this), this._config.interval);
+ }
+ };
+
+ _proto.to = function to(index) {
+ var _this = this;
+
+ this._activeElement = this._element.querySelector(Selector$2.ACTIVE_ITEM);
+
+ var activeIndex = this._getItemIndex(this._activeElement);
+
+ if (index > this._items.length - 1 || index < 0) {
+ return;
+ }
+
+ if (this._isSliding) {
+ $(this._element).one(Event$2.SLID, function () {
+ return _this.to(index);
+ });
+ return;
+ }
+
+ if (activeIndex === index) {
+ this.pause();
+ this.cycle();
+ return;
+ }
+
+ var direction = index > activeIndex ? Direction.NEXT : Direction.PREV;
+
+ this._slide(direction, this._items[index]);
+ };
+
+ _proto.dispose = function dispose() {
+ $(this._element).off(EVENT_KEY$2);
+ $.removeData(this._element, DATA_KEY$2);
+ this._items = null;
+ this._config = null;
+ this._element = null;
+ this._interval = null;
+ this._isPaused = null;
+ this._isSliding = null;
+ this._activeElement = null;
+ this._indicatorsElement = null;
+ } // Private
+ ;
+
+ _proto._getConfig = function _getConfig(config) {
+ config = _objectSpread({}, Default, config);
+ Util.typeCheckConfig(NAME$2, config, DefaultType);
+ return config;
+ };
+
+ _proto._handleSwipe = function _handleSwipe() {
+ var absDeltax = Math.abs(this.touchDeltaX);
+
+ if (absDeltax <= SWIPE_THRESHOLD) {
+ return;
+ }
+
+ var direction = absDeltax / this.touchDeltaX; // swipe left
+
+ if (direction > 0) {
+ this.prev();
+ } // swipe right
+
+
+ if (direction < 0) {
+ this.next();
+ }
+ };
+
+ _proto._addEventListeners = function _addEventListeners() {
+ var _this2 = this;
+
+ if (this._config.keyboard) {
+ $(this._element).on(Event$2.KEYDOWN, function (event) {
+ return _this2._keydown(event);
+ });
+ }
+
+ if (this._config.pause === 'hover') {
+ $(this._element).on(Event$2.MOUSEENTER, function (event) {
+ return _this2.pause(event);
+ }).on(Event$2.MOUSELEAVE, function (event) {
+ return _this2.cycle(event);
+ });
+ }
+
+ if (this._config.touch) {
+ this._addTouchEventListeners();
+ }
+ };
+
+ _proto._addTouchEventListeners = function _addTouchEventListeners() {
+ var _this3 = this;
+
+ if (!this._touchSupported) {
+ return;
+ }
+
+ var start = function start(event) {
+ if (_this3._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) {
+ _this3.touchStartX = event.originalEvent.clientX;
+ } else if (!_this3._pointerEvent) {
+ _this3.touchStartX = event.originalEvent.touches[0].clientX;
+ }
+ };
+
+ var move = function move(event) {
+ // ensure swiping with one touch and not pinching
+ if (event.originalEvent.touches && event.originalEvent.touches.length > 1) {
+ _this3.touchDeltaX = 0;
+ } else {
+ _this3.touchDeltaX = event.originalEvent.touches[0].clientX - _this3.touchStartX;
+ }
+ };
+
+ var end = function end(event) {
+ if (_this3._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) {
+ _this3.touchDeltaX = event.originalEvent.clientX - _this3.touchStartX;
+ }
+
+ _this3._handleSwipe();
+
+ if (_this3._config.pause === 'hover') {
+ // If it's a touch-enabled device, mouseenter/leave are fired as
+ // part of the mouse compatibility events on first tap - the carousel
+ // would stop cycling until user tapped out of it;
+ // here, we listen for touchend, explicitly pause the carousel
+ // (as if it's the second time we tap on it, mouseenter compat event
+ // is NOT fired) and after a timeout (to allow for mouse compatibility
+ // events to fire) we explicitly restart cycling
+ _this3.pause();
+
+ if (_this3.touchTimeout) {
+ clearTimeout(_this3.touchTimeout);
+ }
+
+ _this3.touchTimeout = setTimeout(function (event) {
+ return _this3.cycle(event);
+ }, TOUCHEVENT_COMPAT_WAIT + _this3._config.interval);
+ }
+ };
+
+ $(this._element.querySelectorAll(Selector$2.ITEM_IMG)).on(Event$2.DRAG_START, function (e) {
+ return e.preventDefault();
+ });
+
+ if (this._pointerEvent) {
+ $(this._element).on(Event$2.POINTERDOWN, function (event) {
+ return start(event);
+ });
+ $(this._element).on(Event$2.POINTERUP, function (event) {
+ return end(event);
+ });
+
+ this._element.classList.add(ClassName$2.POINTER_EVENT);
+ } else {
+ $(this._element).on(Event$2.TOUCHSTART, function (event) {
+ return start(event);
+ });
+ $(this._element).on(Event$2.TOUCHMOVE, function (event) {
+ return move(event);
+ });
+ $(this._element).on(Event$2.TOUCHEND, function (event) {
+ return end(event);
+ });
+ }
+ };
+
+ _proto._keydown = function _keydown(event) {
+ if (/input|textarea/i.test(event.target.tagName)) {
+ return;
+ }
+
+ switch (event.which) {
+ case ARROW_LEFT_KEYCODE:
+ event.preventDefault();
+ this.prev();
+ break;
+
+ case ARROW_RIGHT_KEYCODE:
+ event.preventDefault();
+ this.next();
+ break;
+
+ default:
+ }
+ };
+
+ _proto._getItemIndex = function _getItemIndex(element) {
+ this._items = element && element.parentNode ? [].slice.call(element.parentNode.querySelectorAll(Selector$2.ITEM)) : [];
+ return this._items.indexOf(element);
+ };
+
+ _proto._getItemByDirection = function _getItemByDirection(direction, activeElement) {
+ var isNextDirection = direction === Direction.NEXT;
+ var isPrevDirection = direction === Direction.PREV;
+
+ var activeIndex = this._getItemIndex(activeElement);
+
+ var lastItemIndex = this._items.length - 1;
+ var isGoingToWrap = isPrevDirection && activeIndex === 0 || isNextDirection && activeIndex === lastItemIndex;
+
+ if (isGoingToWrap && !this._config.wrap) {
+ return activeElement;
+ }
+
+ var delta = direction === Direction.PREV ? -1 : 1;
+ var itemIndex = (activeIndex + delta) % this._items.length;
+ return itemIndex === -1 ? this._items[this._items.length - 1] : this._items[itemIndex];
+ };
+
+ _proto._triggerSlideEvent = function _triggerSlideEvent(relatedTarget, eventDirectionName) {
+ var targetIndex = this._getItemIndex(relatedTarget);
+
+ var fromIndex = this._getItemIndex(this._element.querySelector(Selector$2.ACTIVE_ITEM));
+
+ var slideEvent = $.Event(Event$2.SLIDE, {
+ relatedTarget: relatedTarget,
+ direction: eventDirectionName,
+ from: fromIndex,
+ to: targetIndex
+ });
+ $(this._element).trigger(slideEvent);
+ return slideEvent;
+ };
+
+ _proto._setActiveIndicatorElement = function _setActiveIndicatorElement(element) {
+ if (this._indicatorsElement) {
+ var indicators = [].slice.call(this._indicatorsElement.querySelectorAll(Selector$2.ACTIVE));
+ $(indicators).removeClass(ClassName$2.ACTIVE);
+
+ var nextIndicator = this._indicatorsElement.children[this._getItemIndex(element)];
+
+ if (nextIndicator) {
+ $(nextIndicator).addClass(ClassName$2.ACTIVE);
+ }
+ }
+ };
+
+ _proto._slide = function _slide(direction, element) {
+ var _this4 = this;
+
+ var activeElement = this._element.querySelector(Selector$2.ACTIVE_ITEM);
+
+ var activeElementIndex = this._getItemIndex(activeElement);
+
+ var nextElement = element || activeElement && this._getItemByDirection(direction, activeElement);
+
+ var nextElementIndex = this._getItemIndex(nextElement);
+
+ var isCycling = Boolean(this._interval);
+ var directionalClassName;
+ var orderClassName;
+ var eventDirectionName;
+
+ if (direction === Direction.NEXT) {
+ directionalClassName = ClassName$2.LEFT;
+ orderClassName = ClassName$2.NEXT;
+ eventDirectionName = Direction.LEFT;
+ } else {
+ directionalClassName = ClassName$2.RIGHT;
+ orderClassName = ClassName$2.PREV;
+ eventDirectionName = Direction.RIGHT;
+ }
+
+ if (nextElement && $(nextElement).hasClass(ClassName$2.ACTIVE)) {
+ this._isSliding = false;
+ return;
+ }
+
+ var slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName);
+
+ if (slideEvent.isDefaultPrevented()) {
+ return;
+ }
+
+ if (!activeElement || !nextElement) {
+ // Some weirdness is happening, so we bail
+ return;
+ }
+
+ this._isSliding = true;
+
+ if (isCycling) {
+ this.pause();
+ }
+
+ this._setActiveIndicatorElement(nextElement);
+
+ var slidEvent = $.Event(Event$2.SLID, {
+ relatedTarget: nextElement,
+ direction: eventDirectionName,
+ from: activeElementIndex,
+ to: nextElementIndex
+ });
+
+ if ($(this._element).hasClass(ClassName$2.SLIDE)) {
+ $(nextElement).addClass(orderClassName);
+ Util.reflow(nextElement);
+ $(activeElement).addClass(directionalClassName);
+ $(nextElement).addClass(directionalClassName);
+ var nextElementInterval = parseInt(nextElement.getAttribute('data-interval'), 10);
+
+ if (nextElementInterval) {
+ this._config.defaultInterval = this._config.defaultInterval || this._config.interval;
+ this._config.interval = nextElementInterval;
+ } else {
+ this._config.interval = this._config.defaultInterval || this._config.interval;
+ }
+
+ var transitionDuration = Util.getTransitionDurationFromElement(activeElement);
+ $(activeElement).one(Util.TRANSITION_END, function () {
+ $(nextElement).removeClass(directionalClassName + " " + orderClassName).addClass(ClassName$2.ACTIVE);
+ $(activeElement).removeClass(ClassName$2.ACTIVE + " " + orderClassName + " " + directionalClassName);
+ _this4._isSliding = false;
+ setTimeout(function () {
+ return $(_this4._element).trigger(slidEvent);
+ }, 0);
+ }).emulateTransitionEnd(transitionDuration);
+ } else {
+ $(activeElement).removeClass(ClassName$2.ACTIVE);
+ $(nextElement).addClass(ClassName$2.ACTIVE);
+ this._isSliding = false;
+ $(this._element).trigger(slidEvent);
+ }
+
+ if (isCycling) {
+ this.cycle();
+ }
+ } // Static
+ ;
+
+ Carousel._jQueryInterface = function _jQueryInterface(config) {
+ return this.each(function () {
+ var data = $(this).data(DATA_KEY$2);
+
+ var _config = _objectSpread({}, Default, $(this).data());
+
+ if (typeof config === 'object') {
+ _config = _objectSpread({}, _config, config);
+ }
+
+ var action = typeof config === 'string' ? config : _config.slide;
+
+ if (!data) {
+ data = new Carousel(this, _config);
+ $(this).data(DATA_KEY$2, data);
+ }
+
+ if (typeof config === 'number') {
+ data.to(config);
+ } else if (typeof action === 'string') {
+ if (typeof data[action] === 'undefined') {
+ throw new TypeError("No method named \"" + action + "\"");
+ }
+
+ data[action]();
+ } else if (_config.interval && _config.ride) {
+ data.pause();
+ data.cycle();
+ }
+ });
+ };
+
+ Carousel._dataApiClickHandler = function _dataApiClickHandler(event) {
+ var selector = Util.getSelectorFromElement(this);
+
+ if (!selector) {
+ return;
+ }
+
+ var target = $(selector)[0];
+
+ if (!target || !$(target).hasClass(ClassName$2.CAROUSEL)) {
+ return;
+ }
+
+ var config = _objectSpread({}, $(target).data(), $(this).data());
+
+ var slideIndex = this.getAttribute('data-slide-to');
+
+ if (slideIndex) {
+ config.interval = false;
+ }
+
+ Carousel._jQueryInterface.call($(target), config);
+
+ if (slideIndex) {
+ $(target).data(DATA_KEY$2).to(slideIndex);
+ }
+
+ event.preventDefault();
+ };
+
+ _createClass(Carousel, null, [{
+ key: "VERSION",
+ get: function get() {
+ return VERSION$2;
+ }
+ }, {
+ key: "Default",
+ get: function get() {
+ return Default;
+ }
+ }]);
+
+ return Carousel;
+ }();
+ /**
+ * ------------------------------------------------------------------------
+ * Data Api implementation
+ * ------------------------------------------------------------------------
+ */
+
+
+ $(document).on(Event$2.CLICK_DATA_API, Selector$2.DATA_SLIDE, Carousel._dataApiClickHandler);
+ $(window).on(Event$2.LOAD_DATA_API, function () {
+ var carousels = [].slice.call(document.querySelectorAll(Selector$2.DATA_RIDE));
+
+ for (var i = 0, len = carousels.length; i < len; i++) {
+ var $carousel = $(carousels[i]);
+
+ Carousel._jQueryInterface.call($carousel, $carousel.data());
+ }
+ });
+ /**
+ * ------------------------------------------------------------------------
+ * jQuery
+ * ------------------------------------------------------------------------
+ */
+
+ $.fn[NAME$2] = Carousel._jQueryInterface;
+ $.fn[NAME$2].Constructor = Carousel;
+
+ $.fn[NAME$2].noConflict = function () {
+ $.fn[NAME$2] = JQUERY_NO_CONFLICT$2;
+ return Carousel._jQueryInterface;
+ };
+
+ /**
+ * ------------------------------------------------------------------------
+ * Constants
+ * ------------------------------------------------------------------------
+ */
+
+ var NAME$3 = 'collapse';
+ var VERSION$3 = '4.3.1';
+ var DATA_KEY$3 = 'bs.collapse';
+ var EVENT_KEY$3 = "." + DATA_KEY$3;
+ var DATA_API_KEY$3 = '.data-api';
+ var JQUERY_NO_CONFLICT$3 = $.fn[NAME$3];
+ var Default$1 = {
+ toggle: true,
+ parent: ''
+ };
+ var DefaultType$1 = {
+ toggle: 'boolean',
+ parent: '(string|element)'
+ };
+ var Event$3 = {
+ SHOW: "show" + EVENT_KEY$3,
+ SHOWN: "shown" + EVENT_KEY$3,
+ HIDE: "hide" + EVENT_KEY$3,
+ HIDDEN: "hidden" + EVENT_KEY$3,
+ CLICK_DATA_API: "click" + EVENT_KEY$3 + DATA_API_KEY$3
+ };
+ var ClassName$3 = {
+ SHOW: 'show',
+ COLLAPSE: 'collapse',
+ COLLAPSING: 'collapsing',
+ COLLAPSED: 'collapsed'
+ };
+ var Dimension = {
+ WIDTH: 'width',
+ HEIGHT: 'height'
+ };
+ var Selector$3 = {
+ ACTIVES: '.show, .collapsing',
+ DATA_TOGGLE: '[data-toggle="collapse"]'
+ /**
+ * ------------------------------------------------------------------------
+ * Class Definition
+ * ------------------------------------------------------------------------
+ */
+
+ };
+
+ var Collapse =
+ /*#__PURE__*/
+ function () {
+ function Collapse(element, config) {
+ this._isTransitioning = false;
+ this._element = element;
+ this._config = this._getConfig(config);
+ this._triggerArray = [].slice.call(document.querySelectorAll("[data-toggle=\"collapse\"][href=\"#" + element.id + "\"]," + ("[data-toggle=\"collapse\"][data-target=\"#" + element.id + "\"]")));
+ var toggleList = [].slice.call(document.querySelectorAll(Selector$3.DATA_TOGGLE));
+
+ for (var i = 0, len = toggleList.length; i < len; i++) {
+ var elem = toggleList[i];
+ var selector = Util.getSelectorFromElement(elem);
+ var filterElement = [].slice.call(document.querySelectorAll(selector)).filter(function (foundElem) {
+ return foundElem === element;
+ });
+
+ if (selector !== null && filterElement.length > 0) {
+ this._selector = selector;
+
+ this._triggerArray.push(elem);
+ }
+ }
+
+ this._parent = this._config.parent ? this._getParent() : null;
+
+ if (!this._config.parent) {
+ this._addAriaAndCollapsedClass(this._element, this._triggerArray);
+ }
+
+ if (this._config.toggle) {
+ this.toggle();
+ }
+ } // Getters
+
+
+ var _proto = Collapse.prototype;
+
+ // Public
+ _proto.toggle = function toggle() {
+ if ($(this._element).hasClass(ClassName$3.SHOW)) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ };
+
+ _proto.show = function show() {
+ var _this = this;
+
+ if (this._isTransitioning || $(this._element).hasClass(ClassName$3.SHOW)) {
+ return;
+ }
+
+ var actives;
+ var activesData;
+
+ if (this._parent) {
+ actives = [].slice.call(this._parent.querySelectorAll(Selector$3.ACTIVES)).filter(function (elem) {
+ if (typeof _this._config.parent === 'string') {
+ return elem.getAttribute('data-parent') === _this._config.parent;
+ }
+
+ return elem.classList.contains(ClassName$3.COLLAPSE);
+ });
+
+ if (actives.length === 0) {
+ actives = null;
+ }
+ }
+
+ if (actives) {
+ activesData = $(actives).not(this._selector).data(DATA_KEY$3);
+
+ if (activesData && activesData._isTransitioning) {
+ return;
+ }
+ }
+
+ var startEvent = $.Event(Event$3.SHOW);
+ $(this._element).trigger(startEvent);
+
+ if (startEvent.isDefaultPrevented()) {
+ return;
+ }
+
+ if (actives) {
+ Collapse._jQueryInterface.call($(actives).not(this._selector), 'hide');
+
+ if (!activesData) {
+ $(actives).data(DATA_KEY$3, null);
+ }
+ }
+
+ var dimension = this._getDimension();
+
+ $(this._element).removeClass(ClassName$3.COLLAPSE).addClass(ClassName$3.COLLAPSING);
+ this._element.style[dimension] = 0;
+
+ if (this._triggerArray.length) {
+ $(this._triggerArray).removeClass(ClassName$3.COLLAPSED).attr('aria-expanded', true);
+ }
+
+ this.setTransitioning(true);
+
+ var complete = function complete() {
+ $(_this._element).removeClass(ClassName$3.COLLAPSING).addClass(ClassName$3.COLLAPSE).addClass(ClassName$3.SHOW);
+ _this._element.style[dimension] = '';
+
+ _this.setTransitioning(false);
+
+ $(_this._element).trigger(Event$3.SHOWN);
+ };
+
+ var capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1);
+ var scrollSize = "scroll" + capitalizedDimension;
+ var transitionDuration = Util.getTransitionDurationFromElement(this._element);
+ $(this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);
+ this._element.style[dimension] = this._element[scrollSize] + "px";
+ };
+
+ _proto.hide = function hide() {
+ var _this2 = this;
+
+ if (this._isTransitioning || !$(this._element).hasClass(ClassName$3.SHOW)) {
+ return;
+ }
+
+ var startEvent = $.Event(Event$3.HIDE);
+ $(this._element).trigger(startEvent);
+
+ if (startEvent.isDefaultPrevented()) {
+ return;
+ }
+
+ var dimension = this._getDimension();
+
+ this._element.style[dimension] = this._element.getBoundingClientRect()[dimension] + "px";
+ Util.reflow(this._element);
+ $(this._element).addClass(ClassName$3.COLLAPSING).removeClass(ClassName$3.COLLAPSE).removeClass(ClassName$3.SHOW);
+ var triggerArrayLength = this._triggerArray.length;
+
+ if (triggerArrayLength > 0) {
+ for (var i = 0; i < triggerArrayLength; i++) {
+ var trigger = this._triggerArray[i];
+ var selector = Util.getSelectorFromElement(trigger);
+
+ if (selector !== null) {
+ var $elem = $([].slice.call(document.querySelectorAll(selector)));
+
+ if (!$elem.hasClass(ClassName$3.SHOW)) {
+ $(trigger).addClass(ClassName$3.COLLAPSED).attr('aria-expanded', false);
+ }
+ }
+ }
+ }
+
+ this.setTransitioning(true);
+
+ var complete = function complete() {
+ _this2.setTransitioning(false);
+
+ $(_this2._element).removeClass(ClassName$3.COLLAPSING).addClass(ClassName$3.COLLAPSE).trigger(Event$3.HIDDEN);
+ };
+
+ this._element.style[dimension] = '';
+ var transitionDuration = Util.getTransitionDurationFromElement(this._element);
+ $(this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);
+ };
+
+ _proto.setTransitioning = function setTransitioning(isTransitioning) {
+ this._isTransitioning = isTransitioning;
+ };
+
+ _proto.dispose = function dispose() {
+ $.removeData(this._element, DATA_KEY$3);
+ this._config = null;
+ this._parent = null;
+ this._element = null;
+ this._triggerArray = null;
+ this._isTransitioning = null;
+ } // Private
+ ;
+
+ _proto._getConfig = function _getConfig(config) {
+ config = _objectSpread({}, Default$1, config);
+ config.toggle = Boolean(config.toggle); // Coerce string values
+
+ Util.typeCheckConfig(NAME$3, config, DefaultType$1);
+ return config;
+ };
+
+ _proto._getDimension = function _getDimension() {
+ var hasWidth = $(this._element).hasClass(Dimension.WIDTH);
+ return hasWidth ? Dimension.WIDTH : Dimension.HEIGHT;
+ };
+
+ _proto._getParent = function _getParent() {
+ var _this3 = this;
+
+ var parent;
+
+ if (Util.isElement(this._config.parent)) {
+ parent = this._config.parent; // It's a jQuery object
+
+ if (typeof this._config.parent.jquery !== 'undefined') {
+ parent = this._config.parent[0];
+ }
+ } else {
+ parent = document.querySelector(this._config.parent);
+ }
+
+ var selector = "[data-toggle=\"collapse\"][data-parent=\"" + this._config.parent + "\"]";
+ var children = [].slice.call(parent.querySelectorAll(selector));
+ $(children).each(function (i, element) {
+ _this3._addAriaAndCollapsedClass(Collapse._getTargetFromElement(element), [element]);
+ });
+ return parent;
+ };
+
+ _proto._addAriaAndCollapsedClass = function _addAriaAndCollapsedClass(element, triggerArray) {
+ var isOpen = $(element).hasClass(ClassName$3.SHOW);
+
+ if (triggerArray.length) {
+ $(triggerArray).toggleClass(ClassName$3.COLLAPSED, !isOpen).attr('aria-expanded', isOpen);
+ }
+ } // Static
+ ;
+
+ Collapse._getTargetFromElement = function _getTargetFromElement(element) {
+ var selector = Util.getSelectorFromElement(element);
+ return selector ? document.querySelector(selector) : null;
+ };
+
+ Collapse._jQueryInterface = function _jQueryInterface(config) {
+ return this.each(function () {
+ var $this = $(this);
+ var data = $this.data(DATA_KEY$3);
+
+ var _config = _objectSpread({}, Default$1, $this.data(), typeof config === 'object' && config ? config : {});
+
+ if (!data && _config.toggle && /show|hide/.test(config)) {
+ _config.toggle = false;
+ }
+
+ if (!data) {
+ data = new Collapse(this, _config);
+ $this.data(DATA_KEY$3, data);
+ }
+
+ if (typeof config === 'string') {
+ if (typeof data[config] === 'undefined') {
+ throw new TypeError("No method named \"" + config + "\"");
+ }
+
+ data[config]();
+ }
+ });
+ };
+
+ _createClass(Collapse, null, [{
+ key: "VERSION",
+ get: function get() {
+ return VERSION$3;
+ }
+ }, {
+ key: "Default",
+ get: function get() {
+ return Default$1;
+ }
+ }]);
+
+ return Collapse;
+ }();
+ /**
+ * ------------------------------------------------------------------------
+ * Data Api implementation
+ * ------------------------------------------------------------------------
+ */
+
+
+ $(document).on(Event$3.CLICK_DATA_API, Selector$3.DATA_TOGGLE, function (event) {
+ // preventDefault only for <a> elements (which change the URL) not inside the collapsible element
+ if (event.currentTarget.tagName === 'A') {
+ event.preventDefault();
+ }
+
+ var $trigger = $(this);
+ var selector = Util.getSelectorFromElement(this);
+ var selectors = [].slice.call(document.querySelectorAll(selector));
+ $(selectors).each(function () {
+ var $target = $(this);
+ var data = $target.data(DATA_KEY$3);
+ var config = data ? 'toggle' : $trigger.data();
+
+ Collapse._jQueryInterface.call($target, config);
+ });
+ });
+ /**
+ * ------------------------------------------------------------------------
+ * jQuery
+ * ------------------------------------------------------------------------
+ */
+
+ $.fn[NAME$3] = Collapse._jQueryInterface;
+ $.fn[NAME$3].Constructor = Collapse;
+
+ $.fn[NAME$3].noConflict = function () {
+ $.fn[NAME$3] = JQUERY_NO_CONFLICT$3;
+ return Collapse._jQueryInterface;
+ };
+
+ /**
+ * ------------------------------------------------------------------------
+ * Constants
+ * ------------------------------------------------------------------------
+ */
+
+ var NAME$4 = 'dropdown';
+ var VERSION$4 = '4.3.1';
+ var DATA_KEY$4 = 'bs.dropdown';
+ var EVENT_KEY$4 = "." + DATA_KEY$4;
+ var DATA_API_KEY$4 = '.data-api';
+ var JQUERY_NO_CONFLICT$4 = $.fn[NAME$4];
+ var ESCAPE_KEYCODE = 27; // KeyboardEvent.which value for Escape (Esc) key
+
+ var SPACE_KEYCODE = 32; // KeyboardEvent.which value for space key
+
+ var TAB_KEYCODE = 9; // KeyboardEvent.which value for tab key
+
+ var ARROW_UP_KEYCODE = 38; // KeyboardEvent.which value for up arrow key
+
+ var ARROW_DOWN_KEYCODE = 40; // KeyboardEvent.which value for down arrow key
+
+ var RIGHT_MOUSE_BUTTON_WHICH = 3; // MouseEvent.which value for the right button (assuming a right-handed mouse)
+
+ var REGEXP_KEYDOWN = new RegExp(ARROW_UP_KEYCODE + "|" + ARROW_DOWN_KEYCODE + "|" + ESCAPE_KEYCODE);
+ var Event$4 = {
+ HIDE: "hide" + EVENT_KEY$4,
+ HIDDEN: "hidden" + EVENT_KEY$4,
+ SHOW: "show" + EVENT_KEY$4,
+ SHOWN: "shown" + EVENT_KEY$4,
+ CLICK: "click" + EVENT_KEY$4,
+ CLICK_DATA_API: "click" + EVENT_KEY$4 + DATA_API_KEY$4,
+ KEYDOWN_DATA_API: "keydown" + EVENT_KEY$4 + DATA_API_KEY$4,
+ KEYUP_DATA_API: "keyup" + EVENT_KEY$4 + DATA_API_KEY$4
+ };
+ var ClassName$4 = {
+ DISABLED: 'disabled',
+ SHOW: 'show',
+ DROPUP: 'dropup',
+ DROPRIGHT: 'dropright',
+ DROPLEFT: 'dropleft',
+ MENURIGHT: 'dropdown-menu-right',
+ MENULEFT: 'dropdown-menu-left',
+ POSITION_STATIC: 'position-static'
+ };
+ var Selector$4 = {
+ DATA_TOGGLE: '[data-toggle="dropdown"]',
+ FORM_CHILD: '.dropdown form',
+ MENU: '.dropdown-menu',
+ NAVBAR_NAV: '.navbar-nav',
+ VISIBLE_ITEMS: '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'
+ };
+ var AttachmentMap = {
+ TOP: 'top-start',
+ TOPEND: 'top-end',
+ BOTTOM: 'bottom-start',
+ BOTTOMEND: 'bottom-end',
+ RIGHT: 'right-start',
+ RIGHTEND: 'right-end',
+ LEFT: 'left-start',
+ LEFTEND: 'left-end'
+ };
+ var Default$2 = {
+ offset: 0,
+ flip: true,
+ boundary: 'scrollParent',
+ reference: 'toggle',
+ display: 'dynamic'
+ };
+ var DefaultType$2 = {
+ offset: '(number|string|function)',
+ flip: 'boolean',
+ boundary: '(string|element)',
+ reference: '(string|element)',
+ display: 'string'
+ /**
+ * ------------------------------------------------------------------------
+ * Class Definition
+ * ------------------------------------------------------------------------
+ */
+
+ };
+
+ var Dropdown =
+ /*#__PURE__*/
+ function () {
+ function Dropdown(element, config) {
+ this._element = element;
+ this._popper = null;
+ this._config = this._getConfig(config);
+ this._menu = this._getMenuElement();
+ this._inNavbar = this._detectNavbar();
+
+ this._addEventListeners();
+ } // Getters
+
+
+ var _proto = Dropdown.prototype;
+
+ // Public
+ _proto.toggle = function toggle() {
+ if (this._element.disabled || $(this._element).hasClass(ClassName$4.DISABLED)) {
+ return;
+ }
+
+ var parent = Dropdown._getParentFromElement(this._element);
+
+ var isActive = $(this._menu).hasClass(ClassName$4.SHOW);
+
+ Dropdown._clearMenus();
+
+ if (isActive) {
+ return;
+ }
+
+ var relatedTarget = {
+ relatedTarget: this._element
+ };
+ var showEvent = $.Event(Event$4.SHOW, relatedTarget);
+ $(parent).trigger(showEvent);
+
+ if (showEvent.isDefaultPrevented()) {
+ return;
+ } // Disable totally Popper.js for Dropdown in Navbar
+
+
+ if (!this._inNavbar) {
+ /**
+ * Check for Popper dependency
+ * Popper - https://popper.js.org
+ */
+ if (typeof Popper === 'undefined') {
+ throw new TypeError('Bootstrap\'s dropdowns require Popper.js (https://popper.js.org/)');
+ }
+
+ var referenceElement = this._element;
+
+ if (this._config.reference === 'parent') {
+ referenceElement = parent;
+ } else if (Util.isElement(this._config.reference)) {
+ referenceElement = this._config.reference; // Check if it's jQuery element
+
+ if (typeof this._config.reference.jquery !== 'undefined') {
+ referenceElement = this._config.reference[0];
+ }
+ } // If boundary is not `scrollParent`, then set position to `static`
+ // to allow the menu to "escape" the scroll parent's boundaries
+ // https://github.com/twbs/bootstrap/issues/24251
+
+
+ if (this._config.boundary !== 'scrollParent') {
+ $(parent).addClass(ClassName$4.POSITION_STATIC);
+ }
+
+ this._popper = new Popper(referenceElement, this._menu, this._getPopperConfig());
+ } // If this is a touch-enabled device we add extra
+ // empty mouseover listeners to the body's immediate children;
+ // only needed because of broken event delegation on iOS
+ // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
+
+
+ if ('ontouchstart' in document.documentElement && $(parent).closest(Selector$4.NAVBAR_NAV).length === 0) {
+ $(document.body).children().on('mouseover', null, $.noop);
+ }
+
+ this._element.focus();
+
+ this._element.setAttribute('aria-expanded', true);
+
+ $(this._menu).toggleClass(ClassName$4.SHOW);
+ $(parent).toggleClass(ClassName$4.SHOW).trigger($.Event(Event$4.SHOWN, relatedTarget));
+ };
+
+ _proto.show = function show() {
+ if (this._element.disabled || $(this._element).hasClass(ClassName$4.DISABLED) || $(this._menu).hasClass(ClassName$4.SHOW)) {
+ return;
+ }
+
+ var relatedTarget = {
+ relatedTarget: this._element
+ };
+ var showEvent = $.Event(Event$4.SHOW, relatedTarget);
+
+ var parent = Dropdown._getParentFromElement(this._element);
+
+ $(parent).trigger(showEvent);
+
+ if (showEvent.isDefaultPrevented()) {
+ return;
+ }
+
+ $(this._menu).toggleClass(ClassName$4.SHOW);
+ $(parent).toggleClass(ClassName$4.SHOW).trigger($.Event(Event$4.SHOWN, relatedTarget));
+ };
+
+ _proto.hide = function hide() {
+ if (this._element.disabled || $(this._element).hasClass(ClassName$4.DISABLED) || !$(this._menu).hasClass(ClassName$4.SHOW)) {
+ return;
+ }
+
+ var relatedTarget = {
+ relatedTarget: this._element
+ };
+ var hideEvent = $.Event(Event$4.HIDE, relatedTarget);
+
+ var parent = Dropdown._getParentFromElement(this._element);
+
+ $(parent).trigger(hideEvent);
+
+ if (hideEvent.isDefaultPrevented()) {
+ return;
+ }
+
+ $(this._menu).toggleClass(ClassName$4.SHOW);
+ $(parent).toggleClass(ClassName$4.SHOW).trigger($.Event(Event$4.HIDDEN, relatedTarget));
+ };
+
+ _proto.dispose = function dispose() {
+ $.removeData(this._element, DATA_KEY$4);
+ $(this._element).off(EVENT_KEY$4);
+ this._element = null;
+ this._menu = null;
+
+ if (this._popper !== null) {
+ this._popper.destroy();
+
+ this._popper = null;
+ }
+ };
+
+ _proto.update = function update() {
+ this._inNavbar = this._detectNavbar();
+
+ if (this._popper !== null) {
+ this._popper.scheduleUpdate();
+ }
+ } // Private
+ ;
+
+ _proto._addEventListeners = function _addEventListeners() {
+ var _this = this;
+
+ $(this._element).on(Event$4.CLICK, function (event) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ _this.toggle();
+ });
+ };
+
+ _proto._getConfig = function _getConfig(config) {
+ config = _objectSpread({}, this.constructor.Default, $(this._element).data(), config);
+ Util.typeCheckConfig(NAME$4, config, this.constructor.DefaultType);
+ return config;
+ };
+
+ _proto._getMenuElement = function _getMenuElement() {
+ if (!this._menu) {
+ var parent = Dropdown._getParentFromElement(this._element);
+
+ if (parent) {
+ this._menu = parent.querySelector(Selector$4.MENU);
+ }
+ }
+
+ return this._menu;
+ };
+
+ _proto._getPlacement = function _getPlacement() {
+ var $parentDropdown = $(this._element.parentNode);
+ var placement = AttachmentMap.BOTTOM; // Handle dropup
+
+ if ($parentDropdown.hasClass(ClassName$4.DROPUP)) {
+ placement = AttachmentMap.TOP;
+
+ if ($(this._menu).hasClass(ClassName$4.MENURIGHT)) {
+ placement = AttachmentMap.TOPEND;
+ }
+ } else if ($parentDropdown.hasClass(ClassName$4.DROPRIGHT)) {
+ placement = AttachmentMap.RIGHT;
+ } else if ($parentDropdown.hasClass(ClassName$4.DROPLEFT)) {
+ placement = AttachmentMap.LEFT;
+ } else if ($(this._menu).hasClass(ClassName$4.MENURIGHT)) {
+ placement = AttachmentMap.BOTTOMEND;
+ }
+
+ return placement;
+ };
+
+ _proto._detectNavbar = function _detectNavbar() {
+ return $(this._element).closest('.navbar').length > 0;
+ };
+
+ _proto._getOffset = function _getOffset() {
+ var _this2 = this;
+
+ var offset = {};
+
+ if (typeof this._config.offset === 'function') {
+ offset.fn = function (data) {
+ data.offsets = _objectSpread({}, data.offsets, _this2._config.offset(data.offsets, _this2._element) || {});
+ return data;
+ };
+ } else {
+ offset.offset = this._config.offset;
+ }
+
+ return offset;
+ };
+
+ _proto._getPopperConfig = function _getPopperConfig() {
+ var popperConfig = {
+ placement: this._getPlacement(),
+ modifiers: {
+ offset: this._getOffset(),
+ flip: {
+ enabled: this._config.flip
+ },
+ preventOverflow: {
+ boundariesElement: this._config.boundary
+ }
+ } // Disable Popper.js if we have a static display
+
+ };
+
+ if (this._config.display === 'static') {
+ popperConfig.modifiers.applyStyle = {
+ enabled: false
+ };
+ }
+
+ return popperConfig;
+ } // Static
+ ;
+
+ Dropdown._jQueryInterface = function _jQueryInterface(config) {
+ return this.each(function () {
+ var data = $(this).data(DATA_KEY$4);
+
+ var _config = typeof config === 'object' ? config : null;
+
+ if (!data) {
+ data = new Dropdown(this, _config);
+ $(this).data(DATA_KEY$4, data);
+ }
+
+ if (typeof config === 'string') {
+ if (typeof data[config] === 'undefined') {
+ throw new TypeError("No method named \"" + config + "\"");
+ }
+
+ data[config]();
+ }
+ });
+ };
+
+ Dropdown._clearMenus = function _clearMenus(event) {
+ if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH || event.type === 'keyup' && event.which !== TAB_KEYCODE)) {
+ return;
+ }
+
+ var toggles = [].slice.call(document.querySelectorAll(Selector$4.DATA_TOGGLE));
+
+ for (var i = 0, len = toggles.length; i < len; i++) {
+ var parent = Dropdown._getParentFromElement(toggles[i]);
+
+ var context = $(toggles[i]).data(DATA_KEY$4);
+ var relatedTarget = {
+ relatedTarget: toggles[i]
+ };
+
+ if (event && event.type === 'click') {
+ relatedTarget.clickEvent = event;
+ }
+
+ if (!context) {
+ continue;
+ }
+
+ var dropdownMenu = context._menu;
+
+ if (!$(parent).hasClass(ClassName$4.SHOW)) {
+ continue;
+ }
+
+ if (event && (event.type === 'click' && /input|textarea/i.test(event.target.tagName) || event.type === 'keyup' && event.which === TAB_KEYCODE) && $.contains(parent, event.target)) {
+ continue;
+ }
+
+ var hideEvent = $.Event(Event$4.HIDE, relatedTarget);
+ $(parent).trigger(hideEvent);
+
+ if (hideEvent.isDefaultPrevented()) {
+ continue;
+ } // If this is a touch-enabled device we remove the extra
+ // empty mouseover listeners we added for iOS support
+
+
+ if ('ontouchstart' in document.documentElement) {
+ $(document.body).children().off('mouseover', null, $.noop);
+ }
+
+ toggles[i].setAttribute('aria-expanded', 'false');
+ $(dropdownMenu).removeClass(ClassName$4.SHOW);
+ $(parent).removeClass(ClassName$4.SHOW).trigger($.Event(Event$4.HIDDEN, relatedTarget));
+ }
+ };
+
+ Dropdown._getParentFromElement = function _getParentFromElement(element) {
+ var parent;
+ var selector = Util.getSelectorFromElement(element);
+
+ if (selector) {
+ parent = document.querySelector(selector);
+ }
+
+ return parent || element.parentNode;
+ } // eslint-disable-next-line complexity
+ ;
+
+ Dropdown._dataApiKeydownHandler = function _dataApiKeydownHandler(event) {
+ // If not input/textarea:
+ // - And not a key in REGEXP_KEYDOWN => not a dropdown command
+ // If input/textarea:
+ // - If space key => not a dropdown command
+ // - If key is other than escape
+ // - If key is not up or down => not a dropdown command
+ // - If trigger inside the menu => not a dropdown command
+ if (/input|textarea/i.test(event.target.tagName) ? event.which === SPACE_KEYCODE || event.which !== ESCAPE_KEYCODE && (event.which !== ARROW_DOWN_KEYCODE && event.which !== ARROW_UP_KEYCODE || $(event.target).closest(Selector$4.MENU).length) : !REGEXP_KEYDOWN.test(event.which)) {
+ return;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ if (this.disabled || $(this).hasClass(ClassName$4.DISABLED)) {
+ return;
+ }
+
+ var parent = Dropdown._getParentFromElement(this);
+
+ var isActive = $(parent).hasClass(ClassName$4.SHOW);
+
+ if (!isActive || isActive && (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE)) {
+ if (event.which === ESCAPE_KEYCODE) {
+ var toggle = parent.querySelector(Selector$4.DATA_TOGGLE);
+ $(toggle).trigger('focus');
+ }
+
+ $(this).trigger('click');
+ return;
+ }
+
+ var items = [].slice.call(parent.querySelectorAll(Selector$4.VISIBLE_ITEMS));
+
+ if (items.length === 0) {
+ return;
+ }
+
+ var index = items.indexOf(event.target);
+
+ if (event.which === ARROW_UP_KEYCODE && index > 0) {
+ // Up
+ index--;
+ }
+
+ if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) {
+ // Down
+ index++;
+ }
+
+ if (index < 0) {
+ index = 0;
+ }
+
+ items[index].focus();
+ };
+
+ _createClass(Dropdown, null, [{
+ key: "VERSION",
+ get: function get() {
+ return VERSION$4;
+ }
+ }, {
+ key: "Default",
+ get: function get() {
+ return Default$2;
+ }
+ }, {
+ key: "DefaultType",
+ get: function get() {
+ return DefaultType$2;
+ }
+ }]);
+
+ return Dropdown;
+ }();
+ /**
+ * ------------------------------------------------------------------------
+ * Data Api implementation
+ * ------------------------------------------------------------------------
+ */
+
+
+ $(document).on(Event$4.KEYDOWN_DATA_API, Selector$4.DATA_TOGGLE, Dropdown._dataApiKeydownHandler).on(Event$4.KEYDOWN_DATA_API, Selector$4.MENU, Dropdown._dataApiKeydownHandler).on(Event$4.CLICK_DATA_API + " " + Event$4.KEYUP_DATA_API, Dropdown._clearMenus).on(Event$4.CLICK_DATA_API, Selector$4.DATA_TOGGLE, function (event) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ Dropdown._jQueryInterface.call($(this), 'toggle');
+ }).on(Event$4.CLICK_DATA_API, Selector$4.FORM_CHILD, function (e) {
+ e.stopPropagation();
+ });
+ /**
+ * ------------------------------------------------------------------------
+ * jQuery
+ * ------------------------------------------------------------------------
+ */
+
+ $.fn[NAME$4] = Dropdown._jQueryInterface;
+ $.fn[NAME$4].Constructor = Dropdown;
+
+ $.fn[NAME$4].noConflict = function () {
+ $.fn[NAME$4] = JQUERY_NO_CONFLICT$4;
+ return Dropdown._jQueryInterface;
+ };
+
+ /**
+ * ------------------------------------------------------------------------
+ * Constants
+ * ------------------------------------------------------------------------
+ */
+
+ var NAME$5 = 'modal';
+ var VERSION$5 = '4.3.1';
+ var DATA_KEY$5 = 'bs.modal';
+ var EVENT_KEY$5 = "." + DATA_KEY$5;
+ var DATA_API_KEY$5 = '.data-api';
+ var JQUERY_NO_CONFLICT$5 = $.fn[NAME$5];
+ var ESCAPE_KEYCODE$1 = 27; // KeyboardEvent.which value for Escape (Esc) key
+
+ var Default$3 = {
+ backdrop: true,
+ keyboard: true,
+ focus: true,
+ show: true
+ };
+ var DefaultType$3 = {
+ backdrop: '(boolean|string)',
+ keyboard: 'boolean',
+ focus: 'boolean',
+ show: 'boolean'
+ };
+ var Event$5 = {
+ HIDE: "hide" + EVENT_KEY$5,
+ HIDDEN: "hidden" + EVENT_KEY$5,
+ SHOW: "show" + EVENT_KEY$5,
+ SHOWN: "shown" + EVENT_KEY$5,
+ FOCUSIN: "focusin" + EVENT_KEY$5,
+ RESIZE: "resize" + EVENT_KEY$5,
+ CLICK_DISMISS: "click.dismiss" + EVENT_KEY$5,
+ KEYDOWN_DISMISS: "keydown.dismiss" + EVENT_KEY$5,
+ MOUSEUP_DISMISS: "mouseup.dismiss" + EVENT_KEY$5,
+ MOUSEDOWN_DISMISS: "mousedown.dismiss" + EVENT_KEY$5,
+ CLICK_DATA_API: "click" + EVENT_KEY$5 + DATA_API_KEY$5
+ };
+ var ClassName$5 = {
+ SCROLLABLE: 'modal-dialog-scrollable',
+ SCROLLBAR_MEASURER: 'modal-scrollbar-measure',
+ BACKDROP: 'modal-backdrop',
+ OPEN: 'modal-open',
+ FADE: 'fade',
+ SHOW: 'show'
+ };
+ var Selector$5 = {
+ DIALOG: '.modal-dialog',
+ MODAL_BODY: '.modal-body',
+ DATA_TOGGLE: '[data-toggle="modal"]',
+ DATA_DISMISS: '[data-dismiss="modal"]',
+ FIXED_CONTENT: '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top',
+ STICKY_CONTENT: '.sticky-top'
+ /**
+ * ------------------------------------------------------------------------
+ * Class Definition
+ * ------------------------------------------------------------------------
+ */
+
+ };
+
+ var Modal =
+ /*#__PURE__*/
+ function () {
+ function Modal(element, config) {
+ this._config = this._getConfig(config);
+ this._element = element;
+ this._dialog = element.querySelector(Selector$5.DIALOG);
+ this._backdrop = null;
+ this._isShown = false;
+ this._isBodyOverflowing = false;
+ this._ignoreBackdropClick = false;
+ this._isTransitioning = false;
+ this._scrollbarWidth = 0;
+ } // Getters
+
+
+ var _proto = Modal.prototype;
+
+ // Public
+ _proto.toggle = function toggle(relatedTarget) {
+ return this._isShown ? this.hide() : this.show(relatedTarget);
+ };
+
+ _proto.show = function show(relatedTarget) {
+ var _this = this;
+
+ if (this._isShown || this._isTransitioning) {
+ return;
+ }
+
+ if ($(this._element).hasClass(ClassName$5.FADE)) {
+ this._isTransitioning = true;
+ }
+
+ var showEvent = $.Event(Event$5.SHOW, {
+ relatedTarget: relatedTarget
+ });
+ $(this._element).trigger(showEvent);
+
+ if (this._isShown || showEvent.isDefaultPrevented()) {
+ return;
+ }
+
+ this._isShown = true;
+
+ this._checkScrollbar();
+
+ this._setScrollbar();
+
+ this._adjustDialog();
+
+ this._setEscapeEvent();
+
+ this._setResizeEvent();
+
+ $(this._element).on(Event$5.CLICK_DISMISS, Selector$5.DATA_DISMISS, function (event) {
+ return _this.hide(event);
+ });
+ $(this._dialog).on(Event$5.MOUSEDOWN_DISMISS, function () {
+ $(_this._element).one(Event$5.MOUSEUP_DISMISS, function (event) {
+ if ($(event.target).is(_this._element)) {
+ _this._ignoreBackdropClick = true;
+ }
+ });
+ });
+
+ this._showBackdrop(function () {
+ return _this._showElement(relatedTarget);
+ });
+ };
+
+ _proto.hide = function hide(event) {
+ var _this2 = this;
+
+ if (event) {
+ event.preventDefault();
+ }
+
+ if (!this._isShown || this._isTransitioning) {
+ return;
+ }
+
+ var hideEvent = $.Event(Event$5.HIDE);
+ $(this._element).trigger(hideEvent);
+
+ if (!this._isShown || hideEvent.isDefaultPrevented()) {
+ return;
+ }
+
+ this._isShown = false;
+ var transition = $(this._element).hasClass(ClassName$5.FADE);
+
+ if (transition) {
+ this._isTransitioning = true;
+ }
+
+ this._setEscapeEvent();
+
+ this._setResizeEvent();
+
+ $(document).off(Event$5.FOCUSIN);
+ $(this._element).removeClass(ClassName$5.SHOW);
+ $(this._element).off(Event$5.CLICK_DISMISS);
+ $(this._dialog).off(Event$5.MOUSEDOWN_DISMISS);
+
+ if (transition) {
+ var transitionDuration = Util.getTransitionDurationFromElement(this._element);
+ $(this._element).one(Util.TRANSITION_END, function (event) {
+ return _this2._hideModal(event);
+ }).emulateTransitionEnd(transitionDuration);
+ } else {
+ this._hideModal();
+ }
+ };
+
+ _proto.dispose = function dispose() {
+ [window, this._element, this._dialog].forEach(function (htmlElement) {
+ return $(htmlElement).off(EVENT_KEY$5);
+ });
+ /**
+ * `document` has 2 events `Event.FOCUSIN` and `Event.CLICK_DATA_API`
+ * Do not move `document` in `htmlElements` array
+ * It will remove `Event.CLICK_DATA_API` event that should remain
+ */
+
+ $(document).off(Event$5.FOCUSIN);
+ $.removeData(this._element, DATA_KEY$5);
+ this._config = null;
+ this._element = null;
+ this._dialog = null;
+ this._backdrop = null;
+ this._isShown = null;
+ this._isBodyOverflowing = null;
+ this._ignoreBackdropClick = null;
+ this._isTransitioning = null;
+ this._scrollbarWidth = null;
+ };
+
+ _proto.handleUpdate = function handleUpdate() {
+ this._adjustDialog();
+ } // Private
+ ;
+
+ _proto._getConfig = function _getConfig(config) {
+ config = _objectSpread({}, Default$3, config);
+ Util.typeCheckConfig(NAME$5, config, DefaultType$3);
+ return config;
+ };
+
+ _proto._showElement = function _showElement(relatedTarget) {
+ var _this3 = this;
+
+ var transition = $(this._element).hasClass(ClassName$5.FADE);
+
+ if (!this._element.parentNode || this._element.parentNode.nodeType !== Node.ELEMENT_NODE) {
+ // Don't move modal's DOM position
+ document.body.appendChild(this._element);
+ }
+
+ this._element.style.display = 'block';
+
+ this._element.removeAttribute('aria-hidden');
+
+ this._element.setAttribute('aria-modal', true);
+
+ if ($(this._dialog).hasClass(ClassName$5.SCROLLABLE)) {
+ this._dialog.querySelector(Selector$5.MODAL_BODY).scrollTop = 0;
+ } else {
+ this._element.scrollTop = 0;
+ }
+
+ if (transition) {
+ Util.reflow(this._element);
+ }
+
+ $(this._element).addClass(ClassName$5.SHOW);
+
+ if (this._config.focus) {
+ this._enforceFocus();
+ }
+
+ var shownEvent = $.Event(Event$5.SHOWN, {
+ relatedTarget: relatedTarget
+ });
+
+ var transitionComplete = function transitionComplete() {
+ if (_this3._config.focus) {
+ _this3._element.focus();
+ }
+
+ _this3._isTransitioning = false;
+ $(_this3._element).trigger(shownEvent);
+ };
+
+ if (transition) {
+ var transitionDuration = Util.getTransitionDurationFromElement(this._dialog);
+ $(this._dialog).one(Util.TRANSITION_END, transitionComplete).emulateTransitionEnd(transitionDuration);
+ } else {
+ transitionComplete();
+ }
+ };
+
+ _proto._enforceFocus = function _enforceFocus() {
+ var _this4 = this;
+
+ $(document).off(Event$5.FOCUSIN) // Guard against infinite focus loop
+ .on(Event$5.FOCUSIN, function (event) {
+ if (document !== event.target && _this4._element !== event.target && $(_this4._element).has(event.target).length === 0) {
+ _this4._element.focus();
+ }
+ });
+ };
+
+ _proto._setEscapeEvent = function _setEscapeEvent() {
+ var _this5 = this;
+
+ if (this._isShown && this._config.keyboard) {
+ $(this._element).on(Event$5.KEYDOWN_DISMISS, function (event) {
+ if (event.which === ESCAPE_KEYCODE$1) {
+ event.preventDefault();
+
+ _this5.hide();
+ }
+ });
+ } else if (!this._isShown) {
+ $(this._element).off(Event$5.KEYDOWN_DISMISS);
+ }
+ };
+
+ _proto._setResizeEvent = function _setResizeEvent() {
+ var _this6 = this;
+
+ if (this._isShown) {
+ $(window).on(Event$5.RESIZE, function (event) {
+ return _this6.handleUpdate(event);
+ });
+ } else {
+ $(window).off(Event$5.RESIZE);
+ }
+ };
+
+ _proto._hideModal = function _hideModal() {
+ var _this7 = this;
+
+ this._element.style.display = 'none';
+
+ this._element.setAttribute('aria-hidden', true);
+
+ this._element.removeAttribute('aria-modal');
+
+ this._isTransitioning = false;
+
+ this._showBackdrop(function () {
+ $(document.body).removeClass(ClassName$5.OPEN);
+
+ _this7._resetAdjustments();
+
+ _this7._resetScrollbar();
+
+ $(_this7._element).trigger(Event$5.HIDDEN);
+ });
+ };
+
+ _proto._removeBackdrop = function _removeBackdrop() {
+ if (this._backdrop) {
+ $(this._backdrop).remove();
+ this._backdrop = null;
+ }
+ };
+
+ _proto._showBackdrop = function _showBackdrop(callback) {
+ var _this8 = this;
+
+ var animate = $(this._element).hasClass(ClassName$5.FADE) ? ClassName$5.FADE : '';
+
+ if (this._isShown && this._config.backdrop) {
+ this._backdrop = document.createElement('div');
+ this._backdrop.className = ClassName$5.BACKDROP;
+
+ if (animate) {
+ this._backdrop.classList.add(animate);
+ }
+
+ $(this._backdrop).appendTo(document.body);
+ $(this._element).on(Event$5.CLICK_DISMISS, function (event) {
+ if (_this8._ignoreBackdropClick) {
+ _this8._ignoreBackdropClick = false;
+ return;
+ }
+
+ if (event.target !== event.currentTarget) {
+ return;
+ }
+
+ if (_this8._config.backdrop === 'static') {
+ _this8._element.focus();
+ } else {
+ _this8.hide();
+ }
+ });
+
+ if (animate) {
+ Util.reflow(this._backdrop);
+ }
+
+ $(this._backdrop).addClass(ClassName$5.SHOW);
+
+ if (!callback) {
+ return;
+ }
+
+ if (!animate) {
+ callback();
+ return;
+ }
+
+ var backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop);
+ $(this._backdrop).one(Util.TRANSITION_END, callback).emulateTransitionEnd(backdropTransitionDuration);
+ } else if (!this._isShown && this._backdrop) {
+ $(this._backdrop).removeClass(ClassName$5.SHOW);
+
+ var callbackRemove = function callbackRemove() {
+ _this8._removeBackdrop();
+
+ if (callback) {
+ callback();
+ }
+ };
+
+ if ($(this._element).hasClass(ClassName$5.FADE)) {
+ var _backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop);
+
+ $(this._backdrop).one(Util.TRANSITION_END, callbackRemove).emulateTransitionEnd(_backdropTransitionDuration);
+ } else {
+ callbackRemove();
+ }
+ } else if (callback) {
+ callback();
+ }
+ } // ----------------------------------------------------------------------
+ // the following methods are used to handle overflowing modals
+ // todo (fat): these should probably be refactored out of modal.js
+ // ----------------------------------------------------------------------
+ ;
+
+ _proto._adjustDialog = function _adjustDialog() {
+ var isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;
+
+ if (!this._isBodyOverflowing && isModalOverflowing) {
+ this._element.style.paddingLeft = this._scrollbarWidth + "px";
+ }
+
+ if (this._isBodyOverflowing && !isModalOverflowing) {
+ this._element.style.paddingRight = this._scrollbarWidth + "px";
+ }
+ };
+
+ _proto._resetAdjustments = function _resetAdjustments() {
+ this._element.style.paddingLeft = '';
+ this._element.style.paddingRight = '';
+ };
+
+ _proto._checkScrollbar = function _checkScrollbar() {
+ var rect = document.body.getBoundingClientRect();
+ this._isBodyOverflowing = rect.left + rect.right < window.innerWidth;
+ this._scrollbarWidth = this._getScrollbarWidth();
+ };
+
+ _proto._setScrollbar = function _setScrollbar() {
+ var _this9 = this;
+
+ if (this._isBodyOverflowing) {
+ // Note: DOMNode.style.paddingRight returns the actual value or '' if not set
+ // while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set
+ var fixedContent = [].slice.call(document.querySelectorAll(Selector$5.FIXED_CONTENT));
+ var stickyContent = [].slice.call(document.querySelectorAll(Selector$5.STICKY_CONTENT)); // Adjust fixed content padding
+
+ $(fixedContent).each(function (index, element) {
+ var actualPadding = element.style.paddingRight;
+ var calculatedPadding = $(element).css('padding-right');
+ $(element).data('padding-right', actualPadding).css('padding-right', parseFloat(calculatedPadding) + _this9._scrollbarWidth + "px");
+ }); // Adjust sticky content margin
+
+ $(stickyContent).each(function (index, element) {
+ var actualMargin = element.style.marginRight;
+ var calculatedMargin = $(element).css('margin-right');
+ $(element).data('margin-right', actualMargin).css('margin-right', parseFloat(calculatedMargin) - _this9._scrollbarWidth + "px");
+ }); // Adjust body padding
+
+ var actualPadding = document.body.style.paddingRight;
+ var calculatedPadding = $(document.body).css('padding-right');
+ $(document.body).data('padding-right', actualPadding).css('padding-right', parseFloat(calculatedPadding) + this._scrollbarWidth + "px");
+ }
+
+ $(document.body).addClass(ClassName$5.OPEN);
+ };
+
+ _proto._resetScrollbar = function _resetScrollbar() {
+ // Restore fixed content padding
+ var fixedContent = [].slice.call(document.querySelectorAll(Selector$5.FIXED_CONTENT));
+ $(fixedContent).each(function (index, element) {
+ var padding = $(element).data('padding-right');
+ $(element).removeData('padding-right');
+ element.style.paddingRight = padding ? padding : '';
+ }); // Restore sticky content
+
+ var elements = [].slice.call(document.querySelectorAll("" + Selector$5.STICKY_CONTENT));
+ $(elements).each(function (index, element) {
+ var margin = $(element).data('margin-right');
+
+ if (typeof margin !== 'undefined') {
+ $(element).css('margin-right', margin).removeData('margin-right');
+ }
+ }); // Restore body padding
+
+ var padding = $(document.body).data('padding-right');
+ $(document.body).removeData('padding-right');
+ document.body.style.paddingRight = padding ? padding : '';
+ };
+
+ _proto._getScrollbarWidth = function _getScrollbarWidth() {
+ // thx d.walsh
+ var scrollDiv = document.createElement('div');
+ scrollDiv.className = ClassName$5.SCROLLBAR_MEASURER;
+ document.body.appendChild(scrollDiv);
+ var scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth;
+ document.body.removeChild(scrollDiv);
+ return scrollbarWidth;
+ } // Static
+ ;
+
+ Modal._jQueryInterface = function _jQueryInterface(config, relatedTarget) {
+ return this.each(function () {
+ var data = $(this).data(DATA_KEY$5);
+
+ var _config = _objectSpread({}, Default$3, $(this).data(), typeof config === 'object' && config ? config : {});
+
+ if (!data) {
+ data = new Modal(this, _config);
+ $(this).data(DATA_KEY$5, data);
+ }
+
+ if (typeof config === 'string') {
+ if (typeof data[config] === 'undefined') {
+ throw new TypeError("No method named \"" + config + "\"");
+ }
+
+ data[config](relatedTarget);
+ } else if (_config.show) {
+ data.show(relatedTarget);
+ }
+ });
+ };
+
+ _createClass(Modal, null, [{
+ key: "VERSION",
+ get: function get() {
+ return VERSION$5;
+ }
+ }, {
+ key: "Default",
+ get: function get() {
+ return Default$3;
+ }
+ }]);
+
+ return Modal;
+ }();
+ /**
+ * ------------------------------------------------------------------------
+ * Data Api implementation
+ * ------------------------------------------------------------------------
+ */
+
+
+ $(document).on(Event$5.CLICK_DATA_API, Selector$5.DATA_TOGGLE, function (event) {
+ var _this10 = this;
+
+ var target;
+ var selector = Util.getSelectorFromElement(this);
+
+ if (selector) {
+ target = document.querySelector(selector);
+ }
+
+ var config = $(target).data(DATA_KEY$5) ? 'toggle' : _objectSpread({}, $(target).data(), $(this).data());
+
+ if (this.tagName === 'A' || this.tagName === 'AREA') {
+ event.preventDefault();
+ }
+
+ var $target = $(target).one(Event$5.SHOW, function (showEvent) {
+ if (showEvent.isDefaultPrevented()) {
+ // Only register focus restorer if modal will actually get shown
+ return;
+ }
+
+ $target.one(Event$5.HIDDEN, function () {
+ if ($(_this10).is(':visible')) {
+ _this10.focus();
+ }
+ });
+ });
+
+ Modal._jQueryInterface.call($(target), config, this);
+ });
+ /**
+ * ------------------------------------------------------------------------
+ * jQuery
+ * ------------------------------------------------------------------------
+ */
+
+ $.fn[NAME$5] = Modal._jQueryInterface;
+ $.fn[NAME$5].Constructor = Modal;
+
+ $.fn[NAME$5].noConflict = function () {
+ $.fn[NAME$5] = JQUERY_NO_CONFLICT$5;
+ return Modal._jQueryInterface;
+ };
+
+ /**
+ * --------------------------------------------------------------------------
+ * Bootstrap (v4.3.1): tools/sanitizer.js
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * --------------------------------------------------------------------------
+ */
+ var uriAttrs = ['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href'];
+ var ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i;
+ var DefaultWhitelist = {
+ // Global attributes allowed on any supplied element below.
+ '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],
+ a: ['target', 'href', 'title', 'rel'],
+ area: [],
+ b: [],
+ br: [],
+ col: [],
+ code: [],
+ div: [],
+ em: [],
+ hr: [],
+ h1: [],
+ h2: [],
+ h3: [],
+ h4: [],
+ h5: [],
+ h6: [],
+ i: [],
+ img: ['src', 'alt', 'title', 'width', 'height'],
+ li: [],
+ ol: [],
+ p: [],
+ pre: [],
+ s: [],
+ small: [],
+ span: [],
+ sub: [],
+ sup: [],
+ strong: [],
+ u: [],
+ ul: []
+ /**
+ * A pattern that recognizes a commonly useful subset of URLs that are safe.
+ *
+ * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts
+ */
+
+ };
+ var SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi;
+ /**
+ * A pattern that matches safe data URLs. Only matches image, video and audio types.
+ *
+ * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts
+ */
+
+ var DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i;
+
+ function allowedAttribute(attr, allowedAttributeList) {
+ var attrName = attr.nodeName.toLowerCase();
+
+ if (allowedAttributeList.indexOf(attrName) !== -1) {
+ if (uriAttrs.indexOf(attrName) !== -1) {
+ return Boolean(attr.nodeValue.match(SAFE_URL_PATTERN) || attr.nodeValue.match(DATA_URL_PATTERN));
+ }
+
+ return true;
+ }
+
+ var regExp = allowedAttributeList.filter(function (attrRegex) {
+ return attrRegex instanceof RegExp;
+ }); // Check if a regular expression validates the attribute.
+
+ for (var i = 0, l = regExp.length; i < l; i++) {
+ if (attrName.match(regExp[i])) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) {
+ if (unsafeHtml.length === 0) {
+ return unsafeHtml;
+ }
+
+ if (sanitizeFn && typeof sanitizeFn === 'function') {
+ return sanitizeFn(unsafeHtml);
+ }
+
+ var domParser = new window.DOMParser();
+ var createdDocument = domParser.parseFromString(unsafeHtml, 'text/html');
+ var whitelistKeys = Object.keys(whiteList);
+ var elements = [].slice.call(createdDocument.body.querySelectorAll('*'));
+
+ var _loop = function _loop(i, len) {
+ var el = elements[i];
+ var elName = el.nodeName.toLowerCase();
+
+ if (whitelistKeys.indexOf(el.nodeName.toLowerCase()) === -1) {
+ el.parentNode.removeChild(el);
+ return "continue";
+ }
+
+ var attributeList = [].slice.call(el.attributes);
+ var whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || []);
+ attributeList.forEach(function (attr) {
+ if (!allowedAttribute(attr, whitelistedAttributes)) {
+ el.removeAttribute(attr.nodeName);
+ }
+ });
+ };
+
+ for (var i = 0, len = elements.length; i < len; i++) {
+ var _ret = _loop(i, len);
+
+ if (_ret === "continue") continue;
+ }
+
+ return createdDocument.body.innerHTML;
+ }
+
+ /**
+ * ------------------------------------------------------------------------
+ * Constants
+ * ------------------------------------------------------------------------
+ */
+
+ var NAME$6 = 'tooltip';
+ var VERSION$6 = '4.3.1';
+ var DATA_KEY$6 = 'bs.tooltip';
+ var EVENT_KEY$6 = "." + DATA_KEY$6;
+ var JQUERY_NO_CONFLICT$6 = $.fn[NAME$6];
+ var CLASS_PREFIX = 'bs-tooltip';
+ var BSCLS_PREFIX_REGEX = new RegExp("(^|\\s)" + CLASS_PREFIX + "\\S+", 'g');
+ var DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn'];
+ var DefaultType$4 = {
+ animation: 'boolean',
+ template: 'string',
+ title: '(string|element|function)',
+ trigger: 'string',
+ delay: '(number|object)',
+ html: 'boolean',
+ selector: '(string|boolean)',
+ placement: '(string|function)',
+ offset: '(number|string|function)',
+ container: '(string|element|boolean)',
+ fallbackPlacement: '(string|array)',
+ boundary: '(string|element)',
+ sanitize: 'boolean',
+ sanitizeFn: '(null|function)',
+ whiteList: 'object'
+ };
+ var AttachmentMap$1 = {
+ AUTO: 'auto',
+ TOP: 'top',
+ RIGHT: 'right',
+ BOTTOM: 'bottom',
+ LEFT: 'left'
+ };
+ var Default$4 = {
+ animation: true,
+ template: '<div class="tooltip" role="tooltip">' + '<div class="arrow"></div>' + '<div class="tooltip-inner"></div></div>',
+ trigger: 'hover focus',
+ title: '',
+ delay: 0,
+ html: false,
+ selector: false,
+ placement: 'top',
+ offset: 0,
+ container: false,
+ fallbackPlacement: 'flip',
+ boundary: 'scrollParent',
+ sanitize: true,
+ sanitizeFn: null,
+ whiteList: DefaultWhitelist
+ };
+ var HoverState = {
+ SHOW: 'show',
+ OUT: 'out'
+ };
+ var Event$6 = {
+ HIDE: "hide" + EVENT_KEY$6,
+ HIDDEN: "hidden" + EVENT_KEY$6,
+ SHOW: "show" + EVENT_KEY$6,
+ SHOWN: "shown" + EVENT_KEY$6,
+ INSERTED: "inserted" + EVENT_KEY$6,
+ CLICK: "click" + EVENT_KEY$6,
+ FOCUSIN: "focusin" + EVENT_KEY$6,
+ FOCUSOUT: "focusout" + EVENT_KEY$6,
+ MOUSEENTER: "mouseenter" + EVENT_KEY$6,
+ MOUSELEAVE: "mouseleave" + EVENT_KEY$6
+ };
+ var ClassName$6 = {
+ FADE: 'fade',
+ SHOW: 'show'
+ };
+ var Selector$6 = {
+ TOOLTIP: '.tooltip',
+ TOOLTIP_INNER: '.tooltip-inner',
+ ARROW: '.arrow'
+ };
+ var Trigger = {
+ HOVER: 'hover',
+ FOCUS: 'focus',
+ CLICK: 'click',
+ MANUAL: 'manual'
+ /**
+ * ------------------------------------------------------------------------
+ * Class Definition
+ * ------------------------------------------------------------------------
+ */
+
+ };
+
+ var Tooltip =
+ /*#__PURE__*/
+ function () {
+ function Tooltip(element, config) {
+ /**
+ * Check for Popper dependency
+ * Popper - https://popper.js.org
+ */
+ if (typeof Popper === 'undefined') {
+ throw new TypeError('Bootstrap\'s tooltips require Popper.js (https://popper.js.org/)');
+ } // private
+
+
+ this._isEnabled = true;
+ this._timeout = 0;
+ this._hoverState = '';
+ this._activeTrigger = {};
+ this._popper = null; // Protected
+
+ this.element = element;
+ this.config = this._getConfig(config);
+ this.tip = null;
+
+ this._setListeners();
+ } // Getters
+
+
+ var _proto = Tooltip.prototype;
+
+ // Public
+ _proto.enable = function enable() {
+ this._isEnabled = true;
+ };
+
+ _proto.disable = function disable() {
+ this._isEnabled = false;
+ };
+
+ _proto.toggleEnabled = function toggleEnabled() {
+ this._isEnabled = !this._isEnabled;
+ };
+
+ _proto.toggle = function toggle(event) {
+ if (!this._isEnabled) {
+ return;
+ }
+
+ if (event) {
+ var dataKey = this.constructor.DATA_KEY;
+ var context = $(event.currentTarget).data(dataKey);
+
+ if (!context) {
+ context = new this.constructor(event.currentTarget, this._getDelegateConfig());
+ $(event.currentTarget).data(dataKey, context);
+ }
+
+ context._activeTrigger.click = !context._activeTrigger.click;
+
+ if (context._isWithActiveTrigger()) {
+ context._enter(null, context);
+ } else {
+ context._leave(null, context);
+ }
+ } else {
+ if ($(this.getTipElement()).hasClass(ClassName$6.SHOW)) {
+ this._leave(null, this);
+
+ return;
+ }
+
+ this._enter(null, this);
+ }
+ };
+
+ _proto.dispose = function dispose() {
+ clearTimeout(this._timeout);
+ $.removeData(this.element, this.constructor.DATA_KEY);
+ $(this.element).off(this.constructor.EVENT_KEY);
+ $(this.element).closest('.modal').off('hide.bs.modal');
+
+ if (this.tip) {
+ $(this.tip).remove();
+ }
+
+ this._isEnabled = null;
+ this._timeout = null;
+ this._hoverState = null;
+ this._activeTrigger = null;
+
+ if (this._popper !== null) {
+ this._popper.destroy();
+ }
+
+ this._popper = null;
+ this.element = null;
+ this.config = null;
+ this.tip = null;
+ };
+
+ _proto.show = function show() {
+ var _this = this;
+
+ if ($(this.element).css('display') === 'none') {
+ throw new Error('Please use show on visible elements');
+ }
+
+ var showEvent = $.Event(this.constructor.Event.SHOW);
+
+ if (this.isWithContent() && this._isEnabled) {
+ $(this.element).trigger(showEvent);
+ var shadowRoot = Util.findShadowRoot(this.element);
+ var isInTheDom = $.contains(shadowRoot !== null ? shadowRoot : this.element.ownerDocument.documentElement, this.element);
+
+ if (showEvent.isDefaultPrevented() || !isInTheDom) {
+ return;
+ }
+
+ var tip = this.getTipElement();
+ var tipId = Util.getUID(this.constructor.NAME);
+ tip.setAttribute('id', tipId);
+ this.element.setAttribute('aria-describedby', tipId);
+ this.setContent();
+
+ if (this.config.animation) {
+ $(tip).addClass(ClassName$6.FADE);
+ }
+
+ var placement = typeof this.config.placement === 'function' ? this.config.placement.call(this, tip, this.element) : this.config.placement;
+
+ var attachment = this._getAttachment(placement);
+
+ this.addAttachmentClass(attachment);
+
+ var container = this._getContainer();
+
+ $(tip).data(this.constructor.DATA_KEY, this);
+
+ if (!$.contains(this.element.ownerDocument.documentElement, this.tip)) {
+ $(tip).appendTo(container);
+ }
+
+ $(this.element).trigger(this.constructor.Event.INSERTED);
+ this._popper = new Popper(this.element, tip, {
+ placement: attachment,
+ modifiers: {
+ offset: this._getOffset(),
+ flip: {
+ behavior: this.config.fallbackPlacement
+ },
+ arrow: {
+ element: Selector$6.ARROW
+ },
+ preventOverflow: {
+ boundariesElement: this.config.boundary
+ }
+ },
+ onCreate: function onCreate(data) {
+ if (data.originalPlacement !== data.placement) {
+ _this._handlePopperPlacementChange(data);
+ }
+ },
+ onUpdate: function onUpdate(data) {
+ return _this._handlePopperPlacementChange(data);
+ }
+ });
+ $(tip).addClass(ClassName$6.SHOW); // If this is a touch-enabled device we add extra
+ // empty mouseover listeners to the body's immediate children;
+ // only needed because of broken event delegation on iOS
+ // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
+
+ if ('ontouchstart' in document.documentElement) {
+ $(document.body).children().on('mouseover', null, $.noop);
+ }
+
+ var complete = function complete() {
+ if (_this.config.animation) {
+ _this._fixTransition();
+ }
+
+ var prevHoverState = _this._hoverState;
+ _this._hoverState = null;
+ $(_this.element).trigger(_this.constructor.Event.SHOWN);
+
+ if (prevHoverState === HoverState.OUT) {
+ _this._leave(null, _this);
+ }
+ };
+
+ if ($(this.tip).hasClass(ClassName$6.FADE)) {
+ var transitionDuration = Util.getTransitionDurationFromElement(this.tip);
+ $(this.tip).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);
+ } else {
+ complete();
+ }
+ }
+ };
+
+ _proto.hide = function hide(callback) {
+ var _this2 = this;
+
+ var tip = this.getTipElement();
+ var hideEvent = $.Event(this.constructor.Event.HIDE);
+
+ var complete = function complete() {
+ if (_this2._hoverState !== HoverState.SHOW && tip.parentNode) {
+ tip.parentNode.removeChild(tip);
+ }
+
+ _this2._cleanTipClass();
+
+ _this2.element.removeAttribute('aria-describedby');
+
+ $(_this2.element).trigger(_this2.constructor.Event.HIDDEN);
+
+ if (_this2._popper !== null) {
+ _this2._popper.destroy();
+ }
+
+ if (callback) {
+ callback();
+ }
+ };
+
+ $(this.element).trigger(hideEvent);
+
+ if (hideEvent.isDefaultPrevented()) {
+ return;
+ }
+
+ $(tip).removeClass(ClassName$6.SHOW); // If this is a touch-enabled device we remove the extra
+ // empty mouseover listeners we added for iOS support
+
+ if ('ontouchstart' in document.documentElement) {
+ $(document.body).children().off('mouseover', null, $.noop);
+ }
+
+ this._activeTrigger[Trigger.CLICK] = false;
+ this._activeTrigger[Trigger.FOCUS] = false;
+ this._activeTrigger[Trigger.HOVER] = false;
+
+ if ($(this.tip).hasClass(ClassName$6.FADE)) {
+ var transitionDuration = Util.getTransitionDurationFromElement(tip);
+ $(tip).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);
+ } else {
+ complete();
+ }
+
+ this._hoverState = '';
+ };
+
+ _proto.update = function update() {
+ if (this._popper !== null) {
+ this._popper.scheduleUpdate();
+ }
+ } // Protected
+ ;
+
+ _proto.isWithContent = function isWithContent() {
+ return Boolean(this.getTitle());
+ };
+
+ _proto.addAttachmentClass = function addAttachmentClass(attachment) {
+ $(this.getTipElement()).addClass(CLASS_PREFIX + "-" + attachment);
+ };
+
+ _proto.getTipElement = function getTipElement() {
+ this.tip = this.tip || $(this.config.template)[0];
+ return this.tip;
+ };
+
+ _proto.setContent = function setContent() {
+ var tip = this.getTipElement();
+ this.setElementContent($(tip.querySelectorAll(Selector$6.TOOLTIP_INNER)), this.getTitle());
+ $(tip).removeClass(ClassName$6.FADE + " " + ClassName$6.SHOW);
+ };
+
+ _proto.setElementContent = function setElementContent($element, content) {
+ if (typeof content === 'object' && (content.nodeType || content.jquery)) {
+ // Content is a DOM node or a jQuery
+ if (this.config.html) {
+ if (!$(content).parent().is($element)) {
+ $element.empty().append(content);
+ }
+ } else {
+ $element.text($(content).text());
+ }
+
+ return;
+ }
+
+ if (this.config.html) {
+ if (this.config.sanitize) {
+ content = sanitizeHtml(content, this.config.whiteList, this.config.sanitizeFn);
+ }
+
+ $element.html(content);
+ } else {
+ $element.text(content);
+ }
+ };
+
+ _proto.getTitle = function getTitle() {
+ var title = this.element.getAttribute('data-original-title');
+
+ if (!title) {
+ title = typeof this.config.title === 'function' ? this.config.title.call(this.element) : this.config.title;
+ }
+
+ return title;
+ } // Private
+ ;
+
+ _proto._getOffset = function _getOffset() {
+ var _this3 = this;
+
+ var offset = {};
+
+ if (typeof this.config.offset === 'function') {
+ offset.fn = function (data) {
+ data.offsets = _objectSpread({}, data.offsets, _this3.config.offset(data.offsets, _this3.element) || {});
+ return data;
+ };
+ } else {
+ offset.offset = this.config.offset;
+ }
+
+ return offset;
+ };
+
+ _proto._getContainer = function _getContainer() {
+ if (this.config.container === false) {
+ return document.body;
+ }
+
+ if (Util.isElement(this.config.container)) {
+ return $(this.config.container);
+ }
+
+ return $(document).find(this.config.container);
+ };
+
+ _proto._getAttachment = function _getAttachment(placement) {
+ return AttachmentMap$1[placement.toUpperCase()];
+ };
+
+ _proto._setListeners = function _setListeners() {
+ var _this4 = this;
+
+ var triggers = this.config.trigger.split(' ');
+ triggers.forEach(function (trigger) {
+ if (trigger === 'click') {
+ $(_this4.element).on(_this4.constructor.Event.CLICK, _this4.config.selector, function (event) {
+ return _this4.toggle(event);
+ });
+ } else if (trigger !== Trigger.MANUAL) {
+ var eventIn = trigger === Trigger.HOVER ? _this4.constructor.Event.MOUSEENTER : _this4.constructor.Event.FOCUSIN;
+ var eventOut = trigger === Trigger.HOVER ? _this4.constructor.Event.MOUSELEAVE : _this4.constructor.Event.FOCUSOUT;
+ $(_this4.element).on(eventIn, _this4.config.selector, function (event) {
+ return _this4._enter(event);
+ }).on(eventOut, _this4.config.selector, function (event) {
+ return _this4._leave(event);
+ });
+ }
+ });
+ $(this.element).closest('.modal').on('hide.bs.modal', function () {
+ if (_this4.element) {
+ _this4.hide();
+ }
+ });
+
+ if (this.config.selector) {
+ this.config = _objectSpread({}, this.config, {
+ trigger: 'manual',
+ selector: ''
+ });
+ } else {
+ this._fixTitle();
+ }
+ };
+
+ _proto._fixTitle = function _fixTitle() {
+ var titleType = typeof this.element.getAttribute('data-original-title');
+
+ if (this.element.getAttribute('title') || titleType !== 'string') {
+ this.element.setAttribute('data-original-title', this.element.getAttribute('title') || '');
+ this.element.setAttribute('title', '');
+ }
+ };
+
+ _proto._enter = function _enter(event, context) {
+ var dataKey = this.constructor.DATA_KEY;
+ context = context || $(event.currentTarget).data(dataKey);
+
+ if (!context) {
+ context = new this.constructor(event.currentTarget, this._getDelegateConfig());
+ $(event.currentTarget).data(dataKey, context);
+ }
+
+ if (event) {
+ context._activeTrigger[event.type === 'focusin' ? Trigger.FOCUS : Trigger.HOVER] = true;
+ }
+
+ if ($(context.getTipElement()).hasClass(ClassName$6.SHOW) || context._hoverState === HoverState.SHOW) {
+ context._hoverState = HoverState.SHOW;
+ return;
+ }
+
+ clearTimeout(context._timeout);
+ context._hoverState = HoverState.SHOW;
+
+ if (!context.config.delay || !context.config.delay.show) {
+ context.show();
+ return;
+ }
+
+ context._timeout = setTimeout(function () {
+ if (context._hoverState === HoverState.SHOW) {
+ context.show();
+ }
+ }, context.config.delay.show);
+ };
+
+ _proto._leave = function _leave(event, context) {
+ var dataKey = this.constructor.DATA_KEY;
+ context = context || $(event.currentTarget).data(dataKey);
+
+ if (!context) {
+ context = new this.constructor(event.currentTarget, this._getDelegateConfig());
+ $(event.currentTarget).data(dataKey, context);
+ }
+
+ if (event) {
+ context._activeTrigger[event.type === 'focusout' ? Trigger.FOCUS : Trigger.HOVER] = false;
+ }
+
+ if (context._isWithActiveTrigger()) {
+ return;
+ }
+
+ clearTimeout(context._timeout);
+ context._hoverState = HoverState.OUT;
+
+ if (!context.config.delay || !context.config.delay.hide) {
+ context.hide();
+ return;
+ }
+
+ context._timeout = setTimeout(function () {
+ if (context._hoverState === HoverState.OUT) {
+ context.hide();
+ }
+ }, context.config.delay.hide);
+ };
+
+ _proto._isWithActiveTrigger = function _isWithActiveTrigger() {
+ for (var trigger in this._activeTrigger) {
+ if (this._activeTrigger[trigger]) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ _proto._getConfig = function _getConfig(config) {
+ var dataAttributes = $(this.element).data();
+ Object.keys(dataAttributes).forEach(function (dataAttr) {
+ if (DISALLOWED_ATTRIBUTES.indexOf(dataAttr) !== -1) {
+ delete dataAttributes[dataAttr];
+ }
+ });
+ config = _objectSpread({}, this.constructor.Default, dataAttributes, typeof config === 'object' && config ? config : {});
+
+ if (typeof config.delay === 'number') {
+ config.delay = {
+ show: config.delay,
+ hide: config.delay
+ };
+ }
+
+ if (typeof config.title === 'number') {
+ config.title = config.title.toString();
+ }
+
+ if (typeof config.content === 'number') {
+ config.content = config.content.toString();
+ }
+
+ Util.typeCheckConfig(NAME$6, config, this.constructor.DefaultType);
+
+ if (config.sanitize) {
+ config.template = sanitizeHtml(config.template, config.whiteList, config.sanitizeFn);
+ }
+
+ return config;
+ };
+
+ _proto._getDelegateConfig = function _getDelegateConfig() {
+ var config = {};
+
+ if (this.config) {
+ for (var key in this.config) {
+ if (this.constructor.Default[key] !== this.config[key]) {
+ config[key] = this.config[key];
+ }
+ }
+ }
+
+ return config;
+ };
+
+ _proto._cleanTipClass = function _cleanTipClass() {
+ var $tip = $(this.getTipElement());
+ var tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX);
+
+ if (tabClass !== null && tabClass.length) {
+ $tip.removeClass(tabClass.join(''));
+ }
+ };
+
+ _proto._handlePopperPlacementChange = function _handlePopperPlacementChange(popperData) {
+ var popperInstance = popperData.instance;
+ this.tip = popperInstance.popper;
+
+ this._cleanTipClass();
+
+ this.addAttachmentClass(this._getAttachment(popperData.placement));
+ };
+
+ _proto._fixTransition = function _fixTransition() {
+ var tip = this.getTipElement();
+ var initConfigAnimation = this.config.animation;
+
+ if (tip.getAttribute('x-placement') !== null) {
+ return;
+ }
+
+ $(tip).removeClass(ClassName$6.FADE);
+ this.config.animation = false;
+ this.hide();
+ this.show();
+ this.config.animation = initConfigAnimation;
+ } // Static
+ ;
+
+ Tooltip._jQueryInterface = function _jQueryInterface(config) {
+ return this.each(function () {
+ var data = $(this).data(DATA_KEY$6);
+
+ var _config = typeof config === 'object' && config;
+
+ if (!data && /dispose|hide/.test(config)) {
+ return;
+ }
+
+ if (!data) {
+ data = new Tooltip(this, _config);
+ $(this).data(DATA_KEY$6, data);
+ }
+
+ if (typeof config === 'string') {
+ if (typeof data[config] === 'undefined') {
+ throw new TypeError("No method named \"" + config + "\"");
+ }
+
+ data[config]();
+ }
+ });
+ };
+
+ _createClass(Tooltip, null, [{
+ key: "VERSION",
+ get: function get() {
+ return VERSION$6;
+ }
+ }, {
+ key: "Default",
+ get: function get() {
+ return Default$4;
+ }
+ }, {
+ key: "NAME",
+ get: function get() {
+ return NAME$6;
+ }
+ }, {
+ key: "DATA_KEY",
+ get: function get() {
+ return DATA_KEY$6;
+ }
+ }, {
+ key: "Event",
+ get: function get() {
+ return Event$6;
+ }
+ }, {
+ key: "EVENT_KEY",
+ get: function get() {
+ return EVENT_KEY$6;
+ }
+ }, {
+ key: "DefaultType",
+ get: function get() {
+ return DefaultType$4;
+ }
+ }]);
+
+ return Tooltip;
+ }();
+ /**
+ * ------------------------------------------------------------------------
+ * jQuery
+ * ------------------------------------------------------------------------
+ */
+
+
+ $.fn[NAME$6] = Tooltip._jQueryInterface;
+ $.fn[NAME$6].Constructor = Tooltip;
+
+ $.fn[NAME$6].noConflict = function () {
+ $.fn[NAME$6] = JQUERY_NO_CONFLICT$6;
+ return Tooltip._jQueryInterface;
+ };
+
+ /**
+ * ------------------------------------------------------------------------
+ * Constants
+ * ------------------------------------------------------------------------
+ */
+
+ var NAME$7 = 'popover';
+ var VERSION$7 = '4.3.1';
+ var DATA_KEY$7 = 'bs.popover';
+ var EVENT_KEY$7 = "." + DATA_KEY$7;
+ var JQUERY_NO_CONFLICT$7 = $.fn[NAME$7];
+ var CLASS_PREFIX$1 = 'bs-popover';
+ var BSCLS_PREFIX_REGEX$1 = new RegExp("(^|\\s)" + CLASS_PREFIX$1 + "\\S+", 'g');
+
+ var Default$5 = _objectSpread({}, Tooltip.Default, {
+ placement: 'right',
+ trigger: 'click',
+ content: '',
+ template: '<div class="popover" role="tooltip">' + '<div class="arrow"></div>' + '<h3 class="popover-header"></h3>' + '<div class="popover-body"></div></div>'
+ });
+
+ var DefaultType$5 = _objectSpread({}, Tooltip.DefaultType, {
+ content: '(string|element|function)'
+ });
+
+ var ClassName$7 = {
+ FADE: 'fade',
+ SHOW: 'show'
+ };
+ var Selector$7 = {
+ TITLE: '.popover-header',
+ CONTENT: '.popover-body'
+ };
+ var Event$7 = {
+ HIDE: "hide" + EVENT_KEY$7,
+ HIDDEN: "hidden" + EVENT_KEY$7,
+ SHOW: "show" + EVENT_KEY$7,
+ SHOWN: "shown" + EVENT_KEY$7,
+ INSERTED: "inserted" + EVENT_KEY$7,
+ CLICK: "click" + EVENT_KEY$7,
+ FOCUSIN: "focusin" + EVENT_KEY$7,
+ FOCUSOUT: "focusout" + EVENT_KEY$7,
+ MOUSEENTER: "mouseenter" + EVENT_KEY$7,
+ MOUSELEAVE: "mouseleave" + EVENT_KEY$7
+ /**
+ * ------------------------------------------------------------------------
+ * Class Definition
+ * ------------------------------------------------------------------------
+ */
+
+ };
+
+ var Popover =
+ /*#__PURE__*/
+ function (_Tooltip) {
+ _inheritsLoose(Popover, _Tooltip);
+
+ function Popover() {
+ return _Tooltip.apply(this, arguments) || this;
+ }
+
+ var _proto = Popover.prototype;
+
+ // Overrides
+ _proto.isWithContent = function isWithContent() {
+ return this.getTitle() || this._getContent();
+ };
+
+ _proto.addAttachmentClass = function addAttachmentClass(attachment) {
+ $(this.getTipElement()).addClass(CLASS_PREFIX$1 + "-" + attachment);
+ };
+
+ _proto.getTipElement = function getTipElement() {
+ this.tip = this.tip || $(this.config.template)[0];
+ return this.tip;
+ };
+
+ _proto.setContent = function setContent() {
+ var $tip = $(this.getTipElement()); // We use append for html objects to maintain js events
+
+ this.setElementContent($tip.find(Selector$7.TITLE), this.getTitle());
+
+ var content = this._getContent();
+
+ if (typeof content === 'function') {
+ content = content.call(this.element);
+ }
+
+ this.setElementContent($tip.find(Selector$7.CONTENT), content);
+ $tip.removeClass(ClassName$7.FADE + " " + ClassName$7.SHOW);
+ } // Private
+ ;
+
+ _proto._getContent = function _getContent() {
+ return this.element.getAttribute('data-content') || this.config.content;
+ };
+
+ _proto._cleanTipClass = function _cleanTipClass() {
+ var $tip = $(this.getTipElement());
+ var tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX$1);
+
+ if (tabClass !== null && tabClass.length > 0) {
+ $tip.removeClass(tabClass.join(''));
+ }
+ } // Static
+ ;
+
+ Popover._jQueryInterface = function _jQueryInterface(config) {
+ return this.each(function () {
+ var data = $(this).data(DATA_KEY$7);
+
+ var _config = typeof config === 'object' ? config : null;
+
+ if (!data && /dispose|hide/.test(config)) {
+ return;
+ }
+
+ if (!data) {
+ data = new Popover(this, _config);
+ $(this).data(DATA_KEY$7, data);
+ }
+
+ if (typeof config === 'string') {
+ if (typeof data[config] === 'undefined') {
+ throw new TypeError("No method named \"" + config + "\"");
+ }
+
+ data[config]();
+ }
+ });
+ };
+
+ _createClass(Popover, null, [{
+ key: "VERSION",
+ // Getters
+ get: function get() {
+ return VERSION$7;
+ }
+ }, {
+ key: "Default",
+ get: function get() {
+ return Default$5;
+ }
+ }, {
+ key: "NAME",
+ get: function get() {
+ return NAME$7;
+ }
+ }, {
+ key: "DATA_KEY",
+ get: function get() {
+ return DATA_KEY$7;
+ }
+ }, {
+ key: "Event",
+ get: function get() {
+ return Event$7;
+ }
+ }, {
+ key: "EVENT_KEY",
+ get: function get() {
+ return EVENT_KEY$7;
+ }
+ }, {
+ key: "DefaultType",
+ get: function get() {
+ return DefaultType$5;
+ }
+ }]);
+
+ return Popover;
+ }(Tooltip);
+ /**
+ * ------------------------------------------------------------------------
+ * jQuery
+ * ------------------------------------------------------------------------
+ */
+
+
+ $.fn[NAME$7] = Popover._jQueryInterface;
+ $.fn[NAME$7].Constructor = Popover;
+
+ $.fn[NAME$7].noConflict = function () {
+ $.fn[NAME$7] = JQUERY_NO_CONFLICT$7;
+ return Popover._jQueryInterface;
+ };
+
+ /**
+ * ------------------------------------------------------------------------
+ * Constants
+ * ------------------------------------------------------------------------
+ */
+
+ var NAME$8 = 'scrollspy';
+ var VERSION$8 = '4.3.1';
+ var DATA_KEY$8 = 'bs.scrollspy';
+ var EVENT_KEY$8 = "." + DATA_KEY$8;
+ var DATA_API_KEY$6 = '.data-api';
+ var JQUERY_NO_CONFLICT$8 = $.fn[NAME$8];
+ var Default$6 = {
+ offset: 10,
+ method: 'auto',
+ target: ''
+ };
+ var DefaultType$6 = {
+ offset: 'number',
+ method: 'string',
+ target: '(string|element)'
+ };
+ var Event$8 = {
+ ACTIVATE: "activate" + EVENT_KEY$8,
+ SCROLL: "scroll" + EVENT_KEY$8,
+ LOAD_DATA_API: "load" + EVENT_KEY$8 + DATA_API_KEY$6
+ };
+ var ClassName$8 = {
+ DROPDOWN_ITEM: 'dropdown-item',
+ DROPDOWN_MENU: 'dropdown-menu',
+ ACTIVE: 'active'
+ };
+ var Selector$8 = {
+ DATA_SPY: '[data-spy="scroll"]',
+ ACTIVE: '.active',
+ NAV_LIST_GROUP: '.nav, .list-group',
+ NAV_LINKS: '.nav-link',
+ NAV_ITEMS: '.nav-item',
+ LIST_ITEMS: '.list-group-item',
+ DROPDOWN: '.dropdown',
+ DROPDOWN_ITEMS: '.dropdown-item',
+ DROPDOWN_TOGGLE: '.dropdown-toggle'
+ };
+ var OffsetMethod = {
+ OFFSET: 'offset',
+ POSITION: 'position'
+ /**
+ * ------------------------------------------------------------------------
+ * Class Definition
+ * ------------------------------------------------------------------------
+ */
+
+ };
+
+ var ScrollSpy =
+ /*#__PURE__*/
+ function () {
+ function ScrollSpy(element, config) {
+ var _this = this;
+
+ this._element = element;
+ this._scrollElement = element.tagName === 'BODY' ? window : element;
+ this._config = this._getConfig(config);
+ this._selector = this._config.target + " " + Selector$8.NAV_LINKS + "," + (this._config.target + " " + Selector$8.LIST_ITEMS + ",") + (this._config.target + " " + Selector$8.DROPDOWN_ITEMS);
+ this._offsets = [];
+ this._targets = [];
+ this._activeTarget = null;
+ this._scrollHeight = 0;
+ $(this._scrollElement).on(Event$8.SCROLL, function (event) {
+ return _this._process(event);
+ });
+ this.refresh();
+
+ this._process();
+ } // Getters
+
+
+ var _proto = ScrollSpy.prototype;
+
+ // Public
+ _proto.refresh = function refresh() {
+ var _this2 = this;
+
+ var autoMethod = this._scrollElement === this._scrollElement.window ? OffsetMethod.OFFSET : OffsetMethod.POSITION;
+ var offsetMethod = this._config.method === 'auto' ? autoMethod : this._config.method;
+ var offsetBase = offsetMethod === OffsetMethod.POSITION ? this._getScrollTop() : 0;
+ this._offsets = [];
+ this._targets = [];
+ this._scrollHeight = this._getScrollHeight();
+ var targets = [].slice.call(document.querySelectorAll(this._selector));
+ targets.map(function (element) {
+ var target;
+ var targetSelector = Util.getSelectorFromElement(element);
+
+ if (targetSelector) {
+ target = document.querySelector(targetSelector);
+ }
+
+ if (target) {
+ var targetBCR = target.getBoundingClientRect();
+
+ if (targetBCR.width || targetBCR.height) {
+ // TODO (fat): remove sketch reliance on jQuery position/offset
+ return [$(target)[offsetMethod]().top + offsetBase, targetSelector];
+ }
+ }
+
+ return null;
+ }).filter(function (item) {
+ return item;
+ }).sort(function (a, b) {
+ return a[0] - b[0];
+ }).forEach(function (item) {
+ _this2._offsets.push(item[0]);
+
+ _this2._targets.push(item[1]);
+ });
+ };
+
+ _proto.dispose = function dispose() {
+ $.removeData(this._element, DATA_KEY$8);
+ $(this._scrollElement).off(EVENT_KEY$8);
+ this._element = null;
+ this._scrollElement = null;
+ this._config = null;
+ this._selector = null;
+ this._offsets = null;
+ this._targets = null;
+ this._activeTarget = null;
+ this._scrollHeight = null;
+ } // Private
+ ;
+
+ _proto._getConfig = function _getConfig(config) {
+ config = _objectSpread({}, Default$6, typeof config === 'object' && config ? config : {});
+
+ if (typeof config.target !== 'string') {
+ var id = $(config.target).attr('id');
+
+ if (!id) {
+ id = Util.getUID(NAME$8);
+ $(config.target).attr('id', id);
+ }
+
+ config.target = "#" + id;
+ }
+
+ Util.typeCheckConfig(NAME$8, config, DefaultType$6);
+ return config;
+ };
+
+ _proto._getScrollTop = function _getScrollTop() {
+ return this._scrollElement === window ? this._scrollElement.pageYOffset : this._scrollElement.scrollTop;
+ };
+
+ _proto._getScrollHeight = function _getScrollHeight() {
+ return this._scrollElement.scrollHeight || Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
+ };
+
+ _proto._getOffsetHeight = function _getOffsetHeight() {
+ return this._scrollElement === window ? window.innerHeight : this._scrollElement.getBoundingClientRect().height;
+ };
+
+ _proto._process = function _process() {
+ var scrollTop = this._getScrollTop() + this._config.offset;
+
+ var scrollHeight = this._getScrollHeight();
+
+ var maxScroll = this._config.offset + scrollHeight - this._getOffsetHeight();
+
+ if (this._scrollHeight !== scrollHeight) {
+ this.refresh();
+ }
+
+ if (scrollTop >= maxScroll) {
+ var target = this._targets[this._targets.length - 1];
+
+ if (this._activeTarget !== target) {
+ this._activate(target);
+ }
+
+ return;
+ }
+
+ if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) {
+ this._activeTarget = null;
+
+ this._clear();
+
+ return;
+ }
+
+ var offsetLength = this._offsets.length;
+
+ for (var i = offsetLength; i--;) {
+ var isActiveTarget = this._activeTarget !== this._targets[i] && scrollTop >= this._offsets[i] && (typeof this._offsets[i + 1] === 'undefined' || scrollTop < this._offsets[i + 1]);
+
+ if (isActiveTarget) {
+ this._activate(this._targets[i]);
+ }
+ }
+ };
+
+ _proto._activate = function _activate(target) {
+ this._activeTarget = target;
+
+ this._clear();
+
+ var queries = this._selector.split(',').map(function (selector) {
+ return selector + "[data-target=\"" + target + "\"]," + selector + "[href=\"" + target + "\"]";
+ });
+
+ var $link = $([].slice.call(document.querySelectorAll(queries.join(','))));
+
+ if ($link.hasClass(ClassName$8.DROPDOWN_ITEM)) {
+ $link.closest(Selector$8.DROPDOWN).find(Selector$8.DROPDOWN_TOGGLE).addClass(ClassName$8.ACTIVE);
+ $link.addClass(ClassName$8.ACTIVE);
+ } else {
+ // Set triggered link as active
+ $link.addClass(ClassName$8.ACTIVE); // Set triggered links parents as active
+ // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor
+
+ $link.parents(Selector$8.NAV_LIST_GROUP).prev(Selector$8.NAV_LINKS + ", " + Selector$8.LIST_ITEMS).addClass(ClassName$8.ACTIVE); // Handle special case when .nav-link is inside .nav-item
+
+ $link.parents(Selector$8.NAV_LIST_GROUP).prev(Selector$8.NAV_ITEMS).children(Selector$8.NAV_LINKS).addClass(ClassName$8.ACTIVE);
+ }
+
+ $(this._scrollElement).trigger(Event$8.ACTIVATE, {
+ relatedTarget: target
+ });
+ };
+
+ _proto._clear = function _clear() {
+ [].slice.call(document.querySelectorAll(this._selector)).filter(function (node) {
+ return node.classList.contains(ClassName$8.ACTIVE);
+ }).forEach(function (node) {
+ return node.classList.remove(ClassName$8.ACTIVE);
+ });
+ } // Static
+ ;
+
+ ScrollSpy._jQueryInterface = function _jQueryInterface(config) {
+ return this.each(function () {
+ var data = $(this).data(DATA_KEY$8);
+
+ var _config = typeof config === 'object' && config;
+
+ if (!data) {
+ data = new ScrollSpy(this, _config);
+ $(this).data(DATA_KEY$8, data);
+ }
+
+ if (typeof config === 'string') {
+ if (typeof data[config] === 'undefined') {
+ throw new TypeError("No method named \"" + config + "\"");
+ }
+
+ data[config]();
+ }
+ });
+ };
+
+ _createClass(ScrollSpy, null, [{
+ key: "VERSION",
+ get: function get() {
+ return VERSION$8;
+ }
+ }, {
+ key: "Default",
+ get: function get() {
+ return Default$6;
+ }
+ }]);
+
+ return ScrollSpy;
+ }();
+ /**
+ * ------------------------------------------------------------------------
+ * Data Api implementation
+ * ------------------------------------------------------------------------
+ */
+
+
+ $(window).on(Event$8.LOAD_DATA_API, function () {
+ var scrollSpys = [].slice.call(document.querySelectorAll(Selector$8.DATA_SPY));
+ var scrollSpysLength = scrollSpys.length;
+
+ for (var i = scrollSpysLength; i--;) {
+ var $spy = $(scrollSpys[i]);
+
+ ScrollSpy._jQueryInterface.call($spy, $spy.data());
+ }
+ });
+ /**
+ * ------------------------------------------------------------------------
+ * jQuery
+ * ------------------------------------------------------------------------
+ */
+
+ $.fn[NAME$8] = ScrollSpy._jQueryInterface;
+ $.fn[NAME$8].Constructor = ScrollSpy;
+
+ $.fn[NAME$8].noConflict = function () {
+ $.fn[NAME$8] = JQUERY_NO_CONFLICT$8;
+ return ScrollSpy._jQueryInterface;
+ };
+
+ /**
+ * ------------------------------------------------------------------------
+ * Constants
+ * ------------------------------------------------------------------------
+ */
+
+ var NAME$9 = 'tab';
+ var VERSION$9 = '4.3.1';
+ var DATA_KEY$9 = 'bs.tab';
+ var EVENT_KEY$9 = "." + DATA_KEY$9;
+ var DATA_API_KEY$7 = '.data-api';
+ var JQUERY_NO_CONFLICT$9 = $.fn[NAME$9];
+ var Event$9 = {
+ HIDE: "hide" + EVENT_KEY$9,
+ HIDDEN: "hidden" + EVENT_KEY$9,
+ SHOW: "show" + EVENT_KEY$9,
+ SHOWN: "shown" + EVENT_KEY$9,
+ CLICK_DATA_API: "click" + EVENT_KEY$9 + DATA_API_KEY$7
+ };
+ var ClassName$9 = {
+ DROPDOWN_MENU: 'dropdown-menu',
+ ACTIVE: 'active',
+ DISABLED: 'disabled',
+ FADE: 'fade',
+ SHOW: 'show'
+ };
+ var Selector$9 = {
+ DROPDOWN: '.dropdown',
+ NAV_LIST_GROUP: '.nav, .list-group',
+ ACTIVE: '.active',
+ ACTIVE_UL: '> li > .active',
+ DATA_TOGGLE: '[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]',
+ DROPDOWN_TOGGLE: '.dropdown-toggle',
+ DROPDOWN_ACTIVE_CHILD: '> .dropdown-menu .active'
+ /**
+ * ------------------------------------------------------------------------
+ * Class Definition
+ * ------------------------------------------------------------------------
+ */
+
+ };
+
+ var Tab =
+ /*#__PURE__*/
+ function () {
+ function Tab(element) {
+ this._element = element;
+ } // Getters
+
+
+ var _proto = Tab.prototype;
+
+ // Public
+ _proto.show = function show() {
+ var _this = this;
+
+ if (this._element.parentNode && this._element.parentNode.nodeType === Node.ELEMENT_NODE && $(this._element).hasClass(ClassName$9.ACTIVE) || $(this._element).hasClass(ClassName$9.DISABLED)) {
+ return;
+ }
+
+ var target;
+ var previous;
+ var listElement = $(this._element).closest(Selector$9.NAV_LIST_GROUP)[0];
+ var selector = Util.getSelectorFromElement(this._element);
+
+ if (listElement) {
+ var itemSelector = listElement.nodeName === 'UL' || listElement.nodeName === 'OL' ? Selector$9.ACTIVE_UL : Selector$9.ACTIVE;
+ previous = $.makeArray($(listElement).find(itemSelector));
+ previous = previous[previous.length - 1];
+ }
+
+ var hideEvent = $.Event(Event$9.HIDE, {
+ relatedTarget: this._element
+ });
+ var showEvent = $.Event(Event$9.SHOW, {
+ relatedTarget: previous
+ });
+
+ if (previous) {
+ $(previous).trigger(hideEvent);
+ }
+
+ $(this._element).trigger(showEvent);
+
+ if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) {
+ return;
+ }
+
+ if (selector) {
+ target = document.querySelector(selector);
+ }
+
+ this._activate(this._element, listElement);
+
+ var complete = function complete() {
+ var hiddenEvent = $.Event(Event$9.HIDDEN, {
+ relatedTarget: _this._element
+ });
+ var shownEvent = $.Event(Event$9.SHOWN, {
+ relatedTarget: previous
+ });
+ $(previous).trigger(hiddenEvent);
+ $(_this._element).trigger(shownEvent);
+ };
+
+ if (target) {
+ this._activate(target, target.parentNode, complete);
+ } else {
+ complete();
+ }
+ };
+
+ _proto.dispose = function dispose() {
+ $.removeData(this._element, DATA_KEY$9);
+ this._element = null;
+ } // Private
+ ;
+
+ _proto._activate = function _activate(element, container, callback) {
+ var _this2 = this;
+
+ var activeElements = container && (container.nodeName === 'UL' || container.nodeName === 'OL') ? $(container).find(Selector$9.ACTIVE_UL) : $(container).children(Selector$9.ACTIVE);
+ var active = activeElements[0];
+ var isTransitioning = callback && active && $(active).hasClass(ClassName$9.FADE);
+
+ var complete = function complete() {
+ return _this2._transitionComplete(element, active, callback);
+ };
+
+ if (active && isTransitioning) {
+ var transitionDuration = Util.getTransitionDurationFromElement(active);
+ $(active).removeClass(ClassName$9.SHOW).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);
+ } else {
+ complete();
+ }
+ };
+
+ _proto._transitionComplete = function _transitionComplete(element, active, callback) {
+ if (active) {
+ $(active).removeClass(ClassName$9.ACTIVE);
+ var dropdownChild = $(active.parentNode).find(Selector$9.DROPDOWN_ACTIVE_CHILD)[0];
+
+ if (dropdownChild) {
+ $(dropdownChild).removeClass(ClassName$9.ACTIVE);
+ }
+
+ if (active.getAttribute('role') === 'tab') {
+ active.setAttribute('aria-selected', false);
+ }
+ }
+
+ $(element).addClass(ClassName$9.ACTIVE);
+
+ if (element.getAttribute('role') === 'tab') {
+ element.setAttribute('aria-selected', true);
+ }
+
+ Util.reflow(element);
+
+ if (element.classList.contains(ClassName$9.FADE)) {
+ element.classList.add(ClassName$9.SHOW);
+ }
+
+ if (element.parentNode && $(element.parentNode).hasClass(ClassName$9.DROPDOWN_MENU)) {
+ var dropdownElement = $(element).closest(Selector$9.DROPDOWN)[0];
+
+ if (dropdownElement) {
+ var dropdownToggleList = [].slice.call(dropdownElement.querySelectorAll(Selector$9.DROPDOWN_TOGGLE));
+ $(dropdownToggleList).addClass(ClassName$9.ACTIVE);
+ }
+
+ element.setAttribute('aria-expanded', true);
+ }
+
+ if (callback) {
+ callback();
+ }
+ } // Static
+ ;
+
+ Tab._jQueryInterface = function _jQueryInterface(config) {
+ return this.each(function () {
+ var $this = $(this);
+ var data = $this.data(DATA_KEY$9);
+
+ if (!data) {
+ data = new Tab(this);
+ $this.data(DATA_KEY$9, data);
+ }
+
+ if (typeof config === 'string') {
+ if (typeof data[config] === 'undefined') {
+ throw new TypeError("No method named \"" + config + "\"");
+ }
+
+ data[config]();
+ }
+ });
+ };
+
+ _createClass(Tab, null, [{
+ key: "VERSION",
+ get: function get() {
+ return VERSION$9;
+ }
+ }]);
+
+ return Tab;
+ }();
+ /**
+ * ------------------------------------------------------------------------
+ * Data Api implementation
+ * ------------------------------------------------------------------------
+ */
+
+
+ $(document).on(Event$9.CLICK_DATA_API, Selector$9.DATA_TOGGLE, function (event) {
+ event.preventDefault();
+
+ Tab._jQueryInterface.call($(this), 'show');
+ });
+ /**
+ * ------------------------------------------------------------------------
+ * jQuery
+ * ------------------------------------------------------------------------
+ */
+
+ $.fn[NAME$9] = Tab._jQueryInterface;
+ $.fn[NAME$9].Constructor = Tab;
+
+ $.fn[NAME$9].noConflict = function () {
+ $.fn[NAME$9] = JQUERY_NO_CONFLICT$9;
+ return Tab._jQueryInterface;
+ };
+
+ /**
+ * ------------------------------------------------------------------------
+ * Constants
+ * ------------------------------------------------------------------------
+ */
+
+ var NAME$a = 'toast';
+ var VERSION$a = '4.3.1';
+ var DATA_KEY$a = 'bs.toast';
+ var EVENT_KEY$a = "." + DATA_KEY$a;
+ var JQUERY_NO_CONFLICT$a = $.fn[NAME$a];
+ var Event$a = {
+ CLICK_DISMISS: "click.dismiss" + EVENT_KEY$a,
+ HIDE: "hide" + EVENT_KEY$a,
+ HIDDEN: "hidden" + EVENT_KEY$a,
+ SHOW: "show" + EVENT_KEY$a,
+ SHOWN: "shown" + EVENT_KEY$a
+ };
+ var ClassName$a = {
+ FADE: 'fade',
+ HIDE: 'hide',
+ SHOW: 'show',
+ SHOWING: 'showing'
+ };
+ var DefaultType$7 = {
+ animation: 'boolean',
+ autohide: 'boolean',
+ delay: 'number'
+ };
+ var Default$7 = {
+ animation: true,
+ autohide: true,
+ delay: 500
+ };
+ var Selector$a = {
+ DATA_DISMISS: '[data-dismiss="toast"]'
+ /**
+ * ------------------------------------------------------------------------
+ * Class Definition
+ * ------------------------------------------------------------------------
+ */
+
+ };
+
+ var Toast =
+ /*#__PURE__*/
+ function () {
+ function Toast(element, config) {
+ this._element = element;
+ this._config = this._getConfig(config);
+ this._timeout = null;
+
+ this._setListeners();
+ } // Getters
+
+
+ var _proto = Toast.prototype;
+
+ // Public
+ _proto.show = function show() {
+ var _this = this;
+
+ $(this._element).trigger(Event$a.SHOW);
+
+ if (this._config.animation) {
+ this._element.classList.add(ClassName$a.FADE);
+ }
+
+ var complete = function complete() {
+ _this._element.classList.remove(ClassName$a.SHOWING);
+
+ _this._element.classList.add(ClassName$a.SHOW);
+
+ $(_this._element).trigger(Event$a.SHOWN);
+
+ if (_this._config.autohide) {
+ _this.hide();
+ }
+ };
+
+ this._element.classList.remove(ClassName$a.HIDE);
+
+ this._element.classList.add(ClassName$a.SHOWING);
+
+ if (this._config.animation) {
+ var transitionDuration = Util.getTransitionDurationFromElement(this._element);
+ $(this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);
+ } else {
+ complete();
+ }
+ };
+
+ _proto.hide = function hide(withoutTimeout) {
+ var _this2 = this;
+
+ if (!this._element.classList.contains(ClassName$a.SHOW)) {
+ return;
+ }
+
+ $(this._element).trigger(Event$a.HIDE);
+
+ if (withoutTimeout) {
+ this._close();
+ } else {
+ this._timeout = setTimeout(function () {
+ _this2._close();
+ }, this._config.delay);
+ }
+ };
+
+ _proto.dispose = function dispose() {
+ clearTimeout(this._timeout);
+ this._timeout = null;
+
+ if (this._element.classList.contains(ClassName$a.SHOW)) {
+ this._element.classList.remove(ClassName$a.SHOW);
+ }
+
+ $(this._element).off(Event$a.CLICK_DISMISS);
+ $.removeData(this._element, DATA_KEY$a);
+ this._element = null;
+ this._config = null;
+ } // Private
+ ;
+
+ _proto._getConfig = function _getConfig(config) {
+ config = _objectSpread({}, Default$7, $(this._element).data(), typeof config === 'object' && config ? config : {});
+ Util.typeCheckConfig(NAME$a, config, this.constructor.DefaultType);
+ return config;
+ };
+
+ _proto._setListeners = function _setListeners() {
+ var _this3 = this;
+
+ $(this._element).on(Event$a.CLICK_DISMISS, Selector$a.DATA_DISMISS, function () {
+ return _this3.hide(true);
+ });
+ };
+
+ _proto._close = function _close() {
+ var _this4 = this;
+
+ var complete = function complete() {
+ _this4._element.classList.add(ClassName$a.HIDE);
+
+ $(_this4._element).trigger(Event$a.HIDDEN);
+ };
+
+ this._element.classList.remove(ClassName$a.SHOW);
+
+ if (this._config.animation) {
+ var transitionDuration = Util.getTransitionDurationFromElement(this._element);
+ $(this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);
+ } else {
+ complete();
+ }
+ } // Static
+ ;
+
+ Toast._jQueryInterface = function _jQueryInterface(config) {
+ return this.each(function () {
+ var $element = $(this);
+ var data = $element.data(DATA_KEY$a);
+
+ var _config = typeof config === 'object' && config;
+
+ if (!data) {
+ data = new Toast(this, _config);
+ $element.data(DATA_KEY$a, data);
+ }
+
+ if (typeof config === 'string') {
+ if (typeof data[config] === 'undefined') {
+ throw new TypeError("No method named \"" + config + "\"");
+ }
+
+ data[config](this);
+ }
+ });
+ };
+
+ _createClass(Toast, null, [{
+ key: "VERSION",
+ get: function get() {
+ return VERSION$a;
+ }
+ }, {
+ key: "DefaultType",
+ get: function get() {
+ return DefaultType$7;
+ }
+ }, {
+ key: "Default",
+ get: function get() {
+ return Default$7;
+ }
+ }]);
+
+ return Toast;
+ }();
+ /**
+ * ------------------------------------------------------------------------
+ * jQuery
+ * ------------------------------------------------------------------------
+ */
+
+
+ $.fn[NAME$a] = Toast._jQueryInterface;
+ $.fn[NAME$a].Constructor = Toast;
+
+ $.fn[NAME$a].noConflict = function () {
+ $.fn[NAME$a] = JQUERY_NO_CONFLICT$a;
+ return Toast._jQueryInterface;
+ };
+
+ /**
+ * --------------------------------------------------------------------------
+ * Bootstrap (v4.3.1): index.js
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * --------------------------------------------------------------------------
+ */
+
+ (function () {
+ if (typeof $ === 'undefined') {
+ throw new TypeError('Bootstrap\'s JavaScript requires jQuery. jQuery must be included before Bootstrap\'s JavaScript.');
+ }
+
+ var version = $.fn.jquery.split(' ')[0].split('.');
+ var minMajor = 1;
+ var ltMajor = 2;
+ var minMinor = 9;
+ var minPatch = 1;
+ var maxMajor = 4;
+
+ if (version[0] < ltMajor && version[1] < minMinor || version[0] === minMajor && version[1] === minMinor && version[2] < minPatch || version[0] >= maxMajor) {
+ throw new Error('Bootstrap\'s JavaScript requires at least jQuery v1.9.1 but less than v4.0.0');
+ }
+ })();
+
+ exports.Util = Util;
+ exports.Alert = Alert;
+ exports.Button = Button;
+ exports.Carousel = Carousel;
+ exports.Collapse = Collapse;
+ exports.Dropdown = Dropdown;
+ exports.Modal = Modal;
+ exports.Popover = Popover;
+ exports.Scrollspy = ScrollSpy;
+ exports.Tab = Tab;
+ exports.Toast = Toast;
+ exports.Tooltip = Tooltip;
+
+ Object.defineProperty(exports, '__esModule', { value: true });
+
+}));
+//# sourceMappingURL=bootstrap.js.map
+"use strict";
+var Platform = {};
+
+(function () {
+
+ Platform.detectDevice = function () {
+ var body = document.body;
+ var ua = navigator.userAgent;
+ var checker = {
+ // OS
+ Windows: ua.match(/Windows/),
+ MacOS: ua.match(/Mac/),
+ Android: ua.match(/Android/),
+
+ // Browser
+ Msie: ua.match(/Trident/),
+ Edge: ua.match(/Edge/),
+ Chrome: ua.match(/Chrome/),
+ Firefox: ua.match(/Firefox/),
+ Safari: ua.match(/Safari/),
+
+ // Device
+ isApple: ua.match(/(iPhone|iPod|iPad)/),
+ iPhone: ua.match(/iPhone/),
+ iPad: ua.match(/iPad/),
+ iPod: ua.match(/iPod/),
+ };
+
+ if (checker.isApple) {
+ // Apple
+ body.classList.add('isApple');
+
+ if (checker.iPhone) {
+ // Apple iPhone
+ body.classList.add('iphone');
+ } else if (checker.iPad) {
+ // Apple iPad
+ body.classList.add('ipad');
+ } else if (checker.iPod) {
+ // Apple iPod
+ body.classList.add('ipod');
+ }
+
+ } else if (checker.Windows){
+ // Windows OS
+ body.classList.add('windowsOS');
+
+ if (checker.Edge){
+ // Edge Browser
+ body.classList.add('edge');
+ } else if (checker.Chrome){
+ // Chrome Browser
+ body.classList.add('chrome');
+ } else if(checker.Safari){
+ // Safari Browser
+ body.classList.add('safari');
+ } else if(checker.Firefox){
+ // Firefox Browser
+ body.classList.add('firefox');
+ } else if(checker.Msie){
+ // IE Browser
+ body.classList.add('msie');
+ }
+
+ } else if (checker.MacOS){
+ // Mac OS
+ body.classList.add('macOS');
+
+ if (checker.Chrome){
+ // Chrome Browser
+ body.classList.add('chrome');
+ } else if(checker.Safari){
+ // Safari Browser
+ body.classList.add('safari');
+ } else if(checker.Firefox){
+ // Firefox Browser
+ body.classList.add('firefox');
+ }
+
+ } else if (checker.Android){
+ // Android OS
+ body.classList.add('AndroidOS');
+ }
+
+ }
+
+ Platform.detectDevice();
+
+})($);
+
+"use strict";
+
+
+jQuery(document).ready(function() {
+ //removeIf(production)
+ console.log("document ready");
+ //endRemoveIf(production)
+});
+
+function copyCodeToClipboard(event, element) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const textInput = element.nextSibling.nextSibling;
+ textInput.select();
+
+ try {
+ if (document.execCommand('copy')) {
+ element.innerHTML = 'Copied';
+
+ setTimeout(function() {
+ element.innerHTML = 'Copy';
+ }, 3000);
+ }
+ } catch (err) {
+ alert('Please use CTRL/CMD + C to copy.');
+ console.log('Oops, unable to copy', err);
+ }
+
+ return false;
+}
+
+//# sourceMappingURL=main.js.map
--- /dev/null
+{"version":3,"sources":["jquery.js","popper.js","bootstrap.js","crossPlatform.js","main.js"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACr2UA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACnjFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACl1IA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"main.js","sourcesContent":["/*!\n * jQuery JavaScript Library v3.4.1\n * https://jquery.com/\n *\n * Includes Sizzle.js\n * https://sizzlejs.com/\n *\n * Copyright JS Foundation and other contributors\n * Released under the MIT license\n * https://jquery.org/license\n *\n * Date: 2019-05-01T21:04Z\n */\n( function( global, factory ) {\n\n\t\"use strict\";\n\n\tif ( typeof module === \"object\" && typeof module.exports === \"object\" ) {\n\n\t\t// For CommonJS and CommonJS-like environments where a proper `window`\n\t\t// is present, execute the factory and get jQuery.\n\t\t// For environments that do not have a `window` with a `document`\n\t\t// (such as Node.js), expose a factory as module.exports.\n\t\t// This accentuates the need for the creation of a real `window`.\n\t\t// e.g. var jQuery = require(\"jquery\")(window);\n\t\t// See ticket #14549 for more info.\n\t\tmodule.exports = global.document ?\n\t\t\tfactory( global, true ) :\n\t\t\tfunction( w ) {\n\t\t\t\tif ( !w.document ) {\n\t\t\t\t\tthrow new Error( \"jQuery requires a window with a document\" );\n\t\t\t\t}\n\t\t\t\treturn factory( w );\n\t\t\t};\n\t} else {\n\t\tfactory( global );\n\t}\n\n// Pass this if window is not defined yet\n} )( typeof window !== \"undefined\" ? window : this, function( window, noGlobal ) {\n\n// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1\n// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode\n// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common\n// enough that all such attempts are guarded in a try block.\n\"use strict\";\n\nvar arr = [];\n\nvar document = window.document;\n\nvar getProto = Object.getPrototypeOf;\n\nvar slice = arr.slice;\n\nvar concat = arr.concat;\n\nvar push = arr.push;\n\nvar indexOf = arr.indexOf;\n\nvar class2type = {};\n\nvar toString = class2type.toString;\n\nvar hasOwn = class2type.hasOwnProperty;\n\nvar fnToString = hasOwn.toString;\n\nvar ObjectFunctionString = fnToString.call( Object );\n\nvar support = {};\n\nvar isFunction = function isFunction( obj ) {\n\n // Support: Chrome <=57, Firefox <=52\n // In some browsers, typeof returns \"function\" for HTML <object> elements\n // (i.e., `typeof document.createElement( \"object\" ) === \"function\"`).\n // We don't want to classify *any* DOM node as a function.\n return typeof obj === \"function\" && typeof obj.nodeType !== \"number\";\n };\n\n\nvar isWindow = function isWindow( obj ) {\n\t\treturn obj != null && obj === obj.window;\n\t};\n\n\n\n\n\tvar preservedScriptAttributes = {\n\t\ttype: true,\n\t\tsrc: true,\n\t\tnonce: true,\n\t\tnoModule: true\n\t};\n\n\tfunction DOMEval( code, node, doc ) {\n\t\tdoc = doc || document;\n\n\t\tvar i, val,\n\t\t\tscript = doc.createElement( \"script\" );\n\n\t\tscript.text = code;\n\t\tif ( node ) {\n\t\t\tfor ( i in preservedScriptAttributes ) {\n\n\t\t\t\t// Support: Firefox 64+, Edge 18+\n\t\t\t\t// Some browsers don't support the \"nonce\" property on scripts.\n\t\t\t\t// On the other hand, just using `getAttribute` is not enough as\n\t\t\t\t// the `nonce` attribute is reset to an empty string whenever it\n\t\t\t\t// becomes browsing-context connected.\n\t\t\t\t// See https://github.com/whatwg/html/issues/2369\n\t\t\t\t// See https://html.spec.whatwg.org/#nonce-attributes\n\t\t\t\t// The `node.getAttribute` check was added for the sake of\n\t\t\t\t// `jQuery.globalEval` so that it can fake a nonce-containing node\n\t\t\t\t// via an object.\n\t\t\t\tval = node[ i ] || node.getAttribute && node.getAttribute( i );\n\t\t\t\tif ( val ) {\n\t\t\t\t\tscript.setAttribute( i, val );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tdoc.head.appendChild( script ).parentNode.removeChild( script );\n\t}\n\n\nfunction toType( obj ) {\n\tif ( obj == null ) {\n\t\treturn obj + \"\";\n\t}\n\n\t// Support: Android <=2.3 only (functionish RegExp)\n\treturn typeof obj === \"object\" || typeof obj === \"function\" ?\n\t\tclass2type[ toString.call( obj ) ] || \"object\" :\n\t\ttypeof obj;\n}\n/* global Symbol */\n// Defining this global in .eslintrc.json would create a danger of using the global\n// unguarded in another place, it seems safer to define global only for this module\n\n\n\nvar\n\tversion = \"3.4.1\",\n\n\t// Define a local copy of jQuery\n\tjQuery = function( selector, context ) {\n\n\t\t// The jQuery object is actually just the init constructor 'enhanced'\n\t\t// Need init if jQuery is called (just allow error to be thrown if not included)\n\t\treturn new jQuery.fn.init( selector, context );\n\t},\n\n\t// Support: Android <=4.0 only\n\t// Make sure we trim BOM and NBSP\n\trtrim = /^[\\s\\uFEFF\\xA0]+|[\\s\\uFEFF\\xA0]+$/g;\n\njQuery.fn = jQuery.prototype = {\n\n\t// The current version of jQuery being used\n\tjquery: version,\n\n\tconstructor: jQuery,\n\n\t// The default length of a jQuery object is 0\n\tlength: 0,\n\n\ttoArray: function() {\n\t\treturn slice.call( this );\n\t},\n\n\t// Get the Nth element in the matched element set OR\n\t// Get the whole matched element set as a clean array\n\tget: function( num ) {\n\n\t\t// Return all the elements in a clean array\n\t\tif ( num == null ) {\n\t\t\treturn slice.call( this );\n\t\t}\n\n\t\t// Return just the one element from the set\n\t\treturn num < 0 ? this[ num + this.length ] : this[ num ];\n\t},\n\n\t// Take an array of elements and push it onto the stack\n\t// (returning the new matched element set)\n\tpushStack: function( elems ) {\n\n\t\t// Build a new jQuery matched element set\n\t\tvar ret = jQuery.merge( this.constructor(), elems );\n\n\t\t// Add the old object onto the stack (as a reference)\n\t\tret.prevObject = this;\n\n\t\t// Return the newly-formed element set\n\t\treturn ret;\n\t},\n\n\t// Execute a callback for every element in the matched set.\n\teach: function( callback ) {\n\t\treturn jQuery.each( this, callback );\n\t},\n\n\tmap: function( callback ) {\n\t\treturn this.pushStack( jQuery.map( this, function( elem, i ) {\n\t\t\treturn callback.call( elem, i, elem );\n\t\t} ) );\n\t},\n\n\tslice: function() {\n\t\treturn this.pushStack( slice.apply( this, arguments ) );\n\t},\n\n\tfirst: function() {\n\t\treturn this.eq( 0 );\n\t},\n\n\tlast: function() {\n\t\treturn this.eq( -1 );\n\t},\n\n\teq: function( i ) {\n\t\tvar len = this.length,\n\t\t\tj = +i + ( i < 0 ? len : 0 );\n\t\treturn this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] );\n\t},\n\n\tend: function() {\n\t\treturn this.prevObject || this.constructor();\n\t},\n\n\t// For internal use only.\n\t// Behaves like an Array's method, not like a jQuery method.\n\tpush: push,\n\tsort: arr.sort,\n\tsplice: arr.splice\n};\n\njQuery.extend = jQuery.fn.extend = function() {\n\tvar options, name, src, copy, copyIsArray, clone,\n\t\ttarget = arguments[ 0 ] || {},\n\t\ti = 1,\n\t\tlength = arguments.length,\n\t\tdeep = false;\n\n\t// Handle a deep copy situation\n\tif ( typeof target === \"boolean\" ) {\n\t\tdeep = target;\n\n\t\t// Skip the boolean and the target\n\t\ttarget = arguments[ i ] || {};\n\t\ti++;\n\t}\n\n\t// Handle case when target is a string or something (possible in deep copy)\n\tif ( typeof target !== \"object\" && !isFunction( target ) ) {\n\t\ttarget = {};\n\t}\n\n\t// Extend jQuery itself if only one argument is passed\n\tif ( i === length ) {\n\t\ttarget = this;\n\t\ti--;\n\t}\n\n\tfor ( ; i < length; i++ ) {\n\n\t\t// Only deal with non-null/undefined values\n\t\tif ( ( options = arguments[ i ] ) != null ) {\n\n\t\t\t// Extend the base object\n\t\t\tfor ( name in options ) {\n\t\t\t\tcopy = options[ name ];\n\n\t\t\t\t// Prevent Object.prototype pollution\n\t\t\t\t// Prevent never-ending loop\n\t\t\t\tif ( name === \"__proto__\" || target === copy ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Recurse if we're merging plain objects or arrays\n\t\t\t\tif ( deep && copy && ( jQuery.isPlainObject( copy ) ||\n\t\t\t\t\t( copyIsArray = Array.isArray( copy ) ) ) ) {\n\t\t\t\t\tsrc = target[ name ];\n\n\t\t\t\t\t// Ensure proper type for the source value\n\t\t\t\t\tif ( copyIsArray && !Array.isArray( src ) ) {\n\t\t\t\t\t\tclone = [];\n\t\t\t\t\t} else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) {\n\t\t\t\t\t\tclone = {};\n\t\t\t\t\t} else {\n\t\t\t\t\t\tclone = src;\n\t\t\t\t\t}\n\t\t\t\t\tcopyIsArray = false;\n\n\t\t\t\t\t// Never move original objects, clone them\n\t\t\t\t\ttarget[ name ] = jQuery.extend( deep, clone, copy );\n\n\t\t\t\t// Don't bring in undefined values\n\t\t\t\t} else if ( copy !== undefined ) {\n\t\t\t\t\ttarget[ name ] = copy;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Return the modified object\n\treturn target;\n};\n\njQuery.extend( {\n\n\t// Unique for each copy of jQuery on the page\n\texpando: \"jQuery\" + ( version + Math.random() ).replace( /\\D/g, \"\" ),\n\n\t// Assume jQuery is ready without the ready module\n\tisReady: true,\n\n\terror: function( msg ) {\n\t\tthrow new Error( msg );\n\t},\n\n\tnoop: function() {},\n\n\tisPlainObject: function( obj ) {\n\t\tvar proto, Ctor;\n\n\t\t// Detect obvious negatives\n\t\t// Use toString instead of jQuery.type to catch host objects\n\t\tif ( !obj || toString.call( obj ) !== \"[object Object]\" ) {\n\t\t\treturn false;\n\t\t}\n\n\t\tproto = getProto( obj );\n\n\t\t// Objects with no prototype (e.g., `Object.create( null )`) are plain\n\t\tif ( !proto ) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// Objects with prototype are plain iff they were constructed by a global Object function\n\t\tCtor = hasOwn.call( proto, \"constructor\" ) && proto.constructor;\n\t\treturn typeof Ctor === \"function\" && fnToString.call( Ctor ) === ObjectFunctionString;\n\t},\n\n\tisEmptyObject: function( obj ) {\n\t\tvar name;\n\n\t\tfor ( name in obj ) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t},\n\n\t// Evaluates a script in a global context\n\tglobalEval: function( code, options ) {\n\t\tDOMEval( code, { nonce: options && options.nonce } );\n\t},\n\n\teach: function( obj, callback ) {\n\t\tvar length, i = 0;\n\n\t\tif ( isArrayLike( obj ) ) {\n\t\t\tlength = obj.length;\n\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\tif ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfor ( i in obj ) {\n\t\t\t\tif ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn obj;\n\t},\n\n\t// Support: Android <=4.0 only\n\ttrim: function( text ) {\n\t\treturn text == null ?\n\t\t\t\"\" :\n\t\t\t( text + \"\" ).replace( rtrim, \"\" );\n\t},\n\n\t// results is for internal usage only\n\tmakeArray: function( arr, results ) {\n\t\tvar ret = results || [];\n\n\t\tif ( arr != null ) {\n\t\t\tif ( isArrayLike( Object( arr ) ) ) {\n\t\t\t\tjQuery.merge( ret,\n\t\t\t\t\ttypeof arr === \"string\" ?\n\t\t\t\t\t[ arr ] : arr\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tpush.call( ret, arr );\n\t\t\t}\n\t\t}\n\n\t\treturn ret;\n\t},\n\n\tinArray: function( elem, arr, i ) {\n\t\treturn arr == null ? -1 : indexOf.call( arr, elem, i );\n\t},\n\n\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t// push.apply(_, arraylike) throws on ancient WebKit\n\tmerge: function( first, second ) {\n\t\tvar len = +second.length,\n\t\t\tj = 0,\n\t\t\ti = first.length;\n\n\t\tfor ( ; j < len; j++ ) {\n\t\t\tfirst[ i++ ] = second[ j ];\n\t\t}\n\n\t\tfirst.length = i;\n\n\t\treturn first;\n\t},\n\n\tgrep: function( elems, callback, invert ) {\n\t\tvar callbackInverse,\n\t\t\tmatches = [],\n\t\t\ti = 0,\n\t\t\tlength = elems.length,\n\t\t\tcallbackExpect = !invert;\n\n\t\t// Go through the array, only saving the items\n\t\t// that pass the validator function\n\t\tfor ( ; i < length; i++ ) {\n\t\t\tcallbackInverse = !callback( elems[ i ], i );\n\t\t\tif ( callbackInverse !== callbackExpect ) {\n\t\t\t\tmatches.push( elems[ i ] );\n\t\t\t}\n\t\t}\n\n\t\treturn matches;\n\t},\n\n\t// arg is for internal usage only\n\tmap: function( elems, callback, arg ) {\n\t\tvar length, value,\n\t\t\ti = 0,\n\t\t\tret = [];\n\n\t\t// Go through the array, translating each of the items to their new values\n\t\tif ( isArrayLike( elems ) ) {\n\t\t\tlength = elems.length;\n\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\tvalue = callback( elems[ i ], i, arg );\n\n\t\t\t\tif ( value != null ) {\n\t\t\t\t\tret.push( value );\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Go through every key on the object,\n\t\t} else {\n\t\t\tfor ( i in elems ) {\n\t\t\t\tvalue = callback( elems[ i ], i, arg );\n\n\t\t\t\tif ( value != null ) {\n\t\t\t\t\tret.push( value );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Flatten any nested arrays\n\t\treturn concat.apply( [], ret );\n\t},\n\n\t// A global GUID counter for objects\n\tguid: 1,\n\n\t// jQuery.support is not used in Core but other projects attach their\n\t// properties to it so it needs to exist.\n\tsupport: support\n} );\n\nif ( typeof Symbol === \"function\" ) {\n\tjQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ];\n}\n\n// Populate the class2type map\njQuery.each( \"Boolean Number String Function Array Date RegExp Object Error Symbol\".split( \" \" ),\nfunction( i, name ) {\n\tclass2type[ \"[object \" + name + \"]\" ] = name.toLowerCase();\n} );\n\nfunction isArrayLike( obj ) {\n\n\t// Support: real iOS 8.2 only (not reproducible in simulator)\n\t// `in` check used to prevent JIT error (gh-2145)\n\t// hasOwn isn't used here due to false negatives\n\t// regarding Nodelist length in IE\n\tvar length = !!obj && \"length\" in obj && obj.length,\n\t\ttype = toType( obj );\n\n\tif ( isFunction( obj ) || isWindow( obj ) ) {\n\t\treturn false;\n\t}\n\n\treturn type === \"array\" || length === 0 ||\n\t\ttypeof length === \"number\" && length > 0 && ( length - 1 ) in obj;\n}\nvar Sizzle =\n/*!\n * Sizzle CSS Selector Engine v2.3.4\n * https://sizzlejs.com/\n *\n * Copyright JS Foundation and other contributors\n * Released under the MIT license\n * https://js.foundation/\n *\n * Date: 2019-04-08\n */\n(function( window ) {\n\nvar i,\n\tsupport,\n\tExpr,\n\tgetText,\n\tisXML,\n\ttokenize,\n\tcompile,\n\tselect,\n\toutermostContext,\n\tsortInput,\n\thasDuplicate,\n\n\t// Local document vars\n\tsetDocument,\n\tdocument,\n\tdocElem,\n\tdocumentIsHTML,\n\trbuggyQSA,\n\trbuggyMatches,\n\tmatches,\n\tcontains,\n\n\t// Instance-specific data\n\texpando = \"sizzle\" + 1 * new Date(),\n\tpreferredDoc = window.document,\n\tdirruns = 0,\n\tdone = 0,\n\tclassCache = createCache(),\n\ttokenCache = createCache(),\n\tcompilerCache = createCache(),\n\tnonnativeSelectorCache = createCache(),\n\tsortOrder = function( a, b ) {\n\t\tif ( a === b ) {\n\t\t\thasDuplicate = true;\n\t\t}\n\t\treturn 0;\n\t},\n\n\t// Instance methods\n\thasOwn = ({}).hasOwnProperty,\n\tarr = [],\n\tpop = arr.pop,\n\tpush_native = arr.push,\n\tpush = arr.push,\n\tslice = arr.slice,\n\t// Use a stripped-down indexOf as it's faster than native\n\t// https://jsperf.com/thor-indexof-vs-for/5\n\tindexOf = function( list, elem ) {\n\t\tvar i = 0,\n\t\t\tlen = list.length;\n\t\tfor ( ; i < len; i++ ) {\n\t\t\tif ( list[i] === elem ) {\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\t\treturn -1;\n\t},\n\n\tbooleans = \"checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped\",\n\n\t// Regular expressions\n\n\t// http://www.w3.org/TR/css3-selectors/#whitespace\n\twhitespace = \"[\\\\x20\\\\t\\\\r\\\\n\\\\f]\",\n\n\t// http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier\n\tidentifier = \"(?:\\\\\\\\.|[\\\\w-]|[^\\0-\\\\xa0])+\",\n\n\t// Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors\n\tattributes = \"\\\\[\" + whitespace + \"*(\" + identifier + \")(?:\" + whitespace +\n\t\t// Operator (capture 2)\n\t\t\"*([*^$|!~]?=)\" + whitespace +\n\t\t// \"Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]\"\n\t\t\"*(?:'((?:\\\\\\\\.|[^\\\\\\\\'])*)'|\\\"((?:\\\\\\\\.|[^\\\\\\\\\\\"])*)\\\"|(\" + identifier + \"))|)\" + whitespace +\n\t\t\"*\\\\]\",\n\n\tpseudos = \":(\" + identifier + \")(?:\\\\((\" +\n\t\t// To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:\n\t\t// 1. quoted (capture 3; capture 4 or capture 5)\n\t\t\"('((?:\\\\\\\\.|[^\\\\\\\\'])*)'|\\\"((?:\\\\\\\\.|[^\\\\\\\\\\\"])*)\\\")|\" +\n\t\t// 2. simple (capture 6)\n\t\t\"((?:\\\\\\\\.|[^\\\\\\\\()[\\\\]]|\" + attributes + \")*)|\" +\n\t\t// 3. anything else (capture 2)\n\t\t\".*\" +\n\t\t\")\\\\)|)\",\n\n\t// Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter\n\trwhitespace = new RegExp( whitespace + \"+\", \"g\" ),\n\trtrim = new RegExp( \"^\" + whitespace + \"+|((?:^|[^\\\\\\\\])(?:\\\\\\\\.)*)\" + whitespace + \"+$\", \"g\" ),\n\n\trcomma = new RegExp( \"^\" + whitespace + \"*,\" + whitespace + \"*\" ),\n\trcombinators = new RegExp( \"^\" + whitespace + \"*([>+~]|\" + whitespace + \")\" + whitespace + \"*\" ),\n\trdescend = new RegExp( whitespace + \"|>\" ),\n\n\trpseudo = new RegExp( pseudos ),\n\tridentifier = new RegExp( \"^\" + identifier + \"$\" ),\n\n\tmatchExpr = {\n\t\t\"ID\": new RegExp( \"^#(\" + identifier + \")\" ),\n\t\t\"CLASS\": new RegExp( \"^\\\\.(\" + identifier + \")\" ),\n\t\t\"TAG\": new RegExp( \"^(\" + identifier + \"|[*])\" ),\n\t\t\"ATTR\": new RegExp( \"^\" + attributes ),\n\t\t\"PSEUDO\": new RegExp( \"^\" + pseudos ),\n\t\t\"CHILD\": new RegExp( \"^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\\\(\" + whitespace +\n\t\t\t\"*(even|odd|(([+-]|)(\\\\d*)n|)\" + whitespace + \"*(?:([+-]|)\" + whitespace +\n\t\t\t\"*(\\\\d+)|))\" + whitespace + \"*\\\\)|)\", \"i\" ),\n\t\t\"bool\": new RegExp( \"^(?:\" + booleans + \")$\", \"i\" ),\n\t\t// For use in libraries implementing .is()\n\t\t// We use this for POS matching in `select`\n\t\t\"needsContext\": new RegExp( \"^\" + whitespace + \"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\\\(\" +\n\t\t\twhitespace + \"*((?:-\\\\d)?\\\\d*)\" + whitespace + \"*\\\\)|)(?=[^-]|$)\", \"i\" )\n\t},\n\n\trhtml = /HTML$/i,\n\trinputs = /^(?:input|select|textarea|button)$/i,\n\trheader = /^h\\d$/i,\n\n\trnative = /^[^{]+\\{\\s*\\[native \\w/,\n\n\t// Easily-parseable/retrievable ID or TAG or CLASS selectors\n\trquickExpr = /^(?:#([\\w-]+)|(\\w+)|\\.([\\w-]+))$/,\n\n\trsibling = /[+~]/,\n\n\t// CSS escapes\n\t// http://www.w3.org/TR/CSS21/syndata.html#escaped-characters\n\trunescape = new RegExp( \"\\\\\\\\([\\\\da-f]{1,6}\" + whitespace + \"?|(\" + whitespace + \")|.)\", \"ig\" ),\n\tfunescape = function( _, escaped, escapedWhitespace ) {\n\t\tvar high = \"0x\" + escaped - 0x10000;\n\t\t// NaN means non-codepoint\n\t\t// Support: Firefox<24\n\t\t// Workaround erroneous numeric interpretation of +\"0x\"\n\t\treturn high !== high || escapedWhitespace ?\n\t\t\tescaped :\n\t\t\thigh < 0 ?\n\t\t\t\t// BMP codepoint\n\t\t\t\tString.fromCharCode( high + 0x10000 ) :\n\t\t\t\t// Supplemental Plane codepoint (surrogate pair)\n\t\t\t\tString.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );\n\t},\n\n\t// CSS string/identifier serialization\n\t// https://drafts.csswg.org/cssom/#common-serializing-idioms\n\trcssescape = /([\\0-\\x1f\\x7f]|^-?\\d)|^-$|[^\\0-\\x1f\\x7f-\\uFFFF\\w-]/g,\n\tfcssescape = function( ch, asCodePoint ) {\n\t\tif ( asCodePoint ) {\n\n\t\t\t// U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER\n\t\t\tif ( ch === \"\\0\" ) {\n\t\t\t\treturn \"\\uFFFD\";\n\t\t\t}\n\n\t\t\t// Control characters and (dependent upon position) numbers get escaped as code points\n\t\t\treturn ch.slice( 0, -1 ) + \"\\\\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + \" \";\n\t\t}\n\n\t\t// Other potentially-special ASCII characters get backslash-escaped\n\t\treturn \"\\\\\" + ch;\n\t},\n\n\t// Used for iframes\n\t// See setDocument()\n\t// Removing the function wrapper causes a \"Permission Denied\"\n\t// error in IE\n\tunloadHandler = function() {\n\t\tsetDocument();\n\t},\n\n\tinDisabledFieldset = addCombinator(\n\t\tfunction( elem ) {\n\t\t\treturn elem.disabled === true && elem.nodeName.toLowerCase() === \"fieldset\";\n\t\t},\n\t\t{ dir: \"parentNode\", next: \"legend\" }\n\t);\n\n// Optimize for push.apply( _, NodeList )\ntry {\n\tpush.apply(\n\t\t(arr = slice.call( preferredDoc.childNodes )),\n\t\tpreferredDoc.childNodes\n\t);\n\t// Support: Android<4.0\n\t// Detect silently failing push.apply\n\tarr[ preferredDoc.childNodes.length ].nodeType;\n} catch ( e ) {\n\tpush = { apply: arr.length ?\n\n\t\t// Leverage slice if possible\n\t\tfunction( target, els ) {\n\t\t\tpush_native.apply( target, slice.call(els) );\n\t\t} :\n\n\t\t// Support: IE<9\n\t\t// Otherwise append directly\n\t\tfunction( target, els ) {\n\t\t\tvar j = target.length,\n\t\t\t\ti = 0;\n\t\t\t// Can't trust NodeList.length\n\t\t\twhile ( (target[j++] = els[i++]) ) {}\n\t\t\ttarget.length = j - 1;\n\t\t}\n\t};\n}\n\nfunction Sizzle( selector, context, results, seed ) {\n\tvar m, i, elem, nid, match, groups, newSelector,\n\t\tnewContext = context && context.ownerDocument,\n\n\t\t// nodeType defaults to 9, since context defaults to document\n\t\tnodeType = context ? context.nodeType : 9;\n\n\tresults = results || [];\n\n\t// Return early from calls with invalid selector or context\n\tif ( typeof selector !== \"string\" || !selector ||\n\t\tnodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) {\n\n\t\treturn results;\n\t}\n\n\t// Try to shortcut find operations (as opposed to filters) in HTML documents\n\tif ( !seed ) {\n\n\t\tif ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {\n\t\t\tsetDocument( context );\n\t\t}\n\t\tcontext = context || document;\n\n\t\tif ( documentIsHTML ) {\n\n\t\t\t// If the selector is sufficiently simple, try using a \"get*By*\" DOM method\n\t\t\t// (excepting DocumentFragment context, where the methods don't exist)\n\t\t\tif ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) {\n\n\t\t\t\t// ID selector\n\t\t\t\tif ( (m = match[1]) ) {\n\n\t\t\t\t\t// Document context\n\t\t\t\t\tif ( nodeType === 9 ) {\n\t\t\t\t\t\tif ( (elem = context.getElementById( m )) ) {\n\n\t\t\t\t\t\t\t// Support: IE, Opera, Webkit\n\t\t\t\t\t\t\t// TODO: identify versions\n\t\t\t\t\t\t\t// getElementById can match elements by name instead of ID\n\t\t\t\t\t\t\tif ( elem.id === m ) {\n\t\t\t\t\t\t\t\tresults.push( elem );\n\t\t\t\t\t\t\t\treturn results;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treturn results;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t// Element context\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t// Support: IE, Opera, Webkit\n\t\t\t\t\t\t// TODO: identify versions\n\t\t\t\t\t\t// getElementById can match elements by name instead of ID\n\t\t\t\t\t\tif ( newContext && (elem = newContext.getElementById( m )) &&\n\t\t\t\t\t\t\tcontains( context, elem ) &&\n\t\t\t\t\t\t\telem.id === m ) {\n\n\t\t\t\t\t\t\tresults.push( elem );\n\t\t\t\t\t\t\treturn results;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t// Type selector\n\t\t\t\t} else if ( match[2] ) {\n\t\t\t\t\tpush.apply( results, context.getElementsByTagName( selector ) );\n\t\t\t\t\treturn results;\n\n\t\t\t\t// Class selector\n\t\t\t\t} else if ( (m = match[3]) && support.getElementsByClassName &&\n\t\t\t\t\tcontext.getElementsByClassName ) {\n\n\t\t\t\t\tpush.apply( results, context.getElementsByClassName( m ) );\n\t\t\t\t\treturn results;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Take advantage of querySelectorAll\n\t\t\tif ( support.qsa &&\n\t\t\t\t!nonnativeSelectorCache[ selector + \" \" ] &&\n\t\t\t\t(!rbuggyQSA || !rbuggyQSA.test( selector )) &&\n\n\t\t\t\t// Support: IE 8 only\n\t\t\t\t// Exclude object elements\n\t\t\t\t(nodeType !== 1 || context.nodeName.toLowerCase() !== \"object\") ) {\n\n\t\t\t\tnewSelector = selector;\n\t\t\t\tnewContext = context;\n\n\t\t\t\t// qSA considers elements outside a scoping root when evaluating child or\n\t\t\t\t// descendant combinators, which is not what we want.\n\t\t\t\t// In such cases, we work around the behavior by prefixing every selector in the\n\t\t\t\t// list with an ID selector referencing the scope context.\n\t\t\t\t// Thanks to Andrew Dupont for this technique.\n\t\t\t\tif ( nodeType === 1 && rdescend.test( selector ) ) {\n\n\t\t\t\t\t// Capture the context ID, setting it first if necessary\n\t\t\t\t\tif ( (nid = context.getAttribute( \"id\" )) ) {\n\t\t\t\t\t\tnid = nid.replace( rcssescape, fcssescape );\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcontext.setAttribute( \"id\", (nid = expando) );\n\t\t\t\t\t}\n\n\t\t\t\t\t// Prefix every selector in the list\n\t\t\t\t\tgroups = tokenize( selector );\n\t\t\t\t\ti = groups.length;\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tgroups[i] = \"#\" + nid + \" \" + toSelector( groups[i] );\n\t\t\t\t\t}\n\t\t\t\t\tnewSelector = groups.join( \",\" );\n\n\t\t\t\t\t// Expand context for sibling selectors\n\t\t\t\t\tnewContext = rsibling.test( selector ) && testContext( context.parentNode ) ||\n\t\t\t\t\t\tcontext;\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tpush.apply( results,\n\t\t\t\t\t\tnewContext.querySelectorAll( newSelector )\n\t\t\t\t\t);\n\t\t\t\t\treturn results;\n\t\t\t\t} catch ( qsaError ) {\n\t\t\t\t\tnonnativeSelectorCache( selector, true );\n\t\t\t\t} finally {\n\t\t\t\t\tif ( nid === expando ) {\n\t\t\t\t\t\tcontext.removeAttribute( \"id\" );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// All others\n\treturn select( selector.replace( rtrim, \"$1\" ), context, results, seed );\n}\n\n/**\n * Create key-value caches of limited size\n * @returns {function(string, object)} Returns the Object data after storing it on itself with\n *\tproperty name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)\n *\tdeleting the oldest entry\n */\nfunction createCache() {\n\tvar keys = [];\n\n\tfunction cache( key, value ) {\n\t\t// Use (key + \" \") to avoid collision with native prototype properties (see Issue #157)\n\t\tif ( keys.push( key + \" \" ) > Expr.cacheLength ) {\n\t\t\t// Only keep the most recent entries\n\t\t\tdelete cache[ keys.shift() ];\n\t\t}\n\t\treturn (cache[ key + \" \" ] = value);\n\t}\n\treturn cache;\n}\n\n/**\n * Mark a function for special use by Sizzle\n * @param {Function} fn The function to mark\n */\nfunction markFunction( fn ) {\n\tfn[ expando ] = true;\n\treturn fn;\n}\n\n/**\n * Support testing using an element\n * @param {Function} fn Passed the created element and returns a boolean result\n */\nfunction assert( fn ) {\n\tvar el = document.createElement(\"fieldset\");\n\n\ttry {\n\t\treturn !!fn( el );\n\t} catch (e) {\n\t\treturn false;\n\t} finally {\n\t\t// Remove from its parent by default\n\t\tif ( el.parentNode ) {\n\t\t\tel.parentNode.removeChild( el );\n\t\t}\n\t\t// release memory in IE\n\t\tel = null;\n\t}\n}\n\n/**\n * Adds the same handler for all of the specified attrs\n * @param {String} attrs Pipe-separated list of attributes\n * @param {Function} handler The method that will be applied\n */\nfunction addHandle( attrs, handler ) {\n\tvar arr = attrs.split(\"|\"),\n\t\ti = arr.length;\n\n\twhile ( i-- ) {\n\t\tExpr.attrHandle[ arr[i] ] = handler;\n\t}\n}\n\n/**\n * Checks document order of two siblings\n * @param {Element} a\n * @param {Element} b\n * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b\n */\nfunction siblingCheck( a, b ) {\n\tvar cur = b && a,\n\t\tdiff = cur && a.nodeType === 1 && b.nodeType === 1 &&\n\t\t\ta.sourceIndex - b.sourceIndex;\n\n\t// Use IE sourceIndex if available on both nodes\n\tif ( diff ) {\n\t\treturn diff;\n\t}\n\n\t// Check if b follows a\n\tif ( cur ) {\n\t\twhile ( (cur = cur.nextSibling) ) {\n\t\t\tif ( cur === b ) {\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn a ? 1 : -1;\n}\n\n/**\n * Returns a function to use in pseudos for input types\n * @param {String} type\n */\nfunction createInputPseudo( type ) {\n\treturn function( elem ) {\n\t\tvar name = elem.nodeName.toLowerCase();\n\t\treturn name === \"input\" && elem.type === type;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for buttons\n * @param {String} type\n */\nfunction createButtonPseudo( type ) {\n\treturn function( elem ) {\n\t\tvar name = elem.nodeName.toLowerCase();\n\t\treturn (name === \"input\" || name === \"button\") && elem.type === type;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for :enabled/:disabled\n * @param {Boolean} disabled true for :disabled; false for :enabled\n */\nfunction createDisabledPseudo( disabled ) {\n\n\t// Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable\n\treturn function( elem ) {\n\n\t\t// Only certain elements can match :enabled or :disabled\n\t\t// https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled\n\t\t// https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled\n\t\tif ( \"form\" in elem ) {\n\n\t\t\t// Check for inherited disabledness on relevant non-disabled elements:\n\t\t\t// * listed form-associated elements in a disabled fieldset\n\t\t\t// https://html.spec.whatwg.org/multipage/forms.html#category-listed\n\t\t\t// https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled\n\t\t\t// * option elements in a disabled optgroup\n\t\t\t// https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled\n\t\t\t// All such elements have a \"form\" property.\n\t\t\tif ( elem.parentNode && elem.disabled === false ) {\n\n\t\t\t\t// Option elements defer to a parent optgroup if present\n\t\t\t\tif ( \"label\" in elem ) {\n\t\t\t\t\tif ( \"label\" in elem.parentNode ) {\n\t\t\t\t\t\treturn elem.parentNode.disabled === disabled;\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn elem.disabled === disabled;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Support: IE 6 - 11\n\t\t\t\t// Use the isDisabled shortcut property to check for disabled fieldset ancestors\n\t\t\t\treturn elem.isDisabled === disabled ||\n\n\t\t\t\t\t// Where there is no isDisabled, check manually\n\t\t\t\t\t/* jshint -W018 */\n\t\t\t\t\telem.isDisabled !== !disabled &&\n\t\t\t\t\t\tinDisabledFieldset( elem ) === disabled;\n\t\t\t}\n\n\t\t\treturn elem.disabled === disabled;\n\n\t\t// Try to winnow out elements that can't be disabled before trusting the disabled property.\n\t\t// Some victims get caught in our net (label, legend, menu, track), but it shouldn't\n\t\t// even exist on them, let alone have a boolean value.\n\t\t} else if ( \"label\" in elem ) {\n\t\t\treturn elem.disabled === disabled;\n\t\t}\n\n\t\t// Remaining elements are neither :enabled nor :disabled\n\t\treturn false;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for positionals\n * @param {Function} fn\n */\nfunction createPositionalPseudo( fn ) {\n\treturn markFunction(function( argument ) {\n\t\targument = +argument;\n\t\treturn markFunction(function( seed, matches ) {\n\t\t\tvar j,\n\t\t\t\tmatchIndexes = fn( [], seed.length, argument ),\n\t\t\t\ti = matchIndexes.length;\n\n\t\t\t// Match elements found at the specified indexes\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( seed[ (j = matchIndexes[i]) ] ) {\n\t\t\t\t\tseed[j] = !(matches[j] = seed[j]);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t});\n}\n\n/**\n * Checks a node for validity as a Sizzle context\n * @param {Element|Object=} context\n * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value\n */\nfunction testContext( context ) {\n\treturn context && typeof context.getElementsByTagName !== \"undefined\" && context;\n}\n\n// Expose support vars for convenience\nsupport = Sizzle.support = {};\n\n/**\n * Detects XML nodes\n * @param {Element|Object} elem An element or a document\n * @returns {Boolean} True iff elem is a non-HTML XML node\n */\nisXML = Sizzle.isXML = function( elem ) {\n\tvar namespace = elem.namespaceURI,\n\t\tdocElem = (elem.ownerDocument || elem).documentElement;\n\n\t// Support: IE <=8\n\t// Assume HTML when documentElement doesn't yet exist, such as inside loading iframes\n\t// https://bugs.jquery.com/ticket/4833\n\treturn !rhtml.test( namespace || docElem && docElem.nodeName || \"HTML\" );\n};\n\n/**\n * Sets document-related variables once based on the current document\n * @param {Element|Object} [doc] An element or document object to use to set the document\n * @returns {Object} Returns the current document\n */\nsetDocument = Sizzle.setDocument = function( node ) {\n\tvar hasCompare, subWindow,\n\t\tdoc = node ? node.ownerDocument || node : preferredDoc;\n\n\t// Return early if doc is invalid or already selected\n\tif ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {\n\t\treturn document;\n\t}\n\n\t// Update global variables\n\tdocument = doc;\n\tdocElem = document.documentElement;\n\tdocumentIsHTML = !isXML( document );\n\n\t// Support: IE 9-11, Edge\n\t// Accessing iframe documents after unload throws \"permission denied\" errors (jQuery #13936)\n\tif ( preferredDoc !== document &&\n\t\t(subWindow = document.defaultView) && subWindow.top !== subWindow ) {\n\n\t\t// Support: IE 11, Edge\n\t\tif ( subWindow.addEventListener ) {\n\t\t\tsubWindow.addEventListener( \"unload\", unloadHandler, false );\n\n\t\t// Support: IE 9 - 10 only\n\t\t} else if ( subWindow.attachEvent ) {\n\t\t\tsubWindow.attachEvent( \"onunload\", unloadHandler );\n\t\t}\n\t}\n\n\t/* Attributes\n\t---------------------------------------------------------------------- */\n\n\t// Support: IE<8\n\t// Verify that getAttribute really returns attributes and not properties\n\t// (excepting IE8 booleans)\n\tsupport.attributes = assert(function( el ) {\n\t\tel.className = \"i\";\n\t\treturn !el.getAttribute(\"className\");\n\t});\n\n\t/* getElement(s)By*\n\t---------------------------------------------------------------------- */\n\n\t// Check if getElementsByTagName(\"*\") returns only elements\n\tsupport.getElementsByTagName = assert(function( el ) {\n\t\tel.appendChild( document.createComment(\"\") );\n\t\treturn !el.getElementsByTagName(\"*\").length;\n\t});\n\n\t// Support: IE<9\n\tsupport.getElementsByClassName = rnative.test( document.getElementsByClassName );\n\n\t// Support: IE<10\n\t// Check if getElementById returns elements by name\n\t// The broken getElementById methods don't pick up programmatically-set names,\n\t// so use a roundabout getElementsByName test\n\tsupport.getById = assert(function( el ) {\n\t\tdocElem.appendChild( el ).id = expando;\n\t\treturn !document.getElementsByName || !document.getElementsByName( expando ).length;\n\t});\n\n\t// ID filter and find\n\tif ( support.getById ) {\n\t\tExpr.filter[\"ID\"] = function( id ) {\n\t\t\tvar attrId = id.replace( runescape, funescape );\n\t\t\treturn function( elem ) {\n\t\t\t\treturn elem.getAttribute(\"id\") === attrId;\n\t\t\t};\n\t\t};\n\t\tExpr.find[\"ID\"] = function( id, context ) {\n\t\t\tif ( typeof context.getElementById !== \"undefined\" && documentIsHTML ) {\n\t\t\t\tvar elem = context.getElementById( id );\n\t\t\t\treturn elem ? [ elem ] : [];\n\t\t\t}\n\t\t};\n\t} else {\n\t\tExpr.filter[\"ID\"] = function( id ) {\n\t\t\tvar attrId = id.replace( runescape, funescape );\n\t\t\treturn function( elem ) {\n\t\t\t\tvar node = typeof elem.getAttributeNode !== \"undefined\" &&\n\t\t\t\t\telem.getAttributeNode(\"id\");\n\t\t\t\treturn node && node.value === attrId;\n\t\t\t};\n\t\t};\n\n\t\t// Support: IE 6 - 7 only\n\t\t// getElementById is not reliable as a find shortcut\n\t\tExpr.find[\"ID\"] = function( id, context ) {\n\t\t\tif ( typeof context.getElementById !== \"undefined\" && documentIsHTML ) {\n\t\t\t\tvar node, i, elems,\n\t\t\t\t\telem = context.getElementById( id );\n\n\t\t\t\tif ( elem ) {\n\n\t\t\t\t\t// Verify the id attribute\n\t\t\t\t\tnode = elem.getAttributeNode(\"id\");\n\t\t\t\t\tif ( node && node.value === id ) {\n\t\t\t\t\t\treturn [ elem ];\n\t\t\t\t\t}\n\n\t\t\t\t\t// Fall back on getElementsByName\n\t\t\t\t\telems = context.getElementsByName( id );\n\t\t\t\t\ti = 0;\n\t\t\t\t\twhile ( (elem = elems[i++]) ) {\n\t\t\t\t\t\tnode = elem.getAttributeNode(\"id\");\n\t\t\t\t\t\tif ( node && node.value === id ) {\n\t\t\t\t\t\t\treturn [ elem ];\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn [];\n\t\t\t}\n\t\t};\n\t}\n\n\t// Tag\n\tExpr.find[\"TAG\"] = support.getElementsByTagName ?\n\t\tfunction( tag, context ) {\n\t\t\tif ( typeof context.getElementsByTagName !== \"undefined\" ) {\n\t\t\t\treturn context.getElementsByTagName( tag );\n\n\t\t\t// DocumentFragment nodes don't have gEBTN\n\t\t\t} else if ( support.qsa ) {\n\t\t\t\treturn context.querySelectorAll( tag );\n\t\t\t}\n\t\t} :\n\n\t\tfunction( tag, context ) {\n\t\t\tvar elem,\n\t\t\t\ttmp = [],\n\t\t\t\ti = 0,\n\t\t\t\t// By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too\n\t\t\t\tresults = context.getElementsByTagName( tag );\n\n\t\t\t// Filter out possible comments\n\t\t\tif ( tag === \"*\" ) {\n\t\t\t\twhile ( (elem = results[i++]) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 ) {\n\t\t\t\t\t\ttmp.push( elem );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn tmp;\n\t\t\t}\n\t\t\treturn results;\n\t\t};\n\n\t// Class\n\tExpr.find[\"CLASS\"] = support.getElementsByClassName && function( className, context ) {\n\t\tif ( typeof context.getElementsByClassName !== \"undefined\" && documentIsHTML ) {\n\t\t\treturn context.getElementsByClassName( className );\n\t\t}\n\t};\n\n\t/* QSA/matchesSelector\n\t---------------------------------------------------------------------- */\n\n\t// QSA and matchesSelector support\n\n\t// matchesSelector(:active) reports false when true (IE9/Opera 11.5)\n\trbuggyMatches = [];\n\n\t// qSa(:focus) reports false when true (Chrome 21)\n\t// We allow this because of a bug in IE8/9 that throws an error\n\t// whenever `document.activeElement` is accessed on an iframe\n\t// So, we allow :focus to pass through QSA all the time to avoid the IE error\n\t// See https://bugs.jquery.com/ticket/13378\n\trbuggyQSA = [];\n\n\tif ( (support.qsa = rnative.test( document.querySelectorAll )) ) {\n\t\t// Build QSA regex\n\t\t// Regex strategy adopted from Diego Perini\n\t\tassert(function( el ) {\n\t\t\t// Select is set to empty string on purpose\n\t\t\t// This is to test IE's treatment of not explicitly\n\t\t\t// setting a boolean content attribute,\n\t\t\t// since its presence should be enough\n\t\t\t// https://bugs.jquery.com/ticket/12359\n\t\t\tdocElem.appendChild( el ).innerHTML = \"<a id='\" + expando + \"'></a>\" +\n\t\t\t\t\"<select id='\" + expando + \"-\\r\\\\' msallowcapture=''>\" +\n\t\t\t\t\"<option selected=''></option></select>\";\n\n\t\t\t// Support: IE8, Opera 11-12.16\n\t\t\t// Nothing should be selected when empty strings follow ^= or $= or *=\n\t\t\t// The test attribute must be unknown in Opera but \"safe\" for WinRT\n\t\t\t// https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section\n\t\t\tif ( el.querySelectorAll(\"[msallowcapture^='']\").length ) {\n\t\t\t\trbuggyQSA.push( \"[*^$]=\" + whitespace + \"*(?:''|\\\"\\\")\" );\n\t\t\t}\n\n\t\t\t// Support: IE8\n\t\t\t// Boolean attributes and \"value\" are not treated correctly\n\t\t\tif ( !el.querySelectorAll(\"[selected]\").length ) {\n\t\t\t\trbuggyQSA.push( \"\\\\[\" + whitespace + \"*(?:value|\" + booleans + \")\" );\n\t\t\t}\n\n\t\t\t// Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+\n\t\t\tif ( !el.querySelectorAll( \"[id~=\" + expando + \"-]\" ).length ) {\n\t\t\t\trbuggyQSA.push(\"~=\");\n\t\t\t}\n\n\t\t\t// Webkit/Opera - :checked should return selected option elements\n\t\t\t// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked\n\t\t\t// IE8 throws error here and will not see later tests\n\t\t\tif ( !el.querySelectorAll(\":checked\").length ) {\n\t\t\t\trbuggyQSA.push(\":checked\");\n\t\t\t}\n\n\t\t\t// Support: Safari 8+, iOS 8+\n\t\t\t// https://bugs.webkit.org/show_bug.cgi?id=136851\n\t\t\t// In-page `selector#id sibling-combinator selector` fails\n\t\t\tif ( !el.querySelectorAll( \"a#\" + expando + \"+*\" ).length ) {\n\t\t\t\trbuggyQSA.push(\".#.+[+~]\");\n\t\t\t}\n\t\t});\n\n\t\tassert(function( el ) {\n\t\t\tel.innerHTML = \"<a href='' disabled='disabled'></a>\" +\n\t\t\t\t\"<select disabled='disabled'><option/></select>\";\n\n\t\t\t// Support: Windows 8 Native Apps\n\t\t\t// The type and name attributes are restricted during .innerHTML assignment\n\t\t\tvar input = document.createElement(\"input\");\n\t\t\tinput.setAttribute( \"type\", \"hidden\" );\n\t\t\tel.appendChild( input ).setAttribute( \"name\", \"D\" );\n\n\t\t\t// Support: IE8\n\t\t\t// Enforce case-sensitivity of name attribute\n\t\t\tif ( el.querySelectorAll(\"[name=d]\").length ) {\n\t\t\t\trbuggyQSA.push( \"name\" + whitespace + \"*[*^$|!~]?=\" );\n\t\t\t}\n\n\t\t\t// FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)\n\t\t\t// IE8 throws error here and will not see later tests\n\t\t\tif ( el.querySelectorAll(\":enabled\").length !== 2 ) {\n\t\t\t\trbuggyQSA.push( \":enabled\", \":disabled\" );\n\t\t\t}\n\n\t\t\t// Support: IE9-11+\n\t\t\t// IE's :disabled selector does not pick up the children of disabled fieldsets\n\t\t\tdocElem.appendChild( el ).disabled = true;\n\t\t\tif ( el.querySelectorAll(\":disabled\").length !== 2 ) {\n\t\t\t\trbuggyQSA.push( \":enabled\", \":disabled\" );\n\t\t\t}\n\n\t\t\t// Opera 10-11 does not throw on post-comma invalid pseudos\n\t\t\tel.querySelectorAll(\"*,:x\");\n\t\t\trbuggyQSA.push(\",.*:\");\n\t\t});\n\t}\n\n\tif ( (support.matchesSelector = rnative.test( (matches = docElem.matches ||\n\t\tdocElem.webkitMatchesSelector ||\n\t\tdocElem.mozMatchesSelector ||\n\t\tdocElem.oMatchesSelector ||\n\t\tdocElem.msMatchesSelector) )) ) {\n\n\t\tassert(function( el ) {\n\t\t\t// Check to see if it's possible to do matchesSelector\n\t\t\t// on a disconnected node (IE 9)\n\t\t\tsupport.disconnectedMatch = matches.call( el, \"*\" );\n\n\t\t\t// This should fail with an exception\n\t\t\t// Gecko does not error, returns false instead\n\t\t\tmatches.call( el, \"[s!='']:x\" );\n\t\t\trbuggyMatches.push( \"!=\", pseudos );\n\t\t});\n\t}\n\n\trbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join(\"|\") );\n\trbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join(\"|\") );\n\n\t/* Contains\n\t---------------------------------------------------------------------- */\n\thasCompare = rnative.test( docElem.compareDocumentPosition );\n\n\t// Element contains another\n\t// Purposefully self-exclusive\n\t// As in, an element does not contain itself\n\tcontains = hasCompare || rnative.test( docElem.contains ) ?\n\t\tfunction( a, b ) {\n\t\t\tvar adown = a.nodeType === 9 ? a.documentElement : a,\n\t\t\t\tbup = b && b.parentNode;\n\t\t\treturn a === bup || !!( bup && bup.nodeType === 1 && (\n\t\t\t\tadown.contains ?\n\t\t\t\t\tadown.contains( bup ) :\n\t\t\t\t\ta.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16\n\t\t\t));\n\t\t} :\n\t\tfunction( a, b ) {\n\t\t\tif ( b ) {\n\t\t\t\twhile ( (b = b.parentNode) ) {\n\t\t\t\t\tif ( b === a ) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n\n\t/* Sorting\n\t---------------------------------------------------------------------- */\n\n\t// Document order sorting\n\tsortOrder = hasCompare ?\n\tfunction( a, b ) {\n\n\t\t// Flag for duplicate removal\n\t\tif ( a === b ) {\n\t\t\thasDuplicate = true;\n\t\t\treturn 0;\n\t\t}\n\n\t\t// Sort on method existence if only one input has compareDocumentPosition\n\t\tvar compare = !a.compareDocumentPosition - !b.compareDocumentPosition;\n\t\tif ( compare ) {\n\t\t\treturn compare;\n\t\t}\n\n\t\t// Calculate position if both inputs belong to the same document\n\t\tcompare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ?\n\t\t\ta.compareDocumentPosition( b ) :\n\n\t\t\t// Otherwise we know they are disconnected\n\t\t\t1;\n\n\t\t// Disconnected nodes\n\t\tif ( compare & 1 ||\n\t\t\t(!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) {\n\n\t\t\t// Choose the first element that is related to our preferred document\n\t\t\tif ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) {\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t\tif ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) {\n\t\t\t\treturn 1;\n\t\t\t}\n\n\t\t\t// Maintain original order\n\t\t\treturn sortInput ?\n\t\t\t\t( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :\n\t\t\t\t0;\n\t\t}\n\n\t\treturn compare & 4 ? -1 : 1;\n\t} :\n\tfunction( a, b ) {\n\t\t// Exit early if the nodes are identical\n\t\tif ( a === b ) {\n\t\t\thasDuplicate = true;\n\t\t\treturn 0;\n\t\t}\n\n\t\tvar cur,\n\t\t\ti = 0,\n\t\t\taup = a.parentNode,\n\t\t\tbup = b.parentNode,\n\t\t\tap = [ a ],\n\t\t\tbp = [ b ];\n\n\t\t// Parentless nodes are either documents or disconnected\n\t\tif ( !aup || !bup ) {\n\t\t\treturn a === document ? -1 :\n\t\t\t\tb === document ? 1 :\n\t\t\t\taup ? -1 :\n\t\t\t\tbup ? 1 :\n\t\t\t\tsortInput ?\n\t\t\t\t( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :\n\t\t\t\t0;\n\n\t\t// If the nodes are siblings, we can do a quick check\n\t\t} else if ( aup === bup ) {\n\t\t\treturn siblingCheck( a, b );\n\t\t}\n\n\t\t// Otherwise we need full lists of their ancestors for comparison\n\t\tcur = a;\n\t\twhile ( (cur = cur.parentNode) ) {\n\t\t\tap.unshift( cur );\n\t\t}\n\t\tcur = b;\n\t\twhile ( (cur = cur.parentNode) ) {\n\t\t\tbp.unshift( cur );\n\t\t}\n\n\t\t// Walk down the tree looking for a discrepancy\n\t\twhile ( ap[i] === bp[i] ) {\n\t\t\ti++;\n\t\t}\n\n\t\treturn i ?\n\t\t\t// Do a sibling check if the nodes have a common ancestor\n\t\t\tsiblingCheck( ap[i], bp[i] ) :\n\n\t\t\t// Otherwise nodes in our document sort first\n\t\t\tap[i] === preferredDoc ? -1 :\n\t\t\tbp[i] === preferredDoc ? 1 :\n\t\t\t0;\n\t};\n\n\treturn document;\n};\n\nSizzle.matches = function( expr, elements ) {\n\treturn Sizzle( expr, null, null, elements );\n};\n\nSizzle.matchesSelector = function( elem, expr ) {\n\t// Set document vars if needed\n\tif ( ( elem.ownerDocument || elem ) !== document ) {\n\t\tsetDocument( elem );\n\t}\n\n\tif ( support.matchesSelector && documentIsHTML &&\n\t\t!nonnativeSelectorCache[ expr + \" \" ] &&\n\t\t( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&\n\t\t( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) {\n\n\t\ttry {\n\t\t\tvar ret = matches.call( elem, expr );\n\n\t\t\t// IE 9's matchesSelector returns false on disconnected nodes\n\t\t\tif ( ret || support.disconnectedMatch ||\n\t\t\t\t\t// As well, disconnected nodes are said to be in a document\n\t\t\t\t\t// fragment in IE 9\n\t\t\t\t\telem.document && elem.document.nodeType !== 11 ) {\n\t\t\t\treturn ret;\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tnonnativeSelectorCache( expr, true );\n\t\t}\n\t}\n\n\treturn Sizzle( expr, document, null, [ elem ] ).length > 0;\n};\n\nSizzle.contains = function( context, elem ) {\n\t// Set document vars if needed\n\tif ( ( context.ownerDocument || context ) !== document ) {\n\t\tsetDocument( context );\n\t}\n\treturn contains( context, elem );\n};\n\nSizzle.attr = function( elem, name ) {\n\t// Set document vars if needed\n\tif ( ( elem.ownerDocument || elem ) !== document ) {\n\t\tsetDocument( elem );\n\t}\n\n\tvar fn = Expr.attrHandle[ name.toLowerCase() ],\n\t\t// Don't get fooled by Object.prototype properties (jQuery #13807)\n\t\tval = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?\n\t\t\tfn( elem, name, !documentIsHTML ) :\n\t\t\tundefined;\n\n\treturn val !== undefined ?\n\t\tval :\n\t\tsupport.attributes || !documentIsHTML ?\n\t\t\telem.getAttribute( name ) :\n\t\t\t(val = elem.getAttributeNode(name)) && val.specified ?\n\t\t\t\tval.value :\n\t\t\t\tnull;\n};\n\nSizzle.escape = function( sel ) {\n\treturn (sel + \"\").replace( rcssescape, fcssescape );\n};\n\nSizzle.error = function( msg ) {\n\tthrow new Error( \"Syntax error, unrecognized expression: \" + msg );\n};\n\n/**\n * Document sorting and removing duplicates\n * @param {ArrayLike} results\n */\nSizzle.uniqueSort = function( results ) {\n\tvar elem,\n\t\tduplicates = [],\n\t\tj = 0,\n\t\ti = 0;\n\n\t// Unless we *know* we can detect duplicates, assume their presence\n\thasDuplicate = !support.detectDuplicates;\n\tsortInput = !support.sortStable && results.slice( 0 );\n\tresults.sort( sortOrder );\n\n\tif ( hasDuplicate ) {\n\t\twhile ( (elem = results[i++]) ) {\n\t\t\tif ( elem === results[ i ] ) {\n\t\t\t\tj = duplicates.push( i );\n\t\t\t}\n\t\t}\n\t\twhile ( j-- ) {\n\t\t\tresults.splice( duplicates[ j ], 1 );\n\t\t}\n\t}\n\n\t// Clear input after sorting to release objects\n\t// See https://github.com/jquery/sizzle/pull/225\n\tsortInput = null;\n\n\treturn results;\n};\n\n/**\n * Utility function for retrieving the text value of an array of DOM nodes\n * @param {Array|Element} elem\n */\ngetText = Sizzle.getText = function( elem ) {\n\tvar node,\n\t\tret = \"\",\n\t\ti = 0,\n\t\tnodeType = elem.nodeType;\n\n\tif ( !nodeType ) {\n\t\t// If no nodeType, this is expected to be an array\n\t\twhile ( (node = elem[i++]) ) {\n\t\t\t// Do not traverse comment nodes\n\t\t\tret += getText( node );\n\t\t}\n\t} else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {\n\t\t// Use textContent for elements\n\t\t// innerText usage removed for consistency of new lines (jQuery #11153)\n\t\tif ( typeof elem.textContent === \"string\" ) {\n\t\t\treturn elem.textContent;\n\t\t} else {\n\t\t\t// Traverse its children\n\t\t\tfor ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {\n\t\t\t\tret += getText( elem );\n\t\t\t}\n\t\t}\n\t} else if ( nodeType === 3 || nodeType === 4 ) {\n\t\treturn elem.nodeValue;\n\t}\n\t// Do not include comment or processing instruction nodes\n\n\treturn ret;\n};\n\nExpr = Sizzle.selectors = {\n\n\t// Can be adjusted by the user\n\tcacheLength: 50,\n\n\tcreatePseudo: markFunction,\n\n\tmatch: matchExpr,\n\n\tattrHandle: {},\n\n\tfind: {},\n\n\trelative: {\n\t\t\">\": { dir: \"parentNode\", first: true },\n\t\t\" \": { dir: \"parentNode\" },\n\t\t\"+\": { dir: \"previousSibling\", first: true },\n\t\t\"~\": { dir: \"previousSibling\" }\n\t},\n\n\tpreFilter: {\n\t\t\"ATTR\": function( match ) {\n\t\t\tmatch[1] = match[1].replace( runescape, funescape );\n\n\t\t\t// Move the given value to match[3] whether quoted or unquoted\n\t\t\tmatch[3] = ( match[3] || match[4] || match[5] || \"\" ).replace( runescape, funescape );\n\n\t\t\tif ( match[2] === \"~=\" ) {\n\t\t\t\tmatch[3] = \" \" + match[3] + \" \";\n\t\t\t}\n\n\t\t\treturn match.slice( 0, 4 );\n\t\t},\n\n\t\t\"CHILD\": function( match ) {\n\t\t\t/* matches from matchExpr[\"CHILD\"]\n\t\t\t\t1 type (only|nth|...)\n\t\t\t\t2 what (child|of-type)\n\t\t\t\t3 argument (even|odd|\\d*|\\d*n([+-]\\d+)?|...)\n\t\t\t\t4 xn-component of xn+y argument ([+-]?\\d*n|)\n\t\t\t\t5 sign of xn-component\n\t\t\t\t6 x of xn-component\n\t\t\t\t7 sign of y-component\n\t\t\t\t8 y of y-component\n\t\t\t*/\n\t\t\tmatch[1] = match[1].toLowerCase();\n\n\t\t\tif ( match[1].slice( 0, 3 ) === \"nth\" ) {\n\t\t\t\t// nth-* requires argument\n\t\t\t\tif ( !match[3] ) {\n\t\t\t\t\tSizzle.error( match[0] );\n\t\t\t\t}\n\n\t\t\t\t// numeric x and y parameters for Expr.filter.CHILD\n\t\t\t\t// remember that false/true cast respectively to 0/1\n\t\t\t\tmatch[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === \"even\" || match[3] === \"odd\" ) );\n\t\t\t\tmatch[5] = +( ( match[7] + match[8] ) || match[3] === \"odd\" );\n\n\t\t\t// other types prohibit arguments\n\t\t\t} else if ( match[3] ) {\n\t\t\t\tSizzle.error( match[0] );\n\t\t\t}\n\n\t\t\treturn match;\n\t\t},\n\n\t\t\"PSEUDO\": function( match ) {\n\t\t\tvar excess,\n\t\t\t\tunquoted = !match[6] && match[2];\n\n\t\t\tif ( matchExpr[\"CHILD\"].test( match[0] ) ) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\t// Accept quoted arguments as-is\n\t\t\tif ( match[3] ) {\n\t\t\t\tmatch[2] = match[4] || match[5] || \"\";\n\n\t\t\t// Strip excess characters from unquoted arguments\n\t\t\t} else if ( unquoted && rpseudo.test( unquoted ) &&\n\t\t\t\t// Get excess from tokenize (recursively)\n\t\t\t\t(excess = tokenize( unquoted, true )) &&\n\t\t\t\t// advance to the next closing parenthesis\n\t\t\t\t(excess = unquoted.indexOf( \")\", unquoted.length - excess ) - unquoted.length) ) {\n\n\t\t\t\t// excess is a negative index\n\t\t\t\tmatch[0] = match[0].slice( 0, excess );\n\t\t\t\tmatch[2] = unquoted.slice( 0, excess );\n\t\t\t}\n\n\t\t\t// Return only captures needed by the pseudo filter method (type and argument)\n\t\t\treturn match.slice( 0, 3 );\n\t\t}\n\t},\n\n\tfilter: {\n\n\t\t\"TAG\": function( nodeNameSelector ) {\n\t\t\tvar nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();\n\t\t\treturn nodeNameSelector === \"*\" ?\n\t\t\t\tfunction() { return true; } :\n\t\t\t\tfunction( elem ) {\n\t\t\t\t\treturn elem.nodeName && elem.nodeName.toLowerCase() === nodeName;\n\t\t\t\t};\n\t\t},\n\n\t\t\"CLASS\": function( className ) {\n\t\t\tvar pattern = classCache[ className + \" \" ];\n\n\t\t\treturn pattern ||\n\t\t\t\t(pattern = new RegExp( \"(^|\" + whitespace + \")\" + className + \"(\" + whitespace + \"|$)\" )) &&\n\t\t\t\tclassCache( className, function( elem ) {\n\t\t\t\t\treturn pattern.test( typeof elem.className === \"string\" && elem.className || typeof elem.getAttribute !== \"undefined\" && elem.getAttribute(\"class\") || \"\" );\n\t\t\t\t});\n\t\t},\n\n\t\t\"ATTR\": function( name, operator, check ) {\n\t\t\treturn function( elem ) {\n\t\t\t\tvar result = Sizzle.attr( elem, name );\n\n\t\t\t\tif ( result == null ) {\n\t\t\t\t\treturn operator === \"!=\";\n\t\t\t\t}\n\t\t\t\tif ( !operator ) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\tresult += \"\";\n\n\t\t\t\treturn operator === \"=\" ? result === check :\n\t\t\t\t\toperator === \"!=\" ? result !== check :\n\t\t\t\t\toperator === \"^=\" ? check && result.indexOf( check ) === 0 :\n\t\t\t\t\toperator === \"*=\" ? check && result.indexOf( check ) > -1 :\n\t\t\t\t\toperator === \"$=\" ? check && result.slice( -check.length ) === check :\n\t\t\t\t\toperator === \"~=\" ? ( \" \" + result.replace( rwhitespace, \" \" ) + \" \" ).indexOf( check ) > -1 :\n\t\t\t\t\toperator === \"|=\" ? result === check || result.slice( 0, check.length + 1 ) === check + \"-\" :\n\t\t\t\t\tfalse;\n\t\t\t};\n\t\t},\n\n\t\t\"CHILD\": function( type, what, argument, first, last ) {\n\t\t\tvar simple = type.slice( 0, 3 ) !== \"nth\",\n\t\t\t\tforward = type.slice( -4 ) !== \"last\",\n\t\t\t\tofType = what === \"of-type\";\n\n\t\t\treturn first === 1 && last === 0 ?\n\n\t\t\t\t// Shortcut for :nth-*(n)\n\t\t\t\tfunction( elem ) {\n\t\t\t\t\treturn !!elem.parentNode;\n\t\t\t\t} :\n\n\t\t\t\tfunction( elem, context, xml ) {\n\t\t\t\t\tvar cache, uniqueCache, outerCache, node, nodeIndex, start,\n\t\t\t\t\t\tdir = simple !== forward ? \"nextSibling\" : \"previousSibling\",\n\t\t\t\t\t\tparent = elem.parentNode,\n\t\t\t\t\t\tname = ofType && elem.nodeName.toLowerCase(),\n\t\t\t\t\t\tuseCache = !xml && !ofType,\n\t\t\t\t\t\tdiff = false;\n\n\t\t\t\t\tif ( parent ) {\n\n\t\t\t\t\t\t// :(first|last|only)-(child|of-type)\n\t\t\t\t\t\tif ( simple ) {\n\t\t\t\t\t\t\twhile ( dir ) {\n\t\t\t\t\t\t\t\tnode = elem;\n\t\t\t\t\t\t\t\twhile ( (node = node[ dir ]) ) {\n\t\t\t\t\t\t\t\t\tif ( ofType ?\n\t\t\t\t\t\t\t\t\t\tnode.nodeName.toLowerCase() === name :\n\t\t\t\t\t\t\t\t\t\tnode.nodeType === 1 ) {\n\n\t\t\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t// Reverse direction for :only-* (if we haven't yet done so)\n\t\t\t\t\t\t\t\tstart = dir = type === \"only\" && !start && \"nextSibling\";\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tstart = [ forward ? parent.firstChild : parent.lastChild ];\n\n\t\t\t\t\t\t// non-xml :nth-child(...) stores cache data on `parent`\n\t\t\t\t\t\tif ( forward && useCache ) {\n\n\t\t\t\t\t\t\t// Seek `elem` from a previously-cached index\n\n\t\t\t\t\t\t\t// ...in a gzip-friendly way\n\t\t\t\t\t\t\tnode = parent;\n\t\t\t\t\t\t\touterCache = node[ expando ] || (node[ expando ] = {});\n\n\t\t\t\t\t\t\t// Support: IE <9 only\n\t\t\t\t\t\t\t// Defend against cloned attroperties (jQuery gh-1709)\n\t\t\t\t\t\t\tuniqueCache = outerCache[ node.uniqueID ] ||\n\t\t\t\t\t\t\t\t(outerCache[ node.uniqueID ] = {});\n\n\t\t\t\t\t\t\tcache = uniqueCache[ type ] || [];\n\t\t\t\t\t\t\tnodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];\n\t\t\t\t\t\t\tdiff = nodeIndex && cache[ 2 ];\n\t\t\t\t\t\t\tnode = nodeIndex && parent.childNodes[ nodeIndex ];\n\n\t\t\t\t\t\t\twhile ( (node = ++nodeIndex && node && node[ dir ] ||\n\n\t\t\t\t\t\t\t\t// Fallback to seeking `elem` from the start\n\t\t\t\t\t\t\t\t(diff = nodeIndex = 0) || start.pop()) ) {\n\n\t\t\t\t\t\t\t\t// When found, cache indexes on `parent` and break\n\t\t\t\t\t\t\t\tif ( node.nodeType === 1 && ++diff && node === elem ) {\n\t\t\t\t\t\t\t\t\tuniqueCache[ type ] = [ dirruns, nodeIndex, diff ];\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Use previously-cached element index if available\n\t\t\t\t\t\t\tif ( useCache ) {\n\t\t\t\t\t\t\t\t// ...in a gzip-friendly way\n\t\t\t\t\t\t\t\tnode = elem;\n\t\t\t\t\t\t\t\touterCache = node[ expando ] || (node[ expando ] = {});\n\n\t\t\t\t\t\t\t\t// Support: IE <9 only\n\t\t\t\t\t\t\t\t// Defend against cloned attroperties (jQuery gh-1709)\n\t\t\t\t\t\t\t\tuniqueCache = outerCache[ node.uniqueID ] ||\n\t\t\t\t\t\t\t\t\t(outerCache[ node.uniqueID ] = {});\n\n\t\t\t\t\t\t\t\tcache = uniqueCache[ type ] || [];\n\t\t\t\t\t\t\t\tnodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];\n\t\t\t\t\t\t\t\tdiff = nodeIndex;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// xml :nth-child(...)\n\t\t\t\t\t\t\t// or :nth-last-child(...) or :nth(-last)?-of-type(...)\n\t\t\t\t\t\t\tif ( diff === false ) {\n\t\t\t\t\t\t\t\t// Use the same loop as above to seek `elem` from the start\n\t\t\t\t\t\t\t\twhile ( (node = ++nodeIndex && node && node[ dir ] ||\n\t\t\t\t\t\t\t\t\t(diff = nodeIndex = 0) || start.pop()) ) {\n\n\t\t\t\t\t\t\t\t\tif ( ( ofType ?\n\t\t\t\t\t\t\t\t\t\tnode.nodeName.toLowerCase() === name :\n\t\t\t\t\t\t\t\t\t\tnode.nodeType === 1 ) &&\n\t\t\t\t\t\t\t\t\t\t++diff ) {\n\n\t\t\t\t\t\t\t\t\t\t// Cache the index of each encountered element\n\t\t\t\t\t\t\t\t\t\tif ( useCache ) {\n\t\t\t\t\t\t\t\t\t\t\touterCache = node[ expando ] || (node[ expando ] = {});\n\n\t\t\t\t\t\t\t\t\t\t\t// Support: IE <9 only\n\t\t\t\t\t\t\t\t\t\t\t// Defend against cloned attroperties (jQuery gh-1709)\n\t\t\t\t\t\t\t\t\t\t\tuniqueCache = outerCache[ node.uniqueID ] ||\n\t\t\t\t\t\t\t\t\t\t\t\t(outerCache[ node.uniqueID ] = {});\n\n\t\t\t\t\t\t\t\t\t\t\tuniqueCache[ type ] = [ dirruns, diff ];\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\tif ( node === elem ) {\n\t\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Incorporate the offset, then check against cycle size\n\t\t\t\t\t\tdiff -= last;\n\t\t\t\t\t\treturn diff === first || ( diff % first === 0 && diff / first >= 0 );\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t},\n\n\t\t\"PSEUDO\": function( pseudo, argument ) {\n\t\t\t// pseudo-class names are case-insensitive\n\t\t\t// http://www.w3.org/TR/selectors/#pseudo-classes\n\t\t\t// Prioritize by case sensitivity in case custom pseudos are added with uppercase letters\n\t\t\t// Remember that setFilters inherits from pseudos\n\t\t\tvar args,\n\t\t\t\tfn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||\n\t\t\t\t\tSizzle.error( \"unsupported pseudo: \" + pseudo );\n\n\t\t\t// The user may use createPseudo to indicate that\n\t\t\t// arguments are needed to create the filter function\n\t\t\t// just as Sizzle does\n\t\t\tif ( fn[ expando ] ) {\n\t\t\t\treturn fn( argument );\n\t\t\t}\n\n\t\t\t// But maintain support for old signatures\n\t\t\tif ( fn.length > 1 ) {\n\t\t\t\targs = [ pseudo, pseudo, \"\", argument ];\n\t\t\t\treturn Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?\n\t\t\t\t\tmarkFunction(function( seed, matches ) {\n\t\t\t\t\t\tvar idx,\n\t\t\t\t\t\t\tmatched = fn( seed, argument ),\n\t\t\t\t\t\t\ti = matched.length;\n\t\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\t\tidx = indexOf( seed, matched[i] );\n\t\t\t\t\t\t\tseed[ idx ] = !( matches[ idx ] = matched[i] );\n\t\t\t\t\t\t}\n\t\t\t\t\t}) :\n\t\t\t\t\tfunction( elem ) {\n\t\t\t\t\t\treturn fn( elem, 0, args );\n\t\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn fn;\n\t\t}\n\t},\n\n\tpseudos: {\n\t\t// Potentially complex pseudos\n\t\t\"not\": markFunction(function( selector ) {\n\t\t\t// Trim the selector passed to compile\n\t\t\t// to avoid treating leading and trailing\n\t\t\t// spaces as combinators\n\t\t\tvar input = [],\n\t\t\t\tresults = [],\n\t\t\t\tmatcher = compile( selector.replace( rtrim, \"$1\" ) );\n\n\t\t\treturn matcher[ expando ] ?\n\t\t\t\tmarkFunction(function( seed, matches, context, xml ) {\n\t\t\t\t\tvar elem,\n\t\t\t\t\t\tunmatched = matcher( seed, null, xml, [] ),\n\t\t\t\t\t\ti = seed.length;\n\n\t\t\t\t\t// Match elements unmatched by `matcher`\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tif ( (elem = unmatched[i]) ) {\n\t\t\t\t\t\t\tseed[i] = !(matches[i] = elem);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}) :\n\t\t\t\tfunction( elem, context, xml ) {\n\t\t\t\t\tinput[0] = elem;\n\t\t\t\t\tmatcher( input, null, xml, results );\n\t\t\t\t\t// Don't keep the element (issue #299)\n\t\t\t\t\tinput[0] = null;\n\t\t\t\t\treturn !results.pop();\n\t\t\t\t};\n\t\t}),\n\n\t\t\"has\": markFunction(function( selector ) {\n\t\t\treturn function( elem ) {\n\t\t\t\treturn Sizzle( selector, elem ).length > 0;\n\t\t\t};\n\t\t}),\n\n\t\t\"contains\": markFunction(function( text ) {\n\t\t\ttext = text.replace( runescape, funescape );\n\t\t\treturn function( elem ) {\n\t\t\t\treturn ( elem.textContent || getText( elem ) ).indexOf( text ) > -1;\n\t\t\t};\n\t\t}),\n\n\t\t// \"Whether an element is represented by a :lang() selector\n\t\t// is based solely on the element's language value\n\t\t// being equal to the identifier C,\n\t\t// or beginning with the identifier C immediately followed by \"-\".\n\t\t// The matching of C against the element's language value is performed case-insensitively.\n\t\t// The identifier C does not have to be a valid language name.\"\n\t\t// http://www.w3.org/TR/selectors/#lang-pseudo\n\t\t\"lang\": markFunction( function( lang ) {\n\t\t\t// lang value must be a valid identifier\n\t\t\tif ( !ridentifier.test(lang || \"\") ) {\n\t\t\t\tSizzle.error( \"unsupported lang: \" + lang );\n\t\t\t}\n\t\t\tlang = lang.replace( runescape, funescape ).toLowerCase();\n\t\t\treturn function( elem ) {\n\t\t\t\tvar elemLang;\n\t\t\t\tdo {\n\t\t\t\t\tif ( (elemLang = documentIsHTML ?\n\t\t\t\t\t\telem.lang :\n\t\t\t\t\t\telem.getAttribute(\"xml:lang\") || elem.getAttribute(\"lang\")) ) {\n\n\t\t\t\t\t\telemLang = elemLang.toLowerCase();\n\t\t\t\t\t\treturn elemLang === lang || elemLang.indexOf( lang + \"-\" ) === 0;\n\t\t\t\t\t}\n\t\t\t\t} while ( (elem = elem.parentNode) && elem.nodeType === 1 );\n\t\t\t\treturn false;\n\t\t\t};\n\t\t}),\n\n\t\t// Miscellaneous\n\t\t\"target\": function( elem ) {\n\t\t\tvar hash = window.location && window.location.hash;\n\t\t\treturn hash && hash.slice( 1 ) === elem.id;\n\t\t},\n\n\t\t\"root\": function( elem ) {\n\t\t\treturn elem === docElem;\n\t\t},\n\n\t\t\"focus\": function( elem ) {\n\t\t\treturn elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);\n\t\t},\n\n\t\t// Boolean properties\n\t\t\"enabled\": createDisabledPseudo( false ),\n\t\t\"disabled\": createDisabledPseudo( true ),\n\n\t\t\"checked\": function( elem ) {\n\t\t\t// In CSS3, :checked should return both checked and selected elements\n\t\t\t// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked\n\t\t\tvar nodeName = elem.nodeName.toLowerCase();\n\t\t\treturn (nodeName === \"input\" && !!elem.checked) || (nodeName === \"option\" && !!elem.selected);\n\t\t},\n\n\t\t\"selected\": function( elem ) {\n\t\t\t// Accessing this property makes selected-by-default\n\t\t\t// options in Safari work properly\n\t\t\tif ( elem.parentNode ) {\n\t\t\t\telem.parentNode.selectedIndex;\n\t\t\t}\n\n\t\t\treturn elem.selected === true;\n\t\t},\n\n\t\t// Contents\n\t\t\"empty\": function( elem ) {\n\t\t\t// http://www.w3.org/TR/selectors/#empty-pseudo\n\t\t\t// :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),\n\t\t\t// but not by others (comment: 8; processing instruction: 7; etc.)\n\t\t\t// nodeType < 6 works because attributes (2) do not appear as children\n\t\t\tfor ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {\n\t\t\t\tif ( elem.nodeType < 6 ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\n\t\t\"parent\": function( elem ) {\n\t\t\treturn !Expr.pseudos[\"empty\"]( elem );\n\t\t},\n\n\t\t// Element/input types\n\t\t\"header\": function( elem ) {\n\t\t\treturn rheader.test( elem.nodeName );\n\t\t},\n\n\t\t\"input\": function( elem ) {\n\t\t\treturn rinputs.test( elem.nodeName );\n\t\t},\n\n\t\t\"button\": function( elem ) {\n\t\t\tvar name = elem.nodeName.toLowerCase();\n\t\t\treturn name === \"input\" && elem.type === \"button\" || name === \"button\";\n\t\t},\n\n\t\t\"text\": function( elem ) {\n\t\t\tvar attr;\n\t\t\treturn elem.nodeName.toLowerCase() === \"input\" &&\n\t\t\t\telem.type === \"text\" &&\n\n\t\t\t\t// Support: IE<8\n\t\t\t\t// New HTML5 attribute values (e.g., \"search\") appear with elem.type === \"text\"\n\t\t\t\t( (attr = elem.getAttribute(\"type\")) == null || attr.toLowerCase() === \"text\" );\n\t\t},\n\n\t\t// Position-in-collection\n\t\t\"first\": createPositionalPseudo(function() {\n\t\t\treturn [ 0 ];\n\t\t}),\n\n\t\t\"last\": createPositionalPseudo(function( matchIndexes, length ) {\n\t\t\treturn [ length - 1 ];\n\t\t}),\n\n\t\t\"eq\": createPositionalPseudo(function( matchIndexes, length, argument ) {\n\t\t\treturn [ argument < 0 ? argument + length : argument ];\n\t\t}),\n\n\t\t\"even\": createPositionalPseudo(function( matchIndexes, length ) {\n\t\t\tvar i = 0;\n\t\t\tfor ( ; i < length; i += 2 ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t}),\n\n\t\t\"odd\": createPositionalPseudo(function( matchIndexes, length ) {\n\t\t\tvar i = 1;\n\t\t\tfor ( ; i < length; i += 2 ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t}),\n\n\t\t\"lt\": createPositionalPseudo(function( matchIndexes, length, argument ) {\n\t\t\tvar i = argument < 0 ?\n\t\t\t\targument + length :\n\t\t\t\targument > length ?\n\t\t\t\t\tlength :\n\t\t\t\t\targument;\n\t\t\tfor ( ; --i >= 0; ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t}),\n\n\t\t\"gt\": createPositionalPseudo(function( matchIndexes, length, argument ) {\n\t\t\tvar i = argument < 0 ? argument + length : argument;\n\t\t\tfor ( ; ++i < length; ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t})\n\t}\n};\n\nExpr.pseudos[\"nth\"] = Expr.pseudos[\"eq\"];\n\n// Add button/input type pseudos\nfor ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {\n\tExpr.pseudos[ i ] = createInputPseudo( i );\n}\nfor ( i in { submit: true, reset: true } ) {\n\tExpr.pseudos[ i ] = createButtonPseudo( i );\n}\n\n// Easy API for creating new setFilters\nfunction setFilters() {}\nsetFilters.prototype = Expr.filters = Expr.pseudos;\nExpr.setFilters = new setFilters();\n\ntokenize = Sizzle.tokenize = function( selector, parseOnly ) {\n\tvar matched, match, tokens, type,\n\t\tsoFar, groups, preFilters,\n\t\tcached = tokenCache[ selector + \" \" ];\n\n\tif ( cached ) {\n\t\treturn parseOnly ? 0 : cached.slice( 0 );\n\t}\n\n\tsoFar = selector;\n\tgroups = [];\n\tpreFilters = Expr.preFilter;\n\n\twhile ( soFar ) {\n\n\t\t// Comma and first run\n\t\tif ( !matched || (match = rcomma.exec( soFar )) ) {\n\t\t\tif ( match ) {\n\t\t\t\t// Don't consume trailing commas as valid\n\t\t\t\tsoFar = soFar.slice( match[0].length ) || soFar;\n\t\t\t}\n\t\t\tgroups.push( (tokens = []) );\n\t\t}\n\n\t\tmatched = false;\n\n\t\t// Combinators\n\t\tif ( (match = rcombinators.exec( soFar )) ) {\n\t\t\tmatched = match.shift();\n\t\t\ttokens.push({\n\t\t\t\tvalue: matched,\n\t\t\t\t// Cast descendant combinators to space\n\t\t\t\ttype: match[0].replace( rtrim, \" \" )\n\t\t\t});\n\t\t\tsoFar = soFar.slice( matched.length );\n\t\t}\n\n\t\t// Filters\n\t\tfor ( type in Expr.filter ) {\n\t\t\tif ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||\n\t\t\t\t(match = preFilters[ type ]( match ))) ) {\n\t\t\t\tmatched = match.shift();\n\t\t\t\ttokens.push({\n\t\t\t\t\tvalue: matched,\n\t\t\t\t\ttype: type,\n\t\t\t\t\tmatches: match\n\t\t\t\t});\n\t\t\t\tsoFar = soFar.slice( matched.length );\n\t\t\t}\n\t\t}\n\n\t\tif ( !matched ) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Return the length of the invalid excess\n\t// if we're just parsing\n\t// Otherwise, throw an error or return tokens\n\treturn parseOnly ?\n\t\tsoFar.length :\n\t\tsoFar ?\n\t\t\tSizzle.error( selector ) :\n\t\t\t// Cache the tokens\n\t\t\ttokenCache( selector, groups ).slice( 0 );\n};\n\nfunction toSelector( tokens ) {\n\tvar i = 0,\n\t\tlen = tokens.length,\n\t\tselector = \"\";\n\tfor ( ; i < len; i++ ) {\n\t\tselector += tokens[i].value;\n\t}\n\treturn selector;\n}\n\nfunction addCombinator( matcher, combinator, base ) {\n\tvar dir = combinator.dir,\n\t\tskip = combinator.next,\n\t\tkey = skip || dir,\n\t\tcheckNonElements = base && key === \"parentNode\",\n\t\tdoneName = done++;\n\n\treturn combinator.first ?\n\t\t// Check against closest ancestor/preceding element\n\t\tfunction( elem, context, xml ) {\n\t\t\twhile ( (elem = elem[ dir ]) ) {\n\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\treturn matcher( elem, context, xml );\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t} :\n\n\t\t// Check against all ancestor/preceding elements\n\t\tfunction( elem, context, xml ) {\n\t\t\tvar oldCache, uniqueCache, outerCache,\n\t\t\t\tnewCache = [ dirruns, doneName ];\n\n\t\t\t// We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching\n\t\t\tif ( xml ) {\n\t\t\t\twhile ( (elem = elem[ dir ]) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\t\tif ( matcher( elem, context, xml ) ) {\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\twhile ( (elem = elem[ dir ]) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\t\touterCache = elem[ expando ] || (elem[ expando ] = {});\n\n\t\t\t\t\t\t// Support: IE <9 only\n\t\t\t\t\t\t// Defend against cloned attroperties (jQuery gh-1709)\n\t\t\t\t\t\tuniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {});\n\n\t\t\t\t\t\tif ( skip && skip === elem.nodeName.toLowerCase() ) {\n\t\t\t\t\t\t\telem = elem[ dir ] || elem;\n\t\t\t\t\t\t} else if ( (oldCache = uniqueCache[ key ]) &&\n\t\t\t\t\t\t\toldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) {\n\n\t\t\t\t\t\t\t// Assign to newCache so results back-propagate to previous elements\n\t\t\t\t\t\t\treturn (newCache[ 2 ] = oldCache[ 2 ]);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Reuse newcache so results back-propagate to previous elements\n\t\t\t\t\t\t\tuniqueCache[ key ] = newCache;\n\n\t\t\t\t\t\t\t// A match means we're done; a fail means we have to keep checking\n\t\t\t\t\t\t\tif ( (newCache[ 2 ] = matcher( elem, context, xml )) ) {\n\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n}\n\nfunction elementMatcher( matchers ) {\n\treturn matchers.length > 1 ?\n\t\tfunction( elem, context, xml ) {\n\t\t\tvar i = matchers.length;\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( !matchers[i]( elem, context, xml ) ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t} :\n\t\tmatchers[0];\n}\n\nfunction multipleContexts( selector, contexts, results ) {\n\tvar i = 0,\n\t\tlen = contexts.length;\n\tfor ( ; i < len; i++ ) {\n\t\tSizzle( selector, contexts[i], results );\n\t}\n\treturn results;\n}\n\nfunction condense( unmatched, map, filter, context, xml ) {\n\tvar elem,\n\t\tnewUnmatched = [],\n\t\ti = 0,\n\t\tlen = unmatched.length,\n\t\tmapped = map != null;\n\n\tfor ( ; i < len; i++ ) {\n\t\tif ( (elem = unmatched[i]) ) {\n\t\t\tif ( !filter || filter( elem, context, xml ) ) {\n\t\t\t\tnewUnmatched.push( elem );\n\t\t\t\tif ( mapped ) {\n\t\t\t\t\tmap.push( i );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn newUnmatched;\n}\n\nfunction setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {\n\tif ( postFilter && !postFilter[ expando ] ) {\n\t\tpostFilter = setMatcher( postFilter );\n\t}\n\tif ( postFinder && !postFinder[ expando ] ) {\n\t\tpostFinder = setMatcher( postFinder, postSelector );\n\t}\n\treturn markFunction(function( seed, results, context, xml ) {\n\t\tvar temp, i, elem,\n\t\t\tpreMap = [],\n\t\t\tpostMap = [],\n\t\t\tpreexisting = results.length,\n\n\t\t\t// Get initial elements from seed or context\n\t\t\telems = seed || multipleContexts( selector || \"*\", context.nodeType ? [ context ] : context, [] ),\n\n\t\t\t// Prefilter to get matcher input, preserving a map for seed-results synchronization\n\t\t\tmatcherIn = preFilter && ( seed || !selector ) ?\n\t\t\t\tcondense( elems, preMap, preFilter, context, xml ) :\n\t\t\t\telems,\n\n\t\t\tmatcherOut = matcher ?\n\t\t\t\t// If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,\n\t\t\t\tpostFinder || ( seed ? preFilter : preexisting || postFilter ) ?\n\n\t\t\t\t\t// ...intermediate processing is necessary\n\t\t\t\t\t[] :\n\n\t\t\t\t\t// ...otherwise use results directly\n\t\t\t\t\tresults :\n\t\t\t\tmatcherIn;\n\n\t\t// Find primary matches\n\t\tif ( matcher ) {\n\t\t\tmatcher( matcherIn, matcherOut, context, xml );\n\t\t}\n\n\t\t// Apply postFilter\n\t\tif ( postFilter ) {\n\t\t\ttemp = condense( matcherOut, postMap );\n\t\t\tpostFilter( temp, [], context, xml );\n\n\t\t\t// Un-match failing elements by moving them back to matcherIn\n\t\t\ti = temp.length;\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( (elem = temp[i]) ) {\n\t\t\t\t\tmatcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif ( seed ) {\n\t\t\tif ( postFinder || preFilter ) {\n\t\t\t\tif ( postFinder ) {\n\t\t\t\t\t// Get the final matcherOut by condensing this intermediate into postFinder contexts\n\t\t\t\t\ttemp = [];\n\t\t\t\t\ti = matcherOut.length;\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tif ( (elem = matcherOut[i]) ) {\n\t\t\t\t\t\t\t// Restore matcherIn since elem is not yet a final match\n\t\t\t\t\t\t\ttemp.push( (matcherIn[i] = elem) );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tpostFinder( null, (matcherOut = []), temp, xml );\n\t\t\t\t}\n\n\t\t\t\t// Move matched elements from seed to results to keep them synchronized\n\t\t\t\ti = matcherOut.length;\n\t\t\t\twhile ( i-- ) {\n\t\t\t\t\tif ( (elem = matcherOut[i]) &&\n\t\t\t\t\t\t(temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) {\n\n\t\t\t\t\t\tseed[temp] = !(results[temp] = elem);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Add elements to results, through postFinder if defined\n\t\t} else {\n\t\t\tmatcherOut = condense(\n\t\t\t\tmatcherOut === results ?\n\t\t\t\t\tmatcherOut.splice( preexisting, matcherOut.length ) :\n\t\t\t\t\tmatcherOut\n\t\t\t);\n\t\t\tif ( postFinder ) {\n\t\t\t\tpostFinder( null, results, matcherOut, xml );\n\t\t\t} else {\n\t\t\t\tpush.apply( results, matcherOut );\n\t\t\t}\n\t\t}\n\t});\n}\n\nfunction matcherFromTokens( tokens ) {\n\tvar checkContext, matcher, j,\n\t\tlen = tokens.length,\n\t\tleadingRelative = Expr.relative[ tokens[0].type ],\n\t\timplicitRelative = leadingRelative || Expr.relative[\" \"],\n\t\ti = leadingRelative ? 1 : 0,\n\n\t\t// The foundational matcher ensures that elements are reachable from top-level context(s)\n\t\tmatchContext = addCombinator( function( elem ) {\n\t\t\treturn elem === checkContext;\n\t\t}, implicitRelative, true ),\n\t\tmatchAnyContext = addCombinator( function( elem ) {\n\t\t\treturn indexOf( checkContext, elem ) > -1;\n\t\t}, implicitRelative, true ),\n\t\tmatchers = [ function( elem, context, xml ) {\n\t\t\tvar ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || (\n\t\t\t\t(checkContext = context).nodeType ?\n\t\t\t\t\tmatchContext( elem, context, xml ) :\n\t\t\t\t\tmatchAnyContext( elem, context, xml ) );\n\t\t\t// Avoid hanging onto element (issue #299)\n\t\t\tcheckContext = null;\n\t\t\treturn ret;\n\t\t} ];\n\n\tfor ( ; i < len; i++ ) {\n\t\tif ( (matcher = Expr.relative[ tokens[i].type ]) ) {\n\t\t\tmatchers = [ addCombinator(elementMatcher( matchers ), matcher) ];\n\t\t} else {\n\t\t\tmatcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );\n\n\t\t\t// Return special upon seeing a positional matcher\n\t\t\tif ( matcher[ expando ] ) {\n\t\t\t\t// Find the next relative operator (if any) for proper handling\n\t\t\t\tj = ++i;\n\t\t\t\tfor ( ; j < len; j++ ) {\n\t\t\t\t\tif ( Expr.relative[ tokens[j].type ] ) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn setMatcher(\n\t\t\t\t\ti > 1 && elementMatcher( matchers ),\n\t\t\t\t\ti > 1 && toSelector(\n\t\t\t\t\t\t// If the preceding token was a descendant combinator, insert an implicit any-element `*`\n\t\t\t\t\t\ttokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === \" \" ? \"*\" : \"\" })\n\t\t\t\t\t).replace( rtrim, \"$1\" ),\n\t\t\t\t\tmatcher,\n\t\t\t\t\ti < j && matcherFromTokens( tokens.slice( i, j ) ),\n\t\t\t\t\tj < len && matcherFromTokens( (tokens = tokens.slice( j )) ),\n\t\t\t\t\tj < len && toSelector( tokens )\n\t\t\t\t);\n\t\t\t}\n\t\t\tmatchers.push( matcher );\n\t\t}\n\t}\n\n\treturn elementMatcher( matchers );\n}\n\nfunction matcherFromGroupMatchers( elementMatchers, setMatchers ) {\n\tvar bySet = setMatchers.length > 0,\n\t\tbyElement = elementMatchers.length > 0,\n\t\tsuperMatcher = function( seed, context, xml, results, outermost ) {\n\t\t\tvar elem, j, matcher,\n\t\t\t\tmatchedCount = 0,\n\t\t\t\ti = \"0\",\n\t\t\t\tunmatched = seed && [],\n\t\t\t\tsetMatched = [],\n\t\t\t\tcontextBackup = outermostContext,\n\t\t\t\t// We must always have either seed elements or outermost context\n\t\t\t\telems = seed || byElement && Expr.find[\"TAG\"]( \"*\", outermost ),\n\t\t\t\t// Use integer dirruns iff this is the outermost matcher\n\t\t\t\tdirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1),\n\t\t\t\tlen = elems.length;\n\n\t\t\tif ( outermost ) {\n\t\t\t\toutermostContext = context === document || context || outermost;\n\t\t\t}\n\n\t\t\t// Add elements passing elementMatchers directly to results\n\t\t\t// Support: IE<9, Safari\n\t\t\t// Tolerate NodeList properties (IE: \"length\"; Safari: <number>) matching elements by id\n\t\t\tfor ( ; i !== len && (elem = elems[i]) != null; i++ ) {\n\t\t\t\tif ( byElement && elem ) {\n\t\t\t\t\tj = 0;\n\t\t\t\t\tif ( !context && elem.ownerDocument !== document ) {\n\t\t\t\t\t\tsetDocument( elem );\n\t\t\t\t\t\txml = !documentIsHTML;\n\t\t\t\t\t}\n\t\t\t\t\twhile ( (matcher = elementMatchers[j++]) ) {\n\t\t\t\t\t\tif ( matcher( elem, context || document, xml) ) {\n\t\t\t\t\t\t\tresults.push( elem );\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif ( outermost ) {\n\t\t\t\t\t\tdirruns = dirrunsUnique;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Track unmatched elements for set filters\n\t\t\t\tif ( bySet ) {\n\t\t\t\t\t// They will have gone through all possible matchers\n\t\t\t\t\tif ( (elem = !matcher && elem) ) {\n\t\t\t\t\t\tmatchedCount--;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Lengthen the array for every element, matched or not\n\t\t\t\t\tif ( seed ) {\n\t\t\t\t\t\tunmatched.push( elem );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// `i` is now the count of elements visited above, and adding it to `matchedCount`\n\t\t\t// makes the latter nonnegative.\n\t\t\tmatchedCount += i;\n\n\t\t\t// Apply set filters to unmatched elements\n\t\t\t// NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount`\n\t\t\t// equals `i`), unless we didn't visit _any_ elements in the above loop because we have\n\t\t\t// no element matchers and no seed.\n\t\t\t// Incrementing an initially-string \"0\" `i` allows `i` to remain a string only in that\n\t\t\t// case, which will result in a \"00\" `matchedCount` that differs from `i` but is also\n\t\t\t// numerically zero.\n\t\t\tif ( bySet && i !== matchedCount ) {\n\t\t\t\tj = 0;\n\t\t\t\twhile ( (matcher = setMatchers[j++]) ) {\n\t\t\t\t\tmatcher( unmatched, setMatched, context, xml );\n\t\t\t\t}\n\n\t\t\t\tif ( seed ) {\n\t\t\t\t\t// Reintegrate element matches to eliminate the need for sorting\n\t\t\t\t\tif ( matchedCount > 0 ) {\n\t\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\t\tif ( !(unmatched[i] || setMatched[i]) ) {\n\t\t\t\t\t\t\t\tsetMatched[i] = pop.call( results );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Discard index placeholder values to get only actual matches\n\t\t\t\t\tsetMatched = condense( setMatched );\n\t\t\t\t}\n\n\t\t\t\t// Add matches to results\n\t\t\t\tpush.apply( results, setMatched );\n\n\t\t\t\t// Seedless set matches succeeding multiple successful matchers stipulate sorting\n\t\t\t\tif ( outermost && !seed && setMatched.length > 0 &&\n\t\t\t\t\t( matchedCount + setMatchers.length ) > 1 ) {\n\n\t\t\t\t\tSizzle.uniqueSort( results );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Override manipulation of globals by nested matchers\n\t\t\tif ( outermost ) {\n\t\t\t\tdirruns = dirrunsUnique;\n\t\t\t\toutermostContext = contextBackup;\n\t\t\t}\n\n\t\t\treturn unmatched;\n\t\t};\n\n\treturn bySet ?\n\t\tmarkFunction( superMatcher ) :\n\t\tsuperMatcher;\n}\n\ncompile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {\n\tvar i,\n\t\tsetMatchers = [],\n\t\telementMatchers = [],\n\t\tcached = compilerCache[ selector + \" \" ];\n\n\tif ( !cached ) {\n\t\t// Generate a function of recursive functions that can be used to check each element\n\t\tif ( !match ) {\n\t\t\tmatch = tokenize( selector );\n\t\t}\n\t\ti = match.length;\n\t\twhile ( i-- ) {\n\t\t\tcached = matcherFromTokens( match[i] );\n\t\t\tif ( cached[ expando ] ) {\n\t\t\t\tsetMatchers.push( cached );\n\t\t\t} else {\n\t\t\t\telementMatchers.push( cached );\n\t\t\t}\n\t\t}\n\n\t\t// Cache the compiled function\n\t\tcached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );\n\n\t\t// Save selector and tokenization\n\t\tcached.selector = selector;\n\t}\n\treturn cached;\n};\n\n/**\n * A low-level selection function that works with Sizzle's compiled\n * selector functions\n * @param {String|Function} selector A selector or a pre-compiled\n * selector function built with Sizzle.compile\n * @param {Element} context\n * @param {Array} [results]\n * @param {Array} [seed] A set of elements to match against\n */\nselect = Sizzle.select = function( selector, context, results, seed ) {\n\tvar i, tokens, token, type, find,\n\t\tcompiled = typeof selector === \"function\" && selector,\n\t\tmatch = !seed && tokenize( (selector = compiled.selector || selector) );\n\n\tresults = results || [];\n\n\t// Try to minimize operations if there is only one selector in the list and no seed\n\t// (the latter of which guarantees us context)\n\tif ( match.length === 1 ) {\n\n\t\t// Reduce context if the leading compound selector is an ID\n\t\ttokens = match[0] = match[0].slice( 0 );\n\t\tif ( tokens.length > 2 && (token = tokens[0]).type === \"ID\" &&\n\t\t\t\tcontext.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[1].type ] ) {\n\n\t\t\tcontext = ( Expr.find[\"ID\"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];\n\t\t\tif ( !context ) {\n\t\t\t\treturn results;\n\n\t\t\t// Precompiled matchers will still verify ancestry, so step up a level\n\t\t\t} else if ( compiled ) {\n\t\t\t\tcontext = context.parentNode;\n\t\t\t}\n\n\t\t\tselector = selector.slice( tokens.shift().value.length );\n\t\t}\n\n\t\t// Fetch a seed set for right-to-left matching\n\t\ti = matchExpr[\"needsContext\"].test( selector ) ? 0 : tokens.length;\n\t\twhile ( i-- ) {\n\t\t\ttoken = tokens[i];\n\n\t\t\t// Abort if we hit a combinator\n\t\t\tif ( Expr.relative[ (type = token.type) ] ) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif ( (find = Expr.find[ type ]) ) {\n\t\t\t\t// Search, expanding context for leading sibling combinators\n\t\t\t\tif ( (seed = find(\n\t\t\t\t\ttoken.matches[0].replace( runescape, funescape ),\n\t\t\t\t\trsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context\n\t\t\t\t)) ) {\n\n\t\t\t\t\t// If seed is empty or no tokens remain, we can return early\n\t\t\t\t\ttokens.splice( i, 1 );\n\t\t\t\t\tselector = seed.length && toSelector( tokens );\n\t\t\t\t\tif ( !selector ) {\n\t\t\t\t\t\tpush.apply( results, seed );\n\t\t\t\t\t\treturn results;\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Compile and execute a filtering function if one is not provided\n\t// Provide `match` to avoid retokenization if we modified the selector above\n\t( compiled || compile( selector, match ) )(\n\t\tseed,\n\t\tcontext,\n\t\t!documentIsHTML,\n\t\tresults,\n\t\t!context || rsibling.test( selector ) && testContext( context.parentNode ) || context\n\t);\n\treturn results;\n};\n\n// One-time assignments\n\n// Sort stability\nsupport.sortStable = expando.split(\"\").sort( sortOrder ).join(\"\") === expando;\n\n// Support: Chrome 14-35+\n// Always assume duplicates if they aren't passed to the comparison function\nsupport.detectDuplicates = !!hasDuplicate;\n\n// Initialize against the default document\nsetDocument();\n\n// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)\n// Detached nodes confoundingly follow *each other*\nsupport.sortDetached = assert(function( el ) {\n\t// Should return 1, but returns 4 (following)\n\treturn el.compareDocumentPosition( document.createElement(\"fieldset\") ) & 1;\n});\n\n// Support: IE<8\n// Prevent attribute/property \"interpolation\"\n// https://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx\nif ( !assert(function( el ) {\n\tel.innerHTML = \"<a href='#'></a>\";\n\treturn el.firstChild.getAttribute(\"href\") === \"#\" ;\n}) ) {\n\taddHandle( \"type|href|height|width\", function( elem, name, isXML ) {\n\t\tif ( !isXML ) {\n\t\t\treturn elem.getAttribute( name, name.toLowerCase() === \"type\" ? 1 : 2 );\n\t\t}\n\t});\n}\n\n// Support: IE<9\n// Use defaultValue in place of getAttribute(\"value\")\nif ( !support.attributes || !assert(function( el ) {\n\tel.innerHTML = \"<input/>\";\n\tel.firstChild.setAttribute( \"value\", \"\" );\n\treturn el.firstChild.getAttribute( \"value\" ) === \"\";\n}) ) {\n\taddHandle( \"value\", function( elem, name, isXML ) {\n\t\tif ( !isXML && elem.nodeName.toLowerCase() === \"input\" ) {\n\t\t\treturn elem.defaultValue;\n\t\t}\n\t});\n}\n\n// Support: IE<9\n// Use getAttributeNode to fetch booleans when getAttribute lies\nif ( !assert(function( el ) {\n\treturn el.getAttribute(\"disabled\") == null;\n}) ) {\n\taddHandle( booleans, function( elem, name, isXML ) {\n\t\tvar val;\n\t\tif ( !isXML ) {\n\t\t\treturn elem[ name ] === true ? name.toLowerCase() :\n\t\t\t\t\t(val = elem.getAttributeNode( name )) && val.specified ?\n\t\t\t\t\tval.value :\n\t\t\t\tnull;\n\t\t}\n\t});\n}\n\nreturn Sizzle;\n\n})( window );\n\n\n\njQuery.find = Sizzle;\njQuery.expr = Sizzle.selectors;\n\n// Deprecated\njQuery.expr[ \":\" ] = jQuery.expr.pseudos;\njQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort;\njQuery.text = Sizzle.getText;\njQuery.isXMLDoc = Sizzle.isXML;\njQuery.contains = Sizzle.contains;\njQuery.escapeSelector = Sizzle.escape;\n\n\n\n\nvar dir = function( elem, dir, until ) {\n\tvar matched = [],\n\t\ttruncate = until !== undefined;\n\n\twhile ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) {\n\t\tif ( elem.nodeType === 1 ) {\n\t\t\tif ( truncate && jQuery( elem ).is( until ) ) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tmatched.push( elem );\n\t\t}\n\t}\n\treturn matched;\n};\n\n\nvar siblings = function( n, elem ) {\n\tvar matched = [];\n\n\tfor ( ; n; n = n.nextSibling ) {\n\t\tif ( n.nodeType === 1 && n !== elem ) {\n\t\t\tmatched.push( n );\n\t\t}\n\t}\n\n\treturn matched;\n};\n\n\nvar rneedsContext = jQuery.expr.match.needsContext;\n\n\n\nfunction nodeName( elem, name ) {\n\n return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();\n\n};\nvar rsingleTag = ( /^<([a-z][^\\/\\0>:\\x20\\t\\r\\n\\f]*)[\\x20\\t\\r\\n\\f]*\\/?>(?:<\\/\\1>|)$/i );\n\n\n\n// Implement the identical functionality for filter and not\nfunction winnow( elements, qualifier, not ) {\n\tif ( isFunction( qualifier ) ) {\n\t\treturn jQuery.grep( elements, function( elem, i ) {\n\t\t\treturn !!qualifier.call( elem, i, elem ) !== not;\n\t\t} );\n\t}\n\n\t// Single element\n\tif ( qualifier.nodeType ) {\n\t\treturn jQuery.grep( elements, function( elem ) {\n\t\t\treturn ( elem === qualifier ) !== not;\n\t\t} );\n\t}\n\n\t// Arraylike of elements (jQuery, arguments, Array)\n\tif ( typeof qualifier !== \"string\" ) {\n\t\treturn jQuery.grep( elements, function( elem ) {\n\t\t\treturn ( indexOf.call( qualifier, elem ) > -1 ) !== not;\n\t\t} );\n\t}\n\n\t// Filtered directly for both simple and complex selectors\n\treturn jQuery.filter( qualifier, elements, not );\n}\n\njQuery.filter = function( expr, elems, not ) {\n\tvar elem = elems[ 0 ];\n\n\tif ( not ) {\n\t\texpr = \":not(\" + expr + \")\";\n\t}\n\n\tif ( elems.length === 1 && elem.nodeType === 1 ) {\n\t\treturn jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [];\n\t}\n\n\treturn jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {\n\t\treturn elem.nodeType === 1;\n\t} ) );\n};\n\njQuery.fn.extend( {\n\tfind: function( selector ) {\n\t\tvar i, ret,\n\t\t\tlen = this.length,\n\t\t\tself = this;\n\n\t\tif ( typeof selector !== \"string\" ) {\n\t\t\treturn this.pushStack( jQuery( selector ).filter( function() {\n\t\t\t\tfor ( i = 0; i < len; i++ ) {\n\t\t\t\t\tif ( jQuery.contains( self[ i ], this ) ) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} ) );\n\t\t}\n\n\t\tret = this.pushStack( [] );\n\n\t\tfor ( i = 0; i < len; i++ ) {\n\t\t\tjQuery.find( selector, self[ i ], ret );\n\t\t}\n\n\t\treturn len > 1 ? jQuery.uniqueSort( ret ) : ret;\n\t},\n\tfilter: function( selector ) {\n\t\treturn this.pushStack( winnow( this, selector || [], false ) );\n\t},\n\tnot: function( selector ) {\n\t\treturn this.pushStack( winnow( this, selector || [], true ) );\n\t},\n\tis: function( selector ) {\n\t\treturn !!winnow(\n\t\t\tthis,\n\n\t\t\t// If this is a positional/relative selector, check membership in the returned set\n\t\t\t// so $(\"p:first\").is(\"p:last\") won't return true for a doc with two \"p\".\n\t\t\ttypeof selector === \"string\" && rneedsContext.test( selector ) ?\n\t\t\t\tjQuery( selector ) :\n\t\t\t\tselector || [],\n\t\t\tfalse\n\t\t).length;\n\t}\n} );\n\n\n// Initialize a jQuery object\n\n\n// A central reference to the root jQuery(document)\nvar rootjQuery,\n\n\t// A simple way to check for HTML strings\n\t// Prioritize #id over <tag> to avoid XSS via location.hash (#9521)\n\t// Strict HTML recognition (#11290: must start with <)\n\t// Shortcut simple #id case for speed\n\trquickExpr = /^(?:\\s*(<[\\w\\W]+>)[^>]*|#([\\w-]+))$/,\n\n\tinit = jQuery.fn.init = function( selector, context, root ) {\n\t\tvar match, elem;\n\n\t\t// HANDLE: $(\"\"), $(null), $(undefined), $(false)\n\t\tif ( !selector ) {\n\t\t\treturn this;\n\t\t}\n\n\t\t// Method init() accepts an alternate rootjQuery\n\t\t// so migrate can support jQuery.sub (gh-2101)\n\t\troot = root || rootjQuery;\n\n\t\t// Handle HTML strings\n\t\tif ( typeof selector === \"string\" ) {\n\t\t\tif ( selector[ 0 ] === \"<\" &&\n\t\t\t\tselector[ selector.length - 1 ] === \">\" &&\n\t\t\t\tselector.length >= 3 ) {\n\n\t\t\t\t// Assume that strings that start and end with <> are HTML and skip the regex check\n\t\t\t\tmatch = [ null, selector, null ];\n\n\t\t\t} else {\n\t\t\t\tmatch = rquickExpr.exec( selector );\n\t\t\t}\n\n\t\t\t// Match html or make sure no context is specified for #id\n\t\t\tif ( match && ( match[ 1 ] || !context ) ) {\n\n\t\t\t\t// HANDLE: $(html) -> $(array)\n\t\t\t\tif ( match[ 1 ] ) {\n\t\t\t\t\tcontext = context instanceof jQuery ? context[ 0 ] : context;\n\n\t\t\t\t\t// Option to run scripts is true for back-compat\n\t\t\t\t\t// Intentionally let the error be thrown if parseHTML is not present\n\t\t\t\t\tjQuery.merge( this, jQuery.parseHTML(\n\t\t\t\t\t\tmatch[ 1 ],\n\t\t\t\t\t\tcontext && context.nodeType ? context.ownerDocument || context : document,\n\t\t\t\t\t\ttrue\n\t\t\t\t\t) );\n\n\t\t\t\t\t// HANDLE: $(html, props)\n\t\t\t\t\tif ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) {\n\t\t\t\t\t\tfor ( match in context ) {\n\n\t\t\t\t\t\t\t// Properties of context are called as methods if possible\n\t\t\t\t\t\t\tif ( isFunction( this[ match ] ) ) {\n\t\t\t\t\t\t\t\tthis[ match ]( context[ match ] );\n\n\t\t\t\t\t\t\t// ...and otherwise set as attributes\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tthis.attr( match, context[ match ] );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn this;\n\n\t\t\t\t// HANDLE: $(#id)\n\t\t\t\t} else {\n\t\t\t\t\telem = document.getElementById( match[ 2 ] );\n\n\t\t\t\t\tif ( elem ) {\n\n\t\t\t\t\t\t// Inject the element directly into the jQuery object\n\t\t\t\t\t\tthis[ 0 ] = elem;\n\t\t\t\t\t\tthis.length = 1;\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\n\t\t\t// HANDLE: $(expr, $(...))\n\t\t\t} else if ( !context || context.jquery ) {\n\t\t\t\treturn ( context || root ).find( selector );\n\n\t\t\t// HANDLE: $(expr, context)\n\t\t\t// (which is just equivalent to: $(context).find(expr)\n\t\t\t} else {\n\t\t\t\treturn this.constructor( context ).find( selector );\n\t\t\t}\n\n\t\t// HANDLE: $(DOMElement)\n\t\t} else if ( selector.nodeType ) {\n\t\t\tthis[ 0 ] = selector;\n\t\t\tthis.length = 1;\n\t\t\treturn this;\n\n\t\t// HANDLE: $(function)\n\t\t// Shortcut for document ready\n\t\t} else if ( isFunction( selector ) ) {\n\t\t\treturn root.ready !== undefined ?\n\t\t\t\troot.ready( selector ) :\n\n\t\t\t\t// Execute immediately if ready is not present\n\t\t\t\tselector( jQuery );\n\t\t}\n\n\t\treturn jQuery.makeArray( selector, this );\n\t};\n\n// Give the init function the jQuery prototype for later instantiation\ninit.prototype = jQuery.fn;\n\n// Initialize central reference\nrootjQuery = jQuery( document );\n\n\nvar rparentsprev = /^(?:parents|prev(?:Until|All))/,\n\n\t// Methods guaranteed to produce a unique set when starting from a unique set\n\tguaranteedUnique = {\n\t\tchildren: true,\n\t\tcontents: true,\n\t\tnext: true,\n\t\tprev: true\n\t};\n\njQuery.fn.extend( {\n\thas: function( target ) {\n\t\tvar targets = jQuery( target, this ),\n\t\t\tl = targets.length;\n\n\t\treturn this.filter( function() {\n\t\t\tvar i = 0;\n\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\tif ( jQuery.contains( this, targets[ i ] ) ) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t} );\n\t},\n\n\tclosest: function( selectors, context ) {\n\t\tvar cur,\n\t\t\ti = 0,\n\t\t\tl = this.length,\n\t\t\tmatched = [],\n\t\t\ttargets = typeof selectors !== \"string\" && jQuery( selectors );\n\n\t\t// Positional selectors never match, since there's no _selection_ context\n\t\tif ( !rneedsContext.test( selectors ) ) {\n\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\tfor ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) {\n\n\t\t\t\t\t// Always skip document fragments\n\t\t\t\t\tif ( cur.nodeType < 11 && ( targets ?\n\t\t\t\t\t\ttargets.index( cur ) > -1 :\n\n\t\t\t\t\t\t// Don't pass non-elements to Sizzle\n\t\t\t\t\t\tcur.nodeType === 1 &&\n\t\t\t\t\t\t\tjQuery.find.matchesSelector( cur, selectors ) ) ) {\n\n\t\t\t\t\t\tmatched.push( cur );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched );\n\t},\n\n\t// Determine the position of an element within the set\n\tindex: function( elem ) {\n\n\t\t// No argument, return index in parent\n\t\tif ( !elem ) {\n\t\t\treturn ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1;\n\t\t}\n\n\t\t// Index in selector\n\t\tif ( typeof elem === \"string\" ) {\n\t\t\treturn indexOf.call( jQuery( elem ), this[ 0 ] );\n\t\t}\n\n\t\t// Locate the position of the desired element\n\t\treturn indexOf.call( this,\n\n\t\t\t// If it receives a jQuery object, the first element is used\n\t\t\telem.jquery ? elem[ 0 ] : elem\n\t\t);\n\t},\n\n\tadd: function( selector, context ) {\n\t\treturn this.pushStack(\n\t\t\tjQuery.uniqueSort(\n\t\t\t\tjQuery.merge( this.get(), jQuery( selector, context ) )\n\t\t\t)\n\t\t);\n\t},\n\n\taddBack: function( selector ) {\n\t\treturn this.add( selector == null ?\n\t\t\tthis.prevObject : this.prevObject.filter( selector )\n\t\t);\n\t}\n} );\n\nfunction sibling( cur, dir ) {\n\twhile ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {}\n\treturn cur;\n}\n\njQuery.each( {\n\tparent: function( elem ) {\n\t\tvar parent = elem.parentNode;\n\t\treturn parent && parent.nodeType !== 11 ? parent : null;\n\t},\n\tparents: function( elem ) {\n\t\treturn dir( elem, \"parentNode\" );\n\t},\n\tparentsUntil: function( elem, i, until ) {\n\t\treturn dir( elem, \"parentNode\", until );\n\t},\n\tnext: function( elem ) {\n\t\treturn sibling( elem, \"nextSibling\" );\n\t},\n\tprev: function( elem ) {\n\t\treturn sibling( elem, \"previousSibling\" );\n\t},\n\tnextAll: function( elem ) {\n\t\treturn dir( elem, \"nextSibling\" );\n\t},\n\tprevAll: function( elem ) {\n\t\treturn dir( elem, \"previousSibling\" );\n\t},\n\tnextUntil: function( elem, i, until ) {\n\t\treturn dir( elem, \"nextSibling\", until );\n\t},\n\tprevUntil: function( elem, i, until ) {\n\t\treturn dir( elem, \"previousSibling\", until );\n\t},\n\tsiblings: function( elem ) {\n\t\treturn siblings( ( elem.parentNode || {} ).firstChild, elem );\n\t},\n\tchildren: function( elem ) {\n\t\treturn siblings( elem.firstChild );\n\t},\n\tcontents: function( elem ) {\n\t\tif ( typeof elem.contentDocument !== \"undefined\" ) {\n\t\t\treturn elem.contentDocument;\n\t\t}\n\n\t\t// Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only\n\t\t// Treat the template element as a regular one in browsers that\n\t\t// don't support it.\n\t\tif ( nodeName( elem, \"template\" ) ) {\n\t\t\telem = elem.content || elem;\n\t\t}\n\n\t\treturn jQuery.merge( [], elem.childNodes );\n\t}\n}, function( name, fn ) {\n\tjQuery.fn[ name ] = function( until, selector ) {\n\t\tvar matched = jQuery.map( this, fn, until );\n\n\t\tif ( name.slice( -5 ) !== \"Until\" ) {\n\t\t\tselector = until;\n\t\t}\n\n\t\tif ( selector && typeof selector === \"string\" ) {\n\t\t\tmatched = jQuery.filter( selector, matched );\n\t\t}\n\n\t\tif ( this.length > 1 ) {\n\n\t\t\t// Remove duplicates\n\t\t\tif ( !guaranteedUnique[ name ] ) {\n\t\t\t\tjQuery.uniqueSort( matched );\n\t\t\t}\n\n\t\t\t// Reverse order for parents* and prev-derivatives\n\t\t\tif ( rparentsprev.test( name ) ) {\n\t\t\t\tmatched.reverse();\n\t\t\t}\n\t\t}\n\n\t\treturn this.pushStack( matched );\n\t};\n} );\nvar rnothtmlwhite = ( /[^\\x20\\t\\r\\n\\f]+/g );\n\n\n\n// Convert String-formatted options into Object-formatted ones\nfunction createOptions( options ) {\n\tvar object = {};\n\tjQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) {\n\t\tobject[ flag ] = true;\n\t} );\n\treturn object;\n}\n\n/*\n * Create a callback list using the following parameters:\n *\n *\toptions: an optional list of space-separated options that will change how\n *\t\t\tthe callback list behaves or a more traditional option object\n *\n * By default a callback list will act like an event callback list and can be\n * \"fired\" multiple times.\n *\n * Possible options:\n *\n *\tonce:\t\t\twill ensure the callback list can only be fired once (like a Deferred)\n *\n *\tmemory:\t\t\twill keep track of previous values and will call any callback added\n *\t\t\t\t\tafter the list has been fired right away with the latest \"memorized\"\n *\t\t\t\t\tvalues (like a Deferred)\n *\n *\tunique:\t\t\twill ensure a callback can only be added once (no duplicate in the list)\n *\n *\tstopOnFalse:\tinterrupt callings when a callback returns false\n *\n */\njQuery.Callbacks = function( options ) {\n\n\t// Convert options from String-formatted to Object-formatted if needed\n\t// (we check in cache first)\n\toptions = typeof options === \"string\" ?\n\t\tcreateOptions( options ) :\n\t\tjQuery.extend( {}, options );\n\n\tvar // Flag to know if list is currently firing\n\t\tfiring,\n\n\t\t// Last fire value for non-forgettable lists\n\t\tmemory,\n\n\t\t// Flag to know if list was already fired\n\t\tfired,\n\n\t\t// Flag to prevent firing\n\t\tlocked,\n\n\t\t// Actual callback list\n\t\tlist = [],\n\n\t\t// Queue of execution data for repeatable lists\n\t\tqueue = [],\n\n\t\t// Index of currently firing callback (modified by add/remove as needed)\n\t\tfiringIndex = -1,\n\n\t\t// Fire callbacks\n\t\tfire = function() {\n\n\t\t\t// Enforce single-firing\n\t\t\tlocked = locked || options.once;\n\n\t\t\t// Execute callbacks for all pending executions,\n\t\t\t// respecting firingIndex overrides and runtime changes\n\t\t\tfired = firing = true;\n\t\t\tfor ( ; queue.length; firingIndex = -1 ) {\n\t\t\t\tmemory = queue.shift();\n\t\t\t\twhile ( ++firingIndex < list.length ) {\n\n\t\t\t\t\t// Run callback and check for early termination\n\t\t\t\t\tif ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false &&\n\t\t\t\t\t\toptions.stopOnFalse ) {\n\n\t\t\t\t\t\t// Jump to end and forget the data so .add doesn't re-fire\n\t\t\t\t\t\tfiringIndex = list.length;\n\t\t\t\t\t\tmemory = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Forget the data if we're done with it\n\t\t\tif ( !options.memory ) {\n\t\t\t\tmemory = false;\n\t\t\t}\n\n\t\t\tfiring = false;\n\n\t\t\t// Clean up if we're done firing for good\n\t\t\tif ( locked ) {\n\n\t\t\t\t// Keep an empty list if we have data for future add calls\n\t\t\t\tif ( memory ) {\n\t\t\t\t\tlist = [];\n\n\t\t\t\t// Otherwise, this object is spent\n\t\t\t\t} else {\n\t\t\t\t\tlist = \"\";\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t// Actual Callbacks object\n\t\tself = {\n\n\t\t\t// Add a callback or a collection of callbacks to the list\n\t\t\tadd: function() {\n\t\t\t\tif ( list ) {\n\n\t\t\t\t\t// If we have memory from a past run, we should fire after adding\n\t\t\t\t\tif ( memory && !firing ) {\n\t\t\t\t\t\tfiringIndex = list.length - 1;\n\t\t\t\t\t\tqueue.push( memory );\n\t\t\t\t\t}\n\n\t\t\t\t\t( function add( args ) {\n\t\t\t\t\t\tjQuery.each( args, function( _, arg ) {\n\t\t\t\t\t\t\tif ( isFunction( arg ) ) {\n\t\t\t\t\t\t\t\tif ( !options.unique || !self.has( arg ) ) {\n\t\t\t\t\t\t\t\t\tlist.push( arg );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else if ( arg && arg.length && toType( arg ) !== \"string\" ) {\n\n\t\t\t\t\t\t\t\t// Inspect recursively\n\t\t\t\t\t\t\t\tadd( arg );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} );\n\t\t\t\t\t} )( arguments );\n\n\t\t\t\t\tif ( memory && !firing ) {\n\t\t\t\t\t\tfire();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// Remove a callback from the list\n\t\t\tremove: function() {\n\t\t\t\tjQuery.each( arguments, function( _, arg ) {\n\t\t\t\t\tvar index;\n\t\t\t\t\twhile ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {\n\t\t\t\t\t\tlist.splice( index, 1 );\n\n\t\t\t\t\t\t// Handle firing indexes\n\t\t\t\t\t\tif ( index <= firingIndex ) {\n\t\t\t\t\t\t\tfiringIndex--;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// Check if a given callback is in the list.\n\t\t\t// If no argument is given, return whether or not list has callbacks attached.\n\t\t\thas: function( fn ) {\n\t\t\t\treturn fn ?\n\t\t\t\t\tjQuery.inArray( fn, list ) > -1 :\n\t\t\t\t\tlist.length > 0;\n\t\t\t},\n\n\t\t\t// Remove all callbacks from the list\n\t\t\tempty: function() {\n\t\t\t\tif ( list ) {\n\t\t\t\t\tlist = [];\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// Disable .fire and .add\n\t\t\t// Abort any current/pending executions\n\t\t\t// Clear all callbacks and values\n\t\t\tdisable: function() {\n\t\t\t\tlocked = queue = [];\n\t\t\t\tlist = memory = \"\";\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\tdisabled: function() {\n\t\t\t\treturn !list;\n\t\t\t},\n\n\t\t\t// Disable .fire\n\t\t\t// Also disable .add unless we have memory (since it would have no effect)\n\t\t\t// Abort any pending executions\n\t\t\tlock: function() {\n\t\t\t\tlocked = queue = [];\n\t\t\t\tif ( !memory && !firing ) {\n\t\t\t\t\tlist = memory = \"\";\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\tlocked: function() {\n\t\t\t\treturn !!locked;\n\t\t\t},\n\n\t\t\t// Call all callbacks with the given context and arguments\n\t\t\tfireWith: function( context, args ) {\n\t\t\t\tif ( !locked ) {\n\t\t\t\t\targs = args || [];\n\t\t\t\t\targs = [ context, args.slice ? args.slice() : args ];\n\t\t\t\t\tqueue.push( args );\n\t\t\t\t\tif ( !firing ) {\n\t\t\t\t\t\tfire();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// Call all the callbacks with the given arguments\n\t\t\tfire: function() {\n\t\t\t\tself.fireWith( this, arguments );\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// To know if the callbacks have already been called at least once\n\t\t\tfired: function() {\n\t\t\t\treturn !!fired;\n\t\t\t}\n\t\t};\n\n\treturn self;\n};\n\n\nfunction Identity( v ) {\n\treturn v;\n}\nfunction Thrower( ex ) {\n\tthrow ex;\n}\n\nfunction adoptValue( value, resolve, reject, noValue ) {\n\tvar method;\n\n\ttry {\n\n\t\t// Check for promise aspect first to privilege synchronous behavior\n\t\tif ( value && isFunction( ( method = value.promise ) ) ) {\n\t\t\tmethod.call( value ).done( resolve ).fail( reject );\n\n\t\t// Other thenables\n\t\t} else if ( value && isFunction( ( method = value.then ) ) ) {\n\t\t\tmethod.call( value, resolve, reject );\n\n\t\t// Other non-thenables\n\t\t} else {\n\n\t\t\t// Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer:\n\t\t\t// * false: [ value ].slice( 0 ) => resolve( value )\n\t\t\t// * true: [ value ].slice( 1 ) => resolve()\n\t\t\tresolve.apply( undefined, [ value ].slice( noValue ) );\n\t\t}\n\n\t// For Promises/A+, convert exceptions into rejections\n\t// Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in\n\t// Deferred#then to conditionally suppress rejection.\n\t} catch ( value ) {\n\n\t\t// Support: Android 4.0 only\n\t\t// Strict mode functions invoked without .call/.apply get global-object context\n\t\treject.apply( undefined, [ value ] );\n\t}\n}\n\njQuery.extend( {\n\n\tDeferred: function( func ) {\n\t\tvar tuples = [\n\n\t\t\t\t// action, add listener, callbacks,\n\t\t\t\t// ... .then handlers, argument index, [final state]\n\t\t\t\t[ \"notify\", \"progress\", jQuery.Callbacks( \"memory\" ),\n\t\t\t\t\tjQuery.Callbacks( \"memory\" ), 2 ],\n\t\t\t\t[ \"resolve\", \"done\", jQuery.Callbacks( \"once memory\" ),\n\t\t\t\t\tjQuery.Callbacks( \"once memory\" ), 0, \"resolved\" ],\n\t\t\t\t[ \"reject\", \"fail\", jQuery.Callbacks( \"once memory\" ),\n\t\t\t\t\tjQuery.Callbacks( \"once memory\" ), 1, \"rejected\" ]\n\t\t\t],\n\t\t\tstate = \"pending\",\n\t\t\tpromise = {\n\t\t\t\tstate: function() {\n\t\t\t\t\treturn state;\n\t\t\t\t},\n\t\t\t\talways: function() {\n\t\t\t\t\tdeferred.done( arguments ).fail( arguments );\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\t\t\t\t\"catch\": function( fn ) {\n\t\t\t\t\treturn promise.then( null, fn );\n\t\t\t\t},\n\n\t\t\t\t// Keep pipe for back-compat\n\t\t\t\tpipe: function( /* fnDone, fnFail, fnProgress */ ) {\n\t\t\t\t\tvar fns = arguments;\n\n\t\t\t\t\treturn jQuery.Deferred( function( newDefer ) {\n\t\t\t\t\t\tjQuery.each( tuples, function( i, tuple ) {\n\n\t\t\t\t\t\t\t// Map tuples (progress, done, fail) to arguments (done, fail, progress)\n\t\t\t\t\t\t\tvar fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ];\n\n\t\t\t\t\t\t\t// deferred.progress(function() { bind to newDefer or newDefer.notify })\n\t\t\t\t\t\t\t// deferred.done(function() { bind to newDefer or newDefer.resolve })\n\t\t\t\t\t\t\t// deferred.fail(function() { bind to newDefer or newDefer.reject })\n\t\t\t\t\t\t\tdeferred[ tuple[ 1 ] ]( function() {\n\t\t\t\t\t\t\t\tvar returned = fn && fn.apply( this, arguments );\n\t\t\t\t\t\t\t\tif ( returned && isFunction( returned.promise ) ) {\n\t\t\t\t\t\t\t\t\treturned.promise()\n\t\t\t\t\t\t\t\t\t\t.progress( newDefer.notify )\n\t\t\t\t\t\t\t\t\t\t.done( newDefer.resolve )\n\t\t\t\t\t\t\t\t\t\t.fail( newDefer.reject );\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tnewDefer[ tuple[ 0 ] + \"With\" ](\n\t\t\t\t\t\t\t\t\t\tthis,\n\t\t\t\t\t\t\t\t\t\tfn ? [ returned ] : arguments\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t} );\n\t\t\t\t\t\tfns = null;\n\t\t\t\t\t} ).promise();\n\t\t\t\t},\n\t\t\t\tthen: function( onFulfilled, onRejected, onProgress ) {\n\t\t\t\t\tvar maxDepth = 0;\n\t\t\t\t\tfunction resolve( depth, deferred, handler, special ) {\n\t\t\t\t\t\treturn function() {\n\t\t\t\t\t\t\tvar that = this,\n\t\t\t\t\t\t\t\targs = arguments,\n\t\t\t\t\t\t\t\tmightThrow = function() {\n\t\t\t\t\t\t\t\t\tvar returned, then;\n\n\t\t\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.3.3.3\n\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-59\n\t\t\t\t\t\t\t\t\t// Ignore double-resolution attempts\n\t\t\t\t\t\t\t\t\tif ( depth < maxDepth ) {\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\treturned = handler.apply( that, args );\n\n\t\t\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.1\n\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-48\n\t\t\t\t\t\t\t\t\tif ( returned === deferred.promise() ) {\n\t\t\t\t\t\t\t\t\t\tthrow new TypeError( \"Thenable self-resolution\" );\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t// Support: Promises/A+ sections 2.3.3.1, 3.5\n\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-54\n\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-75\n\t\t\t\t\t\t\t\t\t// Retrieve `then` only once\n\t\t\t\t\t\t\t\t\tthen = returned &&\n\n\t\t\t\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.4\n\t\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-64\n\t\t\t\t\t\t\t\t\t\t// Only check objects and functions for thenability\n\t\t\t\t\t\t\t\t\t\t( typeof returned === \"object\" ||\n\t\t\t\t\t\t\t\t\t\t\ttypeof returned === \"function\" ) &&\n\t\t\t\t\t\t\t\t\t\treturned.then;\n\n\t\t\t\t\t\t\t\t\t// Handle a returned thenable\n\t\t\t\t\t\t\t\t\tif ( isFunction( then ) ) {\n\n\t\t\t\t\t\t\t\t\t\t// Special processors (notify) just wait for resolution\n\t\t\t\t\t\t\t\t\t\tif ( special ) {\n\t\t\t\t\t\t\t\t\t\t\tthen.call(\n\t\t\t\t\t\t\t\t\t\t\t\treturned,\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Identity, special ),\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Thrower, special )\n\t\t\t\t\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\t\t\t\t// Normal processors (resolve) also hook into progress\n\t\t\t\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\t\t\t\t// ...and disregard older resolution values\n\t\t\t\t\t\t\t\t\t\t\tmaxDepth++;\n\n\t\t\t\t\t\t\t\t\t\t\tthen.call(\n\t\t\t\t\t\t\t\t\t\t\t\treturned,\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Identity, special ),\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Thrower, special ),\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Identity,\n\t\t\t\t\t\t\t\t\t\t\t\t\tdeferred.notifyWith )\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t// Handle all other returned values\n\t\t\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\t\t\t// Only substitute handlers pass on context\n\t\t\t\t\t\t\t\t\t\t// and multiple values (non-spec behavior)\n\t\t\t\t\t\t\t\t\t\tif ( handler !== Identity ) {\n\t\t\t\t\t\t\t\t\t\t\tthat = undefined;\n\t\t\t\t\t\t\t\t\t\t\targs = [ returned ];\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\t// Process the value(s)\n\t\t\t\t\t\t\t\t\t\t// Default process is resolve\n\t\t\t\t\t\t\t\t\t\t( special || deferred.resolveWith )( that, args );\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\n\t\t\t\t\t\t\t\t// Only normal processors (resolve) catch and reject exceptions\n\t\t\t\t\t\t\t\tprocess = special ?\n\t\t\t\t\t\t\t\t\tmightThrow :\n\t\t\t\t\t\t\t\t\tfunction() {\n\t\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\t\tmightThrow();\n\t\t\t\t\t\t\t\t\t\t} catch ( e ) {\n\n\t\t\t\t\t\t\t\t\t\t\tif ( jQuery.Deferred.exceptionHook ) {\n\t\t\t\t\t\t\t\t\t\t\t\tjQuery.Deferred.exceptionHook( e,\n\t\t\t\t\t\t\t\t\t\t\t\t\tprocess.stackTrace );\n\t\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.3.3.4.1\n\t\t\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-61\n\t\t\t\t\t\t\t\t\t\t\t// Ignore post-resolution exceptions\n\t\t\t\t\t\t\t\t\t\t\tif ( depth + 1 >= maxDepth ) {\n\n\t\t\t\t\t\t\t\t\t\t\t\t// Only substitute handlers pass on context\n\t\t\t\t\t\t\t\t\t\t\t\t// and multiple values (non-spec behavior)\n\t\t\t\t\t\t\t\t\t\t\t\tif ( handler !== Thrower ) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tthat = undefined;\n\t\t\t\t\t\t\t\t\t\t\t\t\targs = [ e ];\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\t\t\tdeferred.rejectWith( that, args );\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t};\n\n\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.3.3.1\n\t\t\t\t\t\t\t// https://promisesaplus.com/#point-57\n\t\t\t\t\t\t\t// Re-resolve promises immediately to dodge false rejection from\n\t\t\t\t\t\t\t// subsequent errors\n\t\t\t\t\t\t\tif ( depth ) {\n\t\t\t\t\t\t\t\tprocess();\n\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\t// Call an optional hook to record the stack, in case of exception\n\t\t\t\t\t\t\t\t// since it's otherwise lost when execution goes async\n\t\t\t\t\t\t\t\tif ( jQuery.Deferred.getStackHook ) {\n\t\t\t\t\t\t\t\t\tprocess.stackTrace = jQuery.Deferred.getStackHook();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\twindow.setTimeout( process );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\n\t\t\t\t\treturn jQuery.Deferred( function( newDefer ) {\n\n\t\t\t\t\t\t// progress_handlers.add( ... )\n\t\t\t\t\t\ttuples[ 0 ][ 3 ].add(\n\t\t\t\t\t\t\tresolve(\n\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\tnewDefer,\n\t\t\t\t\t\t\t\tisFunction( onProgress ) ?\n\t\t\t\t\t\t\t\t\tonProgress :\n\t\t\t\t\t\t\t\t\tIdentity,\n\t\t\t\t\t\t\t\tnewDefer.notifyWith\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\t// fulfilled_handlers.add( ... )\n\t\t\t\t\t\ttuples[ 1 ][ 3 ].add(\n\t\t\t\t\t\t\tresolve(\n\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\tnewDefer,\n\t\t\t\t\t\t\t\tisFunction( onFulfilled ) ?\n\t\t\t\t\t\t\t\t\tonFulfilled :\n\t\t\t\t\t\t\t\t\tIdentity\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\t// rejected_handlers.add( ... )\n\t\t\t\t\t\ttuples[ 2 ][ 3 ].add(\n\t\t\t\t\t\t\tresolve(\n\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\tnewDefer,\n\t\t\t\t\t\t\t\tisFunction( onRejected ) ?\n\t\t\t\t\t\t\t\t\tonRejected :\n\t\t\t\t\t\t\t\t\tThrower\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\t\t\t\t\t} ).promise();\n\t\t\t\t},\n\n\t\t\t\t// Get a promise for this deferred\n\t\t\t\t// If obj is provided, the promise aspect is added to the object\n\t\t\t\tpromise: function( obj ) {\n\t\t\t\t\treturn obj != null ? jQuery.extend( obj, promise ) : promise;\n\t\t\t\t}\n\t\t\t},\n\t\t\tdeferred = {};\n\n\t\t// Add list-specific methods\n\t\tjQuery.each( tuples, function( i, tuple ) {\n\t\t\tvar list = tuple[ 2 ],\n\t\t\t\tstateString = tuple[ 5 ];\n\n\t\t\t// promise.progress = list.add\n\t\t\t// promise.done = list.add\n\t\t\t// promise.fail = list.add\n\t\t\tpromise[ tuple[ 1 ] ] = list.add;\n\n\t\t\t// Handle state\n\t\t\tif ( stateString ) {\n\t\t\t\tlist.add(\n\t\t\t\t\tfunction() {\n\n\t\t\t\t\t\t// state = \"resolved\" (i.e., fulfilled)\n\t\t\t\t\t\t// state = \"rejected\"\n\t\t\t\t\t\tstate = stateString;\n\t\t\t\t\t},\n\n\t\t\t\t\t// rejected_callbacks.disable\n\t\t\t\t\t// fulfilled_callbacks.disable\n\t\t\t\t\ttuples[ 3 - i ][ 2 ].disable,\n\n\t\t\t\t\t// rejected_handlers.disable\n\t\t\t\t\t// fulfilled_handlers.disable\n\t\t\t\t\ttuples[ 3 - i ][ 3 ].disable,\n\n\t\t\t\t\t// progress_callbacks.lock\n\t\t\t\t\ttuples[ 0 ][ 2 ].lock,\n\n\t\t\t\t\t// progress_handlers.lock\n\t\t\t\t\ttuples[ 0 ][ 3 ].lock\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// progress_handlers.fire\n\t\t\t// fulfilled_handlers.fire\n\t\t\t// rejected_handlers.fire\n\t\t\tlist.add( tuple[ 3 ].fire );\n\n\t\t\t// deferred.notify = function() { deferred.notifyWith(...) }\n\t\t\t// deferred.resolve = function() { deferred.resolveWith(...) }\n\t\t\t// deferred.reject = function() { deferred.rejectWith(...) }\n\t\t\tdeferred[ tuple[ 0 ] ] = function() {\n\t\t\t\tdeferred[ tuple[ 0 ] + \"With\" ]( this === deferred ? undefined : this, arguments );\n\t\t\t\treturn this;\n\t\t\t};\n\n\t\t\t// deferred.notifyWith = list.fireWith\n\t\t\t// deferred.resolveWith = list.fireWith\n\t\t\t// deferred.rejectWith = list.fireWith\n\t\t\tdeferred[ tuple[ 0 ] + \"With\" ] = list.fireWith;\n\t\t} );\n\n\t\t// Make the deferred a promise\n\t\tpromise.promise( deferred );\n\n\t\t// Call given func if any\n\t\tif ( func ) {\n\t\t\tfunc.call( deferred, deferred );\n\t\t}\n\n\t\t// All done!\n\t\treturn deferred;\n\t},\n\n\t// Deferred helper\n\twhen: function( singleValue ) {\n\t\tvar\n\n\t\t\t// count of uncompleted subordinates\n\t\t\tremaining = arguments.length,\n\n\t\t\t// count of unprocessed arguments\n\t\t\ti = remaining,\n\n\t\t\t// subordinate fulfillment data\n\t\t\tresolveContexts = Array( i ),\n\t\t\tresolveValues = slice.call( arguments ),\n\n\t\t\t// the master Deferred\n\t\t\tmaster = jQuery.Deferred(),\n\n\t\t\t// subordinate callback factory\n\t\t\tupdateFunc = function( i ) {\n\t\t\t\treturn function( value ) {\n\t\t\t\t\tresolveContexts[ i ] = this;\n\t\t\t\t\tresolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;\n\t\t\t\t\tif ( !( --remaining ) ) {\n\t\t\t\t\t\tmaster.resolveWith( resolveContexts, resolveValues );\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t};\n\n\t\t// Single- and empty arguments are adopted like Promise.resolve\n\t\tif ( remaining <= 1 ) {\n\t\t\tadoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject,\n\t\t\t\t!remaining );\n\n\t\t\t// Use .then() to unwrap secondary thenables (cf. gh-3000)\n\t\t\tif ( master.state() === \"pending\" ||\n\t\t\t\tisFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) {\n\n\t\t\t\treturn master.then();\n\t\t\t}\n\t\t}\n\n\t\t// Multiple arguments are aggregated like Promise.all array elements\n\t\twhile ( i-- ) {\n\t\t\tadoptValue( resolveValues[ i ], updateFunc( i ), master.reject );\n\t\t}\n\n\t\treturn master.promise();\n\t}\n} );\n\n\n// These usually indicate a programmer mistake during development,\n// warn about them ASAP rather than swallowing them by default.\nvar rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;\n\njQuery.Deferred.exceptionHook = function( error, stack ) {\n\n\t// Support: IE 8 - 9 only\n\t// Console exists when dev tools are open, which can happen at any time\n\tif ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) {\n\t\twindow.console.warn( \"jQuery.Deferred exception: \" + error.message, error.stack, stack );\n\t}\n};\n\n\n\n\njQuery.readyException = function( error ) {\n\twindow.setTimeout( function() {\n\t\tthrow error;\n\t} );\n};\n\n\n\n\n// The deferred used on DOM ready\nvar readyList = jQuery.Deferred();\n\njQuery.fn.ready = function( fn ) {\n\n\treadyList\n\t\t.then( fn )\n\n\t\t// Wrap jQuery.readyException in a function so that the lookup\n\t\t// happens at the time of error handling instead of callback\n\t\t// registration.\n\t\t.catch( function( error ) {\n\t\t\tjQuery.readyException( error );\n\t\t} );\n\n\treturn this;\n};\n\njQuery.extend( {\n\n\t// Is the DOM ready to be used? Set to true once it occurs.\n\tisReady: false,\n\n\t// A counter to track how many items to wait for before\n\t// the ready event fires. See #6781\n\treadyWait: 1,\n\n\t// Handle when the DOM is ready\n\tready: function( wait ) {\n\n\t\t// Abort if there are pending holds or we're already ready\n\t\tif ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Remember that the DOM is ready\n\t\tjQuery.isReady = true;\n\n\t\t// If a normal DOM Ready event fired, decrement, and wait if need be\n\t\tif ( wait !== true && --jQuery.readyWait > 0 ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// If there are functions bound, to execute\n\t\treadyList.resolveWith( document, [ jQuery ] );\n\t}\n} );\n\njQuery.ready.then = readyList.then;\n\n// The ready event handler and self cleanup method\nfunction completed() {\n\tdocument.removeEventListener( \"DOMContentLoaded\", completed );\n\twindow.removeEventListener( \"load\", completed );\n\tjQuery.ready();\n}\n\n// Catch cases where $(document).ready() is called\n// after the browser event has already occurred.\n// Support: IE <=9 - 10 only\n// Older IE sometimes signals \"interactive\" too soon\nif ( document.readyState === \"complete\" ||\n\t( document.readyState !== \"loading\" && !document.documentElement.doScroll ) ) {\n\n\t// Handle it asynchronously to allow scripts the opportunity to delay ready\n\twindow.setTimeout( jQuery.ready );\n\n} else {\n\n\t// Use the handy event callback\n\tdocument.addEventListener( \"DOMContentLoaded\", completed );\n\n\t// A fallback to window.onload, that will always work\n\twindow.addEventListener( \"load\", completed );\n}\n\n\n\n\n// Multifunctional method to get and set values of a collection\n// The value/s can optionally be executed if it's a function\nvar access = function( elems, fn, key, value, chainable, emptyGet, raw ) {\n\tvar i = 0,\n\t\tlen = elems.length,\n\t\tbulk = key == null;\n\n\t// Sets many values\n\tif ( toType( key ) === \"object\" ) {\n\t\tchainable = true;\n\t\tfor ( i in key ) {\n\t\t\taccess( elems, fn, i, key[ i ], true, emptyGet, raw );\n\t\t}\n\n\t// Sets one value\n\t} else if ( value !== undefined ) {\n\t\tchainable = true;\n\n\t\tif ( !isFunction( value ) ) {\n\t\t\traw = true;\n\t\t}\n\n\t\tif ( bulk ) {\n\n\t\t\t// Bulk operations run against the entire set\n\t\t\tif ( raw ) {\n\t\t\t\tfn.call( elems, value );\n\t\t\t\tfn = null;\n\n\t\t\t// ...except when executing function values\n\t\t\t} else {\n\t\t\t\tbulk = fn;\n\t\t\t\tfn = function( elem, key, value ) {\n\t\t\t\t\treturn bulk.call( jQuery( elem ), value );\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tif ( fn ) {\n\t\t\tfor ( ; i < len; i++ ) {\n\t\t\t\tfn(\n\t\t\t\t\telems[ i ], key, raw ?\n\t\t\t\t\tvalue :\n\t\t\t\t\tvalue.call( elems[ i ], i, fn( elems[ i ], key ) )\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\tif ( chainable ) {\n\t\treturn elems;\n\t}\n\n\t// Gets\n\tif ( bulk ) {\n\t\treturn fn.call( elems );\n\t}\n\n\treturn len ? fn( elems[ 0 ], key ) : emptyGet;\n};\n\n\n// Matches dashed string for camelizing\nvar rmsPrefix = /^-ms-/,\n\trdashAlpha = /-([a-z])/g;\n\n// Used by camelCase as callback to replace()\nfunction fcamelCase( all, letter ) {\n\treturn letter.toUpperCase();\n}\n\n// Convert dashed to camelCase; used by the css and data modules\n// Support: IE <=9 - 11, Edge 12 - 15\n// Microsoft forgot to hump their vendor prefix (#9572)\nfunction camelCase( string ) {\n\treturn string.replace( rmsPrefix, \"ms-\" ).replace( rdashAlpha, fcamelCase );\n}\nvar acceptData = function( owner ) {\n\n\t// Accepts only:\n\t// - Node\n\t// - Node.ELEMENT_NODE\n\t// - Node.DOCUMENT_NODE\n\t// - Object\n\t// - Any\n\treturn owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType );\n};\n\n\n\n\nfunction Data() {\n\tthis.expando = jQuery.expando + Data.uid++;\n}\n\nData.uid = 1;\n\nData.prototype = {\n\n\tcache: function( owner ) {\n\n\t\t// Check if the owner object already has a cache\n\t\tvar value = owner[ this.expando ];\n\n\t\t// If not, create one\n\t\tif ( !value ) {\n\t\t\tvalue = {};\n\n\t\t\t// We can accept data for non-element nodes in modern browsers,\n\t\t\t// but we should not, see #8335.\n\t\t\t// Always return an empty object.\n\t\t\tif ( acceptData( owner ) ) {\n\n\t\t\t\t// If it is a node unlikely to be stringify-ed or looped over\n\t\t\t\t// use plain assignment\n\t\t\t\tif ( owner.nodeType ) {\n\t\t\t\t\towner[ this.expando ] = value;\n\n\t\t\t\t// Otherwise secure it in a non-enumerable property\n\t\t\t\t// configurable must be true to allow the property to be\n\t\t\t\t// deleted when data is removed\n\t\t\t\t} else {\n\t\t\t\t\tObject.defineProperty( owner, this.expando, {\n\t\t\t\t\t\tvalue: value,\n\t\t\t\t\t\tconfigurable: true\n\t\t\t\t\t} );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn value;\n\t},\n\tset: function( owner, data, value ) {\n\t\tvar prop,\n\t\t\tcache = this.cache( owner );\n\n\t\t// Handle: [ owner, key, value ] args\n\t\t// Always use camelCase key (gh-2257)\n\t\tif ( typeof data === \"string\" ) {\n\t\t\tcache[ camelCase( data ) ] = value;\n\n\t\t// Handle: [ owner, { properties } ] args\n\t\t} else {\n\n\t\t\t// Copy the properties one-by-one to the cache object\n\t\t\tfor ( prop in data ) {\n\t\t\t\tcache[ camelCase( prop ) ] = data[ prop ];\n\t\t\t}\n\t\t}\n\t\treturn cache;\n\t},\n\tget: function( owner, key ) {\n\t\treturn key === undefined ?\n\t\t\tthis.cache( owner ) :\n\n\t\t\t// Always use camelCase key (gh-2257)\n\t\t\towner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ];\n\t},\n\taccess: function( owner, key, value ) {\n\n\t\t// In cases where either:\n\t\t//\n\t\t// 1. No key was specified\n\t\t// 2. A string key was specified, but no value provided\n\t\t//\n\t\t// Take the \"read\" path and allow the get method to determine\n\t\t// which value to return, respectively either:\n\t\t//\n\t\t// 1. The entire cache object\n\t\t// 2. The data stored at the key\n\t\t//\n\t\tif ( key === undefined ||\n\t\t\t\t( ( key && typeof key === \"string\" ) && value === undefined ) ) {\n\n\t\t\treturn this.get( owner, key );\n\t\t}\n\n\t\t// When the key is not a string, or both a key and value\n\t\t// are specified, set or extend (existing objects) with either:\n\t\t//\n\t\t// 1. An object of properties\n\t\t// 2. A key and value\n\t\t//\n\t\tthis.set( owner, key, value );\n\n\t\t// Since the \"set\" path can have two possible entry points\n\t\t// return the expected data based on which path was taken[*]\n\t\treturn value !== undefined ? value : key;\n\t},\n\tremove: function( owner, key ) {\n\t\tvar i,\n\t\t\tcache = owner[ this.expando ];\n\n\t\tif ( cache === undefined ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( key !== undefined ) {\n\n\t\t\t// Support array or space separated string of keys\n\t\t\tif ( Array.isArray( key ) ) {\n\n\t\t\t\t// If key is an array of keys...\n\t\t\t\t// We always set camelCase keys, so remove that.\n\t\t\t\tkey = key.map( camelCase );\n\t\t\t} else {\n\t\t\t\tkey = camelCase( key );\n\n\t\t\t\t// If a key with the spaces exists, use it.\n\t\t\t\t// Otherwise, create an array by matching non-whitespace\n\t\t\t\tkey = key in cache ?\n\t\t\t\t\t[ key ] :\n\t\t\t\t\t( key.match( rnothtmlwhite ) || [] );\n\t\t\t}\n\n\t\t\ti = key.length;\n\n\t\t\twhile ( i-- ) {\n\t\t\t\tdelete cache[ key[ i ] ];\n\t\t\t}\n\t\t}\n\n\t\t// Remove the expando if there's no more data\n\t\tif ( key === undefined || jQuery.isEmptyObject( cache ) ) {\n\n\t\t\t// Support: Chrome <=35 - 45\n\t\t\t// Webkit & Blink performance suffers when deleting properties\n\t\t\t// from DOM nodes, so set to undefined instead\n\t\t\t// https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted)\n\t\t\tif ( owner.nodeType ) {\n\t\t\t\towner[ this.expando ] = undefined;\n\t\t\t} else {\n\t\t\t\tdelete owner[ this.expando ];\n\t\t\t}\n\t\t}\n\t},\n\thasData: function( owner ) {\n\t\tvar cache = owner[ this.expando ];\n\t\treturn cache !== undefined && !jQuery.isEmptyObject( cache );\n\t}\n};\nvar dataPriv = new Data();\n\nvar dataUser = new Data();\n\n\n\n//\tImplementation Summary\n//\n//\t1. Enforce API surface and semantic compatibility with 1.9.x branch\n//\t2. Improve the module's maintainability by reducing the storage\n//\t\tpaths to a single mechanism.\n//\t3. Use the same single mechanism to support \"private\" and \"user\" data.\n//\t4. _Never_ expose \"private\" data to user code (TODO: Drop _data, _removeData)\n//\t5. Avoid exposing implementation details on user objects (eg. expando properties)\n//\t6. Provide a clear path for implementation upgrade to WeakMap in 2014\n\nvar rbrace = /^(?:\\{[\\w\\W]*\\}|\\[[\\w\\W]*\\])$/,\n\trmultiDash = /[A-Z]/g;\n\nfunction getData( data ) {\n\tif ( data === \"true\" ) {\n\t\treturn true;\n\t}\n\n\tif ( data === \"false\" ) {\n\t\treturn false;\n\t}\n\n\tif ( data === \"null\" ) {\n\t\treturn null;\n\t}\n\n\t// Only convert to a number if it doesn't change the string\n\tif ( data === +data + \"\" ) {\n\t\treturn +data;\n\t}\n\n\tif ( rbrace.test( data ) ) {\n\t\treturn JSON.parse( data );\n\t}\n\n\treturn data;\n}\n\nfunction dataAttr( elem, key, data ) {\n\tvar name;\n\n\t// If nothing was found internally, try to fetch any\n\t// data from the HTML5 data-* attribute\n\tif ( data === undefined && elem.nodeType === 1 ) {\n\t\tname = \"data-\" + key.replace( rmultiDash, \"-$&\" ).toLowerCase();\n\t\tdata = elem.getAttribute( name );\n\n\t\tif ( typeof data === \"string\" ) {\n\t\t\ttry {\n\t\t\t\tdata = getData( data );\n\t\t\t} catch ( e ) {}\n\n\t\t\t// Make sure we set the data so it isn't changed later\n\t\t\tdataUser.set( elem, key, data );\n\t\t} else {\n\t\t\tdata = undefined;\n\t\t}\n\t}\n\treturn data;\n}\n\njQuery.extend( {\n\thasData: function( elem ) {\n\t\treturn dataUser.hasData( elem ) || dataPriv.hasData( elem );\n\t},\n\n\tdata: function( elem, name, data ) {\n\t\treturn dataUser.access( elem, name, data );\n\t},\n\n\tremoveData: function( elem, name ) {\n\t\tdataUser.remove( elem, name );\n\t},\n\n\t// TODO: Now that all calls to _data and _removeData have been replaced\n\t// with direct calls to dataPriv methods, these can be deprecated.\n\t_data: function( elem, name, data ) {\n\t\treturn dataPriv.access( elem, name, data );\n\t},\n\n\t_removeData: function( elem, name ) {\n\t\tdataPriv.remove( elem, name );\n\t}\n} );\n\njQuery.fn.extend( {\n\tdata: function( key, value ) {\n\t\tvar i, name, data,\n\t\t\telem = this[ 0 ],\n\t\t\tattrs = elem && elem.attributes;\n\n\t\t// Gets all values\n\t\tif ( key === undefined ) {\n\t\t\tif ( this.length ) {\n\t\t\t\tdata = dataUser.get( elem );\n\n\t\t\t\tif ( elem.nodeType === 1 && !dataPriv.get( elem, \"hasDataAttrs\" ) ) {\n\t\t\t\t\ti = attrs.length;\n\t\t\t\t\twhile ( i-- ) {\n\n\t\t\t\t\t\t// Support: IE 11 only\n\t\t\t\t\t\t// The attrs elements can be null (#14894)\n\t\t\t\t\t\tif ( attrs[ i ] ) {\n\t\t\t\t\t\t\tname = attrs[ i ].name;\n\t\t\t\t\t\t\tif ( name.indexOf( \"data-\" ) === 0 ) {\n\t\t\t\t\t\t\t\tname = camelCase( name.slice( 5 ) );\n\t\t\t\t\t\t\t\tdataAttr( elem, name, data[ name ] );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdataPriv.set( elem, \"hasDataAttrs\", true );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn data;\n\t\t}\n\n\t\t// Sets multiple values\n\t\tif ( typeof key === \"object\" ) {\n\t\t\treturn this.each( function() {\n\t\t\t\tdataUser.set( this, key );\n\t\t\t} );\n\t\t}\n\n\t\treturn access( this, function( value ) {\n\t\t\tvar data;\n\n\t\t\t// The calling jQuery object (element matches) is not empty\n\t\t\t// (and therefore has an element appears at this[ 0 ]) and the\n\t\t\t// `value` parameter was not undefined. An empty jQuery object\n\t\t\t// will result in `undefined` for elem = this[ 0 ] which will\n\t\t\t// throw an exception if an attempt to read a data cache is made.\n\t\t\tif ( elem && value === undefined ) {\n\n\t\t\t\t// Attempt to get data from the cache\n\t\t\t\t// The key will always be camelCased in Data\n\t\t\t\tdata = dataUser.get( elem, key );\n\t\t\t\tif ( data !== undefined ) {\n\t\t\t\t\treturn data;\n\t\t\t\t}\n\n\t\t\t\t// Attempt to \"discover\" the data in\n\t\t\t\t// HTML5 custom data-* attrs\n\t\t\t\tdata = dataAttr( elem, key );\n\t\t\t\tif ( data !== undefined ) {\n\t\t\t\t\treturn data;\n\t\t\t\t}\n\n\t\t\t\t// We tried really hard, but the data doesn't exist.\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Set the data...\n\t\t\tthis.each( function() {\n\n\t\t\t\t// We always store the camelCased key\n\t\t\t\tdataUser.set( this, key, value );\n\t\t\t} );\n\t\t}, null, value, arguments.length > 1, null, true );\n\t},\n\n\tremoveData: function( key ) {\n\t\treturn this.each( function() {\n\t\t\tdataUser.remove( this, key );\n\t\t} );\n\t}\n} );\n\n\njQuery.extend( {\n\tqueue: function( elem, type, data ) {\n\t\tvar queue;\n\n\t\tif ( elem ) {\n\t\t\ttype = ( type || \"fx\" ) + \"queue\";\n\t\t\tqueue = dataPriv.get( elem, type );\n\n\t\t\t// Speed up dequeue by getting out quickly if this is just a lookup\n\t\t\tif ( data ) {\n\t\t\t\tif ( !queue || Array.isArray( data ) ) {\n\t\t\t\t\tqueue = dataPriv.access( elem, type, jQuery.makeArray( data ) );\n\t\t\t\t} else {\n\t\t\t\t\tqueue.push( data );\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn queue || [];\n\t\t}\n\t},\n\n\tdequeue: function( elem, type ) {\n\t\ttype = type || \"fx\";\n\n\t\tvar queue = jQuery.queue( elem, type ),\n\t\t\tstartLength = queue.length,\n\t\t\tfn = queue.shift(),\n\t\t\thooks = jQuery._queueHooks( elem, type ),\n\t\t\tnext = function() {\n\t\t\t\tjQuery.dequeue( elem, type );\n\t\t\t};\n\n\t\t// If the fx queue is dequeued, always remove the progress sentinel\n\t\tif ( fn === \"inprogress\" ) {\n\t\t\tfn = queue.shift();\n\t\t\tstartLength--;\n\t\t}\n\n\t\tif ( fn ) {\n\n\t\t\t// Add a progress sentinel to prevent the fx queue from being\n\t\t\t// automatically dequeued\n\t\t\tif ( type === \"fx\" ) {\n\t\t\t\tqueue.unshift( \"inprogress\" );\n\t\t\t}\n\n\t\t\t// Clear up the last queue stop function\n\t\t\tdelete hooks.stop;\n\t\t\tfn.call( elem, next, hooks );\n\t\t}\n\n\t\tif ( !startLength && hooks ) {\n\t\t\thooks.empty.fire();\n\t\t}\n\t},\n\n\t// Not public - generate a queueHooks object, or return the current one\n\t_queueHooks: function( elem, type ) {\n\t\tvar key = type + \"queueHooks\";\n\t\treturn dataPriv.get( elem, key ) || dataPriv.access( elem, key, {\n\t\t\tempty: jQuery.Callbacks( \"once memory\" ).add( function() {\n\t\t\t\tdataPriv.remove( elem, [ type + \"queue\", key ] );\n\t\t\t} )\n\t\t} );\n\t}\n} );\n\njQuery.fn.extend( {\n\tqueue: function( type, data ) {\n\t\tvar setter = 2;\n\n\t\tif ( typeof type !== \"string\" ) {\n\t\t\tdata = type;\n\t\t\ttype = \"fx\";\n\t\t\tsetter--;\n\t\t}\n\n\t\tif ( arguments.length < setter ) {\n\t\t\treturn jQuery.queue( this[ 0 ], type );\n\t\t}\n\n\t\treturn data === undefined ?\n\t\t\tthis :\n\t\t\tthis.each( function() {\n\t\t\t\tvar queue = jQuery.queue( this, type, data );\n\n\t\t\t\t// Ensure a hooks for this queue\n\t\t\t\tjQuery._queueHooks( this, type );\n\n\t\t\t\tif ( type === \"fx\" && queue[ 0 ] !== \"inprogress\" ) {\n\t\t\t\t\tjQuery.dequeue( this, type );\n\t\t\t\t}\n\t\t\t} );\n\t},\n\tdequeue: function( type ) {\n\t\treturn this.each( function() {\n\t\t\tjQuery.dequeue( this, type );\n\t\t} );\n\t},\n\tclearQueue: function( type ) {\n\t\treturn this.queue( type || \"fx\", [] );\n\t},\n\n\t// Get a promise resolved when queues of a certain type\n\t// are emptied (fx is the type by default)\n\tpromise: function( type, obj ) {\n\t\tvar tmp,\n\t\t\tcount = 1,\n\t\t\tdefer = jQuery.Deferred(),\n\t\t\telements = this,\n\t\t\ti = this.length,\n\t\t\tresolve = function() {\n\t\t\t\tif ( !( --count ) ) {\n\t\t\t\t\tdefer.resolveWith( elements, [ elements ] );\n\t\t\t\t}\n\t\t\t};\n\n\t\tif ( typeof type !== \"string\" ) {\n\t\t\tobj = type;\n\t\t\ttype = undefined;\n\t\t}\n\t\ttype = type || \"fx\";\n\n\t\twhile ( i-- ) {\n\t\t\ttmp = dataPriv.get( elements[ i ], type + \"queueHooks\" );\n\t\t\tif ( tmp && tmp.empty ) {\n\t\t\t\tcount++;\n\t\t\t\ttmp.empty.add( resolve );\n\t\t\t}\n\t\t}\n\t\tresolve();\n\t\treturn defer.promise( obj );\n\t}\n} );\nvar pnum = ( /[+-]?(?:\\d*\\.|)\\d+(?:[eE][+-]?\\d+|)/ ).source;\n\nvar rcssNum = new RegExp( \"^(?:([+-])=|)(\" + pnum + \")([a-z%]*)$\", \"i\" );\n\n\nvar cssExpand = [ \"Top\", \"Right\", \"Bottom\", \"Left\" ];\n\nvar documentElement = document.documentElement;\n\n\n\n\tvar isAttached = function( elem ) {\n\t\t\treturn jQuery.contains( elem.ownerDocument, elem );\n\t\t},\n\t\tcomposed = { composed: true };\n\n\t// Support: IE 9 - 11+, Edge 12 - 18+, iOS 10.0 - 10.2 only\n\t// Check attachment across shadow DOM boundaries when possible (gh-3504)\n\t// Support: iOS 10.0-10.2 only\n\t// Early iOS 10 versions support `attachShadow` but not `getRootNode`,\n\t// leading to errors. We need to check for `getRootNode`.\n\tif ( documentElement.getRootNode ) {\n\t\tisAttached = function( elem ) {\n\t\t\treturn jQuery.contains( elem.ownerDocument, elem ) ||\n\t\t\t\telem.getRootNode( composed ) === elem.ownerDocument;\n\t\t};\n\t}\nvar isHiddenWithinTree = function( elem, el ) {\n\n\t\t// isHiddenWithinTree might be called from jQuery#filter function;\n\t\t// in that case, element will be second argument\n\t\telem = el || elem;\n\n\t\t// Inline style trumps all\n\t\treturn elem.style.display === \"none\" ||\n\t\t\telem.style.display === \"\" &&\n\n\t\t\t// Otherwise, check computed style\n\t\t\t// Support: Firefox <=43 - 45\n\t\t\t// Disconnected elements can have computed display: none, so first confirm that elem is\n\t\t\t// in the document.\n\t\t\tisAttached( elem ) &&\n\n\t\t\tjQuery.css( elem, \"display\" ) === \"none\";\n\t};\n\nvar swap = function( elem, options, callback, args ) {\n\tvar ret, name,\n\t\told = {};\n\n\t// Remember the old values, and insert the new ones\n\tfor ( name in options ) {\n\t\told[ name ] = elem.style[ name ];\n\t\telem.style[ name ] = options[ name ];\n\t}\n\n\tret = callback.apply( elem, args || [] );\n\n\t// Revert the old values\n\tfor ( name in options ) {\n\t\telem.style[ name ] = old[ name ];\n\t}\n\n\treturn ret;\n};\n\n\n\n\nfunction adjustCSS( elem, prop, valueParts, tween ) {\n\tvar adjusted, scale,\n\t\tmaxIterations = 20,\n\t\tcurrentValue = tween ?\n\t\t\tfunction() {\n\t\t\t\treturn tween.cur();\n\t\t\t} :\n\t\t\tfunction() {\n\t\t\t\treturn jQuery.css( elem, prop, \"\" );\n\t\t\t},\n\t\tinitial = currentValue(),\n\t\tunit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? \"\" : \"px\" ),\n\n\t\t// Starting value computation is required for potential unit mismatches\n\t\tinitialInUnit = elem.nodeType &&\n\t\t\t( jQuery.cssNumber[ prop ] || unit !== \"px\" && +initial ) &&\n\t\t\trcssNum.exec( jQuery.css( elem, prop ) );\n\n\tif ( initialInUnit && initialInUnit[ 3 ] !== unit ) {\n\n\t\t// Support: Firefox <=54\n\t\t// Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144)\n\t\tinitial = initial / 2;\n\n\t\t// Trust units reported by jQuery.css\n\t\tunit = unit || initialInUnit[ 3 ];\n\n\t\t// Iteratively approximate from a nonzero starting point\n\t\tinitialInUnit = +initial || 1;\n\n\t\twhile ( maxIterations-- ) {\n\n\t\t\t// Evaluate and update our best guess (doubling guesses that zero out).\n\t\t\t// Finish if the scale equals or crosses 1 (making the old*new product non-positive).\n\t\t\tjQuery.style( elem, prop, initialInUnit + unit );\n\t\t\tif ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) {\n\t\t\t\tmaxIterations = 0;\n\t\t\t}\n\t\t\tinitialInUnit = initialInUnit / scale;\n\n\t\t}\n\n\t\tinitialInUnit = initialInUnit * 2;\n\t\tjQuery.style( elem, prop, initialInUnit + unit );\n\n\t\t// Make sure we update the tween properties later on\n\t\tvalueParts = valueParts || [];\n\t}\n\n\tif ( valueParts ) {\n\t\tinitialInUnit = +initialInUnit || +initial || 0;\n\n\t\t// Apply relative offset (+=/-=) if specified\n\t\tadjusted = valueParts[ 1 ] ?\n\t\t\tinitialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] :\n\t\t\t+valueParts[ 2 ];\n\t\tif ( tween ) {\n\t\t\ttween.unit = unit;\n\t\t\ttween.start = initialInUnit;\n\t\t\ttween.end = adjusted;\n\t\t}\n\t}\n\treturn adjusted;\n}\n\n\nvar defaultDisplayMap = {};\n\nfunction getDefaultDisplay( elem ) {\n\tvar temp,\n\t\tdoc = elem.ownerDocument,\n\t\tnodeName = elem.nodeName,\n\t\tdisplay = defaultDisplayMap[ nodeName ];\n\n\tif ( display ) {\n\t\treturn display;\n\t}\n\n\ttemp = doc.body.appendChild( doc.createElement( nodeName ) );\n\tdisplay = jQuery.css( temp, \"display\" );\n\n\ttemp.parentNode.removeChild( temp );\n\n\tif ( display === \"none\" ) {\n\t\tdisplay = \"block\";\n\t}\n\tdefaultDisplayMap[ nodeName ] = display;\n\n\treturn display;\n}\n\nfunction showHide( elements, show ) {\n\tvar display, elem,\n\t\tvalues = [],\n\t\tindex = 0,\n\t\tlength = elements.length;\n\n\t// Determine new display value for elements that need to change\n\tfor ( ; index < length; index++ ) {\n\t\telem = elements[ index ];\n\t\tif ( !elem.style ) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tdisplay = elem.style.display;\n\t\tif ( show ) {\n\n\t\t\t// Since we force visibility upon cascade-hidden elements, an immediate (and slow)\n\t\t\t// check is required in this first loop unless we have a nonempty display value (either\n\t\t\t// inline or about-to-be-restored)\n\t\t\tif ( display === \"none\" ) {\n\t\t\t\tvalues[ index ] = dataPriv.get( elem, \"display\" ) || null;\n\t\t\t\tif ( !values[ index ] ) {\n\t\t\t\t\telem.style.display = \"\";\n\t\t\t\t}\n\t\t\t}\n\t\t\tif ( elem.style.display === \"\" && isHiddenWithinTree( elem ) ) {\n\t\t\t\tvalues[ index ] = getDefaultDisplay( elem );\n\t\t\t}\n\t\t} else {\n\t\t\tif ( display !== \"none\" ) {\n\t\t\t\tvalues[ index ] = \"none\";\n\n\t\t\t\t// Remember what we're overwriting\n\t\t\t\tdataPriv.set( elem, \"display\", display );\n\t\t\t}\n\t\t}\n\t}\n\n\t// Set the display of the elements in a second loop to avoid constant reflow\n\tfor ( index = 0; index < length; index++ ) {\n\t\tif ( values[ index ] != null ) {\n\t\t\telements[ index ].style.display = values[ index ];\n\t\t}\n\t}\n\n\treturn elements;\n}\n\njQuery.fn.extend( {\n\tshow: function() {\n\t\treturn showHide( this, true );\n\t},\n\thide: function() {\n\t\treturn showHide( this );\n\t},\n\ttoggle: function( state ) {\n\t\tif ( typeof state === \"boolean\" ) {\n\t\t\treturn state ? this.show() : this.hide();\n\t\t}\n\n\t\treturn this.each( function() {\n\t\t\tif ( isHiddenWithinTree( this ) ) {\n\t\t\t\tjQuery( this ).show();\n\t\t\t} else {\n\t\t\t\tjQuery( this ).hide();\n\t\t\t}\n\t\t} );\n\t}\n} );\nvar rcheckableType = ( /^(?:checkbox|radio)$/i );\n\nvar rtagName = ( /<([a-z][^\\/\\0>\\x20\\t\\r\\n\\f]*)/i );\n\nvar rscriptType = ( /^$|^module$|\\/(?:java|ecma)script/i );\n\n\n\n// We have to close these tags to support XHTML (#13200)\nvar wrapMap = {\n\n\t// Support: IE <=9 only\n\toption: [ 1, \"<select multiple='multiple'>\", \"</select>\" ],\n\n\t// XHTML parsers do not magically insert elements in the\n\t// same way that tag soup parsers do. So we cannot shorten\n\t// this by omitting <tbody> or other required elements.\n\tthead: [ 1, \"<table>\", \"</table>\" ],\n\tcol: [ 2, \"<table><colgroup>\", \"</colgroup></table>\" ],\n\ttr: [ 2, \"<table><tbody>\", \"</tbody></table>\" ],\n\ttd: [ 3, \"<table><tbody><tr>\", \"</tr></tbody></table>\" ],\n\n\t_default: [ 0, \"\", \"\" ]\n};\n\n// Support: IE <=9 only\nwrapMap.optgroup = wrapMap.option;\n\nwrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;\nwrapMap.th = wrapMap.td;\n\n\nfunction getAll( context, tag ) {\n\n\t// Support: IE <=9 - 11 only\n\t// Use typeof to avoid zero-argument method invocation on host objects (#15151)\n\tvar ret;\n\n\tif ( typeof context.getElementsByTagName !== \"undefined\" ) {\n\t\tret = context.getElementsByTagName( tag || \"*\" );\n\n\t} else if ( typeof context.querySelectorAll !== \"undefined\" ) {\n\t\tret = context.querySelectorAll( tag || \"*\" );\n\n\t} else {\n\t\tret = [];\n\t}\n\n\tif ( tag === undefined || tag && nodeName( context, tag ) ) {\n\t\treturn jQuery.merge( [ context ], ret );\n\t}\n\n\treturn ret;\n}\n\n\n// Mark scripts as having already been evaluated\nfunction setGlobalEval( elems, refElements ) {\n\tvar i = 0,\n\t\tl = elems.length;\n\n\tfor ( ; i < l; i++ ) {\n\t\tdataPriv.set(\n\t\t\telems[ i ],\n\t\t\t\"globalEval\",\n\t\t\t!refElements || dataPriv.get( refElements[ i ], \"globalEval\" )\n\t\t);\n\t}\n}\n\n\nvar rhtml = /<|&#?\\w+;/;\n\nfunction buildFragment( elems, context, scripts, selection, ignored ) {\n\tvar elem, tmp, tag, wrap, attached, j,\n\t\tfragment = context.createDocumentFragment(),\n\t\tnodes = [],\n\t\ti = 0,\n\t\tl = elems.length;\n\n\tfor ( ; i < l; i++ ) {\n\t\telem = elems[ i ];\n\n\t\tif ( elem || elem === 0 ) {\n\n\t\t\t// Add nodes directly\n\t\t\tif ( toType( elem ) === \"object\" ) {\n\n\t\t\t\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t\t\t\t// push.apply(_, arraylike) throws on ancient WebKit\n\t\t\t\tjQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );\n\n\t\t\t// Convert non-html into a text node\n\t\t\t} else if ( !rhtml.test( elem ) ) {\n\t\t\t\tnodes.push( context.createTextNode( elem ) );\n\n\t\t\t// Convert html into DOM nodes\n\t\t\t} else {\n\t\t\t\ttmp = tmp || fragment.appendChild( context.createElement( \"div\" ) );\n\n\t\t\t\t// Deserialize a standard representation\n\t\t\t\ttag = ( rtagName.exec( elem ) || [ \"\", \"\" ] )[ 1 ].toLowerCase();\n\t\t\t\twrap = wrapMap[ tag ] || wrapMap._default;\n\t\t\t\ttmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ];\n\n\t\t\t\t// Descend through wrappers to the right content\n\t\t\t\tj = wrap[ 0 ];\n\t\t\t\twhile ( j-- ) {\n\t\t\t\t\ttmp = tmp.lastChild;\n\t\t\t\t}\n\n\t\t\t\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t\t\t\t// push.apply(_, arraylike) throws on ancient WebKit\n\t\t\t\tjQuery.merge( nodes, tmp.childNodes );\n\n\t\t\t\t// Remember the top-level container\n\t\t\t\ttmp = fragment.firstChild;\n\n\t\t\t\t// Ensure the created nodes are orphaned (#12392)\n\t\t\t\ttmp.textContent = \"\";\n\t\t\t}\n\t\t}\n\t}\n\n\t// Remove wrapper from fragment\n\tfragment.textContent = \"\";\n\n\ti = 0;\n\twhile ( ( elem = nodes[ i++ ] ) ) {\n\n\t\t// Skip elements already in the context collection (trac-4087)\n\t\tif ( selection && jQuery.inArray( elem, selection ) > -1 ) {\n\t\t\tif ( ignored ) {\n\t\t\t\tignored.push( elem );\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tattached = isAttached( elem );\n\n\t\t// Append to fragment\n\t\ttmp = getAll( fragment.appendChild( elem ), \"script\" );\n\n\t\t// Preserve script evaluation history\n\t\tif ( attached ) {\n\t\t\tsetGlobalEval( tmp );\n\t\t}\n\n\t\t// Capture executables\n\t\tif ( scripts ) {\n\t\t\tj = 0;\n\t\t\twhile ( ( elem = tmp[ j++ ] ) ) {\n\t\t\t\tif ( rscriptType.test( elem.type || \"\" ) ) {\n\t\t\t\t\tscripts.push( elem );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn fragment;\n}\n\n\n( function() {\n\tvar fragment = document.createDocumentFragment(),\n\t\tdiv = fragment.appendChild( document.createElement( \"div\" ) ),\n\t\tinput = document.createElement( \"input\" );\n\n\t// Support: Android 4.0 - 4.3 only\n\t// Check state lost if the name is set (#11217)\n\t// Support: Windows Web Apps (WWA)\n\t// `name` and `type` must use .setAttribute for WWA (#14901)\n\tinput.setAttribute( \"type\", \"radio\" );\n\tinput.setAttribute( \"checked\", \"checked\" );\n\tinput.setAttribute( \"name\", \"t\" );\n\n\tdiv.appendChild( input );\n\n\t// Support: Android <=4.1 only\n\t// Older WebKit doesn't clone checked state correctly in fragments\n\tsupport.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked;\n\n\t// Support: IE <=11 only\n\t// Make sure textarea (and checkbox) defaultValue is properly cloned\n\tdiv.innerHTML = \"<textarea>x</textarea>\";\n\tsupport.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue;\n} )();\n\n\nvar\n\trkeyEvent = /^key/,\n\trmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/,\n\trtypenamespace = /^([^.]*)(?:\\.(.+)|)/;\n\nfunction returnTrue() {\n\treturn true;\n}\n\nfunction returnFalse() {\n\treturn false;\n}\n\n// Support: IE <=9 - 11+\n// focus() and blur() are asynchronous, except when they are no-op.\n// So expect focus to be synchronous when the element is already active,\n// and blur to be synchronous when the element is not already active.\n// (focus and blur are always synchronous in other supported browsers,\n// this just defines when we can count on it).\nfunction expectSync( elem, type ) {\n\treturn ( elem === safeActiveElement() ) === ( type === \"focus\" );\n}\n\n// Support: IE <=9 only\n// Accessing document.activeElement can throw unexpectedly\n// https://bugs.jquery.com/ticket/13393\nfunction safeActiveElement() {\n\ttry {\n\t\treturn document.activeElement;\n\t} catch ( err ) { }\n}\n\nfunction on( elem, types, selector, data, fn, one ) {\n\tvar origFn, type;\n\n\t// Types can be a map of types/handlers\n\tif ( typeof types === \"object\" ) {\n\n\t\t// ( types-Object, selector, data )\n\t\tif ( typeof selector !== \"string\" ) {\n\n\t\t\t// ( types-Object, data )\n\t\t\tdata = data || selector;\n\t\t\tselector = undefined;\n\t\t}\n\t\tfor ( type in types ) {\n\t\t\ton( elem, type, selector, data, types[ type ], one );\n\t\t}\n\t\treturn elem;\n\t}\n\n\tif ( data == null && fn == null ) {\n\n\t\t// ( types, fn )\n\t\tfn = selector;\n\t\tdata = selector = undefined;\n\t} else if ( fn == null ) {\n\t\tif ( typeof selector === \"string\" ) {\n\n\t\t\t// ( types, selector, fn )\n\t\t\tfn = data;\n\t\t\tdata = undefined;\n\t\t} else {\n\n\t\t\t// ( types, data, fn )\n\t\t\tfn = data;\n\t\t\tdata = selector;\n\t\t\tselector = undefined;\n\t\t}\n\t}\n\tif ( fn === false ) {\n\t\tfn = returnFalse;\n\t} else if ( !fn ) {\n\t\treturn elem;\n\t}\n\n\tif ( one === 1 ) {\n\t\torigFn = fn;\n\t\tfn = function( event ) {\n\n\t\t\t// Can use an empty set, since event contains the info\n\t\t\tjQuery().off( event );\n\t\t\treturn origFn.apply( this, arguments );\n\t\t};\n\n\t\t// Use same guid so caller can remove using origFn\n\t\tfn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );\n\t}\n\treturn elem.each( function() {\n\t\tjQuery.event.add( this, types, fn, data, selector );\n\t} );\n}\n\n/*\n * Helper functions for managing events -- not part of the public interface.\n * Props to Dean Edwards' addEvent library for many of the ideas.\n */\njQuery.event = {\n\n\tglobal: {},\n\n\tadd: function( elem, types, handler, data, selector ) {\n\n\t\tvar handleObjIn, eventHandle, tmp,\n\t\t\tevents, t, handleObj,\n\t\t\tspecial, handlers, type, namespaces, origType,\n\t\t\telemData = dataPriv.get( elem );\n\n\t\t// Don't attach events to noData or text/comment nodes (but allow plain objects)\n\t\tif ( !elemData ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Caller can pass in an object of custom data in lieu of the handler\n\t\tif ( handler.handler ) {\n\t\t\thandleObjIn = handler;\n\t\t\thandler = handleObjIn.handler;\n\t\t\tselector = handleObjIn.selector;\n\t\t}\n\n\t\t// Ensure that invalid selectors throw exceptions at attach time\n\t\t// Evaluate against documentElement in case elem is a non-element node (e.g., document)\n\t\tif ( selector ) {\n\t\t\tjQuery.find.matchesSelector( documentElement, selector );\n\t\t}\n\n\t\t// Make sure that the handler has a unique ID, used to find/remove it later\n\t\tif ( !handler.guid ) {\n\t\t\thandler.guid = jQuery.guid++;\n\t\t}\n\n\t\t// Init the element's event structure and main handler, if this is the first\n\t\tif ( !( events = elemData.events ) ) {\n\t\t\tevents = elemData.events = {};\n\t\t}\n\t\tif ( !( eventHandle = elemData.handle ) ) {\n\t\t\teventHandle = elemData.handle = function( e ) {\n\n\t\t\t\t// Discard the second event of a jQuery.event.trigger() and\n\t\t\t\t// when an event is called after a page has unloaded\n\t\t\t\treturn typeof jQuery !== \"undefined\" && jQuery.event.triggered !== e.type ?\n\t\t\t\t\tjQuery.event.dispatch.apply( elem, arguments ) : undefined;\n\t\t\t};\n\t\t}\n\n\t\t// Handle multiple events separated by a space\n\t\ttypes = ( types || \"\" ).match( rnothtmlwhite ) || [ \"\" ];\n\t\tt = types.length;\n\t\twhile ( t-- ) {\n\t\t\ttmp = rtypenamespace.exec( types[ t ] ) || [];\n\t\t\ttype = origType = tmp[ 1 ];\n\t\t\tnamespaces = ( tmp[ 2 ] || \"\" ).split( \".\" ).sort();\n\n\t\t\t// There *must* be a type, no attaching namespace-only handlers\n\t\t\tif ( !type ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// If event changes its type, use the special event handlers for the changed type\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\n\t\t\t// If selector defined, determine special event api type, otherwise given type\n\t\t\ttype = ( selector ? special.delegateType : special.bindType ) || type;\n\n\t\t\t// Update special based on newly reset type\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\n\t\t\t// handleObj is passed to all event handlers\n\t\t\thandleObj = jQuery.extend( {\n\t\t\t\ttype: type,\n\t\t\t\torigType: origType,\n\t\t\t\tdata: data,\n\t\t\t\thandler: handler,\n\t\t\t\tguid: handler.guid,\n\t\t\t\tselector: selector,\n\t\t\t\tneedsContext: selector && jQuery.expr.match.needsContext.test( selector ),\n\t\t\t\tnamespace: namespaces.join( \".\" )\n\t\t\t}, handleObjIn );\n\n\t\t\t// Init the event handler queue if we're the first\n\t\t\tif ( !( handlers = events[ type ] ) ) {\n\t\t\t\thandlers = events[ type ] = [];\n\t\t\t\thandlers.delegateCount = 0;\n\n\t\t\t\t// Only use addEventListener if the special events handler returns false\n\t\t\t\tif ( !special.setup ||\n\t\t\t\t\tspecial.setup.call( elem, data, namespaces, eventHandle ) === false ) {\n\n\t\t\t\t\tif ( elem.addEventListener ) {\n\t\t\t\t\t\telem.addEventListener( type, eventHandle );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( special.add ) {\n\t\t\t\tspecial.add.call( elem, handleObj );\n\n\t\t\t\tif ( !handleObj.handler.guid ) {\n\t\t\t\t\thandleObj.handler.guid = handler.guid;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Add to the element's handler list, delegates in front\n\t\t\tif ( selector ) {\n\t\t\t\thandlers.splice( handlers.delegateCount++, 0, handleObj );\n\t\t\t} else {\n\t\t\t\thandlers.push( handleObj );\n\t\t\t}\n\n\t\t\t// Keep track of which events have ever been used, for event optimization\n\t\t\tjQuery.event.global[ type ] = true;\n\t\t}\n\n\t},\n\n\t// Detach an event or set of events from an element\n\tremove: function( elem, types, handler, selector, mappedTypes ) {\n\n\t\tvar j, origCount, tmp,\n\t\t\tevents, t, handleObj,\n\t\t\tspecial, handlers, type, namespaces, origType,\n\t\t\telemData = dataPriv.hasData( elem ) && dataPriv.get( elem );\n\n\t\tif ( !elemData || !( events = elemData.events ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Once for each type.namespace in types; type may be omitted\n\t\ttypes = ( types || \"\" ).match( rnothtmlwhite ) || [ \"\" ];\n\t\tt = types.length;\n\t\twhile ( t-- ) {\n\t\t\ttmp = rtypenamespace.exec( types[ t ] ) || [];\n\t\t\ttype = origType = tmp[ 1 ];\n\t\t\tnamespaces = ( tmp[ 2 ] || \"\" ).split( \".\" ).sort();\n\n\t\t\t// Unbind all events (on this namespace, if provided) for the element\n\t\t\tif ( !type ) {\n\t\t\t\tfor ( type in events ) {\n\t\t\t\t\tjQuery.event.remove( elem, type + types[ t ], handler, selector, true );\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\t\t\ttype = ( selector ? special.delegateType : special.bindType ) || type;\n\t\t\thandlers = events[ type ] || [];\n\t\t\ttmp = tmp[ 2 ] &&\n\t\t\t\tnew RegExp( \"(^|\\\\.)\" + namespaces.join( \"\\\\.(?:.*\\\\.|)\" ) + \"(\\\\.|$)\" );\n\n\t\t\t// Remove matching events\n\t\t\torigCount = j = handlers.length;\n\t\t\twhile ( j-- ) {\n\t\t\t\thandleObj = handlers[ j ];\n\n\t\t\t\tif ( ( mappedTypes || origType === handleObj.origType ) &&\n\t\t\t\t\t( !handler || handler.guid === handleObj.guid ) &&\n\t\t\t\t\t( !tmp || tmp.test( handleObj.namespace ) ) &&\n\t\t\t\t\t( !selector || selector === handleObj.selector ||\n\t\t\t\t\t\tselector === \"**\" && handleObj.selector ) ) {\n\t\t\t\t\thandlers.splice( j, 1 );\n\n\t\t\t\t\tif ( handleObj.selector ) {\n\t\t\t\t\t\thandlers.delegateCount--;\n\t\t\t\t\t}\n\t\t\t\t\tif ( special.remove ) {\n\t\t\t\t\t\tspecial.remove.call( elem, handleObj );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Remove generic event handler if we removed something and no more handlers exist\n\t\t\t// (avoids potential for endless recursion during removal of special event handlers)\n\t\t\tif ( origCount && !handlers.length ) {\n\t\t\t\tif ( !special.teardown ||\n\t\t\t\t\tspecial.teardown.call( elem, namespaces, elemData.handle ) === false ) {\n\n\t\t\t\t\tjQuery.removeEvent( elem, type, elemData.handle );\n\t\t\t\t}\n\n\t\t\t\tdelete events[ type ];\n\t\t\t}\n\t\t}\n\n\t\t// Remove data and the expando if it's no longer used\n\t\tif ( jQuery.isEmptyObject( events ) ) {\n\t\t\tdataPriv.remove( elem, \"handle events\" );\n\t\t}\n\t},\n\n\tdispatch: function( nativeEvent ) {\n\n\t\t// Make a writable jQuery.Event from the native event object\n\t\tvar event = jQuery.event.fix( nativeEvent );\n\n\t\tvar i, j, ret, matched, handleObj, handlerQueue,\n\t\t\targs = new Array( arguments.length ),\n\t\t\thandlers = ( dataPriv.get( this, \"events\" ) || {} )[ event.type ] || [],\n\t\t\tspecial = jQuery.event.special[ event.type ] || {};\n\n\t\t// Use the fix-ed jQuery.Event rather than the (read-only) native event\n\t\targs[ 0 ] = event;\n\n\t\tfor ( i = 1; i < arguments.length; i++ ) {\n\t\t\targs[ i ] = arguments[ i ];\n\t\t}\n\n\t\tevent.delegateTarget = this;\n\n\t\t// Call the preDispatch hook for the mapped type, and let it bail if desired\n\t\tif ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Determine handlers\n\t\thandlerQueue = jQuery.event.handlers.call( this, event, handlers );\n\n\t\t// Run delegates first; they may want to stop propagation beneath us\n\t\ti = 0;\n\t\twhile ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) {\n\t\t\tevent.currentTarget = matched.elem;\n\n\t\t\tj = 0;\n\t\t\twhile ( ( handleObj = matched.handlers[ j++ ] ) &&\n\t\t\t\t!event.isImmediatePropagationStopped() ) {\n\n\t\t\t\t// If the event is namespaced, then each handler is only invoked if it is\n\t\t\t\t// specially universal or its namespaces are a superset of the event's.\n\t\t\t\tif ( !event.rnamespace || handleObj.namespace === false ||\n\t\t\t\t\tevent.rnamespace.test( handleObj.namespace ) ) {\n\n\t\t\t\t\tevent.handleObj = handleObj;\n\t\t\t\t\tevent.data = handleObj.data;\n\n\t\t\t\t\tret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle ||\n\t\t\t\t\t\thandleObj.handler ).apply( matched.elem, args );\n\n\t\t\t\t\tif ( ret !== undefined ) {\n\t\t\t\t\t\tif ( ( event.result = ret ) === false ) {\n\t\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Call the postDispatch hook for the mapped type\n\t\tif ( special.postDispatch ) {\n\t\t\tspecial.postDispatch.call( this, event );\n\t\t}\n\n\t\treturn event.result;\n\t},\n\n\thandlers: function( event, handlers ) {\n\t\tvar i, handleObj, sel, matchedHandlers, matchedSelectors,\n\t\t\thandlerQueue = [],\n\t\t\tdelegateCount = handlers.delegateCount,\n\t\t\tcur = event.target;\n\n\t\t// Find delegate handlers\n\t\tif ( delegateCount &&\n\n\t\t\t// Support: IE <=9\n\t\t\t// Black-hole SVG <use> instance trees (trac-13180)\n\t\t\tcur.nodeType &&\n\n\t\t\t// Support: Firefox <=42\n\t\t\t// Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861)\n\t\t\t// https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click\n\t\t\t// Support: IE 11 only\n\t\t\t// ...but not arrow key \"clicks\" of radio inputs, which can have `button` -1 (gh-2343)\n\t\t\t!( event.type === \"click\" && event.button >= 1 ) ) {\n\n\t\t\tfor ( ; cur !== this; cur = cur.parentNode || this ) {\n\n\t\t\t\t// Don't check non-elements (#13208)\n\t\t\t\t// Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)\n\t\t\t\tif ( cur.nodeType === 1 && !( event.type === \"click\" && cur.disabled === true ) ) {\n\t\t\t\t\tmatchedHandlers = [];\n\t\t\t\t\tmatchedSelectors = {};\n\t\t\t\t\tfor ( i = 0; i < delegateCount; i++ ) {\n\t\t\t\t\t\thandleObj = handlers[ i ];\n\n\t\t\t\t\t\t// Don't conflict with Object.prototype properties (#13203)\n\t\t\t\t\t\tsel = handleObj.selector + \" \";\n\n\t\t\t\t\t\tif ( matchedSelectors[ sel ] === undefined ) {\n\t\t\t\t\t\t\tmatchedSelectors[ sel ] = handleObj.needsContext ?\n\t\t\t\t\t\t\t\tjQuery( sel, this ).index( cur ) > -1 :\n\t\t\t\t\t\t\t\tjQuery.find( sel, this, null, [ cur ] ).length;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif ( matchedSelectors[ sel ] ) {\n\t\t\t\t\t\t\tmatchedHandlers.push( handleObj );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif ( matchedHandlers.length ) {\n\t\t\t\t\t\thandlerQueue.push( { elem: cur, handlers: matchedHandlers } );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Add the remaining (directly-bound) handlers\n\t\tcur = this;\n\t\tif ( delegateCount < handlers.length ) {\n\t\t\thandlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } );\n\t\t}\n\n\t\treturn handlerQueue;\n\t},\n\n\taddProp: function( name, hook ) {\n\t\tObject.defineProperty( jQuery.Event.prototype, name, {\n\t\t\tenumerable: true,\n\t\t\tconfigurable: true,\n\n\t\t\tget: isFunction( hook ) ?\n\t\t\t\tfunction() {\n\t\t\t\t\tif ( this.originalEvent ) {\n\t\t\t\t\t\t\treturn hook( this.originalEvent );\n\t\t\t\t\t}\n\t\t\t\t} :\n\t\t\t\tfunction() {\n\t\t\t\t\tif ( this.originalEvent ) {\n\t\t\t\t\t\t\treturn this.originalEvent[ name ];\n\t\t\t\t\t}\n\t\t\t\t},\n\n\t\t\tset: function( value ) {\n\t\t\t\tObject.defineProperty( this, name, {\n\t\t\t\t\tenumerable: true,\n\t\t\t\t\tconfigurable: true,\n\t\t\t\t\twritable: true,\n\t\t\t\t\tvalue: value\n\t\t\t\t} );\n\t\t\t}\n\t\t} );\n\t},\n\n\tfix: function( originalEvent ) {\n\t\treturn originalEvent[ jQuery.expando ] ?\n\t\t\toriginalEvent :\n\t\t\tnew jQuery.Event( originalEvent );\n\t},\n\n\tspecial: {\n\t\tload: {\n\n\t\t\t// Prevent triggered image.load events from bubbling to window.load\n\t\t\tnoBubble: true\n\t\t},\n\t\tclick: {\n\n\t\t\t// Utilize native event to ensure correct state for checkable inputs\n\t\t\tsetup: function( data ) {\n\n\t\t\t\t// For mutual compressibility with _default, replace `this` access with a local var.\n\t\t\t\t// `|| data` is dead code meant only to preserve the variable through minification.\n\t\t\t\tvar el = this || data;\n\n\t\t\t\t// Claim the first handler\n\t\t\t\tif ( rcheckableType.test( el.type ) &&\n\t\t\t\t\tel.click && nodeName( el, \"input\" ) ) {\n\n\t\t\t\t\t// dataPriv.set( el, \"click\", ... )\n\t\t\t\t\tleverageNative( el, \"click\", returnTrue );\n\t\t\t\t}\n\n\t\t\t\t// Return false to allow normal processing in the caller\n\t\t\t\treturn false;\n\t\t\t},\n\t\t\ttrigger: function( data ) {\n\n\t\t\t\t// For mutual compressibility with _default, replace `this` access with a local var.\n\t\t\t\t// `|| data` is dead code meant only to preserve the variable through minification.\n\t\t\t\tvar el = this || data;\n\n\t\t\t\t// Force setup before triggering a click\n\t\t\t\tif ( rcheckableType.test( el.type ) &&\n\t\t\t\t\tel.click && nodeName( el, \"input\" ) ) {\n\n\t\t\t\t\tleverageNative( el, \"click\" );\n\t\t\t\t}\n\n\t\t\t\t// Return non-false to allow normal event-path propagation\n\t\t\t\treturn true;\n\t\t\t},\n\n\t\t\t// For cross-browser consistency, suppress native .click() on links\n\t\t\t// Also prevent it if we're currently inside a leveraged native-event stack\n\t\t\t_default: function( event ) {\n\t\t\t\tvar target = event.target;\n\t\t\t\treturn rcheckableType.test( target.type ) &&\n\t\t\t\t\ttarget.click && nodeName( target, \"input\" ) &&\n\t\t\t\t\tdataPriv.get( target, \"click\" ) ||\n\t\t\t\t\tnodeName( target, \"a\" );\n\t\t\t}\n\t\t},\n\n\t\tbeforeunload: {\n\t\t\tpostDispatch: function( event ) {\n\n\t\t\t\t// Support: Firefox 20+\n\t\t\t\t// Firefox doesn't alert if the returnValue field is not set.\n\t\t\t\tif ( event.result !== undefined && event.originalEvent ) {\n\t\t\t\t\tevent.originalEvent.returnValue = event.result;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n};\n\n// Ensure the presence of an event listener that handles manually-triggered\n// synthetic events by interrupting progress until reinvoked in response to\n// *native* events that it fires directly, ensuring that state changes have\n// already occurred before other listeners are invoked.\nfunction leverageNative( el, type, expectSync ) {\n\n\t// Missing expectSync indicates a trigger call, which must force setup through jQuery.event.add\n\tif ( !expectSync ) {\n\t\tif ( dataPriv.get( el, type ) === undefined ) {\n\t\t\tjQuery.event.add( el, type, returnTrue );\n\t\t}\n\t\treturn;\n\t}\n\n\t// Register the controller as a special universal handler for all event namespaces\n\tdataPriv.set( el, type, false );\n\tjQuery.event.add( el, type, {\n\t\tnamespace: false,\n\t\thandler: function( event ) {\n\t\t\tvar notAsync, result,\n\t\t\t\tsaved = dataPriv.get( this, type );\n\n\t\t\tif ( ( event.isTrigger & 1 ) && this[ type ] ) {\n\n\t\t\t\t// Interrupt processing of the outer synthetic .trigger()ed event\n\t\t\t\t// Saved data should be false in such cases, but might be a leftover capture object\n\t\t\t\t// from an async native handler (gh-4350)\n\t\t\t\tif ( !saved.length ) {\n\n\t\t\t\t\t// Store arguments for use when handling the inner native event\n\t\t\t\t\t// There will always be at least one argument (an event object), so this array\n\t\t\t\t\t// will not be confused with a leftover capture object.\n\t\t\t\t\tsaved = slice.call( arguments );\n\t\t\t\t\tdataPriv.set( this, type, saved );\n\n\t\t\t\t\t// Trigger the native event and capture its result\n\t\t\t\t\t// Support: IE <=9 - 11+\n\t\t\t\t\t// focus() and blur() are asynchronous\n\t\t\t\t\tnotAsync = expectSync( this, type );\n\t\t\t\t\tthis[ type ]();\n\t\t\t\t\tresult = dataPriv.get( this, type );\n\t\t\t\t\tif ( saved !== result || notAsync ) {\n\t\t\t\t\t\tdataPriv.set( this, type, false );\n\t\t\t\t\t} else {\n\t\t\t\t\t\tresult = {};\n\t\t\t\t\t}\n\t\t\t\t\tif ( saved !== result ) {\n\n\t\t\t\t\t\t// Cancel the outer synthetic event\n\t\t\t\t\t\tevent.stopImmediatePropagation();\n\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\treturn result.value;\n\t\t\t\t\t}\n\n\t\t\t\t// If this is an inner synthetic event for an event with a bubbling surrogate\n\t\t\t\t// (focus or blur), assume that the surrogate already propagated from triggering the\n\t\t\t\t// native event and prevent that from happening again here.\n\t\t\t\t// This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the\n\t\t\t\t// bubbling surrogate propagates *after* the non-bubbling base), but that seems\n\t\t\t\t// less bad than duplication.\n\t\t\t\t} else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) {\n\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t}\n\n\t\t\t// If this is a native event triggered above, everything is now in order\n\t\t\t// Fire an inner synthetic event with the original arguments\n\t\t\t} else if ( saved.length ) {\n\n\t\t\t\t// ...and capture the result\n\t\t\t\tdataPriv.set( this, type, {\n\t\t\t\t\tvalue: jQuery.event.trigger(\n\n\t\t\t\t\t\t// Support: IE <=9 - 11+\n\t\t\t\t\t\t// Extend with the prototype to reset the above stopImmediatePropagation()\n\t\t\t\t\t\tjQuery.extend( saved[ 0 ], jQuery.Event.prototype ),\n\t\t\t\t\t\tsaved.slice( 1 ),\n\t\t\t\t\t\tthis\n\t\t\t\t\t)\n\t\t\t\t} );\n\n\t\t\t\t// Abort handling of the native event\n\t\t\t\tevent.stopImmediatePropagation();\n\t\t\t}\n\t\t}\n\t} );\n}\n\njQuery.removeEvent = function( elem, type, handle ) {\n\n\t// This \"if\" is needed for plain objects\n\tif ( elem.removeEventListener ) {\n\t\telem.removeEventListener( type, handle );\n\t}\n};\n\njQuery.Event = function( src, props ) {\n\n\t// Allow instantiation without the 'new' keyword\n\tif ( !( this instanceof jQuery.Event ) ) {\n\t\treturn new jQuery.Event( src, props );\n\t}\n\n\t// Event object\n\tif ( src && src.type ) {\n\t\tthis.originalEvent = src;\n\t\tthis.type = src.type;\n\n\t\t// Events bubbling up the document may have been marked as prevented\n\t\t// by a handler lower down the tree; reflect the correct value.\n\t\tthis.isDefaultPrevented = src.defaultPrevented ||\n\t\t\t\tsrc.defaultPrevented === undefined &&\n\n\t\t\t\t// Support: Android <=2.3 only\n\t\t\t\tsrc.returnValue === false ?\n\t\t\treturnTrue :\n\t\t\treturnFalse;\n\n\t\t// Create target properties\n\t\t// Support: Safari <=6 - 7 only\n\t\t// Target should not be a text node (#504, #13143)\n\t\tthis.target = ( src.target && src.target.nodeType === 3 ) ?\n\t\t\tsrc.target.parentNode :\n\t\t\tsrc.target;\n\n\t\tthis.currentTarget = src.currentTarget;\n\t\tthis.relatedTarget = src.relatedTarget;\n\n\t// Event type\n\t} else {\n\t\tthis.type = src;\n\t}\n\n\t// Put explicitly provided properties onto the event object\n\tif ( props ) {\n\t\tjQuery.extend( this, props );\n\t}\n\n\t// Create a timestamp if incoming event doesn't have one\n\tthis.timeStamp = src && src.timeStamp || Date.now();\n\n\t// Mark it as fixed\n\tthis[ jQuery.expando ] = true;\n};\n\n// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding\n// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html\njQuery.Event.prototype = {\n\tconstructor: jQuery.Event,\n\tisDefaultPrevented: returnFalse,\n\tisPropagationStopped: returnFalse,\n\tisImmediatePropagationStopped: returnFalse,\n\tisSimulated: false,\n\n\tpreventDefault: function() {\n\t\tvar e = this.originalEvent;\n\n\t\tthis.isDefaultPrevented = returnTrue;\n\n\t\tif ( e && !this.isSimulated ) {\n\t\t\te.preventDefault();\n\t\t}\n\t},\n\tstopPropagation: function() {\n\t\tvar e = this.originalEvent;\n\n\t\tthis.isPropagationStopped = returnTrue;\n\n\t\tif ( e && !this.isSimulated ) {\n\t\t\te.stopPropagation();\n\t\t}\n\t},\n\tstopImmediatePropagation: function() {\n\t\tvar e = this.originalEvent;\n\n\t\tthis.isImmediatePropagationStopped = returnTrue;\n\n\t\tif ( e && !this.isSimulated ) {\n\t\t\te.stopImmediatePropagation();\n\t\t}\n\n\t\tthis.stopPropagation();\n\t}\n};\n\n// Includes all common event props including KeyEvent and MouseEvent specific props\njQuery.each( {\n\taltKey: true,\n\tbubbles: true,\n\tcancelable: true,\n\tchangedTouches: true,\n\tctrlKey: true,\n\tdetail: true,\n\teventPhase: true,\n\tmetaKey: true,\n\tpageX: true,\n\tpageY: true,\n\tshiftKey: true,\n\tview: true,\n\t\"char\": true,\n\tcode: true,\n\tcharCode: true,\n\tkey: true,\n\tkeyCode: true,\n\tbutton: true,\n\tbuttons: true,\n\tclientX: true,\n\tclientY: true,\n\toffsetX: true,\n\toffsetY: true,\n\tpointerId: true,\n\tpointerType: true,\n\tscreenX: true,\n\tscreenY: true,\n\ttargetTouches: true,\n\ttoElement: true,\n\ttouches: true,\n\n\twhich: function( event ) {\n\t\tvar button = event.button;\n\n\t\t// Add which for key events\n\t\tif ( event.which == null && rkeyEvent.test( event.type ) ) {\n\t\t\treturn event.charCode != null ? event.charCode : event.keyCode;\n\t\t}\n\n\t\t// Add which for click: 1 === left; 2 === middle; 3 === right\n\t\tif ( !event.which && button !== undefined && rmouseEvent.test( event.type ) ) {\n\t\t\tif ( button & 1 ) {\n\t\t\t\treturn 1;\n\t\t\t}\n\n\t\t\tif ( button & 2 ) {\n\t\t\t\treturn 3;\n\t\t\t}\n\n\t\t\tif ( button & 4 ) {\n\t\t\t\treturn 2;\n\t\t\t}\n\n\t\t\treturn 0;\n\t\t}\n\n\t\treturn event.which;\n\t}\n}, jQuery.event.addProp );\n\njQuery.each( { focus: \"focusin\", blur: \"focusout\" }, function( type, delegateType ) {\n\tjQuery.event.special[ type ] = {\n\n\t\t// Utilize native event if possible so blur/focus sequence is correct\n\t\tsetup: function() {\n\n\t\t\t// Claim the first handler\n\t\t\t// dataPriv.set( this, \"focus\", ... )\n\t\t\t// dataPriv.set( this, \"blur\", ... )\n\t\t\tleverageNative( this, type, expectSync );\n\n\t\t\t// Return false to allow normal processing in the caller\n\t\t\treturn false;\n\t\t},\n\t\ttrigger: function() {\n\n\t\t\t// Force setup before trigger\n\t\t\tleverageNative( this, type );\n\n\t\t\t// Return non-false to allow normal event-path propagation\n\t\t\treturn true;\n\t\t},\n\n\t\tdelegateType: delegateType\n\t};\n} );\n\n// Create mouseenter/leave events using mouseover/out and event-time checks\n// so that event delegation works in jQuery.\n// Do the same for pointerenter/pointerleave and pointerover/pointerout\n//\n// Support: Safari 7 only\n// Safari sends mouseenter too often; see:\n// https://bugs.chromium.org/p/chromium/issues/detail?id=470258\n// for the description of the bug (it existed in older Chrome versions as well).\njQuery.each( {\n\tmouseenter: \"mouseover\",\n\tmouseleave: \"mouseout\",\n\tpointerenter: \"pointerover\",\n\tpointerleave: \"pointerout\"\n}, function( orig, fix ) {\n\tjQuery.event.special[ orig ] = {\n\t\tdelegateType: fix,\n\t\tbindType: fix,\n\n\t\thandle: function( event ) {\n\t\t\tvar ret,\n\t\t\t\ttarget = this,\n\t\t\t\trelated = event.relatedTarget,\n\t\t\t\thandleObj = event.handleObj;\n\n\t\t\t// For mouseenter/leave call the handler if related is outside the target.\n\t\t\t// NB: No relatedTarget if the mouse left/entered the browser window\n\t\t\tif ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) {\n\t\t\t\tevent.type = handleObj.origType;\n\t\t\t\tret = handleObj.handler.apply( this, arguments );\n\t\t\t\tevent.type = fix;\n\t\t\t}\n\t\t\treturn ret;\n\t\t}\n\t};\n} );\n\njQuery.fn.extend( {\n\n\ton: function( types, selector, data, fn ) {\n\t\treturn on( this, types, selector, data, fn );\n\t},\n\tone: function( types, selector, data, fn ) {\n\t\treturn on( this, types, selector, data, fn, 1 );\n\t},\n\toff: function( types, selector, fn ) {\n\t\tvar handleObj, type;\n\t\tif ( types && types.preventDefault && types.handleObj ) {\n\n\t\t\t// ( event ) dispatched jQuery.Event\n\t\t\thandleObj = types.handleObj;\n\t\t\tjQuery( types.delegateTarget ).off(\n\t\t\t\thandleObj.namespace ?\n\t\t\t\t\thandleObj.origType + \".\" + handleObj.namespace :\n\t\t\t\t\thandleObj.origType,\n\t\t\t\thandleObj.selector,\n\t\t\t\thandleObj.handler\n\t\t\t);\n\t\t\treturn this;\n\t\t}\n\t\tif ( typeof types === \"object\" ) {\n\n\t\t\t// ( types-object [, selector] )\n\t\t\tfor ( type in types ) {\n\t\t\t\tthis.off( type, selector, types[ type ] );\n\t\t\t}\n\t\t\treturn this;\n\t\t}\n\t\tif ( selector === false || typeof selector === \"function\" ) {\n\n\t\t\t// ( types [, fn] )\n\t\t\tfn = selector;\n\t\t\tselector = undefined;\n\t\t}\n\t\tif ( fn === false ) {\n\t\t\tfn = returnFalse;\n\t\t}\n\t\treturn this.each( function() {\n\t\t\tjQuery.event.remove( this, types, fn, selector );\n\t\t} );\n\t}\n} );\n\n\nvar\n\n\t/* eslint-disable max-len */\n\n\t// See https://github.com/eslint/eslint/issues/3229\n\trxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\\/\\0>\\x20\\t\\r\\n\\f]*)[^>]*)\\/>/gi,\n\n\t/* eslint-enable */\n\n\t// Support: IE <=10 - 11, Edge 12 - 13 only\n\t// In IE/Edge using regex groups here causes severe slowdowns.\n\t// See https://connect.microsoft.com/IE/feedback/details/1736512/\n\trnoInnerhtml = /<script|<style|<link/i,\n\n\t// checked=\"checked\" or checked\n\trchecked = /checked\\s*(?:[^=]|=\\s*.checked.)/i,\n\trcleanScript = /^\\s*<!(?:\\[CDATA\\[|--)|(?:\\]\\]|--)>\\s*$/g;\n\n// Prefer a tbody over its parent table for containing new rows\nfunction manipulationTarget( elem, content ) {\n\tif ( nodeName( elem, \"table\" ) &&\n\t\tnodeName( content.nodeType !== 11 ? content : content.firstChild, \"tr\" ) ) {\n\n\t\treturn jQuery( elem ).children( \"tbody\" )[ 0 ] || elem;\n\t}\n\n\treturn elem;\n}\n\n// Replace/restore the type attribute of script elements for safe DOM manipulation\nfunction disableScript( elem ) {\n\telem.type = ( elem.getAttribute( \"type\" ) !== null ) + \"/\" + elem.type;\n\treturn elem;\n}\nfunction restoreScript( elem ) {\n\tif ( ( elem.type || \"\" ).slice( 0, 5 ) === \"true/\" ) {\n\t\telem.type = elem.type.slice( 5 );\n\t} else {\n\t\telem.removeAttribute( \"type\" );\n\t}\n\n\treturn elem;\n}\n\nfunction cloneCopyEvent( src, dest ) {\n\tvar i, l, type, pdataOld, pdataCur, udataOld, udataCur, events;\n\n\tif ( dest.nodeType !== 1 ) {\n\t\treturn;\n\t}\n\n\t// 1. Copy private data: events, handlers, etc.\n\tif ( dataPriv.hasData( src ) ) {\n\t\tpdataOld = dataPriv.access( src );\n\t\tpdataCur = dataPriv.set( dest, pdataOld );\n\t\tevents = pdataOld.events;\n\n\t\tif ( events ) {\n\t\t\tdelete pdataCur.handle;\n\t\t\tpdataCur.events = {};\n\n\t\t\tfor ( type in events ) {\n\t\t\t\tfor ( i = 0, l = events[ type ].length; i < l; i++ ) {\n\t\t\t\t\tjQuery.event.add( dest, type, events[ type ][ i ] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// 2. Copy user data\n\tif ( dataUser.hasData( src ) ) {\n\t\tudataOld = dataUser.access( src );\n\t\tudataCur = jQuery.extend( {}, udataOld );\n\n\t\tdataUser.set( dest, udataCur );\n\t}\n}\n\n// Fix IE bugs, see support tests\nfunction fixInput( src, dest ) {\n\tvar nodeName = dest.nodeName.toLowerCase();\n\n\t// Fails to persist the checked state of a cloned checkbox or radio button.\n\tif ( nodeName === \"input\" && rcheckableType.test( src.type ) ) {\n\t\tdest.checked = src.checked;\n\n\t// Fails to return the selected option to the default selected state when cloning options\n\t} else if ( nodeName === \"input\" || nodeName === \"textarea\" ) {\n\t\tdest.defaultValue = src.defaultValue;\n\t}\n}\n\nfunction domManip( collection, args, callback, ignored ) {\n\n\t// Flatten any nested arrays\n\targs = concat.apply( [], args );\n\n\tvar fragment, first, scripts, hasScripts, node, doc,\n\t\ti = 0,\n\t\tl = collection.length,\n\t\tiNoClone = l - 1,\n\t\tvalue = args[ 0 ],\n\t\tvalueIsFunction = isFunction( value );\n\n\t// We can't cloneNode fragments that contain checked, in WebKit\n\tif ( valueIsFunction ||\n\t\t\t( l > 1 && typeof value === \"string\" &&\n\t\t\t\t!support.checkClone && rchecked.test( value ) ) ) {\n\t\treturn collection.each( function( index ) {\n\t\t\tvar self = collection.eq( index );\n\t\t\tif ( valueIsFunction ) {\n\t\t\t\targs[ 0 ] = value.call( this, index, self.html() );\n\t\t\t}\n\t\t\tdomManip( self, args, callback, ignored );\n\t\t} );\n\t}\n\n\tif ( l ) {\n\t\tfragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored );\n\t\tfirst = fragment.firstChild;\n\n\t\tif ( fragment.childNodes.length === 1 ) {\n\t\t\tfragment = first;\n\t\t}\n\n\t\t// Require either new content or an interest in ignored elements to invoke the callback\n\t\tif ( first || ignored ) {\n\t\t\tscripts = jQuery.map( getAll( fragment, \"script\" ), disableScript );\n\t\t\thasScripts = scripts.length;\n\n\t\t\t// Use the original fragment for the last item\n\t\t\t// instead of the first because it can end up\n\t\t\t// being emptied incorrectly in certain situations (#8070).\n\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\tnode = fragment;\n\n\t\t\t\tif ( i !== iNoClone ) {\n\t\t\t\t\tnode = jQuery.clone( node, true, true );\n\n\t\t\t\t\t// Keep references to cloned scripts for later restoration\n\t\t\t\t\tif ( hasScripts ) {\n\n\t\t\t\t\t\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t\t\t\t\t\t// push.apply(_, arraylike) throws on ancient WebKit\n\t\t\t\t\t\tjQuery.merge( scripts, getAll( node, \"script\" ) );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcallback.call( collection[ i ], node, i );\n\t\t\t}\n\n\t\t\tif ( hasScripts ) {\n\t\t\t\tdoc = scripts[ scripts.length - 1 ].ownerDocument;\n\n\t\t\t\t// Reenable scripts\n\t\t\t\tjQuery.map( scripts, restoreScript );\n\n\t\t\t\t// Evaluate executable scripts on first document insertion\n\t\t\t\tfor ( i = 0; i < hasScripts; i++ ) {\n\t\t\t\t\tnode = scripts[ i ];\n\t\t\t\t\tif ( rscriptType.test( node.type || \"\" ) &&\n\t\t\t\t\t\t!dataPriv.access( node, \"globalEval\" ) &&\n\t\t\t\t\t\tjQuery.contains( doc, node ) ) {\n\n\t\t\t\t\t\tif ( node.src && ( node.type || \"\" ).toLowerCase() !== \"module\" ) {\n\n\t\t\t\t\t\t\t// Optional AJAX dependency, but won't run scripts if not present\n\t\t\t\t\t\t\tif ( jQuery._evalUrl && !node.noModule ) {\n\t\t\t\t\t\t\t\tjQuery._evalUrl( node.src, {\n\t\t\t\t\t\t\t\t\tnonce: node.nonce || node.getAttribute( \"nonce\" )\n\t\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tDOMEval( node.textContent.replace( rcleanScript, \"\" ), node, doc );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn collection;\n}\n\nfunction remove( elem, selector, keepData ) {\n\tvar node,\n\t\tnodes = selector ? jQuery.filter( selector, elem ) : elem,\n\t\ti = 0;\n\n\tfor ( ; ( node = nodes[ i ] ) != null; i++ ) {\n\t\tif ( !keepData && node.nodeType === 1 ) {\n\t\t\tjQuery.cleanData( getAll( node ) );\n\t\t}\n\n\t\tif ( node.parentNode ) {\n\t\t\tif ( keepData && isAttached( node ) ) {\n\t\t\t\tsetGlobalEval( getAll( node, \"script\" ) );\n\t\t\t}\n\t\t\tnode.parentNode.removeChild( node );\n\t\t}\n\t}\n\n\treturn elem;\n}\n\njQuery.extend( {\n\thtmlPrefilter: function( html ) {\n\t\treturn html.replace( rxhtmlTag, \"<$1></$2>\" );\n\t},\n\n\tclone: function( elem, dataAndEvents, deepDataAndEvents ) {\n\t\tvar i, l, srcElements, destElements,\n\t\t\tclone = elem.cloneNode( true ),\n\t\t\tinPage = isAttached( elem );\n\n\t\t// Fix IE cloning issues\n\t\tif ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) &&\n\t\t\t\t!jQuery.isXMLDoc( elem ) ) {\n\n\t\t\t// We eschew Sizzle here for performance reasons: https://jsperf.com/getall-vs-sizzle/2\n\t\t\tdestElements = getAll( clone );\n\t\t\tsrcElements = getAll( elem );\n\n\t\t\tfor ( i = 0, l = srcElements.length; i < l; i++ ) {\n\t\t\t\tfixInput( srcElements[ i ], destElements[ i ] );\n\t\t\t}\n\t\t}\n\n\t\t// Copy the events from the original to the clone\n\t\tif ( dataAndEvents ) {\n\t\t\tif ( deepDataAndEvents ) {\n\t\t\t\tsrcElements = srcElements || getAll( elem );\n\t\t\t\tdestElements = destElements || getAll( clone );\n\n\t\t\t\tfor ( i = 0, l = srcElements.length; i < l; i++ ) {\n\t\t\t\t\tcloneCopyEvent( srcElements[ i ], destElements[ i ] );\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tcloneCopyEvent( elem, clone );\n\t\t\t}\n\t\t}\n\n\t\t// Preserve script evaluation history\n\t\tdestElements = getAll( clone, \"script\" );\n\t\tif ( destElements.length > 0 ) {\n\t\t\tsetGlobalEval( destElements, !inPage && getAll( elem, \"script\" ) );\n\t\t}\n\n\t\t// Return the cloned set\n\t\treturn clone;\n\t},\n\n\tcleanData: function( elems ) {\n\t\tvar data, elem, type,\n\t\t\tspecial = jQuery.event.special,\n\t\t\ti = 0;\n\n\t\tfor ( ; ( elem = elems[ i ] ) !== undefined; i++ ) {\n\t\t\tif ( acceptData( elem ) ) {\n\t\t\t\tif ( ( data = elem[ dataPriv.expando ] ) ) {\n\t\t\t\t\tif ( data.events ) {\n\t\t\t\t\t\tfor ( type in data.events ) {\n\t\t\t\t\t\t\tif ( special[ type ] ) {\n\t\t\t\t\t\t\t\tjQuery.event.remove( elem, type );\n\n\t\t\t\t\t\t\t// This is a shortcut to avoid jQuery.event.remove's overhead\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tjQuery.removeEvent( elem, type, data.handle );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Support: Chrome <=35 - 45+\n\t\t\t\t\t// Assign undefined instead of using delete, see Data#remove\n\t\t\t\t\telem[ dataPriv.expando ] = undefined;\n\t\t\t\t}\n\t\t\t\tif ( elem[ dataUser.expando ] ) {\n\n\t\t\t\t\t// Support: Chrome <=35 - 45+\n\t\t\t\t\t// Assign undefined instead of using delete, see Data#remove\n\t\t\t\t\telem[ dataUser.expando ] = undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n} );\n\njQuery.fn.extend( {\n\tdetach: function( selector ) {\n\t\treturn remove( this, selector, true );\n\t},\n\n\tremove: function( selector ) {\n\t\treturn remove( this, selector );\n\t},\n\n\ttext: function( value ) {\n\t\treturn access( this, function( value ) {\n\t\t\treturn value === undefined ?\n\t\t\t\tjQuery.text( this ) :\n\t\t\t\tthis.empty().each( function() {\n\t\t\t\t\tif ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n\t\t\t\t\t\tthis.textContent = value;\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t}, null, value, arguments.length );\n\t},\n\n\tappend: function() {\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tif ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n\t\t\t\tvar target = manipulationTarget( this, elem );\n\t\t\t\ttarget.appendChild( elem );\n\t\t\t}\n\t\t} );\n\t},\n\n\tprepend: function() {\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tif ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n\t\t\t\tvar target = manipulationTarget( this, elem );\n\t\t\t\ttarget.insertBefore( elem, target.firstChild );\n\t\t\t}\n\t\t} );\n\t},\n\n\tbefore: function() {\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tif ( this.parentNode ) {\n\t\t\t\tthis.parentNode.insertBefore( elem, this );\n\t\t\t}\n\t\t} );\n\t},\n\n\tafter: function() {\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tif ( this.parentNode ) {\n\t\t\t\tthis.parentNode.insertBefore( elem, this.nextSibling );\n\t\t\t}\n\t\t} );\n\t},\n\n\tempty: function() {\n\t\tvar elem,\n\t\t\ti = 0;\n\n\t\tfor ( ; ( elem = this[ i ] ) != null; i++ ) {\n\t\t\tif ( elem.nodeType === 1 ) {\n\n\t\t\t\t// Prevent memory leaks\n\t\t\t\tjQuery.cleanData( getAll( elem, false ) );\n\n\t\t\t\t// Remove any remaining nodes\n\t\t\t\telem.textContent = \"\";\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\tclone: function( dataAndEvents, deepDataAndEvents ) {\n\t\tdataAndEvents = dataAndEvents == null ? false : dataAndEvents;\n\t\tdeepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;\n\n\t\treturn this.map( function() {\n\t\t\treturn jQuery.clone( this, dataAndEvents, deepDataAndEvents );\n\t\t} );\n\t},\n\n\thtml: function( value ) {\n\t\treturn access( this, function( value ) {\n\t\t\tvar elem = this[ 0 ] || {},\n\t\t\t\ti = 0,\n\t\t\t\tl = this.length;\n\n\t\t\tif ( value === undefined && elem.nodeType === 1 ) {\n\t\t\t\treturn elem.innerHTML;\n\t\t\t}\n\n\t\t\t// See if we can take a shortcut and just use innerHTML\n\t\t\tif ( typeof value === \"string\" && !rnoInnerhtml.test( value ) &&\n\t\t\t\t!wrapMap[ ( rtagName.exec( value ) || [ \"\", \"\" ] )[ 1 ].toLowerCase() ] ) {\n\n\t\t\t\tvalue = jQuery.htmlPrefilter( value );\n\n\t\t\t\ttry {\n\t\t\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\t\t\telem = this[ i ] || {};\n\n\t\t\t\t\t\t// Remove element nodes and prevent memory leaks\n\t\t\t\t\t\tif ( elem.nodeType === 1 ) {\n\t\t\t\t\t\t\tjQuery.cleanData( getAll( elem, false ) );\n\t\t\t\t\t\t\telem.innerHTML = value;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\telem = 0;\n\n\t\t\t\t// If using innerHTML throws an exception, use the fallback method\n\t\t\t\t} catch ( e ) {}\n\t\t\t}\n\n\t\t\tif ( elem ) {\n\t\t\t\tthis.empty().append( value );\n\t\t\t}\n\t\t}, null, value, arguments.length );\n\t},\n\n\treplaceWith: function() {\n\t\tvar ignored = [];\n\n\t\t// Make the changes, replacing each non-ignored context element with the new content\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tvar parent = this.parentNode;\n\n\t\t\tif ( jQuery.inArray( this, ignored ) < 0 ) {\n\t\t\t\tjQuery.cleanData( getAll( this ) );\n\t\t\t\tif ( parent ) {\n\t\t\t\t\tparent.replaceChild( elem, this );\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Force callback invocation\n\t\t}, ignored );\n\t}\n} );\n\njQuery.each( {\n\tappendTo: \"append\",\n\tprependTo: \"prepend\",\n\tinsertBefore: \"before\",\n\tinsertAfter: \"after\",\n\treplaceAll: \"replaceWith\"\n}, function( name, original ) {\n\tjQuery.fn[ name ] = function( selector ) {\n\t\tvar elems,\n\t\t\tret = [],\n\t\t\tinsert = jQuery( selector ),\n\t\t\tlast = insert.length - 1,\n\t\t\ti = 0;\n\n\t\tfor ( ; i <= last; i++ ) {\n\t\t\telems = i === last ? this : this.clone( true );\n\t\t\tjQuery( insert[ i ] )[ original ]( elems );\n\n\t\t\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t\t\t// .get() because push.apply(_, arraylike) throws on ancient WebKit\n\t\t\tpush.apply( ret, elems.get() );\n\t\t}\n\n\t\treturn this.pushStack( ret );\n\t};\n} );\nvar rnumnonpx = new RegExp( \"^(\" + pnum + \")(?!px)[a-z%]+$\", \"i\" );\n\nvar getStyles = function( elem ) {\n\n\t\t// Support: IE <=11 only, Firefox <=30 (#15098, #14150)\n\t\t// IE throws on elements created in popups\n\t\t// FF meanwhile throws on frame elements through \"defaultView.getComputedStyle\"\n\t\tvar view = elem.ownerDocument.defaultView;\n\n\t\tif ( !view || !view.opener ) {\n\t\t\tview = window;\n\t\t}\n\n\t\treturn view.getComputedStyle( elem );\n\t};\n\nvar rboxStyle = new RegExp( cssExpand.join( \"|\" ), \"i\" );\n\n\n\n( function() {\n\n\t// Executing both pixelPosition & boxSizingReliable tests require only one layout\n\t// so they're executed at the same time to save the second computation.\n\tfunction computeStyleTests() {\n\n\t\t// This is a singleton, we need to execute it only once\n\t\tif ( !div ) {\n\t\t\treturn;\n\t\t}\n\n\t\tcontainer.style.cssText = \"position:absolute;left:-11111px;width:60px;\" +\n\t\t\t\"margin-top:1px;padding:0;border:0\";\n\t\tdiv.style.cssText =\n\t\t\t\"position:relative;display:block;box-sizing:border-box;overflow:scroll;\" +\n\t\t\t\"margin:auto;border:1px;padding:1px;\" +\n\t\t\t\"width:60%;top:1%\";\n\t\tdocumentElement.appendChild( container ).appendChild( div );\n\n\t\tvar divStyle = window.getComputedStyle( div );\n\t\tpixelPositionVal = divStyle.top !== \"1%\";\n\n\t\t// Support: Android 4.0 - 4.3 only, Firefox <=3 - 44\n\t\treliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12;\n\n\t\t// Support: Android 4.0 - 4.3 only, Safari <=9.1 - 10.1, iOS <=7.0 - 9.3\n\t\t// Some styles come back with percentage values, even though they shouldn't\n\t\tdiv.style.right = \"60%\";\n\t\tpixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36;\n\n\t\t// Support: IE 9 - 11 only\n\t\t// Detect misreporting of content dimensions for box-sizing:border-box elements\n\t\tboxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36;\n\n\t\t// Support: IE 9 only\n\t\t// Detect overflow:scroll screwiness (gh-3699)\n\t\t// Support: Chrome <=64\n\t\t// Don't get tricked when zoom affects offsetWidth (gh-4029)\n\t\tdiv.style.position = \"absolute\";\n\t\tscrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12;\n\n\t\tdocumentElement.removeChild( container );\n\n\t\t// Nullify the div so it wouldn't be stored in the memory and\n\t\t// it will also be a sign that checks already performed\n\t\tdiv = null;\n\t}\n\n\tfunction roundPixelMeasures( measure ) {\n\t\treturn Math.round( parseFloat( measure ) );\n\t}\n\n\tvar pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal,\n\t\treliableMarginLeftVal,\n\t\tcontainer = document.createElement( \"div\" ),\n\t\tdiv = document.createElement( \"div\" );\n\n\t// Finish early in limited (non-browser) environments\n\tif ( !div.style ) {\n\t\treturn;\n\t}\n\n\t// Support: IE <=9 - 11 only\n\t// Style of cloned element affects source element cloned (#8908)\n\tdiv.style.backgroundClip = \"content-box\";\n\tdiv.cloneNode( true ).style.backgroundClip = \"\";\n\tsupport.clearCloneStyle = div.style.backgroundClip === \"content-box\";\n\n\tjQuery.extend( support, {\n\t\tboxSizingReliable: function() {\n\t\t\tcomputeStyleTests();\n\t\t\treturn boxSizingReliableVal;\n\t\t},\n\t\tpixelBoxStyles: function() {\n\t\t\tcomputeStyleTests();\n\t\t\treturn pixelBoxStylesVal;\n\t\t},\n\t\tpixelPosition: function() {\n\t\t\tcomputeStyleTests();\n\t\t\treturn pixelPositionVal;\n\t\t},\n\t\treliableMarginLeft: function() {\n\t\t\tcomputeStyleTests();\n\t\t\treturn reliableMarginLeftVal;\n\t\t},\n\t\tscrollboxSize: function() {\n\t\t\tcomputeStyleTests();\n\t\t\treturn scrollboxSizeVal;\n\t\t}\n\t} );\n} )();\n\n\nfunction curCSS( elem, name, computed ) {\n\tvar width, minWidth, maxWidth, ret,\n\n\t\t// Support: Firefox 51+\n\t\t// Retrieving style before computed somehow\n\t\t// fixes an issue with getting wrong values\n\t\t// on detached elements\n\t\tstyle = elem.style;\n\n\tcomputed = computed || getStyles( elem );\n\n\t// getPropertyValue is needed for:\n\t// .css('filter') (IE 9 only, #12537)\n\t// .css('--customProperty) (#3144)\n\tif ( computed ) {\n\t\tret = computed.getPropertyValue( name ) || computed[ name ];\n\n\t\tif ( ret === \"\" && !isAttached( elem ) ) {\n\t\t\tret = jQuery.style( elem, name );\n\t\t}\n\n\t\t// A tribute to the \"awesome hack by Dean Edwards\"\n\t\t// Android Browser returns percentage for some values,\n\t\t// but width seems to be reliably pixels.\n\t\t// This is against the CSSOM draft spec:\n\t\t// https://drafts.csswg.org/cssom/#resolved-values\n\t\tif ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) {\n\n\t\t\t// Remember the original values\n\t\t\twidth = style.width;\n\t\t\tminWidth = style.minWidth;\n\t\t\tmaxWidth = style.maxWidth;\n\n\t\t\t// Put in the new values to get a computed value out\n\t\t\tstyle.minWidth = style.maxWidth = style.width = ret;\n\t\t\tret = computed.width;\n\n\t\t\t// Revert the changed values\n\t\t\tstyle.width = width;\n\t\t\tstyle.minWidth = minWidth;\n\t\t\tstyle.maxWidth = maxWidth;\n\t\t}\n\t}\n\n\treturn ret !== undefined ?\n\n\t\t// Support: IE <=9 - 11 only\n\t\t// IE returns zIndex value as an integer.\n\t\tret + \"\" :\n\t\tret;\n}\n\n\nfunction addGetHookIf( conditionFn, hookFn ) {\n\n\t// Define the hook, we'll check on the first run if it's really needed.\n\treturn {\n\t\tget: function() {\n\t\t\tif ( conditionFn() ) {\n\n\t\t\t\t// Hook not needed (or it's not possible to use it due\n\t\t\t\t// to missing dependency), remove it.\n\t\t\t\tdelete this.get;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Hook needed; redefine it so that the support test is not executed again.\n\t\t\treturn ( this.get = hookFn ).apply( this, arguments );\n\t\t}\n\t};\n}\n\n\nvar cssPrefixes = [ \"Webkit\", \"Moz\", \"ms\" ],\n\temptyStyle = document.createElement( \"div\" ).style,\n\tvendorProps = {};\n\n// Return a vendor-prefixed property or undefined\nfunction vendorPropName( name ) {\n\n\t// Check for vendor prefixed names\n\tvar capName = name[ 0 ].toUpperCase() + name.slice( 1 ),\n\t\ti = cssPrefixes.length;\n\n\twhile ( i-- ) {\n\t\tname = cssPrefixes[ i ] + capName;\n\t\tif ( name in emptyStyle ) {\n\t\t\treturn name;\n\t\t}\n\t}\n}\n\n// Return a potentially-mapped jQuery.cssProps or vendor prefixed property\nfunction finalPropName( name ) {\n\tvar final = jQuery.cssProps[ name ] || vendorProps[ name ];\n\n\tif ( final ) {\n\t\treturn final;\n\t}\n\tif ( name in emptyStyle ) {\n\t\treturn name;\n\t}\n\treturn vendorProps[ name ] = vendorPropName( name ) || name;\n}\n\n\nvar\n\n\t// Swappable if display is none or starts with table\n\t// except \"table\", \"table-cell\", or \"table-caption\"\n\t// See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display\n\trdisplayswap = /^(none|table(?!-c[ea]).+)/,\n\trcustomProp = /^--/,\n\tcssShow = { position: \"absolute\", visibility: \"hidden\", display: \"block\" },\n\tcssNormalTransform = {\n\t\tletterSpacing: \"0\",\n\t\tfontWeight: \"400\"\n\t};\n\nfunction setPositiveNumber( elem, value, subtract ) {\n\n\t// Any relative (+/-) values have already been\n\t// normalized at this point\n\tvar matches = rcssNum.exec( value );\n\treturn matches ?\n\n\t\t// Guard against undefined \"subtract\", e.g., when used as in cssHooks\n\t\tMath.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || \"px\" ) :\n\t\tvalue;\n}\n\nfunction boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) {\n\tvar i = dimension === \"width\" ? 1 : 0,\n\t\textra = 0,\n\t\tdelta = 0;\n\n\t// Adjustment may not be necessary\n\tif ( box === ( isBorderBox ? \"border\" : \"content\" ) ) {\n\t\treturn 0;\n\t}\n\n\tfor ( ; i < 4; i += 2 ) {\n\n\t\t// Both box models exclude margin\n\t\tif ( box === \"margin\" ) {\n\t\t\tdelta += jQuery.css( elem, box + cssExpand[ i ], true, styles );\n\t\t}\n\n\t\t// If we get here with a content-box, we're seeking \"padding\" or \"border\" or \"margin\"\n\t\tif ( !isBorderBox ) {\n\n\t\t\t// Add padding\n\t\t\tdelta += jQuery.css( elem, \"padding\" + cssExpand[ i ], true, styles );\n\n\t\t\t// For \"border\" or \"margin\", add border\n\t\t\tif ( box !== \"padding\" ) {\n\t\t\t\tdelta += jQuery.css( elem, \"border\" + cssExpand[ i ] + \"Width\", true, styles );\n\n\t\t\t// But still keep track of it otherwise\n\t\t\t} else {\n\t\t\t\textra += jQuery.css( elem, \"border\" + cssExpand[ i ] + \"Width\", true, styles );\n\t\t\t}\n\n\t\t// If we get here with a border-box (content + padding + border), we're seeking \"content\" or\n\t\t// \"padding\" or \"margin\"\n\t\t} else {\n\n\t\t\t// For \"content\", subtract padding\n\t\t\tif ( box === \"content\" ) {\n\t\t\t\tdelta -= jQuery.css( elem, \"padding\" + cssExpand[ i ], true, styles );\n\t\t\t}\n\n\t\t\t// For \"content\" or \"padding\", subtract border\n\t\t\tif ( box !== \"margin\" ) {\n\t\t\t\tdelta -= jQuery.css( elem, \"border\" + cssExpand[ i ] + \"Width\", true, styles );\n\t\t\t}\n\t\t}\n\t}\n\n\t// Account for positive content-box scroll gutter when requested by providing computedVal\n\tif ( !isBorderBox && computedVal >= 0 ) {\n\n\t\t// offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border\n\t\t// Assuming integer scroll gutter, subtract the rest and round down\n\t\tdelta += Math.max( 0, Math.ceil(\n\t\t\telem[ \"offset\" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] -\n\t\t\tcomputedVal -\n\t\t\tdelta -\n\t\t\textra -\n\t\t\t0.5\n\n\t\t// If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter\n\t\t// Use an explicit zero to avoid NaN (gh-3964)\n\t\t) ) || 0;\n\t}\n\n\treturn delta;\n}\n\nfunction getWidthOrHeight( elem, dimension, extra ) {\n\n\t// Start with computed style\n\tvar styles = getStyles( elem ),\n\n\t\t// To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322).\n\t\t// Fake content-box until we know it's needed to know the true value.\n\t\tboxSizingNeeded = !support.boxSizingReliable() || extra,\n\t\tisBorderBox = boxSizingNeeded &&\n\t\t\tjQuery.css( elem, \"boxSizing\", false, styles ) === \"border-box\",\n\t\tvalueIsBorderBox = isBorderBox,\n\n\t\tval = curCSS( elem, dimension, styles ),\n\t\toffsetProp = \"offset\" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 );\n\n\t// Support: Firefox <=54\n\t// Return a confounding non-pixel value or feign ignorance, as appropriate.\n\tif ( rnumnonpx.test( val ) ) {\n\t\tif ( !extra ) {\n\t\t\treturn val;\n\t\t}\n\t\tval = \"auto\";\n\t}\n\n\n\t// Fall back to offsetWidth/offsetHeight when value is \"auto\"\n\t// This happens for inline elements with no explicit setting (gh-3571)\n\t// Support: Android <=4.1 - 4.3 only\n\t// Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602)\n\t// Support: IE 9-11 only\n\t// Also use offsetWidth/offsetHeight for when box sizing is unreliable\n\t// We use getClientRects() to check for hidden/disconnected.\n\t// In those cases, the computed value can be trusted to be border-box\n\tif ( ( !support.boxSizingReliable() && isBorderBox ||\n\t\tval === \"auto\" ||\n\t\t!parseFloat( val ) && jQuery.css( elem, \"display\", false, styles ) === \"inline\" ) &&\n\t\telem.getClientRects().length ) {\n\n\t\tisBorderBox = jQuery.css( elem, \"boxSizing\", false, styles ) === \"border-box\";\n\n\t\t// Where available, offsetWidth/offsetHeight approximate border box dimensions.\n\t\t// Where not available (e.g., SVG), assume unreliable box-sizing and interpret the\n\t\t// retrieved value as a content box dimension.\n\t\tvalueIsBorderBox = offsetProp in elem;\n\t\tif ( valueIsBorderBox ) {\n\t\t\tval = elem[ offsetProp ];\n\t\t}\n\t}\n\n\t// Normalize \"\" and auto\n\tval = parseFloat( val ) || 0;\n\n\t// Adjust for the element's box model\n\treturn ( val +\n\t\tboxModelAdjustment(\n\t\t\telem,\n\t\t\tdimension,\n\t\t\textra || ( isBorderBox ? \"border\" : \"content\" ),\n\t\t\tvalueIsBorderBox,\n\t\t\tstyles,\n\n\t\t\t// Provide the current computed size to request scroll gutter calculation (gh-3589)\n\t\t\tval\n\t\t)\n\t) + \"px\";\n}\n\njQuery.extend( {\n\n\t// Add in style property hooks for overriding the default\n\t// behavior of getting and setting a style property\n\tcssHooks: {\n\t\topacity: {\n\t\t\tget: function( elem, computed ) {\n\t\t\t\tif ( computed ) {\n\n\t\t\t\t\t// We should always get a number back from opacity\n\t\t\t\t\tvar ret = curCSS( elem, \"opacity\" );\n\t\t\t\t\treturn ret === \"\" ? \"1\" : ret;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\n\t// Don't automatically add \"px\" to these possibly-unitless properties\n\tcssNumber: {\n\t\t\"animationIterationCount\": true,\n\t\t\"columnCount\": true,\n\t\t\"fillOpacity\": true,\n\t\t\"flexGrow\": true,\n\t\t\"flexShrink\": true,\n\t\t\"fontWeight\": true,\n\t\t\"gridArea\": true,\n\t\t\"gridColumn\": true,\n\t\t\"gridColumnEnd\": true,\n\t\t\"gridColumnStart\": true,\n\t\t\"gridRow\": true,\n\t\t\"gridRowEnd\": true,\n\t\t\"gridRowStart\": true,\n\t\t\"lineHeight\": true,\n\t\t\"opacity\": true,\n\t\t\"order\": true,\n\t\t\"orphans\": true,\n\t\t\"widows\": true,\n\t\t\"zIndex\": true,\n\t\t\"zoom\": true\n\t},\n\n\t// Add in properties whose names you wish to fix before\n\t// setting or getting the value\n\tcssProps: {},\n\n\t// Get and set the style property on a DOM Node\n\tstyle: function( elem, name, value, extra ) {\n\n\t\t// Don't set styles on text and comment nodes\n\t\tif ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Make sure that we're working with the right name\n\t\tvar ret, type, hooks,\n\t\t\torigName = camelCase( name ),\n\t\t\tisCustomProp = rcustomProp.test( name ),\n\t\t\tstyle = elem.style;\n\n\t\t// Make sure that we're working with the right name. We don't\n\t\t// want to query the value if it is a CSS custom property\n\t\t// since they are user-defined.\n\t\tif ( !isCustomProp ) {\n\t\t\tname = finalPropName( origName );\n\t\t}\n\n\t\t// Gets hook for the prefixed version, then unprefixed version\n\t\thooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];\n\n\t\t// Check if we're setting a value\n\t\tif ( value !== undefined ) {\n\t\t\ttype = typeof value;\n\n\t\t\t// Convert \"+=\" or \"-=\" to relative numbers (#7345)\n\t\t\tif ( type === \"string\" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) {\n\t\t\t\tvalue = adjustCSS( elem, name, ret );\n\n\t\t\t\t// Fixes bug #9237\n\t\t\t\ttype = \"number\";\n\t\t\t}\n\n\t\t\t// Make sure that null and NaN values aren't set (#7116)\n\t\t\tif ( value == null || value !== value ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// If a number was passed in, add the unit (except for certain CSS properties)\n\t\t\t// The isCustomProp check can be removed in jQuery 4.0 when we only auto-append\n\t\t\t// \"px\" to a few hardcoded values.\n\t\t\tif ( type === \"number\" && !isCustomProp ) {\n\t\t\t\tvalue += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? \"\" : \"px\" );\n\t\t\t}\n\n\t\t\t// background-* props affect original clone's values\n\t\t\tif ( !support.clearCloneStyle && value === \"\" && name.indexOf( \"background\" ) === 0 ) {\n\t\t\t\tstyle[ name ] = \"inherit\";\n\t\t\t}\n\n\t\t\t// If a hook was provided, use that value, otherwise just set the specified value\n\t\t\tif ( !hooks || !( \"set\" in hooks ) ||\n\t\t\t\t( value = hooks.set( elem, value, extra ) ) !== undefined ) {\n\n\t\t\t\tif ( isCustomProp ) {\n\t\t\t\t\tstyle.setProperty( name, value );\n\t\t\t\t} else {\n\t\t\t\t\tstyle[ name ] = value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t} else {\n\n\t\t\t// If a hook was provided get the non-computed value from there\n\t\t\tif ( hooks && \"get\" in hooks &&\n\t\t\t\t( ret = hooks.get( elem, false, extra ) ) !== undefined ) {\n\n\t\t\t\treturn ret;\n\t\t\t}\n\n\t\t\t// Otherwise just get the value from the style object\n\t\t\treturn style[ name ];\n\t\t}\n\t},\n\n\tcss: function( elem, name, extra, styles ) {\n\t\tvar val, num, hooks,\n\t\t\torigName = camelCase( name ),\n\t\t\tisCustomProp = rcustomProp.test( name );\n\n\t\t// Make sure that we're working with the right name. We don't\n\t\t// want to modify the value if it is a CSS custom property\n\t\t// since they are user-defined.\n\t\tif ( !isCustomProp ) {\n\t\t\tname = finalPropName( origName );\n\t\t}\n\n\t\t// Try prefixed name followed by the unprefixed name\n\t\thooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];\n\n\t\t// If a hook was provided get the computed value from there\n\t\tif ( hooks && \"get\" in hooks ) {\n\t\t\tval = hooks.get( elem, true, extra );\n\t\t}\n\n\t\t// Otherwise, if a way to get the computed value exists, use that\n\t\tif ( val === undefined ) {\n\t\t\tval = curCSS( elem, name, styles );\n\t\t}\n\n\t\t// Convert \"normal\" to computed value\n\t\tif ( val === \"normal\" && name in cssNormalTransform ) {\n\t\t\tval = cssNormalTransform[ name ];\n\t\t}\n\n\t\t// Make numeric if forced or a qualifier was provided and val looks numeric\n\t\tif ( extra === \"\" || extra ) {\n\t\t\tnum = parseFloat( val );\n\t\t\treturn extra === true || isFinite( num ) ? num || 0 : val;\n\t\t}\n\n\t\treturn val;\n\t}\n} );\n\njQuery.each( [ \"height\", \"width\" ], function( i, dimension ) {\n\tjQuery.cssHooks[ dimension ] = {\n\t\tget: function( elem, computed, extra ) {\n\t\t\tif ( computed ) {\n\n\t\t\t\t// Certain elements can have dimension info if we invisibly show them\n\t\t\t\t// but it must have a current display style that would benefit\n\t\t\t\treturn rdisplayswap.test( jQuery.css( elem, \"display\" ) ) &&\n\n\t\t\t\t\t// Support: Safari 8+\n\t\t\t\t\t// Table columns in Safari have non-zero offsetWidth & zero\n\t\t\t\t\t// getBoundingClientRect().width unless display is changed.\n\t\t\t\t\t// Support: IE <=11 only\n\t\t\t\t\t// Running getBoundingClientRect on a disconnected node\n\t\t\t\t\t// in IE throws an error.\n\t\t\t\t\t( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ?\n\t\t\t\t\t\tswap( elem, cssShow, function() {\n\t\t\t\t\t\t\treturn getWidthOrHeight( elem, dimension, extra );\n\t\t\t\t\t\t} ) :\n\t\t\t\t\t\tgetWidthOrHeight( elem, dimension, extra );\n\t\t\t}\n\t\t},\n\n\t\tset: function( elem, value, extra ) {\n\t\t\tvar matches,\n\t\t\t\tstyles = getStyles( elem ),\n\n\t\t\t\t// Only read styles.position if the test has a chance to fail\n\t\t\t\t// to avoid forcing a reflow.\n\t\t\t\tscrollboxSizeBuggy = !support.scrollboxSize() &&\n\t\t\t\t\tstyles.position === \"absolute\",\n\n\t\t\t\t// To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991)\n\t\t\t\tboxSizingNeeded = scrollboxSizeBuggy || extra,\n\t\t\t\tisBorderBox = boxSizingNeeded &&\n\t\t\t\t\tjQuery.css( elem, \"boxSizing\", false, styles ) === \"border-box\",\n\t\t\t\tsubtract = extra ?\n\t\t\t\t\tboxModelAdjustment(\n\t\t\t\t\t\telem,\n\t\t\t\t\t\tdimension,\n\t\t\t\t\t\textra,\n\t\t\t\t\t\tisBorderBox,\n\t\t\t\t\t\tstyles\n\t\t\t\t\t) :\n\t\t\t\t\t0;\n\n\t\t\t// Account for unreliable border-box dimensions by comparing offset* to computed and\n\t\t\t// faking a content-box to get border and padding (gh-3699)\n\t\t\tif ( isBorderBox && scrollboxSizeBuggy ) {\n\t\t\t\tsubtract -= Math.ceil(\n\t\t\t\t\telem[ \"offset\" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] -\n\t\t\t\t\tparseFloat( styles[ dimension ] ) -\n\t\t\t\t\tboxModelAdjustment( elem, dimension, \"border\", false, styles ) -\n\t\t\t\t\t0.5\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Convert to pixels if value adjustment is needed\n\t\t\tif ( subtract && ( matches = rcssNum.exec( value ) ) &&\n\t\t\t\t( matches[ 3 ] || \"px\" ) !== \"px\" ) {\n\n\t\t\t\telem.style[ dimension ] = value;\n\t\t\t\tvalue = jQuery.css( elem, dimension );\n\t\t\t}\n\n\t\t\treturn setPositiveNumber( elem, value, subtract );\n\t\t}\n\t};\n} );\n\njQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft,\n\tfunction( elem, computed ) {\n\t\tif ( computed ) {\n\t\t\treturn ( parseFloat( curCSS( elem, \"marginLeft\" ) ) ||\n\t\t\t\telem.getBoundingClientRect().left -\n\t\t\t\t\tswap( elem, { marginLeft: 0 }, function() {\n\t\t\t\t\t\treturn elem.getBoundingClientRect().left;\n\t\t\t\t\t} )\n\t\t\t\t) + \"px\";\n\t\t}\n\t}\n);\n\n// These hooks are used by animate to expand properties\njQuery.each( {\n\tmargin: \"\",\n\tpadding: \"\",\n\tborder: \"Width\"\n}, function( prefix, suffix ) {\n\tjQuery.cssHooks[ prefix + suffix ] = {\n\t\texpand: function( value ) {\n\t\t\tvar i = 0,\n\t\t\t\texpanded = {},\n\n\t\t\t\t// Assumes a single number if not a string\n\t\t\t\tparts = typeof value === \"string\" ? value.split( \" \" ) : [ value ];\n\n\t\t\tfor ( ; i < 4; i++ ) {\n\t\t\t\texpanded[ prefix + cssExpand[ i ] + suffix ] =\n\t\t\t\t\tparts[ i ] || parts[ i - 2 ] || parts[ 0 ];\n\t\t\t}\n\n\t\t\treturn expanded;\n\t\t}\n\t};\n\n\tif ( prefix !== \"margin\" ) {\n\t\tjQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;\n\t}\n} );\n\njQuery.fn.extend( {\n\tcss: function( name, value ) {\n\t\treturn access( this, function( elem, name, value ) {\n\t\t\tvar styles, len,\n\t\t\t\tmap = {},\n\t\t\t\ti = 0;\n\n\t\t\tif ( Array.isArray( name ) ) {\n\t\t\t\tstyles = getStyles( elem );\n\t\t\t\tlen = name.length;\n\n\t\t\t\tfor ( ; i < len; i++ ) {\n\t\t\t\t\tmap[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );\n\t\t\t\t}\n\n\t\t\t\treturn map;\n\t\t\t}\n\n\t\t\treturn value !== undefined ?\n\t\t\t\tjQuery.style( elem, name, value ) :\n\t\t\t\tjQuery.css( elem, name );\n\t\t}, name, value, arguments.length > 1 );\n\t}\n} );\n\n\nfunction Tween( elem, options, prop, end, easing ) {\n\treturn new Tween.prototype.init( elem, options, prop, end, easing );\n}\njQuery.Tween = Tween;\n\nTween.prototype = {\n\tconstructor: Tween,\n\tinit: function( elem, options, prop, end, easing, unit ) {\n\t\tthis.elem = elem;\n\t\tthis.prop = prop;\n\t\tthis.easing = easing || jQuery.easing._default;\n\t\tthis.options = options;\n\t\tthis.start = this.now = this.cur();\n\t\tthis.end = end;\n\t\tthis.unit = unit || ( jQuery.cssNumber[ prop ] ? \"\" : \"px\" );\n\t},\n\tcur: function() {\n\t\tvar hooks = Tween.propHooks[ this.prop ];\n\n\t\treturn hooks && hooks.get ?\n\t\t\thooks.get( this ) :\n\t\t\tTween.propHooks._default.get( this );\n\t},\n\trun: function( percent ) {\n\t\tvar eased,\n\t\t\thooks = Tween.propHooks[ this.prop ];\n\n\t\tif ( this.options.duration ) {\n\t\t\tthis.pos = eased = jQuery.easing[ this.easing ](\n\t\t\t\tpercent, this.options.duration * percent, 0, 1, this.options.duration\n\t\t\t);\n\t\t} else {\n\t\t\tthis.pos = eased = percent;\n\t\t}\n\t\tthis.now = ( this.end - this.start ) * eased + this.start;\n\n\t\tif ( this.options.step ) {\n\t\t\tthis.options.step.call( this.elem, this.now, this );\n\t\t}\n\n\t\tif ( hooks && hooks.set ) {\n\t\t\thooks.set( this );\n\t\t} else {\n\t\t\tTween.propHooks._default.set( this );\n\t\t}\n\t\treturn this;\n\t}\n};\n\nTween.prototype.init.prototype = Tween.prototype;\n\nTween.propHooks = {\n\t_default: {\n\t\tget: function( tween ) {\n\t\t\tvar result;\n\n\t\t\t// Use a property on the element directly when it is not a DOM element,\n\t\t\t// or when there is no matching style property that exists.\n\t\t\tif ( tween.elem.nodeType !== 1 ||\n\t\t\t\ttween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) {\n\t\t\t\treturn tween.elem[ tween.prop ];\n\t\t\t}\n\n\t\t\t// Passing an empty string as a 3rd parameter to .css will automatically\n\t\t\t// attempt a parseFloat and fallback to a string if the parse fails.\n\t\t\t// Simple values such as \"10px\" are parsed to Float;\n\t\t\t// complex values such as \"rotate(1rad)\" are returned as-is.\n\t\t\tresult = jQuery.css( tween.elem, tween.prop, \"\" );\n\n\t\t\t// Empty strings, null, undefined and \"auto\" are converted to 0.\n\t\t\treturn !result || result === \"auto\" ? 0 : result;\n\t\t},\n\t\tset: function( tween ) {\n\n\t\t\t// Use step hook for back compat.\n\t\t\t// Use cssHook if its there.\n\t\t\t// Use .style if available and use plain properties where available.\n\t\t\tif ( jQuery.fx.step[ tween.prop ] ) {\n\t\t\t\tjQuery.fx.step[ tween.prop ]( tween );\n\t\t\t} else if ( tween.elem.nodeType === 1 && (\n\t\t\t\t\tjQuery.cssHooks[ tween.prop ] ||\n\t\t\t\t\ttween.elem.style[ finalPropName( tween.prop ) ] != null ) ) {\n\t\t\t\tjQuery.style( tween.elem, tween.prop, tween.now + tween.unit );\n\t\t\t} else {\n\t\t\t\ttween.elem[ tween.prop ] = tween.now;\n\t\t\t}\n\t\t}\n\t}\n};\n\n// Support: IE <=9 only\n// Panic based approach to setting things on disconnected nodes\nTween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {\n\tset: function( tween ) {\n\t\tif ( tween.elem.nodeType && tween.elem.parentNode ) {\n\t\t\ttween.elem[ tween.prop ] = tween.now;\n\t\t}\n\t}\n};\n\njQuery.easing = {\n\tlinear: function( p ) {\n\t\treturn p;\n\t},\n\tswing: function( p ) {\n\t\treturn 0.5 - Math.cos( p * Math.PI ) / 2;\n\t},\n\t_default: \"swing\"\n};\n\njQuery.fx = Tween.prototype.init;\n\n// Back compat <1.8 extension point\njQuery.fx.step = {};\n\n\n\n\nvar\n\tfxNow, inProgress,\n\trfxtypes = /^(?:toggle|show|hide)$/,\n\trrun = /queueHooks$/;\n\nfunction schedule() {\n\tif ( inProgress ) {\n\t\tif ( document.hidden === false && window.requestAnimationFrame ) {\n\t\t\twindow.requestAnimationFrame( schedule );\n\t\t} else {\n\t\t\twindow.setTimeout( schedule, jQuery.fx.interval );\n\t\t}\n\n\t\tjQuery.fx.tick();\n\t}\n}\n\n// Animations created synchronously will run synchronously\nfunction createFxNow() {\n\twindow.setTimeout( function() {\n\t\tfxNow = undefined;\n\t} );\n\treturn ( fxNow = Date.now() );\n}\n\n// Generate parameters to create a standard animation\nfunction genFx( type, includeWidth ) {\n\tvar which,\n\t\ti = 0,\n\t\tattrs = { height: type };\n\n\t// If we include width, step value is 1 to do all cssExpand values,\n\t// otherwise step value is 2 to skip over Left and Right\n\tincludeWidth = includeWidth ? 1 : 0;\n\tfor ( ; i < 4; i += 2 - includeWidth ) {\n\t\twhich = cssExpand[ i ];\n\t\tattrs[ \"margin\" + which ] = attrs[ \"padding\" + which ] = type;\n\t}\n\n\tif ( includeWidth ) {\n\t\tattrs.opacity = attrs.width = type;\n\t}\n\n\treturn attrs;\n}\n\nfunction createTween( value, prop, animation ) {\n\tvar tween,\n\t\tcollection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ \"*\" ] ),\n\t\tindex = 0,\n\t\tlength = collection.length;\n\tfor ( ; index < length; index++ ) {\n\t\tif ( ( tween = collection[ index ].call( animation, prop, value ) ) ) {\n\n\t\t\t// We're done with this property\n\t\t\treturn tween;\n\t\t}\n\t}\n}\n\nfunction defaultPrefilter( elem, props, opts ) {\n\tvar prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display,\n\t\tisBox = \"width\" in props || \"height\" in props,\n\t\tanim = this,\n\t\torig = {},\n\t\tstyle = elem.style,\n\t\thidden = elem.nodeType && isHiddenWithinTree( elem ),\n\t\tdataShow = dataPriv.get( elem, \"fxshow\" );\n\n\t// Queue-skipping animations hijack the fx hooks\n\tif ( !opts.queue ) {\n\t\thooks = jQuery._queueHooks( elem, \"fx\" );\n\t\tif ( hooks.unqueued == null ) {\n\t\t\thooks.unqueued = 0;\n\t\t\toldfire = hooks.empty.fire;\n\t\t\thooks.empty.fire = function() {\n\t\t\t\tif ( !hooks.unqueued ) {\n\t\t\t\t\toldfire();\n\t\t\t\t}\n\t\t\t};\n\t\t}\n\t\thooks.unqueued++;\n\n\t\tanim.always( function() {\n\n\t\t\t// Ensure the complete handler is called before this completes\n\t\t\tanim.always( function() {\n\t\t\t\thooks.unqueued--;\n\t\t\t\tif ( !jQuery.queue( elem, \"fx\" ).length ) {\n\t\t\t\t\thooks.empty.fire();\n\t\t\t\t}\n\t\t\t} );\n\t\t} );\n\t}\n\n\t// Detect show/hide animations\n\tfor ( prop in props ) {\n\t\tvalue = props[ prop ];\n\t\tif ( rfxtypes.test( value ) ) {\n\t\t\tdelete props[ prop ];\n\t\t\ttoggle = toggle || value === \"toggle\";\n\t\t\tif ( value === ( hidden ? \"hide\" : \"show\" ) ) {\n\n\t\t\t\t// Pretend to be hidden if this is a \"show\" and\n\t\t\t\t// there is still data from a stopped show/hide\n\t\t\t\tif ( value === \"show\" && dataShow && dataShow[ prop ] !== undefined ) {\n\t\t\t\t\thidden = true;\n\n\t\t\t\t// Ignore all other no-op show/hide data\n\t\t\t\t} else {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\torig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop );\n\t\t}\n\t}\n\n\t// Bail out if this is a no-op like .hide().hide()\n\tpropTween = !jQuery.isEmptyObject( props );\n\tif ( !propTween && jQuery.isEmptyObject( orig ) ) {\n\t\treturn;\n\t}\n\n\t// Restrict \"overflow\" and \"display\" styles during box animations\n\tif ( isBox && elem.nodeType === 1 ) {\n\n\t\t// Support: IE <=9 - 11, Edge 12 - 15\n\t\t// Record all 3 overflow attributes because IE does not infer the shorthand\n\t\t// from identically-valued overflowX and overflowY and Edge just mirrors\n\t\t// the overflowX value there.\n\t\topts.overflow = [ style.overflow, style.overflowX, style.overflowY ];\n\n\t\t// Identify a display type, preferring old show/hide data over the CSS cascade\n\t\trestoreDisplay = dataShow && dataShow.display;\n\t\tif ( restoreDisplay == null ) {\n\t\t\trestoreDisplay = dataPriv.get( elem, \"display\" );\n\t\t}\n\t\tdisplay = jQuery.css( elem, \"display\" );\n\t\tif ( display === \"none\" ) {\n\t\t\tif ( restoreDisplay ) {\n\t\t\t\tdisplay = restoreDisplay;\n\t\t\t} else {\n\n\t\t\t\t// Get nonempty value(s) by temporarily forcing visibility\n\t\t\t\tshowHide( [ elem ], true );\n\t\t\t\trestoreDisplay = elem.style.display || restoreDisplay;\n\t\t\t\tdisplay = jQuery.css( elem, \"display\" );\n\t\t\t\tshowHide( [ elem ] );\n\t\t\t}\n\t\t}\n\n\t\t// Animate inline elements as inline-block\n\t\tif ( display === \"inline\" || display === \"inline-block\" && restoreDisplay != null ) {\n\t\t\tif ( jQuery.css( elem, \"float\" ) === \"none\" ) {\n\n\t\t\t\t// Restore the original display value at the end of pure show/hide animations\n\t\t\t\tif ( !propTween ) {\n\t\t\t\t\tanim.done( function() {\n\t\t\t\t\t\tstyle.display = restoreDisplay;\n\t\t\t\t\t} );\n\t\t\t\t\tif ( restoreDisplay == null ) {\n\t\t\t\t\t\tdisplay = style.display;\n\t\t\t\t\t\trestoreDisplay = display === \"none\" ? \"\" : display;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstyle.display = \"inline-block\";\n\t\t\t}\n\t\t}\n\t}\n\n\tif ( opts.overflow ) {\n\t\tstyle.overflow = \"hidden\";\n\t\tanim.always( function() {\n\t\t\tstyle.overflow = opts.overflow[ 0 ];\n\t\t\tstyle.overflowX = opts.overflow[ 1 ];\n\t\t\tstyle.overflowY = opts.overflow[ 2 ];\n\t\t} );\n\t}\n\n\t// Implement show/hide animations\n\tpropTween = false;\n\tfor ( prop in orig ) {\n\n\t\t// General show/hide setup for this element animation\n\t\tif ( !propTween ) {\n\t\t\tif ( dataShow ) {\n\t\t\t\tif ( \"hidden\" in dataShow ) {\n\t\t\t\t\thidden = dataShow.hidden;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdataShow = dataPriv.access( elem, \"fxshow\", { display: restoreDisplay } );\n\t\t\t}\n\n\t\t\t// Store hidden/visible for toggle so `.stop().toggle()` \"reverses\"\n\t\t\tif ( toggle ) {\n\t\t\t\tdataShow.hidden = !hidden;\n\t\t\t}\n\n\t\t\t// Show elements before animating them\n\t\t\tif ( hidden ) {\n\t\t\t\tshowHide( [ elem ], true );\n\t\t\t}\n\n\t\t\t/* eslint-disable no-loop-func */\n\n\t\t\tanim.done( function() {\n\n\t\t\t/* eslint-enable no-loop-func */\n\n\t\t\t\t// The final step of a \"hide\" animation is actually hiding the element\n\t\t\t\tif ( !hidden ) {\n\t\t\t\t\tshowHide( [ elem ] );\n\t\t\t\t}\n\t\t\t\tdataPriv.remove( elem, \"fxshow\" );\n\t\t\t\tfor ( prop in orig ) {\n\t\t\t\t\tjQuery.style( elem, prop, orig[ prop ] );\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\n\t\t// Per-property setup\n\t\tpropTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim );\n\t\tif ( !( prop in dataShow ) ) {\n\t\t\tdataShow[ prop ] = propTween.start;\n\t\t\tif ( hidden ) {\n\t\t\t\tpropTween.end = propTween.start;\n\t\t\t\tpropTween.start = 0;\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction propFilter( props, specialEasing ) {\n\tvar index, name, easing, value, hooks;\n\n\t// camelCase, specialEasing and expand cssHook pass\n\tfor ( index in props ) {\n\t\tname = camelCase( index );\n\t\teasing = specialEasing[ name ];\n\t\tvalue = props[ index ];\n\t\tif ( Array.isArray( value ) ) {\n\t\t\teasing = value[ 1 ];\n\t\t\tvalue = props[ index ] = value[ 0 ];\n\t\t}\n\n\t\tif ( index !== name ) {\n\t\t\tprops[ name ] = value;\n\t\t\tdelete props[ index ];\n\t\t}\n\n\t\thooks = jQuery.cssHooks[ name ];\n\t\tif ( hooks && \"expand\" in hooks ) {\n\t\t\tvalue = hooks.expand( value );\n\t\t\tdelete props[ name ];\n\n\t\t\t// Not quite $.extend, this won't overwrite existing keys.\n\t\t\t// Reusing 'index' because we have the correct \"name\"\n\t\t\tfor ( index in value ) {\n\t\t\t\tif ( !( index in props ) ) {\n\t\t\t\t\tprops[ index ] = value[ index ];\n\t\t\t\t\tspecialEasing[ index ] = easing;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tspecialEasing[ name ] = easing;\n\t\t}\n\t}\n}\n\nfunction Animation( elem, properties, options ) {\n\tvar result,\n\t\tstopped,\n\t\tindex = 0,\n\t\tlength = Animation.prefilters.length,\n\t\tdeferred = jQuery.Deferred().always( function() {\n\n\t\t\t// Don't match elem in the :animated selector\n\t\t\tdelete tick.elem;\n\t\t} ),\n\t\ttick = function() {\n\t\t\tif ( stopped ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tvar currentTime = fxNow || createFxNow(),\n\t\t\t\tremaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),\n\n\t\t\t\t// Support: Android 2.3 only\n\t\t\t\t// Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497)\n\t\t\t\ttemp = remaining / animation.duration || 0,\n\t\t\t\tpercent = 1 - temp,\n\t\t\t\tindex = 0,\n\t\t\t\tlength = animation.tweens.length;\n\n\t\t\tfor ( ; index < length; index++ ) {\n\t\t\t\tanimation.tweens[ index ].run( percent );\n\t\t\t}\n\n\t\t\tdeferred.notifyWith( elem, [ animation, percent, remaining ] );\n\n\t\t\t// If there's more to do, yield\n\t\t\tif ( percent < 1 && length ) {\n\t\t\t\treturn remaining;\n\t\t\t}\n\n\t\t\t// If this was an empty animation, synthesize a final progress notification\n\t\t\tif ( !length ) {\n\t\t\t\tdeferred.notifyWith( elem, [ animation, 1, 0 ] );\n\t\t\t}\n\n\t\t\t// Resolve the animation and report its conclusion\n\t\t\tdeferred.resolveWith( elem, [ animation ] );\n\t\t\treturn false;\n\t\t},\n\t\tanimation = deferred.promise( {\n\t\t\telem: elem,\n\t\t\tprops: jQuery.extend( {}, properties ),\n\t\t\topts: jQuery.extend( true, {\n\t\t\t\tspecialEasing: {},\n\t\t\t\teasing: jQuery.easing._default\n\t\t\t}, options ),\n\t\t\toriginalProperties: properties,\n\t\t\toriginalOptions: options,\n\t\t\tstartTime: fxNow || createFxNow(),\n\t\t\tduration: options.duration,\n\t\t\ttweens: [],\n\t\t\tcreateTween: function( prop, end ) {\n\t\t\t\tvar tween = jQuery.Tween( elem, animation.opts, prop, end,\n\t\t\t\t\t\tanimation.opts.specialEasing[ prop ] || animation.opts.easing );\n\t\t\t\tanimation.tweens.push( tween );\n\t\t\t\treturn tween;\n\t\t\t},\n\t\t\tstop: function( gotoEnd ) {\n\t\t\t\tvar index = 0,\n\n\t\t\t\t\t// If we are going to the end, we want to run all the tweens\n\t\t\t\t\t// otherwise we skip this part\n\t\t\t\t\tlength = gotoEnd ? animation.tweens.length : 0;\n\t\t\t\tif ( stopped ) {\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\t\t\t\tstopped = true;\n\t\t\t\tfor ( ; index < length; index++ ) {\n\t\t\t\t\tanimation.tweens[ index ].run( 1 );\n\t\t\t\t}\n\n\t\t\t\t// Resolve when we played the last frame; otherwise, reject\n\t\t\t\tif ( gotoEnd ) {\n\t\t\t\t\tdeferred.notifyWith( elem, [ animation, 1, 0 ] );\n\t\t\t\t\tdeferred.resolveWith( elem, [ animation, gotoEnd ] );\n\t\t\t\t} else {\n\t\t\t\t\tdeferred.rejectWith( elem, [ animation, gotoEnd ] );\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t}\n\t\t} ),\n\t\tprops = animation.props;\n\n\tpropFilter( props, animation.opts.specialEasing );\n\n\tfor ( ; index < length; index++ ) {\n\t\tresult = Animation.prefilters[ index ].call( animation, elem, props, animation.opts );\n\t\tif ( result ) {\n\t\t\tif ( isFunction( result.stop ) ) {\n\t\t\t\tjQuery._queueHooks( animation.elem, animation.opts.queue ).stop =\n\t\t\t\t\tresult.stop.bind( result );\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\t}\n\n\tjQuery.map( props, createTween, animation );\n\n\tif ( isFunction( animation.opts.start ) ) {\n\t\tanimation.opts.start.call( elem, animation );\n\t}\n\n\t// Attach callbacks from options\n\tanimation\n\t\t.progress( animation.opts.progress )\n\t\t.done( animation.opts.done, animation.opts.complete )\n\t\t.fail( animation.opts.fail )\n\t\t.always( animation.opts.always );\n\n\tjQuery.fx.timer(\n\t\tjQuery.extend( tick, {\n\t\t\telem: elem,\n\t\t\tanim: animation,\n\t\t\tqueue: animation.opts.queue\n\t\t} )\n\t);\n\n\treturn animation;\n}\n\njQuery.Animation = jQuery.extend( Animation, {\n\n\ttweeners: {\n\t\t\"*\": [ function( prop, value ) {\n\t\t\tvar tween = this.createTween( prop, value );\n\t\t\tadjustCSS( tween.elem, prop, rcssNum.exec( value ), tween );\n\t\t\treturn tween;\n\t\t} ]\n\t},\n\n\ttweener: function( props, callback ) {\n\t\tif ( isFunction( props ) ) {\n\t\t\tcallback = props;\n\t\t\tprops = [ \"*\" ];\n\t\t} else {\n\t\t\tprops = props.match( rnothtmlwhite );\n\t\t}\n\n\t\tvar prop,\n\t\t\tindex = 0,\n\t\t\tlength = props.length;\n\n\t\tfor ( ; index < length; index++ ) {\n\t\t\tprop = props[ index ];\n\t\t\tAnimation.tweeners[ prop ] = Animation.tweeners[ prop ] || [];\n\t\t\tAnimation.tweeners[ prop ].unshift( callback );\n\t\t}\n\t},\n\n\tprefilters: [ defaultPrefilter ],\n\n\tprefilter: function( callback, prepend ) {\n\t\tif ( prepend ) {\n\t\t\tAnimation.prefilters.unshift( callback );\n\t\t} else {\n\t\t\tAnimation.prefilters.push( callback );\n\t\t}\n\t}\n} );\n\njQuery.speed = function( speed, easing, fn ) {\n\tvar opt = speed && typeof speed === \"object\" ? jQuery.extend( {}, speed ) : {\n\t\tcomplete: fn || !fn && easing ||\n\t\t\tisFunction( speed ) && speed,\n\t\tduration: speed,\n\t\teasing: fn && easing || easing && !isFunction( easing ) && easing\n\t};\n\n\t// Go to the end state if fx are off\n\tif ( jQuery.fx.off ) {\n\t\topt.duration = 0;\n\n\t} else {\n\t\tif ( typeof opt.duration !== \"number\" ) {\n\t\t\tif ( opt.duration in jQuery.fx.speeds ) {\n\t\t\t\topt.duration = jQuery.fx.speeds[ opt.duration ];\n\n\t\t\t} else {\n\t\t\t\topt.duration = jQuery.fx.speeds._default;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Normalize opt.queue - true/undefined/null -> \"fx\"\n\tif ( opt.queue == null || opt.queue === true ) {\n\t\topt.queue = \"fx\";\n\t}\n\n\t// Queueing\n\topt.old = opt.complete;\n\n\topt.complete = function() {\n\t\tif ( isFunction( opt.old ) ) {\n\t\t\topt.old.call( this );\n\t\t}\n\n\t\tif ( opt.queue ) {\n\t\t\tjQuery.dequeue( this, opt.queue );\n\t\t}\n\t};\n\n\treturn opt;\n};\n\njQuery.fn.extend( {\n\tfadeTo: function( speed, to, easing, callback ) {\n\n\t\t// Show any hidden elements after setting opacity to 0\n\t\treturn this.filter( isHiddenWithinTree ).css( \"opacity\", 0 ).show()\n\n\t\t\t// Animate to the value specified\n\t\t\t.end().animate( { opacity: to }, speed, easing, callback );\n\t},\n\tanimate: function( prop, speed, easing, callback ) {\n\t\tvar empty = jQuery.isEmptyObject( prop ),\n\t\t\toptall = jQuery.speed( speed, easing, callback ),\n\t\t\tdoAnimation = function() {\n\n\t\t\t\t// Operate on a copy of prop so per-property easing won't be lost\n\t\t\t\tvar anim = Animation( this, jQuery.extend( {}, prop ), optall );\n\n\t\t\t\t// Empty animations, or finishing resolves immediately\n\t\t\t\tif ( empty || dataPriv.get( this, \"finish\" ) ) {\n\t\t\t\t\tanim.stop( true );\n\t\t\t\t}\n\t\t\t};\n\t\t\tdoAnimation.finish = doAnimation;\n\n\t\treturn empty || optall.queue === false ?\n\t\t\tthis.each( doAnimation ) :\n\t\t\tthis.queue( optall.queue, doAnimation );\n\t},\n\tstop: function( type, clearQueue, gotoEnd ) {\n\t\tvar stopQueue = function( hooks ) {\n\t\t\tvar stop = hooks.stop;\n\t\t\tdelete hooks.stop;\n\t\t\tstop( gotoEnd );\n\t\t};\n\n\t\tif ( typeof type !== \"string\" ) {\n\t\t\tgotoEnd = clearQueue;\n\t\t\tclearQueue = type;\n\t\t\ttype = undefined;\n\t\t}\n\t\tif ( clearQueue && type !== false ) {\n\t\t\tthis.queue( type || \"fx\", [] );\n\t\t}\n\n\t\treturn this.each( function() {\n\t\t\tvar dequeue = true,\n\t\t\t\tindex = type != null && type + \"queueHooks\",\n\t\t\t\ttimers = jQuery.timers,\n\t\t\t\tdata = dataPriv.get( this );\n\n\t\t\tif ( index ) {\n\t\t\t\tif ( data[ index ] && data[ index ].stop ) {\n\t\t\t\t\tstopQueue( data[ index ] );\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor ( index in data ) {\n\t\t\t\t\tif ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {\n\t\t\t\t\t\tstopQueue( data[ index ] );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor ( index = timers.length; index--; ) {\n\t\t\t\tif ( timers[ index ].elem === this &&\n\t\t\t\t\t( type == null || timers[ index ].queue === type ) ) {\n\n\t\t\t\t\ttimers[ index ].anim.stop( gotoEnd );\n\t\t\t\t\tdequeue = false;\n\t\t\t\t\ttimers.splice( index, 1 );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Start the next in the queue if the last step wasn't forced.\n\t\t\t// Timers currently will call their complete callbacks, which\n\t\t\t// will dequeue but only if they were gotoEnd.\n\t\t\tif ( dequeue || !gotoEnd ) {\n\t\t\t\tjQuery.dequeue( this, type );\n\t\t\t}\n\t\t} );\n\t},\n\tfinish: function( type ) {\n\t\tif ( type !== false ) {\n\t\t\ttype = type || \"fx\";\n\t\t}\n\t\treturn this.each( function() {\n\t\t\tvar index,\n\t\t\t\tdata = dataPriv.get( this ),\n\t\t\t\tqueue = data[ type + \"queue\" ],\n\t\t\t\thooks = data[ type + \"queueHooks\" ],\n\t\t\t\ttimers = jQuery.timers,\n\t\t\t\tlength = queue ? queue.length : 0;\n\n\t\t\t// Enable finishing flag on private data\n\t\t\tdata.finish = true;\n\n\t\t\t// Empty the queue first\n\t\t\tjQuery.queue( this, type, [] );\n\n\t\t\tif ( hooks && hooks.stop ) {\n\t\t\t\thooks.stop.call( this, true );\n\t\t\t}\n\n\t\t\t// Look for any active animations, and finish them\n\t\t\tfor ( index = timers.length; index--; ) {\n\t\t\t\tif ( timers[ index ].elem === this && timers[ index ].queue === type ) {\n\t\t\t\t\ttimers[ index ].anim.stop( true );\n\t\t\t\t\ttimers.splice( index, 1 );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Look for any animations in the old queue and finish them\n\t\t\tfor ( index = 0; index < length; index++ ) {\n\t\t\t\tif ( queue[ index ] && queue[ index ].finish ) {\n\t\t\t\t\tqueue[ index ].finish.call( this );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Turn off finishing flag\n\t\t\tdelete data.finish;\n\t\t} );\n\t}\n} );\n\njQuery.each( [ \"toggle\", \"show\", \"hide\" ], function( i, name ) {\n\tvar cssFn = jQuery.fn[ name ];\n\tjQuery.fn[ name ] = function( speed, easing, callback ) {\n\t\treturn speed == null || typeof speed === \"boolean\" ?\n\t\t\tcssFn.apply( this, arguments ) :\n\t\t\tthis.animate( genFx( name, true ), speed, easing, callback );\n\t};\n} );\n\n// Generate shortcuts for custom animations\njQuery.each( {\n\tslideDown: genFx( \"show\" ),\n\tslideUp: genFx( \"hide\" ),\n\tslideToggle: genFx( \"toggle\" ),\n\tfadeIn: { opacity: \"show\" },\n\tfadeOut: { opacity: \"hide\" },\n\tfadeToggle: { opacity: \"toggle\" }\n}, function( name, props ) {\n\tjQuery.fn[ name ] = function( speed, easing, callback ) {\n\t\treturn this.animate( props, speed, easing, callback );\n\t};\n} );\n\njQuery.timers = [];\njQuery.fx.tick = function() {\n\tvar timer,\n\t\ti = 0,\n\t\ttimers = jQuery.timers;\n\n\tfxNow = Date.now();\n\n\tfor ( ; i < timers.length; i++ ) {\n\t\ttimer = timers[ i ];\n\n\t\t// Run the timer and safely remove it when done (allowing for external removal)\n\t\tif ( !timer() && timers[ i ] === timer ) {\n\t\t\ttimers.splice( i--, 1 );\n\t\t}\n\t}\n\n\tif ( !timers.length ) {\n\t\tjQuery.fx.stop();\n\t}\n\tfxNow = undefined;\n};\n\njQuery.fx.timer = function( timer ) {\n\tjQuery.timers.push( timer );\n\tjQuery.fx.start();\n};\n\njQuery.fx.interval = 13;\njQuery.fx.start = function() {\n\tif ( inProgress ) {\n\t\treturn;\n\t}\n\n\tinProgress = true;\n\tschedule();\n};\n\njQuery.fx.stop = function() {\n\tinProgress = null;\n};\n\njQuery.fx.speeds = {\n\tslow: 600,\n\tfast: 200,\n\n\t// Default speed\n\t_default: 400\n};\n\n\n// Based off of the plugin by Clint Helfers, with permission.\n// https://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/\njQuery.fn.delay = function( time, type ) {\n\ttime = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;\n\ttype = type || \"fx\";\n\n\treturn this.queue( type, function( next, hooks ) {\n\t\tvar timeout = window.setTimeout( next, time );\n\t\thooks.stop = function() {\n\t\t\twindow.clearTimeout( timeout );\n\t\t};\n\t} );\n};\n\n\n( function() {\n\tvar input = document.createElement( \"input\" ),\n\t\tselect = document.createElement( \"select\" ),\n\t\topt = select.appendChild( document.createElement( \"option\" ) );\n\n\tinput.type = \"checkbox\";\n\n\t// Support: Android <=4.3 only\n\t// Default value for a checkbox should be \"on\"\n\tsupport.checkOn = input.value !== \"\";\n\n\t// Support: IE <=11 only\n\t// Must access selectedIndex to make default options select\n\tsupport.optSelected = opt.selected;\n\n\t// Support: IE <=11 only\n\t// An input loses its value after becoming a radio\n\tinput = document.createElement( \"input\" );\n\tinput.value = \"t\";\n\tinput.type = \"radio\";\n\tsupport.radioValue = input.value === \"t\";\n} )();\n\n\nvar boolHook,\n\tattrHandle = jQuery.expr.attrHandle;\n\njQuery.fn.extend( {\n\tattr: function( name, value ) {\n\t\treturn access( this, jQuery.attr, name, value, arguments.length > 1 );\n\t},\n\n\tremoveAttr: function( name ) {\n\t\treturn this.each( function() {\n\t\t\tjQuery.removeAttr( this, name );\n\t\t} );\n\t}\n} );\n\njQuery.extend( {\n\tattr: function( elem, name, value ) {\n\t\tvar ret, hooks,\n\t\t\tnType = elem.nodeType;\n\n\t\t// Don't get/set attributes on text, comment and attribute nodes\n\t\tif ( nType === 3 || nType === 8 || nType === 2 ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Fallback to prop when attributes are not supported\n\t\tif ( typeof elem.getAttribute === \"undefined\" ) {\n\t\t\treturn jQuery.prop( elem, name, value );\n\t\t}\n\n\t\t// Attribute hooks are determined by the lowercase version\n\t\t// Grab necessary hook if one is defined\n\t\tif ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {\n\t\t\thooks = jQuery.attrHooks[ name.toLowerCase() ] ||\n\t\t\t\t( jQuery.expr.match.bool.test( name ) ? boolHook : undefined );\n\t\t}\n\n\t\tif ( value !== undefined ) {\n\t\t\tif ( value === null ) {\n\t\t\t\tjQuery.removeAttr( elem, name );\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( hooks && \"set\" in hooks &&\n\t\t\t\t( ret = hooks.set( elem, value, name ) ) !== undefined ) {\n\t\t\t\treturn ret;\n\t\t\t}\n\n\t\t\telem.setAttribute( name, value + \"\" );\n\t\t\treturn value;\n\t\t}\n\n\t\tif ( hooks && \"get\" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {\n\t\t\treturn ret;\n\t\t}\n\n\t\tret = jQuery.find.attr( elem, name );\n\n\t\t// Non-existent attributes return null, we normalize to undefined\n\t\treturn ret == null ? undefined : ret;\n\t},\n\n\tattrHooks: {\n\t\ttype: {\n\t\t\tset: function( elem, value ) {\n\t\t\t\tif ( !support.radioValue && value === \"radio\" &&\n\t\t\t\t\tnodeName( elem, \"input\" ) ) {\n\t\t\t\t\tvar val = elem.value;\n\t\t\t\t\telem.setAttribute( \"type\", value );\n\t\t\t\t\tif ( val ) {\n\t\t\t\t\t\telem.value = val;\n\t\t\t\t\t}\n\t\t\t\t\treturn value;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\n\tremoveAttr: function( elem, value ) {\n\t\tvar name,\n\t\t\ti = 0,\n\n\t\t\t// Attribute names can contain non-HTML whitespace characters\n\t\t\t// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2\n\t\t\tattrNames = value && value.match( rnothtmlwhite );\n\n\t\tif ( attrNames && elem.nodeType === 1 ) {\n\t\t\twhile ( ( name = attrNames[ i++ ] ) ) {\n\t\t\t\telem.removeAttribute( name );\n\t\t\t}\n\t\t}\n\t}\n} );\n\n// Hooks for boolean attributes\nboolHook = {\n\tset: function( elem, value, name ) {\n\t\tif ( value === false ) {\n\n\t\t\t// Remove boolean attributes when set to false\n\t\t\tjQuery.removeAttr( elem, name );\n\t\t} else {\n\t\t\telem.setAttribute( name, name );\n\t\t}\n\t\treturn name;\n\t}\n};\n\njQuery.each( jQuery.expr.match.bool.source.match( /\\w+/g ), function( i, name ) {\n\tvar getter = attrHandle[ name ] || jQuery.find.attr;\n\n\tattrHandle[ name ] = function( elem, name, isXML ) {\n\t\tvar ret, handle,\n\t\t\tlowercaseName = name.toLowerCase();\n\n\t\tif ( !isXML ) {\n\n\t\t\t// Avoid an infinite loop by temporarily removing this function from the getter\n\t\t\thandle = attrHandle[ lowercaseName ];\n\t\t\tattrHandle[ lowercaseName ] = ret;\n\t\t\tret = getter( elem, name, isXML ) != null ?\n\t\t\t\tlowercaseName :\n\t\t\t\tnull;\n\t\t\tattrHandle[ lowercaseName ] = handle;\n\t\t}\n\t\treturn ret;\n\t};\n} );\n\n\n\n\nvar rfocusable = /^(?:input|select|textarea|button)$/i,\n\trclickable = /^(?:a|area)$/i;\n\njQuery.fn.extend( {\n\tprop: function( name, value ) {\n\t\treturn access( this, jQuery.prop, name, value, arguments.length > 1 );\n\t},\n\n\tremoveProp: function( name ) {\n\t\treturn this.each( function() {\n\t\t\tdelete this[ jQuery.propFix[ name ] || name ];\n\t\t} );\n\t}\n} );\n\njQuery.extend( {\n\tprop: function( elem, name, value ) {\n\t\tvar ret, hooks,\n\t\t\tnType = elem.nodeType;\n\n\t\t// Don't get/set properties on text, comment and attribute nodes\n\t\tif ( nType === 3 || nType === 8 || nType === 2 ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {\n\n\t\t\t// Fix name and attach hooks\n\t\t\tname = jQuery.propFix[ name ] || name;\n\t\t\thooks = jQuery.propHooks[ name ];\n\t\t}\n\n\t\tif ( value !== undefined ) {\n\t\t\tif ( hooks && \"set\" in hooks &&\n\t\t\t\t( ret = hooks.set( elem, value, name ) ) !== undefined ) {\n\t\t\t\treturn ret;\n\t\t\t}\n\n\t\t\treturn ( elem[ name ] = value );\n\t\t}\n\n\t\tif ( hooks && \"get\" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {\n\t\t\treturn ret;\n\t\t}\n\n\t\treturn elem[ name ];\n\t},\n\n\tpropHooks: {\n\t\ttabIndex: {\n\t\t\tget: function( elem ) {\n\n\t\t\t\t// Support: IE <=9 - 11 only\n\t\t\t\t// elem.tabIndex doesn't always return the\n\t\t\t\t// correct value when it hasn't been explicitly set\n\t\t\t\t// https://web.archive.org/web/20141116233347/http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/\n\t\t\t\t// Use proper attribute retrieval(#12072)\n\t\t\t\tvar tabindex = jQuery.find.attr( elem, \"tabindex\" );\n\n\t\t\t\tif ( tabindex ) {\n\t\t\t\t\treturn parseInt( tabindex, 10 );\n\t\t\t\t}\n\n\t\t\t\tif (\n\t\t\t\t\trfocusable.test( elem.nodeName ) ||\n\t\t\t\t\trclickable.test( elem.nodeName ) &&\n\t\t\t\t\telem.href\n\t\t\t\t) {\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\t},\n\n\tpropFix: {\n\t\t\"for\": \"htmlFor\",\n\t\t\"class\": \"className\"\n\t}\n} );\n\n// Support: IE <=11 only\n// Accessing the selectedIndex property\n// forces the browser to respect setting selected\n// on the option\n// The getter ensures a default option is selected\n// when in an optgroup\n// eslint rule \"no-unused-expressions\" is disabled for this code\n// since it considers such accessions noop\nif ( !support.optSelected ) {\n\tjQuery.propHooks.selected = {\n\t\tget: function( elem ) {\n\n\t\t\t/* eslint no-unused-expressions: \"off\" */\n\n\t\t\tvar parent = elem.parentNode;\n\t\t\tif ( parent && parent.parentNode ) {\n\t\t\t\tparent.parentNode.selectedIndex;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t\tset: function( elem ) {\n\n\t\t\t/* eslint no-unused-expressions: \"off\" */\n\n\t\t\tvar parent = elem.parentNode;\n\t\t\tif ( parent ) {\n\t\t\t\tparent.selectedIndex;\n\n\t\t\t\tif ( parent.parentNode ) {\n\t\t\t\t\tparent.parentNode.selectedIndex;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n}\n\njQuery.each( [\n\t\"tabIndex\",\n\t\"readOnly\",\n\t\"maxLength\",\n\t\"cellSpacing\",\n\t\"cellPadding\",\n\t\"rowSpan\",\n\t\"colSpan\",\n\t\"useMap\",\n\t\"frameBorder\",\n\t\"contentEditable\"\n], function() {\n\tjQuery.propFix[ this.toLowerCase() ] = this;\n} );\n\n\n\n\n\t// Strip and collapse whitespace according to HTML spec\n\t// https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace\n\tfunction stripAndCollapse( value ) {\n\t\tvar tokens = value.match( rnothtmlwhite ) || [];\n\t\treturn tokens.join( \" \" );\n\t}\n\n\nfunction getClass( elem ) {\n\treturn elem.getAttribute && elem.getAttribute( \"class\" ) || \"\";\n}\n\nfunction classesToArray( value ) {\n\tif ( Array.isArray( value ) ) {\n\t\treturn value;\n\t}\n\tif ( typeof value === \"string\" ) {\n\t\treturn value.match( rnothtmlwhite ) || [];\n\t}\n\treturn [];\n}\n\njQuery.fn.extend( {\n\taddClass: function( value ) {\n\t\tvar classes, elem, cur, curValue, clazz, j, finalValue,\n\t\t\ti = 0;\n\n\t\tif ( isFunction( value ) ) {\n\t\t\treturn this.each( function( j ) {\n\t\t\t\tjQuery( this ).addClass( value.call( this, j, getClass( this ) ) );\n\t\t\t} );\n\t\t}\n\n\t\tclasses = classesToArray( value );\n\n\t\tif ( classes.length ) {\n\t\t\twhile ( ( elem = this[ i++ ] ) ) {\n\t\t\t\tcurValue = getClass( elem );\n\t\t\t\tcur = elem.nodeType === 1 && ( \" \" + stripAndCollapse( curValue ) + \" \" );\n\n\t\t\t\tif ( cur ) {\n\t\t\t\t\tj = 0;\n\t\t\t\t\twhile ( ( clazz = classes[ j++ ] ) ) {\n\t\t\t\t\t\tif ( cur.indexOf( \" \" + clazz + \" \" ) < 0 ) {\n\t\t\t\t\t\t\tcur += clazz + \" \";\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Only assign if different to avoid unneeded rendering.\n\t\t\t\t\tfinalValue = stripAndCollapse( cur );\n\t\t\t\t\tif ( curValue !== finalValue ) {\n\t\t\t\t\t\telem.setAttribute( \"class\", finalValue );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\tremoveClass: function( value ) {\n\t\tvar classes, elem, cur, curValue, clazz, j, finalValue,\n\t\t\ti = 0;\n\n\t\tif ( isFunction( value ) ) {\n\t\t\treturn this.each( function( j ) {\n\t\t\t\tjQuery( this ).removeClass( value.call( this, j, getClass( this ) ) );\n\t\t\t} );\n\t\t}\n\n\t\tif ( !arguments.length ) {\n\t\t\treturn this.attr( \"class\", \"\" );\n\t\t}\n\n\t\tclasses = classesToArray( value );\n\n\t\tif ( classes.length ) {\n\t\t\twhile ( ( elem = this[ i++ ] ) ) {\n\t\t\t\tcurValue = getClass( elem );\n\n\t\t\t\t// This expression is here for better compressibility (see addClass)\n\t\t\t\tcur = elem.nodeType === 1 && ( \" \" + stripAndCollapse( curValue ) + \" \" );\n\n\t\t\t\tif ( cur ) {\n\t\t\t\t\tj = 0;\n\t\t\t\t\twhile ( ( clazz = classes[ j++ ] ) ) {\n\n\t\t\t\t\t\t// Remove *all* instances\n\t\t\t\t\t\twhile ( cur.indexOf( \" \" + clazz + \" \" ) > -1 ) {\n\t\t\t\t\t\t\tcur = cur.replace( \" \" + clazz + \" \", \" \" );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Only assign if different to avoid unneeded rendering.\n\t\t\t\t\tfinalValue = stripAndCollapse( cur );\n\t\t\t\t\tif ( curValue !== finalValue ) {\n\t\t\t\t\t\telem.setAttribute( \"class\", finalValue );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\ttoggleClass: function( value, stateVal ) {\n\t\tvar type = typeof value,\n\t\t\tisValidValue = type === \"string\" || Array.isArray( value );\n\n\t\tif ( typeof stateVal === \"boolean\" && isValidValue ) {\n\t\t\treturn stateVal ? this.addClass( value ) : this.removeClass( value );\n\t\t}\n\n\t\tif ( isFunction( value ) ) {\n\t\t\treturn this.each( function( i ) {\n\t\t\t\tjQuery( this ).toggleClass(\n\t\t\t\t\tvalue.call( this, i, getClass( this ), stateVal ),\n\t\t\t\t\tstateVal\n\t\t\t\t);\n\t\t\t} );\n\t\t}\n\n\t\treturn this.each( function() {\n\t\t\tvar className, i, self, classNames;\n\n\t\t\tif ( isValidValue ) {\n\n\t\t\t\t// Toggle individual class names\n\t\t\t\ti = 0;\n\t\t\t\tself = jQuery( this );\n\t\t\t\tclassNames = classesToArray( value );\n\n\t\t\t\twhile ( ( className = classNames[ i++ ] ) ) {\n\n\t\t\t\t\t// Check each className given, space separated list\n\t\t\t\t\tif ( self.hasClass( className ) ) {\n\t\t\t\t\t\tself.removeClass( className );\n\t\t\t\t\t} else {\n\t\t\t\t\t\tself.addClass( className );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t// Toggle whole class name\n\t\t\t} else if ( value === undefined || type === \"boolean\" ) {\n\t\t\t\tclassName = getClass( this );\n\t\t\t\tif ( className ) {\n\n\t\t\t\t\t// Store className if set\n\t\t\t\t\tdataPriv.set( this, \"__className__\", className );\n\t\t\t\t}\n\n\t\t\t\t// If the element has a class name or if we're passed `false`,\n\t\t\t\t// then remove the whole classname (if there was one, the above saved it).\n\t\t\t\t// Otherwise bring back whatever was previously saved (if anything),\n\t\t\t\t// falling back to the empty string if nothing was stored.\n\t\t\t\tif ( this.setAttribute ) {\n\t\t\t\t\tthis.setAttribute( \"class\",\n\t\t\t\t\t\tclassName || value === false ?\n\t\t\t\t\t\t\"\" :\n\t\t\t\t\t\tdataPriv.get( this, \"__className__\" ) || \"\"\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t} );\n\t},\n\n\thasClass: function( selector ) {\n\t\tvar className, elem,\n\t\t\ti = 0;\n\n\t\tclassName = \" \" + selector + \" \";\n\t\twhile ( ( elem = this[ i++ ] ) ) {\n\t\t\tif ( elem.nodeType === 1 &&\n\t\t\t\t( \" \" + stripAndCollapse( getClass( elem ) ) + \" \" ).indexOf( className ) > -1 ) {\n\t\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n} );\n\n\n\n\nvar rreturn = /\\r/g;\n\njQuery.fn.extend( {\n\tval: function( value ) {\n\t\tvar hooks, ret, valueIsFunction,\n\t\t\telem = this[ 0 ];\n\n\t\tif ( !arguments.length ) {\n\t\t\tif ( elem ) {\n\t\t\t\thooks = jQuery.valHooks[ elem.type ] ||\n\t\t\t\t\tjQuery.valHooks[ elem.nodeName.toLowerCase() ];\n\n\t\t\t\tif ( hooks &&\n\t\t\t\t\t\"get\" in hooks &&\n\t\t\t\t\t( ret = hooks.get( elem, \"value\" ) ) !== undefined\n\t\t\t\t) {\n\t\t\t\t\treturn ret;\n\t\t\t\t}\n\n\t\t\t\tret = elem.value;\n\n\t\t\t\t// Handle most common string cases\n\t\t\t\tif ( typeof ret === \"string\" ) {\n\t\t\t\t\treturn ret.replace( rreturn, \"\" );\n\t\t\t\t}\n\n\t\t\t\t// Handle cases where value is null/undef or number\n\t\t\t\treturn ret == null ? \"\" : ret;\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\tvalueIsFunction = isFunction( value );\n\n\t\treturn this.each( function( i ) {\n\t\t\tvar val;\n\n\t\t\tif ( this.nodeType !== 1 ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( valueIsFunction ) {\n\t\t\t\tval = value.call( this, i, jQuery( this ).val() );\n\t\t\t} else {\n\t\t\t\tval = value;\n\t\t\t}\n\n\t\t\t// Treat null/undefined as \"\"; convert numbers to string\n\t\t\tif ( val == null ) {\n\t\t\t\tval = \"\";\n\n\t\t\t} else if ( typeof val === \"number\" ) {\n\t\t\t\tval += \"\";\n\n\t\t\t} else if ( Array.isArray( val ) ) {\n\t\t\t\tval = jQuery.map( val, function( value ) {\n\t\t\t\t\treturn value == null ? \"\" : value + \"\";\n\t\t\t\t} );\n\t\t\t}\n\n\t\t\thooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];\n\n\t\t\t// If set returns undefined, fall back to normal setting\n\t\t\tif ( !hooks || !( \"set\" in hooks ) || hooks.set( this, val, \"value\" ) === undefined ) {\n\t\t\t\tthis.value = val;\n\t\t\t}\n\t\t} );\n\t}\n} );\n\njQuery.extend( {\n\tvalHooks: {\n\t\toption: {\n\t\t\tget: function( elem ) {\n\n\t\t\t\tvar val = jQuery.find.attr( elem, \"value\" );\n\t\t\t\treturn val != null ?\n\t\t\t\t\tval :\n\n\t\t\t\t\t// Support: IE <=10 - 11 only\n\t\t\t\t\t// option.text throws exceptions (#14686, #14858)\n\t\t\t\t\t// Strip and collapse whitespace\n\t\t\t\t\t// https://html.spec.whatwg.org/#strip-and-collapse-whitespace\n\t\t\t\t\tstripAndCollapse( jQuery.text( elem ) );\n\t\t\t}\n\t\t},\n\t\tselect: {\n\t\t\tget: function( elem ) {\n\t\t\t\tvar value, option, i,\n\t\t\t\t\toptions = elem.options,\n\t\t\t\t\tindex = elem.selectedIndex,\n\t\t\t\t\tone = elem.type === \"select-one\",\n\t\t\t\t\tvalues = one ? null : [],\n\t\t\t\t\tmax = one ? index + 1 : options.length;\n\n\t\t\t\tif ( index < 0 ) {\n\t\t\t\t\ti = max;\n\n\t\t\t\t} else {\n\t\t\t\t\ti = one ? index : 0;\n\t\t\t\t}\n\n\t\t\t\t// Loop through all the selected options\n\t\t\t\tfor ( ; i < max; i++ ) {\n\t\t\t\t\toption = options[ i ];\n\n\t\t\t\t\t// Support: IE <=9 only\n\t\t\t\t\t// IE8-9 doesn't update selected after form reset (#2551)\n\t\t\t\t\tif ( ( option.selected || i === index ) &&\n\n\t\t\t\t\t\t\t// Don't return options that are disabled or in a disabled optgroup\n\t\t\t\t\t\t\t!option.disabled &&\n\t\t\t\t\t\t\t( !option.parentNode.disabled ||\n\t\t\t\t\t\t\t\t!nodeName( option.parentNode, \"optgroup\" ) ) ) {\n\n\t\t\t\t\t\t// Get the specific value for the option\n\t\t\t\t\t\tvalue = jQuery( option ).val();\n\n\t\t\t\t\t\t// We don't need an array for one selects\n\t\t\t\t\t\tif ( one ) {\n\t\t\t\t\t\t\treturn value;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Multi-Selects return an array\n\t\t\t\t\t\tvalues.push( value );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn values;\n\t\t\t},\n\n\t\t\tset: function( elem, value ) {\n\t\t\t\tvar optionSet, option,\n\t\t\t\t\toptions = elem.options,\n\t\t\t\t\tvalues = jQuery.makeArray( value ),\n\t\t\t\t\ti = options.length;\n\n\t\t\t\twhile ( i-- ) {\n\t\t\t\t\toption = options[ i ];\n\n\t\t\t\t\t/* eslint-disable no-cond-assign */\n\n\t\t\t\t\tif ( option.selected =\n\t\t\t\t\t\tjQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1\n\t\t\t\t\t) {\n\t\t\t\t\t\toptionSet = true;\n\t\t\t\t\t}\n\n\t\t\t\t\t/* eslint-enable no-cond-assign */\n\t\t\t\t}\n\n\t\t\t\t// Force browsers to behave consistently when non-matching value is set\n\t\t\t\tif ( !optionSet ) {\n\t\t\t\t\telem.selectedIndex = -1;\n\t\t\t\t}\n\t\t\t\treturn values;\n\t\t\t}\n\t\t}\n\t}\n} );\n\n// Radios and checkboxes getter/setter\njQuery.each( [ \"radio\", \"checkbox\" ], function() {\n\tjQuery.valHooks[ this ] = {\n\t\tset: function( elem, value ) {\n\t\t\tif ( Array.isArray( value ) ) {\n\t\t\t\treturn ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 );\n\t\t\t}\n\t\t}\n\t};\n\tif ( !support.checkOn ) {\n\t\tjQuery.valHooks[ this ].get = function( elem ) {\n\t\t\treturn elem.getAttribute( \"value\" ) === null ? \"on\" : elem.value;\n\t\t};\n\t}\n} );\n\n\n\n\n// Return jQuery for attributes-only inclusion\n\n\nsupport.focusin = \"onfocusin\" in window;\n\n\nvar rfocusMorph = /^(?:focusinfocus|focusoutblur)$/,\n\tstopPropagationCallback = function( e ) {\n\t\te.stopPropagation();\n\t};\n\njQuery.extend( jQuery.event, {\n\n\ttrigger: function( event, data, elem, onlyHandlers ) {\n\n\t\tvar i, cur, tmp, bubbleType, ontype, handle, special, lastElement,\n\t\t\teventPath = [ elem || document ],\n\t\t\ttype = hasOwn.call( event, \"type\" ) ? event.type : event,\n\t\t\tnamespaces = hasOwn.call( event, \"namespace\" ) ? event.namespace.split( \".\" ) : [];\n\n\t\tcur = lastElement = tmp = elem = elem || document;\n\n\t\t// Don't do events on text and comment nodes\n\t\tif ( elem.nodeType === 3 || elem.nodeType === 8 ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// focus/blur morphs to focusin/out; ensure we're not firing them right now\n\t\tif ( rfocusMorph.test( type + jQuery.event.triggered ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( type.indexOf( \".\" ) > -1 ) {\n\n\t\t\t// Namespaced trigger; create a regexp to match event type in handle()\n\t\t\tnamespaces = type.split( \".\" );\n\t\t\ttype = namespaces.shift();\n\t\t\tnamespaces.sort();\n\t\t}\n\t\tontype = type.indexOf( \":\" ) < 0 && \"on\" + type;\n\n\t\t// Caller can pass in a jQuery.Event object, Object, or just an event type string\n\t\tevent = event[ jQuery.expando ] ?\n\t\t\tevent :\n\t\t\tnew jQuery.Event( type, typeof event === \"object\" && event );\n\n\t\t// Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true)\n\t\tevent.isTrigger = onlyHandlers ? 2 : 3;\n\t\tevent.namespace = namespaces.join( \".\" );\n\t\tevent.rnamespace = event.namespace ?\n\t\t\tnew RegExp( \"(^|\\\\.)\" + namespaces.join( \"\\\\.(?:.*\\\\.|)\" ) + \"(\\\\.|$)\" ) :\n\t\t\tnull;\n\n\t\t// Clean up the event in case it is being reused\n\t\tevent.result = undefined;\n\t\tif ( !event.target ) {\n\t\t\tevent.target = elem;\n\t\t}\n\n\t\t// Clone any incoming data and prepend the event, creating the handler arg list\n\t\tdata = data == null ?\n\t\t\t[ event ] :\n\t\t\tjQuery.makeArray( data, [ event ] );\n\n\t\t// Allow special events to draw outside the lines\n\t\tspecial = jQuery.event.special[ type ] || {};\n\t\tif ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Determine event propagation path in advance, per W3C events spec (#9951)\n\t\t// Bubble up to document, then to window; watch for a global ownerDocument var (#9724)\n\t\tif ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) {\n\n\t\t\tbubbleType = special.delegateType || type;\n\t\t\tif ( !rfocusMorph.test( bubbleType + type ) ) {\n\t\t\t\tcur = cur.parentNode;\n\t\t\t}\n\t\t\tfor ( ; cur; cur = cur.parentNode ) {\n\t\t\t\teventPath.push( cur );\n\t\t\t\ttmp = cur;\n\t\t\t}\n\n\t\t\t// Only add window if we got to document (e.g., not plain obj or detached DOM)\n\t\t\tif ( tmp === ( elem.ownerDocument || document ) ) {\n\t\t\t\teventPath.push( tmp.defaultView || tmp.parentWindow || window );\n\t\t\t}\n\t\t}\n\n\t\t// Fire handlers on the event path\n\t\ti = 0;\n\t\twhile ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) {\n\t\t\tlastElement = cur;\n\t\t\tevent.type = i > 1 ?\n\t\t\t\tbubbleType :\n\t\t\t\tspecial.bindType || type;\n\n\t\t\t// jQuery handler\n\t\t\thandle = ( dataPriv.get( cur, \"events\" ) || {} )[ event.type ] &&\n\t\t\t\tdataPriv.get( cur, \"handle\" );\n\t\t\tif ( handle ) {\n\t\t\t\thandle.apply( cur, data );\n\t\t\t}\n\n\t\t\t// Native handler\n\t\t\thandle = ontype && cur[ ontype ];\n\t\t\tif ( handle && handle.apply && acceptData( cur ) ) {\n\t\t\t\tevent.result = handle.apply( cur, data );\n\t\t\t\tif ( event.result === false ) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tevent.type = type;\n\n\t\t// If nobody prevented the default action, do it now\n\t\tif ( !onlyHandlers && !event.isDefaultPrevented() ) {\n\n\t\t\tif ( ( !special._default ||\n\t\t\t\tspecial._default.apply( eventPath.pop(), data ) === false ) &&\n\t\t\t\tacceptData( elem ) ) {\n\n\t\t\t\t// Call a native DOM method on the target with the same name as the event.\n\t\t\t\t// Don't do default actions on window, that's where global variables be (#6170)\n\t\t\t\tif ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) {\n\n\t\t\t\t\t// Don't re-trigger an onFOO event when we call its FOO() method\n\t\t\t\t\ttmp = elem[ ontype ];\n\n\t\t\t\t\tif ( tmp ) {\n\t\t\t\t\t\telem[ ontype ] = null;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Prevent re-triggering of the same event, since we already bubbled it above\n\t\t\t\t\tjQuery.event.triggered = type;\n\n\t\t\t\t\tif ( event.isPropagationStopped() ) {\n\t\t\t\t\t\tlastElement.addEventListener( type, stopPropagationCallback );\n\t\t\t\t\t}\n\n\t\t\t\t\telem[ type ]();\n\n\t\t\t\t\tif ( event.isPropagationStopped() ) {\n\t\t\t\t\t\tlastElement.removeEventListener( type, stopPropagationCallback );\n\t\t\t\t\t}\n\n\t\t\t\t\tjQuery.event.triggered = undefined;\n\n\t\t\t\t\tif ( tmp ) {\n\t\t\t\t\t\telem[ ontype ] = tmp;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn event.result;\n\t},\n\n\t// Piggyback on a donor event to simulate a different one\n\t// Used only for `focus(in | out)` events\n\tsimulate: function( type, elem, event ) {\n\t\tvar e = jQuery.extend(\n\t\t\tnew jQuery.Event(),\n\t\t\tevent,\n\t\t\t{\n\t\t\t\ttype: type,\n\t\t\t\tisSimulated: true\n\t\t\t}\n\t\t);\n\n\t\tjQuery.event.trigger( e, null, elem );\n\t}\n\n} );\n\njQuery.fn.extend( {\n\n\ttrigger: function( type, data ) {\n\t\treturn this.each( function() {\n\t\t\tjQuery.event.trigger( type, data, this );\n\t\t} );\n\t},\n\ttriggerHandler: function( type, data ) {\n\t\tvar elem = this[ 0 ];\n\t\tif ( elem ) {\n\t\t\treturn jQuery.event.trigger( type, data, elem, true );\n\t\t}\n\t}\n} );\n\n\n// Support: Firefox <=44\n// Firefox doesn't have focus(in | out) events\n// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787\n//\n// Support: Chrome <=48 - 49, Safari <=9.0 - 9.1\n// focus(in | out) events fire after focus & blur events,\n// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order\n// Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857\nif ( !support.focusin ) {\n\tjQuery.each( { focus: \"focusin\", blur: \"focusout\" }, function( orig, fix ) {\n\n\t\t// Attach a single capturing handler on the document while someone wants focusin/focusout\n\t\tvar handler = function( event ) {\n\t\t\tjQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) );\n\t\t};\n\n\t\tjQuery.event.special[ fix ] = {\n\t\t\tsetup: function() {\n\t\t\t\tvar doc = this.ownerDocument || this,\n\t\t\t\t\tattaches = dataPriv.access( doc, fix );\n\n\t\t\t\tif ( !attaches ) {\n\t\t\t\t\tdoc.addEventListener( orig, handler, true );\n\t\t\t\t}\n\t\t\t\tdataPriv.access( doc, fix, ( attaches || 0 ) + 1 );\n\t\t\t},\n\t\t\tteardown: function() {\n\t\t\t\tvar doc = this.ownerDocument || this,\n\t\t\t\t\tattaches = dataPriv.access( doc, fix ) - 1;\n\n\t\t\t\tif ( !attaches ) {\n\t\t\t\t\tdoc.removeEventListener( orig, handler, true );\n\t\t\t\t\tdataPriv.remove( doc, fix );\n\n\t\t\t\t} else {\n\t\t\t\t\tdataPriv.access( doc, fix, attaches );\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t} );\n}\nvar location = window.location;\n\nvar nonce = Date.now();\n\nvar rquery = ( /\\?/ );\n\n\n\n// Cross-browser xml parsing\njQuery.parseXML = function( data ) {\n\tvar xml;\n\tif ( !data || typeof data !== \"string\" ) {\n\t\treturn null;\n\t}\n\n\t// Support: IE 9 - 11 only\n\t// IE throws on parseFromString with invalid input.\n\ttry {\n\t\txml = ( new window.DOMParser() ).parseFromString( data, \"text/xml\" );\n\t} catch ( e ) {\n\t\txml = undefined;\n\t}\n\n\tif ( !xml || xml.getElementsByTagName( \"parsererror\" ).length ) {\n\t\tjQuery.error( \"Invalid XML: \" + data );\n\t}\n\treturn xml;\n};\n\n\nvar\n\trbracket = /\\[\\]$/,\n\trCRLF = /\\r?\\n/g,\n\trsubmitterTypes = /^(?:submit|button|image|reset|file)$/i,\n\trsubmittable = /^(?:input|select|textarea|keygen)/i;\n\nfunction buildParams( prefix, obj, traditional, add ) {\n\tvar name;\n\n\tif ( Array.isArray( obj ) ) {\n\n\t\t// Serialize array item.\n\t\tjQuery.each( obj, function( i, v ) {\n\t\t\tif ( traditional || rbracket.test( prefix ) ) {\n\n\t\t\t\t// Treat each array item as a scalar.\n\t\t\t\tadd( prefix, v );\n\n\t\t\t} else {\n\n\t\t\t\t// Item is non-scalar (array or object), encode its numeric index.\n\t\t\t\tbuildParams(\n\t\t\t\t\tprefix + \"[\" + ( typeof v === \"object\" && v != null ? i : \"\" ) + \"]\",\n\t\t\t\t\tv,\n\t\t\t\t\ttraditional,\n\t\t\t\t\tadd\n\t\t\t\t);\n\t\t\t}\n\t\t} );\n\n\t} else if ( !traditional && toType( obj ) === \"object\" ) {\n\n\t\t// Serialize object item.\n\t\tfor ( name in obj ) {\n\t\t\tbuildParams( prefix + \"[\" + name + \"]\", obj[ name ], traditional, add );\n\t\t}\n\n\t} else {\n\n\t\t// Serialize scalar item.\n\t\tadd( prefix, obj );\n\t}\n}\n\n// Serialize an array of form elements or a set of\n// key/values into a query string\njQuery.param = function( a, traditional ) {\n\tvar prefix,\n\t\ts = [],\n\t\tadd = function( key, valueOrFunction ) {\n\n\t\t\t// If value is a function, invoke it and use its return value\n\t\t\tvar value = isFunction( valueOrFunction ) ?\n\t\t\t\tvalueOrFunction() :\n\t\t\t\tvalueOrFunction;\n\n\t\t\ts[ s.length ] = encodeURIComponent( key ) + \"=\" +\n\t\t\t\tencodeURIComponent( value == null ? \"\" : value );\n\t\t};\n\n\tif ( a == null ) {\n\t\treturn \"\";\n\t}\n\n\t// If an array was passed in, assume that it is an array of form elements.\n\tif ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {\n\n\t\t// Serialize the form elements\n\t\tjQuery.each( a, function() {\n\t\t\tadd( this.name, this.value );\n\t\t} );\n\n\t} else {\n\n\t\t// If traditional, encode the \"old\" way (the way 1.3.2 or older\n\t\t// did it), otherwise encode params recursively.\n\t\tfor ( prefix in a ) {\n\t\t\tbuildParams( prefix, a[ prefix ], traditional, add );\n\t\t}\n\t}\n\n\t// Return the resulting serialization\n\treturn s.join( \"&\" );\n};\n\njQuery.fn.extend( {\n\tserialize: function() {\n\t\treturn jQuery.param( this.serializeArray() );\n\t},\n\tserializeArray: function() {\n\t\treturn this.map( function() {\n\n\t\t\t// Can add propHook for \"elements\" to filter or add form elements\n\t\t\tvar elements = jQuery.prop( this, \"elements\" );\n\t\t\treturn elements ? jQuery.makeArray( elements ) : this;\n\t\t} )\n\t\t.filter( function() {\n\t\t\tvar type = this.type;\n\n\t\t\t// Use .is( \":disabled\" ) so that fieldset[disabled] works\n\t\t\treturn this.name && !jQuery( this ).is( \":disabled\" ) &&\n\t\t\t\trsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&\n\t\t\t\t( this.checked || !rcheckableType.test( type ) );\n\t\t} )\n\t\t.map( function( i, elem ) {\n\t\t\tvar val = jQuery( this ).val();\n\n\t\t\tif ( val == null ) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tif ( Array.isArray( val ) ) {\n\t\t\t\treturn jQuery.map( val, function( val ) {\n\t\t\t\t\treturn { name: elem.name, value: val.replace( rCRLF, \"\\r\\n\" ) };\n\t\t\t\t} );\n\t\t\t}\n\n\t\t\treturn { name: elem.name, value: val.replace( rCRLF, \"\\r\\n\" ) };\n\t\t} ).get();\n\t}\n} );\n\n\nvar\n\tr20 = /%20/g,\n\trhash = /#.*$/,\n\trantiCache = /([?&])_=[^&]*/,\n\trheaders = /^(.*?):[ \\t]*([^\\r\\n]*)$/mg,\n\n\t// #7653, #8125, #8152: local protocol detection\n\trlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/,\n\trnoContent = /^(?:GET|HEAD)$/,\n\trprotocol = /^\\/\\//,\n\n\t/* Prefilters\n\t * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)\n\t * 2) These are called:\n\t * - BEFORE asking for a transport\n\t * - AFTER param serialization (s.data is a string if s.processData is true)\n\t * 3) key is the dataType\n\t * 4) the catchall symbol \"*\" can be used\n\t * 5) execution will start with transport dataType and THEN continue down to \"*\" if needed\n\t */\n\tprefilters = {},\n\n\t/* Transports bindings\n\t * 1) key is the dataType\n\t * 2) the catchall symbol \"*\" can be used\n\t * 3) selection will start with transport dataType and THEN go to \"*\" if needed\n\t */\n\ttransports = {},\n\n\t// Avoid comment-prolog char sequence (#10098); must appease lint and evade compression\n\tallTypes = \"*/\".concat( \"*\" ),\n\n\t// Anchor tag for parsing the document origin\n\toriginAnchor = document.createElement( \"a\" );\n\toriginAnchor.href = location.href;\n\n// Base \"constructor\" for jQuery.ajaxPrefilter and jQuery.ajaxTransport\nfunction addToPrefiltersOrTransports( structure ) {\n\n\t// dataTypeExpression is optional and defaults to \"*\"\n\treturn function( dataTypeExpression, func ) {\n\n\t\tif ( typeof dataTypeExpression !== \"string\" ) {\n\t\t\tfunc = dataTypeExpression;\n\t\t\tdataTypeExpression = \"*\";\n\t\t}\n\n\t\tvar dataType,\n\t\t\ti = 0,\n\t\t\tdataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || [];\n\n\t\tif ( isFunction( func ) ) {\n\n\t\t\t// For each dataType in the dataTypeExpression\n\t\t\twhile ( ( dataType = dataTypes[ i++ ] ) ) {\n\n\t\t\t\t// Prepend if requested\n\t\t\t\tif ( dataType[ 0 ] === \"+\" ) {\n\t\t\t\t\tdataType = dataType.slice( 1 ) || \"*\";\n\t\t\t\t\t( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func );\n\n\t\t\t\t// Otherwise append\n\t\t\t\t} else {\n\t\t\t\t\t( structure[ dataType ] = structure[ dataType ] || [] ).push( func );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n}\n\n// Base inspection function for prefilters and transports\nfunction inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {\n\n\tvar inspected = {},\n\t\tseekingTransport = ( structure === transports );\n\n\tfunction inspect( dataType ) {\n\t\tvar selected;\n\t\tinspected[ dataType ] = true;\n\t\tjQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {\n\t\t\tvar dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );\n\t\t\tif ( typeof dataTypeOrTransport === \"string\" &&\n\t\t\t\t!seekingTransport && !inspected[ dataTypeOrTransport ] ) {\n\n\t\t\t\toptions.dataTypes.unshift( dataTypeOrTransport );\n\t\t\t\tinspect( dataTypeOrTransport );\n\t\t\t\treturn false;\n\t\t\t} else if ( seekingTransport ) {\n\t\t\t\treturn !( selected = dataTypeOrTransport );\n\t\t\t}\n\t\t} );\n\t\treturn selected;\n\t}\n\n\treturn inspect( options.dataTypes[ 0 ] ) || !inspected[ \"*\" ] && inspect( \"*\" );\n}\n\n// A special extend for ajax options\n// that takes \"flat\" options (not to be deep extended)\n// Fixes #9887\nfunction ajaxExtend( target, src ) {\n\tvar key, deep,\n\t\tflatOptions = jQuery.ajaxSettings.flatOptions || {};\n\n\tfor ( key in src ) {\n\t\tif ( src[ key ] !== undefined ) {\n\t\t\t( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ];\n\t\t}\n\t}\n\tif ( deep ) {\n\t\tjQuery.extend( true, target, deep );\n\t}\n\n\treturn target;\n}\n\n/* Handles responses to an ajax request:\n * - finds the right dataType (mediates between content-type and expected dataType)\n * - returns the corresponding response\n */\nfunction ajaxHandleResponses( s, jqXHR, responses ) {\n\n\tvar ct, type, finalDataType, firstDataType,\n\t\tcontents = s.contents,\n\t\tdataTypes = s.dataTypes;\n\n\t// Remove auto dataType and get content-type in the process\n\twhile ( dataTypes[ 0 ] === \"*\" ) {\n\t\tdataTypes.shift();\n\t\tif ( ct === undefined ) {\n\t\t\tct = s.mimeType || jqXHR.getResponseHeader( \"Content-Type\" );\n\t\t}\n\t}\n\n\t// Check if we're dealing with a known content-type\n\tif ( ct ) {\n\t\tfor ( type in contents ) {\n\t\t\tif ( contents[ type ] && contents[ type ].test( ct ) ) {\n\t\t\t\tdataTypes.unshift( type );\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check to see if we have a response for the expected dataType\n\tif ( dataTypes[ 0 ] in responses ) {\n\t\tfinalDataType = dataTypes[ 0 ];\n\t} else {\n\n\t\t// Try convertible dataTypes\n\t\tfor ( type in responses ) {\n\t\t\tif ( !dataTypes[ 0 ] || s.converters[ type + \" \" + dataTypes[ 0 ] ] ) {\n\t\t\t\tfinalDataType = type;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif ( !firstDataType ) {\n\t\t\t\tfirstDataType = type;\n\t\t\t}\n\t\t}\n\n\t\t// Or just use first one\n\t\tfinalDataType = finalDataType || firstDataType;\n\t}\n\n\t// If we found a dataType\n\t// We add the dataType to the list if needed\n\t// and return the corresponding response\n\tif ( finalDataType ) {\n\t\tif ( finalDataType !== dataTypes[ 0 ] ) {\n\t\t\tdataTypes.unshift( finalDataType );\n\t\t}\n\t\treturn responses[ finalDataType ];\n\t}\n}\n\n/* Chain conversions given the request and the original response\n * Also sets the responseXXX fields on the jqXHR instance\n */\nfunction ajaxConvert( s, response, jqXHR, isSuccess ) {\n\tvar conv2, current, conv, tmp, prev,\n\t\tconverters = {},\n\n\t\t// Work with a copy of dataTypes in case we need to modify it for conversion\n\t\tdataTypes = s.dataTypes.slice();\n\n\t// Create converters map with lowercased keys\n\tif ( dataTypes[ 1 ] ) {\n\t\tfor ( conv in s.converters ) {\n\t\t\tconverters[ conv.toLowerCase() ] = s.converters[ conv ];\n\t\t}\n\t}\n\n\tcurrent = dataTypes.shift();\n\n\t// Convert to each sequential dataType\n\twhile ( current ) {\n\n\t\tif ( s.responseFields[ current ] ) {\n\t\t\tjqXHR[ s.responseFields[ current ] ] = response;\n\t\t}\n\n\t\t// Apply the dataFilter if provided\n\t\tif ( !prev && isSuccess && s.dataFilter ) {\n\t\t\tresponse = s.dataFilter( response, s.dataType );\n\t\t}\n\n\t\tprev = current;\n\t\tcurrent = dataTypes.shift();\n\n\t\tif ( current ) {\n\n\t\t\t// There's only work to do if current dataType is non-auto\n\t\t\tif ( current === \"*\" ) {\n\n\t\t\t\tcurrent = prev;\n\n\t\t\t// Convert response if prev dataType is non-auto and differs from current\n\t\t\t} else if ( prev !== \"*\" && prev !== current ) {\n\n\t\t\t\t// Seek a direct converter\n\t\t\t\tconv = converters[ prev + \" \" + current ] || converters[ \"* \" + current ];\n\n\t\t\t\t// If none found, seek a pair\n\t\t\t\tif ( !conv ) {\n\t\t\t\t\tfor ( conv2 in converters ) {\n\n\t\t\t\t\t\t// If conv2 outputs current\n\t\t\t\t\t\ttmp = conv2.split( \" \" );\n\t\t\t\t\t\tif ( tmp[ 1 ] === current ) {\n\n\t\t\t\t\t\t\t// If prev can be converted to accepted input\n\t\t\t\t\t\t\tconv = converters[ prev + \" \" + tmp[ 0 ] ] ||\n\t\t\t\t\t\t\t\tconverters[ \"* \" + tmp[ 0 ] ];\n\t\t\t\t\t\t\tif ( conv ) {\n\n\t\t\t\t\t\t\t\t// Condense equivalence converters\n\t\t\t\t\t\t\t\tif ( conv === true ) {\n\t\t\t\t\t\t\t\t\tconv = converters[ conv2 ];\n\n\t\t\t\t\t\t\t\t// Otherwise, insert the intermediate dataType\n\t\t\t\t\t\t\t\t} else if ( converters[ conv2 ] !== true ) {\n\t\t\t\t\t\t\t\t\tcurrent = tmp[ 0 ];\n\t\t\t\t\t\t\t\t\tdataTypes.unshift( tmp[ 1 ] );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Apply converter (if not an equivalence)\n\t\t\t\tif ( conv !== true ) {\n\n\t\t\t\t\t// Unless errors are allowed to bubble, catch and return them\n\t\t\t\t\tif ( conv && s.throws ) {\n\t\t\t\t\t\tresponse = conv( response );\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tresponse = conv( response );\n\t\t\t\t\t\t} catch ( e ) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tstate: \"parsererror\",\n\t\t\t\t\t\t\t\terror: conv ? e : \"No conversion from \" + prev + \" to \" + current\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { state: \"success\", data: response };\n}\n\njQuery.extend( {\n\n\t// Counter for holding the number of active queries\n\tactive: 0,\n\n\t// Last-Modified header cache for next request\n\tlastModified: {},\n\tetag: {},\n\n\tajaxSettings: {\n\t\turl: location.href,\n\t\ttype: \"GET\",\n\t\tisLocal: rlocalProtocol.test( location.protocol ),\n\t\tglobal: true,\n\t\tprocessData: true,\n\t\tasync: true,\n\t\tcontentType: \"application/x-www-form-urlencoded; charset=UTF-8\",\n\n\t\t/*\n\t\ttimeout: 0,\n\t\tdata: null,\n\t\tdataType: null,\n\t\tusername: null,\n\t\tpassword: null,\n\t\tcache: null,\n\t\tthrows: false,\n\t\ttraditional: false,\n\t\theaders: {},\n\t\t*/\n\n\t\taccepts: {\n\t\t\t\"*\": allTypes,\n\t\t\ttext: \"text/plain\",\n\t\t\thtml: \"text/html\",\n\t\t\txml: \"application/xml, text/xml\",\n\t\t\tjson: \"application/json, text/javascript\"\n\t\t},\n\n\t\tcontents: {\n\t\t\txml: /\\bxml\\b/,\n\t\t\thtml: /\\bhtml/,\n\t\t\tjson: /\\bjson\\b/\n\t\t},\n\n\t\tresponseFields: {\n\t\t\txml: \"responseXML\",\n\t\t\ttext: \"responseText\",\n\t\t\tjson: \"responseJSON\"\n\t\t},\n\n\t\t// Data converters\n\t\t// Keys separate source (or catchall \"*\") and destination types with a single space\n\t\tconverters: {\n\n\t\t\t// Convert anything to text\n\t\t\t\"* text\": String,\n\n\t\t\t// Text to html (true = no transformation)\n\t\t\t\"text html\": true,\n\n\t\t\t// Evaluate text as a json expression\n\t\t\t\"text json\": JSON.parse,\n\n\t\t\t// Parse text as xml\n\t\t\t\"text xml\": jQuery.parseXML\n\t\t},\n\n\t\t// For options that shouldn't be deep extended:\n\t\t// you can add your own custom options here if\n\t\t// and when you create one that shouldn't be\n\t\t// deep extended (see ajaxExtend)\n\t\tflatOptions: {\n\t\t\turl: true,\n\t\t\tcontext: true\n\t\t}\n\t},\n\n\t// Creates a full fledged settings object into target\n\t// with both ajaxSettings and settings fields.\n\t// If target is omitted, writes into ajaxSettings.\n\tajaxSetup: function( target, settings ) {\n\t\treturn settings ?\n\n\t\t\t// Building a settings object\n\t\t\tajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) :\n\n\t\t\t// Extending ajaxSettings\n\t\t\tajaxExtend( jQuery.ajaxSettings, target );\n\t},\n\n\tajaxPrefilter: addToPrefiltersOrTransports( prefilters ),\n\tajaxTransport: addToPrefiltersOrTransports( transports ),\n\n\t// Main method\n\tajax: function( url, options ) {\n\n\t\t// If url is an object, simulate pre-1.5 signature\n\t\tif ( typeof url === \"object\" ) {\n\t\t\toptions = url;\n\t\t\turl = undefined;\n\t\t}\n\n\t\t// Force options to be an object\n\t\toptions = options || {};\n\n\t\tvar transport,\n\n\t\t\t// URL without anti-cache param\n\t\t\tcacheURL,\n\n\t\t\t// Response headers\n\t\t\tresponseHeadersString,\n\t\t\tresponseHeaders,\n\n\t\t\t// timeout handle\n\t\t\ttimeoutTimer,\n\n\t\t\t// Url cleanup var\n\t\t\turlAnchor,\n\n\t\t\t// Request state (becomes false upon send and true upon completion)\n\t\t\tcompleted,\n\n\t\t\t// To know if global events are to be dispatched\n\t\t\tfireGlobals,\n\n\t\t\t// Loop variable\n\t\t\ti,\n\n\t\t\t// uncached part of the url\n\t\t\tuncached,\n\n\t\t\t// Create the final options object\n\t\t\ts = jQuery.ajaxSetup( {}, options ),\n\n\t\t\t// Callbacks context\n\t\t\tcallbackContext = s.context || s,\n\n\t\t\t// Context for global events is callbackContext if it is a DOM node or jQuery collection\n\t\t\tglobalEventContext = s.context &&\n\t\t\t\t( callbackContext.nodeType || callbackContext.jquery ) ?\n\t\t\t\t\tjQuery( callbackContext ) :\n\t\t\t\t\tjQuery.event,\n\n\t\t\t// Deferreds\n\t\t\tdeferred = jQuery.Deferred(),\n\t\t\tcompleteDeferred = jQuery.Callbacks( \"once memory\" ),\n\n\t\t\t// Status-dependent callbacks\n\t\t\tstatusCode = s.statusCode || {},\n\n\t\t\t// Headers (they are sent all at once)\n\t\t\trequestHeaders = {},\n\t\t\trequestHeadersNames = {},\n\n\t\t\t// Default abort message\n\t\t\tstrAbort = \"canceled\",\n\n\t\t\t// Fake xhr\n\t\t\tjqXHR = {\n\t\t\t\treadyState: 0,\n\n\t\t\t\t// Builds headers hashtable if needed\n\t\t\t\tgetResponseHeader: function( key ) {\n\t\t\t\t\tvar match;\n\t\t\t\t\tif ( completed ) {\n\t\t\t\t\t\tif ( !responseHeaders ) {\n\t\t\t\t\t\t\tresponseHeaders = {};\n\t\t\t\t\t\t\twhile ( ( match = rheaders.exec( responseHeadersString ) ) ) {\n\t\t\t\t\t\t\t\tresponseHeaders[ match[ 1 ].toLowerCase() + \" \" ] =\n\t\t\t\t\t\t\t\t\t( responseHeaders[ match[ 1 ].toLowerCase() + \" \" ] || [] )\n\t\t\t\t\t\t\t\t\t\t.concat( match[ 2 ] );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tmatch = responseHeaders[ key.toLowerCase() + \" \" ];\n\t\t\t\t\t}\n\t\t\t\t\treturn match == null ? null : match.join( \", \" );\n\t\t\t\t},\n\n\t\t\t\t// Raw string\n\t\t\t\tgetAllResponseHeaders: function() {\n\t\t\t\t\treturn completed ? responseHeadersString : null;\n\t\t\t\t},\n\n\t\t\t\t// Caches the header\n\t\t\t\tsetRequestHeader: function( name, value ) {\n\t\t\t\t\tif ( completed == null ) {\n\t\t\t\t\t\tname = requestHeadersNames[ name.toLowerCase() ] =\n\t\t\t\t\t\t\trequestHeadersNames[ name.toLowerCase() ] || name;\n\t\t\t\t\t\trequestHeaders[ name ] = value;\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\n\t\t\t\t// Overrides response content-type header\n\t\t\t\toverrideMimeType: function( type ) {\n\t\t\t\t\tif ( completed == null ) {\n\t\t\t\t\t\ts.mimeType = type;\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\n\t\t\t\t// Status-dependent callbacks\n\t\t\t\tstatusCode: function( map ) {\n\t\t\t\t\tvar code;\n\t\t\t\t\tif ( map ) {\n\t\t\t\t\t\tif ( completed ) {\n\n\t\t\t\t\t\t\t// Execute the appropriate callbacks\n\t\t\t\t\t\t\tjqXHR.always( map[ jqXHR.status ] );\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t// Lazy-add the new callbacks in a way that preserves old ones\n\t\t\t\t\t\t\tfor ( code in map ) {\n\t\t\t\t\t\t\t\tstatusCode[ code ] = [ statusCode[ code ], map[ code ] ];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\n\t\t\t\t// Cancel the request\n\t\t\t\tabort: function( statusText ) {\n\t\t\t\t\tvar finalText = statusText || strAbort;\n\t\t\t\t\tif ( transport ) {\n\t\t\t\t\t\ttransport.abort( finalText );\n\t\t\t\t\t}\n\t\t\t\t\tdone( 0, finalText );\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\t\t\t};\n\n\t\t// Attach deferreds\n\t\tdeferred.promise( jqXHR );\n\n\t\t// Add protocol if not provided (prefilters might expect it)\n\t\t// Handle falsy url in the settings object (#10093: consistency with old signature)\n\t\t// We also use the url parameter if available\n\t\ts.url = ( ( url || s.url || location.href ) + \"\" )\n\t\t\t.replace( rprotocol, location.protocol + \"//\" );\n\n\t\t// Alias method option to type as per ticket #12004\n\t\ts.type = options.method || options.type || s.method || s.type;\n\n\t\t// Extract dataTypes list\n\t\ts.dataTypes = ( s.dataType || \"*\" ).toLowerCase().match( rnothtmlwhite ) || [ \"\" ];\n\n\t\t// A cross-domain request is in order when the origin doesn't match the current origin.\n\t\tif ( s.crossDomain == null ) {\n\t\t\turlAnchor = document.createElement( \"a\" );\n\n\t\t\t// Support: IE <=8 - 11, Edge 12 - 15\n\t\t\t// IE throws exception on accessing the href property if url is malformed,\n\t\t\t// e.g. http://example.com:80x/\n\t\t\ttry {\n\t\t\t\turlAnchor.href = s.url;\n\n\t\t\t\t// Support: IE <=8 - 11 only\n\t\t\t\t// Anchor's host property isn't correctly set when s.url is relative\n\t\t\t\turlAnchor.href = urlAnchor.href;\n\t\t\t\ts.crossDomain = originAnchor.protocol + \"//\" + originAnchor.host !==\n\t\t\t\t\turlAnchor.protocol + \"//\" + urlAnchor.host;\n\t\t\t} catch ( e ) {\n\n\t\t\t\t// If there is an error parsing the URL, assume it is crossDomain,\n\t\t\t\t// it can be rejected by the transport if it is invalid\n\t\t\t\ts.crossDomain = true;\n\t\t\t}\n\t\t}\n\n\t\t// Convert data if not already a string\n\t\tif ( s.data && s.processData && typeof s.data !== \"string\" ) {\n\t\t\ts.data = jQuery.param( s.data, s.traditional );\n\t\t}\n\n\t\t// Apply prefilters\n\t\tinspectPrefiltersOrTransports( prefilters, s, options, jqXHR );\n\n\t\t// If request was aborted inside a prefilter, stop there\n\t\tif ( completed ) {\n\t\t\treturn jqXHR;\n\t\t}\n\n\t\t// We can fire global events as of now if asked to\n\t\t// Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118)\n\t\tfireGlobals = jQuery.event && s.global;\n\n\t\t// Watch for a new set of requests\n\t\tif ( fireGlobals && jQuery.active++ === 0 ) {\n\t\t\tjQuery.event.trigger( \"ajaxStart\" );\n\t\t}\n\n\t\t// Uppercase the type\n\t\ts.type = s.type.toUpperCase();\n\n\t\t// Determine if request has content\n\t\ts.hasContent = !rnoContent.test( s.type );\n\n\t\t// Save the URL in case we're toying with the If-Modified-Since\n\t\t// and/or If-None-Match header later on\n\t\t// Remove hash to simplify url manipulation\n\t\tcacheURL = s.url.replace( rhash, \"\" );\n\n\t\t// More options handling for requests with no content\n\t\tif ( !s.hasContent ) {\n\n\t\t\t// Remember the hash so we can put it back\n\t\t\tuncached = s.url.slice( cacheURL.length );\n\n\t\t\t// If data is available and should be processed, append data to url\n\t\t\tif ( s.data && ( s.processData || typeof s.data === \"string\" ) ) {\n\t\t\t\tcacheURL += ( rquery.test( cacheURL ) ? \"&\" : \"?\" ) + s.data;\n\n\t\t\t\t// #9682: remove data so that it's not used in an eventual retry\n\t\t\t\tdelete s.data;\n\t\t\t}\n\n\t\t\t// Add or update anti-cache param if needed\n\t\t\tif ( s.cache === false ) {\n\t\t\t\tcacheURL = cacheURL.replace( rantiCache, \"$1\" );\n\t\t\t\tuncached = ( rquery.test( cacheURL ) ? \"&\" : \"?\" ) + \"_=\" + ( nonce++ ) + uncached;\n\t\t\t}\n\n\t\t\t// Put hash and anti-cache on the URL that will be requested (gh-1732)\n\t\t\ts.url = cacheURL + uncached;\n\n\t\t// Change '%20' to '+' if this is encoded form body content (gh-2658)\n\t\t} else if ( s.data && s.processData &&\n\t\t\t( s.contentType || \"\" ).indexOf( \"application/x-www-form-urlencoded\" ) === 0 ) {\n\t\t\ts.data = s.data.replace( r20, \"+\" );\n\t\t}\n\n\t\t// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.\n\t\tif ( s.ifModified ) {\n\t\t\tif ( jQuery.lastModified[ cacheURL ] ) {\n\t\t\t\tjqXHR.setRequestHeader( \"If-Modified-Since\", jQuery.lastModified[ cacheURL ] );\n\t\t\t}\n\t\t\tif ( jQuery.etag[ cacheURL ] ) {\n\t\t\t\tjqXHR.setRequestHeader( \"If-None-Match\", jQuery.etag[ cacheURL ] );\n\t\t\t}\n\t\t}\n\n\t\t// Set the correct header, if data is being sent\n\t\tif ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {\n\t\t\tjqXHR.setRequestHeader( \"Content-Type\", s.contentType );\n\t\t}\n\n\t\t// Set the Accepts header for the server, depending on the dataType\n\t\tjqXHR.setRequestHeader(\n\t\t\t\"Accept\",\n\t\t\ts.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ?\n\t\t\t\ts.accepts[ s.dataTypes[ 0 ] ] +\n\t\t\t\t\t( s.dataTypes[ 0 ] !== \"*\" ? \", \" + allTypes + \"; q=0.01\" : \"\" ) :\n\t\t\t\ts.accepts[ \"*\" ]\n\t\t);\n\n\t\t// Check for headers option\n\t\tfor ( i in s.headers ) {\n\t\t\tjqXHR.setRequestHeader( i, s.headers[ i ] );\n\t\t}\n\n\t\t// Allow custom headers/mimetypes and early abort\n\t\tif ( s.beforeSend &&\n\t\t\t( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) {\n\n\t\t\t// Abort if not done already and return\n\t\t\treturn jqXHR.abort();\n\t\t}\n\n\t\t// Aborting is no longer a cancellation\n\t\tstrAbort = \"abort\";\n\n\t\t// Install callbacks on deferreds\n\t\tcompleteDeferred.add( s.complete );\n\t\tjqXHR.done( s.success );\n\t\tjqXHR.fail( s.error );\n\n\t\t// Get transport\n\t\ttransport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );\n\n\t\t// If no transport, we auto-abort\n\t\tif ( !transport ) {\n\t\t\tdone( -1, \"No Transport\" );\n\t\t} else {\n\t\t\tjqXHR.readyState = 1;\n\n\t\t\t// Send global event\n\t\t\tif ( fireGlobals ) {\n\t\t\t\tglobalEventContext.trigger( \"ajaxSend\", [ jqXHR, s ] );\n\t\t\t}\n\n\t\t\t// If request was aborted inside ajaxSend, stop there\n\t\t\tif ( completed ) {\n\t\t\t\treturn jqXHR;\n\t\t\t}\n\n\t\t\t// Timeout\n\t\t\tif ( s.async && s.timeout > 0 ) {\n\t\t\t\ttimeoutTimer = window.setTimeout( function() {\n\t\t\t\t\tjqXHR.abort( \"timeout\" );\n\t\t\t\t}, s.timeout );\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tcompleted = false;\n\t\t\t\ttransport.send( requestHeaders, done );\n\t\t\t} catch ( e ) {\n\n\t\t\t\t// Rethrow post-completion exceptions\n\t\t\t\tif ( completed ) {\n\t\t\t\t\tthrow e;\n\t\t\t\t}\n\n\t\t\t\t// Propagate others as results\n\t\t\t\tdone( -1, e );\n\t\t\t}\n\t\t}\n\n\t\t// Callback for when everything is done\n\t\tfunction done( status, nativeStatusText, responses, headers ) {\n\t\t\tvar isSuccess, success, error, response, modified,\n\t\t\t\tstatusText = nativeStatusText;\n\n\t\t\t// Ignore repeat invocations\n\t\t\tif ( completed ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tcompleted = true;\n\n\t\t\t// Clear timeout if it exists\n\t\t\tif ( timeoutTimer ) {\n\t\t\t\twindow.clearTimeout( timeoutTimer );\n\t\t\t}\n\n\t\t\t// Dereference transport for early garbage collection\n\t\t\t// (no matter how long the jqXHR object will be used)\n\t\t\ttransport = undefined;\n\n\t\t\t// Cache response headers\n\t\t\tresponseHeadersString = headers || \"\";\n\n\t\t\t// Set readyState\n\t\t\tjqXHR.readyState = status > 0 ? 4 : 0;\n\n\t\t\t// Determine if successful\n\t\t\tisSuccess = status >= 200 && status < 300 || status === 304;\n\n\t\t\t// Get response data\n\t\t\tif ( responses ) {\n\t\t\t\tresponse = ajaxHandleResponses( s, jqXHR, responses );\n\t\t\t}\n\n\t\t\t// Convert no matter what (that way responseXXX fields are always set)\n\t\t\tresponse = ajaxConvert( s, response, jqXHR, isSuccess );\n\n\t\t\t// If successful, handle type chaining\n\t\t\tif ( isSuccess ) {\n\n\t\t\t\t// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.\n\t\t\t\tif ( s.ifModified ) {\n\t\t\t\t\tmodified = jqXHR.getResponseHeader( \"Last-Modified\" );\n\t\t\t\t\tif ( modified ) {\n\t\t\t\t\t\tjQuery.lastModified[ cacheURL ] = modified;\n\t\t\t\t\t}\n\t\t\t\t\tmodified = jqXHR.getResponseHeader( \"etag\" );\n\t\t\t\t\tif ( modified ) {\n\t\t\t\t\t\tjQuery.etag[ cacheURL ] = modified;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// if no content\n\t\t\t\tif ( status === 204 || s.type === \"HEAD\" ) {\n\t\t\t\t\tstatusText = \"nocontent\";\n\n\t\t\t\t// if not modified\n\t\t\t\t} else if ( status === 304 ) {\n\t\t\t\t\tstatusText = \"notmodified\";\n\n\t\t\t\t// If we have data, let's convert it\n\t\t\t\t} else {\n\t\t\t\t\tstatusText = response.state;\n\t\t\t\t\tsuccess = response.data;\n\t\t\t\t\terror = response.error;\n\t\t\t\t\tisSuccess = !error;\n\t\t\t\t}\n\t\t\t} else {\n\n\t\t\t\t// Extract error from statusText and normalize for non-aborts\n\t\t\t\terror = statusText;\n\t\t\t\tif ( status || !statusText ) {\n\t\t\t\t\tstatusText = \"error\";\n\t\t\t\t\tif ( status < 0 ) {\n\t\t\t\t\t\tstatus = 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Set data for the fake xhr object\n\t\t\tjqXHR.status = status;\n\t\t\tjqXHR.statusText = ( nativeStatusText || statusText ) + \"\";\n\n\t\t\t// Success/Error\n\t\t\tif ( isSuccess ) {\n\t\t\t\tdeferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );\n\t\t\t} else {\n\t\t\t\tdeferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );\n\t\t\t}\n\n\t\t\t// Status-dependent callbacks\n\t\t\tjqXHR.statusCode( statusCode );\n\t\t\tstatusCode = undefined;\n\n\t\t\tif ( fireGlobals ) {\n\t\t\t\tglobalEventContext.trigger( isSuccess ? \"ajaxSuccess\" : \"ajaxError\",\n\t\t\t\t\t[ jqXHR, s, isSuccess ? success : error ] );\n\t\t\t}\n\n\t\t\t// Complete\n\t\t\tcompleteDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );\n\n\t\t\tif ( fireGlobals ) {\n\t\t\t\tglobalEventContext.trigger( \"ajaxComplete\", [ jqXHR, s ] );\n\n\t\t\t\t// Handle the global AJAX counter\n\t\t\t\tif ( !( --jQuery.active ) ) {\n\t\t\t\t\tjQuery.event.trigger( \"ajaxStop\" );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn jqXHR;\n\t},\n\n\tgetJSON: function( url, data, callback ) {\n\t\treturn jQuery.get( url, data, callback, \"json\" );\n\t},\n\n\tgetScript: function( url, callback ) {\n\t\treturn jQuery.get( url, undefined, callback, \"script\" );\n\t}\n} );\n\njQuery.each( [ \"get\", \"post\" ], function( i, method ) {\n\tjQuery[ method ] = function( url, data, callback, type ) {\n\n\t\t// Shift arguments if data argument was omitted\n\t\tif ( isFunction( data ) ) {\n\t\t\ttype = type || callback;\n\t\t\tcallback = data;\n\t\t\tdata = undefined;\n\t\t}\n\n\t\t// The url can be an options object (which then must have .url)\n\t\treturn jQuery.ajax( jQuery.extend( {\n\t\t\turl: url,\n\t\t\ttype: method,\n\t\t\tdataType: type,\n\t\t\tdata: data,\n\t\t\tsuccess: callback\n\t\t}, jQuery.isPlainObject( url ) && url ) );\n\t};\n} );\n\n\njQuery._evalUrl = function( url, options ) {\n\treturn jQuery.ajax( {\n\t\turl: url,\n\n\t\t// Make this explicit, since user can override this through ajaxSetup (#11264)\n\t\ttype: \"GET\",\n\t\tdataType: \"script\",\n\t\tcache: true,\n\t\tasync: false,\n\t\tglobal: false,\n\n\t\t// Only evaluate the response if it is successful (gh-4126)\n\t\t// dataFilter is not invoked for failure responses, so using it instead\n\t\t// of the default converter is kludgy but it works.\n\t\tconverters: {\n\t\t\t\"text script\": function() {}\n\t\t},\n\t\tdataFilter: function( response ) {\n\t\t\tjQuery.globalEval( response, options );\n\t\t}\n\t} );\n};\n\n\njQuery.fn.extend( {\n\twrapAll: function( html ) {\n\t\tvar wrap;\n\n\t\tif ( this[ 0 ] ) {\n\t\t\tif ( isFunction( html ) ) {\n\t\t\t\thtml = html.call( this[ 0 ] );\n\t\t\t}\n\n\t\t\t// The elements to wrap the target around\n\t\t\twrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true );\n\n\t\t\tif ( this[ 0 ].parentNode ) {\n\t\t\t\twrap.insertBefore( this[ 0 ] );\n\t\t\t}\n\n\t\t\twrap.map( function() {\n\t\t\t\tvar elem = this;\n\n\t\t\t\twhile ( elem.firstElementChild ) {\n\t\t\t\t\telem = elem.firstElementChild;\n\t\t\t\t}\n\n\t\t\t\treturn elem;\n\t\t\t} ).append( this );\n\t\t}\n\n\t\treturn this;\n\t},\n\n\twrapInner: function( html ) {\n\t\tif ( isFunction( html ) ) {\n\t\t\treturn this.each( function( i ) {\n\t\t\t\tjQuery( this ).wrapInner( html.call( this, i ) );\n\t\t\t} );\n\t\t}\n\n\t\treturn this.each( function() {\n\t\t\tvar self = jQuery( this ),\n\t\t\t\tcontents = self.contents();\n\n\t\t\tif ( contents.length ) {\n\t\t\t\tcontents.wrapAll( html );\n\n\t\t\t} else {\n\t\t\t\tself.append( html );\n\t\t\t}\n\t\t} );\n\t},\n\n\twrap: function( html ) {\n\t\tvar htmlIsFunction = isFunction( html );\n\n\t\treturn this.each( function( i ) {\n\t\t\tjQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html );\n\t\t} );\n\t},\n\n\tunwrap: function( selector ) {\n\t\tthis.parent( selector ).not( \"body\" ).each( function() {\n\t\t\tjQuery( this ).replaceWith( this.childNodes );\n\t\t} );\n\t\treturn this;\n\t}\n} );\n\n\njQuery.expr.pseudos.hidden = function( elem ) {\n\treturn !jQuery.expr.pseudos.visible( elem );\n};\njQuery.expr.pseudos.visible = function( elem ) {\n\treturn !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );\n};\n\n\n\n\njQuery.ajaxSettings.xhr = function() {\n\ttry {\n\t\treturn new window.XMLHttpRequest();\n\t} catch ( e ) {}\n};\n\nvar xhrSuccessStatus = {\n\n\t\t// File protocol always yields status code 0, assume 200\n\t\t0: 200,\n\n\t\t// Support: IE <=9 only\n\t\t// #1450: sometimes IE returns 1223 when it should be 204\n\t\t1223: 204\n\t},\n\txhrSupported = jQuery.ajaxSettings.xhr();\n\nsupport.cors = !!xhrSupported && ( \"withCredentials\" in xhrSupported );\nsupport.ajax = xhrSupported = !!xhrSupported;\n\njQuery.ajaxTransport( function( options ) {\n\tvar callback, errorCallback;\n\n\t// Cross domain only allowed if supported through XMLHttpRequest\n\tif ( support.cors || xhrSupported && !options.crossDomain ) {\n\t\treturn {\n\t\t\tsend: function( headers, complete ) {\n\t\t\t\tvar i,\n\t\t\t\t\txhr = options.xhr();\n\n\t\t\t\txhr.open(\n\t\t\t\t\toptions.type,\n\t\t\t\t\toptions.url,\n\t\t\t\t\toptions.async,\n\t\t\t\t\toptions.username,\n\t\t\t\t\toptions.password\n\t\t\t\t);\n\n\t\t\t\t// Apply custom fields if provided\n\t\t\t\tif ( options.xhrFields ) {\n\t\t\t\t\tfor ( i in options.xhrFields ) {\n\t\t\t\t\t\txhr[ i ] = options.xhrFields[ i ];\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Override mime type if needed\n\t\t\t\tif ( options.mimeType && xhr.overrideMimeType ) {\n\t\t\t\t\txhr.overrideMimeType( options.mimeType );\n\t\t\t\t}\n\n\t\t\t\t// X-Requested-With header\n\t\t\t\t// For cross-domain requests, seeing as conditions for a preflight are\n\t\t\t\t// akin to a jigsaw puzzle, we simply never set it to be sure.\n\t\t\t\t// (it can always be set on a per-request basis or even using ajaxSetup)\n\t\t\t\t// For same-domain requests, won't change header if already provided.\n\t\t\t\tif ( !options.crossDomain && !headers[ \"X-Requested-With\" ] ) {\n\t\t\t\t\theaders[ \"X-Requested-With\" ] = \"XMLHttpRequest\";\n\t\t\t\t}\n\n\t\t\t\t// Set headers\n\t\t\t\tfor ( i in headers ) {\n\t\t\t\t\txhr.setRequestHeader( i, headers[ i ] );\n\t\t\t\t}\n\n\t\t\t\t// Callback\n\t\t\t\tcallback = function( type ) {\n\t\t\t\t\treturn function() {\n\t\t\t\t\t\tif ( callback ) {\n\t\t\t\t\t\t\tcallback = errorCallback = xhr.onload =\n\t\t\t\t\t\t\t\txhr.onerror = xhr.onabort = xhr.ontimeout =\n\t\t\t\t\t\t\t\t\txhr.onreadystatechange = null;\n\n\t\t\t\t\t\t\tif ( type === \"abort\" ) {\n\t\t\t\t\t\t\t\txhr.abort();\n\t\t\t\t\t\t\t} else if ( type === \"error\" ) {\n\n\t\t\t\t\t\t\t\t// Support: IE <=9 only\n\t\t\t\t\t\t\t\t// On a manual native abort, IE9 throws\n\t\t\t\t\t\t\t\t// errors on any property access that is not readyState\n\t\t\t\t\t\t\t\tif ( typeof xhr.status !== \"number\" ) {\n\t\t\t\t\t\t\t\t\tcomplete( 0, \"error\" );\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tcomplete(\n\n\t\t\t\t\t\t\t\t\t\t// File: protocol always yields status 0; see #8605, #14207\n\t\t\t\t\t\t\t\t\t\txhr.status,\n\t\t\t\t\t\t\t\t\t\txhr.statusText\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tcomplete(\n\t\t\t\t\t\t\t\t\txhrSuccessStatus[ xhr.status ] || xhr.status,\n\t\t\t\t\t\t\t\t\txhr.statusText,\n\n\t\t\t\t\t\t\t\t\t// Support: IE <=9 only\n\t\t\t\t\t\t\t\t\t// IE9 has no XHR2 but throws on binary (trac-11426)\n\t\t\t\t\t\t\t\t\t// For XHR2 non-text, let the caller handle it (gh-2498)\n\t\t\t\t\t\t\t\t\t( xhr.responseType || \"text\" ) !== \"text\" ||\n\t\t\t\t\t\t\t\t\ttypeof xhr.responseText !== \"string\" ?\n\t\t\t\t\t\t\t\t\t\t{ binary: xhr.response } :\n\t\t\t\t\t\t\t\t\t\t{ text: xhr.responseText },\n\t\t\t\t\t\t\t\t\txhr.getAllResponseHeaders()\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t};\n\n\t\t\t\t// Listen to events\n\t\t\t\txhr.onload = callback();\n\t\t\t\terrorCallback = xhr.onerror = xhr.ontimeout = callback( \"error\" );\n\n\t\t\t\t// Support: IE 9 only\n\t\t\t\t// Use onreadystatechange to replace onabort\n\t\t\t\t// to handle uncaught aborts\n\t\t\t\tif ( xhr.onabort !== undefined ) {\n\t\t\t\t\txhr.onabort = errorCallback;\n\t\t\t\t} else {\n\t\t\t\t\txhr.onreadystatechange = function() {\n\n\t\t\t\t\t\t// Check readyState before timeout as it changes\n\t\t\t\t\t\tif ( xhr.readyState === 4 ) {\n\n\t\t\t\t\t\t\t// Allow onerror to be called first,\n\t\t\t\t\t\t\t// but that will not handle a native abort\n\t\t\t\t\t\t\t// Also, save errorCallback to a variable\n\t\t\t\t\t\t\t// as xhr.onerror cannot be accessed\n\t\t\t\t\t\t\twindow.setTimeout( function() {\n\t\t\t\t\t\t\t\tif ( callback ) {\n\t\t\t\t\t\t\t\t\terrorCallback();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\t// Create the abort callback\n\t\t\t\tcallback = callback( \"abort\" );\n\n\t\t\t\ttry {\n\n\t\t\t\t\t// Do send the request (this may raise an exception)\n\t\t\t\t\txhr.send( options.hasContent && options.data || null );\n\t\t\t\t} catch ( e ) {\n\n\t\t\t\t\t// #14683: Only rethrow if this hasn't been notified as an error yet\n\t\t\t\t\tif ( callback ) {\n\t\t\t\t\t\tthrow e;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\n\t\t\tabort: function() {\n\t\t\t\tif ( callback ) {\n\t\t\t\t\tcallback();\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n} );\n\n\n\n\n// Prevent auto-execution of scripts when no explicit dataType was provided (See gh-2432)\njQuery.ajaxPrefilter( function( s ) {\n\tif ( s.crossDomain ) {\n\t\ts.contents.script = false;\n\t}\n} );\n\n// Install script dataType\njQuery.ajaxSetup( {\n\taccepts: {\n\t\tscript: \"text/javascript, application/javascript, \" +\n\t\t\t\"application/ecmascript, application/x-ecmascript\"\n\t},\n\tcontents: {\n\t\tscript: /\\b(?:java|ecma)script\\b/\n\t},\n\tconverters: {\n\t\t\"text script\": function( text ) {\n\t\t\tjQuery.globalEval( text );\n\t\t\treturn text;\n\t\t}\n\t}\n} );\n\n// Handle cache's special case and crossDomain\njQuery.ajaxPrefilter( \"script\", function( s ) {\n\tif ( s.cache === undefined ) {\n\t\ts.cache = false;\n\t}\n\tif ( s.crossDomain ) {\n\t\ts.type = \"GET\";\n\t}\n} );\n\n// Bind script tag hack transport\njQuery.ajaxTransport( \"script\", function( s ) {\n\n\t// This transport only deals with cross domain or forced-by-attrs requests\n\tif ( s.crossDomain || s.scriptAttrs ) {\n\t\tvar script, callback;\n\t\treturn {\n\t\t\tsend: function( _, complete ) {\n\t\t\t\tscript = jQuery( \"<script>\" )\n\t\t\t\t\t.attr( s.scriptAttrs || {} )\n\t\t\t\t\t.prop( { charset: s.scriptCharset, src: s.url } )\n\t\t\t\t\t.on( \"load error\", callback = function( evt ) {\n\t\t\t\t\t\tscript.remove();\n\t\t\t\t\t\tcallback = null;\n\t\t\t\t\t\tif ( evt ) {\n\t\t\t\t\t\t\tcomplete( evt.type === \"error\" ? 404 : 200, evt.type );\n\t\t\t\t\t\t}\n\t\t\t\t\t} );\n\n\t\t\t\t// Use native DOM manipulation to avoid our domManip AJAX trickery\n\t\t\t\tdocument.head.appendChild( script[ 0 ] );\n\t\t\t},\n\t\t\tabort: function() {\n\t\t\t\tif ( callback ) {\n\t\t\t\t\tcallback();\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n} );\n\n\n\n\nvar oldCallbacks = [],\n\trjsonp = /(=)\\?(?=&|$)|\\?\\?/;\n\n// Default jsonp settings\njQuery.ajaxSetup( {\n\tjsonp: \"callback\",\n\tjsonpCallback: function() {\n\t\tvar callback = oldCallbacks.pop() || ( jQuery.expando + \"_\" + ( nonce++ ) );\n\t\tthis[ callback ] = true;\n\t\treturn callback;\n\t}\n} );\n\n// Detect, normalize options and install callbacks for jsonp requests\njQuery.ajaxPrefilter( \"json jsonp\", function( s, originalSettings, jqXHR ) {\n\n\tvar callbackName, overwritten, responseContainer,\n\t\tjsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ?\n\t\t\t\"url\" :\n\t\t\ttypeof s.data === \"string\" &&\n\t\t\t\t( s.contentType || \"\" )\n\t\t\t\t\t.indexOf( \"application/x-www-form-urlencoded\" ) === 0 &&\n\t\t\t\trjsonp.test( s.data ) && \"data\"\n\t\t);\n\n\t// Handle iff the expected data type is \"jsonp\" or we have a parameter to set\n\tif ( jsonProp || s.dataTypes[ 0 ] === \"jsonp\" ) {\n\n\t\t// Get callback name, remembering preexisting value associated with it\n\t\tcallbackName = s.jsonpCallback = isFunction( s.jsonpCallback ) ?\n\t\t\ts.jsonpCallback() :\n\t\t\ts.jsonpCallback;\n\n\t\t// Insert callback into url or form data\n\t\tif ( jsonProp ) {\n\t\t\ts[ jsonProp ] = s[ jsonProp ].replace( rjsonp, \"$1\" + callbackName );\n\t\t} else if ( s.jsonp !== false ) {\n\t\t\ts.url += ( rquery.test( s.url ) ? \"&\" : \"?\" ) + s.jsonp + \"=\" + callbackName;\n\t\t}\n\n\t\t// Use data converter to retrieve json after script execution\n\t\ts.converters[ \"script json\" ] = function() {\n\t\t\tif ( !responseContainer ) {\n\t\t\t\tjQuery.error( callbackName + \" was not called\" );\n\t\t\t}\n\t\t\treturn responseContainer[ 0 ];\n\t\t};\n\n\t\t// Force json dataType\n\t\ts.dataTypes[ 0 ] = \"json\";\n\n\t\t// Install callback\n\t\toverwritten = window[ callbackName ];\n\t\twindow[ callbackName ] = function() {\n\t\t\tresponseContainer = arguments;\n\t\t};\n\n\t\t// Clean-up function (fires after converters)\n\t\tjqXHR.always( function() {\n\n\t\t\t// If previous value didn't exist - remove it\n\t\t\tif ( overwritten === undefined ) {\n\t\t\t\tjQuery( window ).removeProp( callbackName );\n\n\t\t\t// Otherwise restore preexisting value\n\t\t\t} else {\n\t\t\t\twindow[ callbackName ] = overwritten;\n\t\t\t}\n\n\t\t\t// Save back as free\n\t\t\tif ( s[ callbackName ] ) {\n\n\t\t\t\t// Make sure that re-using the options doesn't screw things around\n\t\t\t\ts.jsonpCallback = originalSettings.jsonpCallback;\n\n\t\t\t\t// Save the callback name for future use\n\t\t\t\toldCallbacks.push( callbackName );\n\t\t\t}\n\n\t\t\t// Call if it was a function and we have a response\n\t\t\tif ( responseContainer && isFunction( overwritten ) ) {\n\t\t\t\toverwritten( responseContainer[ 0 ] );\n\t\t\t}\n\n\t\t\tresponseContainer = overwritten = undefined;\n\t\t} );\n\n\t\t// Delegate to script\n\t\treturn \"script\";\n\t}\n} );\n\n\n\n\n// Support: Safari 8 only\n// In Safari 8 documents created via document.implementation.createHTMLDocument\n// collapse sibling forms: the second one becomes a child of the first one.\n// Because of that, this security measure has to be disabled in Safari 8.\n// https://bugs.webkit.org/show_bug.cgi?id=137337\nsupport.createHTMLDocument = ( function() {\n\tvar body = document.implementation.createHTMLDocument( \"\" ).body;\n\tbody.innerHTML = \"<form></form><form></form>\";\n\treturn body.childNodes.length === 2;\n} )();\n\n\n// Argument \"data\" should be string of html\n// context (optional): If specified, the fragment will be created in this context,\n// defaults to document\n// keepScripts (optional): If true, will include scripts passed in the html string\njQuery.parseHTML = function( data, context, keepScripts ) {\n\tif ( typeof data !== \"string\" ) {\n\t\treturn [];\n\t}\n\tif ( typeof context === \"boolean\" ) {\n\t\tkeepScripts = context;\n\t\tcontext = false;\n\t}\n\n\tvar base, parsed, scripts;\n\n\tif ( !context ) {\n\n\t\t// Stop scripts or inline event handlers from being executed immediately\n\t\t// by using document.implementation\n\t\tif ( support.createHTMLDocument ) {\n\t\t\tcontext = document.implementation.createHTMLDocument( \"\" );\n\n\t\t\t// Set the base href for the created document\n\t\t\t// so any parsed elements with URLs\n\t\t\t// are based on the document's URL (gh-2965)\n\t\t\tbase = context.createElement( \"base\" );\n\t\t\tbase.href = document.location.href;\n\t\t\tcontext.head.appendChild( base );\n\t\t} else {\n\t\t\tcontext = document;\n\t\t}\n\t}\n\n\tparsed = rsingleTag.exec( data );\n\tscripts = !keepScripts && [];\n\n\t// Single tag\n\tif ( parsed ) {\n\t\treturn [ context.createElement( parsed[ 1 ] ) ];\n\t}\n\n\tparsed = buildFragment( [ data ], context, scripts );\n\n\tif ( scripts && scripts.length ) {\n\t\tjQuery( scripts ).remove();\n\t}\n\n\treturn jQuery.merge( [], parsed.childNodes );\n};\n\n\n/**\n * Load a url into a page\n */\njQuery.fn.load = function( url, params, callback ) {\n\tvar selector, type, response,\n\t\tself = this,\n\t\toff = url.indexOf( \" \" );\n\n\tif ( off > -1 ) {\n\t\tselector = stripAndCollapse( url.slice( off ) );\n\t\turl = url.slice( 0, off );\n\t}\n\n\t// If it's a function\n\tif ( isFunction( params ) ) {\n\n\t\t// We assume that it's the callback\n\t\tcallback = params;\n\t\tparams = undefined;\n\n\t// Otherwise, build a param string\n\t} else if ( params && typeof params === \"object\" ) {\n\t\ttype = \"POST\";\n\t}\n\n\t// If we have elements to modify, make the request\n\tif ( self.length > 0 ) {\n\t\tjQuery.ajax( {\n\t\t\turl: url,\n\n\t\t\t// If \"type\" variable is undefined, then \"GET\" method will be used.\n\t\t\t// Make value of this field explicit since\n\t\t\t// user can override it through ajaxSetup method\n\t\t\ttype: type || \"GET\",\n\t\t\tdataType: \"html\",\n\t\t\tdata: params\n\t\t} ).done( function( responseText ) {\n\n\t\t\t// Save response for use in complete callback\n\t\t\tresponse = arguments;\n\n\t\t\tself.html( selector ?\n\n\t\t\t\t// If a selector was specified, locate the right elements in a dummy div\n\t\t\t\t// Exclude scripts to avoid IE 'Permission Denied' errors\n\t\t\t\tjQuery( \"<div>\" ).append( jQuery.parseHTML( responseText ) ).find( selector ) :\n\n\t\t\t\t// Otherwise use the full result\n\t\t\t\tresponseText );\n\n\t\t// If the request succeeds, this function gets \"data\", \"status\", \"jqXHR\"\n\t\t// but they are ignored because response was set above.\n\t\t// If it fails, this function gets \"jqXHR\", \"status\", \"error\"\n\t\t} ).always( callback && function( jqXHR, status ) {\n\t\t\tself.each( function() {\n\t\t\t\tcallback.apply( this, response || [ jqXHR.responseText, status, jqXHR ] );\n\t\t\t} );\n\t\t} );\n\t}\n\n\treturn this;\n};\n\n\n\n\n// Attach a bunch of functions for handling common AJAX events\njQuery.each( [\n\t\"ajaxStart\",\n\t\"ajaxStop\",\n\t\"ajaxComplete\",\n\t\"ajaxError\",\n\t\"ajaxSuccess\",\n\t\"ajaxSend\"\n], function( i, type ) {\n\tjQuery.fn[ type ] = function( fn ) {\n\t\treturn this.on( type, fn );\n\t};\n} );\n\n\n\n\njQuery.expr.pseudos.animated = function( elem ) {\n\treturn jQuery.grep( jQuery.timers, function( fn ) {\n\t\treturn elem === fn.elem;\n\t} ).length;\n};\n\n\n\n\njQuery.offset = {\n\tsetOffset: function( elem, options, i ) {\n\t\tvar curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition,\n\t\t\tposition = jQuery.css( elem, \"position\" ),\n\t\t\tcurElem = jQuery( elem ),\n\t\t\tprops = {};\n\n\t\t// Set position first, in-case top/left are set even on static elem\n\t\tif ( position === \"static\" ) {\n\t\t\telem.style.position = \"relative\";\n\t\t}\n\n\t\tcurOffset = curElem.offset();\n\t\tcurCSSTop = jQuery.css( elem, \"top\" );\n\t\tcurCSSLeft = jQuery.css( elem, \"left\" );\n\t\tcalculatePosition = ( position === \"absolute\" || position === \"fixed\" ) &&\n\t\t\t( curCSSTop + curCSSLeft ).indexOf( \"auto\" ) > -1;\n\n\t\t// Need to be able to calculate position if either\n\t\t// top or left is auto and position is either absolute or fixed\n\t\tif ( calculatePosition ) {\n\t\t\tcurPosition = curElem.position();\n\t\t\tcurTop = curPosition.top;\n\t\t\tcurLeft = curPosition.left;\n\n\t\t} else {\n\t\t\tcurTop = parseFloat( curCSSTop ) || 0;\n\t\t\tcurLeft = parseFloat( curCSSLeft ) || 0;\n\t\t}\n\n\t\tif ( isFunction( options ) ) {\n\n\t\t\t// Use jQuery.extend here to allow modification of coordinates argument (gh-1848)\n\t\t\toptions = options.call( elem, i, jQuery.extend( {}, curOffset ) );\n\t\t}\n\n\t\tif ( options.top != null ) {\n\t\t\tprops.top = ( options.top - curOffset.top ) + curTop;\n\t\t}\n\t\tif ( options.left != null ) {\n\t\t\tprops.left = ( options.left - curOffset.left ) + curLeft;\n\t\t}\n\n\t\tif ( \"using\" in options ) {\n\t\t\toptions.using.call( elem, props );\n\n\t\t} else {\n\t\t\tcurElem.css( props );\n\t\t}\n\t}\n};\n\njQuery.fn.extend( {\n\n\t// offset() relates an element's border box to the document origin\n\toffset: function( options ) {\n\n\t\t// Preserve chaining for setter\n\t\tif ( arguments.length ) {\n\t\t\treturn options === undefined ?\n\t\t\t\tthis :\n\t\t\t\tthis.each( function( i ) {\n\t\t\t\t\tjQuery.offset.setOffset( this, options, i );\n\t\t\t\t} );\n\t\t}\n\n\t\tvar rect, win,\n\t\t\telem = this[ 0 ];\n\n\t\tif ( !elem ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Return zeros for disconnected and hidden (display: none) elements (gh-2310)\n\t\t// Support: IE <=11 only\n\t\t// Running getBoundingClientRect on a\n\t\t// disconnected node in IE throws an error\n\t\tif ( !elem.getClientRects().length ) {\n\t\t\treturn { top: 0, left: 0 };\n\t\t}\n\n\t\t// Get document-relative position by adding viewport scroll to viewport-relative gBCR\n\t\trect = elem.getBoundingClientRect();\n\t\twin = elem.ownerDocument.defaultView;\n\t\treturn {\n\t\t\ttop: rect.top + win.pageYOffset,\n\t\t\tleft: rect.left + win.pageXOffset\n\t\t};\n\t},\n\n\t// position() relates an element's margin box to its offset parent's padding box\n\t// This corresponds to the behavior of CSS absolute positioning\n\tposition: function() {\n\t\tif ( !this[ 0 ] ) {\n\t\t\treturn;\n\t\t}\n\n\t\tvar offsetParent, offset, doc,\n\t\t\telem = this[ 0 ],\n\t\t\tparentOffset = { top: 0, left: 0 };\n\n\t\t// position:fixed elements are offset from the viewport, which itself always has zero offset\n\t\tif ( jQuery.css( elem, \"position\" ) === \"fixed\" ) {\n\n\t\t\t// Assume position:fixed implies availability of getBoundingClientRect\n\t\t\toffset = elem.getBoundingClientRect();\n\n\t\t} else {\n\t\t\toffset = this.offset();\n\n\t\t\t// Account for the *real* offset parent, which can be the document or its root element\n\t\t\t// when a statically positioned element is identified\n\t\t\tdoc = elem.ownerDocument;\n\t\t\toffsetParent = elem.offsetParent || doc.documentElement;\n\t\t\twhile ( offsetParent &&\n\t\t\t\t( offsetParent === doc.body || offsetParent === doc.documentElement ) &&\n\t\t\t\tjQuery.css( offsetParent, \"position\" ) === \"static\" ) {\n\n\t\t\t\toffsetParent = offsetParent.parentNode;\n\t\t\t}\n\t\t\tif ( offsetParent && offsetParent !== elem && offsetParent.nodeType === 1 ) {\n\n\t\t\t\t// Incorporate borders into its offset, since they are outside its content origin\n\t\t\t\tparentOffset = jQuery( offsetParent ).offset();\n\t\t\t\tparentOffset.top += jQuery.css( offsetParent, \"borderTopWidth\", true );\n\t\t\t\tparentOffset.left += jQuery.css( offsetParent, \"borderLeftWidth\", true );\n\t\t\t}\n\t\t}\n\n\t\t// Subtract parent offsets and element margins\n\t\treturn {\n\t\t\ttop: offset.top - parentOffset.top - jQuery.css( elem, \"marginTop\", true ),\n\t\t\tleft: offset.left - parentOffset.left - jQuery.css( elem, \"marginLeft\", true )\n\t\t};\n\t},\n\n\t// This method will return documentElement in the following cases:\n\t// 1) For the element inside the iframe without offsetParent, this method will return\n\t// documentElement of the parent window\n\t// 2) For the hidden or detached element\n\t// 3) For body or html element, i.e. in case of the html node - it will return itself\n\t//\n\t// but those exceptions were never presented as a real life use-cases\n\t// and might be considered as more preferable results.\n\t//\n\t// This logic, however, is not guaranteed and can change at any point in the future\n\toffsetParent: function() {\n\t\treturn this.map( function() {\n\t\t\tvar offsetParent = this.offsetParent;\n\n\t\t\twhile ( offsetParent && jQuery.css( offsetParent, \"position\" ) === \"static\" ) {\n\t\t\t\toffsetParent = offsetParent.offsetParent;\n\t\t\t}\n\n\t\t\treturn offsetParent || documentElement;\n\t\t} );\n\t}\n} );\n\n// Create scrollLeft and scrollTop methods\njQuery.each( { scrollLeft: \"pageXOffset\", scrollTop: \"pageYOffset\" }, function( method, prop ) {\n\tvar top = \"pageYOffset\" === prop;\n\n\tjQuery.fn[ method ] = function( val ) {\n\t\treturn access( this, function( elem, method, val ) {\n\n\t\t\t// Coalesce documents and windows\n\t\t\tvar win;\n\t\t\tif ( isWindow( elem ) ) {\n\t\t\t\twin = elem;\n\t\t\t} else if ( elem.nodeType === 9 ) {\n\t\t\t\twin = elem.defaultView;\n\t\t\t}\n\n\t\t\tif ( val === undefined ) {\n\t\t\t\treturn win ? win[ prop ] : elem[ method ];\n\t\t\t}\n\n\t\t\tif ( win ) {\n\t\t\t\twin.scrollTo(\n\t\t\t\t\t!top ? val : win.pageXOffset,\n\t\t\t\t\ttop ? val : win.pageYOffset\n\t\t\t\t);\n\n\t\t\t} else {\n\t\t\t\telem[ method ] = val;\n\t\t\t}\n\t\t}, method, val, arguments.length );\n\t};\n} );\n\n// Support: Safari <=7 - 9.1, Chrome <=37 - 49\n// Add the top/left cssHooks using jQuery.fn.position\n// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084\n// Blink bug: https://bugs.chromium.org/p/chromium/issues/detail?id=589347\n// getComputedStyle returns percent when specified for top/left/bottom/right;\n// rather than make the css module depend on the offset module, just check for it here\njQuery.each( [ \"top\", \"left\" ], function( i, prop ) {\n\tjQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition,\n\t\tfunction( elem, computed ) {\n\t\t\tif ( computed ) {\n\t\t\t\tcomputed = curCSS( elem, prop );\n\n\t\t\t\t// If curCSS returns percentage, fallback to offset\n\t\t\t\treturn rnumnonpx.test( computed ) ?\n\t\t\t\t\tjQuery( elem ).position()[ prop ] + \"px\" :\n\t\t\t\t\tcomputed;\n\t\t\t}\n\t\t}\n\t);\n} );\n\n\n// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods\njQuery.each( { Height: \"height\", Width: \"width\" }, function( name, type ) {\n\tjQuery.each( { padding: \"inner\" + name, content: type, \"\": \"outer\" + name },\n\t\tfunction( defaultExtra, funcName ) {\n\n\t\t// Margin is only for outerHeight, outerWidth\n\t\tjQuery.fn[ funcName ] = function( margin, value ) {\n\t\t\tvar chainable = arguments.length && ( defaultExtra || typeof margin !== \"boolean\" ),\n\t\t\t\textra = defaultExtra || ( margin === true || value === true ? \"margin\" : \"border\" );\n\n\t\t\treturn access( this, function( elem, type, value ) {\n\t\t\t\tvar doc;\n\n\t\t\t\tif ( isWindow( elem ) ) {\n\n\t\t\t\t\t// $( window ).outerWidth/Height return w/h including scrollbars (gh-1729)\n\t\t\t\t\treturn funcName.indexOf( \"outer\" ) === 0 ?\n\t\t\t\t\t\telem[ \"inner\" + name ] :\n\t\t\t\t\t\telem.document.documentElement[ \"client\" + name ];\n\t\t\t\t}\n\n\t\t\t\t// Get document width or height\n\t\t\t\tif ( elem.nodeType === 9 ) {\n\t\t\t\t\tdoc = elem.documentElement;\n\n\t\t\t\t\t// Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height],\n\t\t\t\t\t// whichever is greatest\n\t\t\t\t\treturn Math.max(\n\t\t\t\t\t\telem.body[ \"scroll\" + name ], doc[ \"scroll\" + name ],\n\t\t\t\t\t\telem.body[ \"offset\" + name ], doc[ \"offset\" + name ],\n\t\t\t\t\t\tdoc[ \"client\" + name ]\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\treturn value === undefined ?\n\n\t\t\t\t\t// Get width or height on the element, requesting but not forcing parseFloat\n\t\t\t\t\tjQuery.css( elem, type, extra ) :\n\n\t\t\t\t\t// Set width or height on the element\n\t\t\t\t\tjQuery.style( elem, type, value, extra );\n\t\t\t}, type, chainable ? margin : undefined, chainable );\n\t\t};\n\t} );\n} );\n\n\njQuery.each( ( \"blur focus focusin focusout resize scroll click dblclick \" +\n\t\"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave \" +\n\t\"change select submit keydown keypress keyup contextmenu\" ).split( \" \" ),\n\tfunction( i, name ) {\n\n\t// Handle event binding\n\tjQuery.fn[ name ] = function( data, fn ) {\n\t\treturn arguments.length > 0 ?\n\t\t\tthis.on( name, null, data, fn ) :\n\t\t\tthis.trigger( name );\n\t};\n} );\n\njQuery.fn.extend( {\n\thover: function( fnOver, fnOut ) {\n\t\treturn this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );\n\t}\n} );\n\n\n\n\njQuery.fn.extend( {\n\n\tbind: function( types, data, fn ) {\n\t\treturn this.on( types, null, data, fn );\n\t},\n\tunbind: function( types, fn ) {\n\t\treturn this.off( types, null, fn );\n\t},\n\n\tdelegate: function( selector, types, data, fn ) {\n\t\treturn this.on( types, selector, data, fn );\n\t},\n\tundelegate: function( selector, types, fn ) {\n\n\t\t// ( namespace ) or ( selector, types [, fn] )\n\t\treturn arguments.length === 1 ?\n\t\t\tthis.off( selector, \"**\" ) :\n\t\t\tthis.off( types, selector || \"**\", fn );\n\t}\n} );\n\n// Bind a function to a context, optionally partially applying any\n// arguments.\n// jQuery.proxy is deprecated to promote standards (specifically Function#bind)\n// However, it is not slated for removal any time soon\njQuery.proxy = function( fn, context ) {\n\tvar tmp, args, proxy;\n\n\tif ( typeof context === \"string\" ) {\n\t\ttmp = fn[ context ];\n\t\tcontext = fn;\n\t\tfn = tmp;\n\t}\n\n\t// Quick check to determine if target is callable, in the spec\n\t// this throws a TypeError, but we will just return undefined.\n\tif ( !isFunction( fn ) ) {\n\t\treturn undefined;\n\t}\n\n\t// Simulated bind\n\targs = slice.call( arguments, 2 );\n\tproxy = function() {\n\t\treturn fn.apply( context || this, args.concat( slice.call( arguments ) ) );\n\t};\n\n\t// Set the guid of unique handler to the same of original handler, so it can be removed\n\tproxy.guid = fn.guid = fn.guid || jQuery.guid++;\n\n\treturn proxy;\n};\n\njQuery.holdReady = function( hold ) {\n\tif ( hold ) {\n\t\tjQuery.readyWait++;\n\t} else {\n\t\tjQuery.ready( true );\n\t}\n};\njQuery.isArray = Array.isArray;\njQuery.parseJSON = JSON.parse;\njQuery.nodeName = nodeName;\njQuery.isFunction = isFunction;\njQuery.isWindow = isWindow;\njQuery.camelCase = camelCase;\njQuery.type = toType;\n\njQuery.now = Date.now;\n\njQuery.isNumeric = function( obj ) {\n\n\t// As of jQuery 3.0, isNumeric is limited to\n\t// strings and numbers (primitives or objects)\n\t// that can be coerced to finite numbers (gh-2662)\n\tvar type = jQuery.type( obj );\n\treturn ( type === \"number\" || type === \"string\" ) &&\n\n\t\t// parseFloat NaNs numeric-cast false positives (\"\")\n\t\t// ...but misinterprets leading-number strings, particularly hex literals (\"0x...\")\n\t\t// subtraction forces infinities to NaN\n\t\t!isNaN( obj - parseFloat( obj ) );\n};\n\n\n\n\n// Register as a named AMD module, since jQuery can be concatenated with other\n// files that may use define, but not via a proper concatenation script that\n// understands anonymous AMD modules. A named AMD is safest and most robust\n// way to register. Lowercase jquery is used because AMD module names are\n// derived from file names, and jQuery is normally delivered in a lowercase\n// file name. Do this after creating the global so that if an AMD module wants\n// to call noConflict to hide this version of jQuery, it will work.\n\n// Note that for maximum portability, libraries that are not jQuery should\n// declare themselves as anonymous modules, and avoid setting a global if an\n// AMD loader is present. jQuery is a special case. For more information, see\n// https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon\n\nif ( typeof define === \"function\" && define.amd ) {\n\tdefine( \"jquery\", [], function() {\n\t\treturn jQuery;\n\t} );\n}\n\n\n\n\nvar\n\n\t// Map over jQuery in case of overwrite\n\t_jQuery = window.jQuery,\n\n\t// Map over the $ in case of overwrite\n\t_$ = window.$;\n\njQuery.noConflict = function( deep ) {\n\tif ( window.$ === jQuery ) {\n\t\twindow.$ = _$;\n\t}\n\n\tif ( deep && window.jQuery === jQuery ) {\n\t\twindow.jQuery = _jQuery;\n\t}\n\n\treturn jQuery;\n};\n\n// Expose jQuery and $ identifiers, even in AMD\n// (#7102#comment:10, https://github.com/jquery/jquery/pull/557)\n// and CommonJS for browser emulators (#13566)\nif ( !noGlobal ) {\n\twindow.jQuery = window.$ = jQuery;\n}\n\n\n\n\nreturn jQuery;\n} );","/**!\n * @fileOverview Kickass library to create and place poppers near their reference elements.\n * @version 1.15.0\n * @license\n * Copyright (c) 2016 Federico Zivolo and contributors\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n(function (global, factory) {\n\ttypeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n\ttypeof define === 'function' && define.amd ? define(factory) :\n\t(global.Popper = factory());\n}(this, (function () { 'use strict';\n\nvar isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';\n\nvar longerTimeoutBrowsers = ['Edge', 'Trident', 'Firefox'];\nvar timeoutDuration = 0;\nfor (var i = 0; i < longerTimeoutBrowsers.length; i += 1) {\n if (isBrowser && navigator.userAgent.indexOf(longerTimeoutBrowsers[i]) >= 0) {\n timeoutDuration = 1;\n break;\n }\n}\n\nfunction microtaskDebounce(fn) {\n var called = false;\n return function () {\n if (called) {\n return;\n }\n called = true;\n window.Promise.resolve().then(function () {\n called = false;\n fn();\n });\n };\n}\n\nfunction taskDebounce(fn) {\n var scheduled = false;\n return function () {\n if (!scheduled) {\n scheduled = true;\n setTimeout(function () {\n scheduled = false;\n fn();\n }, timeoutDuration);\n }\n };\n}\n\nvar supportsMicroTasks = isBrowser && window.Promise;\n\n/**\n* Create a debounced version of a method, that's asynchronously deferred\n* but called in the minimum time possible.\n*\n* @method\n* @memberof Popper.Utils\n* @argument {Function} fn\n* @returns {Function}\n*/\nvar debounce = supportsMicroTasks ? microtaskDebounce : taskDebounce;\n\n/**\n * Check if the given variable is a function\n * @method\n * @memberof Popper.Utils\n * @argument {Any} functionToCheck - variable to check\n * @returns {Boolean} answer to: is a function?\n */\nfunction isFunction(functionToCheck) {\n var getType = {};\n return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';\n}\n\n/**\n * Get CSS computed property of the given element\n * @method\n * @memberof Popper.Utils\n * @argument {Eement} element\n * @argument {String} property\n */\nfunction getStyleComputedProperty(element, property) {\n if (element.nodeType !== 1) {\n return [];\n }\n // NOTE: 1 DOM access here\n var window = element.ownerDocument.defaultView;\n var css = window.getComputedStyle(element, null);\n return property ? css[property] : css;\n}\n\n/**\n * Returns the parentNode or the host of the element\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @returns {Element} parent\n */\nfunction getParentNode(element) {\n if (element.nodeName === 'HTML') {\n return element;\n }\n return element.parentNode || element.host;\n}\n\n/**\n * Returns the scrolling parent of the given element\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @returns {Element} scroll parent\n */\nfunction getScrollParent(element) {\n // Return body, `getScroll` will take care to get the correct `scrollTop` from it\n if (!element) {\n return document.body;\n }\n\n switch (element.nodeName) {\n case 'HTML':\n case 'BODY':\n return element.ownerDocument.body;\n case '#document':\n return element.body;\n }\n\n // Firefox want us to check `-x` and `-y` variations as well\n\n var _getStyleComputedProp = getStyleComputedProperty(element),\n overflow = _getStyleComputedProp.overflow,\n overflowX = _getStyleComputedProp.overflowX,\n overflowY = _getStyleComputedProp.overflowY;\n\n if (/(auto|scroll|overlay)/.test(overflow + overflowY + overflowX)) {\n return element;\n }\n\n return getScrollParent(getParentNode(element));\n}\n\nvar isIE11 = isBrowser && !!(window.MSInputMethodContext && document.documentMode);\nvar isIE10 = isBrowser && /MSIE 10/.test(navigator.userAgent);\n\n/**\n * Determines if the browser is Internet Explorer\n * @method\n * @memberof Popper.Utils\n * @param {Number} version to check\n * @returns {Boolean} isIE\n */\nfunction isIE(version) {\n if (version === 11) {\n return isIE11;\n }\n if (version === 10) {\n return isIE10;\n }\n return isIE11 || isIE10;\n}\n\n/**\n * Returns the offset parent of the given element\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @returns {Element} offset parent\n */\nfunction getOffsetParent(element) {\n if (!element) {\n return document.documentElement;\n }\n\n var noOffsetParent = isIE(10) ? document.body : null;\n\n // NOTE: 1 DOM access here\n var offsetParent = element.offsetParent || null;\n // Skip hidden elements which don't have an offsetParent\n while (offsetParent === noOffsetParent && element.nextElementSibling) {\n offsetParent = (element = element.nextElementSibling).offsetParent;\n }\n\n var nodeName = offsetParent && offsetParent.nodeName;\n\n if (!nodeName || nodeName === 'BODY' || nodeName === 'HTML') {\n return element ? element.ownerDocument.documentElement : document.documentElement;\n }\n\n // .offsetParent will return the closest TH, TD or TABLE in case\n // no offsetParent is present, I hate this job...\n if (['TH', 'TD', 'TABLE'].indexOf(offsetParent.nodeName) !== -1 && getStyleComputedProperty(offsetParent, 'position') === 'static') {\n return getOffsetParent(offsetParent);\n }\n\n return offsetParent;\n}\n\nfunction isOffsetContainer(element) {\n var nodeName = element.nodeName;\n\n if (nodeName === 'BODY') {\n return false;\n }\n return nodeName === 'HTML' || getOffsetParent(element.firstElementChild) === element;\n}\n\n/**\n * Finds the root node (document, shadowDOM root) of the given element\n * @method\n * @memberof Popper.Utils\n * @argument {Element} node\n * @returns {Element} root node\n */\nfunction getRoot(node) {\n if (node.parentNode !== null) {\n return getRoot(node.parentNode);\n }\n\n return node;\n}\n\n/**\n * Finds the offset parent common to the two provided nodes\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element1\n * @argument {Element} element2\n * @returns {Element} common offset parent\n */\nfunction findCommonOffsetParent(element1, element2) {\n // This check is needed to avoid errors in case one of the elements isn't defined for any reason\n if (!element1 || !element1.nodeType || !element2 || !element2.nodeType) {\n return document.documentElement;\n }\n\n // Here we make sure to give as \"start\" the element that comes first in the DOM\n var order = element1.compareDocumentPosition(element2) & Node.DOCUMENT_POSITION_FOLLOWING;\n var start = order ? element1 : element2;\n var end = order ? element2 : element1;\n\n // Get common ancestor container\n var range = document.createRange();\n range.setStart(start, 0);\n range.setEnd(end, 0);\n var commonAncestorContainer = range.commonAncestorContainer;\n\n // Both nodes are inside #document\n\n if (element1 !== commonAncestorContainer && element2 !== commonAncestorContainer || start.contains(end)) {\n if (isOffsetContainer(commonAncestorContainer)) {\n return commonAncestorContainer;\n }\n\n return getOffsetParent(commonAncestorContainer);\n }\n\n // one of the nodes is inside shadowDOM, find which one\n var element1root = getRoot(element1);\n if (element1root.host) {\n return findCommonOffsetParent(element1root.host, element2);\n } else {\n return findCommonOffsetParent(element1, getRoot(element2).host);\n }\n}\n\n/**\n * Gets the scroll value of the given element in the given side (top and left)\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @argument {String} side `top` or `left`\n * @returns {number} amount of scrolled pixels\n */\nfunction getScroll(element) {\n var side = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'top';\n\n var upperSide = side === 'top' ? 'scrollTop' : 'scrollLeft';\n var nodeName = element.nodeName;\n\n if (nodeName === 'BODY' || nodeName === 'HTML') {\n var html = element.ownerDocument.documentElement;\n var scrollingElement = element.ownerDocument.scrollingElement || html;\n return scrollingElement[upperSide];\n }\n\n return element[upperSide];\n}\n\n/*\n * Sum or subtract the element scroll values (left and top) from a given rect object\n * @method\n * @memberof Popper.Utils\n * @param {Object} rect - Rect object you want to change\n * @param {HTMLElement} element - The element from the function reads the scroll values\n * @param {Boolean} subtract - set to true if you want to subtract the scroll values\n * @return {Object} rect - The modifier rect object\n */\nfunction includeScroll(rect, element) {\n var subtract = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;\n\n var scrollTop = getScroll(element, 'top');\n var scrollLeft = getScroll(element, 'left');\n var modifier = subtract ? -1 : 1;\n rect.top += scrollTop * modifier;\n rect.bottom += scrollTop * modifier;\n rect.left += scrollLeft * modifier;\n rect.right += scrollLeft * modifier;\n return rect;\n}\n\n/*\n * Helper to detect borders of a given element\n * @method\n * @memberof Popper.Utils\n * @param {CSSStyleDeclaration} styles\n * Result of `getStyleComputedProperty` on the given element\n * @param {String} axis - `x` or `y`\n * @return {number} borders - The borders size of the given axis\n */\n\nfunction getBordersSize(styles, axis) {\n var sideA = axis === 'x' ? 'Left' : 'Top';\n var sideB = sideA === 'Left' ? 'Right' : 'Bottom';\n\n return parseFloat(styles['border' + sideA + 'Width'], 10) + parseFloat(styles['border' + sideB + 'Width'], 10);\n}\n\nfunction getSize(axis, body, html, computedStyle) {\n return Math.max(body['offset' + axis], body['scroll' + axis], html['client' + axis], html['offset' + axis], html['scroll' + axis], isIE(10) ? parseInt(html['offset' + axis]) + parseInt(computedStyle['margin' + (axis === 'Height' ? 'Top' : 'Left')]) + parseInt(computedStyle['margin' + (axis === 'Height' ? 'Bottom' : 'Right')]) : 0);\n}\n\nfunction getWindowSizes(document) {\n var body = document.body;\n var html = document.documentElement;\n var computedStyle = isIE(10) && getComputedStyle(html);\n\n return {\n height: getSize('Height', body, html, computedStyle),\n width: getSize('Width', body, html, computedStyle)\n };\n}\n\nvar classCallCheck = function (instance, Constructor) {\n if (!(instance instanceof Constructor)) {\n throw new TypeError(\"Cannot call a class as a function\");\n }\n};\n\nvar createClass = function () {\n function defineProperties(target, props) {\n for (var i = 0; i < props.length; i++) {\n var descriptor = props[i];\n descriptor.enumerable = descriptor.enumerable || false;\n descriptor.configurable = true;\n if (\"value\" in descriptor) descriptor.writable = true;\n Object.defineProperty(target, descriptor.key, descriptor);\n }\n }\n\n return function (Constructor, protoProps, staticProps) {\n if (protoProps) defineProperties(Constructor.prototype, protoProps);\n if (staticProps) defineProperties(Constructor, staticProps);\n return Constructor;\n };\n}();\n\n\n\n\n\nvar defineProperty = function (obj, key, value) {\n if (key in obj) {\n Object.defineProperty(obj, key, {\n value: value,\n enumerable: true,\n configurable: true,\n writable: true\n });\n } else {\n obj[key] = value;\n }\n\n return obj;\n};\n\nvar _extends = Object.assign || function (target) {\n for (var i = 1; i < arguments.length; i++) {\n var source = arguments[i];\n\n for (var key in source) {\n if (Object.prototype.hasOwnProperty.call(source, key)) {\n target[key] = source[key];\n }\n }\n }\n\n return target;\n};\n\n/**\n * Given element offsets, generate an output similar to getBoundingClientRect\n * @method\n * @memberof Popper.Utils\n * @argument {Object} offsets\n * @returns {Object} ClientRect like output\n */\nfunction getClientRect(offsets) {\n return _extends({}, offsets, {\n right: offsets.left + offsets.width,\n bottom: offsets.top + offsets.height\n });\n}\n\n/**\n * Get bounding client rect of given element\n * @method\n * @memberof Popper.Utils\n * @param {HTMLElement} element\n * @return {Object} client rect\n */\nfunction getBoundingClientRect(element) {\n var rect = {};\n\n // IE10 10 FIX: Please, don't ask, the element isn't\n // considered in DOM in some circumstances...\n // This isn't reproducible in IE10 compatibility mode of IE11\n try {\n if (isIE(10)) {\n rect = element.getBoundingClientRect();\n var scrollTop = getScroll(element, 'top');\n var scrollLeft = getScroll(element, 'left');\n rect.top += scrollTop;\n rect.left += scrollLeft;\n rect.bottom += scrollTop;\n rect.right += scrollLeft;\n } else {\n rect = element.getBoundingClientRect();\n }\n } catch (e) {}\n\n var result = {\n left: rect.left,\n top: rect.top,\n width: rect.right - rect.left,\n height: rect.bottom - rect.top\n };\n\n // subtract scrollbar size from sizes\n var sizes = element.nodeName === 'HTML' ? getWindowSizes(element.ownerDocument) : {};\n var width = sizes.width || element.clientWidth || result.right - result.left;\n var height = sizes.height || element.clientHeight || result.bottom - result.top;\n\n var horizScrollbar = element.offsetWidth - width;\n var vertScrollbar = element.offsetHeight - height;\n\n // if an hypothetical scrollbar is detected, we must be sure it's not a `border`\n // we make this check conditional for performance reasons\n if (horizScrollbar || vertScrollbar) {\n var styles = getStyleComputedProperty(element);\n horizScrollbar -= getBordersSize(styles, 'x');\n vertScrollbar -= getBordersSize(styles, 'y');\n\n result.width -= horizScrollbar;\n result.height -= vertScrollbar;\n }\n\n return getClientRect(result);\n}\n\nfunction getOffsetRectRelativeToArbitraryNode(children, parent) {\n var fixedPosition = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;\n\n var isIE10 = isIE(10);\n var isHTML = parent.nodeName === 'HTML';\n var childrenRect = getBoundingClientRect(children);\n var parentRect = getBoundingClientRect(parent);\n var scrollParent = getScrollParent(children);\n\n var styles = getStyleComputedProperty(parent);\n var borderTopWidth = parseFloat(styles.borderTopWidth, 10);\n var borderLeftWidth = parseFloat(styles.borderLeftWidth, 10);\n\n // In cases where the parent is fixed, we must ignore negative scroll in offset calc\n if (fixedPosition && isHTML) {\n parentRect.top = Math.max(parentRect.top, 0);\n parentRect.left = Math.max(parentRect.left, 0);\n }\n var offsets = getClientRect({\n top: childrenRect.top - parentRect.top - borderTopWidth,\n left: childrenRect.left - parentRect.left - borderLeftWidth,\n width: childrenRect.width,\n height: childrenRect.height\n });\n offsets.marginTop = 0;\n offsets.marginLeft = 0;\n\n // Subtract margins of documentElement in case it's being used as parent\n // we do this only on HTML because it's the only element that behaves\n // differently when margins are applied to it. The margins are included in\n // the box of the documentElement, in the other cases not.\n if (!isIE10 && isHTML) {\n var marginTop = parseFloat(styles.marginTop, 10);\n var marginLeft = parseFloat(styles.marginLeft, 10);\n\n offsets.top -= borderTopWidth - marginTop;\n offsets.bottom -= borderTopWidth - marginTop;\n offsets.left -= borderLeftWidth - marginLeft;\n offsets.right -= borderLeftWidth - marginLeft;\n\n // Attach marginTop and marginLeft because in some circumstances we may need them\n offsets.marginTop = marginTop;\n offsets.marginLeft = marginLeft;\n }\n\n if (isIE10 && !fixedPosition ? parent.contains(scrollParent) : parent === scrollParent && scrollParent.nodeName !== 'BODY') {\n offsets = includeScroll(offsets, parent);\n }\n\n return offsets;\n}\n\nfunction getViewportOffsetRectRelativeToArtbitraryNode(element) {\n var excludeScroll = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;\n\n var html = element.ownerDocument.documentElement;\n var relativeOffset = getOffsetRectRelativeToArbitraryNode(element, html);\n var width = Math.max(html.clientWidth, window.innerWidth || 0);\n var height = Math.max(html.clientHeight, window.innerHeight || 0);\n\n var scrollTop = !excludeScroll ? getScroll(html) : 0;\n var scrollLeft = !excludeScroll ? getScroll(html, 'left') : 0;\n\n var offset = {\n top: scrollTop - relativeOffset.top + relativeOffset.marginTop,\n left: scrollLeft - relativeOffset.left + relativeOffset.marginLeft,\n width: width,\n height: height\n };\n\n return getClientRect(offset);\n}\n\n/**\n * Check if the given element is fixed or is inside a fixed parent\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @argument {Element} customContainer\n * @returns {Boolean} answer to \"isFixed?\"\n */\nfunction isFixed(element) {\n var nodeName = element.nodeName;\n if (nodeName === 'BODY' || nodeName === 'HTML') {\n return false;\n }\n if (getStyleComputedProperty(element, 'position') === 'fixed') {\n return true;\n }\n var parentNode = getParentNode(element);\n if (!parentNode) {\n return false;\n }\n return isFixed(parentNode);\n}\n\n/**\n * Finds the first parent of an element that has a transformed property defined\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @returns {Element} first transformed parent or documentElement\n */\n\nfunction getFixedPositionOffsetParent(element) {\n // This check is needed to avoid errors in case one of the elements isn't defined for any reason\n if (!element || !element.parentElement || isIE()) {\n return document.documentElement;\n }\n var el = element.parentElement;\n while (el && getStyleComputedProperty(el, 'transform') === 'none') {\n el = el.parentElement;\n }\n return el || document.documentElement;\n}\n\n/**\n * Computed the boundaries limits and return them\n * @method\n * @memberof Popper.Utils\n * @param {HTMLElement} popper\n * @param {HTMLElement} reference\n * @param {number} padding\n * @param {HTMLElement} boundariesElement - Element used to define the boundaries\n * @param {Boolean} fixedPosition - Is in fixed position mode\n * @returns {Object} Coordinates of the boundaries\n */\nfunction getBoundaries(popper, reference, padding, boundariesElement) {\n var fixedPosition = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;\n\n // NOTE: 1 DOM access here\n\n var boundaries = { top: 0, left: 0 };\n var offsetParent = fixedPosition ? getFixedPositionOffsetParent(popper) : findCommonOffsetParent(popper, reference);\n\n // Handle viewport case\n if (boundariesElement === 'viewport') {\n boundaries = getViewportOffsetRectRelativeToArtbitraryNode(offsetParent, fixedPosition);\n } else {\n // Handle other cases based on DOM element used as boundaries\n var boundariesNode = void 0;\n if (boundariesElement === 'scrollParent') {\n boundariesNode = getScrollParent(getParentNode(reference));\n if (boundariesNode.nodeName === 'BODY') {\n boundariesNode = popper.ownerDocument.documentElement;\n }\n } else if (boundariesElement === 'window') {\n boundariesNode = popper.ownerDocument.documentElement;\n } else {\n boundariesNode = boundariesElement;\n }\n\n var offsets = getOffsetRectRelativeToArbitraryNode(boundariesNode, offsetParent, fixedPosition);\n\n // In case of HTML, we need a different computation\n if (boundariesNode.nodeName === 'HTML' && !isFixed(offsetParent)) {\n var _getWindowSizes = getWindowSizes(popper.ownerDocument),\n height = _getWindowSizes.height,\n width = _getWindowSizes.width;\n\n boundaries.top += offsets.top - offsets.marginTop;\n boundaries.bottom = height + offsets.top;\n boundaries.left += offsets.left - offsets.marginLeft;\n boundaries.right = width + offsets.left;\n } else {\n // for all the other DOM elements, this one is good\n boundaries = offsets;\n }\n }\n\n // Add paddings\n padding = padding || 0;\n var isPaddingNumber = typeof padding === 'number';\n boundaries.left += isPaddingNumber ? padding : padding.left || 0;\n boundaries.top += isPaddingNumber ? padding : padding.top || 0;\n boundaries.right -= isPaddingNumber ? padding : padding.right || 0;\n boundaries.bottom -= isPaddingNumber ? padding : padding.bottom || 0;\n\n return boundaries;\n}\n\nfunction getArea(_ref) {\n var width = _ref.width,\n height = _ref.height;\n\n return width * height;\n}\n\n/**\n * Utility used to transform the `auto` placement to the placement with more\n * available space.\n * @method\n * @memberof Popper.Utils\n * @argument {Object} data - The data object generated by update method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction computeAutoPlacement(placement, refRect, popper, reference, boundariesElement) {\n var padding = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : 0;\n\n if (placement.indexOf('auto') === -1) {\n return placement;\n }\n\n var boundaries = getBoundaries(popper, reference, padding, boundariesElement);\n\n var rects = {\n top: {\n width: boundaries.width,\n height: refRect.top - boundaries.top\n },\n right: {\n width: boundaries.right - refRect.right,\n height: boundaries.height\n },\n bottom: {\n width: boundaries.width,\n height: boundaries.bottom - refRect.bottom\n },\n left: {\n width: refRect.left - boundaries.left,\n height: boundaries.height\n }\n };\n\n var sortedAreas = Object.keys(rects).map(function (key) {\n return _extends({\n key: key\n }, rects[key], {\n area: getArea(rects[key])\n });\n }).sort(function (a, b) {\n return b.area - a.area;\n });\n\n var filteredAreas = sortedAreas.filter(function (_ref2) {\n var width = _ref2.width,\n height = _ref2.height;\n return width >= popper.clientWidth && height >= popper.clientHeight;\n });\n\n var computedPlacement = filteredAreas.length > 0 ? filteredAreas[0].key : sortedAreas[0].key;\n\n var variation = placement.split('-')[1];\n\n return computedPlacement + (variation ? '-' + variation : '');\n}\n\n/**\n * Get offsets to the reference element\n * @method\n * @memberof Popper.Utils\n * @param {Object} state\n * @param {Element} popper - the popper element\n * @param {Element} reference - the reference element (the popper will be relative to this)\n * @param {Element} fixedPosition - is in fixed position mode\n * @returns {Object} An object containing the offsets which will be applied to the popper\n */\nfunction getReferenceOffsets(state, popper, reference) {\n var fixedPosition = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;\n\n var commonOffsetParent = fixedPosition ? getFixedPositionOffsetParent(popper) : findCommonOffsetParent(popper, reference);\n return getOffsetRectRelativeToArbitraryNode(reference, commonOffsetParent, fixedPosition);\n}\n\n/**\n * Get the outer sizes of the given element (offset size + margins)\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @returns {Object} object containing width and height properties\n */\nfunction getOuterSizes(element) {\n var window = element.ownerDocument.defaultView;\n var styles = window.getComputedStyle(element);\n var x = parseFloat(styles.marginTop || 0) + parseFloat(styles.marginBottom || 0);\n var y = parseFloat(styles.marginLeft || 0) + parseFloat(styles.marginRight || 0);\n var result = {\n width: element.offsetWidth + y,\n height: element.offsetHeight + x\n };\n return result;\n}\n\n/**\n * Get the opposite placement of the given one\n * @method\n * @memberof Popper.Utils\n * @argument {String} placement\n * @returns {String} flipped placement\n */\nfunction getOppositePlacement(placement) {\n var hash = { left: 'right', right: 'left', bottom: 'top', top: 'bottom' };\n return placement.replace(/left|right|bottom|top/g, function (matched) {\n return hash[matched];\n });\n}\n\n/**\n * Get offsets to the popper\n * @method\n * @memberof Popper.Utils\n * @param {Object} position - CSS position the Popper will get applied\n * @param {HTMLElement} popper - the popper element\n * @param {Object} referenceOffsets - the reference offsets (the popper will be relative to this)\n * @param {String} placement - one of the valid placement options\n * @returns {Object} popperOffsets - An object containing the offsets which will be applied to the popper\n */\nfunction getPopperOffsets(popper, referenceOffsets, placement) {\n placement = placement.split('-')[0];\n\n // Get popper node sizes\n var popperRect = getOuterSizes(popper);\n\n // Add position, width and height to our offsets object\n var popperOffsets = {\n width: popperRect.width,\n height: popperRect.height\n };\n\n // depending by the popper placement we have to compute its offsets slightly differently\n var isHoriz = ['right', 'left'].indexOf(placement) !== -1;\n var mainSide = isHoriz ? 'top' : 'left';\n var secondarySide = isHoriz ? 'left' : 'top';\n var measurement = isHoriz ? 'height' : 'width';\n var secondaryMeasurement = !isHoriz ? 'height' : 'width';\n\n popperOffsets[mainSide] = referenceOffsets[mainSide] + referenceOffsets[measurement] / 2 - popperRect[measurement] / 2;\n if (placement === secondarySide) {\n popperOffsets[secondarySide] = referenceOffsets[secondarySide] - popperRect[secondaryMeasurement];\n } else {\n popperOffsets[secondarySide] = referenceOffsets[getOppositePlacement(secondarySide)];\n }\n\n return popperOffsets;\n}\n\n/**\n * Mimics the `find` method of Array\n * @method\n * @memberof Popper.Utils\n * @argument {Array} arr\n * @argument prop\n * @argument value\n * @returns index or -1\n */\nfunction find(arr, check) {\n // use native find if supported\n if (Array.prototype.find) {\n return arr.find(check);\n }\n\n // use `filter` to obtain the same behavior of `find`\n return arr.filter(check)[0];\n}\n\n/**\n * Return the index of the matching object\n * @method\n * @memberof Popper.Utils\n * @argument {Array} arr\n * @argument prop\n * @argument value\n * @returns index or -1\n */\nfunction findIndex(arr, prop, value) {\n // use native findIndex if supported\n if (Array.prototype.findIndex) {\n return arr.findIndex(function (cur) {\n return cur[prop] === value;\n });\n }\n\n // use `find` + `indexOf` if `findIndex` isn't supported\n var match = find(arr, function (obj) {\n return obj[prop] === value;\n });\n return arr.indexOf(match);\n}\n\n/**\n * Loop trough the list of modifiers and run them in order,\n * each of them will then edit the data object.\n * @method\n * @memberof Popper.Utils\n * @param {dataObject} data\n * @param {Array} modifiers\n * @param {String} ends - Optional modifier name used as stopper\n * @returns {dataObject}\n */\nfunction runModifiers(modifiers, data, ends) {\n var modifiersToRun = ends === undefined ? modifiers : modifiers.slice(0, findIndex(modifiers, 'name', ends));\n\n modifiersToRun.forEach(function (modifier) {\n if (modifier['function']) {\n // eslint-disable-line dot-notation\n console.warn('`modifier.function` is deprecated, use `modifier.fn`!');\n }\n var fn = modifier['function'] || modifier.fn; // eslint-disable-line dot-notation\n if (modifier.enabled && isFunction(fn)) {\n // Add properties to offsets to make them a complete clientRect object\n // we do this before each modifier to make sure the previous one doesn't\n // mess with these values\n data.offsets.popper = getClientRect(data.offsets.popper);\n data.offsets.reference = getClientRect(data.offsets.reference);\n\n data = fn(data, modifier);\n }\n });\n\n return data;\n}\n\n/**\n * Updates the position of the popper, computing the new offsets and applying\n * the new style.<br />\n * Prefer `scheduleUpdate` over `update` because of performance reasons.\n * @method\n * @memberof Popper\n */\nfunction update() {\n // if popper is destroyed, don't perform any further update\n if (this.state.isDestroyed) {\n return;\n }\n\n var data = {\n instance: this,\n styles: {},\n arrowStyles: {},\n attributes: {},\n flipped: false,\n offsets: {}\n };\n\n // compute reference element offsets\n data.offsets.reference = getReferenceOffsets(this.state, this.popper, this.reference, this.options.positionFixed);\n\n // compute auto placement, store placement inside the data object,\n // modifiers will be able to edit `placement` if needed\n // and refer to originalPlacement to know the original value\n data.placement = computeAutoPlacement(this.options.placement, data.offsets.reference, this.popper, this.reference, this.options.modifiers.flip.boundariesElement, this.options.modifiers.flip.padding);\n\n // store the computed placement inside `originalPlacement`\n data.originalPlacement = data.placement;\n\n data.positionFixed = this.options.positionFixed;\n\n // compute the popper offsets\n data.offsets.popper = getPopperOffsets(this.popper, data.offsets.reference, data.placement);\n\n data.offsets.popper.position = this.options.positionFixed ? 'fixed' : 'absolute';\n\n // run the modifiers\n data = runModifiers(this.modifiers, data);\n\n // the first `update` will call `onCreate` callback\n // the other ones will call `onUpdate` callback\n if (!this.state.isCreated) {\n this.state.isCreated = true;\n this.options.onCreate(data);\n } else {\n this.options.onUpdate(data);\n }\n}\n\n/**\n * Helper used to know if the given modifier is enabled.\n * @method\n * @memberof Popper.Utils\n * @returns {Boolean}\n */\nfunction isModifierEnabled(modifiers, modifierName) {\n return modifiers.some(function (_ref) {\n var name = _ref.name,\n enabled = _ref.enabled;\n return enabled && name === modifierName;\n });\n}\n\n/**\n * Get the prefixed supported property name\n * @method\n * @memberof Popper.Utils\n * @argument {String} property (camelCase)\n * @returns {String} prefixed property (camelCase or PascalCase, depending on the vendor prefix)\n */\nfunction getSupportedPropertyName(property) {\n var prefixes = [false, 'ms', 'Webkit', 'Moz', 'O'];\n var upperProp = property.charAt(0).toUpperCase() + property.slice(1);\n\n for (var i = 0; i < prefixes.length; i++) {\n var prefix = prefixes[i];\n var toCheck = prefix ? '' + prefix + upperProp : property;\n if (typeof document.body.style[toCheck] !== 'undefined') {\n return toCheck;\n }\n }\n return null;\n}\n\n/**\n * Destroys the popper.\n * @method\n * @memberof Popper\n */\nfunction destroy() {\n this.state.isDestroyed = true;\n\n // touch DOM only if `applyStyle` modifier is enabled\n if (isModifierEnabled(this.modifiers, 'applyStyle')) {\n this.popper.removeAttribute('x-placement');\n this.popper.style.position = '';\n this.popper.style.top = '';\n this.popper.style.left = '';\n this.popper.style.right = '';\n this.popper.style.bottom = '';\n this.popper.style.willChange = '';\n this.popper.style[getSupportedPropertyName('transform')] = '';\n }\n\n this.disableEventListeners();\n\n // remove the popper if user explicity asked for the deletion on destroy\n // do not use `remove` because IE11 doesn't support it\n if (this.options.removeOnDestroy) {\n this.popper.parentNode.removeChild(this.popper);\n }\n return this;\n}\n\n/**\n * Get the window associated with the element\n * @argument {Element} element\n * @returns {Window}\n */\nfunction getWindow(element) {\n var ownerDocument = element.ownerDocument;\n return ownerDocument ? ownerDocument.defaultView : window;\n}\n\nfunction attachToScrollParents(scrollParent, event, callback, scrollParents) {\n var isBody = scrollParent.nodeName === 'BODY';\n var target = isBody ? scrollParent.ownerDocument.defaultView : scrollParent;\n target.addEventListener(event, callback, { passive: true });\n\n if (!isBody) {\n attachToScrollParents(getScrollParent(target.parentNode), event, callback, scrollParents);\n }\n scrollParents.push(target);\n}\n\n/**\n * Setup needed event listeners used to update the popper position\n * @method\n * @memberof Popper.Utils\n * @private\n */\nfunction setupEventListeners(reference, options, state, updateBound) {\n // Resize event listener on window\n state.updateBound = updateBound;\n getWindow(reference).addEventListener('resize', state.updateBound, { passive: true });\n\n // Scroll event listener on scroll parents\n var scrollElement = getScrollParent(reference);\n attachToScrollParents(scrollElement, 'scroll', state.updateBound, state.scrollParents);\n state.scrollElement = scrollElement;\n state.eventsEnabled = true;\n\n return state;\n}\n\n/**\n * It will add resize/scroll events and start recalculating\n * position of the popper element when they are triggered.\n * @method\n * @memberof Popper\n */\nfunction enableEventListeners() {\n if (!this.state.eventsEnabled) {\n this.state = setupEventListeners(this.reference, this.options, this.state, this.scheduleUpdate);\n }\n}\n\n/**\n * Remove event listeners used to update the popper position\n * @method\n * @memberof Popper.Utils\n * @private\n */\nfunction removeEventListeners(reference, state) {\n // Remove resize event listener on window\n getWindow(reference).removeEventListener('resize', state.updateBound);\n\n // Remove scroll event listener on scroll parents\n state.scrollParents.forEach(function (target) {\n target.removeEventListener('scroll', state.updateBound);\n });\n\n // Reset state\n state.updateBound = null;\n state.scrollParents = [];\n state.scrollElement = null;\n state.eventsEnabled = false;\n return state;\n}\n\n/**\n * It will remove resize/scroll events and won't recalculate popper position\n * when they are triggered. It also won't trigger `onUpdate` callback anymore,\n * unless you call `update` method manually.\n * @method\n * @memberof Popper\n */\nfunction disableEventListeners() {\n if (this.state.eventsEnabled) {\n cancelAnimationFrame(this.scheduleUpdate);\n this.state = removeEventListeners(this.reference, this.state);\n }\n}\n\n/**\n * Tells if a given input is a number\n * @method\n * @memberof Popper.Utils\n * @param {*} input to check\n * @return {Boolean}\n */\nfunction isNumeric(n) {\n return n !== '' && !isNaN(parseFloat(n)) && isFinite(n);\n}\n\n/**\n * Set the style to the given popper\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element - Element to apply the style to\n * @argument {Object} styles\n * Object with a list of properties and values which will be applied to the element\n */\nfunction setStyles(element, styles) {\n Object.keys(styles).forEach(function (prop) {\n var unit = '';\n // add unit if the value is numeric and is one of the following\n if (['width', 'height', 'top', 'right', 'bottom', 'left'].indexOf(prop) !== -1 && isNumeric(styles[prop])) {\n unit = 'px';\n }\n element.style[prop] = styles[prop] + unit;\n });\n}\n\n/**\n * Set the attributes to the given popper\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element - Element to apply the attributes to\n * @argument {Object} styles\n * Object with a list of properties and values which will be applied to the element\n */\nfunction setAttributes(element, attributes) {\n Object.keys(attributes).forEach(function (prop) {\n var value = attributes[prop];\n if (value !== false) {\n element.setAttribute(prop, attributes[prop]);\n } else {\n element.removeAttribute(prop);\n }\n });\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by `update` method\n * @argument {Object} data.styles - List of style properties - values to apply to popper element\n * @argument {Object} data.attributes - List of attribute properties - values to apply to popper element\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The same data object\n */\nfunction applyStyle(data) {\n // any property present in `data.styles` will be applied to the popper,\n // in this way we can make the 3rd party modifiers add custom styles to it\n // Be aware, modifiers could override the properties defined in the previous\n // lines of this modifier!\n setStyles(data.instance.popper, data.styles);\n\n // any property present in `data.attributes` will be applied to the popper,\n // they will be set as HTML attributes of the element\n setAttributes(data.instance.popper, data.attributes);\n\n // if arrowElement is defined and arrowStyles has some properties\n if (data.arrowElement && Object.keys(data.arrowStyles).length) {\n setStyles(data.arrowElement, data.arrowStyles);\n }\n\n return data;\n}\n\n/**\n * Set the x-placement attribute before everything else because it could be used\n * to add margins to the popper margins needs to be calculated to get the\n * correct popper offsets.\n * @method\n * @memberof Popper.modifiers\n * @param {HTMLElement} reference - The reference element used to position the popper\n * @param {HTMLElement} popper - The HTML element used as popper\n * @param {Object} options - Popper.js options\n */\nfunction applyStyleOnLoad(reference, popper, options, modifierOptions, state) {\n // compute reference element offsets\n var referenceOffsets = getReferenceOffsets(state, popper, reference, options.positionFixed);\n\n // compute auto placement, store placement inside the data object,\n // modifiers will be able to edit `placement` if needed\n // and refer to originalPlacement to know the original value\n var placement = computeAutoPlacement(options.placement, referenceOffsets, popper, reference, options.modifiers.flip.boundariesElement, options.modifiers.flip.padding);\n\n popper.setAttribute('x-placement', placement);\n\n // Apply `position` to popper before anything else because\n // without the position applied we can't guarantee correct computations\n setStyles(popper, { position: options.positionFixed ? 'fixed' : 'absolute' });\n\n return options;\n}\n\n/**\n * @function\n * @memberof Popper.Utils\n * @argument {Object} data - The data object generated by `update` method\n * @argument {Boolean} shouldRound - If the offsets should be rounded at all\n * @returns {Object} The popper's position offsets rounded\n *\n * The tale of pixel-perfect positioning. It's still not 100% perfect, but as\n * good as it can be within reason.\n * Discussion here: https://github.com/FezVrasta/popper.js/pull/715\n *\n * Low DPI screens cause a popper to be blurry if not using full pixels (Safari\n * as well on High DPI screens).\n *\n * Firefox prefers no rounding for positioning and does not have blurriness on\n * high DPI screens.\n *\n * Only horizontal placement and left/right values need to be considered.\n */\nfunction getRoundedOffsets(data, shouldRound) {\n var _data$offsets = data.offsets,\n popper = _data$offsets.popper,\n reference = _data$offsets.reference;\n var round = Math.round,\n floor = Math.floor;\n\n var noRound = function noRound(v) {\n return v;\n };\n\n var referenceWidth = round(reference.width);\n var popperWidth = round(popper.width);\n\n var isVertical = ['left', 'right'].indexOf(data.placement) !== -1;\n var isVariation = data.placement.indexOf('-') !== -1;\n var sameWidthParity = referenceWidth % 2 === popperWidth % 2;\n var bothOddWidth = referenceWidth % 2 === 1 && popperWidth % 2 === 1;\n\n var horizontalToInteger = !shouldRound ? noRound : isVertical || isVariation || sameWidthParity ? round : floor;\n var verticalToInteger = !shouldRound ? noRound : round;\n\n return {\n left: horizontalToInteger(bothOddWidth && !isVariation && shouldRound ? popper.left - 1 : popper.left),\n top: verticalToInteger(popper.top),\n bottom: verticalToInteger(popper.bottom),\n right: horizontalToInteger(popper.right)\n };\n}\n\nvar isFirefox = isBrowser && /Firefox/i.test(navigator.userAgent);\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by `update` method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction computeStyle(data, options) {\n var x = options.x,\n y = options.y;\n var popper = data.offsets.popper;\n\n // Remove this legacy support in Popper.js v2\n\n var legacyGpuAccelerationOption = find(data.instance.modifiers, function (modifier) {\n return modifier.name === 'applyStyle';\n }).gpuAcceleration;\n if (legacyGpuAccelerationOption !== undefined) {\n console.warn('WARNING: `gpuAcceleration` option moved to `computeStyle` modifier and will not be supported in future versions of Popper.js!');\n }\n var gpuAcceleration = legacyGpuAccelerationOption !== undefined ? legacyGpuAccelerationOption : options.gpuAcceleration;\n\n var offsetParent = getOffsetParent(data.instance.popper);\n var offsetParentRect = getBoundingClientRect(offsetParent);\n\n // Styles\n var styles = {\n position: popper.position\n };\n\n var offsets = getRoundedOffsets(data, window.devicePixelRatio < 2 || !isFirefox);\n\n var sideA = x === 'bottom' ? 'top' : 'bottom';\n var sideB = y === 'right' ? 'left' : 'right';\n\n // if gpuAcceleration is set to `true` and transform is supported,\n // we use `translate3d` to apply the position to the popper we\n // automatically use the supported prefixed version if needed\n var prefixedProperty = getSupportedPropertyName('transform');\n\n // now, let's make a step back and look at this code closely (wtf?)\n // If the content of the popper grows once it's been positioned, it\n // may happen that the popper gets misplaced because of the new content\n // overflowing its reference element\n // To avoid this problem, we provide two options (x and y), which allow\n // the consumer to define the offset origin.\n // If we position a popper on top of a reference element, we can set\n // `x` to `top` to make the popper grow towards its top instead of\n // its bottom.\n var left = void 0,\n top = void 0;\n if (sideA === 'bottom') {\n // when offsetParent is <html> the positioning is relative to the bottom of the screen (excluding the scrollbar)\n // and not the bottom of the html element\n if (offsetParent.nodeName === 'HTML') {\n top = -offsetParent.clientHeight + offsets.bottom;\n } else {\n top = -offsetParentRect.height + offsets.bottom;\n }\n } else {\n top = offsets.top;\n }\n if (sideB === 'right') {\n if (offsetParent.nodeName === 'HTML') {\n left = -offsetParent.clientWidth + offsets.right;\n } else {\n left = -offsetParentRect.width + offsets.right;\n }\n } else {\n left = offsets.left;\n }\n if (gpuAcceleration && prefixedProperty) {\n styles[prefixedProperty] = 'translate3d(' + left + 'px, ' + top + 'px, 0)';\n styles[sideA] = 0;\n styles[sideB] = 0;\n styles.willChange = 'transform';\n } else {\n // othwerise, we use the standard `top`, `left`, `bottom` and `right` properties\n var invertTop = sideA === 'bottom' ? -1 : 1;\n var invertLeft = sideB === 'right' ? -1 : 1;\n styles[sideA] = top * invertTop;\n styles[sideB] = left * invertLeft;\n styles.willChange = sideA + ', ' + sideB;\n }\n\n // Attributes\n var attributes = {\n 'x-placement': data.placement\n };\n\n // Update `data` attributes, styles and arrowStyles\n data.attributes = _extends({}, attributes, data.attributes);\n data.styles = _extends({}, styles, data.styles);\n data.arrowStyles = _extends({}, data.offsets.arrow, data.arrowStyles);\n\n return data;\n}\n\n/**\n * Helper used to know if the given modifier depends from another one.<br />\n * It checks if the needed modifier is listed and enabled.\n * @method\n * @memberof Popper.Utils\n * @param {Array} modifiers - list of modifiers\n * @param {String} requestingName - name of requesting modifier\n * @param {String} requestedName - name of requested modifier\n * @returns {Boolean}\n */\nfunction isModifierRequired(modifiers, requestingName, requestedName) {\n var requesting = find(modifiers, function (_ref) {\n var name = _ref.name;\n return name === requestingName;\n });\n\n var isRequired = !!requesting && modifiers.some(function (modifier) {\n return modifier.name === requestedName && modifier.enabled && modifier.order < requesting.order;\n });\n\n if (!isRequired) {\n var _requesting = '`' + requestingName + '`';\n var requested = '`' + requestedName + '`';\n console.warn(requested + ' modifier is required by ' + _requesting + ' modifier in order to work, be sure to include it before ' + _requesting + '!');\n }\n return isRequired;\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by update method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction arrow(data, options) {\n var _data$offsets$arrow;\n\n // arrow depends on keepTogether in order to work\n if (!isModifierRequired(data.instance.modifiers, 'arrow', 'keepTogether')) {\n return data;\n }\n\n var arrowElement = options.element;\n\n // if arrowElement is a string, suppose it's a CSS selector\n if (typeof arrowElement === 'string') {\n arrowElement = data.instance.popper.querySelector(arrowElement);\n\n // if arrowElement is not found, don't run the modifier\n if (!arrowElement) {\n return data;\n }\n } else {\n // if the arrowElement isn't a query selector we must check that the\n // provided DOM node is child of its popper node\n if (!data.instance.popper.contains(arrowElement)) {\n console.warn('WARNING: `arrow.element` must be child of its popper element!');\n return data;\n }\n }\n\n var placement = data.placement.split('-')[0];\n var _data$offsets = data.offsets,\n popper = _data$offsets.popper,\n reference = _data$offsets.reference;\n\n var isVertical = ['left', 'right'].indexOf(placement) !== -1;\n\n var len = isVertical ? 'height' : 'width';\n var sideCapitalized = isVertical ? 'Top' : 'Left';\n var side = sideCapitalized.toLowerCase();\n var altSide = isVertical ? 'left' : 'top';\n var opSide = isVertical ? 'bottom' : 'right';\n var arrowElementSize = getOuterSizes(arrowElement)[len];\n\n //\n // extends keepTogether behavior making sure the popper and its\n // reference have enough pixels in conjunction\n //\n\n // top/left side\n if (reference[opSide] - arrowElementSize < popper[side]) {\n data.offsets.popper[side] -= popper[side] - (reference[opSide] - arrowElementSize);\n }\n // bottom/right side\n if (reference[side] + arrowElementSize > popper[opSide]) {\n data.offsets.popper[side] += reference[side] + arrowElementSize - popper[opSide];\n }\n data.offsets.popper = getClientRect(data.offsets.popper);\n\n // compute center of the popper\n var center = reference[side] + reference[len] / 2 - arrowElementSize / 2;\n\n // Compute the sideValue using the updated popper offsets\n // take popper margin in account because we don't have this info available\n var css = getStyleComputedProperty(data.instance.popper);\n var popperMarginSide = parseFloat(css['margin' + sideCapitalized], 10);\n var popperBorderSide = parseFloat(css['border' + sideCapitalized + 'Width'], 10);\n var sideValue = center - data.offsets.popper[side] - popperMarginSide - popperBorderSide;\n\n // prevent arrowElement from being placed not contiguously to its popper\n sideValue = Math.max(Math.min(popper[len] - arrowElementSize, sideValue), 0);\n\n data.arrowElement = arrowElement;\n data.offsets.arrow = (_data$offsets$arrow = {}, defineProperty(_data$offsets$arrow, side, Math.round(sideValue)), defineProperty(_data$offsets$arrow, altSide, ''), _data$offsets$arrow);\n\n return data;\n}\n\n/**\n * Get the opposite placement variation of the given one\n * @method\n * @memberof Popper.Utils\n * @argument {String} placement variation\n * @returns {String} flipped placement variation\n */\nfunction getOppositeVariation(variation) {\n if (variation === 'end') {\n return 'start';\n } else if (variation === 'start') {\n return 'end';\n }\n return variation;\n}\n\n/**\n * List of accepted placements to use as values of the `placement` option.<br />\n * Valid placements are:\n * - `auto`\n * - `top`\n * - `right`\n * - `bottom`\n * - `left`\n *\n * Each placement can have a variation from this list:\n * - `-start`\n * - `-end`\n *\n * Variations are interpreted easily if you think of them as the left to right\n * written languages. Horizontally (`top` and `bottom`), `start` is left and `end`\n * is right.<br />\n * Vertically (`left` and `right`), `start` is top and `end` is bottom.\n *\n * Some valid examples are:\n * - `top-end` (on top of reference, right aligned)\n * - `right-start` (on right of reference, top aligned)\n * - `bottom` (on bottom, centered)\n * - `auto-end` (on the side with more space available, alignment depends by placement)\n *\n * @static\n * @type {Array}\n * @enum {String}\n * @readonly\n * @method placements\n * @memberof Popper\n */\nvar placements = ['auto-start', 'auto', 'auto-end', 'top-start', 'top', 'top-end', 'right-start', 'right', 'right-end', 'bottom-end', 'bottom', 'bottom-start', 'left-end', 'left', 'left-start'];\n\n// Get rid of `auto` `auto-start` and `auto-end`\nvar validPlacements = placements.slice(3);\n\n/**\n * Given an initial placement, returns all the subsequent placements\n * clockwise (or counter-clockwise).\n *\n * @method\n * @memberof Popper.Utils\n * @argument {String} placement - A valid placement (it accepts variations)\n * @argument {Boolean} counter - Set to true to walk the placements counterclockwise\n * @returns {Array} placements including their variations\n */\nfunction clockwise(placement) {\n var counter = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;\n\n var index = validPlacements.indexOf(placement);\n var arr = validPlacements.slice(index + 1).concat(validPlacements.slice(0, index));\n return counter ? arr.reverse() : arr;\n}\n\nvar BEHAVIORS = {\n FLIP: 'flip',\n CLOCKWISE: 'clockwise',\n COUNTERCLOCKWISE: 'counterclockwise'\n};\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by update method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction flip(data, options) {\n // if `inner` modifier is enabled, we can't use the `flip` modifier\n if (isModifierEnabled(data.instance.modifiers, 'inner')) {\n return data;\n }\n\n if (data.flipped && data.placement === data.originalPlacement) {\n // seems like flip is trying to loop, probably there's not enough space on any of the flippable sides\n return data;\n }\n\n var boundaries = getBoundaries(data.instance.popper, data.instance.reference, options.padding, options.boundariesElement, data.positionFixed);\n\n var placement = data.placement.split('-')[0];\n var placementOpposite = getOppositePlacement(placement);\n var variation = data.placement.split('-')[1] || '';\n\n var flipOrder = [];\n\n switch (options.behavior) {\n case BEHAVIORS.FLIP:\n flipOrder = [placement, placementOpposite];\n break;\n case BEHAVIORS.CLOCKWISE:\n flipOrder = clockwise(placement);\n break;\n case BEHAVIORS.COUNTERCLOCKWISE:\n flipOrder = clockwise(placement, true);\n break;\n default:\n flipOrder = options.behavior;\n }\n\n flipOrder.forEach(function (step, index) {\n if (placement !== step || flipOrder.length === index + 1) {\n return data;\n }\n\n placement = data.placement.split('-')[0];\n placementOpposite = getOppositePlacement(placement);\n\n var popperOffsets = data.offsets.popper;\n var refOffsets = data.offsets.reference;\n\n // using floor because the reference offsets may contain decimals we are not going to consider here\n var floor = Math.floor;\n var overlapsRef = placement === 'left' && floor(popperOffsets.right) > floor(refOffsets.left) || placement === 'right' && floor(popperOffsets.left) < floor(refOffsets.right) || placement === 'top' && floor(popperOffsets.bottom) > floor(refOffsets.top) || placement === 'bottom' && floor(popperOffsets.top) < floor(refOffsets.bottom);\n\n var overflowsLeft = floor(popperOffsets.left) < floor(boundaries.left);\n var overflowsRight = floor(popperOffsets.right) > floor(boundaries.right);\n var overflowsTop = floor(popperOffsets.top) < floor(boundaries.top);\n var overflowsBottom = floor(popperOffsets.bottom) > floor(boundaries.bottom);\n\n var overflowsBoundaries = placement === 'left' && overflowsLeft || placement === 'right' && overflowsRight || placement === 'top' && overflowsTop || placement === 'bottom' && overflowsBottom;\n\n // flip the variation if required\n var isVertical = ['top', 'bottom'].indexOf(placement) !== -1;\n\n // flips variation if reference element overflows boundaries\n var flippedVariationByRef = !!options.flipVariations && (isVertical && variation === 'start' && overflowsLeft || isVertical && variation === 'end' && overflowsRight || !isVertical && variation === 'start' && overflowsTop || !isVertical && variation === 'end' && overflowsBottom);\n\n // flips variation if popper content overflows boundaries\n var flippedVariationByContent = !!options.flipVariationsByContent && (isVertical && variation === 'start' && overflowsRight || isVertical && variation === 'end' && overflowsLeft || !isVertical && variation === 'start' && overflowsBottom || !isVertical && variation === 'end' && overflowsTop);\n\n var flippedVariation = flippedVariationByRef || flippedVariationByContent;\n\n if (overlapsRef || overflowsBoundaries || flippedVariation) {\n // this boolean to detect any flip loop\n data.flipped = true;\n\n if (overlapsRef || overflowsBoundaries) {\n placement = flipOrder[index + 1];\n }\n\n if (flippedVariation) {\n variation = getOppositeVariation(variation);\n }\n\n data.placement = placement + (variation ? '-' + variation : '');\n\n // this object contains `position`, we want to preserve it along with\n // any additional property we may add in the future\n data.offsets.popper = _extends({}, data.offsets.popper, getPopperOffsets(data.instance.popper, data.offsets.reference, data.placement));\n\n data = runModifiers(data.instance.modifiers, data, 'flip');\n }\n });\n return data;\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by update method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction keepTogether(data) {\n var _data$offsets = data.offsets,\n popper = _data$offsets.popper,\n reference = _data$offsets.reference;\n\n var placement = data.placement.split('-')[0];\n var floor = Math.floor;\n var isVertical = ['top', 'bottom'].indexOf(placement) !== -1;\n var side = isVertical ? 'right' : 'bottom';\n var opSide = isVertical ? 'left' : 'top';\n var measurement = isVertical ? 'width' : 'height';\n\n if (popper[side] < floor(reference[opSide])) {\n data.offsets.popper[opSide] = floor(reference[opSide]) - popper[measurement];\n }\n if (popper[opSide] > floor(reference[side])) {\n data.offsets.popper[opSide] = floor(reference[side]);\n }\n\n return data;\n}\n\n/**\n * Converts a string containing value + unit into a px value number\n * @function\n * @memberof {modifiers~offset}\n * @private\n * @argument {String} str - Value + unit string\n * @argument {String} measurement - `height` or `width`\n * @argument {Object} popperOffsets\n * @argument {Object} referenceOffsets\n * @returns {Number|String}\n * Value in pixels, or original string if no values were extracted\n */\nfunction toValue(str, measurement, popperOffsets, referenceOffsets) {\n // separate value from unit\n var split = str.match(/((?:\\-|\\+)?\\d*\\.?\\d*)(.*)/);\n var value = +split[1];\n var unit = split[2];\n\n // If it's not a number it's an operator, I guess\n if (!value) {\n return str;\n }\n\n if (unit.indexOf('%') === 0) {\n var element = void 0;\n switch (unit) {\n case '%p':\n element = popperOffsets;\n break;\n case '%':\n case '%r':\n default:\n element = referenceOffsets;\n }\n\n var rect = getClientRect(element);\n return rect[measurement] / 100 * value;\n } else if (unit === 'vh' || unit === 'vw') {\n // if is a vh or vw, we calculate the size based on the viewport\n var size = void 0;\n if (unit === 'vh') {\n size = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);\n } else {\n size = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);\n }\n return size / 100 * value;\n } else {\n // if is an explicit pixel unit, we get rid of the unit and keep the value\n // if is an implicit unit, it's px, and we return just the value\n return value;\n }\n}\n\n/**\n * Parse an `offset` string to extrapolate `x` and `y` numeric offsets.\n * @function\n * @memberof {modifiers~offset}\n * @private\n * @argument {String} offset\n * @argument {Object} popperOffsets\n * @argument {Object} referenceOffsets\n * @argument {String} basePlacement\n * @returns {Array} a two cells array with x and y offsets in numbers\n */\nfunction parseOffset(offset, popperOffsets, referenceOffsets, basePlacement) {\n var offsets = [0, 0];\n\n // Use height if placement is left or right and index is 0 otherwise use width\n // in this way the first offset will use an axis and the second one\n // will use the other one\n var useHeight = ['right', 'left'].indexOf(basePlacement) !== -1;\n\n // Split the offset string to obtain a list of values and operands\n // The regex addresses values with the plus or minus sign in front (+10, -20, etc)\n var fragments = offset.split(/(\\+|\\-)/).map(function (frag) {\n return frag.trim();\n });\n\n // Detect if the offset string contains a pair of values or a single one\n // they could be separated by comma or space\n var divider = fragments.indexOf(find(fragments, function (frag) {\n return frag.search(/,|\\s/) !== -1;\n }));\n\n if (fragments[divider] && fragments[divider].indexOf(',') === -1) {\n console.warn('Offsets separated by white space(s) are deprecated, use a comma (,) instead.');\n }\n\n // If divider is found, we divide the list of values and operands to divide\n // them by ofset X and Y.\n var splitRegex = /\\s*,\\s*|\\s+/;\n var ops = divider !== -1 ? [fragments.slice(0, divider).concat([fragments[divider].split(splitRegex)[0]]), [fragments[divider].split(splitRegex)[1]].concat(fragments.slice(divider + 1))] : [fragments];\n\n // Convert the values with units to absolute pixels to allow our computations\n ops = ops.map(function (op, index) {\n // Most of the units rely on the orientation of the popper\n var measurement = (index === 1 ? !useHeight : useHeight) ? 'height' : 'width';\n var mergeWithPrevious = false;\n return op\n // This aggregates any `+` or `-` sign that aren't considered operators\n // e.g.: 10 + +5 => [10, +, +5]\n .reduce(function (a, b) {\n if (a[a.length - 1] === '' && ['+', '-'].indexOf(b) !== -1) {\n a[a.length - 1] = b;\n mergeWithPrevious = true;\n return a;\n } else if (mergeWithPrevious) {\n a[a.length - 1] += b;\n mergeWithPrevious = false;\n return a;\n } else {\n return a.concat(b);\n }\n }, [])\n // Here we convert the string values into number values (in px)\n .map(function (str) {\n return toValue(str, measurement, popperOffsets, referenceOffsets);\n });\n });\n\n // Loop trough the offsets arrays and execute the operations\n ops.forEach(function (op, index) {\n op.forEach(function (frag, index2) {\n if (isNumeric(frag)) {\n offsets[index] += frag * (op[index2 - 1] === '-' ? -1 : 1);\n }\n });\n });\n return offsets;\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by update method\n * @argument {Object} options - Modifiers configuration and options\n * @argument {Number|String} options.offset=0\n * The offset value as described in the modifier description\n * @returns {Object} The data object, properly modified\n */\nfunction offset(data, _ref) {\n var offset = _ref.offset;\n var placement = data.placement,\n _data$offsets = data.offsets,\n popper = _data$offsets.popper,\n reference = _data$offsets.reference;\n\n var basePlacement = placement.split('-')[0];\n\n var offsets = void 0;\n if (isNumeric(+offset)) {\n offsets = [+offset, 0];\n } else {\n offsets = parseOffset(offset, popper, reference, basePlacement);\n }\n\n if (basePlacement === 'left') {\n popper.top += offsets[0];\n popper.left -= offsets[1];\n } else if (basePlacement === 'right') {\n popper.top += offsets[0];\n popper.left += offsets[1];\n } else if (basePlacement === 'top') {\n popper.left += offsets[0];\n popper.top -= offsets[1];\n } else if (basePlacement === 'bottom') {\n popper.left += offsets[0];\n popper.top += offsets[1];\n }\n\n data.popper = popper;\n return data;\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by `update` method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction preventOverflow(data, options) {\n var boundariesElement = options.boundariesElement || getOffsetParent(data.instance.popper);\n\n // If offsetParent is the reference element, we really want to\n // go one step up and use the next offsetParent as reference to\n // avoid to make this modifier completely useless and look like broken\n if (data.instance.reference === boundariesElement) {\n boundariesElement = getOffsetParent(boundariesElement);\n }\n\n // NOTE: DOM access here\n // resets the popper's position so that the document size can be calculated excluding\n // the size of the popper element itself\n var transformProp = getSupportedPropertyName('transform');\n var popperStyles = data.instance.popper.style; // assignment to help minification\n var top = popperStyles.top,\n left = popperStyles.left,\n transform = popperStyles[transformProp];\n\n popperStyles.top = '';\n popperStyles.left = '';\n popperStyles[transformProp] = '';\n\n var boundaries = getBoundaries(data.instance.popper, data.instance.reference, options.padding, boundariesElement, data.positionFixed);\n\n // NOTE: DOM access here\n // restores the original style properties after the offsets have been computed\n popperStyles.top = top;\n popperStyles.left = left;\n popperStyles[transformProp] = transform;\n\n options.boundaries = boundaries;\n\n var order = options.priority;\n var popper = data.offsets.popper;\n\n var check = {\n primary: function primary(placement) {\n var value = popper[placement];\n if (popper[placement] < boundaries[placement] && !options.escapeWithReference) {\n value = Math.max(popper[placement], boundaries[placement]);\n }\n return defineProperty({}, placement, value);\n },\n secondary: function secondary(placement) {\n var mainSide = placement === 'right' ? 'left' : 'top';\n var value = popper[mainSide];\n if (popper[placement] > boundaries[placement] && !options.escapeWithReference) {\n value = Math.min(popper[mainSide], boundaries[placement] - (placement === 'right' ? popper.width : popper.height));\n }\n return defineProperty({}, mainSide, value);\n }\n };\n\n order.forEach(function (placement) {\n var side = ['left', 'top'].indexOf(placement) !== -1 ? 'primary' : 'secondary';\n popper = _extends({}, popper, check[side](placement));\n });\n\n data.offsets.popper = popper;\n\n return data;\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by `update` method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction shift(data) {\n var placement = data.placement;\n var basePlacement = placement.split('-')[0];\n var shiftvariation = placement.split('-')[1];\n\n // if shift shiftvariation is specified, run the modifier\n if (shiftvariation) {\n var _data$offsets = data.offsets,\n reference = _data$offsets.reference,\n popper = _data$offsets.popper;\n\n var isVertical = ['bottom', 'top'].indexOf(basePlacement) !== -1;\n var side = isVertical ? 'left' : 'top';\n var measurement = isVertical ? 'width' : 'height';\n\n var shiftOffsets = {\n start: defineProperty({}, side, reference[side]),\n end: defineProperty({}, side, reference[side] + reference[measurement] - popper[measurement])\n };\n\n data.offsets.popper = _extends({}, popper, shiftOffsets[shiftvariation]);\n }\n\n return data;\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by update method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction hide(data) {\n if (!isModifierRequired(data.instance.modifiers, 'hide', 'preventOverflow')) {\n return data;\n }\n\n var refRect = data.offsets.reference;\n var bound = find(data.instance.modifiers, function (modifier) {\n return modifier.name === 'preventOverflow';\n }).boundaries;\n\n if (refRect.bottom < bound.top || refRect.left > bound.right || refRect.top > bound.bottom || refRect.right < bound.left) {\n // Avoid unnecessary DOM access if visibility hasn't changed\n if (data.hide === true) {\n return data;\n }\n\n data.hide = true;\n data.attributes['x-out-of-boundaries'] = '';\n } else {\n // Avoid unnecessary DOM access if visibility hasn't changed\n if (data.hide === false) {\n return data;\n }\n\n data.hide = false;\n data.attributes['x-out-of-boundaries'] = false;\n }\n\n return data;\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by `update` method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction inner(data) {\n var placement = data.placement;\n var basePlacement = placement.split('-')[0];\n var _data$offsets = data.offsets,\n popper = _data$offsets.popper,\n reference = _data$offsets.reference;\n\n var isHoriz = ['left', 'right'].indexOf(basePlacement) !== -1;\n\n var subtractLength = ['top', 'left'].indexOf(basePlacement) === -1;\n\n popper[isHoriz ? 'left' : 'top'] = reference[basePlacement] - (subtractLength ? popper[isHoriz ? 'width' : 'height'] : 0);\n\n data.placement = getOppositePlacement(placement);\n data.offsets.popper = getClientRect(popper);\n\n return data;\n}\n\n/**\n * Modifier function, each modifier can have a function of this type assigned\n * to its `fn` property.<br />\n * These functions will be called on each update, this means that you must\n * make sure they are performant enough to avoid performance bottlenecks.\n *\n * @function ModifierFn\n * @argument {dataObject} data - The data object generated by `update` method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {dataObject} The data object, properly modified\n */\n\n/**\n * Modifiers are plugins used to alter the behavior of your poppers.<br />\n * Popper.js uses a set of 9 modifiers to provide all the basic functionalities\n * needed by the library.\n *\n * Usually you don't want to override the `order`, `fn` and `onLoad` props.\n * All the other properties are configurations that could be tweaked.\n * @namespace modifiers\n */\nvar modifiers = {\n /**\n * Modifier used to shift the popper on the start or end of its reference\n * element.<br />\n * It will read the variation of the `placement` property.<br />\n * It can be one either `-end` or `-start`.\n * @memberof modifiers\n * @inner\n */\n shift: {\n /** @prop {number} order=100 - Index used to define the order of execution */\n order: 100,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: shift\n },\n\n /**\n * The `offset` modifier can shift your popper on both its axis.\n *\n * It accepts the following units:\n * - `px` or unit-less, interpreted as pixels\n * - `%` or `%r`, percentage relative to the length of the reference element\n * - `%p`, percentage relative to the length of the popper element\n * - `vw`, CSS viewport width unit\n * - `vh`, CSS viewport height unit\n *\n * For length is intended the main axis relative to the placement of the popper.<br />\n * This means that if the placement is `top` or `bottom`, the length will be the\n * `width`. In case of `left` or `right`, it will be the `height`.\n *\n * You can provide a single value (as `Number` or `String`), or a pair of values\n * as `String` divided by a comma or one (or more) white spaces.<br />\n * The latter is a deprecated method because it leads to confusion and will be\n * removed in v2.<br />\n * Additionally, it accepts additions and subtractions between different units.\n * Note that multiplications and divisions aren't supported.\n *\n * Valid examples are:\n * ```\n * 10\n * '10%'\n * '10, 10'\n * '10%, 10'\n * '10 + 10%'\n * '10 - 5vh + 3%'\n * '-10px + 5vh, 5px - 6%'\n * ```\n * > **NB**: If you desire to apply offsets to your poppers in a way that may make them overlap\n * > with their reference element, unfortunately, you will have to disable the `flip` modifier.\n * > You can read more on this at this [issue](https://github.com/FezVrasta/popper.js/issues/373).\n *\n * @memberof modifiers\n * @inner\n */\n offset: {\n /** @prop {number} order=200 - Index used to define the order of execution */\n order: 200,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: offset,\n /** @prop {Number|String} offset=0\n * The offset value as described in the modifier description\n */\n offset: 0\n },\n\n /**\n * Modifier used to prevent the popper from being positioned outside the boundary.\n *\n * A scenario exists where the reference itself is not within the boundaries.<br />\n * We can say it has \"escaped the boundaries\" — or just \"escaped\".<br />\n * In this case we need to decide whether the popper should either:\n *\n * - detach from the reference and remain \"trapped\" in the boundaries, or\n * - if it should ignore the boundary and \"escape with its reference\"\n *\n * When `escapeWithReference` is set to`true` and reference is completely\n * outside its boundaries, the popper will overflow (or completely leave)\n * the boundaries in order to remain attached to the edge of the reference.\n *\n * @memberof modifiers\n * @inner\n */\n preventOverflow: {\n /** @prop {number} order=300 - Index used to define the order of execution */\n order: 300,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: preventOverflow,\n /**\n * @prop {Array} [priority=['left','right','top','bottom']]\n * Popper will try to prevent overflow following these priorities by default,\n * then, it could overflow on the left and on top of the `boundariesElement`\n */\n priority: ['left', 'right', 'top', 'bottom'],\n /**\n * @prop {number} padding=5\n * Amount of pixel used to define a minimum distance between the boundaries\n * and the popper. This makes sure the popper always has a little padding\n * between the edges of its container\n */\n padding: 5,\n /**\n * @prop {String|HTMLElement} boundariesElement='scrollParent'\n * Boundaries used by the modifier. Can be `scrollParent`, `window`,\n * `viewport` or any DOM element.\n */\n boundariesElement: 'scrollParent'\n },\n\n /**\n * Modifier used to make sure the reference and its popper stay near each other\n * without leaving any gap between the two. Especially useful when the arrow is\n * enabled and you want to ensure that it points to its reference element.\n * It cares only about the first axis. You can still have poppers with margin\n * between the popper and its reference element.\n * @memberof modifiers\n * @inner\n */\n keepTogether: {\n /** @prop {number} order=400 - Index used to define the order of execution */\n order: 400,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: keepTogether\n },\n\n /**\n * This modifier is used to move the `arrowElement` of the popper to make\n * sure it is positioned between the reference element and its popper element.\n * It will read the outer size of the `arrowElement` node to detect how many\n * pixels of conjunction are needed.\n *\n * It has no effect if no `arrowElement` is provided.\n * @memberof modifiers\n * @inner\n */\n arrow: {\n /** @prop {number} order=500 - Index used to define the order of execution */\n order: 500,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: arrow,\n /** @prop {String|HTMLElement} element='[x-arrow]' - Selector or node used as arrow */\n element: '[x-arrow]'\n },\n\n /**\n * Modifier used to flip the popper's placement when it starts to overlap its\n * reference element.\n *\n * Requires the `preventOverflow` modifier before it in order to work.\n *\n * **NOTE:** this modifier will interrupt the current update cycle and will\n * restart it if it detects the need to flip the placement.\n * @memberof modifiers\n * @inner\n */\n flip: {\n /** @prop {number} order=600 - Index used to define the order of execution */\n order: 600,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: flip,\n /**\n * @prop {String|Array} behavior='flip'\n * The behavior used to change the popper's placement. It can be one of\n * `flip`, `clockwise`, `counterclockwise` or an array with a list of valid\n * placements (with optional variations)\n */\n behavior: 'flip',\n /**\n * @prop {number} padding=5\n * The popper will flip if it hits the edges of the `boundariesElement`\n */\n padding: 5,\n /**\n * @prop {String|HTMLElement} boundariesElement='viewport'\n * The element which will define the boundaries of the popper position.\n * The popper will never be placed outside of the defined boundaries\n * (except if `keepTogether` is enabled)\n */\n boundariesElement: 'viewport',\n /**\n * @prop {Boolean} flipVariations=false\n * The popper will switch placement variation between `-start` and `-end` when\n * the reference element overlaps its boundaries.\n *\n * The original placement should have a set variation.\n */\n flipVariations: false,\n /**\n * @prop {Boolean} flipVariationsByContent=false\n * The popper will switch placement variation between `-start` and `-end` when\n * the popper element overlaps its reference boundaries.\n *\n * The original placement should have a set variation.\n */\n flipVariationsByContent: false\n },\n\n /**\n * Modifier used to make the popper flow toward the inner of the reference element.\n * By default, when this modifier is disabled, the popper will be placed outside\n * the reference element.\n * @memberof modifiers\n * @inner\n */\n inner: {\n /** @prop {number} order=700 - Index used to define the order of execution */\n order: 700,\n /** @prop {Boolean} enabled=false - Whether the modifier is enabled or not */\n enabled: false,\n /** @prop {ModifierFn} */\n fn: inner\n },\n\n /**\n * Modifier used to hide the popper when its reference element is outside of the\n * popper boundaries. It will set a `x-out-of-boundaries` attribute which can\n * be used to hide with a CSS selector the popper when its reference is\n * out of boundaries.\n *\n * Requires the `preventOverflow` modifier before it in order to work.\n * @memberof modifiers\n * @inner\n */\n hide: {\n /** @prop {number} order=800 - Index used to define the order of execution */\n order: 800,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: hide\n },\n\n /**\n * Computes the style that will be applied to the popper element to gets\n * properly positioned.\n *\n * Note that this modifier will not touch the DOM, it just prepares the styles\n * so that `applyStyle` modifier can apply it. This separation is useful\n * in case you need to replace `applyStyle` with a custom implementation.\n *\n * This modifier has `850` as `order` value to maintain backward compatibility\n * with previous versions of Popper.js. Expect the modifiers ordering method\n * to change in future major versions of the library.\n *\n * @memberof modifiers\n * @inner\n */\n computeStyle: {\n /** @prop {number} order=850 - Index used to define the order of execution */\n order: 850,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: computeStyle,\n /**\n * @prop {Boolean} gpuAcceleration=true\n * If true, it uses the CSS 3D transformation to position the popper.\n * Otherwise, it will use the `top` and `left` properties\n */\n gpuAcceleration: true,\n /**\n * @prop {string} [x='bottom']\n * Where to anchor the X axis (`bottom` or `top`). AKA X offset origin.\n * Change this if your popper should grow in a direction different from `bottom`\n */\n x: 'bottom',\n /**\n * @prop {string} [x='left']\n * Where to anchor the Y axis (`left` or `right`). AKA Y offset origin.\n * Change this if your popper should grow in a direction different from `right`\n */\n y: 'right'\n },\n\n /**\n * Applies the computed styles to the popper element.\n *\n * All the DOM manipulations are limited to this modifier. This is useful in case\n * you want to integrate Popper.js inside a framework or view library and you\n * want to delegate all the DOM manipulations to it.\n *\n * Note that if you disable this modifier, you must make sure the popper element\n * has its position set to `absolute` before Popper.js can do its work!\n *\n * Just disable this modifier and define your own to achieve the desired effect.\n *\n * @memberof modifiers\n * @inner\n */\n applyStyle: {\n /** @prop {number} order=900 - Index used to define the order of execution */\n order: 900,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: applyStyle,\n /** @prop {Function} */\n onLoad: applyStyleOnLoad,\n /**\n * @deprecated since version 1.10.0, the property moved to `computeStyle` modifier\n * @prop {Boolean} gpuAcceleration=true\n * If true, it uses the CSS 3D transformation to position the popper.\n * Otherwise, it will use the `top` and `left` properties\n */\n gpuAcceleration: undefined\n }\n};\n\n/**\n * The `dataObject` is an object containing all the information used by Popper.js.\n * This object is passed to modifiers and to the `onCreate` and `onUpdate` callbacks.\n * @name dataObject\n * @property {Object} data.instance The Popper.js instance\n * @property {String} data.placement Placement applied to popper\n * @property {String} data.originalPlacement Placement originally defined on init\n * @property {Boolean} data.flipped True if popper has been flipped by flip modifier\n * @property {Boolean} data.hide True if the reference element is out of boundaries, useful to know when to hide the popper\n * @property {HTMLElement} data.arrowElement Node used as arrow by arrow modifier\n * @property {Object} data.styles Any CSS property defined here will be applied to the popper. It expects the JavaScript nomenclature (eg. `marginBottom`)\n * @property {Object} data.arrowStyles Any CSS property defined here will be applied to the popper arrow. It expects the JavaScript nomenclature (eg. `marginBottom`)\n * @property {Object} data.boundaries Offsets of the popper boundaries\n * @property {Object} data.offsets The measurements of popper, reference and arrow elements\n * @property {Object} data.offsets.popper `top`, `left`, `width`, `height` values\n * @property {Object} data.offsets.reference `top`, `left`, `width`, `height` values\n * @property {Object} data.offsets.arrow] `top` and `left` offsets, only one of them will be different from 0\n */\n\n/**\n * Default options provided to Popper.js constructor.<br />\n * These can be overridden using the `options` argument of Popper.js.<br />\n * To override an option, simply pass an object with the same\n * structure of the `options` object, as the 3rd argument. For example:\n * ```\n * new Popper(ref, pop, {\n * modifiers: {\n * preventOverflow: { enabled: false }\n * }\n * })\n * ```\n * @type {Object}\n * @static\n * @memberof Popper\n */\nvar Defaults = {\n /**\n * Popper's placement.\n * @prop {Popper.placements} placement='bottom'\n */\n placement: 'bottom',\n\n /**\n * Set this to true if you want popper to position it self in 'fixed' mode\n * @prop {Boolean} positionFixed=false\n */\n positionFixed: false,\n\n /**\n * Whether events (resize, scroll) are initially enabled.\n * @prop {Boolean} eventsEnabled=true\n */\n eventsEnabled: true,\n\n /**\n * Set to true if you want to automatically remove the popper when\n * you call the `destroy` method.\n * @prop {Boolean} removeOnDestroy=false\n */\n removeOnDestroy: false,\n\n /**\n * Callback called when the popper is created.<br />\n * By default, it is set to no-op.<br />\n * Access Popper.js instance with `data.instance`.\n * @prop {onCreate}\n */\n onCreate: function onCreate() {},\n\n /**\n * Callback called when the popper is updated. This callback is not called\n * on the initialization/creation of the popper, but only on subsequent\n * updates.<br />\n * By default, it is set to no-op.<br />\n * Access Popper.js instance with `data.instance`.\n * @prop {onUpdate}\n */\n onUpdate: function onUpdate() {},\n\n /**\n * List of modifiers used to modify the offsets before they are applied to the popper.\n * They provide most of the functionalities of Popper.js.\n * @prop {modifiers}\n */\n modifiers: modifiers\n};\n\n/**\n * @callback onCreate\n * @param {dataObject} data\n */\n\n/**\n * @callback onUpdate\n * @param {dataObject} data\n */\n\n// Utils\n// Methods\nvar Popper = function () {\n /**\n * Creates a new Popper.js instance.\n * @class Popper\n * @param {Element|referenceObject} reference - The reference element used to position the popper\n * @param {Element} popper - The HTML / XML element used as the popper\n * @param {Object} options - Your custom options to override the ones defined in [Defaults](#defaults)\n * @return {Object} instance - The generated Popper.js instance\n */\n function Popper(reference, popper) {\n var _this = this;\n\n var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};\n classCallCheck(this, Popper);\n\n this.scheduleUpdate = function () {\n return requestAnimationFrame(_this.update);\n };\n\n // make update() debounced, so that it only runs at most once-per-tick\n this.update = debounce(this.update.bind(this));\n\n // with {} we create a new object with the options inside it\n this.options = _extends({}, Popper.Defaults, options);\n\n // init state\n this.state = {\n isDestroyed: false,\n isCreated: false,\n scrollParents: []\n };\n\n // get reference and popper elements (allow jQuery wrappers)\n this.reference = reference && reference.jquery ? reference[0] : reference;\n this.popper = popper && popper.jquery ? popper[0] : popper;\n\n // Deep merge modifiers options\n this.options.modifiers = {};\n Object.keys(_extends({}, Popper.Defaults.modifiers, options.modifiers)).forEach(function (name) {\n _this.options.modifiers[name] = _extends({}, Popper.Defaults.modifiers[name] || {}, options.modifiers ? options.modifiers[name] : {});\n });\n\n // Refactoring modifiers' list (Object => Array)\n this.modifiers = Object.keys(this.options.modifiers).map(function (name) {\n return _extends({\n name: name\n }, _this.options.modifiers[name]);\n })\n // sort the modifiers by order\n .sort(function (a, b) {\n return a.order - b.order;\n });\n\n // modifiers have the ability to execute arbitrary code when Popper.js get inited\n // such code is executed in the same order of its modifier\n // they could add new properties to their options configuration\n // BE AWARE: don't add options to `options.modifiers.name` but to `modifierOptions`!\n this.modifiers.forEach(function (modifierOptions) {\n if (modifierOptions.enabled && isFunction(modifierOptions.onLoad)) {\n modifierOptions.onLoad(_this.reference, _this.popper, _this.options, modifierOptions, _this.state);\n }\n });\n\n // fire the first update to position the popper in the right place\n this.update();\n\n var eventsEnabled = this.options.eventsEnabled;\n if (eventsEnabled) {\n // setup event listeners, they will take care of update the position in specific situations\n this.enableEventListeners();\n }\n\n this.state.eventsEnabled = eventsEnabled;\n }\n\n // We can't use class properties because they don't get listed in the\n // class prototype and break stuff like Sinon stubs\n\n\n createClass(Popper, [{\n key: 'update',\n value: function update$$1() {\n return update.call(this);\n }\n }, {\n key: 'destroy',\n value: function destroy$$1() {\n return destroy.call(this);\n }\n }, {\n key: 'enableEventListeners',\n value: function enableEventListeners$$1() {\n return enableEventListeners.call(this);\n }\n }, {\n key: 'disableEventListeners',\n value: function disableEventListeners$$1() {\n return disableEventListeners.call(this);\n }\n\n /**\n * Schedules an update. It will run on the next UI update available.\n * @method scheduleUpdate\n * @memberof Popper\n */\n\n\n /**\n * Collection of utilities useful when writing custom modifiers.\n * Starting from version 1.7, this method is available only if you\n * include `popper-utils.js` before `popper.js`.\n *\n * **DEPRECATION**: This way to access PopperUtils is deprecated\n * and will be removed in v2! Use the PopperUtils module directly instead.\n * Due to the high instability of the methods contained in Utils, we can't\n * guarantee them to follow semver. Use them at your own risk!\n * @static\n * @private\n * @type {Object}\n * @deprecated since version 1.8\n * @member Utils\n * @memberof Popper\n */\n\n }]);\n return Popper;\n}();\n\n/**\n * The `referenceObject` is an object that provides an interface compatible with Popper.js\n * and lets you use it as replacement of a real DOM node.<br />\n * You can use this method to position a popper relatively to a set of coordinates\n * in case you don't have a DOM node to use as reference.\n *\n * ```\n * new Popper(referenceObject, popperNode);\n * ```\n *\n * NB: This feature isn't supported in Internet Explorer 10.\n * @name referenceObject\n * @property {Function} data.getBoundingClientRect\n * A function that returns a set of coordinates compatible with the native `getBoundingClientRect` method.\n * @property {number} data.clientWidth\n * An ES6 getter that will return the width of the virtual reference element.\n * @property {number} data.clientHeight\n * An ES6 getter that will return the height of the virtual reference element.\n */\n\n\nPopper.Utils = (typeof window !== 'undefined' ? window : global).PopperUtils;\nPopper.placements = placements;\nPopper.Defaults = Defaults;\n\nreturn Popper;\n\n})));\n//# sourceMappingURL=popper.js.map","/*!\n * Bootstrap v4.3.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n (function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('jquery'), require('popper.js')) :\n typeof define === 'function' && define.amd ? define(['exports', 'jquery', 'popper.js'], factory) :\n (global = global || self, factory(global.bootstrap = {}, global.jQuery, global.Popper));\n}(this, function (exports, $, Popper) { 'use strict';\n\n $ = $ && $.hasOwnProperty('default') ? $['default'] : $;\n Popper = Popper && Popper.hasOwnProperty('default') ? Popper['default'] : Popper;\n\n function _defineProperties(target, props) {\n for (var i = 0; i < props.length; i++) {\n var descriptor = props[i];\n descriptor.enumerable = descriptor.enumerable || false;\n descriptor.configurable = true;\n if (\"value\" in descriptor) descriptor.writable = true;\n Object.defineProperty(target, descriptor.key, descriptor);\n }\n }\n\n function _createClass(Constructor, protoProps, staticProps) {\n if (protoProps) _defineProperties(Constructor.prototype, protoProps);\n if (staticProps) _defineProperties(Constructor, staticProps);\n return Constructor;\n }\n\n function _defineProperty(obj, key, value) {\n if (key in obj) {\n Object.defineProperty(obj, key, {\n value: value,\n enumerable: true,\n configurable: true,\n writable: true\n });\n } else {\n obj[key] = value;\n }\n\n return obj;\n }\n\n function _objectSpread(target) {\n for (var i = 1; i < arguments.length; i++) {\n var source = arguments[i] != null ? arguments[i] : {};\n var ownKeys = Object.keys(source);\n\n if (typeof Object.getOwnPropertySymbols === 'function') {\n ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) {\n return Object.getOwnPropertyDescriptor(source, sym).enumerable;\n }));\n }\n\n ownKeys.forEach(function (key) {\n _defineProperty(target, key, source[key]);\n });\n }\n\n return target;\n }\n\n function _inheritsLoose(subClass, superClass) {\n subClass.prototype = Object.create(superClass.prototype);\n subClass.prototype.constructor = subClass;\n subClass.__proto__ = superClass;\n }\n\n /**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.3.1): util.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n /**\n * ------------------------------------------------------------------------\n * Private TransitionEnd Helpers\n * ------------------------------------------------------------------------\n */\n\n var TRANSITION_END = 'transitionend';\n var MAX_UID = 1000000;\n var MILLISECONDS_MULTIPLIER = 1000; // Shoutout AngusCroll (https://goo.gl/pxwQGp)\n\n function toType(obj) {\n return {}.toString.call(obj).match(/\\s([a-z]+)/i)[1].toLowerCase();\n }\n\n function getSpecialTransitionEndEvent() {\n return {\n bindType: TRANSITION_END,\n delegateType: TRANSITION_END,\n handle: function handle(event) {\n if ($(event.target).is(this)) {\n return event.handleObj.handler.apply(this, arguments); // eslint-disable-line prefer-rest-params\n }\n\n return undefined; // eslint-disable-line no-undefined\n }\n };\n }\n\n function transitionEndEmulator(duration) {\n var _this = this;\n\n var called = false;\n $(this).one(Util.TRANSITION_END, function () {\n called = true;\n });\n setTimeout(function () {\n if (!called) {\n Util.triggerTransitionEnd(_this);\n }\n }, duration);\n return this;\n }\n\n function setTransitionEndSupport() {\n $.fn.emulateTransitionEnd = transitionEndEmulator;\n $.event.special[Util.TRANSITION_END] = getSpecialTransitionEndEvent();\n }\n /**\n * --------------------------------------------------------------------------\n * Public Util Api\n * --------------------------------------------------------------------------\n */\n\n\n var Util = {\n TRANSITION_END: 'bsTransitionEnd',\n getUID: function getUID(prefix) {\n do {\n // eslint-disable-next-line no-bitwise\n prefix += ~~(Math.random() * MAX_UID); // \"~~\" acts like a faster Math.floor() here\n } while (document.getElementById(prefix));\n\n return prefix;\n },\n getSelectorFromElement: function getSelectorFromElement(element) {\n var selector = element.getAttribute('data-target');\n\n if (!selector || selector === '#') {\n var hrefAttr = element.getAttribute('href');\n selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : '';\n }\n\n try {\n return document.querySelector(selector) ? selector : null;\n } catch (err) {\n return null;\n }\n },\n getTransitionDurationFromElement: function getTransitionDurationFromElement(element) {\n if (!element) {\n return 0;\n } // Get transition-duration of the element\n\n\n var transitionDuration = $(element).css('transition-duration');\n var transitionDelay = $(element).css('transition-delay');\n var floatTransitionDuration = parseFloat(transitionDuration);\n var floatTransitionDelay = parseFloat(transitionDelay); // Return 0 if element or transition duration is not found\n\n if (!floatTransitionDuration && !floatTransitionDelay) {\n return 0;\n } // If multiple durations are defined, take the first\n\n\n transitionDuration = transitionDuration.split(',')[0];\n transitionDelay = transitionDelay.split(',')[0];\n return (parseFloat(transitionDuration) + parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER;\n },\n reflow: function reflow(element) {\n return element.offsetHeight;\n },\n triggerTransitionEnd: function triggerTransitionEnd(element) {\n $(element).trigger(TRANSITION_END);\n },\n // TODO: Remove in v5\n supportsTransitionEnd: function supportsTransitionEnd() {\n return Boolean(TRANSITION_END);\n },\n isElement: function isElement(obj) {\n return (obj[0] || obj).nodeType;\n },\n typeCheckConfig: function typeCheckConfig(componentName, config, configTypes) {\n for (var property in configTypes) {\n if (Object.prototype.hasOwnProperty.call(configTypes, property)) {\n var expectedTypes = configTypes[property];\n var value = config[property];\n var valueType = value && Util.isElement(value) ? 'element' : toType(value);\n\n if (!new RegExp(expectedTypes).test(valueType)) {\n throw new Error(componentName.toUpperCase() + \": \" + (\"Option \\\"\" + property + \"\\\" provided type \\\"\" + valueType + \"\\\" \") + (\"but expected type \\\"\" + expectedTypes + \"\\\".\"));\n }\n }\n }\n },\n findShadowRoot: function findShadowRoot(element) {\n if (!document.documentElement.attachShadow) {\n return null;\n } // Can find the shadow root otherwise it'll return the document\n\n\n if (typeof element.getRootNode === 'function') {\n var root = element.getRootNode();\n return root instanceof ShadowRoot ? root : null;\n }\n\n if (element instanceof ShadowRoot) {\n return element;\n } // when we don't find a shadow root\n\n\n if (!element.parentNode) {\n return null;\n }\n\n return Util.findShadowRoot(element.parentNode);\n }\n };\n setTransitionEndSupport();\n\n /**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\n var NAME = 'alert';\n var VERSION = '4.3.1';\n var DATA_KEY = 'bs.alert';\n var EVENT_KEY = \".\" + DATA_KEY;\n var DATA_API_KEY = '.data-api';\n var JQUERY_NO_CONFLICT = $.fn[NAME];\n var Selector = {\n DISMISS: '[data-dismiss=\"alert\"]'\n };\n var Event = {\n CLOSE: \"close\" + EVENT_KEY,\n CLOSED: \"closed\" + EVENT_KEY,\n CLICK_DATA_API: \"click\" + EVENT_KEY + DATA_API_KEY\n };\n var ClassName = {\n ALERT: 'alert',\n FADE: 'fade',\n SHOW: 'show'\n /**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\n };\n\n var Alert =\n /*#__PURE__*/\n function () {\n function Alert(element) {\n this._element = element;\n } // Getters\n\n\n var _proto = Alert.prototype;\n\n // Public\n _proto.close = function close(element) {\n var rootElement = this._element;\n\n if (element) {\n rootElement = this._getRootElement(element);\n }\n\n var customEvent = this._triggerCloseEvent(rootElement);\n\n if (customEvent.isDefaultPrevented()) {\n return;\n }\n\n this._removeElement(rootElement);\n };\n\n _proto.dispose = function dispose() {\n $.removeData(this._element, DATA_KEY);\n this._element = null;\n } // Private\n ;\n\n _proto._getRootElement = function _getRootElement(element) {\n var selector = Util.getSelectorFromElement(element);\n var parent = false;\n\n if (selector) {\n parent = document.querySelector(selector);\n }\n\n if (!parent) {\n parent = $(element).closest(\".\" + ClassName.ALERT)[0];\n }\n\n return parent;\n };\n\n _proto._triggerCloseEvent = function _triggerCloseEvent(element) {\n var closeEvent = $.Event(Event.CLOSE);\n $(element).trigger(closeEvent);\n return closeEvent;\n };\n\n _proto._removeElement = function _removeElement(element) {\n var _this = this;\n\n $(element).removeClass(ClassName.SHOW);\n\n if (!$(element).hasClass(ClassName.FADE)) {\n this._destroyElement(element);\n\n return;\n }\n\n var transitionDuration = Util.getTransitionDurationFromElement(element);\n $(element).one(Util.TRANSITION_END, function (event) {\n return _this._destroyElement(element, event);\n }).emulateTransitionEnd(transitionDuration);\n };\n\n _proto._destroyElement = function _destroyElement(element) {\n $(element).detach().trigger(Event.CLOSED).remove();\n } // Static\n ;\n\n Alert._jQueryInterface = function _jQueryInterface(config) {\n return this.each(function () {\n var $element = $(this);\n var data = $element.data(DATA_KEY);\n\n if (!data) {\n data = new Alert(this);\n $element.data(DATA_KEY, data);\n }\n\n if (config === 'close') {\n data[config](this);\n }\n });\n };\n\n Alert._handleDismiss = function _handleDismiss(alertInstance) {\n return function (event) {\n if (event) {\n event.preventDefault();\n }\n\n alertInstance.close(this);\n };\n };\n\n _createClass(Alert, null, [{\n key: \"VERSION\",\n get: function get() {\n return VERSION;\n }\n }]);\n\n return Alert;\n }();\n /**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n\n $(document).on(Event.CLICK_DATA_API, Selector.DISMISS, Alert._handleDismiss(new Alert()));\n /**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n $.fn[NAME] = Alert._jQueryInterface;\n $.fn[NAME].Constructor = Alert;\n\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT;\n return Alert._jQueryInterface;\n };\n\n /**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\n var NAME$1 = 'button';\n var VERSION$1 = '4.3.1';\n var DATA_KEY$1 = 'bs.button';\n var EVENT_KEY$1 = \".\" + DATA_KEY$1;\n var DATA_API_KEY$1 = '.data-api';\n var JQUERY_NO_CONFLICT$1 = $.fn[NAME$1];\n var ClassName$1 = {\n ACTIVE: 'active',\n BUTTON: 'btn',\n FOCUS: 'focus'\n };\n var Selector$1 = {\n DATA_TOGGLE_CARROT: '[data-toggle^=\"button\"]',\n DATA_TOGGLE: '[data-toggle=\"buttons\"]',\n INPUT: 'input:not([type=\"hidden\"])',\n ACTIVE: '.active',\n BUTTON: '.btn'\n };\n var Event$1 = {\n CLICK_DATA_API: \"click\" + EVENT_KEY$1 + DATA_API_KEY$1,\n FOCUS_BLUR_DATA_API: \"focus\" + EVENT_KEY$1 + DATA_API_KEY$1 + \" \" + (\"blur\" + EVENT_KEY$1 + DATA_API_KEY$1)\n /**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\n };\n\n var Button =\n /*#__PURE__*/\n function () {\n function Button(element) {\n this._element = element;\n } // Getters\n\n\n var _proto = Button.prototype;\n\n // Public\n _proto.toggle = function toggle() {\n var triggerChangeEvent = true;\n var addAriaPressed = true;\n var rootElement = $(this._element).closest(Selector$1.DATA_TOGGLE)[0];\n\n if (rootElement) {\n var input = this._element.querySelector(Selector$1.INPUT);\n\n if (input) {\n if (input.type === 'radio') {\n if (input.checked && this._element.classList.contains(ClassName$1.ACTIVE)) {\n triggerChangeEvent = false;\n } else {\n var activeElement = rootElement.querySelector(Selector$1.ACTIVE);\n\n if (activeElement) {\n $(activeElement).removeClass(ClassName$1.ACTIVE);\n }\n }\n }\n\n if (triggerChangeEvent) {\n if (input.hasAttribute('disabled') || rootElement.hasAttribute('disabled') || input.classList.contains('disabled') || rootElement.classList.contains('disabled')) {\n return;\n }\n\n input.checked = !this._element.classList.contains(ClassName$1.ACTIVE);\n $(input).trigger('change');\n }\n\n input.focus();\n addAriaPressed = false;\n }\n }\n\n if (addAriaPressed) {\n this._element.setAttribute('aria-pressed', !this._element.classList.contains(ClassName$1.ACTIVE));\n }\n\n if (triggerChangeEvent) {\n $(this._element).toggleClass(ClassName$1.ACTIVE);\n }\n };\n\n _proto.dispose = function dispose() {\n $.removeData(this._element, DATA_KEY$1);\n this._element = null;\n } // Static\n ;\n\n Button._jQueryInterface = function _jQueryInterface(config) {\n return this.each(function () {\n var data = $(this).data(DATA_KEY$1);\n\n if (!data) {\n data = new Button(this);\n $(this).data(DATA_KEY$1, data);\n }\n\n if (config === 'toggle') {\n data[config]();\n }\n });\n };\n\n _createClass(Button, null, [{\n key: \"VERSION\",\n get: function get() {\n return VERSION$1;\n }\n }]);\n\n return Button;\n }();\n /**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n\n $(document).on(Event$1.CLICK_DATA_API, Selector$1.DATA_TOGGLE_CARROT, function (event) {\n event.preventDefault();\n var button = event.target;\n\n if (!$(button).hasClass(ClassName$1.BUTTON)) {\n button = $(button).closest(Selector$1.BUTTON);\n }\n\n Button._jQueryInterface.call($(button), 'toggle');\n }).on(Event$1.FOCUS_BLUR_DATA_API, Selector$1.DATA_TOGGLE_CARROT, function (event) {\n var button = $(event.target).closest(Selector$1.BUTTON)[0];\n $(button).toggleClass(ClassName$1.FOCUS, /^focus(in)?$/.test(event.type));\n });\n /**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n $.fn[NAME$1] = Button._jQueryInterface;\n $.fn[NAME$1].Constructor = Button;\n\n $.fn[NAME$1].noConflict = function () {\n $.fn[NAME$1] = JQUERY_NO_CONFLICT$1;\n return Button._jQueryInterface;\n };\n\n /**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\n var NAME$2 = 'carousel';\n var VERSION$2 = '4.3.1';\n var DATA_KEY$2 = 'bs.carousel';\n var EVENT_KEY$2 = \".\" + DATA_KEY$2;\n var DATA_API_KEY$2 = '.data-api';\n var JQUERY_NO_CONFLICT$2 = $.fn[NAME$2];\n var ARROW_LEFT_KEYCODE = 37; // KeyboardEvent.which value for left arrow key\n\n var ARROW_RIGHT_KEYCODE = 39; // KeyboardEvent.which value for right arrow key\n\n var TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch\n\n var SWIPE_THRESHOLD = 40;\n var Default = {\n interval: 5000,\n keyboard: true,\n slide: false,\n pause: 'hover',\n wrap: true,\n touch: true\n };\n var DefaultType = {\n interval: '(number|boolean)',\n keyboard: 'boolean',\n slide: '(boolean|string)',\n pause: '(string|boolean)',\n wrap: 'boolean',\n touch: 'boolean'\n };\n var Direction = {\n NEXT: 'next',\n PREV: 'prev',\n LEFT: 'left',\n RIGHT: 'right'\n };\n var Event$2 = {\n SLIDE: \"slide\" + EVENT_KEY$2,\n SLID: \"slid\" + EVENT_KEY$2,\n KEYDOWN: \"keydown\" + EVENT_KEY$2,\n MOUSEENTER: \"mouseenter\" + EVENT_KEY$2,\n MOUSELEAVE: \"mouseleave\" + EVENT_KEY$2,\n TOUCHSTART: \"touchstart\" + EVENT_KEY$2,\n TOUCHMOVE: \"touchmove\" + EVENT_KEY$2,\n TOUCHEND: \"touchend\" + EVENT_KEY$2,\n POINTERDOWN: \"pointerdown\" + EVENT_KEY$2,\n POINTERUP: \"pointerup\" + EVENT_KEY$2,\n DRAG_START: \"dragstart\" + EVENT_KEY$2,\n LOAD_DATA_API: \"load\" + EVENT_KEY$2 + DATA_API_KEY$2,\n CLICK_DATA_API: \"click\" + EVENT_KEY$2 + DATA_API_KEY$2\n };\n var ClassName$2 = {\n CAROUSEL: 'carousel',\n ACTIVE: 'active',\n SLIDE: 'slide',\n RIGHT: 'carousel-item-right',\n LEFT: 'carousel-item-left',\n NEXT: 'carousel-item-next',\n PREV: 'carousel-item-prev',\n ITEM: 'carousel-item',\n POINTER_EVENT: 'pointer-event'\n };\n var Selector$2 = {\n ACTIVE: '.active',\n ACTIVE_ITEM: '.active.carousel-item',\n ITEM: '.carousel-item',\n ITEM_IMG: '.carousel-item img',\n NEXT_PREV: '.carousel-item-next, .carousel-item-prev',\n INDICATORS: '.carousel-indicators',\n DATA_SLIDE: '[data-slide], [data-slide-to]',\n DATA_RIDE: '[data-ride=\"carousel\"]'\n };\n var PointerType = {\n TOUCH: 'touch',\n PEN: 'pen'\n /**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\n };\n\n var Carousel =\n /*#__PURE__*/\n function () {\n function Carousel(element, config) {\n this._items = null;\n this._interval = null;\n this._activeElement = null;\n this._isPaused = false;\n this._isSliding = false;\n this.touchTimeout = null;\n this.touchStartX = 0;\n this.touchDeltaX = 0;\n this._config = this._getConfig(config);\n this._element = element;\n this._indicatorsElement = this._element.querySelector(Selector$2.INDICATORS);\n this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0;\n this._pointerEvent = Boolean(window.PointerEvent || window.MSPointerEvent);\n\n this._addEventListeners();\n } // Getters\n\n\n var _proto = Carousel.prototype;\n\n // Public\n _proto.next = function next() {\n if (!this._isSliding) {\n this._slide(Direction.NEXT);\n }\n };\n\n _proto.nextWhenVisible = function nextWhenVisible() {\n // Don't call next when the page isn't visible\n // or the carousel or its parent isn't visible\n if (!document.hidden && $(this._element).is(':visible') && $(this._element).css('visibility') !== 'hidden') {\n this.next();\n }\n };\n\n _proto.prev = function prev() {\n if (!this._isSliding) {\n this._slide(Direction.PREV);\n }\n };\n\n _proto.pause = function pause(event) {\n if (!event) {\n this._isPaused = true;\n }\n\n if (this._element.querySelector(Selector$2.NEXT_PREV)) {\n Util.triggerTransitionEnd(this._element);\n this.cycle(true);\n }\n\n clearInterval(this._interval);\n this._interval = null;\n };\n\n _proto.cycle = function cycle(event) {\n if (!event) {\n this._isPaused = false;\n }\n\n if (this._interval) {\n clearInterval(this._interval);\n this._interval = null;\n }\n\n if (this._config.interval && !this._isPaused) {\n this._interval = setInterval((document.visibilityState ? this.nextWhenVisible : this.next).bind(this), this._config.interval);\n }\n };\n\n _proto.to = function to(index) {\n var _this = this;\n\n this._activeElement = this._element.querySelector(Selector$2.ACTIVE_ITEM);\n\n var activeIndex = this._getItemIndex(this._activeElement);\n\n if (index > this._items.length - 1 || index < 0) {\n return;\n }\n\n if (this._isSliding) {\n $(this._element).one(Event$2.SLID, function () {\n return _this.to(index);\n });\n return;\n }\n\n if (activeIndex === index) {\n this.pause();\n this.cycle();\n return;\n }\n\n var direction = index > activeIndex ? Direction.NEXT : Direction.PREV;\n\n this._slide(direction, this._items[index]);\n };\n\n _proto.dispose = function dispose() {\n $(this._element).off(EVENT_KEY$2);\n $.removeData(this._element, DATA_KEY$2);\n this._items = null;\n this._config = null;\n this._element = null;\n this._interval = null;\n this._isPaused = null;\n this._isSliding = null;\n this._activeElement = null;\n this._indicatorsElement = null;\n } // Private\n ;\n\n _proto._getConfig = function _getConfig(config) {\n config = _objectSpread({}, Default, config);\n Util.typeCheckConfig(NAME$2, config, DefaultType);\n return config;\n };\n\n _proto._handleSwipe = function _handleSwipe() {\n var absDeltax = Math.abs(this.touchDeltaX);\n\n if (absDeltax <= SWIPE_THRESHOLD) {\n return;\n }\n\n var direction = absDeltax / this.touchDeltaX; // swipe left\n\n if (direction > 0) {\n this.prev();\n } // swipe right\n\n\n if (direction < 0) {\n this.next();\n }\n };\n\n _proto._addEventListeners = function _addEventListeners() {\n var _this2 = this;\n\n if (this._config.keyboard) {\n $(this._element).on(Event$2.KEYDOWN, function (event) {\n return _this2._keydown(event);\n });\n }\n\n if (this._config.pause === 'hover') {\n $(this._element).on(Event$2.MOUSEENTER, function (event) {\n return _this2.pause(event);\n }).on(Event$2.MOUSELEAVE, function (event) {\n return _this2.cycle(event);\n });\n }\n\n if (this._config.touch) {\n this._addTouchEventListeners();\n }\n };\n\n _proto._addTouchEventListeners = function _addTouchEventListeners() {\n var _this3 = this;\n\n if (!this._touchSupported) {\n return;\n }\n\n var start = function start(event) {\n if (_this3._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) {\n _this3.touchStartX = event.originalEvent.clientX;\n } else if (!_this3._pointerEvent) {\n _this3.touchStartX = event.originalEvent.touches[0].clientX;\n }\n };\n\n var move = function move(event) {\n // ensure swiping with one touch and not pinching\n if (event.originalEvent.touches && event.originalEvent.touches.length > 1) {\n _this3.touchDeltaX = 0;\n } else {\n _this3.touchDeltaX = event.originalEvent.touches[0].clientX - _this3.touchStartX;\n }\n };\n\n var end = function end(event) {\n if (_this3._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) {\n _this3.touchDeltaX = event.originalEvent.clientX - _this3.touchStartX;\n }\n\n _this3._handleSwipe();\n\n if (_this3._config.pause === 'hover') {\n // If it's a touch-enabled device, mouseenter/leave are fired as\n // part of the mouse compatibility events on first tap - the carousel\n // would stop cycling until user tapped out of it;\n // here, we listen for touchend, explicitly pause the carousel\n // (as if it's the second time we tap on it, mouseenter compat event\n // is NOT fired) and after a timeout (to allow for mouse compatibility\n // events to fire) we explicitly restart cycling\n _this3.pause();\n\n if (_this3.touchTimeout) {\n clearTimeout(_this3.touchTimeout);\n }\n\n _this3.touchTimeout = setTimeout(function (event) {\n return _this3.cycle(event);\n }, TOUCHEVENT_COMPAT_WAIT + _this3._config.interval);\n }\n };\n\n $(this._element.querySelectorAll(Selector$2.ITEM_IMG)).on(Event$2.DRAG_START, function (e) {\n return e.preventDefault();\n });\n\n if (this._pointerEvent) {\n $(this._element).on(Event$2.POINTERDOWN, function (event) {\n return start(event);\n });\n $(this._element).on(Event$2.POINTERUP, function (event) {\n return end(event);\n });\n\n this._element.classList.add(ClassName$2.POINTER_EVENT);\n } else {\n $(this._element).on(Event$2.TOUCHSTART, function (event) {\n return start(event);\n });\n $(this._element).on(Event$2.TOUCHMOVE, function (event) {\n return move(event);\n });\n $(this._element).on(Event$2.TOUCHEND, function (event) {\n return end(event);\n });\n }\n };\n\n _proto._keydown = function _keydown(event) {\n if (/input|textarea/i.test(event.target.tagName)) {\n return;\n }\n\n switch (event.which) {\n case ARROW_LEFT_KEYCODE:\n event.preventDefault();\n this.prev();\n break;\n\n case ARROW_RIGHT_KEYCODE:\n event.preventDefault();\n this.next();\n break;\n\n default:\n }\n };\n\n _proto._getItemIndex = function _getItemIndex(element) {\n this._items = element && element.parentNode ? [].slice.call(element.parentNode.querySelectorAll(Selector$2.ITEM)) : [];\n return this._items.indexOf(element);\n };\n\n _proto._getItemByDirection = function _getItemByDirection(direction, activeElement) {\n var isNextDirection = direction === Direction.NEXT;\n var isPrevDirection = direction === Direction.PREV;\n\n var activeIndex = this._getItemIndex(activeElement);\n\n var lastItemIndex = this._items.length - 1;\n var isGoingToWrap = isPrevDirection && activeIndex === 0 || isNextDirection && activeIndex === lastItemIndex;\n\n if (isGoingToWrap && !this._config.wrap) {\n return activeElement;\n }\n\n var delta = direction === Direction.PREV ? -1 : 1;\n var itemIndex = (activeIndex + delta) % this._items.length;\n return itemIndex === -1 ? this._items[this._items.length - 1] : this._items[itemIndex];\n };\n\n _proto._triggerSlideEvent = function _triggerSlideEvent(relatedTarget, eventDirectionName) {\n var targetIndex = this._getItemIndex(relatedTarget);\n\n var fromIndex = this._getItemIndex(this._element.querySelector(Selector$2.ACTIVE_ITEM));\n\n var slideEvent = $.Event(Event$2.SLIDE, {\n relatedTarget: relatedTarget,\n direction: eventDirectionName,\n from: fromIndex,\n to: targetIndex\n });\n $(this._element).trigger(slideEvent);\n return slideEvent;\n };\n\n _proto._setActiveIndicatorElement = function _setActiveIndicatorElement(element) {\n if (this._indicatorsElement) {\n var indicators = [].slice.call(this._indicatorsElement.querySelectorAll(Selector$2.ACTIVE));\n $(indicators).removeClass(ClassName$2.ACTIVE);\n\n var nextIndicator = this._indicatorsElement.children[this._getItemIndex(element)];\n\n if (nextIndicator) {\n $(nextIndicator).addClass(ClassName$2.ACTIVE);\n }\n }\n };\n\n _proto._slide = function _slide(direction, element) {\n var _this4 = this;\n\n var activeElement = this._element.querySelector(Selector$2.ACTIVE_ITEM);\n\n var activeElementIndex = this._getItemIndex(activeElement);\n\n var nextElement = element || activeElement && this._getItemByDirection(direction, activeElement);\n\n var nextElementIndex = this._getItemIndex(nextElement);\n\n var isCycling = Boolean(this._interval);\n var directionalClassName;\n var orderClassName;\n var eventDirectionName;\n\n if (direction === Direction.NEXT) {\n directionalClassName = ClassName$2.LEFT;\n orderClassName = ClassName$2.NEXT;\n eventDirectionName = Direction.LEFT;\n } else {\n directionalClassName = ClassName$2.RIGHT;\n orderClassName = ClassName$2.PREV;\n eventDirectionName = Direction.RIGHT;\n }\n\n if (nextElement && $(nextElement).hasClass(ClassName$2.ACTIVE)) {\n this._isSliding = false;\n return;\n }\n\n var slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName);\n\n if (slideEvent.isDefaultPrevented()) {\n return;\n }\n\n if (!activeElement || !nextElement) {\n // Some weirdness is happening, so we bail\n return;\n }\n\n this._isSliding = true;\n\n if (isCycling) {\n this.pause();\n }\n\n this._setActiveIndicatorElement(nextElement);\n\n var slidEvent = $.Event(Event$2.SLID, {\n relatedTarget: nextElement,\n direction: eventDirectionName,\n from: activeElementIndex,\n to: nextElementIndex\n });\n\n if ($(this._element).hasClass(ClassName$2.SLIDE)) {\n $(nextElement).addClass(orderClassName);\n Util.reflow(nextElement);\n $(activeElement).addClass(directionalClassName);\n $(nextElement).addClass(directionalClassName);\n var nextElementInterval = parseInt(nextElement.getAttribute('data-interval'), 10);\n\n if (nextElementInterval) {\n this._config.defaultInterval = this._config.defaultInterval || this._config.interval;\n this._config.interval = nextElementInterval;\n } else {\n this._config.interval = this._config.defaultInterval || this._config.interval;\n }\n\n var transitionDuration = Util.getTransitionDurationFromElement(activeElement);\n $(activeElement).one(Util.TRANSITION_END, function () {\n $(nextElement).removeClass(directionalClassName + \" \" + orderClassName).addClass(ClassName$2.ACTIVE);\n $(activeElement).removeClass(ClassName$2.ACTIVE + \" \" + orderClassName + \" \" + directionalClassName);\n _this4._isSliding = false;\n setTimeout(function () {\n return $(_this4._element).trigger(slidEvent);\n }, 0);\n }).emulateTransitionEnd(transitionDuration);\n } else {\n $(activeElement).removeClass(ClassName$2.ACTIVE);\n $(nextElement).addClass(ClassName$2.ACTIVE);\n this._isSliding = false;\n $(this._element).trigger(slidEvent);\n }\n\n if (isCycling) {\n this.cycle();\n }\n } // Static\n ;\n\n Carousel._jQueryInterface = function _jQueryInterface(config) {\n return this.each(function () {\n var data = $(this).data(DATA_KEY$2);\n\n var _config = _objectSpread({}, Default, $(this).data());\n\n if (typeof config === 'object') {\n _config = _objectSpread({}, _config, config);\n }\n\n var action = typeof config === 'string' ? config : _config.slide;\n\n if (!data) {\n data = new Carousel(this, _config);\n $(this).data(DATA_KEY$2, data);\n }\n\n if (typeof config === 'number') {\n data.to(config);\n } else if (typeof action === 'string') {\n if (typeof data[action] === 'undefined') {\n throw new TypeError(\"No method named \\\"\" + action + \"\\\"\");\n }\n\n data[action]();\n } else if (_config.interval && _config.ride) {\n data.pause();\n data.cycle();\n }\n });\n };\n\n Carousel._dataApiClickHandler = function _dataApiClickHandler(event) {\n var selector = Util.getSelectorFromElement(this);\n\n if (!selector) {\n return;\n }\n\n var target = $(selector)[0];\n\n if (!target || !$(target).hasClass(ClassName$2.CAROUSEL)) {\n return;\n }\n\n var config = _objectSpread({}, $(target).data(), $(this).data());\n\n var slideIndex = this.getAttribute('data-slide-to');\n\n if (slideIndex) {\n config.interval = false;\n }\n\n Carousel._jQueryInterface.call($(target), config);\n\n if (slideIndex) {\n $(target).data(DATA_KEY$2).to(slideIndex);\n }\n\n event.preventDefault();\n };\n\n _createClass(Carousel, null, [{\n key: \"VERSION\",\n get: function get() {\n return VERSION$2;\n }\n }, {\n key: \"Default\",\n get: function get() {\n return Default;\n }\n }]);\n\n return Carousel;\n }();\n /**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n\n $(document).on(Event$2.CLICK_DATA_API, Selector$2.DATA_SLIDE, Carousel._dataApiClickHandler);\n $(window).on(Event$2.LOAD_DATA_API, function () {\n var carousels = [].slice.call(document.querySelectorAll(Selector$2.DATA_RIDE));\n\n for (var i = 0, len = carousels.length; i < len; i++) {\n var $carousel = $(carousels[i]);\n\n Carousel._jQueryInterface.call($carousel, $carousel.data());\n }\n });\n /**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n $.fn[NAME$2] = Carousel._jQueryInterface;\n $.fn[NAME$2].Constructor = Carousel;\n\n $.fn[NAME$2].noConflict = function () {\n $.fn[NAME$2] = JQUERY_NO_CONFLICT$2;\n return Carousel._jQueryInterface;\n };\n\n /**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\n var NAME$3 = 'collapse';\n var VERSION$3 = '4.3.1';\n var DATA_KEY$3 = 'bs.collapse';\n var EVENT_KEY$3 = \".\" + DATA_KEY$3;\n var DATA_API_KEY$3 = '.data-api';\n var JQUERY_NO_CONFLICT$3 = $.fn[NAME$3];\n var Default$1 = {\n toggle: true,\n parent: ''\n };\n var DefaultType$1 = {\n toggle: 'boolean',\n parent: '(string|element)'\n };\n var Event$3 = {\n SHOW: \"show\" + EVENT_KEY$3,\n SHOWN: \"shown\" + EVENT_KEY$3,\n HIDE: \"hide\" + EVENT_KEY$3,\n HIDDEN: \"hidden\" + EVENT_KEY$3,\n CLICK_DATA_API: \"click\" + EVENT_KEY$3 + DATA_API_KEY$3\n };\n var ClassName$3 = {\n SHOW: 'show',\n COLLAPSE: 'collapse',\n COLLAPSING: 'collapsing',\n COLLAPSED: 'collapsed'\n };\n var Dimension = {\n WIDTH: 'width',\n HEIGHT: 'height'\n };\n var Selector$3 = {\n ACTIVES: '.show, .collapsing',\n DATA_TOGGLE: '[data-toggle=\"collapse\"]'\n /**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\n };\n\n var Collapse =\n /*#__PURE__*/\n function () {\n function Collapse(element, config) {\n this._isTransitioning = false;\n this._element = element;\n this._config = this._getConfig(config);\n this._triggerArray = [].slice.call(document.querySelectorAll(\"[data-toggle=\\\"collapse\\\"][href=\\\"#\" + element.id + \"\\\"],\" + (\"[data-toggle=\\\"collapse\\\"][data-target=\\\"#\" + element.id + \"\\\"]\")));\n var toggleList = [].slice.call(document.querySelectorAll(Selector$3.DATA_TOGGLE));\n\n for (var i = 0, len = toggleList.length; i < len; i++) {\n var elem = toggleList[i];\n var selector = Util.getSelectorFromElement(elem);\n var filterElement = [].slice.call(document.querySelectorAll(selector)).filter(function (foundElem) {\n return foundElem === element;\n });\n\n if (selector !== null && filterElement.length > 0) {\n this._selector = selector;\n\n this._triggerArray.push(elem);\n }\n }\n\n this._parent = this._config.parent ? this._getParent() : null;\n\n if (!this._config.parent) {\n this._addAriaAndCollapsedClass(this._element, this._triggerArray);\n }\n\n if (this._config.toggle) {\n this.toggle();\n }\n } // Getters\n\n\n var _proto = Collapse.prototype;\n\n // Public\n _proto.toggle = function toggle() {\n if ($(this._element).hasClass(ClassName$3.SHOW)) {\n this.hide();\n } else {\n this.show();\n }\n };\n\n _proto.show = function show() {\n var _this = this;\n\n if (this._isTransitioning || $(this._element).hasClass(ClassName$3.SHOW)) {\n return;\n }\n\n var actives;\n var activesData;\n\n if (this._parent) {\n actives = [].slice.call(this._parent.querySelectorAll(Selector$3.ACTIVES)).filter(function (elem) {\n if (typeof _this._config.parent === 'string') {\n return elem.getAttribute('data-parent') === _this._config.parent;\n }\n\n return elem.classList.contains(ClassName$3.COLLAPSE);\n });\n\n if (actives.length === 0) {\n actives = null;\n }\n }\n\n if (actives) {\n activesData = $(actives).not(this._selector).data(DATA_KEY$3);\n\n if (activesData && activesData._isTransitioning) {\n return;\n }\n }\n\n var startEvent = $.Event(Event$3.SHOW);\n $(this._element).trigger(startEvent);\n\n if (startEvent.isDefaultPrevented()) {\n return;\n }\n\n if (actives) {\n Collapse._jQueryInterface.call($(actives).not(this._selector), 'hide');\n\n if (!activesData) {\n $(actives).data(DATA_KEY$3, null);\n }\n }\n\n var dimension = this._getDimension();\n\n $(this._element).removeClass(ClassName$3.COLLAPSE).addClass(ClassName$3.COLLAPSING);\n this._element.style[dimension] = 0;\n\n if (this._triggerArray.length) {\n $(this._triggerArray).removeClass(ClassName$3.COLLAPSED).attr('aria-expanded', true);\n }\n\n this.setTransitioning(true);\n\n var complete = function complete() {\n $(_this._element).removeClass(ClassName$3.COLLAPSING).addClass(ClassName$3.COLLAPSE).addClass(ClassName$3.SHOW);\n _this._element.style[dimension] = '';\n\n _this.setTransitioning(false);\n\n $(_this._element).trigger(Event$3.SHOWN);\n };\n\n var capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1);\n var scrollSize = \"scroll\" + capitalizedDimension;\n var transitionDuration = Util.getTransitionDurationFromElement(this._element);\n $(this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);\n this._element.style[dimension] = this._element[scrollSize] + \"px\";\n };\n\n _proto.hide = function hide() {\n var _this2 = this;\n\n if (this._isTransitioning || !$(this._element).hasClass(ClassName$3.SHOW)) {\n return;\n }\n\n var startEvent = $.Event(Event$3.HIDE);\n $(this._element).trigger(startEvent);\n\n if (startEvent.isDefaultPrevented()) {\n return;\n }\n\n var dimension = this._getDimension();\n\n this._element.style[dimension] = this._element.getBoundingClientRect()[dimension] + \"px\";\n Util.reflow(this._element);\n $(this._element).addClass(ClassName$3.COLLAPSING).removeClass(ClassName$3.COLLAPSE).removeClass(ClassName$3.SHOW);\n var triggerArrayLength = this._triggerArray.length;\n\n if (triggerArrayLength > 0) {\n for (var i = 0; i < triggerArrayLength; i++) {\n var trigger = this._triggerArray[i];\n var selector = Util.getSelectorFromElement(trigger);\n\n if (selector !== null) {\n var $elem = $([].slice.call(document.querySelectorAll(selector)));\n\n if (!$elem.hasClass(ClassName$3.SHOW)) {\n $(trigger).addClass(ClassName$3.COLLAPSED).attr('aria-expanded', false);\n }\n }\n }\n }\n\n this.setTransitioning(true);\n\n var complete = function complete() {\n _this2.setTransitioning(false);\n\n $(_this2._element).removeClass(ClassName$3.COLLAPSING).addClass(ClassName$3.COLLAPSE).trigger(Event$3.HIDDEN);\n };\n\n this._element.style[dimension] = '';\n var transitionDuration = Util.getTransitionDurationFromElement(this._element);\n $(this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);\n };\n\n _proto.setTransitioning = function setTransitioning(isTransitioning) {\n this._isTransitioning = isTransitioning;\n };\n\n _proto.dispose = function dispose() {\n $.removeData(this._element, DATA_KEY$3);\n this._config = null;\n this._parent = null;\n this._element = null;\n this._triggerArray = null;\n this._isTransitioning = null;\n } // Private\n ;\n\n _proto._getConfig = function _getConfig(config) {\n config = _objectSpread({}, Default$1, config);\n config.toggle = Boolean(config.toggle); // Coerce string values\n\n Util.typeCheckConfig(NAME$3, config, DefaultType$1);\n return config;\n };\n\n _proto._getDimension = function _getDimension() {\n var hasWidth = $(this._element).hasClass(Dimension.WIDTH);\n return hasWidth ? Dimension.WIDTH : Dimension.HEIGHT;\n };\n\n _proto._getParent = function _getParent() {\n var _this3 = this;\n\n var parent;\n\n if (Util.isElement(this._config.parent)) {\n parent = this._config.parent; // It's a jQuery object\n\n if (typeof this._config.parent.jquery !== 'undefined') {\n parent = this._config.parent[0];\n }\n } else {\n parent = document.querySelector(this._config.parent);\n }\n\n var selector = \"[data-toggle=\\\"collapse\\\"][data-parent=\\\"\" + this._config.parent + \"\\\"]\";\n var children = [].slice.call(parent.querySelectorAll(selector));\n $(children).each(function (i, element) {\n _this3._addAriaAndCollapsedClass(Collapse._getTargetFromElement(element), [element]);\n });\n return parent;\n };\n\n _proto._addAriaAndCollapsedClass = function _addAriaAndCollapsedClass(element, triggerArray) {\n var isOpen = $(element).hasClass(ClassName$3.SHOW);\n\n if (triggerArray.length) {\n $(triggerArray).toggleClass(ClassName$3.COLLAPSED, !isOpen).attr('aria-expanded', isOpen);\n }\n } // Static\n ;\n\n Collapse._getTargetFromElement = function _getTargetFromElement(element) {\n var selector = Util.getSelectorFromElement(element);\n return selector ? document.querySelector(selector) : null;\n };\n\n Collapse._jQueryInterface = function _jQueryInterface(config) {\n return this.each(function () {\n var $this = $(this);\n var data = $this.data(DATA_KEY$3);\n\n var _config = _objectSpread({}, Default$1, $this.data(), typeof config === 'object' && config ? config : {});\n\n if (!data && _config.toggle && /show|hide/.test(config)) {\n _config.toggle = false;\n }\n\n if (!data) {\n data = new Collapse(this, _config);\n $this.data(DATA_KEY$3, data);\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(\"No method named \\\"\" + config + \"\\\"\");\n }\n\n data[config]();\n }\n });\n };\n\n _createClass(Collapse, null, [{\n key: \"VERSION\",\n get: function get() {\n return VERSION$3;\n }\n }, {\n key: \"Default\",\n get: function get() {\n return Default$1;\n }\n }]);\n\n return Collapse;\n }();\n /**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n\n $(document).on(Event$3.CLICK_DATA_API, Selector$3.DATA_TOGGLE, function (event) {\n // preventDefault only for <a> elements (which change the URL) not inside the collapsible element\n if (event.currentTarget.tagName === 'A') {\n event.preventDefault();\n }\n\n var $trigger = $(this);\n var selector = Util.getSelectorFromElement(this);\n var selectors = [].slice.call(document.querySelectorAll(selector));\n $(selectors).each(function () {\n var $target = $(this);\n var data = $target.data(DATA_KEY$3);\n var config = data ? 'toggle' : $trigger.data();\n\n Collapse._jQueryInterface.call($target, config);\n });\n });\n /**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n $.fn[NAME$3] = Collapse._jQueryInterface;\n $.fn[NAME$3].Constructor = Collapse;\n\n $.fn[NAME$3].noConflict = function () {\n $.fn[NAME$3] = JQUERY_NO_CONFLICT$3;\n return Collapse._jQueryInterface;\n };\n\n /**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\n var NAME$4 = 'dropdown';\n var VERSION$4 = '4.3.1';\n var DATA_KEY$4 = 'bs.dropdown';\n var EVENT_KEY$4 = \".\" + DATA_KEY$4;\n var DATA_API_KEY$4 = '.data-api';\n var JQUERY_NO_CONFLICT$4 = $.fn[NAME$4];\n var ESCAPE_KEYCODE = 27; // KeyboardEvent.which value for Escape (Esc) key\n\n var SPACE_KEYCODE = 32; // KeyboardEvent.which value for space key\n\n var TAB_KEYCODE = 9; // KeyboardEvent.which value for tab key\n\n var ARROW_UP_KEYCODE = 38; // KeyboardEvent.which value for up arrow key\n\n var ARROW_DOWN_KEYCODE = 40; // KeyboardEvent.which value for down arrow key\n\n var RIGHT_MOUSE_BUTTON_WHICH = 3; // MouseEvent.which value for the right button (assuming a right-handed mouse)\n\n var REGEXP_KEYDOWN = new RegExp(ARROW_UP_KEYCODE + \"|\" + ARROW_DOWN_KEYCODE + \"|\" + ESCAPE_KEYCODE);\n var Event$4 = {\n HIDE: \"hide\" + EVENT_KEY$4,\n HIDDEN: \"hidden\" + EVENT_KEY$4,\n SHOW: \"show\" + EVENT_KEY$4,\n SHOWN: \"shown\" + EVENT_KEY$4,\n CLICK: \"click\" + EVENT_KEY$4,\n CLICK_DATA_API: \"click\" + EVENT_KEY$4 + DATA_API_KEY$4,\n KEYDOWN_DATA_API: \"keydown\" + EVENT_KEY$4 + DATA_API_KEY$4,\n KEYUP_DATA_API: \"keyup\" + EVENT_KEY$4 + DATA_API_KEY$4\n };\n var ClassName$4 = {\n DISABLED: 'disabled',\n SHOW: 'show',\n DROPUP: 'dropup',\n DROPRIGHT: 'dropright',\n DROPLEFT: 'dropleft',\n MENURIGHT: 'dropdown-menu-right',\n MENULEFT: 'dropdown-menu-left',\n POSITION_STATIC: 'position-static'\n };\n var Selector$4 = {\n DATA_TOGGLE: '[data-toggle=\"dropdown\"]',\n FORM_CHILD: '.dropdown form',\n MENU: '.dropdown-menu',\n NAVBAR_NAV: '.navbar-nav',\n VISIBLE_ITEMS: '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'\n };\n var AttachmentMap = {\n TOP: 'top-start',\n TOPEND: 'top-end',\n BOTTOM: 'bottom-start',\n BOTTOMEND: 'bottom-end',\n RIGHT: 'right-start',\n RIGHTEND: 'right-end',\n LEFT: 'left-start',\n LEFTEND: 'left-end'\n };\n var Default$2 = {\n offset: 0,\n flip: true,\n boundary: 'scrollParent',\n reference: 'toggle',\n display: 'dynamic'\n };\n var DefaultType$2 = {\n offset: '(number|string|function)',\n flip: 'boolean',\n boundary: '(string|element)',\n reference: '(string|element)',\n display: 'string'\n /**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\n };\n\n var Dropdown =\n /*#__PURE__*/\n function () {\n function Dropdown(element, config) {\n this._element = element;\n this._popper = null;\n this._config = this._getConfig(config);\n this._menu = this._getMenuElement();\n this._inNavbar = this._detectNavbar();\n\n this._addEventListeners();\n } // Getters\n\n\n var _proto = Dropdown.prototype;\n\n // Public\n _proto.toggle = function toggle() {\n if (this._element.disabled || $(this._element).hasClass(ClassName$4.DISABLED)) {\n return;\n }\n\n var parent = Dropdown._getParentFromElement(this._element);\n\n var isActive = $(this._menu).hasClass(ClassName$4.SHOW);\n\n Dropdown._clearMenus();\n\n if (isActive) {\n return;\n }\n\n var relatedTarget = {\n relatedTarget: this._element\n };\n var showEvent = $.Event(Event$4.SHOW, relatedTarget);\n $(parent).trigger(showEvent);\n\n if (showEvent.isDefaultPrevented()) {\n return;\n } // Disable totally Popper.js for Dropdown in Navbar\n\n\n if (!this._inNavbar) {\n /**\n * Check for Popper dependency\n * Popper - https://popper.js.org\n */\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s dropdowns require Popper.js (https://popper.js.org/)');\n }\n\n var referenceElement = this._element;\n\n if (this._config.reference === 'parent') {\n referenceElement = parent;\n } else if (Util.isElement(this._config.reference)) {\n referenceElement = this._config.reference; // Check if it's jQuery element\n\n if (typeof this._config.reference.jquery !== 'undefined') {\n referenceElement = this._config.reference[0];\n }\n } // If boundary is not `scrollParent`, then set position to `static`\n // to allow the menu to \"escape\" the scroll parent's boundaries\n // https://github.com/twbs/bootstrap/issues/24251\n\n\n if (this._config.boundary !== 'scrollParent') {\n $(parent).addClass(ClassName$4.POSITION_STATIC);\n }\n\n this._popper = new Popper(referenceElement, this._menu, this._getPopperConfig());\n } // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n\n\n if ('ontouchstart' in document.documentElement && $(parent).closest(Selector$4.NAVBAR_NAV).length === 0) {\n $(document.body).children().on('mouseover', null, $.noop);\n }\n\n this._element.focus();\n\n this._element.setAttribute('aria-expanded', true);\n\n $(this._menu).toggleClass(ClassName$4.SHOW);\n $(parent).toggleClass(ClassName$4.SHOW).trigger($.Event(Event$4.SHOWN, relatedTarget));\n };\n\n _proto.show = function show() {\n if (this._element.disabled || $(this._element).hasClass(ClassName$4.DISABLED) || $(this._menu).hasClass(ClassName$4.SHOW)) {\n return;\n }\n\n var relatedTarget = {\n relatedTarget: this._element\n };\n var showEvent = $.Event(Event$4.SHOW, relatedTarget);\n\n var parent = Dropdown._getParentFromElement(this._element);\n\n $(parent).trigger(showEvent);\n\n if (showEvent.isDefaultPrevented()) {\n return;\n }\n\n $(this._menu).toggleClass(ClassName$4.SHOW);\n $(parent).toggleClass(ClassName$4.SHOW).trigger($.Event(Event$4.SHOWN, relatedTarget));\n };\n\n _proto.hide = function hide() {\n if (this._element.disabled || $(this._element).hasClass(ClassName$4.DISABLED) || !$(this._menu).hasClass(ClassName$4.SHOW)) {\n return;\n }\n\n var relatedTarget = {\n relatedTarget: this._element\n };\n var hideEvent = $.Event(Event$4.HIDE, relatedTarget);\n\n var parent = Dropdown._getParentFromElement(this._element);\n\n $(parent).trigger(hideEvent);\n\n if (hideEvent.isDefaultPrevented()) {\n return;\n }\n\n $(this._menu).toggleClass(ClassName$4.SHOW);\n $(parent).toggleClass(ClassName$4.SHOW).trigger($.Event(Event$4.HIDDEN, relatedTarget));\n };\n\n _proto.dispose = function dispose() {\n $.removeData(this._element, DATA_KEY$4);\n $(this._element).off(EVENT_KEY$4);\n this._element = null;\n this._menu = null;\n\n if (this._popper !== null) {\n this._popper.destroy();\n\n this._popper = null;\n }\n };\n\n _proto.update = function update() {\n this._inNavbar = this._detectNavbar();\n\n if (this._popper !== null) {\n this._popper.scheduleUpdate();\n }\n } // Private\n ;\n\n _proto._addEventListeners = function _addEventListeners() {\n var _this = this;\n\n $(this._element).on(Event$4.CLICK, function (event) {\n event.preventDefault();\n event.stopPropagation();\n\n _this.toggle();\n });\n };\n\n _proto._getConfig = function _getConfig(config) {\n config = _objectSpread({}, this.constructor.Default, $(this._element).data(), config);\n Util.typeCheckConfig(NAME$4, config, this.constructor.DefaultType);\n return config;\n };\n\n _proto._getMenuElement = function _getMenuElement() {\n if (!this._menu) {\n var parent = Dropdown._getParentFromElement(this._element);\n\n if (parent) {\n this._menu = parent.querySelector(Selector$4.MENU);\n }\n }\n\n return this._menu;\n };\n\n _proto._getPlacement = function _getPlacement() {\n var $parentDropdown = $(this._element.parentNode);\n var placement = AttachmentMap.BOTTOM; // Handle dropup\n\n if ($parentDropdown.hasClass(ClassName$4.DROPUP)) {\n placement = AttachmentMap.TOP;\n\n if ($(this._menu).hasClass(ClassName$4.MENURIGHT)) {\n placement = AttachmentMap.TOPEND;\n }\n } else if ($parentDropdown.hasClass(ClassName$4.DROPRIGHT)) {\n placement = AttachmentMap.RIGHT;\n } else if ($parentDropdown.hasClass(ClassName$4.DROPLEFT)) {\n placement = AttachmentMap.LEFT;\n } else if ($(this._menu).hasClass(ClassName$4.MENURIGHT)) {\n placement = AttachmentMap.BOTTOMEND;\n }\n\n return placement;\n };\n\n _proto._detectNavbar = function _detectNavbar() {\n return $(this._element).closest('.navbar').length > 0;\n };\n\n _proto._getOffset = function _getOffset() {\n var _this2 = this;\n\n var offset = {};\n\n if (typeof this._config.offset === 'function') {\n offset.fn = function (data) {\n data.offsets = _objectSpread({}, data.offsets, _this2._config.offset(data.offsets, _this2._element) || {});\n return data;\n };\n } else {\n offset.offset = this._config.offset;\n }\n\n return offset;\n };\n\n _proto._getPopperConfig = function _getPopperConfig() {\n var popperConfig = {\n placement: this._getPlacement(),\n modifiers: {\n offset: this._getOffset(),\n flip: {\n enabled: this._config.flip\n },\n preventOverflow: {\n boundariesElement: this._config.boundary\n }\n } // Disable Popper.js if we have a static display\n\n };\n\n if (this._config.display === 'static') {\n popperConfig.modifiers.applyStyle = {\n enabled: false\n };\n }\n\n return popperConfig;\n } // Static\n ;\n\n Dropdown._jQueryInterface = function _jQueryInterface(config) {\n return this.each(function () {\n var data = $(this).data(DATA_KEY$4);\n\n var _config = typeof config === 'object' ? config : null;\n\n if (!data) {\n data = new Dropdown(this, _config);\n $(this).data(DATA_KEY$4, data);\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(\"No method named \\\"\" + config + \"\\\"\");\n }\n\n data[config]();\n }\n });\n };\n\n Dropdown._clearMenus = function _clearMenus(event) {\n if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH || event.type === 'keyup' && event.which !== TAB_KEYCODE)) {\n return;\n }\n\n var toggles = [].slice.call(document.querySelectorAll(Selector$4.DATA_TOGGLE));\n\n for (var i = 0, len = toggles.length; i < len; i++) {\n var parent = Dropdown._getParentFromElement(toggles[i]);\n\n var context = $(toggles[i]).data(DATA_KEY$4);\n var relatedTarget = {\n relatedTarget: toggles[i]\n };\n\n if (event && event.type === 'click') {\n relatedTarget.clickEvent = event;\n }\n\n if (!context) {\n continue;\n }\n\n var dropdownMenu = context._menu;\n\n if (!$(parent).hasClass(ClassName$4.SHOW)) {\n continue;\n }\n\n if (event && (event.type === 'click' && /input|textarea/i.test(event.target.tagName) || event.type === 'keyup' && event.which === TAB_KEYCODE) && $.contains(parent, event.target)) {\n continue;\n }\n\n var hideEvent = $.Event(Event$4.HIDE, relatedTarget);\n $(parent).trigger(hideEvent);\n\n if (hideEvent.isDefaultPrevented()) {\n continue;\n } // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n\n\n if ('ontouchstart' in document.documentElement) {\n $(document.body).children().off('mouseover', null, $.noop);\n }\n\n toggles[i].setAttribute('aria-expanded', 'false');\n $(dropdownMenu).removeClass(ClassName$4.SHOW);\n $(parent).removeClass(ClassName$4.SHOW).trigger($.Event(Event$4.HIDDEN, relatedTarget));\n }\n };\n\n Dropdown._getParentFromElement = function _getParentFromElement(element) {\n var parent;\n var selector = Util.getSelectorFromElement(element);\n\n if (selector) {\n parent = document.querySelector(selector);\n }\n\n return parent || element.parentNode;\n } // eslint-disable-next-line complexity\n ;\n\n Dropdown._dataApiKeydownHandler = function _dataApiKeydownHandler(event) {\n // If not input/textarea:\n // - And not a key in REGEXP_KEYDOWN => not a dropdown command\n // If input/textarea:\n // - If space key => not a dropdown command\n // - If key is other than escape\n // - If key is not up or down => not a dropdown command\n // - If trigger inside the menu => not a dropdown command\n if (/input|textarea/i.test(event.target.tagName) ? event.which === SPACE_KEYCODE || event.which !== ESCAPE_KEYCODE && (event.which !== ARROW_DOWN_KEYCODE && event.which !== ARROW_UP_KEYCODE || $(event.target).closest(Selector$4.MENU).length) : !REGEXP_KEYDOWN.test(event.which)) {\n return;\n }\n\n event.preventDefault();\n event.stopPropagation();\n\n if (this.disabled || $(this).hasClass(ClassName$4.DISABLED)) {\n return;\n }\n\n var parent = Dropdown._getParentFromElement(this);\n\n var isActive = $(parent).hasClass(ClassName$4.SHOW);\n\n if (!isActive || isActive && (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE)) {\n if (event.which === ESCAPE_KEYCODE) {\n var toggle = parent.querySelector(Selector$4.DATA_TOGGLE);\n $(toggle).trigger('focus');\n }\n\n $(this).trigger('click');\n return;\n }\n\n var items = [].slice.call(parent.querySelectorAll(Selector$4.VISIBLE_ITEMS));\n\n if (items.length === 0) {\n return;\n }\n\n var index = items.indexOf(event.target);\n\n if (event.which === ARROW_UP_KEYCODE && index > 0) {\n // Up\n index--;\n }\n\n if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) {\n // Down\n index++;\n }\n\n if (index < 0) {\n index = 0;\n }\n\n items[index].focus();\n };\n\n _createClass(Dropdown, null, [{\n key: \"VERSION\",\n get: function get() {\n return VERSION$4;\n }\n }, {\n key: \"Default\",\n get: function get() {\n return Default$2;\n }\n }, {\n key: \"DefaultType\",\n get: function get() {\n return DefaultType$2;\n }\n }]);\n\n return Dropdown;\n }();\n /**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n\n $(document).on(Event$4.KEYDOWN_DATA_API, Selector$4.DATA_TOGGLE, Dropdown._dataApiKeydownHandler).on(Event$4.KEYDOWN_DATA_API, Selector$4.MENU, Dropdown._dataApiKeydownHandler).on(Event$4.CLICK_DATA_API + \" \" + Event$4.KEYUP_DATA_API, Dropdown._clearMenus).on(Event$4.CLICK_DATA_API, Selector$4.DATA_TOGGLE, function (event) {\n event.preventDefault();\n event.stopPropagation();\n\n Dropdown._jQueryInterface.call($(this), 'toggle');\n }).on(Event$4.CLICK_DATA_API, Selector$4.FORM_CHILD, function (e) {\n e.stopPropagation();\n });\n /**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n $.fn[NAME$4] = Dropdown._jQueryInterface;\n $.fn[NAME$4].Constructor = Dropdown;\n\n $.fn[NAME$4].noConflict = function () {\n $.fn[NAME$4] = JQUERY_NO_CONFLICT$4;\n return Dropdown._jQueryInterface;\n };\n\n /**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\n var NAME$5 = 'modal';\n var VERSION$5 = '4.3.1';\n var DATA_KEY$5 = 'bs.modal';\n var EVENT_KEY$5 = \".\" + DATA_KEY$5;\n var DATA_API_KEY$5 = '.data-api';\n var JQUERY_NO_CONFLICT$5 = $.fn[NAME$5];\n var ESCAPE_KEYCODE$1 = 27; // KeyboardEvent.which value for Escape (Esc) key\n\n var Default$3 = {\n backdrop: true,\n keyboard: true,\n focus: true,\n show: true\n };\n var DefaultType$3 = {\n backdrop: '(boolean|string)',\n keyboard: 'boolean',\n focus: 'boolean',\n show: 'boolean'\n };\n var Event$5 = {\n HIDE: \"hide\" + EVENT_KEY$5,\n HIDDEN: \"hidden\" + EVENT_KEY$5,\n SHOW: \"show\" + EVENT_KEY$5,\n SHOWN: \"shown\" + EVENT_KEY$5,\n FOCUSIN: \"focusin\" + EVENT_KEY$5,\n RESIZE: \"resize\" + EVENT_KEY$5,\n CLICK_DISMISS: \"click.dismiss\" + EVENT_KEY$5,\n KEYDOWN_DISMISS: \"keydown.dismiss\" + EVENT_KEY$5,\n MOUSEUP_DISMISS: \"mouseup.dismiss\" + EVENT_KEY$5,\n MOUSEDOWN_DISMISS: \"mousedown.dismiss\" + EVENT_KEY$5,\n CLICK_DATA_API: \"click\" + EVENT_KEY$5 + DATA_API_KEY$5\n };\n var ClassName$5 = {\n SCROLLABLE: 'modal-dialog-scrollable',\n SCROLLBAR_MEASURER: 'modal-scrollbar-measure',\n BACKDROP: 'modal-backdrop',\n OPEN: 'modal-open',\n FADE: 'fade',\n SHOW: 'show'\n };\n var Selector$5 = {\n DIALOG: '.modal-dialog',\n MODAL_BODY: '.modal-body',\n DATA_TOGGLE: '[data-toggle=\"modal\"]',\n DATA_DISMISS: '[data-dismiss=\"modal\"]',\n FIXED_CONTENT: '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top',\n STICKY_CONTENT: '.sticky-top'\n /**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\n };\n\n var Modal =\n /*#__PURE__*/\n function () {\n function Modal(element, config) {\n this._config = this._getConfig(config);\n this._element = element;\n this._dialog = element.querySelector(Selector$5.DIALOG);\n this._backdrop = null;\n this._isShown = false;\n this._isBodyOverflowing = false;\n this._ignoreBackdropClick = false;\n this._isTransitioning = false;\n this._scrollbarWidth = 0;\n } // Getters\n\n\n var _proto = Modal.prototype;\n\n // Public\n _proto.toggle = function toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget);\n };\n\n _proto.show = function show(relatedTarget) {\n var _this = this;\n\n if (this._isShown || this._isTransitioning) {\n return;\n }\n\n if ($(this._element).hasClass(ClassName$5.FADE)) {\n this._isTransitioning = true;\n }\n\n var showEvent = $.Event(Event$5.SHOW, {\n relatedTarget: relatedTarget\n });\n $(this._element).trigger(showEvent);\n\n if (this._isShown || showEvent.isDefaultPrevented()) {\n return;\n }\n\n this._isShown = true;\n\n this._checkScrollbar();\n\n this._setScrollbar();\n\n this._adjustDialog();\n\n this._setEscapeEvent();\n\n this._setResizeEvent();\n\n $(this._element).on(Event$5.CLICK_DISMISS, Selector$5.DATA_DISMISS, function (event) {\n return _this.hide(event);\n });\n $(this._dialog).on(Event$5.MOUSEDOWN_DISMISS, function () {\n $(_this._element).one(Event$5.MOUSEUP_DISMISS, function (event) {\n if ($(event.target).is(_this._element)) {\n _this._ignoreBackdropClick = true;\n }\n });\n });\n\n this._showBackdrop(function () {\n return _this._showElement(relatedTarget);\n });\n };\n\n _proto.hide = function hide(event) {\n var _this2 = this;\n\n if (event) {\n event.preventDefault();\n }\n\n if (!this._isShown || this._isTransitioning) {\n return;\n }\n\n var hideEvent = $.Event(Event$5.HIDE);\n $(this._element).trigger(hideEvent);\n\n if (!this._isShown || hideEvent.isDefaultPrevented()) {\n return;\n }\n\n this._isShown = false;\n var transition = $(this._element).hasClass(ClassName$5.FADE);\n\n if (transition) {\n this._isTransitioning = true;\n }\n\n this._setEscapeEvent();\n\n this._setResizeEvent();\n\n $(document).off(Event$5.FOCUSIN);\n $(this._element).removeClass(ClassName$5.SHOW);\n $(this._element).off(Event$5.CLICK_DISMISS);\n $(this._dialog).off(Event$5.MOUSEDOWN_DISMISS);\n\n if (transition) {\n var transitionDuration = Util.getTransitionDurationFromElement(this._element);\n $(this._element).one(Util.TRANSITION_END, function (event) {\n return _this2._hideModal(event);\n }).emulateTransitionEnd(transitionDuration);\n } else {\n this._hideModal();\n }\n };\n\n _proto.dispose = function dispose() {\n [window, this._element, this._dialog].forEach(function (htmlElement) {\n return $(htmlElement).off(EVENT_KEY$5);\n });\n /**\n * `document` has 2 events `Event.FOCUSIN` and `Event.CLICK_DATA_API`\n * Do not move `document` in `htmlElements` array\n * It will remove `Event.CLICK_DATA_API` event that should remain\n */\n\n $(document).off(Event$5.FOCUSIN);\n $.removeData(this._element, DATA_KEY$5);\n this._config = null;\n this._element = null;\n this._dialog = null;\n this._backdrop = null;\n this._isShown = null;\n this._isBodyOverflowing = null;\n this._ignoreBackdropClick = null;\n this._isTransitioning = null;\n this._scrollbarWidth = null;\n };\n\n _proto.handleUpdate = function handleUpdate() {\n this._adjustDialog();\n } // Private\n ;\n\n _proto._getConfig = function _getConfig(config) {\n config = _objectSpread({}, Default$3, config);\n Util.typeCheckConfig(NAME$5, config, DefaultType$3);\n return config;\n };\n\n _proto._showElement = function _showElement(relatedTarget) {\n var _this3 = this;\n\n var transition = $(this._element).hasClass(ClassName$5.FADE);\n\n if (!this._element.parentNode || this._element.parentNode.nodeType !== Node.ELEMENT_NODE) {\n // Don't move modal's DOM position\n document.body.appendChild(this._element);\n }\n\n this._element.style.display = 'block';\n\n this._element.removeAttribute('aria-hidden');\n\n this._element.setAttribute('aria-modal', true);\n\n if ($(this._dialog).hasClass(ClassName$5.SCROLLABLE)) {\n this._dialog.querySelector(Selector$5.MODAL_BODY).scrollTop = 0;\n } else {\n this._element.scrollTop = 0;\n }\n\n if (transition) {\n Util.reflow(this._element);\n }\n\n $(this._element).addClass(ClassName$5.SHOW);\n\n if (this._config.focus) {\n this._enforceFocus();\n }\n\n var shownEvent = $.Event(Event$5.SHOWN, {\n relatedTarget: relatedTarget\n });\n\n var transitionComplete = function transitionComplete() {\n if (_this3._config.focus) {\n _this3._element.focus();\n }\n\n _this3._isTransitioning = false;\n $(_this3._element).trigger(shownEvent);\n };\n\n if (transition) {\n var transitionDuration = Util.getTransitionDurationFromElement(this._dialog);\n $(this._dialog).one(Util.TRANSITION_END, transitionComplete).emulateTransitionEnd(transitionDuration);\n } else {\n transitionComplete();\n }\n };\n\n _proto._enforceFocus = function _enforceFocus() {\n var _this4 = this;\n\n $(document).off(Event$5.FOCUSIN) // Guard against infinite focus loop\n .on(Event$5.FOCUSIN, function (event) {\n if (document !== event.target && _this4._element !== event.target && $(_this4._element).has(event.target).length === 0) {\n _this4._element.focus();\n }\n });\n };\n\n _proto._setEscapeEvent = function _setEscapeEvent() {\n var _this5 = this;\n\n if (this._isShown && this._config.keyboard) {\n $(this._element).on(Event$5.KEYDOWN_DISMISS, function (event) {\n if (event.which === ESCAPE_KEYCODE$1) {\n event.preventDefault();\n\n _this5.hide();\n }\n });\n } else if (!this._isShown) {\n $(this._element).off(Event$5.KEYDOWN_DISMISS);\n }\n };\n\n _proto._setResizeEvent = function _setResizeEvent() {\n var _this6 = this;\n\n if (this._isShown) {\n $(window).on(Event$5.RESIZE, function (event) {\n return _this6.handleUpdate(event);\n });\n } else {\n $(window).off(Event$5.RESIZE);\n }\n };\n\n _proto._hideModal = function _hideModal() {\n var _this7 = this;\n\n this._element.style.display = 'none';\n\n this._element.setAttribute('aria-hidden', true);\n\n this._element.removeAttribute('aria-modal');\n\n this._isTransitioning = false;\n\n this._showBackdrop(function () {\n $(document.body).removeClass(ClassName$5.OPEN);\n\n _this7._resetAdjustments();\n\n _this7._resetScrollbar();\n\n $(_this7._element).trigger(Event$5.HIDDEN);\n });\n };\n\n _proto._removeBackdrop = function _removeBackdrop() {\n if (this._backdrop) {\n $(this._backdrop).remove();\n this._backdrop = null;\n }\n };\n\n _proto._showBackdrop = function _showBackdrop(callback) {\n var _this8 = this;\n\n var animate = $(this._element).hasClass(ClassName$5.FADE) ? ClassName$5.FADE : '';\n\n if (this._isShown && this._config.backdrop) {\n this._backdrop = document.createElement('div');\n this._backdrop.className = ClassName$5.BACKDROP;\n\n if (animate) {\n this._backdrop.classList.add(animate);\n }\n\n $(this._backdrop).appendTo(document.body);\n $(this._element).on(Event$5.CLICK_DISMISS, function (event) {\n if (_this8._ignoreBackdropClick) {\n _this8._ignoreBackdropClick = false;\n return;\n }\n\n if (event.target !== event.currentTarget) {\n return;\n }\n\n if (_this8._config.backdrop === 'static') {\n _this8._element.focus();\n } else {\n _this8.hide();\n }\n });\n\n if (animate) {\n Util.reflow(this._backdrop);\n }\n\n $(this._backdrop).addClass(ClassName$5.SHOW);\n\n if (!callback) {\n return;\n }\n\n if (!animate) {\n callback();\n return;\n }\n\n var backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop);\n $(this._backdrop).one(Util.TRANSITION_END, callback).emulateTransitionEnd(backdropTransitionDuration);\n } else if (!this._isShown && this._backdrop) {\n $(this._backdrop).removeClass(ClassName$5.SHOW);\n\n var callbackRemove = function callbackRemove() {\n _this8._removeBackdrop();\n\n if (callback) {\n callback();\n }\n };\n\n if ($(this._element).hasClass(ClassName$5.FADE)) {\n var _backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop);\n\n $(this._backdrop).one(Util.TRANSITION_END, callbackRemove).emulateTransitionEnd(_backdropTransitionDuration);\n } else {\n callbackRemove();\n }\n } else if (callback) {\n callback();\n }\n } // ----------------------------------------------------------------------\n // the following methods are used to handle overflowing modals\n // todo (fat): these should probably be refactored out of modal.js\n // ----------------------------------------------------------------------\n ;\n\n _proto._adjustDialog = function _adjustDialog() {\n var isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n\n if (!this._isBodyOverflowing && isModalOverflowing) {\n this._element.style.paddingLeft = this._scrollbarWidth + \"px\";\n }\n\n if (this._isBodyOverflowing && !isModalOverflowing) {\n this._element.style.paddingRight = this._scrollbarWidth + \"px\";\n }\n };\n\n _proto._resetAdjustments = function _resetAdjustments() {\n this._element.style.paddingLeft = '';\n this._element.style.paddingRight = '';\n };\n\n _proto._checkScrollbar = function _checkScrollbar() {\n var rect = document.body.getBoundingClientRect();\n this._isBodyOverflowing = rect.left + rect.right < window.innerWidth;\n this._scrollbarWidth = this._getScrollbarWidth();\n };\n\n _proto._setScrollbar = function _setScrollbar() {\n var _this9 = this;\n\n if (this._isBodyOverflowing) {\n // Note: DOMNode.style.paddingRight returns the actual value or '' if not set\n // while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set\n var fixedContent = [].slice.call(document.querySelectorAll(Selector$5.FIXED_CONTENT));\n var stickyContent = [].slice.call(document.querySelectorAll(Selector$5.STICKY_CONTENT)); // Adjust fixed content padding\n\n $(fixedContent).each(function (index, element) {\n var actualPadding = element.style.paddingRight;\n var calculatedPadding = $(element).css('padding-right');\n $(element).data('padding-right', actualPadding).css('padding-right', parseFloat(calculatedPadding) + _this9._scrollbarWidth + \"px\");\n }); // Adjust sticky content margin\n\n $(stickyContent).each(function (index, element) {\n var actualMargin = element.style.marginRight;\n var calculatedMargin = $(element).css('margin-right');\n $(element).data('margin-right', actualMargin).css('margin-right', parseFloat(calculatedMargin) - _this9._scrollbarWidth + \"px\");\n }); // Adjust body padding\n\n var actualPadding = document.body.style.paddingRight;\n var calculatedPadding = $(document.body).css('padding-right');\n $(document.body).data('padding-right', actualPadding).css('padding-right', parseFloat(calculatedPadding) + this._scrollbarWidth + \"px\");\n }\n\n $(document.body).addClass(ClassName$5.OPEN);\n };\n\n _proto._resetScrollbar = function _resetScrollbar() {\n // Restore fixed content padding\n var fixedContent = [].slice.call(document.querySelectorAll(Selector$5.FIXED_CONTENT));\n $(fixedContent).each(function (index, element) {\n var padding = $(element).data('padding-right');\n $(element).removeData('padding-right');\n element.style.paddingRight = padding ? padding : '';\n }); // Restore sticky content\n\n var elements = [].slice.call(document.querySelectorAll(\"\" + Selector$5.STICKY_CONTENT));\n $(elements).each(function (index, element) {\n var margin = $(element).data('margin-right');\n\n if (typeof margin !== 'undefined') {\n $(element).css('margin-right', margin).removeData('margin-right');\n }\n }); // Restore body padding\n\n var padding = $(document.body).data('padding-right');\n $(document.body).removeData('padding-right');\n document.body.style.paddingRight = padding ? padding : '';\n };\n\n _proto._getScrollbarWidth = function _getScrollbarWidth() {\n // thx d.walsh\n var scrollDiv = document.createElement('div');\n scrollDiv.className = ClassName$5.SCROLLBAR_MEASURER;\n document.body.appendChild(scrollDiv);\n var scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth;\n document.body.removeChild(scrollDiv);\n return scrollbarWidth;\n } // Static\n ;\n\n Modal._jQueryInterface = function _jQueryInterface(config, relatedTarget) {\n return this.each(function () {\n var data = $(this).data(DATA_KEY$5);\n\n var _config = _objectSpread({}, Default$3, $(this).data(), typeof config === 'object' && config ? config : {});\n\n if (!data) {\n data = new Modal(this, _config);\n $(this).data(DATA_KEY$5, data);\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(\"No method named \\\"\" + config + \"\\\"\");\n }\n\n data[config](relatedTarget);\n } else if (_config.show) {\n data.show(relatedTarget);\n }\n });\n };\n\n _createClass(Modal, null, [{\n key: \"VERSION\",\n get: function get() {\n return VERSION$5;\n }\n }, {\n key: \"Default\",\n get: function get() {\n return Default$3;\n }\n }]);\n\n return Modal;\n }();\n /**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n\n $(document).on(Event$5.CLICK_DATA_API, Selector$5.DATA_TOGGLE, function (event) {\n var _this10 = this;\n\n var target;\n var selector = Util.getSelectorFromElement(this);\n\n if (selector) {\n target = document.querySelector(selector);\n }\n\n var config = $(target).data(DATA_KEY$5) ? 'toggle' : _objectSpread({}, $(target).data(), $(this).data());\n\n if (this.tagName === 'A' || this.tagName === 'AREA') {\n event.preventDefault();\n }\n\n var $target = $(target).one(Event$5.SHOW, function (showEvent) {\n if (showEvent.isDefaultPrevented()) {\n // Only register focus restorer if modal will actually get shown\n return;\n }\n\n $target.one(Event$5.HIDDEN, function () {\n if ($(_this10).is(':visible')) {\n _this10.focus();\n }\n });\n });\n\n Modal._jQueryInterface.call($(target), config, this);\n });\n /**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n $.fn[NAME$5] = Modal._jQueryInterface;\n $.fn[NAME$5].Constructor = Modal;\n\n $.fn[NAME$5].noConflict = function () {\n $.fn[NAME$5] = JQUERY_NO_CONFLICT$5;\n return Modal._jQueryInterface;\n };\n\n /**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.3.1): tools/sanitizer.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n var uriAttrs = ['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href'];\n var ARIA_ATTRIBUTE_PATTERN = /^aria-[\\w-]*$/i;\n var DefaultWhitelist = {\n // Global attributes allowed on any supplied element below.\n '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],\n a: ['target', 'href', 'title', 'rel'],\n area: [],\n b: [],\n br: [],\n col: [],\n code: [],\n div: [],\n em: [],\n hr: [],\n h1: [],\n h2: [],\n h3: [],\n h4: [],\n h5: [],\n h6: [],\n i: [],\n img: ['src', 'alt', 'title', 'width', 'height'],\n li: [],\n ol: [],\n p: [],\n pre: [],\n s: [],\n small: [],\n span: [],\n sub: [],\n sup: [],\n strong: [],\n u: [],\n ul: []\n /**\n * A pattern that recognizes a commonly useful subset of URLs that are safe.\n *\n * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts\n */\n\n };\n var SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi;\n /**\n * A pattern that matches safe data URLs. Only matches image, video and audio types.\n *\n * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts\n */\n\n var DATA_URL_PATTERN = /^data:(?:image\\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\\/(?:mpeg|mp4|ogg|webm)|audio\\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i;\n\n function allowedAttribute(attr, allowedAttributeList) {\n var attrName = attr.nodeName.toLowerCase();\n\n if (allowedAttributeList.indexOf(attrName) !== -1) {\n if (uriAttrs.indexOf(attrName) !== -1) {\n return Boolean(attr.nodeValue.match(SAFE_URL_PATTERN) || attr.nodeValue.match(DATA_URL_PATTERN));\n }\n\n return true;\n }\n\n var regExp = allowedAttributeList.filter(function (attrRegex) {\n return attrRegex instanceof RegExp;\n }); // Check if a regular expression validates the attribute.\n\n for (var i = 0, l = regExp.length; i < l; i++) {\n if (attrName.match(regExp[i])) {\n return true;\n }\n }\n\n return false;\n }\n\n function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) {\n if (unsafeHtml.length === 0) {\n return unsafeHtml;\n }\n\n if (sanitizeFn && typeof sanitizeFn === 'function') {\n return sanitizeFn(unsafeHtml);\n }\n\n var domParser = new window.DOMParser();\n var createdDocument = domParser.parseFromString(unsafeHtml, 'text/html');\n var whitelistKeys = Object.keys(whiteList);\n var elements = [].slice.call(createdDocument.body.querySelectorAll('*'));\n\n var _loop = function _loop(i, len) {\n var el = elements[i];\n var elName = el.nodeName.toLowerCase();\n\n if (whitelistKeys.indexOf(el.nodeName.toLowerCase()) === -1) {\n el.parentNode.removeChild(el);\n return \"continue\";\n }\n\n var attributeList = [].slice.call(el.attributes);\n var whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || []);\n attributeList.forEach(function (attr) {\n if (!allowedAttribute(attr, whitelistedAttributes)) {\n el.removeAttribute(attr.nodeName);\n }\n });\n };\n\n for (var i = 0, len = elements.length; i < len; i++) {\n var _ret = _loop(i, len);\n\n if (_ret === \"continue\") continue;\n }\n\n return createdDocument.body.innerHTML;\n }\n\n /**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\n var NAME$6 = 'tooltip';\n var VERSION$6 = '4.3.1';\n var DATA_KEY$6 = 'bs.tooltip';\n var EVENT_KEY$6 = \".\" + DATA_KEY$6;\n var JQUERY_NO_CONFLICT$6 = $.fn[NAME$6];\n var CLASS_PREFIX = 'bs-tooltip';\n var BSCLS_PREFIX_REGEX = new RegExp(\"(^|\\\\s)\" + CLASS_PREFIX + \"\\\\S+\", 'g');\n var DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn'];\n var DefaultType$4 = {\n animation: 'boolean',\n template: 'string',\n title: '(string|element|function)',\n trigger: 'string',\n delay: '(number|object)',\n html: 'boolean',\n selector: '(string|boolean)',\n placement: '(string|function)',\n offset: '(number|string|function)',\n container: '(string|element|boolean)',\n fallbackPlacement: '(string|array)',\n boundary: '(string|element)',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n whiteList: 'object'\n };\n var AttachmentMap$1 = {\n AUTO: 'auto',\n TOP: 'top',\n RIGHT: 'right',\n BOTTOM: 'bottom',\n LEFT: 'left'\n };\n var Default$4 = {\n animation: true,\n template: '<div class=\"tooltip\" role=\"tooltip\">' + '<div class=\"arrow\"></div>' + '<div class=\"tooltip-inner\"></div></div>',\n trigger: 'hover focus',\n title: '',\n delay: 0,\n html: false,\n selector: false,\n placement: 'top',\n offset: 0,\n container: false,\n fallbackPlacement: 'flip',\n boundary: 'scrollParent',\n sanitize: true,\n sanitizeFn: null,\n whiteList: DefaultWhitelist\n };\n var HoverState = {\n SHOW: 'show',\n OUT: 'out'\n };\n var Event$6 = {\n HIDE: \"hide\" + EVENT_KEY$6,\n HIDDEN: \"hidden\" + EVENT_KEY$6,\n SHOW: \"show\" + EVENT_KEY$6,\n SHOWN: \"shown\" + EVENT_KEY$6,\n INSERTED: \"inserted\" + EVENT_KEY$6,\n CLICK: \"click\" + EVENT_KEY$6,\n FOCUSIN: \"focusin\" + EVENT_KEY$6,\n FOCUSOUT: \"focusout\" + EVENT_KEY$6,\n MOUSEENTER: \"mouseenter\" + EVENT_KEY$6,\n MOUSELEAVE: \"mouseleave\" + EVENT_KEY$6\n };\n var ClassName$6 = {\n FADE: 'fade',\n SHOW: 'show'\n };\n var Selector$6 = {\n TOOLTIP: '.tooltip',\n TOOLTIP_INNER: '.tooltip-inner',\n ARROW: '.arrow'\n };\n var Trigger = {\n HOVER: 'hover',\n FOCUS: 'focus',\n CLICK: 'click',\n MANUAL: 'manual'\n /**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\n };\n\n var Tooltip =\n /*#__PURE__*/\n function () {\n function Tooltip(element, config) {\n /**\n * Check for Popper dependency\n * Popper - https://popper.js.org\n */\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s tooltips require Popper.js (https://popper.js.org/)');\n } // private\n\n\n this._isEnabled = true;\n this._timeout = 0;\n this._hoverState = '';\n this._activeTrigger = {};\n this._popper = null; // Protected\n\n this.element = element;\n this.config = this._getConfig(config);\n this.tip = null;\n\n this._setListeners();\n } // Getters\n\n\n var _proto = Tooltip.prototype;\n\n // Public\n _proto.enable = function enable() {\n this._isEnabled = true;\n };\n\n _proto.disable = function disable() {\n this._isEnabled = false;\n };\n\n _proto.toggleEnabled = function toggleEnabled() {\n this._isEnabled = !this._isEnabled;\n };\n\n _proto.toggle = function toggle(event) {\n if (!this._isEnabled) {\n return;\n }\n\n if (event) {\n var dataKey = this.constructor.DATA_KEY;\n var context = $(event.currentTarget).data(dataKey);\n\n if (!context) {\n context = new this.constructor(event.currentTarget, this._getDelegateConfig());\n $(event.currentTarget).data(dataKey, context);\n }\n\n context._activeTrigger.click = !context._activeTrigger.click;\n\n if (context._isWithActiveTrigger()) {\n context._enter(null, context);\n } else {\n context._leave(null, context);\n }\n } else {\n if ($(this.getTipElement()).hasClass(ClassName$6.SHOW)) {\n this._leave(null, this);\n\n return;\n }\n\n this._enter(null, this);\n }\n };\n\n _proto.dispose = function dispose() {\n clearTimeout(this._timeout);\n $.removeData(this.element, this.constructor.DATA_KEY);\n $(this.element).off(this.constructor.EVENT_KEY);\n $(this.element).closest('.modal').off('hide.bs.modal');\n\n if (this.tip) {\n $(this.tip).remove();\n }\n\n this._isEnabled = null;\n this._timeout = null;\n this._hoverState = null;\n this._activeTrigger = null;\n\n if (this._popper !== null) {\n this._popper.destroy();\n }\n\n this._popper = null;\n this.element = null;\n this.config = null;\n this.tip = null;\n };\n\n _proto.show = function show() {\n var _this = this;\n\n if ($(this.element).css('display') === 'none') {\n throw new Error('Please use show on visible elements');\n }\n\n var showEvent = $.Event(this.constructor.Event.SHOW);\n\n if (this.isWithContent() && this._isEnabled) {\n $(this.element).trigger(showEvent);\n var shadowRoot = Util.findShadowRoot(this.element);\n var isInTheDom = $.contains(shadowRoot !== null ? shadowRoot : this.element.ownerDocument.documentElement, this.element);\n\n if (showEvent.isDefaultPrevented() || !isInTheDom) {\n return;\n }\n\n var tip = this.getTipElement();\n var tipId = Util.getUID(this.constructor.NAME);\n tip.setAttribute('id', tipId);\n this.element.setAttribute('aria-describedby', tipId);\n this.setContent();\n\n if (this.config.animation) {\n $(tip).addClass(ClassName$6.FADE);\n }\n\n var placement = typeof this.config.placement === 'function' ? this.config.placement.call(this, tip, this.element) : this.config.placement;\n\n var attachment = this._getAttachment(placement);\n\n this.addAttachmentClass(attachment);\n\n var container = this._getContainer();\n\n $(tip).data(this.constructor.DATA_KEY, this);\n\n if (!$.contains(this.element.ownerDocument.documentElement, this.tip)) {\n $(tip).appendTo(container);\n }\n\n $(this.element).trigger(this.constructor.Event.INSERTED);\n this._popper = new Popper(this.element, tip, {\n placement: attachment,\n modifiers: {\n offset: this._getOffset(),\n flip: {\n behavior: this.config.fallbackPlacement\n },\n arrow: {\n element: Selector$6.ARROW\n },\n preventOverflow: {\n boundariesElement: this.config.boundary\n }\n },\n onCreate: function onCreate(data) {\n if (data.originalPlacement !== data.placement) {\n _this._handlePopperPlacementChange(data);\n }\n },\n onUpdate: function onUpdate(data) {\n return _this._handlePopperPlacementChange(data);\n }\n });\n $(tip).addClass(ClassName$6.SHOW); // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n\n if ('ontouchstart' in document.documentElement) {\n $(document.body).children().on('mouseover', null, $.noop);\n }\n\n var complete = function complete() {\n if (_this.config.animation) {\n _this._fixTransition();\n }\n\n var prevHoverState = _this._hoverState;\n _this._hoverState = null;\n $(_this.element).trigger(_this.constructor.Event.SHOWN);\n\n if (prevHoverState === HoverState.OUT) {\n _this._leave(null, _this);\n }\n };\n\n if ($(this.tip).hasClass(ClassName$6.FADE)) {\n var transitionDuration = Util.getTransitionDurationFromElement(this.tip);\n $(this.tip).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);\n } else {\n complete();\n }\n }\n };\n\n _proto.hide = function hide(callback) {\n var _this2 = this;\n\n var tip = this.getTipElement();\n var hideEvent = $.Event(this.constructor.Event.HIDE);\n\n var complete = function complete() {\n if (_this2._hoverState !== HoverState.SHOW && tip.parentNode) {\n tip.parentNode.removeChild(tip);\n }\n\n _this2._cleanTipClass();\n\n _this2.element.removeAttribute('aria-describedby');\n\n $(_this2.element).trigger(_this2.constructor.Event.HIDDEN);\n\n if (_this2._popper !== null) {\n _this2._popper.destroy();\n }\n\n if (callback) {\n callback();\n }\n };\n\n $(this.element).trigger(hideEvent);\n\n if (hideEvent.isDefaultPrevented()) {\n return;\n }\n\n $(tip).removeClass(ClassName$6.SHOW); // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n\n if ('ontouchstart' in document.documentElement) {\n $(document.body).children().off('mouseover', null, $.noop);\n }\n\n this._activeTrigger[Trigger.CLICK] = false;\n this._activeTrigger[Trigger.FOCUS] = false;\n this._activeTrigger[Trigger.HOVER] = false;\n\n if ($(this.tip).hasClass(ClassName$6.FADE)) {\n var transitionDuration = Util.getTransitionDurationFromElement(tip);\n $(tip).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);\n } else {\n complete();\n }\n\n this._hoverState = '';\n };\n\n _proto.update = function update() {\n if (this._popper !== null) {\n this._popper.scheduleUpdate();\n }\n } // Protected\n ;\n\n _proto.isWithContent = function isWithContent() {\n return Boolean(this.getTitle());\n };\n\n _proto.addAttachmentClass = function addAttachmentClass(attachment) {\n $(this.getTipElement()).addClass(CLASS_PREFIX + \"-\" + attachment);\n };\n\n _proto.getTipElement = function getTipElement() {\n this.tip = this.tip || $(this.config.template)[0];\n return this.tip;\n };\n\n _proto.setContent = function setContent() {\n var tip = this.getTipElement();\n this.setElementContent($(tip.querySelectorAll(Selector$6.TOOLTIP_INNER)), this.getTitle());\n $(tip).removeClass(ClassName$6.FADE + \" \" + ClassName$6.SHOW);\n };\n\n _proto.setElementContent = function setElementContent($element, content) {\n if (typeof content === 'object' && (content.nodeType || content.jquery)) {\n // Content is a DOM node or a jQuery\n if (this.config.html) {\n if (!$(content).parent().is($element)) {\n $element.empty().append(content);\n }\n } else {\n $element.text($(content).text());\n }\n\n return;\n }\n\n if (this.config.html) {\n if (this.config.sanitize) {\n content = sanitizeHtml(content, this.config.whiteList, this.config.sanitizeFn);\n }\n\n $element.html(content);\n } else {\n $element.text(content);\n }\n };\n\n _proto.getTitle = function getTitle() {\n var title = this.element.getAttribute('data-original-title');\n\n if (!title) {\n title = typeof this.config.title === 'function' ? this.config.title.call(this.element) : this.config.title;\n }\n\n return title;\n } // Private\n ;\n\n _proto._getOffset = function _getOffset() {\n var _this3 = this;\n\n var offset = {};\n\n if (typeof this.config.offset === 'function') {\n offset.fn = function (data) {\n data.offsets = _objectSpread({}, data.offsets, _this3.config.offset(data.offsets, _this3.element) || {});\n return data;\n };\n } else {\n offset.offset = this.config.offset;\n }\n\n return offset;\n };\n\n _proto._getContainer = function _getContainer() {\n if (this.config.container === false) {\n return document.body;\n }\n\n if (Util.isElement(this.config.container)) {\n return $(this.config.container);\n }\n\n return $(document).find(this.config.container);\n };\n\n _proto._getAttachment = function _getAttachment(placement) {\n return AttachmentMap$1[placement.toUpperCase()];\n };\n\n _proto._setListeners = function _setListeners() {\n var _this4 = this;\n\n var triggers = this.config.trigger.split(' ');\n triggers.forEach(function (trigger) {\n if (trigger === 'click') {\n $(_this4.element).on(_this4.constructor.Event.CLICK, _this4.config.selector, function (event) {\n return _this4.toggle(event);\n });\n } else if (trigger !== Trigger.MANUAL) {\n var eventIn = trigger === Trigger.HOVER ? _this4.constructor.Event.MOUSEENTER : _this4.constructor.Event.FOCUSIN;\n var eventOut = trigger === Trigger.HOVER ? _this4.constructor.Event.MOUSELEAVE : _this4.constructor.Event.FOCUSOUT;\n $(_this4.element).on(eventIn, _this4.config.selector, function (event) {\n return _this4._enter(event);\n }).on(eventOut, _this4.config.selector, function (event) {\n return _this4._leave(event);\n });\n }\n });\n $(this.element).closest('.modal').on('hide.bs.modal', function () {\n if (_this4.element) {\n _this4.hide();\n }\n });\n\n if (this.config.selector) {\n this.config = _objectSpread({}, this.config, {\n trigger: 'manual',\n selector: ''\n });\n } else {\n this._fixTitle();\n }\n };\n\n _proto._fixTitle = function _fixTitle() {\n var titleType = typeof this.element.getAttribute('data-original-title');\n\n if (this.element.getAttribute('title') || titleType !== 'string') {\n this.element.setAttribute('data-original-title', this.element.getAttribute('title') || '');\n this.element.setAttribute('title', '');\n }\n };\n\n _proto._enter = function _enter(event, context) {\n var dataKey = this.constructor.DATA_KEY;\n context = context || $(event.currentTarget).data(dataKey);\n\n if (!context) {\n context = new this.constructor(event.currentTarget, this._getDelegateConfig());\n $(event.currentTarget).data(dataKey, context);\n }\n\n if (event) {\n context._activeTrigger[event.type === 'focusin' ? Trigger.FOCUS : Trigger.HOVER] = true;\n }\n\n if ($(context.getTipElement()).hasClass(ClassName$6.SHOW) || context._hoverState === HoverState.SHOW) {\n context._hoverState = HoverState.SHOW;\n return;\n }\n\n clearTimeout(context._timeout);\n context._hoverState = HoverState.SHOW;\n\n if (!context.config.delay || !context.config.delay.show) {\n context.show();\n return;\n }\n\n context._timeout = setTimeout(function () {\n if (context._hoverState === HoverState.SHOW) {\n context.show();\n }\n }, context.config.delay.show);\n };\n\n _proto._leave = function _leave(event, context) {\n var dataKey = this.constructor.DATA_KEY;\n context = context || $(event.currentTarget).data(dataKey);\n\n if (!context) {\n context = new this.constructor(event.currentTarget, this._getDelegateConfig());\n $(event.currentTarget).data(dataKey, context);\n }\n\n if (event) {\n context._activeTrigger[event.type === 'focusout' ? Trigger.FOCUS : Trigger.HOVER] = false;\n }\n\n if (context._isWithActiveTrigger()) {\n return;\n }\n\n clearTimeout(context._timeout);\n context._hoverState = HoverState.OUT;\n\n if (!context.config.delay || !context.config.delay.hide) {\n context.hide();\n return;\n }\n\n context._timeout = setTimeout(function () {\n if (context._hoverState === HoverState.OUT) {\n context.hide();\n }\n }, context.config.delay.hide);\n };\n\n _proto._isWithActiveTrigger = function _isWithActiveTrigger() {\n for (var trigger in this._activeTrigger) {\n if (this._activeTrigger[trigger]) {\n return true;\n }\n }\n\n return false;\n };\n\n _proto._getConfig = function _getConfig(config) {\n var dataAttributes = $(this.element).data();\n Object.keys(dataAttributes).forEach(function (dataAttr) {\n if (DISALLOWED_ATTRIBUTES.indexOf(dataAttr) !== -1) {\n delete dataAttributes[dataAttr];\n }\n });\n config = _objectSpread({}, this.constructor.Default, dataAttributes, typeof config === 'object' && config ? config : {});\n\n if (typeof config.delay === 'number') {\n config.delay = {\n show: config.delay,\n hide: config.delay\n };\n }\n\n if (typeof config.title === 'number') {\n config.title = config.title.toString();\n }\n\n if (typeof config.content === 'number') {\n config.content = config.content.toString();\n }\n\n Util.typeCheckConfig(NAME$6, config, this.constructor.DefaultType);\n\n if (config.sanitize) {\n config.template = sanitizeHtml(config.template, config.whiteList, config.sanitizeFn);\n }\n\n return config;\n };\n\n _proto._getDelegateConfig = function _getDelegateConfig() {\n var config = {};\n\n if (this.config) {\n for (var key in this.config) {\n if (this.constructor.Default[key] !== this.config[key]) {\n config[key] = this.config[key];\n }\n }\n }\n\n return config;\n };\n\n _proto._cleanTipClass = function _cleanTipClass() {\n var $tip = $(this.getTipElement());\n var tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX);\n\n if (tabClass !== null && tabClass.length) {\n $tip.removeClass(tabClass.join(''));\n }\n };\n\n _proto._handlePopperPlacementChange = function _handlePopperPlacementChange(popperData) {\n var popperInstance = popperData.instance;\n this.tip = popperInstance.popper;\n\n this._cleanTipClass();\n\n this.addAttachmentClass(this._getAttachment(popperData.placement));\n };\n\n _proto._fixTransition = function _fixTransition() {\n var tip = this.getTipElement();\n var initConfigAnimation = this.config.animation;\n\n if (tip.getAttribute('x-placement') !== null) {\n return;\n }\n\n $(tip).removeClass(ClassName$6.FADE);\n this.config.animation = false;\n this.hide();\n this.show();\n this.config.animation = initConfigAnimation;\n } // Static\n ;\n\n Tooltip._jQueryInterface = function _jQueryInterface(config) {\n return this.each(function () {\n var data = $(this).data(DATA_KEY$6);\n\n var _config = typeof config === 'object' && config;\n\n if (!data && /dispose|hide/.test(config)) {\n return;\n }\n\n if (!data) {\n data = new Tooltip(this, _config);\n $(this).data(DATA_KEY$6, data);\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(\"No method named \\\"\" + config + \"\\\"\");\n }\n\n data[config]();\n }\n });\n };\n\n _createClass(Tooltip, null, [{\n key: \"VERSION\",\n get: function get() {\n return VERSION$6;\n }\n }, {\n key: \"Default\",\n get: function get() {\n return Default$4;\n }\n }, {\n key: \"NAME\",\n get: function get() {\n return NAME$6;\n }\n }, {\n key: \"DATA_KEY\",\n get: function get() {\n return DATA_KEY$6;\n }\n }, {\n key: \"Event\",\n get: function get() {\n return Event$6;\n }\n }, {\n key: \"EVENT_KEY\",\n get: function get() {\n return EVENT_KEY$6;\n }\n }, {\n key: \"DefaultType\",\n get: function get() {\n return DefaultType$4;\n }\n }]);\n\n return Tooltip;\n }();\n /**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n\n $.fn[NAME$6] = Tooltip._jQueryInterface;\n $.fn[NAME$6].Constructor = Tooltip;\n\n $.fn[NAME$6].noConflict = function () {\n $.fn[NAME$6] = JQUERY_NO_CONFLICT$6;\n return Tooltip._jQueryInterface;\n };\n\n /**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\n var NAME$7 = 'popover';\n var VERSION$7 = '4.3.1';\n var DATA_KEY$7 = 'bs.popover';\n var EVENT_KEY$7 = \".\" + DATA_KEY$7;\n var JQUERY_NO_CONFLICT$7 = $.fn[NAME$7];\n var CLASS_PREFIX$1 = 'bs-popover';\n var BSCLS_PREFIX_REGEX$1 = new RegExp(\"(^|\\\\s)\" + CLASS_PREFIX$1 + \"\\\\S+\", 'g');\n\n var Default$5 = _objectSpread({}, Tooltip.Default, {\n placement: 'right',\n trigger: 'click',\n content: '',\n template: '<div class=\"popover\" role=\"tooltip\">' + '<div class=\"arrow\"></div>' + '<h3 class=\"popover-header\"></h3>' + '<div class=\"popover-body\"></div></div>'\n });\n\n var DefaultType$5 = _objectSpread({}, Tooltip.DefaultType, {\n content: '(string|element|function)'\n });\n\n var ClassName$7 = {\n FADE: 'fade',\n SHOW: 'show'\n };\n var Selector$7 = {\n TITLE: '.popover-header',\n CONTENT: '.popover-body'\n };\n var Event$7 = {\n HIDE: \"hide\" + EVENT_KEY$7,\n HIDDEN: \"hidden\" + EVENT_KEY$7,\n SHOW: \"show\" + EVENT_KEY$7,\n SHOWN: \"shown\" + EVENT_KEY$7,\n INSERTED: \"inserted\" + EVENT_KEY$7,\n CLICK: \"click\" + EVENT_KEY$7,\n FOCUSIN: \"focusin\" + EVENT_KEY$7,\n FOCUSOUT: \"focusout\" + EVENT_KEY$7,\n MOUSEENTER: \"mouseenter\" + EVENT_KEY$7,\n MOUSELEAVE: \"mouseleave\" + EVENT_KEY$7\n /**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\n };\n\n var Popover =\n /*#__PURE__*/\n function (_Tooltip) {\n _inheritsLoose(Popover, _Tooltip);\n\n function Popover() {\n return _Tooltip.apply(this, arguments) || this;\n }\n\n var _proto = Popover.prototype;\n\n // Overrides\n _proto.isWithContent = function isWithContent() {\n return this.getTitle() || this._getContent();\n };\n\n _proto.addAttachmentClass = function addAttachmentClass(attachment) {\n $(this.getTipElement()).addClass(CLASS_PREFIX$1 + \"-\" + attachment);\n };\n\n _proto.getTipElement = function getTipElement() {\n this.tip = this.tip || $(this.config.template)[0];\n return this.tip;\n };\n\n _proto.setContent = function setContent() {\n var $tip = $(this.getTipElement()); // We use append for html objects to maintain js events\n\n this.setElementContent($tip.find(Selector$7.TITLE), this.getTitle());\n\n var content = this._getContent();\n\n if (typeof content === 'function') {\n content = content.call(this.element);\n }\n\n this.setElementContent($tip.find(Selector$7.CONTENT), content);\n $tip.removeClass(ClassName$7.FADE + \" \" + ClassName$7.SHOW);\n } // Private\n ;\n\n _proto._getContent = function _getContent() {\n return this.element.getAttribute('data-content') || this.config.content;\n };\n\n _proto._cleanTipClass = function _cleanTipClass() {\n var $tip = $(this.getTipElement());\n var tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX$1);\n\n if (tabClass !== null && tabClass.length > 0) {\n $tip.removeClass(tabClass.join(''));\n }\n } // Static\n ;\n\n Popover._jQueryInterface = function _jQueryInterface(config) {\n return this.each(function () {\n var data = $(this).data(DATA_KEY$7);\n\n var _config = typeof config === 'object' ? config : null;\n\n if (!data && /dispose|hide/.test(config)) {\n return;\n }\n\n if (!data) {\n data = new Popover(this, _config);\n $(this).data(DATA_KEY$7, data);\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(\"No method named \\\"\" + config + \"\\\"\");\n }\n\n data[config]();\n }\n });\n };\n\n _createClass(Popover, null, [{\n key: \"VERSION\",\n // Getters\n get: function get() {\n return VERSION$7;\n }\n }, {\n key: \"Default\",\n get: function get() {\n return Default$5;\n }\n }, {\n key: \"NAME\",\n get: function get() {\n return NAME$7;\n }\n }, {\n key: \"DATA_KEY\",\n get: function get() {\n return DATA_KEY$7;\n }\n }, {\n key: \"Event\",\n get: function get() {\n return Event$7;\n }\n }, {\n key: \"EVENT_KEY\",\n get: function get() {\n return EVENT_KEY$7;\n }\n }, {\n key: \"DefaultType\",\n get: function get() {\n return DefaultType$5;\n }\n }]);\n\n return Popover;\n }(Tooltip);\n /**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n\n $.fn[NAME$7] = Popover._jQueryInterface;\n $.fn[NAME$7].Constructor = Popover;\n\n $.fn[NAME$7].noConflict = function () {\n $.fn[NAME$7] = JQUERY_NO_CONFLICT$7;\n return Popover._jQueryInterface;\n };\n\n /**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\n var NAME$8 = 'scrollspy';\n var VERSION$8 = '4.3.1';\n var DATA_KEY$8 = 'bs.scrollspy';\n var EVENT_KEY$8 = \".\" + DATA_KEY$8;\n var DATA_API_KEY$6 = '.data-api';\n var JQUERY_NO_CONFLICT$8 = $.fn[NAME$8];\n var Default$6 = {\n offset: 10,\n method: 'auto',\n target: ''\n };\n var DefaultType$6 = {\n offset: 'number',\n method: 'string',\n target: '(string|element)'\n };\n var Event$8 = {\n ACTIVATE: \"activate\" + EVENT_KEY$8,\n SCROLL: \"scroll\" + EVENT_KEY$8,\n LOAD_DATA_API: \"load\" + EVENT_KEY$8 + DATA_API_KEY$6\n };\n var ClassName$8 = {\n DROPDOWN_ITEM: 'dropdown-item',\n DROPDOWN_MENU: 'dropdown-menu',\n ACTIVE: 'active'\n };\n var Selector$8 = {\n DATA_SPY: '[data-spy=\"scroll\"]',\n ACTIVE: '.active',\n NAV_LIST_GROUP: '.nav, .list-group',\n NAV_LINKS: '.nav-link',\n NAV_ITEMS: '.nav-item',\n LIST_ITEMS: '.list-group-item',\n DROPDOWN: '.dropdown',\n DROPDOWN_ITEMS: '.dropdown-item',\n DROPDOWN_TOGGLE: '.dropdown-toggle'\n };\n var OffsetMethod = {\n OFFSET: 'offset',\n POSITION: 'position'\n /**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\n };\n\n var ScrollSpy =\n /*#__PURE__*/\n function () {\n function ScrollSpy(element, config) {\n var _this = this;\n\n this._element = element;\n this._scrollElement = element.tagName === 'BODY' ? window : element;\n this._config = this._getConfig(config);\n this._selector = this._config.target + \" \" + Selector$8.NAV_LINKS + \",\" + (this._config.target + \" \" + Selector$8.LIST_ITEMS + \",\") + (this._config.target + \" \" + Selector$8.DROPDOWN_ITEMS);\n this._offsets = [];\n this._targets = [];\n this._activeTarget = null;\n this._scrollHeight = 0;\n $(this._scrollElement).on(Event$8.SCROLL, function (event) {\n return _this._process(event);\n });\n this.refresh();\n\n this._process();\n } // Getters\n\n\n var _proto = ScrollSpy.prototype;\n\n // Public\n _proto.refresh = function refresh() {\n var _this2 = this;\n\n var autoMethod = this._scrollElement === this._scrollElement.window ? OffsetMethod.OFFSET : OffsetMethod.POSITION;\n var offsetMethod = this._config.method === 'auto' ? autoMethod : this._config.method;\n var offsetBase = offsetMethod === OffsetMethod.POSITION ? this._getScrollTop() : 0;\n this._offsets = [];\n this._targets = [];\n this._scrollHeight = this._getScrollHeight();\n var targets = [].slice.call(document.querySelectorAll(this._selector));\n targets.map(function (element) {\n var target;\n var targetSelector = Util.getSelectorFromElement(element);\n\n if (targetSelector) {\n target = document.querySelector(targetSelector);\n }\n\n if (target) {\n var targetBCR = target.getBoundingClientRect();\n\n if (targetBCR.width || targetBCR.height) {\n // TODO (fat): remove sketch reliance on jQuery position/offset\n return [$(target)[offsetMethod]().top + offsetBase, targetSelector];\n }\n }\n\n return null;\n }).filter(function (item) {\n return item;\n }).sort(function (a, b) {\n return a[0] - b[0];\n }).forEach(function (item) {\n _this2._offsets.push(item[0]);\n\n _this2._targets.push(item[1]);\n });\n };\n\n _proto.dispose = function dispose() {\n $.removeData(this._element, DATA_KEY$8);\n $(this._scrollElement).off(EVENT_KEY$8);\n this._element = null;\n this._scrollElement = null;\n this._config = null;\n this._selector = null;\n this._offsets = null;\n this._targets = null;\n this._activeTarget = null;\n this._scrollHeight = null;\n } // Private\n ;\n\n _proto._getConfig = function _getConfig(config) {\n config = _objectSpread({}, Default$6, typeof config === 'object' && config ? config : {});\n\n if (typeof config.target !== 'string') {\n var id = $(config.target).attr('id');\n\n if (!id) {\n id = Util.getUID(NAME$8);\n $(config.target).attr('id', id);\n }\n\n config.target = \"#\" + id;\n }\n\n Util.typeCheckConfig(NAME$8, config, DefaultType$6);\n return config;\n };\n\n _proto._getScrollTop = function _getScrollTop() {\n return this._scrollElement === window ? this._scrollElement.pageYOffset : this._scrollElement.scrollTop;\n };\n\n _proto._getScrollHeight = function _getScrollHeight() {\n return this._scrollElement.scrollHeight || Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);\n };\n\n _proto._getOffsetHeight = function _getOffsetHeight() {\n return this._scrollElement === window ? window.innerHeight : this._scrollElement.getBoundingClientRect().height;\n };\n\n _proto._process = function _process() {\n var scrollTop = this._getScrollTop() + this._config.offset;\n\n var scrollHeight = this._getScrollHeight();\n\n var maxScroll = this._config.offset + scrollHeight - this._getOffsetHeight();\n\n if (this._scrollHeight !== scrollHeight) {\n this.refresh();\n }\n\n if (scrollTop >= maxScroll) {\n var target = this._targets[this._targets.length - 1];\n\n if (this._activeTarget !== target) {\n this._activate(target);\n }\n\n return;\n }\n\n if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) {\n this._activeTarget = null;\n\n this._clear();\n\n return;\n }\n\n var offsetLength = this._offsets.length;\n\n for (var i = offsetLength; i--;) {\n var isActiveTarget = this._activeTarget !== this._targets[i] && scrollTop >= this._offsets[i] && (typeof this._offsets[i + 1] === 'undefined' || scrollTop < this._offsets[i + 1]);\n\n if (isActiveTarget) {\n this._activate(this._targets[i]);\n }\n }\n };\n\n _proto._activate = function _activate(target) {\n this._activeTarget = target;\n\n this._clear();\n\n var queries = this._selector.split(',').map(function (selector) {\n return selector + \"[data-target=\\\"\" + target + \"\\\"],\" + selector + \"[href=\\\"\" + target + \"\\\"]\";\n });\n\n var $link = $([].slice.call(document.querySelectorAll(queries.join(','))));\n\n if ($link.hasClass(ClassName$8.DROPDOWN_ITEM)) {\n $link.closest(Selector$8.DROPDOWN).find(Selector$8.DROPDOWN_TOGGLE).addClass(ClassName$8.ACTIVE);\n $link.addClass(ClassName$8.ACTIVE);\n } else {\n // Set triggered link as active\n $link.addClass(ClassName$8.ACTIVE); // Set triggered links parents as active\n // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor\n\n $link.parents(Selector$8.NAV_LIST_GROUP).prev(Selector$8.NAV_LINKS + \", \" + Selector$8.LIST_ITEMS).addClass(ClassName$8.ACTIVE); // Handle special case when .nav-link is inside .nav-item\n\n $link.parents(Selector$8.NAV_LIST_GROUP).prev(Selector$8.NAV_ITEMS).children(Selector$8.NAV_LINKS).addClass(ClassName$8.ACTIVE);\n }\n\n $(this._scrollElement).trigger(Event$8.ACTIVATE, {\n relatedTarget: target\n });\n };\n\n _proto._clear = function _clear() {\n [].slice.call(document.querySelectorAll(this._selector)).filter(function (node) {\n return node.classList.contains(ClassName$8.ACTIVE);\n }).forEach(function (node) {\n return node.classList.remove(ClassName$8.ACTIVE);\n });\n } // Static\n ;\n\n ScrollSpy._jQueryInterface = function _jQueryInterface(config) {\n return this.each(function () {\n var data = $(this).data(DATA_KEY$8);\n\n var _config = typeof config === 'object' && config;\n\n if (!data) {\n data = new ScrollSpy(this, _config);\n $(this).data(DATA_KEY$8, data);\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(\"No method named \\\"\" + config + \"\\\"\");\n }\n\n data[config]();\n }\n });\n };\n\n _createClass(ScrollSpy, null, [{\n key: \"VERSION\",\n get: function get() {\n return VERSION$8;\n }\n }, {\n key: \"Default\",\n get: function get() {\n return Default$6;\n }\n }]);\n\n return ScrollSpy;\n }();\n /**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n\n $(window).on(Event$8.LOAD_DATA_API, function () {\n var scrollSpys = [].slice.call(document.querySelectorAll(Selector$8.DATA_SPY));\n var scrollSpysLength = scrollSpys.length;\n\n for (var i = scrollSpysLength; i--;) {\n var $spy = $(scrollSpys[i]);\n\n ScrollSpy._jQueryInterface.call($spy, $spy.data());\n }\n });\n /**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n $.fn[NAME$8] = ScrollSpy._jQueryInterface;\n $.fn[NAME$8].Constructor = ScrollSpy;\n\n $.fn[NAME$8].noConflict = function () {\n $.fn[NAME$8] = JQUERY_NO_CONFLICT$8;\n return ScrollSpy._jQueryInterface;\n };\n\n /**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\n var NAME$9 = 'tab';\n var VERSION$9 = '4.3.1';\n var DATA_KEY$9 = 'bs.tab';\n var EVENT_KEY$9 = \".\" + DATA_KEY$9;\n var DATA_API_KEY$7 = '.data-api';\n var JQUERY_NO_CONFLICT$9 = $.fn[NAME$9];\n var Event$9 = {\n HIDE: \"hide\" + EVENT_KEY$9,\n HIDDEN: \"hidden\" + EVENT_KEY$9,\n SHOW: \"show\" + EVENT_KEY$9,\n SHOWN: \"shown\" + EVENT_KEY$9,\n CLICK_DATA_API: \"click\" + EVENT_KEY$9 + DATA_API_KEY$7\n };\n var ClassName$9 = {\n DROPDOWN_MENU: 'dropdown-menu',\n ACTIVE: 'active',\n DISABLED: 'disabled',\n FADE: 'fade',\n SHOW: 'show'\n };\n var Selector$9 = {\n DROPDOWN: '.dropdown',\n NAV_LIST_GROUP: '.nav, .list-group',\n ACTIVE: '.active',\n ACTIVE_UL: '> li > .active',\n DATA_TOGGLE: '[data-toggle=\"tab\"], [data-toggle=\"pill\"], [data-toggle=\"list\"]',\n DROPDOWN_TOGGLE: '.dropdown-toggle',\n DROPDOWN_ACTIVE_CHILD: '> .dropdown-menu .active'\n /**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\n };\n\n var Tab =\n /*#__PURE__*/\n function () {\n function Tab(element) {\n this._element = element;\n } // Getters\n\n\n var _proto = Tab.prototype;\n\n // Public\n _proto.show = function show() {\n var _this = this;\n\n if (this._element.parentNode && this._element.parentNode.nodeType === Node.ELEMENT_NODE && $(this._element).hasClass(ClassName$9.ACTIVE) || $(this._element).hasClass(ClassName$9.DISABLED)) {\n return;\n }\n\n var target;\n var previous;\n var listElement = $(this._element).closest(Selector$9.NAV_LIST_GROUP)[0];\n var selector = Util.getSelectorFromElement(this._element);\n\n if (listElement) {\n var itemSelector = listElement.nodeName === 'UL' || listElement.nodeName === 'OL' ? Selector$9.ACTIVE_UL : Selector$9.ACTIVE;\n previous = $.makeArray($(listElement).find(itemSelector));\n previous = previous[previous.length - 1];\n }\n\n var hideEvent = $.Event(Event$9.HIDE, {\n relatedTarget: this._element\n });\n var showEvent = $.Event(Event$9.SHOW, {\n relatedTarget: previous\n });\n\n if (previous) {\n $(previous).trigger(hideEvent);\n }\n\n $(this._element).trigger(showEvent);\n\n if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) {\n return;\n }\n\n if (selector) {\n target = document.querySelector(selector);\n }\n\n this._activate(this._element, listElement);\n\n var complete = function complete() {\n var hiddenEvent = $.Event(Event$9.HIDDEN, {\n relatedTarget: _this._element\n });\n var shownEvent = $.Event(Event$9.SHOWN, {\n relatedTarget: previous\n });\n $(previous).trigger(hiddenEvent);\n $(_this._element).trigger(shownEvent);\n };\n\n if (target) {\n this._activate(target, target.parentNode, complete);\n } else {\n complete();\n }\n };\n\n _proto.dispose = function dispose() {\n $.removeData(this._element, DATA_KEY$9);\n this._element = null;\n } // Private\n ;\n\n _proto._activate = function _activate(element, container, callback) {\n var _this2 = this;\n\n var activeElements = container && (container.nodeName === 'UL' || container.nodeName === 'OL') ? $(container).find(Selector$9.ACTIVE_UL) : $(container).children(Selector$9.ACTIVE);\n var active = activeElements[0];\n var isTransitioning = callback && active && $(active).hasClass(ClassName$9.FADE);\n\n var complete = function complete() {\n return _this2._transitionComplete(element, active, callback);\n };\n\n if (active && isTransitioning) {\n var transitionDuration = Util.getTransitionDurationFromElement(active);\n $(active).removeClass(ClassName$9.SHOW).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);\n } else {\n complete();\n }\n };\n\n _proto._transitionComplete = function _transitionComplete(element, active, callback) {\n if (active) {\n $(active).removeClass(ClassName$9.ACTIVE);\n var dropdownChild = $(active.parentNode).find(Selector$9.DROPDOWN_ACTIVE_CHILD)[0];\n\n if (dropdownChild) {\n $(dropdownChild).removeClass(ClassName$9.ACTIVE);\n }\n\n if (active.getAttribute('role') === 'tab') {\n active.setAttribute('aria-selected', false);\n }\n }\n\n $(element).addClass(ClassName$9.ACTIVE);\n\n if (element.getAttribute('role') === 'tab') {\n element.setAttribute('aria-selected', true);\n }\n\n Util.reflow(element);\n\n if (element.classList.contains(ClassName$9.FADE)) {\n element.classList.add(ClassName$9.SHOW);\n }\n\n if (element.parentNode && $(element.parentNode).hasClass(ClassName$9.DROPDOWN_MENU)) {\n var dropdownElement = $(element).closest(Selector$9.DROPDOWN)[0];\n\n if (dropdownElement) {\n var dropdownToggleList = [].slice.call(dropdownElement.querySelectorAll(Selector$9.DROPDOWN_TOGGLE));\n $(dropdownToggleList).addClass(ClassName$9.ACTIVE);\n }\n\n element.setAttribute('aria-expanded', true);\n }\n\n if (callback) {\n callback();\n }\n } // Static\n ;\n\n Tab._jQueryInterface = function _jQueryInterface(config) {\n return this.each(function () {\n var $this = $(this);\n var data = $this.data(DATA_KEY$9);\n\n if (!data) {\n data = new Tab(this);\n $this.data(DATA_KEY$9, data);\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(\"No method named \\\"\" + config + \"\\\"\");\n }\n\n data[config]();\n }\n });\n };\n\n _createClass(Tab, null, [{\n key: \"VERSION\",\n get: function get() {\n return VERSION$9;\n }\n }]);\n\n return Tab;\n }();\n /**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n\n $(document).on(Event$9.CLICK_DATA_API, Selector$9.DATA_TOGGLE, function (event) {\n event.preventDefault();\n\n Tab._jQueryInterface.call($(this), 'show');\n });\n /**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n $.fn[NAME$9] = Tab._jQueryInterface;\n $.fn[NAME$9].Constructor = Tab;\n\n $.fn[NAME$9].noConflict = function () {\n $.fn[NAME$9] = JQUERY_NO_CONFLICT$9;\n return Tab._jQueryInterface;\n };\n\n /**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\n var NAME$a = 'toast';\n var VERSION$a = '4.3.1';\n var DATA_KEY$a = 'bs.toast';\n var EVENT_KEY$a = \".\" + DATA_KEY$a;\n var JQUERY_NO_CONFLICT$a = $.fn[NAME$a];\n var Event$a = {\n CLICK_DISMISS: \"click.dismiss\" + EVENT_KEY$a,\n HIDE: \"hide\" + EVENT_KEY$a,\n HIDDEN: \"hidden\" + EVENT_KEY$a,\n SHOW: \"show\" + EVENT_KEY$a,\n SHOWN: \"shown\" + EVENT_KEY$a\n };\n var ClassName$a = {\n FADE: 'fade',\n HIDE: 'hide',\n SHOW: 'show',\n SHOWING: 'showing'\n };\n var DefaultType$7 = {\n animation: 'boolean',\n autohide: 'boolean',\n delay: 'number'\n };\n var Default$7 = {\n animation: true,\n autohide: true,\n delay: 500\n };\n var Selector$a = {\n DATA_DISMISS: '[data-dismiss=\"toast\"]'\n /**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\n };\n\n var Toast =\n /*#__PURE__*/\n function () {\n function Toast(element, config) {\n this._element = element;\n this._config = this._getConfig(config);\n this._timeout = null;\n\n this._setListeners();\n } // Getters\n\n\n var _proto = Toast.prototype;\n\n // Public\n _proto.show = function show() {\n var _this = this;\n\n $(this._element).trigger(Event$a.SHOW);\n\n if (this._config.animation) {\n this._element.classList.add(ClassName$a.FADE);\n }\n\n var complete = function complete() {\n _this._element.classList.remove(ClassName$a.SHOWING);\n\n _this._element.classList.add(ClassName$a.SHOW);\n\n $(_this._element).trigger(Event$a.SHOWN);\n\n if (_this._config.autohide) {\n _this.hide();\n }\n };\n\n this._element.classList.remove(ClassName$a.HIDE);\n\n this._element.classList.add(ClassName$a.SHOWING);\n\n if (this._config.animation) {\n var transitionDuration = Util.getTransitionDurationFromElement(this._element);\n $(this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);\n } else {\n complete();\n }\n };\n\n _proto.hide = function hide(withoutTimeout) {\n var _this2 = this;\n\n if (!this._element.classList.contains(ClassName$a.SHOW)) {\n return;\n }\n\n $(this._element).trigger(Event$a.HIDE);\n\n if (withoutTimeout) {\n this._close();\n } else {\n this._timeout = setTimeout(function () {\n _this2._close();\n }, this._config.delay);\n }\n };\n\n _proto.dispose = function dispose() {\n clearTimeout(this._timeout);\n this._timeout = null;\n\n if (this._element.classList.contains(ClassName$a.SHOW)) {\n this._element.classList.remove(ClassName$a.SHOW);\n }\n\n $(this._element).off(Event$a.CLICK_DISMISS);\n $.removeData(this._element, DATA_KEY$a);\n this._element = null;\n this._config = null;\n } // Private\n ;\n\n _proto._getConfig = function _getConfig(config) {\n config = _objectSpread({}, Default$7, $(this._element).data(), typeof config === 'object' && config ? config : {});\n Util.typeCheckConfig(NAME$a, config, this.constructor.DefaultType);\n return config;\n };\n\n _proto._setListeners = function _setListeners() {\n var _this3 = this;\n\n $(this._element).on(Event$a.CLICK_DISMISS, Selector$a.DATA_DISMISS, function () {\n return _this3.hide(true);\n });\n };\n\n _proto._close = function _close() {\n var _this4 = this;\n\n var complete = function complete() {\n _this4._element.classList.add(ClassName$a.HIDE);\n\n $(_this4._element).trigger(Event$a.HIDDEN);\n };\n\n this._element.classList.remove(ClassName$a.SHOW);\n\n if (this._config.animation) {\n var transitionDuration = Util.getTransitionDurationFromElement(this._element);\n $(this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration);\n } else {\n complete();\n }\n } // Static\n ;\n\n Toast._jQueryInterface = function _jQueryInterface(config) {\n return this.each(function () {\n var $element = $(this);\n var data = $element.data(DATA_KEY$a);\n\n var _config = typeof config === 'object' && config;\n\n if (!data) {\n data = new Toast(this, _config);\n $element.data(DATA_KEY$a, data);\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(\"No method named \\\"\" + config + \"\\\"\");\n }\n\n data[config](this);\n }\n });\n };\n\n _createClass(Toast, null, [{\n key: \"VERSION\",\n get: function get() {\n return VERSION$a;\n }\n }, {\n key: \"DefaultType\",\n get: function get() {\n return DefaultType$7;\n }\n }, {\n key: \"Default\",\n get: function get() {\n return Default$7;\n }\n }]);\n\n return Toast;\n }();\n /**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n\n $.fn[NAME$a] = Toast._jQueryInterface;\n $.fn[NAME$a].Constructor = Toast;\n\n $.fn[NAME$a].noConflict = function () {\n $.fn[NAME$a] = JQUERY_NO_CONFLICT$a;\n return Toast._jQueryInterface;\n };\n\n /**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.3.1): index.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n (function () {\n if (typeof $ === 'undefined') {\n throw new TypeError('Bootstrap\\'s JavaScript requires jQuery. jQuery must be included before Bootstrap\\'s JavaScript.');\n }\n\n var version = $.fn.jquery.split(' ')[0].split('.');\n var minMajor = 1;\n var ltMajor = 2;\n var minMinor = 9;\n var minPatch = 1;\n var maxMajor = 4;\n\n if (version[0] < ltMajor && version[1] < minMinor || version[0] === minMajor && version[1] === minMinor && version[2] < minPatch || version[0] >= maxMajor) {\n throw new Error('Bootstrap\\'s JavaScript requires at least jQuery v1.9.1 but less than v4.0.0');\n }\n })();\n\n exports.Util = Util;\n exports.Alert = Alert;\n exports.Button = Button;\n exports.Carousel = Carousel;\n exports.Collapse = Collapse;\n exports.Dropdown = Dropdown;\n exports.Modal = Modal;\n exports.Popover = Popover;\n exports.Scrollspy = ScrollSpy;\n exports.Tab = Tab;\n exports.Toast = Toast;\n exports.Tooltip = Tooltip;\n\n Object.defineProperty(exports, '__esModule', { value: true });\n\n}));\n//# sourceMappingURL=bootstrap.js.map","\"use strict\";\nvar Platform = {};\n\n(function () {\n\n Platform.detectDevice = function () {\n var body = document.body;\n var ua = navigator.userAgent;\n var checker = {\n // OS\n Windows: ua.match(/Windows/),\n MacOS: ua.match(/Mac/),\n Android: ua.match(/Android/),\n\n // Browser\n Msie: ua.match(/Trident/),\n Edge: ua.match(/Edge/),\n Chrome: ua.match(/Chrome/),\n Firefox: ua.match(/Firefox/),\n Safari: ua.match(/Safari/),\n\n // Device\n isApple: ua.match(/(iPhone|iPod|iPad)/),\n iPhone: ua.match(/iPhone/),\n iPad: ua.match(/iPad/),\n iPod: ua.match(/iPod/),\n };\n\n if (checker.isApple) {\n // Apple\n body.classList.add('isApple');\n\n if (checker.iPhone) {\n // Apple iPhone\n body.classList.add('iphone');\n } else if (checker.iPad) {\n // Apple iPad\n body.classList.add('ipad');\n } else if (checker.iPod) {\n // Apple iPod\n body.classList.add('ipod');\n }\n\n } else if (checker.Windows){\n // Windows OS\n body.classList.add('windowsOS');\n\n if (checker.Edge){\n // Edge Browser\n body.classList.add('edge');\n } else if (checker.Chrome){\n // Chrome Browser\n body.classList.add('chrome');\n } else if(checker.Safari){\n // Safari Browser\n body.classList.add('safari');\n } else if(checker.Firefox){\n // Firefox Browser\n body.classList.add('firefox');\n } else if(checker.Msie){\n // IE Browser\n body.classList.add('msie');\n }\n\n } else if (checker.MacOS){\n // Mac OS\n body.classList.add('macOS');\n\n if (checker.Chrome){\n // Chrome Browser\n body.classList.add('chrome');\n } else if(checker.Safari){\n // Safari Browser\n body.classList.add('safari');\n } else if(checker.Firefox){\n // Firefox Browser\n body.classList.add('firefox');\n }\n\n } else if (checker.Android){\n // Android OS\n body.classList.add('AndroidOS');\n }\n\n }\n\n Platform.detectDevice();\n\n})($);\n","\"use strict\";\n\n\njQuery(document).ready(function() {\n //removeIf(production)\n console.log(\"document ready\");\n //endRemoveIf(production)\n});\n\nfunction copyCodeToClipboard(event, element) {\n event.preventDefault();\n event.stopPropagation();\n\n const textInput = element.nextSibling.nextSibling;\n textInput.select();\n\n\ttry {\n\t\tif (document.execCommand('copy')) {\n element.innerHTML = 'Copied';\n \n setTimeout(function() {\n element.innerHTML = 'Copy';\n }, 3000);\n\t\t}\n\t} catch (err) {\n\t\talert('Please use CTRL/CMD + C to copy.');\n\t\tconsole.log('Oops, unable to copy', err);\n\t}\n\n return false;\n}\n"]}
\ No newline at end of file
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>openHAB - Miele Cloud Binding Configuration - Status</title>
+ <link rel="shortcut icon" type="image/x-icon" href="/mielecloud/assets/img/favicon.ico">
+
+ <link rel="stylesheet" href="/mielecloud/assets/css/main.css">
+ <link rel="stylesheet" href="/mielecloud/assets/css/rtl.css">
+</head>
+
+<body>
+ <div class="container">
+ <div class="logo-container">
+ <img src="/mielecloud/assets/img/OpenHAB_logo.svg" width="330" />
+
+ <img src="/mielecloud/assets/img/miele.png" width="190" />
+ </div>
+
+
+ <h2>Cloud Binding Configuration</h2>
+
+ <ul class="statusbar">
+ <li>Overview</li>
+ <li>Pairing</li>
+ <li class="active">Status</li>
+ </ul>
+ <main role="main">
+ <section class="mt-4 mb-5">
+ <div id="success-body">
+ <h3>Pairing failed!</h3>
+ <p>
+ <!-- ERROR MESSAGE TEXT -->
+ </p>
+ </div>
+
+ <a href="/mielecloud" class="btn btn-danger btn-lg">Go back to account overview</a>
+ </section>
+
+ </main>
+ <script src="/mielecloud/assets/js/main.js"></script>
+ </div>
+</body>
+
+</html>
\ No newline at end of file
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>openHAB - Miele Cloud Binding Configuration - Home</title>
+ <link rel="shortcut icon" type="image/x-icon" href="/mielecloud/assets/img/favicon.ico">
+
+ <link rel="stylesheet" href="/mielecloud/assets/css/main.css">
+ <link rel="stylesheet" href="/mielecloud/assets/css/rtl.css">
+</head>
+
+<body>
+ <div class="container">
+ <div class="logo-container">
+ <img src="/mielecloud/assets/img/OpenHAB_logo.svg" width="330" />
+
+ <img src="/mielecloud/assets/img/miele.png" width="190" />
+ </div>
+
+
+ <h2>Cloud Binding Configuration</h2>
+
+ <ul class="statusbar">
+ <li class="active">Overview</li>
+ <li>Pairing</li>
+ <li>Status</li>
+ </ul>
+ <main role="main">
+ <section class="mt-4 mb-5">
+ <h3><!-- BRIDGES TITLE --></h3>
+
+ <ul class="accounts">
+ <!-- BRIDGES -->
+ </ul>
+
+ <!-- NO SSL WARNING -->
+
+ <div class="controls">
+ <a href="/mielecloud/pair" class="btn btn-danger btn-lg">Pair Account</a>
+ </div>
+ </section>
+
+ </main>
+ <script src="/mielecloud/assets/js/main.js"></script>
+ </div>
+</body>
+
+</html>
\ No newline at end of file
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>openHAB - Miele Cloud Binding Configuration - Pairing</title>
+ <link rel="shortcut icon" type="image/x-icon" href="/mielecloud/assets/img/favicon.ico">
+
+ <link rel="stylesheet" href="/mielecloud/assets/css/main.css">
+ <link rel="stylesheet" href="/mielecloud/assets/css/rtl.css">
+</head>
+
+<body>
+ <div class="container">
+ <div class="logo-container">
+ <img src="/mielecloud/assets/img/OpenHAB_logo.svg" width="330" />
+
+ <img src="/mielecloud/assets/img/miele.png" width="190" />
+ </div>
+
+
+ <h2>Cloud Binding Configuration</h2>
+
+ <ul class="statusbar">
+ <li>Overview</li>
+ <li class="active">Pairing</li>
+ <li>Status</li>
+ </ul>
+ <main role="main">
+ <section class="mt-4 mb-5">
+ <div id="pair-body">
+ <p>
+ Go to <a href="https://www.miele.com/f/com/en/register_api.aspx">the Miele developer portal</a> to obtain your
+ client ID and client secret.
+ </p>
+
+ <!-- ERROR MESSAGE -->
+
+ <form action="/mielecloud/forwardToLogin">
+ <div class="form-group">
+ <label for="clientId">Client ID:</label>
+ <input type="text" class="form-control" id="clientId" name="clientId" placeholder="Enter your client ID" value="<!-- CLIENT ID -->" required />
+ </div>
+ <div class="form-group">
+ <label for="clientSecret">Client Secret:</label>
+ <input type="text" class="form-control" id="clientSecret" name="clientSecret" placeholder="Enter your client secret" value="<!-- CLIENT SECRET -->" required />
+ </div>
+ <div class="form-group">
+ <label for="bridgeId">Bridge ID:</label>
+ <input type="text" class="form-control" id="bridgeId" name="bridgeId" placeholder="Enter the bridge ID to use for pairing" required pattern="[A-Za-z0-9_-]*" />
+ </div>
+ <div class="form-group">
+ <label for="email">E-mail address:</label>
+ <input type="text" class="form-control" id="email" name="email" placeholder="Enter the e-mail address associated with you Miele Cloud Account" required pattern="[a-z0-9._%+-]{3,}@[a-z]{3,}([.]{1}[a-z]{2,}|[.]{1}[a-z]{2,}[.]{1}[a-z]{2,})" />
+ </div>
+ <button type="submit" class="btn btn-danger btn-lg">Pair Account</button>
+ </form>
+ </div>
+ </section>
+
+ </main>
+ <script src="/mielecloud/assets/js/main.js"></script>
+ </div>
+</body>
+
+</html>
\ No newline at end of file
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>openHAB - Miele Cloud Binding Configuration - Status</title>
+ <link rel="shortcut icon" type="image/x-icon" href="/mielecloud/assets/img/favicon.ico">
+
+ <link rel="stylesheet" href="/mielecloud/assets/css/main.css">
+ <link rel="stylesheet" href="/mielecloud/assets/css/rtl.css">
+</head>
+
+<body>
+ <div class="container">
+ <div class="logo-container">
+ <img src="/mielecloud/assets/img/OpenHAB_logo.svg" width="330" />
+
+ <img src="/mielecloud/assets/img/miele.png" width="190" />
+ </div>
+
+
+ <h2>Cloud Binding Configuration</h2>
+
+ <ul class="statusbar">
+ <li>Overview</li>
+ <li>Pairing</li>
+ <li class="active">Status</li>
+ </ul>
+ <main role="main">
+ <section class="mt-4 mb-5">
+ <script type="text/javascript">
+ window.onload = function() {
+ var locale = document.getElementById("locale").value
+ updateLocale(locale)
+ }
+
+ function onLocaleChanged(event) {
+ var locale = event.target.value
+ updateLocale(locale)
+ }
+
+ function updateLocale(locale) {
+ var thingsTemplate = document.getElementById("things-template")
+ thingsTemplate.innerHTML = thingsTemplate.innerHTML.replace(/locale=".."/g, "locale=\"" + locale + "\"")
+ }
+ </script>
+ <div id="success-body">
+ <div>
+ <h3>Pairing successful!</h3>
+ <!-- ERROR MESSAGE TEXT -->
+ <p>You can now either let us automatically create and configure a bridge thing for you or configure it via a things-file.</p>
+ <p>Please choose a locale to use for display purposes.</p>
+
+ <form action="/mielecloud/createBridgeThing">
+ <div class="form-group">
+ <input type="hidden" name="bridgeUid" value="<!-- BRIDGE UID -->" />
+ <input type="hidden" name="email" value="<!-- EMAIL -->" />
+ <label for="locale">Locale:</label>
+ <select class="form-control" id="locale" name="locale" onchange="onLocaleChanged(event);">
+ <!-- LOCALE OPTIONS -->
+ </select>
+ </div>
+ <button type="submit" class="btn btn-danger btn-lg">Create and Configure</button>
+ </form>
+ </div>
+
+ <div class="things">
+ <span class="legend">
+ or use this .things-file template:
+ </span>
+ <div class="code-container">
+ <a href="#" onclick="copyCodeToClipboard(event, this);" class="btn btn-outline-info btn-sm copy">Copy</a>
+ <textarea id="things-template" readonly><!-- THINGS TEMPLATE CODE --></textarea>
+ </div>
+ </div>
+
+ <div class="controls">
+ <a href="/mielecloud" class="btn btn-info btn-lg">Back to overview</a>
+ </div>
+ </div>
+ </section>
+
+ </main>
+ <script src="/mielecloud/assets/js/main.js"></script>
+ </div>
+</body>
+
+</html>
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public final class MieleCloudBindingTestConstants {
+ private MieleCloudBindingTestConstants() {
+ throw new IllegalStateException("MieleCloudTestConstants must not be instantiated");
+ }
+
+ public static final String BRIDGE_ID = "genesis";
+
+ public static final String SERVICE_HANDLE = MieleCloudBindingConstants.THING_TYPE_BRIDGE.getAsString() + ":"
+ + BRIDGE_ID;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.auth;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingTestConstants;
+import org.openhab.binding.mielecloud.internal.util.ReflectionUtil;
+import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener;
+import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
+import org.openhab.core.auth.client.oauth2.OAuthClientService;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
+import org.openhab.core.auth.client.oauth2.OAuthResponseException;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class OpenHabOAuthTokenRefresherTest {
+ private static final String ACCESS_TOKEN = "DE_0123456789abcdef0123456789abcdef";
+
+ private boolean hasAccessTokenRefreshListenerForServiceHandle(OpenHabOAuthTokenRefresher refresher,
+ String serviceHandle)
+ throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+ return ReflectionUtil
+ .<Map<String, @Nullable AccessTokenRefreshListener>> getPrivate(refresher, "listenerByServiceHandle")
+ .get(MieleCloudBindingTestConstants.SERVICE_HANDLE) != null;
+ }
+
+ private AccessTokenRefreshListener getAccessTokenRefreshListenerByServiceHandle(
+ OpenHabOAuthTokenRefresher refresher, String serviceHandle)
+ throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+ AccessTokenRefreshListener listener = ReflectionUtil
+ .<Map<String, @Nullable AccessTokenRefreshListener>> getPrivate(refresher, "listenerByServiceHandle")
+ .get(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+ assertNotNull(listener);
+ return Objects.requireNonNull(listener);
+ }
+
+ @Test
+ public void whenTheAccountWasNotConfiguredPriorToTheThingInitializingThenNoRefreshListenerCanBeRegistered() {
+ // given:
+ OAuthFactory oauthFactory = mock(OAuthFactory.class);
+ OpenHabOAuthTokenRefresher refresher = new OpenHabOAuthTokenRefresher(oauthFactory);
+ OAuthTokenRefreshListener listener = mock(OAuthTokenRefreshListener.class);
+
+ // when:
+ assertThrows(OAuthException.class, () -> {
+ refresher.setRefreshListener(listener, MieleCloudBindingTestConstants.SERVICE_HANDLE);
+ });
+ }
+
+ @Test
+ public void whenARefreshListenerIsRegisteredThenAListenerIsRegisteredAtTheClientService()
+ throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+ // given:
+ OAuthClientService oauthClientService = mock(OAuthClientService.class);
+
+ OAuthFactory oauthFactory = mock(OAuthFactory.class);
+ when(oauthFactory.getOAuthClientService(MieleCloudBindingTestConstants.SERVICE_HANDLE))
+ .thenReturn(oauthClientService);
+
+ OpenHabOAuthTokenRefresher refresher = new OpenHabOAuthTokenRefresher(oauthFactory);
+
+ OAuthTokenRefreshListener listener = mock(OAuthTokenRefreshListener.class);
+
+ // when:
+ refresher.setRefreshListener(listener, MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+ // then:
+ verify(oauthClientService).addAccessTokenRefreshListener(any());
+ assertNotNull(
+ getAccessTokenRefreshListenerByServiceHandle(refresher, MieleCloudBindingTestConstants.SERVICE_HANDLE));
+ }
+
+ @Test
+ public void whenTokenIsRefreshedThenTheListenerIsCalledWithTheNewAccessToken()
+ throws org.openhab.core.auth.client.oauth2.OAuthException, IOException, OAuthResponseException {
+ // given:
+ AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
+ accessTokenResponse.setAccessToken(ACCESS_TOKEN);
+
+ OAuthClientService oauthClientService = mock(OAuthClientService.class);
+
+ OAuthFactory oauthFactory = mock(OAuthFactory.class);
+ when(oauthFactory.getOAuthClientService(MieleCloudBindingTestConstants.SERVICE_HANDLE))
+ .thenReturn(oauthClientService);
+
+ OpenHabOAuthTokenRefresher refresher = new OpenHabOAuthTokenRefresher(oauthFactory);
+ when(oauthClientService.refreshToken()).thenAnswer(new Answer<@Nullable AccessTokenResponse>() {
+ @Override
+ @Nullable
+ public AccessTokenResponse answer(@Nullable InvocationOnMock invocation) throws Throwable {
+ getAccessTokenRefreshListenerByServiceHandle(refresher, MieleCloudBindingTestConstants.SERVICE_HANDLE)
+ .onAccessTokenResponse(accessTokenResponse);
+ return accessTokenResponse;
+ }
+ });
+
+ OAuthTokenRefreshListener listener = mock(OAuthTokenRefreshListener.class);
+ refresher.setRefreshListener(listener, MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+ // when:
+ refresher.refreshToken(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+ // then:
+ verify(listener).onNewAccessToken(ACCESS_TOKEN);
+ }
+
+ @Test
+ public void whenTokenIsRefreshedAndNoAccessTokenIsProvidedThenTheListenerIsNotNotified()
+ throws org.openhab.core.auth.client.oauth2.OAuthException, IOException, OAuthResponseException {
+ // given:
+ AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
+
+ OAuthClientService oauthClientService = mock(OAuthClientService.class);
+
+ OAuthFactory oauthFactory = mock(OAuthFactory.class);
+ when(oauthFactory.getOAuthClientService(MieleCloudBindingTestConstants.SERVICE_HANDLE))
+ .thenReturn(oauthClientService);
+
+ OpenHabOAuthTokenRefresher refresher = new OpenHabOAuthTokenRefresher(oauthFactory);
+ when(oauthClientService.refreshToken()).thenAnswer(new Answer<@Nullable AccessTokenResponse>() {
+ @Override
+ @Nullable
+ public AccessTokenResponse answer(@Nullable InvocationOnMock invocation) throws Throwable {
+ getAccessTokenRefreshListenerByServiceHandle(refresher, MieleCloudBindingTestConstants.SERVICE_HANDLE)
+ .onAccessTokenResponse(accessTokenResponse);
+ return accessTokenResponse;
+ }
+ });
+
+ OAuthTokenRefreshListener listener = mock(OAuthTokenRefreshListener.class);
+ refresher.setRefreshListener(listener, MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+ // when:
+ assertThrows(OAuthException.class, () -> {
+ try {
+ refresher.refreshToken(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+ } catch (OAuthException e) {
+ verifyNoInteractions(listener);
+ throw e;
+ }
+ });
+ }
+
+ @Test
+ public void whenTokenRefreshFailsWithOAuthExceptionThenTheListenerIsNotNotified()
+ throws org.openhab.core.auth.client.oauth2.OAuthException, IOException, OAuthResponseException {
+ // given:
+ OAuthClientService oauthClientService = mock(OAuthClientService.class);
+ when(oauthClientService.refreshToken()).thenThrow(new org.openhab.core.auth.client.oauth2.OAuthException());
+
+ OAuthFactory oauthFactory = mock(OAuthFactory.class);
+ when(oauthFactory.getOAuthClientService(MieleCloudBindingTestConstants.SERVICE_HANDLE))
+ .thenReturn(oauthClientService);
+
+ OpenHabOAuthTokenRefresher refresher = new OpenHabOAuthTokenRefresher(oauthFactory);
+
+ OAuthTokenRefreshListener listener = mock(OAuthTokenRefreshListener.class);
+ refresher.setRefreshListener(listener, MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+ // when:
+ assertThrows(OAuthException.class, () -> {
+ try {
+ refresher.refreshToken(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+ } catch (OAuthException e) {
+ verifyNoInteractions(listener);
+ throw e;
+ }
+ });
+ }
+
+ @Test
+ public void whenTokenRefreshFailsDueToNetworkErrorThenTheListenerIsNotNotified()
+ throws org.openhab.core.auth.client.oauth2.OAuthException, IOException, OAuthResponseException {
+ // given:
+ OAuthClientService oauthClientService = mock(OAuthClientService.class);
+ when(oauthClientService.refreshToken()).thenThrow(new IOException());
+
+ OAuthFactory oauthFactory = mock(OAuthFactory.class);
+ when(oauthFactory.getOAuthClientService(MieleCloudBindingTestConstants.SERVICE_HANDLE))
+ .thenReturn(oauthClientService);
+
+ OpenHabOAuthTokenRefresher refresher = new OpenHabOAuthTokenRefresher(oauthFactory);
+
+ OAuthTokenRefreshListener listener = mock(OAuthTokenRefreshListener.class);
+ refresher.setRefreshListener(listener, MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+ // when:
+ assertThrows(OAuthException.class, () -> {
+ try {
+ refresher.refreshToken(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+ } catch (OAuthException e) {
+ verifyNoInteractions(listener);
+ throw e;
+ }
+ });
+ }
+
+ @Test
+ public void whenTokenRefreshFailsDueToAnIllegalResponseThenTheListenerIsNotNotified()
+ throws org.openhab.core.auth.client.oauth2.OAuthException, IOException, OAuthResponseException {
+ // given:
+ OAuthClientService oauthClientService = mock(OAuthClientService.class);
+ when(oauthClientService.refreshToken()).thenThrow(new OAuthResponseException());
+
+ OAuthFactory oauthFactory = mock(OAuthFactory.class);
+ when(oauthFactory.getOAuthClientService(MieleCloudBindingTestConstants.SERVICE_HANDLE))
+ .thenReturn(oauthClientService);
+
+ OpenHabOAuthTokenRefresher refresher = new OpenHabOAuthTokenRefresher(oauthFactory);
+
+ OAuthTokenRefreshListener listener = mock(OAuthTokenRefreshListener.class);
+ refresher.setRefreshListener(listener, MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+ // when:
+ assertThrows(OAuthException.class, () -> {
+ try {
+ refresher.refreshToken(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+ } catch (OAuthException e) {
+ verifyNoInteractions(listener);
+ throw e;
+ }
+ });
+ }
+
+ @Test
+ public void whenTheRefreshListenerIsUnsetAndWasNotRegisteredBeforeThenNothingHappens()
+ throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+ // given:
+ OAuthFactory oauthFactory = mock(OAuthFactory.class);
+ OpenHabOAuthTokenRefresher refresher = new OpenHabOAuthTokenRefresher(oauthFactory);
+
+ // when:
+ refresher.unsetRefreshListener(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+ // then:
+ assertFalse(hasAccessTokenRefreshListenerForServiceHandle(refresher,
+ MieleCloudBindingTestConstants.SERVICE_HANDLE));
+ }
+
+ @Test
+ public void whenTheRefreshListenerIsUnsetAndTheClientServiceIsNotAvailableThenTheListenerIsCleared()
+ throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+ // given:
+ OAuthClientService oauthClientService = mock(OAuthClientService.class);
+
+ OAuthFactory oauthFactory = mock(OAuthFactory.class);
+ when(oauthFactory.getOAuthClientService(MieleCloudBindingTestConstants.SERVICE_HANDLE))
+ .thenReturn(oauthClientService);
+
+ OpenHabOAuthTokenRefresher refresher = new OpenHabOAuthTokenRefresher(oauthFactory);
+
+ OAuthTokenRefreshListener listener = mock(OAuthTokenRefreshListener.class);
+
+ refresher.setRefreshListener(listener, MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+ // when:
+ refresher.unsetRefreshListener(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+ // then:
+ assertFalse(hasAccessTokenRefreshListenerForServiceHandle(refresher,
+ MieleCloudBindingTestConstants.SERVICE_HANDLE));
+ }
+
+ @Test
+ public void whenTheRefreshListenerIsUnsetThenTheListenerIsClearedAndRemovedFromTheClientService()
+ throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+ // given:
+ OAuthClientService oauthClientService = mock(OAuthClientService.class);
+
+ OAuthFactory oauthFactory = mock(OAuthFactory.class);
+ when(oauthFactory.getOAuthClientService(MieleCloudBindingTestConstants.SERVICE_HANDLE))
+ .thenReturn(oauthClientService);
+
+ OpenHabOAuthTokenRefresher refresher = new OpenHabOAuthTokenRefresher(oauthFactory);
+
+ OAuthTokenRefreshListener listener = mock(OAuthTokenRefreshListener.class);
+
+ refresher.setRefreshListener(listener, MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+ // when:
+ refresher.unsetRefreshListener(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+ // then:
+ verify(oauthClientService).removeAccessTokenRefreshListener(any());
+ assertFalse(hasAccessTokenRefreshListenerForServiceHandle(refresher,
+ MieleCloudBindingTestConstants.SERVICE_HANDLE));
+ }
+
+ @Test
+ public void whenTokensAreRemovedThenTheRuntimeIsRequestedToDeleteServiceAndAccessToken()
+ throws org.openhab.core.auth.client.oauth2.OAuthException, IOException, OAuthResponseException {
+ // given:
+ OAuthClientService oauthClientService = mock(OAuthClientService.class);
+
+ OAuthFactory oauthFactory = mock(OAuthFactory.class);
+ when(oauthFactory.getOAuthClientService(MieleCloudBindingTestConstants.SERVICE_HANDLE))
+ .thenReturn(oauthClientService);
+
+ OpenHabOAuthTokenRefresher refresher = new OpenHabOAuthTokenRefresher(oauthFactory);
+
+ OAuthTokenRefreshListener listener = mock(OAuthTokenRefreshListener.class);
+ refresher.setRefreshListener(listener, MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+ // when:
+ refresher.removeTokensFromStorage(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+ // then:
+ verify(oauthFactory).deleteServiceAndAccessToken(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.getPrivate;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentMatchers;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingTestConstants;
+import org.openhab.binding.mielecloud.internal.auth.OAuthException;
+import org.openhab.binding.mielecloud.internal.config.exception.NoOngoingAuthorizationException;
+import org.openhab.binding.mielecloud.internal.config.exception.OngoingAuthorizationException;
+import org.openhab.core.auth.client.oauth2.OAuthClientService;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
+import org.openhab.core.auth.client.oauth2.OAuthResponseException;
+import org.openhab.core.thing.ThingUID;
+
+/**
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class OAuthAuthorizationHandlerImplTest {
+ private static final String CLIENT_ID = "01234567-890a-bcde-f012-34567890abcd";
+ private static final String CLIENT_SECRET = "0123456789abcdefghijklmnopqrstiu";
+ private static final String REDIRECT_URL = "http://127.0.0.1:8080/mielecloud/result";
+ private static final String AUTH_CODE = "abcdef";
+ private static final ThingUID BRIDGE_UID = new ThingUID(MieleCloudBindingConstants.THING_TYPE_BRIDGE,
+ MieleCloudBindingTestConstants.BRIDGE_ID);
+ private static final String EMAIL = "openhab@openhab.org";
+
+ @Nullable
+ private OAuthClientService clientService;
+ @Nullable
+ private ScheduledFuture<?> timer;
+ @Nullable
+ private Runnable scheduledRunnable;
+ @Nullable
+ private OAuthAuthorizationHandler authorizationHandler;
+
+ private OAuthClientService getClientService() {
+ final OAuthClientService clientService = this.clientService;
+ assertNotNull(clientService);
+ return Objects.requireNonNull(clientService);
+ }
+
+ private ScheduledFuture<?> getTimer() {
+ final ScheduledFuture<?> timer = this.timer;
+ assertNotNull(timer);
+ return Objects.requireNonNull(timer);
+ }
+
+ private Runnable getScheduledRunnable() {
+ final Runnable scheduledRunnable = this.scheduledRunnable;
+ assertNotNull(scheduledRunnable);
+ return Objects.requireNonNull(scheduledRunnable);
+ }
+
+ private OAuthAuthorizationHandler getAuthorizationHandler() {
+ final OAuthAuthorizationHandler authorizationHandler = this.authorizationHandler;
+ assertNotNull(authorizationHandler);
+ return Objects.requireNonNull(authorizationHandler);
+ }
+
+ @BeforeEach
+ public void setUp() {
+ OAuthClientService clientService = mock(OAuthClientService.class);
+
+ OAuthFactory oauthFactory = mock(OAuthFactory.class);
+ when(oauthFactory.createOAuthClientService(anyString(), anyString(), anyString(), anyString(), anyString(),
+ isNull(), any())).thenReturn(clientService);
+
+ ScheduledFuture<?> timer = mock(ScheduledFuture.class);
+ when(timer.isDone()).thenReturn(false);
+
+ ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+ when(scheduler.schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any())).thenAnswer(invocation -> {
+ scheduledRunnable = invocation.getArgument(0);
+ return timer;
+ });
+
+ OAuthAuthorizationHandler authorizationHandler = new OAuthAuthorizationHandlerImpl(oauthFactory, scheduler);
+
+ this.clientService = clientService;
+ this.timer = timer;
+ this.scheduledRunnable = null;
+ this.authorizationHandler = authorizationHandler;
+ }
+
+ @Test
+ public void whenTheAuthorizationIsCompletedInTimeThenTheTimerIsCancelledAndAllResourcesAreCleanedUp()
+ throws Exception {
+ // given:
+ getAuthorizationHandler().beginAuthorization(CLIENT_ID, CLIENT_SECRET, BRIDGE_UID, EMAIL);
+ getAuthorizationHandler().getAuthorizationUrl(REDIRECT_URL);
+
+ // when:
+ getAuthorizationHandler().completeAuthorization("http://127.0.0.1:8080/mielecloud/result?code=abc&state=def");
+
+ // then:
+ assertNull(getPrivate(getAuthorizationHandler(), "timer"));
+ verify(getTimer()).cancel(false);
+
+ assertNull(getPrivate(getAuthorizationHandler(), "oauthClientService"));
+ assertNull(getPrivate(getAuthorizationHandler(), "bridgeUid"));
+ assertNull(getPrivate(getAuthorizationHandler(), "redirectUri"));
+ assertNull(getPrivate(getAuthorizationHandler(), "email"));
+
+ verify(getClientService()).extractAuthCodeFromAuthResponse(anyString());
+ verify(getClientService()).getAccessTokenResponseByAuthorizationCode(isNull(), anyString());
+ }
+
+ @Test
+ public void whenTheAuthorizationTimesOutThenTheOngoingAuthorizationIsCancelled() throws Exception {
+ // given:
+ getAuthorizationHandler().beginAuthorization(CLIENT_ID, CLIENT_SECRET, BRIDGE_UID, EMAIL);
+ getAuthorizationHandler().getAuthorizationUrl(REDIRECT_URL);
+
+ // when:
+ getScheduledRunnable().run();
+
+ // then:
+ assertNull(getPrivate(getAuthorizationHandler(), "oauthClientService"));
+ assertNull(getPrivate(getAuthorizationHandler(), "bridgeUid"));
+ assertNull(getPrivate(getAuthorizationHandler(), "redirectUri"));
+ assertNull(getPrivate(getAuthorizationHandler(), "email"));
+ assertNull(getPrivate(getAuthorizationHandler(), "timer"));
+ verify(getTimer()).cancel(false);
+ }
+
+ @Test
+ public void whenTheAuthorizationCompletesAfterItTimedOutThenAnNoOngoingAuthorizationExceptionIsThrown()
+ throws Exception {
+ // given:
+ getAuthorizationHandler().beginAuthorization(CLIENT_ID, CLIENT_SECRET, BRIDGE_UID, EMAIL);
+ getAuthorizationHandler().getAuthorizationUrl(REDIRECT_URL);
+
+ getScheduledRunnable().run();
+
+ // when:
+ assertThrows(NoOngoingAuthorizationException.class, () -> {
+ getAuthorizationHandler()
+ .completeAuthorization("http://127.0.0.1:8080/mielecloud/result?code=abc&state=def");
+ });
+ }
+
+ @Test
+ public void whenASecondAuthorizationIsBegunWhileAnotherIsStillOngoingThenAnOngoingAuthorizationExceptionIsThrown() {
+ // given:
+ getAuthorizationHandler().beginAuthorization(CLIENT_ID, CLIENT_SECRET, BRIDGE_UID, EMAIL);
+
+ // when:
+ assertThrows(OngoingAuthorizationException.class, () -> {
+ getAuthorizationHandler().beginAuthorization(CLIENT_ID, CLIENT_SECRET, BRIDGE_UID, EMAIL);
+ });
+ }
+
+ @Test
+ public void whenNoAuthorizationIsOngoingAndTheAuthorizationUrlIsRequestedThenAnNoOngoingAuthorizationExceptionIsThrown() {
+ // when:
+ assertThrows(NoOngoingAuthorizationException.class, () -> {
+ getAuthorizationHandler().getAuthorizationUrl(REDIRECT_URL);
+ });
+ }
+
+ @Test
+ public void whenGetAuthorizationUrlFromTheFrameworkFailsThenTheOngoingAuthorizationIsAborted()
+ throws org.openhab.core.auth.client.oauth2.OAuthException, IllegalArgumentException, IllegalAccessException,
+ NoSuchFieldException, SecurityException {
+ // given:
+ when(getClientService().getAuthorizationUrl(anyString(), isNull(), isNull()))
+ .thenThrow(new org.openhab.core.auth.client.oauth2.OAuthException());
+
+ getAuthorizationHandler().beginAuthorization(CLIENT_ID, CLIENT_SECRET, BRIDGE_UID, EMAIL);
+
+ // when:
+ assertThrows(OAuthException.class, () -> {
+ try {
+ getAuthorizationHandler().getAuthorizationUrl(REDIRECT_URL);
+ } catch (OAuthException e) {
+ assertNull(getPrivate(getAuthorizationHandler(), "timer"));
+ assertNull(getPrivate(getAuthorizationHandler(), "oauthClientService"));
+ assertNull(getPrivate(getAuthorizationHandler(), "bridgeUid"));
+ assertNull(getPrivate(getAuthorizationHandler(), "redirectUri"));
+ assertNull(getPrivate(getAuthorizationHandler(), "email"));
+ throw e;
+ }
+ });
+ }
+
+ @Test
+ public void whenExtractingTheAuthCodeFromTheResponseFailsThenAnOAuthExceptionIsThrownAndAllResourcesAreCleanedUp()
+ throws Exception {
+ // given:
+ when(getClientService().extractAuthCodeFromAuthResponse(anyString()))
+ .thenThrow(new org.openhab.core.auth.client.oauth2.OAuthException());
+
+ getAuthorizationHandler().beginAuthorization(CLIENT_ID, CLIENT_SECRET, BRIDGE_UID, EMAIL);
+ getAuthorizationHandler().getAuthorizationUrl(REDIRECT_URL);
+
+ // when:
+ assertThrows(OAuthException.class, () -> {
+ try {
+ getAuthorizationHandler()
+ .completeAuthorization("http://127.0.0.1:8080/mielecloud/result?code=abc&state=def");
+ } catch (OAuthException e) {
+ assertNull(getPrivate(getAuthorizationHandler(), "timer"));
+ assertNull(getPrivate(getAuthorizationHandler(), "oauthClientService"));
+ assertNull(getPrivate(getAuthorizationHandler(), "bridgeUid"));
+ assertNull(getPrivate(getAuthorizationHandler(), "email"));
+ assertNull(getPrivate(getAuthorizationHandler(), "redirectUri"));
+ throw e;
+ }
+ });
+ }
+
+ @Test
+ public void whenRetrievingTheAccessTokenFailsDueToANetworkErrorThenAnOAuthExceptionIsThrownAndAllResourcesAreCleanedUp()
+ throws Exception {
+ // given:
+ when(getClientService().extractAuthCodeFromAuthResponse(anyString())).thenReturn(AUTH_CODE);
+ when(getClientService().getAccessTokenResponseByAuthorizationCode(anyString(), anyString()))
+ .thenThrow(new IOException());
+
+ getAuthorizationHandler().beginAuthorization(CLIENT_ID, CLIENT_SECRET, BRIDGE_UID, EMAIL);
+ getAuthorizationHandler().getAuthorizationUrl(REDIRECT_URL);
+
+ // when:
+ assertThrows(OAuthException.class, () -> {
+ try {
+ getAuthorizationHandler()
+ .completeAuthorization("http://127.0.0.1:8080/mielecloud/result?code=abc&state=def");
+ } catch (OAuthException e) {
+ assertNull(getPrivate(getAuthorizationHandler(), "timer"));
+ assertNull(getPrivate(getAuthorizationHandler(), "oauthClientService"));
+ assertNull(getPrivate(getAuthorizationHandler(), "bridgeUid"));
+ assertNull(getPrivate(getAuthorizationHandler(), "email"));
+ assertNull(getPrivate(getAuthorizationHandler(), "redirectUri"));
+ throw e;
+ }
+ });
+ }
+
+ @Test
+ public void whenRetrievingTheAccessTokenFailsDueToAnIllegalAnswerFromTheMieleServiceThenAnOAuthExceptionIsThrownAndAllResourcesAreCleanedUp()
+ throws Exception {
+ // given:
+ when(getClientService().extractAuthCodeFromAuthResponse(anyString())).thenReturn(AUTH_CODE);
+ when(getClientService().getAccessTokenResponseByAuthorizationCode(anyString(), anyString()))
+ .thenThrow(new OAuthResponseException());
+
+ getAuthorizationHandler().beginAuthorization(CLIENT_ID, CLIENT_SECRET, BRIDGE_UID, EMAIL);
+ getAuthorizationHandler().getAuthorizationUrl(REDIRECT_URL);
+
+ // when:
+ assertThrows(OAuthException.class, () -> {
+ try {
+ getAuthorizationHandler()
+ .completeAuthorization("http://127.0.0.1:8080/mielecloud/result?code=abc&state=def");
+ } catch (OAuthException e) {
+ assertNull(getPrivate(getAuthorizationHandler(), "timer"));
+ assertNull(getPrivate(getAuthorizationHandler(), "oauthClientService"));
+ assertNull(getPrivate(getAuthorizationHandler(), "bridgeUid"));
+ assertNull(getPrivate(getAuthorizationHandler(), "email"));
+ assertNull(getPrivate(getAuthorizationHandler(), "redirectUri"));
+ throw e;
+ }
+ });
+ }
+
+ @Test
+ public void whenRetrievingTheAccessTokenFailsWhileProcessingTheResponseThenAnOAuthExceptionIsThrownAndAllResourcesAreCleanedUp()
+ throws Exception {
+ // given:
+ when(getClientService().extractAuthCodeFromAuthResponse(anyString())).thenReturn(AUTH_CODE);
+ when(getClientService().getAccessTokenResponseByAuthorizationCode(anyString(), anyString()))
+ .thenThrow(new org.openhab.core.auth.client.oauth2.OAuthException());
+
+ getAuthorizationHandler().beginAuthorization(CLIENT_ID, CLIENT_SECRET, BRIDGE_UID, EMAIL);
+ getAuthorizationHandler().getAuthorizationUrl(REDIRECT_URL);
+
+ // when:
+ assertThrows(OAuthException.class, () -> {
+ try {
+ getAuthorizationHandler()
+ .completeAuthorization("http://127.0.0.1:8080/mielecloud/result?code=abc&state=def");
+ } catch (OAuthException e) {
+ assertNull(getPrivate(getAuthorizationHandler(), "timer"));
+ assertNull(getPrivate(getAuthorizationHandler(), "oauthClientService"));
+ assertNull(getPrivate(getAuthorizationHandler(), "bridgeUid"));
+ assertNull(getPrivate(getAuthorizationHandler(), "email"));
+ assertNull(getPrivate(getAuthorizationHandler(), "redirectUri"));
+ throw e;
+ }
+ });
+ }
+
+ @Test
+ public void whenNoAuthorizationIsOngoingThenGetBridgeUidThrowsNoOngoingAuthorizationException() {
+ // when:
+ assertThrows(NoOngoingAuthorizationException.class, () -> {
+ getAuthorizationHandler().getBridgeUid();
+ });
+ }
+
+ @Test
+ public void whenNoAuthorizationIsOngoingThenGetEmailThrowsNoOngoingAuthorizationException() {
+ // when:
+ assertThrows(NoOngoingAuthorizationException.class, () -> {
+ getAuthorizationHandler().getEmail();
+ });
+ }
+
+ @Test
+ public void whenAnAuthorizationIsOngoingThenGetBridgeUidReturnsTheUidOfTheBridgeBeingAuthorized() {
+ // given:
+ getAuthorizationHandler().beginAuthorization(CLIENT_ID, CLIENT_SECRET, BRIDGE_UID, EMAIL);
+
+ // when:
+ ThingUID bridgeUid = getAuthorizationHandler().getBridgeUid();
+
+ // then:
+ assertEquals(BRIDGE_UID, bridgeUid);
+ }
+
+ @Test
+ public void whenAnAuthorizationIsOngoingThenGetEmailReturnsTheEmailBeingAuthorized() {
+ // given:
+ getAuthorizationHandler().beginAuthorization(CLIENT_ID, CLIENT_SECRET, BRIDGE_UID, EMAIL);
+
+ // when:
+ String email = getAuthorizationHandler().getEmail();
+
+ // then:
+ assertEquals(EMAIL, email);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.*;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingTestConstants;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+
+/**
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class ThingsTemplateGeneratorTest {
+ private static final String BRIDGE_ID = "genesis";
+ private static final String ALTERNATIVE_BRIDGE_ID = "mielebridge";
+
+ private static final String LOCALE = "en";
+ private static final String ALTERNATIVE_LOCALE = "de";
+
+ private static final String EMAIL = "openhab@openhab.org";
+ private static final String ALTERNATIVE_EMAIL = "everyone@openhab.org";
+
+ @Test
+ public void whenBridgeIdAndAccessTokenAndLocaleAreProvidedThenAValidBridgeConfigurationTemplateIsGenerated() {
+ // given:
+ ThingsTemplateGenerator templateGenerator = new ThingsTemplateGenerator();
+
+ // when:
+ String template = templateGenerator.createBridgeConfigurationTemplate(BRIDGE_ID, EMAIL, LOCALE);
+
+ // then:
+ assertEquals("Bridge mielecloud:account:genesis [ email=\"openhab@openhab.org\", locale=\"en\" ]", template);
+ }
+
+ @Test
+ public void whenAnAlternativeBridgeIdIsProvidedThenAValidBridgeConfigurationTemplateWithThatIdIsGenerated() {
+ // given:
+ ThingsTemplateGenerator templateGenerator = new ThingsTemplateGenerator();
+
+ // when:
+ String template = templateGenerator.createBridgeConfigurationTemplate(ALTERNATIVE_BRIDGE_ID, EMAIL, LOCALE);
+
+ // then:
+ assertEquals("Bridge mielecloud:account:mielebridge [ email=\"openhab@openhab.org\", locale=\"en\" ]",
+ template);
+ }
+
+ @Test
+ public void whenAnAlternativeAccessTokenIsProvidedThenAValidBridgeConfigurationTemplateWithThatAccessTokenIsGenerated() {
+ // given:
+ ThingsTemplateGenerator templateGenerator = new ThingsTemplateGenerator();
+
+ // when:
+ String template = templateGenerator.createBridgeConfigurationTemplate(BRIDGE_ID, EMAIL, LOCALE);
+
+ // then:
+ assertEquals("Bridge mielecloud:account:genesis [ email=\"openhab@openhab.org\", locale=\"en\" ]", template);
+ }
+
+ @Test
+ public void whenAnAlternativeLocaleIsProvidedThenAValidBridgeConfigurationTemplateWithThatLocaleIsGenerated() {
+ // given:
+ ThingsTemplateGenerator templateGenerator = new ThingsTemplateGenerator();
+
+ // when:
+ String template = templateGenerator.createBridgeConfigurationTemplate(BRIDGE_ID, EMAIL, ALTERNATIVE_LOCALE);
+
+ // then:
+ assertEquals("Bridge mielecloud:account:genesis [ email=\"openhab@openhab.org\", locale=\"de\" ]", template);
+ }
+
+ @Test
+ public void whenAnAlternativeEmailIsProvidedThenAValidBridgeConfigurationTemplateWithThatEmailIsGenerated() {
+ // given:
+ ThingsTemplateGenerator templateGenerator = new ThingsTemplateGenerator();
+
+ // when:
+ String template = templateGenerator.createBridgeConfigurationTemplate(BRIDGE_ID, ALTERNATIVE_EMAIL, LOCALE);
+
+ // then:
+ assertEquals("Bridge mielecloud:account:genesis [ email=\"everyone@openhab.org\", locale=\"en\" ]", template);
+ }
+
+ private Bridge createBridgeMock(String id, String locale, String email) {
+ Configuration configuration = mock(Configuration.class);
+ when(configuration.get(MieleCloudBindingConstants.CONFIG_PARAM_LOCALE)).thenReturn(locale);
+ when(configuration.get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL)).thenReturn(email);
+
+ Bridge bridge = mock(Bridge.class);
+ when(bridge.getUID()).thenReturn(new ThingUID(MieleCloudBindingConstants.THING_TYPE_BRIDGE, id));
+ when(bridge.getConfiguration()).thenReturn(configuration);
+
+ return bridge;
+ }
+
+ private Thing createThingMock(ThingTypeUID thingTypeUid, String deviceIdentifier, @Nullable String label,
+ String bridgeId) {
+ Configuration configuration = mock(Configuration.class);
+ when(configuration.get(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER)).thenReturn(deviceIdentifier);
+
+ Thing thing = mock(Thing.class);
+ when(thing.getThingTypeUID()).thenReturn(thingTypeUid);
+ when(thing.getUID()).thenReturn(new ThingUID(thingTypeUid, deviceIdentifier, bridgeId));
+ when(thing.getLabel()).thenReturn(label);
+ when(thing.getConfiguration()).thenReturn(configuration);
+ return thing;
+ }
+
+ private DiscoveryResult createDiscoveryResultMock(ThingTypeUID thingTypeUid, String id, String label,
+ String bridgeId) {
+ DiscoveryResult discoveryResult = mock(DiscoveryResult.class);
+ when(discoveryResult.getLabel()).thenReturn(label);
+ when(discoveryResult.getThingTypeUID()).thenReturn(thingTypeUid);
+ when(discoveryResult.getThingUID()).thenReturn(new ThingUID(thingTypeUid, id, bridgeId));
+ when(discoveryResult.getProperties())
+ .thenReturn(Collections.singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER, id));
+ return discoveryResult;
+ }
+
+ @Test
+ public void whenNoThingsArePairedAndNoInboxEntriesAreAvailableThenAnEmptyConfigurationTemplateIsGenerated() {
+ // given:
+ ThingsTemplateGenerator templateGenerator = new ThingsTemplateGenerator();
+
+ Bridge bridge = createBridgeMock(MieleCloudBindingTestConstants.BRIDGE_ID, LOCALE, EMAIL);
+
+ // when:
+ String template = templateGenerator.createBridgeAndThingConfigurationTemplate(bridge, Collections.emptyList(),
+ Collections.emptyList());
+
+ // then:
+ assertEquals("Bridge mielecloud:account:genesis [ email=\"openhab@openhab.org\", locale=\"en\" ] {\n}",
+ template);
+ }
+
+ @Test
+ public void whenPairedThingsArePresentThenTheyArePresentInTheConfigurationTemplate() {
+ // given:
+ ThingsTemplateGenerator templateGenerator = new ThingsTemplateGenerator();
+
+ Bridge bridge = createBridgeMock(ALTERNATIVE_BRIDGE_ID, ALTERNATIVE_LOCALE, ALTERNATIVE_EMAIL);
+
+ Thing thing1 = createThingMock(MieleCloudBindingConstants.THING_TYPE_OVEN, "000137439123", "Oven H7860XY",
+ ALTERNATIVE_BRIDGE_ID);
+ Thing thing2 = createThingMock(MieleCloudBindingConstants.THING_TYPE_HOB, "000160106123", null,
+ ALTERNATIVE_BRIDGE_ID);
+
+ List<Thing> pairedThings = Arrays.asList(thing1, thing2);
+
+ // when:
+ String template = templateGenerator.createBridgeAndThingConfigurationTemplate(bridge, pairedThings,
+ Collections.emptyList());
+
+ // then:
+ assertEquals("Bridge mielecloud:account:mielebridge [ email=\"everyone@openhab.org\", locale=\"de\" ] {\n"
+ + " Thing oven 000137439123 \"Oven H7860XY\" [ deviceIdentifier=\"000137439123\" ]\n"
+ + " Thing hob 000160106123 [ deviceIdentifier=\"000160106123\" ]\n}", template);
+ }
+
+ @Test
+ public void whenDiscoveryResultsAreInTheInboxThenTheyArePresentInTheConfigurationTemplate() {
+ // given:
+ ThingsTemplateGenerator templateGenerator = new ThingsTemplateGenerator();
+
+ Bridge bridge = createBridgeMock(ALTERNATIVE_BRIDGE_ID, ALTERNATIVE_LOCALE, ALTERNATIVE_EMAIL);
+
+ DiscoveryResult discoveryResult1 = createDiscoveryResultMock(
+ MieleCloudBindingConstants.THING_TYPE_FRIDGE_FREEZER, "000154106123", "Fridge-Freezer Kitchen",
+ ALTERNATIVE_BRIDGE_ID);
+ DiscoveryResult discoveryResult2 = createDiscoveryResultMock(
+ MieleCloudBindingConstants.THING_TYPE_WASHING_MACHINE, "000189106123", "Washing Machine",
+ ALTERNATIVE_BRIDGE_ID);
+
+ List<DiscoveryResult> discoveredThings = Arrays.asList(discoveryResult1, discoveryResult2);
+
+ // when:
+ String template = templateGenerator.createBridgeAndThingConfigurationTemplate(bridge, Collections.emptyList(),
+ discoveredThings);
+
+ // then:
+ assertEquals("Bridge mielecloud:account:mielebridge [ email=\"everyone@openhab.org\", locale=\"de\" ] {\n"
+ + " Thing fridge_freezer 000154106123 \"Fridge-Freezer Kitchen\" [ deviceIdentifier=\"000154106123\" ]\n"
+ + " Thing washing_machine 000189106123 \"Washing Machine\" [ deviceIdentifier=\"000189106123\" ]\n}",
+ template);
+ }
+
+ @Test
+ public void whenThingsArePresentAndDiscoveryResultsAreInTheInboxThenTheyArePresentInTheConfigurationTemplate() {
+ // given:
+ ThingsTemplateGenerator templateGenerator = new ThingsTemplateGenerator();
+
+ Bridge bridge = createBridgeMock(ALTERNATIVE_BRIDGE_ID, ALTERNATIVE_LOCALE, EMAIL);
+
+ Thing thing1 = createThingMock(MieleCloudBindingConstants.THING_TYPE_OVEN, "000137439123", "Oven H7860XY",
+ ALTERNATIVE_BRIDGE_ID);
+ Thing thing2 = createThingMock(MieleCloudBindingConstants.THING_TYPE_HOB, "000160106123", null,
+ ALTERNATIVE_BRIDGE_ID);
+
+ List<Thing> pairedThings = Arrays.asList(thing1, thing2);
+
+ DiscoveryResult discoveryResult1 = createDiscoveryResultMock(
+ MieleCloudBindingConstants.THING_TYPE_FRIDGE_FREEZER, "000154106123", "Fridge-Freezer Kitchen",
+ ALTERNATIVE_BRIDGE_ID);
+ DiscoveryResult discoveryResult2 = createDiscoveryResultMock(
+ MieleCloudBindingConstants.THING_TYPE_WASHING_MACHINE, "000189106123", "Washing Machine",
+ ALTERNATIVE_BRIDGE_ID);
+
+ List<DiscoveryResult> discoveredThings = Arrays.asList(discoveryResult1, discoveryResult2);
+
+ // when:
+ String template = templateGenerator.createBridgeAndThingConfigurationTemplate(bridge, pairedThings,
+ discoveredThings);
+
+ // then:
+ assertEquals("Bridge mielecloud:account:mielebridge [ email=\"openhab@openhab.org\", locale=\"de\" ] {\n"
+ + " Thing oven 000137439123 \"Oven H7860XY\" [ deviceIdentifier=\"000137439123\" ]\n"
+ + " Thing hob 000160106123 [ deviceIdentifier=\"000160106123\" ]\n"
+ + " Thing fridge_freezer 000154106123 \"Fridge-Freezer Kitchen\" [ deviceIdentifier=\"000154106123\" ]\n"
+ + " Thing washing_machine 000189106123 \"Washing Machine\" [ deviceIdentifier=\"000189106123\" ]\n}",
+ template);
+ }
+
+ @Test
+ public void whenNoLocaleIsConfiguredThenTheDefaultIsUsed() {
+ // given:
+ ThingsTemplateGenerator templateGenerator = new ThingsTemplateGenerator();
+
+ Configuration configuration = mock(Configuration.class);
+ when(configuration.get(MieleCloudBindingConstants.CONFIG_PARAM_LOCALE)).thenReturn(null);
+ when(configuration.get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL)).thenReturn(EMAIL);
+
+ Bridge bridge = mock(Bridge.class);
+ when(bridge.getUID()).thenReturn(
+ new ThingUID(MieleCloudBindingConstants.THING_TYPE_BRIDGE, MieleCloudBindingTestConstants.BRIDGE_ID));
+ when(bridge.getConfiguration()).thenReturn(configuration);
+
+ // when:
+ String template = templateGenerator.createBridgeAndThingConfigurationTemplate(bridge, Collections.emptyList(),
+ Collections.emptyList());
+
+ // then:
+ assertEquals("Bridge mielecloud:account:genesis [ email=\"openhab@openhab.org\", locale=\"en\" ] {\n}",
+ template);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.discovery;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.*;
+
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class ThingInformationExtractorTest {
+ private static Stream<Arguments> extractedPropertiesContainSerialNumberAndModelIdParameterSource() {
+ return Stream.of(
+ Arguments.of(MieleCloudBindingConstants.THING_TYPE_HOOD, DeviceType.HOOD, "000124430018",
+ Optional.of("000124430017"), Optional.of("Ventilation Hood"), Optional.of("DA-6996"),
+ "000124430017", "Ventilation Hood DA-6996", "000124430018"),
+ Arguments.of(MieleCloudBindingConstants.THING_TYPE_COFFEE_SYSTEM, DeviceType.COFFEE_SYSTEM,
+ "000124431235", Optional.of("000124431234"), Optional.of("Coffee Machine"),
+ Optional.of("CM-1234"), "000124431234", "Coffee Machine CM-1234", "000124431235"),
+ Arguments.of(MieleCloudBindingConstants.THING_TYPE_HOOD, DeviceType.HOOD, "000124430018",
+ Optional.empty(), Optional.of("Ventilation Hood"), Optional.of("DA-6996"), "000124430018",
+ "Ventilation Hood DA-6996", "000124430018"),
+ Arguments.of(MieleCloudBindingConstants.THING_TYPE_HOOD, DeviceType.HOOD, "000124430018",
+ Optional.empty(), Optional.empty(), Optional.of("DA-6996"), "000124430018", "DA-6996",
+ "000124430018"),
+ Arguments.of(MieleCloudBindingConstants.THING_TYPE_HOOD, DeviceType.HOOD, "000124430018",
+ Optional.empty(), Optional.of("Ventilation Hood"), Optional.empty(), "000124430018",
+ "Ventilation Hood", "000124430018"),
+ Arguments.of(MieleCloudBindingConstants.THING_TYPE_HOOD, DeviceType.HOOD, "000124430018",
+ Optional.empty(), Optional.empty(), Optional.empty(), "000124430018", "Unknown",
+ "000124430018"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("extractedPropertiesContainSerialNumberAndModelIdParameterSource")
+ void extractedPropertiesContainSerialNumberAndModelId(ThingTypeUID thingTypeUid, DeviceType deviceType,
+ String deviceIdentifier, Optional<String> fabNumber, Optional<String> type, Optional<String> techType,
+ String expectedSerialNumber, String expectedModelId, String expectedDeviceIdentifier) {
+ // given:
+ var deviceState = mock(DeviceState.class);
+ when(deviceState.getRawType()).thenReturn(deviceType);
+ when(deviceState.getDeviceIdentifier()).thenReturn(deviceIdentifier);
+ when(deviceState.getFabNumber()).thenReturn(fabNumber);
+ when(deviceState.getType()).thenReturn(type);
+ when(deviceState.getTechType()).thenReturn(techType);
+
+ // when:
+ var properties = ThingInformationExtractor.extractProperties(thingTypeUid, deviceState);
+
+ // then:
+ assertEquals(3, properties.size());
+ assertEquals(expectedSerialNumber, properties.get(Thing.PROPERTY_SERIAL_NUMBER));
+ assertEquals(expectedModelId, properties.get(Thing.PROPERTY_MODEL_ID));
+ assertEquals(expectedDeviceIdentifier,
+ properties.get(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER));
+ }
+
+ @ParameterizedTest
+ @CsvSource({ "2,2", "4,4" })
+ void propertiesForHobContainPlateCount(int plateCount, String expectedPlateCountPropertyValue) {
+ // given:
+ var deviceState = mock(DeviceState.class);
+ when(deviceState.getRawType()).thenReturn(DeviceType.HOB_INDUCTION);
+ when(deviceState.getDeviceIdentifier()).thenReturn("000124430019");
+ when(deviceState.getFabNumber()).thenReturn(Optional.of("000124430019"));
+ when(deviceState.getType()).thenReturn(Optional.of("Induction Hob"));
+ when(deviceState.getTechType()).thenReturn(Optional.of("IH-7890"));
+ when(deviceState.getPlateStepCount()).thenReturn(Optional.of(plateCount));
+
+ // when:
+ var properties = ThingInformationExtractor.extractProperties(MieleCloudBindingConstants.THING_TYPE_HOB,
+ deviceState);
+
+ // then:
+ assertEquals(4, properties.size());
+ assertEquals("000124430019", properties.get(Thing.PROPERTY_SERIAL_NUMBER));
+ assertEquals("Induction Hob IH-7890", properties.get(Thing.PROPERTY_MODEL_ID));
+ assertEquals("000124430019", properties.get(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER));
+ assertEquals(expectedPlateCountPropertyValue, properties.get(MieleCloudBindingConstants.PROPERTY_PLATE_COUNT));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.util;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class LocaleValidatorTest {
+ @Test
+ public void enIsAValidLanguage() {
+ // given:
+ String language = "en";
+
+ // when:
+ boolean valid = LocaleValidator.isValidLanguage(language);
+
+ // then:
+ assertTrue(valid);
+ }
+
+ @Test
+ public void deIsAValidLanguage() {
+ // given:
+ String language = "de";
+
+ // when:
+ boolean valid = LocaleValidator.isValidLanguage(language);
+
+ // then:
+ assertTrue(valid);
+ }
+
+ @Test
+ public void aFullLocaleIsNotAValidLanguage() {
+ // given:
+ String language = "en_us";
+
+ // when:
+ boolean valid = LocaleValidator.isValidLanguage(language);
+
+ // then:
+ assertFalse(valid);
+ }
+
+ @Test
+ public void textIsNotAValidLanguage() {
+ // given:
+ String language = "Hello World!";
+
+ // when:
+ boolean valid = LocaleValidator.isValidLanguage(language);
+
+ // then:
+ assertFalse(valid);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.util;
+
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.*;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.Request;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Device;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceIdentLabel;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Ident;
+
+/**
+ * Utility class for creating common mocks.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public final class MockUtil {
+ private MockUtil() {
+ }
+
+ public static Device mockDevice(String fabNumber) {
+ DeviceIdentLabel deviceIdentLabel = mock(DeviceIdentLabel.class);
+ when(deviceIdentLabel.getFabNumber()).thenReturn(Optional.of(fabNumber));
+
+ Ident ident = mock(Ident.class);
+ when(ident.getDeviceIdentLabel()).thenReturn(Optional.of(deviceIdentLabel));
+
+ Device device = mock(Device.class);
+ when(device.getIdent()).thenReturn(Optional.of(ident));
+
+ return device;
+ }
+
+ public static <T> T requireNonNull(@Nullable T obj) {
+ if (obj == null) {
+ throw new IllegalArgumentException("Object must not be null");
+ }
+ return obj;
+ }
+
+ /**
+ * Creates a mock for {@link HttpClient} circumventing the problem that {@link HttpClient#start()} is {@code final}
+ * and {@link HttpClient#doStart()} {@code protected} and unaccessible when mocking with Mockito.
+ */
+ public static HttpClient mockHttpClient() {
+ return new HttpClient() {
+ @Override
+ protected void doStart() throws Exception {
+ }
+ };
+ }
+
+ /**
+ * Creates a mock for {@link HttpClient} circumventing the problem that {@link HttpClient#start()} is {@code final}
+ * and {@link HttpClient#doStart()} {@code protected} and unaccessible when mocking with Mockito.
+ *
+ * @param newRequestUri {@code uri} parameter of {@link HttpClient#newRequest(String)} to mock.
+ * @param newRequestReturnValue Return value of {@link HttpClient#newRequest(String)} to mock.
+ */
+ public static HttpClient mockHttpClient(String newRequestUri, Request newRequestReturnValue) {
+ return new HttpClient() {
+ @Override
+ protected void doStart() throws Exception {
+ }
+
+ @Override
+ public Request newRequest(@Nullable String uri) {
+ if (newRequestUri.equals(uri)) {
+ return newRequestReturnValue;
+ } else {
+ fail();
+ throw new IllegalStateException();
+ }
+ }
+ };
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.util;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Utility class for reflection operations such as accessing private fields or methods.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public final class ReflectionUtil {
+ private ReflectionUtil() {
+ }
+
+ /**
+ * Gets a private attribute.
+ *
+ * @param object The object to get the attribute from.
+ * @param fieldName The name of the field to get.
+ * @return The obtained value.
+ * @throws SecurityException if the operation is not allowed.
+ * @throws NoSuchFieldException if no field with the given name exists.
+ * @throws IllegalAccessException if the field is enforcing Java language access control and is inaccessible.
+ * @throws IllegalArgumentException if one of the passed parameters is invalid.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T getPrivate(Object object, String fieldName)
+ throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+ Field field = getFieldFromClassHierarchy(object.getClass(), fieldName);
+ field.setAccessible(true);
+ return (T) field.get(object);
+ }
+
+ private static Field getFieldFromClassHierarchy(Class<?> clazz, String fieldName)
+ throws NoSuchFieldException, SecurityException {
+ Class<?> iteratedClass = clazz;
+ do {
+ try {
+ return iteratedClass.getDeclaredField(fieldName);
+ } catch (NoSuchFieldException e) {
+ }
+ iteratedClass = iteratedClass.getSuperclass();
+ } while (iteratedClass != null);
+ throw new NoSuchFieldException();
+ }
+
+ /**
+ * Sets a private attribute.
+ *
+ * @param object The object to set the attribute on.
+ * @param fieldName The name of the field to set.
+ * @param value The value to set.
+ * @throws SecurityException if the operation is not allowed.
+ * @throws NoSuchFieldException if no field with the given name exists.
+ * @throws IllegalAccessException if the field is enforcing Java language access control and is inaccessible.
+ * @throws IllegalArgumentException if one of the passed parameters is invalid.
+ */
+ public static void setPrivate(Object object, String fieldName, Object value)
+ throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+ Field field = object.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ field.set(object, value);
+ }
+
+ /**
+ * Invokes a private method on an object.
+ *
+ * @param object The object to invoke the method on.
+ * @param methodName The name of the method to invoke.
+ * @param parameters The parameters of the method invocation.
+ * @return The method call's return value.
+ * @throws NoSuchMethodException if no method with the given parameters or name exists.
+ * @throws SecurityException if the operation is not allowed.
+ * @throws IllegalAccessException if the method is enforcing Java language access control and is inaccessible.
+ * @throws IllegalArgumentException if one of the passed parameters is invalid.
+ * @throws InvocationTargetException if the invoked method throws an exception.
+ */
+ public static <T> T invokePrivate(Object object, String methodName, Object... parameters)
+ throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException {
+ Class<?>[] parameterTypes = new Class[parameters.length];
+ for (int i = 0; i < parameters.length; i++) {
+ parameterTypes[i] = parameters[i].getClass();
+ }
+
+ return invokePrivate(object, methodName, parameterTypes, parameters);
+ }
+
+ /**
+ * Invokes a private method on an object.
+ *
+ * @param object The object to invoke the method on.
+ * @param methodName The name of the method to invoke.
+ * @param parameterTypes The types of the parameters.
+ * @param parameters The parameters of the method invocation.
+ * @return The method call's return value.
+ * @throws NoSuchMethodException if no method with the given parameters or name exists.
+ * @throws SecurityException if the operation is not allowed.
+ * @throws IllegalAccessException if the method is enforcing Java language access control and is inaccessible.
+ * @throws IllegalArgumentException if one of the passed parameters is invalid.
+ * @throws InvocationTargetException if the invoked method throws an exception.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T invokePrivate(Object object, String methodName, Class<?>[] parameterTypes, Object... parameters)
+ throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException {
+ Method method = getMethodFromClassHierarchy(object.getClass(), methodName, parameterTypes);
+ method.setAccessible(true);
+ try {
+ return (T) method.invoke(object, parameters);
+ } catch (InvocationTargetException e) {
+ throw new IllegalStateException(e.getCause());
+ }
+ }
+
+ private static Method getMethodFromClassHierarchy(Class<?> clazz, String methodName, Class<?>[] parameterTypes)
+ throws NoSuchMethodException {
+ Class<?> iteratedClass = clazz;
+ do {
+ try {
+ return iteratedClass.getDeclaredMethod(methodName, parameterTypes);
+ } catch (NoSuchMethodException e) {
+ }
+ iteratedClass = iteratedClass.getSuperclass();
+ } while (iteratedClass != null);
+ throw new NoSuchMethodException();
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.util;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Utility class for handling test resources.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public final class ResourceUtil {
+ private ResourceUtil() {
+ }
+
+ /**
+ * Gets the contents of a resource file as {@link String}.
+ *
+ * @param resourceName The resource name (path inside the resources source folder).
+ * @return The file contents.
+ * @throws IOException if reading the resource fails or it cannot be found.
+ */
+ public static String getResourceAsString(String resourceName) throws IOException {
+ InputStream inputStream = ResourceUtil.class.getResourceAsStream(resourceName);
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
+ return reader.lines().collect(Collectors.joining("\n"));
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+import java.util.Optional;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentMatchers;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.openhab.binding.mielecloud.internal.util.MockUtil;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
+
+/**
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class ActionStateFetcherTest {
+ private ScheduledExecutorService mockImmediatelyExecutingExecutorService() {
+ ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+ when(scheduler.submit(ArgumentMatchers.<Runnable> any()))
+ .thenAnswer(new Answer<@Nullable ScheduledFuture<?>>() {
+ @Override
+ @Nullable
+ public ScheduledFuture<?> answer(@Nullable InvocationOnMock invocation) throws Throwable {
+ ((Runnable) MockUtil.requireNonNull(invocation).getArgument(0)).run();
+ return null;
+ }
+ });
+ return scheduler;
+ }
+
+ @Test
+ public void testFetchActionsIsInvokedWhenInitialDeviceStateIsSet() {
+ // given:
+ ScheduledExecutorService scheduler = mockImmediatelyExecutingExecutorService();
+
+ MieleWebservice webservice = mock(MieleWebservice.class);
+ DeviceState deviceState = mock(DeviceState.class);
+ DeviceState newDeviceState = mock(DeviceState.class);
+ ActionStateFetcher actionsfetcher = new ActionStateFetcher(() -> webservice, scheduler);
+
+ when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(newDeviceState.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+
+ // when:
+ actionsfetcher.onDeviceStateUpdated(deviceState);
+
+ // then:
+ verify(webservice).fetchActions(any());
+ }
+
+ @Test
+ public void testFetchActionsIsInvokedOnStateTransition() {
+ // given:
+ ScheduledExecutorService scheduler = mockImmediatelyExecutingExecutorService();
+
+ MieleWebservice webservice = mock(MieleWebservice.class);
+ DeviceState deviceState = mock(DeviceState.class);
+ DeviceState newDeviceState = mock(DeviceState.class);
+ ActionStateFetcher actionsfetcher = new ActionStateFetcher(() -> webservice, scheduler);
+
+ when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(newDeviceState.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+
+ actionsfetcher.onDeviceStateUpdated(deviceState);
+
+ // when:
+ actionsfetcher.onDeviceStateUpdated(newDeviceState);
+
+ // then:
+ verify(webservice, times(2)).fetchActions(any());
+ }
+
+ @Test
+ public void testFetchActionsIsNotInvokedWhenNoStateTransitionOccurrs() {
+ // given:
+ ScheduledExecutorService scheduler = mockImmediatelyExecutingExecutorService();
+
+ MieleWebservice webservice = mock(MieleWebservice.class);
+ DeviceState deviceState = mock(DeviceState.class);
+ DeviceState newDeviceState = mock(DeviceState.class);
+ ActionStateFetcher actionsfetcher = new ActionStateFetcher(() -> webservice, scheduler);
+
+ when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(newDeviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+
+ actionsfetcher.onDeviceStateUpdated(deviceState);
+
+ // when:
+ actionsfetcher.onDeviceStateUpdated(newDeviceState);
+
+ // then:
+ verify(webservice, times(1)).fetchActions(any());
+ }
+
+ @Test
+ public void whenFetchActionsFailsWithAMieleWebserviceExceptionThenNoExceptionIsThrown() {
+ // given:
+ ScheduledExecutorService scheduler = mockImmediatelyExecutingExecutorService();
+
+ MieleWebservice webservice = mock(MieleWebservice.class);
+ doThrow(new MieleWebserviceException("It went wrong", ConnectionError.REQUEST_EXECUTION_FAILED))
+ .when(webservice).fetchActions(any());
+
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+
+ ActionStateFetcher actionsfetcher = new ActionStateFetcher(() -> webservice, scheduler);
+
+ // when:
+ actionsfetcher.onDeviceStateUpdated(deviceState);
+
+ // then:
+ verify(webservice, times(1)).fetchActions(any());
+ }
+
+ @Test
+ public void whenFetchActionsFailsWithAnAuthorizationFailedExceptionThenNoExceptionIsThrown() {
+ // given:
+ ScheduledExecutorService scheduler = mockImmediatelyExecutingExecutorService();
+
+ MieleWebservice webservice = mock(MieleWebservice.class);
+ doThrow(new AuthorizationFailedException("Authorization failed")).when(webservice).fetchActions(any());
+
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+
+ ActionStateFetcher actionsfetcher = new ActionStateFetcher(() -> webservice, scheduler);
+
+ // when:
+ actionsfetcher.onDeviceStateUpdated(deviceState);
+
+ // then:
+ verify(webservice, times(1)).fetchActions(any());
+ }
+
+ @Test
+ public void whenFetchActionsFailsWithATooManyRequestsExceptionThenNoExceptionIsThrown() {
+ // given:
+ ScheduledExecutorService scheduler = mockImmediatelyExecutingExecutorService();
+
+ MieleWebservice webservice = mock(MieleWebservice.class);
+ doThrow(new TooManyRequestsException("Too many requests", null)).when(webservice).fetchActions(any());
+
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+
+ ActionStateFetcher actionsfetcher = new ActionStateFetcher(() -> webservice, scheduler);
+
+ // when:
+ actionsfetcher.onDeviceStateUpdated(deviceState);
+
+ // then:
+ verify(webservice, times(1)).fetchActions(any());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.getPrivate;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.http.HttpFields;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingTestConstants;
+import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.util.MockUtil;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
+import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceTransientException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
+import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider;
+import org.openhab.binding.mielecloud.internal.webservice.request.RequestFactory;
+import org.openhab.binding.mielecloud.internal.webservice.retry.AuthorizationFailedRetryStrategy;
+import org.openhab.binding.mielecloud.internal.webservice.retry.NTimesRetryStrategy;
+import org.openhab.binding.mielecloud.internal.webservice.retry.RetryStrategy;
+import org.openhab.binding.mielecloud.internal.webservice.retry.RetryStrategyCombiner;
+import org.openhab.core.io.net.http.HttpClientFactory;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class DefaultMieleWebserviceTest {
+ private static final String MESSAGE_INTERNAL_SERVER_ERROR = "{\"message\": \"Internal Server Error\"}";
+ private static final String MESSAGE_SERVICE_UNAVAILABLE = "{\"message\": \"unavailable\"}";
+ private static final String MESSAGE_INVALID_JSON = "{\"abc123: \"äfgh\"}";
+
+ private static final String DEVICE_IDENTIFIER = "000124430016";
+
+ private static final String SERVER_ADDRESS = "https://api.mcs3.miele.com";
+ private static final String ENDPOINT_DEVICES = SERVER_ADDRESS + "/v1/devices/";
+ private static final String ENDPOINT_ACTIONS = ENDPOINT_DEVICES + DEVICE_IDENTIFIER + "/actions";
+ private static final String ENDPOINT_LOGOUT = SERVER_ADDRESS + "/thirdparty/logout";
+
+ private static final String ACCESS_TOKEN = "DE_0123456789abcdef0123456789abcdef";
+
+ private final RetryStrategy retryStrategy = new UncatchedRetryStrategy();
+ private final Request request = mock(Request.class);
+
+ @Test
+ public void testDefaultRetryStrategyIsCombinationOfOneTimeRetryStrategyAndAuthorizationFailedStrategy()
+ throws Exception {
+ // given:
+ HttpClientFactory httpClientFactory = mock(HttpClientFactory.class);
+ when(httpClientFactory.createHttpClient(anyString())).thenReturn(MockUtil.mockHttpClient());
+ LanguageProvider languageProvider = mock(LanguageProvider.class);
+ OAuthTokenRefresher tokenRefresher = mock(OAuthTokenRefresher.class);
+ ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+ // when:
+ DefaultMieleWebservice webservice = new DefaultMieleWebservice(MieleWebserviceConfiguration.builder()
+ .withHttpClientFactory(httpClientFactory).withLanguageProvider(languageProvider)
+ .withTokenRefresher(tokenRefresher).withServiceHandle(MieleCloudBindingTestConstants.SERVICE_HANDLE)
+ .withScheduler(scheduler).build());
+
+ // then:
+ RetryStrategy retryStrategy = getPrivate(webservice, "retryStrategy");
+ assertTrue(retryStrategy instanceof RetryStrategyCombiner);
+
+ RetryStrategy first = getPrivate(retryStrategy, "first");
+ assertTrue(first instanceof NTimesRetryStrategy);
+ int numberOfRetries = getPrivate(first, "numberOfRetries");
+ assertEquals(1, numberOfRetries);
+
+ RetryStrategy second = getPrivate(retryStrategy, "second");
+ assertTrue(second instanceof AuthorizationFailedRetryStrategy);
+ OAuthTokenRefresher internalTokenRefresher = getPrivate(second, "tokenRefresher");
+ assertEquals(tokenRefresher, internalTokenRefresher);
+ }
+
+ private ContentResponse createContentResponseMock(int errorCode, String content) {
+ ContentResponse response = mock(ContentResponse.class);
+ when(response.getStatus()).thenReturn(errorCode);
+ when(response.getContentAsString()).thenReturn(content);
+ return response;
+ }
+
+ private void performFetchActions() throws Exception {
+ RequestFactory requestFactory = mock(RequestFactory.class);
+ when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
+
+ ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+ try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
+ new DeviceStateDispatcher(), scheduler)) {
+ webservice.setAccessToken(ACCESS_TOKEN);
+
+ webservice.fetchActions(DEVICE_IDENTIFIER);
+ }
+ }
+
+ private void performFetchActionsExpectingFailure(ConnectionError expectedError) throws Exception {
+ try {
+ performFetchActions();
+ } catch (MieleWebserviceException e) {
+ assertEquals(expectedError, e.getConnectionError());
+ throw e;
+ } catch (MieleWebserviceTransientException e) {
+ assertEquals(expectedError, e.getConnectionError());
+ throw e;
+ }
+ }
+
+ @Test
+ public void testTimeoutExceptionWhilePerformingFetchActionsRequest() throws Exception {
+ // given:
+ when(request.send()).thenThrow(TimeoutException.class);
+
+ // when:
+ assertThrows(MieleWebserviceTransientException.class, () -> {
+ performFetchActionsExpectingFailure(ConnectionError.TIMEOUT);
+ });
+ }
+
+ @Test
+ public void test500InternalServerErrorWhilePerformingFetchActionsRequest() throws Exception {
+ // given:
+ ContentResponse contentResponse = createContentResponseMock(500, MESSAGE_INTERNAL_SERVER_ERROR);
+ when(request.send()).thenReturn(contentResponse);
+
+ // when:
+ assertThrows(MieleWebserviceTransientException.class, () -> {
+ performFetchActionsExpectingFailure(ConnectionError.SERVER_ERROR);
+ });
+ }
+
+ @Test
+ public void test503ServiceUnavailableWhilePerformingFetchActionsRequest() throws Exception {
+ // given:
+ ContentResponse contentResponse = createContentResponseMock(503, MESSAGE_SERVICE_UNAVAILABLE);
+ when(request.send()).thenReturn(contentResponse);
+
+ // when:
+ assertThrows(MieleWebserviceTransientException.class, () -> {
+ performFetchActionsExpectingFailure(ConnectionError.SERVICE_UNAVAILABLE);
+ });
+ }
+
+ @Test
+ public void testInvalidJsonWhilePerformingFetchActionsRequest() throws Exception {
+ // given:
+ ContentResponse contentResponse = createContentResponseMock(200, MESSAGE_INVALID_JSON);
+ when(request.send()).thenReturn(contentResponse);
+
+ // when:
+ assertThrows(MieleWebserviceTransientException.class, () -> {
+ performFetchActionsExpectingFailure(ConnectionError.RESPONSE_MALFORMED);
+ });
+ }
+
+ @Test
+ public void testInterruptedExceptionWhilePerformingFetchActionsRequest() throws Exception {
+ // given:
+ when(request.send()).thenThrow(InterruptedException.class);
+
+ // when:
+ assertThrows(MieleWebserviceException.class, () -> {
+ performFetchActionsExpectingFailure(ConnectionError.REQUEST_INTERRUPTED);
+ });
+ }
+
+ @Test
+ public void testExecutionExceptionWhilePerformingFetchActionsRequest() throws Exception {
+ // given:
+ when(request.send()).thenThrow(ExecutionException.class);
+
+ // when:
+ assertThrows(MieleWebserviceException.class, () -> {
+ performFetchActionsExpectingFailure(ConnectionError.REQUEST_EXECUTION_FAILED);
+ });
+ }
+
+ @Test
+ public void test400BadRequestWhilePerformingFetchActionsRequest() throws Exception {
+ // given:
+ ContentResponse response = createContentResponseMock(400, "{\"message\": \"grant_type is invalid\"}");
+ when(request.send()).thenReturn(response);
+
+ // when:
+ assertThrows(MieleWebserviceException.class, () -> {
+ performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
+ });
+ }
+
+ @Test
+ public void test401UnauthorizedWhilePerformingFetchActionsRequest() throws Exception {
+ // given:
+ ContentResponse response = createContentResponseMock(401, "{\"message\": \"Unauthorized\"}");
+ when(request.send()).thenReturn(response);
+
+ // when:
+ assertThrows(AuthorizationFailedException.class, () -> {
+ performFetchActions();
+ });
+ }
+
+ @Test
+ public void test404NotFoundWhilePerformingFetchActionsRequest() throws Exception {
+ // given:
+ ContentResponse response = createContentResponseMock(404, "{\"message\": \"Not found\"}");
+ when(request.send()).thenReturn(response);
+
+ // when:
+ assertThrows(MieleWebserviceException.class, () -> {
+ performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
+ });
+ }
+
+ @Test
+ public void test405MethodNotAllowedWhilePerformingFetchActionsRequest() throws Exception {
+ // given:
+ ContentResponse response = createContentResponseMock(405, "{\"message\": \"HTTP 405 Method Not Allowed\"}");
+ when(request.send()).thenReturn(response);
+
+ // when:
+ assertThrows(MieleWebserviceException.class, () -> {
+ performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
+ });
+ }
+
+ @Test
+ public void test429TooManyRequestsWhilePerformingFetchActionsRequest() throws Exception {
+ // given:
+ HttpFields headerFields = mock(HttpFields.class);
+ when(headerFields.containsKey(anyString())).thenReturn(false);
+
+ ContentResponse response = createContentResponseMock(429, "{\"message\": \"Too Many Requests\"}");
+ when(response.getHeaders()).thenReturn(headerFields);
+
+ when(request.send()).thenReturn(response);
+
+ // when:
+ assertThrows(TooManyRequestsException.class, () -> {
+ performFetchActions();
+ });
+ }
+
+ @Test
+ public void test502BadGatewayWhilePerforminggFetchActionsRequest() throws Exception {
+ // given:
+ ContentResponse response = createContentResponseMock(502, "{\"message\": \"Bad Gateway\"}");
+ when(request.send()).thenReturn(response);
+
+ // when:
+ assertThrows(MieleWebserviceException.class, () -> {
+ performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
+ });
+ }
+
+ @Test
+ public void testMalformatedBodyWhilePerforminggFetchActionsRequest() throws Exception {
+ // given:
+ ContentResponse response = createContentResponseMock(502, "{\"message \"Bad Gateway\"}");
+ when(request.send()).thenReturn(response);
+
+ // when:
+ assertThrows(MieleWebserviceException.class, () -> {
+ performFetchActionsExpectingFailure(ConnectionError.OTHER_HTTP_ERROR);
+ });
+ }
+
+ private void fillRequestMockWithDefaultContent() throws InterruptedException, TimeoutException, ExecutionException {
+ ContentResponse response = createContentResponseMock(200,
+ "{\"000124430016\":{\"ident\": {\"deviceName\": \"MyFancyHood\", \"deviceIdentLabel\": {\"fabNumber\": \"000124430016\"}}}}");
+ when(request.send()).thenReturn(response);
+ }
+
+ @Test
+ public void testAddDeviceStateListenerIsDelegatedToDeviceStateDispatcher() throws Exception {
+ // given:
+ RequestFactory requestFactory = mock(RequestFactory.class);
+ DeviceStateDispatcher dispatcher = mock(DeviceStateDispatcher.class);
+ ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+ try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
+ dispatcher, scheduler)) {
+ DeviceStateListener listener = mock(DeviceStateListener.class);
+
+ // when:
+ webservice.addDeviceStateListener(listener);
+
+ // then:
+ verify(dispatcher).addListener(listener);
+ verifyNoMoreInteractions(dispatcher);
+ }
+ }
+
+ @Test
+ public void testFetchActionsDelegatesDeviceStateListenerDispatchingToDeviceStateDispatcher() throws Exception {
+ // given:
+ fillRequestMockWithDefaultContent();
+
+ RequestFactory requestFactory = mock(RequestFactory.class);
+ when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
+
+ DeviceStateDispatcher dispatcher = mock(DeviceStateDispatcher.class);
+
+ ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+ try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy, dispatcher,
+ scheduler)) {
+ webservice.setAccessToken(ACCESS_TOKEN);
+
+ // when:
+ webservice.fetchActions(DEVICE_IDENTIFIER);
+
+ // then:
+ verify(dispatcher).dispatchActionStateUpdates(any(), any());
+ verifyNoMoreInteractions(dispatcher);
+ }
+ }
+
+ @Test
+ public void testFetchActionsThrowsMieleWebserviceTransientExceptionWhenRequestContentIsMalformatted()
+ throws Exception {
+ // given:
+ ContentResponse response = createContentResponseMock(200, "{\"}");
+ when(request.send()).thenReturn(response);
+
+ RequestFactory requestFactory = mock(RequestFactory.class);
+ when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
+
+ ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+ try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
+ new DeviceStateDispatcher(), scheduler)) {
+ webservice.setAccessToken(ACCESS_TOKEN);
+
+ // when:
+ assertThrows(MieleWebserviceTransientException.class, () -> {
+ webservice.fetchActions(DEVICE_IDENTIFIER);
+ });
+ }
+ }
+
+ @Test
+ public void testPutProcessActionSendsRequestWithCorrectJsonContent() throws Exception {
+ // given:
+ ContentResponse response = createContentResponseMock(204, "");
+ when(request.send()).thenReturn(response);
+
+ RequestFactory requestFactory = mock(RequestFactory.class);
+ when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"processAction\":1}"))
+ .thenReturn(request);
+
+ ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+ try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
+ new DeviceStateDispatcher(), scheduler)) {
+ webservice.setAccessToken(ACCESS_TOKEN);
+
+ // when:
+ webservice.putProcessAction(DEVICE_IDENTIFIER, ProcessAction.START);
+
+ // then:
+ verify(request).send();
+ verifyNoMoreInteractions(request);
+ }
+ }
+
+ @Test
+ public void testPutProcessActionThrowsIllegalArgumentExceptionWhenProcessActionIsUnknown() throws Exception {
+ // given:
+ RequestFactory requestFactory = mock(RequestFactory.class);
+
+ ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+ try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
+ new DeviceStateDispatcher(), scheduler)) {
+
+ // when:
+ assertThrows(IllegalArgumentException.class, () -> {
+ webservice.putProcessAction(DEVICE_IDENTIFIER, ProcessAction.UNKNOWN);
+ });
+ }
+ }
+
+ @Test
+ public void testPutProcessActionThrowsTooManyRequestsExceptionWhenHttpResponseCodeIs429() throws Exception {
+ // given:
+ HttpFields responseHeaders = mock(HttpFields.class);
+ when(responseHeaders.containsKey(anyString())).thenReturn(false);
+
+ ContentResponse response = createContentResponseMock(429, "{\"message\":\"Too many requests\"}");
+ when(response.getHeaders()).thenReturn(responseHeaders);
+
+ when(request.send()).thenReturn(response);
+
+ RequestFactory requestFactory = mock(RequestFactory.class);
+ when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"processAction\":1}"))
+ .thenReturn(request);
+
+ ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+ try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
+ new DeviceStateDispatcher(), scheduler)) {
+ webservice.setAccessToken(ACCESS_TOKEN);
+
+ // when:
+ assertThrows(TooManyRequestsException.class, () -> {
+ webservice.putProcessAction(DEVICE_IDENTIFIER, ProcessAction.START);
+ });
+ }
+ }
+
+ @Test
+ public void testPutLightSendsRequestWithCorrectJsonContentWhenTurningTheLightOn() throws Exception {
+ // given:
+ ContentResponse response = createContentResponseMock(204, "");
+ when(request.send()).thenReturn(response);
+
+ RequestFactory requestFactory = mock(RequestFactory.class);
+ when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"light\":1}")).thenReturn(request);
+
+ ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+ try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
+ new DeviceStateDispatcher(), scheduler)) {
+ webservice.setAccessToken(ACCESS_TOKEN);
+
+ // when:
+ webservice.putLight(DEVICE_IDENTIFIER, true);
+
+ // then:
+ verify(request).send();
+ verifyNoMoreInteractions(request);
+ }
+ }
+
+ @Test
+ public void testPutLightSendsRequestWithCorrectJsonContentWhenTurningTheLightOff() throws Exception {
+ // given:
+ ContentResponse response = createContentResponseMock(204, "");
+ when(request.send()).thenReturn(response);
+
+ RequestFactory requestFactory = mock(RequestFactory.class);
+ when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"light\":2}")).thenReturn(request);
+
+ ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+ try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
+ new DeviceStateDispatcher(), scheduler)) {
+ webservice.setAccessToken(ACCESS_TOKEN);
+
+ // when:
+ webservice.putLight(DEVICE_IDENTIFIER, false);
+
+ // then:
+ verify(request).send();
+ verifyNoMoreInteractions(request);
+ }
+ }
+
+ @Test
+ public void testPutLightThrowsTooManyRequestsExceptionWhenHttpResponseCodeIs429() throws Exception {
+ // given:
+ HttpFields responseHeaders = mock(HttpFields.class);
+ when(responseHeaders.containsKey(anyString())).thenReturn(false);
+
+ ContentResponse response = createContentResponseMock(429, "{\"message\":\"Too many requests\"}");
+ when(response.getHeaders()).thenReturn(responseHeaders);
+
+ when(request.send()).thenReturn(response);
+
+ RequestFactory requestFactory = mock(RequestFactory.class);
+ when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"light\":2}")).thenReturn(request);
+
+ ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+ try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
+ new DeviceStateDispatcher(), scheduler)) {
+ webservice.setAccessToken(ACCESS_TOKEN);
+
+ // when:
+ assertThrows(TooManyRequestsException.class, () -> {
+ webservice.putLight(DEVICE_IDENTIFIER, false);
+ });
+ }
+ }
+
+ @Test
+ public void testLogoutInvalidatesAccessTokenOnSuccess() throws Exception {
+ // given:
+ ContentResponse response = createContentResponseMock(204, "");
+ when(request.send()).thenReturn(response);
+
+ RequestFactory requestFactory = mock(RequestFactory.class);
+ when(requestFactory.createPostRequest(ENDPOINT_LOGOUT, ACCESS_TOKEN)).thenReturn(request);
+
+ ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+ try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
+ new DeviceStateDispatcher(), scheduler)) {
+ webservice.setAccessToken(ACCESS_TOKEN);
+
+ // when:
+ webservice.logout();
+
+ // then:
+ assertFalse(webservice.hasAccessToken());
+ verify(request).send();
+ verifyNoMoreInteractions(request);
+ }
+ }
+
+ @Test
+ public void testLogoutThrowsMieleWebserviceExceptionWhenMieleWebserviceTransientExceptionIsThrownInternally()
+ throws Exception {
+ // given:
+ when(request.send()).thenThrow(TimeoutException.class);
+
+ RequestFactory requestFactory = mock(RequestFactory.class);
+ when(requestFactory.createPostRequest(ENDPOINT_LOGOUT, ACCESS_TOKEN)).thenReturn(request);
+
+ ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+ try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
+ new DeviceStateDispatcher(), scheduler)) {
+ webservice.setAccessToken(ACCESS_TOKEN);
+
+ // when:
+ assertThrows(MieleWebserviceException.class, () -> {
+ webservice.logout();
+ });
+ }
+ }
+
+ @Test
+ public void testLogoutInvalidatesAccessTokenWhenOperationFails() throws Exception {
+ // given:
+ when(request.send()).thenThrow(TimeoutException.class);
+
+ RequestFactory requestFactory = mock(RequestFactory.class);
+ when(requestFactory.createPostRequest(ENDPOINT_LOGOUT, ACCESS_TOKEN)).thenReturn(request);
+
+ ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+ try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, retryStrategy,
+ new DeviceStateDispatcher(), scheduler)) {
+ webservice.setAccessToken(ACCESS_TOKEN);
+
+ // when:
+ try {
+ webservice.logout();
+ } catch (MieleWebserviceException e) {
+ }
+
+ // then:
+ assertFalse(webservice.hasAccessToken());
+ }
+ }
+
+ @Test
+ public void testRemoveDeviceStateListenerIsDelegatedToDeviceStateDispatcher() throws Exception {
+ // given:
+ RequestFactory requestFactory = mock(RequestFactory.class);
+
+ DeviceStateDispatcher dispatcher = mock(DeviceStateDispatcher.class);
+
+ ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+ try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
+ dispatcher, scheduler)) {
+ DeviceStateListener listener = mock(DeviceStateListener.class);
+ webservice.addDeviceStateListener(listener);
+
+ // when:
+ webservice.removeDeviceStateListener(listener);
+
+ // then:
+ verify(dispatcher).addListener(listener);
+ verify(dispatcher).removeListener(listener);
+ verifyNoMoreInteractions(dispatcher);
+ }
+ }
+
+ @Test
+ public void testPutPowerStateSendsRequestWithCorrectJsonContentWhenSwitchingTheDeviceOn() throws Exception {
+ // given:
+ ContentResponse response = createContentResponseMock(204, "");
+ when(request.send()).thenReturn(response);
+
+ RequestFactory requestFactory = mock(RequestFactory.class);
+ when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"powerOn\":true}")).thenReturn(request);
+
+ ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+ try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
+ new DeviceStateDispatcher(), scheduler)) {
+ webservice.setAccessToken(ACCESS_TOKEN);
+
+ // when:
+ webservice.putPowerState(DEVICE_IDENTIFIER, true);
+
+ // then:
+ verify(request).send();
+ verifyNoMoreInteractions(request);
+ }
+ }
+
+ @Test
+ public void testPutPowerStateSendsRequestWithCorrectJsonContentWhenDeviceIsSwitchedOff() throws Exception {
+ // given:
+ ContentResponse response = createContentResponseMock(204, "");
+ when(request.send()).thenReturn(response);
+
+ RequestFactory requestFactory = mock(RequestFactory.class);
+ when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"powerOff\":true}"))
+ .thenReturn(request);
+
+ ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+ try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
+ new DeviceStateDispatcher(), scheduler)) {
+ webservice.setAccessToken(ACCESS_TOKEN);
+
+ // when:
+ webservice.putPowerState(DEVICE_IDENTIFIER, false);
+
+ // then:
+ verify(request).send();
+ verifyNoMoreInteractions(request);
+ }
+ }
+
+ @Test
+ public void testPutPowerStateThrowsTooManyRequestsExceptionWhenHttpResponseCodeIs429() throws Exception {
+ // given:
+ HttpFields responseHeaders = mock(HttpFields.class);
+ when(responseHeaders.containsKey(anyString())).thenReturn(false);
+
+ ContentResponse response = createContentResponseMock(429, "{\"message\":\"Too many requests\"}");
+ when(response.getHeaders()).thenReturn(responseHeaders);
+
+ when(request.send()).thenReturn(response);
+
+ RequestFactory requestFactory = mock(RequestFactory.class);
+ when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"powerOff\":true}"))
+ .thenReturn(request);
+
+ ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+ try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
+ new DeviceStateDispatcher(), scheduler)) {
+ webservice.setAccessToken(ACCESS_TOKEN);
+
+ // when:
+ assertThrows(TooManyRequestsException.class, () -> {
+ webservice.putPowerState(DEVICE_IDENTIFIER, false);
+ });
+ }
+ }
+
+ @Test
+ public void testPutProgramResultsInARequestWithCorrectJson() throws Exception {
+ // given:
+ ContentResponse response = createContentResponseMock(204, "");
+ when(request.send()).thenReturn(response);
+ RequestFactory requestFactory = mock(RequestFactory.class);
+ when(requestFactory.createPutRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN, "{\"programId\":1}")).thenReturn(request);
+
+ ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+ try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
+ new DeviceStateDispatcher(), scheduler)) {
+ webservice.setAccessToken(ACCESS_TOKEN);
+
+ // when:
+ webservice.putProgram(DEVICE_IDENTIFIER, 1);
+
+ // then:
+ verify(request).send();
+ verifyNoMoreInteractions(request);
+ }
+ }
+
+ @Test
+ public void testDispatchDeviceStateIsDelegatedToDeviceStateDispatcher() throws Exception {
+ // given:
+ RequestFactory requestFactory = mock(RequestFactory.class);
+ DeviceStateDispatcher dispatcher = mock(DeviceStateDispatcher.class);
+ ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
+
+ try (DefaultMieleWebservice webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0),
+ dispatcher, scheduler)) {
+ // when:
+ webservice.dispatchDeviceState(DEVICE_IDENTIFIER);
+
+ // then:
+ verify(dispatcher).dispatchDeviceState(DEVICE_IDENTIFIER);
+ verifyNoMoreInteractions(dispatcher);
+ }
+ }
+
+ /**
+ * {@link RetryStrategy} for testing purposes. No exceptions will be catched.
+ *
+ * @author Roland Edelhoff - Initial contribution.
+ */
+ private static class UncatchedRetryStrategy implements RetryStrategy {
+
+ @Override
+ public <@Nullable T> T performRetryableOperation(Supplier<T> operation,
+ Consumer<Exception> onTransientException) {
+ return operation.get();
+ }
+
+ @Override
+ public void performRetryableOperation(Runnable operation, Consumer<Exception> onTransientException) {
+ operation.run();
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.*;
+
+import java.util.Arrays;
+import java.util.HashSet;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Device;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceCollection;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class DeviceCacheTest {
+ private static final String FIRST_DEVICE_IDENTIFIER = "000124430016";
+ private static final String SECOND_DEVICE_IDENTIFIER = "000124430017";
+ private static final String THIRD_DEVICE_IDENTIFIER = "400124430017";
+
+ private final Device firstDevice = mock(Device.class);
+ private final Device secondDevice = mock(Device.class);
+ private final Device thirdDevice = mock(Device.class);
+
+ private final DeviceCache deviceCache = new DeviceCache();
+
+ @Test
+ public void testCacheIsEmptyAfterConstruction() {
+ // then:
+ assertEquals(0, deviceCache.getDeviceIds().size());
+ }
+
+ @Test
+ public void testReplaceAllDevicesClearsTheCacheAndPutsAllNewDevicesIntoTheCache() {
+ // given:
+ DeviceCollection deviceCollection = mock(DeviceCollection.class);
+ when(deviceCollection.getDeviceIdentifiers())
+ .thenReturn(new HashSet<>(Arrays.asList(FIRST_DEVICE_IDENTIFIER, SECOND_DEVICE_IDENTIFIER)));
+ when(deviceCollection.getDevice(FIRST_DEVICE_IDENTIFIER)).thenReturn(firstDevice);
+ when(deviceCollection.getDevice(SECOND_DEVICE_IDENTIFIER)).thenReturn(secondDevice);
+
+ // when:
+ deviceCache.replaceAllDevices(deviceCollection);
+
+ // then:
+ assertEquals(new HashSet<>(Arrays.asList(FIRST_DEVICE_IDENTIFIER, SECOND_DEVICE_IDENTIFIER)),
+ deviceCache.getDeviceIds());
+ assertEquals(firstDevice, deviceCache.getDevice(FIRST_DEVICE_IDENTIFIER).get());
+ assertEquals(secondDevice, deviceCache.getDevice(SECOND_DEVICE_IDENTIFIER).get());
+ }
+
+ @Test
+ public void testReplaceAllDevicesClearsTheCachePriorToCachingThePassedDevices() {
+ // given:
+ testReplaceAllDevicesClearsTheCacheAndPutsAllNewDevicesIntoTheCache();
+
+ DeviceCollection deviceCollection = mock(DeviceCollection.class);
+ when(deviceCollection.getDeviceIdentifiers()).thenReturn(new HashSet<>(Arrays.asList(THIRD_DEVICE_IDENTIFIER)));
+ when(deviceCollection.getDevice(THIRD_DEVICE_IDENTIFIER)).thenReturn(thirdDevice);
+
+ // when:
+ deviceCache.replaceAllDevices(deviceCollection);
+
+ // then:
+ assertEquals(new HashSet<>(Arrays.asList(THIRD_DEVICE_IDENTIFIER)), deviceCache.getDeviceIds());
+ assertEquals(thirdDevice, deviceCache.getDevice(THIRD_DEVICE_IDENTIFIER).get());
+ }
+
+ @Test
+ public void testClearClearsTheCachedDevices() {
+ // given:
+ testReplaceAllDevicesClearsTheCacheAndPutsAllNewDevicesIntoTheCache();
+
+ // when:
+ deviceCache.clear();
+
+ // then:
+ assertEquals(0, deviceCache.getDeviceIds().size());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.util.MockUtil.mockDevice;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Actions;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Device;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceCollection;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class DeviceStateDispatcherTest {
+ private static final String FIRST_DEVICE_IDENTIFIER = "000124430016";
+ private static final String SECOND_DEVICE_IDENTIFIER = "000124430017";
+ private static final String UNKNOWN_DEVICE_IDENTIFIER = "100124430016";
+
+ @Nullable
+ private Device firstDevice;
+ @Nullable
+ private Device secondDevice;
+ @Nullable
+ private DeviceCollection devices;
+
+ private Device getFirstDevice() {
+ assertNotNull(firstDevice);
+ return Objects.requireNonNull(firstDevice);
+ }
+
+ private Device getSecondDevice() {
+ assertNotNull(secondDevice);
+ return Objects.requireNonNull(secondDevice);
+ }
+
+ private DeviceCollection getDevices() {
+ assertNotNull(devices);
+ return Objects.requireNonNull(devices);
+ }
+
+ @BeforeEach
+ public void setUp() {
+ firstDevice = mockDevice(FIRST_DEVICE_IDENTIFIER);
+ secondDevice = mockDevice(SECOND_DEVICE_IDENTIFIER);
+
+ devices = mock(DeviceCollection.class);
+ when(getDevices().getDeviceIdentifiers())
+ .thenReturn(new HashSet<String>(Arrays.asList(FIRST_DEVICE_IDENTIFIER, SECOND_DEVICE_IDENTIFIER)));
+ when(getDevices().getDevice(FIRST_DEVICE_IDENTIFIER)).thenReturn(getFirstDevice());
+ when(getDevices().getDevice(SECOND_DEVICE_IDENTIFIER)).thenReturn(getSecondDevice());
+ }
+
+ @Test
+ public void testAddListenerDispatchesStateUpdatesToPassedListenerForCachedDevices()
+ throws InterruptedException, TimeoutException, ExecutionException {
+ // given:
+ DeviceStateListener listener = mock(DeviceStateListener.class);
+
+ DeviceStateDispatcher dispatcher = new DeviceStateDispatcher();
+ dispatcher.dispatchDeviceStateUpdates(getDevices());
+
+ // when:
+ dispatcher.addListener(listener);
+
+ // then:
+ verify(listener).onDeviceStateUpdated(new DeviceState(FIRST_DEVICE_IDENTIFIER, firstDevice));
+ verify(listener).onDeviceStateUpdated(new DeviceState(SECOND_DEVICE_IDENTIFIER, secondDevice));
+ verifyNoMoreInteractions(listener);
+ }
+
+ @Test
+ public void testDeviceStateUpdatesAreNotDispatchedToRemovedListeners() {
+ // given:
+ DeviceStateListener listener = mock(DeviceStateListener.class);
+
+ DeviceStateDispatcher dispatcher = new DeviceStateDispatcher();
+ dispatcher.addListener(listener);
+
+ // when:
+ dispatcher.removeListener(listener);
+ dispatcher.dispatchDeviceStateUpdates(getDevices());
+
+ // then:
+ verifyNoMoreInteractions(listener);
+ }
+
+ @Test
+ public void testClearCachePreventsDeviceStateUpdateDispatchingOnListenerRegistration() {
+ // given:
+ DeviceStateListener listener = mock(DeviceStateListener.class);
+
+ DeviceStateDispatcher dispatcher = new DeviceStateDispatcher();
+ dispatcher.dispatchDeviceStateUpdates(getDevices());
+
+ // when:
+ dispatcher.clearCache();
+ dispatcher.addListener(listener);
+
+ // then:
+ verifyNoMoreInteractions(listener);
+ }
+
+ @Test
+ public void testDeviceStateUpdatesAreDispatchedToSubscribedListeners() {
+ // given:
+ DeviceStateListener listener = mock(DeviceStateListener.class);
+
+ DeviceStateDispatcher dispatcher = new DeviceStateDispatcher();
+ dispatcher.addListener(listener);
+
+ // when:
+ dispatcher.dispatchDeviceStateUpdates(getDevices());
+
+ // then:
+ verify(listener).onDeviceStateUpdated(new DeviceState(FIRST_DEVICE_IDENTIFIER, firstDevice));
+ verify(listener).onDeviceStateUpdated(new DeviceState(SECOND_DEVICE_IDENTIFIER, secondDevice));
+ verifyNoMoreInteractions(listener);
+ }
+
+ @Test
+ public void testRemovalEventsAreDispatchedToSubscribedListeners()
+ throws InterruptedException, TimeoutException, ExecutionException {
+ // given:
+ DeviceStateListener listener = mock(DeviceStateListener.class);
+
+ Device deviceWithUnknownIdentifier = mockDevice(UNKNOWN_DEVICE_IDENTIFIER);
+ DeviceCollection devicesWithUnknownDevice = mock(DeviceCollection.class);
+ when(devicesWithUnknownDevice.getDeviceIdentifiers())
+ .thenReturn(new HashSet<String>(Arrays.asList(UNKNOWN_DEVICE_IDENTIFIER)));
+ when(devicesWithUnknownDevice.getDevice(UNKNOWN_DEVICE_IDENTIFIER)).thenReturn(deviceWithUnknownIdentifier);
+
+ DeviceStateDispatcher dispatcher = new DeviceStateDispatcher();
+ dispatcher.dispatchDeviceStateUpdates(devicesWithUnknownDevice);
+ dispatcher.clearCache();
+ dispatcher.addListener(listener);
+
+ // when:
+ dispatcher.dispatchDeviceStateUpdates(getDevices());
+
+ // then:
+ verify(listener).onDeviceRemoved(UNKNOWN_DEVICE_IDENTIFIER);
+ verify(listener, times(2)).onDeviceStateUpdated(any());
+ verifyNoMoreInteractions(listener);
+ }
+
+ @Test
+ public void testRemovalEventsAreDispatchedToSubscribedListenersMatchingAllDeviceIds()
+ throws InterruptedException, TimeoutException, ExecutionException {
+ // given:
+ DeviceStateListener listener = mock(DeviceStateListener.class);
+
+ Device deviceWithUnknownIdentifier = mockDevice(UNKNOWN_DEVICE_IDENTIFIER);
+ DeviceCollection devicesWithUnknownDevice = mock(DeviceCollection.class);
+ when(devicesWithUnknownDevice.getDeviceIdentifiers())
+ .thenReturn(new HashSet<String>(Arrays.asList(UNKNOWN_DEVICE_IDENTIFIER)));
+ when(devicesWithUnknownDevice.getDevice(UNKNOWN_DEVICE_IDENTIFIER)).thenReturn(deviceWithUnknownIdentifier);
+
+ DeviceCollection emptyDevices = mock(DeviceCollection.class);
+ when(emptyDevices.getDeviceIdentifiers()).thenReturn(new HashSet<String>());
+
+ DeviceStateDispatcher dispatcher = new DeviceStateDispatcher();
+ dispatcher.dispatchDeviceStateUpdates(devicesWithUnknownDevice);
+ dispatcher.clearCache();
+ dispatcher.addListener(listener);
+
+ // when:
+ dispatcher.dispatchDeviceStateUpdates(emptyDevices);
+
+ // then:
+ verify(listener).onDeviceRemoved(UNKNOWN_DEVICE_IDENTIFIER);
+ verifyNoMoreInteractions(listener);
+ }
+
+ @Test
+ public void testDeviceEventDispatchingForSubscribedListenersWithAnyDeviceIdFilter()
+ throws InterruptedException, TimeoutException, ExecutionException {
+ // given:
+ DeviceStateListener listener = mock(DeviceStateListener.class);
+
+ DeviceStateDispatcher dispatcher = new DeviceStateDispatcher();
+ dispatcher.addListener(listener);
+
+ // when:
+ dispatcher.dispatchDeviceStateUpdates(getDevices());
+
+ // then:
+ verify(listener).onDeviceStateUpdated(new DeviceState(FIRST_DEVICE_IDENTIFIER, firstDevice));
+ verify(listener).onDeviceStateUpdated(new DeviceState(SECOND_DEVICE_IDENTIFIER, secondDevice));
+ verifyNoMoreInteractions(listener);
+ }
+
+ @Test
+ public void testActionsEventDispatchingForSubscribedListeners()
+ throws InterruptedException, TimeoutException, ExecutionException {
+ // given:
+ DeviceStateListener listener = mock(DeviceStateListener.class);
+ Actions actions = mock(Actions.class);
+
+ DeviceStateDispatcher dispatcher = new DeviceStateDispatcher();
+ dispatcher.addListener(listener);
+
+ // when:
+ dispatcher.dispatchActionStateUpdates(FIRST_DEVICE_IDENTIFIER, actions);
+
+ // then:
+ verify(listener).onProcessActionUpdated(new ActionsState(FIRST_DEVICE_IDENTIFIER, actions));
+ verifyNoMoreInteractions(listener);
+ }
+
+ @Test
+ public void testDeviceStateDispatcherDispatchesDeviceStatesAndActions() {
+ // given:
+ DeviceStateListener listener = mock(DeviceStateListener.class);
+ Actions actions = mock(Actions.class);
+
+ DeviceStateDispatcher dispatcher = new DeviceStateDispatcher();
+ dispatcher.addListener(listener);
+
+ dispatcher.dispatchDeviceStateUpdates(getDevices());
+ dispatcher.dispatchActionStateUpdates(FIRST_DEVICE_IDENTIFIER, actions);
+
+ // when:
+ dispatcher.dispatchDeviceState(FIRST_DEVICE_IDENTIFIER);
+
+ // then:
+ verify(listener, times(2)).onDeviceStateUpdated(new DeviceState(FIRST_DEVICE_IDENTIFIER, firstDevice));
+ verify(listener).onDeviceStateUpdated(new DeviceState(SECOND_DEVICE_IDENTIFIER, secondDevice));
+ verify(listener).onProcessActionUpdated(new ActionsState(FIRST_DEVICE_IDENTIFIER, actions));
+ verifyNoMoreInteractions(listener);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.http.HttpFields;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class HttpUtilTest {
+ @Test
+ public void whenTheResponseHasARetryAfterHeaderThenItIsParsedAndPassedWithTheException() {
+ // given:
+ HttpFields httpFields = mock(HttpFields.class);
+ when(httpFields.containsKey("Retry-After")).thenReturn(true);
+ when(httpFields.get("Retry-After")).thenReturn("100");
+
+ Response response = mock(Response.class);
+ when(response.getStatus()).thenReturn(429);
+ when(response.getReason()).thenReturn("Too many requests!");
+ when(response.getHeaders()).thenReturn(httpFields);
+
+ // when:
+ try {
+ HttpUtil.checkHttpSuccess(response);
+ fail();
+ } catch (TooManyRequestsException e) {
+ // then:
+ assertEquals(100L, e.getSecondsUntilRetry());
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.util.MockUtil;
+import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider;
+import org.openhab.binding.mielecloud.internal.webservice.request.RequestFactoryImpl;
+import org.openhab.core.io.net.http.HttpClientFactory;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class RequestFactoryImplTest {
+ private static final String URL = "https://www.openhab.org/";
+ private static final String ACCESS_TOKEN = "DE_0123456789abcdef0123456789abcdef";
+ private static final String JSON_CONTENT = "{ \"update\": 1 }";
+
+ private static final String LANGUAGE = "de";
+
+ private static final long REQUEST_TIMEOUT = 5;
+ private static final long EXTENDED_REQUEST_TIMEOUT = 10;
+ private static final TimeUnit REQUEST_TIMEOUT_UNIT = TimeUnit.SECONDS;
+
+ @Nullable
+ private String contentString;
+ @Nullable
+ private String contentType;
+
+ private final LanguageProvider defaultLanguageProvider = new LanguageProvider() {
+ @Override
+ public Optional<String> getLanguage() {
+ return Optional.of(LANGUAGE);
+ }
+ };
+ private final LanguageProvider emptyStringLanguageProvider = new LanguageProvider() {
+ @Override
+ public Optional<String> getLanguage() {
+ return Optional.of("");
+ }
+ };
+
+ private Request getRequestMock() {
+ Request requestMock = mock(Request.class);
+ when(requestMock.header(anyString(), anyString())).thenReturn(requestMock);
+ when(requestMock.timeout(anyLong(), any())).thenReturn(requestMock);
+ when(requestMock.method(any(HttpMethod.class))).thenReturn(requestMock);
+ when(requestMock.param(anyString(), anyString())).thenReturn(requestMock);
+ when(requestMock.content(any())).thenAnswer(i -> {
+ StringContentProvider provider = i.getArgument(0);
+ List<Byte> rawData = new ArrayList<Byte>();
+ provider.forEach(b -> {
+ b.rewind();
+ while (b.hasRemaining()) {
+ rawData.add(b.get());
+ }
+ });
+ byte[] data = new byte[rawData.size()];
+ for (int j = 0; j < data.length; j++) {
+ data[j] = rawData.get(j);
+ }
+ contentString = new String(data, StandardCharsets.UTF_8);
+ contentType = provider.getContentType();
+ return requestMock;
+ });
+ return requestMock;
+ }
+
+ private RequestFactoryImpl createRequestFactoryImpl(Request requestMock, LanguageProvider languageProvider) {
+ HttpClient httpClient = MockUtil.mockHttpClient(URL, requestMock);
+
+ HttpClientFactory httpClientFactory = mock(HttpClientFactory.class);
+ when(httpClientFactory.createHttpClient(anyString())).thenReturn(httpClient);
+
+ return new RequestFactoryImpl(httpClientFactory, languageProvider);
+ }
+
+ @Test
+ public void testCreateGetRequestReturnsRequestWithExpectedHeaders() {
+ // given:
+ Request requestMock = getRequestMock();
+ RequestFactoryImpl requestFactory = createRequestFactoryImpl(requestMock, defaultLanguageProvider);
+
+ // when:
+ Request request = requestFactory.createGetRequest(URL, ACCESS_TOKEN);
+
+ // then:
+ assertEquals(requestMock, request);
+ verify(request).header("Content-type", "application/json");
+ verify(request).header("Accept", "*/*");
+ verify(request).header("Authorization", "Bearer " + ACCESS_TOKEN);
+ verify(request).timeout(REQUEST_TIMEOUT, REQUEST_TIMEOUT_UNIT);
+ verify(request).method(HttpMethod.GET);
+ verify(request).param("language", LANGUAGE);
+ verifyNoMoreInteractions(request);
+ }
+
+ @Test
+ public void testCreatePutRequestReturnsRequestWithExpectedHeadersAndContent() {
+ Request requestMock = getRequestMock();
+ RequestFactoryImpl requestFactory = createRequestFactoryImpl(requestMock, defaultLanguageProvider);
+
+ // when:
+ Request request = requestFactory.createPutRequest(URL, ACCESS_TOKEN, JSON_CONTENT);
+
+ // then:
+ assertEquals(requestMock, request);
+ verify(request).header("Content-type", "application/json");
+ verify(request).header("Accept", "*/*");
+ verify(request).header("Authorization", "Bearer " + ACCESS_TOKEN);
+ verify(request).timeout(EXTENDED_REQUEST_TIMEOUT, REQUEST_TIMEOUT_UNIT);
+ verify(request).method(HttpMethod.PUT);
+ verify(request).content(any());
+ verify(request).param("language", LANGUAGE);
+ assertEquals(JSON_CONTENT, contentString);
+ assertEquals("application/json", contentType);
+ verifyNoMoreInteractions(request);
+ }
+
+ @Test
+ public void testCreatePostRequestReturnsRequestWithExpectedHeaders() {
+ Request requestMock = getRequestMock();
+ RequestFactoryImpl requestFactory = createRequestFactoryImpl(requestMock, defaultLanguageProvider);
+
+ // when:
+ Request request = requestFactory.createPostRequest(URL, ACCESS_TOKEN);
+
+ // then:
+ assertEquals(requestMock, request);
+ verify(request).header("Content-type", "application/json");
+ verify(request).header("Accept", "*/*");
+ verify(request).header("Authorization", "Bearer " + ACCESS_TOKEN);
+ verify(request).timeout(REQUEST_TIMEOUT, REQUEST_TIMEOUT_UNIT);
+ verify(request).method(HttpMethod.POST);
+ verify(request).param("language", LANGUAGE);
+ verifyNoMoreInteractions(request);
+ }
+
+ @Test
+ public void testCreateRequestWithoutSuppliedLangugeCreatesNoLanguageParameter() {
+ // given:
+ Request requestMock = getRequestMock();
+ RequestFactoryImpl requestFactory = createRequestFactoryImpl(requestMock, new LanguageProvider() {
+ @Override
+ public Optional<String> getLanguage() {
+ return Optional.empty();
+ }
+ });
+
+ // when:
+ Request request = requestFactory.createGetRequest(URL, ACCESS_TOKEN);
+
+ // then:
+ assertEquals(requestMock, request);
+ verify(request).header("Content-type", "application/json");
+ verify(request).header("Accept", "*/*");
+ verify(request).header("Authorization", "Bearer " + ACCESS_TOKEN);
+ verify(request).timeout(REQUEST_TIMEOUT, REQUEST_TIMEOUT_UNIT);
+ verify(request).method(HttpMethod.GET);
+ verifyNoMoreInteractions(request);
+ }
+
+ @Test
+ public void testCreateRequestWithEmptyLanguageCreatesNoLanguageParameter() {
+ // given:
+ Request requestMock = getRequestMock();
+ RequestFactoryImpl requestFactory = createRequestFactoryImpl(requestMock, emptyStringLanguageProvider);
+
+ // when:
+ Request request = requestFactory.createGetRequest(URL, ACCESS_TOKEN);
+
+ // then:
+ assertEquals(requestMock, request);
+ verify(request).header("Content-type", "application/json");
+ verify(request).header("Accept", "*/*");
+ verify(request).header("Authorization", "Bearer " + ACCESS_TOKEN);
+ verify(request).timeout(REQUEST_TIMEOUT, REQUEST_TIMEOUT_UNIT);
+ verify(request).method(HttpMethod.GET);
+ verifyNoMoreInteractions(request);
+ }
+
+ @Test
+ public void whenAnSseRequestIsCreatedWithoutLanguageThenTheRequiredParametersAreSet() {
+ Request requestMock = getRequestMock();
+ RequestFactoryImpl requestFactory = createRequestFactoryImpl(requestMock, emptyStringLanguageProvider);
+
+ // when:
+ Request request = requestFactory.createSseRequest(URL, ACCESS_TOKEN);
+
+ // then:
+ assertEquals(requestMock, request);
+ verify(request).header("Content-type", "application/json");
+ verify(request).header("Accept", "text/event-stream");
+ verify(request).header("Authorization", "Bearer " + ACCESS_TOKEN);
+ verifyNoMoreInteractions(request);
+ }
+
+ @Test
+ public void whenAnSseRequestIsCreatedWithLanguageThenTheAcceptLanguageHeaderIsSet() {
+ Request requestMock = getRequestMock();
+ RequestFactoryImpl requestFactory = createRequestFactoryImpl(requestMock, defaultLanguageProvider);
+
+ // when:
+ Request request = requestFactory.createSseRequest(URL, ACCESS_TOKEN);
+
+ // then:
+ assertEquals(requestMock, request);
+ verify(request).header("Content-type", "application/json");
+ verify(request).header("Accept", "text/event-stream");
+ verify(request).header("Authorization", "Bearer " + ACCESS_TOKEN);
+ verify(request).header("Accept-Language", LANGUAGE);
+ verifyNoMoreInteractions(request);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Actions;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Light;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class ActionsStateTest {
+ private static final String DEVICE_IDENTIFIER = "003458276345";
+
+ @Test
+ public void testGetDeviceIdentifierReturnsDeviceIdentifier() {
+ // given:
+ Actions actions = mock(Actions.class);
+ ActionsState actionsState = new ActionsState(DEVICE_IDENTIFIER, actions);
+
+ // when:
+ String deviceId = actionsState.getDeviceIdentifier();
+
+ // then:
+ assertEquals(DEVICE_IDENTIFIER, deviceId);
+ }
+
+ @Test
+ public void testReturnValuesWhenActionsIsNull() {
+ // given:
+ ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, null);
+
+ // when:
+ boolean canBeStarted = actionState.canBeStarted();
+ boolean canBeStopped = actionState.canBeStopped();
+ boolean canBePaused = actionState.canBePaused();
+ boolean canStartSupercooling = actionState.canStartSupercooling();
+ boolean canStopSupercooling = actionState.canStopSupercooling();
+ boolean canContolSupercooling = actionState.canContolSupercooling();
+ boolean canStartSuperfreezing = actionState.canStartSuperfreezing();
+ boolean canStopSuperfreezing = actionState.canStopSuperfreezing();
+ boolean canControlSuperfreezing = actionState.canControlSuperfreezing();
+ boolean canEnableLight = actionState.canEnableLight();
+ boolean canDisableLight = actionState.canDisableLight();
+
+ // then:
+ assertFalse(canBeStarted);
+ assertFalse(canBeStopped);
+ assertFalse(canBePaused);
+ assertFalse(canStartSupercooling);
+ assertFalse(canStopSupercooling);
+ assertFalse(canContolSupercooling);
+ assertFalse(canStartSuperfreezing);
+ assertFalse(canStopSuperfreezing);
+ assertFalse(canControlSuperfreezing);
+ assertFalse(canEnableLight);
+ assertFalse(canDisableLight);
+ }
+
+ @Test
+ public void testReturnValuesWhenProcessActionIsEmpty() {
+ // given:
+ Actions actions = mock(Actions.class);
+ ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, null);
+ when(actions.getProcessAction()).thenReturn(Collections.emptyList());
+
+ // when:
+ boolean canBeStarted = actionState.canBeStarted();
+ boolean canBeStopped = actionState.canBeStopped();
+ boolean canBePaused = actionState.canBePaused();
+ boolean canStartSupercooling = actionState.canStartSupercooling();
+ boolean canStopSupercooling = actionState.canStopSupercooling();
+ boolean canStartSuperfreezing = actionState.canStartSuperfreezing();
+ boolean canStopSuperfreezing = actionState.canStopSuperfreezing();
+
+ // then:
+ assertFalse(canBeStarted);
+ assertFalse(canBeStopped);
+ assertFalse(canBePaused);
+ assertFalse(canStartSupercooling);
+ assertFalse(canStopSupercooling);
+ assertFalse(canStartSuperfreezing);
+ assertFalse(canStopSuperfreezing);
+ }
+
+ @Test
+ public void testReturnValuesWhenLightIsEmpty() {
+ // given:
+ Actions actions = mock(Actions.class);
+ ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, null);
+ when(actions.getLight()).thenReturn(Collections.emptyList());
+
+ // when:
+ boolean canEnableLight = actionState.canEnableLight();
+ boolean canDisableLight = actionState.canDisableLight();
+
+ // then:
+ assertFalse(canEnableLight);
+ assertFalse(canDisableLight);
+ }
+
+ @Test
+ public void testReturnValueWhenProcessActionStartIsAvailable() {
+ // given:
+ Actions actions = mock(Actions.class);
+ ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
+ when(actions.getProcessAction()).thenReturn(Collections.singletonList(ProcessAction.START));
+
+ // when:
+ boolean canBeStarted = actionState.canBeStarted();
+
+ // then:
+ assertTrue(canBeStarted);
+ }
+
+ @Test
+ public void testReturnValueWhenProcessActionStopIsAvailable() {
+ // given:
+ Actions actions = mock(Actions.class);
+ ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
+ when(actions.getProcessAction()).thenReturn(Collections.singletonList(ProcessAction.STOP));
+
+ // when:
+ boolean canBeStopped = actionState.canBeStopped();
+
+ // then:
+ assertTrue(canBeStopped);
+ }
+
+ @Test
+ public void testReturnValueWhenProcessActionStartSupercoolIsAvailable() {
+ // given:
+ Actions actions = mock(Actions.class);
+ ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
+ when(actions.getProcessAction()).thenReturn(Collections.singletonList(ProcessAction.START_SUPERCOOLING));
+
+ // when:
+ boolean canStartSupercooling = actionState.canStartSupercooling();
+ boolean canContolSupercooling = actionState.canContolSupercooling();
+
+ // then:
+ assertTrue(canStartSupercooling);
+ assertTrue(canContolSupercooling);
+ }
+
+ @Test
+ public void testReturnValueWhenProcessActionStartSuperfreezeIsAvailable() {
+ // given:
+ Actions actions = mock(Actions.class);
+ ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
+ when(actions.getProcessAction()).thenReturn(Collections.singletonList(ProcessAction.START_SUPERFREEZING));
+
+ // when:
+ boolean canStartSuperfreezing = actionState.canStartSuperfreezing();
+ boolean canControlSuperfreezing = actionState.canControlSuperfreezing();
+
+ // then:
+ assertTrue(canStartSuperfreezing);
+ assertTrue(canControlSuperfreezing);
+ }
+
+ @Test
+ public void testReturnValueWhenLightEnableIsAvailable() {
+ // given:
+ Actions actions = mock(Actions.class);
+ ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
+ when(actions.getLight()).thenReturn(Collections.singletonList(Light.ENABLE));
+
+ // when:
+ boolean canEnableLight = actionState.canEnableLight();
+
+ // then:
+ assertTrue(canEnableLight);
+ }
+
+ @Test
+ public void testReturnValueWhenLightDisableIsAvailable() {
+ // given:
+ Actions actions = mock(Actions.class);
+ ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
+ when(actions.getLight()).thenReturn(Collections.singletonList(Light.DISABLE));
+
+ // when:
+ boolean canDisableLight = actionState.canDisableLight();
+
+ // then:
+ assertTrue(canDisableLight);
+ }
+
+ @Test
+ public void testCanControlLightReturnsTrueWhenLightCanBeEnabled() {
+ // given:
+ Actions actions = mock(Actions.class);
+ when(actions.getLight()).thenReturn(Collections.singletonList(Light.ENABLE));
+
+ ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
+
+ // when:
+ boolean canControlLight = actionState.canControlLight();
+
+ // then:
+ assertTrue(canControlLight);
+ }
+
+ @Test
+ public void testCanControlLightReturnsTrueWhenLightCanBeDisabled() {
+ // given:
+ Actions actions = mock(Actions.class);
+ when(actions.getLight()).thenReturn(Collections.singletonList(Light.DISABLE));
+
+ ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
+
+ // when:
+ boolean canControlLight = actionState.canControlLight();
+
+ // then:
+ assertTrue(canControlLight);
+ }
+
+ @Test
+ public void testCanControlLightReturnsTrueWhenLightCanBeEnabledAndDisabled() {
+ // given:
+ Actions actions = mock(Actions.class);
+ when(actions.getLight()).thenReturn(Arrays.asList(Light.ENABLE, Light.DISABLE));
+
+ ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
+
+ // when:
+ boolean canControlLight = actionState.canControlLight();
+
+ // then:
+ assertTrue(canControlLight);
+ }
+
+ @Test
+ public void testCanControlLightReturnsFalseWhenNoLightOptionIsAvailable() {
+ // given:
+ Actions actions = mock(Actions.class);
+ when(actions.getLight()).thenReturn(new LinkedList<Light>());
+
+ ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
+
+ // when:
+ boolean canControlLight = actionState.canControlLight();
+
+ // then:
+ assertFalse(canControlLight);
+ }
+
+ @Test
+ public void testNoProgramCanBeSetWhenNoProgramIdIsPresent() {
+ // given:
+ Actions actions = mock(Actions.class);
+ when(actions.getProgramId()).thenReturn(Collections.emptyList());
+
+ ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
+
+ // when:
+ boolean canSetActiveProgram = actionState.canSetActiveProgramId();
+
+ // then:
+ assertFalse(canSetActiveProgram);
+ }
+
+ @Test
+ public void testProgramIdCanBeSetWhenProgramIdIsPresent() {
+ // given:
+ Actions actions = mock(Actions.class);
+ when(actions.getProgramId()).thenReturn(Collections.singletonList(1));
+
+ ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
+
+ // when:
+ boolean canSetActiveProgram = actionState.canSetActiveProgramId();
+
+ // then:
+ assertTrue(canSetActiveProgram);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class CoolingDeviceTemperatureStateTest {
+ private static final Integer TEMPERATURE_0 = 8;
+ private static final Integer TEMPERATURE_1 = -10;
+
+ private static final Integer TARGET_TEMPERATURE_0 = 5;
+ private static final Integer TARGET_TEMPERATURE_1 = -18;
+
+ @Nullable
+ private DeviceState deviceState;
+
+ private DeviceState getDeviceState() {
+ assertNotNull(deviceState);
+ return Objects.requireNonNull(deviceState);
+ }
+
+ @BeforeEach
+ public void setUp() {
+ deviceState = mock(DeviceState.class);
+ when(getDeviceState().getTemperature(0)).thenReturn(Optional.of(TEMPERATURE_0));
+ when(getDeviceState().getTemperature(1)).thenReturn(Optional.of(TEMPERATURE_1));
+ when(getDeviceState().getTargetTemperature(0)).thenReturn(Optional.of(TARGET_TEMPERATURE_0));
+ when(getDeviceState().getTargetTemperature(1)).thenReturn(Optional.of(TARGET_TEMPERATURE_1));
+ }
+
+ @Test
+ public void testGetFridgeTemperaturesForFridge() {
+ // given:
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.FRIDGE);
+ CoolingDeviceTemperatureState state = new CoolingDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Integer current = state.getFridgeTemperature().get();
+ Integer target = state.getFridgeTargetTemperature().get();
+
+ // then:
+ assertEquals(TEMPERATURE_0, current);
+ assertEquals(TARGET_TEMPERATURE_0, target);
+ }
+
+ @Test
+ public void testGetFridgeTemperaturesForFridgeFreezerCombination() {
+ // given:
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.FRIDGE_FREEZER_COMBINATION);
+ CoolingDeviceTemperatureState state = new CoolingDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Integer current = state.getFridgeTemperature().get();
+ Integer target = state.getFridgeTargetTemperature().get();
+
+ // then:
+ assertEquals(TEMPERATURE_0, current);
+ assertEquals(TARGET_TEMPERATURE_0, target);
+ }
+
+ @Test
+ public void testGetFridgeTemperaturesForFreezer() {
+ // given:
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.FREEZER);
+ CoolingDeviceTemperatureState state = new CoolingDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Optional<Integer> current = state.getFridgeTemperature();
+ Optional<Integer> target = state.getFridgeTargetTemperature();
+
+ // then:
+ assertFalse(current.isPresent());
+ assertFalse(target.isPresent());
+ }
+
+ @Test
+ public void testGetFreezerTemperaturesForFridge() {
+ // given:
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.FRIDGE);
+ CoolingDeviceTemperatureState state = new CoolingDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Optional<Integer> current = state.getFreezerTemperature();
+ Optional<Integer> target = state.getFreezerTargetTemperature();
+
+ // then:
+ assertFalse(current.isPresent());
+ assertFalse(target.isPresent());
+ }
+
+ @Test
+ public void testGetFreezerTemperaturesForFridgeFreezerCombination() {
+ // given:
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.FRIDGE_FREEZER_COMBINATION);
+ CoolingDeviceTemperatureState state = new CoolingDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Integer current = state.getFreezerTemperature().get();
+ Integer target = state.getFreezerTargetTemperature().get();
+
+ // then:
+ assertEquals(TEMPERATURE_1, current);
+ assertEquals(TARGET_TEMPERATURE_1, target);
+ }
+
+ @Test
+ public void testGetFreezerTemperaturesForFreezer() {
+ // given:
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.FREEZER);
+ CoolingDeviceTemperatureState state = new CoolingDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Integer current = state.getFreezerTemperature().get();
+ Integer target = state.getFreezerTargetTemperature().get();
+
+ // then:
+ assertEquals(TEMPERATURE_0, current);
+ assertEquals(TARGET_TEMPERATURE_0, target);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Device;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceIdentLabel;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DryingStep;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Ident;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Light;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.PlateStep;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProgramId;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProgramPhase;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProgramType;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.RemoteEnable;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.SpinningSpeed;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.State;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Status;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Temperature;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Type;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.VentilationStep;
+
+/**
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add pre-heat finished, plate step, door state, door alarm and info state channels and map
+ * signal flags from API
+ * @author Björn Lange - Add elapsed time channel, robotic vacuum cleaner
+ */
+@NonNullByDefault
+public class DeviceStateTest {
+ private static final String DEVICE_IDENTIFIER = "mac-f83001f37d45ffff";
+
+ @Test
+ public void testGetDeviceIdentifierReturnsDeviceIdentifier() {
+ // given:
+ Device device = mock(Device.class);
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ String deviceId = deviceState.getDeviceIdentifier();
+
+ // then:
+ assertEquals(DEVICE_IDENTIFIER, deviceId);
+ }
+
+ @Test
+ public void testReturnValuesWhenDeviceIsNull() {
+ // given:
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, null);
+
+ // when:
+ Optional<String> status = deviceState.getStatus();
+ Optional<Integer> statusRaw = deviceState.getStatusRaw();
+ Optional<StateType> stateType = deviceState.getStateType();
+ Optional<String> selectedProgram = deviceState.getSelectedProgram();
+ Optional<Long> selectedProgramId = deviceState.getSelectedProgramId();
+ Optional<String> programPhase = deviceState.getProgramPhase();
+ Optional<Integer> programPhaseRaw = deviceState.getProgramPhaseRaw();
+ Optional<String> dryingTarget = deviceState.getDryingTarget();
+ Optional<Integer> dryingTargetRaw = deviceState.getDryingTargetRaw();
+ Optional<Boolean> hasPreHeatFinished = deviceState.hasPreHeatFinished();
+ Optional<Integer> targetTemperature = deviceState.getTargetTemperature(0);
+ Optional<Integer> temperature = deviceState.getTemperature(0);
+ Optional<Boolean> remoteControlEnabled = deviceState.isRemoteControlEnabled();
+ Optional<String> ventilationStep = deviceState.getVentilationStep();
+ Optional<Integer> ventilationStepRaw = deviceState.getVentilationStepRaw();
+ Optional<Integer> plateStepCount = deviceState.getPlateStepCount();
+ Optional<String> plateStep = deviceState.getPlateStep(0);
+ Optional<Integer> plateStepRaw = deviceState.getPlateStepRaw(0);
+ boolean hasError = deviceState.hasError();
+ boolean hasInfo = deviceState.hasInfo();
+ Optional<Boolean> doorState = deviceState.getDoorState();
+ Optional<Boolean> doorAlarm = deviceState.getDoorAlarm();
+ Optional<String> type = deviceState.getType();
+ DeviceType rawType = deviceState.getRawType();
+ Optional<Integer> batteryLevel = deviceState.getBatteryLevel();
+
+ Optional<String> deviceName = deviceState.getDeviceName();
+ Optional<String> fabNumber = deviceState.getFabNumber();
+ Optional<String> techType = deviceState.getTechType();
+ Optional<Integer> progress = deviceState.getProgress();
+
+ // then:
+ assertFalse(status.isPresent());
+ assertFalse(statusRaw.isPresent());
+ assertFalse(stateType.isPresent());
+ assertFalse(selectedProgram.isPresent());
+ assertFalse(selectedProgramId.isPresent());
+ assertFalse(programPhase.isPresent());
+ assertFalse(programPhaseRaw.isPresent());
+ assertFalse(dryingTarget.isPresent());
+ assertFalse(dryingTargetRaw.isPresent());
+ assertFalse(hasPreHeatFinished.isPresent());
+ assertFalse(targetTemperature.isPresent());
+ assertFalse(temperature.isPresent());
+ assertFalse(remoteControlEnabled.isPresent());
+ assertFalse(ventilationStep.isPresent());
+ assertFalse(ventilationStepRaw.isPresent());
+ assertFalse(plateStepCount.isPresent());
+ assertFalse(plateStep.isPresent());
+ assertFalse(plateStepRaw.isPresent());
+ assertFalse(hasError);
+ assertFalse(hasInfo);
+ assertFalse(doorState.isPresent());
+ assertFalse(doorAlarm.isPresent());
+ assertFalse(type.isPresent());
+ assertEquals(DeviceType.UNKNOWN, rawType);
+ assertFalse(deviceName.isPresent());
+ assertFalse(fabNumber.isPresent());
+ assertFalse(techType.isPresent());
+ assertFalse(progress.isPresent());
+ assertFalse(batteryLevel.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenStateIsNull() {
+ // given:
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.empty());
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<String> status = deviceState.getStatus();
+ Optional<Integer> statusRaw = deviceState.getStatusRaw();
+ Optional<StateType> stateType = deviceState.getStateType();
+ Optional<String> selectedProgram = deviceState.getSelectedProgram();
+ Optional<Long> selectedProgramId = deviceState.getSelectedProgramId();
+ Optional<String> programPhase = deviceState.getProgramPhase();
+ Optional<Integer> programPhaseRaw = deviceState.getProgramPhaseRaw();
+ Optional<String> dryingTarget = deviceState.getDryingTarget();
+ Optional<Integer> dryingTargetRaw = deviceState.getDryingTargetRaw();
+ Optional<Boolean> hasPreHeatFinished = deviceState.hasPreHeatFinished();
+ Optional<Integer> targetTemperature = deviceState.getTargetTemperature(0);
+ Optional<Integer> temperature = deviceState.getTemperature(0);
+ Optional<Boolean> remoteControlEnabled = deviceState.isRemoteControlEnabled();
+ Optional<Integer> progress = deviceState.getProgress();
+ Optional<String> ventilationStep = deviceState.getVentilationStep();
+ Optional<Integer> ventilationStepRaw = deviceState.getVentilationStepRaw();
+ Optional<Integer> plateStepCount = deviceState.getPlateStepCount();
+ Optional<String> plateStep = deviceState.getPlateStep(0);
+ Optional<Integer> plateStepRaw = deviceState.getPlateStepRaw(0);
+ Boolean hasError = deviceState.hasError();
+ Optional<Boolean> doorState = deviceState.getDoorState();
+ Optional<Boolean> doorAlarm = deviceState.getDoorAlarm();
+ Optional<Integer> batteryLevel = deviceState.getBatteryLevel();
+
+ // then:
+ assertFalse(status.isPresent());
+ assertFalse(statusRaw.isPresent());
+ assertFalse(stateType.isPresent());
+ assertFalse(selectedProgram.isPresent());
+ assertFalse(selectedProgramId.isPresent());
+ assertFalse(programPhase.isPresent());
+ assertFalse(programPhaseRaw.isPresent());
+ assertFalse(dryingTarget.isPresent());
+ assertFalse(dryingTargetRaw.isPresent());
+ assertFalse(hasPreHeatFinished.isPresent());
+ assertFalse(targetTemperature.isPresent());
+ assertFalse(temperature.isPresent());
+ assertFalse(remoteControlEnabled.isPresent());
+ assertFalse(progress.isPresent());
+ assertFalse(ventilationStep.isPresent());
+ assertFalse(ventilationStepRaw.isPresent());
+ assertFalse(plateStepCount.isPresent());
+ assertFalse(plateStep.isPresent());
+ assertFalse(plateStepRaw.isPresent());
+ assertFalse(hasError);
+ assertFalse(doorState.isPresent());
+ assertFalse(doorAlarm.isPresent());
+ assertFalse(batteryLevel.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenStatusIsEmpty() {
+ // given:
+ State state = mock(State.class);
+ when(state.getStatus()).thenReturn(Optional.empty());
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<String> status = deviceState.getStatus();
+ Optional<Integer> statusRaw = deviceState.getStatusRaw();
+ Optional<StateType> stateType = deviceState.getStateType();
+
+ // then:
+ assertFalse(status.isPresent());
+ assertFalse(statusRaw.isPresent());
+ assertFalse(stateType.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenStatusValuesAreEmpty() {
+ // given:
+ Status statusMock = mock(Status.class);
+ when(statusMock.getValueLocalized()).thenReturn(Optional.empty());
+ when(statusMock.getValueRaw()).thenReturn(Optional.empty());
+
+ State state = mock(State.class);
+ when(state.getStatus()).thenReturn(Optional.of(statusMock));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<String> status = deviceState.getStatus();
+ Optional<Integer> statusRaw = deviceState.getStatusRaw();
+ Optional<StateType> stateType = deviceState.getStateType();
+
+ // then:
+ assertFalse(status.isPresent());
+ assertFalse(statusRaw.isPresent());
+ assertFalse(stateType.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenStatusValueLocalizedIsNotNull() {
+ // given:
+ Status statusMock = mock(Status.class);
+ when(statusMock.getValueLocalized()).thenReturn(Optional.of("Not connected"));
+ when(statusMock.getValueRaw()).thenReturn(Optional.of(StateType.NOT_CONNECTED.getCode()));
+
+ State state = mock(State.class);
+ when(state.getStatus()).thenReturn(Optional.of(statusMock));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ String status = deviceState.getStatus().get();
+ int statusRaw = deviceState.getStatusRaw().get();
+ StateType stateType = deviceState.getStateType().get();
+
+ // then:
+ assertEquals("Not connected", status);
+ assertEquals(StateType.NOT_CONNECTED.getCode(), statusRaw);
+ assertEquals(StateType.NOT_CONNECTED, stateType);
+ }
+
+ @Test
+ public void testReturnValuesWhenStatusValueRawIsNotNull() {
+ // given:
+ Status statusMock = mock(Status.class);
+ when(statusMock.getValueRaw()).thenReturn(Optional.of(StateType.END_PROGRAMMED.getCode()));
+
+ State state = mock(State.class);
+ when(state.getStatus()).thenReturn(Optional.of(statusMock));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ StateType stateType = deviceState.getStateType().get();
+
+ // then:
+ assertEquals(StateType.END_PROGRAMMED, stateType);
+ }
+
+ @Test
+ public void testReturnValuesWhenProgramTypeIsEmpty() {
+ // given:
+ State state = mock(State.class);
+ when(state.getProgramType()).thenReturn(Optional.empty());
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<String> selectedProgram = deviceState.getSelectedProgram();
+ Optional<Long> selectedProgramId = deviceState.getSelectedProgramId();
+
+ // then:
+ assertFalse(selectedProgram.isPresent());
+ assertFalse(selectedProgramId.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenProgramTypeValueLocalizedIsEmpty() {
+ // given:
+ ProgramType programType = mock(ProgramType.class);
+ when(programType.getValueLocalized()).thenReturn(Optional.empty());
+
+ State state = mock(State.class);
+ when(state.getProgramType()).thenReturn(Optional.of(programType));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<String> selectedProgram = deviceState.getSelectedProgram();
+ Optional<Long> selectedProgramId = deviceState.getSelectedProgramId();
+
+ // then:
+ assertFalse(selectedProgram.isPresent());
+ assertFalse(selectedProgramId.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenProgramTypeValueLocalizedIsNotNull() {
+ // given:
+ ProgramId programId = mock(ProgramId.class);
+ when(programId.getValueRaw()).thenReturn(Optional.of(3L));
+ when(programId.getValueLocalized()).thenReturn(Optional.of("Washing"));
+
+ State state = mock(State.class);
+ Status status = mock(Status.class);
+ Device device = mock(Device.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+ when(state.getProgramId()).thenReturn(Optional.of(programId));
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ String selectedProgram = deviceState.getSelectedProgram().get();
+ long selectedProgramId = deviceState.getSelectedProgramId().get();
+
+ // then:
+ assertEquals("Washing", selectedProgram);
+ assertEquals(3L, selectedProgramId);
+ }
+
+ @Test
+ public void testReturnValuesWhenProgramPhaseIsEmpty() {
+ // given:
+ State state = mock(State.class);
+ when(state.getProgramPhase()).thenReturn(Optional.empty());
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<String> programPhase = deviceState.getProgramPhase();
+ Optional<Integer> programPhaseRaw = deviceState.getProgramPhaseRaw();
+
+ // then:
+ assertFalse(programPhase.isPresent());
+ assertFalse(programPhaseRaw.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenProgramPhaseValueLocalizedIsEmpty() {
+ // given:
+ ProgramPhase programPhaseMock = mock(ProgramPhase.class);
+ when(programPhaseMock.getValueLocalized()).thenReturn(Optional.empty());
+
+ State state = mock(State.class);
+ when(state.getProgramPhase()).thenReturn(Optional.of(programPhaseMock));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<String> programPhase = deviceState.getProgramPhase();
+ Optional<Integer> programPhaseRaw = deviceState.getProgramPhaseRaw();
+
+ // then:
+ assertFalse(programPhase.isPresent());
+ assertFalse(programPhaseRaw.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenProgramPhaseValueLocalizedIsNotNull() {
+ // given:
+ ProgramPhase programPhaseMock = mock(ProgramPhase.class);
+ when(programPhaseMock.getValueLocalized()).thenReturn(Optional.of("Spülen"));
+ when(programPhaseMock.getValueRaw()).thenReturn(Optional.of(4));
+
+ State state = mock(State.class);
+ Status status = mock(Status.class);
+ Device device = mock(Device.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+ when(state.getProgramPhase()).thenReturn(Optional.of(programPhaseMock));
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ String programPhase = deviceState.getProgramPhase().get();
+ int programPhaseRaw = deviceState.getProgramPhaseRaw().get();
+
+ // then:
+ assertEquals("Spülen", programPhase);
+ assertEquals(4, programPhaseRaw);
+ }
+
+ @Test
+ public void testReturnValuesWhenDryingStepIsEmpty() {
+ // given:
+ State state = mock(State.class);
+ when(state.getDryingStep()).thenReturn(Optional.empty());
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<String> dryingTarget = deviceState.getDryingTarget();
+ Optional<Integer> dryingTargetRaw = deviceState.getDryingTargetRaw();
+
+ // then:
+ assertFalse(dryingTarget.isPresent());
+ assertFalse(dryingTargetRaw.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenDryingStepValueLocalizedIsEmpty() {
+ // given:
+ DryingStep dryingStep = mock(DryingStep.class);
+ when(dryingStep.getValueLocalized()).thenReturn(Optional.empty());
+ when(dryingStep.getValueRaw()).thenReturn(Optional.empty());
+
+ State state = mock(State.class);
+ when(state.getDryingStep()).thenReturn(Optional.of(dryingStep));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<String> dryingTarget = deviceState.getDryingTarget();
+ Optional<Integer> dryingTargetRaw = deviceState.getDryingTargetRaw();
+
+ // then:
+ assertFalse(dryingTarget.isPresent());
+ assertFalse(dryingTargetRaw.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenDryingStepValueLocalizedIsNotNull() {
+ // given:
+ DryingStep dryingStep = mock(DryingStep.class);
+ when(dryingStep.getValueLocalized()).thenReturn(Optional.of("Hot"));
+ when(dryingStep.getValueRaw()).thenReturn(Optional.of(5));
+
+ State state = mock(State.class);
+ Device device = mock(Device.class);
+ Status status = mock(Status.class);
+ when(state.getDryingStep()).thenReturn(Optional.of(dryingStep));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ String dryingTarget = deviceState.getDryingTarget().get();
+ int dryingTargetRaw = deviceState.getDryingTargetRaw().get();
+
+ // then:
+ assertEquals("Hot", dryingTarget);
+ assertEquals(5, dryingTargetRaw);
+ }
+
+ @Test
+ public void testReturnValuesPreHeatFinishedWhenStateIsNotRunning() {
+ // given:
+ Temperature targetTemperature = mock(Temperature.class);
+ when(targetTemperature.getValueLocalized()).thenReturn(Optional.of(0));
+ Temperature currentTemperature = mock(Temperature.class);
+ when(currentTemperature.getValueLocalized()).thenReturn(Optional.of(0));
+
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ State state = mock(State.class);
+ when(state.getTargetTemperature()).thenReturn(Arrays.asList(targetTemperature));
+ when(state.getTemperature()).thenReturn(Arrays.asList(currentTemperature));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Boolean> hasPreHeatFinished = deviceState.hasPreHeatFinished();
+
+ // then:
+ assertFalse(hasPreHeatFinished.get());
+ }
+
+ @Test
+ public void testReturnValuesPreHeatFinishedWhenTargetTemperatureIsEmpty() {
+ // given:
+ Temperature targetTemperature = mock(Temperature.class);
+ when(targetTemperature.getValueLocalized()).thenReturn(Optional.empty());
+ Temperature currentTemperature = mock(Temperature.class);
+ when(currentTemperature.getValueLocalized()).thenReturn(Optional.of(180));
+
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+
+ State state = mock(State.class);
+ when(state.getTargetTemperature()).thenReturn(Arrays.asList(targetTemperature));
+ when(state.getTemperature()).thenReturn(Arrays.asList(currentTemperature));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Boolean> hasPreHeatFinished = deviceState.hasPreHeatFinished();
+
+ // then:
+ assertFalse(hasPreHeatFinished.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesPreHeatFinishedWhenCurrentTemperatureIsEmpty() {
+ // given:
+ Temperature targetTemperature = mock(Temperature.class);
+ when(targetTemperature.getValueLocalized()).thenReturn(Optional.of(180));
+ Temperature currentTemperature = mock(Temperature.class);
+ when(currentTemperature.getValueLocalized()).thenReturn(Optional.empty());
+
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+
+ State state = mock(State.class);
+ when(state.getTargetTemperature()).thenReturn(Arrays.asList(targetTemperature));
+ when(state.getTemperature()).thenReturn(Arrays.asList(currentTemperature));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Boolean> hasPreHeatFinished = deviceState.hasPreHeatFinished();
+
+ // then:
+ assertFalse(hasPreHeatFinished.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesPreHeatFinishedWhenPreHeatingHasFinished() {
+ // given:
+ Temperature targetTemperature = mock(Temperature.class);
+ when(targetTemperature.getValueLocalized()).thenReturn(Optional.of(180));
+ Temperature currentTemperature = mock(Temperature.class);
+ when(currentTemperature.getValueLocalized()).thenReturn(Optional.of(180));
+
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+
+ State state = mock(State.class);
+ when(state.getTargetTemperature()).thenReturn(Arrays.asList(targetTemperature));
+ when(state.getTemperature()).thenReturn(Arrays.asList(currentTemperature));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Boolean> hasPreHeatFinished = deviceState.hasPreHeatFinished();
+
+ // then:
+ assertTrue(hasPreHeatFinished.get());
+ }
+
+ @Test
+ public void testReturnValuesPreHeatFinishedWhenPreHeatingHasNotFinished() {
+ // given:
+ Temperature targetTemperature = mock(Temperature.class);
+ when(targetTemperature.getValueLocalized()).thenReturn(Optional.of(180));
+ Temperature currentTemperature = mock(Temperature.class);
+ when(currentTemperature.getValueLocalized()).thenReturn(Optional.of(179));
+
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+
+ State state = mock(State.class);
+ when(state.getTargetTemperature()).thenReturn(Arrays.asList(targetTemperature));
+ when(state.getTemperature()).thenReturn(Arrays.asList(currentTemperature));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Boolean> hasPreHeatFinished = deviceState.hasPreHeatFinished();
+
+ // then:
+ assertFalse(hasPreHeatFinished.get());
+ }
+
+ @Test
+ public void testReturnValuesWhenTargetTemperatureIsEmpty() {
+ // given:
+ State state = mock(State.class);
+ when(state.getTargetTemperature()).thenReturn(Collections.emptyList());
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Integer> targetTemperature = deviceState.getTargetTemperature(0);
+
+ // then:
+ assertFalse(targetTemperature.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenTargetTemperatureIndexIsOutOfRange() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ State state = mock(State.class);
+ when(state.getTargetTemperature()).thenReturn(new LinkedList<>());
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Integer> targetTemperature = deviceState.getTargetTemperature(0);
+
+ // then:
+ assertFalse(targetTemperature.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenTargetTemperatureValueLocalizedIsEmpty() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ Temperature temperature = mock(Temperature.class);
+ when(temperature.getValueLocalized()).thenReturn(Optional.empty());
+
+ State state = mock(State.class);
+ when(state.getTargetTemperature()).thenReturn(Arrays.asList(temperature));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Integer> targetTemperature = deviceState.getTargetTemperature(0);
+
+ // then:
+ assertFalse(targetTemperature.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenTargetTemperatureValueLocalizedIsValid() {
+ // given:
+ Temperature temperature = mock(Temperature.class);
+ when(temperature.getValueLocalized()).thenReturn(Optional.of(20));
+
+ State state = mock(State.class);
+ Status status = mock(Status.class);
+ Device device = mock(Device.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+ when(state.getTargetTemperature()).thenReturn(Arrays.asList(temperature));
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Integer targetTemperature = deviceState.getTargetTemperature(0).get();
+
+ // then:
+ assertEquals(Integer.valueOf(20), targetTemperature);
+ }
+
+ @Test
+ public void testReturnValuesWhenTemperatureIsEmpty() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ State state = mock(State.class);
+ when(state.getTemperature()).thenReturn(Collections.emptyList());
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Integer> temperature = deviceState.getTemperature(0);
+
+ // then:
+ assertFalse(temperature.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenVentilationStepIsEmpty() {
+ // given:
+ State state = mock(State.class);
+ when(state.getVentilationStep()).thenReturn(Optional.empty());
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<String> ventilationStep = deviceState.getVentilationStep();
+ Optional<Integer> ventilationStepRaw = deviceState.getVentilationStepRaw();
+
+ // then:
+ assertFalse(ventilationStep.isPresent());
+ assertFalse(ventilationStepRaw.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenTemperatureIndexIsOutOfRange() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ State state = mock(State.class);
+ when(state.getTemperature()).thenReturn(new LinkedList<>());
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Integer> temperature = deviceState.getTemperature(-1);
+
+ // then:
+ assertFalse(temperature.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenTemperatureValueLocalizedIsEmpty() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ Temperature temperatureMock = mock(Temperature.class);
+ when(temperatureMock.getValueLocalized()).thenReturn(Optional.empty());
+
+ State state = mock(State.class);
+ when(state.getTemperature()).thenReturn(Arrays.asList(temperatureMock));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Integer> temperature = deviceState.getTemperature(0);
+
+ // then:
+ assertFalse(temperature.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenTemperatureValueLocalizedIsValid() {
+ // given:
+ Temperature temperatureMock = mock(Temperature.class);
+ when(temperatureMock.getValueLocalized()).thenReturn(Optional.of(10));
+
+ State state = mock(State.class);
+ Device device = mock(Device.class);
+ Status status = mock(Status.class);
+ when(state.getTemperature()).thenReturn(Arrays.asList(temperatureMock));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Integer temperature = deviceState.getTemperature(0).get();
+
+ // then:
+ assertEquals(Integer.valueOf(10), temperature);
+ }
+
+ @Test
+ public void testReturnValuesWhenPlatStepIndexIsOutOfRange() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ State state = mock(State.class);
+ when(state.getPlateStep()).thenReturn(Collections.emptyList());
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ int plateStepCount = deviceState.getPlateStepCount().get();
+ Optional<String> plateStep = deviceState.getPlateStep(0);
+ Optional<Integer> plateStepRaw = deviceState.getPlateStepRaw(0);
+
+ // then:
+ assertEquals(0, plateStepCount);
+ assertFalse(plateStep.isPresent());
+ assertFalse(plateStepRaw.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenPlateStepValueIsEmpty() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ PlateStep plateStepMock = mock(PlateStep.class);
+ when(plateStepMock.getValueRaw()).thenReturn(Optional.empty());
+ when(plateStepMock.getValueLocalized()).thenReturn(Optional.empty());
+
+ State state = mock(State.class);
+ when(state.getPlateStep()).thenReturn(Collections.singletonList(plateStepMock));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ int plateStepCount = deviceState.getPlateStepCount().get();
+ Optional<String> plateStep = deviceState.getPlateStep(0);
+ Optional<Integer> plateStepRaw = deviceState.getPlateStepRaw(0);
+
+ // then:
+ assertEquals(1, plateStepCount);
+ assertFalse(plateStep.isPresent());
+ assertFalse(plateStepRaw.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenPlateStepValueIsValid() {
+ // given:
+ PlateStep plateStepMock = mock(PlateStep.class);
+ when(plateStepMock.getValueRaw()).thenReturn(Optional.of(2));
+ when(plateStepMock.getValueLocalized()).thenReturn(Optional.of("1."));
+
+ State state = mock(State.class);
+ Status status = mock(Status.class);
+ Device device = mock(Device.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+ when(state.getPlateStep()).thenReturn(Arrays.asList(plateStepMock));
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ int plateStepCount = deviceState.getPlateStepCount().get();
+ String plateStep = deviceState.getPlateStep(0).get();
+ int plateStepRaw = deviceState.getPlateStepRaw(0).get();
+
+ // then:
+ assertEquals(1, plateStepCount);
+ assertEquals("1.", plateStep);
+ assertEquals(2, plateStepRaw);
+ }
+
+ @Test
+ public void testReturnValuesWhenRemainingTimeIsEmpty() {
+ // given:
+ State state = mock(State.class);
+ when(state.getRemainingTime()).thenReturn(Optional.empty());
+ when(state.getElapsedTime()).thenReturn(Optional.of(Arrays.asList(0, 0)));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Integer> progress = deviceState.getProgress();
+
+ // then:
+ assertFalse(progress.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenRemainingTimeSizeIsNotTwo() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ State state = mock(State.class);
+ when(state.getRemainingTime()).thenReturn(Optional.of(Arrays.asList(2)));
+ when(state.getElapsedTime()).thenReturn(Optional.of(Arrays.asList(0, 0)));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Integer> progress = deviceState.getProgress();
+
+ // then:
+ assertFalse(progress.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenRemoteEnableIsEmpty() {
+ // given:
+ State state = mock(State.class);
+ when(state.getRemoteEnable()).thenReturn(Optional.empty());
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Boolean> remoteControlEnabled = deviceState.isRemoteControlEnabled();
+
+ // then:
+ assertFalse(remoteControlEnabled.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenFullRemoteControlIsEmpty() {
+ // given:
+ RemoteEnable remoteEnable = mock(RemoteEnable.class);
+ when(remoteEnable.getFullRemoteControl()).thenReturn(Optional.empty());
+
+ State state = mock(State.class);
+ when(state.getRemoteEnable()).thenReturn(Optional.of(remoteEnable));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Boolean> remoteControlEnabled = deviceState.isRemoteControlEnabled();
+
+ // then:
+ assertFalse(remoteControlEnabled.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenFullRemoteControlIsNotNull() {
+ // given:
+ RemoteEnable remoteEnable = mock(RemoteEnable.class);
+ when(remoteEnable.getFullRemoteControl()).thenReturn(Optional.of(true));
+
+ State state = mock(State.class);
+ when(state.getRemoteEnable()).thenReturn(Optional.of(remoteEnable));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Boolean remoteControlEnabled = deviceState.isRemoteControlEnabled().get();
+
+ // then:
+ assertTrue(remoteControlEnabled);
+ }
+
+ @Test
+ public void testReturnValuesWhenElapsedTimeIsEmpty() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ State state = mock(State.class);
+ when(state.getElapsedTime()).thenReturn(Optional.empty());
+ when(state.getRemainingTime()).thenReturn(Optional.of(Arrays.asList(0, 0)));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Integer> progress = deviceState.getProgress();
+
+ // then:
+ assertFalse(progress.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenElapsedTimeSizeIsNotTwo() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ State state = mock(State.class);
+ when(state.getElapsedTime()).thenReturn(Optional.of(Arrays.asList(0)));
+ when(state.getRemainingTime()).thenReturn(Optional.of(Arrays.asList(0, 0)));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Integer> progress = deviceState.getProgress();
+
+ // then:
+ assertFalse(progress.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenElapsedTimeAndRemainingTimeIsZero() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ State state = mock(State.class);
+ when(state.getElapsedTime()).thenReturn(Optional.of(Arrays.asList(0, 0)));
+ when(state.getRemainingTime()).thenReturn(Optional.of(Arrays.asList(0, 0)));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Integer> progress = deviceState.getProgress();
+
+ // then:
+ assertFalse(progress.isPresent());
+ }
+
+ @Test
+ public void whenElapsedTimeIsNotPresentThenEmptyIsReturned() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ State state = mock(State.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(state.getElapsedTime()).thenReturn(Optional.empty());
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Integer> elapsedTime = deviceState.getElapsedTime();
+
+ // then:
+ assertFalse(elapsedTime.isPresent());
+ }
+
+ @Test
+ public void whenElapsedTimeIsAnEmptyListThenEmptyIsReturned() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ State state = mock(State.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(state.getElapsedTime()).thenReturn(Optional.of(Collections.emptyList()));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Integer> elapsedTime = deviceState.getElapsedTime();
+
+ // then:
+ assertFalse(elapsedTime.isPresent());
+ }
+
+ @Test
+ public void whenElapsedTimeHasOnlyOneElementThenEmptyIsReturned() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ State state = mock(State.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(state.getElapsedTime()).thenReturn(Optional.of(Collections.singletonList(2)));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Integer> elapsedTime = deviceState.getElapsedTime();
+
+ // then:
+ assertFalse(elapsedTime.isPresent());
+ }
+
+ @Test
+ public void whenElapsedTimeHasThreeElementsThenEmptyIsReturned() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ State state = mock(State.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(state.getElapsedTime()).thenReturn(Optional.of(Arrays.asList(1, 2, 3)));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Integer> elapsedTime = deviceState.getElapsedTime();
+
+ // then:
+ assertFalse(elapsedTime.isPresent());
+ }
+
+ @Test
+ public void whenElapsedTimeHasTwoElementsThenTheTotalNumberOfSecondsIsReturned() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ State state = mock(State.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(state.getElapsedTime()).thenReturn(Optional.of(Arrays.asList(1, 2)));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Integer> elapsedTime = deviceState.getElapsedTime();
+
+ // then:
+ assertTrue(elapsedTime.isPresent());
+ assertEquals(Integer.valueOf((60 + 2) * 60), elapsedTime.get());
+ }
+
+ @Test
+ public void whenDeviceIsInOffStateThenElapsedTimeIsEmpty() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.OFF.getCode()));
+
+ State state = mock(State.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(state.getElapsedTime()).thenReturn(Optional.of(Arrays.asList(1, 2)));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Integer> elapsedTime = deviceState.getElapsedTime();
+
+ // then:
+ assertFalse(elapsedTime.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenProgressIs50Percent() {
+ // given:
+ State state = mock(State.class);
+ when(state.getElapsedTime()).thenReturn(Optional.of(Arrays.asList(0, 45)));
+ when(state.getRemainingTime()).thenReturn(Optional.of(Arrays.asList(0, 45)));
+
+ Device device = mock(Device.class);
+ Status status = mock(Status.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Integer progress = deviceState.getProgress().get();
+
+ // then:
+ assertEquals(Integer.valueOf(50), progress);
+ }
+
+ @Test
+ public void testReturnValuesWhenProgressIs25Percent() {
+ // given:
+ State state = mock(State.class);
+ when(state.getElapsedTime()).thenReturn(Optional.of(Arrays.asList(0, 15)));
+ when(state.getRemainingTime()).thenReturn(Optional.of(Arrays.asList(0, 45)));
+
+ Device device = mock(Device.class);
+ Status status = mock(Status.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Integer progress = deviceState.getProgress().get();
+
+ // then:
+ assertEquals(Integer.valueOf(25), progress);
+ }
+
+ @Test
+ public void testReturnValuesWhenProgressIs0Percent() {
+ // given:
+ State state = mock(State.class);
+ when(state.getElapsedTime()).thenReturn(Optional.of(Arrays.asList(0, 0)));
+ when(state.getRemainingTime()).thenReturn(Optional.of(Arrays.asList(0, 45)));
+
+ Device device = mock(Device.class);
+ Status status = mock(Status.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Integer progress = deviceState.getProgress().get();
+
+ // then:
+ assertEquals(Integer.valueOf(0), progress);
+ }
+
+ @Test
+ public void testReturnValuesWhenSignalDoorIsEmpty() {
+ // given:
+ State state = mock(State.class);
+ when(state.getSignalDoor()).thenReturn(Optional.empty());
+
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Boolean> doorState = deviceState.getDoorState();
+ Optional<Boolean> doorAlarm = deviceState.getDoorAlarm();
+
+ // then:
+ assertFalse(doorState.isPresent());
+ assertFalse(doorAlarm.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenSignalDoorIsTrue() {
+ // given:
+ State state = mock(State.class);
+ when(state.getSignalDoor()).thenReturn(Optional.of(true));
+
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Boolean> doorState = deviceState.getDoorState();
+
+ // then:
+ assertTrue(doorState.get());
+ }
+
+ @Test
+ public void testReturnValuesWhenSignalDoorIsFalse() {
+ // given:
+ State state = mock(State.class);
+ when(state.getSignalDoor()).thenReturn(Optional.of(false));
+
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Boolean> doorState = deviceState.getDoorState();
+
+ // then:
+ assertFalse(doorState.get());
+ }
+
+ @Test
+ public void testReturnValuesWhenSignalFailureIsEmpty() {
+ // given:
+ State state = mock(State.class);
+ when(state.getSignalDoor()).thenReturn(Optional.of(true));
+ when(state.getSignalFailure()).thenReturn(Optional.empty());
+
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Boolean> doorAlarm = deviceState.getDoorAlarm();
+
+ // then:
+ assertFalse(doorAlarm.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenDoorAlarmIsActive() {
+ // given:
+ State state = mock(State.class);
+ when(state.getSignalDoor()).thenReturn(Optional.of(true));
+ when(state.getSignalFailure()).thenReturn(Optional.of(true));
+
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Boolean> doorAlarm = deviceState.getDoorAlarm();
+
+ // then:
+ assertTrue(doorAlarm.get());
+ }
+
+ @Test
+ public void testReturnValuesWhenDoorAlarmIsNotActiveBecauseOfNoDoorSignal() {
+ // given:
+ State state = mock(State.class);
+ when(state.getSignalDoor()).thenReturn(Optional.of(false));
+ when(state.getSignalFailure()).thenReturn(Optional.of(true));
+
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Boolean> doorAlarm = deviceState.getDoorAlarm();
+
+ // then:
+ assertFalse(doorAlarm.get());
+ }
+
+ @Test
+ public void testReturnValuesWhenDoorAlarmIsNotActiveBecauseOfNoFailureSignal() {
+ // given:
+ State state = mock(State.class);
+ when(state.getSignalDoor()).thenReturn(Optional.of(true));
+ when(state.getSignalFailure()).thenReturn(Optional.of(false));
+
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Boolean> doorAlarm = deviceState.getDoorAlarm();
+
+ // then:
+ assertFalse(doorAlarm.get());
+ }
+
+ @Test
+ public void testReturnValuesWhenIdentIsEmpty() {
+ // given:
+ Device device = mock(Device.class);
+ when(device.getIdent()).thenReturn(Optional.empty());
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<String> type = deviceState.getType();
+ DeviceType rawType = deviceState.getRawType();
+ Optional<String> deviceName = deviceState.getDeviceName();
+ Optional<String> fabNumber = deviceState.getFabNumber();
+ Optional<String> techType = deviceState.getTechType();
+
+ // then:
+ assertFalse(type.isPresent());
+ assertEquals(DeviceType.UNKNOWN, rawType);
+ assertFalse(deviceName.isPresent());
+ assertFalse(fabNumber.isPresent());
+ assertFalse(techType.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenTypeIsEmpty() {
+ // given:
+ Ident ident = mock(Ident.class);
+ when(ident.getType()).thenReturn(Optional.empty());
+
+ Device device = mock(Device.class);
+ when(device.getIdent()).thenReturn(Optional.of(ident));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<String> type = deviceState.getType();
+ DeviceType rawType = deviceState.getRawType();
+
+ // then:
+ assertFalse(type.isPresent());
+ assertEquals(DeviceType.UNKNOWN, rawType);
+ }
+
+ @Test
+ public void testReturnValuesWhenTypeValueLocalizedIsEmpty() {
+ // given:
+ Type typeMock = mock(Type.class);
+ when(typeMock.getValueLocalized()).thenReturn(Optional.empty());
+
+ Ident ident = mock(Ident.class);
+ when(ident.getType()).thenReturn(Optional.of(typeMock));
+
+ Device device = mock(Device.class);
+ when(device.getIdent()).thenReturn(Optional.of(ident));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<String> type = deviceState.getType();
+
+ // then:
+ assertFalse(type.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenTypeValueLocalizedIsNotNull() {
+ // given:
+ Type typeMock = mock(Type.class);
+ when(typeMock.getValueLocalized()).thenReturn(Optional.of("Hood"));
+
+ Ident ident = mock(Ident.class);
+ when(ident.getType()).thenReturn(Optional.of(typeMock));
+
+ Device device = mock(Device.class);
+ when(device.getIdent()).thenReturn(Optional.of(ident));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ String type = deviceState.getType().get();
+
+ // then:
+ assertEquals("Hood", type);
+ }
+
+ @Test
+ public void testReturnValuesWhenTypeValueRawIsNotNull() {
+ // given:
+ Type typeMock = mock(Type.class);
+ when(typeMock.getValueRaw()).thenReturn(DeviceType.COFFEE_SYSTEM);
+
+ Ident ident = mock(Ident.class);
+ when(ident.getType()).thenReturn(Optional.of(typeMock));
+
+ Device device = mock(Device.class);
+ when(device.getIdent()).thenReturn(Optional.of(ident));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ DeviceType rawType = deviceState.getRawType();
+
+ // then:
+ assertEquals(DeviceType.COFFEE_SYSTEM, rawType);
+ }
+
+ @Test
+ public void testReturnValuesWhenDeviceNameIsEmpty() {
+ // given:
+ Ident ident = mock(Ident.class);
+ when(ident.getDeviceName()).thenReturn(Optional.empty());
+
+ Device device = mock(Device.class);
+ when(device.getIdent()).thenReturn(Optional.of(ident));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<String> deviceName = deviceState.getDeviceName();
+
+ // then:
+ assertFalse(deviceName.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenDeviceNameIsEmptyString() {
+ // given:
+ Ident ident = mock(Ident.class);
+ when(ident.getDeviceName()).thenReturn(Optional.of(""));
+
+ Device device = mock(Device.class);
+ when(device.getIdent()).thenReturn(Optional.of(ident));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<String> deviceName = deviceState.getDeviceName();
+
+ // then:
+ assertFalse(deviceName.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenDeviceNameIsValid() {
+ // given:
+ Ident ident = mock(Ident.class);
+ when(ident.getDeviceName()).thenReturn(Optional.of("MyWashingMachine"));
+
+ Device device = mock(Device.class);
+ when(device.getIdent()).thenReturn(Optional.of(ident));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<String> deviceName = deviceState.getDeviceName();
+
+ // then:
+ assertEquals(Optional.of("MyWashingMachine"), deviceName);
+ }
+
+ @Test
+ public void testReturnValuesWhenFabNumberIsNotNull() {
+ // given:
+ DeviceIdentLabel deviceIdentLabel = mock(DeviceIdentLabel.class);
+ when(deviceIdentLabel.getFabNumber()).thenReturn(Optional.of("000061431659"));
+
+ Ident ident = mock(Ident.class);
+ when(ident.getDeviceIdentLabel()).thenReturn(Optional.of(deviceIdentLabel));
+
+ Device device = mock(Device.class);
+ when(device.getIdent()).thenReturn(Optional.of(ident));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ String fabNumber = deviceState.getFabNumber().get();
+
+ // then:
+ assertEquals("000061431659", fabNumber);
+ }
+
+ @Test
+ public void testReturnValuesWhenTechTypeIsNotNull() {
+ // given:
+ DeviceIdentLabel deviceIdentLabel = mock(DeviceIdentLabel.class);
+ when(deviceIdentLabel.getTechType()).thenReturn(Optional.of("XKM3100WEC"));
+
+ Ident ident = mock(Ident.class);
+ when(ident.getDeviceIdentLabel()).thenReturn(Optional.of(deviceIdentLabel));
+
+ Device device = mock(Device.class);
+ when(device.getIdent()).thenReturn(Optional.of(ident));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ String techType = deviceState.getTechType().get();
+
+ // then:
+ assertEquals("XKM3100WEC", techType);
+ }
+
+ @Test
+ public void whenDeviceIsInFailureStateThenItHasAnError() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.FAILURE.getCode()));
+
+ State state = mock(State.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ boolean hasError = deviceState.hasError();
+
+ // then:
+ assertTrue(hasError);
+ }
+
+ @Test
+ public void whenDeviceIsInRunningStateAndDoesNotSignalAFailureThenItHasNoError() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+
+ State state = mock(State.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ boolean hasError = deviceState.hasError();
+
+ // then:
+ assertFalse(hasError);
+ }
+
+ @Test
+ public void whenDeviceSignalsAFailureThenItHasAnError() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+
+ State state = mock(State.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(state.getSignalFailure()).thenReturn(Optional.of(true));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ boolean hasError = deviceState.hasError();
+
+ // then:
+ assertTrue(hasError);
+ }
+
+ @Test
+ public void testReturnValuesForHasInfoWhenSignalInfoIsEmpty() {
+ // given:
+ State state = mock(State.class);
+ when(state.getSignalInfo()).thenReturn(Optional.empty());
+
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ boolean hasInfo = deviceState.hasInfo();
+
+ // then:
+ assertFalse(hasInfo);
+ }
+
+ @Test
+ public void whenDeviceSignalsAnInfoThenItHasAnInfo() {
+ // given:
+ State state = mock(State.class);
+ when(state.getSignalInfo()).thenReturn(Optional.of(true));
+
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ boolean hasInfo = deviceState.hasInfo();
+
+ // then:
+ assertTrue(hasInfo);
+ }
+
+ @Test
+ public void whenDeviceSignalsNoInfoThenItHasNoInfo() {
+ // given:
+ State state = mock(State.class);
+ when(state.getSignalInfo()).thenReturn(Optional.of(false));
+
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ boolean hasInfo = deviceState.hasInfo();
+
+ // then:
+ assertFalse(hasInfo);
+ }
+
+ @Test
+ public void testReturnValuesForVentilationStep() {
+ // given:
+ VentilationStep ventilationStepMock = mock(VentilationStep.class);
+ when(ventilationStepMock.getValueLocalized()).thenReturn(Optional.of("Step 1"));
+ when(ventilationStepMock.getValueRaw()).thenReturn(Optional.of(1));
+
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ State state = mock(State.class);
+ when(state.getVentilationStep()).thenReturn(Optional.of(ventilationStepMock));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ String ventilationStep = deviceState.getVentilationStep().get();
+ int ventilationStepRaw = deviceState.getVentilationStepRaw().get();
+
+ // then:
+ assertEquals("Step 1", ventilationStep);
+ assertEquals(1, ventilationStepRaw);
+ }
+
+ @Test
+ public void testProgramPhaseWhenDeviceIsInOffState() {
+ // given:
+ ProgramPhase programPhase = mock(ProgramPhase.class);
+ when(programPhase.getValueLocalized()).thenReturn(Optional.of("Washing"));
+ when(programPhase.getValueRaw()).thenReturn(Optional.of(3));
+
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.OFF.getCode()));
+
+ State state = mock(State.class);
+ when(state.getProgramPhase()).thenReturn(Optional.of(programPhase));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<String> phase = deviceState.getProgramPhase();
+ Optional<Integer> phaseRaw = deviceState.getProgramPhaseRaw();
+
+ // then:
+ assertFalse(phase.isPresent());
+ assertFalse(phaseRaw.isPresent());
+ }
+
+ @Test
+ public void testDryingTargetWhenDeviceIsInOffState() {
+ // given:
+ DryingStep dryingStep = mock(DryingStep.class);
+ when(dryingStep.getValueLocalized()).thenReturn(Optional.of("Schranktrocken"));
+ when(dryingStep.getValueRaw()).thenReturn(Optional.of(3));
+
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.OFF.getCode()));
+
+ State state = mock(State.class);
+ when(state.getDryingStep()).thenReturn(Optional.of(dryingStep));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<String> dryingTarget = deviceState.getDryingTarget();
+ Optional<Integer> dryingTargetRaw = deviceState.getDryingTargetRaw();
+
+ // then:
+ assertFalse(dryingTarget.isPresent());
+ assertFalse(dryingTargetRaw.isPresent());
+ }
+
+ @Test
+ public void testVentilationStepWhenDeviceIsInOffState() {
+ // given:
+ VentilationStep ventilationStep = mock(VentilationStep.class);
+ when(ventilationStep.getValueLocalized()).thenReturn(Optional.of("Stufe 1"));
+ when(ventilationStep.getValueRaw()).thenReturn(Optional.of(1));
+
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.OFF.getCode()));
+
+ State state = mock(State.class);
+ when(state.getVentilationStep()).thenReturn(Optional.of(ventilationStep));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<String> step = deviceState.getVentilationStep();
+ Optional<Integer> stepRaw = deviceState.getVentilationStepRaw();
+
+ // then:
+ assertFalse(step.isPresent());
+ assertFalse(stepRaw.isPresent());
+ }
+
+ @Test
+ public void testReturnValuesWhenDeviceIsInOffState() {
+ // given:
+ Device device = mock(Device.class);
+ State state = mock(State.class);
+ Status status = mock(Status.class);
+
+ when(device.getState()).thenReturn(Optional.of(state));
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.OFF.getCode()));
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // Test SelectedProgram:
+ ProgramId programId = mock(ProgramId.class);
+ when(state.getProgramId()).thenReturn(Optional.of(programId));
+ when(programId.getValueLocalized()).thenReturn(Optional.of("Washing"));
+ // when:
+ Optional<String> selectedProgram = deviceState.getSelectedProgram();
+ // then:
+ assertFalse(selectedProgram.isPresent());
+
+ // Test TargetTemperature:
+ Temperature targetTemperatureMock = mock(Temperature.class);
+ when(state.getTargetTemperature()).thenReturn(Collections.singletonList(targetTemperatureMock));
+ when(targetTemperatureMock.getValueLocalized()).thenReturn(Optional.of(200));
+ // when:
+ Optional<Integer> targetTemperature = deviceState.getTargetTemperature(0);
+ // then:
+ assertFalse(targetTemperature.isPresent());
+
+ // Test Temperature:
+ Temperature temperature = mock(Temperature.class);
+ when(state.getTemperature()).thenReturn(Collections.singletonList(temperature));
+ when(temperature.getValueLocalized()).thenReturn(Optional.of(200));
+ // when:
+ Optional<Integer> t = deviceState.getTemperature(0);
+ // then:
+ assertFalse(t.isPresent());
+
+ // Test Progress:
+ when(state.getElapsedTime()).thenReturn(Optional.of(Arrays.asList(0, 5)));
+ when(state.getRemainingTime()).thenReturn(Optional.of(Arrays.asList(1, 5)));
+ // when:
+ Optional<Integer> progress = deviceState.getProgress();
+ // then:
+ assertFalse(progress.isPresent());
+ }
+
+ @Test
+ public void testWhenDeviceIsInOffStateThenGetSpinningSpeedReturnsNull() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.OFF.getCode()));
+
+ SpinningSpeed spinningSpeed = mock(SpinningSpeed.class);
+ when(spinningSpeed.getValueRaw()).thenReturn(Optional.of(800));
+ when(spinningSpeed.getValueLocalized()).thenReturn(Optional.of("800"));
+ when(spinningSpeed.getUnit()).thenReturn(Optional.of("rpm"));
+
+ State state = mock(State.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(state.getSpinningSpeed()).thenReturn(Optional.of(spinningSpeed));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<String> speed = deviceState.getSpinningSpeed();
+ Optional<Integer> speedRaw = deviceState.getSpinningSpeedRaw();
+
+ // then:
+ assertFalse(speed.isPresent());
+ assertFalse(speedRaw.isPresent());
+ }
+
+ @Test
+ public void testGetSpinningSpeedReturnsNullWhenSpinningSpeedIsEmpty() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ State state = mock(State.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(state.getSpinningSpeed()).thenReturn(Optional.empty());
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<String> spinningSpeed = deviceState.getSpinningSpeed();
+ Optional<Integer> spinningSpeedRaw = deviceState.getSpinningSpeedRaw();
+
+ // then:
+ assertFalse(spinningSpeed.isPresent());
+ assertFalse(spinningSpeedRaw.isPresent());
+ }
+
+ @Test
+ public void testGetSpinningSpeedReturnsNullWhenSpinningSpeedRawValueIsEmpty() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ SpinningSpeed spinningSpeedMock = mock(SpinningSpeed.class);
+ when(spinningSpeedMock.getValueRaw()).thenReturn(Optional.empty());
+ when(spinningSpeedMock.getValueLocalized()).thenReturn(Optional.of("1200"));
+
+ State state = mock(State.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(state.getSpinningSpeed()).thenReturn(Optional.of(spinningSpeedMock));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<String> spinningSpeed = deviceState.getSpinningSpeed();
+ Optional<Integer> spinningSpeedRaw = deviceState.getSpinningSpeedRaw();
+
+ // then:
+ assertFalse(spinningSpeed.isPresent());
+ assertFalse(spinningSpeedRaw.isPresent());
+ }
+
+ @Test
+ public void testGetSpinningSpeedReturnsValidValueWhenSpinningSpeedRawValueIsNotNull() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ SpinningSpeed spinningSpeedMock = mock(SpinningSpeed.class);
+ when(spinningSpeedMock.getValueRaw()).thenReturn(Optional.of(1200));
+ when(spinningSpeedMock.getValueLocalized()).thenReturn(Optional.of("1200"));
+
+ State state = mock(State.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(state.getSpinningSpeed()).thenReturn(Optional.of(spinningSpeedMock));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ String spinningSpeed = deviceState.getSpinningSpeed().get();
+ int spinningSpeedRaw = deviceState.getSpinningSpeedRaw().get();
+
+ // then:
+ assertEquals("1200", spinningSpeed);
+ assertEquals(1200, spinningSpeedRaw);
+ }
+
+ @Test
+ public void testGetLightStateWhenDeviceIsOff() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.OFF.getCode()));
+
+ State state = mock(State.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Boolean> lightState = deviceState.getLightState();
+
+ // then:
+ assertFalse(lightState.isPresent());
+ }
+
+ @Test
+ public void testGetLightStateWhenLightIsUnknown() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ State state = mock(State.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(state.getLight()).thenReturn(Light.UNKNOWN);
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Boolean> lightState = deviceState.getLightState();
+
+ // then:
+ assertFalse(lightState.isPresent());
+ }
+
+ @Test
+ public void testGetLightStateWhenLightIsEnabled() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ State state = mock(State.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(state.getLight()).thenReturn(Light.ENABLE);
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Boolean lightState = deviceState.getLightState().get();
+
+ // then:
+ assertEquals(Boolean.valueOf(true), lightState);
+ }
+
+ @Test
+ public void testGetLightStateWhenLightIsDisabled() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ State state = mock(State.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(state.getLight()).thenReturn(Light.DISABLE);
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Boolean lightState = deviceState.getLightState().get();
+
+ // then:
+ assertEquals(Boolean.valueOf(false), lightState);
+ }
+
+ @Test
+ public void testGetLightStateWhenLightIsNotSupported() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ State state = mock(State.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(state.getLight()).thenReturn(Light.NOT_SUPPORTED);
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Optional<Boolean> lightState = deviceState.getLightState();
+
+ // then:
+ assertFalse(lightState.isPresent());
+ }
+
+ @Test
+ public void testGetBatteryLevel() {
+ // given:
+ Status status = mock(Status.class);
+ when(status.getValueRaw()).thenReturn(Optional.of(StateType.ON.getCode()));
+
+ State state = mock(State.class);
+ when(state.getStatus()).thenReturn(Optional.of(status));
+ when(state.getBatteryLevel()).thenReturn(Optional.of(4));
+
+ Device device = mock(Device.class);
+ when(device.getState()).thenReturn(Optional.of(state));
+
+ DeviceState deviceState = new DeviceState(DEVICE_IDENTIFIER, device);
+
+ // when:
+ Integer batteryLevel = deviceState.getBatteryLevel().get();
+
+ // then:
+ assertEquals(Integer.valueOf(4), batteryLevel);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class TransitionStateTest {
+ private final DeviceState historic = mock(DeviceState.class);
+ private final DeviceState previous = mock(DeviceState.class);
+ private final DeviceState next = mock(DeviceState.class);
+
+ @Test
+ public void testHasFinishedChangedReturnsTrueWhenPreviousStateIsNull() {
+ // given:
+ when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+
+ TransitionState transitionState = new TransitionState(null, next);
+
+ // when:
+ boolean hasFinishedChanged = transitionState.hasFinishedChanged();
+
+ // then:
+ assertTrue(hasFinishedChanged);
+ }
+
+ @Test
+ public void testHasFinishedChangedReturnsTrueWhenPreviousStateIsUnknown() {
+ // given:
+ when(previous.getStateType()).thenReturn(Optional.empty());
+ when(previous.isInState(any())).thenCallRealMethod();
+ when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(next.isInState(any())).thenCallRealMethod();
+
+ TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+ // when:
+ boolean hasFinishedChanged = transitionState.hasFinishedChanged();
+
+ // then:
+ assertTrue(hasFinishedChanged);
+ }
+
+ @Test
+ public void testHasFinishedChangedReturnsFalseWhenNoStateTransitionOccurred() {
+ // given:
+ when(previous.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+
+ TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+ // when:
+ boolean hasFinishedChanged = transitionState.hasFinishedChanged();
+
+ // then:
+ assertFalse(hasFinishedChanged);
+ }
+
+ @Test
+ public void testHasFinishedChangedReturnsTrueWhenStateChangedFromRunningToEndProgrammed() {
+ // given:
+ when(previous.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(previous.isInState(any())).thenCallRealMethod();
+ when(next.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+ when(next.isInState(any())).thenCallRealMethod();
+
+ TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+ // when:
+ boolean hasFinishedChanged = transitionState.hasFinishedChanged();
+
+ // then:
+ assertTrue(hasFinishedChanged);
+ }
+
+ @Test
+ public void testHasFinishedChangedReturnsTrueWhenStateChangedFromRunningToProgrammed() {
+ // given:
+ when(previous.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(previous.isInState(any())).thenCallRealMethod();
+ when(next.getStateType()).thenReturn(Optional.of(StateType.PROGRAMMED));
+ when(next.isInState(any())).thenCallRealMethod();
+
+ TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+ // when:
+ boolean hasFinishedChanged = transitionState.hasFinishedChanged();
+
+ // then:
+ assertTrue(hasFinishedChanged);
+ }
+
+ @Test
+ public void testHasFinishedChangedReturnsFalseWhenStateChangedFromRunningToPause() {
+ // given:
+ when(previous.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(next.getStateType()).thenReturn(Optional.of(StateType.PAUSE));
+
+ TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+ // when:
+ boolean hasFinishedChanged = transitionState.hasFinishedChanged();
+
+ // then:
+ assertFalse(hasFinishedChanged);
+ }
+
+ @Test
+ public void testHasFinishedChangedReturnsTrueWhenStateChangedFromProgrammedWaitingToStartToRunning() {
+ // given:
+ when(previous.getStateType()).thenReturn(Optional.of(StateType.PROGRAMMED_WAITING_TO_START));
+ when(previous.isInState(any())).thenCallRealMethod();
+ when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(next.isInState(any())).thenCallRealMethod();
+
+ TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+ // when:
+ boolean hasFinishedChanged = transitionState.hasFinishedChanged();
+
+ // then:
+ assertTrue(hasFinishedChanged);
+ }
+
+ @Test
+ public void testHasFinishedChangedReturnsFalseWhenStateRemainsProgrammedWaitingToStart() {
+ // given:
+ when(previous.getStateType()).thenReturn(Optional.of(StateType.PROGRAMMED_WAITING_TO_START));
+ when(next.getStateType()).thenReturn(Optional.of(StateType.PROGRAMMED_WAITING_TO_START));
+
+ TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+ // when:
+ boolean hasFinishedChanged = transitionState.hasFinishedChanged();
+
+ // then:
+ assertFalse(hasFinishedChanged);
+ }
+
+ @Test
+ public void testHasFinishedChangedReturnsFalseWhenStateChangedFromPauseToRunning() {
+ // given:
+ when(previous.getStateType()).thenReturn(Optional.of(StateType.PAUSE));
+ when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+
+ TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+ // when:
+ boolean hasFinishedChanged = transitionState.hasFinishedChanged();
+
+ // then:
+ assertFalse(hasFinishedChanged);
+ }
+
+ @Test
+ public void testHasFinishedChangedReturnsTrueWhenStateChangedFromEndProgrammedToOff() {
+ // given:
+ when(previous.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+ when(previous.isInState(any())).thenCallRealMethod();
+ when(next.getStateType()).thenReturn(Optional.of(StateType.OFF));
+ when(next.isInState(any())).thenCallRealMethod();
+
+ TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+ // when:
+ boolean hasFinishedChanged = transitionState.hasFinishedChanged();
+
+ // then:
+ assertTrue(hasFinishedChanged);
+ }
+
+ @Test
+ public void testHasFinishedChangedReturnsFalseWhenStateChangedFromRunningToFailure() {
+ // given:
+ when(previous.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(next.getStateType()).thenReturn(Optional.of(StateType.FAILURE));
+
+ TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+ // when:
+ boolean hasFinishedChanged = transitionState.hasFinishedChanged();
+
+ // then:
+ assertFalse(hasFinishedChanged);
+ }
+
+ @Test
+ public void testHasFinishedChangedReturnsFalseWhenStateChangedFromPauseToFailure() {
+ // given:
+ when(previous.getStateType()).thenReturn(Optional.of(StateType.PAUSE));
+ when(next.getStateType()).thenReturn(Optional.of(StateType.FAILURE));
+
+ TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+ // when:
+ boolean hasFinishedChanged = transitionState.hasFinishedChanged();
+
+ // then:
+ assertFalse(hasFinishedChanged);
+ }
+
+ @Test
+ public void testIsFinishedReturnsTrueWhenStateChangedFromRunningToEndProgrammed() {
+ // given:
+ when(previous.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(next.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+
+ TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+ // when:
+ Boolean isFinished = transitionState.isFinished().get();
+
+ // then:
+ assertTrue(isFinished);
+ }
+
+ @Test
+ public void testIsFinishedReturnsTrueWhenStateChangedFromRunningToProgrammed() {
+ // given:
+ when(previous.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(next.getStateType()).thenReturn(Optional.of(StateType.PROGRAMMED));
+
+ TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+ // when:
+ Boolean isFinished = transitionState.isFinished().get();
+
+ // then:
+ assertTrue(isFinished);
+ }
+
+ @Test
+ public void testIsFinishedReturnsFalseWhenStateChangedFromProgrammedWaitingToStartToRunning() {
+ // given:
+ when(previous.getStateType()).thenReturn(Optional.of(StateType.PROGRAMMED_WAITING_TO_START));
+ when(previous.isInState(any())).thenCallRealMethod();
+ when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(next.isInState(any())).thenCallRealMethod();
+
+ TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+ // when:
+ Boolean isFinished = transitionState.isFinished().get();
+
+ // then:
+ assertFalse(isFinished);
+ }
+
+ @Test
+ public void testIsFinishedReturnsFalseWhenStateChangedFromRunningToFailure() {
+ // given:
+ when(previous.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(previous.isInState(any())).thenCallRealMethod();
+ when(next.getStateType()).thenReturn(Optional.of(StateType.FAILURE));
+ when(next.isInState(any())).thenCallRealMethod();
+
+ TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+ // when:
+ Boolean isFinished = transitionState.isFinished().get();
+
+ // then:
+ assertFalse(isFinished);
+ }
+
+ @Test
+ public void testIsFinishedReturnsFalseWhenStateChangedFromPauseToFailure() {
+ // given:
+ when(previous.getStateType()).thenReturn(Optional.of(StateType.PAUSE));
+ when(previous.isInState(any())).thenCallRealMethod();
+ when(next.getStateType()).thenReturn(Optional.of(StateType.FAILURE));
+ when(next.isInState(any())).thenCallRealMethod();
+
+ TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+ // when:
+ Boolean isFinished = transitionState.isFinished().get();
+
+ // then:
+ assertFalse(isFinished);
+ }
+
+ @Test
+ public void testIsFinishedReturnsTrueWhenStateChangedFromEndProgrammedToOff() {
+ // given:
+ when(previous.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+ when(previous.isInState(any())).thenCallRealMethod();
+ when(next.getStateType()).thenReturn(Optional.of(StateType.OFF));
+ when(next.isInState(any())).thenCallRealMethod();
+
+ TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+ // when:
+ Boolean isFinished = transitionState.isFinished().get();
+
+ // then:
+ assertFalse(isFinished);
+ }
+
+ @Test
+ public void testIsFinishedReturnsNullWhenPreviousStateIsNull() {
+ // given:
+ when(next.getStateType()).thenReturn(Optional.of(StateType.IDLE));
+
+ TransitionState transitionState = new TransitionState(null, next);
+
+ // when:
+ Optional<Boolean> isFinished = transitionState.isFinished();
+
+ // then:
+ assertFalse(isFinished.isPresent());
+ }
+
+ @Test
+ public void testIsFinishedReturnsNullWhenPreviousStateIsUnknown() {
+ // given:
+ when(previous.getStateType()).thenReturn(Optional.empty());
+ when(next.getStateType()).thenReturn(Optional.of(StateType.IDLE));
+
+ TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+ // when:
+ Optional<Boolean> isFinished = transitionState.isFinished();
+
+ // then:
+ assertFalse(isFinished.isPresent());
+ }
+
+ @Test
+ public void testProgramStartedWithZeroRemainingTimeShowsNoRemainingTimeAndProgress() {
+ // given:
+ when(previous.isInState(any())).thenCallRealMethod();
+ when(previous.getStateType()).thenReturn(Optional.of(StateType.PROGRAMMED_WAITING_TO_START));
+
+ when(next.isInState(any())).thenCallRealMethod();
+ when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(next.getRemainingTime()).thenReturn(Optional.of(0));
+ when(next.getProgress()).thenReturn(Optional.of(100));
+
+ TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+ // when:
+ Optional<Integer> remainingTime = transitionState.getRemainingTime();
+ Optional<Integer> progress = transitionState.getProgress();
+
+ // then:
+ assertFalse(remainingTime.isPresent());
+ assertFalse(progress.isPresent());
+ }
+
+ @Test
+ public void testProgramStartetdWithRemainingTimeShowsRemainingTimeAndProgress() {
+ // given:
+ when(previous.isInState(any())).thenCallRealMethod();
+ when(previous.getStateType()).thenReturn(Optional.of(StateType.PROGRAMMED_WAITING_TO_START));
+
+ when(next.isInState(any())).thenCallRealMethod();
+ when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(next.getRemainingTime()).thenReturn(Optional.of(2));
+ when(next.getProgress()).thenReturn(Optional.of(50));
+
+ TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+ // when:
+ int remainingTime = transitionState.getRemainingTime().get();
+ int progress = transitionState.getProgress().get();
+
+ // then:
+ assertEquals(2, remainingTime);
+ assertEquals(50, progress);
+ }
+
+ @Test
+ public void testProgramCountingDownRemainingTimeToZeroShowsRemainingTimeAndProgress() {
+ // given:
+ when(previous.isInState(any())).thenCallRealMethod();
+ when(previous.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(previous.getRemainingTime()).thenReturn(Optional.of(1));
+
+ when(next.isInState(any())).thenCallRealMethod();
+ when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(next.getRemainingTime()).thenReturn(Optional.of(0));
+ when(next.getProgress()).thenReturn(Optional.of(100));
+
+ TransitionState transitionState = new TransitionState(new TransitionState(null, previous), next);
+
+ // when:
+ int remainingTime = transitionState.getRemainingTime().get();
+ int progress = transitionState.getProgress().get();
+
+ // then:
+ assertEquals(0, remainingTime);
+ assertEquals(100, progress);
+ }
+
+ @Test
+ public void testDevicePairedWhileRunningWithZeroRemainingTimeShowsNoRemainingTimeAndProgress() {
+ // given:
+ when(next.isInState(any())).thenCallRealMethod();
+ when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(next.getRemainingTime()).thenReturn(Optional.of(0));
+ when(next.getProgress()).thenReturn(Optional.of(100));
+
+ TransitionState transitionState = new TransitionState(null, next);
+
+ // when:
+ Optional<Integer> remainingTime = transitionState.getRemainingTime();
+ Optional<Integer> progress = transitionState.getProgress();
+
+ // then:
+ assertFalse(remainingTime.isPresent());
+ assertFalse(progress.isPresent());
+ }
+
+ @Test
+ public void testDevicePairedWhileRunningWithRemainingTimeShowsRemainingTimeAndProgress() {
+ // given:
+ when(next.isInState(any())).thenCallRealMethod();
+ when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(next.getRemainingTime()).thenReturn(Optional.of(3));
+ when(next.getProgress()).thenReturn(Optional.of(80));
+
+ TransitionState transitionState = new TransitionState(null, next);
+
+ // when:
+ int remainingTime = transitionState.getRemainingTime().get();
+ int progress = transitionState.getProgress().get();
+
+ // then:
+ assertEquals(3, remainingTime);
+ assertEquals(80, progress);
+ }
+
+ @Test
+ public void testWhenNoRemainingTimeIsSetWhileProgramIsRunningThenNoRemainingTimeAndProgressIsShown() {
+ // given:
+ when(historic.isInState(any())).thenCallRealMethod();
+ when(historic.getStateType()).thenReturn(Optional.of(StateType.PROGRAMMED_WAITING_TO_START));
+
+ when(previous.isInState(any())).thenCallRealMethod();
+ when(previous.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(previous.getRemainingTime()).thenReturn(Optional.of(0));
+
+ when(next.isInState(any())).thenCallRealMethod();
+ when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(next.getRemainingTime()).thenReturn(Optional.of(0));
+ when(next.getProgress()).thenReturn(Optional.of(100));
+
+ TransitionState transitionState = new TransitionState(
+ new TransitionState(new TransitionState(null, historic), previous), next);
+
+ // when:
+ Optional<Integer> remainingTime = transitionState.getRemainingTime();
+ Optional<Integer> progress = transitionState.getProgress();
+
+ // then:
+ assertFalse(remainingTime.isPresent());
+ assertFalse(progress.isPresent());
+ }
+
+ @Test
+ public void testRemainingTimeIsSetWhileRunningShowsRemainingTimeAndProgress() {
+ // given:
+ when(historic.isInState(any())).thenCallRealMethod();
+ when(historic.getStateType()).thenReturn(Optional.of(StateType.PROGRAMMED_WAITING_TO_START));
+
+ when(previous.isInState(any())).thenCallRealMethod();
+ when(previous.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(previous.getRemainingTime()).thenReturn(Optional.of(0));
+
+ when(next.isInState(any())).thenCallRealMethod();
+ when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(next.getRemainingTime()).thenReturn(Optional.of(100));
+ when(next.getProgress()).thenReturn(Optional.of(10));
+
+ TransitionState transitionState = new TransitionState(
+ new TransitionState(new TransitionState(null, historic), previous), next);
+
+ // when:
+ int remainingTime = transitionState.getRemainingTime().get();
+ int progress = transitionState.getProgress().get();
+
+ // then:
+ assertEquals(100, remainingTime);
+ assertEquals(10, progress);
+ }
+
+ @Test
+ public void testPreviousProgramDoesNotAffectHandlingOfRemainingTimeAndProgressForNextProgramCase1() {
+ // given:
+ DeviceState beforeHistoric = mock(DeviceState.class);
+ when(beforeHistoric.isInState(any())).thenCallRealMethod();
+ when(beforeHistoric.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(beforeHistoric.getRemainingTime()).thenReturn(Optional.of(1));
+
+ when(historic.isInState(any())).thenCallRealMethod();
+ when(historic.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+ when(historic.getRemainingTime()).thenReturn(Optional.of(0));
+
+ when(previous.isInState(any())).thenCallRealMethod();
+ when(previous.getStateType()).thenReturn(Optional.of(StateType.PROGRAMMED_WAITING_TO_START));
+ when(previous.getRemainingTime()).thenReturn(Optional.of(0));
+
+ when(next.isInState(any())).thenCallRealMethod();
+ when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(next.getRemainingTime()).thenReturn(Optional.of(0));
+ when(next.getProgress()).thenReturn(Optional.of(100));
+
+ TransitionState transitionState = new TransitionState(
+ new TransitionState(new TransitionState(new TransitionState(null, beforeHistoric), historic), previous),
+ next);
+
+ // when:
+ Optional<Integer> remainingTime = transitionState.getRemainingTime();
+ Optional<Integer> progress = transitionState.getProgress();
+
+ // then:
+ assertFalse(remainingTime.isPresent());
+ assertFalse(progress.isPresent());
+ }
+
+ @Test
+ public void testPreviousProgramDoesNotAffectHandlingOfRemainingTimeAndProgressForNextProgramCase2() {
+ // given:
+ DeviceState beforeHistoric = mock(DeviceState.class);
+ when(beforeHistoric.isInState(any())).thenCallRealMethod();
+ when(beforeHistoric.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(beforeHistoric.getRemainingTime()).thenReturn(Optional.of(1));
+
+ when(historic.isInState(any())).thenCallRealMethod();
+ when(historic.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+ when(historic.getRemainingTime()).thenReturn(Optional.of(0));
+
+ when(previous.isInState(any())).thenCallRealMethod();
+ when(previous.getStateType()).thenReturn(Optional.of(StateType.PROGRAMMED_WAITING_TO_START));
+ when(previous.getRemainingTime()).thenReturn(Optional.of(0));
+
+ when(next.isInState(any())).thenCallRealMethod();
+ when(next.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(next.getRemainingTime()).thenReturn(Optional.of(10));
+ when(next.getProgress()).thenReturn(Optional.of(60));
+
+ TransitionState transitionState = new TransitionState(
+ new TransitionState(new TransitionState(new TransitionState(null, beforeHistoric), historic), previous),
+ next);
+
+ // when:
+ int remainingTime = transitionState.getRemainingTime().get();
+ int progress = transitionState.getProgress().get();
+
+ // then:
+ assertEquals(10, remainingTime);
+ assertEquals(60, progress);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class WineStorageDeviceTemperatureStateTest {
+ private static final Integer TEMPERATURE_0 = 8;
+ private static final Integer TEMPERATURE_1 = 10;
+ private static final Integer TEMPERATURE_2 = 12;
+
+ private static final Integer TARGET_TEMPERATURE_0 = 5;
+ private static final Integer TARGET_TEMPERATURE_1 = 9;
+ private static final Integer TARGET_TEMPERATURE_2 = 11;
+
+ @Nullable
+ private DeviceState deviceState;
+
+ private DeviceState getDeviceState() {
+ assertNotNull(deviceState);
+ return Objects.requireNonNull(deviceState);
+ }
+
+ private void setUpDeviceStateMock(int numberOfTemperatures) {
+ deviceState = mock(DeviceState.class);
+ if (numberOfTemperatures > 0) {
+ when(getDeviceState().getTemperature(0)).thenReturn(Optional.of(TEMPERATURE_0));
+ when(getDeviceState().getTargetTemperature(0)).thenReturn(Optional.of(TARGET_TEMPERATURE_0));
+ } else {
+ when(getDeviceState().getTemperature(0)).thenReturn(Optional.empty());
+ when(getDeviceState().getTargetTemperature(0)).thenReturn(Optional.empty());
+ }
+ if (numberOfTemperatures > 1) {
+ when(getDeviceState().getTemperature(1)).thenReturn(Optional.of(TEMPERATURE_1));
+ when(getDeviceState().getTargetTemperature(1)).thenReturn(Optional.of(TARGET_TEMPERATURE_1));
+ } else {
+ when(getDeviceState().getTemperature(1)).thenReturn(Optional.empty());
+ when(getDeviceState().getTargetTemperature(1)).thenReturn(Optional.empty());
+ }
+ if (numberOfTemperatures > 2) {
+ when(getDeviceState().getTemperature(2)).thenReturn(Optional.of(TEMPERATURE_2));
+ when(getDeviceState().getTargetTemperature(2)).thenReturn(Optional.of(TARGET_TEMPERATURE_2));
+ } else {
+ when(getDeviceState().getTemperature(2)).thenReturn(Optional.empty());
+ when(getDeviceState().getTargetTemperature(2)).thenReturn(Optional.empty());
+ }
+ }
+
+ @Test
+ public void testGetTemperaturesForWineCabinetWithThreeCompartments() {
+ // given:
+ setUpDeviceStateMock(3);
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+ WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Optional<Integer> temperature = state.getTemperature();
+ Optional<Integer> targetTemperature = state.getTargetTemperature();
+
+ // then:
+ assertFalse(temperature.isPresent());
+ assertFalse(targetTemperature.isPresent());
+ }
+
+ @Test
+ public void testGetTemperaturesForWineCabinetWithTwoCompartments() {
+ // given:
+ setUpDeviceStateMock(2);
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+ WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Optional<Integer> temperature = state.getTemperature();
+ Optional<Integer> targetTemperature = state.getTargetTemperature();
+
+ // then:
+ assertFalse(temperature.isPresent());
+ assertFalse(targetTemperature.isPresent());
+ }
+
+ @Test
+ public void testGetTemperaturesForWineCabinetWithOneCompartment() {
+ // given:
+ setUpDeviceStateMock(1);
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+ WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Integer temperature = state.getTemperature().get();
+ Integer targetTemperature = state.getTargetTemperature().get();
+
+ // then:
+ assertEquals(TEMPERATURE_0, temperature);
+ assertEquals(TARGET_TEMPERATURE_0, targetTemperature);
+ }
+
+ @Test
+ public void testGetTemperaturesForWineCabinetFreezerCombination() {
+ // given:
+ setUpDeviceStateMock(2);
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET_FREEZER_COMBINATION);
+ WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Optional<Integer> temperature = state.getTemperature();
+ Optional<Integer> targetTemperature = state.getTargetTemperature();
+
+ // then:
+ assertFalse(temperature.isPresent());
+ assertFalse(targetTemperature.isPresent());
+ }
+
+ @Test
+ public void testGetTemperaturesForOtherDeviceWithOneTemperature() {
+ // given:
+ setUpDeviceStateMock(1);
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.OVEN);
+ WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Optional<Integer> temperature = state.getTemperature();
+ Optional<Integer> targetTemperature = state.getTargetTemperature();
+
+ // then:
+ assertFalse(temperature.isPresent());
+ assertFalse(targetTemperature.isPresent());
+ }
+
+ @Test
+ public void testGetTemperaturesWhenNoTemperaturesAreAvailable() {
+ // given:
+ setUpDeviceStateMock(0);
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+ WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Optional<Integer> temperature = state.getTemperature();
+ Optional<Integer> targetTemperature = state.getTargetTemperature();
+
+ // then:
+ assertFalse(temperature.isPresent());
+ assertFalse(targetTemperature.isPresent());
+ }
+
+ @Test
+ public void testGetTopTemperaturesForWineCabinetWithThreeCompartments() {
+ // given:
+ setUpDeviceStateMock(3);
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+ WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Integer temperature = state.getTopTemperature().get();
+ Integer targetTemperature = state.getTopTargetTemperature().get();
+
+ // then:
+ assertEquals(TEMPERATURE_0, temperature);
+ assertEquals(TARGET_TEMPERATURE_0, targetTemperature);
+ }
+
+ @Test
+ public void testGetTopTemperaturesForWineCabinetWithTwoCompartments() {
+ // given:
+ setUpDeviceStateMock(2);
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+ WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Integer temperature = state.getTopTemperature().get();
+ Integer targetTemperature = state.getTopTargetTemperature().get();
+
+ // then:
+ assertEquals(TEMPERATURE_0, temperature);
+ assertEquals(TARGET_TEMPERATURE_0, targetTemperature);
+ }
+
+ @Test
+ public void testGetTopTemperaturesForWineCabinetWithOneCompartment() {
+ // given:
+ setUpDeviceStateMock(1);
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+ WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Optional<Integer> temperature = state.getTopTemperature();
+ Optional<Integer> targetTemperature = state.getTopTargetTemperature();
+
+ // then:
+ assertFalse(temperature.isPresent());
+ assertFalse(targetTemperature.isPresent());
+ }
+
+ @Test
+ public void testGetTopTemperaturesForWineCabinetFreezerCombination() {
+ // given:
+ setUpDeviceStateMock(2);
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET_FREEZER_COMBINATION);
+ WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Integer temperature = state.getTopTemperature().get();
+ Integer targetTemperature = state.getTopTargetTemperature().get();
+
+ // then:
+ assertEquals(TEMPERATURE_0, temperature);
+ assertEquals(TARGET_TEMPERATURE_0, targetTemperature);
+ }
+
+ @Test
+ public void testGetTopTemperaturesForOtherDeviceWithTwoTemperatures() {
+ // given:
+ setUpDeviceStateMock(2);
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.OVEN);
+ WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Optional<Integer> temperature = state.getTopTemperature();
+ Optional<Integer> targetTemperature = state.getTopTargetTemperature();
+
+ // then:
+ assertFalse(temperature.isPresent());
+ assertFalse(targetTemperature.isPresent());
+ }
+
+ @Test
+ public void testGetTopTemperaturesWhenNoTemperaturesAreAvailable() {
+ // given:
+ setUpDeviceStateMock(0);
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+ WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Optional<Integer> temperature = state.getTopTemperature();
+ Optional<Integer> targetTemperature = state.getTopTargetTemperature();
+
+ // then:
+ assertFalse(temperature.isPresent());
+ assertFalse(targetTemperature.isPresent());
+ }
+
+ @Test
+ public void testGetMiddleTemperaturesForWineCabinetWithThreeCompartments() {
+ // given:
+ setUpDeviceStateMock(3);
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+ WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Integer temperature = state.getMiddleTemperature().get();
+ Integer targetTemperature = state.getMiddleTargetTemperature().get();
+
+ // then:
+ assertEquals(TEMPERATURE_1, temperature);
+ assertEquals(TARGET_TEMPERATURE_1, targetTemperature);
+ }
+
+ @Test
+ public void testGetMiddleTemperaturesForWineCabinetWithTwoCompartments() {
+ // given:
+ setUpDeviceStateMock(2);
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+ WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Optional<Integer> temperature = state.getMiddleTemperature();
+ Optional<Integer> targetTemperature = state.getMiddleTargetTemperature();
+
+ // then:
+ assertFalse(temperature.isPresent());
+ assertFalse(targetTemperature.isPresent());
+ }
+
+ @Test
+ public void testGetMiddleTemperaturesForWineCabinetWithOneCompartment() {
+ // given:
+ setUpDeviceStateMock(1);
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+ WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Optional<Integer> temperature = state.getMiddleTemperature();
+ Optional<Integer> targetTemperature = state.getMiddleTargetTemperature();
+
+ // then:
+ assertFalse(temperature.isPresent());
+ assertFalse(targetTemperature.isPresent());
+ }
+
+ @Test
+ public void testGetMiddleTemperaturesForWineCabinetFreezerCombination() {
+ // given:
+ setUpDeviceStateMock(2);
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET_FREEZER_COMBINATION);
+ WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Optional<Integer> temperature = state.getMiddleTemperature();
+ Optional<Integer> targetTemperature = state.getMiddleTargetTemperature();
+
+ // then:
+ assertFalse(temperature.isPresent());
+ assertFalse(targetTemperature.isPresent());
+ }
+
+ @Test
+ public void testGetMiddleTemperaturesForOtherDeviceWithTwoTemperatures() {
+ // given:
+ setUpDeviceStateMock(2);
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.OVEN);
+ WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Optional<Integer> temperature = state.getMiddleTemperature();
+ Optional<Integer> targetTemperature = state.getMiddleTargetTemperature();
+
+ // then:
+ assertFalse(temperature.isPresent());
+ assertFalse(targetTemperature.isPresent());
+ }
+
+ @Test
+ public void testGetMiddleTemperaturesWhenNoTemperaturesAreAvailable() {
+ // given:
+ setUpDeviceStateMock(0);
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+ WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Optional<Integer> temperature = state.getMiddleTemperature();
+ Optional<Integer> targetTemperature = state.getMiddleTargetTemperature();
+
+ // then:
+ assertFalse(temperature.isPresent());
+ assertFalse(targetTemperature.isPresent());
+ }
+
+ @Test
+ public void testGetBottomTemperaturesForWineCabinetWithThreeCompartments() {
+ // given:
+ setUpDeviceStateMock(3);
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+ WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Integer temperature = state.getBottomTemperature().get();
+ Integer targetTemperature = state.getBottomTargetTemperature().get();
+
+ // then:
+ assertEquals(TEMPERATURE_2, temperature);
+ assertEquals(TARGET_TEMPERATURE_2, targetTemperature);
+ }
+
+ @Test
+ public void testGetBottomTemperaturesForWineCabinetWithTwoCompartments() {
+ // given:
+ setUpDeviceStateMock(2);
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+ WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Integer temperature = state.getBottomTemperature().get();
+ Integer targetTemperature = state.getBottomTargetTemperature().get();
+
+ // then:
+ assertEquals(TEMPERATURE_1, temperature);
+ assertEquals(TARGET_TEMPERATURE_1, targetTemperature);
+ }
+
+ @Test
+ public void testGetBottomTemperaturesForWineCabinetWithOneCompartment() {
+ // given:
+ setUpDeviceStateMock(1);
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+ WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Optional<Integer> temperature = state.getBottomTemperature();
+ Optional<Integer> targetTemperature = state.getBottomTargetTemperature();
+
+ // then:
+ assertFalse(temperature.isPresent());
+ assertFalse(targetTemperature.isPresent());
+ }
+
+ @Test
+ public void testGetBottomTemperaturesForWineCabinetFreezerCombination() {
+ // given:
+ setUpDeviceStateMock(2);
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET_FREEZER_COMBINATION);
+ WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Integer temperature = state.getBottomTemperature().get();
+ Integer targetTemperature = state.getBottomTargetTemperature().get();
+
+ // then:
+ assertEquals(TEMPERATURE_1, temperature);
+ assertEquals(TARGET_TEMPERATURE_1, targetTemperature);
+ }
+
+ @Test
+ public void testGetBottomTemperaturesForOtherDeviceWithTwoTemperatures() {
+ // given:
+ setUpDeviceStateMock(2);
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.OVEN);
+ WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Optional<Integer> temperature = state.getBottomTemperature();
+ Optional<Integer> targetTemperature = state.getBottomTargetTemperature();
+
+ // then:
+ assertFalse(temperature.isPresent());
+ assertFalse(targetTemperature.isPresent());
+ }
+
+ @Test
+ public void testGetBottomTemperaturesWhenNoTemperaturesAreAvailable() {
+ // given:
+ setUpDeviceStateMock(0);
+ when(getDeviceState().getRawType()).thenReturn(DeviceType.WINE_CABINET);
+ WineStorageDeviceTemperatureState state = new WineStorageDeviceTemperatureState(getDeviceState());
+
+ // when:
+ Optional<Integer> temperature = state.getBottomTemperature();
+ Optional<Integer> targetTemperature = state.getBottomTargetTemperature();
+
+ // then:
+ assertFalse(temperature.isPresent());
+ assertFalse(targetTemperature.isPresent());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+import com.google.gson.Gson;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class ActionsTest {
+ @Test
+ public void testNullProcessActionInJsonIsConvertedToEmptyList() throws IOException {
+ // given:
+ String json = "{ \"processAction\": null, \"light\": [1], \"startTime\": [ [0, 0],[23,59] ] }";
+
+ // when:
+ Actions actions = new Gson().fromJson(json, Actions.class);
+
+ // then:
+ assertNotNull(actions.getProcessAction());
+ assertTrue(actions.getProcessAction().isEmpty());
+ }
+
+ @Test
+ public void testNullLightInJsonIsConvertedToEmptyList() throws IOException {
+ // given:
+ String json = "{ \"processAction\": [1], \"light\": null, \"startTime\": [ [0, 0],[23,59] ] }";
+
+ // when:
+ Actions actions = new Gson().fromJson(json, Actions.class);
+
+ // then:
+ assertNotNull(actions.getLight());
+ assertTrue(actions.getLight().isEmpty());
+ }
+
+ @Test
+ public void testNullStartTimeInJsonIsReturnedAsNull() throws IOException {
+ // given:
+ String json = "{ \"processAction\": [1], \"light\": [1], \"startTime\": null }";
+
+ // when:
+ Actions actions = new Gson().fromJson(json, Actions.class);
+
+ // then:
+ assertFalse(actions.getStartTime().isPresent());
+ }
+
+ @Test
+ public void testIdListIsEmptyWhenProgramIdFieldIsMissing() {
+ // given:
+ String json = "{ \"processAction\": [1] }";
+
+ // when:
+ Actions actions = new Gson().fromJson(json, Actions.class);
+
+ // then:
+ assertTrue(actions.getProgramId().isEmpty());
+ }
+
+ @Test
+ public void testIdListIsEmptyWhenProgramIdFieldIsNull() {
+ // given:
+ String json = "{ \"programId\": null }";
+
+ // when:
+ Actions actions = new Gson().fromJson(json, Actions.class);
+
+ // then:
+ assertTrue(actions.getProgramId().isEmpty());
+ }
+
+ @Test
+ public void testIdListContainsEntriesWhenProgramIdFieldIsPresent() {
+ // given:
+ String json = "{ \"programId\": [1,2,3,4] }";
+
+ // when:
+ Actions actions = new Gson().fromJson(json, Actions.class);
+
+ // then:
+ assertEquals(Arrays.asList(1, 2, 3, 4), actions.getProgramId());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.openhab.binding.mielecloud.internal.util.ResourceUtil.getResourceAsString;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add plate step
+ */
+@NonNullByDefault
+public class DeviceCollectionTest {
+ @Test
+ public void testCreateDeviceCollection() throws IOException {
+ // given:
+ String json = getResourceAsString(
+ "/org/openhab/binding/mielecloud/internal/webservice/api/json/deviceCollection.json");
+
+ // when:
+ DeviceCollection collection = DeviceCollection.fromJson(json);
+
+ // then:
+ assertEquals(1, collection.getDeviceIdentifiers().size());
+ Device device = collection.getDevice(collection.getDeviceIdentifiers().iterator().next());
+
+ Ident ident = device.getIdent().get();
+ Type type = ident.getType().get();
+ assertEquals("Devicetype", type.getKeyLocalized().get());
+ assertEquals(DeviceType.HOOD, type.getValueRaw());
+ assertEquals("Ventilation Hood", type.getValueLocalized().get());
+
+ assertEquals("My Hood", ident.getDeviceName().get());
+
+ DeviceIdentLabel deviceIdentLabel = ident.getDeviceIdentLabel().get();
+ assertEquals("000124430017", deviceIdentLabel.getFabNumber().get());
+ assertEquals("00", deviceIdentLabel.getFabIndex().get());
+ assertEquals("DA-6996", deviceIdentLabel.getTechType().get());
+ assertEquals("10101010", deviceIdentLabel.getMatNumber().get());
+ assertEquals(Arrays.asList("4164", "20380", "25226"), deviceIdentLabel.getSwids());
+
+ XkmIdentLabel xkmIdentLabel = ident.getXkmIdentLabel().get();
+ assertEquals("EK039W", xkmIdentLabel.getTechType().get());
+ assertEquals("02.31", xkmIdentLabel.getReleaseVersion().get());
+
+ State state = device.getState().get();
+ Status status = state.getStatus().get();
+ assertEquals(Integer.valueOf(StateType.RUNNING.getCode()), status.getValueRaw().get());
+ assertEquals("In use", status.getValueLocalized().get());
+ assertEquals("State", status.getKeyLocalized().get());
+
+ ProgramType programType = state.getProgramType().get();
+ assertEquals(Integer.valueOf(0), programType.getValueRaw().get());
+ assertEquals("", programType.getValueLocalized().get());
+ assertEquals("Programme", programType.getKeyLocalized().get());
+
+ ProgramPhase programPhase = state.getProgramPhase().get();
+ assertEquals(Integer.valueOf(4609), programPhase.getValueRaw().get());
+ assertEquals("", programPhase.getValueLocalized().get());
+ assertEquals("Phase", programPhase.getKeyLocalized().get());
+
+ assertEquals(Arrays.asList(0, 0), state.getRemainingTime().get());
+ assertEquals(Arrays.asList(0, 0), state.getStartTime().get());
+
+ assertEquals(1, state.getTargetTemperature().size());
+ Temperature targetTemperature = state.getTargetTemperature().get(0);
+ assertNotNull(targetTemperature);
+ assertEquals(Integer.valueOf(-32768), targetTemperature.getValueRaw().get());
+ assertFalse(targetTemperature.getValueLocalized().isPresent());
+ assertEquals("Celsius", targetTemperature.getUnit().get());
+
+ assertEquals(3, state.getTemperature().size());
+ Temperature temperature0 = state.getTemperature().get(0);
+ assertNotNull(temperature0);
+ assertEquals(Integer.valueOf(-32768), temperature0.getValueRaw().get());
+ assertFalse(temperature0.getValueLocalized().isPresent());
+ assertEquals("Celsius", temperature0.getUnit().get());
+ Temperature temperature1 = state.getTemperature().get(1);
+ assertNotNull(temperature1);
+ assertEquals(Integer.valueOf(-32768), temperature1.getValueRaw().get());
+ assertFalse(temperature1.getValueLocalized().isPresent());
+ assertEquals("Celsius", temperature1.getUnit().get());
+ Temperature temperature2 = state.getTemperature().get(2);
+ assertNotNull(temperature2);
+ assertEquals(Integer.valueOf(-32768), temperature2.getValueRaw().get());
+ assertFalse(temperature2.getValueLocalized().isPresent());
+ assertEquals("Celsius", temperature2.getUnit().get());
+
+ assertEquals(false, state.getSignalInfo().get());
+ assertEquals(false, state.getSignalFailure().get());
+ assertEquals(false, state.getSignalDoor().get());
+
+ RemoteEnable remoteEnable = state.getRemoteEnable().get();
+ assertEquals(false, remoteEnable.getFullRemoteControl().get());
+ assertEquals(false, remoteEnable.getSmartGrid().get());
+
+ assertEquals(Light.ENABLE, state.getLight());
+ assertEquals(new ArrayList<Object>(), state.getElapsedTime().get());
+
+ SpinningSpeed spinningSpeed = state.getSpinningSpeed().get();
+ assertEquals(Integer.valueOf(1200), spinningSpeed.getValueRaw().get());
+ assertEquals("1200", spinningSpeed.getValueLocalized().get());
+ assertEquals("rpm", spinningSpeed.getUnit().get());
+
+ DryingStep dryingStep = state.getDryingStep().get();
+ assertFalse(dryingStep.getValueRaw().isPresent());
+ assertEquals("", dryingStep.getValueLocalized().get());
+ assertEquals("Drying level", dryingStep.getKeyLocalized().get());
+
+ VentilationStep ventilationStep = state.getVentilationStep().get();
+ assertEquals(Integer.valueOf(2), ventilationStep.getValueRaw().get());
+ assertEquals("2", ventilationStep.getValueLocalized().get());
+ assertEquals("Power Level", ventilationStep.getKeyLocalized().get());
+
+ List<PlateStep> plateStep = state.getPlateStep();
+ assertEquals(4, plateStep.size());
+ assertEquals(Integer.valueOf(0), plateStep.get(0).getValueRaw().get());
+ assertEquals("0", plateStep.get(0).getValueLocalized().get());
+ assertEquals("Plate Step", plateStep.get(0).getKeyLocalized().get());
+ assertEquals(Integer.valueOf(1), plateStep.get(1).getValueRaw().get());
+ assertEquals("1", plateStep.get(1).getValueLocalized().get());
+ assertEquals("Plate Step", plateStep.get(1).getKeyLocalized().get());
+ assertEquals(Integer.valueOf(2), plateStep.get(2).getValueRaw().get());
+ assertEquals("1.", plateStep.get(2).getValueLocalized().get());
+ assertEquals("Plate Step", plateStep.get(2).getKeyLocalized().get());
+ assertEquals(Integer.valueOf(3), plateStep.get(3).getValueRaw().get());
+ assertEquals("2", plateStep.get(3).getValueLocalized().get());
+ assertEquals("Plate Step", plateStep.get(3).getKeyLocalized().get());
+
+ assertEquals(Integer.valueOf(20), state.getBatteryLevel().get());
+ }
+
+ @Test
+ public void testCreateDeviceCollectionFromInvalidJsonThrowsMieleSyntaxException() throws IOException {
+ // given:
+ String invalidJson = getResourceAsString(
+ "/org/openhab/binding/mielecloud/internal/webservice/api/json/invalidDeviceCollection.json");
+
+ // when:
+ assertThrows(MieleSyntaxException.class, () -> {
+ DeviceCollection.fromJson(invalidJson);
+ });
+ }
+
+ @Test
+ public void testCreateDeviceCollectionWithLargeProgramID() throws IOException {
+ // given:
+ String json = getResourceAsString(
+ "/org/openhab/binding/mielecloud/internal/webservice/api/json/deviceCollectionWithLargeProgramID.json");
+
+ // when:
+ DeviceCollection collection = DeviceCollection.fromJson(json);
+
+ // then:
+ assertEquals(1, collection.getDeviceIdentifiers().size());
+ Device device = collection.getDevice(collection.getDeviceIdentifiers().iterator().next());
+
+ Ident ident = device.getIdent().get();
+ Type type = ident.getType().get();
+ assertEquals("Devicetype", type.getKeyLocalized().get());
+ assertEquals(DeviceType.UNKNOWN, type.getValueRaw());
+ assertEquals("", type.getValueLocalized().get());
+
+ assertEquals("Some Devicename", ident.getDeviceName().get());
+
+ DeviceIdentLabel deviceIdentLabel = ident.getDeviceIdentLabel().get();
+ assertEquals("", deviceIdentLabel.getFabNumber().get());
+ assertEquals("", deviceIdentLabel.getFabIndex().get());
+ assertEquals("", deviceIdentLabel.getTechType().get());
+ assertEquals("", deviceIdentLabel.getMatNumber().get());
+ assertEquals(Arrays.asList(), deviceIdentLabel.getSwids());
+
+ XkmIdentLabel xkmIdentLabel = ident.getXkmIdentLabel().get();
+ assertEquals("", xkmIdentLabel.getTechType().get());
+ assertEquals("", xkmIdentLabel.getReleaseVersion().get());
+
+ State state = device.getState().get();
+ ProgramId programId = state.getProgramId().get();
+ assertEquals(Long.valueOf(2499805184L), programId.getValueRaw().get());
+ assertEquals("", programId.getValueLocalized().get());
+ assertEquals("Program Id", programId.getKeyLocalized().get());
+
+ Status status = state.getStatus().get();
+ assertEquals(Integer.valueOf(StateType.RUNNING.getCode()), status.getValueRaw().get());
+ assertEquals("In use", status.getValueLocalized().get());
+ assertEquals("State", status.getKeyLocalized().get());
+
+ ProgramType programType = state.getProgramType().get();
+ assertEquals(Integer.valueOf(0), programType.getValueRaw().get());
+ assertEquals("Operation mode", programType.getValueLocalized().get());
+ assertEquals("Program type", programType.getKeyLocalized().get());
+
+ ProgramPhase programPhase = state.getProgramPhase().get();
+ assertEquals(Integer.valueOf(0), programPhase.getValueRaw().get());
+ assertEquals("", programPhase.getValueLocalized().get());
+ assertEquals("Phase", programPhase.getKeyLocalized().get());
+
+ assertEquals(Arrays.asList(0, 0), state.getRemainingTime().get());
+ assertEquals(Arrays.asList(0, 0), state.getStartTime().get());
+
+ assertTrue(state.getTargetTemperature().isEmpty());
+ assertTrue(state.getTemperature().isEmpty());
+
+ assertEquals(false, state.getSignalInfo().get());
+ assertEquals(false, state.getSignalFailure().get());
+ assertEquals(false, state.getSignalDoor().get());
+
+ RemoteEnable remoteEnable = state.getRemoteEnable().get();
+ assertEquals(true, remoteEnable.getFullRemoteControl().get());
+ assertEquals(false, remoteEnable.getSmartGrid().get());
+
+ assertEquals(Light.NOT_SUPPORTED, state.getLight());
+ assertEquals(new ArrayList<Object>(), state.getElapsedTime().get());
+
+ DryingStep dryingStep = state.getDryingStep().get();
+ assertFalse(dryingStep.getValueRaw().isPresent());
+ assertEquals("", dryingStep.getValueLocalized().get());
+ assertEquals("Drying level", dryingStep.getKeyLocalized().get());
+
+ VentilationStep ventilationStep = state.getVentilationStep().get();
+ assertFalse(ventilationStep.getValueRaw().isPresent());
+ assertEquals("", ventilationStep.getValueLocalized().get());
+ assertEquals("Power Level", ventilationStep.getKeyLocalized().get());
+
+ List<PlateStep> plateStep = state.getPlateStep();
+ assertEquals(0, plateStep.size());
+ }
+
+ @Test
+ public void testCreateDeviceCollectionWithSpinningSpeedObject() throws IOException {
+ // given:
+ String json = getResourceAsString(
+ "/org/openhab/binding/mielecloud/internal/webservice/api/json/deviceCollectionWithSpinningSpeedObject.json");
+
+ // when:
+ DeviceCollection collection = DeviceCollection.fromJson(json);
+
+ // then:
+ assertEquals(1, collection.getDeviceIdentifiers().size());
+ Device device = collection.getDevice(collection.getDeviceIdentifiers().iterator().next());
+
+ State state = device.getState().get();
+ SpinningSpeed spinningSpeed = state.getSpinningSpeed().get();
+ assertNotNull(spinningSpeed);
+ assertEquals(Integer.valueOf(1600), spinningSpeed.getValueRaw().get());
+ assertEquals("1600", spinningSpeed.getValueLocalized().get());
+ assertEquals("U/min", spinningSpeed.getUnit().get());
+ }
+
+ @Test
+ public void testCreateDeviceCollectionWithFloatingPointTemperature() throws IOException {
+ // given:
+ String json = getResourceAsString(
+ "/org/openhab/binding/mielecloud/internal/webservice/api/json/deviceCollectionWithFloatingPointTargetTemperature.json");
+
+ // when:
+ DeviceCollection collection = DeviceCollection.fromJson(json);
+
+ // then:
+ assertEquals(1, collection.getDeviceIdentifiers().size());
+ Device device = collection.getDevice(collection.getDeviceIdentifiers().iterator().next());
+
+ State state = device.getState().get();
+ List<Temperature> targetTemperatures = state.getTargetTemperature();
+ assertEquals(1, targetTemperatures.size());
+
+ Temperature targetTemperature = targetTemperatures.get(0);
+ assertEquals(Integer.valueOf(80), targetTemperature.getValueRaw().get());
+ assertEquals(Integer.valueOf(0), targetTemperature.getValueLocalized().get());
+ assertEquals("Celsius", targetTemperature.getUnit().get());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+import com.google.gson.Gson;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class DeviceIdentLabelTest {
+ @Test
+ public void testNullSwidsInJsonAreConvertedToEmptyList() throws IOException {
+ // given:
+ String json = "{ \"swids\": null }";
+
+ // when:
+ DeviceIdentLabel deviceIdentLabel = new Gson().fromJson(json, DeviceIdentLabel.class);
+
+ // then:
+ assertNotNull(deviceIdentLabel.getSwids());
+ assertTrue(deviceIdentLabel.getSwids().isEmpty());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class ErrorMessageTest {
+
+ @Test
+ public void testErrorMessageCanBeCreated() {
+ // given:
+ String json = "{\"message\": \"Unauthorized\"}";
+
+ // when:
+ ErrorMessage errorMessage = ErrorMessage.fromJson(json);
+
+ // then:
+ assertEquals("Unauthorized", errorMessage.getMessage().get());
+ }
+
+ @Test
+ public void testErrorMessageCreationThrowsMieleSyntaxExceptionWhenJsonIsInvalid() {
+ // given:
+ String json = "\"message\": \"Unauthorized}";
+
+ // when:
+ assertThrows(MieleSyntaxException.class, () -> {
+ ErrorMessage.fromJson(json);
+ });
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class LightTest {
+ @Test
+ public void testFromNullId() {
+ // when:
+ Light light = Light.fromId(null);
+
+ // then:
+ assertEquals(Light.UNKNOWN, light);
+ }
+
+ @Test
+ public void testFromNotSupportedId() {
+ // when:
+ Light light = Light.fromId(0);
+
+ // then:
+ assertEquals(Light.NOT_SUPPORTED, light);
+ }
+
+ @Test
+ public void testFromNotSupportedAlternativeId() {
+ // when:
+ Light light = Light.fromId(255);
+
+ // then:
+ assertEquals(Light.NOT_SUPPORTED, light);
+ }
+
+ @Test
+ public void testFromEnabledId() {
+ // when:
+ Light light = Light.fromId(1);
+
+ // then:
+ assertEquals(Light.ENABLE, light);
+ }
+
+ @Test
+ public void testFromDisabledId() {
+ // when:
+ Light light = Light.fromId(2);
+
+ // then:
+ assertEquals(Light.DISABLE, light);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+import com.google.gson.Gson;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class StateTest {
+ @Test
+ public void testNullRemainingTimeInJsonCausesRemainingTimeListToBeNull() throws IOException {
+ // given:
+ String json = "{ \"remainingTime\": null, \"startTime\": [0, 0], \"targetTemperature\": [{}], \"temperature\": [{}], \"elapsedTime\": [0, 0] }";
+
+ // when:
+ State state = new Gson().fromJson(json, State.class);
+
+ // then:
+ assertFalse(state.getRemainingTime().isPresent());
+ }
+
+ @Test
+ public void testNullStartTimeInJsonCausesStartTimeListToBeNull() throws IOException {
+ // given:
+ String json = "{ \"remainingTime\": [0, 0], \"startTime\": null, \"targetTemperature\": [{}], \"temperature\": [{}], \"elapsedTime\": [0, 0] }";
+
+ // when:
+ State state = new Gson().fromJson(json, State.class);
+
+ // then:
+ assertFalse(state.getStartTime().isPresent());
+ }
+
+ @Test
+ public void testNullElapsedTimeInJsonCausesElapsedTimeListToBeNull() throws IOException {
+ // given:
+ String json = "{ \"remainingTime\": [0, 0], \"startTime\": [0, 0], \"targetTemperature\": [{}], \"temperature\": [{}], \"elapsedTime\": null }";
+
+ // when:
+ State state = new Gson().fromJson(json, State.class);
+
+ // then:
+ assertFalse(state.getElapsedTime().isPresent());
+ }
+
+ @Test
+ public void testNullTargetTemperatureInJsonIsConvertedToEmptyList() throws IOException {
+ // given:
+ String json = "{ \"remainingTime\": [0, 0], \"startTime\": [0, 0], \"targetTemperature\": null, \"temperature\": [{}], \"elapsedTime\": [0, 0] }";
+
+ // when:
+ State state = new Gson().fromJson(json, State.class);
+
+ // then:
+ assertNotNull(state.getTargetTemperature());
+ assertTrue(state.getTargetTemperature().isEmpty());
+ }
+
+ @Test
+ public void testNullTemperatureInJsonIsConvertedToEmptyList() throws IOException {
+ // given:
+ String json = "{ \"remainingTime\": [0, 0], \"startTime\": [0, 0], \"targetTemperature\": [{}], \"temperature\": null, \"elapsedTime\": [0, 0] }";
+
+ // when:
+ State state = new Gson().fromJson(json, State.class);
+
+ // then:
+ assertNotNull(state.getTemperature());
+ assertTrue(state.getTemperature().isEmpty());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+import com.google.gson.Gson;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class StatusTest {
+ @Test
+ public void testParseStatusWithUnknownRawValue() {
+ // given:
+ String json = "{ \"key_localized\": \"State\", \"value_raw\": 99, \"value_localized\": \"Booting\" }";
+
+ // when:
+ Status status = new Gson().fromJson(json, Status.class);
+
+ // then:
+ assertNotNull(status);
+ assertEquals("State", status.getKeyLocalized().get());
+ assertEquals(Integer.valueOf(99), status.getValueRaw().get());
+ assertEquals("Booting", status.getValueLocalized().get());
+ }
+
+ @Test
+ public void testParseStatusWithKnownRawValue() {
+ // given:
+ String json = "{ \"key_localized\": \"State\", \"value_raw\": 1, \"value_localized\": \"Off\" }";
+
+ // when:
+ Status status = new Gson().fromJson(json, Status.class);
+
+ // then:
+ assertNotNull(status);
+ assertEquals("State", status.getKeyLocalized().get());
+ assertEquals(Integer.valueOf(StateType.OFF.getCode()), status.getValueRaw().get());
+ assertEquals("Off", status.getValueLocalized().get());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.api.json;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+import com.google.gson.Gson;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class TypeTest {
+ @Test
+ public void testParseTypeWithUnknownRawValue() {
+ // given:
+ String json = "{ \"key_localized\": \"Devicetype\", \"value_raw\": 99, \"value_localized\": \"Car Vaccuum Robot\" }";
+
+ // when:
+ Type type = new Gson().fromJson(json, Type.class);
+
+ // then:
+ assertNotNull(type);
+ assertEquals("Devicetype", type.getKeyLocalized().get());
+ assertEquals(DeviceType.UNKNOWN, type.getValueRaw());
+ assertEquals("Car Vaccuum Robot", type.getValueLocalized().get());
+ }
+
+ @Test
+ public void testParseTypeWithKnownRawValue() {
+ // given:
+ String json = "{ \"key_localized\": \"Devicetype\", \"value_raw\": 1, \"value_localized\": \"Washing Machine\" }";
+
+ // when:
+ Type type = new Gson().fromJson(json, Type.class);
+
+ // then:
+ assertNotNull(type);
+ assertEquals("Devicetype", type.getKeyLocalized().get());
+ assertEquals(DeviceType.WASHING_MACHINE, type.getValueRaw());
+ assertEquals("Washing Machine", type.getValueLocalized().get());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.exception;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class TooManyRequestsExceptionTest {
+ @Test
+ public void testHasRetryAfterHintReturnsFalseWhenNoRetryAfterWasPassedToConstructor() {
+ // given:
+ TooManyRequestsException exception = new TooManyRequestsException("", null);
+
+ // when:
+ boolean result = exception.hasRetryAfterHint();
+
+ // then:
+ assertFalse(result);
+ }
+
+ @Test
+ public void testHasRetryAfterHintReturnsTrueWhenRetryAfterWasPassedToConstructor() {
+ // given:
+ TooManyRequestsException exception = new TooManyRequestsException("", "25");
+
+ // when:
+ boolean result = exception.hasRetryAfterHint();
+
+ // then:
+ assertTrue(result);
+ }
+
+ @Test
+ public void testGetSecondsUntilRetryReturnsMinusOneWhenNoRetryAfterHintIsPresent() {
+ // given:
+ TooManyRequestsException exception = new TooManyRequestsException("", null);
+
+ // when:
+ long result = exception.getSecondsUntilRetry();
+
+ // then:
+ assertEquals(-1L, result);
+ }
+
+ @Test
+ public void testGetSecondsUntilRetryParsesNumber() {
+ // given:
+ TooManyRequestsException exception = new TooManyRequestsException("", "30");
+
+ // when:
+ long result = exception.getSecondsUntilRetry();
+
+ // then:
+ assertEquals(30L, result);
+ }
+
+ @Test
+ public void testGetSecondsUntilRetryParsesDate() {
+ // given:
+ TooManyRequestsException exception = new TooManyRequestsException("", "Thu, 12 Jan 5015 15:02:30 GMT");
+
+ // when:
+ long result = exception.getSecondsUntilRetry();
+
+ // then:
+ assertNotEquals(0L, result);
+ }
+
+ @Test
+ public void testGetSecondsUntilRetryParsesDateFromThePast() {
+ // given:
+ TooManyRequestsException exception = new TooManyRequestsException("", "Wed, 21 Oct 2015 07:28:00 GMT");
+
+ // when:
+ long result = exception.getSecondsUntilRetry();
+
+ // then:
+ assertEquals(0L, result);
+ }
+
+ @Test
+ public void testGetSecondsUntilRetryReturnsMinusOneWhenDateCannotBeParsed() {
+ // given:
+ TooManyRequestsException exception = new TooManyRequestsException("", "50 Minutes");
+
+ // when:
+ long result = exception.getSecondsUntilRetry();
+
+ // then:
+ assertEquals(-1L, result);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.language;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class CombiningLanguageProviderTest {
+ private static final Optional<String> PRIORITIZED_LANGUAGE = Optional.of("de");
+ private static final Optional<String> FALLBACK_LANGUAGE = Optional.of("en");
+
+ private static final LanguageProvider PRIORITIZED_PROVIDER = new LanguageProvider() {
+ @Override
+ public Optional<String> getLanguage() {
+ return PRIORITIZED_LANGUAGE;
+ }
+ };
+
+ private static final LanguageProvider FALLBACK_PROVIDER = new LanguageProvider() {
+ @Override
+ public Optional<String> getLanguage() {
+ return FALLBACK_LANGUAGE;
+ }
+ };
+
+ private static final LanguageProvider NULL_PROVIDER = new LanguageProvider() {
+ @Override
+ public Optional<String> getLanguage() {
+ return Optional.empty();
+ }
+ };
+
+ @Test
+ public void testPrioritizedLanguageProviderIsUsed() {
+ // given:
+ LanguageProvider provider = new CombiningLanguageProvider(PRIORITIZED_PROVIDER, FALLBACK_PROVIDER);
+
+ // when:
+ Optional<String> language = provider.getLanguage();
+
+ // then:
+ assertEquals(PRIORITIZED_LANGUAGE, language);
+ }
+
+ @Test
+ public void testFallbackProviderIsUsedWhenPrioritizedProviderIsNull() {
+ // given:
+ LanguageProvider provider = new CombiningLanguageProvider(null, FALLBACK_PROVIDER);
+
+ // when:
+ Optional<String> language = provider.getLanguage();
+
+ // then:
+ assertEquals(FALLBACK_LANGUAGE, language);
+ }
+
+ @Test
+ public void testFallbackProviderIsUsedWhenPrioritizedProviderProvidesNull() {
+ // given:
+ LanguageProvider provider = new CombiningLanguageProvider(NULL_PROVIDER, FALLBACK_PROVIDER);
+
+ // when:
+ Optional<String> language = provider.getLanguage();
+
+ // then:
+ assertEquals(FALLBACK_LANGUAGE, language);
+ }
+
+ @Test
+ public void testProvidesNullWhenBothProvidersAreNull() {
+ // given:
+ LanguageProvider provider = new CombiningLanguageProvider(null, null);
+
+ // when:
+ Optional<String> language = provider.getLanguage();
+
+ // then:
+ assertFalse(language.isPresent());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.language;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.*;
+
+import java.util.Locale;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.core.i18n.LocaleProvider;
+
+/**
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class OpenHabLanguageProviderTest {
+ @Test
+ public void whenTheLocaleIsSetToEnglishThenTheLanguageCodeIsEn() {
+ // given:
+ LocaleProvider localeProvider = mock(LocaleProvider.class);
+ when(localeProvider.getLocale()).thenReturn(Locale.ENGLISH);
+
+ LanguageProvider languageProvider = new OpenHabLanguageProvider(localeProvider);
+
+ // when:
+ Optional<String> language = languageProvider.getLanguage();
+
+ // then:
+ assertEquals(Optional.of("en"), language);
+ }
+
+ @Test
+ public void whenTheLocaleIsSetToGermanThenTheLanguageCodeIsDe() {
+ // given:
+ LocaleProvider localeProvider = mock(LocaleProvider.class);
+ when(localeProvider.getLocale()).thenReturn(Locale.GERMAN);
+
+ LanguageProvider languageProvider = new OpenHabLanguageProvider(localeProvider);
+
+ // when:
+ Optional<String> language = languageProvider.getLanguage();
+
+ // then:
+ assertEquals(Optional.of("de"), language);
+ }
+
+ @Test
+ public void whenTheLocaleIsSetToGermanyThenTheLanguageCodeIsDe() {
+ // given:
+ LocaleProvider localeProvider = mock(LocaleProvider.class);
+ when(localeProvider.getLocale()).thenReturn(Locale.GERMANY);
+
+ LanguageProvider languageProvider = new OpenHabLanguageProvider(localeProvider);
+
+ // when:
+ Optional<String> language = languageProvider.getLanguage();
+
+ // then:
+ assertEquals(Optional.of("de"), language);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.retry;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingTestConstants;
+import org.openhab.binding.mielecloud.internal.auth.OAuthException;
+import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@NonNullByDefault
+public class AuthorizationFailedRetryStrategyTest {
+ private static final String TEST_STRING = "Some Test String";
+
+ @Mock
+ @Nullable
+ private Supplier<@Nullable String> operationWithReturnValue;
+ @Mock
+ @Nullable
+ private Consumer<Exception> onException;
+ @Mock
+ @Nullable
+ private Runnable operation;
+
+ private final OAuthTokenRefresher refresher = mock(OAuthTokenRefresher.class);
+
+ private Supplier<@Nullable String> getOperationWithReturnValue() {
+ assertNotNull(operationWithReturnValue);
+ return Objects.requireNonNull(operationWithReturnValue);
+ }
+
+ private Consumer<Exception> getOnException() {
+ assertNotNull(onException);
+ return Objects.requireNonNull(onException);
+ }
+
+ private Runnable getOperation() {
+ assertNotNull(operation);
+ return Objects.requireNonNull(operation);
+ }
+
+ @Test
+ public void testPerformRetryableOperationWithReturnValueInvokesOperation() {
+ // given:
+ when(getOperationWithReturnValue().get()).thenReturn(TEST_STRING);
+
+ AuthorizationFailedRetryStrategy retryStrategy = new AuthorizationFailedRetryStrategy(refresher,
+ MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+ // when:
+ String result = retryStrategy.performRetryableOperation(getOperationWithReturnValue(), getOnException());
+
+ // then:
+ assertEquals(TEST_STRING, result);
+ }
+
+ @Test
+ public void testPerformRetryableOperationWithReturnValueInvokesRefreshTokenAndRetriesOperation() {
+ // given:
+ when(getOperationWithReturnValue().get()).thenThrow(AuthorizationFailedException.class).thenReturn(TEST_STRING);
+
+ AuthorizationFailedRetryStrategy retryStrategy = new AuthorizationFailedRetryStrategy(refresher,
+ MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+ // when:
+ String result = retryStrategy.performRetryableOperation(getOperationWithReturnValue(), getOnException());
+
+ // then:
+ assertEquals(TEST_STRING, result);
+ verify(getOnException()).accept(any());
+ verify(refresher).refreshToken(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+ verifyNoMoreInteractions(getOnException(), refresher);
+ }
+
+ @Test
+ public void testPerformRetryableOperationWithReturnValueThrowsMieleWebserviceExceptionWhenRetryingTheOperationFails() {
+ // given:
+ when(getOperationWithReturnValue().get()).thenThrow(AuthorizationFailedException.class);
+
+ AuthorizationFailedRetryStrategy retryStrategy = new AuthorizationFailedRetryStrategy(refresher,
+ MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+ assertThrows(MieleWebserviceException.class, () -> {
+ try {
+ // when:
+ retryStrategy.performRetryableOperation(getOperationWithReturnValue(), getOnException());
+ } catch (Exception e) {
+ // then:
+ verify(getOnException()).accept(any());
+ verify(refresher).refreshToken(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+ verifyNoMoreInteractions(getOnException(), refresher);
+ throw e;
+ }
+ });
+ }
+
+ @Test
+ public void testPerformRetryableOperationInvokesOperation() {
+ // given:
+ AuthorizationFailedRetryStrategy retryStrategy = new AuthorizationFailedRetryStrategy(refresher,
+ MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+ // when:
+ retryStrategy.performRetryableOperation(getOperation(), getOnException());
+
+ // then:
+ verify(getOperation()).run();
+ verifyNoMoreInteractions(getOperation());
+ }
+
+ @Test
+ public void testPerformRetryableOperationInvokesRefreshTokenAndRetriesOperation() {
+ // given:
+ doThrow(AuthorizationFailedException.class).doNothing().when(getOperation()).run();
+
+ AuthorizationFailedRetryStrategy retryStrategy = new AuthorizationFailedRetryStrategy(refresher,
+ MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+ // when:
+ retryStrategy.performRetryableOperation(getOperation(), getOnException());
+
+ // then:
+ verify(getOnException()).accept(any());
+ verify(refresher).refreshToken(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+ verify(getOperation(), times(2)).run();
+ verifyNoMoreInteractions(getOnException(), refresher, getOperation());
+ }
+
+ @Test
+ public void testPerformRetryableOperationThrowsMieleWebserviceExceptionWhenRetryingTheOperationFails() {
+ // given:
+ doThrow(AuthorizationFailedException.class).when(getOperation()).run();
+
+ AuthorizationFailedRetryStrategy retryStrategy = new AuthorizationFailedRetryStrategy(refresher,
+ MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+ assertThrows(MieleWebserviceException.class, () -> {
+ try {
+ // when:
+ retryStrategy.performRetryableOperation(getOperation(), getOnException());
+ } catch (Exception e) {
+ // then:
+ verify(getOnException()).accept(any());
+ verify(refresher).refreshToken(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+ verify(getOperation(), times(2)).run();
+ verifyNoMoreInteractions(getOnException(), refresher, getOperation());
+ throw e;
+ }
+ });
+ }
+
+ @Test
+ public void testPerformRetryableOperationThrowsMieleWebserviceExceptionWhenTokenRefreshingFails() {
+ // given:
+ doThrow(AuthorizationFailedException.class).when(getOperation()).run();
+ doThrow(OAuthException.class).when(refresher).refreshToken(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+ AuthorizationFailedRetryStrategy retryStrategy = new AuthorizationFailedRetryStrategy(refresher,
+ MieleCloudBindingTestConstants.SERVICE_HANDLE);
+
+ assertThrows(MieleWebserviceException.class, () -> {
+ try {
+ // when:
+ retryStrategy.performRetryableOperation(getOperation(), getOnException());
+ } catch (Exception e) {
+ // then:
+ verify(getOnException()).accept(any());
+ verify(refresher).refreshToken(MieleCloudBindingTestConstants.SERVICE_HANDLE);
+ verify(getOperation()).run();
+ verifyNoMoreInteractions(getOnException(), refresher, getOperation());
+ throw e;
+ }
+ });
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.retry;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceTransientException;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@NonNullByDefault
+public class NTimesRetryStrategyTest {
+ private static final int SUCCESSFUL_RETURN_VALUE = 42;
+
+ @Mock
+ @Nullable
+ private Supplier<@Nullable Integer> operation;
+
+ @Mock
+ @Nullable
+ private Consumer<Exception> onTransientException;
+
+ private Supplier<@Nullable Integer> getOperation() {
+ assertNotNull(operation);
+ return Objects.requireNonNull(operation);
+ }
+
+ private Consumer<Exception> getOnTransientException() {
+ assertNotNull(onTransientException);
+ return Objects.requireNonNull(onTransientException);
+ }
+
+ @Test
+ public void testConstructorThrowsIllegalArgumentExceptionIfNumberOfRetriesIsSmallerThanZero() {
+ // when:
+ assertThrows(IllegalArgumentException.class, () -> {
+ new NTimesRetryStrategy(-1);
+ });
+ }
+
+ @Test
+ public void testSuccessfulOperationReturnsCorrectValue() {
+ // given:
+ when(getOperation().get()).thenReturn(SUCCESSFUL_RETURN_VALUE);
+
+ NTimesRetryStrategy retryStrategy = new NTimesRetryStrategy(1);
+
+ // when:
+ Integer result = retryStrategy.performRetryableOperation(getOperation(), getOnTransientException());
+
+ // then:
+ assertEquals(Integer.valueOf(SUCCESSFUL_RETURN_VALUE), result);
+ verifyNoMoreInteractions(onTransientException);
+ }
+
+ @Test
+ public void testFailingOperationReturnsCorrectValueOnRetry() {
+ // given:
+ when(getOperation().get()).thenThrow(MieleWebserviceTransientException.class)
+ .thenReturn(SUCCESSFUL_RETURN_VALUE);
+
+ NTimesRetryStrategy retryStrategy = new NTimesRetryStrategy(1);
+
+ // when:
+ Integer result = retryStrategy.performRetryableOperation(getOperation(), getOnTransientException());
+
+ // then:
+ assertEquals(Integer.valueOf(SUCCESSFUL_RETURN_VALUE), result);
+ verify(getOnTransientException()).accept(any());
+ verifyNoMoreInteractions(onTransientException);
+ }
+
+ @Test
+ public void testFailingOperationReturnsCorrectValueOnSecondRetry() {
+ // given:
+ when(getOperation().get()).thenThrow(MieleWebserviceTransientException.class)
+ .thenThrow(MieleWebserviceTransientException.class).thenReturn(SUCCESSFUL_RETURN_VALUE);
+
+ NTimesRetryStrategy retryStrategy = new NTimesRetryStrategy(2);
+
+ // when:
+ Integer result = retryStrategy.performRetryableOperation(getOperation(), getOnTransientException());
+
+ // then:
+ assertEquals(Integer.valueOf(SUCCESSFUL_RETURN_VALUE), result);
+ verify(getOnTransientException(), times(2)).accept(any());
+ verifyNoMoreInteractions(onTransientException);
+ }
+
+ @Test
+ public void testAlwaysFailingOperationThrowsMieleWebserviceException() {
+ // given:
+ when(getOperation().get()).thenThrow(MieleWebserviceTransientException.class);
+
+ NTimesRetryStrategy retryStrategy = new NTimesRetryStrategy(1);
+
+ // when:
+ try {
+ retryStrategy.performRetryableOperation(getOperation(), getOnTransientException());
+ fail();
+ return;
+ } catch (MieleWebserviceException e) {
+ }
+
+ // then:
+ verify(getOnTransientException()).accept(any());
+ verifyNoMoreInteractions(onTransientException);
+ }
+
+ @Test
+ public void testNullReturnValueDoesNotCauseMultipleRetries() {
+ // given:
+ when(getOperation().get()).thenReturn(null);
+
+ NTimesRetryStrategy retryStrategy = new NTimesRetryStrategy(1);
+
+ // when:
+ retryStrategy.performRetryableOperation(getOperation(), getOnTransientException());
+
+ // then:
+ verifyNoInteractions(getOnTransientException());
+ }
+
+ @Test
+ public void testSuccessfulOperation() {
+ // given:
+ Runnable operation = mock(Runnable.class);
+ doNothing().when(operation).run();
+
+ NTimesRetryStrategy retryStrategy = new NTimesRetryStrategy(1);
+
+ // when:
+ retryStrategy.performRetryableOperation(operation, getOnTransientException());
+
+ // then:
+ verify(operation).run();
+ verifyNoInteractions(getOnTransientException());
+ }
+
+ @Test
+ public void testFailingOperationCausesRetry() {
+ // given:
+ Runnable operation = mock(Runnable.class);
+ doThrow(MieleWebserviceTransientException.class).doNothing().when(operation).run();
+
+ NTimesRetryStrategy retryStrategy = new NTimesRetryStrategy(1);
+
+ // when:
+ retryStrategy.performRetryableOperation(operation, getOnTransientException());
+
+ // then:
+ verify(getOnTransientException()).accept(any());
+ verify(operation, times(2)).run();
+ verifyNoMoreInteractions(getOnTransientException());
+ }
+
+ @Test
+ public void testTwoTimesFailingOperationCausesTwoRetries() {
+ // given:
+ Runnable operation = mock(Runnable.class);
+ doThrow(MieleWebserviceTransientException.class).doThrow(MieleWebserviceTransientException.class).doNothing()
+ .when(operation).run();
+
+ NTimesRetryStrategy retryStrategy = new NTimesRetryStrategy(2);
+
+ // when:
+ retryStrategy.performRetryableOperation(operation, getOnTransientException());
+
+ // then:
+ verify(getOnTransientException(), times(2)).accept(any());
+ verify(operation, times(3)).run();
+ verifyNoMoreInteractions(getOnTransientException());
+ }
+
+ @Test
+ public void testAlwaysFailingRunnableOperationThrowsMieleWebserviceException() {
+ // given:
+ Runnable operation = mock(Runnable.class);
+ doThrow(MieleWebserviceTransientException.class).when(operation).run();
+
+ NTimesRetryStrategy retryStrategy = new NTimesRetryStrategy(1);
+
+ // when:
+ try {
+ retryStrategy.performRetryableOperation(operation, getOnTransientException());
+ fail();
+ return;
+ } catch (MieleWebserviceException e) {
+ }
+
+ // then:
+ verify(getOnTransientException()).accept(any());
+ verifyNoMoreInteractions(getOnTransientException());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.retry;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.stubbing.Answer;
+import org.openhab.binding.mielecloud.internal.util.MockUtil;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@NonNullByDefault
+public class RetryStrategyCombinerTest {
+ private static final String STRING_CONSTANT = "Some String";
+
+ private final RetryStrategy first = mock(RetryStrategy.class);
+ private final RetryStrategy second = mock(RetryStrategy.class);
+
+ @Mock
+ @Nullable
+ private Supplier<@Nullable String> supplier;
+ @Mock
+ @Nullable
+ private Consumer<Exception> consumer;
+
+ private Supplier<@Nullable String> getSupplier() {
+ assertNotNull(supplier);
+ return Objects.requireNonNull(supplier);
+ }
+
+ private Consumer<Exception> getConsumer() {
+ assertNotNull(consumer);
+ return Objects.requireNonNull(consumer);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testPerformRetryableOperationInvokesRetryStrategiesInCorrectOrder() {
+ // given:
+ when(first.<@Nullable String> performRetryableOperation(any(Supplier.class), any()))
+ .thenAnswer(new Answer<@Nullable String>() {
+ @Override
+ @Nullable
+ public String answer(@Nullable InvocationOnMock invocation) throws Throwable {
+ Supplier<String> inner = MockUtil.requireNonNull(invocation).getArgument(0);
+ return inner.get();
+ }
+ });
+ when(second.<@Nullable String> performRetryableOperation(any(Supplier.class), any()))
+ .thenAnswer(new Answer<@Nullable String>() {
+ @Override
+ @Nullable
+ public String answer(@Nullable InvocationOnMock invocation) throws Throwable {
+ Supplier<String> inner = MockUtil.requireNonNull(invocation).getArgument(0);
+ return inner.get();
+ }
+ });
+ when(getSupplier().get()).thenReturn(STRING_CONSTANT);
+
+ RetryStrategyCombiner combiner = new RetryStrategyCombiner(first, second);
+
+ // when:
+ String result = combiner.performRetryableOperation(getSupplier(), getConsumer());
+
+ // then:
+ assertEquals(STRING_CONSTANT, result);
+ verify(first).performRetryableOperation(any(Supplier.class), eq(getConsumer()));
+ verify(second).performRetryableOperation(any(Supplier.class), eq(getConsumer()));
+ verify(getSupplier()).get();
+ verifyNoMoreInteractions(first, second, getSupplier());
+ verifyNoInteractions(getConsumer());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.sse;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Random;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class ExponentialBackoffWithJitterTest {
+ private static final long RETRY_INTERVAL = 2;
+ private static final long ALTERNATIVE_RETRY_INTERVAL = 50;
+ private static final long MINIMUM_WAIT_TIME = 1;
+ private static final long ALTERNATIVE_MINIMUM_WAIT_TIME = 2;
+ private static final long MAXIMUM_WAIT_TIME = 100;
+ private static final long ALTERNATIVE_MAXIMUM_WAIT_TIME = 150;
+
+ @Test
+ public void whenMinimumWaitTimeIsSmallerThanZeroThenAnIllegalArgumentExceptionIsThrown() {
+ // when:
+ assertThrows(IllegalArgumentException.class, () -> {
+ new ExponentialBackoffWithJitter(-MINIMUM_WAIT_TIME, MAXIMUM_WAIT_TIME, RETRY_INTERVAL);
+ });
+ }
+
+ @Test
+ public void whenMaximumWaitTimeIsSmallerThanZeroThenAnIllegalArgumentExceptionIsThrown() {
+ // when:
+ assertThrows(IllegalArgumentException.class, () -> {
+ new ExponentialBackoffWithJitter(MINIMUM_WAIT_TIME, -MAXIMUM_WAIT_TIME, RETRY_INTERVAL);
+ });
+ }
+
+ @Test
+ public void whenRetryIntervalIsSmallerThanZeroThenAnIllegalArgumentExceptionIsThrown() {
+ // when:
+ assertThrows(IllegalArgumentException.class, () -> {
+ new ExponentialBackoffWithJitter(MINIMUM_WAIT_TIME, MAXIMUM_WAIT_TIME, -RETRY_INTERVAL);
+ });
+ }
+
+ @Test
+ public void whenMinimumWaitTimeIsLargerThanMaximumWaitTimeThenAnIllegalArgumentExceptionIsThrown() {
+ // when:
+ assertThrows(IllegalArgumentException.class, () -> {
+ new ExponentialBackoffWithJitter(MAXIMUM_WAIT_TIME, MINIMUM_WAIT_TIME, RETRY_INTERVAL);
+ });
+ }
+
+ @Test
+ public void whenRetryIntervalIsLargerThanMaximumWaitTimeThenAnIllegalArgumentExceptionIsThrown() {
+ // when:
+ assertThrows(IllegalArgumentException.class, () -> {
+ new ExponentialBackoffWithJitter(MINIMUM_WAIT_TIME, RETRY_INTERVAL, MAXIMUM_WAIT_TIME);
+ });
+ }
+
+ @Test
+ public void whenTheNumberOfFailedAttemptsIsNegativeThenZeroIsAssumedInstead() {
+ // given:
+ Random random = mock(Random.class);
+ when(random.nextLong()).thenReturn(RETRY_INTERVAL);
+
+ ExponentialBackoffWithJitter backoffStrategy = new ExponentialBackoffWithJitter(MINIMUM_WAIT_TIME,
+ MAXIMUM_WAIT_TIME, RETRY_INTERVAL, random);
+
+ // when:
+ long result = backoffStrategy.getSecondsUntilRetry(-10);
+
+ // then:
+ assertEquals(MINIMUM_WAIT_TIME + RETRY_INTERVAL, result);
+ }
+
+ @Test
+ public void whenThereIsNoFailedAttemptThenTheMaximalResultIsMinimumWaitTimePlusRetryInterval() {
+ // given:
+ Random random = mock(Random.class);
+ when(random.nextLong()).thenReturn(RETRY_INTERVAL);
+
+ ExponentialBackoffWithJitter backoffStrategy = new ExponentialBackoffWithJitter(MINIMUM_WAIT_TIME,
+ MAXIMUM_WAIT_TIME, RETRY_INTERVAL, random);
+
+ // when:
+ long result = backoffStrategy.getSecondsUntilRetry(0);
+
+ // then:
+ assertEquals(MINIMUM_WAIT_TIME + RETRY_INTERVAL, result);
+ }
+
+ @Test
+ public void whenThereIsOneFailedAttemptThenTheMaximalResultIsMinimumWaitTimePlusTwiceTheRetryInterval() {
+ // given:
+ Random random = mock(Random.class);
+ when(random.nextLong()).thenReturn(RETRY_INTERVAL * 2);
+
+ ExponentialBackoffWithJitter backoffStrategy = new ExponentialBackoffWithJitter(MINIMUM_WAIT_TIME,
+ MAXIMUM_WAIT_TIME, RETRY_INTERVAL, random);
+
+ // when:
+ long result = backoffStrategy.getSecondsUntilRetry(1);
+
+ // then:
+ assertEquals(MINIMUM_WAIT_TIME + RETRY_INTERVAL * 2, result);
+ }
+
+ @Test
+ public void whenThereAreTwoFailedAttemptsThenTheMaximalResultIsMinimumWaitTimePlusFourTimesTheRetryInterval() {
+ // given:
+ Random random = mock(Random.class);
+ when(random.nextLong()).thenReturn(RETRY_INTERVAL * 4);
+
+ ExponentialBackoffWithJitter backoffStrategy = new ExponentialBackoffWithJitter(MINIMUM_WAIT_TIME,
+ MAXIMUM_WAIT_TIME, RETRY_INTERVAL, random);
+
+ // when:
+ long result = backoffStrategy.getSecondsUntilRetry(2);
+
+ // then:
+ assertEquals(MINIMUM_WAIT_TIME + RETRY_INTERVAL * 4, result);
+ }
+
+ @Test
+ public void whenThereAreTwoFailedAttemptsThenTheMinimalResultIsTheMinimumWaitTime() {
+ // given:
+ Random random = mock(Random.class);
+ when(random.nextLong()).thenReturn(0L);
+
+ ExponentialBackoffWithJitter backoffStrategy = new ExponentialBackoffWithJitter(MINIMUM_WAIT_TIME,
+ MAXIMUM_WAIT_TIME, RETRY_INTERVAL, random);
+
+ // when:
+ long result = backoffStrategy.getSecondsUntilRetry(2);
+
+ // then:
+ assertEquals(MINIMUM_WAIT_TIME, result);
+ }
+
+ @Test
+ public void whenTheDrawnRandomValueIsNegativeThenItIsProjectedToAPositiveValue() {
+ // given:
+ Random random = mock(Random.class);
+ when(random.nextLong()).thenReturn(-RETRY_INTERVAL * 4 - 1);
+
+ ExponentialBackoffWithJitter backoffStrategy = new ExponentialBackoffWithJitter(MINIMUM_WAIT_TIME,
+ MAXIMUM_WAIT_TIME, RETRY_INTERVAL, random);
+
+ // when:
+ long result = backoffStrategy.getSecondsUntilRetry(2);
+
+ // then:
+ assertEquals(MINIMUM_WAIT_TIME, result);
+ }
+
+ @Test
+ public void whenTheResultWouldBeLargerThanTheMaximumThenItIsCappedToTheMaximum() {
+ // given:
+ Random random = mock(Random.class);
+ when(random.nextLong()).thenReturn(MAXIMUM_WAIT_TIME - ALTERNATIVE_MINIMUM_WAIT_TIME);
+
+ ExponentialBackoffWithJitter backoffStrategy = new ExponentialBackoffWithJitter(ALTERNATIVE_MINIMUM_WAIT_TIME,
+ MAXIMUM_WAIT_TIME, ALTERNATIVE_RETRY_INTERVAL, random);
+
+ // when:
+ long result = backoffStrategy.getSecondsUntilRetry(2);
+
+ // then:
+ assertEquals(MAXIMUM_WAIT_TIME, result);
+ }
+
+ @Test
+ public void whenTheResultWouldBeLargerThanTheAlternativeMaximumThenItIsCappedToTheAlternativeMaximum() {
+ // given:
+ Random random = mock(Random.class);
+ when(random.nextLong()).thenReturn(ALTERNATIVE_MAXIMUM_WAIT_TIME - ALTERNATIVE_MINIMUM_WAIT_TIME);
+
+ ExponentialBackoffWithJitter backoffStrategy = new ExponentialBackoffWithJitter(ALTERNATIVE_MINIMUM_WAIT_TIME,
+ ALTERNATIVE_MAXIMUM_WAIT_TIME, ALTERNATIVE_RETRY_INTERVAL, random);
+
+ // when:
+ long result = backoffStrategy.getSecondsUntilRetry(2);
+
+ // then:
+ assertEquals(ALTERNATIVE_MAXIMUM_WAIT_TIME, result);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.sse;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.*;
+
+import java.util.Objects;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.api.Response.CompleteListener;
+import org.eclipse.jetty.client.api.Response.HeadersListener;
+import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.http.HttpFields;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentMatchers;
+import org.openhab.binding.mielecloud.internal.webservice.ConnectionError;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceDisconnectSseException;
+import org.openhab.binding.mielecloud.internal.webservice.retry.AuthorizationFailedRetryStrategy;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class SseConnectionTest {
+ private final String URL = "https://openhab.org/";
+
+ @Nullable
+ private Request request;
+
+ @Nullable
+ private SseRequestFactory sseRequestFactory;
+
+ @Nullable
+ private ScheduledExecutorService scheduler;
+
+ @Nullable
+ private BackoffStrategy backoffStrategy;
+
+ @Nullable
+ private SseListener sseListener;
+
+ @Nullable
+ private SseConnection sseConnection;
+
+ @Nullable
+ private HeadersListener registeredHeadersListener;
+
+ @Nullable
+ private CompleteListener registeredCompleteListener;
+
+ private SseRequestFactory mockSseRequestFactory(@Nullable Request request) {
+ SseRequestFactory factory = mock(SseRequestFactory.class);
+ when(factory.createSseRequest(URL)).thenReturn(request);
+ return factory;
+ }
+
+ private ScheduledExecutorService mockScheduler() {
+ return mock(ScheduledExecutorService.class);
+ }
+
+ private Request mockRequest() {
+ Request request = mock(Request.class);
+ when(request.onResponseHeaders(any())).thenAnswer(invocation -> {
+ registeredHeadersListener = invocation.getArgument(0);
+ return request;
+ });
+ when(request.onComplete(any())).thenAnswer(invocation -> {
+ registeredCompleteListener = invocation.getArgument(0);
+ return request;
+ });
+ when(request.idleTimeout(anyLong(), any())).thenReturn(request);
+ when(request.timeout(anyLong(), any())).thenReturn(request);
+ return request;
+ }
+
+ private BackoffStrategy mockBackoffStrategy() {
+ BackoffStrategy backoffStrategy = mock(BackoffStrategy.class);
+ when(backoffStrategy.getSecondsUntilRetry(anyInt())).thenReturn(10L);
+ when(backoffStrategy.getMinimumSecondsUntilRetry()).thenReturn(5L);
+ when(backoffStrategy.getMaximumSecondsUntilRetry()).thenReturn(3600L);
+ return backoffStrategy;
+ }
+
+ private void setUpRunningConnection() {
+ request = mockRequest();
+ sseRequestFactory = mockSseRequestFactory(request);
+ scheduler = mockScheduler();
+ backoffStrategy = mockBackoffStrategy();
+ sseConnection = new SseConnection(URL, getMockedSseRequestFactory(), getMockedScheduler(),
+ getMockedBackoffStrategy());
+
+ sseListener = mock(SseListener.class);
+ getSseConnection().addSseListener(getMockedSseListener());
+ getSseConnection().connect();
+
+ getRegisteredHeadersListener().onHeaders(null);
+ }
+
+ private Request getMockedRequest() {
+ Request request = this.request;
+ assertNotNull(request);
+ return Objects.requireNonNull(request);
+ }
+
+ private SseRequestFactory getMockedSseRequestFactory() {
+ SseRequestFactory sseRequestFactory = this.sseRequestFactory;
+ assertNotNull(sseRequestFactory);
+ return Objects.requireNonNull(sseRequestFactory);
+ }
+
+ private ScheduledExecutorService getMockedScheduler() {
+ ScheduledExecutorService scheduler = this.scheduler;
+ assertNotNull(scheduler);
+ return Objects.requireNonNull(scheduler);
+ }
+
+ private BackoffStrategy getMockedBackoffStrategy() {
+ BackoffStrategy backoffStrategy = this.backoffStrategy;
+ assertNotNull(backoffStrategy);
+ return Objects.requireNonNull(backoffStrategy);
+ }
+
+ private SseListener getMockedSseListener() {
+ SseListener sseListener = this.sseListener;
+ assertNotNull(sseListener);
+ return Objects.requireNonNull(sseListener);
+ }
+
+ private SseConnection getSseConnection() {
+ SseConnection sseConnection = this.sseConnection;
+ assertNotNull(sseConnection);
+ return Objects.requireNonNull(sseConnection);
+ }
+
+ private HeadersListener getRegisteredHeadersListener() {
+ HeadersListener headersListener = registeredHeadersListener;
+ assertNotNull(headersListener);
+ return Objects.requireNonNull(headersListener);
+ }
+
+ private CompleteListener getRegisteredCompleteListener() {
+ CompleteListener completeListener = registeredCompleteListener;
+ assertNotNull(completeListener);
+ return Objects.requireNonNull(completeListener);
+ }
+
+ @Test
+ public void whenSseConnectionIsConnectedThenTheConnectionRequestIsMade() throws Exception {
+ // given:
+ Request request = mockRequest();
+ SseRequestFactory sseRequestFactory = mockSseRequestFactory(request);
+ ScheduledExecutorService scheduler = mockScheduler();
+ SseConnection sseConnection = new SseConnection(URL, sseRequestFactory, scheduler);
+
+ // when:
+ sseConnection.connect();
+
+ // then:
+ verify(request).send(any());
+ }
+
+ @Test
+ public void whenSseConnectionIsConnectedButNoRequestIsCreatedThenOnlyTheDesiredConnectionStateChanges()
+ throws Exception {
+ // given:
+ SseRequestFactory sseRequestFactory = mockSseRequestFactory(null);
+ ScheduledExecutorService scheduler = mockScheduler();
+ SseConnection sseConnection = new SseConnection(URL, sseRequestFactory, scheduler);
+
+ // when:
+ sseConnection.connect();
+
+ // then:
+ assertTrue(((Boolean) getPrivate(sseConnection, "active")).booleanValue());
+ }
+
+ @Test
+ public void whenHeadersAreReceivedAfterTheSseConnectionWasConnectedThenTheEventStreamParserIsScheduled()
+ throws Exception {
+ // given:
+ Request request = mockRequest();
+ SseRequestFactory sseRequestFactory = mockSseRequestFactory(request);
+ ScheduledExecutorService scheduler = mockScheduler();
+ SseConnection sseConnection = new SseConnection(URL, sseRequestFactory, scheduler);
+ sseConnection.connect();
+ HeadersListener headersListener = registeredHeadersListener;
+ assertNotNull(headersListener);
+
+ // when:
+ headersListener.onHeaders(null);
+
+ // then:
+ verify(scheduler).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+ }
+
+ @Test
+ public void whenTheSseStreamIsClosedWithATimeoutThenAReconnectIsScheduledAndTheListenersAreNotified()
+ throws Exception {
+ // given:
+ setUpRunningConnection();
+
+ // when:
+ invokePrivate(getSseConnection(), "onSseStreamClosed", new Class[] { Throwable.class }, new TimeoutException());
+
+ // then:
+ verify(getMockedScheduler(), times(2)).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+ verify(getMockedSseListener()).onConnectionError(ConnectionError.TIMEOUT, 0);
+ verify(getMockedBackoffStrategy()).getSecondsUntilRetry(anyInt());
+ }
+
+ @Test
+ public void whenTheSseStreamIsClosedDueToAJetty401ErrorThenNoReconnectIsScheduledAndATokenRefreshIsRequested()
+ throws Exception {
+ // given:
+ setUpRunningConnection();
+
+ // when:
+ invokePrivate(getSseConnection(), "onSseStreamClosed", new Class[] { Throwable.class }, new RuntimeException(
+ AuthorizationFailedRetryStrategy.JETTY_401_HEADER_BODY_MISMATCH_EXCEPTION_MESSAGE));
+
+ // then:
+ verify(getMockedScheduler()).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+ verifyNoMoreInteractions(getMockedScheduler());
+ verify(getMockedSseListener()).onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
+ }
+
+ @Test
+ public void whenTheSseStreamIsClosedWithADifferentExceptionThanATimeoutThenAReconnectIsScheduledAndTheListenersAreNotified()
+ throws Exception {
+ // given:
+ setUpRunningConnection();
+
+ // when:
+ invokePrivate(getSseConnection(), "onSseStreamClosed", new Class[] { Throwable.class },
+ new IllegalStateException());
+
+ // then:
+ verify(getMockedScheduler(), times(2)).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+ verify(getMockedSseListener()).onConnectionError(ConnectionError.SSE_STREAM_ENDED, 0);
+ verify(getMockedBackoffStrategy()).getSecondsUntilRetry(anyInt());
+ }
+
+ @Test
+ public void whenTheSseRequestCompletesWithoutResultThenAReconnectIsScheduledAndTheListenersAreNotified()
+ throws Exception {
+ // given:
+ setUpRunningConnection();
+
+ // when:
+ getRegisteredCompleteListener().onComplete(null);
+
+ // then:
+ verify(getMockedScheduler(), times(2)).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+ verify(getMockedSseListener()).onConnectionError(ConnectionError.SSE_STREAM_ENDED, 0);
+ verify(getMockedBackoffStrategy()).getSecondsUntilRetry(anyInt());
+ }
+
+ @Test
+ public void whenTheSseRequestCompletesWithoutResponseThenAReconnectIsScheduledAndTheListenersAreNotified()
+ throws Exception {
+ // given:
+ setUpRunningConnection();
+
+ Result result = mock(Result.class);
+
+ // when:
+ getRegisteredCompleteListener().onComplete(result);
+
+ // then:
+ verify(getMockedScheduler(), times(2)).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+ verify(getMockedSseListener()).onConnectionError(ConnectionError.SSE_STREAM_ENDED, 0);
+ verify(getMockedBackoffStrategy()).getSecondsUntilRetry(anyInt());
+ }
+
+ @Test
+ public void whenTheSseRequestCompletesWithASuccessfulResponseThenAReconnectIsScheduledAndTheListenersAreNotified()
+ throws Exception {
+ // given:
+ setUpRunningConnection();
+
+ Response response = mock(Response.class);
+ when(response.getStatus()).thenReturn(200);
+
+ Result result = mock(Result.class);
+ when(result.getResponse()).thenReturn(response);
+
+ // when:
+ getRegisteredCompleteListener().onComplete(result);
+
+ // then:
+ verify(getMockedScheduler(), times(2)).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+ verify(getMockedSseListener()).onConnectionError(ConnectionError.SSE_STREAM_ENDED, 0);
+ verify(getMockedBackoffStrategy()).getSecondsUntilRetry(anyInt());
+ }
+
+ @Test
+ public void whenTheSseRequestCompletesWithAnAuthorizationFailedResponseThenTheListenersAreNotified()
+ throws Exception {
+ // given:
+ setUpRunningConnection();
+
+ Response response = mock(Response.class);
+ when(response.getStatus()).thenReturn(401);
+
+ Result result = mock(Result.class);
+ when(result.getResponse()).thenReturn(response);
+
+ // when:
+ getRegisteredCompleteListener().onComplete(result);
+
+ // then:
+ verify(getMockedScheduler()).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+ verify(getMockedSseListener()).onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
+ }
+
+ @Test
+ public void whenTheSseRequestCompletesWithATooManyRequestsResponseWithoutRetryAfterHeaderThenAReconnectIsScheduledAccordingToTheBackoffStrategyAndTheListenersAreNotified()
+ throws Exception {
+ // given:
+ setUpRunningConnection();
+
+ Response response = mock(Response.class);
+ when(response.getStatus()).thenReturn(429);
+ when(response.getHeaders()).thenReturn(new HttpFields());
+
+ Result result = mock(Result.class);
+ when(result.getResponse()).thenReturn(response);
+
+ // when:
+ getRegisteredCompleteListener().onComplete(result);
+
+ // then:
+ verify(getMockedScheduler(), times(2)).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+ verify(getMockedScheduler()).schedule(ArgumentMatchers.<Runnable> any(), eq(10L), eq(TimeUnit.SECONDS));
+ verify(getMockedSseListener()).onConnectionError(ConnectionError.TOO_MANY_RERQUESTS, 0);
+ verify(getMockedBackoffStrategy()).getSecondsUntilRetry(anyInt());
+ }
+
+ @Test
+ public void whenTheSseRequestCompletesWithATooManyRequestsResponseWithRetryAfterHeaderThenAReconnectIsScheduledAndTheListenersAreNotified()
+ throws Exception {
+ // given:
+ setUpRunningConnection();
+
+ Response response = mock(Response.class);
+ when(response.getStatus()).thenReturn(429);
+ HttpFields httpFields = new HttpFields();
+ httpFields.add("Retry-After", "3600");
+ when(response.getHeaders()).thenReturn(httpFields);
+
+ Result result = mock(Result.class);
+ when(result.getResponse()).thenReturn(response);
+
+ // when:
+ getRegisteredCompleteListener().onComplete(result);
+
+ // then:
+ verify(getMockedScheduler()).schedule(ArgumentMatchers.<Runnable> any(), eq(3600L), eq(TimeUnit.SECONDS));
+ verify(getMockedSseListener()).onConnectionError(ConnectionError.TOO_MANY_RERQUESTS, 0);
+ }
+
+ @Test
+ public void whenTheSseRequestCompletesWithATooManyRequestsResponseWithRetryAfterHeaderWithTooLowValueThenAReconnectIsScheduledWithTheMinimumWaitTime()
+ throws Exception {
+ // given:
+ setUpRunningConnection();
+
+ Response response = mock(Response.class);
+ when(response.getStatus()).thenReturn(429);
+ HttpFields httpFields = new HttpFields();
+ httpFields.add("Retry-After", "1");
+ when(response.getHeaders()).thenReturn(httpFields);
+
+ Result result = mock(Result.class);
+ when(result.getResponse()).thenReturn(response);
+
+ // when:
+ getRegisteredCompleteListener().onComplete(result);
+
+ // then:
+ verify(getMockedScheduler()).schedule(ArgumentMatchers.<Runnable> any(), eq(5L), eq(TimeUnit.SECONDS));
+ verify(getMockedSseListener()).onConnectionError(ConnectionError.TOO_MANY_RERQUESTS, 0);
+ }
+
+ @Test
+ public void whenTheSseRequestCompletesWithATooManyRequestsResponseWithRetryAfterHeaderWithTooHighValueThenAReconnectIsScheduledWithTheMaximumWaitTime()
+ throws Exception {
+ // given:
+ setUpRunningConnection();
+
+ Response response = mock(Response.class);
+ when(response.getStatus()).thenReturn(429);
+ HttpFields httpFields = new HttpFields();
+ httpFields.add("Retry-After", "3601");
+ when(response.getHeaders()).thenReturn(httpFields);
+
+ Result result = mock(Result.class);
+ when(result.getResponse()).thenReturn(response);
+
+ // when:
+ getRegisteredCompleteListener().onComplete(result);
+
+ // then:
+ verify(getMockedScheduler()).schedule(ArgumentMatchers.<Runnable> any(), eq(3600L), eq(TimeUnit.SECONDS));
+ verify(getMockedSseListener()).onConnectionError(ConnectionError.TOO_MANY_RERQUESTS, 0);
+ }
+
+ @Test
+ public void whenTheSseRequestCompletesWithAnInternalServerErrorResponseThenAReconnectIsScheduledAndTheListenersAreNotified()
+ throws Exception {
+ // given:
+ setUpRunningConnection();
+
+ Response response = mock(Response.class);
+ when(response.getStatus()).thenReturn(500);
+
+ Result result = mock(Result.class);
+ when(result.getResponse()).thenReturn(response);
+
+ // when:
+ getRegisteredCompleteListener().onComplete(result);
+
+ // then:
+ verify(getMockedScheduler(), times(2)).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+ verify(getMockedSseListener()).onConnectionError(ConnectionError.SERVER_ERROR, 0);
+ }
+
+ @Test
+ public void whenTheSseRequestCompletesWithAnInternalServerErrorResponseMultipleTimesThenTheConnectionFailedCounterIsIncrementedEachTime()
+ throws Exception {
+ // given:
+ setUpRunningConnection();
+
+ Response response = mock(Response.class);
+ when(response.getStatus()).thenReturn(500);
+
+ Result result = mock(Result.class);
+ when(result.getResponse()).thenReturn(response);
+
+ // when:
+ getRegisteredCompleteListener().onComplete(result);
+ getRegisteredCompleteListener().onComplete(result);
+
+ // then:
+ verify(getMockedSseListener()).onConnectionError(ConnectionError.SERVER_ERROR, 0);
+ verify(getMockedSseListener()).onConnectionError(ConnectionError.SERVER_ERROR, 1);
+ }
+
+ @Test
+ public void whenTheSseRequestCompletesWithAnUnknownErrorResponseThenAReconnectIsScheduledAndTheListenersAreNotified()
+ throws Exception {
+ // given:
+ setUpRunningConnection();
+
+ Response response = mock(Response.class);
+ when(response.getStatus()).thenReturn(600);
+
+ Result result = mock(Result.class);
+ when(result.getResponse()).thenReturn(response);
+
+ // when:
+ getRegisteredCompleteListener().onComplete(result);
+
+ // then:
+ verify(getMockedScheduler(), times(2)).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+ verify(getMockedSseListener()).onConnectionError(ConnectionError.OTHER_HTTP_ERROR, 0);
+ verify(getMockedBackoffStrategy()).getSecondsUntilRetry(anyInt());
+ }
+
+ @Test
+ public void whenAServerSentEventIsReceivedThenItIsForwardedToTheListenersAndTheFailedConnectionCounterIsReset()
+ throws Exception {
+ // given:
+ Request request = mockRequest();
+ SseRequestFactory sseRequestFactory = mockSseRequestFactory(request);
+ ScheduledExecutorService scheduler = mockScheduler();
+
+ BackoffStrategy backoffStrategy = mock(BackoffStrategy.class);
+ when(backoffStrategy.getSecondsUntilRetry(anyInt())).thenReturn(10L);
+
+ SseConnection sseConnection = new SseConnection(URL, sseRequestFactory, scheduler, backoffStrategy);
+ SseListener sseListener = mock(SseListener.class);
+ sseConnection.addSseListener(sseListener);
+ setPrivate(sseConnection, "failedConnectionAttempts", 10);
+ sseConnection.connect();
+
+ HeadersListener headersListener = registeredHeadersListener;
+ assertNotNull(headersListener);
+ headersListener.onHeaders(null);
+
+ ServerSentEvent serverSentEvent = new ServerSentEvent("ping", "ping");
+
+ // when:
+ invokePrivate(sseConnection, "onServerSentEvent", serverSentEvent);
+
+ // then:
+ verify(sseListener).onServerSentEvent(serverSentEvent);
+ assertEquals(0, (int) getPrivate(sseConnection, "failedConnectionAttempts"));
+ }
+
+ @Test
+ public void whenTheSseStreamIsDisconnectedThenTheRunningRequestIsAborted() throws Exception {
+ // given:
+ setUpRunningConnection();
+
+ // when:
+ getSseConnection().disconnect();
+
+ // then:
+ verify(getMockedRequest()).abort(any());
+ assertNull(getPrivate(getSseConnection(), "sseRequest"));
+ }
+
+ @Test
+ public void whenTheSseStreamIsDisconnectedThenTheConnectionIsClosedAndNoReconnectIsScheduledAndTheListenersAreNotNotified()
+ throws Exception {
+ // given:
+ setUpRunningConnection();
+
+ // when:
+ getSseConnection().disconnect();
+ invokePrivate(getSseConnection(), "onSseStreamClosed", new Class[] { Throwable.class },
+ new MieleWebserviceDisconnectSseException());
+
+ // then:
+ verify(getMockedScheduler()).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+ verifyNoMoreInteractions(getMockedScheduler());
+ verifyNoInteractions(getMockedSseListener());
+ }
+
+ @Test
+ public void whenAPendingReconnectAttemptIsPerformedAfterTheSseConnectionWasDisconnectedThenTheConnectionIsNotRestored()
+ throws Exception {
+ // given:
+ setUpRunningConnection();
+ getSseConnection().disconnect();
+
+ // when:
+ invokePrivate(getSseConnection(), "connectInternal");
+
+ // then:
+ verify(getMockedScheduler()).schedule(ArgumentMatchers.<Runnable> any(), anyLong(), any());
+ verifyNoMoreInteractions(getMockedScheduler());
+ verifyNoInteractions(getMockedSseListener());
+ }
+
+ @Test
+ public void whenTheSseConnectionIsConnectedMultipleTimesWithoutDisconnectingThenOnlyTheFirstConnectResultsInAnConnectionAttempt()
+ throws Exception {
+ // given:
+ Request request = mockRequest();
+ SseRequestFactory sseRequestFactory = mockSseRequestFactory(request);
+ ScheduledExecutorService scheduler = mockScheduler();
+ SseConnection sseConnection = new SseConnection(URL, sseRequestFactory, scheduler);
+ sseConnection.connect();
+
+ // when:
+ sseConnection.connect();
+
+ // then:
+ verify(request, times(1)).onResponseHeaders(any());
+ }
+
+ @Test
+ public void whenTheSseConnectionIsDisconnectedMultipleTimesWithoutConnectingAgainThenOnlyTheFirstDisconnectIsPerformed()
+ throws Exception {
+ // given:
+ Request request = mockRequest();
+ SseRequestFactory sseRequestFactory = mockSseRequestFactory(request);
+ ScheduledExecutorService scheduler = mockScheduler();
+ SseConnection sseConnection = new SseConnection(URL, sseRequestFactory, scheduler);
+ sseConnection.connect();
+ sseConnection.disconnect();
+
+ // when:
+ sseConnection.disconnect();
+
+ // then:
+ verify(request, times(1)).abort(any());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.webservice.sse;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceDisconnectSseException;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@NonNullByDefault
+public class SseStreamParserTest {
+ @Mock
+ @NonNullByDefault({})
+ private Consumer<ServerSentEvent> serverSentEventCallback;
+
+ @Mock
+ @NonNullByDefault({})
+ private Consumer<@Nullable Throwable> streamClosedCallback;
+
+ private InputStream getInputStreamReadingUtf8Data(String data) {
+ return new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8));
+ }
+
+ @Test
+ public void whenNoEventIsProvidedThenTheStreamClosedCallbackIsInvoked() {
+ // given:
+ InputStream inputStream = getInputStreamReadingUtf8Data("");
+
+ SseStreamParser parser = new SseStreamParser(inputStream, serverSentEventCallback, streamClosedCallback);
+
+ // when:
+ parser.parseAndDispatchEvents();
+
+ // then:
+ verify(streamClosedCallback).accept(null);
+ verifyNoMoreInteractions(streamClosedCallback);
+ verifyNoInteractions(serverSentEventCallback);
+ }
+
+ @Test
+ public void whenNoEventAndOnlyWhitespaceIsProvidedThenTheStreamClosedCallbackIsInvoked() {
+ // given:
+ InputStream inputStream = getInputStreamReadingUtf8Data("\r\n");
+
+ SseStreamParser parser = new SseStreamParser(inputStream, serverSentEventCallback, streamClosedCallback);
+
+ // when:
+ parser.parseAndDispatchEvents();
+
+ // then:
+ verify(streamClosedCallback).accept(null);
+ verifyNoMoreInteractions(streamClosedCallback);
+ verifyNoInteractions(serverSentEventCallback);
+ }
+
+ @Test
+ public void whenAnEventIsProvidedThenItIsPassedToTheCallback() {
+ // given:
+ InputStream inputStream = getInputStreamReadingUtf8Data("event: ping\r\ndata: pong\r\n");
+
+ SseStreamParser parser = new SseStreamParser(inputStream, serverSentEventCallback, streamClosedCallback);
+
+ // when:
+ parser.parseAndDispatchEvents();
+
+ // then:
+ verify(streamClosedCallback).accept(null);
+ verify(serverSentEventCallback).accept(new ServerSentEvent("ping", "pong"));
+ verifyNoMoreInteractions(streamClosedCallback, serverSentEventCallback);
+ }
+
+ @Test
+ public void whenALineWithInvalidKeyIsProvidedThenItIsIgnored() {
+ // given:
+ InputStream inputStream = getInputStreamReadingUtf8Data("name: ping\r\n");
+
+ SseStreamParser parser = new SseStreamParser(inputStream, serverSentEventCallback, streamClosedCallback);
+
+ // when:
+ parser.parseAndDispatchEvents();
+
+ // then:
+ verify(streamClosedCallback).accept(null);
+ verifyNoMoreInteractions(streamClosedCallback);
+ verifyNoInteractions(serverSentEventCallback);
+ }
+
+ @Test
+ public void whenDataWithoutEventIsProvidedThenItIsIgnored() {
+ // given:
+ InputStream inputStream = getInputStreamReadingUtf8Data("data: ping\r\n");
+
+ SseStreamParser parser = new SseStreamParser(inputStream, serverSentEventCallback, streamClosedCallback);
+
+ // when:
+ parser.parseAndDispatchEvents();
+
+ // then:
+ verify(streamClosedCallback).accept(null);
+ verifyNoMoreInteractions(streamClosedCallback);
+ verifyNoInteractions(serverSentEventCallback);
+ }
+
+ @Test
+ public void whenTheEventStreamBreaksThenTheStreamClosedCallbackIsNotifiedWithTheCause() throws IOException {
+ // given:
+ InputStream inputStream = mock(InputStream.class);
+ TimeoutException timeoutException = new TimeoutException();
+ when(inputStream.read(any(), anyInt(), anyInt())).thenThrow(new IOException(timeoutException));
+
+ SseStreamParser parser = new SseStreamParser(inputStream, serverSentEventCallback, streamClosedCallback);
+
+ // when:
+ parser.parseAndDispatchEvents();
+
+ // then:
+ verify(streamClosedCallback).accept(timeoutException);
+ verifyNoMoreInteractions(streamClosedCallback);
+ verifyNoInteractions(serverSentEventCallback);
+ }
+
+ @Test
+ public void whenTheEventStreamBreaksBecauseOfAnSseDisconnectThenTheStreamCloseCallbackIsNotNotifiedToPreventSseReconnect()
+ throws IOException {
+ // given:
+ InputStream inputStream = mock(InputStream.class);
+ when(inputStream.read(any(), anyInt(), anyInt()))
+ .thenThrow(new IOException(new MieleWebserviceDisconnectSseException()));
+
+ SseStreamParser parser = new SseStreamParser(inputStream, serverSentEventCallback, streamClosedCallback);
+
+ // when:
+ parser.parseAndDispatchEvents();
+
+ // then:
+ verifyNoInteractions(streamClosedCallback, serverSentEventCallback);
+ }
+
+ @Test
+ public void whenTheEventStreamBreaksAndTheResourceCleanupFailsThenItIsIgnored() throws IOException {
+ // given:
+ InputStream inputStream = mock(InputStream.class);
+ when(inputStream.read(any(), anyInt(), anyInt()))
+ .thenThrow(new IOException(new MieleWebserviceDisconnectSseException()));
+ doThrow(new IOException()).when(inputStream).close();
+
+ SseStreamParser parser = new SseStreamParser(inputStream, serverSentEventCallback, streamClosedCallback);
+
+ // when:
+ parser.parseAndDispatchEvents();
+
+ // then:
+ verifyNoInteractions(streamClosedCallback, serverSentEventCallback);
+ }
+}
--- /dev/null
+{
+ "000124430017": {
+ "ident": {
+ "type": {
+ "key_localized": "Devicetype",
+ "value_raw": 18,
+ "value_localized": "Ventilation Hood"
+ },
+ "deviceName": "My Hood",
+ "deviceIdentLabel": {
+ "fabNumber": "000124430017",
+ "fabIndex": "00",
+ "techType": "DA-6996",
+ "matNumber": "10101010",
+ "swids": [
+ "4164",
+ "20380",
+ "25226"
+ ]
+ },
+ "xkmIdentLabel": {
+ "techType": "EK039W",
+ "releaseVersion": "02.31"
+ }
+ },
+ "state": {
+ "status": {
+ "value_raw": 5,
+ "value_localized": "In use",
+ "key_localized": "State"
+ },
+ "programType": {
+ "value_raw": 0,
+ "value_localized": "",
+ "key_localized": "Programme"
+ },
+ "programPhase": {
+ "value_raw": 4609,
+ "value_localized": "",
+ "key_localized": "Phase"
+ },
+ "remainingTime": [
+ 0,
+ 0
+ ],
+ "startTime": [
+ 0,
+ 0
+ ],
+ "targetTemperature": [
+ {
+ "value_raw": -32768,
+ "value_localized": null,
+ "unit": "Celsius"
+ }
+ ],
+ "temperature": [
+ {
+ "value_raw": -32768,
+ "value_localized": null,
+ "unit": "Celsius"
+ },
+ {
+ "value_raw": -32768,
+ "value_localized": null,
+ "unit": "Celsius"
+ },
+ {
+ "value_raw": -32768,
+ "value_localized": null,
+ "unit": "Celsius"
+ }
+ ],
+ "signalInfo": false,
+ "signalFailure": false,
+ "signalDoor": false,
+ "remoteEnable": {
+ "fullRemoteControl": false,
+ "smartGrid": false
+ },
+ "light": 1,
+ "elapsedTime": [],
+ "spinningSpeed": {
+ "value_raw" : 1200,
+ "value_localized" : 1200,
+ "unit" : "rpm"
+ },
+ "dryingStep": {
+ "value_raw": null,
+ "value_localized": "",
+ "key_localized": "Drying level"
+ },
+ "ventilationStep": {
+ "value_raw": 2,
+ "value_localized": "2",
+ "key_localized": "Power Level"
+ },
+ "plateStep": [
+ {
+ "value_raw": 0,
+ "value_localized": "0",
+ "key_localized": "Plate Step"
+ },
+ {
+ "value_raw": 1,
+ "value_localized": "1",
+ "key_localized": "Plate Step"
+ },
+ {
+ "value_raw": 2,
+ "value_localized": "1.",
+ "key_localized": "Plate Step"
+ },
+ {
+ "value_raw": 3,
+ "value_localized": "2",
+ "key_localized": "Plate Step"
+ }
+ ],
+ "batteryLevel": 20
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "000091465021": {
+ "state": {
+ "targetTemperature": [
+ {
+ "value_raw": 80,
+ "value_localized": 0.8,
+ "unit": "Celsius"
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "mac-00124B000AE539D6": {
+ "ident": {
+ "type": {
+ "key_localized": "Devicetype",
+ "value_raw": 100,
+ "value_localized": ""
+ },
+ "deviceName": "Some Devicename",
+ "deviceIdentLabel": {
+ "fabNumber": "",
+ "fabIndex": "",
+ "techType": "",
+ "matNumber": "",
+ "swids": []
+ },
+ "xkmIdentLabel": {
+ "techType": "",
+ "releaseVersion": ""
+ }
+ },
+ "state": {
+ "ProgramID": {
+ "value_raw": 2499805184,
+ "value_localized": "",
+ "key_localized": "Program Id"
+ },
+ "status": {
+ "value_raw": 5,
+ "value_localized": "In use",
+ "key_localized": "State"
+ },
+ "programType": {
+ "value_raw": 0,
+ "value_localized": "Operation mode",
+ "key_localized": "Program type"
+ },
+ "programPhase": {
+ "value_raw": 0,
+ "value_localized": "",
+ "key_localized": "Phase"
+ },
+ "remainingTime": [
+ 0,
+ 0
+ ],
+ "startTime": [
+ 0,
+ 0
+ ],
+ "targetTemperature": [],
+ "signalInfo": false,
+ "signalFailure": false,
+ "signalDoor": false,
+ "remoteEnable": {
+ "fullRemoteControl": true,
+ "smartGrid": false
+ },
+ "light": 0,
+ "elapsedTime": [],
+ "dryingStep": {
+ "value_raw": null,
+ "value_localized": "",
+ "key_localized": "Drying level"
+ },
+ "ventilationStep": {
+ "value_raw": null,
+ "value_localized": "",
+ "key_localized": "Power Level"
+ }
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "000124430017": {
+ "state": {
+ "spinningSpeed": {
+ "value_raw" : 1600,
+ "value_localized" : 1600,
+ "unit" : "U/min"
+ }
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "000124430017": {
+ "ident": {
+ "type": {
+ "key_localized": "Devicetype",
+ 10",
+ "swids": [
+ "4164",
+ "20380",
+ "25226"
+ ]
+ },
+ "xkmIdentLabel": {
+ "techType": "EK039W",
+ "releaseVersion": "02.31"
+ }
+ },
+ "state": {
+ "status": {
+ "value_raw": 5,
+ "value_localized": "In use",
+ "key_localized": "State"
+ },
+ "programType": {
+ "value_raw": 0,
+ "value_localized": "",
+ "key_localized": "Programme"
+ },
+ "programPhase": {
+ "value_raw": 4609,
+ "value_localized": "",
+ "key_localized": "Phase"
+ },
+ "remainingTime": [
+ 0,
+ 0
+ ],
+ "startTime": [
+ 0,
+ 0
+ ],
+ "targetT
+ "unit": "Celsius"
+ },
+ {
+ "value_raw": -32768,
+ "value_localized": null,
+ "unit": "Celsius"
+ }
+ ],
+ "signalInfo": false,
+ "signalFailure": false,
+ "signalDoor": false,
+ "remoteEnable": {
+ "fullRemoteControl": false,
+ "smartGrid": false
+ },
+ "light": 1,
+ "elapsedTime": [],
+ "dryingStep": {
+ "value_raw": null,
+ "value_localized": "",
+ "key_localized": "Drying level"
+ },
+ "ventilationStep": {
+ "value_raw": 2,
+ "value_localized": "2",
+ "key_localized": "Power Level"
+ }
+ }
+ },
+ "$$ref": "#/components/examples/devicesExample"
+}
\ No newline at end of file
<module>org.openhab.binding.meteoblue</module>
<module>org.openhab.binding.meteostick</module>
<module>org.openhab.binding.miele</module>
+ <module>org.openhab.binding.mielecloud</module>
<module>org.openhab.binding.mihome</module>
<module>org.openhab.binding.miio</module>
<module>org.openhab.binding.millheat</module>
--- /dev/null
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
--- /dev/null
+-include: ../itest-common.bndrun
+
+Bundle-SymbolicName: ${project.artifactId}
+Fragment-Host: org.openhab.binding.mielecloud
+
+-runrequires: \
+ bnd.identity;id='org.openhab.binding.mielecloud.tests',\
+ bnd.identity;id='org.openhab.core.binding.xml',\
+ bnd.identity;id='org.openhab.core.thing.xml'
+
+-runblacklist: \
+ bnd.identity;id='org.openhab.core.storage.json',\
+ bnd.identity;id='org.openhab.core.storage.mapdb'
+
+#
+# done
+#
+-runbundles: \
+ org.eclipse.equinox.event;version='[1.4.300,1.4.301)',\
+ org.osgi.service.event;version='[1.4.0,1.4.1)',\
+ org.apache.servicemix.specs.activation-api-1.2.1;version='[1.2.1,1.2.2)',\
+ org.apache.felix.http.servlet-api;version='[1.1.2,1.1.3)',\
+ com.sun.xml.bind.jaxb-osgi;version='[2.3.3,2.3.4)',\
+ jakarta.xml.bind-api;version='[2.3.3,2.3.4)',\
+ org.opentest4j;version='[1.2.0,1.2.1)',\
+ org.hamcrest;version='[2.2.0,2.2.1)',\
+ junit-jupiter-api;version='[5.7.0,5.7.1)',\
+ junit-jupiter-engine;version='[5.7.0,5.7.1)',\
+ junit-platform-commons;version='[1.7.0,1.7.1)',\
+ junit-platform-engine;version='[1.7.0,1.7.1)',\
+ junit-platform-launcher;version='[1.7.0,1.7.1)',\
+ net.bytebuddy.byte-buddy;version='[1.10.19,1.10.20)',\
+ net.bytebuddy.byte-buddy-agent;version='[1.10.19,1.10.20)',\
+ org.glassfish.hk2.osgi-resource-locator;version='[1.0.3,1.0.4)',\
+ org.mockito.mockito-core;version='[3.7.0,3.7.1)',\
+ org.objenesis;version='[3.1.0,3.1.1)',\
+ org.openhab.binding.mielecloud;version='[3.1.0,3.1.1)',\
+ org.openhab.binding.mielecloud.tests;version='[3.1.0,3.1.1)',\
+ org.openhab.core;version='[3.1.0,3.1.1)',\
+ org.openhab.core.auth.oauth2client;version='[3.1.0,3.1.1)',\
+ org.openhab.core.binding.xml;version='[3.1.0,3.1.1)',\
+ org.openhab.core.config.core;version='[3.1.0,3.1.1)',\
+ org.openhab.core.config.discovery;version='[3.1.0,3.1.1)',\
+ org.openhab.core.config.xml;version='[3.1.0,3.1.1)',\
+ org.openhab.core.io.console;version='[3.1.0,3.1.1)',\
+ org.openhab.core.io.net;version='[3.1.0,3.1.1)',\
+ org.openhab.core.test;version='[3.1.0,3.1.1)',\
+ org.openhab.core.thing;version='[3.1.0,3.1.1)',\
+ org.openhab.core.thing.xml;version='[3.1.0,3.1.1)',\
+ biz.aQute.tester.junit-platform;version='[5.3.0,5.3.1)',\
+ com.google.gson;version='[2.8.6,2.8.7)',\
+ org.apache.felix.scr;version='[2.1.26,2.1.27)',\
+ org.objectweb.asm;version='[9.1.0,9.1.1)',\
+ org.objectweb.asm.commons;version='[9.0.0,9.0.1)',\
+ org.objectweb.asm.tree;version='[9.0.0,9.0.1)',\
+ org.osgi.util.function;version='[1.1.0,1.1.1)',\
+ org.osgi.util.promise;version='[1.1.1,1.1.2)',\
+ jakarta.annotation-api;version='[2.0.0,2.0.1)',\
+ jakarta.inject.jakarta.inject-api;version='[2.0.0,2.0.1)',\
+ javax.measure.unit-api;version='[2.1.2,2.1.3)',\
+ org.apache.felix.configadmin;version='[1.9.22,1.9.23)',\
+ org.apache.xbean.bundleutils;version='[4.19.0,4.19.1)',\
+ org.apache.xbean.finder;version='[4.19.0,4.19.1)',\
+ org.eclipse.jetty.client;version='[9.4.40,9.4.41)',\
+ org.eclipse.jetty.http;version='[9.4.40,9.4.41)',\
+ org.eclipse.jetty.io;version='[9.4.40,9.4.41)',\
+ org.eclipse.jetty.security;version='[9.4.40,9.4.41)',\
+ org.eclipse.jetty.server;version='[9.4.40,9.4.41)',\
+ org.eclipse.jetty.servlet;version='[9.4.40,9.4.41)',\
+ org.eclipse.jetty.util;version='[9.4.40,9.4.41)',\
+ org.eclipse.jetty.util.ajax;version='[9.4.40,9.4.41)',\
+ org.eclipse.jetty.websocket.api;version='[9.4.40,9.4.41)',\
+ org.eclipse.jetty.websocket.client;version='[9.4.40,9.4.41)',\
+ org.eclipse.jetty.websocket.common;version='[9.4.40,9.4.41)',\
+ org.eclipse.jetty.xml;version='[9.4.40,9.4.41)',\
+ org.glassfish.hk2.external.javax.inject;version='[2.4.0,2.4.1)',\
+ org.jsr-305;version='[3.0.2,3.0.3)',\
+ org.ops4j.pax.web.pax-web-api;version='[7.3.16,7.3.17)',\
+ org.ops4j.pax.web.pax-web-jetty;version='[7.3.16,7.3.17)',\
+ org.ops4j.pax.web.pax-web-runtime;version='[7.3.16,7.3.17)',\
+ org.ops4j.pax.web.pax-web-spi;version='[7.3.16,7.3.17)',\
+ org.osgi.service.cm;version='[1.6.0,1.6.1)',\
+ si-units;version='[2.0.1,2.0.2)',\
+ si.uom.si-quantity;version='[2.0.1,2.0.2)',\
+ tech.units.indriya;version='[2.1.2,2.1.3)',\
+ uom-lib-common;version='[2.1.0,2.1.1)',\
+ xstream;version='[1.4.17,1.4.18)',\
+ org.ops4j.pax.logging.pax-logging-api;version='[2.0.9,2.0.10)'
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.openhab.addons.itests</groupId>
+ <artifactId>org.openhab.addons.reactor.itests</artifactId>
+ <version>3.1.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>org.openhab.binding.mielecloud.tests</artifactId>
+
+ <name>openHAB Add-ons :: Integration Tests :: mielecloud Binding Tests</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.openhab.addons.bundles</groupId>
+ <artifactId>org.openhab.binding.mielecloud</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ </dependencies>
+
+</project>
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.setPrivate;
+
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.config.servlet.CreateBridgeServlet;
+import org.openhab.binding.mielecloud.internal.config.servlet.ForwardToLoginServlet;
+import org.openhab.binding.mielecloud.internal.handler.MieleBridgeHandler;
+import org.openhab.binding.mielecloud.internal.handler.MieleHandlerFactory;
+import org.openhab.binding.mielecloud.internal.util.AbstractConfigFlowTest;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.util.ReflectionUtil;
+import org.openhab.binding.mielecloud.internal.util.Website;
+import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
+import org.openhab.binding.mielecloud.internal.webservice.MieleWebserviceFactory;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+
+/**
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class ConfigFlowTest extends AbstractConfigFlowTest {
+ private void setUpAuthorizationHandler() throws NoSuchFieldException, IllegalAccessException {
+ OAuthAuthorizationHandler authorizationHandler = mock(OAuthAuthorizationHandler.class);
+ when(authorizationHandler.getAccessToken(MieleCloudBindingIntegrationTestConstants.EMAIL))
+ .thenReturn(MieleCloudBindingIntegrationTestConstants.ACCESS_TOKEN);
+ when(authorizationHandler.getBridgeUid())
+ .thenReturn(MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID);
+ when(authorizationHandler.getEmail()).thenReturn(MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+ setPrivate(getResultServlet(), "authorizationHandler", authorizationHandler);
+ }
+
+ private void setUpWebservice() throws Exception {
+ MieleWebservice webservice = mock(MieleWebservice.class);
+ doAnswer(invocation -> {
+ Thing bridge = getThingRegistry().get(MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID);
+ assertNotNull(bridge);
+ ThingHandler handler = bridge.getHandler();
+ if (handler instanceof MieleBridgeHandler) {
+ ((MieleBridgeHandler) handler).onConnectionAlive();
+ }
+ return null;
+ }).when(webservice).addConnectionStatusListener(any());
+
+ MieleWebserviceFactory webserviceFactory = mock(MieleWebserviceFactory.class);
+ when(webserviceFactory.create(any())).thenReturn(webservice);
+
+ MieleHandlerFactory handlerFactory = getService(ThingHandlerFactory.class, MieleHandlerFactory.class);
+ assertNotNull(handlerFactory);
+ setPrivate(Objects.requireNonNull(handlerFactory), "webserviceFactory", webserviceFactory);
+ }
+
+ private Website configureBridgeWithConfigFlow() throws Exception {
+ Website accountOverviewSite = getCrawler().doGetRelative("/mielecloud");
+ String pairAccountUrl = accountOverviewSite.getTargetOfLink("Pair Account");
+
+ Website pairAccountSite = getCrawler().doGetRelative(pairAccountUrl);
+ String forwardToLoginUrl = pairAccountSite.getFormAction();
+
+ Website mieleLoginSite = getCrawler().doGetRelative(forwardToLoginUrl + "?"
+ + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_ID + "&"
+ + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET + "&"
+ + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.BRIDGE_ID + "&" + ForwardToLoginServlet.EMAIL_PARAMETER_NAME
+ + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+ String redirectionUrl = mieleLoginSite.getValueOfInput("redirect_uri").replace("http://127.0.0.1:8080", "");
+ String state = mieleLoginSite.getValueOfInput("state");
+
+ Website resultSite = getCrawler().doGetRelative(redirectionUrl + "?code="
+ + MieleCloudBindingIntegrationTestConstants.AUTHORIZATION_CODE + "&state=" + state);
+ String createBridgeUrl = resultSite.getFormAction();
+
+ Website finalOverview = getCrawler().doGetRelative(createBridgeUrl + "?"
+ + CreateBridgeServlet.BRIDGE_UID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.toString() + "&"
+ + CreateBridgeServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+ return finalOverview;
+ }
+
+ @Test
+ public void configFlowHappyPathCreatesABridge() throws Exception {
+ // given:
+ setUpAuthorizationHandler();
+ setUpWebservice();
+
+ // when:
+ Website finalOverview = configureBridgeWithConfigFlow();
+
+ // then:
+ assertTrue(finalOverview.contains("<span class=\"status online\">ONLINE</span>"));
+
+ Thing bridge = getThingRegistry().get(MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID);
+ assertNotNull(bridge);
+ assertEquals(ThingStatus.ONLINE, bridge.getStatus());
+ }
+
+ @Test
+ public void configFlowWaitTimeoutExpiresWhenBridgeDoesNotComeOnline() throws Exception {
+ // given:
+ setUpAuthorizationHandler();
+ ReflectionUtil.setPrivateStaticFinal(CreateBridgeServlet.class, "ONLINE_WAIT_TIMEOUT_IN_MILLISECONDS", 0);
+
+ // when:
+ configureBridgeWithConfigFlow();
+
+ // then:
+ Thing bridge = getThingRegistry().get(MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID);
+ assertNotNull(bridge);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.*;
+
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.AbstractConfigFlowTest;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.util.ReflectionUtil;
+import org.openhab.binding.mielecloud.internal.util.Website;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ThingRegistry;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.ThingUID;
+
+/**
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class AccountOverviewServletTest extends AbstractConfigFlowTest {
+ @Test
+ public void whenAccountOverviewServletIsCalledOverNonSslConnectionThenAWarningIsShown() throws Exception {
+ // when:
+ Website accountOverviewSite = getCrawler().doGetRelative("/mielecloud");
+
+ // then:
+ assertTrue(accountOverviewSite
+ .contains("Warning: We strongly advice to proceed only with SSL enabled for a secure data exchange."));
+ assertTrue(accountOverviewSite.contains(
+ "See <a href=\"https://www.openhab.org/docs/installation/security.html\">Securing access to openHAB</a> for details."));
+ }
+
+ @Test
+ public void whenAccountOverviewServletIsCalledAndNoBridgeIsPresentThenThePageSaysThatThereIsNoBridgePaired()
+ throws Exception {
+ // when:
+ Website accountOverviewSite = getCrawler().doGetRelative("/mielecloud");
+
+ // then:
+ assertTrue(accountOverviewSite.contains("There is no account paired at the moment."));
+ }
+
+ @Test
+ public void whenAccountOverviewServletIsCalledAndBridgesArePresentThenThePageDisplaysInformationAboutThem()
+ throws Exception {
+ // given:
+ Configuration configuration1 = mock(Configuration.class);
+ when(configuration1.get(MieleCloudBindingConstants.CONFIG_PARAM_LOCALE)).thenReturn("de");
+ when(configuration1.get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL)).thenReturn("openhab@openhab.org");
+
+ Bridge bridge1 = mock(Bridge.class);
+ when(bridge1.getThingTypeUID()).thenReturn(MieleCloudBindingConstants.THING_TYPE_BRIDGE);
+ when(bridge1.getUID()).thenReturn(MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID);
+ when(bridge1.getStatus()).thenReturn(ThingStatus.ONLINE);
+ when(bridge1.getStatusInfo()).thenReturn(new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null));
+ when(bridge1.getConfiguration()).thenReturn(configuration1);
+
+ Configuration configuration2 = mock(Configuration.class);
+ when(configuration2.get(MieleCloudBindingConstants.CONFIG_PARAM_LOCALE)).thenReturn("en");
+ when(configuration2.get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL)).thenReturn("everyone@openhab.org");
+
+ Bridge bridge2 = mock(Bridge.class);
+ when(bridge2.getThingTypeUID()).thenReturn(MieleCloudBindingConstants.THING_TYPE_BRIDGE);
+ when(bridge2.getUID()).thenReturn(new ThingUID(MieleCloudBindingConstants.THING_TYPE_BRIDGE, "test"));
+ when(bridge2.getStatus()).thenReturn(ThingStatus.OFFLINE);
+ when(bridge2.getStatusInfo()).thenReturn(
+ new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error message"));
+ when(bridge2.getConfiguration()).thenReturn(configuration2);
+
+ ThingRegistry thingRegistry = mock(ThingRegistry.class);
+ when(thingRegistry.stream()).thenAnswer(invocation -> Stream.of(bridge1, bridge2));
+ ReflectionUtil.setPrivate(getAccountOverviewServlet(), "thingRegistry", thingRegistry);
+
+ // when:
+ Website accountOverviewSite = getCrawler().doGetRelative("/mielecloud");
+
+ // then:
+ assertTrue(accountOverviewSite.contains("The following bridges are paired"));
+ assertTrue(accountOverviewSite.contains("openhab@openhab.org"));
+ assertTrue(accountOverviewSite.contains("mielecloud:account:genesis"));
+ assertTrue(accountOverviewSite.contains("<span class=\"status online\">"));
+ assertTrue(accountOverviewSite.contains("everyone@openhab.org"));
+ assertTrue(accountOverviewSite.contains("mielecloud:account:test"));
+ assertTrue(accountOverviewSite.contains("<span class=\"status offline\">"));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.setPrivate;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.config.MieleCloudConfigService;
+import org.openhab.binding.mielecloud.internal.util.AbstractConfigFlowTest;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.util.Website;
+import org.openhab.core.config.discovery.inbox.Inbox;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingRegistry;
+import org.openhab.core.thing.binding.ThingHandler;
+
+/**
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class CreateBridgeServletTest extends AbstractConfigFlowTest {
+ @Test
+ public void whenBridgeCreationFailsThenAWarningIsShownOnTheSuccessPage() throws Exception {
+ // given:
+ MieleCloudConfigService configService = getService(MieleCloudConfigService.class);
+ assertNotNull(configService);
+
+ CreateBridgeServlet createBridgeServlet = configService.getCreateBridgeServlet();
+ assertNotNull(createBridgeServlet);
+
+ Inbox inbox = mock(Inbox.class);
+ when(inbox.add(any())).thenReturn(true);
+ when(inbox.approve(any(), anyString(), anyString())).thenReturn(null);
+ setPrivate(Objects.requireNonNull(createBridgeServlet), "inbox", inbox);
+
+ // when:
+ Website website = getCrawler().doGetRelative("/mielecloud/createBridgeThing?"
+ + CreateBridgeServlet.BRIDGE_UID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString() + "&"
+ + CreateBridgeServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+ // then:
+ assertTrue(website.contains("Pairing successful!"));
+ assertTrue(website.contains(
+ "Could not auto configure the bridge. Failed to approve the bridge from the inbox. Please try the configuration flow again."));
+ }
+
+ @Test
+ public void whenBridgeReconfigurationFailsDueToMissingBridgeThenAWarningIsShownOnTheSuccessPage() throws Exception {
+ // given:
+ MieleCloudConfigService configService = getService(MieleCloudConfigService.class);
+ assertNotNull(configService);
+
+ CreateBridgeServlet createBridgeServlet = configService.getCreateBridgeServlet();
+ assertNotNull(createBridgeServlet);
+
+ Inbox inbox = mock(Inbox.class);
+ when(inbox.add(any())).thenReturn(false);
+ setPrivate(Objects.requireNonNull(createBridgeServlet), "inbox", inbox);
+
+ ThingRegistry thingRegistry = mock(ThingRegistry.class);
+ when(thingRegistry.get(any())).thenReturn(null);
+ setPrivate(Objects.requireNonNull(createBridgeServlet), "thingRegistry", thingRegistry);
+
+ // when:
+ Website website = getCrawler().doGetRelative("/mielecloud/createBridgeThing?"
+ + CreateBridgeServlet.BRIDGE_UID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString() + "&"
+ + CreateBridgeServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+ // then:
+ assertTrue(website.contains("Pairing successful!"));
+ assertTrue(website.contains(
+ "Could not auto reconfigure the bridge. Bridge thing or thing handler is not available. Please try the configuration flow again."));
+ }
+
+ @Test
+ public void whenBridgeReconfigurationFailsDueToMissingBridgeHandlerThenAWarningIsShownOnTheSuccessPage()
+ throws Exception {
+ // given:
+ MieleCloudConfigService configService = getService(MieleCloudConfigService.class);
+ assertNotNull(configService);
+
+ CreateBridgeServlet createBridgeServlet = configService.getCreateBridgeServlet();
+ assertNotNull(createBridgeServlet);
+
+ Inbox inbox = mock(Inbox.class);
+ when(inbox.add(any())).thenReturn(false);
+ setPrivate(Objects.requireNonNull(createBridgeServlet), "inbox", inbox);
+
+ Thing bridge = mock(Thing.class);
+ when(bridge.getHandler()).thenReturn(null);
+
+ ThingRegistry thingRegistry = mock(ThingRegistry.class);
+ when(thingRegistry.get(any())).thenReturn(bridge);
+ setPrivate(Objects.requireNonNull(createBridgeServlet), "thingRegistry", thingRegistry);
+
+ // when:
+ Website website = getCrawler().doGetRelative("/mielecloud/createBridgeThing?"
+ + CreateBridgeServlet.BRIDGE_UID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString() + "&"
+ + CreateBridgeServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+ // then:
+ assertTrue(website.contains("Pairing successful!"));
+ assertTrue(website.contains(
+ "Could not auto reconfigure the bridge. Bridge thing or thing handler is not available. Please try the configuration flow again."));
+ }
+
+ @Test
+ public void whenBridgeIsReconfiguredThenTheConfigurationParametersAreUpdatedAndTheOverviewPageIsDisplayed()
+ throws Exception {
+ // given:
+ setUpBridge();
+
+ MieleCloudConfigService configService = getService(MieleCloudConfigService.class);
+ assertNotNull(configService);
+
+ CreateBridgeServlet createBridgeServlet = configService.getCreateBridgeServlet();
+ assertNotNull(createBridgeServlet);
+
+ OAuthTokenRefresher tokenRefresher = mock(OAuthTokenRefresher.class);
+ when(tokenRefresher.getAccessTokenFromStorage(anyString()))
+ .thenReturn(Optional.of(MieleCloudBindingIntegrationTestConstants.ALTERNATIVE_ACCESS_TOKEN));
+
+ Thing bridge = getThingRegistry().get(MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID);
+ assertNotNull(bridge);
+ ThingHandler bridgeHandler = bridge.getHandler();
+ assertNotNull(bridgeHandler);
+ setPrivate(Objects.requireNonNull(bridgeHandler), "tokenRefresher", tokenRefresher);
+
+ // when:
+ Website website = getCrawler().doGetRelative("/mielecloud/createBridgeThing?"
+ + CreateBridgeServlet.BRIDGE_UID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString() + "&"
+ + CreateBridgeServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+ // then:
+ assertTrue(website.contains("<li class=\"active\">Overview</li>"));
+
+ assertEquals(MieleCloudBindingIntegrationTestConstants.ALTERNATIVE_ACCESS_TOKEN,
+ bridge.getProperties().get(MieleCloudBindingConstants.PROPERTY_ACCESS_TOKEN));
+ }
+
+ @Test
+ public void whenNoBridgeUidIsPassedToBridgeCreationThenTheBrowserIsRedirectedToTheFailurePageAndAnErrorIsShown()
+ throws Exception {
+ // when:
+ Website website = getCrawler().doGetRelative("/mielecloud/createBridgeThing?"
+ + CreateBridgeServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+ // then:
+ assertTrue(website.contains("Pairing failed!"));
+ assertTrue(website.contains("Missing bridge UID."));
+ }
+
+ @Test
+ public void whenAnEmptyBridgeUidIsPassedToBridgeCreationThenTheBrowserIsRedirectedToTheFailurePageAndAnErrorIsShown()
+ throws Exception {
+ // when:
+ Website website = getCrawler().doGetRelative("/mielecloud/createBridgeThing?"
+ + CreateBridgeServlet.BRIDGE_UID_PARAMETER_NAME + "=&" + CreateBridgeServlet.EMAIL_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+ // then:
+ assertTrue(website.contains("Pairing failed!"));
+ assertTrue(website.contains("Missing bridge UID."));
+ }
+
+ @Test
+ public void whenAMalformedBridgeUidIsPassedToBridgeCreationThenTheBrowserIsRedirectedToTheFailurePageAndAnErrorIsShown()
+ throws Exception {
+ // when:
+ Website website = getCrawler().doGetRelative("/mielecloud/createBridgeThing?"
+ + CreateBridgeServlet.BRIDGE_UID_PARAMETER_NAME + "=gen!e!sis&"
+ + CreateBridgeServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+ // then:
+ assertTrue(website.contains("Pairing failed!"));
+ assertTrue(website.contains("Malformed bridge UID."));
+ }
+
+ @Test
+ public void whenNoEmailIsPassedToBridgeCreationThenTheBrowserIsRedirectedToTheFailurePageAndAnErrorIsShown()
+ throws Exception {
+ // when:
+ Website website = getCrawler()
+ .doGetRelative("/mielecloud/createBridgeThing?" + CreateBridgeServlet.BRIDGE_UID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString());
+
+ // then:
+ assertTrue(website.contains("Pairing failed!"));
+ assertTrue(website.contains("Missing e-mail address."));
+ }
+
+ @Test
+ public void whenAnEmptyEmailIsPassedToBridgeCreationThenTheBrowserIsRedirectedToTheFailurePageAndAnErrorIsShown()
+ throws Exception {
+ // when:
+ Website website = getCrawler()
+ .doGetRelative("/mielecloud/createBridgeThing?" + CreateBridgeServlet.BRIDGE_UID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString() + "&"
+ + CreateBridgeServlet.EMAIL_PARAMETER_NAME + "=");
+
+ // then:
+ assertTrue(website.contains("Pairing failed!"));
+ assertTrue(website.contains("Missing e-mail address."));
+ }
+
+ @Test
+ public void whenAMalformedEmailIsPassedToBridgeCreationThenTheBrowserIsRedirectedToTheFailurePageAndAnErrorIsShown()
+ throws Exception {
+ // when:
+ Website website = getCrawler()
+ .doGetRelative("/mielecloud/createBridgeThing?" + CreateBridgeServlet.BRIDGE_UID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString() + "&"
+ + CreateBridgeServlet.EMAIL_PARAMETER_NAME + "=openhab.openhab.org");
+
+ // then:
+ assertTrue(website.contains("Pairing failed!"));
+ assertTrue(website.contains("Malformed e-mail address."));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.setPrivate;
+
+import java.time.LocalDateTime;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.auth.OAuthException;
+import org.openhab.binding.mielecloud.internal.config.OAuthAuthorizationHandler;
+import org.openhab.binding.mielecloud.internal.config.exception.NoOngoingAuthorizationException;
+import org.openhab.binding.mielecloud.internal.config.exception.OngoingAuthorizationException;
+import org.openhab.binding.mielecloud.internal.util.AbstractConfigFlowTest;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.util.Website;
+
+/**
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class ForwardToLoginServletTest extends AbstractConfigFlowTest {
+ @Test
+ public void whenAuthorizationCannotBeBegunThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed()
+ throws Exception {
+ // given:
+ OAuthAuthorizationHandler authorizationHandler = mock(OAuthAuthorizationHandler.class);
+ doThrow(new OngoingAuthorizationException("", LocalDateTime.now().plusMinutes(3))).when(authorizationHandler)
+ .beginAuthorization(anyString(), anyString(), any(), anyString());
+ setPrivate(getForwardToLoginServlet(), "authorizationHandler", authorizationHandler);
+
+ // when:
+ Website maybePairAccountSite = getCrawler().doGetRelative("/mielecloud/forwardToLogin?"
+ + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_ID + "&"
+ + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET + "&"
+ + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.BRIDGE_ID + "&" + ForwardToLoginServlet.EMAIL_PARAMETER_NAME
+ + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+ // then:
+ assertTrue(maybePairAccountSite.contains(
+ "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+ assertTrue(maybePairAccountSite.contains(
+ "There is an authorization ongoing at the moment. Please complete that authorization prior to starting a new one or try again in 3 minutes."));
+ }
+
+ @Test
+ public void whenNoAuthorizationIsOngoingWhenTheAuthorizationUrlIsRequestedThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed()
+ throws Exception {
+ // given:
+ OAuthAuthorizationHandler authorizationHandler = mock(OAuthAuthorizationHandler.class);
+ doThrow(new NoOngoingAuthorizationException("")).when(authorizationHandler).getAuthorizationUrl(anyString());
+ setPrivate(getForwardToLoginServlet(), "authorizationHandler", authorizationHandler);
+
+ // when:
+ Website maybePairAccountSite = getCrawler().doGetRelative("/mielecloud/forwardToLogin?"
+ + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_ID + "&"
+ + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET + "&"
+ + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.BRIDGE_ID + "&" + ForwardToLoginServlet.EMAIL_PARAMETER_NAME
+ + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+ // then:
+ assertTrue(maybePairAccountSite.contains(
+ "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+ assertTrue(maybePairAccountSite.contains(
+ "Failed to start auhtorization process. Are you trying to perform multiple authorizations at the same time?"));
+ }
+
+ @Test
+ public void whenNoClientIdIsPassedThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed() throws Exception {
+ // when:
+ Website maybePairAccountSite = getCrawler().doGetRelative("/mielecloud/forwardToLogin?"
+ + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET + "&"
+ + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.BRIDGE_ID + "&" + ForwardToLoginServlet.EMAIL_PARAMETER_NAME
+ + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+ // then:
+ assertTrue(maybePairAccountSite.contains(
+ "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+ assertTrue(maybePairAccountSite.contains("Missing client ID."));
+ }
+
+ @Test
+ public void whenAnEmptyClientIdIsPassedThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed()
+ throws Exception {
+ // when:
+ Website maybePairAccountSite = getCrawler().doGetRelative("/mielecloud/forwardToLogin?"
+ + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "=&"
+ + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET + "&"
+ + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.BRIDGE_ID + "&" + ForwardToLoginServlet.EMAIL_PARAMETER_NAME
+ + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+ // then:
+ assertTrue(maybePairAccountSite.contains(
+ "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+ assertTrue(maybePairAccountSite.contains("Missing client ID."));
+ }
+
+ @Test
+ public void whenNoClientSecretIsPassedThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed()
+ throws Exception {
+ // when:
+ Website maybePairAccountSite = getCrawler().doGetRelative("/mielecloud/forwardToLogin?"
+ + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_ID + "&"
+ + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.BRIDGE_ID + "&" + ForwardToLoginServlet.EMAIL_PARAMETER_NAME
+ + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+ // then:
+ assertTrue(maybePairAccountSite.contains(
+ "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+ assertTrue(maybePairAccountSite.contains("Missing client secret."));
+ }
+
+ @Test
+ public void whenAnEmptyClientSecretIsPassedThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed()
+ throws Exception {
+ // when:
+ Website maybePairAccountSite = getCrawler().doGetRelative("/mielecloud/forwardToLogin?"
+ + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_ID + "&"
+ + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "=" + "&"
+ + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.BRIDGE_ID + "&" + ForwardToLoginServlet.EMAIL_PARAMETER_NAME
+ + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+ // then:
+ assertTrue(maybePairAccountSite.contains(
+ "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+ assertTrue(maybePairAccountSite.contains("Missing client secret."));
+ }
+
+ @Test
+ public void whenOAuthClientDoesNotProvideAnAuthorizationUrlThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed()
+ throws Exception {
+ // given:
+ OAuthAuthorizationHandler authorizationHandler = mock(OAuthAuthorizationHandler.class);
+ doThrow(new OAuthException("")).when(authorizationHandler).getAuthorizationUrl(anyString());
+ setPrivate(getForwardToLoginServlet(), "authorizationHandler", authorizationHandler);
+
+ // when:
+ Website maybePairAccountSite = getCrawler().doGetRelative("/mielecloud/forwardToLogin?"
+ + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_ID + "&"
+ + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET + "&"
+ + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.BRIDGE_ID + "&" + ForwardToLoginServlet.EMAIL_PARAMETER_NAME
+ + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+ // then:
+ assertTrue(maybePairAccountSite.contains(
+ "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+ assertTrue(maybePairAccountSite.contains("Failed to derive redirect URL."));
+ }
+
+ @Test
+ public void whenNoBridgeUidIsPassedThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed()
+ throws Exception {
+ // when:
+ Website maybePairAccountSite = getCrawler().doGetRelative("/mielecloud/forwardToLogin?"
+ + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_ID + "&"
+ + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET + "&"
+ + ForwardToLoginServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+ // then:
+ assertTrue(maybePairAccountSite.contains(
+ "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+ assertTrue(maybePairAccountSite.contains("Missing bridge ID."));
+ }
+
+ @Test
+ public void whenAnEmptyBridgeUidIsPassedThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed()
+ throws Exception {
+ // when:
+ Website maybePairAccountSite = getCrawler().doGetRelative("/mielecloud/forwardToLogin?"
+ + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_ID + "&"
+ + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET + "&"
+ + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "=" + "&"
+ + ForwardToLoginServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+ // then:
+ assertTrue(maybePairAccountSite.contains(
+ "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+ assertTrue(maybePairAccountSite.contains("Missing bridge ID."));
+ }
+
+ @Test
+ public void whenAMalformedBridgeUidIsPassedThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed()
+ throws Exception {
+ // when:
+ Website maybePairAccountSite = getCrawler().doGetRelative("/mielecloud/forwardToLogin?"
+ + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_ID + "&"
+ + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET + "&"
+ + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "=genesis!" + "&"
+ + ForwardToLoginServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+ // then:
+ assertTrue(maybePairAccountSite.contains(
+ "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+ assertTrue(maybePairAccountSite
+ .contains("Malformed bridge ID. A bridge ID may only contain letters, numbers, '-' and '_'!"));
+ }
+
+ @Test
+ public void whenNoEmailIsPassedThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed() throws Exception {
+ // when:
+ Website maybePairAccountSite = getCrawler()
+ .doGetRelative("/mielecloud/forwardToLogin?" + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_ID + "&"
+ + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET + "&"
+ + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.BRIDGE_ID);
+
+ // then:
+ assertTrue(maybePairAccountSite.contains(
+ "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+ assertTrue(maybePairAccountSite.contains("Missing e-mail address."));
+ }
+
+ @Test
+ public void whenAnEmptyEmailIsPassedThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed()
+ throws Exception {
+ // when:
+ Website maybePairAccountSite = getCrawler()
+ .doGetRelative("/mielecloud/forwardToLogin?" + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_ID + "&"
+ + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET + "&"
+ + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.BRIDGE_ID + "&"
+ + ForwardToLoginServlet.EMAIL_PARAMETER_NAME + "=");
+
+ // then:
+ assertTrue(maybePairAccountSite.contains(
+ "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+ assertTrue(maybePairAccountSite.contains("Missing e-mail address."));
+ }
+
+ @Test
+ public void whenAMalformedEmailIsPassedThenTheBrowserIsRedirectedToThePairSiteAndAWarningIsDisplayed()
+ throws Exception {
+ // when:
+ Website maybePairAccountSite = getCrawler()
+ .doGetRelative("/mielecloud/forwardToLogin?" + ForwardToLoginServlet.CLIENT_ID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_ID + "&"
+ + ForwardToLoginServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET + "&"
+ + ForwardToLoginServlet.BRIDGE_ID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.BRIDGE_ID + "&"
+ + ForwardToLoginServlet.EMAIL_PARAMETER_NAME + "=not_an_Email");
+
+ // then:
+ assertTrue(maybePairAccountSite.contains(
+ "Go to <a href=\"https://www.miele.com/f/com/en/register_api.aspx\">the Miele developer portal</a> to obtain your"));
+ assertTrue(maybePairAccountSite.contains("Malformed e-mail address"));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.util.AbstractConfigFlowTest;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.util.Website;
+
+/**
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class PairAccountServletTest extends AbstractConfigFlowTest {
+ private static final String CLIENT_ID_INPUT_NAME = "clientId";
+ private static final String CLIENT_SECRET_INPUT_NAME = "clientSecret";
+
+ @Test
+ public void whenPairAccountIsInvokedWithClientIdParameterThenTheParameterIsPlacedInTheInputBox() throws Exception {
+ // when:
+ Website pairAccountSite = getCrawler()
+ .doGetRelative("/mielecloud/pair?" + PairAccountServlet.CLIENT_ID_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_ID);
+
+ // then:
+ assertEquals(MieleCloudBindingIntegrationTestConstants.CLIENT_ID,
+ pairAccountSite.getValueOfInput(CLIENT_ID_INPUT_NAME));
+ assertEquals("", pairAccountSite.getValueOfInput(CLIENT_SECRET_INPUT_NAME));
+ }
+
+ @Test
+ public void whenPairAccountIsInvokedWithClientSecretParameterThenTheParameterIsPlacedInTheInputBox()
+ throws Exception {
+ // when:
+ Website pairAccountSite = getCrawler()
+ .doGetRelative("/mielecloud/pair?" + PairAccountServlet.CLIENT_SECRET_PARAMETER_NAME + "="
+ + MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET);
+
+ // then:
+ assertEquals("", pairAccountSite.getValueOfInput(CLIENT_ID_INPUT_NAME));
+ assertEquals(MieleCloudBindingIntegrationTestConstants.CLIENT_SECRET,
+ pairAccountSite.getValueOfInput(CLIENT_SECRET_INPUT_NAME));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.setPrivate;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.auth.OAuthException;
+import org.openhab.binding.mielecloud.internal.config.OAuthAuthorizationHandler;
+import org.openhab.binding.mielecloud.internal.config.exception.NoOngoingAuthorizationException;
+import org.openhab.binding.mielecloud.internal.util.AbstractConfigFlowTest;
+import org.openhab.binding.mielecloud.internal.util.Website;
+
+/**
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class ResultServletTest extends AbstractConfigFlowTest {
+ @Test
+ public void whenOAuthErrorAccessDeniedIsReturnedByMieleServiceThenTheFailurePageWithAccordingErrorMessageIsDisplayed()
+ throws Exception {
+ // when:
+ Website website = getCrawler().doGetRelative("/mielecloud/result?" + ResultServlet.ERROR_PARAMETER_NAME + "="
+ + FailureServlet.OAUTH2_ERROR_ACCESS_DENIED);
+
+ // then:
+ assertTrue(website.contains("Pairing failed!"));
+ assertTrue(website.contains("OAuth2 authentication with Miele cloud service failed: Access denied."));
+ }
+
+ @Test
+ public void whenOAuthErrorInvalidRequestIsReturnedByMieleServiceThenTheFailurePageWithAccordingErrorMessageIsDisplayed()
+ throws Exception {
+ // when:
+ Website website = getCrawler().doGetRelative("/mielecloud/result?" + ResultServlet.ERROR_PARAMETER_NAME + "="
+ + FailureServlet.OAUTH2_ERROR_INVALID_REQUEST);
+
+ // then:
+ assertTrue(website.contains("Pairing failed!"));
+ assertTrue(website.contains("OAuth2 authentication with Miele cloud service failed: Malformed request."));
+ }
+
+ @Test
+ public void whenOAuthErrorUnauthorizedClientIsReturnedByMieleServiceThenTheFailurePageWithAccordingErrorMessageIsDisplayed()
+ throws Exception {
+ // when:
+ Website website = getCrawler().doGetRelative("/mielecloud/result?" + ResultServlet.ERROR_PARAMETER_NAME + "="
+ + FailureServlet.OAUTH2_ERROR_UNAUTHORIZED_CLIENT);
+
+ // then:
+ assertTrue(website.contains("Pairing failed!"));
+ assertTrue(website.contains(
+ "OAuth2 authentication with Miele cloud service failed: Account not authorized to request authorization code."));
+ }
+
+ @Test
+ public void whenOAuthErrorUnsupportedResponseTypeIsReturnedByMieleServiceThenTheFailurePageWithAccordingErrorMessageIsDisplayed()
+ throws Exception {
+ // when:
+ Website website = getCrawler().doGetRelative("/mielecloud/result?" + ResultServlet.ERROR_PARAMETER_NAME + "="
+ + FailureServlet.OAUTH2_ERROR_UNSUPPORTED_RESPONSE_TYPE);
+
+ // then:
+ assertTrue(website.contains("Pairing failed!"));
+ assertTrue(website.contains(
+ "OAuth2 authentication with Miele cloud service failed: Obtaining an authorization code is not supported."));
+ }
+
+ @Test
+ public void whenOAuthErrorInvalidScopeIsReturnedByMieleServiceThenTheFailurePageWithAccordingErrorMessageIsDisplayed()
+ throws Exception {
+ // when:
+ Website website = getCrawler().doGetRelative("/mielecloud/result?" + ResultServlet.ERROR_PARAMETER_NAME + "="
+ + FailureServlet.OAUTH2_ERROR_INVALID_SCOPE);
+
+ // then:
+ assertTrue(website.contains("Pairing failed!"));
+ assertTrue(website.contains("OAuth2 authentication with Miele cloud service failed: Invalid scope."));
+ }
+
+ @Test
+ public void whenOAuthErrorServerErrorIsReturnedByMieleServiceThenTheFailurePageWithAccordingErrorMessageIsDisplayed()
+ throws Exception {
+ // when:
+ Website website = getCrawler().doGetRelative("/mielecloud/result?" + ResultServlet.ERROR_PARAMETER_NAME + "="
+ + FailureServlet.OAUTH2_ERROR_SERVER_ERROR);
+
+ // then:
+ assertTrue(website.contains("Pairing failed!"));
+ assertTrue(website.contains("OAuth2 authentication with Miele cloud service failed: Unexpected server error."));
+ }
+
+ @Test
+ public void whenOAuthErrorTemporarilyUnavailableIsReturnedByMieleServiceThenTheFailurePageWithAccordingErrorMessageIsDisplayed()
+ throws Exception {
+ // when:
+ Website website = getCrawler().doGetRelative("/mielecloud/result?" + ResultServlet.ERROR_PARAMETER_NAME + "="
+ + FailureServlet.OAUTH2_ERROR_TEMPORARY_UNAVAILABLE);
+
+ // then:
+ assertTrue(website.contains("Pairing failed!"));
+ assertTrue(website.contains(
+ "OAuth2 authentication with Miele cloud service failed: Authorization server temporarily unavailable."));
+ }
+
+ @Test
+ public void whenUnknwonOAuthErrorIsReturnedByMieleServiceThenTheFailurePageWithAccordingErrorMessageIsDisplayed()
+ throws Exception {
+ // when:
+ Website website = getCrawler()
+ .doGetRelative("/mielecloud/result?" + ResultServlet.ERROR_PARAMETER_NAME + "=unknown_oauth_2_error");
+
+ // then:
+ assertTrue(website.contains("Pairing failed!"));
+ assertTrue(website.contains(
+ "OAuth2 authentication with Miele cloud service failed: Unknown error code \"unknown_oauth_2_error\"."));
+ }
+
+ @Test
+ public void whenCodeParameterIsNotPassedByMieleServiceThenTheFailurePageWithAccordingErrorMessageIsDisplayed()
+ throws Exception {
+ // when:
+ Website website = getCrawler()
+ .doGetRelative("/mielecloud/result?" + ResultServlet.STATE_PARAMETER_NAME + "=state");
+
+ // then:
+ assertTrue(website.contains("Pairing failed!"));
+ assertTrue(website.contains("Miele cloud service returned an illegal response."));
+ }
+
+ @Test
+ public void whenStateParameterIsNotPassedByMieleServiceThenTheFailurePageWithAccordingErrorMessageIsDisplayed()
+ throws Exception {
+ // when:
+ Website website = getCrawler()
+ .doGetRelative("/mielecloud/result?" + ResultServlet.CODE_PARAMETER_NAME + "=code");
+
+ // then:
+ assertTrue(website.contains("Pairing failed!"));
+ assertTrue(website.contains("Miele cloud service returned an illegal response."));
+ }
+
+ @Test
+ public void whenNoAuthorizationIsOngoingThenTheFailurePageWithAccordingErrorMessageIsDisplayed() throws Exception {
+ // given:
+ OAuthAuthorizationHandler authorizationHandler = mock(OAuthAuthorizationHandler.class);
+ doThrow(new NoOngoingAuthorizationException("")).when(authorizationHandler).completeAuthorization(anyString());
+ setPrivate(getResultServlet(), "authorizationHandler", authorizationHandler);
+
+ // when:
+ Website website = getCrawler().doGetRelative("/mielecloud/result?" + ResultServlet.CODE_PARAMETER_NAME
+ + "=code&" + ResultServlet.STATE_PARAMETER_NAME + "=state");
+
+ // then:
+ assertTrue(website.contains("Pairing failed!"));
+ assertTrue(website.contains("There is no ongoing authorization. Please start an authorization first."));
+ }
+
+ @Test
+ public void whenLastStepOfAuthorizationFailsThenTheFailurePageWithAccordingErrorMessageIsDisplayed()
+ throws Exception {
+ // given:
+ OAuthAuthorizationHandler authorizationHandler = mock(OAuthAuthorizationHandler.class);
+ doThrow(new OAuthException("")).when(authorizationHandler).completeAuthorization(anyString());
+ setPrivate(getResultServlet(), "authorizationHandler", authorizationHandler);
+
+ // when:
+ Website website = getCrawler().doGetRelative("/mielecloud/result?" + ResultServlet.CODE_PARAMETER_NAME
+ + "=code&" + ResultServlet.STATE_PARAMETER_NAME + "=state");
+
+ // then:
+ assertTrue(website.contains("Pairing failed!"));
+ assertTrue(website
+ .contains("Completing the final authorization request failed. Please try the config flow again."));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.config.servlet;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.setPrivate;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.util.AbstractConfigFlowTest;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.util.Website;
+import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider;
+
+/**
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public class SuccessServletTest extends AbstractConfigFlowTest {
+ @Test
+ public void whenTheSuccessPageIsShownThenAThingsFileTemplateIsPresent() throws Exception {
+ // when:
+ Website website = getCrawler().doGetRelative("/mielecloud/success?" + SuccessServlet.BRIDGE_UID_PARAMETER_NAME
+ + "=" + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString() + "&"
+ + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+ // then:
+ assertTrue(website.contains("Bridge mielecloud:account:genesis [ email=\"openhab@openhab.org\""));
+ }
+
+ @Test
+ public void whenTheSuccessPageIsShownThenTheLocaleIsSelectedAutomatically() throws Exception {
+ // given:
+ LanguageProvider languageProvider = mock(LanguageProvider.class);
+ when(languageProvider.getLanguage()).thenReturn(Optional.of("de"));
+ setPrivate(getSuccessServlet(), "languageProvider", languageProvider);
+
+ // when:
+ Website website = getCrawler().doGetRelative("/mielecloud/success?" + SuccessServlet.BRIDGE_UID_PARAMETER_NAME
+ + "=" + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString() + "&"
+ + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+ // then:
+ assertTrue(website.contains("<option value=\"de\" selected=\"selected\">Deutsch - de</option>"));
+ assertTrue(website.contains("locale=\"de\""));
+ }
+
+ @Test
+ public void whenTheSuccessPageIsShownAndNoLocaleIsProvidedThenEnglishIsSelectedAutomatically() throws Exception {
+ // given:
+ LanguageProvider languageProvider = mock(LanguageProvider.class);
+ when(languageProvider.getLanguage()).thenReturn(Optional.of("en"));
+ setPrivate(getSuccessServlet(), "languageProvider", languageProvider);
+
+ // when:
+ Website website = getCrawler().doGetRelative("/mielecloud/success?" + SuccessServlet.BRIDGE_UID_PARAMETER_NAME
+ + "=" + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString() + "&"
+ + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+ // then:
+ assertTrue(website.contains("<option value=\"en\" selected=\"selected\">English - en</option>"));
+ assertTrue(website.contains("locale=\"en\""));
+ }
+
+ @Test
+ public void whenTheSuccessPageIsRequestedAndNoBridgeUidIsPassedThenTheFailurePageIsShown() throws Exception {
+ // when:
+ Website website = getCrawler().doGetRelative("/mielecloud/success");
+
+ // then:
+ assertTrue(website.contains("Pairing failed!"));
+ assertTrue(website.contains("Missing bridge UID."));
+ }
+
+ @Test
+ public void whenTheSuccessPageIsRequestedAndAnEmptyBridgeUidIsPassedThenTheFailurePageIsShown() throws Exception {
+ // when:
+ Website website = getCrawler().doGetRelative("/mielecloud/success?" + SuccessServlet.BRIDGE_UID_PARAMETER_NAME
+ + "=&" + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+ // then:
+ assertTrue(website.contains("Pairing failed!"));
+ assertTrue(website.contains("Missing bridge UID."));
+ }
+
+ @Test
+ public void whenTheSuccessPageIsRequestedAndAMalformedBridgeUidIsPassedThenTheFailurePageIsShown()
+ throws Exception {
+ // when:
+ Website website = getCrawler()
+ .doGetRelative("/mielecloud/success?" + SuccessServlet.BRIDGE_UID_PARAMETER_NAME + "=!genesis&"
+ + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + MieleCloudBindingIntegrationTestConstants.EMAIL);
+
+ // then:
+ assertTrue(website.contains("Pairing failed!"));
+ assertTrue(website.contains("Malformed bridge UID."));
+ }
+
+ @Test
+ public void whenTheSuccessPageIsRequestedAndNoEmailIsPassedThenTheFailurePageIsShown() throws Exception {
+ // when:
+ Website website = getCrawler().doGetRelative("/mielecloud/success?" + SuccessServlet.BRIDGE_UID_PARAMETER_NAME
+ + "=" + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString());
+
+ // then:
+ assertTrue(website.contains("Pairing failed!"));
+ assertTrue(website.contains("Missing e-mail address."));
+ }
+
+ @Test
+ public void whenTheSuccessPageIsRequestedAndAnEmptyEmailIsPassedThenTheFailurePageIsShown() throws Exception {
+ // when:
+ Website website = getCrawler().doGetRelative("/mielecloud/success?" + SuccessServlet.BRIDGE_UID_PARAMETER_NAME
+ + "=" + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString() + "&"
+ + SuccessServlet.EMAIL_PARAMETER_NAME + "=");
+
+ // then:
+ assertTrue(website.contains("Pairing failed!"));
+ assertTrue(website.contains("Missing e-mail address."));
+ }
+
+ @Test
+ public void whenTheSuccessPageIsRequestedAndAMalformedEmailIsPassedThenTheFailurePageIsShown() throws Exception {
+ // when:
+ Website website = getCrawler().doGetRelative("/mielecloud/success?" + SuccessServlet.BRIDGE_UID_PARAMETER_NAME
+ + "=" + MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString() + "&"
+ + SuccessServlet.EMAIL_PARAMETER_NAME + "=not:an!email");
+
+ // then:
+ assertTrue(website.contains("Pairing failed!"));
+ assertTrue(website.contains("Malformed e-mail address."));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.discovery;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.*;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.util.OpenHabOsgiTest;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Device;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceIdentLabel;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Ident;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.Type;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.config.discovery.inbox.InboxPredicates;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class ThingDiscoveryTest extends OpenHabOsgiTest {
+ private static final String DEVICE_TYPE_NAME_COFFEE_SYSTEM = "Coffee System";
+ private static final String DEVICE_TYPE_NAME_DISHWASHER = "Dishwasher";
+ private static final String DEVICE_TYPE_NAME_DISH_WARMER = "Dish Warmer";
+ private static final String DEVICE_TYPE_NAME_DRYER = "Dryer";
+ private static final String DEVICE_TYPE_NAME_FRIDGE_FREEZER = "Fridge Freezer";
+ private static final String DEVICE_TYPE_NAME_HOB = "Hob";
+ private static final String DEVICE_TYPE_NAME_HOOD = "Hood";
+ private static final String DEVICE_TYPE_NAME_OVEN = "Oven";
+ private static final String DEVICE_TYPE_NAME_ROBOTIC_VACUUM_CLEANER = "Robotic Vacuum Cleaner";
+ private static final String DEVICE_TYPE_NAME_WASHING_MACHINE = "Washing Machine";
+ private static final String DEVICE_TYPE_NAME_WINE_STORAGE = "Wine Storage";
+
+ private static final String TECH_TYPE = "WM1234";
+ private static final String TECH_TYPE_2 = "CM1234";
+ private static final String DEVICE_NAME = "My Device";
+ private static final String DEVICE_NAME_2 = "My Other Device";
+ private static final String SERIAL_NUMBER_2 = "900124430017";
+
+ private static final ThingUID DISHWASHER_DEVICE_THING_UID_WITH_SERIAL_NUMBER_2 = new ThingUID(
+ new ThingTypeUID(MieleCloudBindingConstants.BINDING_ID, "dishwasher"), BRIDGE_THING_UID, SERIAL_NUMBER_2);
+
+ @Nullable
+ private ThingDiscoveryService discoveryService;
+
+ private ThingDiscoveryService getDiscoveryService() {
+ assertNotNull(discoveryService);
+ return Objects.requireNonNull(discoveryService);
+ }
+
+ @BeforeEach
+ public void setUp()
+ throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+ setUpBridge();
+ setUpDiscoveryService();
+ }
+
+ private void setUpDiscoveryService()
+ throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+ waitForAssert(() -> {
+ discoveryService = getService(DiscoveryService.class, ThingDiscoveryService.class);
+ assertNotNull(discoveryService);
+ });
+
+ getDiscoveryService().activate();
+ }
+
+ private DeviceState createDeviceState(String fabNumber, String techType, String deviceName, DeviceType deviceType,
+ String deviceTypeText) {
+ // given:
+ DeviceIdentLabel deviceIdentLabel = mock(DeviceIdentLabel.class);
+ when(deviceIdentLabel.getFabNumber()).thenReturn(Optional.of(fabNumber));
+ when(deviceIdentLabel.getTechType()).thenReturn(Optional.of(techType));
+
+ Type type = mock(Type.class);
+ when(type.getValueRaw()).thenReturn(deviceType);
+ when(type.getValueLocalized()).thenReturn(Optional.of(deviceTypeText));
+
+ Ident ident = mock(Ident.class);
+ when(ident.getDeviceIdentLabel()).thenReturn(Optional.of(deviceIdentLabel));
+ when(ident.getType()).thenReturn(Optional.of(type));
+ when(ident.getDeviceName()).thenReturn(Optional.of(deviceName));
+
+ Device device = mock(Device.class);
+ when(device.getIdent()).thenReturn(Optional.of(ident));
+
+ return new DeviceState(fabNumber, device);
+ }
+
+ private void assertValidDiscoveryResult(ThingUID expectedThingUID, String expectedSerialNumber,
+ String expectedDeviceIdentifier, String expectedLabel, String expectedModelId) {
+ List<DiscoveryResult> results = getInbox().stream().filter(InboxPredicates.forThingUID(expectedThingUID))
+ .collect(Collectors.toList());
+ assertEquals(1, results.size(), "Amount of things in inbox does not match expected number");
+
+ DiscoveryResult result = results.get(0);
+ assertEquals(MieleCloudBindingConstants.BINDING_ID, result.getBindingId(), "Invalid binding ID");
+ assertEquals(MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID, result.getBridgeUID(),
+ "Invalid bridge UID");
+ assertEquals(Thing.PROPERTY_SERIAL_NUMBER, result.getRepresentationProperty(),
+ "Invalid representation property");
+ assertEquals(expectedModelId, result.getProperties().get(Thing.PROPERTY_MODEL_ID), "Invalid model ID");
+ assertEquals(expectedLabel, result.getLabel(), "Invalid label");
+ assertEquals(expectedSerialNumber, result.getProperties().get(Thing.PROPERTY_SERIAL_NUMBER),
+ "Invalid serial number");
+ assertEquals(expectedDeviceIdentifier,
+ result.getProperties().get(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER),
+ "Invalid serial number");
+ }
+
+ private void testMieleDeviceInboxDiscoveryResult(DeviceType deviceType, ThingUID expectedThingUid,
+ String deviceTypeName) {
+ // given:
+ DeviceState deviceState = createDeviceState(SERIAL_NUMBER, TECH_TYPE, DEVICE_NAME, deviceType, deviceTypeName);
+
+ // when:
+ getDiscoveryService().onDeviceStateUpdated(deviceState);
+
+ // then:
+ assertValidDiscoveryResult(expectedThingUid, SERIAL_NUMBER, SERIAL_NUMBER, DEVICE_NAME,
+ deviceTypeName + " " + TECH_TYPE);
+ }
+
+ @Test
+ public void testWashingDeviceInboxDiscoveryResult() {
+ testMieleDeviceInboxDiscoveryResult(DeviceType.WASHING_MACHINE, WASHING_MACHINE_THING_UID,
+ DEVICE_TYPE_NAME_WASHING_MACHINE);
+ }
+
+ @Test
+ public void testOvenInboxDiscoveryResult() {
+ testMieleDeviceInboxDiscoveryResult(DeviceType.OVEN, OVEN_DEVICE_THING_UID, DEVICE_TYPE_NAME_OVEN);
+ }
+
+ @Test
+ public void testHobInboxDiscoveryResult() {
+ testMieleDeviceInboxDiscoveryResult(DeviceType.HOB_HIGHLIGHT, HOB_DEVICE_THING_UID, DEVICE_TYPE_NAME_HOB);
+ }
+
+ @Test
+ public void testCoolingDeviceInboxDiscoveryResult() {
+ testMieleDeviceInboxDiscoveryResult(DeviceType.FRIDGE_FREEZER_COMBINATION, FRIDGE_FREEZER_DEVICE_THING_UID,
+ DEVICE_TYPE_NAME_FRIDGE_FREEZER);
+ }
+
+ @Test
+ public void testHoodInboxDiscoveryResult() {
+ testMieleDeviceInboxDiscoveryResult(DeviceType.HOOD, HOOD_DEVICE_THING_UID, DEVICE_TYPE_NAME_HOOD);
+ }
+
+ @Test
+ public void testCoffeeDeviceInboxDiscoveryResult() {
+ testMieleDeviceInboxDiscoveryResult(DeviceType.COFFEE_SYSTEM, COFFEE_SYSTEM_THING_UID,
+ DEVICE_TYPE_NAME_COFFEE_SYSTEM);
+ }
+
+ @Test
+ public void testWineStorageDeviceInboxDiscoveryResult() {
+ testMieleDeviceInboxDiscoveryResult(DeviceType.WINE_CABINET, WINE_STORAGE_DEVICE_THING_UID,
+ DEVICE_TYPE_NAME_WINE_STORAGE);
+ }
+
+ @Test
+ public void testDryerInboxDiscoveryResult() {
+ testMieleDeviceInboxDiscoveryResult(DeviceType.TUMBLE_DRYER, DRYER_DEVICE_THING_UID, DEVICE_TYPE_NAME_DRYER);
+ }
+
+ @Test
+ public void testDishwasherInboxDiscoveryResult() {
+ testMieleDeviceInboxDiscoveryResult(DeviceType.DISHWASHER, DISHWASHER_DEVICE_THING_UID,
+ DEVICE_TYPE_NAME_DISHWASHER);
+ }
+
+ @Test
+ public void testDishWarmerInboxDiscoveryResult() {
+ testMieleDeviceInboxDiscoveryResult(DeviceType.DISH_WARMER, DISH_WARMER_DEVICE_THING_UID,
+ DEVICE_TYPE_NAME_DISH_WARMER);
+ }
+
+ @Test
+ public void testRoboticVacuumCleanerInboxDiscoveryResult() {
+ testMieleDeviceInboxDiscoveryResult(DeviceType.VACUUM_CLEANER, ROBOTIC_VACUUM_CLEANER_THING_UID,
+ DEVICE_TYPE_NAME_ROBOTIC_VACUUM_CLEANER);
+ }
+
+ @Test
+ public void testUnknownDeviceCreatesNoInboxDiscoveryResult() {
+ // given:
+ DeviceState deviceState = createDeviceState(SERIAL_NUMBER, TECH_TYPE, DEVICE_NAME, DeviceType.VACUUM_DRAWER,
+ "Vacuum Drawer");
+
+ // when:
+ getDiscoveryService().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
+ assertEquals(0, results.size(), "Amount of things in inbox does not match expected number");
+ });
+ }
+
+ @Test
+ public void testDeviceDiscoveryResultOfDeviceRemovedInTheCloudIsRemovedFromTheInbox() throws InterruptedException {
+ // given:
+ testMieleDeviceInboxDiscoveryResult(DeviceType.HOOD, HOOD_DEVICE_THING_UID, DEVICE_TYPE_NAME_HOOD);
+
+ Thread.sleep(10);
+
+ // when:
+ getDiscoveryService().onDeviceRemoved(SERIAL_NUMBER);
+
+ // then:
+ waitForAssert(() -> {
+ List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
+ assertEquals(0, results.size(), "Amount of things in inbox does not match expected number");
+ });
+ }
+
+ @Test
+ public void testDiscoveryResultsForTwoDevices() {
+ // given:
+ DeviceState hoodDevice = createDeviceState(SERIAL_NUMBER, TECH_TYPE, DEVICE_NAME, DeviceType.HOOD,
+ DEVICE_TYPE_NAME_HOOD);
+ DeviceState dishwasherDevice = createDeviceState(SERIAL_NUMBER_2, TECH_TYPE_2, DEVICE_NAME_2,
+ DeviceType.DISHWASHER, DEVICE_TYPE_NAME_DISHWASHER);
+
+ // when:
+ getDiscoveryService().onDeviceStateUpdated(hoodDevice);
+ getDiscoveryService().onDeviceStateUpdated(dishwasherDevice);
+
+ // then:
+ waitForAssert(() -> {
+ List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
+ assertEquals(2, results.size(), "Amount of things in inbox does not match expected number");
+
+ assertValidDiscoveryResult(HOOD_DEVICE_THING_UID, SERIAL_NUMBER, SERIAL_NUMBER, DEVICE_NAME,
+ "Hood " + TECH_TYPE);
+ assertValidDiscoveryResult(DISHWASHER_DEVICE_THING_UID_WITH_SERIAL_NUMBER_2, SERIAL_NUMBER_2,
+ SERIAL_NUMBER_2, DEVICE_NAME_2, DEVICE_TYPE_NAME_DISHWASHER + " " + TECH_TYPE_2);
+ });
+ }
+
+ @Test
+ public void testOnlyDeviceDiscoveryResultsOfDevicesRemovedInTheCloudAreRemovedFromTheInbox()
+ throws InterruptedException {
+ // given:
+ DeviceState hoodDevice = createDeviceState(SERIAL_NUMBER, TECH_TYPE, DEVICE_NAME, DeviceType.HOOD,
+ DEVICE_TYPE_NAME_HOOD);
+ DeviceState dishwasherDevice = createDeviceState(SERIAL_NUMBER_2, TECH_TYPE_2, DEVICE_NAME_2,
+ DeviceType.DISHWASHER, DEVICE_TYPE_NAME_DISHWASHER);
+ getDiscoveryService().onDeviceStateUpdated(hoodDevice);
+ getDiscoveryService().onDeviceStateUpdated(dishwasherDevice);
+
+ Thread.sleep(10);
+
+ // when:
+ // This order of invocation is enforced by the webservice implementation.
+ getDiscoveryService().onDeviceRemoved(SERIAL_NUMBER_2);
+ getDiscoveryService().onDeviceStateUpdated(hoodDevice);
+
+ // then:
+ waitForAssert(() -> {
+ List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
+ assertEquals(1, results.size(), "Amount of things in inbox does not match expected number");
+
+ assertValidDiscoveryResult(HOOD_DEVICE_THING_UID, SERIAL_NUMBER, SERIAL_NUMBER, DEVICE_NAME,
+ DEVICE_TYPE_NAME_HOOD + " " + TECH_TYPE);
+ });
+ }
+
+ @Test
+ public void testIfNoDeviceNameIsSetThenTheDiscoveryLabelIsTheDeviceTypePlusTheTechType() {
+ // given:
+ DeviceState deviceState = createDeviceState(SERIAL_NUMBER, TECH_TYPE, "", DeviceType.FRIDGE_FREEZER_COMBINATION,
+ DEVICE_TYPE_NAME_FRIDGE_FREEZER);
+
+ // when:
+ getDiscoveryService().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
+ assertEquals(1, results.size(), "Amount of things in inbox does not match expected number");
+
+ assertValidDiscoveryResult(FRIDGE_FREEZER_DEVICE_THING_UID, SERIAL_NUMBER, SERIAL_NUMBER,
+ "Fridge Freezer " + TECH_TYPE, DEVICE_TYPE_NAME_FRIDGE_FREEZER + " " + TECH_TYPE);
+ });
+ }
+
+ @Test
+ public void testIfNeitherDeviceTypeNorDeviceNameAreSetThenTheDiscoveryModelIdAndTheLabelAreTheTechType() {
+ // given:
+ DeviceState deviceState = createDeviceState(SERIAL_NUMBER, TECH_TYPE, "", DeviceType.FRIDGE_FREEZER_COMBINATION,
+ "");
+
+ // when:
+ getDiscoveryService().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
+ assertEquals(1, results.size(), "Amount of things in inbox does not match expected number");
+
+ assertValidDiscoveryResult(FRIDGE_FREEZER_DEVICE_THING_UID, SERIAL_NUMBER, SERIAL_NUMBER, TECH_TYPE,
+ TECH_TYPE);
+ });
+ }
+
+ @Test
+ public void testIfNeitherTechTypeNorDeviceNameAreSetThenTheDiscoveryModelIdAndTheLabelAreTheDeviceType() {
+ // given:
+ DeviceState deviceState = createDeviceState(SERIAL_NUMBER, "", "", DeviceType.FRIDGE_FREEZER_COMBINATION,
+ DEVICE_TYPE_NAME_FRIDGE_FREEZER);
+
+ // when:
+ getDiscoveryService().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
+ assertEquals(1, results.size(), "Amount of things in inbox does not match expected number");
+
+ assertValidDiscoveryResult(FRIDGE_FREEZER_DEVICE_THING_UID, SERIAL_NUMBER, SERIAL_NUMBER,
+ DEVICE_TYPE_NAME_FRIDGE_FREEZER, DEVICE_TYPE_NAME_FRIDGE_FREEZER);
+ });
+ }
+
+ @Test
+ public void testIfNeitherTechTypeNorDeviceTypeNorDeviceNameAreSetThenTheDiscoveryModelIdIsUnknownAndTheLabelIsMieleDevice() {
+ // given:
+ DeviceState deviceState = createDeviceState(SERIAL_NUMBER, "", "", DeviceType.FRIDGE_FREEZER_COMBINATION, "");
+
+ // when:
+ getDiscoveryService().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
+ assertEquals(1, results.size(), "Amount of things in inbox does not match expected number");
+
+ assertValidDiscoveryResult(FRIDGE_FREEZER_DEVICE_THING_UID, SERIAL_NUMBER, SERIAL_NUMBER, "Miele Device",
+ "Unknown");
+ });
+ }
+
+ @Test
+ public void testIfNoSerialNumberIsSetThenTheDeviceIdentifierIsUsedAsReplacement() {
+ // given:
+ DeviceIdentLabel deviceIdentLabel = mock(DeviceIdentLabel.class);
+ when(deviceIdentLabel.getFabNumber()).thenReturn(Optional.of(""));
+ when(deviceIdentLabel.getTechType()).thenReturn(Optional.of(TECH_TYPE));
+
+ Type type = mock(Type.class);
+ when(type.getValueRaw()).thenReturn(DeviceType.FRIDGE_FREEZER_COMBINATION);
+ when(type.getValueLocalized()).thenReturn(Optional.of(DEVICE_TYPE_NAME_FRIDGE_FREEZER));
+
+ Ident ident = mock(Ident.class);
+ when(ident.getDeviceIdentLabel()).thenReturn(Optional.of(deviceIdentLabel));
+ when(ident.getType()).thenReturn(Optional.of(type));
+ when(ident.getDeviceName()).thenReturn(Optional.of(""));
+
+ Device device = mock(Device.class);
+ when(device.getIdent()).thenReturn(Optional.of(ident));
+ DeviceState deviceState = new DeviceState(SERIAL_NUMBER, device);
+
+ // when:
+ getDiscoveryService().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
+ assertEquals(1, results.size(), "Amount of things in inbox does not match expected number");
+
+ assertValidDiscoveryResult(FRIDGE_FREEZER_DEVICE_THING_UID, SERIAL_NUMBER, SERIAL_NUMBER,
+ DEVICE_TYPE_NAME_FRIDGE_FREEZER + " " + TECH_TYPE,
+ DEVICE_TYPE_NAME_FRIDGE_FREEZER + " " + TECH_TYPE);
+ });
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.*;
+import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.setPrivate;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.auth.OpenHabOAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
+import org.openhab.binding.mielecloud.internal.webservice.MieleWebserviceFactory;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
+import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
+import org.openhab.core.auth.client.oauth2.OAuthClientService;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.items.Item;
+import org.openhab.core.items.ItemBuilder;
+import org.openhab.core.items.ItemBuilderFactory;
+import org.openhab.core.items.ItemRegistry;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.test.java.JavaOSGiTest;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingRegistry;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.openhab.core.thing.binding.builder.BridgeBuilder;
+import org.openhab.core.thing.binding.builder.ChannelBuilder;
+import org.openhab.core.thing.binding.builder.ThingBuilder;
+import org.openhab.core.thing.link.ItemChannelLink;
+import org.openhab.core.thing.link.ItemChannelLinkRegistry;
+import org.openhab.core.thing.type.ChannelDefinition;
+import org.openhab.core.thing.type.ChannelType;
+import org.openhab.core.thing.type.ChannelTypeRegistry;
+import org.openhab.core.thing.type.ChannelTypeUID;
+import org.openhab.core.thing.type.ThingType;
+import org.openhab.core.thing.type.ThingTypeRegistry;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public abstract class AbstractMieleThingHandlerTest extends JavaOSGiTest {
+ protected static final State NULL_VALUE_STATE = UnDefType.UNDEF;
+
+ @Nullable
+ private Bridge bridge;
+ @Nullable
+ private MieleBridgeHandler bridgeHandler;
+ @Nullable
+ private ThingRegistry thingRegistry;
+ @Nullable
+ private MieleWebservice webserviceMock;
+ @Nullable
+ private AbstractMieleThingHandler thingHandler;
+
+ @Nullable
+ private ItemRegistry itemRegistry;
+
+ protected Bridge getBridge() {
+ assertNotNull(bridge);
+ return Objects.requireNonNull(bridge);
+ }
+
+ protected MieleBridgeHandler getBridgeHandler() {
+ assertNotNull(bridgeHandler);
+ return Objects.requireNonNull(bridgeHandler);
+ }
+
+ protected ThingRegistry getThingRegistry() {
+ assertNotNull(thingRegistry);
+ return Objects.requireNonNull(thingRegistry);
+ }
+
+ protected MieleWebservice getWebserviceMock() {
+ assertNotNull(webserviceMock);
+ return Objects.requireNonNull(webserviceMock);
+ }
+
+ protected AbstractMieleThingHandler getThingHandler() {
+ assertNotNull(thingHandler);
+ return Objects.requireNonNull(thingHandler);
+ }
+
+ protected ItemRegistry getItemRegistry() {
+ assertNotNull(itemRegistry);
+ return Objects.requireNonNull(itemRegistry);
+ }
+
+ private void setUpThingRegistry() {
+ thingRegistry = getService(ThingRegistry.class, ThingRegistry.class);
+ assertNotNull(thingRegistry, "Thing registry is missing");
+ }
+
+ private void setUpItemRegistry() {
+ itemRegistry = getService(ItemRegistry.class, ItemRegistry.class);
+ assertNotNull(itemRegistry);
+ }
+
+ private void setUpWebservice()
+ throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+ webserviceMock = mock(MieleWebservice.class);
+ when(getWebserviceMock().hasAccessToken()).thenReturn(true);
+
+ MieleWebserviceFactory webserviceFactory = mock(MieleWebserviceFactory.class);
+ when(webserviceFactory.create(any())).thenReturn(getWebserviceMock());
+
+ MieleHandlerFactory factory = getService(ThingHandlerFactory.class, MieleHandlerFactory.class);
+ assertNotNull(factory);
+ setPrivate(Objects.requireNonNull(factory), "webserviceFactory", webserviceFactory);
+ }
+
+ private void setUpBridge() throws Exception {
+ AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
+ accessTokenResponse.setAccessToken(ACCESS_TOKEN);
+
+ OAuthClientService oAuthClientService = mock(OAuthClientService.class);
+ when(oAuthClientService.getAccessTokenResponse()).thenReturn(accessTokenResponse);
+
+ OAuthFactory oAuthFactory = mock(OAuthFactory.class);
+ when(oAuthFactory
+ .getOAuthClientService(MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString()))
+ .thenReturn(oAuthClientService);
+
+ OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
+ OpenHabOAuthTokenRefresher.class);
+ assertNotNull(tokenRefresher);
+ setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
+
+ bridge = BridgeBuilder
+ .create(MieleCloudBindingConstants.THING_TYPE_BRIDGE,
+ MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID)
+ .withLabel("Miele@home Account")
+ .withConfiguration(
+ new Configuration(Collections.singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL,
+ MieleCloudBindingIntegrationTestConstants.EMAIL)))
+ .build();
+ assertNotNull(bridge);
+
+ getThingRegistry().add(getBridge());
+
+ // then:
+ waitForAssert(() -> {
+ assertNotNull(getBridge().getHandler());
+ assertTrue(getBridge().getHandler() instanceof MieleBridgeHandler, "Handler type is wrong");
+ });
+
+ MieleBridgeHandler bridgeHandler = (MieleBridgeHandler) getBridge().getHandler();
+ assertNotNull(bridgeHandler);
+
+ waitForAssert(() -> {
+ assertNotNull(bridgeHandler.getThing());
+ });
+
+ bridgeHandler.initialize();
+ bridgeHandler.onConnectionAlive();
+ setPrivate(bridgeHandler, "discoveryService", null);
+ this.bridgeHandler = bridgeHandler;
+ }
+
+ protected AbstractMieleThingHandler createThingHandler(ThingTypeUID thingTypeUid, ThingUID thingUid,
+ Class<? extends AbstractMieleThingHandler> expectedHandlerClass, String deviceIdentifier) {
+ ThingRegistry registry = getThingRegistry();
+
+ List<Channel> channels = createChannelsForThingHandler(thingTypeUid, thingUid);
+
+ Thing thing = ThingBuilder.create(thingTypeUid, thingUid)
+ .withConfiguration(new Configuration(Collections
+ .singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER, deviceIdentifier)))
+ .withBridge(getBridge().getUID()).withChannels(channels).withLabel("DA-6996").build();
+ assertNotNull(thing);
+
+ registry.add(thing);
+
+ waitForAssert(() -> {
+ ThingHandler handler = thing.getHandler();
+ assertNotNull(handler);
+ assertTrue(expectedHandlerClass.isAssignableFrom(handler.getClass()), "Handler type is wrong");
+ });
+
+ createItemsForChannels(thing);
+ linkChannelsToItems(thing);
+
+ ThingHandler handler = thing.getHandler();
+ assertNotNull(handler);
+ return (AbstractMieleThingHandler) Objects.requireNonNull(handler);
+ }
+
+ private List<Channel> createChannelsForThingHandler(ThingTypeUID thingTypeUid, ThingUID thingUid) {
+ ChannelTypeRegistry channelTypeRegistry = getService(ChannelTypeRegistry.class, ChannelTypeRegistry.class);
+ assertNotNull(channelTypeRegistry);
+
+ ThingTypeRegistry thingTypeRegistry = getService(ThingTypeRegistry.class, ThingTypeRegistry.class);
+ assertNotNull(thingTypeRegistry);
+
+ ThingType thingType = thingTypeRegistry.getThingType(thingTypeUid);
+ assertNotNull(thingType);
+
+ List<ChannelDefinition> channelDefinitions = thingType.getChannelDefinitions();
+ assertNotNull(channelDefinitions);
+
+ List<Channel> channels = new ArrayList<Channel>();
+ for (ChannelDefinition channelDefinition : channelDefinitions) {
+ ChannelTypeUID channelTypeUid = channelDefinition.getChannelTypeUID();
+ assertNotNull(channelTypeUid);
+
+ ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUid);
+ assertNotNull(channelType);
+
+ String acceptedItemType = channelType.getItemType();
+ assertNotNull(acceptedItemType);
+
+ String channelId = channelDefinition.getId();
+ assertNotNull(channelId);
+
+ ChannelUID channelUid = new ChannelUID(thingUid, channelId);
+ assertNotNull(channelUid);
+
+ Channel channel = ChannelBuilder.create(channelUid, acceptedItemType).build();
+ assertNotNull(channel);
+
+ channels.add(channel);
+ }
+
+ return channels;
+ }
+
+ private void createItemsForChannels(Thing thing) {
+ ItemBuilderFactory itemBuilderFactory = getService(ItemBuilderFactory.class);
+ assertNotNull(itemBuilderFactory);
+
+ for (Channel channel : thing.getChannels()) {
+ String acceptedItemType = channel.getAcceptedItemType();
+ assertNotNull(acceptedItemType);
+
+ ItemBuilder itemBuilder = itemBuilderFactory.newItemBuilder(Objects.requireNonNull(acceptedItemType),
+ channel.getUID().getId());
+ assertNotNull(itemBuilder);
+
+ Item item = itemBuilder.build();
+ assertNotNull(item);
+
+ getItemRegistry().add(item);
+ }
+ }
+
+ private void linkChannelsToItems(Thing thing) {
+ ItemChannelLinkRegistry itemChannelLinkRegistry = getService(ItemChannelLinkRegistry.class,
+ ItemChannelLinkRegistry.class);
+ assertNotNull(itemChannelLinkRegistry);
+
+ for (Channel channel : thing.getChannels()) {
+ String itemName = channel.getUID().getId();
+ assertNotNull(itemName);
+
+ ChannelUID channelUid = channel.getUID();
+ assertNotNull(channelUid);
+
+ ItemChannelLink link = itemChannelLinkRegistry.add(new ItemChannelLink(itemName, channelUid));
+ assertNotNull(link);
+ }
+ }
+
+ protected ChannelUID channel(String id) {
+ return new ChannelUID(getThingHandler().getThing().getUID(), id);
+ }
+
+ @BeforeEach
+ public void setUpAbstractMieleThingHandlerTest() throws Exception {
+ registerVolatileStorageService();
+ setUpThingRegistry();
+ setUpItemRegistry();
+ setUpWebservice();
+ setUpBridge();
+ thingHandler = setUpThingHandler();
+ }
+
+ private void assertThingStatusIs(Thing thing, ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail) {
+ assertThingStatusIs(thing, expectedStatus, expectedStatusDetail, null);
+ }
+
+ private void assertThingStatusIs(Thing thing, ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail,
+ @Nullable String expectedDescription) {
+ assertEquals(expectedStatus, thing.getStatus());
+ assertEquals(expectedStatusDetail, thing.getStatusInfo().getStatusDetail());
+ if (expectedDescription == null) {
+ assertNull(thing.getStatusInfo().getDescription());
+ } else {
+ assertEquals(expectedDescription, thing.getStatusInfo().getDescription());
+ }
+ }
+
+ protected State getChannelState(String channelUid) {
+ Item item = getItemRegistry().get(channelUid);
+ assertNotNull(item, "Item for channel UID " + channelUid + " is null.");
+ return item.getState();
+ }
+
+ /**
+ * Sets up the {@link ThingHandler} under test.
+ *
+ * @return The created {@link ThingHandler}.
+ */
+ protected abstract AbstractMieleThingHandler setUpThingHandler();
+
+ @Test
+ public void testCachedStateIsQueriedOnInitialize() throws Exception {
+ // then:
+ verify(getWebserviceMock()).dispatchDeviceState(SERIAL_NUMBER);
+ }
+
+ @Test
+ public void testThingStatusIsOfflineWithDetailGoneAndDetailMessageWhenDeviceIsRemoved() {
+ // when:
+ getBridgeHandler().onDeviceRemoved(SERIAL_NUMBER);
+
+ // then:
+ Thing thing = getThingHandler().getThing();
+ assertThingStatusIs(thing, ThingStatus.OFFLINE, ThingStatusDetail.GONE,
+ "@text/mielecloud.thing.status.removed");
+ }
+
+ private DeviceState createDeviceStateMock(StateType stateType, String localizedState) {
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.getDeviceIdentifier()).thenReturn(getThingHandler().getThing().getUID().getId());
+ when(deviceState.getRawType()).thenReturn(DeviceType.UNKNOWN);
+ when(deviceState.getStateType()).thenReturn(Optional.of(stateType));
+ when(deviceState.isInState(any())).thenCallRealMethod();
+ when(deviceState.getStatus()).thenReturn(Optional.of(localizedState));
+ return deviceState;
+ }
+
+ @Test
+ public void testStatusIsSetToOnlineWhenDeviceStateIsValid() {
+ // given:
+ DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
+ }
+
+ @Test
+ public void testStatusIsSetToOfflineWhenDeviceIsNotConnected() {
+ // given:
+ DeviceState deviceState = createDeviceStateMock(StateType.NOT_CONNECTED, "Not connected");
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ assertThingStatusIs(getThingHandler().getThing(), ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/mielecloud.thing.status.disconnected");
+ }
+
+ @Test
+ public void testFailingPutProcessActionDoesNotSetTheDeviceToOffline() {
+ // given:
+ DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+ assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
+
+ doThrow(MieleWebserviceException.class).when(getWebserviceMock()).putProcessAction(any(),
+ eq(ProcessAction.STOP));
+
+ // when:
+ getThingHandler().triggerProcessAction(ProcessAction.STOP);
+
+ // then:
+ assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
+ }
+
+ @Test
+ public void testHandleCommandProgramStartToStartStopChannel() {
+ // when:
+ getThingHandler().handleCommand(channel(PROGRAM_START_STOP),
+ new StringType(ProgramStatus.PROGRAM_STARTED.getState()));
+
+ // then:
+ waitForAssert(() -> {
+ verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.START);
+ });
+ }
+
+ @Test
+ public void testHandleCommandProgramStopToStartStopChannel() {
+ // when:
+ getThingHandler().handleCommand(channel(PROGRAM_START_STOP),
+ new StringType(ProgramStatus.PROGRAM_STOPPED.getState()));
+
+ // then:
+ waitForAssert(() -> {
+ verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.STOP);
+ });
+ }
+
+ @Test
+ public void testHandleCommandProgramStartToStartStopPauseChannel() {
+ // when:
+ getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
+ new StringType(ProgramStatus.PROGRAM_STARTED.getState()));
+
+ // then:
+ waitForAssert(() -> {
+ verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.START);
+ });
+ }
+
+ @Test
+ public void testHandleCommandProgramStopToStartStopPauseChannel() {
+ // when:
+ getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
+ new StringType(ProgramStatus.PROGRAM_STOPPED.getState()));
+
+ // then:
+ waitForAssert(() -> {
+ verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.STOP);
+ });
+ }
+
+ @Test
+ public void testHandleCommandProgramPauseToStartStopPauseChannel() {
+ // when:
+ getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
+ new StringType(ProgramStatus.PROGRAM_PAUSED.getState()));
+
+ // then:
+ waitForAssert(() -> {
+ verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.PAUSE);
+ });
+ }
+
+ @Test
+ public void testFailingPutLightDoesNotSetTheDeviceToOffline() {
+ // given:
+ DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+ assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
+
+ doThrow(MieleWebserviceException.class).when(getWebserviceMock()).putLight(any(), eq(true));
+
+ // when:
+ getThingHandler().triggerLight(true);
+
+ // then:
+ assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
+ }
+
+ @Test
+ public void testHandleCommandLightOff() {
+ // when:
+ getThingHandler().handleCommand(channel(LIGHT_SWITCH), OnOffType.OFF);
+
+ // then:
+ waitForAssert(() -> {
+ verify(getWebserviceMock()).putLight(getThingHandler().getDeviceId(), false);
+ });
+ }
+
+ @Test
+ public void testHandleCommandLightOn() {
+ // when:
+ getThingHandler().handleCommand(channel(LIGHT_SWITCH), OnOffType.ON);
+
+ // then:
+ waitForAssert(() -> {
+ verify(getWebserviceMock()).putLight(getThingHandler().getDeviceId(), true);
+ });
+ }
+
+ @Test
+ public void testHandleCommandDoesNothingWhenCommandIsNotOfOnOffType() {
+ // when:
+ getThingHandler().handleCommand(channel(LIGHT_SWITCH), new DecimalType(0));
+
+ // then:
+ verify(getWebserviceMock(), never()).putLight(anyString(), anyBoolean());
+ }
+
+ @Test
+ public void testHandleCommandPowerOn() {
+ // when:
+ getThingHandler().handleCommand(channel(POWER_ON_OFF), OnOffType.ON);
+
+ // then:
+ waitForAssert(() -> {
+ verify(getWebserviceMock()).putPowerState(getThingHandler().getDeviceId(), true);
+ });
+ }
+
+ @Test
+ public void testHandleCommandPowerOff() {
+ // when:
+ getThingHandler().handleCommand(channel(POWER_ON_OFF), OnOffType.OFF);
+
+ // then:
+ waitForAssert(() -> {
+ verify(getWebserviceMock()).putPowerState(getThingHandler().getDeviceId(), false);
+ });
+ }
+
+ @Test
+ public void testHandleCommandDoesNothingWhenPowerCommandIsNotOfOnOffType() {
+ // when:
+ getThingHandler().handleCommand(channel(POWER_ON_OFF), new DecimalType(0));
+
+ // then:
+ verify(getWebserviceMock(), never()).putPowerState(anyString(), anyBoolean());
+ }
+
+ @Test
+ public void testMissingPropertiesAreSetWhenAStateUpdateIsReceivedFromTheCloud() {
+ // given:
+ assertFalse(getThingHandler().getThing().getProperties().containsKey(Thing.PROPERTY_SERIAL_NUMBER));
+ assertFalse(getThingHandler().getThing().getProperties().containsKey(Thing.PROPERTY_MODEL_ID));
+
+ var deviceState = mock(DeviceState.class);
+ when(deviceState.getRawType()).thenReturn(DeviceType.UNKNOWN);
+ when(deviceState.getDeviceIdentifier()).thenReturn(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
+ when(deviceState.getFabNumber())
+ .thenReturn(Optional.of(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER));
+ when(deviceState.getType()).thenReturn(Optional.of("Unknown device type"));
+ when(deviceState.getTechType()).thenReturn(Optional.of("UK-4567"));
+
+ // when:
+ getThingHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ assertEquals(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER,
+ getThingHandler().getThing().getProperties().get(Thing.PROPERTY_SERIAL_NUMBER));
+ assertEquals("Unknown device type UK-4567",
+ getThingHandler().getThing().getProperties().get(Thing.PROPERTY_MODEL_ID));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.COFFEE_SYSTEM_THING_UID;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+
+/**
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API tests
+ * @author Björn Lange - Add elapsed time channel
+ */
+@NonNullByDefault
+public class CoffeeDeviceThingHandlerTest extends AbstractMieleThingHandlerTest {
+ @Override
+ protected AbstractMieleThingHandler setUpThingHandler() {
+ return createThingHandler(MieleCloudBindingConstants.THING_TYPE_COFFEE_SYSTEM, COFFEE_SYSTEM_THING_UID,
+ CoffeeSystemThingHandler.class, MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
+ }
+
+ @Test
+ public void testChannelUpdatesForNullValues() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.getDeviceIdentifier()).thenReturn(COFFEE_SYSTEM_THING_UID.getId());
+ when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.empty());
+ when(deviceState.getSelectedProgram()).thenReturn(Optional.empty());
+ when(deviceState.getSelectedProgramId()).thenReturn(Optional.empty());
+ when(deviceState.getProgramPhase()).thenReturn(Optional.empty());
+ when(deviceState.getProgramPhaseRaw()).thenReturn(Optional.empty());
+ when(deviceState.getStateType()).thenReturn(Optional.empty());
+ when(deviceState.getStatus()).thenReturn(Optional.empty());
+ when(deviceState.getStatusRaw()).thenReturn(Optional.empty());
+ when(deviceState.getElapsedTime()).thenReturn(Optional.empty());
+ when(deviceState.getLightState()).thenReturn(Optional.empty());
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ACTIVE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ACTIVE_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PHASE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PHASE_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ELAPSED_TIME));
+ assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+ assertEquals(NULL_VALUE_STATE, getChannelState(LIGHT_SWITCH));
+ });
+ }
+
+ @Test
+ public void testChannelUpdatesForValidValues() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.getDeviceIdentifier()).thenReturn(COFFEE_SYSTEM_THING_UID.getId());
+ when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.of(true));
+ when(deviceState.getSelectedProgram()).thenReturn(Optional.of("Latte Macchiato"));
+ when(deviceState.getSelectedProgramId()).thenReturn(Optional.of(5L));
+ when(deviceState.getProgramPhase()).thenReturn(Optional.of("Spühlen"));
+ when(deviceState.getProgramPhaseRaw()).thenReturn(Optional.of(1));
+ when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceState.getStatus()).thenReturn(Optional.of("Running"));
+ when(deviceState.getStatusRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+ when(deviceState.hasError()).thenReturn(false);
+ when(deviceState.hasInfo()).thenReturn(true);
+ when(deviceState.getLightState()).thenReturn(Optional.of(false));
+ when(deviceState.getElapsedTime()).thenReturn(Optional.of(3));
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(new StringType("Latte Macchiato"), getChannelState(PROGRAM_ACTIVE));
+ assertEquals(new DecimalType(5), getChannelState(PROGRAM_ACTIVE_RAW));
+ assertEquals(new StringType("Spühlen"), getChannelState(PROGRAM_PHASE));
+ assertEquals(new DecimalType(1), getChannelState(PROGRAM_PHASE_RAW));
+ assertEquals(new StringType("Running"), getChannelState(OPERATION_STATE));
+ assertEquals(new DecimalType(StateType.RUNNING.getCode()), getChannelState(OPERATION_STATE_RAW));
+ assertEquals(new DecimalType(3), getChannelState(PROGRAM_ELAPSED_TIME));
+ assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+ assertEquals(OnOffType.OFF, getChannelState(ERROR_STATE));
+ assertEquals(OnOffType.ON, getChannelState(INFO_STATE));
+ assertEquals(OnOffType.OFF, getChannelState(LIGHT_SWITCH));
+ });
+ }
+
+ @Test
+ public void testFinishStateChannelIsSetToOnWhenProgramHasFinished() {
+ // given:
+ DeviceState deviceStateBefore = mock(DeviceState.class);
+ when(deviceStateBefore.getDeviceIdentifier()).thenReturn(COFFEE_SYSTEM_THING_UID.getId());
+ when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+
+ getBridgeHandler().onDeviceStateUpdated(deviceStateBefore);
+
+ DeviceState deviceStateAfter = mock(DeviceState.class);
+ when(deviceStateAfter.getDeviceIdentifier()).thenReturn(COFFEE_SYSTEM_THING_UID.getId());
+ when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+ when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceStateAfter);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(OnOffType.ON, getChannelState(FINISH_STATE));
+ });
+ }
+
+ @Test
+ public void testTransitionChannelUpdatesForNullValues() {
+ // given:
+ DeviceState deviceStateBefore = mock(DeviceState.class);
+ when(deviceStateBefore.getDeviceIdentifier()).thenReturn(COFFEE_SYSTEM_THING_UID.getId());
+ when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+ when(deviceStateBefore.getRemainingTime()).thenReturn(Optional.empty());
+
+ getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+ DeviceState deviceStateAfter = mock(DeviceState.class);
+ when(deviceStateAfter.getDeviceIdentifier()).thenReturn(COFFEE_SYSTEM_THING_UID.getId());
+ when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+ when(deviceStateAfter.getRemainingTime()).thenReturn(Optional.empty());
+
+ // when:
+ getThingHandler().onDeviceStateUpdated(deviceStateAfter);
+
+ waitForAssert(() -> {
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_REMAINING_TIME));
+ });
+ }
+
+ @Test
+ public void testTransitionChannelUpdatesForValidValues() {
+ // given:
+ DeviceState deviceStateBefore = mock(DeviceState.class);
+ when(deviceStateBefore.getDeviceIdentifier()).thenReturn(COFFEE_SYSTEM_THING_UID.getId());
+ when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+ when(deviceStateBefore.getRemainingTime()).thenReturn(Optional.of(10));
+
+ getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+ DeviceState deviceStateAfter = mock(DeviceState.class);
+ when(deviceStateAfter.getDeviceIdentifier()).thenReturn(COFFEE_SYSTEM_THING_UID.getId());
+ when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+ when(deviceStateAfter.getRemainingTime()).thenReturn(Optional.of(10));
+
+ // when:
+ getThingHandler().onDeviceStateUpdated(deviceStateAfter);
+
+ waitForAssert(() -> {
+ assertEquals(new DecimalType(10), getChannelState(PROGRAM_REMAINING_TIME));
+ });
+ }
+
+ @Test
+ public void testActionsChannelUpdatesForValidValues() {
+ // given:
+ ActionsState actionsState = mock(ActionsState.class);
+ when(actionsState.getDeviceIdentifier()).thenReturn(COFFEE_SYSTEM_THING_UID.getId());
+ when(actionsState.canBeSwitchedOn()).thenReturn(true);
+ when(actionsState.canBeSwitchedOff()).thenReturn(false);
+ when(actionsState.canControlLight()).thenReturn(true);
+
+ // when:
+ getBridgeHandler().onProcessActionUpdated(actionsState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_ON));
+ assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF));
+ assertEquals(OnOffType.ON, getChannelState(LIGHT_CAN_BE_CONTROLLED));
+ });
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.FRIDGE_FREEZER_DEVICE_THING_UID;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.SIUnits;
+
+/**
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add door state and door alarm
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API tests
+ */
+@NonNullByDefault
+public class CoolingDeviceThingHandlerTest extends AbstractMieleThingHandlerTest {
+ @Override
+ protected AbstractMieleThingHandler setUpThingHandler() {
+ return createThingHandler(MieleCloudBindingConstants.THING_TYPE_FRIDGE_FREEZER, FRIDGE_FREEZER_DEVICE_THING_UID,
+ CoolingDeviceThingHandler.class, MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
+ }
+
+ @Test
+ public void testChannelUpdatesForNullValues() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.getDeviceIdentifier()).thenReturn(FRIDGE_FREEZER_DEVICE_THING_UID.getId());
+ when(deviceState.getRawType()).thenReturn(DeviceType.FRIDGE_FREEZER_COMBINATION);
+ when(deviceState.getStateType()).thenReturn(Optional.empty());
+ when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.empty());
+ when(deviceState.getStatus()).thenReturn(Optional.empty());
+ when(deviceState.getStatusRaw()).thenReturn(Optional.empty());
+ when(deviceState.getTargetTemperature(0)).thenReturn(Optional.empty());
+ when(deviceState.getTargetTemperature(1)).thenReturn(Optional.empty());
+ when(deviceState.getTemperature(0)).thenReturn(Optional.empty());
+ when(deviceState.getTemperature(1)).thenReturn(Optional.empty());
+ when(deviceState.getDoorState()).thenReturn(Optional.empty());
+ when(deviceState.getDoorAlarm()).thenReturn(Optional.empty());
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(FRIDGE_SUPER_COOL));
+ assertEquals(NULL_VALUE_STATE, getChannelState(FREEZER_SUPER_FREEZE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(FRIDGE_TEMPERATURE_TARGET));
+ assertEquals(NULL_VALUE_STATE, getChannelState(FREEZER_TEMPERATURE_TARGET));
+ assertEquals(NULL_VALUE_STATE, getChannelState(FRIDGE_TEMPERATURE_CURRENT));
+ assertEquals(NULL_VALUE_STATE, getChannelState(FREEZER_TEMPERATURE_CURRENT));
+ assertEquals(NULL_VALUE_STATE, getChannelState(DOOR_STATE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(DOOR_ALARM));
+ });
+ }
+
+ @Test
+ public void testChannelUpdatesForValidValues() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.getDeviceIdentifier()).thenReturn(FRIDGE_FREEZER_DEVICE_THING_UID.getId());
+ when(deviceState.getRawType()).thenReturn(DeviceType.FRIDGE_FREEZER_COMBINATION);
+ when(deviceState.getStateType()).thenReturn(Optional.of(StateType.SUPERCOOLING));
+ when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.of(true));
+ when(deviceState.getStatus()).thenReturn(Optional.of("Super Cooling"));
+ when(deviceState.getStatusRaw()).thenReturn(Optional.of(StateType.SUPERCOOLING.getCode()));
+ when(deviceState.getTargetTemperature(0)).thenReturn(Optional.of(6));
+ when(deviceState.getTargetTemperature(1)).thenReturn(Optional.of(-18));
+ when(deviceState.getTemperature(0)).thenReturn(Optional.of(8));
+ when(deviceState.getTemperature(1)).thenReturn(Optional.of(-10));
+ when(deviceState.hasError()).thenReturn(false);
+ when(deviceState.getDoorState()).thenReturn(Optional.of(true));
+ when(deviceState.getDoorAlarm()).thenReturn(Optional.of(false));
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(new StringType("Super Cooling"), getChannelState(OPERATION_STATE));
+ assertEquals(new DecimalType(StateType.SUPERCOOLING.getCode()), getChannelState(OPERATION_STATE_RAW));
+ assertEquals(OnOffType.ON, getChannelState(FRIDGE_SUPER_COOL));
+ assertEquals(OnOffType.OFF, getChannelState(FREEZER_SUPER_FREEZE));
+ assertEquals(new QuantityType<>(6, SIUnits.CELSIUS), getChannelState(FRIDGE_TEMPERATURE_TARGET));
+ assertEquals(new QuantityType<>(-18, SIUnits.CELSIUS), getChannelState(FREEZER_TEMPERATURE_TARGET));
+ assertEquals(new QuantityType<>(8, SIUnits.CELSIUS), getChannelState(FRIDGE_TEMPERATURE_CURRENT));
+ assertEquals(new QuantityType<>(-10, SIUnits.CELSIUS), getChannelState(FREEZER_TEMPERATURE_CURRENT));
+ assertEquals(OnOffType.OFF, getChannelState(ERROR_STATE));
+ assertEquals(OnOffType.ON, getChannelState(DOOR_STATE));
+ assertEquals(OnOffType.OFF, getChannelState(DOOR_ALARM));
+ });
+ }
+
+ @Test
+ public void testChannelUpdatesForSuperCooling() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.getDeviceIdentifier()).thenReturn(FRIDGE_FREEZER_DEVICE_THING_UID.getId());
+ when(deviceState.getRawType()).thenReturn(DeviceType.FRIDGE_FREEZER_COMBINATION);
+ when(deviceState.getStateType()).thenReturn(Optional.of(StateType.SUPERCOOLING));
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(OnOffType.ON, getChannelState(FRIDGE_SUPER_COOL));
+ assertEquals(OnOffType.OFF, getChannelState(FREEZER_SUPER_FREEZE));
+ });
+ }
+
+ @Test
+ public void testChannelUpdatesForSuperFreezing() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.getDeviceIdentifier()).thenReturn(FRIDGE_FREEZER_DEVICE_THING_UID.getId());
+ when(deviceState.getRawType()).thenReturn(DeviceType.FRIDGE_FREEZER_COMBINATION);
+ when(deviceState.getStateType()).thenReturn(Optional.of(StateType.SUPERFREEZING));
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(OnOffType.OFF, getChannelState(FRIDGE_SUPER_COOL));
+ assertEquals(OnOffType.ON, getChannelState(FREEZER_SUPER_FREEZE));
+ });
+ }
+
+ @Test
+ public void testChannelUpdatesForSuperCollingSuperFreezing() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.getDeviceIdentifier()).thenReturn(FRIDGE_FREEZER_DEVICE_THING_UID.getId());
+ when(deviceState.getRawType()).thenReturn(DeviceType.FRIDGE_FREEZER_COMBINATION);
+ when(deviceState.getStateType()).thenReturn(Optional.of(StateType.SUPERCOOLING_SUPERFREEZING));
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(OnOffType.ON, getChannelState(FRIDGE_SUPER_COOL));
+ assertEquals(OnOffType.ON, getChannelState(FREEZER_SUPER_FREEZE));
+ });
+ }
+
+ @Test
+ public void testActionsChannelUpdatesForValidValues() {
+ // given:
+ ActionsState actionsState = mock(ActionsState.class);
+ when(actionsState.getDeviceIdentifier()).thenReturn(FRIDGE_FREEZER_DEVICE_THING_UID.getId());
+ when(actionsState.canContolSupercooling()).thenReturn(true);
+ when(actionsState.canControlSuperfreezing()).thenReturn(false);
+
+ // when:
+ getBridgeHandler().onProcessActionUpdated(actionsState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(OnOffType.ON, getChannelState(SUPER_COOL_CAN_BE_CONTROLLED));
+ assertEquals(OnOffType.OFF, getChannelState(SUPER_FREEZE_CAN_BE_CONTROLLED));
+ });
+ }
+
+ @Override
+ @Test
+ public void testHandleCommandDoesNothingWhenCommandIsNotOfOnOffType() {
+ // when:
+ getThingHandler().handleCommand(channel(FRIDGE_SUPER_COOL), new DecimalType(50));
+
+ // then:
+ verify(getWebserviceMock(), never()).putProcessAction(anyString(), any());
+ }
+
+ @Test
+ public void testHandleCommandStartsSupercoolingWhenRequested() {
+ // when:
+ getThingHandler().handleCommand(channel(FRIDGE_SUPER_COOL), OnOffType.ON);
+
+ // then:
+ waitForAssert(() -> {
+ verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(),
+ ProcessAction.START_SUPERCOOLING);
+ });
+ }
+
+ @Test
+ public void testHandleCommandStopsSupercoolingWhenRequested() {
+ // when:
+ getThingHandler().handleCommand(channel(FRIDGE_SUPER_COOL), OnOffType.OFF);
+
+ // then:
+ waitForAssert(() -> {
+ verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(),
+ ProcessAction.STOP_SUPERCOOLING);
+ });
+ }
+
+ @Test
+ public void testHandleCommandStartsSuperfreezingWhenRequested() {
+ // when:
+ getThingHandler().handleCommand(channel(FREEZER_SUPER_FREEZE), OnOffType.ON);
+
+ // then:
+ waitForAssert(() -> {
+ verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(),
+ ProcessAction.START_SUPERFREEZING);
+ });
+ }
+
+ @Test
+ public void testHandleCommandStopsSuperfreezingWhenRequested() {
+ // when:
+ getThingHandler().handleCommand(channel(FREEZER_SUPER_FREEZE), OnOffType.OFF);
+
+ // then:
+ waitForAssert(() -> {
+ verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(),
+ ProcessAction.STOP_SUPERFREEZING);
+ });
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.SIUnits;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class DishWarmerDeviceThingHandlerTest extends AbstractMieleThingHandlerTest {
+ @Override
+ protected AbstractMieleThingHandler setUpThingHandler() {
+ return createThingHandler(MieleCloudBindingConstants.THING_TYPE_DISH_WARMER,
+ MieleCloudBindingIntegrationTestConstants.DISH_WARMER_DEVICE_THING_UID,
+ DishWarmerDeviceThingHandler.class, MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
+ }
+
+ @Test
+ public void testChannelUpdatesForNullValues() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.getDeviceIdentifier())
+ .thenReturn(MieleCloudBindingIntegrationTestConstants.DISH_WARMER_DEVICE_THING_UID.getId());
+ when(deviceState.getSelectedProgramId()).thenReturn(Optional.empty());
+ when(deviceState.getStatus()).thenReturn(Optional.empty());
+ when(deviceState.getStatusRaw()).thenReturn(Optional.empty());
+ when(deviceState.getStateType()).thenReturn(Optional.empty());
+ when(deviceState.getElapsedTime()).thenReturn(Optional.empty());
+ when(deviceState.getTargetTemperature(0)).thenReturn(Optional.empty());
+ when(deviceState.getTemperature(0)).thenReturn(Optional.empty());
+ when(deviceState.hasError()).thenReturn(false);
+ when(deviceState.hasInfo()).thenReturn(false);
+ when(deviceState.getDoorState()).thenReturn(Optional.empty());
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(NULL_VALUE_STATE, getChannelState(DISH_WARMER_PROGRAM_ACTIVE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE_RAW));
+ assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ELAPSED_TIME));
+ assertEquals(NULL_VALUE_STATE, getChannelState(TEMPERATURE_TARGET));
+ assertEquals(NULL_VALUE_STATE, getChannelState(TEMPERATURE_CURRENT));
+ assertEquals(OnOffType.OFF, getChannelState(ERROR_STATE));
+ assertEquals(OnOffType.OFF, getChannelState(INFO_STATE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(DOOR_STATE));
+ });
+ }
+
+ @Test
+ public void testChannelUpdatesForValidValues() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.getDeviceIdentifier())
+ .thenReturn(MieleCloudBindingIntegrationTestConstants.DISH_WARMER_DEVICE_THING_UID.getId());
+ when(deviceState.getSelectedProgramId()).thenReturn(Optional.of(2L));
+ when(deviceState.getStatus()).thenReturn(Optional.of("Running"));
+ when(deviceState.getStatusRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+ when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceState.getElapsedTime()).thenReturn(Optional.of(98));
+ when(deviceState.getTargetTemperature(0)).thenReturn(Optional.of(30));
+ when(deviceState.getTemperature(0)).thenReturn(Optional.of(29));
+ when(deviceState.hasError()).thenReturn(true);
+ when(deviceState.hasInfo()).thenReturn(true);
+ when(deviceState.getDoorState()).thenReturn(Optional.of(false));
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(new StringType("2"), getChannelState(DISH_WARMER_PROGRAM_ACTIVE));
+ assertEquals(new StringType("Running"), getChannelState(OPERATION_STATE));
+ assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+ assertEquals(new DecimalType(98), getChannelState(PROGRAM_ELAPSED_TIME));
+ assertEquals(new QuantityType<>(30, SIUnits.CELSIUS), getChannelState(TEMPERATURE_TARGET));
+ assertEquals(new QuantityType<>(29, SIUnits.CELSIUS), getChannelState(TEMPERATURE_CURRENT));
+ assertEquals(OnOffType.ON, getChannelState(ERROR_STATE));
+ assertEquals(OnOffType.ON, getChannelState(INFO_STATE));
+ assertEquals(OnOffType.OFF, getChannelState(DOOR_STATE));
+ });
+ }
+
+ @Test
+ public void testFinishStateChannelIsSetToOnWhenProgramHasFinished() {
+ // given:
+ DeviceState deviceStateBefore = mock(DeviceState.class);
+ when(deviceStateBefore.getDeviceIdentifier())
+ .thenReturn(MieleCloudBindingIntegrationTestConstants.DISH_WARMER_DEVICE_THING_UID.getId());
+ when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+
+ getBridgeHandler().onDeviceStateUpdated(deviceStateBefore);
+
+ DeviceState deviceStateAfter = mock(DeviceState.class);
+ when(deviceStateAfter.getDeviceIdentifier())
+ .thenReturn(MieleCloudBindingIntegrationTestConstants.DISH_WARMER_DEVICE_THING_UID.getId());
+ when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+ when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceStateAfter);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(OnOffType.ON, getChannelState(FINISH_STATE));
+ });
+ }
+
+ @Test
+ public void testTransitionChannelUpdatesForNullValues() {
+ // given:
+ DeviceState deviceStateBefore = mock(DeviceState.class);
+ when(deviceStateBefore.getDeviceIdentifier())
+ .thenReturn(MieleCloudBindingIntegrationTestConstants.DISH_WARMER_DEVICE_THING_UID.getId());
+ when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+ when(deviceStateBefore.getRemainingTime()).thenReturn(Optional.empty());
+ when(deviceStateBefore.getProgress()).thenReturn(Optional.empty());
+
+ getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+ DeviceState deviceStateAfter = mock(DeviceState.class);
+ when(deviceStateAfter.getDeviceIdentifier())
+ .thenReturn(MieleCloudBindingIntegrationTestConstants.DISH_WARMER_DEVICE_THING_UID.getId());
+ when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+ when(deviceStateAfter.getRemainingTime()).thenReturn(Optional.empty());
+ when(deviceStateAfter.getProgress()).thenReturn(Optional.empty());
+
+ // when:
+ getThingHandler().onDeviceStateUpdated(deviceStateAfter);
+
+ waitForAssert(() -> {
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_REMAINING_TIME));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PROGRESS));
+ });
+ }
+
+ @Test
+ public void testTransitionChannelUpdatesForValidValues() {
+ // given:
+ DeviceState deviceStateBefore = mock(DeviceState.class);
+ when(deviceStateBefore.getDeviceIdentifier())
+ .thenReturn(MieleCloudBindingIntegrationTestConstants.DISH_WARMER_DEVICE_THING_UID.getId());
+ when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+ when(deviceStateBefore.getRemainingTime()).thenReturn(Optional.of(10));
+ when(deviceStateBefore.getProgress()).thenReturn(Optional.of(80));
+
+ getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+ DeviceState deviceStateAfter = mock(DeviceState.class);
+ when(deviceStateAfter.getDeviceIdentifier())
+ .thenReturn(MieleCloudBindingIntegrationTestConstants.DISH_WARMER_DEVICE_THING_UID.getId());
+ when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+ when(deviceStateAfter.getRemainingTime()).thenReturn(Optional.of(10));
+ when(deviceStateAfter.getProgress()).thenReturn(Optional.of(80));
+
+ // when:
+ getThingHandler().onDeviceStateUpdated(deviceStateAfter);
+
+ waitForAssert(() -> {
+ assertEquals(new DecimalType(10), getChannelState(PROGRAM_REMAINING_TIME));
+ assertEquals(new DecimalType(80), getChannelState(PROGRAM_PROGRESS));
+ });
+ }
+
+ @Test
+ public void testActionsChannelUpdatesForValidValues() {
+ // given:
+ ActionsState actionsState = mock(ActionsState.class);
+ when(actionsState.getDeviceIdentifier())
+ .thenReturn(MieleCloudBindingIntegrationTestConstants.DISH_WARMER_DEVICE_THING_UID.getId());
+ when(actionsState.canBeSwitchedOn()).thenReturn(true);
+ when(actionsState.canBeSwitchedOff()).thenReturn(false);
+
+ // when:
+ getBridgeHandler().onProcessActionUpdated(actionsState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_ON));
+ assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF));
+ });
+ }
+
+ @Test
+ public void testHandleCommandDishWarmerProgramActive() {
+ // when:
+ getThingHandler().handleCommand(channel(DISH_WARMER_PROGRAM_ACTIVE), new StringType("3"));
+
+ // then:
+ waitForAssert(() -> {
+ verify(getWebserviceMock()).putProgram(getThingHandler().getDeviceId(), 3);
+ });
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.DISHWASHER_DEVICE_THING_UID;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+
+/**
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API tests
+ * @author Björn Lange - Add elapsed time channel
+ */
+@NonNullByDefault
+public class DishwasherDeviceThingHandlerTest extends AbstractMieleThingHandlerTest {
+ @Override
+ protected AbstractMieleThingHandler setUpThingHandler() {
+ return createThingHandler(MieleCloudBindingConstants.THING_TYPE_DISHWASHER, DISHWASHER_DEVICE_THING_UID,
+ DishwasherDeviceThingHandler.class, MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
+ }
+
+ @Test
+ public void testChannelUpdatesForNullValues() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.getDeviceIdentifier()).thenReturn(DISHWASHER_DEVICE_THING_UID.getId());
+ when(deviceState.getStateType()).thenReturn(Optional.empty());
+ when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.empty());
+ when(deviceState.getSelectedProgram()).thenReturn(Optional.empty());
+ when(deviceState.getSelectedProgramId()).thenReturn(Optional.empty());
+ when(deviceState.getProgramPhase()).thenReturn(Optional.empty());
+ when(deviceState.getProgramPhaseRaw()).thenReturn(Optional.empty());
+ when(deviceState.getStatus()).thenReturn(Optional.empty());
+ when(deviceState.getStatusRaw()).thenReturn(Optional.empty());
+ when(deviceState.getStartTime()).thenReturn(Optional.empty());
+ when(deviceState.getElapsedTime()).thenReturn(Optional.empty());
+ when(deviceState.getDoorState()).thenReturn(Optional.empty());
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ACTIVE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ACTIVE_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PHASE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PHASE_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE_RAW));
+ assertEquals(new StringType(ProgramStatus.PROGRAM_STOPPED.getState()), getChannelState(PROGRAM_START_STOP));
+ assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+ assertEquals(NULL_VALUE_STATE, getChannelState(DELAYED_START_TIME));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ELAPSED_TIME));
+ assertEquals(NULL_VALUE_STATE, getChannelState(DOOR_STATE));
+ });
+ }
+
+ @Test
+ public void testChannelUpdatesForValidValues() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.isInState(any())).thenCallRealMethod();
+ when(deviceState.getDeviceIdentifier()).thenReturn(DISHWASHER_DEVICE_THING_UID.getId());
+ when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.of(true));
+ when(deviceState.getSelectedProgram()).thenReturn(Optional.of("Eco"));
+ when(deviceState.getSelectedProgramId()).thenReturn(Optional.of(4L));
+ when(deviceState.getProgramPhase()).thenReturn(Optional.of("Spülen"));
+ when(deviceState.getProgramPhaseRaw()).thenReturn(Optional.of(2));
+ when(deviceState.getStatus()).thenReturn(Optional.of("Running"));
+ when(deviceState.getStatusRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+ when(deviceState.getStartTime()).thenReturn(Optional.of(3600));
+ when(deviceState.getElapsedTime()).thenReturn(Optional.of(4));
+ when(deviceState.hasError()).thenReturn(false);
+ when(deviceState.hasInfo()).thenReturn(true);
+ when(deviceState.getDoorState()).thenReturn(Optional.of(true));
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(new StringType("Eco"), getChannelState(PROGRAM_ACTIVE));
+ assertEquals(new DecimalType(4), getChannelState(PROGRAM_ACTIVE_RAW));
+ assertEquals(new StringType("Spülen"), getChannelState(PROGRAM_PHASE));
+ assertEquals(new DecimalType(2), getChannelState(PROGRAM_PHASE_RAW));
+ assertEquals(new StringType("Running"), getChannelState(OPERATION_STATE));
+ assertEquals(new DecimalType(StateType.RUNNING.getCode()), getChannelState(OPERATION_STATE_RAW));
+ assertEquals(new StringType(ProgramStatus.PROGRAM_STARTED.getState()), getChannelState(PROGRAM_START_STOP));
+ assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+ assertEquals(new DecimalType(3600), getChannelState(DELAYED_START_TIME));
+ assertEquals(new DecimalType(4), getChannelState(PROGRAM_ELAPSED_TIME));
+ assertEquals(OnOffType.OFF, getChannelState(ERROR_STATE));
+ assertEquals(OnOffType.ON, getChannelState(INFO_STATE));
+ assertEquals(OnOffType.ON, getChannelState(DOOR_STATE));
+ });
+ }
+
+ @Test
+ public void testFinishStateChannelIsSetToOnWhenProgramHasFinished() {
+ // given:
+ DeviceState deviceStateBefore = mock(DeviceState.class);
+ when(deviceStateBefore.getDeviceIdentifier()).thenReturn(DISHWASHER_DEVICE_THING_UID.getId());
+ when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+
+ getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+ DeviceState deviceStateAfter = mock(DeviceState.class);
+ when(deviceStateAfter.getDeviceIdentifier()).thenReturn(DISHWASHER_DEVICE_THING_UID.getId());
+ when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+ when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceStateAfter);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(OnOffType.ON, getChannelState(FINISH_STATE));
+ });
+ }
+
+ @Test
+ public void testTransitionChannelUpdatesForNullValues() {
+ // given:
+ DeviceState deviceStateBefore = mock(DeviceState.class);
+ when(deviceStateBefore.getDeviceIdentifier()).thenReturn(DISHWASHER_DEVICE_THING_UID.getId());
+ when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+ when(deviceStateBefore.getRemainingTime()).thenReturn(Optional.empty());
+ when(deviceStateBefore.getProgress()).thenReturn(Optional.empty());
+
+ getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+ DeviceState deviceStateAfter = mock(DeviceState.class);
+ when(deviceStateAfter.getDeviceIdentifier()).thenReturn(DISHWASHER_DEVICE_THING_UID.getId());
+ when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+ when(deviceStateAfter.getRemainingTime()).thenReturn(Optional.empty());
+ when(deviceStateAfter.getProgress()).thenReturn(Optional.empty());
+
+ // when:
+ getThingHandler().onDeviceStateUpdated(deviceStateAfter);
+
+ waitForAssert(() -> {
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_REMAINING_TIME));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PROGRESS));
+ });
+ }
+
+ @Test
+ public void testTransitionChannelUpdatesForValidValues() {
+ // given:
+ DeviceState deviceStateBefore = mock(DeviceState.class);
+ when(deviceStateBefore.getDeviceIdentifier()).thenReturn(DISHWASHER_DEVICE_THING_UID.getId());
+ when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+ when(deviceStateBefore.getRemainingTime()).thenReturn(Optional.of(10));
+ when(deviceStateBefore.getProgress()).thenReturn(Optional.of(80));
+
+ getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+ DeviceState deviceStateAfter = mock(DeviceState.class);
+ when(deviceStateAfter.getDeviceIdentifier()).thenReturn(DISHWASHER_DEVICE_THING_UID.getId());
+ when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+ when(deviceStateAfter.getRemainingTime()).thenReturn(Optional.of(10));
+ when(deviceStateAfter.getProgress()).thenReturn(Optional.of(80));
+
+ // when:
+ getThingHandler().onDeviceStateUpdated(deviceStateAfter);
+
+ waitForAssert(() -> {
+ assertEquals(new DecimalType(10), getChannelState(PROGRAM_REMAINING_TIME));
+ assertEquals(new DecimalType(80), getChannelState(PROGRAM_PROGRESS));
+ });
+ }
+
+ @Test
+ public void testActionsChannelUpdatesForValidValues() {
+ // given:
+ ActionsState actionsState = mock(ActionsState.class);
+ when(actionsState.getDeviceIdentifier()).thenReturn(DISHWASHER_DEVICE_THING_UID.getId());
+ when(actionsState.canBeStarted()).thenReturn(true);
+ when(actionsState.canBeStopped()).thenReturn(false);
+ when(actionsState.canBeSwitchedOn()).thenReturn(true);
+ when(actionsState.canBeSwitchedOff()).thenReturn(false);
+
+ // when:
+ getBridgeHandler().onProcessActionUpdated(actionsState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_STARTED));
+ assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_STOPPED));
+ assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_ON));
+ assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF));
+ });
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.DRYER_DEVICE_THING_UID;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+
+/**
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API tests
+ * @author Björn Lange - Add elapsed time channel
+ */
+@NonNullByDefault
+public class DryerDeviceThingHandlerTest extends AbstractMieleThingHandlerTest {
+ @Override
+ protected AbstractMieleThingHandler setUpThingHandler() {
+ return createThingHandler(MieleCloudBindingConstants.THING_TYPE_DRYER, DRYER_DEVICE_THING_UID,
+ DryerDeviceThingHandler.class, MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
+ }
+
+ @Test
+ public void testChannelUpdatesForNullValues() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.getDeviceIdentifier()).thenReturn(DRYER_DEVICE_THING_UID.getId());
+ when(deviceState.getStateType()).thenReturn(Optional.empty());
+ when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.empty());
+ when(deviceState.getSelectedProgram()).thenReturn(Optional.empty());
+ when(deviceState.getSelectedProgramId()).thenReturn(Optional.empty());
+ when(deviceState.getProgramPhase()).thenReturn(Optional.empty());
+ when(deviceState.getProgramPhaseRaw()).thenReturn(Optional.empty());
+ when(deviceState.getStatus()).thenReturn(Optional.empty());
+ when(deviceState.getStatusRaw()).thenReturn(Optional.empty());
+ when(deviceState.getStartTime()).thenReturn(Optional.empty());
+ when(deviceState.getElapsedTime()).thenReturn(Optional.empty());
+ when(deviceState.getDryingTarget()).thenReturn(Optional.empty());
+ when(deviceState.getDryingTargetRaw()).thenReturn(Optional.empty());
+ when(deviceState.getLightState()).thenReturn(Optional.empty());
+ when(deviceState.getDoorState()).thenReturn(Optional.empty());
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ACTIVE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ACTIVE_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PHASE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PHASE_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE_RAW));
+ assertEquals(new StringType(ProgramStatus.PROGRAM_STOPPED.getState()), getChannelState(PROGRAM_START_STOP));
+ assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+ assertEquals(NULL_VALUE_STATE, getChannelState(DELAYED_START_TIME));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ELAPSED_TIME));
+ assertEquals(NULL_VALUE_STATE, getChannelState(DRYING_TARGET));
+ assertEquals(NULL_VALUE_STATE, getChannelState(DRYING_TARGET_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(LIGHT_SWITCH));
+ assertEquals(NULL_VALUE_STATE, getChannelState(DOOR_STATE));
+ });
+ }
+
+ @Test
+ public void testChannelUpdatesForValidValues() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.isInState(any())).thenCallRealMethod();
+ when(deviceState.getDeviceIdentifier()).thenReturn(DRYER_DEVICE_THING_UID.getId());
+ when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.of(true));
+ when(deviceState.getSelectedProgram()).thenReturn(Optional.of("Baumwolle"));
+ when(deviceState.getSelectedProgramId()).thenReturn(Optional.of(34L));
+ when(deviceState.getProgramPhase()).thenReturn(Optional.of("Schleudern"));
+ when(deviceState.getProgramPhaseRaw()).thenReturn(Optional.of(3));
+ when(deviceState.getStatus()).thenReturn(Optional.of("Running"));
+ when(deviceState.getStatusRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+ when(deviceState.getStartTime()).thenReturn(Optional.of(3600));
+ when(deviceState.getElapsedTime()).thenReturn(Optional.of(61));
+ when(deviceState.getDryingTarget()).thenReturn(Optional.of("Schranktrocken"));
+ when(deviceState.getDryingTargetRaw()).thenReturn(Optional.of(3));
+ when(deviceState.hasError()).thenReturn(true);
+ when(deviceState.hasInfo()).thenReturn(true);
+ when(deviceState.getLightState()).thenReturn(Optional.of(false));
+ when(deviceState.getDoorState()).thenReturn(Optional.of(false));
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(new StringType("Baumwolle"), getChannelState(PROGRAM_ACTIVE));
+ assertEquals(new DecimalType(34), getChannelState(PROGRAM_ACTIVE_RAW));
+ assertEquals(new StringType("Schleudern"), getChannelState(PROGRAM_PHASE));
+ assertEquals(new DecimalType(3), getChannelState(PROGRAM_PHASE_RAW));
+ assertEquals(new StringType("Running"), getChannelState(OPERATION_STATE));
+ assertEquals(new DecimalType(StateType.RUNNING.getCode()), getChannelState(OPERATION_STATE_RAW));
+ assertEquals(new StringType(ProgramStatus.PROGRAM_STARTED.getState()), getChannelState(PROGRAM_START_STOP));
+ assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+ assertEquals(new DecimalType(3600), getChannelState(DELAYED_START_TIME));
+ assertEquals(new DecimalType(61), getChannelState(PROGRAM_ELAPSED_TIME));
+ assertEquals(new StringType("Schranktrocken"), getChannelState(DRYING_TARGET));
+ assertEquals(new DecimalType(3), getChannelState(DRYING_TARGET_RAW));
+ assertEquals(OnOffType.ON, getChannelState(ERROR_STATE));
+ assertEquals(OnOffType.ON, getChannelState(INFO_STATE));
+ assertEquals(OnOffType.OFF, getChannelState(LIGHT_SWITCH));
+ assertEquals(OnOffType.OFF, getChannelState(DOOR_STATE));
+ });
+ }
+
+ @Test
+ public void testFinishStateChannelIsSetToOnWhenProgramHasFinished() {
+ // given:
+ DeviceState deviceStateBefore = mock(DeviceState.class);
+ when(deviceStateBefore.getDeviceIdentifier()).thenReturn(DRYER_DEVICE_THING_UID.getId());
+ when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+
+ getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+ DeviceState deviceStateAfter = mock(DeviceState.class);
+ when(deviceStateAfter.getDeviceIdentifier()).thenReturn(DRYER_DEVICE_THING_UID.getId());
+ when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+ when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceStateAfter);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(OnOffType.ON, getChannelState(FINISH_STATE));
+ });
+ }
+
+ @Test
+ public void testTransitionChannelUpdatesForNullValues() {
+ // given:
+ DeviceState deviceStateBefore = mock(DeviceState.class);
+ when(deviceStateBefore.getDeviceIdentifier()).thenReturn(DRYER_DEVICE_THING_UID.getId());
+ when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+ when(deviceStateBefore.getRemainingTime()).thenReturn(Optional.empty());
+ when(deviceStateBefore.getProgress()).thenReturn(Optional.empty());
+
+ getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+ DeviceState deviceStateAfter = mock(DeviceState.class);
+ when(deviceStateAfter.getDeviceIdentifier()).thenReturn(DRYER_DEVICE_THING_UID.getId());
+ when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+ when(deviceStateAfter.getRemainingTime()).thenReturn(Optional.empty());
+ when(deviceStateAfter.getProgress()).thenReturn(Optional.empty());
+
+ // when:
+ getThingHandler().onDeviceStateUpdated(deviceStateAfter);
+
+ waitForAssert(() -> {
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_REMAINING_TIME));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PROGRESS));
+ });
+ }
+
+ @Test
+ public void testTransitionChannelUpdatesForValidValues() {
+ // given:
+ DeviceState deviceStateBefore = mock(DeviceState.class);
+ when(deviceStateBefore.getDeviceIdentifier()).thenReturn(DRYER_DEVICE_THING_UID.getId());
+ when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+ when(deviceStateBefore.getRemainingTime()).thenReturn(Optional.of(10));
+ when(deviceStateBefore.getProgress()).thenReturn(Optional.of(80));
+
+ getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+ DeviceState deviceStateAfter = mock(DeviceState.class);
+ when(deviceStateAfter.getDeviceIdentifier()).thenReturn(DRYER_DEVICE_THING_UID.getId());
+ when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+ when(deviceStateAfter.getRemainingTime()).thenReturn(Optional.of(10));
+ when(deviceStateAfter.getProgress()).thenReturn(Optional.of(80));
+
+ // when:
+ getThingHandler().onDeviceStateUpdated(deviceStateAfter);
+
+ waitForAssert(() -> {
+ assertEquals(new DecimalType(10), getChannelState(PROGRAM_REMAINING_TIME));
+ assertEquals(new DecimalType(80), getChannelState(PROGRAM_PROGRESS));
+ });
+ }
+
+ @Test
+ public void testActionsChannelUpdatesForValidValues() {
+ // given:
+ ActionsState actionsState = mock(ActionsState.class);
+ when(actionsState.getDeviceIdentifier()).thenReturn(DRYER_DEVICE_THING_UID.getId());
+ when(actionsState.canBeStarted()).thenReturn(true);
+ when(actionsState.canBeStopped()).thenReturn(false);
+ when(actionsState.canBeSwitchedOn()).thenReturn(true);
+ when(actionsState.canBeSwitchedOff()).thenReturn(false);
+ when(actionsState.canControlLight()).thenReturn(true);
+
+ // when:
+ getBridgeHandler().onProcessActionUpdated(actionsState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_STARTED));
+ assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_STOPPED));
+ assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_ON));
+ assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF));
+ assertEquals(OnOffType.ON, getChannelState(LIGHT_CAN_BE_CONTROLLED));
+ });
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.HOB_DEVICE_THING_UID;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+
+/**
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add plate step
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API tests
+ */
+@NonNullByDefault
+public class HobDeviceThingHandlerTest extends AbstractMieleThingHandlerTest {
+ @Override
+ protected AbstractMieleThingHandler setUpThingHandler() {
+ return createThingHandler(MieleCloudBindingConstants.THING_TYPE_HOB, HOB_DEVICE_THING_UID,
+ HobDeviceThingHandler.class, MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
+ }
+
+ @Test
+ public void testChannelUpdatesForNullValues() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.getDeviceIdentifier()).thenReturn(HOB_DEVICE_THING_UID.getId());
+ when(deviceState.getStateType()).thenReturn(Optional.empty());
+ when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.empty());
+ when(deviceState.getStatus()).thenReturn(Optional.empty());
+ when(deviceState.getStatusRaw()).thenReturn(Optional.empty());
+ when(deviceState.getPlateStep(anyInt())).thenReturn(Optional.empty());
+ when(deviceState.getPlateStepRaw(anyInt())).thenReturn(Optional.empty());
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_1_POWER_STEP));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_1_POWER_STEP_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_2_POWER_STEP));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_2_POWER_STEP_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_3_POWER_STEP));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_3_POWER_STEP_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_4_POWER_STEP));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_4_POWER_STEP_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_5_POWER_STEP));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_5_POWER_STEP_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_6_POWER_STEP));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_6_POWER_STEP_RAW));
+ });
+ }
+
+ @Test
+ public void testChannelUpdatesForValidValues() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.getDeviceIdentifier()).thenReturn(HOB_DEVICE_THING_UID.getId());
+ when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.of(false));
+ when(deviceState.getStatus()).thenReturn(Optional.of("Running"));
+ when(deviceState.getStatusRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+ when(deviceState.hasError()).thenReturn(false);
+ when(deviceState.hasInfo()).thenReturn(true);
+ when(deviceState.getPlateStep(0)).thenReturn(Optional.of("1."));
+ when(deviceState.getPlateStepRaw(0)).thenReturn(Optional.of(2));
+ when(deviceState.getPlateStep(1)).thenReturn(Optional.empty());
+ when(deviceState.getPlateStep(2)).thenReturn(Optional.empty());
+ when(deviceState.getPlateStep(3)).thenReturn(Optional.empty());
+ when(deviceState.getPlateStep(4)).thenReturn(Optional.empty());
+ when(deviceState.getPlateStep(5)).thenReturn(Optional.empty());
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(new StringType("Running"), getChannelState(OPERATION_STATE));
+ assertEquals(new DecimalType(StateType.RUNNING.getCode()), getChannelState(OPERATION_STATE_RAW));
+ assertEquals(OnOffType.OFF, getChannelState(ERROR_STATE));
+ assertEquals(OnOffType.ON, getChannelState(INFO_STATE));
+ assertEquals(new StringType("1."), getChannelState(PLATE_1_POWER_STEP));
+ assertEquals(new DecimalType(2), getChannelState(PLATE_1_POWER_STEP_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_2_POWER_STEP));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_2_POWER_STEP_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_3_POWER_STEP));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_3_POWER_STEP_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_4_POWER_STEP));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_4_POWER_STEP_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_5_POWER_STEP));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_5_POWER_STEP_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_6_POWER_STEP));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PLATE_6_POWER_STEP_RAW));
+ });
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.HOOD_DEVICE_THING_UID;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+
+/**
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API tests
+ */
+@NonNullByDefault
+public class HoodDeviceThingHandlerTest extends AbstractMieleThingHandlerTest {
+ @Override
+ protected AbstractMieleThingHandler setUpThingHandler() {
+ return createThingHandler(MieleCloudBindingConstants.THING_TYPE_HOOD, HOOD_DEVICE_THING_UID,
+ HoodDeviceThingHandler.class, MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
+ }
+
+ @Test
+ public void testChannelUpdatesForNullValues() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.getDeviceIdentifier()).thenReturn(HOOD_DEVICE_THING_UID.getId());
+ when(deviceState.getStateType()).thenReturn(Optional.empty());
+ when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.empty());
+ when(deviceState.getProgramPhase()).thenReturn(Optional.empty());
+ when(deviceState.getProgramPhaseRaw()).thenReturn(Optional.empty());
+ when(deviceState.getStatus()).thenReturn(Optional.empty());
+ when(deviceState.getStatusRaw()).thenReturn(Optional.empty());
+ when(deviceState.getVentilationStep()).thenReturn(Optional.empty());
+ when(deviceState.getVentilationStepRaw()).thenReturn(Optional.empty());
+ when(deviceState.getLightState()).thenReturn(Optional.empty());
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PHASE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PHASE_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(VENTILATION_POWER));
+ assertEquals(NULL_VALUE_STATE, getChannelState(VENTILATION_POWER_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(LIGHT_SWITCH));
+ assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+ });
+ }
+
+ @Test
+ public void testChannelUpdatesForValidValues() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.getDeviceIdentifier()).thenReturn(HOOD_DEVICE_THING_UID.getId());
+ when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.of(false));
+ when(deviceState.getProgramPhase()).thenReturn(Optional.of("Kochen"));
+ when(deviceState.getProgramPhaseRaw()).thenReturn(Optional.of(5));
+ when(deviceState.getStatus()).thenReturn(Optional.of("Running"));
+ when(deviceState.getStatusRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+ when(deviceState.getVentilationStep()).thenReturn(Optional.of("2"));
+ when(deviceState.getVentilationStepRaw()).thenReturn(Optional.of(2));
+ when(deviceState.hasError()).thenReturn(false);
+ when(deviceState.hasInfo()).thenReturn(true);
+ when(deviceState.getLightState()).thenReturn(Optional.of(false));
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(new StringType("Kochen"), getChannelState(PROGRAM_PHASE));
+ assertEquals(new DecimalType(5), getChannelState(PROGRAM_PHASE_RAW));
+ assertEquals(new StringType("Running"), getChannelState(OPERATION_STATE));
+ assertEquals(new DecimalType(StateType.RUNNING.getCode()), getChannelState(OPERATION_STATE_RAW));
+ assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+ assertEquals(new StringType("2"), getChannelState(VENTILATION_POWER));
+ assertEquals(new DecimalType(2), getChannelState(VENTILATION_POWER_RAW));
+ assertEquals(OnOffType.OFF, getChannelState(ERROR_STATE));
+ assertEquals(OnOffType.ON, getChannelState(INFO_STATE));
+ assertEquals(OnOffType.OFF, getChannelState(LIGHT_SWITCH));
+ });
+ }
+
+ @Test
+ public void testActionsChannelUpdatesForValidValues() {
+ // given:
+ ActionsState actionsState = mock(ActionsState.class);
+ when(actionsState.getDeviceIdentifier()).thenReturn(HOOD_DEVICE_THING_UID.getId());
+ when(actionsState.canBeSwitchedOn()).thenReturn(true);
+ when(actionsState.canBeSwitchedOff()).thenReturn(false);
+ when(actionsState.canControlLight()).thenReturn(true);
+
+ // when:
+ getBridgeHandler().onProcessActionUpdated(actionsState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_ON));
+ assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF));
+ assertEquals(OnOffType.ON, getChannelState(LIGHT_CAN_BE_CONTROLLED));
+ });
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.*;
+import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.*;
+
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.I18NKeys;
+import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.auth.OpenHabOAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.util.OpenHabOsgiTest;
+import org.openhab.binding.mielecloud.internal.webservice.ConnectionError;
+import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
+import org.openhab.binding.mielecloud.internal.webservice.MieleWebserviceFactory;
+import org.openhab.binding.mielecloud.internal.webservice.language.CombiningLanguageProvider;
+import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
+import org.openhab.core.auth.client.oauth2.OAuthClientService;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.openhab.core.thing.binding.builder.BridgeBuilder;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class MieleBridgeHandlerTest extends OpenHabOsgiTest {
+ private static final String SERVICE_HANDLE = MieleCloudBindingIntegrationTestConstants.EMAIL;
+ private static final String CONFIG_PARAM_LOCALE = "locale";
+
+ @Nullable
+ private MieleWebservice webserviceMock;
+ @Nullable
+ private String webserviceAccessToken;
+ @Nullable
+ private OAuthFactory oauthFactoryMock;
+ @Nullable
+ private OAuthClientService oauthClientServiceMock;
+
+ @Nullable
+ private Bridge bridge;
+ @Nullable
+ private MieleBridgeHandler handler;
+
+ private MieleWebservice getWebserviceMock() {
+ assertNotNull(webserviceMock);
+ return Objects.requireNonNull(webserviceMock);
+ }
+
+ private OAuthFactory getOAuthFactoryMock() {
+ assertNotNull(oauthFactoryMock);
+ return Objects.requireNonNull(oauthFactoryMock);
+ }
+
+ private OAuthClientService getOAuthClientServiceMock() {
+ OAuthClientService oauthClientServiceMock = this.oauthClientServiceMock;
+ assertNotNull(oauthClientServiceMock);
+ return Objects.requireNonNull(oauthClientServiceMock);
+ }
+
+ private Bridge getBridge() {
+ assertNotNull(bridge);
+ return Objects.requireNonNull(bridge);
+ }
+
+ private MieleBridgeHandler getHandler() {
+ assertNotNull(handler);
+ return Objects.requireNonNull(handler);
+ }
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ setUpWebservice();
+ setUpBridgeThingAndHandler();
+ setUpOAuthFactory();
+ }
+
+ private void setUpWebservice() throws NoSuchFieldException, IllegalAccessException {
+ webserviceMock = mock(MieleWebservice.class);
+ doAnswer(invocation -> {
+ if (invocation != null) {
+ webserviceAccessToken = invocation.getArgument(0);
+ }
+ return null;
+ }).when(getWebserviceMock()).setAccessToken(anyString());
+ when(getWebserviceMock().hasAccessToken()).then(invocation -> webserviceAccessToken != null);
+
+ MieleWebserviceFactory webserviceFactory = mock(MieleWebserviceFactory.class);
+ when(webserviceFactory.create(any())).thenReturn(getWebserviceMock());
+
+ MieleHandlerFactory handlerFactory = getService(ThingHandlerFactory.class, MieleHandlerFactory.class);
+ assertNotNull(handlerFactory);
+ setPrivate(Objects.requireNonNull(handlerFactory), "webserviceFactory", webserviceFactory);
+ }
+
+ private void setUpBridgeThingAndHandler() {
+ when(getWebserviceMock().hasAccessToken()).thenReturn(false);
+
+ bridge = BridgeBuilder
+ .create(MieleCloudBindingConstants.THING_TYPE_BRIDGE,
+ MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID)
+ .withConfiguration(
+ new Configuration(Collections.singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL,
+ MieleCloudBindingIntegrationTestConstants.EMAIL)))
+ .withLabel(MIELE_CLOUD_ACCOUNT_LABEL).build();
+ assertNotNull(bridge);
+
+ getThingRegistry().add(getBridge());
+
+ waitForAssert(() -> {
+ assertNotNull(getBridge().getHandler());
+ assertTrue(getBridge().getHandler() instanceof MieleBridgeHandler, "Handler type is wrong");
+ });
+ handler = (MieleBridgeHandler) getBridge().getHandler();
+ }
+
+ private void setUpOAuthFactory() throws Exception {
+ AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
+ accessTokenResponse.setAccessToken(ACCESS_TOKEN);
+
+ oauthClientServiceMock = mock(OAuthClientService.class);
+ when(oauthClientServiceMock.getAccessTokenResponse()).thenReturn(accessTokenResponse);
+
+ OAuthFactory oAuthFactory = mock(OAuthFactory.class);
+ Mockito.when(oAuthFactory.getOAuthClientService(SERVICE_HANDLE)).thenReturn(getOAuthClientServiceMock());
+ oauthFactoryMock = oAuthFactory;
+
+ OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
+ OpenHabOAuthTokenRefresher.class);
+ assertNotNull(tokenRefresher);
+ setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
+ }
+
+ private void initializeBridgeWithTokens() {
+ getHandler().initialize();
+ assertThingStatusIs(ThingStatus.UNKNOWN);
+ }
+
+ private void assertThingStatusIs(ThingStatus expectedStatus) {
+ assertThingStatusIs(expectedStatus, ThingStatusDetail.NONE);
+ }
+
+ private void assertThingStatusIs(ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail) {
+ assertThingStatusIs(expectedStatus, expectedStatusDetail, null);
+ }
+
+ private void assertThingStatusIs(ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail,
+ @Nullable String expectedDescription) {
+ assertEquals(expectedStatus, getBridge().getStatus());
+ assertEquals(expectedStatusDetail, getBridge().getStatusInfo().getStatusDetail());
+ if (expectedDescription == null) {
+ assertNull(getBridge().getStatusInfo().getDescription());
+ } else {
+ assertEquals(expectedDescription, getBridge().getStatusInfo().getDescription());
+ }
+ }
+
+ @Test
+ public void testThingStatusIsSetToOfflineWithDetailConfigurationPendingAndDescriptionWhenTokensAreNotPassedViaInitialConfiguration()
+ throws Exception {
+ when(getOAuthClientServiceMock().getAccessTokenResponse()).thenReturn(null);
+
+ // when:
+ getHandler().initialize();
+
+ // then:
+ assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
+ MieleCloudBindingConstants.I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_NOT_CONFIGURED);
+ }
+
+ @Test
+ public void testThingStatusIsSetToOfflineWithDetailConfigurationErrorAndDescriptionWhenTheEmailAddressIsInvalid()
+ throws Exception {
+ // given:
+ getBridge().getConfiguration().setProperties(
+ Collections.singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL, "not!a!mail$address"));
+
+ // when:
+ getHandler().initialize();
+
+ // then:
+ assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ MieleCloudBindingConstants.I18NKeys.BRIDGE_STATUS_DESCRIPTION_INVALID_EMAIL);
+ }
+
+ @Test
+ public void testThingStatusIsSetToOfflineWithDetailConfigurationErrorAndDescriptionWhenTheMieleAccountHasNotBeenAuthorized()
+ throws Exception {
+ // given:
+ OAuthFactory oAuthFactory = mock(OAuthFactory.class);
+ Mockito.when(oAuthFactory.getOAuthClientService(SERVICE_HANDLE)).thenReturn(null);
+
+ OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
+ OpenHabOAuthTokenRefresher.class);
+ assertNotNull(tokenRefresher);
+ // Clear the setup configuration and use the failing one for this test.
+ setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
+
+ // when:
+ getHandler().initialize();
+
+ // then:
+ assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ MieleCloudBindingConstants.I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED);
+ }
+
+ @Test
+ public void testThingStatusIsSetToUnknownAndThingWaitsForCloudConnectionWhenTheMieleAccountBecomesAuthorizedAfterTheBridgeWasInitialized()
+ throws Exception {
+ // given:
+ OAuthFactory oAuthFactory = mock(OAuthFactory.class);
+ Mockito.when(oAuthFactory.getOAuthClientService(SERVICE_HANDLE)).thenReturn(null);
+
+ OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
+ OpenHabOAuthTokenRefresher.class);
+ assertNotNull(tokenRefresher);
+ // Clear the setup configuration and use the failing one for this test.
+ setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
+
+ getHandler().initialize();
+
+ assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED);
+
+ setUpOAuthFactory();
+
+ // when:
+ getHandler().dispose();
+ getHandler().initialize();
+
+ // then:
+ assertThingStatusIs(ThingStatus.UNKNOWN);
+ }
+
+ @Test
+ public void whenTheSseConnectionIsEstablishedThenTheThingStatusIsSetToOnline() throws Exception {
+ // given:
+ initializeBridgeWithTokens();
+
+ // when:
+ getHandler().onConnectionAlive();
+
+ // then:
+ assertThingStatusIs(ThingStatus.ONLINE);
+ }
+
+ @Test
+ public void whenAnAuthorizationFailedErrorIsReportedThenTheAccessTokenIsRefreshedAndTheSseConnectionRestored()
+ throws Exception {
+ // given:
+ AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
+ accessTokenResponse.setAccessToken(ACCESS_TOKEN);
+ when(getOAuthClientServiceMock().refreshToken()).thenReturn(accessTokenResponse);
+
+ initializeBridgeWithTokens();
+ getHandler().onConnectionAlive();
+
+ // when:
+ getHandler().onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
+
+ // then:
+ verify(getOAuthClientServiceMock()).refreshToken();
+ verify(getWebserviceMock()).connectSse();
+ assertThingStatusIs(ThingStatus.ONLINE);
+ }
+
+ @Test
+ public void whenAnAuthorizationFailedErrorIsReportedAndTokenRefreshFailsThenSseConnectionIsTerminatedAndTheStatusSetToOfflineWithDetailConfigurationError()
+ throws Exception {
+ // given:
+ when(getOAuthClientServiceMock().refreshToken()).thenReturn(new AccessTokenResponse());
+ initializeBridgeWithTokens();
+ getHandler().onConnectionAlive();
+
+ // when:
+ getHandler().onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
+
+ // then:
+ verify(getOAuthClientServiceMock()).refreshToken();
+ verify(getWebserviceMock()).disconnectSse();
+ assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_REFRESH_FAILED);
+ }
+
+ @Test
+ public void whenARequestExecutionFailedErrorIsReportedAndNoRetriesHaveBeenMadeThenItHasNoEffectOnTheThingStatus()
+ throws Exception {
+ // given:
+ initializeBridgeWithTokens();
+ getHandler().onConnectionAlive();
+
+ // when:
+ getHandler().onConnectionError(ConnectionError.REQUEST_EXECUTION_FAILED, 0);
+
+ // then:
+ assertThingStatusIs(ThingStatus.ONLINE);
+ }
+
+ @Test
+ public void whenARequestExecutionFailedErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
+ throws Exception {
+ // given:
+ initializeBridgeWithTokens();
+ getHandler().onConnectionAlive();
+
+ // when:
+ getHandler().onConnectionError(ConnectionError.REQUEST_EXECUTION_FAILED, 10);
+
+ // then:
+ assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+ }
+
+ @Test
+ public void whenARequestExecutionFailedErrorIsReportedAndThingIsInStatusUnknownThenTheThingStatusIsOfflineWithDetailCommunicationError()
+ throws Exception {
+ // given:
+ initializeBridgeWithTokens();
+
+ // when:
+ getHandler().onConnectionError(ConnectionError.REQUEST_EXECUTION_FAILED, 0);
+
+ // then:
+ assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+ }
+
+ @Test
+ public void whenAServiceUnavailableErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
+ throws Exception {
+ // given:
+ initializeBridgeWithTokens();
+ getHandler().onConnectionAlive();
+
+ // when:
+ getHandler().onConnectionError(ConnectionError.SERVICE_UNAVAILABLE, 10);
+
+ // then:
+ assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+ }
+
+ @Test
+ public void whenAResponseMalformedErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
+ throws Exception {
+ // given:
+ initializeBridgeWithTokens();
+
+ // when:
+ getHandler().onConnectionError(ConnectionError.RESPONSE_MALFORMED, 10);
+
+ // then:
+ assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+ }
+
+ @Test
+ public void whenATimeoutErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
+ throws Exception {
+ // given:
+ initializeBridgeWithTokens();
+
+ // when:
+ getHandler().onConnectionError(ConnectionError.TIMEOUT, 10);
+
+ // then:
+ assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+ }
+
+ @Test
+ public void whenATooManyRequestsErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
+ throws Exception {
+ // given:
+ initializeBridgeWithTokens();
+
+ // when:
+ getHandler().onConnectionError(ConnectionError.TOO_MANY_RERQUESTS, 10);
+
+ // then:
+ assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+ }
+
+ @Test
+ public void whenAServerErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
+ throws Exception {
+ // given:
+ initializeBridgeWithTokens();
+
+ // when:
+ getHandler().onConnectionError(ConnectionError.SERVER_ERROR, 10);
+
+ // then:
+ assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
+ I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
+ }
+
+ @Test
+ public void whenARequestInterruptedErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
+ throws Exception {
+ // given:
+ initializeBridgeWithTokens();
+
+ // when:
+ getHandler().onConnectionError(ConnectionError.REQUEST_INTERRUPTED, 10);
+
+ // then:
+ assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
+ I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
+ }
+
+ @Test
+ public void whenSomeOtherHttpErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
+ throws Exception {
+ // given:
+ initializeBridgeWithTokens();
+
+ // when:
+ getHandler().onConnectionError(ConnectionError.OTHER_HTTP_ERROR, 10);
+
+ // then:
+ assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
+ I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
+ }
+
+ @Test
+ public void whenARequestIsInterruptedDuringInitializationThenTheThingStatusIsNotModified() throws Exception {
+ // given:
+ initializeBridgeWithTokens();
+
+ // when:
+ getHandler().onConnectionError(ConnectionError.REQUEST_INTERRUPTED, 0);
+
+ // then:
+ assertThingStatusIs(ThingStatus.UNKNOWN);
+ }
+
+ @Test
+ public void whenTheAccessTokenWasRefreshedThenTheWebserviceIsSetIntoAnOperationalState()
+ throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+ // given:
+ getHandler().initialize();
+
+ // when:
+ getHandler().onNewAccessToken(ACCESS_TOKEN);
+
+ // then:
+ verify(getWebserviceMock(), atLeast(1)).setAccessToken(ACCESS_TOKEN);
+ verify(getWebserviceMock(), atLeast(1)).connectSse();
+ }
+
+ @Test
+ public void whenTheHandlerIsDisposedThenTheSseConnectionIsDisconnectedAndTheLanguageProviderIsUnset()
+ throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+ // given:
+ getHandler().initialize();
+
+ // when:
+ getHandler().dispose();
+
+ // then:
+ verify(getWebserviceMock()).disconnectSse();
+
+ CombiningLanguageProvider languageProvider = getPrivate(getHandler(), "languageProvider");
+ assertNull(getPrivate(languageProvider, "prioritizedLanguageProvider"));
+ }
+
+ @Test
+ public void testNoLanguageIsReturnedWhenTheConfigurationParameterIsNotSet() {
+ // when:
+ Optional<String> language = getHandler().getLanguage();
+
+ // then:
+ assertFalse(language.isPresent());
+ }
+
+ @Test
+ public void testNoLanguageIsReturnedWhenTheConfigurationParameterIsEmpty() {
+ // given:
+ getHandler().handleConfigurationUpdate(Collections.singletonMap(CONFIG_PARAM_LOCALE, ""));
+
+ // when:
+ Optional<String> language = getHandler().getLanguage();
+
+ // then:
+ assertFalse(language.isPresent());
+ }
+
+ @Test
+ public void testNoLanguageIsReturnedWhenTheConfigurationParameterIsNotAValidTwoLetterLanguageCode() {
+ // given:
+ getHandler().handleConfigurationUpdate(Collections.singletonMap(CONFIG_PARAM_LOCALE, "Deutsch"));
+
+ // when:
+ Optional<String> language = getHandler().getLanguage();
+
+ // then:
+ assertFalse(language.isPresent());
+ }
+
+ @Test
+ public void testAValidTwoLetterLanguageCodeIsReturnedWhenTheConfigurationParameterIsSetToTheTwoLetterLanguageCode() {
+ // given:
+ getHandler().handleConfigurationUpdate(Collections.singletonMap(CONFIG_PARAM_LOCALE, "DE"));
+
+ // when:
+ String language = getHandler().getLanguage().get();
+
+ // then:
+ assertEquals("DE", language);
+ }
+
+ @Test
+ public void testWhenTheThingIsRemovedThenTheWebserviceIsLoggedOut() throws Exception {
+ // given:
+ initializeBridgeWithTokens();
+
+ // when:
+ getThingRegistry().remove(getHandler().getThing().getUID());
+
+ // then:
+ waitForAssert(() -> {
+ verify(getWebserviceMock()).logout();
+ });
+ }
+
+ @Test
+ public void testWhenTheThingIsRemovedThenTheTokensAreRemovedFromTheStorage() throws Exception {
+ // given:
+ initializeBridgeWithTokens();
+
+ // when:
+ getThingRegistry().remove(getHandler().getThing().getUID());
+
+ // then:
+ waitForAssert(() -> {
+ verify(getOAuthFactoryMock()).deleteServiceAndAccessToken(SERVICE_HANDLE);
+ });
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.*;
+import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.*;
+
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.auth.OpenHabOAuthTokenRefresher;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
+import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
+import org.openhab.core.auth.client.oauth2.OAuthClientService;
+import org.openhab.core.auth.client.oauth2.OAuthFactory;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.test.java.JavaOSGiTest;
+import org.openhab.core.test.storage.VolatileStorageService;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingRegistry;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.openhab.core.thing.binding.builder.BridgeBuilder;
+import org.openhab.core.thing.binding.builder.ThingBuilder;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class MieleHandlerFactoryTest extends JavaOSGiTest {
+ private static final String DEVICE_IDENTIFIER = "000124430016";
+
+ private static final ThingUID WASHING_MACHINE_TYPE = new ThingUID(
+ MieleCloudBindingConstants.THING_TYPE_WASHING_MACHINE, DEVICE_IDENTIFIER);
+ private static final ThingUID OVEN_DEVICE_TYPE = new ThingUID(MieleCloudBindingConstants.THING_TYPE_OVEN,
+ DEVICE_IDENTIFIER);
+ private static final ThingUID HOB_DEVICE_TYPE = new ThingUID(MieleCloudBindingConstants.THING_TYPE_HOB,
+ DEVICE_IDENTIFIER);
+ private static final ThingUID FRIDGE_FREEZER_DEVICE_TYPE = new ThingUID(
+ MieleCloudBindingConstants.THING_TYPE_FRIDGE_FREEZER, DEVICE_IDENTIFIER);
+ private static final ThingUID HOOD_DEVICE_TYPE = new ThingUID(MieleCloudBindingConstants.THING_TYPE_HOOD,
+ DEVICE_IDENTIFIER);
+ private static final ThingUID COFFEE_DEVICE_TYPE = new ThingUID(MieleCloudBindingConstants.THING_TYPE_COFFEE_SYSTEM,
+ DEVICE_IDENTIFIER);
+ private static final ThingUID WINE_STORAGE_DEVICE_TYPE = new ThingUID(
+ MieleCloudBindingConstants.THING_TYPE_WINE_STORAGE, DEVICE_IDENTIFIER);
+ private static final ThingUID DRYER_DEVICE_TYPE = new ThingUID(MieleCloudBindingConstants.THING_TYPE_DRYER,
+ DEVICE_IDENTIFIER);
+ private static final ThingUID DISHWASHER_DEVICE_TYPE = new ThingUID(
+ MieleCloudBindingConstants.THING_TYPE_DISHWASHER, DEVICE_IDENTIFIER);
+ private static final ThingUID DISH_WARMER_DEVICE_TYPE = new ThingUID(
+ MieleCloudBindingConstants.THING_TYPE_DISH_WARMER, DEVICE_IDENTIFIER);
+ private static final ThingUID ROBOTIC_VACUUM_CLEANER_DEVICE_TYPE = new ThingUID(
+ MieleCloudBindingConstants.THING_TYPE_ROBOTIC_VACUUM_CLEANER, DEVICE_IDENTIFIER);
+
+ @Nullable
+ private ThingRegistry thingRegistry;
+
+ private ThingRegistry getThingRegistry() {
+ assertNotNull(thingRegistry);
+ return Objects.requireNonNull(thingRegistry);
+ }
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ registerVolatileStorageService();
+ thingRegistry = getService(ThingRegistry.class, ThingRegistry.class);
+ assertNotNull(thingRegistry, "Thing registry is missing");
+
+ // Ensure the MieleWebservice is not initialized.
+ MieleHandlerFactory factory = getService(ThingHandlerFactory.class, MieleHandlerFactory.class);
+ assertNotNull(factory);
+
+ // Assume an access token has already been stored
+ AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
+ accessTokenResponse.setAccessToken(ACCESS_TOKEN);
+
+ OAuthClientService oAuthClientService = mock(OAuthClientService.class);
+ when(oAuthClientService.getAccessTokenResponse()).thenReturn(accessTokenResponse);
+
+ OAuthFactory oAuthFactory = mock(OAuthFactory.class);
+ when(oAuthFactory.getOAuthClientService(MieleCloudBindingIntegrationTestConstants.EMAIL))
+ .thenReturn(oAuthClientService);
+
+ OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
+ OpenHabOAuthTokenRefresher.class);
+ assertNotNull(tokenRefresher);
+ setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
+ }
+
+ @Test
+ public void testHandlerCanBeCreatedForGenesisBridge() throws Exception {
+ // when:
+ Bridge bridge = BridgeBuilder
+ .create(MieleCloudBindingConstants.THING_TYPE_BRIDGE,
+ MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID)
+ .withConfiguration(
+ new Configuration(Collections.singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL,
+ MieleCloudBindingIntegrationTestConstants.EMAIL)))
+ .withLabel(MIELE_CLOUD_ACCOUNT_LABEL).build();
+ assertNotNull(bridge);
+
+ getThingRegistry().add(bridge);
+
+ // then:
+ waitForAssert(() -> {
+ assertNotNull(bridge.getHandler());
+ assertTrue(bridge.getHandler() instanceof MieleBridgeHandler, "Handler type is wrong");
+ });
+
+ MieleBridgeHandler handler = (MieleBridgeHandler) bridge.getHandler();
+ assertNotNull(handler);
+ }
+
+ @Test
+ public void testWebserviceIsInitializedOnHandlerInitialization() throws Exception {
+ // given:
+ Bridge bridge = BridgeBuilder
+ .create(MieleCloudBindingConstants.THING_TYPE_BRIDGE,
+ MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID)
+ .withConfiguration(
+ new Configuration(Collections.singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL,
+ MieleCloudBindingIntegrationTestConstants.EMAIL)))
+ .withLabel(MIELE_CLOUD_ACCOUNT_LABEL).build();
+ assertNotNull(bridge);
+
+ getThingRegistry().add(bridge);
+
+ waitForAssert(() -> {
+ assertNotNull(bridge.getHandler());
+ assertTrue(bridge.getHandler() instanceof MieleBridgeHandler, "Handler type is wrong");
+ });
+
+ MieleBridgeHandler handler = (MieleBridgeHandler) bridge.getHandler();
+ assertNotNull(handler);
+
+ // when:
+ handler.initialize();
+
+ // then:
+ assertEquals(ACCESS_TOKEN,
+ handler.getThing().getProperties().get(MieleCloudBindingConstants.PROPERTY_ACCESS_TOKEN));
+
+ MieleWebservice webservice = getPrivate(handler, "webService");
+ assertNotNull(webservice);
+ Optional<String> accessToken = getPrivate(webservice, "accessToken");
+ assertEquals(Optional.of(ACCESS_TOKEN), accessToken);
+ }
+
+ private void verifyHandlerCreation(MieleWebservice webservice, Thing thing,
+ Class<? extends ThingHandler> expectedHandlerClass)
+ throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+ getThingRegistry().add(thing);
+
+ // then:
+ waitForAssert(() -> {
+ ThingHandler handler = thing.getHandler();
+ assertNotNull(handler);
+ assertTrue(expectedHandlerClass.isAssignableFrom(handler.getClass()), "Handler type is wrong");
+ });
+ }
+
+ private void testHandlerCanBeCreatedForMieleDevice(ThingTypeUID thingTypeUid, ThingUID thingUid, String label,
+ Class<? extends ThingHandler> expectedHandlerClass)
+ throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+ // given:
+ MieleWebservice webservice = mock(MieleWebservice.class);
+
+ MieleHandlerFactory factory = getService(ThingHandlerFactory.class, MieleHandlerFactory.class);
+ assertNotNull(factory);
+
+ // when:
+ Thing device = ThingBuilder.create(thingTypeUid, thingUid)
+ .withConfiguration(new Configuration(Collections
+ .singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER, DEVICE_IDENTIFIER)))
+ .withLabel(label).build();
+
+ assertNotNull(device);
+ verifyHandlerCreation(webservice, device, expectedHandlerClass);
+ }
+
+ @Test
+ public void testHandlerCanBeCreatedForGenesisBridgeWithEmptyConfiguration() throws Exception {
+ // when:
+ Bridge bridge = BridgeBuilder
+ .create(MieleCloudBindingConstants.THING_TYPE_BRIDGE,
+ MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID)
+ .withConfiguration(
+ new Configuration(Collections.singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL,
+ MieleCloudBindingIntegrationTestConstants.EMAIL)))
+ .withLabel(MIELE_CLOUD_ACCOUNT_LABEL).build();
+ assertNotNull(bridge);
+
+ getThingRegistry().add(bridge);
+
+ // then:
+ waitForAssert(() -> {
+ assertNotNull(bridge.getHandler());
+ assertTrue(bridge.getHandler() instanceof MieleBridgeHandler, "Handler type is wrong");
+ });
+ }
+
+ @Test
+ public void testHandlerCanBeCreatedForWashingDevice()
+ throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+ testHandlerCanBeCreatedForMieleDevice(MieleCloudBindingConstants.THING_TYPE_WASHING_MACHINE,
+ WASHING_MACHINE_TYPE, "DA-6996", WashingDeviceThingHandler.class);
+ }
+
+ @Test
+ public void testHandlerCanBeCreatedForOvenDevice()
+ throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+ testHandlerCanBeCreatedForMieleDevice(MieleCloudBindingConstants.THING_TYPE_OVEN, OVEN_DEVICE_TYPE, "OV-6887",
+ OvenDeviceThingHandler.class);
+ }
+
+ @Test
+ public void testHandlerCanBeCreatedForHobDevice()
+ throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+ testHandlerCanBeCreatedForMieleDevice(MieleCloudBindingConstants.THING_TYPE_HOB, HOB_DEVICE_TYPE, "HB-3887",
+ HobDeviceThingHandler.class);
+ }
+
+ @Test
+ public void testHandlerCanBeCreatedForFridgeFreezerDevice()
+ throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+ testHandlerCanBeCreatedForMieleDevice(MieleCloudBindingConstants.THING_TYPE_FRIDGE_FREEZER,
+ FRIDGE_FREEZER_DEVICE_TYPE, "CD-6097", CoolingDeviceThingHandler.class);
+ }
+
+ @Test
+ public void testHandlerCanBeCreatedForHoodDevice()
+ throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+ testHandlerCanBeCreatedForMieleDevice(MieleCloudBindingConstants.THING_TYPE_HOOD, HOOD_DEVICE_TYPE, "HD-2097",
+ HoodDeviceThingHandler.class);
+ }
+
+ @Test
+ public void testHandlerCanBeCreatedForCoffeeDevice()
+ throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+ testHandlerCanBeCreatedForMieleDevice(MieleCloudBindingConstants.THING_TYPE_COFFEE_SYSTEM, COFFEE_DEVICE_TYPE,
+ "DA-6997", CoffeeSystemThingHandler.class);
+ }
+
+ @Test
+ public void testHandlerCanBeCreatedForWineStorageDevice()
+ throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+ testHandlerCanBeCreatedForMieleDevice(MieleCloudBindingConstants.THING_TYPE_WINE_STORAGE,
+ WINE_STORAGE_DEVICE_TYPE, "WS-6907", WineStorageDeviceThingHandler.class);
+ }
+
+ @Test
+ public void testHandlerCanBeCreatedForDryerDevice()
+ throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+ testHandlerCanBeCreatedForMieleDevice(MieleCloudBindingConstants.THING_TYPE_DRYER, DRYER_DEVICE_TYPE, "DR-0907",
+ DryerDeviceThingHandler.class);
+ }
+
+ @Test
+ public void testHandlerCanBeCreatedForDishwasherDevice()
+ throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+ testHandlerCanBeCreatedForMieleDevice(MieleCloudBindingConstants.THING_TYPE_DISHWASHER, DISHWASHER_DEVICE_TYPE,
+ "DR-0907", DishwasherDeviceThingHandler.class);
+ }
+
+ @Test
+ public void testHandlerCanBeCreatedForDishWarmerDevice()
+ throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+ testHandlerCanBeCreatedForMieleDevice(MieleCloudBindingConstants.THING_TYPE_DISH_WARMER,
+ DISH_WARMER_DEVICE_TYPE, "DW-0907", DishWarmerDeviceThingHandler.class);
+ }
+
+ @Test
+ public void testHandlerCanBeCreatedForRoboticVacuumCleanerDevice()
+ throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+ testHandlerCanBeCreatedForMieleDevice(MieleCloudBindingConstants.THING_TYPE_ROBOTIC_VACUUM_CLEANER,
+ ROBOTIC_VACUUM_CLEANER_DEVICE_TYPE, "RVC-0907", RoboticVacuumCleanerDeviceThingHandler.class);
+ }
+
+ /**
+ * Registers a volatile storage service.
+ */
+ @Override
+ protected void registerVolatileStorageService() {
+ registerService(new VolatileStorageService());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.OVEN_DEVICE_THING_UID;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.SIUnits;
+
+/**
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add pre-heat finished channel
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API tests
+ * @author Björn Lange - Add elapsed time channel
+ */
+@NonNullByDefault
+public class OvenDeviceThingHandlerTest extends AbstractMieleThingHandlerTest {
+ @Override
+ protected AbstractMieleThingHandler setUpThingHandler() {
+ return createThingHandler(MieleCloudBindingConstants.THING_TYPE_OVEN, OVEN_DEVICE_THING_UID,
+ OvenDeviceThingHandler.class, MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
+ }
+
+ @Test
+ public void testChannelUpdatesForNullValues() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.getDeviceIdentifier()).thenReturn(OVEN_DEVICE_THING_UID.getId());
+ when(deviceState.getStateType()).thenReturn(Optional.empty());
+ when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.empty());
+ when(deviceState.getSelectedProgram()).thenReturn(Optional.empty());
+ when(deviceState.getSelectedProgramId()).thenReturn(Optional.empty());
+ when(deviceState.getProgramPhase()).thenReturn(Optional.empty());
+ when(deviceState.getProgramPhaseRaw()).thenReturn(Optional.empty());
+ when(deviceState.getStatus()).thenReturn(Optional.empty());
+ when(deviceState.getStatusRaw()).thenReturn(Optional.empty());
+ when(deviceState.getStartTime()).thenReturn(Optional.empty());
+ when(deviceState.getElapsedTime()).thenReturn(Optional.empty());
+ when(deviceState.hasPreHeatFinished()).thenReturn(Optional.empty());
+ when(deviceState.getTargetTemperature(0)).thenReturn(Optional.empty());
+ when(deviceState.getTemperature(0)).thenReturn(Optional.empty());
+ when(deviceState.getLightState()).thenReturn(Optional.empty());
+ when(deviceState.getDoorState()).thenReturn(Optional.empty());
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ACTIVE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ACTIVE_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PHASE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PHASE_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE_RAW));
+ assertEquals(new StringType(ProgramStatus.PROGRAM_STOPPED.getState()), getChannelState(PROGRAM_START_STOP));
+ assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+ assertEquals(NULL_VALUE_STATE, getChannelState(DELAYED_START_TIME));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ELAPSED_TIME));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PRE_HEAT_FINISHED));
+ assertEquals(NULL_VALUE_STATE, getChannelState(TEMPERATURE_TARGET));
+ assertEquals(NULL_VALUE_STATE, getChannelState(TEMPERATURE_CURRENT));
+ assertEquals(NULL_VALUE_STATE, getChannelState(LIGHT_SWITCH));
+ assertEquals(NULL_VALUE_STATE, getChannelState(DOOR_STATE));
+ });
+ }
+
+ @Test
+ public void testChannelUpdatesForValidValues() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.isInState(any())).thenCallRealMethod();
+ when(deviceState.getDeviceIdentifier()).thenReturn(OVEN_DEVICE_THING_UID.getId());
+ when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.of(false));
+ when(deviceState.getSelectedProgram()).thenReturn(Optional.of("Grill"));
+ when(deviceState.getSelectedProgramId()).thenReturn(Optional.of(6L));
+ when(deviceState.getProgramPhase()).thenReturn(Optional.of("Heat"));
+ when(deviceState.getProgramPhaseRaw()).thenReturn(Optional.of(6));
+ when(deviceState.getStatus()).thenReturn(Optional.of("Running"));
+ when(deviceState.getStatusRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+ when(deviceState.getStartTime()).thenReturn(Optional.of(3600));
+ when(deviceState.getElapsedTime()).thenReturn(Optional.of(62));
+ when(deviceState.hasPreHeatFinished()).thenReturn(Optional.of(true));
+ when(deviceState.getTargetTemperature(0)).thenReturn(Optional.of(180));
+ when(deviceState.getTemperature(0)).thenReturn(Optional.of(181));
+ when(deviceState.hasError()).thenReturn(false);
+ when(deviceState.hasInfo()).thenReturn(true);
+ when(deviceState.getLightState()).thenReturn(Optional.of(false));
+ when(deviceState.getDoorState()).thenReturn(Optional.of(false));
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(new StringType("Grill"), getChannelState(PROGRAM_ACTIVE));
+ assertEquals(new DecimalType(6), getChannelState(PROGRAM_ACTIVE_RAW));
+ assertEquals(new StringType("Heat"), getChannelState(PROGRAM_PHASE));
+ assertEquals(new DecimalType(6), getChannelState(PROGRAM_PHASE_RAW));
+ assertEquals(new StringType("Running"), getChannelState(OPERATION_STATE));
+ assertEquals(new DecimalType(StateType.RUNNING.getCode()), getChannelState(OPERATION_STATE_RAW));
+ assertEquals(new StringType(ProgramStatus.PROGRAM_STARTED.getState()), getChannelState(PROGRAM_START_STOP));
+ assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+ assertEquals(new DecimalType(3600), getChannelState(DELAYED_START_TIME));
+ assertEquals(new DecimalType(62), getChannelState(PROGRAM_ELAPSED_TIME));
+ assertEquals(OnOffType.ON, getChannelState(PRE_HEAT_FINISHED));
+ assertEquals(new QuantityType<>(180, SIUnits.CELSIUS), getChannelState(TEMPERATURE_TARGET));
+ assertEquals(new QuantityType<>(181, SIUnits.CELSIUS), getChannelState(TEMPERATURE_CURRENT));
+ assertEquals(OnOffType.OFF, getChannelState(ERROR_STATE));
+ assertEquals(OnOffType.ON, getChannelState(INFO_STATE));
+ assertEquals(OnOffType.OFF, getChannelState(LIGHT_SWITCH));
+ assertEquals(OnOffType.OFF, getChannelState(DOOR_STATE));
+ });
+ }
+
+ @Test
+ public void testFinishStateChannelIsSetToOnWhenProgramHasFinished() {
+ // given:
+ DeviceState deviceStateBefore = mock(DeviceState.class);
+ when(deviceStateBefore.getDeviceIdentifier()).thenReturn(OVEN_DEVICE_THING_UID.getId());
+ when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+
+ getBridgeHandler().onDeviceStateUpdated(deviceStateBefore);
+
+ DeviceState deviceStateAfter = mock(DeviceState.class);
+ when(deviceStateAfter.getDeviceIdentifier()).thenReturn(OVEN_DEVICE_THING_UID.getId());
+ when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+ when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceStateAfter);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(OnOffType.ON, getChannelState(FINISH_STATE));
+ });
+ }
+
+ @Test
+ public void testTransitionChannelUpdatesForNullValues() {
+ // given:
+ DeviceState deviceStateBefore = mock(DeviceState.class);
+ when(deviceStateBefore.getDeviceIdentifier()).thenReturn(OVEN_DEVICE_THING_UID.getId());
+ when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+ when(deviceStateBefore.getRemainingTime()).thenReturn(Optional.empty());
+ when(deviceStateBefore.getProgress()).thenReturn(Optional.empty());
+
+ getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+ DeviceState deviceStateAfter = mock(DeviceState.class);
+ when(deviceStateAfter.getDeviceIdentifier()).thenReturn(OVEN_DEVICE_THING_UID.getId());
+ when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+ when(deviceStateAfter.getRemainingTime()).thenReturn(Optional.empty());
+ when(deviceStateAfter.getProgress()).thenReturn(Optional.empty());
+
+ // when:
+ getThingHandler().onDeviceStateUpdated(deviceStateAfter);
+
+ waitForAssert(() -> {
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_REMAINING_TIME));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PROGRESS));
+ });
+ }
+
+ @Test
+ public void testTransitionChannelUpdatesForValidValues() {
+ // given:
+ DeviceState deviceStateBefore = mock(DeviceState.class);
+ when(deviceStateBefore.getDeviceIdentifier()).thenReturn(OVEN_DEVICE_THING_UID.getId());
+ when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+ when(deviceStateBefore.getRemainingTime()).thenReturn(Optional.of(10));
+ when(deviceStateBefore.getProgress()).thenReturn(Optional.of(80));
+
+ getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+ DeviceState deviceStateAfter = mock(DeviceState.class);
+ when(deviceStateAfter.getDeviceIdentifier()).thenReturn(OVEN_DEVICE_THING_UID.getId());
+ when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+ when(deviceStateAfter.getRemainingTime()).thenReturn(Optional.of(10));
+ when(deviceStateAfter.getProgress()).thenReturn(Optional.of(80));
+
+ // when:
+ getThingHandler().onDeviceStateUpdated(deviceStateAfter);
+
+ waitForAssert(() -> {
+ assertEquals(new DecimalType(10), getChannelState(PROGRAM_REMAINING_TIME));
+ assertEquals(new DecimalType(80), getChannelState(PROGRAM_PROGRESS));
+ });
+ }
+
+ @Test
+ public void testActionsChannelUpdatesForValidValues() {
+ // given:
+ ActionsState actionsState = mock(ActionsState.class);
+ when(actionsState.getDeviceIdentifier()).thenReturn(OVEN_DEVICE_THING_UID.getId());
+ when(actionsState.canBeStarted()).thenReturn(true);
+ when(actionsState.canBeStopped()).thenReturn(false);
+ when(actionsState.canBeSwitchedOn()).thenReturn(true);
+ when(actionsState.canBeSwitchedOff()).thenReturn(false);
+ when(actionsState.canControlLight()).thenReturn(true);
+
+ // when:
+ getBridgeHandler().onProcessActionUpdated(actionsState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_STARTED));
+ assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_STOPPED));
+ assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_ON));
+ assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF));
+ assertEquals(OnOffType.ON, getChannelState(LIGHT_CAN_BE_CONTROLLED));
+ });
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+
+/**
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public class RoboticVacuumCleanerDeviceThingHandlerTest extends AbstractMieleThingHandlerTest {
+ @Override
+ protected AbstractMieleThingHandler setUpThingHandler() {
+ return createThingHandler(MieleCloudBindingConstants.THING_TYPE_ROBOTIC_VACUUM_CLEANER,
+ MieleCloudBindingIntegrationTestConstants.ROBOTIC_VACUUM_CLEANER_THING_UID,
+ RoboticVacuumCleanerDeviceThingHandler.class, MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
+ }
+
+ @Test
+ public void testChannelUpdatesForNullValues() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.getDeviceIdentifier())
+ .thenReturn(MieleCloudBindingIntegrationTestConstants.ROBOTIC_VACUUM_CLEANER_THING_UID.getId());
+ when(deviceState.getSelectedProgramId()).thenReturn(Optional.empty());
+ when(deviceState.getStatus()).thenReturn(Optional.empty());
+ when(deviceState.getStatusRaw()).thenReturn(Optional.empty());
+ when(deviceState.getStateType()).thenReturn(Optional.empty());
+ when(deviceState.hasError()).thenReturn(false);
+ when(deviceState.hasInfo()).thenReturn(false);
+ when(deviceState.getBatteryLevel()).thenReturn(Optional.empty());
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(NULL_VALUE_STATE, getChannelState(VACUUM_CLEANER_PROGRAM_ACTIVE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE_RAW));
+ assertEquals(new StringType(ProgramStatus.PROGRAM_STOPPED.getState()),
+ getChannelState(PROGRAM_START_STOP_PAUSE));
+ assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+ assertEquals(OnOffType.OFF, getChannelState(ERROR_STATE));
+ assertEquals(OnOffType.OFF, getChannelState(INFO_STATE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(BATTERY_LEVEL));
+ });
+ }
+
+ @Test
+ public void testChannelUpdatesForValidValues() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.isInState(any())).thenCallRealMethod();
+ when(deviceState.getDeviceIdentifier())
+ .thenReturn(MieleCloudBindingIntegrationTestConstants.ROBOTIC_VACUUM_CLEANER_THING_UID.getId());
+ when(deviceState.getSelectedProgramId()).thenReturn(Optional.of(1L));
+ when(deviceState.getStatus()).thenReturn(Optional.of("Running"));
+ when(deviceState.getStatusRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+ when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceState.hasError()).thenReturn(true);
+ when(deviceState.hasInfo()).thenReturn(true);
+ when(deviceState.getBatteryLevel()).thenReturn(Optional.of(25));
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(new StringType("1"), getChannelState(VACUUM_CLEANER_PROGRAM_ACTIVE));
+ assertEquals(new StringType("Running"), getChannelState(OPERATION_STATE));
+ assertEquals(new DecimalType(StateType.RUNNING.getCode()), getChannelState(OPERATION_STATE_RAW));
+ assertEquals(new StringType(ProgramStatus.PROGRAM_STARTED.getState()),
+ getChannelState(PROGRAM_START_STOP_PAUSE));
+ assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+ assertEquals(OnOffType.ON, getChannelState(ERROR_STATE));
+ assertEquals(OnOffType.ON, getChannelState(INFO_STATE));
+ assertEquals(new DecimalType(25), getChannelState(BATTERY_LEVEL));
+ });
+ }
+
+ @Test
+ public void testFinishStateChannelIsSetToOnWhenProgramHasFinished() {
+ // given:
+ DeviceState deviceStateBefore = mock(DeviceState.class);
+ when(deviceStateBefore.getDeviceIdentifier())
+ .thenReturn(MieleCloudBindingIntegrationTestConstants.ROBOTIC_VACUUM_CLEANER_THING_UID.getId());
+ when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+
+ getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+ DeviceState deviceStateAfter = mock(DeviceState.class);
+ when(deviceStateAfter.getDeviceIdentifier())
+ .thenReturn(MieleCloudBindingIntegrationTestConstants.ROBOTIC_VACUUM_CLEANER_THING_UID.getId());
+ when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+ when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceStateAfter);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(OnOffType.ON, getChannelState(FINISH_STATE));
+ });
+ }
+
+ @Test
+ public void testActionsChannelUpdatesForValidValues() {
+ // given:
+ ActionsState actionsState = mock(ActionsState.class);
+ when(actionsState.getDeviceIdentifier())
+ .thenReturn(MieleCloudBindingIntegrationTestConstants.ROBOTIC_VACUUM_CLEANER_THING_UID.getId());
+ when(actionsState.canBeStarted()).thenReturn(true);
+ when(actionsState.canBeStopped()).thenReturn(false);
+ when(actionsState.canBePaused()).thenReturn(true);
+ when(actionsState.canSetActiveProgramId()).thenReturn(false);
+
+ // when:
+ getBridgeHandler().onProcessActionUpdated(actionsState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_STARTED));
+ assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_STOPPED));
+ assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_PAUSED));
+ assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_SET_PROGRAM_ACTIVE));
+ });
+ }
+
+ @Test
+ public void testHandleCommandVacuumCleanerProgramActive() {
+ // when:
+ getThingHandler().handleCommand(channel(VACUUM_CLEANER_PROGRAM_ACTIVE), new StringType("1"));
+
+ // then:
+ waitForAssert(() -> {
+ verify(getWebserviceMock()).putProgram(getThingHandler().getDeviceId(), 1);
+ });
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.WASHING_MACHINE_THING_UID;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.SIUnits;
+
+/**
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API tests
+ * @author Björn Lange - Add elapsed time channel
+ */
+@NonNullByDefault
+public class WashingDeviceThingHandlerTest extends AbstractMieleThingHandlerTest {
+
+ @Override
+ protected AbstractMieleThingHandler setUpThingHandler() {
+ return createThingHandler(MieleCloudBindingConstants.THING_TYPE_WASHING_MACHINE, WASHING_MACHINE_THING_UID,
+ WashingDeviceThingHandler.class, MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
+ }
+
+ @Test
+ public void testChannelUpdatesForNullValues() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.getDeviceIdentifier()).thenReturn(WASHING_MACHINE_THING_UID.getId());
+ when(deviceState.getStateType()).thenReturn(Optional.empty());
+ when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.empty());
+ when(deviceState.getSpinningSpeed()).thenReturn(Optional.empty());
+ when(deviceState.getSpinningSpeedRaw()).thenReturn(Optional.empty());
+ when(deviceState.getSelectedProgram()).thenReturn(Optional.empty());
+ when(deviceState.getSelectedProgramId()).thenReturn(Optional.empty());
+ when(deviceState.getProgramPhase()).thenReturn(Optional.empty());
+ when(deviceState.getProgramPhaseRaw()).thenReturn(Optional.empty());
+ when(deviceState.getStatus()).thenReturn(Optional.empty());
+ when(deviceState.getStatusRaw()).thenReturn(Optional.empty());
+ when(deviceState.getStartTime()).thenReturn(Optional.empty());
+ when(deviceState.getElapsedTime()).thenReturn(Optional.empty());
+ when(deviceState.getTargetTemperature(0)).thenReturn(Optional.empty());
+ when(deviceState.getLightState()).thenReturn(Optional.empty());
+ when(deviceState.getDoorState()).thenReturn(Optional.empty());
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(NULL_VALUE_STATE, getChannelState(SPINNING_SPEED));
+ assertEquals(NULL_VALUE_STATE, getChannelState(SPINNING_SPEED_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ACTIVE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ACTIVE_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PHASE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PHASE_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE_RAW));
+ assertEquals(new StringType(ProgramStatus.PROGRAM_STOPPED.getState()), getChannelState(PROGRAM_START_STOP));
+ assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+ assertEquals(NULL_VALUE_STATE, getChannelState(DELAYED_START_TIME));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_ELAPSED_TIME));
+ assertEquals(NULL_VALUE_STATE, getChannelState(TEMPERATURE_TARGET));
+ assertEquals(NULL_VALUE_STATE, getChannelState(LIGHT_SWITCH));
+ assertEquals(NULL_VALUE_STATE, getChannelState(DOOR_STATE));
+ });
+ }
+
+ @Test
+ public void testChannelUpdatesForValidValues() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.isInState(any())).thenCallRealMethod();
+ when(deviceState.getDeviceIdentifier()).thenReturn(WASHING_MACHINE_THING_UID.getId());
+ when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.of(true));
+ when(deviceState.getSpinningSpeed()).thenReturn(Optional.of("1200"));
+ when(deviceState.getSpinningSpeedRaw()).thenReturn(Optional.of(1200));
+ when(deviceState.getSelectedProgram()).thenReturn(Optional.of("Buntwäsche"));
+ when(deviceState.getSelectedProgramId()).thenReturn(Optional.of(1L));
+ when(deviceState.getProgramPhase()).thenReturn(Optional.of("Waschen"));
+ when(deviceState.getProgramPhaseRaw()).thenReturn(Optional.of(7));
+ when(deviceState.getStatus()).thenReturn(Optional.of("Läuft"));
+ when(deviceState.getStatusRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+ when(deviceState.getStartTime()).thenReturn(Optional.of(3600));
+ when(deviceState.getElapsedTime()).thenReturn(Optional.of(63));
+ when(deviceState.getTargetTemperature(0)).thenReturn(Optional.of(30));
+ when(deviceState.hasError()).thenReturn(true);
+ when(deviceState.hasInfo()).thenReturn(true);
+ when(deviceState.getLightState()).thenReturn(Optional.of(false));
+ when(deviceState.getDoorState()).thenReturn(Optional.of(true));
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(new StringType("1200"), getChannelState(SPINNING_SPEED));
+ assertEquals(new DecimalType(1200), getChannelState(SPINNING_SPEED_RAW));
+ assertEquals(new StringType("Buntwäsche"), getChannelState(PROGRAM_ACTIVE));
+ assertEquals(new DecimalType(1), getChannelState(PROGRAM_ACTIVE_RAW));
+ assertEquals(new StringType("Waschen"), getChannelState(PROGRAM_PHASE));
+ assertEquals(new DecimalType(7), getChannelState(PROGRAM_PHASE_RAW));
+ assertEquals(new StringType("Läuft"), getChannelState(OPERATION_STATE));
+ assertEquals(new DecimalType(StateType.RUNNING.getCode()), getChannelState(OPERATION_STATE_RAW));
+ assertEquals(new StringType(ProgramStatus.PROGRAM_STARTED.getState()), getChannelState(PROGRAM_START_STOP));
+ assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+ assertEquals(new DecimalType(3600), getChannelState(DELAYED_START_TIME));
+ assertEquals(new DecimalType(63), getChannelState(PROGRAM_ELAPSED_TIME));
+ assertEquals(new QuantityType<>(30, SIUnits.CELSIUS), getChannelState(TEMPERATURE_TARGET));
+ assertEquals(OnOffType.ON, getChannelState(ERROR_STATE));
+ assertEquals(OnOffType.ON, getChannelState(INFO_STATE));
+ assertEquals(OnOffType.OFF, getChannelState(LIGHT_SWITCH));
+ assertEquals(OnOffType.ON, getChannelState(DOOR_STATE));
+ });
+ }
+
+ @Test
+ public void testFinishStateChannelIsSetToOnWhenProgramHasFinished() {
+ // given:
+ DeviceState deviceStateBefore = mock(DeviceState.class);
+ when(deviceStateBefore.getDeviceIdentifier()).thenReturn(WASHING_MACHINE_THING_UID.getId());
+ when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+
+ getBridgeHandler().onDeviceStateUpdated(deviceStateBefore);
+
+ DeviceState deviceStateAfter = mock(DeviceState.class);
+ when(deviceStateAfter.getDeviceIdentifier()).thenReturn(WASHING_MACHINE_THING_UID.getId());
+ when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
+ when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceStateAfter);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(OnOffType.ON, getChannelState(FINISH_STATE));
+ });
+ }
+
+ @Test
+ public void testTransitionChannelUpdatesForNullValues() {
+ // given:
+ DeviceState deviceStateBefore = mock(DeviceState.class);
+ when(deviceStateBefore.getDeviceIdentifier()).thenReturn(WASHING_MACHINE_THING_UID.getId());
+ when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+ when(deviceStateBefore.getRemainingTime()).thenReturn(Optional.empty());
+ when(deviceStateBefore.getProgress()).thenReturn(Optional.empty());
+
+ getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+ DeviceState deviceStateAfter = mock(DeviceState.class);
+ when(deviceStateAfter.getDeviceIdentifier()).thenReturn(WASHING_MACHINE_THING_UID.getId());
+ when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+ when(deviceStateAfter.getRemainingTime()).thenReturn(Optional.empty());
+ when(deviceStateAfter.getProgress()).thenReturn(Optional.empty());
+
+ // when:
+ getThingHandler().onDeviceStateUpdated(deviceStateAfter);
+
+ waitForAssert(() -> {
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_REMAINING_TIME));
+ assertEquals(NULL_VALUE_STATE, getChannelState(PROGRAM_PROGRESS));
+ });
+ }
+
+ @Test
+ public void testTransitionChannelUpdatesForValidValues() {
+ // given:
+ DeviceState deviceStateBefore = mock(DeviceState.class);
+ when(deviceStateBefore.getDeviceIdentifier()).thenReturn(WASHING_MACHINE_THING_UID.getId());
+ when(deviceStateBefore.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateBefore.isInState(any())).thenCallRealMethod();
+ when(deviceStateBefore.getRemainingTime()).thenReturn(Optional.of(10));
+ when(deviceStateBefore.getProgress()).thenReturn(Optional.of(80));
+
+ getThingHandler().onDeviceStateUpdated(deviceStateBefore);
+
+ DeviceState deviceStateAfter = mock(DeviceState.class);
+ when(deviceStateAfter.getDeviceIdentifier()).thenReturn(WASHING_MACHINE_THING_UID.getId());
+ when(deviceStateAfter.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceStateAfter.isInState(any())).thenCallRealMethod();
+ when(deviceStateAfter.getRemainingTime()).thenReturn(Optional.of(10));
+ when(deviceStateAfter.getProgress()).thenReturn(Optional.of(80));
+
+ // when:
+ getThingHandler().onDeviceStateUpdated(deviceStateAfter);
+
+ waitForAssert(() -> {
+ assertEquals(new DecimalType(10), getChannelState(PROGRAM_REMAINING_TIME));
+ assertEquals(new DecimalType(80), getChannelState(PROGRAM_PROGRESS));
+ });
+ }
+
+ @Test
+ public void testActionsChannelUpdatesForValidValues() {
+ // given:
+ ActionsState actionsState = mock(ActionsState.class);
+ when(actionsState.getDeviceIdentifier()).thenReturn(WASHING_MACHINE_THING_UID.getId());
+ when(actionsState.canBeStarted()).thenReturn(true);
+ when(actionsState.canBeStopped()).thenReturn(false);
+ when(actionsState.canBeSwitchedOn()).thenReturn(true);
+ when(actionsState.canBeSwitchedOff()).thenReturn(false);
+ when(actionsState.canControlLight()).thenReturn(false);
+
+ // when:
+ getBridgeHandler().onProcessActionUpdated(actionsState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_STARTED));
+ assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_STOPPED));
+ assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_ON));
+ assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF));
+ assertEquals(OnOffType.OFF, getChannelState(LIGHT_CAN_BE_CONTROLLED));
+ });
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.WINE_STORAGE_DEVICE_THING_UID;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
+import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
+import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
+import org.openhab.binding.mielecloud.internal.webservice.api.PowerStatus;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
+import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.SIUnits;
+
+/**
+ * @author Björn Lange - Initial contribution
+ * @author Benjamin Bolte - Add info state channel and map signal flags from API tests
+ */
+@NonNullByDefault
+public class WineStorageDeviceThingHandlerTest extends AbstractMieleThingHandlerTest {
+ @Override
+ protected AbstractMieleThingHandler setUpThingHandler() {
+ return createThingHandler(MieleCloudBindingConstants.THING_TYPE_WINE_STORAGE, WINE_STORAGE_DEVICE_THING_UID,
+ WineStorageDeviceThingHandler.class, MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
+ }
+
+ @Test
+ public void testChannelUpdatesForNullValues() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.getDeviceIdentifier()).thenReturn(WINE_STORAGE_DEVICE_THING_UID.getId());
+ when(deviceState.getRawType()).thenReturn(DeviceType.WINE_CONDITIONING_UNIT);
+ when(deviceState.getStateType()).thenReturn(Optional.empty());
+ when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.empty());
+ when(deviceState.getStatus()).thenReturn(Optional.empty());
+ when(deviceState.getStatusRaw()).thenReturn(Optional.empty());
+ when(deviceState.getTargetTemperature(0)).thenReturn(Optional.empty());
+ when(deviceState.getTemperature(0)).thenReturn(Optional.empty());
+ when(deviceState.getTargetTemperature(1)).thenReturn(Optional.empty());
+ when(deviceState.getTemperature(1)).thenReturn(Optional.empty());
+ when(deviceState.getTargetTemperature(2)).thenReturn(Optional.empty());
+ when(deviceState.getTemperature(2)).thenReturn(Optional.empty());
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE));
+ assertEquals(NULL_VALUE_STATE, getChannelState(OPERATION_STATE_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(TEMPERATURE_TARGET));
+ assertEquals(NULL_VALUE_STATE, getChannelState(TEMPERATURE_CURRENT));
+ assertEquals(NULL_VALUE_STATE, getChannelState(TOP_TEMPERATURE_TARGET));
+ assertEquals(NULL_VALUE_STATE, getChannelState(TOP_TEMPERATURE_CURRENT));
+ assertEquals(NULL_VALUE_STATE, getChannelState(MIDDLE_TEMPERATURE_TARGET));
+ assertEquals(NULL_VALUE_STATE, getChannelState(MIDDLE_TEMPERATURE_CURRENT));
+ assertEquals(NULL_VALUE_STATE, getChannelState(BOTTOM_TEMPERATURE_TARGET));
+ assertEquals(NULL_VALUE_STATE, getChannelState(BOTTOM_TEMPERATURE_CURRENT));
+ assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+ });
+ }
+
+ @Test
+ public void testChannelUpdatesForValidValues() {
+ // given:
+ DeviceState deviceState = mock(DeviceState.class);
+ when(deviceState.getDeviceIdentifier()).thenReturn(WINE_STORAGE_DEVICE_THING_UID.getId());
+ when(deviceState.getRawType()).thenReturn(DeviceType.WINE_CONDITIONING_UNIT);
+ when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
+ when(deviceState.isRemoteControlEnabled()).thenReturn(Optional.empty());
+ when(deviceState.getStatus()).thenReturn(Optional.of("Im Betrieb"));
+ when(deviceState.getStatusRaw()).thenReturn(Optional.of(StateType.RUNNING.getCode()));
+ when(deviceState.getTargetTemperature(0)).thenReturn(Optional.of(8));
+ when(deviceState.getTemperature(0)).thenReturn(Optional.of(9));
+ when(deviceState.getTargetTemperature(1)).thenReturn(Optional.of(10));
+ when(deviceState.getTemperature(1)).thenReturn(Optional.of(11));
+ when(deviceState.getTargetTemperature(2)).thenReturn(Optional.of(12));
+ when(deviceState.getTemperature(2)).thenReturn(Optional.of(14));
+ when(deviceState.hasError()).thenReturn(true);
+ when(deviceState.hasInfo()).thenReturn(true);
+
+ // when:
+ getBridgeHandler().onDeviceStateUpdated(deviceState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(new StringType("Im Betrieb"), getChannelState(OPERATION_STATE));
+ assertEquals(new DecimalType(StateType.RUNNING.getCode()), getChannelState(OPERATION_STATE_RAW));
+ assertEquals(NULL_VALUE_STATE, getChannelState(TEMPERATURE_TARGET));
+ assertEquals(NULL_VALUE_STATE, getChannelState(TEMPERATURE_CURRENT));
+ assertEquals(new QuantityType<>(8, SIUnits.CELSIUS), getChannelState(TOP_TEMPERATURE_TARGET));
+ assertEquals(new QuantityType<>(9, SIUnits.CELSIUS), getChannelState(TOP_TEMPERATURE_CURRENT));
+ assertEquals(new QuantityType<>(10, SIUnits.CELSIUS), getChannelState(MIDDLE_TEMPERATURE_TARGET));
+ assertEquals(new QuantityType<>(11, SIUnits.CELSIUS), getChannelState(MIDDLE_TEMPERATURE_CURRENT));
+ assertEquals(new QuantityType<>(12, SIUnits.CELSIUS), getChannelState(BOTTOM_TEMPERATURE_TARGET));
+ assertEquals(new QuantityType<>(14, SIUnits.CELSIUS), getChannelState(BOTTOM_TEMPERATURE_CURRENT));
+ assertEquals(new StringType(PowerStatus.POWER_ON.getState()), getChannelState(POWER_ON_OFF));
+ assertEquals(OnOffType.ON, getChannelState(ERROR_STATE));
+ assertEquals(OnOffType.ON, getChannelState(INFO_STATE));
+ });
+ }
+
+ @Test
+ public void testActionsChannelUpdatesForValidValues() {
+ // given:
+ ActionsState actionsState = mock(ActionsState.class);
+ when(actionsState.getDeviceIdentifier()).thenReturn(WINE_STORAGE_DEVICE_THING_UID.getId());
+ when(actionsState.canBeSwitchedOn()).thenReturn(true);
+ when(actionsState.canBeSwitchedOff()).thenReturn(false);
+
+ // when:
+ getBridgeHandler().onProcessActionUpdated(actionsState);
+
+ // then:
+ waitForAssert(() -> {
+ assertEquals(OnOffType.ON, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_ON));
+ assertEquals(OnOffType.OFF, getChannelState(REMOTE_CONTROL_CAN_BE_SWITCHED_OFF));
+ });
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.util;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.BeforeEach;
+import org.openhab.binding.mielecloud.internal.config.MieleCloudConfigService;
+import org.openhab.binding.mielecloud.internal.config.servlet.AccountOverviewServlet;
+import org.openhab.binding.mielecloud.internal.config.servlet.CreateBridgeServlet;
+import org.openhab.binding.mielecloud.internal.config.servlet.ForwardToLoginServlet;
+import org.openhab.binding.mielecloud.internal.config.servlet.ResultServlet;
+import org.openhab.binding.mielecloud.internal.config.servlet.SuccessServlet;
+import org.openhab.core.io.net.http.HttpClientFactory;
+
+/**
+ * Common base class for all config flow tests.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public abstract class AbstractConfigFlowTest extends OpenHabOsgiTest {
+ @Nullable
+ private WebsiteCrawler crawler;
+
+ @Nullable
+ private AccountOverviewServlet accountOverviewServlet;
+
+ @Nullable
+ private ForwardToLoginServlet forwardToLoginServlet;
+
+ @Nullable
+ private ResultServlet resultServlet;
+
+ @Nullable
+ private SuccessServlet successServlet;
+
+ @Nullable
+ private CreateBridgeServlet createBridgeServlet;
+
+ protected final WebsiteCrawler getCrawler() {
+ final WebsiteCrawler crawler = this.crawler;
+ assertNotNull(crawler);
+ return Objects.requireNonNull(crawler);
+ }
+
+ protected final AccountOverviewServlet getAccountOverviewServlet() {
+ final AccountOverviewServlet accountOverviewServlet = this.accountOverviewServlet;
+ assertNotNull(accountOverviewServlet);
+ return Objects.requireNonNull(accountOverviewServlet);
+ }
+
+ protected final ForwardToLoginServlet getForwardToLoginServlet() {
+ final ForwardToLoginServlet forwardToLoginServlet = this.forwardToLoginServlet;
+ assertNotNull(forwardToLoginServlet);
+ return Objects.requireNonNull(forwardToLoginServlet);
+ }
+
+ protected final ResultServlet getResultServlet() {
+ final ResultServlet resultServlet = this.resultServlet;
+ assertNotNull(resultServlet);
+ return Objects.requireNonNull(resultServlet);
+ }
+
+ protected final SuccessServlet getSuccessServlet() {
+ final SuccessServlet successServlet = this.successServlet;
+ assertNotNull(successServlet);
+ return Objects.requireNonNull(successServlet);
+ }
+
+ protected final CreateBridgeServlet getCreateBridgeServlet() {
+ final CreateBridgeServlet createBridgeServlet = this.createBridgeServlet;
+ assertNotNull(createBridgeServlet);
+ return Objects.requireNonNull(createBridgeServlet);
+ }
+
+ @BeforeEach
+ public final void setUpConfigFlowTest() {
+ setUpCrawler();
+ setUpServlets();
+ }
+
+ private void setUpCrawler() {
+ HttpClientFactory clientFactory = getService(HttpClientFactory.class);
+ assertNotNull(clientFactory);
+ crawler = new WebsiteCrawler(Objects.requireNonNull(clientFactory));
+ }
+
+ private void setUpServlets() {
+ MieleCloudConfigService configService = getService(MieleCloudConfigService.class);
+ assertNotNull(configService);
+
+ accountOverviewServlet = configService.getAccountOverviewServlet();
+ forwardToLoginServlet = configService.getForwardToLoginServlet();
+ resultServlet = configService.getResultServlet();
+ successServlet = configService.getSuccessServlet();
+ createBridgeServlet = configService.getCreateBridgeServlet();
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.util;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.core.thing.ThingUID;
+
+/**
+ * The {@link MieleCloudBindingIntegrationTestConstants} class holds common constants used in integration tests.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public final class MieleCloudBindingIntegrationTestConstants {
+ private MieleCloudBindingIntegrationTestConstants() {
+ }
+
+ public static final String SERIAL_NUMBER = "000124430017";
+
+ public static final String BRIDGE_ID = "genesis";
+
+ public static final ThingUID BRIDGE_THING_UID = new ThingUID(MieleCloudBindingConstants.THING_TYPE_BRIDGE,
+ BRIDGE_ID);
+
+ public static final ThingUID WASHING_MACHINE_THING_UID = new ThingUID(
+ MieleCloudBindingConstants.THING_TYPE_WASHING_MACHINE, BRIDGE_THING_UID, SERIAL_NUMBER);
+ public static final ThingUID OVEN_DEVICE_THING_UID = new ThingUID(MieleCloudBindingConstants.THING_TYPE_OVEN,
+ BRIDGE_THING_UID, SERIAL_NUMBER);
+ public static final ThingUID HOB_DEVICE_THING_UID = new ThingUID(MieleCloudBindingConstants.THING_TYPE_HOB,
+ BRIDGE_THING_UID, SERIAL_NUMBER);
+ public static final ThingUID FRIDGE_FREEZER_DEVICE_THING_UID = new ThingUID(
+ MieleCloudBindingConstants.THING_TYPE_FRIDGE_FREEZER, BRIDGE_THING_UID, SERIAL_NUMBER);
+ public static final ThingUID HOOD_DEVICE_THING_UID = new ThingUID(MieleCloudBindingConstants.THING_TYPE_HOOD,
+ BRIDGE_THING_UID, SERIAL_NUMBER);
+ public static final ThingUID COFFEE_SYSTEM_THING_UID = new ThingUID(
+ MieleCloudBindingConstants.THING_TYPE_COFFEE_SYSTEM, BRIDGE_THING_UID, SERIAL_NUMBER);
+ public static final ThingUID WINE_STORAGE_DEVICE_THING_UID = new ThingUID(
+ MieleCloudBindingConstants.THING_TYPE_WINE_STORAGE, BRIDGE_THING_UID, SERIAL_NUMBER);
+ public static final ThingUID DRYER_DEVICE_THING_UID = new ThingUID(MieleCloudBindingConstants.THING_TYPE_DRYER,
+ BRIDGE_THING_UID, SERIAL_NUMBER);
+ public static final ThingUID DISHWASHER_DEVICE_THING_UID = new ThingUID(
+ MieleCloudBindingConstants.THING_TYPE_DISHWASHER, BRIDGE_THING_UID, SERIAL_NUMBER);
+ public static final ThingUID DISH_WARMER_DEVICE_THING_UID = new ThingUID(
+ MieleCloudBindingConstants.THING_TYPE_DISH_WARMER, BRIDGE_THING_UID, SERIAL_NUMBER);
+ public static final ThingUID ROBOTIC_VACUUM_CLEANER_THING_UID = new ThingUID(
+ MieleCloudBindingConstants.THING_TYPE_ROBOTIC_VACUUM_CLEANER, BRIDGE_THING_UID, SERIAL_NUMBER);
+
+ public static final String MIELE_CLOUD_ACCOUNT_LABEL = "Miele Cloud Account";
+ public static final String CONFIG_PARAM_REFRESH_TOKEN = "refreshToken";
+
+ public static final String ACCESS_TOKEN = "DE_ABCDE";
+ public static final String ALTERNATIVE_ACCESS_TOKEN = "DE_01234";
+ public static final String REFRESH_TOKEN = "AT_12345";
+
+ public static final String CLIENT_ID = "01234567-890a-bcde-f012-34567890abcd";
+ public static final String CLIENT_SECRET = "0123456789abcdefghijklmnopqrstiu";
+
+ public static final String AUTHORIZATION_CODE = "0123456789";
+
+ public static final String EMAIL = "openhab@openhab.org";
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.util;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.MIELE_CLOUD_ACCOUNT_LABEL;
+
+import java.util.Collections;
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.BeforeEach;
+import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
+import org.openhab.binding.mielecloud.internal.handler.MieleBridgeHandler;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.config.discovery.inbox.Inbox;
+import org.openhab.core.test.java.JavaOSGiTest;
+import org.openhab.core.test.storage.VolatileStorageService;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ManagedThingProvider;
+import org.openhab.core.thing.ThingRegistry;
+import org.openhab.core.thing.binding.builder.BridgeBuilder;
+
+/**
+ * Parent class for openHAB OSGi tests offering helper methods for common interactions with the openHAB runtime and its
+ * services.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public abstract class OpenHabOsgiTest extends JavaOSGiTest {
+ @Nullable
+ private Inbox inbox;
+ @Nullable
+ private ThingRegistry thingRegistry;
+
+ protected Inbox getInbox() {
+ assertNotNull(inbox);
+ return Objects.requireNonNull(inbox);
+ }
+
+ protected ThingRegistry getThingRegistry() {
+ assertNotNull(thingRegistry);
+ return Objects.requireNonNull(thingRegistry);
+ }
+
+ @BeforeEach
+ public void setUpEshOsgiTest() {
+ registerVolatileStorageService();
+ inbox = getService(Inbox.class);
+ setUpThingRegistry();
+ }
+
+ private void setUpThingRegistry() {
+ thingRegistry = getService(ThingRegistry.class, ThingRegistry.class);
+ assertNotNull(thingRegistry, "Thing registry is missing");
+ }
+
+ /**
+ * Sets up a {@link Bridge} with an attached {@link MieleBridgeHandler} and registers it with the
+ * {@link ManagedThingProvider} and {@link ThingRegistry}.
+ */
+ public void setUpBridge() {
+ ManagedThingProvider managedThingProvider = getService(ManagedThingProvider.class);
+ assertNotNull(managedThingProvider);
+
+ Bridge bridge = BridgeBuilder
+ .create(MieleCloudBindingConstants.THING_TYPE_BRIDGE,
+ MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID)
+ .withConfiguration(
+ new Configuration(Collections.singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL,
+ MieleCloudBindingIntegrationTestConstants.EMAIL)))
+ .withLabel(MIELE_CLOUD_ACCOUNT_LABEL).build();
+ assertNotNull(bridge);
+
+ managedThingProvider.add(bridge);
+
+ waitForAssert(() -> {
+ assertNotNull(bridge.getHandler());
+ assertTrue(bridge.getHandler() instanceof MieleBridgeHandler, "Handler type is wrong");
+ });
+ }
+
+ /**
+ * Registers a volatile storage service.
+ */
+ @Override
+ protected void registerVolatileStorageService() {
+ registerService(new VolatileStorageService());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.util;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Utility class for reflection operations such as accessing private fields or methods.
+ *
+ * @author Björn Lange - Initial contribution
+ */
+@NonNullByDefault
+public final class ReflectionUtil {
+ private ReflectionUtil() {
+ }
+
+ /**
+ * Gets a private attribute.
+ *
+ * @param object The object to get the attribute from.
+ * @param fieldName The name of the field to get.
+ * @return The obtained value.
+ * @throws SecurityException if the operation is not allowed.
+ * @throws NoSuchFieldException if no field with the given name exists.
+ * @throws IllegalAccessException if the field is enforcing Java language access control and is inaccessible.
+ * @throws IllegalArgumentException if one of the passed parameters is invalid.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T getPrivate(Object object, String fieldName)
+ throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+ Field field = getFieldFromClassHierarchy(object.getClass(), fieldName);
+ field.setAccessible(true);
+ return (T) field.get(object);
+ }
+
+ private static Field getFieldFromClassHierarchy(Class<?> clazz, String fieldName)
+ throws NoSuchFieldException, SecurityException {
+ Class<?> iteratedClass = clazz;
+ do {
+ try {
+ return iteratedClass.getDeclaredField(fieldName);
+ } catch (NoSuchFieldException e) {
+ }
+ iteratedClass = iteratedClass.getSuperclass();
+ } while (iteratedClass != null);
+ throw new NoSuchFieldException();
+ }
+
+ /**
+ * Sets a private attribute.
+ *
+ * @param object The object to set the attribute on.
+ * @param fieldName The name of the field to set.
+ * @param value The value to set.
+ * @throws SecurityException if the operation is not allowed.
+ * @throws NoSuchFieldException if no field with the given name exists.
+ * @throws IllegalAccessException if the field is enforcing Java language access control and is inaccessible.
+ * @throws IllegalArgumentException if one of the passed parameters is invalid.
+ */
+ public static void setPrivate(Object object, String fieldName, @Nullable Object value)
+ throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+ Field field = object.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ field.set(object, value);
+ }
+
+ /**
+ * Sets an attribute declared as {@code private static final}.
+ *
+ * @param clazz The class owning the static attribute.
+ * @param fieldName The name of the attribute.
+ * @param value The new value.
+ * @throws NoSuchFieldException if no field with the given name exists.
+ * @throws SecurityException if the operation is not allowed.
+ * @throws IllegalArgumentException if one of the passed parameters is invalid.
+ * @throws IllegalAccessException if the field is enforcing Java language access control and is inaccessible.
+ */
+ public static void setPrivateStaticFinal(Class<?> clazz, String fieldName, @Nullable Object value)
+ throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+ Field field = clazz.getDeclaredField(fieldName);
+ field.setAccessible(true);
+
+ Field modifiersField = Field.class.getDeclaredField("modifiers");
+ modifiersField.setAccessible(true);
+ modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
+
+ field.set(null, value);
+ }
+
+ /**
+ * Invokes a private method on an object.
+ *
+ * @param object The object to invoke the method on.
+ * @param methodName The name of the method to invoke.
+ * @param parameters The parameters of the method invocation.
+ * @return The method call's return value.
+ * @throws NoSuchMethodException if no method with the given parameters or name exists.
+ * @throws SecurityException if the operation is not allowed.
+ * @throws IllegalAccessException if the method is enforcing Java language access control and is inaccessible.
+ * @throws IllegalArgumentException if one of the passed parameters is invalid.
+ * @throws InvocationTargetException if the invoked method throws an exception.
+ */
+ public static <T> T invokePrivate(Object object, String methodName, Object... parameters)
+ throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException {
+ Class<?>[] parameterTypes = new Class[parameters.length];
+ for (int i = 0; i < parameters.length; i++) {
+ parameterTypes[i] = parameters[i].getClass();
+ }
+
+ return invokePrivate(object, methodName, parameterTypes, parameters);
+ }
+
+ /**
+ * Invokes a private method on an object.
+ *
+ * @param object The object to invoke the method on.
+ * @param methodName The name of the method to invoke.
+ * @param parameterTypes The types of the parameters.
+ * @param parameters The parameters of the method invocation.
+ * @return The method call's return value.
+ * @throws NoSuchMethodException if no method with the given parameters or name exists.
+ * @throws SecurityException if the operation is not allowed.
+ * @throws IllegalAccessException if the method is enforcing Java language access control and is inaccessible.
+ * @throws IllegalArgumentException if one of the passed parameters is invalid.
+ * @throws InvocationTargetException if the invoked method throws an exception.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T invokePrivate(Object object, String methodName, Class<?>[] parameterTypes, Object... parameters)
+ throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException {
+ Method method = getMethodFromClassHierarchy(object.getClass(), methodName, parameterTypes);
+ method.setAccessible(true);
+ try {
+ return (T) method.invoke(object, parameters);
+ } catch (InvocationTargetException e) {
+ throw new IllegalStateException(e.getCause());
+ }
+ }
+
+ private static Method getMethodFromClassHierarchy(Class<?> clazz, String methodName, Class<?>[] parameterTypes)
+ throws NoSuchMethodException {
+ Class<?> iteratedClass = clazz;
+ do {
+ try {
+ return iteratedClass.getDeclaredMethod(methodName, parameterTypes);
+ } catch (NoSuchMethodException e) {
+ }
+ iteratedClass = iteratedClass.getSuperclass();
+ } while (iteratedClass != null);
+ throw new NoSuchMethodException();
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.util;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Helper class for testing websites. Allows for easy access to the document contents.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class Website {
+ private String content;
+
+ protected Website(String content) {
+ this.content = content;
+ }
+
+ /**
+ * Gets the part of the content representing the element that surrounds the given text.
+ */
+ private String getElementSurrounding(String text) {
+ int index = content.indexOf(text);
+ if (index == -1) {
+ throw new IllegalStateException("Could not find \"" + text + "\" in \"" + content + "\"");
+ }
+
+ int elementBegin = content.lastIndexOf('<', index);
+ if (elementBegin == -1) {
+ throw new IllegalStateException("\"" + text + "\" is not contained in \"" + content + "\"");
+ }
+
+ int elementEnd = content.indexOf('>', index);
+ if (elementEnd == -1) {
+ throw new IllegalStateException("Malformatted HTML content: " + content);
+ }
+
+ return content.substring(elementBegin, elementEnd + 1);
+ }
+
+ /**
+ * Gets the value of an attribute from an element.
+ */
+ private String getAttributeFromElement(String element, String attribute) {
+ int valueStart = element.indexOf(attribute + "=\"");
+ if (valueStart == -1) {
+ throw new IllegalStateException("Element \"" + element + "\" has no " + attribute);
+ }
+
+ int valueEnd = element.indexOf('\"', valueStart + attribute.length() + 2);
+ if (valueEnd == -1) {
+ throw new IllegalStateException("Malformatted HTML content in element: " + element);
+ }
+
+ return element.substring(valueStart + attribute.length() + 2, valueEnd);
+ }
+
+ /**
+ * Gets the value of the input field with the given name.
+ *
+ * @param inputName Name of the input field.
+ * @return The value of the input field.
+ */
+ public String getValueOfInput(String inputName) {
+ return getAttributeFromElement(getElementSurrounding("name=\"" + inputName + "\""), "value");
+ }
+
+ /**
+ * Gets the value of the href attribute of the link with the given title text.
+ */
+ public String getTargetOfLink(String linkTitle) {
+ return getAttributeFromElement(getElementSurrounding(linkTitle), "href");
+ }
+
+ /**
+ * Checks whether the given raw text is contained in the raw website code.
+ */
+ public boolean contains(String expectedContent) {
+ return this.content.contains(expectedContent);
+ }
+
+ /**
+ * Gets the value of the action attribute of the first form found in the website body.
+ */
+ public String getFormAction() {
+ int formActionStart = content.indexOf("<form action=\"");
+ if (formActionStart == -1) {
+ throw new IllegalStateException("Could not find a form in \"" + content + "\"");
+ }
+
+ int formActionEnd = content.indexOf('\"', formActionStart + 15);
+ if (formActionEnd == -1) {
+ throw new IllegalStateException("Malformatted HTML content in form: " + content);
+ }
+
+ return content.substring(formActionStart + 14, formActionEnd);
+ }
+
+ public String getContent() {
+ return content;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mielecloud.internal.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.openhab.core.io.net.http.HttpClientFactory;
+
+/**
+ * Allows for requesting website content from URLs.
+ *
+ * @author Björn Lange - Initial Contribution
+ */
+@NonNullByDefault
+public final class WebsiteCrawler {
+ private HttpClient httpClient;
+
+ public WebsiteCrawler(HttpClientFactory httpClientFactory) {
+ this.httpClient = httpClientFactory.getCommonHttpClient();
+ }
+
+ /**
+ * Gets a website relative to the address of the openHAB installation running in test mode during integration tests.
+ *
+ * @param relativeUrl The relative URL.
+ * @return The website.
+ * @throws Exception if anything goes wrong.
+ */
+ public Website doGetRelative(String relativeUrl) throws Exception {
+ ContentResponse response = httpClient.GET("http://127.0.0.1:8080" + relativeUrl);
+ assertEquals(200, response.getStatus());
+ return new Website(response.getContentAsString());
+ }
+}
<module>org.openhab.binding.feed.tests</module>
<module>org.openhab.binding.hue.tests</module>
<module>org.openhab.binding.max.tests</module>
+ <module>org.openhab.binding.mielecloud.tests</module>
<module>org.openhab.binding.modbus.tests</module>
<!-- MQTT tests need to be refactored to not use the embedded broker bundle anymore
<module>org.openhab.binding.mqtt.homeassistant.tests</module>